From 7975395d10bb381de8cd15b5ee15198318af6d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Viande?= Date: Wed, 11 Apr 2018 11:42:52 +0200 Subject: [PATCH 001/107] Entry: add archived_at property and updateArchived method --- .../Version20180405182455.php | 68 +++++++++++++++++++ .../Controller/EntryRestController.php | 4 +- .../DataFixtures/ORM/LoadEntryData.php | 2 +- src/Wallabag/CoreBundle/Entity/Entry.php | 49 ++++++++++++- .../ImportBundle/Import/BrowserImport.php | 2 +- .../ImportBundle/Import/InstapaperImport.php | 2 +- .../ImportBundle/Import/PinboardImport.php | 2 +- .../ImportBundle/Import/PocketImport.php | 2 +- .../ImportBundle/Import/ReadabilityImport.php | 2 +- .../ImportBundle/Import/WallabagImport.php | 2 +- .../Controller/EntryRestControllerTest.php | 2 + .../Controller/ConfigControllerTest.php | 2 +- .../Controller/EntryControllerTest.php | 8 +-- 13 files changed, 132 insertions(+), 15 deletions(-) create mode 100755 app/DoctrineMigrations/Version20180405182455.php diff --git a/app/DoctrineMigrations/Version20180405182455.php b/app/DoctrineMigrations/Version20180405182455.php new file mode 100755 index 000000000..71879c0ea --- /dev/null +++ b/app/DoctrineMigrations/Version20180405182455.php @@ -0,0 +1,68 @@ +container = $container; + } + + /** + * @param Schema $schema + */ + public function up(Schema $schema) + { + $entryTable = $schema->getTable($this->getTable('entry')); + + $this->skipIf($entryTable->hasColumn('archived_at'), 'It seems that you already played this migration.'); + + $entryTable->addColumn('archived_at', 'datetime', [ + 'notnull' => false, + ]); + } + + public function postUp(Schema $schema) + { + $entryTable = $schema->getTable($this->getTable('entry')); + $this->skipIf(!$entryTable->hasColumn('archived_at'), 'Unable to add archived_at colum'); + + $this->connection->executeQuery( + 'UPDATE ' . $this->getTable('entry') . ' SET archived_at = updated_at WHERE is_archived = :is_archived', + [ + 'is_archived' => true, + ] + ); + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + $entryTable = $schema->getTable($this->getTable('entry')); + + $this->skipIf(!$entryTable->hasColumn('archived_at'), 'It seems that you already played this migration.'); + + $entryTable->dropColumn('archived_at'); + } + + private function getTable($tableName) + { + return $this->container->getParameter('database_table_prefix') . $tableName; + } +} diff --git a/src/Wallabag/ApiBundle/Controller/EntryRestController.php b/src/Wallabag/ApiBundle/Controller/EntryRestController.php index 0b4e74a0f..dc63b98de 100644 --- a/src/Wallabag/ApiBundle/Controller/EntryRestController.php +++ b/src/Wallabag/ApiBundle/Controller/EntryRestController.php @@ -358,7 +358,7 @@ class EntryRestController extends WallabagRestController } if (null !== $data['isArchived']) { - $entry->setArchived((bool) $data['isArchived']); + $entry->updateArchived((bool) $data['isArchived']); } if (null !== $data['isStarred']) { @@ -474,7 +474,7 @@ class EntryRestController extends WallabagRestController } if (null !== $data['isArchived']) { - $entry->setArchived((bool) $data['isArchived']); + $entry->updateArchived((bool) $data['isArchived']); } if (null !== $data['isStarred']) { diff --git a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadEntryData.php b/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadEntryData.php index 0e1510a29..62fb5fa68 100644 --- a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadEntryData.php +++ b/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadEntryData.php @@ -98,7 +98,7 @@ class LoadEntryData extends AbstractFixture implements OrderedFixtureInterface $entry6->setMimetype('text/html'); $entry6->setTitle('test title entry6'); $entry6->setContent('This is my content /o/'); - $entry6->setArchived(true); + $entry6->updateArchived(true); $entry6->setLanguage('de'); $entry6->addTag($this->getReference('bar-tag')); diff --git a/src/Wallabag/CoreBundle/Entity/Entry.php b/src/Wallabag/CoreBundle/Entity/Entry.php index 2b1f2e050..b3cfdc4a4 100644 --- a/src/Wallabag/CoreBundle/Entity/Entry.php +++ b/src/Wallabag/CoreBundle/Entity/Entry.php @@ -86,6 +86,15 @@ class Entry */ private $isArchived = false; + /** + * @var \DateTime + * + * @ORM\Column(name="archived_at", type="datetime", nullable=true) + * + * @Groups({"entries_for_user", "export_all"}) + */ + private $archivedAt = null; + /** * @var bool * @@ -335,6 +344,44 @@ class Entry return $this; } + /** + * update isArchived and archive_at fields. + * + * @param bool $isArchived + * + * @return Entry + */ + public function updateArchived($isArchived = false) + { + $this->setArchived($isArchived); + $this->setArchivedAt(null); + if ($this->isArchived()) { + $this->setArchivedAt(new \DateTime()); + } + + return $this; + } + + /** + * @return \DateTime|null + */ + public function getArchivedAt() + { + return $this->archivedAt; + } + + /** + * @param \DateTime|null $archivedAt + * + * @return Entry + */ + public function setArchivedAt($archivedAt = null) + { + $this->archivedAt = $archivedAt; + + return $this; + } + /** * Get isArchived. * @@ -357,7 +404,7 @@ class Entry public function toggleArchive() { - $this->isArchived = $this->isArchived() ^ 1; + $this->updateArchived($this->isArchived() ^ 1); return $this; } diff --git a/src/Wallabag/ImportBundle/Import/BrowserImport.php b/src/Wallabag/ImportBundle/Import/BrowserImport.php index 225f1791f..614386cb2 100644 --- a/src/Wallabag/ImportBundle/Import/BrowserImport.php +++ b/src/Wallabag/ImportBundle/Import/BrowserImport.php @@ -133,7 +133,7 @@ abstract class BrowserImport extends AbstractImport ); } - $entry->setArchived($data['is_archived']); + $entry->updateArchived($data['is_archived']); if (!empty($data['created_at'])) { $dt = new \DateTime(); diff --git a/src/Wallabag/ImportBundle/Import/InstapaperImport.php b/src/Wallabag/ImportBundle/Import/InstapaperImport.php index e4f0970c0..e113ba008 100644 --- a/src/Wallabag/ImportBundle/Import/InstapaperImport.php +++ b/src/Wallabag/ImportBundle/Import/InstapaperImport.php @@ -135,7 +135,7 @@ class InstapaperImport extends AbstractImport ); } - $entry->setArchived($importedEntry['is_archived']); + $entry->updateArchived($importedEntry['is_archived']); $entry->setStarred($importedEntry['is_starred']); $this->em->persist($entry); diff --git a/src/Wallabag/ImportBundle/Import/PinboardImport.php b/src/Wallabag/ImportBundle/Import/PinboardImport.php index 110b04642..9a5e8cb6c 100644 --- a/src/Wallabag/ImportBundle/Import/PinboardImport.php +++ b/src/Wallabag/ImportBundle/Import/PinboardImport.php @@ -119,7 +119,7 @@ class PinboardImport extends AbstractImport ); } - $entry->setArchived($data['is_archived']); + $entry->updateArchived($data['is_archived']); $entry->setStarred($data['is_starred']); $entry->setCreatedAt(new \DateTime($data['created_at'])); diff --git a/src/Wallabag/ImportBundle/Import/PocketImport.php b/src/Wallabag/ImportBundle/Import/PocketImport.php index c1b35b7ef..4b1ad1d75 100644 --- a/src/Wallabag/ImportBundle/Import/PocketImport.php +++ b/src/Wallabag/ImportBundle/Import/PocketImport.php @@ -194,7 +194,7 @@ class PocketImport extends AbstractImport $this->fetchContent($entry, $url); // 0, 1, 2 - 1 if the item is archived - 2 if the item should be deleted - $entry->setArchived(1 === $importedEntry['status'] || $this->markAsRead); + $entry->updateArchived(1 === $importedEntry['status'] || $this->markAsRead); // 0 or 1 - 1 If the item is starred $entry->setStarred(1 === $importedEntry['favorite']); diff --git a/src/Wallabag/ImportBundle/Import/ReadabilityImport.php b/src/Wallabag/ImportBundle/Import/ReadabilityImport.php index 002b27f46..d67775821 100644 --- a/src/Wallabag/ImportBundle/Import/ReadabilityImport.php +++ b/src/Wallabag/ImportBundle/Import/ReadabilityImport.php @@ -111,7 +111,7 @@ class ReadabilityImport extends AbstractImport // update entry with content (in case fetching failed, the given entry will be return) $this->fetchContent($entry, $data['url'], $data); - $entry->setArchived($data['is_archived']); + $entry->updateArchived($data['is_archived']); $entry->setStarred($data['is_starred']); $entry->setCreatedAt(new \DateTime($data['created_at'])); diff --git a/src/Wallabag/ImportBundle/Import/WallabagImport.php b/src/Wallabag/ImportBundle/Import/WallabagImport.php index c64ccd64d..916137f17 100644 --- a/src/Wallabag/ImportBundle/Import/WallabagImport.php +++ b/src/Wallabag/ImportBundle/Import/WallabagImport.php @@ -122,7 +122,7 @@ abstract class WallabagImport extends AbstractImport $entry->setPreviewPicture($importedEntry['preview_picture']); } - $entry->setArchived($data['is_archived']); + $entry->updateArchived($data['is_archived']); $entry->setStarred($data['is_starred']); if (!empty($data['created_at'])) { diff --git a/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php b/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php index 58b617f3d..6b26376db 100644 --- a/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php +++ b/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php @@ -438,6 +438,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $this->assertSame(0, $content['is_archived']); $this->assertSame(0, $content['is_starred']); $this->assertNull($content['starred_at']); + $this->assertNull($content['archived_at']); $this->assertSame('New title for my article', $content['title']); $this->assertSame(1, $content['user_id']); $this->assertCount(2, $content['tags']); @@ -533,6 +534,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $this->assertSame(1, $content['is_archived']); $this->assertSame(1, $content['is_starred']); $this->assertGreaterThanOrEqual($now->getTimestamp(), (new \DateTime($content['starred_at']))->getTimestamp()); + $this->assertGreaterThanOrEqual($now->getTimestamp(), (new \DateTime($content['archived_at']))->getTimestamp()); $this->assertSame(1, $content['user_id']); } diff --git a/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php b/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php index e07c57dd3..d709f4ebd 100644 --- a/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php @@ -849,7 +849,7 @@ class ConfigControllerTest extends WallabagCoreTestCase $entryArchived->setContent('Youhou'); $entryArchived->setTitle('Youhou'); $entryArchived->addTag($tagArchived); - $entryArchived->setArchived(true); + $entryArchived->updateArchived(true); $em->persist($entryArchived); $annotationArchived = new Annotation($user); diff --git a/tests/Wallabag/CoreBundle/Controller/EntryControllerTest.php b/tests/Wallabag/CoreBundle/Controller/EntryControllerTest.php index bf0068b4b..0ac119d86 100644 --- a/tests/Wallabag/CoreBundle/Controller/EntryControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/EntryControllerTest.php @@ -621,7 +621,7 @@ class EntryControllerTest extends WallabagCoreTestCase $content->setMimetype('text/html'); $content->setTitle('test title entry'); $content->setContent('This is my content /o/'); - $content->setArchived(true); + $content->updateArchived(true); $content->setLanguage('fr'); $em->persist($content); @@ -774,7 +774,7 @@ class EntryControllerTest extends WallabagCoreTestCase $entry = new Entry($this->getLoggedInUser()); $entry->setUrl($this->url); - $entry->setArchived(false); + $entry->updateArchived(false); $this->getEntityManager()->persist($entry); $this->getEntityManager()->flush(); @@ -1245,7 +1245,7 @@ class EntryControllerTest extends WallabagCoreTestCase $entry = new Entry($this->getLoggedInUser()); $entry->setUrl('http://0.0.0.0/foo/baz/qux'); $entry->setTitle('Le manège'); - $entry->setArchived(true); + $entry->updateArchived(true); $this->getEntityManager()->persist($entry); $this->getEntityManager()->flush(); @@ -1275,7 +1275,7 @@ class EntryControllerTest extends WallabagCoreTestCase $entry = new Entry($this->getLoggedInUser()); $entry->setUrl('http://domain/qux'); $entry->setTitle('Le manège'); - $entry->setArchived(true); + $entry->updateArchived(true); $this->getEntityManager()->persist($entry); $this->getEntityManager()->flush(); From 0e70e812274a4e13b02e821e8a64a3206c7688ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Viande?= Date: Thu, 26 Apr 2018 07:20:52 +0200 Subject: [PATCH 002/107] Entry: add sort parameter archived --- src/Wallabag/ApiBundle/Controller/EntryRestController.php | 2 +- src/Wallabag/CoreBundle/Repository/EntryRepository.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Wallabag/ApiBundle/Controller/EntryRestController.php b/src/Wallabag/ApiBundle/Controller/EntryRestController.php index dc63b98de..5882aaee3 100644 --- a/src/Wallabag/ApiBundle/Controller/EntryRestController.php +++ b/src/Wallabag/ApiBundle/Controller/EntryRestController.php @@ -79,7 +79,7 @@ class EntryRestController extends WallabagRestController * parameters={ * {"name"="archive", "dataType"="integer", "required"=false, "format"="1 or 0, all entries by default", "description"="filter by archived status."}, * {"name"="starred", "dataType"="integer", "required"=false, "format"="1 or 0, all entries by default", "description"="filter by starred status."}, - * {"name"="sort", "dataType"="string", "required"=false, "format"="'created' or 'updated', default 'created'", "description"="sort entries by date."}, + * {"name"="sort", "dataType"="string", "required"=false, "format"="'created' or 'updated' or 'archived', default 'created'", "description"="sort entries by date."}, * {"name"="order", "dataType"="string", "required"=false, "format"="'asc' or 'desc', default 'desc'", "description"="order of sort."}, * {"name"="page", "dataType"="integer", "required"=false, "format"="default '1'", "description"="what page you want."}, * {"name"="perPage", "dataType"="integer", "required"=false, "format"="default'30'", "description"="results per page."}, diff --git a/src/Wallabag/CoreBundle/Repository/EntryRepository.php b/src/Wallabag/CoreBundle/Repository/EntryRepository.php index 83379998d..c0818ca09 100644 --- a/src/Wallabag/CoreBundle/Repository/EntryRepository.php +++ b/src/Wallabag/CoreBundle/Repository/EntryRepository.php @@ -189,6 +189,8 @@ class EntryRepository extends EntityRepository $qb->orderBy('e.id', $order); } elseif ('updated' === $sort) { $qb->orderBy('e.updatedAt', $order); + } else if ('archived' === $sort) { + $qb->orderBy('e.archivedAt', $order); } $pagerAdapter = new DoctrineORMAdapter($qb, true, false); From 7c0d682687e9e79266573eeed73b50a3e2674e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Viande?= Date: Fri, 15 Jun 2018 11:30:54 +0200 Subject: [PATCH 003/107] Code Style --- src/Wallabag/CoreBundle/Repository/EntryRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Wallabag/CoreBundle/Repository/EntryRepository.php b/src/Wallabag/CoreBundle/Repository/EntryRepository.php index c0818ca09..10389cc19 100644 --- a/src/Wallabag/CoreBundle/Repository/EntryRepository.php +++ b/src/Wallabag/CoreBundle/Repository/EntryRepository.php @@ -189,7 +189,7 @@ class EntryRepository extends EntityRepository $qb->orderBy('e.id', $order); } elseif ('updated' === $sort) { $qb->orderBy('e.updatedAt', $order); - } else if ('archived' === $sort) { + } elseif ('archived' === $sort) { $qb->orderBy('e.archivedAt', $order); } From 9007fe006286c63a7793326d9429792383ebd76d Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Fri, 21 Sep 2018 11:18:29 +0200 Subject: [PATCH 004/107] Sort archive page by archived at --- src/Wallabag/CoreBundle/Controller/EntryController.php | 2 -- src/Wallabag/CoreBundle/Repository/EntryRepository.php | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Wallabag/CoreBundle/Controller/EntryController.php b/src/Wallabag/CoreBundle/Controller/EntryController.php index b7fdea279..294008339 100644 --- a/src/Wallabag/CoreBundle/Controller/EntryController.php +++ b/src/Wallabag/CoreBundle/Controller/EntryController.php @@ -532,11 +532,9 @@ class EntryController extends Controller switch ($type) { case 'search': $qb = $repository->getBuilderForSearchByUser($this->getUser()->getId(), $searchTerm, $currentRoute); - break; case 'untagged': $qb = $repository->getBuilderForUntaggedByUser($this->getUser()->getId()); - break; case 'starred': $qb = $repository->getBuilderForStarredByUser($this->getUser()->getId()); diff --git a/src/Wallabag/CoreBundle/Repository/EntryRepository.php b/src/Wallabag/CoreBundle/Repository/EntryRepository.php index 10389cc19..93c630c00 100644 --- a/src/Wallabag/CoreBundle/Repository/EntryRepository.php +++ b/src/Wallabag/CoreBundle/Repository/EntryRepository.php @@ -50,7 +50,7 @@ class EntryRepository extends EntityRepository public function getBuilderForArchiveByUser($userId) { return $this - ->getSortedQueryBuilderByUser($userId) + ->getSortedQueryBuilderByUser($userId, 'archivedAt', 'desc') ->andWhere('e.isArchived = true') ; } From a664a1d876174e9d61d8324039ee86d2b6bf164d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20HULARD?= Date: Tue, 23 Jan 2018 19:09:03 +0100 Subject: [PATCH 005/107] Rename Tag : Add a new FormType --- .../CoreBundle/Form/Type/RenameTagType.php | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/Wallabag/CoreBundle/Form/Type/RenameTagType.php diff --git a/src/Wallabag/CoreBundle/Form/Type/RenameTagType.php b/src/Wallabag/CoreBundle/Form/Type/RenameTagType.php new file mode 100644 index 000000000..e62700487 --- /dev/null +++ b/src/Wallabag/CoreBundle/Form/Type/RenameTagType.php @@ -0,0 +1,35 @@ +add('label', TextType::class, [ + 'required' => true, + 'attr' => [ + 'placeholder' => 'tag.rename.placeholder', + ], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => 'Wallabag\CoreBundle\Entity\Tag', + ]); + } + + public function getBlockPrefix() + { + return 'tag'; + } +} From be326a22f90a5f4006ff41ee7b4ed0ca73a8fddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20HULARD?= Date: Wed, 24 Jan 2018 17:27:16 +0100 Subject: [PATCH 006/107] Create a new Tag action to rename tags. The current tag is removed from all the current logged user entries. Then the new one is created and attached. --- .../CoreBundle/Controller/TagController.php | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/Wallabag/CoreBundle/Controller/TagController.php b/src/Wallabag/CoreBundle/Controller/TagController.php index b6d28e59d..70b273e01 100644 --- a/src/Wallabag/CoreBundle/Controller/TagController.php +++ b/src/Wallabag/CoreBundle/Controller/TagController.php @@ -11,6 +11,7 @@ use Symfony\Component\HttpFoundation\Request; use Wallabag\CoreBundle\Entity\Entry; use Wallabag\CoreBundle\Entity\Tag; use Wallabag\CoreBundle\Form\Type\NewTagType; +use Wallabag\CoreBundle\Form\Type\RenameTagType; class TagController extends Controller { @@ -130,4 +131,48 @@ class TagController extends Controller 'tag' => $tag, ]); } + + /** + * Rename a given tag with a new label + * Create a new tag with the new name and drop the old one. + * + * @param Tag $tag + * @param Request $request + * + * @Route("/tag/rename/{slug}", name="tag_rename") + * @ParamConverter("tag", options={"mapping": {"slug": "slug"}}) + * + * @return \Symfony\Component\HttpFoundation\Response + */ + public function renameTagAction(Tag $tag, Request $request) + { + $form = $this->createForm(RenameTagType::class, new Tag()); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entries = $this->get('wallabag_core.entry_repository')->findAllByTagId( + $this->getUser()->getId(), + $tag->getId() + ); + foreach ($entries as $entry) { + $this->get('wallabag_core.tags_assigner')->assignTagsToEntry( + $entry, + $form->get('label')->getData() + ); + $entry->removeTag($tag); + } + + $em = $this->getDoctrine()->getManager(); + $em->flush(); + } + + $this->get('session')->getFlashBag()->add( + 'notice', + 'flashes.tag.notice.tag_renamed' + ); + + $redirectUrl = $this->get('wallabag_core.helper.redirect')->to($request->headers->get('referer'), '', true); + + return $this->redirect($redirectUrl); + } } From b846c1e4d03ec51ae6b740040d79d47d9bdec12c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20HULARD?= Date: Wed, 24 Jan 2018 18:04:39 +0100 Subject: [PATCH 007/107] Add RenameForm as tag list view parameters. This will help handling the CSRF protection token and use symfony HTML generation layer. Also a FormView instance is generated for each tag because we need to render a form for each tag and FormView are not reusable. --- src/Wallabag/CoreBundle/Controller/TagController.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Wallabag/CoreBundle/Controller/TagController.php b/src/Wallabag/CoreBundle/Controller/TagController.php index 70b273e01..a041510d0 100644 --- a/src/Wallabag/CoreBundle/Controller/TagController.php +++ b/src/Wallabag/CoreBundle/Controller/TagController.php @@ -88,8 +88,14 @@ class TagController extends Controller $tags = $this->get('wallabag_core.tag_repository') ->findAllFlatTagsWithNbEntries($this->getUser()->getId()); + $renameForms = []; + foreach ($tags as $tag) { + $renameForms[$tag['id']] = $this->createForm(RenameTagType::class, new Tag())->createView(); + } + return $this->render('WallabagCoreBundle:Tag:tags.html.twig', [ 'tags' => $tags, + 'renameForms' => $renameForms, ]); } From 9b0aef9171389aa9cdc39e8434df0f912ee5bdae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20HULARD?= Date: Wed, 24 Jan 2018 17:29:26 +0100 Subject: [PATCH 008/107] Update tag list template to allow renaming. * Add a form on each tag to handle rename action. * Add JavaScript to handle action on the corresponding page inside the global index.js file. * Add support for the 2 active themes : material / baggy The form solution is cleaner than an Ajax one because it let the browser validate input data and make the POST easier without the need to handle JSON response. --- app/Resources/static/themes/_global/index.js | 19 +++++++++++++++++++ .../views/themes/baggy/Tag/tags.html.twig | 18 +++++++++++++++--- .../views/themes/material/Tag/tags.html.twig | 13 ++++++++++++- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/app/Resources/static/themes/_global/index.js b/app/Resources/static/themes/_global/index.js index ae598e56e..bb3e95b6d 100644 --- a/app/Resources/static/themes/_global/index.js +++ b/app/Resources/static/themes/_global/index.js @@ -70,4 +70,23 @@ $(document).ready(() => { retrievePercent(x.entryId, true); }); } + + document.querySelectorAll('[data-handler=tag-rename]').forEach((item) => { + const current = item; + current.wallabag_edit_mode = false; + current.onclick = (event) => { + const target = event.currentTarget; + + if (target.wallabag_edit_mode === false) { + $(target.parentNode.querySelector('[data-handle=tag-link]')).addClass('hidden'); + $(target.parentNode.querySelector('[data-handle=tag-rename-form]')).removeClass('hidden'); + target.parentNode.querySelector('[data-handle=tag-rename-form] input').focus(); + target.querySelector('.material-icons').innerHTML = 'done'; + + target.wallabag_edit_mode = true; + } else { + target.parentNode.querySelector('[data-handle=tag-rename-form]').submit(); + } + }; + }); }); 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 070d5629a..35351ab15 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 @@ -10,10 +10,22 @@ 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 c15b5146e..21e88a9a2 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 @@ -13,7 +13,18 @@
    {% for tag in tags %}
  • - {{tag.label}} ({{ tag.nbEntries }}) + + {{ tag.label }} ({{ tag.nbEntries }}) + + {% if renameForms is defined and renameForms[tag.id] is defined %} + + + mode_edit + + {% endif %} {% if app.user.config.rssToken %} rss_feed {% endif %} From 559f708cae6143564284034369771737119a6bf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20HULARD?= Date: Wed, 24 Jan 2018 17:29:41 +0100 Subject: [PATCH 009/107] Add translations about latest Tag changes. Add new translations in each language file. --- .../CoreBundle/Resources/translations/messages.da.yml | 3 +++ .../CoreBundle/Resources/translations/messages.de.yml | 3 +++ .../CoreBundle/Resources/translations/messages.en.yml | 3 +++ .../CoreBundle/Resources/translations/messages.es.yml | 3 +++ .../CoreBundle/Resources/translations/messages.fa.yml | 3 +++ .../CoreBundle/Resources/translations/messages.fr.yml | 3 +++ .../CoreBundle/Resources/translations/messages.it.yml | 3 +++ .../CoreBundle/Resources/translations/messages.oc.yml | 3 +++ .../CoreBundle/Resources/translations/messages.pl.yml | 3 +++ .../CoreBundle/Resources/translations/messages.pt.yml | 3 +++ .../CoreBundle/Resources/translations/messages.ro.yml | 3 +++ .../CoreBundle/Resources/translations/messages.ru.yml | 5 ++++- .../CoreBundle/Resources/translations/messages.th.yml | 3 +++ .../CoreBundle/Resources/translations/messages.tr.yml | 3 +++ 14 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml index e13846754..c8500ad38 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml @@ -399,6 +399,8 @@ tag: new: # add: 'Add' # placeholder: 'You can add several tags, separated by a comma.' + rename: + # placeholder: 'You can update tag name.' # export: # footer_template: '

    Produced by wallabag with %method%

    Please open an issue if you have trouble with the display of this E-Book on your device.

    ' @@ -585,6 +587,7 @@ flashes: tag: notice: # tag_added: 'Tag added' + # tag_renamed: 'Tag renamed' import: notice: # failed: 'Import failed, please try again.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml index c297ffb5d..888d9b398 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml @@ -399,6 +399,8 @@ tag: new: add: 'Hinzufügen' placeholder: 'Du kannst verschiedene Tags, getrennt von einem Komma, hinzufügen.' + rename: + # placeholder: 'You can update tag name.' export: footer_template: '

    Generiert von wallabag mit Hilfe von %method%

    Bitte öffne ein Ticket wenn du ein Problem mit der Darstellung von diesem E-Book auf deinem Gerät hast.

    ' @@ -585,6 +587,7 @@ flashes: tag: notice: tag_added: 'Tag hinzugefügt' + #tag_renamed: 'Tag renamed' import: notice: failed: 'Import fehlgeschlagen, bitte erneut probieren.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml index bd81c72f2..827bf7706 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml @@ -399,6 +399,8 @@ tag: new: add: 'Add' placeholder: 'You can add several tags, separated by a comma.' + rename: + placeholder: 'You can update tag name.' export: footer_template: '

    Produced by wallabag with %method%

    Please open an issue if you have trouble with the display of this E-Book on your device.

    ' @@ -585,6 +587,7 @@ flashes: tag: notice: tag_added: 'Tag added' + tag_renamed: 'Tag renamed' import: notice: failed: 'Import failed, please try again.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml index 700190a67..e5878f2c4 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml @@ -399,6 +399,8 @@ tag: new: add: 'Añadir' placeholder: 'Puedes añadir varias etiquetas, separadas por una coma.' + rename: + # placeholder: 'You can update tag name.' # export: # footer_template: '

    Produced by wallabag with %method%

    Please open an issue if you have trouble with the display of this E-Book on your device.

    ' @@ -585,6 +587,7 @@ flashes: tag: notice: tag_added: 'Etiqueta añadida' + # tag_renamed: 'Tag renamed' import: notice: failed: 'Importación fallida, por favor, inténtelo de nuevo.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml index 836459336..2e922358c 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml @@ -399,6 +399,8 @@ tag: new: # add: 'Add' # placeholder: 'You can add several tags, separated by a comma.' + rename: + # placeholder: 'You can update tag name.' # export: # footer_template: '

    Produced by wallabag with %method%

    Please open an issue if you have trouble with the display of this E-Book on your device.

    ' @@ -585,6 +587,7 @@ flashes: tag: notice: tag_added: 'برچسب افزوده شد' + # tag_renamed: 'Tag renamed' import: notice: failed: 'درون‌ریزی شکست خورد. لطفاً دوباره تلاش کنید.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml index edf296543..cf5031d38 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml @@ -399,6 +399,8 @@ tag: new: add: "Ajouter" placeholder: "Vous pouvez ajouter plusieurs tags, séparés par une virgule." + rename: + placeholder: 'Vous pouvez changer le nom de votre tag.' export: footer_template: '

    Généré par wallabag with %method%

    Merci d''ouvrir un ticket si vous rencontrez des soucis d''affichage avec ce document sur votre support.

    ' @@ -585,6 +587,7 @@ flashes: tag: notice: tag_added: "Tag ajouté" + tag_renamed: "Tag renommé" import: notice: failed: "L’import a échoué, veuillez ré-essayer" diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml index 47292116f..1563703ac 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml @@ -399,6 +399,8 @@ tag: new: add: 'Aggiungi' placeholder: 'Puoi aggiungere varie etichette, separate da una virgola.' + rename: + # placeholder: 'You can update tag name.' # export: # footer_template: '

    Produced by wallabag with %method%

    Please open an issue if you have trouble with the display of this E-Book on your device.

    ' @@ -585,6 +587,7 @@ flashes: tag: notice: tag_added: 'Etichetta aggiunta' + # tag_renamed: 'Tag renamed' import: notice: failed: 'Importazione fallita, riprova.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml index 95bc9560f..9e9f8a2f5 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml @@ -399,6 +399,8 @@ tag: new: add: 'Ajustar' placeholder: "Podètz ajustar mai qu'una etiqueta, separadas per de virgula." + rename: + # placeholder: 'You can update tag name.' export: footer_template: '

    Produch per wallabag amb %method%

    Mercés de dobrir una sollicitacion s’avètz de problèmas amb l’afichatge d’aqueste E-Book sus vòstre periferic.

    ' @@ -585,6 +587,7 @@ flashes: tag: notice: tag_added: 'Etiqueta ajustada' + # tag_renamed: 'Tag renamed' import: notice: failed: "L'importacion a fracassat, mercés de tornar ensajar." diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml index a64e60b01..4e2238d25 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml @@ -399,6 +399,8 @@ tag: new: add: 'Dodaj' placeholder: 'Możesz dodać kilka tagów, oddzielając je przecinkami.' + rename: + # placeholder: 'You can update tag name.' export: footer_template: '

    Stworzone przez wallabag z %method%

    Proszę zgłoś sprawę, jeżeli masz problem z wyświetleniem tego e-booka na swoim urządzeniu.

    ' @@ -585,6 +587,7 @@ flashes: tag: notice: tag_added: 'Tag dodany' + # tag_renamed: 'Tag renamed' import: notice: failed: 'Nieudany import, prosimy spróbować ponownie.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml index 7aef9694e..127b425ee 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml @@ -399,6 +399,8 @@ tag: new: # add: 'Add' # placeholder: 'You can add several tags, separated by a comma.' + rename: + # placeholder: 'You can update tag name.' # export: # footer_template: '

    Produced by wallabag with %method%

    Please open an issue if you have trouble with the display of this E-Book on your device.

    ' @@ -585,6 +587,7 @@ flashes: tag: notice: tag_added: 'Tag adicionada' + # tag_renamed: 'Tag renamed' import: notice: failed: 'Importação falhou, por favor tente novamente.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml index 9b7068c66..e68a91ecc 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml @@ -399,6 +399,8 @@ tag: new: # add: 'Add' # placeholder: 'You can add several tags, separated by a comma.' + rename: + # placeholder: 'You can update tag name.' # export: # footer_template: '

    Produced by wallabag with %method%

    Please open an issue if you have trouble with the display of this E-Book on your device.

    ' @@ -585,6 +587,7 @@ flashes: tag: notice: # tag_added: 'Tag added' + # tag_renamed: 'Tag renamed' import: notice: # failed: 'Import failed, please try again.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml index 5f210c934..d713f13f3 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml @@ -387,6 +387,8 @@ tag: new: add: 'Добавить' placeholder: 'Вы можете добавить несколько тегов, разделенных запятой.' + rename: + # placeholder: 'You can update tag name.' import: page_title: 'Импорт' @@ -547,6 +549,7 @@ flashes: tag: notice: tag_added: 'Тег добавлен' + # tag_renamed: 'Tag renamed' import: notice: failed: 'Во время импорта произошла ошибка, повторите попытку.' @@ -564,4 +567,4 @@ flashes: notice: added: 'Пользователь "%username%" добавлен' updated: 'Пользователь "%username%" обновлен' - deleted: 'Пользователь "%username%" удален' \ No newline at end of file + deleted: 'Пользователь "%username%" удален' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml index 9d22f90d3..78e0f0ee4 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml @@ -397,6 +397,8 @@ tag: new: add: 'เพิ่ม' placeholder: 'คุณสามารถเพิ่มได้หลายแท็ก, จากการแบ่งโดย comma' + rename: + # placeholder: 'You can update tag name.' export: footer_template: '

    ผลิตโดย wallabag กับ %method%

    ให้ทำการเปิด ฉบับนี้ ถ้าคุณมีข้อบกพร่องif you have trouble with the display of this E-Book on your device.

    ' @@ -583,6 +585,7 @@ flashes: tag: notice: tag_added: 'แท็กที่เพิ่ม' + # tag_renamed: 'Tag renamed' import: notice: failed: 'นำข้อมูลเข้าล้มเหลว, ลองใหม่อีกครั้ง' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml index 5c95fe630..c48a885fd 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml @@ -397,6 +397,8 @@ tag: new: # add: 'Add' # placeholder: 'You can add several tags, separated by a comma.' + rename: + # placeholder: 'You can update tag name.' # export: # footer_template: '

    Produced by wallabag with %method%

    Please open an issue if you have trouble with the display of this E-Book on your device.

    ' @@ -563,6 +565,7 @@ flashes: tag: notice: tag_added: 'Etiket eklendi' + # tag_renamed: 'Tag renamed' import: notice: # failed: 'Import failed, please try again.' From 32968bd30e907dcb681831c149a9afcc12a664da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20HULARD?= Date: Wed, 24 Jan 2018 17:30:06 +0100 Subject: [PATCH 010/107] Add specific styles for the card tag form element. Also add a `.hidden` class in the baggy theme to have consistency with material. --- app/Resources/static/themes/baggy/css/layout.scss | 11 ++++++++++- app/Resources/static/themes/material/css/cards.scss | 11 +++++++++++ web/wallassets/material.css | 4 ++-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/Resources/static/themes/baggy/css/layout.scss b/app/Resources/static/themes/baggy/css/layout.scss index cb14e62d4..0293ebe57 100644 --- a/app/Resources/static/themes/baggy/css/layout.scss +++ b/app/Resources/static/themes/baggy/css/layout.scss @@ -295,6 +295,15 @@ div.pagination ul { } } -.hide { +.card-tag-form { + display: inline-block; +} + +.card-tag-form input[type="text"] { + min-width: 20em; +} + +.hide, +.hidden { display: none; } diff --git a/app/Resources/static/themes/material/css/cards.scss b/app/Resources/static/themes/material/css/cards.scss index 8f7f8f7b4..f3319f3d3 100644 --- a/app/Resources/static/themes/material/css/cards.scss +++ b/app/Resources/static/themes/material/css/cards.scss @@ -180,6 +180,17 @@ a.original:not(.waves-effect) { flex-grow: 1; } +.card-tag-form { + display: flex; + min-width: 100px; + flex-grow: 1; +} + +.card-tag-form input { + margin-bottom: 0; + height: 2rem; +} + .card-tag-rss { display: flex; } diff --git a/web/wallassets/material.css b/web/wallassets/material.css index 4a5f9b54c..d6d51082d 100644 --- a/web/wallassets/material.css +++ b/web/wallassets/material.css @@ -1,2 +1,2 @@ -.materialize-red{background-color:#e51c23!important}.materialize-red-text{color:#e51c23!important}.materialize-red.lighten-5{background-color:#fdeaeb!important}.materialize-red-text.text-lighten-5{color:#fdeaeb!important}.materialize-red.lighten-4{background-color:#f8c1c3!important}.materialize-red-text.text-lighten-4{color:#f8c1c3!important}.materialize-red.lighten-3{background-color:#f3989b!important}.materialize-red-text.text-lighten-3{color:#f3989b!important}.materialize-red.lighten-2{background-color:#ee6e73!important}.materialize-red-text.text-lighten-2{color:#ee6e73!important}.materialize-red.lighten-1{background-color:#ea454b!important}.materialize-red-text.text-lighten-1{color:#ea454b!important}.materialize-red.darken-1{background-color:#d0181e!important}.materialize-red-text.text-darken-1{color:#d0181e!important}.materialize-red.darken-2{background-color:#b9151b!important}.materialize-red-text.text-darken-2{color:#b9151b!important}.materialize-red.darken-3{background-color:#a21318!important}.materialize-red-text.text-darken-3{color:#a21318!important}.materialize-red.darken-4{background-color:#8b1014!important}.materialize-red-text.text-darken-4{color:#8b1014!important}.red{background-color:#f44336!important}.red-text{color:#f44336!important}.red.lighten-5{background-color:#ffebee!important}.red-text.text-lighten-5{color:#ffebee!important}.red.lighten-4{background-color:#ffcdd2!important}.red-text.text-lighten-4{color:#ffcdd2!important}.red.lighten-3{background-color:#ef9a9a!important}.red-text.text-lighten-3{color:#ef9a9a!important}.red.lighten-2{background-color:#e57373!important}.red-text.text-lighten-2{color:#e57373!important}.red.lighten-1{background-color:#ef5350!important}.red-text.text-lighten-1{color:#ef5350!important}.red.darken-1{background-color:#e53935!important}.red-text.text-darken-1{color:#e53935!important}.red.darken-2{background-color:#d32f2f!important}.red-text.text-darken-2{color:#d32f2f!important}.red.darken-3{background-color:#c62828!important}.red-text.text-darken-3{color:#c62828!important}.red.darken-4{background-color:#b71c1c!important}.red-text.text-darken-4{color:#b71c1c!important}.red.accent-1{background-color:#ff8a80!important}.red-text.text-accent-1{color:#ff8a80!important}.red.accent-2{background-color:#ff5252!important}.red-text.text-accent-2{color:#ff5252!important}.red.accent-3{background-color:#ff1744!important}.red-text.text-accent-3{color:#ff1744!important}.red.accent-4{background-color:#d50000!important}.red-text.text-accent-4{color:#d50000!important}.pink{background-color:#e91e63!important}.pink-text{color:#e91e63!important}.pink.lighten-5{background-color:#fce4ec!important}.pink-text.text-lighten-5{color:#fce4ec!important}.pink.lighten-4{background-color:#f8bbd0!important}.pink-text.text-lighten-4{color:#f8bbd0!important}.pink.lighten-3{background-color:#f48fb1!important}.pink-text.text-lighten-3{color:#f48fb1!important}.pink.lighten-2{background-color:#f06292!important}.pink-text.text-lighten-2{color:#f06292!important}.pink.lighten-1{background-color:#ec407a!important}.pink-text.text-lighten-1{color:#ec407a!important}.pink.darken-1{background-color:#d81b60!important}.pink-text.text-darken-1{color:#d81b60!important}.pink.darken-2{background-color:#c2185b!important}.pink-text.text-darken-2{color:#c2185b!important}.pink.darken-3{background-color:#ad1457!important}.pink-text.text-darken-3{color:#ad1457!important}.pink.darken-4{background-color:#880e4f!important}.pink-text.text-darken-4{color:#880e4f!important}.pink.accent-1{background-color:#ff80ab!important}.pink-text.text-accent-1{color:#ff80ab!important}.pink.accent-2{background-color:#ff4081!important}.pink-text.text-accent-2{color:#ff4081!important}.pink.accent-3{background-color:#f50057!important}.pink-text.text-accent-3{color:#f50057!important}.pink.accent-4{background-color:#c51162!important}.pink-text.text-accent-4{color:#c51162!important}.purple{background-color:#9c27b0!important}.purple-text{color:#9c27b0!important}.purple.lighten-5{background-color:#f3e5f5!important}.purple-text.text-lighten-5{color:#f3e5f5!important}.purple.lighten-4{background-color:#e1bee7!important}.purple-text.text-lighten-4{color:#e1bee7!important}.purple.lighten-3{background-color:#ce93d8!important}.purple-text.text-lighten-3{color:#ce93d8!important}.purple.lighten-2{background-color:#ba68c8!important}.purple-text.text-lighten-2{color:#ba68c8!important}.purple.lighten-1{background-color:#ab47bc!important}.purple-text.text-lighten-1{color:#ab47bc!important}.purple.darken-1{background-color:#8e24aa!important}.purple-text.text-darken-1{color:#8e24aa!important}.purple.darken-2{background-color:#7b1fa2!important}.purple-text.text-darken-2{color:#7b1fa2!important}.purple.darken-3{background-color:#6a1b9a!important}.purple-text.text-darken-3{color:#6a1b9a!important}.purple.darken-4{background-color:#4a148c!important}.purple-text.text-darken-4{color:#4a148c!important}.purple.accent-1{background-color:#ea80fc!important}.purple-text.text-accent-1{color:#ea80fc!important}.purple.accent-2{background-color:#e040fb!important}.purple-text.text-accent-2{color:#e040fb!important}.purple.accent-3{background-color:#d500f9!important}.purple-text.text-accent-3{color:#d500f9!important}.purple.accent-4{background-color:#a0f!important}.purple-text.text-accent-4{color:#a0f!important}.deep-purple{background-color:#673ab7!important}.deep-purple-text{color:#673ab7!important}.deep-purple.lighten-5{background-color:#ede7f6!important}.deep-purple-text.text-lighten-5{color:#ede7f6!important}.deep-purple.lighten-4{background-color:#d1c4e9!important}.deep-purple-text.text-lighten-4{color:#d1c4e9!important}.deep-purple.lighten-3{background-color:#b39ddb!important}.deep-purple-text.text-lighten-3{color:#b39ddb!important}.deep-purple.lighten-2{background-color:#9575cd!important}.deep-purple-text.text-lighten-2{color:#9575cd!important}.deep-purple.lighten-1{background-color:#7e57c2!important}.deep-purple-text.text-lighten-1{color:#7e57c2!important}.deep-purple.darken-1{background-color:#5e35b1!important}.deep-purple-text.text-darken-1{color:#5e35b1!important}.deep-purple.darken-2{background-color:#512da8!important}.deep-purple-text.text-darken-2{color:#512da8!important}.deep-purple.darken-3{background-color:#4527a0!important}.deep-purple-text.text-darken-3{color:#4527a0!important}.deep-purple.darken-4{background-color:#311b92!important}.deep-purple-text.text-darken-4{color:#311b92!important}.deep-purple.accent-1{background-color:#b388ff!important}.deep-purple-text.text-accent-1{color:#b388ff!important}.deep-purple.accent-2{background-color:#7c4dff!important}.deep-purple-text.text-accent-2{color:#7c4dff!important}.deep-purple.accent-3{background-color:#651fff!important}.deep-purple-text.text-accent-3{color:#651fff!important}.deep-purple.accent-4{background-color:#6200ea!important}.deep-purple-text.text-accent-4{color:#6200ea!important}.indigo{background-color:#3f51b5!important}.indigo-text{color:#3f51b5!important}.indigo.lighten-5{background-color:#e8eaf6!important}.indigo-text.text-lighten-5{color:#e8eaf6!important}.indigo.lighten-4{background-color:#c5cae9!important}.indigo-text.text-lighten-4{color:#c5cae9!important}.indigo.lighten-3{background-color:#9fa8da!important}.indigo-text.text-lighten-3{color:#9fa8da!important}.indigo.lighten-2{background-color:#7986cb!important}.indigo-text.text-lighten-2{color:#7986cb!important}.indigo.lighten-1{background-color:#5c6bc0!important}.indigo-text.text-lighten-1{color:#5c6bc0!important}.indigo.darken-1{background-color:#3949ab!important}.indigo-text.text-darken-1{color:#3949ab!important}.indigo.darken-2{background-color:#303f9f!important}.indigo-text.text-darken-2{color:#303f9f!important}.indigo.darken-3{background-color:#283593!important}.indigo-text.text-darken-3{color:#283593!important}.indigo.darken-4{background-color:#1a237e!important}.indigo-text.text-darken-4{color:#1a237e!important}.indigo.accent-1{background-color:#8c9eff!important}.indigo-text.text-accent-1{color:#8c9eff!important}.indigo.accent-2{background-color:#536dfe!important}.indigo-text.text-accent-2{color:#536dfe!important}.indigo.accent-3{background-color:#3d5afe!important}.indigo-text.text-accent-3{color:#3d5afe!important}.indigo.accent-4{background-color:#304ffe!important}.indigo-text.text-accent-4{color:#304ffe!important}.blue{background-color:#2196f3!important}.blue-text{color:#2196f3!important}.blue.lighten-5{background-color:#e3f2fd!important}.blue-text.text-lighten-5{color:#e3f2fd!important}.blue.lighten-4{background-color:#bbdefb!important}.blue-text.text-lighten-4{color:#bbdefb!important}.blue.lighten-3{background-color:#90caf9!important}.blue-text.text-lighten-3{color:#90caf9!important}.blue.lighten-2{background-color:#64b5f6!important}.blue-text.text-lighten-2{color:#64b5f6!important}.blue.lighten-1{background-color:#42a5f5!important}.blue-text.text-lighten-1{color:#42a5f5!important}.blue.darken-1{background-color:#1e88e5!important}.blue-text.text-darken-1{color:#1e88e5!important}.blue.darken-2{background-color:#1976d2!important}.blue-text.text-darken-2{color:#1976d2!important}.blue.darken-3{background-color:#1565c0!important}.blue-text.text-darken-3{color:#1565c0!important}.blue.darken-4{background-color:#0d47a1!important}.blue-text.text-darken-4{color:#0d47a1!important}.blue.accent-1{background-color:#82b1ff!important}.blue-text.text-accent-1{color:#82b1ff!important}.blue.accent-2{background-color:#448aff!important}.blue-text.text-accent-2{color:#448aff!important}.blue.accent-3{background-color:#2979ff!important}.blue-text.text-accent-3{color:#2979ff!important}.blue.accent-4{background-color:#2962ff!important}.blue-text.text-accent-4{color:#2962ff!important}.light-blue{background-color:#03a9f4!important}.light-blue-text{color:#03a9f4!important}.light-blue.lighten-5{background-color:#e1f5fe!important}.light-blue-text.text-lighten-5{color:#e1f5fe!important}.light-blue.lighten-4{background-color:#b3e5fc!important}.light-blue-text.text-lighten-4{color:#b3e5fc!important}.light-blue.lighten-3{background-color:#81d4fa!important}.light-blue-text.text-lighten-3{color:#81d4fa!important}.light-blue.lighten-2{background-color:#4fc3f7!important}.light-blue-text.text-lighten-2{color:#4fc3f7!important}.light-blue.lighten-1{background-color:#29b6f6!important}.light-blue-text.text-lighten-1{color:#29b6f6!important}.light-blue.darken-1{background-color:#039be5!important}.light-blue-text.text-darken-1{color:#039be5!important}.light-blue.darken-2{background-color:#0288d1!important}.light-blue-text.text-darken-2{color:#0288d1!important}.light-blue.darken-3{background-color:#0277bd!important}.light-blue-text.text-darken-3{color:#0277bd!important}.light-blue.darken-4{background-color:#01579b!important}.light-blue-text.text-darken-4{color:#01579b!important}.light-blue.accent-1{background-color:#80d8ff!important}.light-blue-text.text-accent-1{color:#80d8ff!important}.light-blue.accent-2{background-color:#40c4ff!important}.light-blue-text.text-accent-2{color:#40c4ff!important}.light-blue.accent-3{background-color:#00b0ff!important}.light-blue-text.text-accent-3{color:#00b0ff!important}.light-blue.accent-4{background-color:#0091ea!important}.light-blue-text.text-accent-4{color:#0091ea!important}.cyan{background-color:#00bcd4!important}.cyan-text{color:#00bcd4!important}.cyan.lighten-5{background-color:#e0f7fa!important}.cyan-text.text-lighten-5{color:#e0f7fa!important}.cyan.lighten-4{background-color:#b2ebf2!important}.cyan-text.text-lighten-4{color:#b2ebf2!important}.cyan.lighten-3{background-color:#80deea!important}.cyan-text.text-lighten-3{color:#80deea!important}.cyan.lighten-2{background-color:#4dd0e1!important}.cyan-text.text-lighten-2{color:#4dd0e1!important}.cyan.lighten-1{background-color:#26c6da!important}.cyan-text.text-lighten-1{color:#26c6da!important}.cyan.darken-1{background-color:#00acc1!important}.cyan-text.text-darken-1{color:#00acc1!important}.cyan.darken-2{background-color:#0097a7!important}.cyan-text.text-darken-2{color:#0097a7!important}.cyan.darken-3{background-color:#00838f!important}.cyan-text.text-darken-3{color:#00838f!important}.cyan.darken-4{background-color:#006064!important}.cyan-text.text-darken-4{color:#006064!important}.cyan.accent-1{background-color:#84ffff!important}.cyan-text.text-accent-1{color:#84ffff!important}.cyan.accent-2{background-color:#18ffff!important}.cyan-text.text-accent-2{color:#18ffff!important}.cyan.accent-3{background-color:#00e5ff!important}.cyan-text.text-accent-3{color:#00e5ff!important}.cyan.accent-4{background-color:#00b8d4!important}.cyan-text.text-accent-4{color:#00b8d4!important}.teal{background-color:#009688!important}.teal-text{color:#009688!important}.teal.lighten-5{background-color:#e0f2f1!important}.teal-text.text-lighten-5{color:#e0f2f1!important}.teal.lighten-4{background-color:#b2dfdb!important}.teal-text.text-lighten-4{color:#b2dfdb!important}.teal.lighten-3{background-color:#80cbc4!important}.teal-text.text-lighten-3{color:#80cbc4!important}.teal.lighten-2{background-color:#4db6ac!important}.teal-text.text-lighten-2{color:#4db6ac!important}.teal.lighten-1{background-color:#26a69a!important}.teal-text.text-lighten-1{color:#26a69a!important}.teal.darken-1{background-color:#00897b!important}.teal-text.text-darken-1{color:#00897b!important}.teal.darken-2{background-color:#00796b!important}.teal-text.text-darken-2{color:#00796b!important}.teal.darken-3{background-color:#00695c!important}.teal-text.text-darken-3{color:#00695c!important}.teal.darken-4{background-color:#004d40!important}.teal-text.text-darken-4{color:#004d40!important}.teal.accent-1{background-color:#a7ffeb!important}.teal-text.text-accent-1{color:#a7ffeb!important}.teal.accent-2{background-color:#64ffda!important}.teal-text.text-accent-2{color:#64ffda!important}.teal.accent-3{background-color:#1de9b6!important}.teal-text.text-accent-3{color:#1de9b6!important}.teal.accent-4{background-color:#00bfa5!important}.teal-text.text-accent-4{color:#00bfa5!important}.green{background-color:#4caf50!important}.green-text{color:#4caf50!important}.green.lighten-5{background-color:#e8f5e9!important}.green-text.text-lighten-5{color:#e8f5e9!important}.green.lighten-4{background-color:#c8e6c9!important}.green-text.text-lighten-4{color:#c8e6c9!important}.green.lighten-3{background-color:#a5d6a7!important}.green-text.text-lighten-3{color:#a5d6a7!important}.green.lighten-2{background-color:#81c784!important}.green-text.text-lighten-2{color:#81c784!important}.green.lighten-1{background-color:#66bb6a!important}.green-text.text-lighten-1{color:#66bb6a!important}.green.darken-1{background-color:#43a047!important}.green-text.text-darken-1{color:#43a047!important}.green.darken-2{background-color:#388e3c!important}.green-text.text-darken-2{color:#388e3c!important}.green.darken-3{background-color:#2e7d32!important}.green-text.text-darken-3{color:#2e7d32!important}.green.darken-4{background-color:#1b5e20!important}.green-text.text-darken-4{color:#1b5e20!important}.green.accent-1{background-color:#b9f6ca!important}.green-text.text-accent-1{color:#b9f6ca!important}.green.accent-2{background-color:#69f0ae!important}.green-text.text-accent-2{color:#69f0ae!important}.green.accent-3{background-color:#00e676!important}.green-text.text-accent-3{color:#00e676!important}.green.accent-4{background-color:#00c853!important}.green-text.text-accent-4{color:#00c853!important}.light-green{background-color:#8bc34a!important}.light-green-text{color:#8bc34a!important}.light-green.lighten-5{background-color:#f1f8e9!important}.light-green-text.text-lighten-5{color:#f1f8e9!important}.light-green.lighten-4{background-color:#dcedc8!important}.light-green-text.text-lighten-4{color:#dcedc8!important}.light-green.lighten-3{background-color:#c5e1a5!important}.light-green-text.text-lighten-3{color:#c5e1a5!important}.light-green.lighten-2{background-color:#aed581!important}.light-green-text.text-lighten-2{color:#aed581!important}.light-green.lighten-1{background-color:#9ccc65!important}.light-green-text.text-lighten-1{color:#9ccc65!important}.light-green.darken-1{background-color:#7cb342!important}.light-green-text.text-darken-1{color:#7cb342!important}.light-green.darken-2{background-color:#689f38!important}.light-green-text.text-darken-2{color:#689f38!important}.light-green.darken-3{background-color:#558b2f!important}.light-green-text.text-darken-3{color:#558b2f!important}.light-green.darken-4{background-color:#33691e!important}.light-green-text.text-darken-4{color:#33691e!important}.light-green.accent-1{background-color:#ccff90!important}.light-green-text.text-accent-1{color:#ccff90!important}.light-green.accent-2{background-color:#b2ff59!important}.light-green-text.text-accent-2{color:#b2ff59!important}.light-green.accent-3{background-color:#76ff03!important}.light-green-text.text-accent-3{color:#76ff03!important}.light-green.accent-4{background-color:#64dd17!important}.light-green-text.text-accent-4{color:#64dd17!important}.lime{background-color:#cddc39!important}.lime-text{color:#cddc39!important}.lime.lighten-5{background-color:#f9fbe7!important}.lime-text.text-lighten-5{color:#f9fbe7!important}.lime.lighten-4{background-color:#f0f4c3!important}.lime-text.text-lighten-4{color:#f0f4c3!important}.lime.lighten-3{background-color:#e6ee9c!important}.lime-text.text-lighten-3{color:#e6ee9c!important}.lime.lighten-2{background-color:#dce775!important}.lime-text.text-lighten-2{color:#dce775!important}.lime.lighten-1{background-color:#d4e157!important}.lime-text.text-lighten-1{color:#d4e157!important}.lime.darken-1{background-color:#c0ca33!important}.lime-text.text-darken-1{color:#c0ca33!important}.lime.darken-2{background-color:#afb42b!important}.lime-text.text-darken-2{color:#afb42b!important}.lime.darken-3{background-color:#9e9d24!important}.lime-text.text-darken-3{color:#9e9d24!important}.lime.darken-4{background-color:#827717!important}.lime-text.text-darken-4{color:#827717!important}.lime.accent-1{background-color:#f4ff81!important}.lime-text.text-accent-1{color:#f4ff81!important}.lime.accent-2{background-color:#eeff41!important}.lime-text.text-accent-2{color:#eeff41!important}.lime.accent-3{background-color:#c6ff00!important}.lime-text.text-accent-3{color:#c6ff00!important}.lime.accent-4{background-color:#aeea00!important}.lime-text.text-accent-4{color:#aeea00!important}.yellow{background-color:#ffeb3b!important}.yellow-text{color:#ffeb3b!important}.yellow.lighten-5{background-color:#fffde7!important}.yellow-text.text-lighten-5{color:#fffde7!important}.yellow.lighten-4{background-color:#fff9c4!important}.yellow-text.text-lighten-4{color:#fff9c4!important}.yellow.lighten-3{background-color:#fff59d!important}.yellow-text.text-lighten-3{color:#fff59d!important}.yellow.lighten-2{background-color:#fff176!important}.yellow-text.text-lighten-2{color:#fff176!important}.yellow.lighten-1{background-color:#ffee58!important}.yellow-text.text-lighten-1{color:#ffee58!important}.yellow.darken-1{background-color:#fdd835!important}.yellow-text.text-darken-1{color:#fdd835!important}.yellow.darken-2{background-color:#fbc02d!important}.yellow-text.text-darken-2{color:#fbc02d!important}.yellow.darken-3{background-color:#f9a825!important}.yellow-text.text-darken-3{color:#f9a825!important}.yellow.darken-4{background-color:#f57f17!important}.yellow-text.text-darken-4{color:#f57f17!important}.yellow.accent-1{background-color:#ffff8d!important}.yellow-text.text-accent-1{color:#ffff8d!important}.yellow.accent-2{background-color:#ff0!important}.yellow-text.text-accent-2{color:#ff0!important}.yellow.accent-3{background-color:#ffea00!important}.yellow-text.text-accent-3{color:#ffea00!important}.yellow.accent-4{background-color:#ffd600!important}.yellow-text.text-accent-4{color:#ffd600!important}.amber{background-color:#ffc107!important}.amber-text{color:#ffc107!important}.amber.lighten-5{background-color:#fff8e1!important}.amber-text.text-lighten-5{color:#fff8e1!important}.amber.lighten-4{background-color:#ffecb3!important}.amber-text.text-lighten-4{color:#ffecb3!important}.amber.lighten-3{background-color:#ffe082!important}.amber-text.text-lighten-3{color:#ffe082!important}.amber.lighten-2{background-color:#ffd54f!important}.amber-text.text-lighten-2{color:#ffd54f!important}.amber.lighten-1{background-color:#ffca28!important}.amber-text.text-lighten-1{color:#ffca28!important}.amber.darken-1{background-color:#ffb300!important}.amber-text.text-darken-1{color:#ffb300!important}.amber.darken-2{background-color:#ffa000!important}.amber-text.text-darken-2{color:#ffa000!important}.amber.darken-3{background-color:#ff8f00!important}.amber-text.text-darken-3{color:#ff8f00!important}.amber.darken-4{background-color:#ff6f00!important}.amber-text.text-darken-4{color:#ff6f00!important}.amber.accent-1{background-color:#ffe57f!important}.amber-text.text-accent-1{color:#ffe57f!important}.amber.accent-2{background-color:#ffd740!important}.amber-text.text-accent-2{color:#ffd740!important}.amber.accent-3{background-color:#ffc400!important}.amber-text.text-accent-3{color:#ffc400!important}.amber.accent-4{background-color:#ffab00!important}.amber-text.text-accent-4{color:#ffab00!important}.orange{background-color:#ff9800!important}.orange-text{color:#ff9800!important}.orange.lighten-5{background-color:#fff3e0!important}.orange-text.text-lighten-5{color:#fff3e0!important}.orange.lighten-4{background-color:#ffe0b2!important}.orange-text.text-lighten-4{color:#ffe0b2!important}.orange.lighten-3{background-color:#ffcc80!important}.orange-text.text-lighten-3{color:#ffcc80!important}.orange.lighten-2{background-color:#ffb74d!important}.orange-text.text-lighten-2{color:#ffb74d!important}.orange.lighten-1{background-color:#ffa726!important}.orange-text.text-lighten-1{color:#ffa726!important}.orange.darken-1{background-color:#fb8c00!important}.orange-text.text-darken-1{color:#fb8c00!important}.orange.darken-2{background-color:#f57c00!important}.orange-text.text-darken-2{color:#f57c00!important}.orange.darken-3{background-color:#ef6c00!important}.orange-text.text-darken-3{color:#ef6c00!important}.orange.darken-4{background-color:#e65100!important}.orange-text.text-darken-4{color:#e65100!important}.orange.accent-1{background-color:#ffd180!important}.orange-text.text-accent-1{color:#ffd180!important}.orange.accent-2{background-color:#ffab40!important}.orange-text.text-accent-2{color:#ffab40!important}.orange.accent-3{background-color:#ff9100!important}.orange-text.text-accent-3{color:#ff9100!important}.orange.accent-4{background-color:#ff6d00!important}.orange-text.text-accent-4{color:#ff6d00!important}.deep-orange{background-color:#ff5722!important}.deep-orange-text{color:#ff5722!important}.deep-orange.lighten-5{background-color:#fbe9e7!important}.deep-orange-text.text-lighten-5{color:#fbe9e7!important}.deep-orange.lighten-4{background-color:#ffccbc!important}.deep-orange-text.text-lighten-4{color:#ffccbc!important}.deep-orange.lighten-3{background-color:#ffab91!important}.deep-orange-text.text-lighten-3{color:#ffab91!important}.deep-orange.lighten-2{background-color:#ff8a65!important}.deep-orange-text.text-lighten-2{color:#ff8a65!important}.deep-orange.lighten-1{background-color:#ff7043!important}.deep-orange-text.text-lighten-1{color:#ff7043!important}.deep-orange.darken-1{background-color:#f4511e!important}.deep-orange-text.text-darken-1{color:#f4511e!important}.deep-orange.darken-2{background-color:#e64a19!important}.deep-orange-text.text-darken-2{color:#e64a19!important}.deep-orange.darken-3{background-color:#d84315!important}.deep-orange-text.text-darken-3{color:#d84315!important}.deep-orange.darken-4{background-color:#bf360c!important}.deep-orange-text.text-darken-4{color:#bf360c!important}.deep-orange.accent-1{background-color:#ff9e80!important}.deep-orange-text.text-accent-1{color:#ff9e80!important}.deep-orange.accent-2{background-color:#ff6e40!important}.deep-orange-text.text-accent-2{color:#ff6e40!important}.deep-orange.accent-3{background-color:#ff3d00!important}.deep-orange-text.text-accent-3{color:#ff3d00!important}.deep-orange.accent-4{background-color:#dd2c00!important}.deep-orange-text.text-accent-4{color:#dd2c00!important}.brown{background-color:#795548!important}.brown-text{color:#795548!important}.brown.lighten-5{background-color:#efebe9!important}.brown-text.text-lighten-5{color:#efebe9!important}.brown.lighten-4{background-color:#d7ccc8!important}.brown-text.text-lighten-4{color:#d7ccc8!important}.brown.lighten-3{background-color:#bcaaa4!important}.brown-text.text-lighten-3{color:#bcaaa4!important}.brown.lighten-2{background-color:#a1887f!important}.brown-text.text-lighten-2{color:#a1887f!important}.brown.lighten-1{background-color:#8d6e63!important}.brown-text.text-lighten-1{color:#8d6e63!important}.brown.darken-1{background-color:#6d4c41!important}.brown-text.text-darken-1{color:#6d4c41!important}.brown.darken-2{background-color:#5d4037!important}.brown-text.text-darken-2{color:#5d4037!important}.brown.darken-3{background-color:#4e342e!important}.brown-text.text-darken-3{color:#4e342e!important}.brown.darken-4{background-color:#3e2723!important}.brown-text.text-darken-4{color:#3e2723!important}.blue-grey{background-color:#607d8b!important}.blue-grey-text{color:#607d8b!important}.blue-grey.lighten-5{background-color:#eceff1!important}.blue-grey-text.text-lighten-5{color:#eceff1!important}.blue-grey.lighten-4{background-color:#cfd8dc!important}.blue-grey-text.text-lighten-4{color:#cfd8dc!important}.blue-grey.lighten-3{background-color:#b0bec5!important}.blue-grey-text.text-lighten-3{color:#b0bec5!important}.blue-grey.lighten-2{background-color:#90a4ae!important}.blue-grey-text.text-lighten-2{color:#90a4ae!important}.blue-grey.lighten-1{background-color:#78909c!important}.blue-grey-text.text-lighten-1{color:#78909c!important}.blue-grey.darken-1{background-color:#546e7a!important}.blue-grey-text.text-darken-1{color:#546e7a!important}.blue-grey.darken-2{background-color:#455a64!important}.blue-grey-text.text-darken-2{color:#455a64!important}.blue-grey.darken-3{background-color:#37474f!important}.blue-grey-text.text-darken-3{color:#37474f!important}.blue-grey.darken-4{background-color:#263238!important}.blue-grey-text.text-darken-4{color:#263238!important}.grey{background-color:#9e9e9e!important}.grey-text{color:#9e9e9e!important}.grey.lighten-5{background-color:#fafafa!important}.grey-text.text-lighten-5{color:#fafafa!important}.grey.lighten-4{background-color:#f5f5f5!important}.grey-text.text-lighten-4{color:#f5f5f5!important}.grey.lighten-3{background-color:#eee!important}.grey-text.text-lighten-3{color:#eee!important}.grey.lighten-2{background-color:#e0e0e0!important}.grey-text.text-lighten-2{color:#e0e0e0!important}.grey.lighten-1{background-color:#bdbdbd!important}.grey-text.text-lighten-1{color:#bdbdbd!important}.grey.darken-1{background-color:#757575!important}.grey-text.text-darken-1{color:#757575!important}.grey.darken-2{background-color:#616161!important}.grey-text.text-darken-2{color:#616161!important}.grey.darken-3{background-color:#424242!important}.grey-text.text-darken-3{color:#424242!important}.grey.darken-4{background-color:#212121!important}.grey-text.text-darken-4{color:#212121!important}.black{background-color:#000!important}.black-text{color:#000!important}.white{background-color:#fff!important}.white-text{color:#fff!important}.transparent{background-color:transparent!important}.transparent-text{color:transparent!important}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}ul:not(.browser-default){padding-left:0;list-style-type:none}ul:not(.browser-default) li{list-style-type:none}a{color:#039be5;-webkit-tap-highlight-color:transparent}.valign-wrapper{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.clearfix{clear:both}.z-depth-0{box-shadow:none!important}.btn,.btn-floating,.btn-large,.card,.card-panel,.collapsible,.dropdown-content,.side-nav,.toast,.z-depth-1,nav{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2)}.btn-floating:hover,.btn-large:hover,.btn:hover,.z-depth-1-half{box-shadow:0 3px 3px 0 rgba(0,0,0,.14),0 1px 7px 0 rgba(0,0,0,.12),0 3px 1px -1px rgba(0,0,0,.2)}.z-depth-2{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.3)}.z-depth-3{box-shadow:0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12),0 3px 5px -1px rgba(0,0,0,.3)}.modal,.z-depth-4{box-shadow:0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.3)}.z-depth-5{box-shadow:0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.3)}.hoverable{transition:box-shadow .25s;box-shadow:0}.hoverable:hover{transition:box-shadow .25s;box-shadow:0 8px 17px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19)}.divider{height:1px;overflow:hidden;background-color:#e0e0e0}blockquote{margin:20px 0;padding-left:1.5rem;border-left:5px solid #ee6e73}i{line-height:inherit}i.left{float:left;margin-right:15px}i.right{float:right;margin-left:15px}i.tiny{font-size:1rem}i.small{font-size:2rem}i.medium{font-size:4rem}i.large{font-size:6rem}img.responsive-img,video.responsive-video{max-width:100%;height:auto}.pagination li{display:inline-block;border-radius:2px;text-align:center;vertical-align:top;height:30px}.pagination li a{color:#444;display:inline-block;font-size:1.2rem;padding:0 10px;line-height:30px}.pagination li.active a{color:#fff}.pagination li.active{background-color:#ee6e73}.pagination li.disabled a{cursor:default;color:#999}.pagination li i{font-size:2rem}.pagination li.pages ul li{display:inline-block;float:none}@media only screen and (max-width:992px){.pagination{width:100%}.pagination li.next,.pagination li.prev{width:10%}.pagination li.pages{width:80%;overflow:hidden;white-space:nowrap}}.breadcrumb{font-size:18px;color:hsla(0,0%,100%,.7)}.breadcrumb [class*=mdi-],.breadcrumb [class^=mdi-],.breadcrumb i,.breadcrumb i.material-icons{display:inline-block;float:left;font-size:24px}.breadcrumb:before{content:"\E5CC";color:hsla(0,0%,100%,.7);vertical-align:top;display:inline-block;font-family:Material Icons;font-weight:400;font-style:normal;font-size:25px;margin:0 10px 0 8px;-webkit-font-smoothing:antialiased}.breadcrumb:first-child:before{display:none}.breadcrumb:last-child{color:#fff}.parallax-container{position:relative;overflow:hidden;height:500px}.parallax{top:0;left:0;right:0;z-index:-1}.parallax,.parallax img{position:absolute;bottom:0}.parallax img{display:none;left:50%;min-width:100%;min-height:100%;-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-transform:translateX(-50%);transform:translateX(-50%)}.pin-bottom,.pin-top{position:relative}.pinned{position:fixed!important}.fade-in,ul.staggered-list li{opacity:0}.fade-in{-webkit-transform-origin:0 50%;transform-origin:0 50%}@media only screen and (max-width:600px){.hide-on-small-and-down,.hide-on-small-only{display:none!important}}@media only screen and (max-width:992px){.hide-on-med-and-down{display:none!important}}@media only screen and (min-width:601px){.hide-on-med-and-up{display:none!important}}@media only screen and (min-width:600px) and (max-width:992px){.hide-on-med-only{display:none!important}}@media only screen and (min-width:993px){.hide-on-large-only{display:none!important}}@media only screen and (min-width:993px){.show-on-large{display:block!important}}@media only screen and (min-width:600px) and (max-width:992px){.show-on-medium{display:block!important}}@media only screen and (max-width:600px){.show-on-small{display:block!important}}@media only screen and (min-width:601px){.show-on-medium-and-up{display:block!important}}@media only screen and (max-width:992px){.show-on-medium-and-down{display:block!important}}@media only screen and (max-width:600px){.center-on-small-only{text-align:center}}.page-footer{padding-top:20px;background-color:#ee6e73}.page-footer .footer-copyright{overflow:hidden;min-height:50px;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center;padding:10px 0;color:hsla(0,0%,100%,.8);background-color:rgba(51,51,51,.08)}table,td,th{border:none}table{width:100%;display:table}table.bordered>tbody>tr,table.bordered>thead>tr{border-bottom:1px solid #d0d0d0}table.striped>tbody>tr:nth-child(odd){background-color:#f2f2f2}table.striped>tbody>tr>td{border-radius:0}table.highlight>tbody>tr{transition:background-color .25s ease}table.highlight>tbody>tr:hover{background-color:#f2f2f2}table.centered tbody tr td,table.centered thead tr th{text-align:center}thead{border-bottom:1px solid #d0d0d0}td,th{padding:15px 5px;display:table-cell;text-align:left;vertical-align:middle;border-radius:2px}@media only screen and (max-width:992px){table.responsive-table{width:100%;border-collapse:collapse;border-spacing:0;display:block;position:relative}table.responsive-table td:empty:before{content:"\A0"}table.responsive-table td,table.responsive-table th{margin:0;vertical-align:top}table.responsive-table th{text-align:left}table.responsive-table thead{display:block;float:left}table.responsive-table thead tr{display:block;padding:0 10px 0 0}table.responsive-table thead tr th:before{content:"\A0"}table.responsive-table tbody{display:block;width:auto;position:relative;overflow-x:auto;white-space:nowrap}table.responsive-table tbody tr{display:inline-block;vertical-align:top}table.responsive-table th{display:block;text-align:right}table.responsive-table td{display:block;min-height:1.25em;text-align:left}table.responsive-table tr{padding:0 10px}table.responsive-table thead{border:0;border-right:1px solid #d0d0d0}table.responsive-table.bordered th{border-bottom:0;border-left:0}table.responsive-table.bordered td{border-left:0;border-right:0;border-bottom:0}table.responsive-table.bordered tr{border:0}table.responsive-table.bordered tbody tr{border-right:1px solid #d0d0d0}}.collection{margin:.5rem 0 1rem;border:1px solid #e0e0e0;border-radius:2px;overflow:hidden;position:relative}.collection .collection-item{background-color:#fff;line-height:1.5rem;padding:10px 20px;margin:0;border-bottom:1px solid #e0e0e0}.collection .collection-item.avatar{min-height:84px;padding-left:72px;position:relative}.collection .collection-item.avatar .circle{position:absolute;width:42px;height:42px;overflow:hidden;left:15px;display:inline-block;vertical-align:middle}.collection .collection-item.avatar i.circle{font-size:18px;line-height:42px;color:#fff;background-color:#999;text-align:center}.collection .collection-item.avatar .title{font-size:16px}.collection .collection-item.avatar p{margin:0}.collection .collection-item.avatar .secondary-content{position:absolute;top:16px;right:16px}.collection .collection-item:last-child{border-bottom:none}.collection .collection-item.active{background-color:#26a69a;color:#eafaf9}.collection .collection-item.active .secondary-content{color:#fff}.collection a.collection-item{display:block;transition:.25s;color:#26a69a}.collection a.collection-item:not(.active):hover{background-color:#ddd}.collection.with-header .collection-header{background-color:#fff;border-bottom:1px solid #e0e0e0;padding:10px 20px}.collection.with-header .collection-item{padding-left:30px}.collection.with-header .collection-item.avatar{padding-left:72px}.secondary-content{float:right;color:#26a69a}.collapsible .collection{margin:0;border:none}.video-container{position:relative;padding-bottom:56.25%;height:0;overflow:hidden}.video-container embed,.video-container iframe,.video-container object{position:absolute;top:0;left:0;width:100%;height:100%}.progress{position:relative;height:4px;display:block;width:100%;background-color:#acece6;border-radius:2px;margin:.5rem 0 1rem;overflow:hidden}.progress .determinate{position:absolute;top:0;left:0;bottom:0;transition:width .3s linear}.progress .determinate,.progress .indeterminate{background-color:#26a69a}.progress .indeterminate:before{-webkit-animation:indeterminate 2.1s cubic-bezier(.65,.815,.735,.395) infinite;animation:indeterminate 2.1s cubic-bezier(.65,.815,.735,.395) infinite}.progress .indeterminate:after,.progress .indeterminate:before{content:"";position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left,right}.progress .indeterminate:after{-webkit-animation:indeterminate-short 2.1s cubic-bezier(.165,.84,.44,1) infinite;animation:indeterminate-short 2.1s cubic-bezier(.165,.84,.44,1) infinite;-webkit-animation-delay:1.15s;animation-delay:1.15s}@-webkit-keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}to{left:100%;right:-90%}}@keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}to{left:100%;right:-90%}}@-webkit-keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}to{left:107%;right:-8%}}@keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}to{left:107%;right:-8%}}.hide{display:none!important}.left-align{text-align:left}.right-align{text-align:right}.center,.center-align{text-align:center}.left{float:left!important}.right{float:right!important}.no-select,input[type=range],input[type=range]+.thumb{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.circle{border-radius:50%}.center-block{display:block;margin-left:auto;margin-right:auto}.truncate{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.no-padding{padding:0!important}span.badge{min-width:3rem;padding:0 6px;margin-left:14px;text-align:center;font-size:1rem;line-height:22px;height:22px;color:#757575;float:right;box-sizing:border-box}span.badge.new{font-weight:300;font-size:.8rem;color:#fff;background-color:#26a69a;border-radius:2px}span.badge.new:after{content:" new"}span.badge[data-badge-caption]:after{content:" " attr(data-badge-caption)}nav ul a span.badge{display:inline-block;float:none;margin-left:4px;line-height:22px;height:22px}.collection-item span.badge{margin-top:calc(.75rem - 11px)}.collapsible span.badge{margin-top:calc(1.5rem - 11px)}.side-nav span.badge{margin-top:13px}.material-icons{text-rendering:optimizeLegibility;-webkit-font-feature-settings:"liga";-moz-font-feature-settings:"liga";font-feature-settings:"liga"}.container{margin:0 auto;max-width:1280px;width:90%}@media only screen and (min-width:601px){.container{width:85%}}@media only screen and (min-width:993px){.container{width:70%}}.container .row{margin-left:-.75rem;margin-right:-.75rem}.section{padding-top:1rem;padding-bottom:1rem}.section.no-pad{padding:0}.section.no-pad-bot{padding-bottom:0}.section.no-pad-top{padding-top:0}.row{margin-left:auto;margin-right:auto;margin-bottom:20px}.row:after{content:"";display:table;clear:both}.row .col{float:left;box-sizing:border-box;padding:0 .75rem;min-height:1px}.row .col[class*=pull-],.row .col[class*=push-]{position:relative}.row .col.s1{width:8.3333333333%}.row .col.s1,.row .col.s2{margin-left:auto;left:auto;right:auto}.row .col.s2{width:16.6666666667%}.row .col.s3{width:25%}.row .col.s3,.row .col.s4{margin-left:auto;left:auto;right:auto}.row .col.s4{width:33.3333333333%}.row .col.s5{width:41.6666666667%}.row .col.s5,.row .col.s6{margin-left:auto;left:auto;right:auto}.row .col.s6{width:50%}.row .col.s7{width:58.3333333333%}.row .col.s7,.row .col.s8{margin-left:auto;left:auto;right:auto}.row .col.s8{width:66.6666666667%}.row .col.s9{width:75%}.row .col.s9,.row .col.s10{margin-left:auto;left:auto;right:auto}.row .col.s10{width:83.3333333333%}.row .col.s11{width:91.6666666667%}.row .col.s11,.row .col.s12{margin-left:auto;left:auto;right:auto}.row .col.s12{width:100%}.row .col.offset-s1{margin-left:8.3333333333%}.row .col.pull-s1{right:8.3333333333%}.row .col.push-s1{left:8.3333333333%}.row .col.offset-s2{margin-left:16.6666666667%}.row .col.pull-s2{right:16.6666666667%}.row .col.push-s2{left:16.6666666667%}.row .col.offset-s3{margin-left:25%}.row .col.pull-s3{right:25%}.row .col.push-s3{left:25%}.row .col.offset-s4{margin-left:33.3333333333%}.row .col.pull-s4{right:33.3333333333%}.row .col.push-s4{left:33.3333333333%}.row .col.offset-s5{margin-left:41.6666666667%}.row .col.pull-s5{right:41.6666666667%}.row .col.push-s5{left:41.6666666667%}.row .col.offset-s6{margin-left:50%}.row .col.pull-s6{right:50%}.row .col.push-s6{left:50%}.row .col.offset-s7{margin-left:58.3333333333%}.row .col.pull-s7{right:58.3333333333%}.row .col.push-s7{left:58.3333333333%}.row .col.offset-s8{margin-left:66.6666666667%}.row .col.pull-s8{right:66.6666666667%}.row .col.push-s8{left:66.6666666667%}.row .col.offset-s9{margin-left:75%}.row .col.pull-s9{right:75%}.row .col.push-s9{left:75%}.row .col.offset-s10{margin-left:83.3333333333%}.row .col.pull-s10{right:83.3333333333%}.row .col.push-s10{left:83.3333333333%}.row .col.offset-s11{margin-left:91.6666666667%}.row .col.pull-s11{right:91.6666666667%}.row .col.push-s11{left:91.6666666667%}.row .col.offset-s12{margin-left:100%}.row .col.pull-s12{right:100%}.row .col.push-s12{left:100%}@media only screen and (min-width:601px){.row .col.m1{width:8.3333333333%}.row .col.m1,.row .col.m2{margin-left:auto;left:auto;right:auto}.row .col.m2{width:16.6666666667%}.row .col.m3{width:25%}.row .col.m3,.row .col.m4{margin-left:auto;left:auto;right:auto}.row .col.m4{width:33.3333333333%}.row .col.m5{width:41.6666666667%}.row .col.m5,.row .col.m6{margin-left:auto;left:auto;right:auto}.row .col.m6{width:50%}.row .col.m7{width:58.3333333333%}.row .col.m7,.row .col.m8{margin-left:auto;left:auto;right:auto}.row .col.m8{width:66.6666666667%}.row .col.m9{width:75%}.row .col.m9,.row .col.m10{margin-left:auto;left:auto;right:auto}.row .col.m10{width:83.3333333333%}.row .col.m11{width:91.6666666667%}.row .col.m11,.row .col.m12{margin-left:auto;left:auto;right:auto}.row .col.m12{width:100%}.row .col.offset-m1{margin-left:8.3333333333%}.row .col.pull-m1{right:8.3333333333%}.row .col.push-m1{left:8.3333333333%}.row .col.offset-m2{margin-left:16.6666666667%}.row .col.pull-m2{right:16.6666666667%}.row .col.push-m2{left:16.6666666667%}.row .col.offset-m3{margin-left:25%}.row .col.pull-m3{right:25%}.row .col.push-m3{left:25%}.row .col.offset-m4{margin-left:33.3333333333%}.row .col.pull-m4{right:33.3333333333%}.row .col.push-m4{left:33.3333333333%}.row .col.offset-m5{margin-left:41.6666666667%}.row .col.pull-m5{right:41.6666666667%}.row .col.push-m5{left:41.6666666667%}.row .col.offset-m6{margin-left:50%}.row .col.pull-m6{right:50%}.row .col.push-m6{left:50%}.row .col.offset-m7{margin-left:58.3333333333%}.row .col.pull-m7{right:58.3333333333%}.row .col.push-m7{left:58.3333333333%}.row .col.offset-m8{margin-left:66.6666666667%}.row .col.pull-m8{right:66.6666666667%}.row .col.push-m8{left:66.6666666667%}.row .col.offset-m9{margin-left:75%}.row .col.pull-m9{right:75%}.row .col.push-m9{left:75%}.row .col.offset-m10{margin-left:83.3333333333%}.row .col.pull-m10{right:83.3333333333%}.row .col.push-m10{left:83.3333333333%}.row .col.offset-m11{margin-left:91.6666666667%}.row .col.pull-m11{right:91.6666666667%}.row .col.push-m11{left:91.6666666667%}.row .col.offset-m12{margin-left:100%}.row .col.pull-m12{right:100%}.row .col.push-m12{left:100%}}@media only screen and (min-width:993px){.row .col.l1{width:8.3333333333%}.row .col.l1,.row .col.l2{margin-left:auto;left:auto;right:auto}.row .col.l2{width:16.6666666667%}.row .col.l3{width:25%}.row .col.l3,.row .col.l4{margin-left:auto;left:auto;right:auto}.row .col.l4{width:33.3333333333%}.row .col.l5{width:41.6666666667%}.row .col.l5,.row .col.l6{margin-left:auto;left:auto;right:auto}.row .col.l6{width:50%}.row .col.l7{width:58.3333333333%}.row .col.l7,.row .col.l8{margin-left:auto;left:auto;right:auto}.row .col.l8{width:66.6666666667%}.row .col.l9{width:75%}.row .col.l9,.row .col.l10{margin-left:auto;left:auto;right:auto}.row .col.l10{width:83.3333333333%}.row .col.l11{width:91.6666666667%}.row .col.l11,.row .col.l12{margin-left:auto;left:auto;right:auto}.row .col.l12{width:100%}.row .col.offset-l1{margin-left:8.3333333333%}.row .col.pull-l1{right:8.3333333333%}.row .col.push-l1{left:8.3333333333%}.row .col.offset-l2{margin-left:16.6666666667%}.row .col.pull-l2{right:16.6666666667%}.row .col.push-l2{left:16.6666666667%}.row .col.offset-l3{margin-left:25%}.row .col.pull-l3{right:25%}.row .col.push-l3{left:25%}.row .col.offset-l4{margin-left:33.3333333333%}.row .col.pull-l4{right:33.3333333333%}.row .col.push-l4{left:33.3333333333%}.row .col.offset-l5{margin-left:41.6666666667%}.row .col.pull-l5{right:41.6666666667%}.row .col.push-l5{left:41.6666666667%}.row .col.offset-l6{margin-left:50%}.row .col.pull-l6{right:50%}.row .col.push-l6{left:50%}.row .col.offset-l7{margin-left:58.3333333333%}.row .col.pull-l7{right:58.3333333333%}.row .col.push-l7{left:58.3333333333%}.row .col.offset-l8{margin-left:66.6666666667%}.row .col.pull-l8{right:66.6666666667%}.row .col.push-l8{left:66.6666666667%}.row .col.offset-l9{margin-left:75%}.row .col.pull-l9{right:75%}.row .col.push-l9{left:75%}.row .col.offset-l10{margin-left:83.3333333333%}.row .col.pull-l10{right:83.3333333333%}.row .col.push-l10{left:83.3333333333%}.row .col.offset-l11{margin-left:91.6666666667%}.row .col.pull-l11{right:91.6666666667%}.row .col.push-l11{left:91.6666666667%}.row .col.offset-l12{margin-left:100%}.row .col.pull-l12{right:100%}.row .col.push-l12{left:100%}}@media only screen and (min-width:1201px){.row .col.xl1{width:8.3333333333%}.row .col.xl1,.row .col.xl2{margin-left:auto;left:auto;right:auto}.row .col.xl2{width:16.6666666667%}.row .col.xl3{width:25%}.row .col.xl3,.row .col.xl4{margin-left:auto;left:auto;right:auto}.row .col.xl4{width:33.3333333333%}.row .col.xl5{width:41.6666666667%}.row .col.xl5,.row .col.xl6{margin-left:auto;left:auto;right:auto}.row .col.xl6{width:50%}.row .col.xl7{width:58.3333333333%}.row .col.xl7,.row .col.xl8{margin-left:auto;left:auto;right:auto}.row .col.xl8{width:66.6666666667%}.row .col.xl9{width:75%}.row .col.xl9,.row .col.xl10{margin-left:auto;left:auto;right:auto}.row .col.xl10{width:83.3333333333%}.row .col.xl11{width:91.6666666667%}.row .col.xl11,.row .col.xl12{margin-left:auto;left:auto;right:auto}.row .col.xl12{width:100%}.row .col.offset-xl1{margin-left:8.3333333333%}.row .col.pull-xl1{right:8.3333333333%}.row .col.push-xl1{left:8.3333333333%}.row .col.offset-xl2{margin-left:16.6666666667%}.row .col.pull-xl2{right:16.6666666667%}.row .col.push-xl2{left:16.6666666667%}.row .col.offset-xl3{margin-left:25%}.row .col.pull-xl3{right:25%}.row .col.push-xl3{left:25%}.row .col.offset-xl4{margin-left:33.3333333333%}.row .col.pull-xl4{right:33.3333333333%}.row .col.push-xl4{left:33.3333333333%}.row .col.offset-xl5{margin-left:41.6666666667%}.row .col.pull-xl5{right:41.6666666667%}.row .col.push-xl5{left:41.6666666667%}.row .col.offset-xl6{margin-left:50%}.row .col.pull-xl6{right:50%}.row .col.push-xl6{left:50%}.row .col.offset-xl7{margin-left:58.3333333333%}.row .col.pull-xl7{right:58.3333333333%}.row .col.push-xl7{left:58.3333333333%}.row .col.offset-xl8{margin-left:66.6666666667%}.row .col.pull-xl8{right:66.6666666667%}.row .col.push-xl8{left:66.6666666667%}.row .col.offset-xl9{margin-left:75%}.row .col.pull-xl9{right:75%}.row .col.push-xl9{left:75%}.row .col.offset-xl10{margin-left:83.3333333333%}.row .col.pull-xl10{right:83.3333333333%}.row .col.push-xl10{left:83.3333333333%}.row .col.offset-xl11{margin-left:91.6666666667%}.row .col.pull-xl11{right:91.6666666667%}.row .col.push-xl11{left:91.6666666667%}.row .col.offset-xl12{margin-left:100%}.row .col.pull-xl12{right:100%}.row .col.push-xl12{left:100%}}nav{color:#fff;background-color:#ee6e73;width:100%;height:56px;line-height:56px}nav.nav-extended{height:auto}nav.nav-extended .nav-wrapper{min-height:56px;height:auto}nav.nav-extended .nav-content{position:relative;line-height:normal}nav a{color:#fff}nav [class*=mdi-],nav [class^=mdi-],nav i,nav i.material-icons{display:block;font-size:24px;height:56px;line-height:56px}nav .nav-wrapper{position:relative;height:100%}@media only screen and (min-width:993px){nav a.button-collapse{display:none}}nav .button-collapse{float:left;position:relative;z-index:1;height:56px;margin:0 18px}nav .button-collapse i{height:56px;line-height:56px}nav .brand-logo{position:absolute;color:#fff;display:inline-block;font-size:2.1rem;padding:0;white-space:nowrap}nav .brand-logo.center{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}@media only screen and (max-width:992px){nav .brand-logo{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}nav .brand-logo.left,nav .brand-logo.right{padding:0;-webkit-transform:none;transform:none}nav .brand-logo.left{left:.5rem}nav .brand-logo.right{right:.5rem;left:auto}}nav .brand-logo.right{right:.5rem;padding:0}nav .brand-logo [class*=mdi-],nav .brand-logo [class^=mdi-],nav .brand-logo i,nav .brand-logo i.material-icons{float:left;margin-right:15px}nav .nav-title{display:inline-block;font-size:32px;padding:28px 0}nav ul{margin:0}nav ul li{transition:background-color .3s;float:left;padding:0}nav ul li.active{background-color:rgba(0,0,0,.1)}nav ul a{transition:background-color .3s;font-size:1rem;color:#fff;display:block;padding:0 15px;cursor:pointer}nav ul a.btn,nav ul a.btn-flat,nav ul a.btn-floating,nav ul a.btn-large{margin-top:-2px;margin-left:15px;margin-right:15px}nav ul a.btn-flat>.material-icons,nav ul a.btn-floating>.material-icons,nav ul a.btn-large>.material-icons,nav ul a.btn>.material-icons{height:inherit;line-height:inherit}nav ul a:hover{background-color:rgba(0,0,0,.1)}nav ul.left{float:left}nav form{height:100%}nav .input-field{margin:0;height:100%}nav .input-field input{height:100%;font-size:1.2rem;border:none;padding-left:2rem}nav .input-field input:focus,nav .input-field input[type=date]:valid,nav .input-field input[type=email]:valid,nav .input-field input[type=password]:valid,nav .input-field input[type=text]:valid,nav .input-field input[type=url]:valid{border:none;box-shadow:none}nav .input-field label{top:0;left:0}nav .input-field label i{color:hsla(0,0%,100%,.7);transition:color .3s}nav .input-field label.active i{color:#fff}.navbar-fixed{position:relative;height:56px;z-index:997}.navbar-fixed nav{position:fixed}@media only screen and (min-width:601px){nav.nav-extended .nav-wrapper{min-height:64px}nav,nav .nav-wrapper i,nav a.button-collapse,nav a.button-collapse i{height:64px;line-height:64px}.navbar-fixed{height:64px}}@font-face{font-family:Roboto;src:local(Roboto Thin),url(fonts/Roboto-Thin.woff2) format("woff2"),url(fonts/Roboto-Thin.woff) format("woff");font-weight:100}@font-face{font-family:Roboto;src:local(Roboto Light),url(fonts/Roboto-Light.woff2) format("woff2"),url(fonts/Roboto-Light.woff) format("woff");font-weight:300}@font-face{font-family:Roboto;src:local(Roboto Regular),url(fonts/Roboto-Regular.woff2) format("woff2"),url(fonts/Roboto-Regular.woff) format("woff");font-weight:400}@font-face{font-family:Roboto;src:local(Roboto Medium),url(fonts/Roboto-Medium.woff2) format("woff2"),url(fonts/Roboto-Medium.woff) format("woff");font-weight:500}@font-face{font-family:Roboto;src:local(Roboto Bold),url(fonts/Roboto-Bold.woff2) format("woff2"),url(fonts/Roboto-Bold.woff) format("woff");font-weight:700}a{text-decoration:none}html{line-height:1.5;font-family:Roboto,sans-serif;font-weight:400;color:rgba(0,0,0,.87)}@media only screen and (min-width:0){html{font-size:14px}}@media only screen and (min-width:992px){html{font-size:14.5px}}@media only screen and (min-width:1200px){html{font-size:15px}}h1,h2,h3,h4,h5,h6{font-weight:400;line-height:1.1}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{font-weight:inherit}h1{font-size:4.2rem;margin:2.1rem 0 1.68rem}h1,h2{line-height:110%}h2{font-size:3.56rem;margin:1.78rem 0 1.424rem}h3{font-size:2.92rem;margin:1.46rem 0 1.168rem}h3,h4{line-height:110%}h4{font-size:2.28rem;margin:1.14rem 0 .912rem}h5{font-size:1.64rem;margin:.82rem 0 .656rem}h5,h6{line-height:110%}h6{font-size:1rem;margin:.5rem 0 .4rem}em{font-style:italic}strong{font-weight:500}small{font-size:75%}.light,.page-footer .footer-copyright{font-weight:300}.thin{font-weight:200}.flow-text{font-weight:300}@media only screen and (min-width:360px){.flow-text{font-size:1.2rem}}@media only screen and (min-width:390px){.flow-text{font-size:1.224rem}}@media only screen and (min-width:420px){.flow-text{font-size:1.248rem}}@media only screen and (min-width:450px){.flow-text{font-size:1.272rem}}@media only screen and (min-width:480px){.flow-text{font-size:1.296rem}}@media only screen and (min-width:510px){.flow-text{font-size:1.32rem}}@media only screen and (min-width:540px){.flow-text{font-size:1.344rem}}@media only screen and (min-width:570px){.flow-text{font-size:1.368rem}}@media only screen and (min-width:600px){.flow-text{font-size:1.392rem}}@media only screen and (min-width:630px){.flow-text{font-size:1.416rem}}@media only screen and (min-width:660px){.flow-text{font-size:1.44rem}}@media only screen and (min-width:690px){.flow-text{font-size:1.464rem}}@media only screen and (min-width:720px){.flow-text{font-size:1.488rem}}@media only screen and (min-width:750px){.flow-text{font-size:1.512rem}}@media only screen and (min-width:780px){.flow-text{font-size:1.536rem}}@media only screen and (min-width:810px){.flow-text{font-size:1.56rem}}@media only screen and (min-width:840px){.flow-text{font-size:1.584rem}}@media only screen and (min-width:870px){.flow-text{font-size:1.608rem}}@media only screen and (min-width:900px){.flow-text{font-size:1.632rem}}@media only screen and (min-width:930px){.flow-text{font-size:1.656rem}}@media only screen and (min-width:960px){.flow-text{font-size:1.68rem}}@media only screen and (max-width:360px){.flow-text{font-size:1.2rem}}.scale-transition{transition:-webkit-transform .3s cubic-bezier(.53,.01,.36,1.63)!important;transition:transform .3s cubic-bezier(.53,.01,.36,1.63)!important;transition:transform .3s cubic-bezier(.53,.01,.36,1.63),-webkit-transform .3s cubic-bezier(.53,.01,.36,1.63)!important}.scale-transition.scale-out{-webkit-transform:scale(0);transform:scale(0);transition:-webkit-transform .2s!important;transition:transform .2s!important;transition:transform .2s,-webkit-transform .2s!important}.scale-transition.scale-in{-webkit-transform:scale(1);transform:scale(1)}.card-panel{padding:24px}.card,.card-panel{transition:box-shadow .25s;margin:.5rem 0 1rem;border-radius:2px;background-color:#fff}.card{position:relative}.card .card-title{font-size:24px;font-weight:300}.card .card-title.activator{cursor:pointer}.card.large,.card.medium,.card.small{position:relative}.card.large .card-image,.card.medium .card-image,.card.small .card-image{max-height:60%;overflow:hidden}.card.large .card-image+.card-content,.card.medium .card-image+.card-content,.card.small .card-image+.card-content{max-height:40%}.card.large .card-content,.card.medium .card-content,.card.small .card-content{max-height:100%;overflow:hidden}.card.large .card-action,.card.medium .card-action,.card.small .card-action{position:absolute;bottom:0;left:0;right:0}.card.small{height:300px}.card.medium{height:400px}.card.large{height:500px}.card.horizontal{display:-webkit-flex;display:-ms-flexbox;display:flex}.card.horizontal.large .card-image,.card.horizontal.medium .card-image,.card.horizontal.small .card-image{height:100%;max-height:none;overflow:visible}.card.horizontal.large .card-image img,.card.horizontal.medium .card-image img,.card.horizontal.small .card-image img{height:100%}.card.horizontal .card-image{max-width:50%}.card.horizontal .card-image img{border-radius:2px 0 0 2px;max-width:100%;width:auto}.card.horizontal .card-stacked{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex:1;-ms-flex:1;flex:1;position:relative}.card.horizontal .card-stacked .card-content{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.card.sticky-action .card-action{z-index:2}.card.sticky-action .card-reveal{z-index:1;padding-bottom:64px}.card .card-image{position:relative}.card .card-image img{display:block;border-radius:2px 2px 0 0;position:relative;left:0;right:0;top:0;bottom:0;width:100%}.card .card-image .card-title{color:#fff;position:absolute;bottom:0;left:0;max-width:100%;padding:24px}.card .card-content{padding:24px;border-radius:0 0 2px 2px}.card .card-content p{margin:0;color:inherit}.card .card-content .card-title{display:block;line-height:32px;margin-bottom:8px}.card .card-content .card-title i{line-height:32px}.card .card-action{position:relative;background-color:inherit;border-top:1px solid hsla(0,0%,63%,.2);padding:16px 24px}.card .card-action:last-child{border-radius:0 0 2px 2px}.card .card-action a:not(.btn):not(.btn-large):not(.btn-large):not(.btn-floating){color:#ffab40;margin-right:24px;transition:color .3s ease;text-transform:uppercase}.card .card-action a:not(.btn):not(.btn-large):not(.btn-large):not(.btn-floating):hover{color:#ffd8a6}.card .card-reveal{padding:24px;position:absolute;background-color:#fff;width:100%;overflow-y:auto;left:0;top:100%;height:100%;z-index:3;display:none}.card .card-reveal .card-title{cursor:pointer;display:block}#toast-container{display:block;position:fixed;z-index:10000}@media only screen and (max-width:600px){#toast-container{min-width:100%;bottom:0}}@media only screen and (min-width:601px) and (max-width:992px){#toast-container{left:5%;bottom:7%;max-width:90%}}@media only screen and (min-width:993px){#toast-container{top:10%;right:7%;max-width:86%}}.toast{border-radius:2px;top:35px;width:auto;clear:both;margin-top:10px;position:relative;max-width:100%;height:auto;min-height:48px;line-height:1.5em;word-break:break-all;background-color:#323232;padding:10px 25px;font-size:1.1rem;font-weight:300;color:#fff;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.toast .btn,.toast .btn-flat,.toast .btn-large{margin:0;margin-left:3rem}.toast.rounded{border-radius:24px}@media only screen and (max-width:600px){.toast{width:100%;border-radius:0}}@media only screen and (min-width:601px) and (max-width:992px){.toast{float:left}}@media only screen and (min-width:993px){.toast{float:right}}.tabs{position:relative;overflow-x:auto;overflow-y:hidden;height:48px;width:100%;background-color:#fff;margin:0 auto;white-space:nowrap}.tabs.tabs-transparent{background-color:transparent}.tabs.tabs-transparent .tab.disabled a,.tabs.tabs-transparent .tab.disabled a:hover,.tabs.tabs-transparent .tab a{color:hsla(0,0%,100%,.7)}.tabs.tabs-transparent .tab a.active,.tabs.tabs-transparent .tab a:hover{color:#fff}.tabs.tabs-transparent .indicator{background-color:#fff}.tabs.tabs-fixed-width{display:-webkit-flex;display:-ms-flexbox;display:flex}.tabs.tabs-fixed-width .tab{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.tabs .tab{display:inline-block;text-align:center;line-height:48px;height:48px;padding:0;margin:0;text-transform:uppercase}.tabs .tab a{color:rgba(238,110,115,.7);display:block;width:100%;height:100%;padding:0 24px;font-size:14px;text-overflow:ellipsis;overflow:hidden;transition:color .28s ease}.tabs .tab a.active,.tabs .tab a:hover{background-color:transparent;color:#ee6e73}.tabs .tab.disabled a,.tabs .tab.disabled a:hover{color:rgba(238,110,115,.7);cursor:default}.tabs .indicator{position:absolute;bottom:0;height:2px;background-color:#f6b2b5;will-change:left,right}@media only screen and (max-width:992px){.tabs{display:-webkit-flex;display:-ms-flexbox;display:flex}.tabs .tab{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.tabs .tab a{padding:0 12px}}.material-tooltip{padding:10px 8px;font-size:1rem;z-index:2000;background-color:transparent;border-radius:2px;color:#fff;min-height:36px;line-height:120%;text-align:center;max-width:calc(100% - 4px);overflow:hidden;left:0;top:0;pointer-events:none}.backdrop,.material-tooltip{opacity:0;position:absolute;visibility:hidden}.backdrop{height:7px;width:14px;border-radius:0 0 50% 50%;background-color:#323232;z-index:-1;-webkit-transform-origin:50% 0;transform-origin:50% 0}.btn,.btn-flat,.btn-large{border:none;border-radius:2px;display:inline-block;height:36px;line-height:36px;padding:0 2rem;text-transform:uppercase;vertical-align:middle;-webkit-tap-highlight-color:transparent}.btn-flat.disabled,.btn-flat:disabled,.btn-flat[disabled],.btn-floating.disabled,.btn-floating:disabled,.btn-floating[disabled],.btn-large.disabled,.btn-large:disabled,.btn-large[disabled],.btn.disabled,.btn:disabled,.btn[disabled],.disabled.btn-large,[disabled].btn-large{pointer-events:none;background-color:#dfdfdf!important;box-shadow:none;color:#9f9f9f!important;cursor:default}.btn-flat.disabled:hover,.btn-flat:disabled:hover,.btn-flat[disabled]:hover,.btn-floating.disabled:hover,.btn-floating:disabled:hover,.btn-floating[disabled]:hover,.btn-large.disabled:hover,.btn-large:disabled:hover,.btn-large[disabled]:hover,.btn.disabled:hover,.btn:disabled:hover,.btn[disabled]:hover,.disabled.btn-large:hover,[disabled].btn-large:hover{background-color:#dfdfdf!important;color:#9f9f9f!important}.btn,.btn-flat,.btn-floating,.btn-large{font-size:1rem;outline:0}.btn-flat i,.btn-floating i,.btn-large i,.btn i{font-size:1.3rem;line-height:inherit}.btn-floating:focus,.btn-large:focus,.btn:focus{background-color:#1d7d74}.btn,.btn-large{text-decoration:none;color:#fff;background-color:#26a69a;text-align:center;letter-spacing:.5px;transition:.2s ease-out;cursor:pointer}.btn-large:hover,.btn:hover{background-color:#2bbbad}.btn-floating{display:inline-block;color:#fff;position:relative;overflow:hidden;z-index:1;width:40px;height:40px;line-height:40px;padding:0;border-radius:50%;transition:.3s;cursor:pointer;vertical-align:middle}.btn-floating,.btn-floating:hover{background-color:#26a69a}.btn-floating:before{border-radius:0}.btn-floating.btn-large{width:56px;height:56px}.btn-floating.btn-large.halfway-fab{bottom:-28px}.btn-floating.btn-large i{line-height:56px}.btn-floating.halfway-fab{position:absolute;right:24px;bottom:-20px}.btn-floating.halfway-fab.left{right:auto;left:24px}.btn-floating i{width:inherit;display:inline-block;text-align:center;color:#fff;font-size:1.6rem;line-height:40px}button.btn-floating{border:none}.fixed-action-btn{position:fixed;right:23px;bottom:23px;padding-top:15px;margin-bottom:0;z-index:998}.fixed-action-btn.active ul{visibility:visible}.fixed-action-btn.horizontal{padding:0 0 0 15px}.fixed-action-btn.horizontal ul{text-align:right;right:64px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);height:100%;left:auto;width:500px}.fixed-action-btn.horizontal ul li{display:inline-block;margin:15px 15px 0 0}.fixed-action-btn.toolbar{padding:0;height:56px}.fixed-action-btn.toolbar.active>a i{opacity:0}.fixed-action-btn.toolbar ul{display:-webkit-flex;display:-ms-flexbox;display:flex;top:0;bottom:0}.fixed-action-btn.toolbar ul li{-webkit-flex:1;-ms-flex:1;flex:1;display:inline-block;margin:0;height:100%;transition:none}.fixed-action-btn.toolbar ul li a{display:block;overflow:hidden;position:relative;width:100%;height:100%;background-color:transparent;box-shadow:none;color:#fff;line-height:56px;z-index:1}.fixed-action-btn.toolbar ul li a i{line-height:inherit}.fixed-action-btn ul{left:0;right:0;text-align:center;position:absolute;bottom:64px;margin:0;visibility:hidden}.fixed-action-btn ul li{margin-bottom:15px}.fixed-action-btn ul a.btn-floating{opacity:0}.fixed-action-btn .fab-backdrop{position:absolute;top:0;left:0;z-index:-1;width:40px;height:40px;background-color:#26a69a;border-radius:50%;-webkit-transform:scale(0);transform:scale(0)}.btn-flat{box-shadow:none;color:#343434;cursor:pointer;transition:background-color .2s}.btn-flat,.btn-flat:active,.btn-flat:focus{background-color:transparent}.btn-flat:focus,.btn-flat:hover{background-color:rgba(0,0,0,.1);box-shadow:none}.btn-flat:active{background-color:rgba(0,0,0,.2)}.btn-flat.disabled{background-color:transparent!important;color:#b3b3b3!important;cursor:default}.btn-large{height:54px;line-height:54px}.btn-large i{font-size:1.6rem}.btn-block{display:block}.dropdown-content{background-color:#fff;margin:0;display:none;min-width:100px;max-height:650px;overflow-y:auto;opacity:0;position:absolute;z-index:999;will-change:width,height}.dropdown-content li{clear:both;color:rgba(0,0,0,.87);cursor:pointer;min-height:50px;line-height:1.5rem;width:100%;text-align:left;text-transform:none}.dropdown-content li.active,.dropdown-content li.selected,.dropdown-content li:hover{background-color:#eee}.dropdown-content li.active.selected{background-color:#e1e1e1}.dropdown-content li.divider{min-height:0;height:1px}.dropdown-content li>a,.dropdown-content li>span{font-size:16px;color:#26a69a;display:block;line-height:22px;padding:14px 16px}.dropdown-content li>span>label{top:1px;left:0;height:18px}.dropdown-content li>a>i{height:inherit;line-height:inherit;float:left;margin:0 24px 0 0;width:24px}.input-field.col .dropdown-content [type=checkbox]+label{top:1px;left:0;height:18px}.waves-effect{position:relative;cursor:pointer;display:inline-block;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;vertical-align:middle;z-index:1;transition:.3s ease-out}.waves-effect .waves-ripple{position:absolute;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;opacity:0;background:rgba(0,0,0,.2);transition:all .7s ease-out;transition-property:opacity,-webkit-transform;transition-property:transform,opacity;transition-property:transform,opacity,-webkit-transform;-webkit-transform:scale(0);transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:hsla(0,0%,100%,.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,.7)}.waves-effect input[type=button],.waves-effect input[type=reset],.waves-effect input[type=submit]{border:0;font-style:normal;font-size:inherit;text-transform:inherit;background:none}.waves-effect img{position:relative;z-index:-1}.waves-notransition{transition:none!important}.waves-circle{-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle,#fff 100%,#000 0)}.waves-input-wrapper{border-radius:.2em;vertical-align:bottom}.waves-input-wrapper .waves-button-input{position:relative;top:0;left:0;z-index:1}.waves-circle{text-align:center;width:2.5em;height:2.5em;line-height:2.5em;border-radius:50%;-webkit-mask-image:none}.waves-block{display:block}.waves-effect .waves-ripple{z-index:-1}.modal{display:none;position:fixed;left:0;right:0;background-color:#fafafa;padding:0;max-height:70%;width:55%;margin:auto;overflow-y:auto;border-radius:2px;will-change:top,opacity}@media only screen and (max-width:992px){.modal{width:80%}}.modal h1,.modal h2,.modal h3,.modal h4{margin-top:0}.modal .modal-content{padding:24px}.modal .modal-close{cursor:pointer}.modal .modal-footer{border-radius:0 0 2px 2px;background-color:#fafafa;padding:4px 6px;height:56px;width:100%}.modal .modal-footer .btn,.modal .modal-footer .btn-flat,.modal .modal-footer .btn-large{float:right;margin:6px 0}.modal-overlay{position:fixed;z-index:999;top:-100px;left:0;bottom:0;right:0;height:125%;width:100%;background:#000;display:none;will-change:opacity}.modal.modal-fixed-footer{padding:0;height:70%}.modal.modal-fixed-footer .modal-content{position:absolute;height:calc(100% - 56px);max-height:100%;width:100%;overflow-y:auto}.modal.modal-fixed-footer .modal-footer{border-top:1px solid rgba(0,0,0,.1);position:absolute;bottom:0}.modal.bottom-sheet{top:auto;bottom:-100%;margin:0;width:100%;max-height:45%;border-radius:0;will-change:bottom,opacity}.collapsible{border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;margin:.5rem 0 1rem}.collapsible-header{display:block;cursor:pointer;min-height:3rem;line-height:3rem;padding:0 1rem;background-color:#fff;border-bottom:1px solid #ddd}.collapsible-header i{width:2rem;font-size:1.6rem;line-height:3rem;display:block;float:left;text-align:center;margin-right:1rem}.collapsible-body{display:none;border-bottom:1px solid #ddd;box-sizing:border-box;padding:2rem}.side-nav .collapsible,.side-nav.fixed .collapsible{border:none;box-shadow:none}.side-nav .collapsible li,.side-nav.fixed .collapsible li{padding:0}.side-nav .collapsible-header,.side-nav.fixed .collapsible-header{background-color:transparent;border:none;line-height:inherit;height:inherit;padding:0 16px}.side-nav .collapsible-header:hover,.side-nav.fixed .collapsible-header:hover{background-color:rgba(0,0,0,.05)}.side-nav .collapsible-header i,.side-nav.fixed .collapsible-header i{line-height:inherit}.side-nav .collapsible-body,.side-nav.fixed .collapsible-body{border:0;background-color:#fff}.side-nav .collapsible-body li a,.side-nav.fixed .collapsible-body li a{padding:0 23.5px 0 31px}.collapsible.popout{border:none;box-shadow:none}.collapsible.popout>li{box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);margin:0 24px;transition:margin .35s cubic-bezier(.25,.46,.45,.94)}.collapsible.popout>li.active{box-shadow:0 5px 11px 0 rgba(0,0,0,.18),0 4px 15px 0 rgba(0,0,0,.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(0,0,0,.6);line-height:32px;padding:0 12px;border-radius:16px;background-color:#e4e4e4;margin-bottom:5px;margin-right:5px}.chip>img{float:left;margin:0 8px 0 -12px;height:32px;width:32px;border-radius:50%}.chip .close{cursor:pointer;float:right;font-size:16px;line-height:32px;padding-left:8px}.chips{border:none;border-bottom:1px solid #9e9e9e;box-shadow:none;margin:0 0 20px;min-height:45px;outline:none;transition:all .3s}.chips.focus{border-bottom:1px solid #26a69a;box-shadow:0 1px 0 0 #26a69a}.chips:hover{cursor:text}.chips .chip.selected{background-color:#26a69a;color:#fff}.chips .input{background:none;border:0;color:rgba(0,0,0,.6);display:inline-block;font-size:1rem;height:3rem;line-height:32px;outline:0;margin:0;padding:0!important;width:120px!important}.chips .input:focus{border:0!important;box-shadow:none!important}.chips .autocomplete-content{margin-top:0}.prefix~.chips{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.chips:empty~label{font-size:.8rem;-webkit-transform:translateY(-140%);transform:translateY(-140%)}.materialboxed{display:block;cursor:-webkit-zoom-in;cursor:zoom-in;position:relative;transition:opacity .4s;-webkit-backface-visibility:hidden}.materialboxed:hover:not(.active){opacity:.8}.materialboxed.active{cursor:-webkit-zoom-out;cursor:zoom-out}#materialbox-overlay{top:0;right:0;background-color:#292929;will-change:opacity}#materialbox-overlay,.materialbox-caption{position:fixed;bottom:0;left:0;z-index:1000}.materialbox-caption{display:none;color:#fff;line-height:50px;width:100%;text-align:center;padding:0 15%;height:50px;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #c9f3ef}button:focus{outline:none;background-color:#2ab7a9}label{font-size:.8rem;color:#9e9e9e}::-webkit-input-placeholder{color:#d1d1d1}:-moz-placeholder,::-moz-placeholder{color:#d1d1d1}:-ms-input-placeholder{color:#d1d1d1}input:not([type]),input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],textarea.materialize-textarea{background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;border-radius:0;outline:none;height:3rem;width:100%;font-size:1rem;margin:0 0 20px;padding:0;box-shadow:none;box-sizing:content-box;transition:all .3s}input:not([type]):disabled,input:not([type])[readonly=readonly],input[type=date]:disabled,input[type=date][readonly=readonly],input[type=datetime-local]:disabled,input[type=datetime-local][readonly=readonly],input[type=datetime]:disabled,input[type=datetime][readonly=readonly],input[type=email]:disabled,input[type=email][readonly=readonly],input[type=number]:disabled,input[type=number][readonly=readonly],input[type=password]:disabled,input[type=password][readonly=readonly],input[type=search]:disabled,input[type=search][readonly=readonly],input[type=tel]:disabled,input[type=tel][readonly=readonly],input[type=text]:disabled,input[type=text][readonly=readonly],input[type=time]:disabled,input[type=time][readonly=readonly],input[type=url]:disabled,input[type=url][readonly=readonly],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly=readonly]{color:rgba(0,0,0,.26);border-bottom:1px dotted rgba(0,0,0,.26)}input:not([type]):disabled+label,input:not([type])[readonly=readonly]+label,input[type=date]:disabled+label,input[type=date][readonly=readonly]+label,input[type=datetime-local]:disabled+label,input[type=datetime-local][readonly=readonly]+label,input[type=datetime]:disabled+label,input[type=datetime][readonly=readonly]+label,input[type=email]:disabled+label,input[type=email][readonly=readonly]+label,input[type=number]:disabled+label,input[type=number][readonly=readonly]+label,input[type=password]:disabled+label,input[type=password][readonly=readonly]+label,input[type=search]:disabled+label,input[type=search][readonly=readonly]+label,input[type=tel]:disabled+label,input[type=tel][readonly=readonly]+label,input[type=text]:disabled+label,input[type=text][readonly=readonly]+label,input[type=time]:disabled+label,input[type=time][readonly=readonly]+label,input[type=url]:disabled+label,input[type=url][readonly=readonly]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly=readonly]+label{color:rgba(0,0,0,.26)}input:not([type]):focus:not([readonly]),input[type=date]:focus:not([readonly]),input[type=datetime-local]:focus:not([readonly]),input[type=datetime]:focus:not([readonly]),input[type=email]:focus:not([readonly]),input[type=number]:focus:not([readonly]),input[type=password]:focus:not([readonly]),input[type=search]:focus:not([readonly]),input[type=tel]:focus:not([readonly]),input[type=text]:focus:not([readonly]),input[type=time]:focus:not([readonly]),input[type=url]:focus:not([readonly]),textarea.materialize-textarea:focus:not([readonly]){border-bottom:1px solid #26a69a;box-shadow:0 1px 0 0 #26a69a}input:not([type]):focus:not([readonly])+label,input[type=date]:focus:not([readonly])+label,input[type=datetime-local]:focus:not([readonly])+label,input[type=datetime]:focus:not([readonly])+label,input[type=email]:focus:not([readonly])+label,input[type=number]:focus:not([readonly])+label,input[type=password]:focus:not([readonly])+label,input[type=search]:focus:not([readonly])+label,input[type=tel]:focus:not([readonly])+label,input[type=text]:focus:not([readonly])+label,input[type=time]:focus:not([readonly])+label,input[type=url]:focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#26a69a}input:not([type]).valid,input:not([type]):focus.valid,input[type=date].valid,input[type=date]:focus.valid,input[type=datetime-local].valid,input[type=datetime-local]:focus.valid,input[type=datetime].valid,input[type=datetime]:focus.valid,input[type=email].valid,input[type=email]:focus.valid,input[type=number].valid,input[type=number]:focus.valid,input[type=password].valid,input[type=password]:focus.valid,input[type=search].valid,input[type=search]:focus.valid,input[type=tel].valid,input[type=tel]:focus.valid,input[type=text].valid,input[type=text]:focus.valid,input[type=time].valid,input[type=time]:focus.valid,input[type=url].valid,input[type=url]:focus.valid,textarea.materialize-textarea.valid,textarea.materialize-textarea:focus.valid{border-bottom:1px solid #4caf50;box-shadow:0 1px 0 0 #4caf50}input:not([type]).valid+label:after,input:not([type]):focus.valid+label:after,input[type=date].valid+label:after,input[type=date]:focus.valid+label:after,input[type=datetime-local].valid+label:after,input[type=datetime-local]:focus.valid+label:after,input[type=datetime].valid+label:after,input[type=datetime]:focus.valid+label:after,input[type=email].valid+label:after,input[type=email]:focus.valid+label:after,input[type=number].valid+label:after,input[type=number]:focus.valid+label:after,input[type=password].valid+label:after,input[type=password]:focus.valid+label:after,input[type=search].valid+label:after,input[type=search]:focus.valid+label:after,input[type=tel].valid+label:after,input[type=tel]:focus.valid+label:after,input[type=text].valid+label:after,input[type=text]:focus.valid+label:after,input[type=time].valid+label:after,input[type=time]:focus.valid+label:after,input[type=url].valid+label:after,input[type=url]:focus.valid+label:after,textarea.materialize-textarea.valid+label:after,textarea.materialize-textarea:focus.valid+label:after{content:attr(data-success);color:#4caf50;opacity:1}input:not([type]).invalid,input:not([type]):focus.invalid,input[type=date].invalid,input[type=date]:focus.invalid,input[type=datetime-local].invalid,input[type=datetime-local]:focus.invalid,input[type=datetime].invalid,input[type=datetime]:focus.invalid,input[type=email].invalid,input[type=email]:focus.invalid,input[type=number].invalid,input[type=number]:focus.invalid,input[type=password].invalid,input[type=password]:focus.invalid,input[type=search].invalid,input[type=search]:focus.invalid,input[type=tel].invalid,input[type=tel]:focus.invalid,input[type=text].invalid,input[type=text]:focus.invalid,input[type=time].invalid,input[type=time]:focus.invalid,input[type=url].invalid,input[type=url]:focus.invalid,textarea.materialize-textarea.invalid,textarea.materialize-textarea:focus.invalid{border-bottom:1px solid #f44336;box-shadow:0 1px 0 0 #f44336}input:not([type]).invalid+label:after,input:not([type]):focus.invalid+label:after,input[type=date].invalid+label:after,input[type=date]:focus.invalid+label:after,input[type=datetime-local].invalid+label:after,input[type=datetime-local]:focus.invalid+label:after,input[type=datetime].invalid+label:after,input[type=datetime]:focus.invalid+label:after,input[type=email].invalid+label:after,input[type=email]:focus.invalid+label:after,input[type=number].invalid+label:after,input[type=number]:focus.invalid+label:after,input[type=password].invalid+label:after,input[type=password]:focus.invalid+label:after,input[type=search].invalid+label:after,input[type=search]:focus.invalid+label:after,input[type=tel].invalid+label:after,input[type=tel]:focus.invalid+label:after,input[type=text].invalid+label:after,input[type=text]:focus.invalid+label:after,input[type=time].invalid+label:after,input[type=time]:focus.invalid+label:after,input[type=url].invalid+label:after,input[type=url]:focus.invalid+label:after,textarea.materialize-textarea.invalid+label:after,textarea.materialize-textarea:focus.invalid+label:after{content:attr(data-error);color:#f44336;opacity:1}input:not([type]).validate+label,input[type=date].validate+label,input[type=datetime-local].validate+label,input[type=datetime].validate+label,input[type=email].validate+label,input[type=number].validate+label,input[type=password].validate+label,input[type=search].validate+label,input[type=tel].validate+label,input[type=text].validate+label,input[type=time].validate+label,input[type=url].validate+label,textarea.materialize-textarea.validate+label{width:100%;pointer-events:none}input:not([type])+label:after,input[type=date]+label:after,input[type=datetime-local]+label:after,input[type=datetime]+label:after,input[type=email]+label:after,input[type=number]+label:after,input[type=password]+label:after,input[type=search]+label:after,input[type=tel]+label:after,input[type=text]+label:after,input[type=time]+label:after,input[type=url]+label:after,textarea.materialize-textarea+label:after{display:block;content:"";position:absolute;top:60px;opacity:0;transition:opacity .2s ease-out,color .2s ease-out}.input-field{position:relative;margin-top:1rem}.input-field.inline{display:inline-block;vertical-align:middle;margin-left:5px}.input-field.inline .select-dropdown,.input-field.inline input{margin-bottom:1rem}.input-field.col label{left:.75rem}.input-field.col .prefix~.validate~label,.input-field.col .prefix~label{width:calc(100% - 3rem - 1.5rem)}.input-field label{color:#9e9e9e;position:absolute;top:.8rem;left:0;font-size:1rem;cursor:text;transition:.2s ease-out;text-align:initial}.input-field label:not(.label-icon).active{font-size:.8rem;-webkit-transform:translateY(-140%);transform:translateY(-140%)}.input-field .prefix{position:absolute;width:3rem;font-size:2rem;transition:color .2s}.input-field .prefix.active{color:#26a69a}.input-field .prefix~.autocomplete-content,.input-field .prefix~.validate~label,.input-field .prefix~input,.input-field .prefix~label,.input-field .prefix~textarea{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.input-field .prefix~label{margin-left:3rem}@media only screen and (max-width:992px){.input-field .prefix~input{width:86%;width:calc(100% - 3rem)}}@media only screen and (max-width:600px){.input-field .prefix~input{width:80%;width:calc(100% - 3rem)}}.input-field input[type=search]{display:block;line-height:inherit;padding-left:4rem;width:calc(100% - 4rem)}.input-field input[type=search]:focus{background-color:#fff;border:0;box-shadow:none;color:#444}.input-field input[type=search]:focus+label i,.input-field input[type=search]:focus~.material-icons,.input-field input[type=search]:focus~.mdi-navigation-close{color:#444}.input-field input[type=search]+label{left:1rem}.input-field input[type=search]~.material-icons,.input-field input[type=search]~.mdi-navigation-close{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;transition:color .3s}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{overflow-y:hidden;padding:.8rem 0 1.6rem;resize:none;min-height:3rem}.hiddendiv{display:none;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;padding-top:1.2rem;position:absolute;top:0}.autocomplete-content{margin-top:-20px;display:block;opacity:1;position:static}.autocomplete-content li .highlight{color:#444}.autocomplete-content li img{height:40px;width:40px;margin:5px 15px}[type=radio]:checked,[type=radio]:not(:checked){position:absolute;left:-9999px;opacity:0}[type=radio]:checked+label,[type=radio]:not(:checked)+label{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;transition:.28s ease;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}[type=radio]+label:after,[type=radio]+label:before{content:"";position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;transition:.28s ease}[type=radio].with-gap:checked+label:after,[type=radio].with-gap:checked+label:before,[type=radio]:checked+label:after,[type=radio]:checked+label:before,[type=radio]:not(:checked)+label:after,[type=radio]:not(:checked)+label:before{border-radius:50%}[type=radio]:not(:checked)+label:after,[type=radio]:not(:checked)+label:before{border:2px solid #5a5a5a}[type=radio]:not(:checked)+label:after{-webkit-transform:scale(0);transform:scale(0)}[type=radio]:checked+label:before{border:2px solid transparent}[type=radio].with-gap:checked+label:after,[type=radio].with-gap:checked+label:before,[type=radio]:checked+label:after{border:2px solid #26a69a}[type=radio].with-gap:checked+label:after,[type=radio]:checked+label:after{background-color:#26a69a}[type=radio]:checked+label:after{-webkit-transform:scale(1.02);transform:scale(1.02)}[type=radio].with-gap:checked+label:after{-webkit-transform:scale(.5);transform:scale(.5)}[type=radio].tabbed:focus+label:before{box-shadow:0 0 0 10px rgba(0,0,0,.1)}[type=radio].with-gap:disabled:checked+label:before{border:2px solid rgba(0,0,0,.26)}[type=radio].with-gap:disabled:checked+label:after{border:none;background-color:rgba(0,0,0,.26)}[type=radio]:disabled:checked+label:before,[type=radio]:disabled:not(:checked)+label:before{background-color:transparent;border-color:rgba(0,0,0,.26)}[type=radio]:disabled+label{color:rgba(0,0,0,.26)}[type=radio]:disabled:not(:checked)+label:before{border-color:rgba(0,0,0,.26)}[type=radio]:disabled:checked+label:after{background-color:rgba(0,0,0,.26);border-color:#bdbdbd}form p{margin-bottom:10px;text-align:left}form p:last-child{margin-bottom:0}[type=checkbox]:checked,[type=checkbox]:not(:checked){position:absolute;left:-9999px;opacity:0}[type=checkbox]+label{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;-webkit-user-select:none;-moz-user-select:none;-khtml-user-select:none;-ms-user-select:none}[type=checkbox]+label:before,[type=checkbox]:not(.filled-in)+label:after{content:"";position:absolute;top:0;left:0;width:18px;height:18px;z-index:0;border:2px solid #5a5a5a;border-radius:1px;margin-top:2px;transition:.2s}[type=checkbox]:not(.filled-in)+label:after{border:0;-webkit-transform:scale(0);transform:scale(0)}[type=checkbox]:not(:checked):disabled+label:before{border:none;background-color:rgba(0,0,0,.26)}[type=checkbox].tabbed:focus+label:after{-webkit-transform:scale(1);transform:scale(1);border:0;border-radius:50%;box-shadow:0 0 0 10px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}[type=checkbox]:checked+label:before{top:-4px;left:-5px;width:12px;height:22px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #26a69a;border-bottom:2px solid #26a69a;-webkit-transform:rotate(40deg);transform:rotate(40deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type=checkbox]:checked:disabled+label:before{border-right:2px solid rgba(0,0,0,.26);border-bottom:2px solid rgba(0,0,0,.26)}[type=checkbox]:indeterminate+label:before{top:-11px;left:-12px;width:10px;height:22px;border-top:none;border-left:none;border-right:2px solid #26a69a;border-bottom:none;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type=checkbox]:indeterminate:disabled+label:before{border-right:2px solid rgba(0,0,0,.26);background-color:transparent}[type=checkbox].filled-in+label:after{border-radius:2px}[type=checkbox].filled-in+label:after,[type=checkbox].filled-in+label:before{content:"";left:0;position:absolute;transition:border .25s,background-color .25s,width .2s .1s,height .2s .1s,top .2s .1s,left .2s .1s;z-index:1}[type=checkbox].filled-in:not(:checked)+label:before{width:0;height:0;border:3px solid transparent;left:6px;top:10px;-webkit-transform:rotate(37deg);transform:rotate(37deg);-webkit-transform-origin:20% 40%;transform-origin:100% 100%}[type=checkbox].filled-in:not(:checked)+label:after{height:20px;width:20px;background-color:transparent;border:2px solid #5a5a5a;top:0;z-index:0}[type=checkbox].filled-in:checked+label:before{top:0;left:1px;width:8px;height:13px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #fff;border-bottom:2px solid #fff;-webkit-transform:rotate(37deg);transform:rotate(37deg);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type=checkbox].filled-in:checked+label:after{top:0;width:20px;height:20px;border:2px solid #26a69a;background-color:#26a69a;z-index:0}[type=checkbox].filled-in.tabbed:focus+label:after{border-radius:2px;border-color:#5a5a5a;background-color:rgba(0,0,0,.1)}[type=checkbox].filled-in.tabbed:checked:focus+label:after{border-radius:2px;background-color:#26a69a;border-color:#26a69a}[type=checkbox].filled-in:disabled:not(:checked)+label:before{background-color:transparent;border:2px solid transparent}[type=checkbox].filled-in:disabled:not(:checked)+label:after{border-color:transparent;background-color:#bdbdbd}[type=checkbox].filled-in:disabled:checked+label:before{background-color:transparent}[type=checkbox].filled-in:disabled:checked+label:after{background-color:#bdbdbd;border-color:#bdbdbd}.switch,.switch *{-webkit-user-select:none;-moz-user-select:none;-khtml-user-select:none;-ms-user-select:none}.switch label{cursor:pointer}.switch label input[type=checkbox]{opacity:0;width:0;height:0}.switch label input[type=checkbox]:checked+.lever{background-color:#84c7c1}.switch label input[type=checkbox]:checked+.lever:after{background-color:#26a69a;left:24px}.switch label .lever{content:"";display:inline-block;position:relative;width:40px;height:15px;background-color:#818181;border-radius:15px;margin-right:10px;transition:background .3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:after{content:"";position:absolute;display:inline-block;width:21px;height:21px;background-color:#f1f1f1;border-radius:21px;box-shadow:0 1px 3px 1px rgba(0,0,0,.4);left:-5px;top:-3px;transition:left .3s ease,background .3s ease,box-shadow .1s ease}input[type=checkbox]:checked:not(:disabled).tabbed:focus~.lever:after,input[type=checkbox]:checked:not(:disabled)~.lever:active:after{box-shadow:0 1px 3px 1px rgba(0,0,0,.4),0 0 0 15px rgba(38,166,154,.1)}input[type=checkbox]:not(:disabled).tabbed:focus~.lever:after,input[type=checkbox]:not(:disabled)~.lever:active:after{box-shadow:0 1px 3px 1px rgba(0,0,0,.4),0 0 0 15px rgba(0,0,0,.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default}.switch label input[type=checkbox][disabled]+.lever:after,.switch label input[type=checkbox][disabled]:checked+.lever:after{background-color:#bdbdbd}select{display:none}select.browser-default{display:block}select{background-color:hsla(0,0%,100%,.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper input.select-dropdown{position:relative;cursor:pointer;background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;outline:none;height:3rem;line-height:3rem;width:100%;font-size:1rem;margin:0 0 20px;padding:0;display:block}.select-wrapper span.caret{color:initial;position:absolute;right:0;top:0;bottom:0;height:10px;margin:auto 0;font-size:10px;line-height:10px}.select-wrapper span.caret.disabled{color:rgba(0,0,0,.26)}.select-wrapper+label{position:absolute;top:-14px;font-size:.8rem}select:disabled{color:rgba(0,0,0,.3)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,.3);cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;border-bottom:1px solid rgba(0,0,0,.3)}.select-wrapper i{color:rgba(0,0,0,.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,.3);background-color:transparent}.prefix~.select-wrapper{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.prefix~label{margin-left:3rem}.select-dropdown li img{height:40px;width:40px;margin:5px 15px;float:right}.select-dropdown li.optgroup{border-top:1px solid #eee}.select-dropdown li.optgroup.selected>span{color:rgba(0,0,0,.7)}.select-dropdown li.optgroup>span{color:rgba(0,0,0,.4)}.select-dropdown li.optgroup~li.optgroup-option{padding-left:1rem}.file-field{position:relative}.file-field .file-path-wrapper{overflow:hidden;padding-left:10px}.file-field input.file-path{width:100%}.file-field .btn,.file-field .btn-large{float:left;height:3rem;line-height:3rem}.file-field span{cursor:pointer}.file-field input[type=file]{position:absolute;top:0;right:0;left:0;bottom:0;width:100%;margin:0;padding:0;font-size:20px;cursor:pointer;opacity:0;filter:alpha(opacity=0)}.range-field{position:relative}input[type=range],input[type=range]+.thumb{cursor:pointer}input[type=range]{position:relative;background-color:transparent;border:none;outline:none;width:100%;margin:15px 0;padding:0}input[type=range]:focus{outline:none}input[type=range]+.thumb{position:absolute;top:10px;left:0;border:none;height:0;width:0;border-radius:50%;background-color:#26a69a;margin-left:7px;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#26a69a;font-size:0;-webkit-transform:rotate(45deg);transform:rotate(45deg)}input[type=range]+.thumb.active{border-radius:50% 50% 50% 0}input[type=range]+.thumb.active .value{color:#fff;margin-left:-1px;margin-top:8px;font-size:10px}input[type=range]{-webkit-appearance:none}input[type=range]::-webkit-slider-runnable-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;border:none;height:14px;width:14px;border-radius:50%;background-color:#26a69a;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;margin:-5px 0 0;transition:.3s}input[type=range]:focus::-webkit-slider-runnable-track{background:#ccc}input[type=range]{border:1px solid #fff}input[type=range]::-moz-range-track{height:3px;background:#ddd;border:none}input[type=range]::-moz-range-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;margin-top:-5px}input[type=range]:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}input[type=range]:focus::-moz-range-track{background:#ccc}input[type=range]::-ms-track{height:3px;background:transparent;border-color:transparent;border-width:6px 0;color:transparent}input[type=range]::-ms-fill-lower{background:#777}input[type=range]::-ms-fill-upper{background:#ddd}input[type=range]::-ms-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a}input[type=range]:focus::-ms-fill-lower{background:#888}input[type=range]:focus::-ms-fill-upper{background:#ccc}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{font-weight:300;color:#757575;padding-left:20px;height:1.5rem;line-height:1.5rem;letter-spacing:.4;display:inline-block}.table-of-contents a:hover{color:#a8a8a8;padding-left:19px;border-left:1px solid #ee6e73}.table-of-contents a.active{font-weight:500;padding-left:18px;border-left:2px solid #ee6e73}.side-nav{position:fixed;width:300px;left:0;top:0;margin:0;-webkit-transform:translateX(-100%);transform:translateX(-100%);height:calc(100% + 60px);height:100%;padding-bottom:60px;background-color:#fff;z-index:999;overflow-y:auto;will-change:transform;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform:translateX(-105%);transform:translateX(-105%)}.side-nav.right-aligned{right:0;-webkit-transform:translateX(105%);transform:translateX(105%);left:auto;-webkit-transform:translateX(100%);transform:translateX(100%)}.side-nav .collapsible{margin:0}.side-nav li{float:none;line-height:48px}.side-nav li.active{background-color:rgba(0,0,0,.05)}.side-nav li>a{color:rgba(0,0,0,.87);display:block;font-size:14px;font-weight:500;height:48px;line-height:48px;padding:0 32px}.side-nav li>a:hover{background-color:rgba(0,0,0,.05)}.side-nav li>a.btn,.side-nav li>a.btn-flat,.side-nav li>a.btn-floating,.side-nav li>a.btn-large{margin:10px 15px}.side-nav li>a.btn,.side-nav li>a.btn-floating,.side-nav li>a.btn-large{color:#fff}.side-nav li>a.btn-flat{color:#343434}.side-nav li>a.btn-large:hover,.side-nav li>a.btn:hover{background-color:#2bbbad}.side-nav li>a.btn-floating:hover{background-color:#26a69a}.side-nav li>a>[class^=mdi-],.side-nav li>a>i,.side-nav li>a>i.material-icons,.side-nav li>a li>a>[class*=mdi-]{float:left;height:48px;line-height:48px;margin:0 32px 0 0;width:24px;color:rgba(0,0,0,.54)}.side-nav .divider{margin:8px 0 0}.side-nav .subheader{cursor:auto;pointer-events:none;color:rgba(0,0,0,.54);font-size:14px;font-weight:500;line-height:48px}.side-nav .subheader:hover{background-color:transparent}.side-nav .userView{position:relative;padding:32px 32px 0;margin-bottom:8px}.side-nav .userView>a{height:auto;padding:0}.side-nav .userView>a:hover{background-color:transparent}.side-nav .userView .background{overflow:hidden;position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1}.side-nav .userView .circle,.side-nav .userView .email,.side-nav .userView .name{display:block}.side-nav .userView .circle{height:64px;width:64px}.side-nav .userView .email,.side-nav .userView .name{font-size:14px;line-height:24px}.side-nav .userView .name{margin-top:16px;font-weight:500}.side-nav .userView .email{padding-bottom:16px;font-weight:400}.drag-target{height:100%;width:10px;position:fixed;top:0;z-index:998}.side-nav.fixed{left:0;-webkit-transform:translateX(0);transform:translateX(0);position:fixed}.side-nav.fixed.right-aligned{right:0;left:auto}@media only screen and (max-width:992px){.side-nav.fixed{-webkit-transform:translateX(-105%);transform:translateX(-105%)}.side-nav.fixed.right-aligned{-webkit-transform:translateX(105%);transform:translateX(105%)}.side-nav a{padding:0 16px}.side-nav .userView{padding:16px 16px 0}}.side-nav .collapsible-body>ul:not(.collapsible)>li.active,.side-nav.fixed .collapsible-body>ul:not(.collapsible)>li.active{background-color:#ee6e73}.side-nav .collapsible-body>ul:not(.collapsible)>li.active a,.side-nav.fixed .collapsible-body>ul:not(.collapsible)>li.active a{color:#fff}.side-nav .collapsible-body{padding:0}#sidenav-overlay{position:fixed;top:0;left:0;right:0;height:120vh;background-color:rgba(0,0,0,.5);z-index:997;will-change:opacity}.preloader-wrapper{display:inline-block;position:relative;width:50px;height:50px}.preloader-wrapper.small{width:36px;height:36px}.preloader-wrapper.big{width:64px;height:64px}.preloader-wrapper.active{-webkit-animation:container-rotate 1568ms linear infinite;animation:container-rotate 1568ms linear infinite}@-webkit-keyframes container-rotate{to{-webkit-transform:rotate(1turn)}}@keyframes container-rotate{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.spinner-layer{position:absolute;width:100%;height:100%;opacity:0;border-color:#26a69a}.spinner-blue,.spinner-blue-only{border-color:#4285f4}.spinner-red,.spinner-red-only{border-color:#db4437}.spinner-yellow,.spinner-yellow-only{border-color:#f4b400}.spinner-green,.spinner-green-only{border-color:#0f9d58}.active .spinner-layer.spinner-blue{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,blue-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,blue-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,red-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,red-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,green-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,green-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-green-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both}@-webkit-keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg)}to{-webkit-transform:rotate(3turn)}}@keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(3turn);transform:rotate(3turn)}}@-webkit-keyframes blue-fade-in-out{0%{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}to{opacity:1}}@keyframes blue-fade-in-out{0%{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}to{opacity:1}}@-webkit-keyframes red-fade-in-out{0%{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{0%{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{0%{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{0%{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{0%{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}to{opacity:0}}@keyframes green-fade-in-out{0%{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}to{opacity:0}}.gap-patch{position:absolute;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.gap-patch .circle{width:1000%;left:-450%}.circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.circle-clipper .circle{width:200%;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent!important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0}.circle-clipper.left .circle{left:0;border-right-color:transparent!important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.circle-clipper.right .circle{left:-100%;border-left-color:transparent!important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.active .circle-clipper.left .circle{-webkit-animation:left-spin 1333ms cubic-bezier(.4,0,.2,1) infinite both;animation:left-spin 1333ms cubic-bezier(.4,0,.2,1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(.4,0,.2,1) infinite both;animation:right-spin 1333ms cubic-bezier(.4,0,.2,1) infinite both}@-webkit-keyframes left-spin{0%{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{0%{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes right-spin{0%{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{0%{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out .4s cubic-bezier(.4,0,.2,1);animation:container-rotate 1568ms linear infinite,fade-out .4s cubic-bezier(.4,0,.2,1)}@-webkit-keyframes fade-out{0%{opacity:1}to{opacity:0}}@keyframes fade-out{0%{opacity:1}to{opacity:0}}.slider{position:relative;height:400px;width:100%}.slider.fullscreen{height:100%;width:100%;position:absolute;top:0;left:0;right:0;bottom:0}.slider.fullscreen ul.slides{height:100%}.slider.fullscreen ul.indicators{z-index:2;bottom:30px}.slider .slides{background-color:#9e9e9e;margin:0;height:400px}.slider .slides li{opacity:0;position:absolute;top:0;left:0;z-index:1;width:100%;height:inherit;overflow:hidden}.slider .slides li img{height:100%;width:100%;background-size:cover;background-position:50%}.slider .slides li .caption{color:#fff;position:absolute;top:15%;left:15%;width:70%;opacity:0}.slider .slides li .caption p{color:#e0e0e0}.slider .slides li.active{z-index:2}.slider .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.slider .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:16px;width:16px;margin:0 12px;background-color:#e0e0e0;transition:background-color .3s;border-radius:50%}.slider .indicators .indicator-item.active{background-color:#4caf50}.carousel{overflow:hidden;position:relative;width:100%;height:400px;-webkit-perspective:500px;perspective:500px;-webkit-transform-style:preserve-3d;transform-style:preserve-3d;-webkit-transform-origin:0 50%;transform-origin:0 50%}.carousel.carousel-slider{top:0;left:0;height:0}.carousel.carousel-slider .carousel-fixed-item{position:absolute;left:0;right:0;bottom:20px;z-index:1}.carousel.carousel-slider .carousel-fixed-item.with-indicators{bottom:68px}.carousel.carousel-slider .carousel-item{width:100%;height:100%;min-height:400px;position:absolute;top:0;left:0}.carousel.carousel-slider .carousel-item h2{font-size:24px;font-weight:500;line-height:32px}.carousel.carousel-slider .carousel-item p{font-size:15px}.carousel .carousel-item{display:none;width:200px;height:200px;position:absolute;top:0;left:0}.carousel .carousel-item>img{width:100%}.carousel .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.carousel .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:8px;width:8px;margin:24px 4px;background-color:hsla(0,0%,100%,.5);transition:background-color .3s;border-radius:50%}.carousel .indicators .indicator-item.active{background-color:#fff}.carousel .carousel-item:not(.active) .materialboxed,.carousel.scrolling .carousel-item .materialboxed{pointer-events:none}.tap-target-wrapper{width:800px;height:800px;position:fixed;z-index:1000;visibility:hidden;transition:visibility 0s .3s}.tap-target-wrapper.open{visibility:visible;transition:visibility 0s}.tap-target-wrapper.open .tap-target{opacity:.95;transition:opacity .3s ease-in-out,-webkit-transform .3s ease-in-out;transition:transform .3s ease-in-out,opacity .3s ease-in-out;transition:transform .3s ease-in-out,opacity .3s ease-in-out,-webkit-transform .3s ease-in-out}.tap-target-wrapper.open .tap-target,.tap-target-wrapper.open .tap-target-wave:before{-webkit-transform:scale(1);transform:scale(1)}.tap-target-wrapper.open .tap-target-wave:after{visibility:visible;-webkit-animation:pulse-animation 1s cubic-bezier(.24,0,.38,1) infinite;animation:pulse-animation 1s cubic-bezier(.24,0,.38,1) infinite;transition:opacity .3s,visibility 0s 1s,-webkit-transform .3s;transition:opacity .3s,transform .3s,visibility 0s 1s;transition:opacity .3s,transform .3s,visibility 0s 1s,-webkit-transform .3s}.tap-target{position:absolute;font-size:1rem;border-radius:50%;background-color:#ee6e73;box-shadow:0 20px 20px 0 rgba(0,0,0,.14),0 10px 50px 0 rgba(0,0,0,.12),0 30px 10px -20px rgba(0,0,0,.2);width:100%;height:100%;opacity:0;-webkit-transform:scale(0);transform:scale(0);transition:opacity .3s ease-in-out,-webkit-transform .3s ease-in-out;transition:transform .3s ease-in-out,opacity .3s ease-in-out;transition:transform .3s ease-in-out,opacity .3s ease-in-out,-webkit-transform .3s ease-in-out}.tap-target-content{position:relative;display:table-cell}.tap-target-wave{position:absolute;border-radius:50%;z-index:10001}.tap-target-wave:after,.tap-target-wave:before{content:"";display:block;position:absolute;width:100%;height:100%;border-radius:50%;background-color:#fff}.tap-target-wave:before{-webkit-transform:scale(0);transform:scale(0);transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s}.tap-target-wave:after{visibility:hidden;transition:opacity .3s,visibility 0s,-webkit-transform .3s;transition:opacity .3s,transform .3s,visibility 0s;transition:opacity .3s,transform .3s,visibility 0s,-webkit-transform .3s;z-index:-1}.tap-target-origin{top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);z-index:10002;position:absolute!important}.tap-target-origin:not(.btn):not(.btn-large),.tap-target-origin:not(.btn):not(.btn-large):hover{background:none}@media only screen and (max-width:600px){.tap-target,.tap-target-wrapper{width:600px;height:600px}}.pulse{overflow:initial;position:relative}.pulse:before{content:"";display:block;position:absolute;width:100%;height:100%;top:0;left:0;background-color:inherit;border-radius:inherit;transition:opacity .3s,-webkit-transform .3s;transition:opacity .3s,transform .3s;transition:opacity .3s,transform .3s,-webkit-transform .3s;-webkit-animation:pulse-animation 1s cubic-bezier(.24,0,.38,1) infinite;animation:pulse-animation 1s cubic-bezier(.24,0,.38,1) infinite;z-index:-1}@-webkit-keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}to{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}}@keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}to{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}}.picker{font-size:16px;text-align:left;line-height:1.2;color:#000;position:absolute;z-index:10000;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.picker__input{cursor:default}.picker__input.picker__input--active{border-color:#0089ec}.picker__holder{width:100%;overflow-y:auto;-webkit-overflow-scrolling:touch}.picker__frame,.picker__holder{bottom:0;left:0;right:0;top:100%}.picker__holder{position:fixed;transition:background .15s ease-out,top 0s .15s;-webkit-backface-visibility:hidden}.picker__frame{position:absolute;min-width:256px;width:300px;max-height:350px;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";filter:alpha(opacity=0);-moz-opacity:0;opacity:0;transition:all .15s ease-out}@media (min-height:28.875em){.picker__frame{overflow:visible;top:auto;bottom:-100%;max-height:80%}}@media (min-height:40.125em){.picker__frame{margin-bottom:7.5%}}.picker__wrap{display:table;width:100%;height:100%}@media (min-height:28.875em){.picker__wrap{display:block}}.picker__box{background:#fff;display:table-cell;vertical-align:middle}@media (min-height:28.875em){.picker__box{display:block;border:1px solid #777;border-top-color:#898989;border-bottom-width:0;border-radius:5px 5px 0 0;box-shadow:0 12px 36px 16px rgba(0,0,0,.24)}}.picker--opened .picker__holder{top:0;background:transparent;-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#1E000000,endColorstr=#1E000000)";zoom:1;background:rgba(0,0,0,.32);transition:background .15s ease-out}.picker--opened .picker__frame{top:0;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";filter:alpha(opacity=100);-moz-opacity:1;opacity:1}@media (min-height:35.875em){.picker--opened .picker__frame{top:10%;bottom:auto}}.picker__input.picker__input--active{border-color:#e3f2fd}.picker__frame{margin:0 auto;max-width:325px}@media (min-height:38.875em){.picker--opened .picker__frame{top:10%;bottom:auto}}.picker__box{padding:0 1em}.picker__header{text-align:center;position:relative;margin-top:.75em}.picker__month,.picker__year{display:inline-block;margin-left:.25em;margin-right:.25em}.picker__select--month,.picker__select--year{height:2em;padding:0;margin-left:.25em;margin-right:.25em}.picker__select--month.browser-default{display:inline;background-color:#fff;width:40%}.picker__select--year.browser-default{display:inline;background-color:#fff;width:26%}.picker__select--month:focus,.picker__select--year:focus{border-color:rgba(0,0,0,.05)}.picker__nav--next,.picker__nav--prev{position:absolute;padding:.5em 1.25em;width:1em;height:1em;box-sizing:content-box;top:-.25em}.picker__nav--prev{left:-1em;padding-right:1.25em}.picker__nav--next{right:-1em;padding-left:1.25em}.picker__nav--disabled,.picker__nav--disabled:before,.picker__nav--disabled:before:hover,.picker__nav--disabled:hover{cursor:default;background:none;border-right-color:#f5f5f5;border-left-color:#f5f5f5}.picker__table{border-collapse:collapse;border-spacing:0;table-layout:fixed;font-size:1rem;width:100%;margin-top:.75em}.picker__table,.picker__table td,.picker__table th{text-align:center}.picker__table td{margin:0;padding:0}.picker__weekday{width:14.285714286%;font-size:.75em;padding-bottom:.25em;color:#999;font-weight:500}@media (min-height:33.875em){.picker__weekday{padding-bottom:.5em}}.picker__day--today{position:relative;color:#595959;letter-spacing:-.3;padding:.75rem 0;font-weight:400;border:1px solid transparent}.picker__day--disabled:before{border-top-color:#aaa}.picker__day--infocus:hover{cursor:pointer;color:#000;font-weight:500}.picker__day--outfocus{display:none;padding:.75rem 0;color:#fff}.picker__day--outfocus:hover{cursor:pointer;color:#ddd;font-weight:500}.picker--focused .picker__day--highlighted,.picker__day--highlighted:hover{cursor:pointer}.picker--focused .picker__day--selected,.picker__day--selected,.picker__day--selected:hover{-webkit-transform:scale(.75);transform:scale(.75);background:#0089ec}.picker--focused .picker__day--disabled,.picker__day--disabled,.picker__day--disabled:hover{background:#f5f5f5;border-color:#f5f5f5;color:#ddd;cursor:default}.picker__day--highlighted.picker__day--disabled,.picker__day--highlighted.picker__day--disabled:hover{background:#bbb}.picker__footer{text-align:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.picker__button--clear,.picker__button--close,.picker__button--today{border:1px solid #fff;background:#fff;font-size:.8em;padding:.66em 0;font-weight:700;width:33%;display:inline-block;vertical-align:bottom}.picker__button--clear:hover,.picker__button--close:hover,.picker__button--today:hover{cursor:pointer;color:#000;background:#b1dcfb;border-bottom-color:#b1dcfb}.picker__button--clear:focus,.picker__button--close:focus,.picker__button--today:focus{background:#b1dcfb;border-color:rgba(0,0,0,.05);outline:none}.picker__button--clear:before,.picker__button--close:before,.picker__button--today:before{position:relative;display:inline-block;height:0}.picker__button--clear:before,.picker__button--today:before{content:" ";margin-right:.45em}.picker__button--today:before{top:-.05em;width:0;border-top:.66em solid #0059bc;border-left:.66em solid transparent}.picker__button--clear:before{top:-.25em;width:.66em;border-top:3px solid #e20}.picker__button--close:before{content:"\D7";top:-.1em;vertical-align:top;font-size:1.1em;margin-right:.35em;color:#777}.picker__button--today[disabled],.picker__button--today[disabled]:hover{background:#f5f5f5;border-color:#f5f5f5;color:#ddd;cursor:default}.picker__button--today[disabled]:before{border-top-color:#aaa}.picker__box{border-radius:2px;overflow:hidden}.picker__date-display{text-align:center;background-color:#26a69a;color:#fff;padding-bottom:15px;font-weight:300}.picker__nav--next:hover,.picker__nav--prev:hover{cursor:pointer;color:#000;background:#a1ded8}.picker__weekday-display{background-color:#1f897f;padding:10px;font-weight:200;letter-spacing:.5;font-size:1rem;margin-bottom:15px}.picker__month-display{text-transform:uppercase;font-size:2rem}.picker__day-display{font-size:4.5rem;font-weight:400}.picker__year-display{font-size:1.8rem;color:hsla(0,0%,100%,.4)}.picker__box{padding:0}.picker__calendar-container{padding:0 1rem}.picker__calendar-container thead{border:none}.picker__table{margin-top:0;margin-bottom:.5em}.picker__day--infocus{color:#595959;letter-spacing:-.3;padding:.75rem 0;font-weight:400;border:1px solid transparent}.picker__day.picker__day--today{color:#26a69a}.picker__day.picker__day--today.picker__day--selected{color:#fff}.picker__weekday{font-size:.9rem}.picker--focused .picker__day--selected,.picker__day--selected,.picker__day--selected:hover{border-radius:50%;-webkit-transform:scale(.9);transform:scale(.9);background-color:#26a69a;color:#fff}.picker--focused .picker__day--selected.picker__day--outfocus,.picker__day--selected.picker__day--outfocus,.picker__day--selected:hover.picker__day--outfocus{background-color:#a1ded8}.picker__footer{text-align:right;padding:5px 10px}.picker__close,.picker__today{font-size:1.1rem;padding:0 1rem;color:#26a69a}.picker__nav--next:before,.picker__nav--prev:before{content:" ";border-top:.5em solid transparent;border-bottom:.5em solid transparent;border-right:.75em solid #676767;width:0;height:0;display:block;margin:0 auto}.picker__nav--next:before{border-right:0;border-left:.75em solid #676767}button.picker__clear:focus,button.picker__close:focus,button.picker__today:focus{background-color:#a1ded8}.picker__list{list-style:none;padding:.75em 0 4.2em;margin:0}.picker__list-item{border-bottom:1px solid #ddd;border-top:1px solid #ddd;margin-bottom:-1px;position:relative;background:#fff;padding:.75em 1.25em}@media (min-height:46.75em){.picker__list-item{padding:.5em 1em}}.picker__list-item:hover{cursor:pointer;color:#000;background:#b1dcfb}.picker__list-item--highlighted,.picker__list-item:hover{border-color:#0089ec;z-index:10}.picker--focused .picker__list-item--highlighted,.picker__list-item--highlighted:hover{cursor:pointer;color:#000;background:#b1dcfb}.picker--focused .picker__list-item--selected,.picker__list-item--selected,.picker__list-item--selected:hover{background:#0089ec;color:#fff;z-index:10}.picker--focused .picker__list-item--disabled,.picker__list-item--disabled,.picker__list-item--disabled:hover{background:#f5f5f5;border-color:#f5f5f5;color:#ddd;cursor:default;border-color:#ddd;z-index:auto}.picker--time .picker__button--clear{display:block;width:80%;margin:1em auto 0;padding:1em 1.25em;background:none;border:0;font-weight:500;font-size:.67em;text-align:center;text-transform:uppercase;color:#666}.picker--time .picker__button--clear:focus,.picker--time .picker__button--clear:hover{color:#000;background:#b1dcfb;background:#e20;border-color:#e20;cursor:pointer;color:#fff;outline:none}.picker--time .picker__button--clear:before{top:-.25em;color:#666;font-size:1.25em;font-weight:700}.picker--time .picker__button--clear:focus:before,.picker--time .picker__button--clear:hover:before{color:#fff}.picker--time .picker__frame{min-width:256px;max-width:320px}.picker--time .picker__box{font-size:1em;background:#f2f2f2;padding:0}@media (min-height:40.125em){.picker--time .picker__box{margin-bottom:5em}}.annotator-filter *,.annotator-notice,.annotator-widget *{font-family:Helvetica Neue,Arial,Helvetica,sans-serif;font-weight:400;text-align:left;margin:0;padding:0;background:none;-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none;-moz-box-shadow:none;-webkit-box-shadow:none;-o-box-shadow:none;box-shadow:none;color:#909090}.annotator-adder{background-image:url(img/annotator-icon-sprite.png);background-repeat:no-repeat}.annotator-editor a:after,.annotator-filter .annotator-filter-navigation button:after,.annotator-filter .annotator-filter-property .annotator-filter-clear,.annotator-resize,.annotator-viewer .annotator-controls a,.annotator-viewer .annotator-controls button,.annotator-widget:after{background-image:url(img/annotator-glyph-sprite.png);background-repeat:no-repeat}.annotator-hl{background:#ffff0a;background:rgba(255,255,10,.3);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#4DFFFF0A, endColorstr=#4DFFFF0A)"}.annotator-hl-temporary{background:#007cff;background:rgba(0,124,255,.3);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#4D007CFF, endColorstr=#4D007CFF)"}.annotator-wrapper{position:relative}.annotator-adder,.annotator-notice,.annotator-outer{z-index:1020}.annotator-filter{z-index:1010}.annotator-adder,.annotator-notice,.annotator-outer,.annotator-widget{position:absolute;font-size:10px;line-height:1}.annotator-hide{display:none;visibility:hidden}.annotator-adder{margin-top:-48px;margin-left:-24px;width:48px;height:48px;background-position:0 0}.annotator-adder:hover{background-position:top}.annotator-adder:active{background-position:100%}.annotator-adder button{display:block;width:36px;height:41px;margin:0 auto;border:none;background:none;text-indent:-999em;cursor:pointer}.annotator-outer{width:0;height:0}.annotator-widget{margin:0;padding:0;bottom:15px;left:-18px;min-width:265px;background-color:#fbfbfb;background-color:hsla(0,0%,98%,.98);border:1px solid #7a7a7a;border:1px solid hsla(0,0%,48%,.6);-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-webkit-box-shadow:0 5px 15px rgba(0,0,0,.2);-moz-box-shadow:0 5px 15px rgba(0,0,0,.2);-o-box-shadow:0 5px 15px rgba(0,0,0,.2);box-shadow:0 5px 15px rgba(0,0,0,.2)}.annotator-invert-x .annotator-widget{left:auto;right:-18px}.annotator-invert-y .annotator-widget{bottom:auto;top:8px}.annotator-widget strong{font-weight:700}.annotator-widget .annotator-item,.annotator-widget .annotator-listing{padding:0;margin:0;list-style:none}.annotator-widget:after{content:"";display:block;width:18px;height:10px;background-position:0 0;position:absolute;bottom:-10px;left:8px}.annotator-invert-x .annotator-widget:after{left:auto;right:8px}.annotator-invert-y .annotator-widget:after{background-position:0 -15px;bottom:auto;top:-9px}.annotator-editor .annotator-item input,.annotator-editor .annotator-item textarea,.annotator-widget .annotator-item{position:relative;font-size:12px}.annotator-viewer .annotator-item{border-top:2px solid #7a7a7a;border-top:2px solid hsla(0,0%,48%,.2)}.annotator-widget .annotator-item:first-child{border-top:none}.annotator-editor .annotator-item,.annotator-viewer div{border-top:1px solid #858585;border-top:1px solid hsla(0,0%,52%,.11)}.annotator-viewer div{padding:6px}.annotator-viewer .annotator-item ol,.annotator-viewer .annotator-item ul{padding:4px 16px}.annotator-editor .annotator-item:first-child textarea,.annotator-viewer div:first-of-type{padding-top:12px;padding-bottom:12px;color:#3c3c3c;font-size:13px;font-style:italic;line-height:1.3;border-top:none}.annotator-viewer .annotator-controls{position:relative;top:5px;right:5px;padding-left:5px;opacity:0;-webkit-transition:opacity .2s ease-in;-moz-transition:opacity .2s ease-in;-o-transition:opacity .2s ease-in;transition:opacity .2s ease-in;float:right}.annotator-viewer li .annotator-controls.annotator-visible,.annotator-viewer li:hover .annotator-controls{opacity:1}.annotator-viewer .annotator-controls a,.annotator-viewer .annotator-controls button{cursor:pointer;display:inline-block;width:13px;height:13px;margin-left:2px;border:none;opacity:.2;text-indent:-900em;background-color:transparent;outline:none}.annotator-viewer .annotator-controls a:focus,.annotator-viewer .annotator-controls a:hover,.annotator-viewer .annotator-controls button:focus,.annotator-viewer .annotator-controls button:hover{opacity:.9}.annotator-viewer .annotator-controls a:active,.annotator-viewer .annotator-controls button:active{opacity:1}.annotator-viewer .annotator-controls button[disabled]{display:none}.annotator-viewer .annotator-controls .annotator-edit{background-position:0 -60px}.annotator-viewer .annotator-controls .annotator-delete{background-position:0 -75px}.annotator-viewer .annotator-controls .annotator-link{background-position:0 -270px}.annotator-editor .annotator-item{position:relative}.annotator-editor .annotator-item label{top:0;display:inline;cursor:pointer;font-size:12px}.annotator-editor .annotator-item input,.annotator-editor .annotator-item textarea{display:block;min-width:100%;padding:10px 8px;border:none;margin:0;color:#3c3c3c;background:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;resize:none}.annotator-editor .annotator-item textarea::-webkit-scrollbar{height:8px;width:8px}.annotator-editor .annotator-item textarea::-webkit-scrollbar-track-piece{margin:13px 0 3px;background-color:#e5e5e5;-webkit-border-radius:4px}.annotator-editor .annotator-item textarea::-webkit-scrollbar-thumb:vertical{height:25px;background-color:#ccc;-webkit-border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1)}.annotator-editor .annotator-item textarea::-webkit-scrollbar-thumb:horizontal{width:25px;background-color:#ccc;-webkit-border-radius:4px}.annotator-editor .annotator-item:first-child textarea{min-height:5.5em;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;-o-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.annotator-editor .annotator-item input:focus,.annotator-editor .annotator-item textarea:focus{background-color:#f3f3f3;outline:none}.annotator-editor .annotator-item input[type=checkbox],.annotator-editor .annotator-item input[type=radio]{width:auto;min-width:0;padding:0;display:inline;margin:0 4px 0 0;cursor:pointer}.annotator-editor .annotator-checkbox{padding:8px 6px}.annotator-editor .annotator-controls,.annotator-filter,.annotator-filter .annotator-filter-navigation button{text-align:right;padding:3px;border-top:1px solid #d4d4d4;background-color:#d4d4d4;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),color-stop(.6,#dcdcdc),to(#d2d2d2));background-image:-moz-linear-gradient(to bottom,#f5f5f5,#dcdcdc 60%,#d2d2d2);background-image:-webkit-linear-gradient(180deg,#f5f5f5,#dcdcdc 60%,#d2d2d2);background-image:linear-gradient(180deg,#f5f5f5,#dcdcdc 60%,#d2d2d2);-webkit-box-shadow:inset 1px 0 0 hsla(0,0%,100%,.7),inset -1px 0 0 hsla(0,0%,100%,.7),inset 0 1px 0 hsla(0,0%,100%,.7);-moz-box-shadow:inset 1px 0 0 hsla(0,0%,100%,.7),inset -1px 0 0 hsla(0,0%,100%,.7),inset 0 1px 0 hsla(0,0%,100%,.7);-o-box-shadow:inset 1px 0 0 hsla(0,0%,100%,.7),inset -1px 0 0 hsla(0,0%,100%,.7),inset 0 1px 0 hsla(0,0%,100%,.7);box-shadow:inset 1px 0 0 hsla(0,0%,100%,.7),inset -1px 0 0 hsla(0,0%,100%,.7),inset 0 1px 0 hsla(0,0%,100%,.7);-webkit-border-radius:0 0 5px 5px;-moz-border-radius:0 0 5px 5px;-o-border-radius:0 0 5px 5px;border-radius:0 0 5px 5px}.annotator-editor.annotator-invert-y .annotator-controls{border-top:none;border-bottom:1px solid #b4b4b4;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;-o-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.annotator-editor a,.annotator-filter .annotator-filter-property label{position:relative;display:inline-block;padding:0 6px 0 22px;color:#363636;text-shadow:0 1px 0 hsla(0,0%,100%,.75);text-decoration:none;line-height:24px;font-size:12px;font-weight:700;border:1px solid #a2a2a2;background-color:#d4d4d4;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),color-stop(.5,#d2d2d2),color-stop(.5,#bebebe),to(#d2d2d2));background-image:-moz-linear-gradient(to bottom,#f5f5f5,#d2d2d2 50%,#bebebe 50%,#d2d2d2);background-image:-webkit-linear-gradient(180deg,#f5f5f5,#d2d2d2 50%,#bebebe 0,#d2d2d2);background-image:linear-gradient(180deg,#f5f5f5,#d2d2d2 50%,#bebebe 0,#d2d2d2);-webkit-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-moz-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-o-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-webkit-border-radius:5px;-moz-border-radius:5px;-o-border-radius:5px;border-radius:5px}.annotator-editor a:after{position:absolute;top:50%;left:5px;display:block;content:"";width:15px;height:15px;margin-top:-7px;background-position:0 -90px}.annotator-editor a.annotator-focus,.annotator-editor a:focus,.annotator-editor a:hover,.annotator-filter .annotator-filter-active label,.annotator-filter .annotator-filter-navigation button:hover{outline:none;border-color:#435aa0;background-color:#3865f9;background-image:-webkit-gradient(linear,left top,left bottom,from(#7691fb),color-stop(.5,#5075fb),color-stop(.5,#3865f9),to(#3665fa));background-image:-moz-linear-gradient(to bottom,#7691fb,#5075fb 50%,#3865f9 50%,#3665fa);background-image:-webkit-linear-gradient(180deg,#7691fb,#5075fb 50%,#3865f9 0,#3665fa);background-image:linear-gradient(180deg,#7691fb,#5075fb 50%,#3865f9 0,#3665fa);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.42)}.annotator-editor a:focus:after,.annotator-editor a:hover:after{margin-top:-8px;background-position:0 -105px}.annotator-editor a:active,.annotator-filter .annotator-filter-navigation button:active{border-color:#700c49;background-color:#d12e8e;background-image:-webkit-gradient(linear,left top,left bottom,from(#fc7cca),color-stop(.5,#e85db2),color-stop(.5,#d12e8e),to(#ff009c));background-image:-moz-linear-gradient(to bottom,#fc7cca,#e85db2 50%,#d12e8e 50%,#ff009c);background-image:-webkit-linear-gradient(180deg,#fc7cca,#e85db2 50%,#d12e8e 0,#ff009c);background-image:linear-gradient(180deg,#fc7cca,#e85db2 50%,#d12e8e 0,#ff009c)}.annotator-editor a.annotator-save:after{background-position:0 -120px}.annotator-editor a.annotator-save.annotator-focus:after,.annotator-editor a.annotator-save:focus:after,.annotator-editor a.annotator-save:hover:after{margin-top:-8px;background-position:0 -135px}.annotator-editor .annotator-widget:after{background-position:0 -30px}.annotator-editor.annotator-invert-y .annotator-widget .annotator-controls{background-color:#f2f2f2}.annotator-editor.annotator-invert-y .annotator-widget:after{background-position:0 -45px;height:11px}.annotator-resize{position:absolute;top:0;right:0;width:12px;height:12px;background-position:2px -150px}.annotator-invert-x .annotator-resize{right:auto;left:0;background-position:0 -195px}.annotator-invert-y .annotator-resize{top:auto;bottom:0;background-position:2px -165px}.annotator-invert-y.annotator-invert-x .annotator-resize{background-position:0 -180px}.annotator-notice{color:#fff;position:fixed;top:-54px;left:0;width:100%;font-size:14px;line-height:50px;text-align:center;background:#000;background:rgba(0,0,0,.9);border-bottom:4px solid #d4d4d4;-webkit-transition:top .4s ease-out;-moz-transition:top .4s ease-out;-o-transition:top .4s ease-out;transition:top .4s ease-out}.annotator-notice-success{border-color:#3665f9}.annotator-notice-error{border-color:#ff7e00}.annotator-notice p{margin:0}.annotator-notice a{color:#fff}.annotator-notice-show{top:0}.annotator-tags{margin-bottom:-2px}.annotator-tags .annotator-tag{display:inline-block;padding:0 8px;margin-bottom:2px;line-height:1.6;font-weight:700;background-color:#e6e6e6;-webkit-border-radius:8px;-moz-border-radius:8px;-o-border-radius:8px;border-radius:8px}.annotator-filter{position:fixed;top:0;right:0;left:0;text-align:left;line-height:0;border:none;border-bottom:1px solid #878787;padding-left:10px;padding-right:10px;-webkit-border-radius:0;-moz-border-radius:0;-o-border-radius:0;border-radius:0;-webkit-box-shadow:inset 0 -1px 0 hsla(0,0%,100%,.3);-moz-box-shadow:inset 0 -1px 0 hsla(0,0%,100%,.3);-o-box-shadow:inset 0 -1px 0 hsla(0,0%,100%,.3);box-shadow:inset 0 -1px 0 hsla(0,0%,100%,.3)}.annotator-filter strong{font-size:12px;font-weight:700;color:#3c3c3c;text-shadow:0 1px 0 hsla(0,0%,100%,.7);position:relative;top:-9px}.annotator-filter .annotator-filter-navigation,.annotator-filter .annotator-filter-property{position:relative;display:inline-block;overflow:hidden;line-height:10px;padding:2px 0;margin-right:8px}.annotator-filter .annotator-filter-navigation button,.annotator-filter .annotator-filter-property label{text-align:left;display:block;float:left;line-height:20px;-webkit-border-radius:10px 0 0 10px;-moz-border-radius:10px 0 0 10px;-o-border-radius:10px 0 0 10px;border-radius:10px 0 0 10px}.annotator-filter .annotator-filter-property label{padding-left:8px}.annotator-filter .annotator-filter-property input{display:block;float:right;-webkit-appearance:none;background-color:#fff;border:1px solid #878787;border-left:none;padding:2px 4px;line-height:16px;min-height:16px;font-size:12px;width:150px;color:#333;background-color:#f8f8f8;-webkit-border-radius:0 10px 10px 0;-moz-border-radius:0 10px 10px 0;-o-border-radius:0 10px 10px 0;border-radius:0 10px 10px 0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.2);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,.2);-o-box-shadow:inset 0 1px 1px rgba(0,0,0,.2);box-shadow:inset 0 1px 1px rgba(0,0,0,.2)}.annotator-filter .annotator-filter-property input:focus{outline:none;background-color:#fff}.annotator-filter .annotator-filter-clear{position:absolute;right:3px;top:6px;border:none;text-indent:-900em;width:15px;height:15px;background-position:0 -90px;opacity:.4}.annotator-filter .annotator-filter-clear:focus,.annotator-filter .annotator-filter-clear:hover{opacity:.8}.annotator-filter .annotator-filter-clear:active{opacity:1}.annotator-filter .annotator-filter-navigation button{border:1px solid #a2a2a2;padding:0;text-indent:-900px;width:20px;min-height:22px;-webkit-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-moz-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-o-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8)}.annotator-filter .annotator-filter-navigation button,.annotator-filter .annotator-filter-navigation button:focus,.annotator-filter .annotator-filter-navigation button:hover{color:transparent}.annotator-filter .annotator-filter-navigation button:after{position:absolute;top:8px;left:8px;content:"";display:block;width:9px;height:9px;background-position:0 -210px}.annotator-filter .annotator-filter-navigation button:hover:after{background-position:0 -225px}.annotator-filter .annotator-filter-navigation .annotator-filter-next{-webkit-border-radius:0 10px 10px 0;-moz-border-radius:0 10px 10px 0;-o-border-radius:0 10px 10px 0;border-radius:0 10px 10px 0;border-left:none}.annotator-filter .annotator-filter-navigation .annotator-filter-next:after{left:auto;right:7px;background-position:0 -240px}.annotator-filter .annotator-filter-navigation .annotator-filter-next:hover:after{background-position:0 -255px}.annotator-hl-active{background:#ffff0a;background:rgba(255,255,10,.8);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#CCFFFF0A, endColorstr=#CCFFFF0A)"}.annotator-hl-filtered{background-color:transparent}@font-face{font-family:Material Icons;font-style:normal;font-weight:400;src:url(fonts/MaterialIcons-Regular.eot);src:local("Material Icons"),local("MaterialIcons-Regular"),url(fonts/MaterialIcons-Regular.woff2) format("woff2"),url(fonts/MaterialIcons-Regular.woff) format("woff"),url(fonts/MaterialIcons-Regular.ttf) format("truetype")}.material-icons{font-family:Material Icons;font-weight:400;font-style:normal;font-size:24px;display:inline-block;line-height:1;text-transform:none;letter-spacing:normal;word-wrap:normal;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:"liga"}@font-face{font-family:Lato;font-weight:100;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-hairline.woff2) format("woff2"),url(fonts/lato-hairline.woff) format("woff")}@font-face{font-family:Lato;font-weight:100;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-hairline-italic.woff2) format("woff2"),url(fonts/lato-hairline-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:200;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-thin.woff2) format("woff2"),url(fonts/lato-thin.woff) format("woff")}@font-face{font-family:Lato;font-weight:200;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-thin-italic.woff2) format("woff2"),url(fonts/lato-thin-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:300;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-light.woff2) format("woff2"),url(fonts/lato-light.woff) format("woff")}@font-face{font-family:Lato;font-weight:300;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-light-italic.woff2) format("woff2"),url(fonts/lato-light-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:400;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-normal.woff2) format("woff2"),url(fonts/lato-normal.woff) format("woff")}@font-face{font-family:Lato;font-weight:400;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-normal-italic.woff2) format("woff2"),url(fonts/lato-normal-italic.woff) format("woff")}@font-face{font-family:Lato Medium;font-weight:400;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-medium.woff2) format("woff2"),url(fonts/lato-medium.woff) format("woff")}@font-face{font-family:Lato Medium;font-weight:400;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-medium-italic.woff2) format("woff2"),url(fonts/lato-medium-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:500;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-semibold.woff2) format("woff2"),url(fonts/lato-semibold.woff) format("woff")}@font-face{font-family:Lato;font-weight:500;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-semibold-italic.woff2) format("woff2"),url(fonts/lato-semibold-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:600;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-bold.woff2) format("woff2"),url(fonts/lato-bold.woff) format("woff")}@font-face{font-family:Lato;font-weight:600;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-bold-italic.woff2) format("woff2"),url(fonts/lato-bold-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:800;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-heavy.woff2) format("woff2"),url(fonts/lato-heavy.woff) format("woff")}@font-face{font-family:Lato;font-weight:800;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-heavy-italic.woff2) format("woff2"),url(fonts/lato-heavy-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:900;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-black.woff2) format("woff2"),url(fonts/lato-black.woff) format("woff")}@font-face{font-family:Lato;font-weight:900;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-black-italic.woff2) format("woff2"),url(fonts/lato-black-italic.woff) format("woff")}.material-icons.md-18{font-size:18px}.material-icons.md-24{font-size:24px}.material-icons.md-36{font-size:36px}.material-icons.md-48{font-size:48px}.material-icons.md-dark{color:rgba(0,0,0,.54)}.material-icons.md-dark.md-inactive{color:rgba(0,0,0,.26)}.material-icons.md-light{color:#fff}.material-icons.md-light.md-inactive{color:hsla(0,0%,100%,.3)}.hljs{display:block;overflow-x:auto;padding:.5em;color:#383a42;background:#fafafa}.hljs-comment,.hljs-quote{color:#a0a1a7;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#a626a4}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e45649}.hljs-literal{color:#0184bb}.hljs-addition,.hljs-attribute,.hljs-meta-string,.hljs-regexp,.hljs-string{color:#50a14f}.hljs-built_in,.hljs-class .hljs-title{color:#c18401}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#986801}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#4078f2}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}#article{font-size:20px;margin:0 auto;max-width:45em}#article article{color:#424242;font-size:18px;line-height:1.7em;overflow-wrap:break-word}#article article h1,#article article h2,#article article h3,#article article h4,#article article h5,#article article h6{color:#212121}#article article h1 strong,#article article h2 strong,#article article h3 strong,#article article h4 strong,#article article h5 strong,#article article h6 strong{font-weight:500}#article article h6{font-size:1.2rem}#article article h5{font-size:1.6rem}#article article h4{font-size:1.9rem}#article article h3{font-size:2.2rem}#article article h2{font-size:2.5rem}#article article h1{font-size:2.7rem}#article article a{border-bottom:1px dotted #03a9f4;text-decoration:none}#article article a:hover{border-bottom-style:solid}#article article ul{padding-left:30px}#article article ul,#article article ul li{list-style-type:disc}#article article blockquote{font-style:italic}#article article strong{font-weight:700}#article figure,#article img{max-width:100%;height:auto}#article pre{box-sizing:border-box;margin:0 0 1.75em;border:1px solid #e3f2fd;width:100%;padding:10px;font-family:monospace;font-size:.8em;white-space:pre;overflow:auto;background:#f5f5f5;border-radius:3px}#article>header>h1{font-size:2em;margin:2.1rem 0 .68rem}#article aside .tools{display:flex;flex-flow:row wrap}#article aside .tools .stats{font-size:.8em;margin:8px 5px 5px}#article aside .tools .stats li{display:inline-flex;vertical-align:middle;margin:3px 5px}#article aside .tools .stats li i.material-icons{color:#3e3e3e;margin-right:3px}#article aside .tools .stats a{color:#000;text-decoration:none}#article aside .tools .tags{float:right;margin:5px 15px 10px}#article aside .chip{background-color:rgba(0,151,167,.85);padding:0 15px 0 10px;margin:auto 2px;border-radius:6px}#article aside .chip a,#article aside .chip i{color:#fff}#article aside .chip i.material-icons{float:right;font-size:20px;line-height:32px;padding-left:8px}.reader-mode{width:70px!important;transition:width .2s ease}.reader-mode .collapsible-body{height:0;overflow:hidden}.reader-mode span{opacity:0;transition:opacity .2s ease}.reader-mode:hover{width:260px!important}.reader-mode:hover .collapsible-body{height:auto}.reader-mode:hover .collapsible-body li a i.material-icons{margin:auto 5px auto -8px}.reader-mode:hover span{opacity:1}.progress{position:fixed;top:0;width:100%;height:3px;margin:0;z-index:9999}main #content{padding:0 .5rem}main ul.row{margin:.4rem 0 0;padding:0 .75rem}.data .card .card-body{height:19em;overflow:hidden}.card .card-content .card-title,.card .card-reveal .card-title{line-height:22.8px;max-height:80px;font-size:19px;font-family:roberto,Helvetica Neue,Helvetica,Arial,sans-serif}.card .card-stacked .card-content .card-title{display:inline-block}.card .card-content .activator,.card .card-reveal .activator{cursor:pointer;font-family:Material Icons}.card .card-content i.right,.card .card-reveal i.right{margin-left:0}.card .card-content .original{line-height:24px;font-size:15px}.card .card-entry-labels{position:absolute;top:10px;z-index:90;max-width:50%}.card .card-entry-labels-hidden{margin:2.5px auto}.card .card-entry-labels-hidden li{display:inline-block;background-color:rgba(0,151,167,.85);margin:0 5px;padding:5px 12px;border-radius:3px;color:#fff;max-height:2em;max-width:calc(100% - 15px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card .card-content .estimatedTime{margin-bottom:10px}.card .card-action{padding:10px 5px 10px 15px}.card .card-action ul.links{margin:0;font-size:24px;line-height:24px}.card .card-action a{color:#fff;margin:0}.card .card-action a:hover{color:#fff}.card .card-action ul.tools li a.tool{margin-right:5px!important}.card .card-action .reading-time{display:inline-flex;vertical-align:middle}.card .card-action .reading-time span{margin-right:5px}.card .card-image{height:10em}.card .card-fullimage{height:13.5em}.card .card-fullimage .preview,.card .card-image .preview{height:14em;background:no-repeat 50%/cover;display:block}.card.sw{max-width:370px;margin-left:auto;margin-right:auto}a.original:not(.waves-effect){text-overflow:ellipsis;white-space:nowrap;overflow:hidden;display:block}.card-entry-labels li,.card-tag-labels li{margin:10px 10px 10px auto;padding:5px 12px 5px 16px!important;background-color:rgba(0,151,167,.85);border-radius:3px;color:#fff;cursor:default}.card-entry-labels li{text-overflow:ellipsis;white-space:nowrap;border-radius:0 3px 3px 0;overflow:hidden}.card-tag-labels li{display:flex;justify-content:space-between}#list .chip a,.card-entry-labels-hidden a,.card-entry-labels a,.card-entry-tags a,.card-tag-labels a{text-decoration:none;font-weight:400;color:#fff}.card-tag-labels a{height:100%;align-items:center}.card-tag-link{display:flex;min-width:100px;flex-grow:1}.card-tag-rss{display:flex}.card-tag-labels{display:flex;flex-wrap:wrap}.card-tag-labels li{margin:10px;flex-basis:19%;flex-grow:1;align-items:center}.card-stacked{display:flex;flex-flow:row wrap}.card-stacked:hover ul.tools-list{display:inline;text-align:right}.card-stacked .preview{max-width:100px;height:auto;margin-right:10px;flex:1}.card-stacked .preview img{max-width:100%;max-height:100%}.card-stacked div.metadata .chip{background-color:rgba(0,151,167,.85);padding:0 15px 0 10px;margin:auto 2px;border-radius:6px}.card-stacked div.metadata .chip a,.card-stacked div.metadata .chip i{color:#fff}.card-stacked div.metadata .chip i.material-icons{float:right;font-size:20px;line-height:32px;padding-left:8px}.card-stacked div.card-content{flex:4}.card-stacked ul.tools-list{flex:1;display:none;flex-basis:5em;align-self:flex-end;float:right;max-width:6em}.card-stacked .tags{display:inline-block}#content .collection .collection-item{min-height:65px;height:auto}.quickstart .card .card-action a,.quickstart .card .card-action a:hover{color:#fff!important}.settings .div_tabs{padding-bottom:15px}@media only screen and (min-width:992px){.card-tag-labels li{max-width:50%}}.collection{margin:15px 15px 0}.collection .collection-item{padding:7px;height:65px}.results{display:flex;padding:1rem 1rem 0;flex-wrap:wrap;justify-content:space-between}.results .nb-results{display:inline-flex}.results a{color:#444}.pagination ul{display:flex;margin:0;flex-wrap:wrap;justify-content:space-around}.pagination ul .next.disabled,.pagination ul .prev.disabled{display:none}.pagination li{padding:0}.pagination a,.pagination span{padding:0 10px;height:30px;display:block;line-height:30px}.pagination .disabled{margin-right:10px;margin-left:10px}.pagination li.active span{padding:0 10px;height:30px;display:block;color:#fff}.footer-text{margin:.7rem .5rem}.hidden,.picker__date-display{display:none}footer.page-footer{margin-top:10px;padding-top:0}footer .row{margin-bottom:10px}#filters button{padding:0;width:100%}#filters div.with-checkbox{height:3rem;margin-top:0}body{display:flex;min-height:100vh;flex-direction:column;background:#fafafa}body.login main{padding:0;min-height:100vh}.border-bottom{border-bottom:1px solid #ddd}#content,.valign-wrapper,main{height:100%}.typo-logo{max-width:150px}#main{flex:1 0 auto}#main .logo a{height:100pt}#main .logo img{height:100pt;width:100pt}#main .logo:hover{background:transparent}nav{height:auto;line-height:normal}nav input{color:#aaa}nav ul a:hover{background-color:initial}.nav-panel-item .button-collapse{margin-left:0;margin-right:.5rem;padding:0 .5rem;height:auto;line-height:1;background-color:transparent;border:none}.nav-panel-item{display:flex;padding:.6rem .4rem .6rem .75rem;flex-wrap:wrap;justify-content:space-between;align-items:center}.nav-panel-item .material-icons{height:46px;line-height:46px}.nav-input{display:none}.nav-panel-buttom{display:flex;flex-grow:1;justify-content:flex-end}.nav-panel-item .add,.nav-panel-item .search,.nav-panels .close{color:#444!important}.nav-panels{transition:background .2s ease}.nav-panels .action{margin:0;font-size:2.1rem}.nav-panels .input-field input{display:block;line-height:inherit;height:3rem}.nav-panels .input-field input:focus{background-color:#fff;border:0;box-shadow:none;color:#444}.nav-panel-top{display:flex;align-items:center}.input-field.nav-panel-item label{left:1rem}.input-field.nav-panel-item .close{color:transparent;cursor:pointer;font-size:2rem;transition:color .3s}.input-field.nav-panel-item{display:flex;flex:1;flex-wrap:nowrap;align-items:center}.input-field.nav-panel-add.disabled,.input-field.nav-panel-add.disabled input{background-color:#f5f5f5}.nav-form-button{padding:0;background-color:transparent;border:none}.nav-form-button:focus{background-color:inherit}.nav-form-button,.nav-panel-item .close{margin:0 1%}#button_export,#button_filters{display:none}@media (min-width:993px){.button-collapse{display:none}}.side-nav{width:240px}.side-nav li{padding:0}.side-nav li.logo>a:hover{background:initial}.side-nav a{margin:0}.side-nav.fixed a{font-size:13px;line-height:44px;height:44px}.side-nav .collapsible-header,.side-nav.fixed .collapsible-header{height:45px;line-height:44px;padding:0 20px}.side-nav>li.logo{line-height:0;text-align:center}.bold>a{font-weight:700}span.numberItems{float:right}div.settings div.file-field div,div.settings div.file-field ul{margin-top:40px}div.settings div.file-field div{margin-top:inherit}.input-field label.active{font-size:1rem}nav .input-field input{margin:0;padding-left:.5rem}.tabs{display:flex}.tab{flex:1}@font-face{font-family:icomoon;src:url(fonts/IcoMoon-Free.ttf);font-weight:400;font-style:normal}.material-icons{font-family:Material Icons;font-weight:400;font-style:normal;font-size:24px;width:1em;height:1em;display:inline-block;line-height:1;text-transform:none;letter-spacing:normal;word-wrap:normal;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:"liga"}.material-icons .md-18{font-size:18px}.material-icons .md-24{font-size:24px}.material-icons .md-36{font-size:36px}.material-icons .md-48{font-size:48px}.material-icons .md-dark{color:rgba(0,0,0,.54)}.material-icons .md-dark .md-inactive{color:rgba(0,0,0,.26)}.material-icons .md-light{color:#fff}.material-icons .md-light .md-inactive{color:hsla(0,0%,100%,.3)}[class*=" icon-"]:before,[class^=icon-]:before{font-family:icomoon;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;background-size:24px;letter-spacing:0;font-feature-settings:"liga"}.icon-eye:before{content:"\E9CE"}.icon-no-eye:before{content:"\E9D1"}.icon-calendar:before{content:"\E953"}.icon-mail:before{content:"\EA86"}.icon-time:before{content:"\E952"}a.icon-image{background-repeat:no-repeat;padding-right:.4em!important;padding-left:0!important;margin-left:25px}a.icon-image:before{content:"";display:block;width:24px;height:24px;float:left;margin:7px 1.5px 0 0}a.icon-image.carrot:before{background:url(themes/_global/img/icons/carrot-icon--black.png) no-repeat 50%/90%}a.icon-image.diaspora:before{background:url(themes/_global/img/icons/diaspora-icon--black.png) no-repeat 50%/80%}a.icon-image.unmark:before{background:url(themes/_global/img/icons/unmark-icon--black.png) no-repeat 50%/80%}a.icon-image.shaarli:before{background:url(themes/_global/img/icons/shaarli.png) no-repeat 50%/80%}a.icon-image.scuttle:before{background:url(themes/_global/img/icons/scuttle.png) no-repeat 50%/80%}.icon-google-plus2:before{content:"\EA89"}.icon-facebook2:before{content:"\EA8D"}.icon-twitter:before{content:"\EA96"}.icon-apple:before{content:"\EABF"}.icon-android:before{content:"\EAC1"}.icon-chrome:before{content:"\EAE5"}.icon-firefox:before{content:"\EAE6"}.icon-link:before{content:"\E9CB"}footer [class*=" icon-"],footer [class^=icon-]{font-size:2em;transition:text-shadow .2s ease;padding-right:10px}footer [class*=" icon-"]:hover,footer [class^=icon-]:hover{text-shadow:0 0 10px rgba(0,0,0,.3)}@media print{body{font-family:Serif;background-color:#fff}@page{margin:1cm}img{max-width:100%!important}#article .mbm a,#article>aside,#article_toolbar,#links,#slide-out,#sort,.entry+.results,.hide-on-large-only,.messages,.progress,.top_link,body>footer,body>header,div.tools,header div{display:none!important}main{padding-left:0!important}#article{margin:inherit!important}article{border:none!important}.vieworiginal a:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.pagination span.current{border-style:dashed}#main{margin:0;padding:0}#article,#main{width:100%}}@media only screen and (min-width:992px){body:not(.entry):not(.login) main,footer,nav{padding-left:240px}.pagination{margin-left:auto}}@media only screen and (max-width:992px){footer,header,main,nav{padding-left:0}table{display:block;overflow:auto}iframe{max-width:100%;height:auto}.nav-panels .action{padding-right:.75rem}.nav-panel-buttom{justify-content:space-between}#article{max-width:35em;margin-left:auto;margin-right:auto;font-size:18px}#article>header>h1{font-size:1.33em}.reader-mode{width:240px!important}.reader-mode span{opacity:1}.tabs{display:inline-block;height:auto}.tab{min-width:100%}.indicator{display:none}.pagination li{margin-bottom:.5rem}.pagination li.next,.pagination li.prev{width:auto}.drag-target+.drag-target{height:50%}.drag-target+.drag-target+.drag-target{top:50%}}@media only screen and (min-width:1200px) and (max-width:1650px){.row .col.l3{width:33.33333%;margin-left:0}}@media only screen and (min-width:993px) and (max-width:1200px){.row .col.l1{width:25%;margin-left:0}.row .col.l2{width:33.33333%;margin-left:0}.row .col.l3{width:41.66667%;margin-left:0}.row .col.l4{width:50%;margin-left:0}.row .col.l5{width:58.33333%;margin-left:0}.row .col.l6{width:66.66667%;margin-left:0}.row .col.l7{width:75%;margin-left:0}.row .col.l8{width:83.33333%;margin-left:0}.row .col.l9{width:91.66667%;margin-left:0}.row .col.l10{width:100%;margin-left:0}}@media only screen and (max-width:350px){.nb-results{display:none}.row .col,main ul.row{padding:0}} -/*# sourceMappingURL=material.css.map*/ \ No newline at end of file +.materialize-red{background-color:#e51c23!important}.materialize-red-text{color:#e51c23!important}.materialize-red.lighten-5{background-color:#fdeaeb!important}.materialize-red-text.text-lighten-5{color:#fdeaeb!important}.materialize-red.lighten-4{background-color:#f8c1c3!important}.materialize-red-text.text-lighten-4{color:#f8c1c3!important}.materialize-red.lighten-3{background-color:#f3989b!important}.materialize-red-text.text-lighten-3{color:#f3989b!important}.materialize-red.lighten-2{background-color:#ee6e73!important}.materialize-red-text.text-lighten-2{color:#ee6e73!important}.materialize-red.lighten-1{background-color:#ea454b!important}.materialize-red-text.text-lighten-1{color:#ea454b!important}.materialize-red.darken-1{background-color:#d0181e!important}.materialize-red-text.text-darken-1{color:#d0181e!important}.materialize-red.darken-2{background-color:#b9151b!important}.materialize-red-text.text-darken-2{color:#b9151b!important}.materialize-red.darken-3{background-color:#a21318!important}.materialize-red-text.text-darken-3{color:#a21318!important}.materialize-red.darken-4{background-color:#8b1014!important}.materialize-red-text.text-darken-4{color:#8b1014!important}.red{background-color:#f44336!important}.red-text{color:#f44336!important}.red.lighten-5{background-color:#ffebee!important}.red-text.text-lighten-5{color:#ffebee!important}.red.lighten-4{background-color:#ffcdd2!important}.red-text.text-lighten-4{color:#ffcdd2!important}.red.lighten-3{background-color:#ef9a9a!important}.red-text.text-lighten-3{color:#ef9a9a!important}.red.lighten-2{background-color:#e57373!important}.red-text.text-lighten-2{color:#e57373!important}.red.lighten-1{background-color:#ef5350!important}.red-text.text-lighten-1{color:#ef5350!important}.red.darken-1{background-color:#e53935!important}.red-text.text-darken-1{color:#e53935!important}.red.darken-2{background-color:#d32f2f!important}.red-text.text-darken-2{color:#d32f2f!important}.red.darken-3{background-color:#c62828!important}.red-text.text-darken-3{color:#c62828!important}.red.darken-4{background-color:#b71c1c!important}.red-text.text-darken-4{color:#b71c1c!important}.red.accent-1{background-color:#ff8a80!important}.red-text.text-accent-1{color:#ff8a80!important}.red.accent-2{background-color:#ff5252!important}.red-text.text-accent-2{color:#ff5252!important}.red.accent-3{background-color:#ff1744!important}.red-text.text-accent-3{color:#ff1744!important}.red.accent-4{background-color:#d50000!important}.red-text.text-accent-4{color:#d50000!important}.pink{background-color:#e91e63!important}.pink-text{color:#e91e63!important}.pink.lighten-5{background-color:#fce4ec!important}.pink-text.text-lighten-5{color:#fce4ec!important}.pink.lighten-4{background-color:#f8bbd0!important}.pink-text.text-lighten-4{color:#f8bbd0!important}.pink.lighten-3{background-color:#f48fb1!important}.pink-text.text-lighten-3{color:#f48fb1!important}.pink.lighten-2{background-color:#f06292!important}.pink-text.text-lighten-2{color:#f06292!important}.pink.lighten-1{background-color:#ec407a!important}.pink-text.text-lighten-1{color:#ec407a!important}.pink.darken-1{background-color:#d81b60!important}.pink-text.text-darken-1{color:#d81b60!important}.pink.darken-2{background-color:#c2185b!important}.pink-text.text-darken-2{color:#c2185b!important}.pink.darken-3{background-color:#ad1457!important}.pink-text.text-darken-3{color:#ad1457!important}.pink.darken-4{background-color:#880e4f!important}.pink-text.text-darken-4{color:#880e4f!important}.pink.accent-1{background-color:#ff80ab!important}.pink-text.text-accent-1{color:#ff80ab!important}.pink.accent-2{background-color:#ff4081!important}.pink-text.text-accent-2{color:#ff4081!important}.pink.accent-3{background-color:#f50057!important}.pink-text.text-accent-3{color:#f50057!important}.pink.accent-4{background-color:#c51162!important}.pink-text.text-accent-4{color:#c51162!important}.purple{background-color:#9c27b0!important}.purple-text{color:#9c27b0!important}.purple.lighten-5{background-color:#f3e5f5!important}.purple-text.text-lighten-5{color:#f3e5f5!important}.purple.lighten-4{background-color:#e1bee7!important}.purple-text.text-lighten-4{color:#e1bee7!important}.purple.lighten-3{background-color:#ce93d8!important}.purple-text.text-lighten-3{color:#ce93d8!important}.purple.lighten-2{background-color:#ba68c8!important}.purple-text.text-lighten-2{color:#ba68c8!important}.purple.lighten-1{background-color:#ab47bc!important}.purple-text.text-lighten-1{color:#ab47bc!important}.purple.darken-1{background-color:#8e24aa!important}.purple-text.text-darken-1{color:#8e24aa!important}.purple.darken-2{background-color:#7b1fa2!important}.purple-text.text-darken-2{color:#7b1fa2!important}.purple.darken-3{background-color:#6a1b9a!important}.purple-text.text-darken-3{color:#6a1b9a!important}.purple.darken-4{background-color:#4a148c!important}.purple-text.text-darken-4{color:#4a148c!important}.purple.accent-1{background-color:#ea80fc!important}.purple-text.text-accent-1{color:#ea80fc!important}.purple.accent-2{background-color:#e040fb!important}.purple-text.text-accent-2{color:#e040fb!important}.purple.accent-3{background-color:#d500f9!important}.purple-text.text-accent-3{color:#d500f9!important}.purple.accent-4{background-color:#a0f!important}.purple-text.text-accent-4{color:#a0f!important}.deep-purple{background-color:#673ab7!important}.deep-purple-text{color:#673ab7!important}.deep-purple.lighten-5{background-color:#ede7f6!important}.deep-purple-text.text-lighten-5{color:#ede7f6!important}.deep-purple.lighten-4{background-color:#d1c4e9!important}.deep-purple-text.text-lighten-4{color:#d1c4e9!important}.deep-purple.lighten-3{background-color:#b39ddb!important}.deep-purple-text.text-lighten-3{color:#b39ddb!important}.deep-purple.lighten-2{background-color:#9575cd!important}.deep-purple-text.text-lighten-2{color:#9575cd!important}.deep-purple.lighten-1{background-color:#7e57c2!important}.deep-purple-text.text-lighten-1{color:#7e57c2!important}.deep-purple.darken-1{background-color:#5e35b1!important}.deep-purple-text.text-darken-1{color:#5e35b1!important}.deep-purple.darken-2{background-color:#512da8!important}.deep-purple-text.text-darken-2{color:#512da8!important}.deep-purple.darken-3{background-color:#4527a0!important}.deep-purple-text.text-darken-3{color:#4527a0!important}.deep-purple.darken-4{background-color:#311b92!important}.deep-purple-text.text-darken-4{color:#311b92!important}.deep-purple.accent-1{background-color:#b388ff!important}.deep-purple-text.text-accent-1{color:#b388ff!important}.deep-purple.accent-2{background-color:#7c4dff!important}.deep-purple-text.text-accent-2{color:#7c4dff!important}.deep-purple.accent-3{background-color:#651fff!important}.deep-purple-text.text-accent-3{color:#651fff!important}.deep-purple.accent-4{background-color:#6200ea!important}.deep-purple-text.text-accent-4{color:#6200ea!important}.indigo{background-color:#3f51b5!important}.indigo-text{color:#3f51b5!important}.indigo.lighten-5{background-color:#e8eaf6!important}.indigo-text.text-lighten-5{color:#e8eaf6!important}.indigo.lighten-4{background-color:#c5cae9!important}.indigo-text.text-lighten-4{color:#c5cae9!important}.indigo.lighten-3{background-color:#9fa8da!important}.indigo-text.text-lighten-3{color:#9fa8da!important}.indigo.lighten-2{background-color:#7986cb!important}.indigo-text.text-lighten-2{color:#7986cb!important}.indigo.lighten-1{background-color:#5c6bc0!important}.indigo-text.text-lighten-1{color:#5c6bc0!important}.indigo.darken-1{background-color:#3949ab!important}.indigo-text.text-darken-1{color:#3949ab!important}.indigo.darken-2{background-color:#303f9f!important}.indigo-text.text-darken-2{color:#303f9f!important}.indigo.darken-3{background-color:#283593!important}.indigo-text.text-darken-3{color:#283593!important}.indigo.darken-4{background-color:#1a237e!important}.indigo-text.text-darken-4{color:#1a237e!important}.indigo.accent-1{background-color:#8c9eff!important}.indigo-text.text-accent-1{color:#8c9eff!important}.indigo.accent-2{background-color:#536dfe!important}.indigo-text.text-accent-2{color:#536dfe!important}.indigo.accent-3{background-color:#3d5afe!important}.indigo-text.text-accent-3{color:#3d5afe!important}.indigo.accent-4{background-color:#304ffe!important}.indigo-text.text-accent-4{color:#304ffe!important}.blue{background-color:#2196f3!important}.blue-text{color:#2196f3!important}.blue.lighten-5{background-color:#e3f2fd!important}.blue-text.text-lighten-5{color:#e3f2fd!important}.blue.lighten-4{background-color:#bbdefb!important}.blue-text.text-lighten-4{color:#bbdefb!important}.blue.lighten-3{background-color:#90caf9!important}.blue-text.text-lighten-3{color:#90caf9!important}.blue.lighten-2{background-color:#64b5f6!important}.blue-text.text-lighten-2{color:#64b5f6!important}.blue.lighten-1{background-color:#42a5f5!important}.blue-text.text-lighten-1{color:#42a5f5!important}.blue.darken-1{background-color:#1e88e5!important}.blue-text.text-darken-1{color:#1e88e5!important}.blue.darken-2{background-color:#1976d2!important}.blue-text.text-darken-2{color:#1976d2!important}.blue.darken-3{background-color:#1565c0!important}.blue-text.text-darken-3{color:#1565c0!important}.blue.darken-4{background-color:#0d47a1!important}.blue-text.text-darken-4{color:#0d47a1!important}.blue.accent-1{background-color:#82b1ff!important}.blue-text.text-accent-1{color:#82b1ff!important}.blue.accent-2{background-color:#448aff!important}.blue-text.text-accent-2{color:#448aff!important}.blue.accent-3{background-color:#2979ff!important}.blue-text.text-accent-3{color:#2979ff!important}.blue.accent-4{background-color:#2962ff!important}.blue-text.text-accent-4{color:#2962ff!important}.light-blue{background-color:#03a9f4!important}.light-blue-text{color:#03a9f4!important}.light-blue.lighten-5{background-color:#e1f5fe!important}.light-blue-text.text-lighten-5{color:#e1f5fe!important}.light-blue.lighten-4{background-color:#b3e5fc!important}.light-blue-text.text-lighten-4{color:#b3e5fc!important}.light-blue.lighten-3{background-color:#81d4fa!important}.light-blue-text.text-lighten-3{color:#81d4fa!important}.light-blue.lighten-2{background-color:#4fc3f7!important}.light-blue-text.text-lighten-2{color:#4fc3f7!important}.light-blue.lighten-1{background-color:#29b6f6!important}.light-blue-text.text-lighten-1{color:#29b6f6!important}.light-blue.darken-1{background-color:#039be5!important}.light-blue-text.text-darken-1{color:#039be5!important}.light-blue.darken-2{background-color:#0288d1!important}.light-blue-text.text-darken-2{color:#0288d1!important}.light-blue.darken-3{background-color:#0277bd!important}.light-blue-text.text-darken-3{color:#0277bd!important}.light-blue.darken-4{background-color:#01579b!important}.light-blue-text.text-darken-4{color:#01579b!important}.light-blue.accent-1{background-color:#80d8ff!important}.light-blue-text.text-accent-1{color:#80d8ff!important}.light-blue.accent-2{background-color:#40c4ff!important}.light-blue-text.text-accent-2{color:#40c4ff!important}.light-blue.accent-3{background-color:#00b0ff!important}.light-blue-text.text-accent-3{color:#00b0ff!important}.light-blue.accent-4{background-color:#0091ea!important}.light-blue-text.text-accent-4{color:#0091ea!important}.cyan{background-color:#00bcd4!important}.cyan-text{color:#00bcd4!important}.cyan.lighten-5{background-color:#e0f7fa!important}.cyan-text.text-lighten-5{color:#e0f7fa!important}.cyan.lighten-4{background-color:#b2ebf2!important}.cyan-text.text-lighten-4{color:#b2ebf2!important}.cyan.lighten-3{background-color:#80deea!important}.cyan-text.text-lighten-3{color:#80deea!important}.cyan.lighten-2{background-color:#4dd0e1!important}.cyan-text.text-lighten-2{color:#4dd0e1!important}.cyan.lighten-1{background-color:#26c6da!important}.cyan-text.text-lighten-1{color:#26c6da!important}.cyan.darken-1{background-color:#00acc1!important}.cyan-text.text-darken-1{color:#00acc1!important}.cyan.darken-2{background-color:#0097a7!important}.cyan-text.text-darken-2{color:#0097a7!important}.cyan.darken-3{background-color:#00838f!important}.cyan-text.text-darken-3{color:#00838f!important}.cyan.darken-4{background-color:#006064!important}.cyan-text.text-darken-4{color:#006064!important}.cyan.accent-1{background-color:#84ffff!important}.cyan-text.text-accent-1{color:#84ffff!important}.cyan.accent-2{background-color:#18ffff!important}.cyan-text.text-accent-2{color:#18ffff!important}.cyan.accent-3{background-color:#00e5ff!important}.cyan-text.text-accent-3{color:#00e5ff!important}.cyan.accent-4{background-color:#00b8d4!important}.cyan-text.text-accent-4{color:#00b8d4!important}.teal{background-color:#009688!important}.teal-text{color:#009688!important}.teal.lighten-5{background-color:#e0f2f1!important}.teal-text.text-lighten-5{color:#e0f2f1!important}.teal.lighten-4{background-color:#b2dfdb!important}.teal-text.text-lighten-4{color:#b2dfdb!important}.teal.lighten-3{background-color:#80cbc4!important}.teal-text.text-lighten-3{color:#80cbc4!important}.teal.lighten-2{background-color:#4db6ac!important}.teal-text.text-lighten-2{color:#4db6ac!important}.teal.lighten-1{background-color:#26a69a!important}.teal-text.text-lighten-1{color:#26a69a!important}.teal.darken-1{background-color:#00897b!important}.teal-text.text-darken-1{color:#00897b!important}.teal.darken-2{background-color:#00796b!important}.teal-text.text-darken-2{color:#00796b!important}.teal.darken-3{background-color:#00695c!important}.teal-text.text-darken-3{color:#00695c!important}.teal.darken-4{background-color:#004d40!important}.teal-text.text-darken-4{color:#004d40!important}.teal.accent-1{background-color:#a7ffeb!important}.teal-text.text-accent-1{color:#a7ffeb!important}.teal.accent-2{background-color:#64ffda!important}.teal-text.text-accent-2{color:#64ffda!important}.teal.accent-3{background-color:#1de9b6!important}.teal-text.text-accent-3{color:#1de9b6!important}.teal.accent-4{background-color:#00bfa5!important}.teal-text.text-accent-4{color:#00bfa5!important}.green{background-color:#4caf50!important}.green-text{color:#4caf50!important}.green.lighten-5{background-color:#e8f5e9!important}.green-text.text-lighten-5{color:#e8f5e9!important}.green.lighten-4{background-color:#c8e6c9!important}.green-text.text-lighten-4{color:#c8e6c9!important}.green.lighten-3{background-color:#a5d6a7!important}.green-text.text-lighten-3{color:#a5d6a7!important}.green.lighten-2{background-color:#81c784!important}.green-text.text-lighten-2{color:#81c784!important}.green.lighten-1{background-color:#66bb6a!important}.green-text.text-lighten-1{color:#66bb6a!important}.green.darken-1{background-color:#43a047!important}.green-text.text-darken-1{color:#43a047!important}.green.darken-2{background-color:#388e3c!important}.green-text.text-darken-2{color:#388e3c!important}.green.darken-3{background-color:#2e7d32!important}.green-text.text-darken-3{color:#2e7d32!important}.green.darken-4{background-color:#1b5e20!important}.green-text.text-darken-4{color:#1b5e20!important}.green.accent-1{background-color:#b9f6ca!important}.green-text.text-accent-1{color:#b9f6ca!important}.green.accent-2{background-color:#69f0ae!important}.green-text.text-accent-2{color:#69f0ae!important}.green.accent-3{background-color:#00e676!important}.green-text.text-accent-3{color:#00e676!important}.green.accent-4{background-color:#00c853!important}.green-text.text-accent-4{color:#00c853!important}.light-green{background-color:#8bc34a!important}.light-green-text{color:#8bc34a!important}.light-green.lighten-5{background-color:#f1f8e9!important}.light-green-text.text-lighten-5{color:#f1f8e9!important}.light-green.lighten-4{background-color:#dcedc8!important}.light-green-text.text-lighten-4{color:#dcedc8!important}.light-green.lighten-3{background-color:#c5e1a5!important}.light-green-text.text-lighten-3{color:#c5e1a5!important}.light-green.lighten-2{background-color:#aed581!important}.light-green-text.text-lighten-2{color:#aed581!important}.light-green.lighten-1{background-color:#9ccc65!important}.light-green-text.text-lighten-1{color:#9ccc65!important}.light-green.darken-1{background-color:#7cb342!important}.light-green-text.text-darken-1{color:#7cb342!important}.light-green.darken-2{background-color:#689f38!important}.light-green-text.text-darken-2{color:#689f38!important}.light-green.darken-3{background-color:#558b2f!important}.light-green-text.text-darken-3{color:#558b2f!important}.light-green.darken-4{background-color:#33691e!important}.light-green-text.text-darken-4{color:#33691e!important}.light-green.accent-1{background-color:#ccff90!important}.light-green-text.text-accent-1{color:#ccff90!important}.light-green.accent-2{background-color:#b2ff59!important}.light-green-text.text-accent-2{color:#b2ff59!important}.light-green.accent-3{background-color:#76ff03!important}.light-green-text.text-accent-3{color:#76ff03!important}.light-green.accent-4{background-color:#64dd17!important}.light-green-text.text-accent-4{color:#64dd17!important}.lime{background-color:#cddc39!important}.lime-text{color:#cddc39!important}.lime.lighten-5{background-color:#f9fbe7!important}.lime-text.text-lighten-5{color:#f9fbe7!important}.lime.lighten-4{background-color:#f0f4c3!important}.lime-text.text-lighten-4{color:#f0f4c3!important}.lime.lighten-3{background-color:#e6ee9c!important}.lime-text.text-lighten-3{color:#e6ee9c!important}.lime.lighten-2{background-color:#dce775!important}.lime-text.text-lighten-2{color:#dce775!important}.lime.lighten-1{background-color:#d4e157!important}.lime-text.text-lighten-1{color:#d4e157!important}.lime.darken-1{background-color:#c0ca33!important}.lime-text.text-darken-1{color:#c0ca33!important}.lime.darken-2{background-color:#afb42b!important}.lime-text.text-darken-2{color:#afb42b!important}.lime.darken-3{background-color:#9e9d24!important}.lime-text.text-darken-3{color:#9e9d24!important}.lime.darken-4{background-color:#827717!important}.lime-text.text-darken-4{color:#827717!important}.lime.accent-1{background-color:#f4ff81!important}.lime-text.text-accent-1{color:#f4ff81!important}.lime.accent-2{background-color:#eeff41!important}.lime-text.text-accent-2{color:#eeff41!important}.lime.accent-3{background-color:#c6ff00!important}.lime-text.text-accent-3{color:#c6ff00!important}.lime.accent-4{background-color:#aeea00!important}.lime-text.text-accent-4{color:#aeea00!important}.yellow{background-color:#ffeb3b!important}.yellow-text{color:#ffeb3b!important}.yellow.lighten-5{background-color:#fffde7!important}.yellow-text.text-lighten-5{color:#fffde7!important}.yellow.lighten-4{background-color:#fff9c4!important}.yellow-text.text-lighten-4{color:#fff9c4!important}.yellow.lighten-3{background-color:#fff59d!important}.yellow-text.text-lighten-3{color:#fff59d!important}.yellow.lighten-2{background-color:#fff176!important}.yellow-text.text-lighten-2{color:#fff176!important}.yellow.lighten-1{background-color:#ffee58!important}.yellow-text.text-lighten-1{color:#ffee58!important}.yellow.darken-1{background-color:#fdd835!important}.yellow-text.text-darken-1{color:#fdd835!important}.yellow.darken-2{background-color:#fbc02d!important}.yellow-text.text-darken-2{color:#fbc02d!important}.yellow.darken-3{background-color:#f9a825!important}.yellow-text.text-darken-3{color:#f9a825!important}.yellow.darken-4{background-color:#f57f17!important}.yellow-text.text-darken-4{color:#f57f17!important}.yellow.accent-1{background-color:#ffff8d!important}.yellow-text.text-accent-1{color:#ffff8d!important}.yellow.accent-2{background-color:#ff0!important}.yellow-text.text-accent-2{color:#ff0!important}.yellow.accent-3{background-color:#ffea00!important}.yellow-text.text-accent-3{color:#ffea00!important}.yellow.accent-4{background-color:#ffd600!important}.yellow-text.text-accent-4{color:#ffd600!important}.amber{background-color:#ffc107!important}.amber-text{color:#ffc107!important}.amber.lighten-5{background-color:#fff8e1!important}.amber-text.text-lighten-5{color:#fff8e1!important}.amber.lighten-4{background-color:#ffecb3!important}.amber-text.text-lighten-4{color:#ffecb3!important}.amber.lighten-3{background-color:#ffe082!important}.amber-text.text-lighten-3{color:#ffe082!important}.amber.lighten-2{background-color:#ffd54f!important}.amber-text.text-lighten-2{color:#ffd54f!important}.amber.lighten-1{background-color:#ffca28!important}.amber-text.text-lighten-1{color:#ffca28!important}.amber.darken-1{background-color:#ffb300!important}.amber-text.text-darken-1{color:#ffb300!important}.amber.darken-2{background-color:#ffa000!important}.amber-text.text-darken-2{color:#ffa000!important}.amber.darken-3{background-color:#ff8f00!important}.amber-text.text-darken-3{color:#ff8f00!important}.amber.darken-4{background-color:#ff6f00!important}.amber-text.text-darken-4{color:#ff6f00!important}.amber.accent-1{background-color:#ffe57f!important}.amber-text.text-accent-1{color:#ffe57f!important}.amber.accent-2{background-color:#ffd740!important}.amber-text.text-accent-2{color:#ffd740!important}.amber.accent-3{background-color:#ffc400!important}.amber-text.text-accent-3{color:#ffc400!important}.amber.accent-4{background-color:#ffab00!important}.amber-text.text-accent-4{color:#ffab00!important}.orange{background-color:#ff9800!important}.orange-text{color:#ff9800!important}.orange.lighten-5{background-color:#fff3e0!important}.orange-text.text-lighten-5{color:#fff3e0!important}.orange.lighten-4{background-color:#ffe0b2!important}.orange-text.text-lighten-4{color:#ffe0b2!important}.orange.lighten-3{background-color:#ffcc80!important}.orange-text.text-lighten-3{color:#ffcc80!important}.orange.lighten-2{background-color:#ffb74d!important}.orange-text.text-lighten-2{color:#ffb74d!important}.orange.lighten-1{background-color:#ffa726!important}.orange-text.text-lighten-1{color:#ffa726!important}.orange.darken-1{background-color:#fb8c00!important}.orange-text.text-darken-1{color:#fb8c00!important}.orange.darken-2{background-color:#f57c00!important}.orange-text.text-darken-2{color:#f57c00!important}.orange.darken-3{background-color:#ef6c00!important}.orange-text.text-darken-3{color:#ef6c00!important}.orange.darken-4{background-color:#e65100!important}.orange-text.text-darken-4{color:#e65100!important}.orange.accent-1{background-color:#ffd180!important}.orange-text.text-accent-1{color:#ffd180!important}.orange.accent-2{background-color:#ffab40!important}.orange-text.text-accent-2{color:#ffab40!important}.orange.accent-3{background-color:#ff9100!important}.orange-text.text-accent-3{color:#ff9100!important}.orange.accent-4{background-color:#ff6d00!important}.orange-text.text-accent-4{color:#ff6d00!important}.deep-orange{background-color:#ff5722!important}.deep-orange-text{color:#ff5722!important}.deep-orange.lighten-5{background-color:#fbe9e7!important}.deep-orange-text.text-lighten-5{color:#fbe9e7!important}.deep-orange.lighten-4{background-color:#ffccbc!important}.deep-orange-text.text-lighten-4{color:#ffccbc!important}.deep-orange.lighten-3{background-color:#ffab91!important}.deep-orange-text.text-lighten-3{color:#ffab91!important}.deep-orange.lighten-2{background-color:#ff8a65!important}.deep-orange-text.text-lighten-2{color:#ff8a65!important}.deep-orange.lighten-1{background-color:#ff7043!important}.deep-orange-text.text-lighten-1{color:#ff7043!important}.deep-orange.darken-1{background-color:#f4511e!important}.deep-orange-text.text-darken-1{color:#f4511e!important}.deep-orange.darken-2{background-color:#e64a19!important}.deep-orange-text.text-darken-2{color:#e64a19!important}.deep-orange.darken-3{background-color:#d84315!important}.deep-orange-text.text-darken-3{color:#d84315!important}.deep-orange.darken-4{background-color:#bf360c!important}.deep-orange-text.text-darken-4{color:#bf360c!important}.deep-orange.accent-1{background-color:#ff9e80!important}.deep-orange-text.text-accent-1{color:#ff9e80!important}.deep-orange.accent-2{background-color:#ff6e40!important}.deep-orange-text.text-accent-2{color:#ff6e40!important}.deep-orange.accent-3{background-color:#ff3d00!important}.deep-orange-text.text-accent-3{color:#ff3d00!important}.deep-orange.accent-4{background-color:#dd2c00!important}.deep-orange-text.text-accent-4{color:#dd2c00!important}.brown{background-color:#795548!important}.brown-text{color:#795548!important}.brown.lighten-5{background-color:#efebe9!important}.brown-text.text-lighten-5{color:#efebe9!important}.brown.lighten-4{background-color:#d7ccc8!important}.brown-text.text-lighten-4{color:#d7ccc8!important}.brown.lighten-3{background-color:#bcaaa4!important}.brown-text.text-lighten-3{color:#bcaaa4!important}.brown.lighten-2{background-color:#a1887f!important}.brown-text.text-lighten-2{color:#a1887f!important}.brown.lighten-1{background-color:#8d6e63!important}.brown-text.text-lighten-1{color:#8d6e63!important}.brown.darken-1{background-color:#6d4c41!important}.brown-text.text-darken-1{color:#6d4c41!important}.brown.darken-2{background-color:#5d4037!important}.brown-text.text-darken-2{color:#5d4037!important}.brown.darken-3{background-color:#4e342e!important}.brown-text.text-darken-3{color:#4e342e!important}.brown.darken-4{background-color:#3e2723!important}.brown-text.text-darken-4{color:#3e2723!important}.blue-grey{background-color:#607d8b!important}.blue-grey-text{color:#607d8b!important}.blue-grey.lighten-5{background-color:#eceff1!important}.blue-grey-text.text-lighten-5{color:#eceff1!important}.blue-grey.lighten-4{background-color:#cfd8dc!important}.blue-grey-text.text-lighten-4{color:#cfd8dc!important}.blue-grey.lighten-3{background-color:#b0bec5!important}.blue-grey-text.text-lighten-3{color:#b0bec5!important}.blue-grey.lighten-2{background-color:#90a4ae!important}.blue-grey-text.text-lighten-2{color:#90a4ae!important}.blue-grey.lighten-1{background-color:#78909c!important}.blue-grey-text.text-lighten-1{color:#78909c!important}.blue-grey.darken-1{background-color:#546e7a!important}.blue-grey-text.text-darken-1{color:#546e7a!important}.blue-grey.darken-2{background-color:#455a64!important}.blue-grey-text.text-darken-2{color:#455a64!important}.blue-grey.darken-3{background-color:#37474f!important}.blue-grey-text.text-darken-3{color:#37474f!important}.blue-grey.darken-4{background-color:#263238!important}.blue-grey-text.text-darken-4{color:#263238!important}.grey{background-color:#9e9e9e!important}.grey-text{color:#9e9e9e!important}.grey.lighten-5{background-color:#fafafa!important}.grey-text.text-lighten-5{color:#fafafa!important}.grey.lighten-4{background-color:#f5f5f5!important}.grey-text.text-lighten-4{color:#f5f5f5!important}.grey.lighten-3{background-color:#eee!important}.grey-text.text-lighten-3{color:#eee!important}.grey.lighten-2{background-color:#e0e0e0!important}.grey-text.text-lighten-2{color:#e0e0e0!important}.grey.lighten-1{background-color:#bdbdbd!important}.grey-text.text-lighten-1{color:#bdbdbd!important}.grey.darken-1{background-color:#757575!important}.grey-text.text-darken-1{color:#757575!important}.grey.darken-2{background-color:#616161!important}.grey-text.text-darken-2{color:#616161!important}.grey.darken-3{background-color:#424242!important}.grey-text.text-darken-3{color:#424242!important}.grey.darken-4{background-color:#212121!important}.grey-text.text-darken-4{color:#212121!important}.black{background-color:#000!important}.black-text{color:#000!important}.white{background-color:#fff!important}.white-text{color:#fff!important}.transparent{background-color:transparent!important}.transparent-text{color:transparent!important}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}ul:not(.browser-default){padding-left:0;list-style-type:none}ul:not(.browser-default) li{list-style-type:none}a{color:#039be5;-webkit-tap-highlight-color:transparent}.valign-wrapper{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.clearfix{clear:both}.z-depth-0{box-shadow:none!important}.btn,.btn-floating,.btn-large,.card,.card-panel,.collapsible,.dropdown-content,.side-nav,.toast,.z-depth-1,nav{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2)}.btn-floating:hover,.btn-large:hover,.btn:hover,.z-depth-1-half{box-shadow:0 3px 3px 0 rgba(0,0,0,.14),0 1px 7px 0 rgba(0,0,0,.12),0 3px 1px -1px rgba(0,0,0,.2)}.z-depth-2{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.3)}.z-depth-3{box-shadow:0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12),0 3px 5px -1px rgba(0,0,0,.3)}.modal,.z-depth-4{box-shadow:0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.3)}.z-depth-5{box-shadow:0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.3)}.hoverable{transition:box-shadow .25s;box-shadow:0}.hoverable:hover{transition:box-shadow .25s;box-shadow:0 8px 17px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19)}.divider{height:1px;overflow:hidden;background-color:#e0e0e0}blockquote{margin:20px 0;padding-left:1.5rem;border-left:5px solid #ee6e73}i{line-height:inherit}i.left{float:left;margin-right:15px}i.right{float:right;margin-left:15px}i.tiny{font-size:1rem}i.small{font-size:2rem}i.medium{font-size:4rem}i.large{font-size:6rem}img.responsive-img,video.responsive-video{max-width:100%;height:auto}.pagination li{display:inline-block;border-radius:2px;text-align:center;vertical-align:top;height:30px}.pagination li a{color:#444;display:inline-block;font-size:1.2rem;padding:0 10px;line-height:30px}.pagination li.active a{color:#fff}.pagination li.active{background-color:#ee6e73}.pagination li.disabled a{cursor:default;color:#999}.pagination li i{font-size:2rem}.pagination li.pages ul li{display:inline-block;float:none}@media only screen and (max-width:992px){.pagination{width:100%}.pagination li.next,.pagination li.prev{width:10%}.pagination li.pages{width:80%;overflow:hidden;white-space:nowrap}}.breadcrumb{font-size:18px;color:hsla(0,0%,100%,.7)}.breadcrumb [class*=mdi-],.breadcrumb [class^=mdi-],.breadcrumb i,.breadcrumb i.material-icons{display:inline-block;float:left;font-size:24px}.breadcrumb:before{content:"\E5CC";color:hsla(0,0%,100%,.7);vertical-align:top;display:inline-block;font-family:Material Icons;font-weight:400;font-style:normal;font-size:25px;margin:0 10px 0 8px;-webkit-font-smoothing:antialiased}.breadcrumb:first-child:before{display:none}.breadcrumb:last-child{color:#fff}.parallax-container{position:relative;overflow:hidden;height:500px}.parallax{top:0;left:0;right:0;z-index:-1}.parallax,.parallax img{position:absolute;bottom:0}.parallax img{display:none;left:50%;min-width:100%;min-height:100%;-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-transform:translateX(-50%);transform:translateX(-50%)}.pin-bottom,.pin-top{position:relative}.pinned{position:fixed!important}.fade-in,ul.staggered-list li{opacity:0}.fade-in{-webkit-transform-origin:0 50%;transform-origin:0 50%}@media only screen and (max-width:600px){.hide-on-small-and-down,.hide-on-small-only{display:none!important}}@media only screen and (max-width:992px){.hide-on-med-and-down{display:none!important}}@media only screen and (min-width:601px){.hide-on-med-and-up{display:none!important}}@media only screen and (min-width:600px) and (max-width:992px){.hide-on-med-only{display:none!important}}@media only screen and (min-width:993px){.hide-on-large-only{display:none!important}}@media only screen and (min-width:993px){.show-on-large{display:block!important}}@media only screen and (min-width:600px) and (max-width:992px){.show-on-medium{display:block!important}}@media only screen and (max-width:600px){.show-on-small{display:block!important}}@media only screen and (min-width:601px){.show-on-medium-and-up{display:block!important}}@media only screen and (max-width:992px){.show-on-medium-and-down{display:block!important}}@media only screen and (max-width:600px){.center-on-small-only{text-align:center}}.page-footer{padding-top:20px;background-color:#ee6e73}.page-footer .footer-copyright{overflow:hidden;min-height:50px;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center;padding:10px 0;color:hsla(0,0%,100%,.8);background-color:rgba(51,51,51,.08)}table,td,th{border:none}table{width:100%;display:table}table.bordered>tbody>tr,table.bordered>thead>tr{border-bottom:1px solid #d0d0d0}table.striped>tbody>tr:nth-child(odd){background-color:#f2f2f2}table.striped>tbody>tr>td{border-radius:0}table.highlight>tbody>tr{transition:background-color .25s ease}table.highlight>tbody>tr:hover{background-color:#f2f2f2}table.centered tbody tr td,table.centered thead tr th{text-align:center}thead{border-bottom:1px solid #d0d0d0}td,th{padding:15px 5px;display:table-cell;text-align:left;vertical-align:middle;border-radius:2px}@media only screen and (max-width:992px){table.responsive-table{width:100%;border-collapse:collapse;border-spacing:0;display:block;position:relative}table.responsive-table td:empty:before{content:"\A0"}table.responsive-table td,table.responsive-table th{margin:0;vertical-align:top}table.responsive-table th{text-align:left}table.responsive-table thead{display:block;float:left}table.responsive-table thead tr{display:block;padding:0 10px 0 0}table.responsive-table thead tr th:before{content:"\A0"}table.responsive-table tbody{display:block;width:auto;position:relative;overflow-x:auto;white-space:nowrap}table.responsive-table tbody tr{display:inline-block;vertical-align:top}table.responsive-table th{display:block;text-align:right}table.responsive-table td{display:block;min-height:1.25em;text-align:left}table.responsive-table tr{padding:0 10px}table.responsive-table thead{border:0;border-right:1px solid #d0d0d0}table.responsive-table.bordered th{border-bottom:0;border-left:0}table.responsive-table.bordered td{border-left:0;border-right:0;border-bottom:0}table.responsive-table.bordered tr{border:0}table.responsive-table.bordered tbody tr{border-right:1px solid #d0d0d0}}.collection{margin:.5rem 0 1rem;border:1px solid #e0e0e0;border-radius:2px;overflow:hidden;position:relative}.collection .collection-item{background-color:#fff;line-height:1.5rem;padding:10px 20px;margin:0;border-bottom:1px solid #e0e0e0}.collection .collection-item.avatar{min-height:84px;padding-left:72px;position:relative}.collection .collection-item.avatar .circle{position:absolute;width:42px;height:42px;overflow:hidden;left:15px;display:inline-block;vertical-align:middle}.collection .collection-item.avatar i.circle{font-size:18px;line-height:42px;color:#fff;background-color:#999;text-align:center}.collection .collection-item.avatar .title{font-size:16px}.collection .collection-item.avatar p{margin:0}.collection .collection-item.avatar .secondary-content{position:absolute;top:16px;right:16px}.collection .collection-item:last-child{border-bottom:none}.collection .collection-item.active{background-color:#26a69a;color:#eafaf9}.collection .collection-item.active .secondary-content{color:#fff}.collection a.collection-item{display:block;transition:.25s;color:#26a69a}.collection a.collection-item:not(.active):hover{background-color:#ddd}.collection.with-header .collection-header{background-color:#fff;border-bottom:1px solid #e0e0e0;padding:10px 20px}.collection.with-header .collection-item{padding-left:30px}.collection.with-header .collection-item.avatar{padding-left:72px}.secondary-content{float:right;color:#26a69a}.collapsible .collection{margin:0;border:none}.video-container{position:relative;padding-bottom:56.25%;height:0;overflow:hidden}.video-container embed,.video-container iframe,.video-container object{position:absolute;top:0;left:0;width:100%;height:100%}.progress{position:relative;height:4px;display:block;width:100%;background-color:#acece6;border-radius:2px;margin:.5rem 0 1rem;overflow:hidden}.progress .determinate{position:absolute;top:0;left:0;bottom:0;transition:width .3s linear}.progress .determinate,.progress .indeterminate{background-color:#26a69a}.progress .indeterminate:before{-webkit-animation:indeterminate 2.1s cubic-bezier(.65,.815,.735,.395) infinite;animation:indeterminate 2.1s cubic-bezier(.65,.815,.735,.395) infinite}.progress .indeterminate:after,.progress .indeterminate:before{content:"";position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left,right}.progress .indeterminate:after{-webkit-animation:indeterminate-short 2.1s cubic-bezier(.165,.84,.44,1) infinite;animation:indeterminate-short 2.1s cubic-bezier(.165,.84,.44,1) infinite;-webkit-animation-delay:1.15s;animation-delay:1.15s}@-webkit-keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}to{left:100%;right:-90%}}@keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}to{left:100%;right:-90%}}@-webkit-keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}to{left:107%;right:-8%}}@keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}to{left:107%;right:-8%}}.hide{display:none!important}.left-align{text-align:left}.right-align{text-align:right}.center,.center-align{text-align:center}.left{float:left!important}.right{float:right!important}.no-select,input[type=range],input[type=range]+.thumb{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.circle{border-radius:50%}.center-block{display:block;margin-left:auto;margin-right:auto}.truncate{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.no-padding{padding:0!important}span.badge{min-width:3rem;padding:0 6px;margin-left:14px;text-align:center;font-size:1rem;line-height:22px;height:22px;color:#757575;float:right;box-sizing:border-box}span.badge.new{font-weight:300;font-size:.8rem;color:#fff;background-color:#26a69a;border-radius:2px}span.badge.new:after{content:" new"}span.badge[data-badge-caption]:after{content:" " attr(data-badge-caption)}nav ul a span.badge{display:inline-block;float:none;margin-left:4px;line-height:22px;height:22px}.collection-item span.badge{margin-top:calc(.75rem - 11px)}.collapsible span.badge{margin-top:calc(1.5rem - 11px)}.side-nav span.badge{margin-top:13px}.material-icons{text-rendering:optimizeLegibility;-webkit-font-feature-settings:"liga";-moz-font-feature-settings:"liga";font-feature-settings:"liga"}.container{margin:0 auto;max-width:1280px;width:90%}@media only screen and (min-width:601px){.container{width:85%}}@media only screen and (min-width:993px){.container{width:70%}}.container .row{margin-left:-.75rem;margin-right:-.75rem}.section{padding-top:1rem;padding-bottom:1rem}.section.no-pad{padding:0}.section.no-pad-bot{padding-bottom:0}.section.no-pad-top{padding-top:0}.row{margin-left:auto;margin-right:auto;margin-bottom:20px}.row:after{content:"";display:table;clear:both}.row .col{float:left;box-sizing:border-box;padding:0 .75rem;min-height:1px}.row .col[class*=pull-],.row .col[class*=push-]{position:relative}.row .col.s1{width:8.3333333333%}.row .col.s1,.row .col.s2{margin-left:auto;left:auto;right:auto}.row .col.s2{width:16.6666666667%}.row .col.s3{width:25%}.row .col.s3,.row .col.s4{margin-left:auto;left:auto;right:auto}.row .col.s4{width:33.3333333333%}.row .col.s5{width:41.6666666667%}.row .col.s5,.row .col.s6{margin-left:auto;left:auto;right:auto}.row .col.s6{width:50%}.row .col.s7{width:58.3333333333%}.row .col.s7,.row .col.s8{margin-left:auto;left:auto;right:auto}.row .col.s8{width:66.6666666667%}.row .col.s9{width:75%}.row .col.s9,.row .col.s10{margin-left:auto;left:auto;right:auto}.row .col.s10{width:83.3333333333%}.row .col.s11{width:91.6666666667%}.row .col.s11,.row .col.s12{margin-left:auto;left:auto;right:auto}.row .col.s12{width:100%}.row .col.offset-s1{margin-left:8.3333333333%}.row .col.pull-s1{right:8.3333333333%}.row .col.push-s1{left:8.3333333333%}.row .col.offset-s2{margin-left:16.6666666667%}.row .col.pull-s2{right:16.6666666667%}.row .col.push-s2{left:16.6666666667%}.row .col.offset-s3{margin-left:25%}.row .col.pull-s3{right:25%}.row .col.push-s3{left:25%}.row .col.offset-s4{margin-left:33.3333333333%}.row .col.pull-s4{right:33.3333333333%}.row .col.push-s4{left:33.3333333333%}.row .col.offset-s5{margin-left:41.6666666667%}.row .col.pull-s5{right:41.6666666667%}.row .col.push-s5{left:41.6666666667%}.row .col.offset-s6{margin-left:50%}.row .col.pull-s6{right:50%}.row .col.push-s6{left:50%}.row .col.offset-s7{margin-left:58.3333333333%}.row .col.pull-s7{right:58.3333333333%}.row .col.push-s7{left:58.3333333333%}.row .col.offset-s8{margin-left:66.6666666667%}.row .col.pull-s8{right:66.6666666667%}.row .col.push-s8{left:66.6666666667%}.row .col.offset-s9{margin-left:75%}.row .col.pull-s9{right:75%}.row .col.push-s9{left:75%}.row .col.offset-s10{margin-left:83.3333333333%}.row .col.pull-s10{right:83.3333333333%}.row .col.push-s10{left:83.3333333333%}.row .col.offset-s11{margin-left:91.6666666667%}.row .col.pull-s11{right:91.6666666667%}.row .col.push-s11{left:91.6666666667%}.row .col.offset-s12{margin-left:100%}.row .col.pull-s12{right:100%}.row .col.push-s12{left:100%}@media only screen and (min-width:601px){.row .col.m1{width:8.3333333333%}.row .col.m1,.row .col.m2{margin-left:auto;left:auto;right:auto}.row .col.m2{width:16.6666666667%}.row .col.m3{width:25%}.row .col.m3,.row .col.m4{margin-left:auto;left:auto;right:auto}.row .col.m4{width:33.3333333333%}.row .col.m5{width:41.6666666667%}.row .col.m5,.row .col.m6{margin-left:auto;left:auto;right:auto}.row .col.m6{width:50%}.row .col.m7{width:58.3333333333%}.row .col.m7,.row .col.m8{margin-left:auto;left:auto;right:auto}.row .col.m8{width:66.6666666667%}.row .col.m9{width:75%}.row .col.m9,.row .col.m10{margin-left:auto;left:auto;right:auto}.row .col.m10{width:83.3333333333%}.row .col.m11{width:91.6666666667%}.row .col.m11,.row .col.m12{margin-left:auto;left:auto;right:auto}.row .col.m12{width:100%}.row .col.offset-m1{margin-left:8.3333333333%}.row .col.pull-m1{right:8.3333333333%}.row .col.push-m1{left:8.3333333333%}.row .col.offset-m2{margin-left:16.6666666667%}.row .col.pull-m2{right:16.6666666667%}.row .col.push-m2{left:16.6666666667%}.row .col.offset-m3{margin-left:25%}.row .col.pull-m3{right:25%}.row .col.push-m3{left:25%}.row .col.offset-m4{margin-left:33.3333333333%}.row .col.pull-m4{right:33.3333333333%}.row .col.push-m4{left:33.3333333333%}.row .col.offset-m5{margin-left:41.6666666667%}.row .col.pull-m5{right:41.6666666667%}.row .col.push-m5{left:41.6666666667%}.row .col.offset-m6{margin-left:50%}.row .col.pull-m6{right:50%}.row .col.push-m6{left:50%}.row .col.offset-m7{margin-left:58.3333333333%}.row .col.pull-m7{right:58.3333333333%}.row .col.push-m7{left:58.3333333333%}.row .col.offset-m8{margin-left:66.6666666667%}.row .col.pull-m8{right:66.6666666667%}.row .col.push-m8{left:66.6666666667%}.row .col.offset-m9{margin-left:75%}.row .col.pull-m9{right:75%}.row .col.push-m9{left:75%}.row .col.offset-m10{margin-left:83.3333333333%}.row .col.pull-m10{right:83.3333333333%}.row .col.push-m10{left:83.3333333333%}.row .col.offset-m11{margin-left:91.6666666667%}.row .col.pull-m11{right:91.6666666667%}.row .col.push-m11{left:91.6666666667%}.row .col.offset-m12{margin-left:100%}.row .col.pull-m12{right:100%}.row .col.push-m12{left:100%}}@media only screen and (min-width:993px){.row .col.l1{width:8.3333333333%}.row .col.l1,.row .col.l2{margin-left:auto;left:auto;right:auto}.row .col.l2{width:16.6666666667%}.row .col.l3{width:25%}.row .col.l3,.row .col.l4{margin-left:auto;left:auto;right:auto}.row .col.l4{width:33.3333333333%}.row .col.l5{width:41.6666666667%}.row .col.l5,.row .col.l6{margin-left:auto;left:auto;right:auto}.row .col.l6{width:50%}.row .col.l7{width:58.3333333333%}.row .col.l7,.row .col.l8{margin-left:auto;left:auto;right:auto}.row .col.l8{width:66.6666666667%}.row .col.l9{width:75%}.row .col.l9,.row .col.l10{margin-left:auto;left:auto;right:auto}.row .col.l10{width:83.3333333333%}.row .col.l11{width:91.6666666667%}.row .col.l11,.row .col.l12{margin-left:auto;left:auto;right:auto}.row .col.l12{width:100%}.row .col.offset-l1{margin-left:8.3333333333%}.row .col.pull-l1{right:8.3333333333%}.row .col.push-l1{left:8.3333333333%}.row .col.offset-l2{margin-left:16.6666666667%}.row .col.pull-l2{right:16.6666666667%}.row .col.push-l2{left:16.6666666667%}.row .col.offset-l3{margin-left:25%}.row .col.pull-l3{right:25%}.row .col.push-l3{left:25%}.row .col.offset-l4{margin-left:33.3333333333%}.row .col.pull-l4{right:33.3333333333%}.row .col.push-l4{left:33.3333333333%}.row .col.offset-l5{margin-left:41.6666666667%}.row .col.pull-l5{right:41.6666666667%}.row .col.push-l5{left:41.6666666667%}.row .col.offset-l6{margin-left:50%}.row .col.pull-l6{right:50%}.row .col.push-l6{left:50%}.row .col.offset-l7{margin-left:58.3333333333%}.row .col.pull-l7{right:58.3333333333%}.row .col.push-l7{left:58.3333333333%}.row .col.offset-l8{margin-left:66.6666666667%}.row .col.pull-l8{right:66.6666666667%}.row .col.push-l8{left:66.6666666667%}.row .col.offset-l9{margin-left:75%}.row .col.pull-l9{right:75%}.row .col.push-l9{left:75%}.row .col.offset-l10{margin-left:83.3333333333%}.row .col.pull-l10{right:83.3333333333%}.row .col.push-l10{left:83.3333333333%}.row .col.offset-l11{margin-left:91.6666666667%}.row .col.pull-l11{right:91.6666666667%}.row .col.push-l11{left:91.6666666667%}.row .col.offset-l12{margin-left:100%}.row .col.pull-l12{right:100%}.row .col.push-l12{left:100%}}@media only screen and (min-width:1201px){.row .col.xl1{width:8.3333333333%}.row .col.xl1,.row .col.xl2{margin-left:auto;left:auto;right:auto}.row .col.xl2{width:16.6666666667%}.row .col.xl3{width:25%}.row .col.xl3,.row .col.xl4{margin-left:auto;left:auto;right:auto}.row .col.xl4{width:33.3333333333%}.row .col.xl5{width:41.6666666667%}.row .col.xl5,.row .col.xl6{margin-left:auto;left:auto;right:auto}.row .col.xl6{width:50%}.row .col.xl7{width:58.3333333333%}.row .col.xl7,.row .col.xl8{margin-left:auto;left:auto;right:auto}.row .col.xl8{width:66.6666666667%}.row .col.xl9{width:75%}.row .col.xl9,.row .col.xl10{margin-left:auto;left:auto;right:auto}.row .col.xl10{width:83.3333333333%}.row .col.xl11{width:91.6666666667%}.row .col.xl11,.row .col.xl12{margin-left:auto;left:auto;right:auto}.row .col.xl12{width:100%}.row .col.offset-xl1{margin-left:8.3333333333%}.row .col.pull-xl1{right:8.3333333333%}.row .col.push-xl1{left:8.3333333333%}.row .col.offset-xl2{margin-left:16.6666666667%}.row .col.pull-xl2{right:16.6666666667%}.row .col.push-xl2{left:16.6666666667%}.row .col.offset-xl3{margin-left:25%}.row .col.pull-xl3{right:25%}.row .col.push-xl3{left:25%}.row .col.offset-xl4{margin-left:33.3333333333%}.row .col.pull-xl4{right:33.3333333333%}.row .col.push-xl4{left:33.3333333333%}.row .col.offset-xl5{margin-left:41.6666666667%}.row .col.pull-xl5{right:41.6666666667%}.row .col.push-xl5{left:41.6666666667%}.row .col.offset-xl6{margin-left:50%}.row .col.pull-xl6{right:50%}.row .col.push-xl6{left:50%}.row .col.offset-xl7{margin-left:58.3333333333%}.row .col.pull-xl7{right:58.3333333333%}.row .col.push-xl7{left:58.3333333333%}.row .col.offset-xl8{margin-left:66.6666666667%}.row .col.pull-xl8{right:66.6666666667%}.row .col.push-xl8{left:66.6666666667%}.row .col.offset-xl9{margin-left:75%}.row .col.pull-xl9{right:75%}.row .col.push-xl9{left:75%}.row .col.offset-xl10{margin-left:83.3333333333%}.row .col.pull-xl10{right:83.3333333333%}.row .col.push-xl10{left:83.3333333333%}.row .col.offset-xl11{margin-left:91.6666666667%}.row .col.pull-xl11{right:91.6666666667%}.row .col.push-xl11{left:91.6666666667%}.row .col.offset-xl12{margin-left:100%}.row .col.pull-xl12{right:100%}.row .col.push-xl12{left:100%}}nav{color:#fff;background-color:#ee6e73;width:100%;height:56px;line-height:56px}nav.nav-extended{height:auto}nav.nav-extended .nav-wrapper{min-height:56px;height:auto}nav.nav-extended .nav-content{position:relative;line-height:normal}nav a{color:#fff}nav [class*=mdi-],nav [class^=mdi-],nav i,nav i.material-icons{display:block;font-size:24px;height:56px;line-height:56px}nav .nav-wrapper{position:relative;height:100%}@media only screen and (min-width:993px){nav a.button-collapse{display:none}}nav .button-collapse{float:left;position:relative;z-index:1;height:56px;margin:0 18px}nav .button-collapse i{height:56px;line-height:56px}nav .brand-logo{position:absolute;color:#fff;display:inline-block;font-size:2.1rem;padding:0;white-space:nowrap}nav .brand-logo.center{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}@media only screen and (max-width:992px){nav .brand-logo{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}nav .brand-logo.left,nav .brand-logo.right{padding:0;-webkit-transform:none;transform:none}nav .brand-logo.left{left:.5rem}nav .brand-logo.right{right:.5rem;left:auto}}nav .brand-logo.right{right:.5rem;padding:0}nav .brand-logo [class*=mdi-],nav .brand-logo [class^=mdi-],nav .brand-logo i,nav .brand-logo i.material-icons{float:left;margin-right:15px}nav .nav-title{display:inline-block;font-size:32px;padding:28px 0}nav ul{margin:0}nav ul li{transition:background-color .3s;float:left;padding:0}nav ul li.active{background-color:rgba(0,0,0,.1)}nav ul a{transition:background-color .3s;font-size:1rem;color:#fff;display:block;padding:0 15px;cursor:pointer}nav ul a.btn,nav ul a.btn-flat,nav ul a.btn-floating,nav ul a.btn-large{margin-top:-2px;margin-left:15px;margin-right:15px}nav ul a.btn-flat>.material-icons,nav ul a.btn-floating>.material-icons,nav ul a.btn-large>.material-icons,nav ul a.btn>.material-icons{height:inherit;line-height:inherit}nav ul a:hover{background-color:rgba(0,0,0,.1)}nav ul.left{float:left}nav form{height:100%}nav .input-field{margin:0;height:100%}nav .input-field input{height:100%;font-size:1.2rem;border:none;padding-left:2rem}nav .input-field input:focus,nav .input-field input[type=date]:valid,nav .input-field input[type=email]:valid,nav .input-field input[type=password]:valid,nav .input-field input[type=text]:valid,nav .input-field input[type=url]:valid{border:none;box-shadow:none}nav .input-field label{top:0;left:0}nav .input-field label i{color:hsla(0,0%,100%,.7);transition:color .3s}nav .input-field label.active i{color:#fff}.navbar-fixed{position:relative;height:56px;z-index:997}.navbar-fixed nav{position:fixed}@media only screen and (min-width:601px){nav.nav-extended .nav-wrapper{min-height:64px}nav,nav .nav-wrapper i,nav a.button-collapse,nav a.button-collapse i{height:64px;line-height:64px}.navbar-fixed{height:64px}}@font-face{font-family:Roboto;src:local(Roboto Thin),url(fonts/Roboto-Thin.woff2) format("woff2"),url(fonts/Roboto-Thin.woff) format("woff");font-weight:100}@font-face{font-family:Roboto;src:local(Roboto Light),url(fonts/Roboto-Light.woff2) format("woff2"),url(fonts/Roboto-Light.woff) format("woff");font-weight:300}@font-face{font-family:Roboto;src:local(Roboto Regular),url(fonts/Roboto-Regular.woff2) format("woff2"),url(fonts/Roboto-Regular.woff) format("woff");font-weight:400}@font-face{font-family:Roboto;src:local(Roboto Medium),url(fonts/Roboto-Medium.woff2) format("woff2"),url(fonts/Roboto-Medium.woff) format("woff");font-weight:500}@font-face{font-family:Roboto;src:local(Roboto Bold),url(fonts/Roboto-Bold.woff2) format("woff2"),url(fonts/Roboto-Bold.woff) format("woff");font-weight:700}a{text-decoration:none}html{line-height:1.5;font-family:Roboto,sans-serif;font-weight:400;color:rgba(0,0,0,.87)}@media only screen and (min-width:0){html{font-size:14px}}@media only screen and (min-width:992px){html{font-size:14.5px}}@media only screen and (min-width:1200px){html{font-size:15px}}h1,h2,h3,h4,h5,h6{font-weight:400;line-height:1.1}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{font-weight:inherit}h1{font-size:4.2rem;margin:2.1rem 0 1.68rem}h1,h2{line-height:110%}h2{font-size:3.56rem;margin:1.78rem 0 1.424rem}h3{font-size:2.92rem;margin:1.46rem 0 1.168rem}h3,h4{line-height:110%}h4{font-size:2.28rem;margin:1.14rem 0 .912rem}h5{font-size:1.64rem;margin:.82rem 0 .656rem}h5,h6{line-height:110%}h6{font-size:1rem;margin:.5rem 0 .4rem}em{font-style:italic}strong{font-weight:500}small{font-size:75%}.light,.page-footer .footer-copyright{font-weight:300}.thin{font-weight:200}.flow-text{font-weight:300}@media only screen and (min-width:360px){.flow-text{font-size:1.2rem}}@media only screen and (min-width:390px){.flow-text{font-size:1.224rem}}@media only screen and (min-width:420px){.flow-text{font-size:1.248rem}}@media only screen and (min-width:450px){.flow-text{font-size:1.272rem}}@media only screen and (min-width:480px){.flow-text{font-size:1.296rem}}@media only screen and (min-width:510px){.flow-text{font-size:1.32rem}}@media only screen and (min-width:540px){.flow-text{font-size:1.344rem}}@media only screen and (min-width:570px){.flow-text{font-size:1.368rem}}@media only screen and (min-width:600px){.flow-text{font-size:1.392rem}}@media only screen and (min-width:630px){.flow-text{font-size:1.416rem}}@media only screen and (min-width:660px){.flow-text{font-size:1.44rem}}@media only screen and (min-width:690px){.flow-text{font-size:1.464rem}}@media only screen and (min-width:720px){.flow-text{font-size:1.488rem}}@media only screen and (min-width:750px){.flow-text{font-size:1.512rem}}@media only screen and (min-width:780px){.flow-text{font-size:1.536rem}}@media only screen and (min-width:810px){.flow-text{font-size:1.56rem}}@media only screen and (min-width:840px){.flow-text{font-size:1.584rem}}@media only screen and (min-width:870px){.flow-text{font-size:1.608rem}}@media only screen and (min-width:900px){.flow-text{font-size:1.632rem}}@media only screen and (min-width:930px){.flow-text{font-size:1.656rem}}@media only screen and (min-width:960px){.flow-text{font-size:1.68rem}}@media only screen and (max-width:360px){.flow-text{font-size:1.2rem}}.scale-transition{transition:-webkit-transform .3s cubic-bezier(.53,.01,.36,1.63)!important;transition:transform .3s cubic-bezier(.53,.01,.36,1.63)!important;transition:transform .3s cubic-bezier(.53,.01,.36,1.63),-webkit-transform .3s cubic-bezier(.53,.01,.36,1.63)!important}.scale-transition.scale-out{-webkit-transform:scale(0);transform:scale(0);transition:-webkit-transform .2s!important;transition:transform .2s!important;transition:transform .2s,-webkit-transform .2s!important}.scale-transition.scale-in{-webkit-transform:scale(1);transform:scale(1)}.card-panel{padding:24px}.card,.card-panel{transition:box-shadow .25s;margin:.5rem 0 1rem;border-radius:2px;background-color:#fff}.card{position:relative}.card .card-title{font-size:24px;font-weight:300}.card .card-title.activator{cursor:pointer}.card.large,.card.medium,.card.small{position:relative}.card.large .card-image,.card.medium .card-image,.card.small .card-image{max-height:60%;overflow:hidden}.card.large .card-image+.card-content,.card.medium .card-image+.card-content,.card.small .card-image+.card-content{max-height:40%}.card.large .card-content,.card.medium .card-content,.card.small .card-content{max-height:100%;overflow:hidden}.card.large .card-action,.card.medium .card-action,.card.small .card-action{position:absolute;bottom:0;left:0;right:0}.card.small{height:300px}.card.medium{height:400px}.card.large{height:500px}.card.horizontal{display:-webkit-flex;display:-ms-flexbox;display:flex}.card.horizontal.large .card-image,.card.horizontal.medium .card-image,.card.horizontal.small .card-image{height:100%;max-height:none;overflow:visible}.card.horizontal.large .card-image img,.card.horizontal.medium .card-image img,.card.horizontal.small .card-image img{height:100%}.card.horizontal .card-image{max-width:50%}.card.horizontal .card-image img{border-radius:2px 0 0 2px;max-width:100%;width:auto}.card.horizontal .card-stacked{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex:1;-ms-flex:1;flex:1;position:relative}.card.horizontal .card-stacked .card-content{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.card.sticky-action .card-action{z-index:2}.card.sticky-action .card-reveal{z-index:1;padding-bottom:64px}.card .card-image{position:relative}.card .card-image img{display:block;border-radius:2px 2px 0 0;position:relative;left:0;right:0;top:0;bottom:0;width:100%}.card .card-image .card-title{color:#fff;position:absolute;bottom:0;left:0;max-width:100%;padding:24px}.card .card-content{padding:24px;border-radius:0 0 2px 2px}.card .card-content p{margin:0;color:inherit}.card .card-content .card-title{display:block;line-height:32px;margin-bottom:8px}.card .card-content .card-title i{line-height:32px}.card .card-action{position:relative;background-color:inherit;border-top:1px solid hsla(0,0%,63%,.2);padding:16px 24px}.card .card-action:last-child{border-radius:0 0 2px 2px}.card .card-action a:not(.btn):not(.btn-large):not(.btn-large):not(.btn-floating){color:#ffab40;margin-right:24px;transition:color .3s ease;text-transform:uppercase}.card .card-action a:not(.btn):not(.btn-large):not(.btn-large):not(.btn-floating):hover{color:#ffd8a6}.card .card-reveal{padding:24px;position:absolute;background-color:#fff;width:100%;overflow-y:auto;left:0;top:100%;height:100%;z-index:3;display:none}.card .card-reveal .card-title{cursor:pointer;display:block}#toast-container{display:block;position:fixed;z-index:10000}@media only screen and (max-width:600px){#toast-container{min-width:100%;bottom:0}}@media only screen and (min-width:601px) and (max-width:992px){#toast-container{left:5%;bottom:7%;max-width:90%}}@media only screen and (min-width:993px){#toast-container{top:10%;right:7%;max-width:86%}}.toast{border-radius:2px;top:35px;width:auto;clear:both;margin-top:10px;position:relative;max-width:100%;height:auto;min-height:48px;line-height:1.5em;word-break:break-all;background-color:#323232;padding:10px 25px;font-size:1.1rem;font-weight:300;color:#fff;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.toast .btn,.toast .btn-flat,.toast .btn-large{margin:0;margin-left:3rem}.toast.rounded{border-radius:24px}@media only screen and (max-width:600px){.toast{width:100%;border-radius:0}}@media only screen and (min-width:601px) and (max-width:992px){.toast{float:left}}@media only screen and (min-width:993px){.toast{float:right}}.tabs{position:relative;overflow-x:auto;overflow-y:hidden;height:48px;width:100%;background-color:#fff;margin:0 auto;white-space:nowrap}.tabs.tabs-transparent{background-color:transparent}.tabs.tabs-transparent .tab.disabled a,.tabs.tabs-transparent .tab.disabled a:hover,.tabs.tabs-transparent .tab a{color:hsla(0,0%,100%,.7)}.tabs.tabs-transparent .tab a.active,.tabs.tabs-transparent .tab a:hover{color:#fff}.tabs.tabs-transparent .indicator{background-color:#fff}.tabs.tabs-fixed-width{display:-webkit-flex;display:-ms-flexbox;display:flex}.tabs.tabs-fixed-width .tab{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.tabs .tab{display:inline-block;text-align:center;line-height:48px;height:48px;padding:0;margin:0;text-transform:uppercase}.tabs .tab a{color:rgba(238,110,115,.7);display:block;width:100%;height:100%;padding:0 24px;font-size:14px;text-overflow:ellipsis;overflow:hidden;transition:color .28s ease}.tabs .tab a.active,.tabs .tab a:hover{background-color:transparent;color:#ee6e73}.tabs .tab.disabled a,.tabs .tab.disabled a:hover{color:rgba(238,110,115,.7);cursor:default}.tabs .indicator{position:absolute;bottom:0;height:2px;background-color:#f6b2b5;will-change:left,right}@media only screen and (max-width:992px){.tabs{display:-webkit-flex;display:-ms-flexbox;display:flex}.tabs .tab{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.tabs .tab a{padding:0 12px}}.material-tooltip{padding:10px 8px;font-size:1rem;z-index:2000;background-color:transparent;border-radius:2px;color:#fff;min-height:36px;line-height:120%;text-align:center;max-width:calc(100% - 4px);overflow:hidden;left:0;top:0;pointer-events:none}.backdrop,.material-tooltip{opacity:0;position:absolute;visibility:hidden}.backdrop{height:7px;width:14px;border-radius:0 0 50% 50%;background-color:#323232;z-index:-1;-webkit-transform-origin:50% 0;transform-origin:50% 0}.btn,.btn-flat,.btn-large{border:none;border-radius:2px;display:inline-block;height:36px;line-height:36px;padding:0 2rem;text-transform:uppercase;vertical-align:middle;-webkit-tap-highlight-color:transparent}.btn-flat.disabled,.btn-flat:disabled,.btn-flat[disabled],.btn-floating.disabled,.btn-floating:disabled,.btn-floating[disabled],.btn-large.disabled,.btn-large:disabled,.btn-large[disabled],.btn.disabled,.btn:disabled,.btn[disabled],.disabled.btn-large,[disabled].btn-large{pointer-events:none;background-color:#dfdfdf!important;box-shadow:none;color:#9f9f9f!important;cursor:default}.btn-flat.disabled:hover,.btn-flat:disabled:hover,.btn-flat[disabled]:hover,.btn-floating.disabled:hover,.btn-floating:disabled:hover,.btn-floating[disabled]:hover,.btn-large.disabled:hover,.btn-large:disabled:hover,.btn-large[disabled]:hover,.btn.disabled:hover,.btn:disabled:hover,.btn[disabled]:hover,.disabled.btn-large:hover,[disabled].btn-large:hover{background-color:#dfdfdf!important;color:#9f9f9f!important}.btn,.btn-flat,.btn-floating,.btn-large{font-size:1rem;outline:0}.btn-flat i,.btn-floating i,.btn-large i,.btn i{font-size:1.3rem;line-height:inherit}.btn-floating:focus,.btn-large:focus,.btn:focus{background-color:#1d7d74}.btn,.btn-large{text-decoration:none;color:#fff;background-color:#26a69a;text-align:center;letter-spacing:.5px;transition:.2s ease-out;cursor:pointer}.btn-large:hover,.btn:hover{background-color:#2bbbad}.btn-floating{display:inline-block;color:#fff;position:relative;overflow:hidden;z-index:1;width:40px;height:40px;line-height:40px;padding:0;border-radius:50%;transition:.3s;cursor:pointer;vertical-align:middle}.btn-floating,.btn-floating:hover{background-color:#26a69a}.btn-floating:before{border-radius:0}.btn-floating.btn-large{width:56px;height:56px}.btn-floating.btn-large.halfway-fab{bottom:-28px}.btn-floating.btn-large i{line-height:56px}.btn-floating.halfway-fab{position:absolute;right:24px;bottom:-20px}.btn-floating.halfway-fab.left{right:auto;left:24px}.btn-floating i{width:inherit;display:inline-block;text-align:center;color:#fff;font-size:1.6rem;line-height:40px}button.btn-floating{border:none}.fixed-action-btn{position:fixed;right:23px;bottom:23px;padding-top:15px;margin-bottom:0;z-index:998}.fixed-action-btn.active ul{visibility:visible}.fixed-action-btn.horizontal{padding:0 0 0 15px}.fixed-action-btn.horizontal ul{text-align:right;right:64px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);height:100%;left:auto;width:500px}.fixed-action-btn.horizontal ul li{display:inline-block;margin:15px 15px 0 0}.fixed-action-btn.toolbar{padding:0;height:56px}.fixed-action-btn.toolbar.active>a i{opacity:0}.fixed-action-btn.toolbar ul{display:-webkit-flex;display:-ms-flexbox;display:flex;top:0;bottom:0}.fixed-action-btn.toolbar ul li{-webkit-flex:1;-ms-flex:1;flex:1;display:inline-block;margin:0;height:100%;transition:none}.fixed-action-btn.toolbar ul li a{display:block;overflow:hidden;position:relative;width:100%;height:100%;background-color:transparent;box-shadow:none;color:#fff;line-height:56px;z-index:1}.fixed-action-btn.toolbar ul li a i{line-height:inherit}.fixed-action-btn ul{left:0;right:0;text-align:center;position:absolute;bottom:64px;margin:0;visibility:hidden}.fixed-action-btn ul li{margin-bottom:15px}.fixed-action-btn ul a.btn-floating{opacity:0}.fixed-action-btn .fab-backdrop{position:absolute;top:0;left:0;z-index:-1;width:40px;height:40px;background-color:#26a69a;border-radius:50%;-webkit-transform:scale(0);transform:scale(0)}.btn-flat{box-shadow:none;color:#343434;cursor:pointer;transition:background-color .2s}.btn-flat,.btn-flat:active,.btn-flat:focus{background-color:transparent}.btn-flat:focus,.btn-flat:hover{background-color:rgba(0,0,0,.1);box-shadow:none}.btn-flat:active{background-color:rgba(0,0,0,.2)}.btn-flat.disabled{background-color:transparent!important;color:#b3b3b3!important;cursor:default}.btn-large{height:54px;line-height:54px}.btn-large i{font-size:1.6rem}.btn-block{display:block}.dropdown-content{background-color:#fff;margin:0;display:none;min-width:100px;max-height:650px;overflow-y:auto;opacity:0;position:absolute;z-index:999;will-change:width,height}.dropdown-content li{clear:both;color:rgba(0,0,0,.87);cursor:pointer;min-height:50px;line-height:1.5rem;width:100%;text-align:left;text-transform:none}.dropdown-content li.active,.dropdown-content li.selected,.dropdown-content li:hover{background-color:#eee}.dropdown-content li.active.selected{background-color:#e1e1e1}.dropdown-content li.divider{min-height:0;height:1px}.dropdown-content li>a,.dropdown-content li>span{font-size:16px;color:#26a69a;display:block;line-height:22px;padding:14px 16px}.dropdown-content li>span>label{top:1px;left:0;height:18px}.dropdown-content li>a>i{height:inherit;line-height:inherit;float:left;margin:0 24px 0 0;width:24px}.input-field.col .dropdown-content [type=checkbox]+label{top:1px;left:0;height:18px}.waves-effect{position:relative;cursor:pointer;display:inline-block;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;vertical-align:middle;z-index:1;transition:.3s ease-out}.waves-effect .waves-ripple{position:absolute;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;opacity:0;background:rgba(0,0,0,.2);transition:all .7s ease-out;transition-property:opacity,-webkit-transform;transition-property:transform,opacity;transition-property:transform,opacity,-webkit-transform;-webkit-transform:scale(0);transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:hsla(0,0%,100%,.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,.7)}.waves-effect input[type=button],.waves-effect input[type=reset],.waves-effect input[type=submit]{border:0;font-style:normal;font-size:inherit;text-transform:inherit;background:none}.waves-effect img{position:relative;z-index:-1}.waves-notransition{transition:none!important}.waves-circle{-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle,#fff 100%,#000 0)}.waves-input-wrapper{border-radius:.2em;vertical-align:bottom}.waves-input-wrapper .waves-button-input{position:relative;top:0;left:0;z-index:1}.waves-circle{text-align:center;width:2.5em;height:2.5em;line-height:2.5em;border-radius:50%;-webkit-mask-image:none}.waves-block{display:block}.waves-effect .waves-ripple{z-index:-1}.modal{display:none;position:fixed;left:0;right:0;background-color:#fafafa;padding:0;max-height:70%;width:55%;margin:auto;overflow-y:auto;border-radius:2px;will-change:top,opacity}@media only screen and (max-width:992px){.modal{width:80%}}.modal h1,.modal h2,.modal h3,.modal h4{margin-top:0}.modal .modal-content{padding:24px}.modal .modal-close{cursor:pointer}.modal .modal-footer{border-radius:0 0 2px 2px;background-color:#fafafa;padding:4px 6px;height:56px;width:100%}.modal .modal-footer .btn,.modal .modal-footer .btn-flat,.modal .modal-footer .btn-large{float:right;margin:6px 0}.modal-overlay{position:fixed;z-index:999;top:-100px;left:0;bottom:0;right:0;height:125%;width:100%;background:#000;display:none;will-change:opacity}.modal.modal-fixed-footer{padding:0;height:70%}.modal.modal-fixed-footer .modal-content{position:absolute;height:calc(100% - 56px);max-height:100%;width:100%;overflow-y:auto}.modal.modal-fixed-footer .modal-footer{border-top:1px solid rgba(0,0,0,.1);position:absolute;bottom:0}.modal.bottom-sheet{top:auto;bottom:-100%;margin:0;width:100%;max-height:45%;border-radius:0;will-change:bottom,opacity}.collapsible{border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;margin:.5rem 0 1rem}.collapsible-header{display:block;cursor:pointer;min-height:3rem;line-height:3rem;padding:0 1rem;background-color:#fff;border-bottom:1px solid #ddd}.collapsible-header i{width:2rem;font-size:1.6rem;line-height:3rem;display:block;float:left;text-align:center;margin-right:1rem}.collapsible-body{display:none;border-bottom:1px solid #ddd;box-sizing:border-box;padding:2rem}.side-nav .collapsible,.side-nav.fixed .collapsible{border:none;box-shadow:none}.side-nav .collapsible li,.side-nav.fixed .collapsible li{padding:0}.side-nav .collapsible-header,.side-nav.fixed .collapsible-header{background-color:transparent;border:none;line-height:inherit;height:inherit;padding:0 16px}.side-nav .collapsible-header:hover,.side-nav.fixed .collapsible-header:hover{background-color:rgba(0,0,0,.05)}.side-nav .collapsible-header i,.side-nav.fixed .collapsible-header i{line-height:inherit}.side-nav .collapsible-body,.side-nav.fixed .collapsible-body{border:0;background-color:#fff}.side-nav .collapsible-body li a,.side-nav.fixed .collapsible-body li a{padding:0 23.5px 0 31px}.collapsible.popout{border:none;box-shadow:none}.collapsible.popout>li{box-shadow:0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12);margin:0 24px;transition:margin .35s cubic-bezier(.25,.46,.45,.94)}.collapsible.popout>li.active{box-shadow:0 5px 11px 0 rgba(0,0,0,.18),0 4px 15px 0 rgba(0,0,0,.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(0,0,0,.6);line-height:32px;padding:0 12px;border-radius:16px;background-color:#e4e4e4;margin-bottom:5px;margin-right:5px}.chip>img{float:left;margin:0 8px 0 -12px;height:32px;width:32px;border-radius:50%}.chip .close{cursor:pointer;float:right;font-size:16px;line-height:32px;padding-left:8px}.chips{border:none;border-bottom:1px solid #9e9e9e;box-shadow:none;margin:0 0 20px;min-height:45px;outline:none;transition:all .3s}.chips.focus{border-bottom:1px solid #26a69a;box-shadow:0 1px 0 0 #26a69a}.chips:hover{cursor:text}.chips .chip.selected{background-color:#26a69a;color:#fff}.chips .input{background:none;border:0;color:rgba(0,0,0,.6);display:inline-block;font-size:1rem;height:3rem;line-height:32px;outline:0;margin:0;padding:0!important;width:120px!important}.chips .input:focus{border:0!important;box-shadow:none!important}.chips .autocomplete-content{margin-top:0}.prefix~.chips{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.chips:empty~label{font-size:.8rem;-webkit-transform:translateY(-140%);transform:translateY(-140%)}.materialboxed{display:block;cursor:-webkit-zoom-in;cursor:zoom-in;position:relative;transition:opacity .4s;-webkit-backface-visibility:hidden}.materialboxed:hover:not(.active){opacity:.8}.materialboxed.active{cursor:-webkit-zoom-out;cursor:zoom-out}#materialbox-overlay{top:0;right:0;background-color:#292929;will-change:opacity}#materialbox-overlay,.materialbox-caption{position:fixed;bottom:0;left:0;z-index:1000}.materialbox-caption{display:none;color:#fff;line-height:50px;width:100%;text-align:center;padding:0 15%;height:50px;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #c9f3ef}button:focus{outline:none;background-color:#2ab7a9}label{font-size:.8rem;color:#9e9e9e}::-webkit-input-placeholder{color:#d1d1d1}:-moz-placeholder,::-moz-placeholder{color:#d1d1d1}:-ms-input-placeholder{color:#d1d1d1}input:not([type]),input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],textarea.materialize-textarea{background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;border-radius:0;outline:none;height:3rem;width:100%;font-size:1rem;margin:0 0 20px;padding:0;box-shadow:none;box-sizing:content-box;transition:all .3s}input:not([type]):disabled,input:not([type])[readonly=readonly],input[type=date]:disabled,input[type=date][readonly=readonly],input[type=datetime-local]:disabled,input[type=datetime-local][readonly=readonly],input[type=datetime]:disabled,input[type=datetime][readonly=readonly],input[type=email]:disabled,input[type=email][readonly=readonly],input[type=number]:disabled,input[type=number][readonly=readonly],input[type=password]:disabled,input[type=password][readonly=readonly],input[type=search]:disabled,input[type=search][readonly=readonly],input[type=tel]:disabled,input[type=tel][readonly=readonly],input[type=text]:disabled,input[type=text][readonly=readonly],input[type=time]:disabled,input[type=time][readonly=readonly],input[type=url]:disabled,input[type=url][readonly=readonly],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly=readonly]{color:rgba(0,0,0,.26);border-bottom:1px dotted rgba(0,0,0,.26)}input:not([type]):disabled+label,input:not([type])[readonly=readonly]+label,input[type=date]:disabled+label,input[type=date][readonly=readonly]+label,input[type=datetime-local]:disabled+label,input[type=datetime-local][readonly=readonly]+label,input[type=datetime]:disabled+label,input[type=datetime][readonly=readonly]+label,input[type=email]:disabled+label,input[type=email][readonly=readonly]+label,input[type=number]:disabled+label,input[type=number][readonly=readonly]+label,input[type=password]:disabled+label,input[type=password][readonly=readonly]+label,input[type=search]:disabled+label,input[type=search][readonly=readonly]+label,input[type=tel]:disabled+label,input[type=tel][readonly=readonly]+label,input[type=text]:disabled+label,input[type=text][readonly=readonly]+label,input[type=time]:disabled+label,input[type=time][readonly=readonly]+label,input[type=url]:disabled+label,input[type=url][readonly=readonly]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly=readonly]+label{color:rgba(0,0,0,.26)}input:not([type]):focus:not([readonly]),input[type=date]:focus:not([readonly]),input[type=datetime-local]:focus:not([readonly]),input[type=datetime]:focus:not([readonly]),input[type=email]:focus:not([readonly]),input[type=number]:focus:not([readonly]),input[type=password]:focus:not([readonly]),input[type=search]:focus:not([readonly]),input[type=tel]:focus:not([readonly]),input[type=text]:focus:not([readonly]),input[type=time]:focus:not([readonly]),input[type=url]:focus:not([readonly]),textarea.materialize-textarea:focus:not([readonly]){border-bottom:1px solid #26a69a;box-shadow:0 1px 0 0 #26a69a}input:not([type]):focus:not([readonly])+label,input[type=date]:focus:not([readonly])+label,input[type=datetime-local]:focus:not([readonly])+label,input[type=datetime]:focus:not([readonly])+label,input[type=email]:focus:not([readonly])+label,input[type=number]:focus:not([readonly])+label,input[type=password]:focus:not([readonly])+label,input[type=search]:focus:not([readonly])+label,input[type=tel]:focus:not([readonly])+label,input[type=text]:focus:not([readonly])+label,input[type=time]:focus:not([readonly])+label,input[type=url]:focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#26a69a}input:not([type]).valid,input:not([type]):focus.valid,input[type=date].valid,input[type=date]:focus.valid,input[type=datetime-local].valid,input[type=datetime-local]:focus.valid,input[type=datetime].valid,input[type=datetime]:focus.valid,input[type=email].valid,input[type=email]:focus.valid,input[type=number].valid,input[type=number]:focus.valid,input[type=password].valid,input[type=password]:focus.valid,input[type=search].valid,input[type=search]:focus.valid,input[type=tel].valid,input[type=tel]:focus.valid,input[type=text].valid,input[type=text]:focus.valid,input[type=time].valid,input[type=time]:focus.valid,input[type=url].valid,input[type=url]:focus.valid,textarea.materialize-textarea.valid,textarea.materialize-textarea:focus.valid{border-bottom:1px solid #4caf50;box-shadow:0 1px 0 0 #4caf50}input:not([type]).valid+label:after,input:not([type]):focus.valid+label:after,input[type=date].valid+label:after,input[type=date]:focus.valid+label:after,input[type=datetime-local].valid+label:after,input[type=datetime-local]:focus.valid+label:after,input[type=datetime].valid+label:after,input[type=datetime]:focus.valid+label:after,input[type=email].valid+label:after,input[type=email]:focus.valid+label:after,input[type=number].valid+label:after,input[type=number]:focus.valid+label:after,input[type=password].valid+label:after,input[type=password]:focus.valid+label:after,input[type=search].valid+label:after,input[type=search]:focus.valid+label:after,input[type=tel].valid+label:after,input[type=tel]:focus.valid+label:after,input[type=text].valid+label:after,input[type=text]:focus.valid+label:after,input[type=time].valid+label:after,input[type=time]:focus.valid+label:after,input[type=url].valid+label:after,input[type=url]:focus.valid+label:after,textarea.materialize-textarea.valid+label:after,textarea.materialize-textarea:focus.valid+label:after{content:attr(data-success);color:#4caf50;opacity:1}input:not([type]).invalid,input:not([type]):focus.invalid,input[type=date].invalid,input[type=date]:focus.invalid,input[type=datetime-local].invalid,input[type=datetime-local]:focus.invalid,input[type=datetime].invalid,input[type=datetime]:focus.invalid,input[type=email].invalid,input[type=email]:focus.invalid,input[type=number].invalid,input[type=number]:focus.invalid,input[type=password].invalid,input[type=password]:focus.invalid,input[type=search].invalid,input[type=search]:focus.invalid,input[type=tel].invalid,input[type=tel]:focus.invalid,input[type=text].invalid,input[type=text]:focus.invalid,input[type=time].invalid,input[type=time]:focus.invalid,input[type=url].invalid,input[type=url]:focus.invalid,textarea.materialize-textarea.invalid,textarea.materialize-textarea:focus.invalid{border-bottom:1px solid #f44336;box-shadow:0 1px 0 0 #f44336}input:not([type]).invalid+label:after,input:not([type]):focus.invalid+label:after,input[type=date].invalid+label:after,input[type=date]:focus.invalid+label:after,input[type=datetime-local].invalid+label:after,input[type=datetime-local]:focus.invalid+label:after,input[type=datetime].invalid+label:after,input[type=datetime]:focus.invalid+label:after,input[type=email].invalid+label:after,input[type=email]:focus.invalid+label:after,input[type=number].invalid+label:after,input[type=number]:focus.invalid+label:after,input[type=password].invalid+label:after,input[type=password]:focus.invalid+label:after,input[type=search].invalid+label:after,input[type=search]:focus.invalid+label:after,input[type=tel].invalid+label:after,input[type=tel]:focus.invalid+label:after,input[type=text].invalid+label:after,input[type=text]:focus.invalid+label:after,input[type=time].invalid+label:after,input[type=time]:focus.invalid+label:after,input[type=url].invalid+label:after,input[type=url]:focus.invalid+label:after,textarea.materialize-textarea.invalid+label:after,textarea.materialize-textarea:focus.invalid+label:after{content:attr(data-error);color:#f44336;opacity:1}input:not([type]).validate+label,input[type=date].validate+label,input[type=datetime-local].validate+label,input[type=datetime].validate+label,input[type=email].validate+label,input[type=number].validate+label,input[type=password].validate+label,input[type=search].validate+label,input[type=tel].validate+label,input[type=text].validate+label,input[type=time].validate+label,input[type=url].validate+label,textarea.materialize-textarea.validate+label{width:100%;pointer-events:none}input:not([type])+label:after,input[type=date]+label:after,input[type=datetime-local]+label:after,input[type=datetime]+label:after,input[type=email]+label:after,input[type=number]+label:after,input[type=password]+label:after,input[type=search]+label:after,input[type=tel]+label:after,input[type=text]+label:after,input[type=time]+label:after,input[type=url]+label:after,textarea.materialize-textarea+label:after{display:block;content:"";position:absolute;top:60px;opacity:0;transition:opacity .2s ease-out,color .2s ease-out}.input-field{position:relative;margin-top:1rem}.input-field.inline{display:inline-block;vertical-align:middle;margin-left:5px}.input-field.inline .select-dropdown,.input-field.inline input{margin-bottom:1rem}.input-field.col label{left:.75rem}.input-field.col .prefix~.validate~label,.input-field.col .prefix~label{width:calc(100% - 3rem - 1.5rem)}.input-field label{color:#9e9e9e;position:absolute;top:.8rem;left:0;font-size:1rem;cursor:text;transition:.2s ease-out;text-align:initial}.input-field label:not(.label-icon).active{font-size:.8rem;-webkit-transform:translateY(-140%);transform:translateY(-140%)}.input-field .prefix{position:absolute;width:3rem;font-size:2rem;transition:color .2s}.input-field .prefix.active{color:#26a69a}.input-field .prefix~.autocomplete-content,.input-field .prefix~.validate~label,.input-field .prefix~input,.input-field .prefix~label,.input-field .prefix~textarea{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.input-field .prefix~label{margin-left:3rem}@media only screen and (max-width:992px){.input-field .prefix~input{width:86%;width:calc(100% - 3rem)}}@media only screen and (max-width:600px){.input-field .prefix~input{width:80%;width:calc(100% - 3rem)}}.input-field input[type=search]{display:block;line-height:inherit;padding-left:4rem;width:calc(100% - 4rem)}.input-field input[type=search]:focus{background-color:#fff;border:0;box-shadow:none;color:#444}.input-field input[type=search]:focus+label i,.input-field input[type=search]:focus~.material-icons,.input-field input[type=search]:focus~.mdi-navigation-close{color:#444}.input-field input[type=search]+label{left:1rem}.input-field input[type=search]~.material-icons,.input-field input[type=search]~.mdi-navigation-close{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;transition:color .3s}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{overflow-y:hidden;padding:.8rem 0 1.6rem;resize:none;min-height:3rem}.hiddendiv{display:none;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;padding-top:1.2rem;position:absolute;top:0}.autocomplete-content{margin-top:-20px;display:block;opacity:1;position:static}.autocomplete-content li .highlight{color:#444}.autocomplete-content li img{height:40px;width:40px;margin:5px 15px}[type=radio]:checked,[type=radio]:not(:checked){position:absolute;left:-9999px;opacity:0}[type=radio]:checked+label,[type=radio]:not(:checked)+label{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;transition:.28s ease;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}[type=radio]+label:after,[type=radio]+label:before{content:"";position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;transition:.28s ease}[type=radio].with-gap:checked+label:after,[type=radio].with-gap:checked+label:before,[type=radio]:checked+label:after,[type=radio]:checked+label:before,[type=radio]:not(:checked)+label:after,[type=radio]:not(:checked)+label:before{border-radius:50%}[type=radio]:not(:checked)+label:after,[type=radio]:not(:checked)+label:before{border:2px solid #5a5a5a}[type=radio]:not(:checked)+label:after{-webkit-transform:scale(0);transform:scale(0)}[type=radio]:checked+label:before{border:2px solid transparent}[type=radio].with-gap:checked+label:after,[type=radio].with-gap:checked+label:before,[type=radio]:checked+label:after{border:2px solid #26a69a}[type=radio].with-gap:checked+label:after,[type=radio]:checked+label:after{background-color:#26a69a}[type=radio]:checked+label:after{-webkit-transform:scale(1.02);transform:scale(1.02)}[type=radio].with-gap:checked+label:after{-webkit-transform:scale(.5);transform:scale(.5)}[type=radio].tabbed:focus+label:before{box-shadow:0 0 0 10px rgba(0,0,0,.1)}[type=radio].with-gap:disabled:checked+label:before{border:2px solid rgba(0,0,0,.26)}[type=radio].with-gap:disabled:checked+label:after{border:none;background-color:rgba(0,0,0,.26)}[type=radio]:disabled:checked+label:before,[type=radio]:disabled:not(:checked)+label:before{background-color:transparent;border-color:rgba(0,0,0,.26)}[type=radio]:disabled+label{color:rgba(0,0,0,.26)}[type=radio]:disabled:not(:checked)+label:before{border-color:rgba(0,0,0,.26)}[type=radio]:disabled:checked+label:after{background-color:rgba(0,0,0,.26);border-color:#bdbdbd}form p{margin-bottom:10px;text-align:left}form p:last-child{margin-bottom:0}[type=checkbox]:checked,[type=checkbox]:not(:checked){position:absolute;left:-9999px;opacity:0}[type=checkbox]+label{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;-webkit-user-select:none;-moz-user-select:none;-khtml-user-select:none;-ms-user-select:none}[type=checkbox]+label:before,[type=checkbox]:not(.filled-in)+label:after{content:"";position:absolute;top:0;left:0;width:18px;height:18px;z-index:0;border:2px solid #5a5a5a;border-radius:1px;margin-top:2px;transition:.2s}[type=checkbox]:not(.filled-in)+label:after{border:0;-webkit-transform:scale(0);transform:scale(0)}[type=checkbox]:not(:checked):disabled+label:before{border:none;background-color:rgba(0,0,0,.26)}[type=checkbox].tabbed:focus+label:after{-webkit-transform:scale(1);transform:scale(1);border:0;border-radius:50%;box-shadow:0 0 0 10px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}[type=checkbox]:checked+label:before{top:-4px;left:-5px;width:12px;height:22px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #26a69a;border-bottom:2px solid #26a69a;-webkit-transform:rotate(40deg);transform:rotate(40deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type=checkbox]:checked:disabled+label:before{border-right:2px solid rgba(0,0,0,.26);border-bottom:2px solid rgba(0,0,0,.26)}[type=checkbox]:indeterminate+label:before{top:-11px;left:-12px;width:10px;height:22px;border-top:none;border-left:none;border-right:2px solid #26a69a;border-bottom:none;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type=checkbox]:indeterminate:disabled+label:before{border-right:2px solid rgba(0,0,0,.26);background-color:transparent}[type=checkbox].filled-in+label:after{border-radius:2px}[type=checkbox].filled-in+label:after,[type=checkbox].filled-in+label:before{content:"";left:0;position:absolute;transition:border .25s,background-color .25s,width .2s .1s,height .2s .1s,top .2s .1s,left .2s .1s;z-index:1}[type=checkbox].filled-in:not(:checked)+label:before{width:0;height:0;border:3px solid transparent;left:6px;top:10px;-webkit-transform:rotate(37deg);transform:rotate(37deg);-webkit-transform-origin:20% 40%;transform-origin:100% 100%}[type=checkbox].filled-in:not(:checked)+label:after{height:20px;width:20px;background-color:transparent;border:2px solid #5a5a5a;top:0;z-index:0}[type=checkbox].filled-in:checked+label:before{top:0;left:1px;width:8px;height:13px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #fff;border-bottom:2px solid #fff;-webkit-transform:rotate(37deg);transform:rotate(37deg);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type=checkbox].filled-in:checked+label:after{top:0;width:20px;height:20px;border:2px solid #26a69a;background-color:#26a69a;z-index:0}[type=checkbox].filled-in.tabbed:focus+label:after{border-radius:2px;border-color:#5a5a5a;background-color:rgba(0,0,0,.1)}[type=checkbox].filled-in.tabbed:checked:focus+label:after{border-radius:2px;background-color:#26a69a;border-color:#26a69a}[type=checkbox].filled-in:disabled:not(:checked)+label:before{background-color:transparent;border:2px solid transparent}[type=checkbox].filled-in:disabled:not(:checked)+label:after{border-color:transparent;background-color:#bdbdbd}[type=checkbox].filled-in:disabled:checked+label:before{background-color:transparent}[type=checkbox].filled-in:disabled:checked+label:after{background-color:#bdbdbd;border-color:#bdbdbd}.switch,.switch *{-webkit-user-select:none;-moz-user-select:none;-khtml-user-select:none;-ms-user-select:none}.switch label{cursor:pointer}.switch label input[type=checkbox]{opacity:0;width:0;height:0}.switch label input[type=checkbox]:checked+.lever{background-color:#84c7c1}.switch label input[type=checkbox]:checked+.lever:after{background-color:#26a69a;left:24px}.switch label .lever{content:"";display:inline-block;position:relative;width:40px;height:15px;background-color:#818181;border-radius:15px;margin-right:10px;transition:background .3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:after{content:"";position:absolute;display:inline-block;width:21px;height:21px;background-color:#f1f1f1;border-radius:21px;box-shadow:0 1px 3px 1px rgba(0,0,0,.4);left:-5px;top:-3px;transition:left .3s ease,background .3s ease,box-shadow .1s ease}input[type=checkbox]:checked:not(:disabled).tabbed:focus~.lever:after,input[type=checkbox]:checked:not(:disabled)~.lever:active:after{box-shadow:0 1px 3px 1px rgba(0,0,0,.4),0 0 0 15px rgba(38,166,154,.1)}input[type=checkbox]:not(:disabled).tabbed:focus~.lever:after,input[type=checkbox]:not(:disabled)~.lever:active:after{box-shadow:0 1px 3px 1px rgba(0,0,0,.4),0 0 0 15px rgba(0,0,0,.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default}.switch label input[type=checkbox][disabled]+.lever:after,.switch label input[type=checkbox][disabled]:checked+.lever:after{background-color:#bdbdbd}select{display:none}select.browser-default{display:block}select{background-color:hsla(0,0%,100%,.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper input.select-dropdown{position:relative;cursor:pointer;background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;outline:none;height:3rem;line-height:3rem;width:100%;font-size:1rem;margin:0 0 20px;padding:0;display:block}.select-wrapper span.caret{color:initial;position:absolute;right:0;top:0;bottom:0;height:10px;margin:auto 0;font-size:10px;line-height:10px}.select-wrapper span.caret.disabled{color:rgba(0,0,0,.26)}.select-wrapper+label{position:absolute;top:-14px;font-size:.8rem}select:disabled{color:rgba(0,0,0,.3)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,.3);cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;border-bottom:1px solid rgba(0,0,0,.3)}.select-wrapper i{color:rgba(0,0,0,.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,.3);background-color:transparent}.prefix~.select-wrapper{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.prefix~label{margin-left:3rem}.select-dropdown li img{height:40px;width:40px;margin:5px 15px;float:right}.select-dropdown li.optgroup{border-top:1px solid #eee}.select-dropdown li.optgroup.selected>span{color:rgba(0,0,0,.7)}.select-dropdown li.optgroup>span{color:rgba(0,0,0,.4)}.select-dropdown li.optgroup~li.optgroup-option{padding-left:1rem}.file-field{position:relative}.file-field .file-path-wrapper{overflow:hidden;padding-left:10px}.file-field input.file-path{width:100%}.file-field .btn,.file-field .btn-large{float:left;height:3rem;line-height:3rem}.file-field span{cursor:pointer}.file-field input[type=file]{position:absolute;top:0;right:0;left:0;bottom:0;width:100%;margin:0;padding:0;font-size:20px;cursor:pointer;opacity:0;filter:alpha(opacity=0)}.range-field{position:relative}input[type=range],input[type=range]+.thumb{cursor:pointer}input[type=range]{position:relative;background-color:transparent;border:none;outline:none;width:100%;margin:15px 0;padding:0}input[type=range]:focus{outline:none}input[type=range]+.thumb{position:absolute;top:10px;left:0;border:none;height:0;width:0;border-radius:50%;background-color:#26a69a;margin-left:7px;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#26a69a;font-size:0;-webkit-transform:rotate(45deg);transform:rotate(45deg)}input[type=range]+.thumb.active{border-radius:50% 50% 50% 0}input[type=range]+.thumb.active .value{color:#fff;margin-left:-1px;margin-top:8px;font-size:10px}input[type=range]{-webkit-appearance:none}input[type=range]::-webkit-slider-runnable-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;border:none;height:14px;width:14px;border-radius:50%;background-color:#26a69a;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;margin:-5px 0 0;transition:.3s}input[type=range]:focus::-webkit-slider-runnable-track{background:#ccc}input[type=range]{border:1px solid #fff}input[type=range]::-moz-range-track{height:3px;background:#ddd;border:none}input[type=range]::-moz-range-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;margin-top:-5px}input[type=range]:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}input[type=range]:focus::-moz-range-track{background:#ccc}input[type=range]::-ms-track{height:3px;background:transparent;border-color:transparent;border-width:6px 0;color:transparent}input[type=range]::-ms-fill-lower{background:#777}input[type=range]::-ms-fill-upper{background:#ddd}input[type=range]::-ms-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a}input[type=range]:focus::-ms-fill-lower{background:#888}input[type=range]:focus::-ms-fill-upper{background:#ccc}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{font-weight:300;color:#757575;padding-left:20px;height:1.5rem;line-height:1.5rem;letter-spacing:.4;display:inline-block}.table-of-contents a:hover{color:#a8a8a8;padding-left:19px;border-left:1px solid #ee6e73}.table-of-contents a.active{font-weight:500;padding-left:18px;border-left:2px solid #ee6e73}.side-nav{position:fixed;width:300px;left:0;top:0;margin:0;-webkit-transform:translateX(-100%);transform:translateX(-100%);height:calc(100% + 60px);height:100%;padding-bottom:60px;background-color:#fff;z-index:999;overflow-y:auto;will-change:transform;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform:translateX(-105%);transform:translateX(-105%)}.side-nav.right-aligned{right:0;-webkit-transform:translateX(105%);transform:translateX(105%);left:auto;-webkit-transform:translateX(100%);transform:translateX(100%)}.side-nav .collapsible{margin:0}.side-nav li{float:none;line-height:48px}.side-nav li.active{background-color:rgba(0,0,0,.05)}.side-nav li>a{color:rgba(0,0,0,.87);display:block;font-size:14px;font-weight:500;height:48px;line-height:48px;padding:0 32px}.side-nav li>a:hover{background-color:rgba(0,0,0,.05)}.side-nav li>a.btn,.side-nav li>a.btn-flat,.side-nav li>a.btn-floating,.side-nav li>a.btn-large{margin:10px 15px}.side-nav li>a.btn,.side-nav li>a.btn-floating,.side-nav li>a.btn-large{color:#fff}.side-nav li>a.btn-flat{color:#343434}.side-nav li>a.btn-large:hover,.side-nav li>a.btn:hover{background-color:#2bbbad}.side-nav li>a.btn-floating:hover{background-color:#26a69a}.side-nav li>a>[class^=mdi-],.side-nav li>a>i,.side-nav li>a>i.material-icons,.side-nav li>a li>a>[class*=mdi-]{float:left;height:48px;line-height:48px;margin:0 32px 0 0;width:24px;color:rgba(0,0,0,.54)}.side-nav .divider{margin:8px 0 0}.side-nav .subheader{cursor:auto;pointer-events:none;color:rgba(0,0,0,.54);font-size:14px;font-weight:500;line-height:48px}.side-nav .subheader:hover{background-color:transparent}.side-nav .userView{position:relative;padding:32px 32px 0;margin-bottom:8px}.side-nav .userView>a{height:auto;padding:0}.side-nav .userView>a:hover{background-color:transparent}.side-nav .userView .background{overflow:hidden;position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1}.side-nav .userView .circle,.side-nav .userView .email,.side-nav .userView .name{display:block}.side-nav .userView .circle{height:64px;width:64px}.side-nav .userView .email,.side-nav .userView .name{font-size:14px;line-height:24px}.side-nav .userView .name{margin-top:16px;font-weight:500}.side-nav .userView .email{padding-bottom:16px;font-weight:400}.drag-target{height:100%;width:10px;position:fixed;top:0;z-index:998}.side-nav.fixed{left:0;-webkit-transform:translateX(0);transform:translateX(0);position:fixed}.side-nav.fixed.right-aligned{right:0;left:auto}@media only screen and (max-width:992px){.side-nav.fixed{-webkit-transform:translateX(-105%);transform:translateX(-105%)}.side-nav.fixed.right-aligned{-webkit-transform:translateX(105%);transform:translateX(105%)}.side-nav a{padding:0 16px}.side-nav .userView{padding:16px 16px 0}}.side-nav .collapsible-body>ul:not(.collapsible)>li.active,.side-nav.fixed .collapsible-body>ul:not(.collapsible)>li.active{background-color:#ee6e73}.side-nav .collapsible-body>ul:not(.collapsible)>li.active a,.side-nav.fixed .collapsible-body>ul:not(.collapsible)>li.active a{color:#fff}.side-nav .collapsible-body{padding:0}#sidenav-overlay{position:fixed;top:0;left:0;right:0;height:120vh;background-color:rgba(0,0,0,.5);z-index:997;will-change:opacity}.preloader-wrapper{display:inline-block;position:relative;width:50px;height:50px}.preloader-wrapper.small{width:36px;height:36px}.preloader-wrapper.big{width:64px;height:64px}.preloader-wrapper.active{-webkit-animation:container-rotate 1568ms linear infinite;animation:container-rotate 1568ms linear infinite}@-webkit-keyframes container-rotate{to{-webkit-transform:rotate(1turn)}}@keyframes container-rotate{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.spinner-layer{position:absolute;width:100%;height:100%;opacity:0;border-color:#26a69a}.spinner-blue,.spinner-blue-only{border-color:#4285f4}.spinner-red,.spinner-red-only{border-color:#db4437}.spinner-yellow,.spinner-yellow-only{border-color:#f4b400}.spinner-green,.spinner-green-only{border-color:#0f9d58}.active .spinner-layer.spinner-blue{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,blue-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,blue-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,red-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,red-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,green-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,green-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-green-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both}@-webkit-keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg)}to{-webkit-transform:rotate(3turn)}}@keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(3turn);transform:rotate(3turn)}}@-webkit-keyframes blue-fade-in-out{0%{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}to{opacity:1}}@keyframes blue-fade-in-out{0%{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}to{opacity:1}}@-webkit-keyframes red-fade-in-out{0%{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{0%{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{0%{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{0%{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{0%{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}to{opacity:0}}@keyframes green-fade-in-out{0%{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}to{opacity:0}}.gap-patch{position:absolute;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.gap-patch .circle{width:1000%;left:-450%}.circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.circle-clipper .circle{width:200%;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent!important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0}.circle-clipper.left .circle{left:0;border-right-color:transparent!important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.circle-clipper.right .circle{left:-100%;border-left-color:transparent!important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.active .circle-clipper.left .circle{-webkit-animation:left-spin 1333ms cubic-bezier(.4,0,.2,1) infinite both;animation:left-spin 1333ms cubic-bezier(.4,0,.2,1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(.4,0,.2,1) infinite both;animation:right-spin 1333ms cubic-bezier(.4,0,.2,1) infinite both}@-webkit-keyframes left-spin{0%{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{0%{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes right-spin{0%{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{0%{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out .4s cubic-bezier(.4,0,.2,1);animation:container-rotate 1568ms linear infinite,fade-out .4s cubic-bezier(.4,0,.2,1)}@-webkit-keyframes fade-out{0%{opacity:1}to{opacity:0}}@keyframes fade-out{0%{opacity:1}to{opacity:0}}.slider{position:relative;height:400px;width:100%}.slider.fullscreen{height:100%;width:100%;position:absolute;top:0;left:0;right:0;bottom:0}.slider.fullscreen ul.slides{height:100%}.slider.fullscreen ul.indicators{z-index:2;bottom:30px}.slider .slides{background-color:#9e9e9e;margin:0;height:400px}.slider .slides li{opacity:0;position:absolute;top:0;left:0;z-index:1;width:100%;height:inherit;overflow:hidden}.slider .slides li img{height:100%;width:100%;background-size:cover;background-position:50%}.slider .slides li .caption{color:#fff;position:absolute;top:15%;left:15%;width:70%;opacity:0}.slider .slides li .caption p{color:#e0e0e0}.slider .slides li.active{z-index:2}.slider .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.slider .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:16px;width:16px;margin:0 12px;background-color:#e0e0e0;transition:background-color .3s;border-radius:50%}.slider .indicators .indicator-item.active{background-color:#4caf50}.carousel{overflow:hidden;position:relative;width:100%;height:400px;-webkit-perspective:500px;perspective:500px;-webkit-transform-style:preserve-3d;transform-style:preserve-3d;-webkit-transform-origin:0 50%;transform-origin:0 50%}.carousel.carousel-slider{top:0;left:0;height:0}.carousel.carousel-slider .carousel-fixed-item{position:absolute;left:0;right:0;bottom:20px;z-index:1}.carousel.carousel-slider .carousel-fixed-item.with-indicators{bottom:68px}.carousel.carousel-slider .carousel-item{width:100%;height:100%;min-height:400px;position:absolute;top:0;left:0}.carousel.carousel-slider .carousel-item h2{font-size:24px;font-weight:500;line-height:32px}.carousel.carousel-slider .carousel-item p{font-size:15px}.carousel .carousel-item{display:none;width:200px;height:200px;position:absolute;top:0;left:0}.carousel .carousel-item>img{width:100%}.carousel .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.carousel .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:8px;width:8px;margin:24px 4px;background-color:hsla(0,0%,100%,.5);transition:background-color .3s;border-radius:50%}.carousel .indicators .indicator-item.active{background-color:#fff}.carousel .carousel-item:not(.active) .materialboxed,.carousel.scrolling .carousel-item .materialboxed{pointer-events:none}.tap-target-wrapper{width:800px;height:800px;position:fixed;z-index:1000;visibility:hidden;transition:visibility 0s .3s}.tap-target-wrapper.open{visibility:visible;transition:visibility 0s}.tap-target-wrapper.open .tap-target{opacity:.95;transition:opacity .3s ease-in-out,-webkit-transform .3s ease-in-out;transition:transform .3s ease-in-out,opacity .3s ease-in-out;transition:transform .3s ease-in-out,opacity .3s ease-in-out,-webkit-transform .3s ease-in-out}.tap-target-wrapper.open .tap-target,.tap-target-wrapper.open .tap-target-wave:before{-webkit-transform:scale(1);transform:scale(1)}.tap-target-wrapper.open .tap-target-wave:after{visibility:visible;-webkit-animation:pulse-animation 1s cubic-bezier(.24,0,.38,1) infinite;animation:pulse-animation 1s cubic-bezier(.24,0,.38,1) infinite;transition:opacity .3s,visibility 0s 1s,-webkit-transform .3s;transition:opacity .3s,transform .3s,visibility 0s 1s;transition:opacity .3s,transform .3s,visibility 0s 1s,-webkit-transform .3s}.tap-target{position:absolute;font-size:1rem;border-radius:50%;background-color:#ee6e73;box-shadow:0 20px 20px 0 rgba(0,0,0,.14),0 10px 50px 0 rgba(0,0,0,.12),0 30px 10px -20px rgba(0,0,0,.2);width:100%;height:100%;opacity:0;-webkit-transform:scale(0);transform:scale(0);transition:opacity .3s ease-in-out,-webkit-transform .3s ease-in-out;transition:transform .3s ease-in-out,opacity .3s ease-in-out;transition:transform .3s ease-in-out,opacity .3s ease-in-out,-webkit-transform .3s ease-in-out}.tap-target-content{position:relative;display:table-cell}.tap-target-wave{position:absolute;border-radius:50%;z-index:10001}.tap-target-wave:after,.tap-target-wave:before{content:"";display:block;position:absolute;width:100%;height:100%;border-radius:50%;background-color:#fff}.tap-target-wave:before{-webkit-transform:scale(0);transform:scale(0);transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s}.tap-target-wave:after{visibility:hidden;transition:opacity .3s,visibility 0s,-webkit-transform .3s;transition:opacity .3s,transform .3s,visibility 0s;transition:opacity .3s,transform .3s,visibility 0s,-webkit-transform .3s;z-index:-1}.tap-target-origin{top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);z-index:10002;position:absolute!important}.tap-target-origin:not(.btn):not(.btn-large),.tap-target-origin:not(.btn):not(.btn-large):hover{background:none}@media only screen and (max-width:600px){.tap-target,.tap-target-wrapper{width:600px;height:600px}}.pulse{overflow:initial;position:relative}.pulse:before{content:"";display:block;position:absolute;width:100%;height:100%;top:0;left:0;background-color:inherit;border-radius:inherit;transition:opacity .3s,-webkit-transform .3s;transition:opacity .3s,transform .3s;transition:opacity .3s,transform .3s,-webkit-transform .3s;-webkit-animation:pulse-animation 1s cubic-bezier(.24,0,.38,1) infinite;animation:pulse-animation 1s cubic-bezier(.24,0,.38,1) infinite;z-index:-1}@-webkit-keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}to{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}}@keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}to{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}}.picker{font-size:16px;text-align:left;line-height:1.2;color:#000;position:absolute;z-index:10000;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.picker__input{cursor:default}.picker__input.picker__input--active{border-color:#0089ec}.picker__holder{width:100%;overflow-y:auto;-webkit-overflow-scrolling:touch}.picker__frame,.picker__holder{bottom:0;left:0;right:0;top:100%}.picker__holder{position:fixed;transition:background .15s ease-out,top 0s .15s;-webkit-backface-visibility:hidden}.picker__frame{position:absolute;min-width:256px;width:300px;max-height:350px;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";filter:alpha(opacity=0);-moz-opacity:0;opacity:0;transition:all .15s ease-out}@media (min-height:28.875em){.picker__frame{overflow:visible;top:auto;bottom:-100%;max-height:80%}}@media (min-height:40.125em){.picker__frame{margin-bottom:7.5%}}.picker__wrap{display:table;width:100%;height:100%}@media (min-height:28.875em){.picker__wrap{display:block}}.picker__box{background:#fff;display:table-cell;vertical-align:middle}@media (min-height:28.875em){.picker__box{display:block;border:1px solid #777;border-top-color:#898989;border-bottom-width:0;border-radius:5px 5px 0 0;box-shadow:0 12px 36px 16px rgba(0,0,0,.24)}}.picker--opened .picker__holder{top:0;background:transparent;-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#1E000000,endColorstr=#1E000000)";zoom:1;background:rgba(0,0,0,.32);transition:background .15s ease-out}.picker--opened .picker__frame{top:0;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";filter:alpha(opacity=100);-moz-opacity:1;opacity:1}@media (min-height:35.875em){.picker--opened .picker__frame{top:10%;bottom:auto}}.picker__input.picker__input--active{border-color:#e3f2fd}.picker__frame{margin:0 auto;max-width:325px}@media (min-height:38.875em){.picker--opened .picker__frame{top:10%;bottom:auto}}.picker__box{padding:0 1em}.picker__header{text-align:center;position:relative;margin-top:.75em}.picker__month,.picker__year{display:inline-block;margin-left:.25em;margin-right:.25em}.picker__select--month,.picker__select--year{height:2em;padding:0;margin-left:.25em;margin-right:.25em}.picker__select--month.browser-default{display:inline;background-color:#fff;width:40%}.picker__select--year.browser-default{display:inline;background-color:#fff;width:26%}.picker__select--month:focus,.picker__select--year:focus{border-color:rgba(0,0,0,.05)}.picker__nav--next,.picker__nav--prev{position:absolute;padding:.5em 1.25em;width:1em;height:1em;box-sizing:content-box;top:-.25em}.picker__nav--prev{left:-1em;padding-right:1.25em}.picker__nav--next{right:-1em;padding-left:1.25em}.picker__nav--disabled,.picker__nav--disabled:before,.picker__nav--disabled:before:hover,.picker__nav--disabled:hover{cursor:default;background:none;border-right-color:#f5f5f5;border-left-color:#f5f5f5}.picker__table{border-collapse:collapse;border-spacing:0;table-layout:fixed;font-size:1rem;width:100%;margin-top:.75em}.picker__table,.picker__table td,.picker__table th{text-align:center}.picker__table td{margin:0;padding:0}.picker__weekday{width:14.285714286%;font-size:.75em;padding-bottom:.25em;color:#999;font-weight:500}@media (min-height:33.875em){.picker__weekday{padding-bottom:.5em}}.picker__day--today{position:relative;color:#595959;letter-spacing:-.3;padding:.75rem 0;font-weight:400;border:1px solid transparent}.picker__day--disabled:before{border-top-color:#aaa}.picker__day--infocus:hover{cursor:pointer;color:#000;font-weight:500}.picker__day--outfocus{display:none;padding:.75rem 0;color:#fff}.picker__day--outfocus:hover{cursor:pointer;color:#ddd;font-weight:500}.picker--focused .picker__day--highlighted,.picker__day--highlighted:hover{cursor:pointer}.picker--focused .picker__day--selected,.picker__day--selected,.picker__day--selected:hover{-webkit-transform:scale(.75);transform:scale(.75);background:#0089ec}.picker--focused .picker__day--disabled,.picker__day--disabled,.picker__day--disabled:hover{background:#f5f5f5;border-color:#f5f5f5;color:#ddd;cursor:default}.picker__day--highlighted.picker__day--disabled,.picker__day--highlighted.picker__day--disabled:hover{background:#bbb}.picker__footer{text-align:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.picker__button--clear,.picker__button--close,.picker__button--today{border:1px solid #fff;background:#fff;font-size:.8em;padding:.66em 0;font-weight:700;width:33%;display:inline-block;vertical-align:bottom}.picker__button--clear:hover,.picker__button--close:hover,.picker__button--today:hover{cursor:pointer;color:#000;background:#b1dcfb;border-bottom-color:#b1dcfb}.picker__button--clear:focus,.picker__button--close:focus,.picker__button--today:focus{background:#b1dcfb;border-color:rgba(0,0,0,.05);outline:none}.picker__button--clear:before,.picker__button--close:before,.picker__button--today:before{position:relative;display:inline-block;height:0}.picker__button--clear:before,.picker__button--today:before{content:" ";margin-right:.45em}.picker__button--today:before{top:-.05em;width:0;border-top:.66em solid #0059bc;border-left:.66em solid transparent}.picker__button--clear:before{top:-.25em;width:.66em;border-top:3px solid #e20}.picker__button--close:before{content:"\D7";top:-.1em;vertical-align:top;font-size:1.1em;margin-right:.35em;color:#777}.picker__button--today[disabled],.picker__button--today[disabled]:hover{background:#f5f5f5;border-color:#f5f5f5;color:#ddd;cursor:default}.picker__button--today[disabled]:before{border-top-color:#aaa}.picker__box{border-radius:2px;overflow:hidden}.picker__date-display{text-align:center;background-color:#26a69a;color:#fff;padding-bottom:15px;font-weight:300}.picker__nav--next:hover,.picker__nav--prev:hover{cursor:pointer;color:#000;background:#a1ded8}.picker__weekday-display{background-color:#1f897f;padding:10px;font-weight:200;letter-spacing:.5;font-size:1rem;margin-bottom:15px}.picker__month-display{text-transform:uppercase;font-size:2rem}.picker__day-display{font-size:4.5rem;font-weight:400}.picker__year-display{font-size:1.8rem;color:hsla(0,0%,100%,.4)}.picker__box{padding:0}.picker__calendar-container{padding:0 1rem}.picker__calendar-container thead{border:none}.picker__table{margin-top:0;margin-bottom:.5em}.picker__day--infocus{color:#595959;letter-spacing:-.3;padding:.75rem 0;font-weight:400;border:1px solid transparent}.picker__day.picker__day--today{color:#26a69a}.picker__day.picker__day--today.picker__day--selected{color:#fff}.picker__weekday{font-size:.9rem}.picker--focused .picker__day--selected,.picker__day--selected,.picker__day--selected:hover{border-radius:50%;-webkit-transform:scale(.9);transform:scale(.9);background-color:#26a69a;color:#fff}.picker--focused .picker__day--selected.picker__day--outfocus,.picker__day--selected.picker__day--outfocus,.picker__day--selected:hover.picker__day--outfocus{background-color:#a1ded8}.picker__footer{text-align:right;padding:5px 10px}.picker__close,.picker__today{font-size:1.1rem;padding:0 1rem;color:#26a69a}.picker__nav--next:before,.picker__nav--prev:before{content:" ";border-top:.5em solid transparent;border-bottom:.5em solid transparent;border-right:.75em solid #676767;width:0;height:0;display:block;margin:0 auto}.picker__nav--next:before{border-right:0;border-left:.75em solid #676767}button.picker__clear:focus,button.picker__close:focus,button.picker__today:focus{background-color:#a1ded8}.picker__list{list-style:none;padding:.75em 0 4.2em;margin:0}.picker__list-item{border-bottom:1px solid #ddd;border-top:1px solid #ddd;margin-bottom:-1px;position:relative;background:#fff;padding:.75em 1.25em}@media (min-height:46.75em){.picker__list-item{padding:.5em 1em}}.picker__list-item:hover{cursor:pointer;color:#000;background:#b1dcfb}.picker__list-item--highlighted,.picker__list-item:hover{border-color:#0089ec;z-index:10}.picker--focused .picker__list-item--highlighted,.picker__list-item--highlighted:hover{cursor:pointer;color:#000;background:#b1dcfb}.picker--focused .picker__list-item--selected,.picker__list-item--selected,.picker__list-item--selected:hover{background:#0089ec;color:#fff;z-index:10}.picker--focused .picker__list-item--disabled,.picker__list-item--disabled,.picker__list-item--disabled:hover{background:#f5f5f5;border-color:#f5f5f5;color:#ddd;cursor:default;border-color:#ddd;z-index:auto}.picker--time .picker__button--clear{display:block;width:80%;margin:1em auto 0;padding:1em 1.25em;background:none;border:0;font-weight:500;font-size:.67em;text-align:center;text-transform:uppercase;color:#666}.picker--time .picker__button--clear:focus,.picker--time .picker__button--clear:hover{color:#000;background:#b1dcfb;background:#e20;border-color:#e20;cursor:pointer;color:#fff;outline:none}.picker--time .picker__button--clear:before{top:-.25em;color:#666;font-size:1.25em;font-weight:700}.picker--time .picker__button--clear:focus:before,.picker--time .picker__button--clear:hover:before{color:#fff}.picker--time .picker__frame{min-width:256px;max-width:320px}.picker--time .picker__box{font-size:1em;background:#f2f2f2;padding:0}@media (min-height:40.125em){.picker--time .picker__box{margin-bottom:5em}}.annotator-filter *,.annotator-notice,.annotator-widget *{font-family:Helvetica Neue,Arial,Helvetica,sans-serif;font-weight:400;text-align:left;margin:0;padding:0;background:none;-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none;-moz-box-shadow:none;-webkit-box-shadow:none;-o-box-shadow:none;box-shadow:none;color:#909090}.annotator-adder{background-image:url(img/annotator-icon-sprite.png);background-repeat:no-repeat}.annotator-editor a:after,.annotator-filter .annotator-filter-navigation button:after,.annotator-filter .annotator-filter-property .annotator-filter-clear,.annotator-resize,.annotator-viewer .annotator-controls a,.annotator-viewer .annotator-controls button,.annotator-widget:after{background-image:url(img/annotator-glyph-sprite.png);background-repeat:no-repeat}.annotator-hl{background:#ffff0a;background:rgba(255,255,10,.3);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#4DFFFF0A, endColorstr=#4DFFFF0A)"}.annotator-hl-temporary{background:#007cff;background:rgba(0,124,255,.3);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#4D007CFF, endColorstr=#4D007CFF)"}.annotator-wrapper{position:relative}.annotator-adder,.annotator-notice,.annotator-outer{z-index:1020}.annotator-filter{z-index:1010}.annotator-adder,.annotator-notice,.annotator-outer,.annotator-widget{position:absolute;font-size:10px;line-height:1}.annotator-hide{display:none;visibility:hidden}.annotator-adder{margin-top:-48px;margin-left:-24px;width:48px;height:48px;background-position:0 0}.annotator-adder:hover{background-position:top}.annotator-adder:active{background-position:100%}.annotator-adder button{display:block;width:36px;height:41px;margin:0 auto;border:none;background:none;text-indent:-999em;cursor:pointer}.annotator-outer{width:0;height:0}.annotator-widget{margin:0;padding:0;bottom:15px;left:-18px;min-width:265px;background-color:#fbfbfb;background-color:hsla(0,0%,98%,.98);border:1px solid #7a7a7a;border:1px solid hsla(0,0%,48%,.6);-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-webkit-box-shadow:0 5px 15px rgba(0,0,0,.2);-moz-box-shadow:0 5px 15px rgba(0,0,0,.2);-o-box-shadow:0 5px 15px rgba(0,0,0,.2);box-shadow:0 5px 15px rgba(0,0,0,.2)}.annotator-invert-x .annotator-widget{left:auto;right:-18px}.annotator-invert-y .annotator-widget{bottom:auto;top:8px}.annotator-widget strong{font-weight:700}.annotator-widget .annotator-item,.annotator-widget .annotator-listing{padding:0;margin:0;list-style:none}.annotator-widget:after{content:"";display:block;width:18px;height:10px;background-position:0 0;position:absolute;bottom:-10px;left:8px}.annotator-invert-x .annotator-widget:after{left:auto;right:8px}.annotator-invert-y .annotator-widget:after{background-position:0 -15px;bottom:auto;top:-9px}.annotator-editor .annotator-item input,.annotator-editor .annotator-item textarea,.annotator-widget .annotator-item{position:relative;font-size:12px}.annotator-viewer .annotator-item{border-top:2px solid #7a7a7a;border-top:2px solid hsla(0,0%,48%,.2)}.annotator-widget .annotator-item:first-child{border-top:none}.annotator-editor .annotator-item,.annotator-viewer div{border-top:1px solid #858585;border-top:1px solid hsla(0,0%,52%,.11)}.annotator-viewer div{padding:6px}.annotator-viewer .annotator-item ol,.annotator-viewer .annotator-item ul{padding:4px 16px}.annotator-editor .annotator-item:first-child textarea,.annotator-viewer div:first-of-type{padding-top:12px;padding-bottom:12px;color:#3c3c3c;font-size:13px;font-style:italic;line-height:1.3;border-top:none}.annotator-viewer .annotator-controls{position:relative;top:5px;right:5px;padding-left:5px;opacity:0;-webkit-transition:opacity .2s ease-in;-moz-transition:opacity .2s ease-in;-o-transition:opacity .2s ease-in;transition:opacity .2s ease-in;float:right}.annotator-viewer li .annotator-controls.annotator-visible,.annotator-viewer li:hover .annotator-controls{opacity:1}.annotator-viewer .annotator-controls a,.annotator-viewer .annotator-controls button{cursor:pointer;display:inline-block;width:13px;height:13px;margin-left:2px;border:none;opacity:.2;text-indent:-900em;background-color:transparent;outline:none}.annotator-viewer .annotator-controls a:focus,.annotator-viewer .annotator-controls a:hover,.annotator-viewer .annotator-controls button:focus,.annotator-viewer .annotator-controls button:hover{opacity:.9}.annotator-viewer .annotator-controls a:active,.annotator-viewer .annotator-controls button:active{opacity:1}.annotator-viewer .annotator-controls button[disabled]{display:none}.annotator-viewer .annotator-controls .annotator-edit{background-position:0 -60px}.annotator-viewer .annotator-controls .annotator-delete{background-position:0 -75px}.annotator-viewer .annotator-controls .annotator-link{background-position:0 -270px}.annotator-editor .annotator-item{position:relative}.annotator-editor .annotator-item label{top:0;display:inline;cursor:pointer;font-size:12px}.annotator-editor .annotator-item input,.annotator-editor .annotator-item textarea{display:block;min-width:100%;padding:10px 8px;border:none;margin:0;color:#3c3c3c;background:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;resize:none}.annotator-editor .annotator-item textarea::-webkit-scrollbar{height:8px;width:8px}.annotator-editor .annotator-item textarea::-webkit-scrollbar-track-piece{margin:13px 0 3px;background-color:#e5e5e5;-webkit-border-radius:4px}.annotator-editor .annotator-item textarea::-webkit-scrollbar-thumb:vertical{height:25px;background-color:#ccc;-webkit-border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1)}.annotator-editor .annotator-item textarea::-webkit-scrollbar-thumb:horizontal{width:25px;background-color:#ccc;-webkit-border-radius:4px}.annotator-editor .annotator-item:first-child textarea{min-height:5.5em;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;-o-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.annotator-editor .annotator-item input:focus,.annotator-editor .annotator-item textarea:focus{background-color:#f3f3f3;outline:none}.annotator-editor .annotator-item input[type=checkbox],.annotator-editor .annotator-item input[type=radio]{width:auto;min-width:0;padding:0;display:inline;margin:0 4px 0 0;cursor:pointer}.annotator-editor .annotator-checkbox{padding:8px 6px}.annotator-editor .annotator-controls,.annotator-filter,.annotator-filter .annotator-filter-navigation button{text-align:right;padding:3px;border-top:1px solid #d4d4d4;background-color:#d4d4d4;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),color-stop(.6,#dcdcdc),to(#d2d2d2));background-image:-moz-linear-gradient(to bottom,#f5f5f5,#dcdcdc 60%,#d2d2d2);background-image:-webkit-linear-gradient(180deg,#f5f5f5,#dcdcdc 60%,#d2d2d2);background-image:linear-gradient(180deg,#f5f5f5,#dcdcdc 60%,#d2d2d2);-webkit-box-shadow:inset 1px 0 0 hsla(0,0%,100%,.7),inset -1px 0 0 hsla(0,0%,100%,.7),inset 0 1px 0 hsla(0,0%,100%,.7);-moz-box-shadow:inset 1px 0 0 hsla(0,0%,100%,.7),inset -1px 0 0 hsla(0,0%,100%,.7),inset 0 1px 0 hsla(0,0%,100%,.7);-o-box-shadow:inset 1px 0 0 hsla(0,0%,100%,.7),inset -1px 0 0 hsla(0,0%,100%,.7),inset 0 1px 0 hsla(0,0%,100%,.7);box-shadow:inset 1px 0 0 hsla(0,0%,100%,.7),inset -1px 0 0 hsla(0,0%,100%,.7),inset 0 1px 0 hsla(0,0%,100%,.7);-webkit-border-radius:0 0 5px 5px;-moz-border-radius:0 0 5px 5px;-o-border-radius:0 0 5px 5px;border-radius:0 0 5px 5px}.annotator-editor.annotator-invert-y .annotator-controls{border-top:none;border-bottom:1px solid #b4b4b4;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;-o-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.annotator-editor a,.annotator-filter .annotator-filter-property label{position:relative;display:inline-block;padding:0 6px 0 22px;color:#363636;text-shadow:0 1px 0 hsla(0,0%,100%,.75);text-decoration:none;line-height:24px;font-size:12px;font-weight:700;border:1px solid #a2a2a2;background-color:#d4d4d4;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),color-stop(.5,#d2d2d2),color-stop(.5,#bebebe),to(#d2d2d2));background-image:-moz-linear-gradient(to bottom,#f5f5f5,#d2d2d2 50%,#bebebe 50%,#d2d2d2);background-image:-webkit-linear-gradient(180deg,#f5f5f5,#d2d2d2 50%,#bebebe 0,#d2d2d2);background-image:linear-gradient(180deg,#f5f5f5,#d2d2d2 50%,#bebebe 0,#d2d2d2);-webkit-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-moz-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-o-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-webkit-border-radius:5px;-moz-border-radius:5px;-o-border-radius:5px;border-radius:5px}.annotator-editor a:after{position:absolute;top:50%;left:5px;display:block;content:"";width:15px;height:15px;margin-top:-7px;background-position:0 -90px}.annotator-editor a.annotator-focus,.annotator-editor a:focus,.annotator-editor a:hover,.annotator-filter .annotator-filter-active label,.annotator-filter .annotator-filter-navigation button:hover{outline:none;border-color:#435aa0;background-color:#3865f9;background-image:-webkit-gradient(linear,left top,left bottom,from(#7691fb),color-stop(.5,#5075fb),color-stop(.5,#3865f9),to(#3665fa));background-image:-moz-linear-gradient(to bottom,#7691fb,#5075fb 50%,#3865f9 50%,#3665fa);background-image:-webkit-linear-gradient(180deg,#7691fb,#5075fb 50%,#3865f9 0,#3665fa);background-image:linear-gradient(180deg,#7691fb,#5075fb 50%,#3865f9 0,#3665fa);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.42)}.annotator-editor a:focus:after,.annotator-editor a:hover:after{margin-top:-8px;background-position:0 -105px}.annotator-editor a:active,.annotator-filter .annotator-filter-navigation button:active{border-color:#700c49;background-color:#d12e8e;background-image:-webkit-gradient(linear,left top,left bottom,from(#fc7cca),color-stop(.5,#e85db2),color-stop(.5,#d12e8e),to(#ff009c));background-image:-moz-linear-gradient(to bottom,#fc7cca,#e85db2 50%,#d12e8e 50%,#ff009c);background-image:-webkit-linear-gradient(180deg,#fc7cca,#e85db2 50%,#d12e8e 0,#ff009c);background-image:linear-gradient(180deg,#fc7cca,#e85db2 50%,#d12e8e 0,#ff009c)}.annotator-editor a.annotator-save:after{background-position:0 -120px}.annotator-editor a.annotator-save.annotator-focus:after,.annotator-editor a.annotator-save:focus:after,.annotator-editor a.annotator-save:hover:after{margin-top:-8px;background-position:0 -135px}.annotator-editor .annotator-widget:after{background-position:0 -30px}.annotator-editor.annotator-invert-y .annotator-widget .annotator-controls{background-color:#f2f2f2}.annotator-editor.annotator-invert-y .annotator-widget:after{background-position:0 -45px;height:11px}.annotator-resize{position:absolute;top:0;right:0;width:12px;height:12px;background-position:2px -150px}.annotator-invert-x .annotator-resize{right:auto;left:0;background-position:0 -195px}.annotator-invert-y .annotator-resize{top:auto;bottom:0;background-position:2px -165px}.annotator-invert-y.annotator-invert-x .annotator-resize{background-position:0 -180px}.annotator-notice{color:#fff;position:fixed;top:-54px;left:0;width:100%;font-size:14px;line-height:50px;text-align:center;background:#000;background:rgba(0,0,0,.9);border-bottom:4px solid #d4d4d4;-webkit-transition:top .4s ease-out;-moz-transition:top .4s ease-out;-o-transition:top .4s ease-out;transition:top .4s ease-out}.annotator-notice-success{border-color:#3665f9}.annotator-notice-error{border-color:#ff7e00}.annotator-notice p{margin:0}.annotator-notice a{color:#fff}.annotator-notice-show{top:0}.annotator-tags{margin-bottom:-2px}.annotator-tags .annotator-tag{display:inline-block;padding:0 8px;margin-bottom:2px;line-height:1.6;font-weight:700;background-color:#e6e6e6;-webkit-border-radius:8px;-moz-border-radius:8px;-o-border-radius:8px;border-radius:8px}.annotator-filter{position:fixed;top:0;right:0;left:0;text-align:left;line-height:0;border:none;border-bottom:1px solid #878787;padding-left:10px;padding-right:10px;-webkit-border-radius:0;-moz-border-radius:0;-o-border-radius:0;border-radius:0;-webkit-box-shadow:inset 0 -1px 0 hsla(0,0%,100%,.3);-moz-box-shadow:inset 0 -1px 0 hsla(0,0%,100%,.3);-o-box-shadow:inset 0 -1px 0 hsla(0,0%,100%,.3);box-shadow:inset 0 -1px 0 hsla(0,0%,100%,.3)}.annotator-filter strong{font-size:12px;font-weight:700;color:#3c3c3c;text-shadow:0 1px 0 hsla(0,0%,100%,.7);position:relative;top:-9px}.annotator-filter .annotator-filter-navigation,.annotator-filter .annotator-filter-property{position:relative;display:inline-block;overflow:hidden;line-height:10px;padding:2px 0;margin-right:8px}.annotator-filter .annotator-filter-navigation button,.annotator-filter .annotator-filter-property label{text-align:left;display:block;float:left;line-height:20px;-webkit-border-radius:10px 0 0 10px;-moz-border-radius:10px 0 0 10px;-o-border-radius:10px 0 0 10px;border-radius:10px 0 0 10px}.annotator-filter .annotator-filter-property label{padding-left:8px}.annotator-filter .annotator-filter-property input{display:block;float:right;-webkit-appearance:none;background-color:#fff;border:1px solid #878787;border-left:none;padding:2px 4px;line-height:16px;min-height:16px;font-size:12px;width:150px;color:#333;background-color:#f8f8f8;-webkit-border-radius:0 10px 10px 0;-moz-border-radius:0 10px 10px 0;-o-border-radius:0 10px 10px 0;border-radius:0 10px 10px 0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.2);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,.2);-o-box-shadow:inset 0 1px 1px rgba(0,0,0,.2);box-shadow:inset 0 1px 1px rgba(0,0,0,.2)}.annotator-filter .annotator-filter-property input:focus{outline:none;background-color:#fff}.annotator-filter .annotator-filter-clear{position:absolute;right:3px;top:6px;border:none;text-indent:-900em;width:15px;height:15px;background-position:0 -90px;opacity:.4}.annotator-filter .annotator-filter-clear:focus,.annotator-filter .annotator-filter-clear:hover{opacity:.8}.annotator-filter .annotator-filter-clear:active{opacity:1}.annotator-filter .annotator-filter-navigation button{border:1px solid #a2a2a2;padding:0;text-indent:-900px;width:20px;min-height:22px;-webkit-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-moz-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-o-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8)}.annotator-filter .annotator-filter-navigation button,.annotator-filter .annotator-filter-navigation button:focus,.annotator-filter .annotator-filter-navigation button:hover{color:transparent}.annotator-filter .annotator-filter-navigation button:after{position:absolute;top:8px;left:8px;content:"";display:block;width:9px;height:9px;background-position:0 -210px}.annotator-filter .annotator-filter-navigation button:hover:after{background-position:0 -225px}.annotator-filter .annotator-filter-navigation .annotator-filter-next{-webkit-border-radius:0 10px 10px 0;-moz-border-radius:0 10px 10px 0;-o-border-radius:0 10px 10px 0;border-radius:0 10px 10px 0;border-left:none}.annotator-filter .annotator-filter-navigation .annotator-filter-next:after{left:auto;right:7px;background-position:0 -240px}.annotator-filter .annotator-filter-navigation .annotator-filter-next:hover:after{background-position:0 -255px}.annotator-hl-active{background:#ffff0a;background:rgba(255,255,10,.8);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#CCFFFF0A, endColorstr=#CCFFFF0A)"}.annotator-hl-filtered{background-color:transparent}@font-face{font-family:Material Icons;font-style:normal;font-weight:400;src:url(fonts/MaterialIcons-Regular.eot);src:local("Material Icons"),local("MaterialIcons-Regular"),url(fonts/MaterialIcons-Regular.woff2) format("woff2"),url(fonts/MaterialIcons-Regular.woff) format("woff"),url(fonts/MaterialIcons-Regular.ttf) format("truetype")}.material-icons{font-family:Material Icons;font-weight:400;font-style:normal;font-size:24px;display:inline-block;line-height:1;text-transform:none;letter-spacing:normal;word-wrap:normal;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:"liga"}@font-face{font-family:Lato;font-weight:100;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-hairline.woff2) format("woff2"),url(fonts/lato-hairline.woff) format("woff")}@font-face{font-family:Lato;font-weight:100;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-hairline-italic.woff2) format("woff2"),url(fonts/lato-hairline-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:200;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-thin.woff2) format("woff2"),url(fonts/lato-thin.woff) format("woff")}@font-face{font-family:Lato;font-weight:200;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-thin-italic.woff2) format("woff2"),url(fonts/lato-thin-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:300;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-light.woff2) format("woff2"),url(fonts/lato-light.woff) format("woff")}@font-face{font-family:Lato;font-weight:300;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-light-italic.woff2) format("woff2"),url(fonts/lato-light-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:400;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-normal.woff2) format("woff2"),url(fonts/lato-normal.woff) format("woff")}@font-face{font-family:Lato;font-weight:400;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-normal-italic.woff2) format("woff2"),url(fonts/lato-normal-italic.woff) format("woff")}@font-face{font-family:Lato Medium;font-weight:400;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-medium.woff2) format("woff2"),url(fonts/lato-medium.woff) format("woff")}@font-face{font-family:Lato Medium;font-weight:400;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-medium-italic.woff2) format("woff2"),url(fonts/lato-medium-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:500;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-semibold.woff2) format("woff2"),url(fonts/lato-semibold.woff) format("woff")}@font-face{font-family:Lato;font-weight:500;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-semibold-italic.woff2) format("woff2"),url(fonts/lato-semibold-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:600;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-bold.woff2) format("woff2"),url(fonts/lato-bold.woff) format("woff")}@font-face{font-family:Lato;font-weight:600;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-bold-italic.woff2) format("woff2"),url(fonts/lato-bold-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:800;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-heavy.woff2) format("woff2"),url(fonts/lato-heavy.woff) format("woff")}@font-face{font-family:Lato;font-weight:800;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-heavy-italic.woff2) format("woff2"),url(fonts/lato-heavy-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:900;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-black.woff2) format("woff2"),url(fonts/lato-black.woff) format("woff")}@font-face{font-family:Lato;font-weight:900;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-black-italic.woff2) format("woff2"),url(fonts/lato-black-italic.woff) format("woff")}.material-icons.md-18{font-size:18px}.material-icons.md-24{font-size:24px}.material-icons.md-36{font-size:36px}.material-icons.md-48{font-size:48px}.material-icons.md-dark{color:rgba(0,0,0,.54)}.material-icons.md-dark.md-inactive{color:rgba(0,0,0,.26)}.material-icons.md-light{color:#fff}.material-icons.md-light.md-inactive{color:hsla(0,0%,100%,.3)}#article{font-size:20px;margin:0 auto;max-width:45em}#article article{color:#424242;font-size:18px;line-height:1.7em}#article article h1,#article article h2,#article article h3,#article article h4,#article article h5,#article article h6{color:#212121}#article article h1 strong,#article article h2 strong,#article article h3 strong,#article article h4 strong,#article article h5 strong,#article article h6 strong{font-weight:500}#article article h6{font-size:1.2rem}#article article h5{font-size:1.6rem}#article article h4{font-size:1.9rem}#article article h3{font-size:2.2rem}#article article h2{font-size:2.5rem}#article article h1{font-size:2.7rem}#article article a{border-bottom:1px dotted #03a9f4;text-decoration:none}#article article a:hover{border-bottom-style:solid}#article article ul{padding-left:30px}#article article ul,#article article ul li{list-style-type:disc}#article article blockquote{font-style:italic}#article article strong{font-weight:700}#article figure,#article img{max-width:100%;height:auto}#article pre{box-sizing:border-box;margin:0 0 1.75em;border:1px solid #e3f2fd;width:100%;padding:10px;font-family:monospace;font-size:.8em;white-space:pre;overflow:auto;background:#f5f5f5;border-radius:3px}#article>header>h1{font-size:2em;margin:2.1rem 0 .68rem}#article aside .tools{display:flex;flex-flow:row wrap}#article aside .tools .stats{font-size:.8em;margin:8px 5px 5px}#article aside .tools .stats li{display:inline-flex;vertical-align:middle;margin:3px 5px}#article aside .tools .stats li i.material-icons{color:#3e3e3e;margin-right:3px}#article aside .tools .stats a{color:#000;text-decoration:none}#article aside .tools .tags{float:right;margin:5px 15px 10px}#article aside .chip{background-color:rgba(0,151,167,.85);padding:0 15px 0 10px;margin:auto 2px;border-radius:6px}#article aside .chip a,#article aside .chip i{color:#fff}#article aside .chip i.material-icons{float:right;font-size:20px;line-height:32px;padding-left:8px}.reader-mode{width:70px!important;transition:width .2s ease}.reader-mode .collapsible-body{height:0;overflow:hidden}.reader-mode span{opacity:0;transition:opacity .2s ease}.reader-mode:hover{width:260px!important}.reader-mode:hover .collapsible-body{height:auto}.reader-mode:hover .collapsible-body li a i.material-icons{margin:auto 5px auto -8px}.reader-mode:hover span{opacity:1}.progress{position:fixed;top:0;width:100%;height:3px;margin:0;z-index:9999}main #content{padding:0 .5rem}main ul.row{margin:.4rem 0 0;padding:0 .75rem}.data .card .card-body{height:19em;overflow:hidden}.card .card-content .card-title,.card .card-reveal .card-title{line-height:22.8px;max-height:80px;font-size:19px;font-family:roberto,Helvetica Neue,Helvetica,Arial,sans-serif}.card .card-stacked .card-content .card-title{display:inline-block}.card .card-content .activator,.card .card-reveal .activator{cursor:pointer;font-family:Material Icons}.card .card-content i.right,.card .card-reveal i.right{margin-left:0}.card .card-content .original{line-height:24px;font-size:15px}.card .card-entry-labels{position:absolute;top:10px;z-index:90;max-width:50%}.card .card-entry-labels-hidden{margin:2.5px auto}.card .card-entry-labels-hidden li{display:inline-block;background-color:rgba(0,151,167,.85);margin:0 5px;padding:5px 12px;border-radius:3px;color:#fff;max-height:2em;max-width:calc(100% - 15px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card .card-content .estimatedTime{margin-bottom:10px}.card .card-action{padding:10px 5px 10px 15px}.card .card-action ul.links{margin:0;font-size:24px;line-height:24px}.card .card-action a{color:#fff;margin:0}.card .card-action a:hover{color:#fff}.card .card-action ul.tools li a.tool{margin-right:5px!important}.card .card-action .reading-time{display:inline-flex;vertical-align:middle}.card .card-action .reading-time span{margin-right:5px}.card .card-image{height:10em}.card .card-fullimage{height:13.5em}.card .card-fullimage .preview,.card .card-image .preview{height:14em;background:no-repeat 50%/cover;display:block}.card.sw{max-width:370px;margin-left:auto;margin-right:auto}a.original:not(.waves-effect){text-overflow:ellipsis;white-space:nowrap;overflow:hidden;display:block}.card-entry-labels li,.card-tag-labels li{margin:10px 10px 10px auto;padding:5px 12px 5px 16px!important;background-color:rgba(0,151,167,.85);border-radius:3px;color:#fff;cursor:default}.card-entry-labels li{text-overflow:ellipsis;white-space:nowrap;border-radius:0 3px 3px 0;overflow:hidden}.card-tag-labels li{display:flex;justify-content:space-between}#list .chip a,.card-entry-labels-hidden a,.card-entry-labels a,.card-entry-tags a,.card-tag-labels a{text-decoration:none;font-weight:400;color:#fff}.card-tag-labels a{height:100%;align-items:center}.card-tag-form,.card-tag-link{display:flex;min-width:100px;flex-grow:1}.card-tag-form input{margin-bottom:0;height:2rem}.card-tag-rss{display:flex}.card-tag-labels{display:flex;flex-wrap:wrap}.card-tag-labels li{margin:10px;flex-basis:19%;flex-grow:1;align-items:center}.card-stacked{display:flex;flex-flow:row wrap}.card-stacked:hover ul.tools-list{display:inline;text-align:right}.card-stacked .preview{max-width:100px;height:auto;margin-right:10px;flex:1}.card-stacked .preview img{max-width:100%;max-height:100%}.card-stacked div.metadata .chip{background-color:rgba(0,151,167,.85);padding:0 15px 0 10px;margin:auto 2px;border-radius:6px}.card-stacked div.metadata .chip a,.card-stacked div.metadata .chip i{color:#fff}.card-stacked div.metadata .chip i.material-icons{float:right;font-size:20px;line-height:32px;padding-left:8px}.card-stacked div.card-content{flex:4}.card-stacked ul.tools-list{flex:1;display:none;flex-basis:5em;align-self:flex-end;float:right;max-width:6em}.card-stacked .tags{display:inline-block}#content .collection .collection-item{min-height:65px;height:auto}.quickstart .card .card-action a,.quickstart .card .card-action a:hover{color:#fff!important}.settings .div_tabs{padding-bottom:15px}@media only screen and (min-width:992px){.card-tag-labels li{max-width:50%}}.collection{margin:15px 15px 0}.collection .collection-item{padding:7px;height:65px}.results{display:flex;padding:1rem 1rem 0;flex-wrap:wrap;justify-content:space-between}.results .nb-results{display:inline-flex}.results a{color:#444}.pagination ul{display:flex;margin:0;flex-wrap:wrap;justify-content:space-around}.pagination ul .next.disabled,.pagination ul .prev.disabled{display:none}.pagination li{padding:0}.pagination a,.pagination span{padding:0 10px;height:30px;display:block;line-height:30px}.pagination .disabled{margin-right:10px;margin-left:10px}.pagination li.active span{padding:0 10px;height:30px;display:block;color:#fff}.footer-text{margin:.7rem .5rem}.hidden,.picker__date-display{display:none}footer.page-footer{margin-top:10px;padding-top:0}footer .row{margin-bottom:10px}#filters button{padding:0;width:100%}#filters div.with-checkbox{height:3rem;margin-top:0}body{display:flex;min-height:100vh;flex-direction:column;background:#fafafa}body.login main{padding:0;min-height:100vh}.border-bottom{border-bottom:1px solid #ddd}#content,.valign-wrapper,main{height:100%}#main{flex:1 0 auto}#main .logo a{height:100pt}#main .logo img{height:100pt;width:100pt}#main .logo:hover{background:transparent}nav{height:auto;line-height:normal}nav input{color:#aaa}nav ul a:hover{background-color:initial}.nav-panel-item .button-collapse{margin-left:0;margin-right:.5rem;padding:0 .5rem;height:auto;line-height:1;background-color:transparent;border:none}.nav-panel-item{display:flex;padding:.6rem .4rem .6rem .75rem;flex-wrap:wrap;justify-content:space-between;align-items:center}.nav-panel-item .material-icons{height:46px;line-height:46px}.nav-input{display:none}.nav-panel-buttom{display:flex;flex-grow:1;justify-content:flex-end}.nav-panel-item .add,.nav-panel-item .search,.nav-panels .close{color:#444!important}.nav-panels{transition:background .2s ease}.nav-panels .action{margin:0;font-size:2.1rem}.nav-panels .input-field input{display:block;line-height:inherit;height:3rem}.nav-panels .input-field input:focus{background-color:#fff;border:0;box-shadow:none;color:#444}.nav-panel-top{display:flex;align-items:center}.input-field.nav-panel-item label{left:1rem}.input-field.nav-panel-item .close{color:transparent;cursor:pointer;font-size:2rem;transition:color .3s}.input-field.nav-panel-item{display:flex;flex:1;flex-wrap:nowrap;align-items:center}.input-field.nav-panel-add.disabled,.input-field.nav-panel-add.disabled input{background-color:#f5f5f5}.nav-form-button{padding:0;background-color:transparent;border:none}.nav-form-button:focus{background-color:inherit}.nav-form-button,.nav-panel-item .close{margin:0 1%}#button_export,#button_filters{display:none}@media (min-width:993px){.button-collapse{display:none}}.side-nav{width:240px}.side-nav li{padding:0}.side-nav li.logo>a:hover{background:initial}.side-nav a{margin:0}.side-nav.fixed a{font-size:13px;line-height:44px;height:44px}.side-nav .collapsible-header,.side-nav.fixed .collapsible-header{height:45px;line-height:44px;padding:0 20px}.side-nav>li.logo{line-height:0;text-align:center}.bold>a{font-weight:700}span.numberItems{float:right}div.settings div.file-field div,div.settings div.file-field ul{margin-top:40px}div.settings div.file-field div{margin-top:inherit}.input-field label.active{font-size:1rem}nav .input-field input{margin:0;padding-left:.5rem}.tabs{display:flex}.tab{flex:1}@font-face{font-family:icomoon;src:url(fonts/IcoMoon-Free.ttf);font-weight:400;font-style:normal}.material-icons{font-family:Material Icons;font-weight:400;font-style:normal;font-size:24px;width:1em;height:1em;display:inline-block;line-height:1;text-transform:none;letter-spacing:normal;word-wrap:normal;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:"liga"}.material-icons .md-18{font-size:18px}.material-icons .md-24{font-size:24px}.material-icons .md-36{font-size:36px}.material-icons .md-48{font-size:48px}.material-icons .md-dark{color:rgba(0,0,0,.54)}.material-icons .md-dark .md-inactive{color:rgba(0,0,0,.26)}.material-icons .md-light{color:#fff}.material-icons .md-light .md-inactive{color:hsla(0,0%,100%,.3)}[class*=" icon-"]:before,[class^=icon-]:before{font-family:icomoon;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;background-size:24px;letter-spacing:0;font-feature-settings:"liga"}.icon-eye:before{content:"\E9CE"}.icon-no-eye:before{content:"\E9D1"}.icon-calendar:before{content:"\E953"}.icon-mail:before{content:"\EA86"}.icon-time:before{content:"\E952"}a.icon-image{background-repeat:no-repeat;padding-right:.4em!important;padding-left:0!important;margin-left:25px}a.icon-image:before{content:"";display:block;width:24px;height:24px;float:left;margin:7px 1.5px 0 0}a.icon-image.carrot:before{background:url(themes/_global/img/icons/carrot-icon--black.png) no-repeat 50%/90%}a.icon-image.diaspora:before{background:url(themes/_global/img/icons/diaspora-icon--black.png) no-repeat 50%/80%}a.icon-image.unmark:before{background:url(themes/_global/img/icons/unmark-icon--black.png) no-repeat 50%/80%}a.icon-image.shaarli:before{background:url(themes/_global/img/icons/shaarli.png) no-repeat 50%/80%}a.icon-image.scuttle:before{background:url(themes/_global/img/icons/scuttle.png) no-repeat 50%/80%}.icon-google-plus2:before{content:"\EA89"}.icon-facebook2:before{content:"\EA8D"}.icon-twitter:before{content:"\EA96"}.icon-apple:before{content:"\EABF"}.icon-android:before{content:"\EAC1"}.icon-chrome:before{content:"\EAE5"}.icon-firefox:before{content:"\EAE6"}.icon-link:before{content:"\E9CB"}footer [class*=" icon-"],footer [class^=icon-]{font-size:2em;transition:text-shadow .2s ease;padding-right:10px}footer [class*=" icon-"]:hover,footer [class^=icon-]:hover{text-shadow:0 0 10px rgba(0,0,0,.3)}@media print{body{font-family:Serif;background-color:#fff}@page{margin:1cm}img{max-width:100%!important}#article .mbm a,#article>aside,#article_toolbar,#links,#slide-out,#sort,.entry+.results,.hide-on-large-only,.messages,.progress,.top_link,body>footer,body>header,div.tools,header div{display:none!important}main{padding-left:0!important}#article{margin:inherit!important}article{border:none!important}.vieworiginal a:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.pagination span.current{border-style:dashed}#main{margin:0;padding:0}#article,#main{width:100%}}@media only screen and (min-width:992px){body:not(.entry):not(.login) main,footer,nav{padding-left:240px}.pagination{margin-left:auto}}@media only screen and (max-width:992px){footer,header,main,nav{padding-left:0}.nav-panels .action{padding-right:.75rem}.nav-panel-buttom{justify-content:space-between}#article{max-width:35em;margin-left:auto;margin-right:auto;font-size:18px}#article>header>h1{font-size:1.33em}.reader-mode{width:240px!important}.reader-mode span{opacity:1}.tabs{display:inline-block;height:auto}.tab{min-width:100%}.indicator{display:none}.pagination li{margin-bottom:.5rem}.pagination li.next,.pagination li.prev{width:auto}.drag-target+.drag-target{height:50%}.drag-target+.drag-target+.drag-target{top:50%}}@media only screen and (min-width:1200px) and (max-width:1650px){.row .col.l3{width:33.33333%;margin-left:0}}@media only screen and (min-width:993px) and (max-width:1200px){.row .col.l1{width:25%;margin-left:0}.row .col.l2{width:33.33333%;margin-left:0}.row .col.l3{width:41.66667%;margin-left:0}.row .col.l4{width:50%;margin-left:0}.row .col.l5{width:58.33333%;margin-left:0}.row .col.l6{width:66.66667%;margin-left:0}.row .col.l7{width:75%;margin-left:0}.row .col.l8{width:83.33333%;margin-left:0}.row .col.l9{width:91.66667%;margin-left:0}.row .col.l10{width:100%;margin-left:0}}@media only screen and (max-width:350px){.nb-results{display:none}.row .col,main ul.row{padding:0}} +/*# sourceMappingURL=material.css.map*/ From 03b2058dbe792f539611fe77ae6e730f5b60ff86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20HULARD?= Date: Wed, 24 Jan 2018 17:53:00 +0100 Subject: [PATCH 011/107] Add tests about the tag renaming process. --- .../Controller/TagControllerTest.php | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/Wallabag/CoreBundle/Controller/TagControllerTest.php b/tests/Wallabag/CoreBundle/Controller/TagControllerTest.php index 768f4c078..be17dcf51 100644 --- a/tests/Wallabag/CoreBundle/Controller/TagControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/TagControllerTest.php @@ -176,4 +176,49 @@ class TagControllerTest extends WallabagCoreTestCase $em->remove($tag); $em->flush(); } + + public function testRenameTagUsingTheFormInsideTagList() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $tag = new Tag(); + $tag->setLabel($this->tagName); + $entry = new Entry($this->getLoggedInUser()); + $entry->setUrl('http://0.0.0.0/foo'); + $entry->addTag($tag); + $this->getEntityManager()->persist($entry); + $this->getEntityManager()->flush(); + $this->getEntityManager()->clear(); + + // We make a first request to set an history and test redirection after tag deletion + $crawler = $client->request('GET', '/tag/list'); + $form = $crawler->filter('#tag-' . $tag->getId() . ' form')->form(); + + $data = [ + 'tag[label]' => 'specific label', + ]; + + $client->submit($form, $data); + $this->assertSame(302, $client->getResponse()->getStatusCode()); + + $freshEntry = $client->getContainer() + ->get('doctrine.orm.entity_manager') + ->getRepository('WallabagCoreBundle:Entry') + ->find($entry->getId()); + + $tags = $freshEntry->getTags()->toArray(); + foreach ($tags as $key => $item) { + $tags[$key] = $item->getLabel(); + } + + $this->assertFalse(array_search($tag->getLabel(), $tags, true), 'Previous tag is not attach to entry anymore.'); + + $newTag = $client->getContainer() + ->get('doctrine.orm.entity_manager') + ->getRepository('WallabagCoreBundle:Tag') + ->findOneByLabel('specific label'); + $this->assertInstanceOf(Tag::class, $newTag, 'Tag "specific label" exists.'); + $this->assertTrue($newTag->hasEntry($freshEntry), 'Tag "specific label" is assigned to the entry.'); + } } From 84d59603c53878bfba32a8fb846d3c00e756c4f1 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Tue, 25 Sep 2018 10:21:21 +0200 Subject: [PATCH 012/107] Update assets --- web/wallassets/baggy.css | 2 +- web/wallassets/baggy.js | 2 +- web/wallassets/manifest.json | 3 +-- web/wallassets/material.css | 4 ++-- web/wallassets/material.js | 2 +- web/wallassets/public.js | 2 +- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/web/wallassets/baggy.css b/web/wallassets/baggy.css index 8c1ed86bd..fb8084679 100644 --- a/web/wallassets/baggy.css +++ b/web/wallassets/baggy.css @@ -1,2 +1,2 @@ -.annotator-filter *,.annotator-notice,.annotator-widget *{font-family:Helvetica Neue,Arial,Helvetica,sans-serif;font-weight:400;text-align:left;margin:0;padding:0;background:none;-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none;-moz-box-shadow:none;-webkit-box-shadow:none;-o-box-shadow:none;box-shadow:none;color:#909090}.annotator-adder{background-image:url(img/annotator-icon-sprite.png);background-repeat:no-repeat}.annotator-editor a:after,.annotator-filter .annotator-filter-navigation button:after,.annotator-filter .annotator-filter-property .annotator-filter-clear,.annotator-resize,.annotator-viewer .annotator-controls a,.annotator-viewer .annotator-controls button,.annotator-widget:after{background-image:url(img/annotator-glyph-sprite.png);background-repeat:no-repeat}.annotator-hl{background:#ffff0a;background:rgba(255,255,10,.3);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#4DFFFF0A, endColorstr=#4DFFFF0A)"}.annotator-hl-temporary{background:#007cff;background:rgba(0,124,255,.3);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#4D007CFF, endColorstr=#4D007CFF)"}.annotator-wrapper{position:relative}.annotator-adder,.annotator-notice,.annotator-outer{z-index:1020}.annotator-filter{z-index:1010}.annotator-adder,.annotator-notice,.annotator-outer,.annotator-widget{position:absolute;font-size:10px;line-height:1}.annotator-hide{display:none;visibility:hidden}.annotator-adder{margin-top:-48px;margin-left:-24px;width:48px;height:48px;background-position:0 0}.annotator-adder:hover{background-position:top}.annotator-adder:active{background-position:100%}.annotator-adder button{display:block;width:36px;height:41px;margin:0 auto;border:none;background:none;text-indent:-999em;cursor:pointer}.annotator-outer{width:0;height:0}.annotator-widget{margin:0;padding:0;bottom:15px;left:-18px;min-width:265px;background-color:#fbfbfb;background-color:hsla(0,0%,98%,.98);border:1px solid #7a7a7a;border:1px solid hsla(0,0%,48%,.6);-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-webkit-box-shadow:0 5px 15px rgba(0,0,0,.2);-moz-box-shadow:0 5px 15px rgba(0,0,0,.2);-o-box-shadow:0 5px 15px rgba(0,0,0,.2);box-shadow:0 5px 15px rgba(0,0,0,.2)}.annotator-invert-x .annotator-widget{left:auto;right:-18px}.annotator-invert-y .annotator-widget{bottom:auto;top:8px}.annotator-widget strong{font-weight:700}.annotator-widget .annotator-item,.annotator-widget .annotator-listing{padding:0;margin:0;list-style:none}.annotator-widget:after{content:"";display:block;width:18px;height:10px;background-position:0 0;position:absolute;bottom:-10px;left:8px}.annotator-invert-x .annotator-widget:after{left:auto;right:8px}.annotator-invert-y .annotator-widget:after{background-position:0 -15px;bottom:auto;top:-9px}.annotator-editor .annotator-item input,.annotator-editor .annotator-item textarea,.annotator-widget .annotator-item{position:relative;font-size:12px}.annotator-viewer .annotator-item{border-top:2px solid #7a7a7a;border-top:2px solid hsla(0,0%,48%,.2)}.annotator-widget .annotator-item:first-child{border-top:none}.annotator-editor .annotator-item,.annotator-viewer div{border-top:1px solid #858585;border-top:1px solid hsla(0,0%,52%,.11)}.annotator-viewer div{padding:6px}.annotator-viewer .annotator-item ol,.annotator-viewer .annotator-item ul{padding:4px 16px}.annotator-editor .annotator-item:first-child textarea,.annotator-viewer div:first-of-type{padding-top:12px;padding-bottom:12px;color:#3c3c3c;font-size:13px;font-style:italic;line-height:1.3;border-top:none}.annotator-viewer .annotator-controls{position:relative;top:5px;right:5px;padding-left:5px;opacity:0;-webkit-transition:opacity .2s ease-in;-moz-transition:opacity .2s ease-in;-o-transition:opacity .2s ease-in;transition:opacity .2s ease-in;float:right}.annotator-viewer li .annotator-controls.annotator-visible,.annotator-viewer li:hover .annotator-controls{opacity:1}.annotator-viewer .annotator-controls a,.annotator-viewer .annotator-controls button{cursor:pointer;display:inline-block;width:13px;height:13px;margin-left:2px;border:none;opacity:.2;text-indent:-900em;background-color:transparent;outline:none}.annotator-viewer .annotator-controls a:focus,.annotator-viewer .annotator-controls a:hover,.annotator-viewer .annotator-controls button:focus,.annotator-viewer .annotator-controls button:hover{opacity:.9}.annotator-viewer .annotator-controls a:active,.annotator-viewer .annotator-controls button:active{opacity:1}.annotator-viewer .annotator-controls button[disabled]{display:none}.annotator-viewer .annotator-controls .annotator-edit{background-position:0 -60px}.annotator-viewer .annotator-controls .annotator-delete{background-position:0 -75px}.annotator-viewer .annotator-controls .annotator-link{background-position:0 -270px}.annotator-editor .annotator-item{position:relative}.annotator-editor .annotator-item label{top:0;display:inline;cursor:pointer;font-size:12px}.annotator-editor .annotator-item input,.annotator-editor .annotator-item textarea{display:block;min-width:100%;padding:10px 8px;border:none;margin:0;color:#3c3c3c;background:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;resize:none}.annotator-editor .annotator-item textarea::-webkit-scrollbar{height:8px;width:8px}.annotator-editor .annotator-item textarea::-webkit-scrollbar-track-piece{margin:13px 0 3px;background-color:#e5e5e5;-webkit-border-radius:4px}.annotator-editor .annotator-item textarea::-webkit-scrollbar-thumb:vertical{height:25px;background-color:#ccc;-webkit-border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1)}.annotator-editor .annotator-item textarea::-webkit-scrollbar-thumb:horizontal{width:25px;background-color:#ccc;-webkit-border-radius:4px}.annotator-editor .annotator-item:first-child textarea{min-height:5.5em;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;-o-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.annotator-editor .annotator-item input:focus,.annotator-editor .annotator-item textarea:focus{background-color:#f3f3f3;outline:none}.annotator-editor .annotator-item input[type=checkbox],.annotator-editor .annotator-item input[type=radio]{width:auto;min-width:0;padding:0;display:inline;margin:0 4px 0 0;cursor:pointer}.annotator-editor .annotator-checkbox{padding:8px 6px}.annotator-editor .annotator-controls,.annotator-filter,.annotator-filter .annotator-filter-navigation button{text-align:right;padding:3px;border-top:1px solid #d4d4d4;background-color:#d4d4d4;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),color-stop(.6,#dcdcdc),to(#d2d2d2));background-image:-moz-linear-gradient(to bottom,#f5f5f5,#dcdcdc 60%,#d2d2d2);background-image:-webkit-linear-gradient(180deg,#f5f5f5,#dcdcdc 60%,#d2d2d2);background-image:linear-gradient(180deg,#f5f5f5,#dcdcdc 60%,#d2d2d2);-webkit-box-shadow:inset 1px 0 0 hsla(0,0%,100%,.7),inset -1px 0 0 hsla(0,0%,100%,.7),inset 0 1px 0 hsla(0,0%,100%,.7);-moz-box-shadow:inset 1px 0 0 hsla(0,0%,100%,.7),inset -1px 0 0 hsla(0,0%,100%,.7),inset 0 1px 0 hsla(0,0%,100%,.7);-o-box-shadow:inset 1px 0 0 hsla(0,0%,100%,.7),inset -1px 0 0 hsla(0,0%,100%,.7),inset 0 1px 0 hsla(0,0%,100%,.7);box-shadow:inset 1px 0 0 hsla(0,0%,100%,.7),inset -1px 0 0 hsla(0,0%,100%,.7),inset 0 1px 0 hsla(0,0%,100%,.7);-webkit-border-radius:0 0 5px 5px;-moz-border-radius:0 0 5px 5px;-o-border-radius:0 0 5px 5px;border-radius:0 0 5px 5px}.annotator-editor.annotator-invert-y .annotator-controls{border-top:none;border-bottom:1px solid #b4b4b4;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;-o-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.annotator-editor a,.annotator-filter .annotator-filter-property label{position:relative;display:inline-block;padding:0 6px 0 22px;color:#363636;text-shadow:0 1px 0 hsla(0,0%,100%,.75);text-decoration:none;line-height:24px;font-size:12px;font-weight:700;border:1px solid #a2a2a2;background-color:#d4d4d4;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),color-stop(.5,#d2d2d2),color-stop(.5,#bebebe),to(#d2d2d2));background-image:-moz-linear-gradient(to bottom,#f5f5f5,#d2d2d2 50%,#bebebe 50%,#d2d2d2);background-image:-webkit-linear-gradient(180deg,#f5f5f5,#d2d2d2 50%,#bebebe 0,#d2d2d2);background-image:linear-gradient(180deg,#f5f5f5,#d2d2d2 50%,#bebebe 0,#d2d2d2);-webkit-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-moz-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-o-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-webkit-border-radius:5px;-moz-border-radius:5px;-o-border-radius:5px;border-radius:5px}.annotator-editor a:after{position:absolute;top:50%;left:5px;display:block;content:"";width:15px;height:15px;margin-top:-7px;background-position:0 -90px}.annotator-editor a.annotator-focus,.annotator-editor a:focus,.annotator-editor a:hover,.annotator-filter .annotator-filter-active label,.annotator-filter .annotator-filter-navigation button:hover{outline:none;border-color:#435aa0;background-color:#3865f9;background-image:-webkit-gradient(linear,left top,left bottom,from(#7691fb),color-stop(.5,#5075fb),color-stop(.5,#3865f9),to(#3665fa));background-image:-moz-linear-gradient(to bottom,#7691fb,#5075fb 50%,#3865f9 50%,#3665fa);background-image:-webkit-linear-gradient(180deg,#7691fb,#5075fb 50%,#3865f9 0,#3665fa);background-image:linear-gradient(180deg,#7691fb,#5075fb 50%,#3865f9 0,#3665fa);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.42)}.annotator-editor a:focus:after,.annotator-editor a:hover:after{margin-top:-8px;background-position:0 -105px}.annotator-editor a:active,.annotator-filter .annotator-filter-navigation button:active{border-color:#700c49;background-color:#d12e8e;background-image:-webkit-gradient(linear,left top,left bottom,from(#fc7cca),color-stop(.5,#e85db2),color-stop(.5,#d12e8e),to(#ff009c));background-image:-moz-linear-gradient(to bottom,#fc7cca,#e85db2 50%,#d12e8e 50%,#ff009c);background-image:-webkit-linear-gradient(180deg,#fc7cca,#e85db2 50%,#d12e8e 0,#ff009c);background-image:linear-gradient(180deg,#fc7cca,#e85db2 50%,#d12e8e 0,#ff009c)}.annotator-editor a.annotator-save:after{background-position:0 -120px}.annotator-editor a.annotator-save.annotator-focus:after,.annotator-editor a.annotator-save:focus:after,.annotator-editor a.annotator-save:hover:after{margin-top:-8px;background-position:0 -135px}.annotator-editor .annotator-widget:after{background-position:0 -30px}.annotator-editor.annotator-invert-y .annotator-widget .annotator-controls{background-color:#f2f2f2}.annotator-editor.annotator-invert-y .annotator-widget:after{background-position:0 -45px;height:11px}.annotator-resize{position:absolute;top:0;right:0;width:12px;height:12px;background-position:2px -150px}.annotator-invert-x .annotator-resize{right:auto;left:0;background-position:0 -195px}.annotator-invert-y .annotator-resize{top:auto;bottom:0;background-position:2px -165px}.annotator-invert-y.annotator-invert-x .annotator-resize{background-position:0 -180px}.annotator-notice{color:#fff;position:fixed;top:-54px;left:0;width:100%;font-size:14px;line-height:50px;text-align:center;background:#000;background:rgba(0,0,0,.9);border-bottom:4px solid #d4d4d4;-webkit-transition:top .4s ease-out;-moz-transition:top .4s ease-out;-o-transition:top .4s ease-out;transition:top .4s ease-out}.annotator-notice-success{border-color:#3665f9}.annotator-notice-error{border-color:#ff7e00}.annotator-notice p{margin:0}.annotator-notice a{color:#fff}.annotator-notice-show{top:0}.annotator-tags{margin-bottom:-2px}.annotator-tags .annotator-tag{display:inline-block;padding:0 8px;margin-bottom:2px;line-height:1.6;font-weight:700;background-color:#e6e6e6;-webkit-border-radius:8px;-moz-border-radius:8px;-o-border-radius:8px;border-radius:8px}.annotator-filter{position:fixed;top:0;right:0;left:0;text-align:left;line-height:0;border:none;border-bottom:1px solid #878787;padding-left:10px;padding-right:10px;-webkit-border-radius:0;-moz-border-radius:0;-o-border-radius:0;border-radius:0;-webkit-box-shadow:inset 0 -1px 0 hsla(0,0%,100%,.3);-moz-box-shadow:inset 0 -1px 0 hsla(0,0%,100%,.3);-o-box-shadow:inset 0 -1px 0 hsla(0,0%,100%,.3);box-shadow:inset 0 -1px 0 hsla(0,0%,100%,.3)}.annotator-filter strong{font-size:12px;font-weight:700;color:#3c3c3c;text-shadow:0 1px 0 hsla(0,0%,100%,.7);position:relative;top:-9px}.annotator-filter .annotator-filter-navigation,.annotator-filter .annotator-filter-property{position:relative;display:inline-block;overflow:hidden;line-height:10px;padding:2px 0;margin-right:8px}.annotator-filter .annotator-filter-navigation button,.annotator-filter .annotator-filter-property label{text-align:left;display:block;float:left;line-height:20px;-webkit-border-radius:10px 0 0 10px;-moz-border-radius:10px 0 0 10px;-o-border-radius:10px 0 0 10px;border-radius:10px 0 0 10px}.annotator-filter .annotator-filter-property label{padding-left:8px}.annotator-filter .annotator-filter-property input{display:block;float:right;-webkit-appearance:none;background-color:#fff;border:1px solid #878787;border-left:none;padding:2px 4px;line-height:16px;min-height:16px;font-size:12px;width:150px;color:#333;background-color:#f8f8f8;-webkit-border-radius:0 10px 10px 0;-moz-border-radius:0 10px 10px 0;-o-border-radius:0 10px 10px 0;border-radius:0 10px 10px 0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.2);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,.2);-o-box-shadow:inset 0 1px 1px rgba(0,0,0,.2);box-shadow:inset 0 1px 1px rgba(0,0,0,.2)}.annotator-filter .annotator-filter-property input:focus{outline:none;background-color:#fff}.annotator-filter .annotator-filter-clear{position:absolute;right:3px;top:6px;border:none;text-indent:-900em;width:15px;height:15px;background-position:0 -90px;opacity:.4}.annotator-filter .annotator-filter-clear:focus,.annotator-filter .annotator-filter-clear:hover{opacity:.8}.annotator-filter .annotator-filter-clear:active{opacity:1}.annotator-filter .annotator-filter-navigation button{border:1px solid #a2a2a2;padding:0;text-indent:-900px;width:20px;min-height:22px;-webkit-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-moz-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-o-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8)}.annotator-filter .annotator-filter-navigation button,.annotator-filter .annotator-filter-navigation button:focus,.annotator-filter .annotator-filter-navigation button:hover{color:transparent}.annotator-filter .annotator-filter-navigation button:after{position:absolute;top:8px;left:8px;content:"";display:block;width:9px;height:9px;background-position:0 -210px}.annotator-filter .annotator-filter-navigation button:hover:after{background-position:0 -225px}.annotator-filter .annotator-filter-navigation .annotator-filter-next{-webkit-border-radius:0 10px 10px 0;-moz-border-radius:0 10px 10px 0;-o-border-radius:0 10px 10px 0;border-radius:0 10px 10px 0;border-left:none}.annotator-filter .annotator-filter-navigation .annotator-filter-next:after{left:auto;right:7px;background-position:0 -240px}.annotator-filter .annotator-filter-navigation .annotator-filter-next:hover:after{background-position:0 -255px}.annotator-hl-active{background:#ffff0a;background:rgba(255,255,10,.8);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#CCFFFF0A, endColorstr=#CCFFFF0A)"}.annotator-hl-filtered{background-color:transparent}@font-face{font-family:Material Icons;font-style:normal;font-weight:400;src:url(fonts/MaterialIcons-Regular.eot);src:local("Material Icons"),local("MaterialIcons-Regular"),url(fonts/MaterialIcons-Regular.woff2) format("woff2"),url(fonts/MaterialIcons-Regular.woff) format("woff"),url(fonts/MaterialIcons-Regular.ttf) format("truetype")}.material-icons{font-family:Material Icons;font-weight:400;font-style:normal;font-size:24px;display:inline-block;line-height:1;text-transform:none;letter-spacing:normal;word-wrap:normal;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:"liga"}@font-face{font-family:Lato;font-weight:100;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-hairline.woff2) format("woff2"),url(fonts/lato-hairline.woff) format("woff")}@font-face{font-family:Lato;font-weight:100;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-hairline-italic.woff2) format("woff2"),url(fonts/lato-hairline-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:200;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-thin.woff2) format("woff2"),url(fonts/lato-thin.woff) format("woff")}@font-face{font-family:Lato;font-weight:200;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-thin-italic.woff2) format("woff2"),url(fonts/lato-thin-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:300;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-light.woff2) format("woff2"),url(fonts/lato-light.woff) format("woff")}@font-face{font-family:Lato;font-weight:300;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-light-italic.woff2) format("woff2"),url(fonts/lato-light-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:400;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-normal.woff2) format("woff2"),url(fonts/lato-normal.woff) format("woff")}@font-face{font-family:Lato;font-weight:400;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-normal-italic.woff2) format("woff2"),url(fonts/lato-normal-italic.woff) format("woff")}@font-face{font-family:Lato Medium;font-weight:400;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-medium.woff2) format("woff2"),url(fonts/lato-medium.woff) format("woff")}@font-face{font-family:Lato Medium;font-weight:400;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-medium-italic.woff2) format("woff2"),url(fonts/lato-medium-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:500;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-semibold.woff2) format("woff2"),url(fonts/lato-semibold.woff) format("woff")}@font-face{font-family:Lato;font-weight:500;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-semibold-italic.woff2) format("woff2"),url(fonts/lato-semibold-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:600;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-bold.woff2) format("woff2"),url(fonts/lato-bold.woff) format("woff")}@font-face{font-family:Lato;font-weight:600;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-bold-italic.woff2) format("woff2"),url(fonts/lato-bold-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:800;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-heavy.woff2) format("woff2"),url(fonts/lato-heavy.woff) format("woff")}@font-face{font-family:Lato;font-weight:800;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-heavy-italic.woff2) format("woff2"),url(fonts/lato-heavy-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:900;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-black.woff2) format("woff2"),url(fonts/lato-black.woff) format("woff")}@font-face{font-family:Lato;font-weight:900;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-black-italic.woff2) format("woff2"),url(fonts/lato-black-italic.woff) format("woff")}.material-icons.md-18{font-size:18px}.material-icons.md-24{font-size:24px}.material-icons.md-36{font-size:36px}.material-icons.md-48{font-size:48px}.material-icons.md-dark{color:rgba(0,0,0,.54)}.material-icons.md-dark.md-inactive{color:rgba(0,0,0,.26)}.material-icons.md-light{color:#fff}.material-icons.md-light.md-inactive{color:hsla(0,0%,100%,.3)}.hljs{display:block;overflow-x:auto;padding:.5em;color:#383a42;background:#fafafa}.hljs-comment,.hljs-quote{color:#a0a1a7;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#a626a4}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e45649}.hljs-literal{color:#0184bb}.hljs-addition,.hljs-attribute,.hljs-meta-string,.hljs-regexp,.hljs-string{color:#50a14f}.hljs-built_in,.hljs-class .hljs-title{color:#c18401}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#986801}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#4078f2}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}::selection{color:#fff;background-color:#000}.desktopHide{display:none}.logo{position:fixed;z-index:20;top:.4em;left:.6em}h2,h3,h4{font-family:PT Sans,sans-serif;text-transform:uppercase}label,li,p{color:#666}a{color:#000;font-weight:700}a.nostyle,a:focus,a:hover{text-decoration:none}form fieldset{border:0;padding:0;margin:0}form input[type=email],form input[type=number],form input[type=password],form input[type=text],form input[type=url],select{border:1px solid #999;padding:.5em 1em;min-width:12em;color:#666}@media screen and (-webkit-min-device-pixel-ratio:0){select{-webkit-appearance:none;border-radius:0;background:#fff url(themes/_global/img/bg-select.png) no-repeat 100%}}.inline .row{display:inline-block;margin-right:.5em}.inline label{min-width:6em}fieldset label{display:inline-block;min-width:12.5em;color:#666}label{margin-right:.5em}form .row{margin-bottom:.5em}form button,input[type=submit]{cursor:pointer;background-color:#000;color:#fff;padding:.5em 1em;display:inline-block;border:1px solid #000}form button:focus,form button:hover,input[type=submit]:focus,input[type=submit]:hover{background-color:#fff;color:#000;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-ms-transition:all .5s ease;-o-transition:all .5s ease;transition:all .5s ease}#bookmarklet{cursor:move}h2:after{content:"";height:4px;width:20%;background-color:#000;display:block}.links,.links li{padding:0;margin:0}.links li{list-style:none}#links{position:fixed;top:0;width:10em;left:0;text-align:right;background-color:#333;padding-top:9.5em;height:100%;box-shadow:inset -4px 0 20px rgba(0,0,0,.6);z-index:15}#links>li>a{display:block;padding:.5em 2em .5em 1em;color:#fff;position:relative;text-transform:uppercase;text-decoration:none;font-weight:400;font-family:PT Sans,sans-serif;transition:all .5s ease}#links>li>a:focus,#links>li>a:hover{background-color:#999;color:#000}#links .current:after{content:"";width:0;height:0;position:absolute;border:10px solid transparent;border-right-color:#eee;right:0;top:50%;margin-top:-10px}#links li:last-child{position:fixed;bottom:1em;width:10em}#links li:last-child a:before{font-size:1.2em;position:relative;top:2px}#main{margin-left:12em;position:relative;z-index:10;padding-right:5%;padding-bottom:1em}#sort{padding:0;list-style-type:none;opacity:.5;display:inline-block}#sort li{display:inline;font-size:.9em}#sort li+li{margin-left:10px}#sort a{padding:2px 2px 0;vertical-align:middle}#sort img{vertical-align:baseline}#sort img :hover{cursor:pointer}#display-mode{float:right;margin-top:10px;margin-bottom:10px;opacity:.5}#listmode{width:16px;display:inline-block;text-decoration:none}#listmode.tablemode{background:url(themes/_global/img/table.png) no-repeat bottom}#listmode .listmode{background:url(themes/_global/img/list.png) no-repeat bottom}#warning_message{position:fixed;background-color:tomato;z-index:1000;bottom:0;left:0;width:100%;color:#000}#content{margin-top:2em;min-height:30em}footer{text-align:right;position:relative;bottom:0;right:5em;color:#999;font-size:.8em;font-style:italic;z-index:20}footer a{color:#999;font-weight:400}.list-entries{letter-spacing:-5px}.listmode.entry{width:100%;height:inherit}.card-entry-tags{max-height:2em;overflow-y:hidden;padding:0;margin:0}.card-entry-tags li,.card-entry-tags span{display:inline-block;margin:0 5px;padding:5px 12px;background-color:rgba(0,0,0,.6);border-radius:3px;max-height:2em;overflow:hidden;text-overflow:ellipsis}.card-entry-labels a,.card-entry-tags a{text-decoration:none;font-weight:400;color:#fff}.nav-panel-add-tag{margin-top:10px}.list-entries+.results{margin-bottom:2em}.created-at,.reading-time{color:#999;font-style:italic;font-weight:400;font-size:.9em}.estimatedTime small{position:relative;top:-1px}.entry{background-color:#fff;letter-spacing:normal;box-shadow:0 3px 7px rgba(0,0,0,.3);display:inline-block;width:32%;margin-bottom:1.5em;vertical-align:top;margin-right:1%;position:relative;overflow:hidden;padding:1.5em 0 3em;height:440px}.entry img.preview{width:100%;object-fit:cover;height:100%}.entry:before{width:0;height:0;border:10px solid transparent;border-bottom-color:#000;bottom:.7em;z-index:10;right:1.5em}.entry:after,.entry:before{content:"";position:absolute;transition:all .5s ease}.entry:after{height:7px;width:100%;bottom:0;left:0;background-color:#000}.entry:hover{box-shadow:0 3px 10px #000}.entry:hover:after{height:40px}.entry:hover:before{bottom:2.3em}.entry:hover h2 a{color:#666}.entry:hover .tools{bottom:0}.entry h2{text-transform:none;margin-bottom:0;line-height:1.2;margin-left:5px}.entry:after{content:none}.entry a{display:block;text-decoration:none;color:#000;word-wrap:break-word;transition:all .5s ease}.entry p{color:#666;font-size:.9em;line-height:1.7;margin:5px 5px auto}.entry h2 a:first-letter{text-transform:uppercase}.entry .tools{position:absolute;bottom:-40px;left:0;background:#000;width:100%;z-index:10;padding-right:.5em;text-align:right;transition:all .5s ease}.entry .tools a{color:#666;text-decoration:none;display:block;padding:.4em}.entry .tools a:hover{color:#fff}.entry .tools li{display:inline-block;margin-top:10px}.entry .tools li:first-child{float:left;font-size:.9em;max-width:calc(100% - 40px * 4);text-overflow:ellipsis;overflow:hidden;white-space:nowrap;max-height:2em;margin-left:10px}.entry .card-entry-labels{position:absolute;top:100px;left:-1em;z-index:90;max-width:50%;padding-left:0}.entry .card-entry-labels li{margin:10px 10px 10px auto;padding:5px 12px 5px 25px;background-color:rgba(0,0,0,.6);border-radius:0 3px 3px 0;color:#fff;cursor:default;max-height:2em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.entry .card-entry-labels li a{color:#fff}.entry:nth-child(3n+1){margin-left:0}.results{letter-spacing:-5px;padding:0 0 .5em}.results>*{display:inline-block;vertical-align:top;letter-spacing:normal;width:50%}.results>*,div.pagination ul{text-align:right}.nb-results{text-align:left;font-style:italic;color:#999;display:inline-flex}div.pagination ul a{color:#999;text-decoration:none}div.pagination ul a:focus,div.pagination ul a:hover{text-decoration:underline}div.pagination ul>*{display:inline-block;margin-left:.5em}div.pagination ul .next.disabled,div.pagination ul .prev.disabled{display:none}div.pagination ul .current{height:25px;padding:4px 8px;border:1px solid #d5d5d5;text-decoration:none;font-weight:700;color:#000;background-color:#ccc}.hide{display:none}#article{width:70%;margin-bottom:3em;text-align:justify}#article .tags{margin-bottom:1em}#article i{font-style:normal}#article h1{text-align:left}#article h2:after{content:none}#article h2,#article h3,#article h4{text-transform:none}blockquote{border:1px solid #999;background-color:#fff;padding:1em;margin:0}.topPosF{position:fixed;right:20%;bottom:2em;font-size:1.5em}#article_toolbar{margin-bottom:1em}#article_toolbar li{display:inline-block;margin:3px auto}#article_toolbar a{background-color:#000;padding:.3em .5em .2em;color:#fff;text-decoration:none}#article_toolbar a:focus,#article_toolbar a:hover{background-color:#999}#nav-btn-add-tag{cursor:pointer}.shaarli:before{content:"*"}.return{text-decoration:none;margin-top:1em;display:block}.return:before{margin-right:.5em}.notags{font-style:italic;color:#999}.icon-rss{background-color:#000;color:#fff;padding:.2em .5em}.icon-rss:before{position:relative;top:2px}.list-tags li{margin-bottom:.5em}.list-tags .icon-rss:focus,.list-tags .icon-rss:hover{background-color:#fff;color:#000;text-decoration:none}.list-tags a{text-decoration:none}.list-tags a:focus,.list-tags a:hover{text-decoration:underline}pre code{font-family:Courier New,Courier,monospace}#filters{position:fixed;width:20%;height:100%;top:0;right:0;background-color:#fff;padding:30px 30px 15px 15px;border-left:1px solid #333;z-index:12;min-width:300px}#filters form .filter-group{margin:5px}#download-form{position:fixed;width:10%;height:100%;top:0;right:0;background-color:#fff;padding:30px 30px 15px 15px;border-left:1px solid #333;z-index:12;min-width:200px}#download-form li{display:block;padding:.5em 2em .5em 1em;color:#fff;position:relative;text-transform:uppercase;text-decoration:none;font-weight:400;font-family:PT Sans,sans-serif;transition:all .5s ease}@font-face{font-family:icomoon;src:url(fonts/IcoMoon-Free.ttf);font-weight:400;font-style:normal}.material-icons{font-family:Material Icons;font-weight:400;font-style:normal;font-size:1em;width:1em;height:1em;display:inline-block;line-height:1;text-transform:none;letter-spacing:normal;word-wrap:normal;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:"liga"}.material-icons .md-18{font-size:18px}.material-icons .md-24{font-size:24px}.material-icons .md-36{font-size:36px}.material-icons .md-48{font-size:48px}.material-icons .vertical-align-middle{vertical-align:middle!important}.icon-image span,.icon span{position:absolute;top:-9999px}[class*=" icon-"]:before,[class^=icon-]:before{font-family:icomoon;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;letter-spacing:0;-webkit-font-feature-settings:"liga";-moz-font-feature-settings:"liga=1";-moz-font-feature-settings:"liga";-ms-font-feature-settings:"liga" 1;-o-font-feature-settings:"liga";font-feature-settings:"liga";-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-flattr:before{content:"\EAD4"}.icon-mail:before{content:"\EA86"}.icon-up-open:before{content:"\E80B"}.icon-star:before{content:"\E9D9"}.icon-check:before{content:"\EA10"}.icon-link:before{content:"\E9CB"}.icon-reply:before{content:"\E806"}.icon-menu:before{content:"\E9BD"}.icon-clock:before{content:"\E803"}.icon-twitter:before{content:"\EA96"}.icon-down-open:before{content:"\E809"}.icon-trash:before{content:"\E9AC"}.icon-delete:before{content:"\EA0D"}.icon-power:before{content:"\EA14"}.icon-arrow-up-thick:before{content:"\EA3A"}.icon-rss:before{content:"\E808"}.icon-print:before{content:"\E954"}.icon-reload:before{content:"\EA2E"}.icon-price-tags:before{content:"\E936"}.icon-eye:before{content:"\E9CE"}.icon-no-eye:before{content:"\E9D1"}.icon-calendar:before{content:"\E953"}.icon-time:before{content:"\E952"}.icon-image{background:no-repeat 50%/80%;padding-right:1em!important;padding-left:1em!important}.icon-image--carrot{background-image:url(themes/_global/img/icons/carrot-icon--white.png)}.icon-image--diaspora{background-image:url(themes/_global/img/icons/Diaspora-asterisk.svg)}.icon-image--unmark{background-image:url(themes/_global/img/icons/unmark-icon--black.png)}.icon-image--shaarli{background-image:url(themes/_global/img/icons/shaarli.png)}.icon-check.archive:before,.icon-star.fav:before{color:#fff}.login{background-color:#333}.login #main{padding:0;margin:0}.login form{background-color:#fff;padding:1.5em;box-shadow:0 1px 8px rgba(0,0,0,.9);width:20em;top:8em;margin-left:-10em}.login .logo,.login form{position:absolute;left:50%}.login .logo{top:2em;margin-left:-55px}.popup-form{background:rgba(0,0,0,.5);left:10em;height:100%;width:100%;margin:0;margin-top:-30%!important;display:none;border-left:1px solid #eee}.popup-form,.popup-form form{position:absolute;top:0;z-index:20;padding:2em}.popup-form form{background-color:#fff;left:0;border:10px solid #000;width:400px;height:200px}#bagit-form-form .addurl{margin-left:0}.close-button,.closeMessage{background-color:#000;color:#fff;font-size:1.2em;line-height:1.6;width:1.6em;height:1.6em;text-align:center;text-decoration:none}.close-button:focus,.close-button:hover,.closeMessage:focus,.closeMessage:hover{background-color:#999;color:#000}.close-button--popup{display:inline-block;position:absolute;top:0;right:0;font-size:1.4em}.active-current{background-color:#999}.active-current:after{content:"";width:0;height:0;position:absolute;border:10px solid transparent;border-right-color:#eee;right:0;top:50%;margin-top:-10px}.opacity03{opacity:.3}.add-to-wallabag-link-after{background-color:#000;color:#fff;padding:0 3px 2px}a.add-to-wallabag-link-after{visibility:hidden;position:absolute;opacity:0;transition-duration:2s;transition-timing-function:ease-out}#article article a:hover+a.add-to-wallabag-link-after,a.add-to-wallabag-link-after:hover{opacity:1;visibility:visible;transition-duration:.3s;transition-timing-function:ease-in}a.add-to-wallabag-link-after:after{content:"w"}#add-link-result{font-weight:700;font-size:.9em}.btn-clickable{cursor:pointer}.messages{text-align:left;width:60%;margin:auto 17%}.messages>*{display:inline-block}.messages .install{text-align:left}.messages .install.error{border:1px solid #c42608;color:#c00!important;background:#fff0ef}.messages .install.notice{border:1px solid #ebcd41;color:#000;background:#fffcd3}.messages .install.success{border:1px solid #6dc70c;background:#e0fbcc!important}.warning{font-weight:700;display:block;width:100%}.more-info{font-size:.85em;line-height:1.5;color:#aaa}.more-info a{color:#aaa}@media screen and (max-width:1050px){.entry{width:49%}.entry:nth-child(3n+1){margin-left:1.5%}.entry:nth-child(odd){margin-left:0}}@media screen and (max-width:900px){#article{width:80%}.topPosF{right:2.5em}}@media screen and (max-width:700px){.entry{width:100%;margin-left:0}#display-mode{display:none}}@media screen and (max-height:770px){.menu.developer,.menu.internal,.menu.users{display:none}}@media screen and (max-width:500px){.entry{width:100%;margin-left:0}body>header{background-color:#333;position:fixed;top:0;width:100%;height:3em;z-index:11}#links li:last-child{position:static;width:auto}#links li:last-child a:before{content:none}.logo{width:1.25em;height:1.25em;left:0;top:0}.login>header,.login form{position:static}.login form{width:100%;margin-left:0}.login .logo{height:auto;top:.5em;width:75px;margin-left:-37.5px}.desktopHide{display:block;position:fixed;z-index:20;top:0;right:0;border:0;width:2.5em;height:2.5em;cursor:pointer;background-color:#999;font-size:1.2em}.desktopHide:focus,.desktopHide:hover{background-color:#fff}#links{display:none;width:100%;height:auto;padding-top:3em}#links.menu--open{display:block}footer{margin-right:3em}#main,footer{position:static}#main{margin-left:1.5em;padding-right:1.5em;margin-top:3em}#article_toolbar .topPosF,.card-entry-labels{display:none}#article{width:100%}#article h1{font-size:1.5em}#article_toolbar a{padding:.3em .4em .2em}#display-mode{display:none}#bagit-form,#search-form,.popup-form{left:0;width:100%;border-left:none}#bagit-form form,#search-form form,.popup-form form{width:100%}}@media print{body{font-family:Serif;background-color:#fff}@page{margin:1cm}img{max-width:100%!important}#article-informations,#article .mbm a,#article_toolbar,#links,#sort,.entrie+.results,.messages,.top_link,body>.logo,body>footer,div.tools,header div{display:none!important}article{border:none!important}.vieworiginal a:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.pagination span.current{border-style:dashed}#main{margin:0;padding:0}#article,#main{width:100%}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{font-size:1em;line-height:1.5;margin:0}dl:first-child,h1:first-child,h2:first-child,h3:first-child,h4:first-child,h5:first-child,h6:first-child,ol:first-child,p:first-child,ul:first-child{margin-top:0}code,kbd,pre,samp{font-family:monospace,serif}pre{white-space:pre-wrap}.upper{text-transform:uppercase}.bold{font-weight:700}.inner{margin:0 auto;max-width:61.25em}figure,img,table{max-width:100%;height:auto}iframe{max-width:100%}.fl{float:left}.fr{float:right}table{border-collapse:collapse}figure{margin:0}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}input[type=search]{-webkit-appearance:textfield}.dib{display:inline-block;vertical-align:middle}.dnone{display:none}.dtable{display:table}.dtable>*{display:table-row}.dtable>*>*{display:table-cell}.element-invisible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.small{font-size:.8em}.big{font-size:1.2em}.w100{width:100%}.w90{width:90%}.w80{width:80%}.w70{width:70%}.w60{width:60%}.w50{width:50%}.w40{width:40%}.w30{width:30%}.w20{width:20%}.w10{width:10%}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}@media screen and (-webkit-min-device-pixel-ratio:0){select{-webkit-appearance:none;border-radius:0}} +.annotator-filter *,.annotator-notice,.annotator-widget *{font-family:Helvetica Neue,Arial,Helvetica,sans-serif;font-weight:400;text-align:left;margin:0;padding:0;background:none;-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none;-moz-box-shadow:none;-webkit-box-shadow:none;-o-box-shadow:none;box-shadow:none;color:#909090}.annotator-adder{background-image:url(img/annotator-icon-sprite.png);background-repeat:no-repeat}.annotator-editor a:after,.annotator-filter .annotator-filter-navigation button:after,.annotator-filter .annotator-filter-property .annotator-filter-clear,.annotator-resize,.annotator-viewer .annotator-controls a,.annotator-viewer .annotator-controls button,.annotator-widget:after{background-image:url(img/annotator-glyph-sprite.png);background-repeat:no-repeat}.annotator-hl{background:#ffff0a;background:rgba(255,255,10,.3);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#4DFFFF0A, endColorstr=#4DFFFF0A)"}.annotator-hl-temporary{background:#007cff;background:rgba(0,124,255,.3);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#4D007CFF, endColorstr=#4D007CFF)"}.annotator-wrapper{position:relative}.annotator-adder,.annotator-notice,.annotator-outer{z-index:1020}.annotator-filter{z-index:1010}.annotator-adder,.annotator-notice,.annotator-outer,.annotator-widget{position:absolute;font-size:10px;line-height:1}.annotator-hide{display:none;visibility:hidden}.annotator-adder{margin-top:-48px;margin-left:-24px;width:48px;height:48px;background-position:0 0}.annotator-adder:hover{background-position:top}.annotator-adder:active{background-position:100%}.annotator-adder button{display:block;width:36px;height:41px;margin:0 auto;border:none;background:none;text-indent:-999em;cursor:pointer}.annotator-outer{width:0;height:0}.annotator-widget{margin:0;padding:0;bottom:15px;left:-18px;min-width:265px;background-color:#fbfbfb;background-color:hsla(0,0%,98%,.98);border:1px solid #7a7a7a;border:1px solid hsla(0,0%,48%,.6);-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-webkit-box-shadow:0 5px 15px rgba(0,0,0,.2);-moz-box-shadow:0 5px 15px rgba(0,0,0,.2);-o-box-shadow:0 5px 15px rgba(0,0,0,.2);box-shadow:0 5px 15px rgba(0,0,0,.2)}.annotator-invert-x .annotator-widget{left:auto;right:-18px}.annotator-invert-y .annotator-widget{bottom:auto;top:8px}.annotator-widget strong{font-weight:700}.annotator-widget .annotator-item,.annotator-widget .annotator-listing{padding:0;margin:0;list-style:none}.annotator-widget:after{content:"";display:block;width:18px;height:10px;background-position:0 0;position:absolute;bottom:-10px;left:8px}.annotator-invert-x .annotator-widget:after{left:auto;right:8px}.annotator-invert-y .annotator-widget:after{background-position:0 -15px;bottom:auto;top:-9px}.annotator-editor .annotator-item input,.annotator-editor .annotator-item textarea,.annotator-widget .annotator-item{position:relative;font-size:12px}.annotator-viewer .annotator-item{border-top:2px solid #7a7a7a;border-top:2px solid hsla(0,0%,48%,.2)}.annotator-widget .annotator-item:first-child{border-top:none}.annotator-editor .annotator-item,.annotator-viewer div{border-top:1px solid #858585;border-top:1px solid hsla(0,0%,52%,.11)}.annotator-viewer div{padding:6px}.annotator-viewer .annotator-item ol,.annotator-viewer .annotator-item ul{padding:4px 16px}.annotator-editor .annotator-item:first-child textarea,.annotator-viewer div:first-of-type{padding-top:12px;padding-bottom:12px;color:#3c3c3c;font-size:13px;font-style:italic;line-height:1.3;border-top:none}.annotator-viewer .annotator-controls{position:relative;top:5px;right:5px;padding-left:5px;opacity:0;-webkit-transition:opacity .2s ease-in;-moz-transition:opacity .2s ease-in;-o-transition:opacity .2s ease-in;transition:opacity .2s ease-in;float:right}.annotator-viewer li .annotator-controls.annotator-visible,.annotator-viewer li:hover .annotator-controls{opacity:1}.annotator-viewer .annotator-controls a,.annotator-viewer .annotator-controls button{cursor:pointer;display:inline-block;width:13px;height:13px;margin-left:2px;border:none;opacity:.2;text-indent:-900em;background-color:transparent;outline:none}.annotator-viewer .annotator-controls a:focus,.annotator-viewer .annotator-controls a:hover,.annotator-viewer .annotator-controls button:focus,.annotator-viewer .annotator-controls button:hover{opacity:.9}.annotator-viewer .annotator-controls a:active,.annotator-viewer .annotator-controls button:active{opacity:1}.annotator-viewer .annotator-controls button[disabled]{display:none}.annotator-viewer .annotator-controls .annotator-edit{background-position:0 -60px}.annotator-viewer .annotator-controls .annotator-delete{background-position:0 -75px}.annotator-viewer .annotator-controls .annotator-link{background-position:0 -270px}.annotator-editor .annotator-item{position:relative}.annotator-editor .annotator-item label{top:0;display:inline;cursor:pointer;font-size:12px}.annotator-editor .annotator-item input,.annotator-editor .annotator-item textarea{display:block;min-width:100%;padding:10px 8px;border:none;margin:0;color:#3c3c3c;background:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;resize:none}.annotator-editor .annotator-item textarea::-webkit-scrollbar{height:8px;width:8px}.annotator-editor .annotator-item textarea::-webkit-scrollbar-track-piece{margin:13px 0 3px;background-color:#e5e5e5;-webkit-border-radius:4px}.annotator-editor .annotator-item textarea::-webkit-scrollbar-thumb:vertical{height:25px;background-color:#ccc;-webkit-border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1)}.annotator-editor .annotator-item textarea::-webkit-scrollbar-thumb:horizontal{width:25px;background-color:#ccc;-webkit-border-radius:4px}.annotator-editor .annotator-item:first-child textarea{min-height:5.5em;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;-o-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.annotator-editor .annotator-item input:focus,.annotator-editor .annotator-item textarea:focus{background-color:#f3f3f3;outline:none}.annotator-editor .annotator-item input[type=checkbox],.annotator-editor .annotator-item input[type=radio]{width:auto;min-width:0;padding:0;display:inline;margin:0 4px 0 0;cursor:pointer}.annotator-editor .annotator-checkbox{padding:8px 6px}.annotator-editor .annotator-controls,.annotator-filter,.annotator-filter .annotator-filter-navigation button{text-align:right;padding:3px;border-top:1px solid #d4d4d4;background-color:#d4d4d4;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),color-stop(.6,#dcdcdc),to(#d2d2d2));background-image:-moz-linear-gradient(to bottom,#f5f5f5,#dcdcdc 60%,#d2d2d2);background-image:-webkit-linear-gradient(180deg,#f5f5f5,#dcdcdc 60%,#d2d2d2);background-image:linear-gradient(180deg,#f5f5f5,#dcdcdc 60%,#d2d2d2);-webkit-box-shadow:inset 1px 0 0 hsla(0,0%,100%,.7),inset -1px 0 0 hsla(0,0%,100%,.7),inset 0 1px 0 hsla(0,0%,100%,.7);-moz-box-shadow:inset 1px 0 0 hsla(0,0%,100%,.7),inset -1px 0 0 hsla(0,0%,100%,.7),inset 0 1px 0 hsla(0,0%,100%,.7);-o-box-shadow:inset 1px 0 0 hsla(0,0%,100%,.7),inset -1px 0 0 hsla(0,0%,100%,.7),inset 0 1px 0 hsla(0,0%,100%,.7);box-shadow:inset 1px 0 0 hsla(0,0%,100%,.7),inset -1px 0 0 hsla(0,0%,100%,.7),inset 0 1px 0 hsla(0,0%,100%,.7);-webkit-border-radius:0 0 5px 5px;-moz-border-radius:0 0 5px 5px;-o-border-radius:0 0 5px 5px;border-radius:0 0 5px 5px}.annotator-editor.annotator-invert-y .annotator-controls{border-top:none;border-bottom:1px solid #b4b4b4;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;-o-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.annotator-editor a,.annotator-filter .annotator-filter-property label{position:relative;display:inline-block;padding:0 6px 0 22px;color:#363636;text-shadow:0 1px 0 hsla(0,0%,100%,.75);text-decoration:none;line-height:24px;font-size:12px;font-weight:700;border:1px solid #a2a2a2;background-color:#d4d4d4;background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),color-stop(.5,#d2d2d2),color-stop(.5,#bebebe),to(#d2d2d2));background-image:-moz-linear-gradient(to bottom,#f5f5f5,#d2d2d2 50%,#bebebe 50%,#d2d2d2);background-image:-webkit-linear-gradient(180deg,#f5f5f5,#d2d2d2 50%,#bebebe 0,#d2d2d2);background-image:linear-gradient(180deg,#f5f5f5,#d2d2d2 50%,#bebebe 0,#d2d2d2);-webkit-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-moz-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-o-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-webkit-border-radius:5px;-moz-border-radius:5px;-o-border-radius:5px;border-radius:5px}.annotator-editor a:after{position:absolute;top:50%;left:5px;display:block;content:"";width:15px;height:15px;margin-top:-7px;background-position:0 -90px}.annotator-editor a.annotator-focus,.annotator-editor a:focus,.annotator-editor a:hover,.annotator-filter .annotator-filter-active label,.annotator-filter .annotator-filter-navigation button:hover{outline:none;border-color:#435aa0;background-color:#3865f9;background-image:-webkit-gradient(linear,left top,left bottom,from(#7691fb),color-stop(.5,#5075fb),color-stop(.5,#3865f9),to(#3665fa));background-image:-moz-linear-gradient(to bottom,#7691fb,#5075fb 50%,#3865f9 50%,#3665fa);background-image:-webkit-linear-gradient(180deg,#7691fb,#5075fb 50%,#3865f9 0,#3665fa);background-image:linear-gradient(180deg,#7691fb,#5075fb 50%,#3865f9 0,#3665fa);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.42)}.annotator-editor a:focus:after,.annotator-editor a:hover:after{margin-top:-8px;background-position:0 -105px}.annotator-editor a:active,.annotator-filter .annotator-filter-navigation button:active{border-color:#700c49;background-color:#d12e8e;background-image:-webkit-gradient(linear,left top,left bottom,from(#fc7cca),color-stop(.5,#e85db2),color-stop(.5,#d12e8e),to(#ff009c));background-image:-moz-linear-gradient(to bottom,#fc7cca,#e85db2 50%,#d12e8e 50%,#ff009c);background-image:-webkit-linear-gradient(180deg,#fc7cca,#e85db2 50%,#d12e8e 0,#ff009c);background-image:linear-gradient(180deg,#fc7cca,#e85db2 50%,#d12e8e 0,#ff009c)}.annotator-editor a.annotator-save:after{background-position:0 -120px}.annotator-editor a.annotator-save.annotator-focus:after,.annotator-editor a.annotator-save:focus:after,.annotator-editor a.annotator-save:hover:after{margin-top:-8px;background-position:0 -135px}.annotator-editor .annotator-widget:after{background-position:0 -30px}.annotator-editor.annotator-invert-y .annotator-widget .annotator-controls{background-color:#f2f2f2}.annotator-editor.annotator-invert-y .annotator-widget:after{background-position:0 -45px;height:11px}.annotator-resize{position:absolute;top:0;right:0;width:12px;height:12px;background-position:2px -150px}.annotator-invert-x .annotator-resize{right:auto;left:0;background-position:0 -195px}.annotator-invert-y .annotator-resize{top:auto;bottom:0;background-position:2px -165px}.annotator-invert-y.annotator-invert-x .annotator-resize{background-position:0 -180px}.annotator-notice{color:#fff;position:fixed;top:-54px;left:0;width:100%;font-size:14px;line-height:50px;text-align:center;background:#000;background:rgba(0,0,0,.9);border-bottom:4px solid #d4d4d4;-webkit-transition:top .4s ease-out;-moz-transition:top .4s ease-out;-o-transition:top .4s ease-out;transition:top .4s ease-out}.annotator-notice-success{border-color:#3665f9}.annotator-notice-error{border-color:#ff7e00}.annotator-notice p{margin:0}.annotator-notice a{color:#fff}.annotator-notice-show{top:0}.annotator-tags{margin-bottom:-2px}.annotator-tags .annotator-tag{display:inline-block;padding:0 8px;margin-bottom:2px;line-height:1.6;font-weight:700;background-color:#e6e6e6;-webkit-border-radius:8px;-moz-border-radius:8px;-o-border-radius:8px;border-radius:8px}.annotator-filter{position:fixed;top:0;right:0;left:0;text-align:left;line-height:0;border:none;border-bottom:1px solid #878787;padding-left:10px;padding-right:10px;-webkit-border-radius:0;-moz-border-radius:0;-o-border-radius:0;border-radius:0;-webkit-box-shadow:inset 0 -1px 0 hsla(0,0%,100%,.3);-moz-box-shadow:inset 0 -1px 0 hsla(0,0%,100%,.3);-o-box-shadow:inset 0 -1px 0 hsla(0,0%,100%,.3);box-shadow:inset 0 -1px 0 hsla(0,0%,100%,.3)}.annotator-filter strong{font-size:12px;font-weight:700;color:#3c3c3c;text-shadow:0 1px 0 hsla(0,0%,100%,.7);position:relative;top:-9px}.annotator-filter .annotator-filter-navigation,.annotator-filter .annotator-filter-property{position:relative;display:inline-block;overflow:hidden;line-height:10px;padding:2px 0;margin-right:8px}.annotator-filter .annotator-filter-navigation button,.annotator-filter .annotator-filter-property label{text-align:left;display:block;float:left;line-height:20px;-webkit-border-radius:10px 0 0 10px;-moz-border-radius:10px 0 0 10px;-o-border-radius:10px 0 0 10px;border-radius:10px 0 0 10px}.annotator-filter .annotator-filter-property label{padding-left:8px}.annotator-filter .annotator-filter-property input{display:block;float:right;-webkit-appearance:none;background-color:#fff;border:1px solid #878787;border-left:none;padding:2px 4px;line-height:16px;min-height:16px;font-size:12px;width:150px;color:#333;background-color:#f8f8f8;-webkit-border-radius:0 10px 10px 0;-moz-border-radius:0 10px 10px 0;-o-border-radius:0 10px 10px 0;border-radius:0 10px 10px 0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.2);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,.2);-o-box-shadow:inset 0 1px 1px rgba(0,0,0,.2);box-shadow:inset 0 1px 1px rgba(0,0,0,.2)}.annotator-filter .annotator-filter-property input:focus{outline:none;background-color:#fff}.annotator-filter .annotator-filter-clear{position:absolute;right:3px;top:6px;border:none;text-indent:-900em;width:15px;height:15px;background-position:0 -90px;opacity:.4}.annotator-filter .annotator-filter-clear:focus,.annotator-filter .annotator-filter-clear:hover{opacity:.8}.annotator-filter .annotator-filter-clear:active{opacity:1}.annotator-filter .annotator-filter-navigation button{border:1px solid #a2a2a2;padding:0;text-indent:-900px;width:20px;min-height:22px;-webkit-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-moz-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);-o-box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8);box-shadow:inset 0 0 5px hsla(0,0%,100%,.2),inset 0 0 1px hsla(0,0%,100%,.8)}.annotator-filter .annotator-filter-navigation button,.annotator-filter .annotator-filter-navigation button:focus,.annotator-filter .annotator-filter-navigation button:hover{color:transparent}.annotator-filter .annotator-filter-navigation button:after{position:absolute;top:8px;left:8px;content:"";display:block;width:9px;height:9px;background-position:0 -210px}.annotator-filter .annotator-filter-navigation button:hover:after{background-position:0 -225px}.annotator-filter .annotator-filter-navigation .annotator-filter-next{-webkit-border-radius:0 10px 10px 0;-moz-border-radius:0 10px 10px 0;-o-border-radius:0 10px 10px 0;border-radius:0 10px 10px 0;border-left:none}.annotator-filter .annotator-filter-navigation .annotator-filter-next:after{left:auto;right:7px;background-position:0 -240px}.annotator-filter .annotator-filter-navigation .annotator-filter-next:hover:after{background-position:0 -255px}.annotator-hl-active{background:#ffff0a;background:rgba(255,255,10,.8);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#CCFFFF0A, endColorstr=#CCFFFF0A)"}.annotator-hl-filtered{background-color:transparent}@font-face{font-family:Material Icons;font-style:normal;font-weight:400;src:url(fonts/MaterialIcons-Regular.eot);src:local("Material Icons"),local("MaterialIcons-Regular"),url(fonts/MaterialIcons-Regular.woff2) format("woff2"),url(fonts/MaterialIcons-Regular.woff) format("woff"),url(fonts/MaterialIcons-Regular.ttf) format("truetype")}.material-icons{font-family:Material Icons;font-weight:400;font-style:normal;font-size:24px;display:inline-block;line-height:1;text-transform:none;letter-spacing:normal;word-wrap:normal;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:"liga"}@font-face{font-family:Lato;font-weight:100;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-hairline.woff2) format("woff2"),url(fonts/lato-hairline.woff) format("woff")}@font-face{font-family:Lato;font-weight:100;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-hairline-italic.woff2) format("woff2"),url(fonts/lato-hairline-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:200;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-thin.woff2) format("woff2"),url(fonts/lato-thin.woff) format("woff")}@font-face{font-family:Lato;font-weight:200;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-thin-italic.woff2) format("woff2"),url(fonts/lato-thin-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:300;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-light.woff2) format("woff2"),url(fonts/lato-light.woff) format("woff")}@font-face{font-family:Lato;font-weight:300;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-light-italic.woff2) format("woff2"),url(fonts/lato-light-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:400;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-normal.woff2) format("woff2"),url(fonts/lato-normal.woff) format("woff")}@font-face{font-family:Lato;font-weight:400;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-normal-italic.woff2) format("woff2"),url(fonts/lato-normal-italic.woff) format("woff")}@font-face{font-family:Lato Medium;font-weight:400;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-medium.woff2) format("woff2"),url(fonts/lato-medium.woff) format("woff")}@font-face{font-family:Lato Medium;font-weight:400;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-medium-italic.woff2) format("woff2"),url(fonts/lato-medium-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:500;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-semibold.woff2) format("woff2"),url(fonts/lato-semibold.woff) format("woff")}@font-face{font-family:Lato;font-weight:500;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-semibold-italic.woff2) format("woff2"),url(fonts/lato-semibold-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:600;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-bold.woff2) format("woff2"),url(fonts/lato-bold.woff) format("woff")}@font-face{font-family:Lato;font-weight:600;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-bold-italic.woff2) format("woff2"),url(fonts/lato-bold-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:800;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-heavy.woff2) format("woff2"),url(fonts/lato-heavy.woff) format("woff")}@font-face{font-family:Lato;font-weight:800;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-heavy-italic.woff2) format("woff2"),url(fonts/lato-heavy-italic.woff) format("woff")}@font-face{font-family:Lato;font-weight:900;font-style:normal;text-rendering:optimizeLegibility;src:url(fonts/lato-black.woff2) format("woff2"),url(fonts/lato-black.woff) format("woff")}@font-face{font-family:Lato;font-weight:900;font-style:italic;text-rendering:optimizeLegibility;src:url(fonts/lato-black-italic.woff2) format("woff2"),url(fonts/lato-black-italic.woff) format("woff")}.material-icons.md-18{font-size:18px}.material-icons.md-24{font-size:24px}.material-icons.md-36{font-size:36px}.material-icons.md-48{font-size:48px}.material-icons.md-dark{color:rgba(0,0,0,.54)}.material-icons.md-dark.md-inactive{color:rgba(0,0,0,.26)}.material-icons.md-light{color:#fff}.material-icons.md-light.md-inactive{color:hsla(0,0%,100%,.3)}.hljs{display:block;overflow-x:auto;padding:.5em;color:#383a42;background:#fafafa}.hljs-comment,.hljs-quote{color:#a0a1a7;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#a626a4}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e45649}.hljs-literal{color:#0184bb}.hljs-addition,.hljs-attribute,.hljs-meta-string,.hljs-regexp,.hljs-string{color:#50a14f}.hljs-built_in,.hljs-class .hljs-title{color:#c18401}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#986801}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#4078f2}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}::selection{color:#fff;background-color:#000}.desktopHide{display:none}.logo{position:fixed;z-index:20;top:.4em;left:.6em}h2,h3,h4{font-family:PT Sans,sans-serif;text-transform:uppercase}label,li,p{color:#666}a{color:#000;font-weight:700}a.nostyle,a:focus,a:hover{text-decoration:none}form fieldset{border:0;padding:0;margin:0}form input[type=email],form input[type=number],form input[type=password],form input[type=text],form input[type=url],select{border:1px solid #999;padding:.5em 1em;min-width:12em;color:#666}@media screen and (-webkit-min-device-pixel-ratio:0){select{-webkit-appearance:none;border-radius:0;background:#fff url(themes/_global/img/bg-select.png) no-repeat 100%}}.inline .row{display:inline-block;margin-right:.5em}.inline label{min-width:6em}fieldset label{display:inline-block;min-width:12.5em;color:#666}label{margin-right:.5em}form .row{margin-bottom:.5em}form button,input[type=submit]{cursor:pointer;background-color:#000;color:#fff;padding:.5em 1em;display:inline-block;border:1px solid #000}form button:focus,form button:hover,input[type=submit]:focus,input[type=submit]:hover{background-color:#fff;color:#000;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-ms-transition:all .5s ease;-o-transition:all .5s ease;transition:all .5s ease}#bookmarklet{cursor:move}h2:after{content:"";height:4px;width:20%;background-color:#000;display:block}.links,.links li{padding:0;margin:0}.links li{list-style:none}#links{position:fixed;top:0;width:10em;left:0;text-align:right;background-color:#333;padding-top:9.5em;height:100%;box-shadow:inset -4px 0 20px rgba(0,0,0,.6);z-index:15}#links>li>a{display:block;padding:.5em 2em .5em 1em;color:#fff;position:relative;text-transform:uppercase;text-decoration:none;font-weight:400;font-family:PT Sans,sans-serif;transition:all .5s ease}#links>li>a:focus,#links>li>a:hover{background-color:#999;color:#000}#links .current:after{content:"";width:0;height:0;position:absolute;border:10px solid transparent;border-right-color:#eee;right:0;top:50%;margin-top:-10px}#links li:last-child{position:fixed;bottom:1em;width:10em}#links li:last-child a:before{font-size:1.2em;position:relative;top:2px}#main{margin-left:12em;position:relative;z-index:10;padding-right:5%;padding-bottom:1em}#sort{padding:0;list-style-type:none;opacity:.5;display:inline-block}#sort li{display:inline;font-size:.9em}#sort li+li{margin-left:10px}#sort a{padding:2px 2px 0;vertical-align:middle}#sort img{vertical-align:baseline}#sort img :hover{cursor:pointer}#display-mode{float:right;margin-top:10px;margin-bottom:10px;opacity:.5}#listmode{width:16px;display:inline-block;text-decoration:none}#listmode.tablemode{background:url(themes/_global/img/table.png) no-repeat bottom}#listmode .listmode{background:url(themes/_global/img/list.png) no-repeat bottom}#warning_message{position:fixed;background-color:tomato;z-index:1000;bottom:0;left:0;width:100%;color:#000}#content{margin-top:2em;min-height:30em}footer{text-align:right;position:relative;bottom:0;right:5em;color:#999;font-size:.8em;font-style:italic;z-index:20}footer a{color:#999;font-weight:400}.list-entries{letter-spacing:-5px}.listmode.entry{width:100%;height:inherit}.card-entry-tags{max-height:2em;overflow-y:hidden;padding:0;margin:0}.card-entry-tags li,.card-entry-tags span{display:inline-block;margin:0 5px;padding:5px 12px;background-color:rgba(0,0,0,.6);border-radius:3px;max-height:2em;overflow:hidden;text-overflow:ellipsis}.card-entry-labels a,.card-entry-tags a{text-decoration:none;font-weight:400;color:#fff}.nav-panel-add-tag{margin-top:10px}.list-entries+.results{margin-bottom:2em}.created-at,.reading-time{color:#999;font-style:italic;font-weight:400;font-size:.9em}.estimatedTime small{position:relative;top:-1px}.entry{background-color:#fff;letter-spacing:normal;box-shadow:0 3px 7px rgba(0,0,0,.3);display:inline-block;width:32%;margin-bottom:1.5em;vertical-align:top;margin-right:1%;position:relative;overflow:hidden;padding:1.5em 0 3em;height:440px}.entry img.preview{width:100%;object-fit:cover;height:100%}.entry:before{width:0;height:0;border:10px solid transparent;border-bottom-color:#000;bottom:.7em;z-index:10;right:1.5em}.entry:after,.entry:before{content:"";position:absolute;transition:all .5s ease}.entry:after{height:7px;width:100%;bottom:0;left:0;background-color:#000}.entry:hover{box-shadow:0 3px 10px #000}.entry:hover:after{height:40px}.entry:hover:before{bottom:2.3em}.entry:hover h2 a{color:#666}.entry:hover .tools{bottom:0}.entry h2{text-transform:none;margin-bottom:0;line-height:1.2;margin-left:5px}.entry:after{content:none}.entry a{display:block;text-decoration:none;color:#000;word-wrap:break-word;transition:all .5s ease}.entry p{color:#666;font-size:.9em;line-height:1.7;margin:5px 5px auto}.entry h2 a:first-letter{text-transform:uppercase}.entry .tools{position:absolute;bottom:-40px;left:0;background:#000;width:100%;z-index:10;padding-right:.5em;text-align:right;transition:all .5s ease}.entry .tools a{color:#666;text-decoration:none;display:block;padding:.4em}.entry .tools a:hover{color:#fff}.entry .tools li{display:inline-block;margin-top:10px}.entry .tools li:first-child{float:left;font-size:.9em;max-width:calc(100% - 40px * 4);text-overflow:ellipsis;overflow:hidden;white-space:nowrap;max-height:2em;margin-left:10px}.entry .card-entry-labels{position:absolute;top:100px;left:-1em;z-index:90;max-width:50%;padding-left:0}.entry .card-entry-labels li{margin:10px 10px 10px auto;padding:5px 12px 5px 25px;background-color:rgba(0,0,0,.6);border-radius:0 3px 3px 0;color:#fff;cursor:default;max-height:2em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.entry .card-entry-labels li a{color:#fff}.entry:nth-child(3n+1){margin-left:0}.results{letter-spacing:-5px;padding:0 0 .5em}.results>*{display:inline-block;vertical-align:top;letter-spacing:normal;width:50%}.results>*,div.pagination ul{text-align:right}.nb-results{text-align:left;font-style:italic;color:#999;display:inline-flex}div.pagination ul a{color:#999;text-decoration:none}div.pagination ul a:focus,div.pagination ul a:hover{text-decoration:underline}div.pagination ul>*{display:inline-block;margin-left:.5em}div.pagination ul .next.disabled,div.pagination ul .prev.disabled{display:none}div.pagination ul .current{height:25px;padding:4px 8px;border:1px solid #d5d5d5;text-decoration:none;font-weight:700;color:#000;background-color:#ccc}.card-tag-form{display:inline-block}.card-tag-form input[type=text]{min-width:20em}.hidden,.hide{display:none}#article{width:70%;margin-bottom:3em;text-align:justify}#article .tags{margin-bottom:1em}#article i{font-style:normal}#article h1{text-align:left}#article h2:after{content:none}#article h2,#article h3,#article h4{text-transform:none}blockquote{border:1px solid #999;background-color:#fff;padding:1em;margin:0}.topPosF{position:fixed;right:20%;bottom:2em;font-size:1.5em}#article_toolbar{margin-bottom:1em}#article_toolbar li{display:inline-block;margin:3px auto}#article_toolbar a{background-color:#000;padding:.3em .5em .2em;color:#fff;text-decoration:none}#article_toolbar a:focus,#article_toolbar a:hover{background-color:#999}#nav-btn-add-tag{cursor:pointer}.shaarli:before{content:"*"}.return{text-decoration:none;margin-top:1em;display:block}.return:before{margin-right:.5em}.notags{font-style:italic;color:#999}.icon-rss{background-color:#000;color:#fff;padding:.2em .5em}.icon-rss:before{position:relative;top:2px}.list-tags li{margin-bottom:.5em}.list-tags .icon-rss:focus,.list-tags .icon-rss:hover{background-color:#fff;color:#000;text-decoration:none}.list-tags a{text-decoration:none}.list-tags a:focus,.list-tags a:hover{text-decoration:underline}pre code{font-family:Courier New,Courier,monospace}#filters{position:fixed;width:20%;height:100%;top:0;right:0;background-color:#fff;padding:30px 30px 15px 15px;border-left:1px solid #333;z-index:12;min-width:300px}#filters form .filter-group{margin:5px}#download-form{position:fixed;width:10%;height:100%;top:0;right:0;background-color:#fff;padding:30px 30px 15px 15px;border-left:1px solid #333;z-index:12;min-width:200px}#download-form li{display:block;padding:.5em 2em .5em 1em;color:#fff;position:relative;text-transform:uppercase;text-decoration:none;font-weight:400;font-family:PT Sans,sans-serif;transition:all .5s ease}@font-face{font-family:icomoon;src:url(fonts/IcoMoon-Free.ttf);font-weight:400;font-style:normal}.material-icons{font-family:Material Icons;font-weight:400;font-style:normal;font-size:1em;width:1em;height:1em;display:inline-block;line-height:1;text-transform:none;letter-spacing:normal;word-wrap:normal;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:"liga"}.material-icons .md-18{font-size:18px}.material-icons .md-24{font-size:24px}.material-icons .md-36{font-size:36px}.material-icons .md-48{font-size:48px}.material-icons .vertical-align-middle{vertical-align:middle!important}.icon-image span,.icon span{position:absolute;top:-9999px}[class*=" icon-"]:before,[class^=icon-]:before{font-family:icomoon;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;letter-spacing:0;-webkit-font-feature-settings:"liga";-moz-font-feature-settings:"liga=1";-moz-font-feature-settings:"liga";-ms-font-feature-settings:"liga" 1;-o-font-feature-settings:"liga";font-feature-settings:"liga";-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-flattr:before{content:"\EAD4"}.icon-mail:before{content:"\EA86"}.icon-up-open:before{content:"\E80B"}.icon-star:before{content:"\E9D9"}.icon-check:before{content:"\EA10"}.icon-link:before{content:"\E9CB"}.icon-reply:before{content:"\E806"}.icon-menu:before{content:"\E9BD"}.icon-clock:before{content:"\E803"}.icon-twitter:before{content:"\EA96"}.icon-down-open:before{content:"\E809"}.icon-trash:before{content:"\E9AC"}.icon-delete:before{content:"\EA0D"}.icon-power:before{content:"\EA14"}.icon-arrow-up-thick:before{content:"\EA3A"}.icon-rss:before{content:"\E808"}.icon-print:before{content:"\E954"}.icon-reload:before{content:"\EA2E"}.icon-price-tags:before{content:"\E936"}.icon-eye:before{content:"\E9CE"}.icon-no-eye:before{content:"\E9D1"}.icon-calendar:before{content:"\E953"}.icon-time:before{content:"\E952"}.icon-image{background:no-repeat 50%/80%;padding-right:1em!important;padding-left:1em!important}.icon-image--carrot{background-image:url(themes/_global/img/icons/carrot-icon--white.png)}.icon-image--diaspora{background-image:url(themes/_global/img/icons/Diaspora-asterisk.svg)}.icon-image--unmark{background-image:url(themes/_global/img/icons/unmark-icon--black.png)}.icon-image--shaarli{background-image:url(themes/_global/img/icons/shaarli.png)}.icon-check.archive:before,.icon-star.fav:before{color:#fff}.login{background-color:#333}.login #main{padding:0;margin:0}.login form{background-color:#fff;padding:1.5em;box-shadow:0 1px 8px rgba(0,0,0,.9);width:20em;top:8em;margin-left:-10em}.login .logo,.login form{position:absolute;left:50%}.login .logo{top:2em;margin-left:-55px}.popup-form{background:rgba(0,0,0,.5);left:10em;height:100%;width:100%;margin:0;margin-top:-30%!important;display:none;border-left:1px solid #eee}.popup-form,.popup-form form{position:absolute;top:0;z-index:20;padding:2em}.popup-form form{background-color:#fff;left:0;border:10px solid #000;width:400px;height:200px}#bagit-form-form .addurl{margin-left:0}.close-button,.closeMessage{background-color:#000;color:#fff;font-size:1.2em;line-height:1.6;width:1.6em;height:1.6em;text-align:center;text-decoration:none}.close-button:focus,.close-button:hover,.closeMessage:focus,.closeMessage:hover{background-color:#999;color:#000}.close-button--popup{display:inline-block;position:absolute;top:0;right:0;font-size:1.4em}.active-current{background-color:#999}.active-current:after{content:"";width:0;height:0;position:absolute;border:10px solid transparent;border-right-color:#eee;right:0;top:50%;margin-top:-10px}.opacity03{opacity:.3}.add-to-wallabag-link-after{background-color:#000;color:#fff;padding:0 3px 2px}a.add-to-wallabag-link-after{visibility:hidden;position:absolute;opacity:0;transition-duration:2s;transition-timing-function:ease-out}#article article a:hover+a.add-to-wallabag-link-after,a.add-to-wallabag-link-after:hover{opacity:1;visibility:visible;transition-duration:.3s;transition-timing-function:ease-in}a.add-to-wallabag-link-after:after{content:"w"}#add-link-result{font-weight:700;font-size:.9em}.btn-clickable{cursor:pointer}.messages{text-align:left;width:60%;margin:auto 17%}.messages>*{display:inline-block}.messages .install{text-align:left}.messages .install.error{border:1px solid #c42608;color:#c00!important;background:#fff0ef}.messages .install.notice{border:1px solid #ebcd41;color:#000;background:#fffcd3}.messages .install.success{border:1px solid #6dc70c;background:#e0fbcc!important}.warning{font-weight:700;display:block;width:100%}.more-info{font-size:.85em;line-height:1.5;color:#aaa}.more-info a{color:#aaa}@media screen and (max-width:1050px){.entry{width:49%}.entry:nth-child(3n+1){margin-left:1.5%}.entry:nth-child(odd){margin-left:0}}@media screen and (max-width:900px){#article{width:80%}.topPosF{right:2.5em}}@media screen and (max-width:700px){.entry{width:100%;margin-left:0}#display-mode{display:none}}@media screen and (max-height:770px){.menu.developer,.menu.internal,.menu.users{display:none}}@media screen and (max-width:500px){.entry{width:100%;margin-left:0}body>header{background-color:#333;position:fixed;top:0;width:100%;height:3em;z-index:11}#links li:last-child{position:static;width:auto}#links li:last-child a:before{content:none}.logo{width:1.25em;height:1.25em;left:0;top:0}.login>header,.login form{position:static}.login form{width:100%;margin-left:0}.login .logo{height:auto;top:.5em;width:75px;margin-left:-37.5px}.desktopHide{display:block;position:fixed;z-index:20;top:0;right:0;border:0;width:2.5em;height:2.5em;cursor:pointer;background-color:#999;font-size:1.2em}.desktopHide:focus,.desktopHide:hover{background-color:#fff}#links{display:none;width:100%;height:auto;padding-top:3em}#links.menu--open{display:block}footer{margin-right:3em}#main,footer{position:static}#main{margin-left:1.5em;padding-right:1.5em;margin-top:3em}#article_toolbar .topPosF,.card-entry-labels{display:none}#article{width:100%}#article h1{font-size:1.5em}#article_toolbar a{padding:.3em .4em .2em}#display-mode{display:none}#bagit-form,#search-form,.popup-form{left:0;width:100%;border-left:none}#bagit-form form,#search-form form,.popup-form form{width:100%}}@media print{body{font-family:Serif;background-color:#fff}@page{margin:1cm}img{max-width:100%!important}#article-informations,#article .mbm a,#article_toolbar,#links,#sort,.entrie+.results,.messages,.top_link,body>.logo,body>footer,div.tools,header div{display:none!important}article{border:none!important}.vieworiginal a:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.pagination span.current{border-style:dashed}#main{margin:0;padding:0}#article,#main{width:100%}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{font-size:1em;line-height:1.5;margin:0}dl:first-child,h1:first-child,h2:first-child,h3:first-child,h4:first-child,h5:first-child,h6:first-child,ol:first-child,p:first-child,ul:first-child{margin-top:0}code,kbd,pre,samp{font-family:monospace,serif}pre{white-space:pre-wrap}.upper{text-transform:uppercase}.bold{font-weight:700}.inner{margin:0 auto;max-width:61.25em}figure,img,table{max-width:100%;height:auto}iframe{max-width:100%}.fl{float:left}.fr{float:right}table{border-collapse:collapse}figure{margin:0}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}input[type=search]{-webkit-appearance:textfield}.dib{display:inline-block;vertical-align:middle}.dnone{display:none}.dtable{display:table}.dtable>*{display:table-row}.dtable>*>*{display:table-cell}.element-invisible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.small{font-size:.8em}.big{font-size:1.2em}.w100{width:100%}.w90{width:90%}.w80{width:80%}.w70{width:70%}.w60{width:60%}.w50{width:50%}.w40{width:40%}.w30{width:30%}.w20{width:20%}.w10{width:10%}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}@media screen and (-webkit-min-device-pixel-ratio:0){select{-webkit-appearance:none;border-radius:0}} /*# sourceMappingURL=baggy.css.map*/ \ No newline at end of file diff --git a/web/wallassets/baggy.js b/web/wallassets/baggy.js index 655a80e36..a157f0ed4 100644 --- a/web/wallassets/baggy.js +++ b/web/wallassets/baggy.js @@ -1 +1 @@ -!function(e){function __webpack_require__(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:!1,exports:{}};return e[n].call(r.exports,r,r.exports,__webpack_require__),r.l=!0,r.exports}var t={};__webpack_require__.m=e,__webpack_require__.c=t,__webpack_require__.i=function(e){return e},__webpack_require__.d=function(e,t,n){__webpack_require__.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:n})},__webpack_require__.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return __webpack_require__.d(t,"a",t),t},__webpack_require__.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},__webpack_require__.p="",__webpack_require__(__webpack_require__.s=53)}([function(e,t,n){var r,o;!function(t,n){"object"==typeof e&&"object"==typeof e.exports?e.exports=t.document?n(t,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return n(e)}:n(t)}("undefined"!=typeof window?window:this,function(n,i){function isArrayLike(e){var t=!!e&&"length"in e&&e.length,n=m.type(e);return"function"!==n&&!m.isWindow(e)&&("array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e)}function winnow(e,t,n){if(m.isFunction(t))return m.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return m.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(k.test(t))return m.filter(t,e,n);t=m.filter(t,e)}return m.grep(e,function(e){return f.call(t,e)>-1!==n})}function sibling(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}function createOptions(e){var t={};return m.each(e.match(P)||[],function(e,n){t[n]=!0}),t}function completed(){s.removeEventListener("DOMContentLoaded",completed),n.removeEventListener("load",completed),m.ready()}function Data(){this.expando=m.expando+Data.uid++}function dataAttr(e,t,n){var r;if(void 0===n&&1===e.nodeType)if(r="data-"+t.replace(H,"-$&").toLowerCase(),"string"==typeof(n=e.getAttribute(r))){try{n="true"===n||"false"!==n&&("null"===n?null:+n+""===n?+n:F.test(n)?m.parseJSON(n):n)}catch(e){}j.set(e,t,n)}else n=void 0;return n}function adjustCSS(e,t,n,r){var o,i=1,a=20,s=r?function(){return r.cur()}:function(){return m.css(e,t,"")},u=s(),l=n&&n[3]||(m.cssNumber[t]?"":"px"),c=(m.cssNumber[t]||"px"!==l&&+u)&&B.exec(m.css(e,t));if(c&&c[3]!==l){l=l||c[3],n=n||[],c=+u||1;do{i=i||".5",c/=i,m.style(e,t,c+l)}while(i!==(i=s()/u)&&1!==i&&--a)}return n&&(c=+c||+u||0,o=n[1]?c+(n[1]+1)*n[2]:+n[2],r&&(r.unit=l,r.start=c,r.end=o)),o}function getAll(e,t){var n=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||"*"):void 0!==e.querySelectorAll?e.querySelectorAll(t||"*"):[];return void 0===t||t&&m.nodeName(e,t)?m.merge([e],n):n}function setGlobalEval(e,t){for(var n=0,r=e.length;n-1)o&&o.push(i);else if(l=m.contains(i.ownerDocument,i),a=getAll(f.appendChild(i),"script"),l&&setGlobalEval(a),n)for(c=0;i=a[c++];)$.test(i.type||"")&&n.push(i);return f}function returnTrue(){return!0}function returnFalse(){return!1}function safeActiveElement(){try{return s.activeElement}catch(e){}}function on(e,t,n,r,o,i){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)on(e,s,n,r,t[s],i);return e}if(null==r&&null==o?(o=n,r=n=void 0):null==o&&("string"==typeof n?(o=r,r=void 0):(o=r,r=n,n=void 0)),!1===o)o=returnFalse;else if(!o)return e;return 1===i&&(a=o,o=function(e){return m().off(e),a.apply(this,arguments)},o.guid=a.guid||(a.guid=m.guid++)),e.each(function(){m.event.add(this,t,o,r,n)})}function manipulationTarget(e,t){return m.nodeName(e,"table")&&m.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function disableScript(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function restoreScript(e){var t=ee.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function cloneCopyEvent(e,t){var n,r,o,i,a,s,u,l;if(1===t.nodeType){if(R.hasData(e)&&(i=R.access(e),a=R.set(t,i),l=i.events)){delete a.handle,a.events={};for(o in l)for(n=0,r=l[o].length;n1&&"string"==typeof p&&!g.checkClone&&Z.test(p))return e.each(function(o){var i=e.eq(o);v&&(t[0]=p.call(this,o,i.html())),domManip(i,t,n,r)});if(d&&(o=buildFragment(t,e[0].ownerDocument,!1,e,r),i=o.firstChild,1===o.childNodes.length&&(o=i),i||r)){for(a=m.map(getAll(o,"script"),disableScript),s=a.length;f")).appendTo(t.documentElement),t=ne[0].contentDocument,t.write(),t.close(),n=actualDisplay(e,t),ne.detach()),re[e]=n),n}function curCSS(e,t,n){var r,o,i,a,s=e.style;return n=n||ae(e),a=n?n.getPropertyValue(t)||n[t]:void 0,""!==a&&void 0!==a||m.contains(e.ownerDocument,e)||(a=m.style(e,t)),n&&!g.pixelMarginRight()&&ie.test(a)&&oe.test(t)&&(r=s.width,o=s.minWidth,i=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=o,s.maxWidth=i),void 0!==a?a+"":a}function addGetHookIf(e,t){return{get:function(){return e()?void delete this.get:(this.get=t).apply(this,arguments)}}}function vendorPropName(e){if(e in he)return e;for(var t=e[0].toUpperCase()+e.slice(1),n=de.length;n--;)if((e=de[n]+t)in he)return e}function setPositiveNumber(e,t,n){var r=B.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):t}function augmentWidthOrHeight(e,t,n,r,o){for(var i=n===(r?"border":"content")?4:"width"===t?1:0,a=0;i<4;i+=2)"margin"===n&&(a+=m.css(e,n+z[i],!0,o)),r?("content"===n&&(a-=m.css(e,"padding"+z[i],!0,o)),"margin"!==n&&(a-=m.css(e,"border"+z[i]+"Width",!0,o))):(a+=m.css(e,"padding"+z[i],!0,o),"padding"!==n&&(a+=m.css(e,"border"+z[i]+"Width",!0,o)));return a}function getWidthOrHeight(e,t,n){var r=!0,o="width"===t?e.offsetWidth:e.offsetHeight,i=ae(e),a="border-box"===m.css(e,"boxSizing",!1,i);if(o<=0||null==o){if(o=curCSS(e,t,i),(o<0||null==o)&&(o=e.style[t]),ie.test(o))return o;r=a&&(g.boxSizingReliable()||o===e.style[t]),o=parseFloat(o)||0}return o+augmentWidthOrHeight(e,t,n||(a?"border":"content"),r,i)+"px"}function showHide(e,t){for(var n,r,o,i=[],a=0,s=e.length;a=0&&n=0},isPlainObject:function(e){var t;if("object"!==m.type(e)||e.nodeType||m.isWindow(e))return!1;if(e.constructor&&!p.call(e,"constructor")&&!p.call(e.constructor.prototype||{},"isPrototypeOf"))return!1;for(t in e);return void 0===t||p.call(e,t)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?d[h.call(e)]||"object":typeof e},globalEval:function(e){var t,n=eval;(e=m.trim(e))&&(1===e.indexOf("use strict")?(t=s.createElement("script"),t.text=e,s.head.appendChild(t).parentNode.removeChild(t)):n(e))},camelCase:function(e){return e.replace(y,"ms-").replace(w,b)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t){var n,r=0;if(isArrayLike(e))for(n=e.length;rr.cacheLength&&delete cache[e.shift()],cache[t+" "]=n}var e=[];return cache}function markFunction(e){return e[b]=!0,e}function assert(e){var t=h.createElement("div");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function addHandle(e,t){for(var n=e.split("|"),o=n.length;o--;)r.attrHandle[n[o]]=t}function siblingCheck(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||A)-(~e.sourceIndex||A);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function createPositionalPseudo(e){return markFunction(function(t){return t=+t,markFunction(function(n,r){for(var o,i=e([],n.length,t),a=i.length;a--;)n[o=i[a]]&&(n[o]=!(r[o]=n[o]))})})}function testContext(e){return e&&void 0!==e.getElementsByTagName&&e}function setFilters(){}function toSelector(e){for(var t=0,n=e.length,r="";t1?function(t,n,r){for(var o=e.length;o--;)if(!e[o](t,n,r))return!1;return!0}:e[0]}function multipleContexts(e,t,n){for(var r=0,o=t.length;r-1&&(i[l]=!(a[l]=f))}}else v=condense(v===a?v.splice(p,v.length):v),o?o(null,a,v,u):L.apply(a,v)})}function matcherFromTokens(e){for(var t,n,o,i=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=addCombinator(function(e){return e===t},s,!0),f=addCombinator(function(e){return R(t,e)>-1},s,!0),d=[function(e,n,r){var o=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,o}];u1&&elementMatcher(d),u>1&&toSelector(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(I,"$1"),n,u0,o=e.length>0,i=function(i,a,s,u,c){var f,p,m,v=0,y="0",w=i&&[],b=[],x=l,C=i||o&&r.find.TAG("*",c),_=T+=null==x?1:Math.random()||.1,E=C.length;for(c&&(l=a===h||a||c);y!==E&&null!=(f=C[y]);y++){if(o&&f){for(p=0,a||f.ownerDocument===h||(d(f),s=!g);m=e[p++];)if(m(f,a||h,s)){u.push(f);break}c&&(T=_)}n&&((f=!m&&f)&&v--,i&&w.push(f))}if(v+=y,n&&y!==v){for(p=0;m=t[p++];)m(w,b,a,s);if(i){if(v>0)for(;y--;)w[y]||b[y]||(b[y]=P.call(u));b=condense(b)}L.apply(u,b),c&&!i&&b.length>0&&v+t.length>1&&Sizzle.uniqueSort(u)}return c&&(T=_,l=x),w};return n?markFunction(i):i}var t,n,r,o,i,a,s,u,l,c,f,d,h,p,g,m,v,y,w,b="sizzle"+1*new Date,x=e.document,T=0,C=0,_=createCache(),E=createCache(),k=createCache(),S=function(e,t){return e===t&&(f=!0),0},A=1<<31,N={}.hasOwnProperty,D=[],P=D.pop,O=D.push,L=D.push,M=D.slice,R=function(e,t){for(var n=0,r=e.length;n+~]|"+F+")"+F+"*"),$=new RegExp("="+F+"*([^\\]'\"]*?)"+F+"*\\]","g"),X=new RegExp(B),V=new RegExp("^"+H+"$"),Y={ID:new RegExp("^#("+H+")"),CLASS:new RegExp("^\\.("+H+")"),TAG:new RegExp("^("+H+"|[*])"),ATTR:new RegExp("^"+q),PSEUDO:new RegExp("^"+B),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+F+"*(even|odd|(([+-]|)(\\d*)n|)"+F+"*(?:([+-]|)"+F+"*(\\d+)|))"+F+"*\\)|)","i"),bool:new RegExp("^(?:"+j+")$","i"),needsContext:new RegExp("^"+F+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+F+"*((?:-\\d)?\\d*)"+F+"*\\)|)(?=[^-]|$)","i")},K=/^(?:input|select|textarea|button)$/i,G=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Z=/[+~]/,ee=/'|\\/g,te=new RegExp("\\\\([\\da-f]{1,6}"+F+"?|("+F+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=function(){d()};try{L.apply(D=M.call(x.childNodes),x.childNodes),D[x.childNodes.length].nodeType}catch(e){L={apply:D.length?function(e,t){O.apply(e,M.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}n=Sizzle.support={},i=Sizzle.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},d=Sizzle.setDocument=function(e){var t,o,a=e?e.ownerDocument||e:x;return a!==h&&9===a.nodeType&&a.documentElement?(h=a,p=h.documentElement,g=!i(h),(o=h.defaultView)&&o.top!==o&&(o.addEventListener?o.addEventListener("unload",re,!1):o.attachEvent&&o.attachEvent("onunload",re)),n.attributes=assert(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=assert(function(e){return e.appendChild(h.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(h.getElementsByClassName),n.getById=assert(function(e){return p.appendChild(e).id=b,!h.getElementsByName||!h.getElementsByName(b).length}),n.getById?(r.find.ID=function(e,t){if(void 0!==t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}},r.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}}):(delete r.find.ID,r.filter.ID=function(e){var t=e.replace(te,ne);return function(e){var n=void 0!==e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}}),r.find.TAG=n.getElementsByTagName?function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],o=0,i=t.getElementsByTagName(e);if("*"===e){for(;n=i[o++];)1===n.nodeType&&r.push(n);return r}return i},r.find.CLASS=n.getElementsByClassName&&function(e,t){if(void 0!==t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],m=[],(n.qsa=Q.test(h.querySelectorAll))&&(assert(function(e){p.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&m.push("[*^$]="+F+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||m.push("\\["+F+"*(?:value|"+j+")"),e.querySelectorAll("[id~="+b+"-]").length||m.push("~="),e.querySelectorAll(":checked").length||m.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||m.push(".#.+[+~]")}),assert(function(e){var t=h.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&m.push("name"+F+"*[*^$|!~]?="),e.querySelectorAll(":enabled").length||m.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),m.push(",.*:")})),(n.matchesSelector=Q.test(y=p.matches||p.webkitMatchesSelector||p.mozMatchesSelector||p.oMatchesSelector||p.msMatchesSelector))&&assert(function(e){n.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),v.push("!=",B)}),m=m.length&&new RegExp(m.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(p.compareDocumentPosition),w=t||Q.test(p.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},S=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1,1&r||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===h||e.ownerDocument===x&&w(x,e)?-1:t===h||t.ownerDocument===x&&w(x,t)?1:c?R(c,e)-R(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,o=e.parentNode,i=t.parentNode,a=[e],s=[t];if(!o||!i)return e===h?-1:t===h?1:o?-1:i?1:c?R(c,e)-R(c,t):0;if(o===i)return siblingCheck(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;a[r]===s[r];)r++;return r?siblingCheck(a[r],s[r]):a[r]===x?-1:s[r]===x?1:0},h):h},Sizzle.matches=function(e,t){return Sizzle(e,null,null,t)},Sizzle.matchesSelector=function(e,t){if((e.ownerDocument||e)!==h&&d(e),t=t.replace($,"='$1']"),n.matchesSelector&&g&&!k[t+" "]&&(!v||!v.test(t))&&(!m||!m.test(t)))try{var r=y.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return Sizzle(t,h,null,[e]).length>0},Sizzle.contains=function(e,t){return(e.ownerDocument||e)!==h&&d(e),w(e,t)},Sizzle.attr=function(e,t){(e.ownerDocument||e)!==h&&d(e);var o=r.attrHandle[t.toLowerCase()],i=o&&N.call(r.attrHandle,t.toLowerCase())?o(e,t,!g):void 0;return void 0!==i?i:n.attributes||!g?e.getAttribute(t):(i=e.getAttributeNode(t))&&i.specified?i.value:null},Sizzle.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},Sizzle.uniqueSort=function(e){var t,r=[],o=0,i=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(S),f){for(;t=e[i++];)t===e[i]&&(o=r.push(i));for(;o--;)e.splice(r[o],1)}return c=null,e},o=Sizzle.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r++];)n+=o(t);return n},r=Sizzle.selectors={cacheLength:50,createPseudo:markFunction,match:Y,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||Sizzle.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&Sizzle.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return Y.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=_[e+" "];return t||(t=new RegExp("(^|"+F+")"+e+"("+F+"|$)"))&&_(e,function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var o=Sizzle.attr(r,e);return null==o?"!="===t:!t||(o+="","="===t?o===n:"!="===t?o!==n:"^="===t?n&&0===o.indexOf(n):"*="===t?n&&o.indexOf(n)>-1:"$="===t?n&&o.slice(-n.length)===n:"~="===t?(" "+o.replace(z," ")+" ").indexOf(n)>-1:"|="===t&&(o===n||o.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,o){var i="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===o?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,d,h,p,g=i!==a?"nextSibling":"previousSibling",m=t.parentNode,v=s&&t.nodeName.toLowerCase(),y=!u&&!s,w=!1;if(m){if(i){for(;g;){for(d=t;d=d[g];)if(s?d.nodeName.toLowerCase()===v:1===d.nodeType)return!1;p=g="only"===e&&!p&&"nextSibling"}return!0}if(p=[a?m.firstChild:m.lastChild],a&&y){for(d=m,f=d[b]||(d[b]={}),c=f[d.uniqueID]||(f[d.uniqueID]={}),l=c[e]||[],h=l[0]===T&&l[1],w=h&&l[2],d=h&&m.childNodes[h];d=++h&&d&&d[g]||(w=h=0)||p.pop();)if(1===d.nodeType&&++w&&d===t){c[e]=[T,h,w];break}}else if(y&&(d=t,f=d[b]||(d[b]={}),c=f[d.uniqueID]||(f[d.uniqueID]={}),l=c[e]||[],h=l[0]===T&&l[1],w=h),!1===w)for(;(d=++h&&d&&d[g]||(w=h=0)||p.pop())&&((s?d.nodeName.toLowerCase()!==v:1!==d.nodeType)||!++w||(y&&(f=d[b]||(d[b]={}),c=f[d.uniqueID]||(f[d.uniqueID]={}),c[e]=[T,w]),d!==t)););return(w-=o)===r||w%r==0&&w/r>=0}}},PSEUDO:function(e,t){var n,o=r.pseudos[e]||r.setFilters[e.toLowerCase()]||Sizzle.error("unsupported pseudo: "+e);return o[b]?o(t):o.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?markFunction(function(e,n){for(var r,i=o(e,t),a=i.length;a--;)r=R(e,i[a]),e[r]=!(n[r]=i[a])}):function(e){return o(e,0,n)}):o}},pseudos:{not:markFunction(function(e){var t=[],n=[],r=s(e.replace(I,"$1"));return r[b]?markFunction(function(e,t,n,o){for(var i,a=r(e,null,o,[]),s=e.length;s--;)(i=a[s])&&(e[s]=!(t[s]=i))}):function(e,o,i){return t[0]=e,r(t,null,i,n),t[0]=null,!n.pop()}}),has:markFunction(function(e){return function(t){return Sizzle(e,t).length>0}}),contains:markFunction(function(e){return e=e.replace(te,ne),function(t){return(t.textContent||t.innerText||o(t)).indexOf(e)>-1}}),lang:markFunction(function(e){return V.test(e||"")||Sizzle.error("unsupported lang: "+e),e=e.replace(te,ne).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===p},focus:function(e){return e===h.activeElement&&(!h.hasFocus||h.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return!1===e.disabled},disabled:function(e){return!0===e.disabled},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return G.test(e.nodeName)},input:function(e){return K.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:createPositionalPseudo(function(){return[0]}),last:createPositionalPseudo(function(e,t){return[t-1]}),eq:createPositionalPseudo(function(e,t,n){return[n<0?n+t:n]}),even:createPositionalPseudo(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:createPositionalPseudo(function(e,t,n){for(var r=n<0?n+t:n;++r2&&"ID"===(c=l[0]).type&&n.getById&&9===t.nodeType&&g&&r.relative[l[1].type]){if(!(t=(r.find.ID(c.matches[0].replace(te,ne),t)||[])[0]))return o;h&&(t=t.parentNode),e=e.slice(l.shift().value.length)}for(u=Y.needsContext.test(e)?0:l.length;u--&&(c=l[u],!r.relative[f=c.type]);)if((d=r.find[f])&&(i=d(c.matches[0].replace(te,ne),Z.test(l[0].type)&&testContext(t.parentNode)||t))){if(l.splice(u,1),!(e=i.length&&toSelector(l)))return L.apply(o,i),o;break}}return(h||s(e,p))(i,t,!g,o,!t||Z.test(e)&&testContext(t.parentNode)||t),o},n.sortStable=b.split("").sort(S).join("")===b,n.detectDuplicates=!!f,d(),n.sortDetached=assert(function(e){return 1&e.compareDocumentPosition(h.createElement("div"))}),assert(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||addHandle("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&assert(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||addHandle("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),assert(function(e){return null==e.getAttribute("disabled")})||addHandle(j,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),Sizzle}(n);m.find=x,m.expr=x.selectors,m.expr[":"]=m.expr.pseudos,m.uniqueSort=m.unique=x.uniqueSort,m.text=x.getText,m.isXMLDoc=x.isXML,m.contains=x.contains;var T=function(e,t,n){for(var r=[],o=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(o&&m(e).is(n))break;r.push(e)}return r},C=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},_=m.expr.match.needsContext,E=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,k=/^.[^:#\[\.,]*$/;m.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?m.find.matchesSelector(r,e)?[r]:[]:m.find.matches(e,m.grep(t,function(e){return 1===e.nodeType}))},m.fn.extend({find:function(e){var t,n=this.length,r=[],o=this;if("string"!=typeof e)return this.pushStack(m(e).filter(function(){for(t=0;t1?m.unique(r):r),r.selector=this.selector?this.selector+" "+e:e,r},filter:function(e){return this.pushStack(winnow(this,e||[],!1))},not:function(e){return this.pushStack(winnow(this,e||[],!0))},is:function(e){return!!winnow(this,"string"==typeof e&&_.test(e)?m(e):e||[],!1).length}});var S,A=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/;(m.fn.init=function(e,t,n){var r,o;if(!e)return this;if(n=n||S,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:A.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof m?t[0]:t,m.merge(this,m.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:s,!0)),E.test(r[1])&&m.isPlainObject(t))for(r in t)m.isFunction(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return o=s.getElementById(r[2]),o&&o.parentNode&&(this.length=1,this[0]=o),this.context=s,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):m.isFunction(e)?void 0!==n.ready?n.ready(e):e(m):(void 0!==e.selector&&(this.selector=e.selector,this.context=e.context),m.makeArray(e,this))}).prototype=m.fn,S=m(s);var N=/^(?:parents|prev(?:Until|All))/,D={children:!0,contents:!0,next:!0,prev:!0};m.fn.extend({has:function(e){var t=m(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&m.find.matchesSelector(n,e))){i.push(n);break}return this.pushStack(i.length>1?m.uniqueSort(i):i)},index:function(e){return e?"string"==typeof e?f.call(m(e),this[0]):f.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(m.uniqueSort(m.merge(this.get(),m(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),m.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return T(e,"parentNode")},parentsUntil:function(e,t,n){return T(e,"parentNode",n)},next:function(e){return sibling(e,"nextSibling")},prev:function(e){return sibling(e,"previousSibling")},nextAll:function(e){return T(e,"nextSibling")},prevAll:function(e){return T(e,"previousSibling")},nextUntil:function(e,t,n){return T(e,"nextSibling",n)},prevUntil:function(e,t,n){return T(e,"previousSibling",n)},siblings:function(e){return C((e.parentNode||{}).firstChild,e)},children:function(e){return C(e.firstChild)},contents:function(e){return e.contentDocument||m.merge([],e.childNodes)}},function(e,t){m.fn[e]=function(n,r){var o=m.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(o=m.filter(r,o)),this.length>1&&(D[e]||m.uniqueSort(o),N.test(e)&&o.reverse()),this.pushStack(o)}});var P=/\S+/g;m.Callbacks=function(e){e="string"==typeof e?createOptions(e):m.extend({},e);var t,n,r,o,i=[],a=[],s=-1,u=function(){for(o=e.once,r=t=!0;a.length;s=-1)for(n=a.shift();++s-1;)i.splice(n,1),n<=s&&s--}),this},has:function(e){return e?m.inArray(e,i)>-1:i.length>0},empty:function(){return i&&(i=[]),this},disable:function(){return o=a=[],i=n="",this},disabled:function(){return!i},lock:function(){return o=a=[],n||(i=n=""),this},locked:function(){return!!o},fireWith:function(e,n){return o||(n=n||[],n=[e,n.slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l},m.extend({Deferred:function(e){var t=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return o.done(arguments).fail(arguments),this},then:function(){var e=arguments;return m.Deferred(function(n){m.each(t,function(t,i){var a=m.isFunction(e[t])&&e[t];o[i[1]](function(){var e=a&&a.apply(this,arguments);e&&m.isFunction(e.promise)?e.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[i[0]+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?m.extend(e,r):r}},o={};return r.pipe=r.then,m.each(t,function(e,i){var a=i[2],s=i[3];r[i[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),o[i[0]]=function(){return o[i[0]+"With"](this===o?r:this,arguments),this},o[i[0]+"With"]=a.fireWith}),r.promise(o),e&&e.call(o,o),o},when:function(e){var t,n,r,o=0,i=u.call(arguments),a=i.length,s=1!==a||e&&m.isFunction(e.promise)?a:0,l=1===s?e:m.Deferred(),c=function(e,n,r){return function(o){n[e]=this,r[e]=arguments.length>1?u.call(arguments):o,r===t?l.notifyWith(n,r):--s||l.resolveWith(n,r)}};if(a>1)for(t=new Array(a),n=new Array(a),r=new Array(a);o0||(O.resolveWith(s,[m]),m.fn.triggerHandler&&(m(s).triggerHandler("ready"),m(s).off("ready"))))}}),m.ready.promise=function(e){return O||(O=m.Deferred(),"complete"===s.readyState||"loading"!==s.readyState&&!s.documentElement.doScroll?n.setTimeout(m.ready):(s.addEventListener("DOMContentLoaded",completed),n.addEventListener("load",completed))),O.promise(e)},m.ready.promise();var L=function(e,t,n,r,o,i,a){var s=0,u=e.length,l=null==n;if("object"===m.type(n)){o=!0;for(s in n)L(e,t,s,n[s],!0,i,a)}else if(void 0!==r&&(o=!0,m.isFunction(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(m(e),n)})),t))for(;s-1&&void 0!==n&&j.set(this,e,t)})},null,t,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){j.remove(this,e)})}}),m.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=R.get(e,t),n&&(!r||m.isArray(n)?r=R.access(e,t,m.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=m.queue(e,t),r=n.length,o=n.shift(),i=m._queueHooks(e,t),a=function(){m.dequeue(e,t)};"inprogress"===o&&(o=n.shift(),r--),o&&("fx"===t&&n.unshift("inprogress"),delete i.stop,o.call(e,a,i)),!r&&i&&i.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return R.get(e,n)||R.access(e,n,{empty:m.Callbacks("once memory").add(function(){R.remove(e,[t+"queue",n])})})}}),m.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length",""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};X.optgroup=X.option,X.tbody=X.tfoot=X.colgroup=X.caption=X.thead,X.th=X.td;var V=/<|&#?\w+;/;!function(){var e=s.createDocumentFragment(),t=e.appendChild(s.createElement("div")),n=s.createElement("input");n.setAttribute("type","radio"),n.setAttribute("checked","checked"),n.setAttribute("name","t"),t.appendChild(n),g.checkClone=t.cloneNode(!0).cloneNode(!0).lastChild.checked,t.innerHTML="",g.noCloneChecked=!!t.cloneNode(!0).lastChild.defaultValue}();var Y=/^key/,K=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,G=/^([^.]*)(?:\.(.+)|)/;m.event={global:{},add:function(e,t,n,r,o){var i,a,s,u,l,c,f,d,h,p,g,v=R.get(e);if(v)for(n.handler&&(i=n,n=i.handler,o=i.selector),n.guid||(n.guid=m.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(t){return void 0!==m&&m.event.triggered!==t.type?m.event.dispatch.apply(e,arguments):void 0}),t=(t||"").match(P)||[""],l=t.length;l--;)s=G.exec(t[l])||[],h=g=s[1],p=(s[2]||"").split(".").sort(),h&&(f=m.event.special[h]||{},h=(o?f.delegateType:f.bindType)||h,f=m.event.special[h]||{},c=m.extend({type:h,origType:g,data:r,handler:n,guid:n.guid,selector:o,needsContext:o&&m.expr.match.needsContext.test(o),namespace:p.join(".")},i),(d=u[h])||(d=u[h]=[],d.delegateCount=0,f.setup&&!1!==f.setup.call(e,r,p,a)||e.addEventListener&&e.addEventListener(h,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),o?d.splice(d.delegateCount++,0,c):d.push(c),m.event.global[h]=!0)},remove:function(e,t,n,r,o){var i,a,s,u,l,c,f,d,h,p,g,v=R.hasData(e)&&R.get(e);if(v&&(u=v.events)){for(t=(t||"").match(P)||[""],l=t.length;l--;)if(s=G.exec(t[l])||[],h=g=s[1],p=(s[2]||"").split(".").sort(),h){for(f=m.event.special[h]||{},h=(r?f.delegateType:f.bindType)||h,d=u[h]||[],s=s[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=i=d.length;i--;)c=d[i],!o&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(d.splice(i,1),c.selector&&d.delegateCount--,f.remove&&f.remove.call(e,c));a&&!d.length&&(f.teardown&&!1!==f.teardown.call(e,p,v.handle)||m.removeEvent(e,h,v.handle),delete u[h])}else for(h in u)m.event.remove(e,h+t[l],n,r,!0);m.isEmptyObject(u)&&R.remove(e,"handle events")}},dispatch:function(e){e=m.event.fix(e);var t,n,r,o,i,a=[],s=u.call(arguments),l=(R.get(this,"events")||{})[e.type]||[],c=m.event.special[e.type]||{};if(s[0]=e,e.delegateTarget=this,!c.preDispatch||!1!==c.preDispatch.call(this,e)){for(a=m.event.handlers.call(this,e,l),t=0;(o=a[t++])&&!e.isPropagationStopped();)for(e.currentTarget=o.elem,n=0;(i=o.handlers[n++])&&!e.isImmediatePropagationStopped();)e.rnamespace&&!e.rnamespace.test(i.namespace)||(e.handleObj=i,e.data=i.data,void 0!==(r=((m.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,s))&&!1===(e.result=r)&&(e.preventDefault(),e.stopPropagation()));return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,o,i,a=[],s=t.delegateCount,u=e.target;if(s&&u.nodeType&&("click"!==e.type||isNaN(e.button)||e.button<1))for(;u!==this;u=u.parentNode||this)if(1===u.nodeType&&(!0!==u.disabled||"click"!==e.type)){for(r=[],n=0;n-1:m.find(o,this,null,[u]).length),r[o]&&r.push(i);r.length&&a.push({elem:u,handlers:r})}return s]*)\/>/gi,J=/\s*$/g;m.extend({htmlPrefilter:function(e){return e.replace(Q,"<$1>")},clone:function(e,t,n){var r,o,i,a,s=e.cloneNode(!0),u=m.contains(e.ownerDocument,e);if(!(g.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||m.isXMLDoc(e)))for(a=getAll(s),i=getAll(e),r=0,o=i.length;r0&&setGlobalEval(a,!u&&getAll(e,"script")),s},cleanData:function(e){for(var t,n,r,o=m.event.special,i=0;void 0!==(n=e[i]);i++)if(M(n)){if(t=n[R.expando]){if(t.events)for(r in t.events)o[r]?m.event.remove(n,r):m.removeEvent(n,r,t.handle);n[R.expando]=void 0}n[j.expando]&&(n[j.expando]=void 0)}}}),m.fn.extend({domManip:domManip,detach:function(e){return remove(this,e,!0)},remove:function(e){return remove(this,e)},text:function(e){return L(this,function(e){return void 0===e?m.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return domManip(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){manipulationTarget(this,e).appendChild(e)}})},prepend:function(){return domManip(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=manipulationTarget(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return domManip(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return domManip(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(m.cleanData(getAll(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return m.clone(this,e,t)})},html:function(e){return L(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!J.test(e)&&!X[(U.exec(e)||["",""])[1].toLowerCase()]){e=m.htmlPrefilter(e);try{for(;n1)},show:function(){return showHide(this,!0)},hide:function(){return showHide(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){I(this)?m(this).show():m(this).hide()})}}),m.Tween=Tween,Tween.prototype={constructor:Tween,init:function(e,t,n,r,o,i){this.elem=e,this.prop=n,this.easing=o||m.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=i||(m.cssNumber[n]?"":"px")},cur:function(){var e=Tween.propHooks[this.prop];return e&&e.get?e.get(this):Tween.propHooks._default.get(this)},run:function(e){var t,n=Tween.propHooks[this.prop];return this.options.duration?this.pos=t=m.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):Tween.propHooks._default.set(this),this}},Tween.prototype.init.prototype=Tween.prototype,Tween.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=m.css(e.elem,e.prop,""),t&&"auto"!==t?t:0)},set:function(e){m.fx.step[e.prop]?m.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[m.cssProps[e.prop]]&&!m.cssHooks[e.prop]?e.elem[e.prop]=e.now:m.style(e.elem,e.prop,e.now+e.unit)}}},Tween.propHooks.scrollTop=Tween.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},m.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},m.fx=Tween.prototype.init,m.fx.step={};var pe,ge,me=/^(?:toggle|show|hide)$/,ve=/queueHooks$/;m.Animation=m.extend(Animation,{tweeners:{"*":[function(e,t){var n=this.createTween(e,t);return adjustCSS(n.elem,e,B.exec(t),n),n}]},tweener:function(e,t){m.isFunction(e)?(t=e,e=["*"]):e=e.match(P);for(var n,r=0,o=e.length;r1)},removeAttr:function(e){return this.each(function(){m.removeAttr(this,e)})}}),m.extend({attr:function(e,t,n){var r,o,i=e.nodeType;if(3!==i&&8!==i&&2!==i)return void 0===e.getAttribute?m.prop(e,t,n):(1===i&&m.isXMLDoc(e)||(t=t.toLowerCase(),o=m.attrHooks[t]||(m.expr.match.bool.test(t)?ye:void 0)),void 0!==n?null===n?void m.removeAttr(e,t):o&&"set"in o&&void 0!==(r=o.set(e,n,t))?r:(e.setAttribute(t,n+""),n):o&&"get"in o&&null!==(r=o.get(e,t))?r:(r=m.find.attr(e,t),null==r?void 0:r))},attrHooks:{type:{set:function(e,t){if(!g.radioValue&&"radio"===t&&m.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r,o=0,i=t&&t.match(P);if(i&&1===e.nodeType)for(;n=i[o++];)r=m.propFix[n]||n,m.expr.match.bool.test(n)&&(e[r]=!1),e.removeAttribute(n)}}),ye={set:function(e,t,n){return!1===t?m.removeAttr(e,n):e.setAttribute(n,n),n}},m.each(m.expr.match.bool.source.match(/\w+/g),function(e,t){var n=we[t]||m.find.attr;we[t]=function(e,t,r){var o,i;return r||(i=we[t],we[t]=o,o=null!=n(e,t,r)?t.toLowerCase():null,we[t]=i),o}});var be=/^(?:input|select|textarea|button)$/i,xe=/^(?:a|area)$/i;m.fn.extend({prop:function(e,t){return L(this,m.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[m.propFix[e]||e]})}}),m.extend({prop:function(e,t,n){var r,o,i=e.nodeType;if(3!==i&&8!==i&&2!==i)return 1===i&&m.isXMLDoc(e)||(t=m.propFix[t]||t,o=m.propHooks[t]),void 0!==n?o&&"set"in o&&void 0!==(r=o.set(e,n,t))?r:e[t]=n:o&&"get"in o&&null!==(r=o.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=m.find.attr(e,"tabindex");return t?parseInt(t,10):be.test(e.nodeName)||xe.test(e.nodeName)&&e.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),g.optSelected||(m.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),m.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){m.propFix[this.toLowerCase()]=this});var Te=/[\t\r\n\f]/g;m.fn.extend({addClass:function(e){var t,n,r,o,i,a,s,u=0;if(m.isFunction(e))return this.each(function(t){m(this).addClass(e.call(this,t,getClass(this)))});if("string"==typeof e&&e)for(t=e.match(P)||[];n=this[u++];)if(o=getClass(n),r=1===n.nodeType&&(" "+o+" ").replace(Te," ")){for(a=0;i=t[a++];)r.indexOf(" "+i+" ")<0&&(r+=i+" ");s=m.trim(r),o!==s&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,o,i,a,s,u=0;if(m.isFunction(e))return this.each(function(t){m(this).removeClass(e.call(this,t,getClass(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof e&&e)for(t=e.match(P)||[];n=this[u++];)if(o=getClass(n),r=1===n.nodeType&&(" "+o+" ").replace(Te," ")){for(a=0;i=t[a++];)for(;r.indexOf(" "+i+" ")>-1;)r=r.replace(" "+i+" "," ");s=m.trim(r),o!==s&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):m.isFunction(e)?this.each(function(n){m(this).toggleClass(e.call(this,n,getClass(this),t),t)}):this.each(function(){var t,r,o,i;if("string"===n)for(r=0,o=m(this),i=e.match(P)||[];t=i[r++];)o.hasClass(t)?o.removeClass(t):o.addClass(t);else void 0!==e&&"boolean"!==n||(t=getClass(this),t&&R.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":R.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;for(t=" "+e+" ";n=this[r++];)if(1===n.nodeType&&(" "+getClass(n)+" ").replace(Te," ").indexOf(t)>-1)return!0;return!1}});var Ce=/\r/g,_e=/[\x20\t\r\n\f]+/g;m.fn.extend({val:function(e){var t,n,r,o=this[0];{if(arguments.length)return r=m.isFunction(e),this.each(function(n){var o;1===this.nodeType&&(o=r?e.call(this,n,m(this).val()):e,null==o?o="":"number"==typeof o?o+="":m.isArray(o)&&(o=m.map(o,function(e){return null==e?"":e+""})),(t=m.valHooks[this.type]||m.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,o,"value")||(this.value=o))});if(o)return(t=m.valHooks[o.type]||m.valHooks[o.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(o,"value"))?n:(n=o.value,"string"==typeof n?n.replace(Ce,""):null==n?"":n)}}}),m.extend({valHooks:{option:{get:function(e){var t=m.find.attr(e,"value");return null!=t?t:m.trim(m.text(e)).replace(_e," ")}},select:{get:function(e){for(var t,n,r=e.options,o=e.selectedIndex,i="select-one"===e.type||o<0,a=i?null:[],s=i?o+1:r.length,u=o<0?s:i?o:0;u-1)&&(n=!0);return n||(e.selectedIndex=-1),i}}}}),m.each(["radio","checkbox"],function(){m.valHooks[this]={set:function(e,t){if(m.isArray(t))return e.checked=m.inArray(m(e).val(),t)>-1}},g.checkOn||(m.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Ee=/^(?:focusinfocus|focusoutblur)$/;m.extend(m.event,{trigger:function(e,t,r,o){var i,a,u,l,c,f,d,h=[r||s],g=p.call(e,"type")?e.type:e,v=p.call(e,"namespace")?e.namespace.split("."):[];if(a=u=r=r||s,3!==r.nodeType&&8!==r.nodeType&&!Ee.test(g+m.event.triggered)&&(g.indexOf(".")>-1&&(v=g.split("."),g=v.shift(),v.sort()),c=g.indexOf(":")<0&&"on"+g,e=e[m.expando]?e:new m.Event(g,"object"==typeof e&&e),e.isTrigger=o?2:3,e.namespace=v.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+v.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=r),t=null==t?[e]:m.makeArray(t,[e]),d=m.event.special[g]||{},o||!d.trigger||!1!==d.trigger.apply(r,t))){if(!o&&!d.noBubble&&!m.isWindow(r)){for(l=d.delegateType||g,Ee.test(l+g)||(a=a.parentNode);a;a=a.parentNode)h.push(a),u=a;u===(r.ownerDocument||s)&&h.push(u.defaultView||u.parentWindow||n)}for(i=0;(a=h[i++])&&!e.isPropagationStopped();)e.type=i>1?l:d.bindType||g,f=(R.get(a,"events")||{})[e.type]&&R.get(a,"handle"),f&&f.apply(a,t),(f=c&&a[c])&&f.apply&&M(a)&&(e.result=f.apply(a,t),!1===e.result&&e.preventDefault());return e.type=g,o||e.isDefaultPrevented()||d._default&&!1!==d._default.apply(h.pop(),t)||!M(r)||c&&m.isFunction(r[g])&&!m.isWindow(r)&&(u=r[c],u&&(r[c]=null),m.event.triggered=g,r[g](),m.event.triggered=void 0,u&&(r[c]=u)),e.result}},simulate:function(e,t,n){var r=m.extend(new m.Event,n,{type:e,isSimulated:!0});m.event.trigger(r,null,t)}}),m.fn.extend({trigger:function(e,t){return this.each(function(){m.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return m.event.trigger(e,t,n,!0)}}),m.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){m.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),m.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),g.focusin="onfocusin"in n,g.focusin||m.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){m.event.simulate(t,e.target,m.event.fix(e))};m.event.special[t]={setup:function(){var r=this.ownerDocument||this,o=R.access(r,t);o||r.addEventListener(e,n,!0),R.access(r,t,(o||0)+1)},teardown:function(){var r=this.ownerDocument||this,o=R.access(r,t)-1;o?R.access(r,t,o):(r.removeEventListener(e,n,!0),R.remove(r,t))}}});var ke=n.location,Se=m.now(),Ae=/\?/;m.parseJSON=function(e){return JSON.parse(e+"")},m.parseXML=function(e){var t;if(!e||"string"!=typeof e)return null;try{t=(new n.DOMParser).parseFromString(e,"text/xml")}catch(e){t=void 0}return t&&!t.getElementsByTagName("parsererror").length||m.error("Invalid XML: "+e),t};var Ne=/#.*$/,De=/([?&])_=[^&]*/,Pe=/^(.*?):[ \t]*([^\r\n]*)$/gm,Oe=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Le=/^(?:GET|HEAD)$/,Me=/^\/\//,Re={},je={},Fe="*/".concat("*"),He=s.createElement("a");He.href=ke.href,m.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:ke.href,type:"GET",isLocal:Oe.test(ke.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Fe,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":m.parseJSON,"text xml":m.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?ajaxExtend(ajaxExtend(e,m.ajaxSettings),t):ajaxExtend(m.ajaxSettings,e)},ajaxPrefilter:addToPrefiltersOrTransports(Re),ajaxTransport:addToPrefiltersOrTransports(je),ajax:function(e,t){function done(e,t,a,s){var l,f,w,b,T,_=t;2!==x&&(x=2,u&&n.clearTimeout(u),r=void 0,i=s||"",C.readyState=e>0?4:0,l=e>=200&&e<300||304===e,a&&(b=ajaxHandleResponses(d,C,a)),b=ajaxConvert(d,b,C,l),l?(d.ifModified&&(T=C.getResponseHeader("Last-Modified"),T&&(m.lastModified[o]=T),(T=C.getResponseHeader("etag"))&&(m.etag[o]=T)),204===e||"HEAD"===d.type?_="nocontent":304===e?_="notmodified":(_=b.state,f=b.data,w=b.error,l=!w)):(w=_,!e&&_||(_="error",e<0&&(e=0))),C.status=e,C.statusText=(t||_)+"",l?g.resolveWith(h,[f,_,C]):g.rejectWith(h,[C,_,w]),C.statusCode(y),y=void 0,c&&p.trigger(l?"ajaxSuccess":"ajaxError",[C,d,l?f:w]),v.fireWith(h,[C,_]),c&&(p.trigger("ajaxComplete",[C,d]),--m.active||m.event.trigger("ajaxStop")))}"object"==typeof e&&(t=e,e=void 0),t=t||{};var r,o,i,a,u,l,c,f,d=m.ajaxSetup({},t),h=d.context||d,p=d.context&&(h.nodeType||h.jquery)?m(h):m.event,g=m.Deferred(),v=m.Callbacks("once memory"),y=d.statusCode||{},w={},b={},x=0,T="canceled",C={readyState:0,getResponseHeader:function(e){var t;if(2===x){if(!a)for(a={};t=Pe.exec(i);)a[t[1].toLowerCase()]=t[2];t=a[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===x?i:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return x||(e=b[n]=b[n]||e,w[e]=t),this},overrideMimeType:function(e){return x||(d.mimeType=e),this},statusCode:function(e){var t;if(e)if(x<2)for(t in e)y[t]=[y[t],e[t]];else C.always(e[C.status]);return this},abort:function(e){var t=e||T;return r&&r.abort(t),done(0,t),this}};if(g.promise(C).complete=v.add,C.success=C.done,C.error=C.fail,d.url=((e||d.url||ke.href)+"").replace(Ne,"").replace(Me,ke.protocol+"//"),d.type=t.method||t.type||d.method||d.type,d.dataTypes=m.trim(d.dataType||"*").toLowerCase().match(P)||[""],null==d.crossDomain){l=s.createElement("a");try{l.href=d.url,l.href=l.href,d.crossDomain=He.protocol+"//"+He.host!=l.protocol+"//"+l.host}catch(e){d.crossDomain=!0}}if(d.data&&d.processData&&"string"!=typeof d.data&&(d.data=m.param(d.data,d.traditional)),inspectPrefiltersOrTransports(Re,d,t,C),2===x)return C;c=m.event&&d.global,c&&0==m.active++&&m.event.trigger("ajaxStart"),d.type=d.type.toUpperCase(),d.hasContent=!Le.test(d.type),o=d.url,d.hasContent||(d.data&&(o=d.url+=(Ae.test(o)?"&":"?")+d.data,delete d.data),!1===d.cache&&(d.url=De.test(o)?o.replace(De,"$1_="+Se++):o+(Ae.test(o)?"&":"?")+"_="+Se++)),d.ifModified&&(m.lastModified[o]&&C.setRequestHeader("If-Modified-Since",m.lastModified[o]),m.etag[o]&&C.setRequestHeader("If-None-Match",m.etag[o])),(d.data&&d.hasContent&&!1!==d.contentType||t.contentType)&&C.setRequestHeader("Content-Type",d.contentType),C.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+("*"!==d.dataTypes[0]?", "+Fe+"; q=0.01":""):d.accepts["*"]);for(f in d.headers)C.setRequestHeader(f,d.headers[f]);if(d.beforeSend&&(!1===d.beforeSend.call(h,C,d)||2===x))return C.abort();T="abort";for(f in{success:1,error:1,complete:1})C[f](d[f]);if(r=inspectPrefiltersOrTransports(je,d,t,C)){if(C.readyState=1,c&&p.trigger("ajaxSend",[C,d]),2===x)return C;d.async&&d.timeout>0&&(u=n.setTimeout(function(){C.abort("timeout")},d.timeout));try{x=1,r.send(w,done)}catch(e){if(!(x<2))throw e;done(-1,e)}}else done(-1,"No Transport");return C},getJSON:function(e,t,n){return m.get(e,t,n,"json")},getScript:function(e,t){return m.get(e,void 0,t,"script")}}),m.each(["get","post"],function(e,t){m[t]=function(e,n,r,o){return m.isFunction(n)&&(o=o||r,r=n,n=void 0),m.ajax(m.extend({url:e,type:t,dataType:o,data:n,success:r},m.isPlainObject(e)&&e))}}),m._evalUrl=function(e){return m.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,throws:!0})},m.fn.extend({wrapAll:function(e){var t;return m.isFunction(e)?this.each(function(t){m(this).wrapAll(e.call(this,t))}):(this[0]&&(t=m(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstElementChild;)e=e.firstElementChild;return e}).append(this)),this)},wrapInner:function(e){return m.isFunction(e)?this.each(function(t){m(this).wrapInner(e.call(this,t))}):this.each(function(){var t=m(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=m.isFunction(e);return this.each(function(n){m(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){m.nodeName(this,"body")||m(this).replaceWith(this.childNodes)}).end()}}),m.expr.filters.hidden=function(e){return!m.expr.filters.visible(e)},m.expr.filters.visible=function(e){return e.offsetWidth>0||e.offsetHeight>0||e.getClientRects().length>0};var qe=/%20/g,Be=/\[\]$/,ze=/\r?\n/g,Ie=/^(?:submit|button|image|reset|file)$/i,We=/^(?:input|select|textarea|keygen)/i;m.param=function(e,t){var n,r=[],o=function(e,t){t=m.isFunction(t)?t():null==t?"":t,r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(void 0===t&&(t=m.ajaxSettings&&m.ajaxSettings.traditional),m.isArray(e)||e.jquery&&!m.isPlainObject(e))m.each(e,function(){o(this.name,this.value)});else for(n in e)buildParams(n,e[n],t,o);return r.join("&").replace(qe,"+")},m.fn.extend({serialize:function(){return m.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=m.prop(this,"elements");return e?m.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!m(this).is(":disabled")&&We.test(this.nodeName)&&!Ie.test(e)&&(this.checked||!W.test(e))}).map(function(e,t){var n=m(this).val();return null==n?null:m.isArray(n)?m.map(n,function(e){return{name:t.name,value:e.replace(ze,"\r\n")}}):{name:t.name,value:n.replace(ze,"\r\n")}}).get()}}),m.ajaxSettings.xhr=function(){try{return new n.XMLHttpRequest}catch(e){}};var Ue={0:200,1223:204},$e=m.ajaxSettings.xhr();g.cors=!!$e&&"withCredentials"in $e,g.ajax=$e=!!$e,m.ajaxTransport(function(e){var t,r;if(g.cors||$e&&!e.crossDomain)return{send:function(o,i){var a,s=e.xhr();if(s.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(a in e.xhrFields)s[a]=e.xhrFields[a];e.mimeType&&s.overrideMimeType&&s.overrideMimeType(e.mimeType),e.crossDomain||o["X-Requested-With"]||(o["X-Requested-With"]="XMLHttpRequest");for(a in o)s.setRequestHeader(a,o[a]);t=function(e){return function(){t&&(t=r=s.onload=s.onerror=s.onabort=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?i(0,"error"):i(s.status,s.statusText):i(Ue[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=t(),r=s.onerror=t("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&n.setTimeout(function(){t&&r()})},t=t("abort");try{s.send(e.hasContent&&e.data||null)}catch(e){if(t)throw e}},abort:function(){t&&t()}}}),m.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return m.globalEval(e),e}}}),m.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),m.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(r,o){t=m(" + + {% endfor %} {% endif %} -

    {{ 'config.reset.title'|trans }}

    -
    -

    {{ 'config.reset.description'|trans }}

    - -
    - {{ form_widget(form.user._token) }} {{ form_widget(form.user.save) }} @@ -277,7 +270,7 @@ {% endfor %}
- {{ form_start(form.new_tagging_rule) }} + {{ form_start(form.new_tagging_rule) }} {{ form_errors(form.new_tagging_rule) }}
@@ -382,4 +375,31 @@ + +

{{ 'config.reset.title'|trans }}

+
+

{{ 'config.reset.description'|trans }}

+ +
{% endblock %} 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 358009897..887d154f4 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 @@ -112,8 +112,7 @@ @@ -198,22 +197,38 @@ {% if twofactor_auth %} -
-
+
{{ 'config.form_user.two_factor_description'|trans }} -
+
+ {{ form_widget(form.user.emailTwoFactor) }} + {{ form_label(form.user.emailTwoFactor) }} + {{ form_errors(form.user.emailTwoFactor) }} +
+
+ {{ form_widget(form.user.googleTwoFactor) }} + {{ form_label(form.user.googleTwoFactor) }} + {{ form_errors(form.user.googleTwoFactor) }} +
+
- {{ form_widget(form.user.twoFactorAuthentication) }} - {{ form_label(form.user.twoFactorAuthentication) }} - {{ form_errors(form.user.twoFactorAuthentication) }} -
- -
+ {% for OTPSecret in app.session.flashbag.get('OTPSecret') %} +
+ You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. +
+ That code will disapear after a page reload. +

+ {{ OTPSecret.code }} +

+ Or you can scan that QR Code with your app: +
+ + + +
+ {% endfor %} {% endif %} {{ form_widget(form.user.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }} diff --git a/src/Wallabag/UserBundle/Controller/ManageController.php b/src/Wallabag/UserBundle/Controller/ManageController.php index a9746fb47..08ed25dd1 100644 --- a/src/Wallabag/UserBundle/Controller/ManageController.php +++ b/src/Wallabag/UserBundle/Controller/ManageController.php @@ -8,6 +8,7 @@ use Pagerfanta\Adapter\DoctrineORMAdapter; use Pagerfanta\Exception\OutOfRangeCurrentPageException; use Pagerfanta\Pagerfanta; use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Wallabag\UserBundle\Entity\User; @@ -31,10 +32,10 @@ class ManageController extends Controller // enable created user by default $user->setEnabled(true); - $form = $this->createForm('Wallabag\UserBundle\Form\NewUserType', $user); - $form->handleRequest($request); + $form = $this->createEditForm('NewUserType', $user, $request); if ($form->isSubmitted() && $form->isValid()) { + $user = $this->handleOtp($form, $user); $userManager->updateUser($user); // dispatch a created event so the associated config will be created @@ -62,14 +63,14 @@ class ManageController extends Controller */ public function editAction(Request $request, User $user) { - $deleteForm = $this->createDeleteForm($user); - $editForm = $this->createForm('Wallabag\UserBundle\Form\UserType', $user); - $editForm->handleRequest($request); + $userManager = $this->container->get('fos_user.user_manager'); - if ($editForm->isSubmitted() && $editForm->isValid()) { - $em = $this->getDoctrine()->getManager(); - $em->persist($user); - $em->flush(); + $deleteForm = $this->createDeleteForm($user); + $form = $this->createEditForm('UserType', $user, $request); + + if ($form->isSubmitted() && $form->isValid()) { + $user = $this->handleOtp($form, $user); + $userManager->updateUser($user); $this->get('session')->getFlashBag()->add( 'notice', @@ -81,7 +82,7 @@ class ManageController extends Controller return $this->render('WallabagUserBundle:Manage:edit.html.twig', [ 'user' => $user, - 'edit_form' => $editForm->createView(), + 'edit_form' => $form->createView(), 'delete_form' => $deleteForm->createView(), 'twofactor_auth' => $this->getParameter('twofactor_auth'), ]); @@ -157,7 +158,7 @@ class ManageController extends Controller } /** - * Creates a form to delete a User entity. + * Create a form to delete a User entity. * * @param User $user The User entity * @@ -171,4 +172,50 @@ class ManageController extends Controller ->getForm() ; } + + /** + * Create a form to create or edit a User entity. + * + * @param string $type Might be NewUserType or UserType + * @param User $user The new / edit user + * @param Request $request The request + * + * @return FormInterface + */ + private function createEditForm($type, User $user, Request $request) + { + $form = $this->createForm('Wallabag\UserBundle\Form\\' . $type, $user); + $form->handleRequest($request); + + // `googleTwoFactor` isn't a field within the User entity, we need to define it's value in a different way + if (true === $user->isGoogleAuthenticatorEnabled() && false === $form->isSubmitted()) { + $form->get('googleTwoFactor')->setData(true); + } + + return $form; + } + + /** + * Handle OTP update, taking care to only have one 2fa enable at a time. + * + * @see ConfigController + * + * @param FormInterface $form + * @param User $user + * + * @return User + */ + private function handleOtp(FormInterface $form, User $user) + { + if (true === $form->get('googleTwoFactor')->getData() && false === $user->isGoogleAuthenticatorEnabled()) { + $user->setGoogleAuthenticatorSecret($this->get('scheb_two_factor.security.google_authenticator')->generateSecret()); + $user->setEmailTwoFactor(false); + + return $user; + } + + $user->setGoogleAuthenticatorSecret(null); + + return $user; + } } diff --git a/src/Wallabag/UserBundle/Entity/User.php b/src/Wallabag/UserBundle/Entity/User.php index 48446e3c1..6e305719f 100644 --- a/src/Wallabag/UserBundle/Entity/User.php +++ b/src/Wallabag/UserBundle/Entity/User.php @@ -8,8 +8,8 @@ use FOS\UserBundle\Model\User as BaseUser; use JMS\Serializer\Annotation\Accessor; use JMS\Serializer\Annotation\Groups; use JMS\Serializer\Annotation\XmlRoot; -use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface; -use Scheb\TwoFactorBundle\Model\TrustedComputerInterface; +use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface as EmailTwoFactorInterface; +use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface as GoogleTwoFactorInterface; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Security\Core\User\UserInterface; use Wallabag\ApiBundle\Entity\Client; @@ -28,7 +28,7 @@ use Wallabag\CoreBundle\Helper\EntityTimestampsTrait; * @UniqueEntity("email") * @UniqueEntity("username") */ -class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterface +class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorInterface { use EntityTimestampsTrait; @@ -122,17 +122,17 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf */ private $authCode; + /** + * @ORM\Column(name="googleAuthenticatorSecret", type="string", nullable=true) + */ + private $googleAuthenticatorSecret; + /** * @var bool * * @ORM\Column(type="boolean") */ - private $twoFactorAuthentication = false; - - /** - * @ORM\Column(type="json_array", nullable=true) - */ - private $trusted; + private $emailTwoFactor = false; public function __construct() { @@ -233,49 +233,89 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf /** * @return bool */ - public function isTwoFactorAuthentication() + public function isEmailTwoFactor() { - return $this->twoFactorAuthentication; + return $this->emailTwoFactor; } /** - * @param bool $twoFactorAuthentication + * @param bool $emailTwoFactor */ - public function setTwoFactorAuthentication($twoFactorAuthentication) + public function setEmailTwoFactor($emailTwoFactor) { - $this->twoFactorAuthentication = $twoFactorAuthentication; + $this->emailTwoFactor = $emailTwoFactor; } - public function isEmailAuthEnabled() + /** + * Used in the user config form to be "like" the email option. + */ + public function isGoogleTwoFactor() { - return $this->twoFactorAuthentication; + return $this->isGoogleAuthenticatorEnabled(); } - public function getEmailAuthCode() + /** + * {@inheritdoc} + */ + public function isEmailAuthEnabled(): bool + { + return $this->emailTwoFactor; + } + + /** + * {@inheritdoc} + */ + public function getEmailAuthCode(): string { return $this->authCode; } - public function setEmailAuthCode($authCode) + /** + * {@inheritdoc} + */ + public function setEmailAuthCode(string $authCode): void { $this->authCode = $authCode; } - public function addTrustedComputer($token, \DateTime $validUntil) + /** + * {@inheritdoc} + */ + public function getEmailAuthRecipient(): string { - $this->trusted[$token] = $validUntil->format('r'); + return $this->email; } - public function isTrustedComputer($token) + /** + * {@inheritdoc} + */ + public function isGoogleAuthenticatorEnabled(): bool { - if (isset($this->trusted[$token])) { - $now = new \DateTime(); - $validUntil = new \DateTime($this->trusted[$token]); + return $this->googleAuthenticatorSecret ? true : false; + } - return $now < $validUntil; - } + /** + * {@inheritdoc} + */ + public function getGoogleAuthenticatorUsername(): string + { + return $this->username; + } - return false; + /** + * {@inheritdoc} + */ + public function getGoogleAuthenticatorSecret(): string + { + return $this->googleAuthenticatorSecret; + } + + /** + * {@inheritdoc} + */ + public function setGoogleAuthenticatorSecret(?string $googleAuthenticatorSecret): void + { + $this->googleAuthenticatorSecret = $googleAuthenticatorSecret; } /** diff --git a/src/Wallabag/UserBundle/Form/UserType.php b/src/Wallabag/UserBundle/Form/UserType.php index 56fea640b..026db9a2c 100644 --- a/src/Wallabag/UserBundle/Form/UserType.php +++ b/src/Wallabag/UserBundle/Form/UserType.php @@ -35,9 +35,14 @@ class UserType extends AbstractType 'required' => false, 'label' => 'user.form.enabled_label', ]) - ->add('twoFactorAuthentication', CheckboxType::class, [ + ->add('emailTwoFactor', CheckboxType::class, [ 'required' => false, - 'label' => 'user.form.twofactor_label', + 'label' => 'user.form.twofactor_email_label', + ]) + ->add('googleTwoFactor', CheckboxType::class, [ + 'required' => false, + 'label' => 'user.form.twofactor_google_label', + 'mapped' => false, ]) ->add('save', SubmitType::class, [ 'label' => 'user.form.save', diff --git a/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php b/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php index aed805c95..e8e29aa9e 100644 --- a/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php +++ b/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php @@ -78,7 +78,7 @@ class AuthCodeMailer implements AuthCodeMailerInterface * * @param TwoFactorInterface $user */ - public function sendAuthCode(TwoFactorInterface $user) + public function sendAuthCode(TwoFactorInterface $user): void { $template = $this->twig->loadTemplate('WallabagUserBundle:TwoFactor:email_auth_code.html.twig'); diff --git a/src/Wallabag/UserBundle/Resources/views/Authentication/form.html.twig b/src/Wallabag/UserBundle/Resources/views/Authentication/form.html.twig index c8471bdda..47a5cb784 100644 --- a/src/Wallabag/UserBundle/Resources/views/Authentication/form.html.twig +++ b/src/Wallabag/UserBundle/Resources/views/Authentication/form.html.twig @@ -1,7 +1,8 @@ +{# Override `vendor/scheb/two-factor-bundle/Resources/views/Authentication/form.html.twig` #} {% extends "WallabagUserBundle::layout.html.twig" %} {% block fos_user_content %} -
+
@@ -9,14 +10,19 @@

{{ flashMessage|trans }}

{% endfor %} + {# Authentication errors #} + {% if authenticationError %} +

{{ authenticationError|trans(authenticationErrorData) }}

+ {% endif %} +
- +
- {% if useTrustedOption %} + {% if displayTrustedOption %}
- +
{% endif %} diff --git a/src/Wallabag/UserBundle/Resources/views/Manage/edit.html.twig b/src/Wallabag/UserBundle/Resources/views/Manage/edit.html.twig index 3ffd15f5d..8be37e79a 100644 --- a/src/Wallabag/UserBundle/Resources/views/Manage/edit.html.twig +++ b/src/Wallabag/UserBundle/Resources/views/Manage/edit.html.twig @@ -50,10 +50,21 @@ {% if twofactor_auth %}
- {{ form_widget(edit_form.twoFactorAuthentication) }} - {{ form_label(edit_form.twoFactorAuthentication) }} - {{ form_errors(edit_form.twoFactorAuthentication) }} + {{ form_widget(edit_form.emailTwoFactor) }} + {{ form_label(edit_form.emailTwoFactor) }} + {{ form_errors(edit_form.emailTwoFactor) }}
+
+ {{ form_widget(edit_form.googleTwoFactor) }} + {{ form_label(edit_form.googleTwoFactor) }} + {{ form_errors(edit_form.googleTwoFactor) }} +
+ + {% if user.isGoogleAuthenticatorEnabled %} +
+

OTP Secret: {{ user.googleAuthenticatorSecret }}

+
+ {% endif %}
{% endif %} diff --git a/tests/Wallabag/CoreBundle/Command/ShowUserCommandTest.php b/tests/Wallabag/CoreBundle/Command/ShowUserCommandTest.php index 9b34f2a08..ed383a2c9 100644 --- a/tests/Wallabag/CoreBundle/Command/ShowUserCommandTest.php +++ b/tests/Wallabag/CoreBundle/Command/ShowUserCommandTest.php @@ -59,7 +59,8 @@ class ShowUserCommandTest extends WallabagCoreTestCase $this->assertContains('Username: admin', $tester->getDisplay()); $this->assertContains('Email: bigboss@wallabag.org', $tester->getDisplay()); $this->assertContains('Display name: Big boss', $tester->getDisplay()); - $this->assertContains('2FA activated: no', $tester->getDisplay()); + $this->assertContains('2FA (email) activated', $tester->getDisplay()); + $this->assertContains('2FA (OTP) activated', $tester->getDisplay()); } public function testShowUser() diff --git a/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php b/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php index c9dbbaa3b..9ca52c643 100644 --- a/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php @@ -297,6 +297,119 @@ class ConfigControllerTest extends WallabagCoreTestCase $this->assertContains('flashes.config.notice.user_updated', $alert[0]); } + public function testUserEnable2faEmail() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $crawler = $client->request('GET', '/config'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $form = $crawler->filter('button[id=update_user_save]')->form(); + + $data = [ + 'update_user[emailTwoFactor]' => '1', + ]; + + $client->submit($form, $data); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + + $crawler = $client->followRedirect(); + + $this->assertGreaterThan(1, $alert = $crawler->filter('body')->extract(['_text'])); + $this->assertContains('flashes.config.notice.user_updated', $alert[0]); + + // restore user + $em = $this->getEntityManager(); + $user = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUsername('admin'); + + $this->assertTrue($user->isEmailTwoFactor()); + + $user->setEmailTwoFactor(false); + $em->persist($user); + $em->flush(); + } + + public function testUserEnable2faGoogle() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $crawler = $client->request('GET', '/config'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $form = $crawler->filter('button[id=update_user_save]')->form(); + + $data = [ + 'update_user[googleTwoFactor]' => '1', + ]; + + $client->submit($form, $data); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + + $crawler = $client->followRedirect(); + + $this->assertGreaterThan(1, $alert = $crawler->filter('body')->extract(['_text'])); + $this->assertContains('flashes.config.notice.user_updated', $alert[0]); + + // restore user + $em = $this->getEntityManager(); + $user = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUsername('admin'); + + $this->assertTrue($user->isGoogleAuthenticatorEnabled()); + + $user->setGoogleAuthenticatorSecret(null); + $em->persist($user); + $em->flush(); + } + + public function testUserEnable2faBoth() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $crawler = $client->request('GET', '/config'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $form = $crawler->filter('button[id=update_user_save]')->form(); + + $data = [ + 'update_user[googleTwoFactor]' => '1', + 'update_user[emailTwoFactor]' => '1', + ]; + + $client->submit($form, $data); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + + $crawler = $client->followRedirect(); + + $this->assertGreaterThan(1, $alert = $crawler->filter('body')->extract(['_text'])); + $this->assertContains('flashes.config.notice.user_updated', $alert[0]); + + // restore user + $em = $this->getEntityManager(); + $user = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUsername('admin'); + + $this->assertTrue($user->isGoogleAuthenticatorEnabled()); + $this->assertFalse($user->isEmailTwoFactor()); + + $user->setGoogleAuthenticatorSecret(null); + $em->persist($user); + $em->flush(); + } + public function testRssUpdateResetToken() { $this->logInAs('admin'); diff --git a/tests/Wallabag/CoreBundle/Controller/SecurityControllerTest.php b/tests/Wallabag/CoreBundle/Controller/SecurityControllerTest.php index 395208a2f..b03c7550d 100644 --- a/tests/Wallabag/CoreBundle/Controller/SecurityControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/SecurityControllerTest.php @@ -26,7 +26,7 @@ class SecurityControllerTest extends WallabagCoreTestCase $this->assertContains('config.form_rss.description', $crawler->filter('body')->extract(['_text'])[0]); } - public function testLoginWith2Factor() + public function testLoginWith2FactorEmail() { $client = $this->getClient(); @@ -42,7 +42,7 @@ class SecurityControllerTest extends WallabagCoreTestCase $user = $em ->getRepository('WallabagUserBundle:User') ->findOneByUsername('admin'); - $user->setTwoFactorAuthentication(true); + $user->setEmailTwoFactor(true); $em->persist($user); $em->flush(); @@ -54,12 +54,12 @@ class SecurityControllerTest extends WallabagCoreTestCase $user = $em ->getRepository('WallabagUserBundle:User') ->findOneByUsername('admin'); - $user->setTwoFactorAuthentication(false); + $user->setEmailTwoFactor(false); $em->persist($user); $em->flush(); } - public function testTrustedComputer() + public function testLoginWith2FactorGoogle() { $client = $this->getClient(); @@ -69,15 +69,27 @@ class SecurityControllerTest extends WallabagCoreTestCase return; } + $client->followRedirects(); + $em = $client->getContainer()->get('doctrine.orm.entity_manager'); $user = $em ->getRepository('WallabagUserBundle:User') ->findOneByUsername('admin'); + $user->setGoogleAuthenticatorSecret('26LDIHYGHNELOQEM'); + $em->persist($user); + $em->flush(); - $date = new \DateTime(); - $user->addTrustedComputer('ABCDEF', $date->add(new \DateInterval('P1M'))); - $this->assertTrue($user->isTrustedComputer('ABCDEF')); - $this->assertFalse($user->isTrustedComputer('FEDCBA')); + $this->logInAsUsingHttp('admin'); + $crawler = $client->request('GET', '/config'); + $this->assertContains('scheb_two_factor.trusted', $crawler->filter('body')->extract(['_text'])[0]); + + // restore user + $user = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUsername('admin'); + $user->setGoogleAuthenticatorSecret(null); + $em->persist($user); + $em->flush(); } public function testEnabledRegistration() diff --git a/tests/Wallabag/UserBundle/Mailer/AuthCodeMailerTest.php b/tests/Wallabag/UserBundle/Mailer/AuthCodeMailerTest.php index e34e13a8a..1713c10c8 100644 --- a/tests/Wallabag/UserBundle/Mailer/AuthCodeMailerTest.php +++ b/tests/Wallabag/UserBundle/Mailer/AuthCodeMailerTest.php @@ -33,7 +33,7 @@ TWIG; public function testSendEmail() { $user = new User(); - $user->setTwoFactorAuthentication(true); + $user->setEmailTwoFactor(true); $user->setEmailAuthCode(666666); $user->setEmail('test@wallabag.io'); $user->setName('Bob'); From edc79ad886e4c96d1c2d205fedf5a9c19a177ee1 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Sun, 2 Dec 2018 17:25:56 +0100 Subject: [PATCH 052/107] Fix test for custom version of the tidy extension --- .../CoreBundle/Helper/ContentProxyTest.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php b/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php index 3dd9273c8..508adb1b6 100644 --- a/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php +++ b/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php @@ -163,7 +163,7 @@ class ContentProxyTest extends TestCase $this->assertSame('http://1.1.1.1', $entry->getUrl()); $this->assertSame('this is my title', $entry->getTitle()); - $this->assertContains('this is my content', $entry->getContent()); + $this->assertContains('content', $entry->getContent()); $this->assertSame('http://3.3.3.3/cover.jpg', $entry->getPreviewPicture()); $this->assertSame('text/html', $entry->getMimetype()); $this->assertSame('fr', $entry->getLanguage()); @@ -205,7 +205,7 @@ class ContentProxyTest extends TestCase $this->assertSame('http://1.1.1.1', $entry->getUrl()); $this->assertSame('this is my title', $entry->getTitle()); - $this->assertContains('this is my content', $entry->getContent()); + $this->assertContains('content', $entry->getContent()); $this->assertNull($entry->getPreviewPicture()); $this->assertSame('text/html', $entry->getMimetype()); $this->assertSame('fr', $entry->getLanguage()); @@ -247,7 +247,7 @@ class ContentProxyTest extends TestCase $this->assertSame('http://1.1.1.1', $entry->getUrl()); $this->assertSame('this is my title', $entry->getTitle()); - $this->assertContains('this is my content', $entry->getContent()); + $this->assertContains('content', $entry->getContent()); $this->assertSame('text/html', $entry->getMimetype()); $this->assertNull($entry->getLanguage()); $this->assertSame('200', $entry->getHttpStatus()); @@ -296,7 +296,7 @@ class ContentProxyTest extends TestCase $this->assertSame('http://1.1.1.1', $entry->getUrl()); $this->assertSame('this is my title', $entry->getTitle()); - $this->assertContains('this is my content', $entry->getContent()); + $this->assertContains('content', $entry->getContent()); $this->assertNull($entry->getPreviewPicture()); $this->assertSame('text/html', $entry->getMimetype()); $this->assertSame('fr', $entry->getLanguage()); @@ -332,7 +332,7 @@ class ContentProxyTest extends TestCase $this->assertSame('http://1.1.1.1', $entry->getUrl()); $this->assertSame('this is my title', $entry->getTitle()); - $this->assertContains('this is my content', $entry->getContent()); + $this->assertContains('content', $entry->getContent()); $this->assertSame('text/html', $entry->getMimetype()); $this->assertSame('fr', $entry->getLanguage()); $this->assertSame(4.0, $entry->getReadingTime()); @@ -371,7 +371,7 @@ class ContentProxyTest extends TestCase $this->assertSame('http://1.1.1.1', $entry->getUrl()); $this->assertSame('this is my title', $entry->getTitle()); - $this->assertContains('this is my content', $entry->getContent()); + $this->assertContains('content', $entry->getContent()); $this->assertSame('text/html', $entry->getMimetype()); $this->assertSame('fr', $entry->getLanguage()); $this->assertSame(4.0, $entry->getReadingTime()); @@ -406,7 +406,7 @@ class ContentProxyTest extends TestCase $this->assertSame('http://1.1.1.1', $entry->getUrl()); $this->assertSame('this is my title', $entry->getTitle()); - $this->assertContains('this is my content', $entry->getContent()); + $this->assertContains('content', $entry->getContent()); $this->assertSame('text/html', $entry->getMimetype()); $this->assertSame('fr', $entry->getLanguage()); $this->assertSame(4.0, $entry->getReadingTime()); From 2dfbe9e5faf40364b60e6c76f3cc9fac5bf11fa4 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Sun, 2 Dec 2018 18:39:02 +0100 Subject: [PATCH 053/107] Fix tests --- .../Controller/ConfigController.php | 20 +++--- .../views/themes/baggy/Config/index.html.twig | 9 ++- .../themes/material/Config/index.html.twig | 6 +- .../Controller/ManageController.php | 70 +++++-------------- 4 files changed, 36 insertions(+), 69 deletions(-) diff --git a/src/Wallabag/CoreBundle/Controller/ConfigController.php b/src/Wallabag/CoreBundle/Controller/ConfigController.php index 5bbe1c743..846e96ff9 100644 --- a/src/Wallabag/CoreBundle/Controller/ConfigController.php +++ b/src/Wallabag/CoreBundle/Controller/ConfigController.php @@ -81,23 +81,23 @@ class ConfigController extends Controller $userForm->handleRequest($request); // `googleTwoFactor` isn't a field within the User entity, we need to define it's value in a different way - if (true === $user->isGoogleAuthenticatorEnabled() && false === $userForm->isSubmitted()) { + if ($this->getParameter('twofactor_auth') && true === $user->isGoogleAuthenticatorEnabled() && false === $userForm->isSubmitted()) { $userForm->get('googleTwoFactor')->setData(true); } if ($userForm->isSubmitted() && $userForm->isValid()) { // handle creation / reset of the OTP secret if checkbox changed from the previous state - if (true === $userForm->get('googleTwoFactor')->getData() && false === $user->isGoogleAuthenticatorEnabled()) { - $secret = $this->get('scheb_two_factor.security.google_authenticator')->generateSecret(); + if ($this->getParameter('twofactor_auth')) { + if (true === $userForm->get('googleTwoFactor')->getData() && false === $user->isGoogleAuthenticatorEnabled()) { + $secret = $this->get('scheb_two_factor.security.google_authenticator')->generateSecret(); - $user->setGoogleAuthenticatorSecret($secret); - $user->setEmailTwoFactor(false); + $user->setGoogleAuthenticatorSecret($secret); + $user->setEmailTwoFactor(false); - $qrCode = $this->get('scheb_two_factor.security.google_authenticator')->getQRContent($user); - - $this->addFlash('OTPSecret', ['code' => $secret, 'qrCode' => $qrCode]); - } elseif (false === $userForm->get('googleTwoFactor')->getData() && true === $user->isGoogleAuthenticatorEnabled()) { - $user->setGoogleAuthenticatorSecret(null); + $this->addFlash('OtpQrCode', $this->get('scheb_two_factor.security.google_authenticator')->getQRContent($user)); + } elseif (false === $userForm->get('googleTwoFactor')->getData() && true === $user->isGoogleAuthenticatorEnabled()) { + $user->setGoogleAuthenticatorSecret(null); + } } $userManager->updateUser($user, true); 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 5c4e44dd9..6ee574436 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 @@ -86,8 +86,7 @@
@@ -186,20 +185,20 @@ {{ form_widget(form.user.googleTwoFactor) }} {{ form_errors(form.user.googleTwoFactor) }} - {% for OTPSecret in app.session.flashbag.get('OTPSecret') %} + {% for OtpQrCode in app.session.flashbag.get('OtpQrCode') %}
You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password.
That code will disapear after a page reload.

- {{ OTPSecret.code }} + {{ app.user.getGoogleAuthenticatorSecret }}

Or you can scan that QR Code with your app:
{% endfor %} 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 887d154f4..ca7eb9f3b 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 @@ -212,20 +212,20 @@ - {% for OTPSecret in app.session.flashbag.get('OTPSecret') %} + {% for OtpQrCode in app.session.flashbag.get('OtpQrCode') %}
You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password.
That code will disapear after a page reload.

- {{ OTPSecret.code }} + {{ app.user.getGoogleAuthenticatorSecret }}

Or you can scan that QR Code with your app:
{% endfor %} diff --git a/src/Wallabag/UserBundle/Controller/ManageController.php b/src/Wallabag/UserBundle/Controller/ManageController.php index 08ed25dd1..b9fd86605 100644 --- a/src/Wallabag/UserBundle/Controller/ManageController.php +++ b/src/Wallabag/UserBundle/Controller/ManageController.php @@ -8,7 +8,6 @@ use Pagerfanta\Adapter\DoctrineORMAdapter; use Pagerfanta\Exception\OutOfRangeCurrentPageException; use Pagerfanta\Pagerfanta; use Symfony\Bundle\FrameworkBundle\Controller\Controller; -use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Wallabag\UserBundle\Entity\User; @@ -32,10 +31,10 @@ class ManageController extends Controller // enable created user by default $user->setEnabled(true); - $form = $this->createEditForm('NewUserType', $user, $request); + $form = $this->createForm('Wallabag\UserBundle\Form\NewUserType', $user); + $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $user = $this->handleOtp($form, $user); $userManager->updateUser($user); // dispatch a created event so the associated config will be created @@ -66,10 +65,25 @@ class ManageController extends Controller $userManager = $this->container->get('fos_user.user_manager'); $deleteForm = $this->createDeleteForm($user); - $form = $this->createEditForm('UserType', $user, $request); + $form = $this->createForm('Wallabag\UserBundle\Form\UserType', $user); + $form->handleRequest($request); + + // `googleTwoFactor` isn't a field within the User entity, we need to define it's value in a different way + if ($this->getParameter('twofactor_auth') && true === $user->isGoogleAuthenticatorEnabled() && false === $form->isSubmitted()) { + $form->get('googleTwoFactor')->setData(true); + } if ($form->isSubmitted() && $form->isValid()) { - $user = $this->handleOtp($form, $user); + // handle creation / reset of the OTP secret if checkbox changed from the previous state + if ($this->getParameter('twofactor_auth')) { + if (true === $form->get('googleTwoFactor')->getData() && false === $user->isGoogleAuthenticatorEnabled()) { + $user->setGoogleAuthenticatorSecret($this->get('scheb_two_factor.security.google_authenticator')->generateSecret()); + $user->setEmailTwoFactor(false); + } elseif (false === $form->get('googleTwoFactor')->getData() && true === $user->isGoogleAuthenticatorEnabled()) { + $user->setGoogleAuthenticatorSecret(null); + } + } + $userManager->updateUser($user); $this->get('session')->getFlashBag()->add( @@ -172,50 +186,4 @@ class ManageController extends Controller ->getForm() ; } - - /** - * Create a form to create or edit a User entity. - * - * @param string $type Might be NewUserType or UserType - * @param User $user The new / edit user - * @param Request $request The request - * - * @return FormInterface - */ - private function createEditForm($type, User $user, Request $request) - { - $form = $this->createForm('Wallabag\UserBundle\Form\\' . $type, $user); - $form->handleRequest($request); - - // `googleTwoFactor` isn't a field within the User entity, we need to define it's value in a different way - if (true === $user->isGoogleAuthenticatorEnabled() && false === $form->isSubmitted()) { - $form->get('googleTwoFactor')->setData(true); - } - - return $form; - } - - /** - * Handle OTP update, taking care to only have one 2fa enable at a time. - * - * @see ConfigController - * - * @param FormInterface $form - * @param User $user - * - * @return User - */ - private function handleOtp(FormInterface $form, User $user) - { - if (true === $form->get('googleTwoFactor')->getData() && false === $user->isGoogleAuthenticatorEnabled()) { - $user->setGoogleAuthenticatorSecret($this->get('scheb_two_factor.security.google_authenticator')->generateSecret()); - $user->setEmailTwoFactor(false); - - return $user; - } - - $user->setGoogleAuthenticatorSecret(null); - - return $user; - } } From 43ccf4b1787c294dbfa7b052c41e95ac9eeca3af Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Sun, 2 Dec 2018 18:47:34 +0100 Subject: [PATCH 054/107] Cleanup --- src/Wallabag/UserBundle/Controller/ManageController.php | 2 -- src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Wallabag/UserBundle/Controller/ManageController.php b/src/Wallabag/UserBundle/Controller/ManageController.php index b9fd86605..63a062061 100644 --- a/src/Wallabag/UserBundle/Controller/ManageController.php +++ b/src/Wallabag/UserBundle/Controller/ManageController.php @@ -146,8 +146,6 @@ class ManageController extends Controller $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $this->get('logger')->info('searching users'); - $searchTerm = (isset($request->get('search_user')['term']) ? $request->get('search_user')['term'] : ''); $qb = $em->getRepository('WallabagUserBundle:User')->getQueryBuilderForSearch($searchTerm); diff --git a/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php b/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php index e8e29aa9e..2797efde9 100644 --- a/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php +++ b/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php @@ -97,7 +97,7 @@ class AuthCodeMailer implements AuthCodeMailerInterface $message = new \Swift_Message(); $message - ->setTo($user->getEmail()) + ->setTo($user->getEmailAuthRecipient()) ->setFrom($this->senderEmail, $this->senderName) ->setSubject($subject) ->setBody($bodyText, 'text/plain') From 6e4fc956abc909232044e7af0fa37cbb1b510f18 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Mon, 3 Dec 2018 06:15:57 +0100 Subject: [PATCH 055/107] Better translations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace “Google Authenticator” by “Google Authenticator, Authy or FreeOTP” in all text. Translate how to use the code / qr code. --- .../Resources/translations/messages.da.yml | 7 +++++-- .../Resources/translations/messages.de.yml | 7 +++++-- .../Resources/translations/messages.en.yml | 7 +++++-- .../Resources/translations/messages.es.yml | 7 +++++-- .../Resources/translations/messages.fa.yml | 7 +++++-- .../Resources/translations/messages.fr.yml | 7 +++++-- .../Resources/translations/messages.it.yml | 7 +++++-- .../Resources/translations/messages.oc.yml | 7 +++++-- .../Resources/translations/messages.pl.yml | 7 +++++-- .../Resources/translations/messages.pt.yml | 7 +++++-- .../Resources/translations/messages.ro.yml | 7 +++++-- .../Resources/translations/messages.ru.yml | 7 +++++-- .../Resources/translations/messages.th.yml | 7 +++++-- .../Resources/translations/messages.tr.yml | 7 +++++-- .../views/themes/material/Config/index.html.twig | 13 ++++++------- 15 files changed, 76 insertions(+), 35 deletions(-) diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml index e62ba6d05..d3e96e5c5 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml @@ -99,11 +99,14 @@ config: # all: 'All' # rss_limit: 'Number of items in the feed' form_user: - # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator) to get a one time code on every new untrusted connection. You can't choose both option." + # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Navn' email_label: 'Emailadresse' # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, to get a one time code)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Or use that code:' delete: # title: Delete my account (a.k.a danger zone) # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml index f2d0408f6..9aeddceba 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml @@ -99,11 +99,14 @@ config: all: 'Alle' rss_limit: 'Anzahl der Einträge pro Feed' form_user: - # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator) to get a one time code on every new untrusted connection. You can't choose both option." + # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Name' email_label: 'E-Mail-Adresse' # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, to get a one time code)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Or use that code:' delete: title: 'Lösche mein Konto (a.k.a Gefahrenzone)' 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.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml index 859acdc04..22c68c799 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml @@ -99,11 +99,14 @@ config: all: 'All' rss_limit: 'Number of items in the feed' form_user: - two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator) to get a one time code on every new untrusted connection. You can't choose both option." + two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Name' email_label: 'Email' emailTwoFactor_label: 'Using email (receive a code by email)' - googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, to get a one time code)' + googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + two_factor_code_description_2: 'You can scan that QR Code with your app:' + two_factor_code_description_3: 'Or use that code:' delete: title: Delete my account (a.k.a danger zone) description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml index 3c3cbed42..6e710e56a 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml @@ -99,11 +99,14 @@ config: # all: 'All' rss_limit: 'Límite de artículos en feed RSS' form_user: - # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator) to get a one time code on every new untrusted connection. You can't choose both option." + # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Nombre' email_label: 'Dirección de e-mail' # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, to get a one time code)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Or use that code:' delete: title: Eliminar mi cuenta (Zona peligrosa) description: Si eliminas tu cuenta, TODOS tus artículos, TODAS tus etiquetas, TODAS tus anotaciones y tu cuenta serán eliminadas de forma PERMANENTE (no se puede deshacer). Después serás desconectado. diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml index ca25b3904..855f38cc8 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml @@ -99,11 +99,14 @@ config: # all: 'All' rss_limit: 'محدودیت آر-اس-اس' form_user: - # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator) to get a one time code on every new untrusted connection. You can't choose both option." + # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'نام' email_label: 'نشانی ایمیل' # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, to get a one time code)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Or use that code:' delete: # title: Delete my account (a.k.a danger zone) # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml index b809ca32e..f92b64a56 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml @@ -99,11 +99,14 @@ config: all: "Tous" rss_limit: "Nombre d’articles dans le flux" form_user: - two_factor_description: "Activer l’authentification double-facteur veut dire que vous allez recevoir un code par courriel OU que vous devriez utiliser une application de mot de passe à usage unique (comme Google Authenticator) pour obtenir un code temporaire à chaque nouvelle connexion non approuvée. Vous ne pouvez pas choisir les deux options." + two_factor_description: "Activer l’authentification double-facteur veut dire que vous allez recevoir un code par courriel OU que vous devriez utiliser une application de mot de passe à usage unique (comme Google Authenticator, Authy or FreeOTP) pour obtenir un code temporaire à chaque nouvelle connexion non approuvée. Vous ne pouvez pas choisir les deux options." name_label: "Nom" email_label: "Adresse courriel" emailTwoFactor_label: 'En utlisant l’email (recevez un code par email)' - googleTwoFactor_label: 'En utilisant une application de mot de passe à usage unique (ouvrez l’app, comme Google Authenticator, pour obtenir un mot de passe à usage unique)' + googleTwoFactor_label: 'En utilisant une application de mot de passe à usage unique (ouvrez l’app, comme Google Authenticator, Authy or FreeOTP, pour obtenir un mot de passe à usage unique)' + two_factor_code_description_1: Vous venez d’activer l’authentification double-facteur, ouvrez votre application OTP pour configurer la génération du mot de passe à usage unique. Ces informations disparaîtront après un rechargement de la page. + two_factor_code_description_2: 'Vous pouvez scanner le QR code avec votre application :' + two_factor_code_description_3: 'Ou utiliser le code suivant :' delete: title: "Supprimer mon compte (attention danger !)" 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é." diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml index 7279dba18..95d4ac200 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml @@ -99,11 +99,14 @@ config: # all: 'All' rss_limit: 'Numero di elementi nel feed' form_user: - # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator) to get a one time code on every new untrusted connection. You can't choose both option." + # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Nome' email_label: 'E-mail' # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, to get a one time code)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Or use that code:' delete: title: Cancella il mio account (zona pericolosa) description: Rimuovendo il tuo account, TUTTI i tuoi articoli, TUTTE le tue etichette, TUTTE le tue annotazioni ed il tuo account verranno rimossi PERMANENTEMENTE (impossibile da ANNULLARE). Verrai poi disconnesso. diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml index f262ba7b8..96725a069 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml @@ -99,11 +99,14 @@ config: all: 'Totes' rss_limit: "Nombre d'articles dins un flux RSS" form_user: - # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator) to get a one time code on every new untrusted connection. You can't choose both option." + # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Nom' email_label: 'Adreça de corrièl' # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, to get a one time code)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Or use that code:' delete: title: Suprimir mon compte (Mèfi zòna perilhosa) 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. diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml index 99c2183e4..5f77061c0 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml @@ -99,11 +99,14 @@ config: all: 'Wszystkie' rss_limit: 'Link do RSS' form_user: - two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator) to get a one time code on every new untrusted connection. You can't choose both option." + two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Nazwa' email_label: 'Adres email' # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, to get a one time code)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Or use that code:' delete: title: Usuń moje konto (niebezpieczna strefa !) description: Jeżeli usuniesz swoje konto, wszystkie twoje artykuły, tagi, adnotacje, oraz konto zostaną trwale usunięte (operacja jest NIEODWRACALNA). Następnie zostaniesz wylogowany. diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml index 806c2d78e..f40f97958 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml @@ -99,11 +99,14 @@ config: # all: 'All' rss_limit: 'Número de itens no feed' form_user: - # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator) to get a one time code on every new untrusted connection. You can't choose both option." + # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Nome' email_label: 'E-mail' # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, to get a one time code)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Or use that code:' delete: # title: Delete my account (a.k.a danger zone) # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml index ed75ed6e7..369d2d44a 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml @@ -99,11 +99,14 @@ config: # all: 'All' rss_limit: 'Limită RSS' form_user: - # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator) to get a one time code on every new untrusted connection. You can't choose both option." + # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Nume' email_label: 'E-mail' # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, to get a one time code)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Or use that code:' delete: # title: Delete my account (a.k.a danger zone) # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml index 1c6e6771c..d9b33fed0 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml @@ -96,11 +96,14 @@ config: archive: 'архивные' rss_limit: 'Количество записей в фиде' form_user: - # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator) to get a one time code on every new untrusted connection. You can't choose both option." + # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Имя' email_label: 'Email' # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, to get a one time code)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Or use that code:' delete: title: "Удалить мой аккаунт (или опасная зона)" description: "Если Вы удалите ваш аккаунт, ВСЕ ваши записи, теги и другие данные, будут БЕЗВОЗВРАТНО удалены (операция не может быть отменена после). Затем Вы выйдете из системы." diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml index af7989436..f25bac846 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml @@ -99,11 +99,14 @@ config: all: 'ทั้งหมด' rss_limit: 'จำนวนไอเทมที่เก็บ' form_user: - # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator) to get a one time code on every new untrusted connection. You can't choose both option." + # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'ชื่อ' email_label: 'อีเมล' # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, to get a one time code)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Or use that code:' delete: title: ลบบัญชีของฉัน (โซนที่เป็นภัย!) description: ถ้าคุณลบบัญชีของคุณIf , รายการทั้งหมดของคุณ, แท็กทั้งหมดของคุณ, หมายเหตุทั้งหมดของคุณและบัญชีของคุณจะถูกลบอย่างถาวร (มันไม่สามารถยกเลิกได้) คุณจะต้องลงชื่อออก diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml index 352a2cc41..d65fc0016 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml @@ -99,11 +99,14 @@ config: # all: 'All' rss_limit: 'RSS içeriğinden talep edilecek makale limiti' form_user: - # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator) to get a one time code on every new untrusted connection. You can't choose both option." + # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'İsim' email_label: 'E-posta' # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, to get a one time code)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Or use that code:' delete: # title: Delete my account (a.k.a danger zone) # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. 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 ca7eb9f3b..73cf592ea 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 @@ -214,19 +214,18 @@ {% for OtpQrCode in app.session.flashbag.get('OtpQrCode') %}
- You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. + {{ 'config.form_user.two_factor_code_description_1'|trans }}
- That code will disapear after a page reload. + {{ 'config.form_user.two_factor_code_description_2'|trans }}

- {{ app.user.getGoogleAuthenticatorSecret }} -

- Or you can scan that QR Code with your app: -
- +

+ {{ 'config.form_user.two_factor_code_description_3'|trans }} +

+ {{ app.user.getGoogleAuthenticatorSecret }}
{% endfor %} {% endif %} From dfd0a7bc5feb4fd7b77d7e2f3a25c5c3febc1eba Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Mon, 3 Dec 2018 06:51:06 +0100 Subject: [PATCH 056/107] Add backup codes --- .../Version20181202073750.php | 22 ++++++++++- app/config/config.yml | 3 ++ composer.json | 3 +- .../Controller/ConfigController.php | 3 ++ .../Resources/translations/messages.da.yml | 1 + .../Resources/translations/messages.de.yml | 1 + .../Resources/translations/messages.en.yml | 1 + .../Resources/translations/messages.es.yml | 1 + .../Resources/translations/messages.fa.yml | 1 + .../Resources/translations/messages.fr.yml | 1 + .../Resources/translations/messages.it.yml | 1 + .../Resources/translations/messages.oc.yml | 1 + .../Resources/translations/messages.pl.yml | 1 + .../Resources/translations/messages.pt.yml | 1 + .../Resources/translations/messages.ro.yml | 1 + .../Resources/translations/messages.ru.yml | 1 + .../Resources/translations/messages.th.yml | 1 + .../Resources/translations/messages.tr.yml | 1 + .../views/themes/baggy/Config/index.html.twig | 21 +++++----- .../themes/material/Config/index.html.twig | 8 +++- src/Wallabag/UserBundle/Entity/User.php | 38 ++++++++++++++++++- 21 files changed, 97 insertions(+), 15 deletions(-) diff --git a/app/DoctrineMigrations/Version20181202073750.php b/app/DoctrineMigrations/Version20181202073750.php index a2308b998..b6ad8bd73 100644 --- a/app/DoctrineMigrations/Version20181202073750.php +++ b/app/DoctrineMigrations/Version20181202073750.php @@ -12,11 +12,29 @@ final class Version20181202073750 extends WallabagMigration { public function up(Schema $schema): void { - $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' ADD googleAuthenticatorSecret VARCHAR(191) DEFAULT NULL, CHANGE twoFactorAuthentication emailTwoFactor BOOLEAN NOT NULL, DROP trusted'); + $tableName = $this->getTable('annotation'); + + switch ($this->connection->getDatabasePlatform()->getName()) { + case 'sqlite': + break; + case 'mysql': + $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' ADD googleAuthenticatorSecret VARCHAR(191) DEFAULT NULL, CHANGE twoFactorAuthentication emailTwoFactor BOOLEAN NOT NULL, DROP trusted, ADD backupCodes LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:json_array)\''); + break; + case 'postgresql': + break; + } } public function down(Schema $schema): void { - $this->addSql('ALTER TABLE `' . $this->getTable('user') . '` DROP googleAuthenticatorSecret, CHANGE emailtwofactor twoFactorAuthentication BOOLEAN NOT NULL, ADD trusted TEXT DEFAULT NULL'); + switch ($this->connection->getDatabasePlatform()->getName()) { + case 'sqlite': + break; + case 'mysql': + $this->addSql('ALTER TABLE `' . $this->getTable('user') . '` DROP googleAuthenticatorSecret, CHANGE emailtwofactor twoFactorAuthentication BOOLEAN NOT NULL, ADD trusted TEXT DEFAULT NULL, DROP backupCodes'); + break; + case 'postgresql': + break; + } } } diff --git a/app/config/config.yml b/app/config/config.yml index 908f53b7e..2d8f9bf01 100644 --- a/app/config/config.yml +++ b/app/config/config.yml @@ -203,6 +203,9 @@ scheb_two_factor: cookie_name: wllbg_trusted_computer lifetime: 2592000 + backup_codes: + enabled: "%twofactor_auth%" + google: enabled: "%twofactor_auth%" template: WallabagUserBundle:Authentication:form.html.twig diff --git a/composer.json b/composer.json index 771580c67..7678d7b87 100644 --- a/composer.json +++ b/composer.json @@ -87,7 +87,8 @@ "friendsofsymfony/jsrouting-bundle": "^2.2", "bdunogier/guzzle-site-authenticator": "^1.0.0", "defuse/php-encryption": "^2.1", - "html2text/html2text": "^4.1" + "html2text/html2text": "^4.1", + "pragmarx/recovery": "^0.1.0" }, "require-dev": { "doctrine/doctrine-fixtures-bundle": "~3.0", diff --git a/src/Wallabag/CoreBundle/Controller/ConfigController.php b/src/Wallabag/CoreBundle/Controller/ConfigController.php index 846e96ff9..c9fc57026 100644 --- a/src/Wallabag/CoreBundle/Controller/ConfigController.php +++ b/src/Wallabag/CoreBundle/Controller/ConfigController.php @@ -2,6 +2,7 @@ namespace Wallabag\CoreBundle\Controller; +use PragmaRX\Recovery\Recovery as BackupCodes; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -93,10 +94,12 @@ class ConfigController extends Controller $user->setGoogleAuthenticatorSecret($secret); $user->setEmailTwoFactor(false); + $user->setBackupCodes((new BackupCodes())->toArray()); $this->addFlash('OtpQrCode', $this->get('scheb_two_factor.security.google_authenticator')->getQRContent($user)); } elseif (false === $userForm->get('googleTwoFactor')->getData() && true === $user->isGoogleAuthenticatorEnabled()) { $user->setGoogleAuthenticatorSecret(null); + $user->setBackupCodes(null); } } diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml index d3e96e5c5..0114a983b 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml @@ -107,6 +107,7 @@ config: # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. # two_factor_code_description_2: 'You can scan that QR Code with your app:' # two_factor_code_description_3: 'Or use that code:' + # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' delete: # title: Delete my account (a.k.a danger zone) # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml index 9aeddceba..fd9796ba8 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml @@ -107,6 +107,7 @@ config: # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. # two_factor_code_description_2: 'You can scan that QR Code with your app:' # two_factor_code_description_3: 'Or use that code:' + # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' delete: title: 'Lösche mein Konto (a.k.a Gefahrenzone)' 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.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml index 22c68c799..ddc079ed0 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml @@ -107,6 +107,7 @@ config: two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. two_factor_code_description_2: 'You can scan that QR Code with your app:' two_factor_code_description_3: 'Or use that code:' + two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' delete: title: Delete my account (a.k.a danger zone) description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml index 6e710e56a..8ac661692 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml @@ -107,6 +107,7 @@ config: # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. # two_factor_code_description_2: 'You can scan that QR Code with your app:' # two_factor_code_description_3: 'Or use that code:' + # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' delete: title: Eliminar mi cuenta (Zona peligrosa) description: Si eliminas tu cuenta, TODOS tus artículos, TODAS tus etiquetas, TODAS tus anotaciones y tu cuenta serán eliminadas de forma PERMANENTE (no se puede deshacer). Después serás desconectado. diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml index 855f38cc8..bc754ca21 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml @@ -107,6 +107,7 @@ config: # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. # two_factor_code_description_2: 'You can scan that QR Code with your app:' # two_factor_code_description_3: 'Or use that code:' + # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' delete: # title: Delete my account (a.k.a danger zone) # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml index f92b64a56..288411451 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml @@ -107,6 +107,7 @@ config: two_factor_code_description_1: Vous venez d’activer l’authentification double-facteur, ouvrez votre application OTP pour configurer la génération du mot de passe à usage unique. Ces informations disparaîtront après un rechargement de la page. two_factor_code_description_2: 'Vous pouvez scanner le QR code avec votre application :' two_factor_code_description_3: 'Ou utiliser le code suivant :' + two_factor_code_description_4: 'N’oubliez pas de sauvegarder ces codes de secours dans un endroit sûr, vous pourrez les utiliser si vous ne pouvez plus accéder à votre application OTP :' delete: title: "Supprimer mon compte (attention danger !)" 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é." diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml index 95d4ac200..b78dcb322 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml @@ -107,6 +107,7 @@ config: # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. # two_factor_code_description_2: 'You can scan that QR Code with your app:' # two_factor_code_description_3: 'Or use that code:' + # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' delete: title: Cancella il mio account (zona pericolosa) description: Rimuovendo il tuo account, TUTTI i tuoi articoli, TUTTE le tue etichette, TUTTE le tue annotazioni ed il tuo account verranno rimossi PERMANENTEMENTE (impossibile da ANNULLARE). Verrai poi disconnesso. diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml index 96725a069..c1f57bc72 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml @@ -107,6 +107,7 @@ config: # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. # two_factor_code_description_2: 'You can scan that QR Code with your app:' # two_factor_code_description_3: 'Or use that code:' + # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' delete: title: Suprimir mon compte (Mèfi zòna perilhosa) 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. diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml index 5f77061c0..2dc8d8547 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml @@ -107,6 +107,7 @@ config: # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. # two_factor_code_description_2: 'You can scan that QR Code with your app:' # two_factor_code_description_3: 'Or use that code:' + # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' delete: title: Usuń moje konto (niebezpieczna strefa !) description: Jeżeli usuniesz swoje konto, wszystkie twoje artykuły, tagi, adnotacje, oraz konto zostaną trwale usunięte (operacja jest NIEODWRACALNA). Następnie zostaniesz wylogowany. diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml index f40f97958..a81d8d0df 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml @@ -107,6 +107,7 @@ config: # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. # two_factor_code_description_2: 'You can scan that QR Code with your app:' # two_factor_code_description_3: 'Or use that code:' + # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' delete: # title: Delete my account (a.k.a danger zone) # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml index 369d2d44a..fd5658195 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml @@ -107,6 +107,7 @@ config: # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. # two_factor_code_description_2: 'You can scan that QR Code with your app:' # two_factor_code_description_3: 'Or use that code:' + # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' delete: # title: Delete my account (a.k.a danger zone) # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml index d9b33fed0..5a0c54451 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml @@ -104,6 +104,7 @@ config: # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. # two_factor_code_description_2: 'You can scan that QR Code with your app:' # two_factor_code_description_3: 'Or use that code:' + # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' delete: title: "Удалить мой аккаунт (или опасная зона)" description: "Если Вы удалите ваш аккаунт, ВСЕ ваши записи, теги и другие данные, будут БЕЗВОЗВРАТНО удалены (операция не может быть отменена после). Затем Вы выйдете из системы." diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml index f25bac846..a69b50085 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml @@ -107,6 +107,7 @@ config: # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. # two_factor_code_description_2: 'You can scan that QR Code with your app:' # two_factor_code_description_3: 'Or use that code:' + # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' delete: title: ลบบัญชีของฉัน (โซนที่เป็นภัย!) description: ถ้าคุณลบบัญชีของคุณIf , รายการทั้งหมดของคุณ, แท็กทั้งหมดของคุณ, หมายเหตุทั้งหมดของคุณและบัญชีของคุณจะถูกลบอย่างถาวร (มันไม่สามารถยกเลิกได้) คุณจะต้องลงชื่อออก diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml index d65fc0016..0c3d84e9b 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml @@ -107,6 +107,7 @@ config: # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. # two_factor_code_description_2: 'You can scan that QR Code with your app:' # two_factor_code_description_3: 'Or use that code:' + # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' delete: # title: Delete my account (a.k.a danger zone) # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. 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 6ee574436..cf4394081 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 @@ -187,19 +187,22 @@ {% for OtpQrCode in app.session.flashbag.get('OtpQrCode') %}
- You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. + {{ 'config.form_user.two_factor_code_description_1'|trans }}
- That code will disapear after a page reload. + {{ 'config.form_user.two_factor_code_description_2'|trans }} +

+ + +

+ {{ 'config.form_user.two_factor_code_description_3'|trans }}

{{ app.user.getGoogleAuthenticatorSecret }}

- Or you can scan that QR Code with your app: -
- - - + {{ 'config.form_user.two_factor_code_description_4'|trans }} +

+ {{ app.user.getBackupCodes|join("\n")|nl2br }}
{% endfor %} 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 73cf592ea..5b00eb7bd 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 @@ -112,7 +112,7 @@ @@ -220,12 +220,16 @@



{{ 'config.form_user.two_factor_code_description_3'|trans }}

{{ app.user.getGoogleAuthenticatorSecret }} +

+ {{ 'config.form_user.two_factor_code_description_4'|trans }} +

+ {{ app.user.getBackupCodes|join("\n")|nl2br }} {% endfor %} {% endif %} diff --git a/src/Wallabag/UserBundle/Entity/User.php b/src/Wallabag/UserBundle/Entity/User.php index 6e305719f..ab34e2bfc 100644 --- a/src/Wallabag/UserBundle/Entity/User.php +++ b/src/Wallabag/UserBundle/Entity/User.php @@ -8,6 +8,7 @@ use FOS\UserBundle\Model\User as BaseUser; use JMS\Serializer\Annotation\Accessor; use JMS\Serializer\Annotation\Groups; use JMS\Serializer\Annotation\XmlRoot; +use Scheb\TwoFactorBundle\Model\BackupCodeInterface; use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface as EmailTwoFactorInterface; use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface as GoogleTwoFactorInterface; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; @@ -28,7 +29,7 @@ use Wallabag\CoreBundle\Helper\EntityTimestampsTrait; * @UniqueEntity("email") * @UniqueEntity("username") */ -class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorInterface +class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorInterface, BackupCodeInterface { use EntityTimestampsTrait; @@ -127,6 +128,11 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI */ private $googleAuthenticatorSecret; + /** + * @ORM\Column(type="json_array", nullable=true) + */ + private $backupCodes; + /** * @var bool * @@ -318,6 +324,36 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI $this->googleAuthenticatorSecret = $googleAuthenticatorSecret; } + public function setBackupCodes(array $codes = null) + { + $this->backupCodes = $codes; + } + + public function getBackupCodes() + { + return $this->backupCodes; + } + + /** + * {@inheritdoc} + */ + public function isBackupCode(string $code): bool + { + return \in_array($code, $this->backupCodes, true); + } + + /** + * {@inheritdoc} + */ + public function invalidateBackupCode(string $code): void + { + $key = array_search($code, $this->backupCodes, true); + + if (false !== $key) { + unset($this->backupCodes[$key]); + } + } + /** * @param Client $client * From 842af5c3571c5318ae4e1c81dc52457fbf6d3f21 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Wed, 5 Dec 2018 11:39:51 +0100 Subject: [PATCH 057/107] Add SQLite & PG migration Also remove the forced `server_version` from dbal config to avoid an hard overriding across all database. --- .../Version20181202073750.php | 46 +++++++++++++++++-- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/app/DoctrineMigrations/Version20181202073750.php b/app/DoctrineMigrations/Version20181202073750.php index b6ad8bd73..5978291e8 100644 --- a/app/DoctrineMigrations/Version20181202073750.php +++ b/app/DoctrineMigrations/Version20181202073750.php @@ -6,21 +6,39 @@ use Doctrine\DBAL\Schema\Schema; use Wallabag\CoreBundle\Doctrine\WallabagMigration; /** - * Add 2fa OTP (named google authenticator). + * Add 2fa OTP stuff. */ final class Version20181202073750 extends WallabagMigration { public function up(Schema $schema): void { - $tableName = $this->getTable('annotation'); - switch ($this->connection->getDatabasePlatform()->getName()) { case 'sqlite': + $this->addSql('DROP INDEX UNIQ_1D63E7E5C05FB297'); + $this->addSql('DROP INDEX UNIQ_1D63E7E5A0D96FBF'); + $this->addSql('DROP INDEX UNIQ_1D63E7E592FC23A8'); + $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('user', true) . ' AS SELECT id, username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, name, created_at, updated_at, authCode, twoFactorAuthentication FROM ' . $this->getTable('user', true) . ''); + $this->addSql('DROP TABLE ' . $this->getTable('user', true) . ''); + $this->addSql('CREATE TABLE ' . $this->getTable('user', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username VARCHAR(180) NOT NULL COLLATE BINARY, username_canonical VARCHAR(180) NOT NULL COLLATE BINARY, email VARCHAR(180) NOT NULL COLLATE BINARY, email_canonical VARCHAR(180) NOT NULL COLLATE BINARY, enabled BOOLEAN NOT NULL, password VARCHAR(255) NOT NULL COLLATE BINARY, last_login DATETIME DEFAULT NULL, password_requested_at DATETIME DEFAULT NULL, name CLOB DEFAULT NULL COLLATE BINARY, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, authCode INTEGER DEFAULT NULL, emailTwoFactor BOOLEAN NOT NULL, salt VARCHAR(255) DEFAULT NULL, confirmation_token VARCHAR(180) DEFAULT NULL, roles CLOB NOT NULL --(DC2Type:array) + , googleAuthenticatorSecret VARCHAR(255) DEFAULT NULL, backupCodes CLOB DEFAULT NULL --(DC2Type:json_array) + )'); + $this->addSql('INSERT INTO ' . $this->getTable('user', true) . ' (id, username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, name, created_at, updated_at, authCode, emailTwoFactor) SELECT id, username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, name, created_at, updated_at, authCode, twoFactorAuthentication FROM __temp__' . $this->getTable('user', true) . ''); + $this->addSql('DROP TABLE __temp__' . $this->getTable('user', true) . ''); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1D63E7E5C05FB297 ON ' . $this->getTable('user', true) . ' (confirmation_token)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1D63E7E5A0D96FBF ON ' . $this->getTable('user', true) . ' (email_canonical)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1D63E7E592FC23A8 ON ' . $this->getTable('user', true) . ' (username_canonical)'); break; case 'mysql': - $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' ADD googleAuthenticatorSecret VARCHAR(191) DEFAULT NULL, CHANGE twoFactorAuthentication emailTwoFactor BOOLEAN NOT NULL, DROP trusted, ADD backupCodes LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:json_array)\''); + $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' ADD googleAuthenticatorSecret VARCHAR(191) DEFAULT NULL'); + $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' CHANGE twoFactorAuthentication emailTwoFactor BOOLEAN NOT NULL'); + $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' DROP trusted'); + $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' ADD backupCodes LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:json_array)\''); break; case 'postgresql': + $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' ADD googleAuthenticatorSecret VARCHAR(191) DEFAULT NULL'); + $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' RENAME COLUMN twofactorauthentication TO emailTwoFactor'); + $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' DROP trusted'); + $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' ADD backupCodes TEXT DEFAULT NULL'); break; } } @@ -29,11 +47,29 @@ final class Version20181202073750 extends WallabagMigration { switch ($this->connection->getDatabasePlatform()->getName()) { case 'sqlite': + $this->addSql('DROP INDEX UNIQ_1D63E7E592FC23A8'); + $this->addSql('DROP INDEX UNIQ_1D63E7E5A0D96FBF'); + $this->addSql('DROP INDEX UNIQ_1D63E7E5C05FB297'); + $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('user', true) . ' AS SELECT id, username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, name, created_at, updated_at, authCode, emailTwoFactor FROM "' . $this->getTable('user', true) . '"'); + $this->addSql('DROP TABLE "' . $this->getTable('user', true) . '"'); + $this->addSql('CREATE TABLE "' . $this->getTable('user', true) . '" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username VARCHAR(180) NOT NULL, username_canonical VARCHAR(180) NOT NULL, email VARCHAR(180) NOT NULL, email_canonical VARCHAR(180) NOT NULL, enabled BOOLEAN NOT NULL, password VARCHAR(255) NOT NULL, last_login DATETIME DEFAULT NULL, password_requested_at DATETIME DEFAULT NULL, name CLOB DEFAULT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, authCode INTEGER DEFAULT NULL, twoFactorAuthentication BOOLEAN NOT NULL, salt VARCHAR(255) NOT NULL COLLATE BINARY, confirmation_token VARCHAR(255) DEFAULT NULL COLLATE BINARY, roles CLOB NOT NULL COLLATE BINARY, trusted CLOB DEFAULT NULL COLLATE BINARY)'); + $this->addSql('INSERT INTO "' . $this->getTable('user', true) . '" (id, username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, name, created_at, updated_at, authCode, twoFactorAuthentication) SELECT id, username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, name, created_at, updated_at, authCode, emailTwoFactor FROM __temp__' . $this->getTable('user', true) . ''); + $this->addSql('DROP TABLE __temp__' . $this->getTable('user', true) . ''); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1D63E7E592FC23A8 ON "' . $this->getTable('user', true) . '" (username_canonical)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1D63E7E5A0D96FBF ON "' . $this->getTable('user', true) . '" (email_canonical)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1D63E7E5C05FB297 ON "' . $this->getTable('user', true) . '" (confirmation_token)'); break; case 'mysql': - $this->addSql('ALTER TABLE `' . $this->getTable('user') . '` DROP googleAuthenticatorSecret, CHANGE emailtwofactor twoFactorAuthentication BOOLEAN NOT NULL, ADD trusted TEXT DEFAULT NULL, DROP backupCodes'); + $this->addSql('ALTER TABLE `' . $this->getTable('user') . '` DROP googleAuthenticatorSecret'); + $this->addSql('ALTER TABLE `' . $this->getTable('user') . '` CHANGE emailtwofactor twoFactorAuthentication BOOLEAN NOT NULL'); + $this->addSql('ALTER TABLE `' . $this->getTable('user') . '` ADD trusted TEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE `' . $this->getTable('user') . '` DROP backupCodes'); break; case 'postgresql': + $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' DROP googleAuthenticatorSecret'); + $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' RENAME COLUMN emailTwoFactor TO twofactorauthentication'); + $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' ADD trusted TEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' DROP backupCodes'); break; } } From 6df8b9c6a90de333c9e24a49615fffa9e350e382 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Wed, 5 Dec 2018 14:29:46 +0100 Subject: [PATCH 058/107] Fix PG & Travis drop/create database --- .editorconfig | 2 +- .travis.yml | 10 ++++++---- GNUmakefile | 6 ++++++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.editorconfig b/.editorconfig index 6553d30fd..140440443 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,5 +13,5 @@ insert_final_newline = true indent_style = space indent_size = 2 -[Makefile] +[*akefile] indent_style = tab diff --git a/.travis.yml b/.travis.yml index 0ca1e192c..c660bb5e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,15 +51,17 @@ install: before_script: - PHP=$TRAVIS_PHP_VERSION - - if [[ ! $PHP = hhvm* ]]; then echo "memory_limit=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; fi; - # xdebug isn't enable for PHP 7.1 - - if [[ ! $PHP = hhvm* ]]; then phpenv config-rm xdebug.ini || echo "xdebug not available"; fi + - echo "memory_limit=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini + - phpenv config-rm xdebug.ini || echo "xdebug not available" - composer self-update --no-progress script: - travis_wait bash composer install -o --no-interaction --no-progress --prefer-dist + - echo "travis_fold:start:prepare" - - make prepare DB=$DB + # custom "prepare" for PG because the database should be created with a different user (see "before_script") + - if [[ ! $DB = pgsql ]]; then make prepare DB=$DB; fi; + - if [[ $DB = pgsql ]]; then make prepare-travis-pg DB=$DB; fi; - echo "travis_fold:end:prepare" - make fixtures diff --git a/GNUmakefile b/GNUmakefile index a04468cb1..d8c162028 100755 --- a/GNUmakefile +++ b/GNUmakefile @@ -25,6 +25,12 @@ run: ## Run the wallabag built-in server build: ## Run webpack @npm run build:$(ENV) +prepare-travis-pg: ## Custom prepare for Travis & Postgres (do not drop/create the database) +ifdef DB + cp app/config/tests/parameters_test.$(DB).yml app/config/parameters_test.yml +endif + php bin/console doctrine:migrations:migrate --no-interaction --env=test + prepare: clean ## Prepare database for testsuite ifdef DB cp app/config/tests/parameters_test.$(DB).yml app/config/parameters_test.yml From e073090b8d86ce925f8a08b68e212cc2af66e639 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Fri, 7 Dec 2018 18:00:57 +0100 Subject: [PATCH 059/107] Update translation --- src/Wallabag/CoreBundle/Resources/translations/messages.da.yml | 2 +- src/Wallabag/CoreBundle/Resources/translations/messages.de.yml | 2 +- src/Wallabag/CoreBundle/Resources/translations/messages.en.yml | 2 +- src/Wallabag/CoreBundle/Resources/translations/messages.es.yml | 2 +- src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml | 2 +- src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml | 2 +- src/Wallabag/CoreBundle/Resources/translations/messages.it.yml | 2 +- src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml | 2 +- src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml | 2 +- src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml | 2 +- src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml | 2 +- src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml | 2 +- src/Wallabag/CoreBundle/Resources/translations/messages.th.yml | 2 +- src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml index 0114a983b..ae8f8695d 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml @@ -538,7 +538,7 @@ user: # enabled_label: 'Enabled' # last_login_label: 'Last login' # twofactor_email_label: Two factor authentication by email - # twofactor_google_label: Two factor authentication by Google + # twofactor_google_label: Two factor authentication by OTP app # save: Save # delete: Delete # delete_confirm: Are you sure? diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml index fd9796ba8..7b66e5dcc 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml @@ -538,7 +538,7 @@ user: enabled_label: 'Aktiviert' last_login_label: 'Letzter Login' # twofactor_email_label: Two factor authentication by email - # twofactor_google_label: Two factor authentication by Google + # twofactor_google_label: Two factor authentication by OTP app save: 'Speichern' delete: 'Löschen' delete_confirm: 'Bist du sicher?' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml index ddc079ed0..567584b2d 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml @@ -538,7 +538,7 @@ user: enabled_label: 'Enabled' last_login_label: 'Last login' twofactor_email_label: Two factor authentication by email - twofactor_google_label: Two factor authentication by Google + twofactor_google_label: Two factor authentication by OTP app save: Save delete: Delete delete_confirm: Are you sure? diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml index 8ac661692..1ba4bce4e 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml @@ -538,7 +538,7 @@ user: enabled_label: 'Activado' last_login_label: 'Último inicio de sesión' # twofactor_email_label: Two factor authentication by email - # twofactor_google_label: Two factor authentication by Google + # twofactor_google_label: Two factor authentication by OTP app save: Guardar delete: Eliminar delete_confirm: ¿Estás seguro? diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml index bc754ca21..d20c89d90 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml @@ -538,7 +538,7 @@ user: # enabled_label: 'Enabled' # last_login_label: 'Last login' # twofactor_email_label: Two factor authentication by email - # twofactor_google_label: Two factor authentication by Google + # twofactor_google_label: Two factor authentication by OTP app # save: Save # delete: Delete # delete_confirm: Are you sure? diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml index 288411451..fd405059e 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml @@ -539,7 +539,7 @@ user: last_login_label: "Dernière connexion" twofactor_label: "Double authentification" twofactor_email_label: Double authentification par email - twofactor_google_label: Double authentification par Google + twofactor_google_label: Double authentification par OTP app save: "Sauvegarder" delete: "Supprimer" delete_confirm: "Êtes-vous sûr ?" diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml index b78dcb322..333262312 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml @@ -538,7 +538,7 @@ user: enabled_label: 'Abilitato' last_login_label: 'Ultima connessione' # twofactor_email_label: Two factor authentication by email - # twofactor_google_label: Two factor authentication by Google + # twofactor_google_label: Two factor authentication by OTP app save: Salva delete: Cancella delete_confirm: Sei sicuro? diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml index c1f57bc72..599490e1a 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml @@ -538,7 +538,7 @@ user: enabled_label: 'Actiu' last_login_label: 'Darrièra connexion' # twofactor_email_label: Two factor authentication by email - # twofactor_google_label: Two factor authentication by Google + # twofactor_google_label: Two factor authentication by OTP app save: 'Enregistrar' delete: 'Suprimir' delete_confirm: 'Sètz segur ?' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml index 2dc8d8547..89fd34dcd 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml @@ -538,7 +538,7 @@ user: enabled_label: 'Włączony' last_login_label: 'Ostatnie logowanie' # twofactor_email_label: Two factor authentication by email - # twofactor_google_label: Two factor authentication by Google + # twofactor_google_label: Two factor authentication by OTP app save: Zapisz delete: Usuń delete_confirm: Jesteś pewien? diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml index a81d8d0df..f37aeb91f 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml @@ -538,7 +538,7 @@ user: enabled_label: 'Habilitado' last_login_label: 'Último login' # twofactor_email_label: Two factor authentication by email - # twofactor_google_label: Two factor authentication by Google + # twofactor_google_label: Two factor authentication by OTP app save: 'Salvar' delete: 'Apagar' delete_confirm: 'Tem certeza?' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml index fd5658195..c9d9500d2 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml @@ -538,7 +538,7 @@ user: # enabled_label: 'Enabled' # last_login_label: 'Last login' # twofactor_email_label: Two factor authentication by email - # twofactor_google_label: Two factor authentication by Google + # twofactor_google_label: Two factor authentication by OTP app # save: Save # delete: Delete # delete_confirm: Are you sure? diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml index 5a0c54451..62a078d40 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml @@ -526,7 +526,7 @@ user: enabled_label: 'Включить' last_login_label: 'Последний вход' # twofactor_email_label: Two factor authentication by email - # twofactor_google_label: Two factor authentication by Google + # twofactor_google_label: Two factor authentication by OTP app save: "Сохранить" delete: "Удалить" delete_confirm: "Вы уверены?" diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml index a69b50085..78b5727a0 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml @@ -536,7 +536,7 @@ user: enabled_label: 'เปิดใช้งาน' last_login_label: 'ลงชื้อเข้าใช้ครั้งสุดท้าย' # twofactor_email_label: Two factor authentication by email - # twofactor_google_label: Two factor authentication by Google + # twofactor_google_label: Two factor authentication by OTP app save: บันทึก delete: ลบ delete_confirm: ตุณแน่ใจหรือไม่? diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml index 0c3d84e9b..9f4c01f79 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml @@ -536,7 +536,7 @@ user: # enabled_label: 'Enabled' # last_login_label: 'Last login' # twofactor_email_label: Two factor authentication by email - # twofactor_google_label: Two factor authentication by Google + # twofactor_google_label: Two factor authentication by OTP app # save: Save # delete: Delete # delete_confirm: Are you sure? From 4c0e747940ac39630f1d2a6a14c628ba6729ecfd Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Fri, 7 Dec 2018 18:01:06 +0100 Subject: [PATCH 060/107] Remove secret from admin --- .../UserBundle/Resources/views/Manage/edit.html.twig | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Wallabag/UserBundle/Resources/views/Manage/edit.html.twig b/src/Wallabag/UserBundle/Resources/views/Manage/edit.html.twig index 8be37e79a..2de8f3a55 100644 --- a/src/Wallabag/UserBundle/Resources/views/Manage/edit.html.twig +++ b/src/Wallabag/UserBundle/Resources/views/Manage/edit.html.twig @@ -59,12 +59,6 @@ {{ form_label(edit_form.googleTwoFactor) }} {{ form_errors(edit_form.googleTwoFactor) }} - - {% if user.isGoogleAuthenticatorEnabled %} -
-

OTP Secret: {{ user.googleAuthenticatorSecret }}

-
- {% endif %} {% endif %} From a0c5eb003f1cbeef10d5620e98870c7556e17c75 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Fri, 18 Jan 2019 22:46:44 +0100 Subject: [PATCH 061/107] Change the way to enable 2FA And add a step to validate a generated code from the OTP app --- .../Controller/ConfigController.php | 132 +++++++++--- .../Resources/translations/messages.da.yml | 25 ++- .../Resources/translations/messages.de.yml | 16 +- .../Resources/translations/messages.en.yml | 26 ++- .../Resources/translations/messages.es.yml | 25 ++- .../Resources/translations/messages.fa.yml | 25 ++- .../Resources/translations/messages.fr.yml | 26 ++- .../Resources/translations/messages.it.yml | 24 ++- .../Resources/translations/messages.oc.yml | 24 ++- .../Resources/translations/messages.pl.yml | 26 ++- .../Resources/translations/messages.pt.yml | 24 ++- .../Resources/translations/messages.ro.yml | 24 ++- .../Resources/translations/messages.ru.yml | 24 ++- .../Resources/translations/messages.th.yml | 24 ++- .../Resources/translations/messages.tr.yml | 24 ++- .../views/themes/baggy/Config/index.html.twig | 61 +++--- .../themes/baggy/Config/otp_app.html.twig | 55 +++++ .../themes/material/Config/index.html.twig | 71 +++---- .../themes/material/Config/otp_app.html.twig | 63 ++++++ .../Controller/ConfigControllerTest.php | 194 ++++++++---------- 20 files changed, 620 insertions(+), 293 deletions(-) create mode 100644 src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/otp_app.html.twig create mode 100644 src/Wallabag/CoreBundle/Resources/views/themes/material/Config/otp_app.html.twig diff --git a/src/Wallabag/CoreBundle/Controller/ConfigController.php b/src/Wallabag/CoreBundle/Controller/ConfigController.php index c9fc57026..2643eed03 100644 --- a/src/Wallabag/CoreBundle/Controller/ConfigController.php +++ b/src/Wallabag/CoreBundle/Controller/ConfigController.php @@ -81,28 +81,7 @@ class ConfigController extends Controller ]); $userForm->handleRequest($request); - // `googleTwoFactor` isn't a field within the User entity, we need to define it's value in a different way - if ($this->getParameter('twofactor_auth') && true === $user->isGoogleAuthenticatorEnabled() && false === $userForm->isSubmitted()) { - $userForm->get('googleTwoFactor')->setData(true); - } - if ($userForm->isSubmitted() && $userForm->isValid()) { - // handle creation / reset of the OTP secret if checkbox changed from the previous state - if ($this->getParameter('twofactor_auth')) { - if (true === $userForm->get('googleTwoFactor')->getData() && false === $user->isGoogleAuthenticatorEnabled()) { - $secret = $this->get('scheb_two_factor.security.google_authenticator')->generateSecret(); - - $user->setGoogleAuthenticatorSecret($secret); - $user->setEmailTwoFactor(false); - $user->setBackupCodes((new BackupCodes())->toArray()); - - $this->addFlash('OtpQrCode', $this->get('scheb_two_factor.security.google_authenticator')->getQRContent($user)); - } elseif (false === $userForm->get('googleTwoFactor')->getData() && true === $user->isGoogleAuthenticatorEnabled()) { - $user->setGoogleAuthenticatorSecret(null); - $user->setBackupCodes(null); - } - } - $userManager->updateUser($user, true); $this->addFlash( @@ -175,11 +154,118 @@ class ConfigController extends Controller ], 'twofactor_auth' => $this->getParameter('twofactor_auth'), 'wallabag_url' => $this->getParameter('domain_name'), - 'enabled_users' => $this->get('wallabag_user.user_repository') - ->getSumEnabledUsers(), + 'enabled_users' => $this->get('wallabag_user.user_repository')->getSumEnabledUsers(), ]); } + /** + * Enable 2FA using email. + * + * @param Request $request + * + * @Route("/config/otp/email", name="config_otp_email") + */ + public function otpEmailAction(Request $request) + { + if (!$this->getParameter('twofactor_auth')) { + return $this->createNotFoundException('two_factor not enabled'); + } + + $user = $this->getUser(); + + $user->setGoogleAuthenticatorSecret(null); + $user->setBackupCodes(null); + $user->setEmailTwoFactor(true); + + $this->container->get('fos_user.user_manager')->updateUser($user, true); + + $this->addFlash( + 'notice', + 'flashes.config.notice.otp_enabled' + ); + + return $this->redirect($this->generateUrl('config') . '#set3'); + } + + /** + * Enable 2FA using OTP app, user will need to confirm the generated code from the app. + * + * @Route("/config/otp/app", name="config_otp_app") + */ + public function otpAppAction() + { + if (!$this->getParameter('twofactor_auth')) { + return $this->createNotFoundException('two_factor not enabled'); + } + + $user = $this->getUser(); + + if (!$user->isGoogleTwoFactor()) { + $secret = $this->get('scheb_two_factor.security.google_authenticator')->generateSecret(); + + $user->setGoogleAuthenticatorSecret($secret); + $user->setEmailTwoFactor(false); + $user->setBackupCodes((new BackupCodes())->toArray()); + + $this->container->get('fos_user.user_manager')->updateUser($user, true); + } + + return $this->render('WallabagCoreBundle:Config:otp_app.html.twig', [ + 'qr_code' => $this->get('scheb_two_factor.security.google_authenticator')->getQRContent($user), + ]); + } + + /** + * Cancelling 2FA using OTP app. + * + * @Route("/config/otp/app/cancel", name="config_otp_app_cancel") + */ + public function otpAppCancelAction() + { + if (!$this->getParameter('twofactor_auth')) { + return $this->createNotFoundException('two_factor not enabled'); + } + + $user = $this->getUser(); + $user->setGoogleAuthenticatorSecret(null); + $user->setBackupCodes(null); + + $this->container->get('fos_user.user_manager')->updateUser($user, true); + + return $this->redirect($this->generateUrl('config') . '#set3'); + } + + /** + * Validate OTP code. + * + * @param Request $request + * + * @Route("/config/otp/app/check", name="config_otp_app_check") + */ + public function otpAppCheckAction(Request $request) + { + $isValid = $this->get('scheb_two_factor.security.google_authenticator')->checkCode( + $this->getUser(), + $request->get('_auth_code') + ); + + if (true === $isValid) { + $this->addFlash( + 'notice', + 'flashes.config.notice.otp_enabled' + ); + + return $this->redirect($this->generateUrl('config') . '#set3'); + } + + $this->addFlash( + 'two_factor', + 'scheb_two_factor.code_invalid' + ); + + return $this->redirect($this->generateUrl('config_otp_app')); + } + /** * @param Request $request * diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml index ae8f8695d..454f547de 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml @@ -102,12 +102,16 @@ config: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Navn' email_label: 'Emailadresse' - # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' - # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. - # two_factor_code_description_2: 'You can scan that QR Code with your app:' - # two_factor_code_description_3: 'Or use that code:' - # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + two_factor: + # emailTwoFactor_label: 'Using email (receive a code by email)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # table_method: Method + # table_state: State + # table_action: Action + # state_enabled: Enabled + # state_disabled: Disabled + # action_email: Use email + # action_app: Use OTP App delete: # title: Delete my account (a.k.a danger zone) # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. @@ -165,6 +169,15 @@ config: # and: 'One rule AND another' # matches: 'Tests that a subject matches a search (case-insensitive).
Example: title matches "football"' # notmatches: 'Tests that a subject doesn''t match match a search (case-insensitive).
Example: title notmatches "football"' + otp: + # page_title: Two-factor authentication + # app: + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # two_factor_code_description_4: 'Test an OTP code from your configured app:' + # cancel: Cancel + # enable: Enable entry: # default_title: 'Title of the entry' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml index 7b66e5dcc..dc1d4723f 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml @@ -102,12 +102,16 @@ config: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Name' email_label: 'E-Mail-Adresse' - # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' - # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. - # two_factor_code_description_2: 'You can scan that QR Code with your app:' - # two_factor_code_description_3: 'Or use that code:' - # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + two_factor: + # emailTwoFactor_label: 'Using email (receive a code by email)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # table_method: Method + # table_state: State + # table_action: Action + # state_enabled: Enabled + # state_disabled: Disabled + # action_email: Use email + # action_app: Use OTP App delete: title: 'Lösche mein Konto (a.k.a Gefahrenzone)' 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.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml index 567584b2d..45145c806 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml @@ -102,12 +102,16 @@ config: two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Name' email_label: 'Email' - emailTwoFactor_label: 'Using email (receive a code by email)' - googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' - two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. - two_factor_code_description_2: 'You can scan that QR Code with your app:' - two_factor_code_description_3: 'Or use that code:' - two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + two_factor: + emailTwoFactor_label: 'Using email (receive a code by email)' + googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + table_method: Method + table_state: State + table_action: Action + state_enabled: Enabled + state_disabled: Disabled + action_email: Use email + action_app: Use OTP App delete: title: Delete my account (a.k.a danger zone) description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. @@ -165,6 +169,15 @@ config: and: 'One rule AND another' matches: 'Tests that a subject matches a search (case-insensitive).
Example: title matches "football"' notmatches: 'Tests that a subject doesn''t match match a search (case-insensitive).
Example: title notmatches "football"' + otp: + page_title: Two-factor authentication + app: + two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + two_factor_code_description_2: 'You can scan that QR Code with your app:' + two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + two_factor_code_description_4: 'Test an OTP code from your configured app:' + cancel: Cancel + enable: Enable entry: default_title: 'Title of the entry' @@ -584,6 +597,7 @@ flashes: tags_reset: Tags reset entries_reset: Entries reset archived_reset: Archived entries deleted + otp_enabled: Two-factor authentication enabled entry: notice: entry_already_saved: 'Entry already saved on %date%' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml index 1ba4bce4e..c1047e55a 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml @@ -102,12 +102,16 @@ config: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Nombre' email_label: 'Dirección de e-mail' - # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' - # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. - # two_factor_code_description_2: 'You can scan that QR Code with your app:' - # two_factor_code_description_3: 'Or use that code:' - # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + two_factor: + # emailTwoFactor_label: 'Using email (receive a code by email)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # table_method: Method + # table_state: State + # table_action: Action + # state_enabled: Enabled + # state_disabled: Disabled + # action_email: Use email + # action_app: Use OTP App delete: title: Eliminar mi cuenta (Zona peligrosa) description: Si eliminas tu cuenta, TODOS tus artículos, TODAS tus etiquetas, TODAS tus anotaciones y tu cuenta serán eliminadas de forma PERMANENTE (no se puede deshacer). Después serás desconectado. @@ -165,6 +169,15 @@ config: and: 'Una regla Y la otra' matches: 'Prueba si un sujeto corresponde a una búsqueda (insensible a mayusculas).
Ejemplo : title matches "fútbol"' # notmatches: 'Tests that a subject doesn''t match match a search (case-insensitive).
Example: title notmatches "football"' + otp: + # page_title: Two-factor authentication + # app: + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # two_factor_code_description_4: 'Test an OTP code from your configured app:' + # cancel: Cancel + # enable: Enable entry: default_title: 'Título del artículo' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml index d20c89d90..3042de2ef 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml @@ -102,12 +102,16 @@ config: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'نام' email_label: 'نشانی ایمیل' - # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' - # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. - # two_factor_code_description_2: 'You can scan that QR Code with your app:' - # two_factor_code_description_3: 'Or use that code:' - # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + two_factor: + # emailTwoFactor_label: 'Using email (receive a code by email)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # table_method: Method + # table_state: State + # table_action: Action + # state_enabled: Enabled + # state_disabled: Disabled + # action_email: Use email + # action_app: Use OTP App delete: # title: Delete my account (a.k.a danger zone) # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. @@ -165,6 +169,15 @@ config: # and: 'One rule AND another' # matches: 'Tests that a subject matches a search (case-insensitive).
Example: title matches "football"' # notmatches: 'Tests that a subject doesn''t match match a search (case-insensitive).
Example: title notmatches "football"' + otp: + # page_title: Two-factor authentication + # app: + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # two_factor_code_description_4: 'Test an OTP code from your configured app:' + # cancel: Cancel + # enable: Enable entry: # default_title: 'Title of the entry' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml index fd405059e..57740ba23 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml @@ -102,12 +102,16 @@ config: two_factor_description: "Activer l’authentification double-facteur veut dire que vous allez recevoir un code par courriel OU que vous devriez utiliser une application de mot de passe à usage unique (comme Google Authenticator, Authy or FreeOTP) pour obtenir un code temporaire à chaque nouvelle connexion non approuvée. Vous ne pouvez pas choisir les deux options." name_label: "Nom" email_label: "Adresse courriel" - emailTwoFactor_label: 'En utlisant l’email (recevez un code par email)' - googleTwoFactor_label: 'En utilisant une application de mot de passe à usage unique (ouvrez l’app, comme Google Authenticator, Authy or FreeOTP, pour obtenir un mot de passe à usage unique)' - two_factor_code_description_1: Vous venez d’activer l’authentification double-facteur, ouvrez votre application OTP pour configurer la génération du mot de passe à usage unique. Ces informations disparaîtront après un rechargement de la page. - two_factor_code_description_2: 'Vous pouvez scanner le QR code avec votre application :' - two_factor_code_description_3: 'Ou utiliser le code suivant :' - two_factor_code_description_4: 'N’oubliez pas de sauvegarder ces codes de secours dans un endroit sûr, vous pourrez les utiliser si vous ne pouvez plus accéder à votre application OTP :' + two_factor: + emailTwoFactor_label: 'En utlisant l’email (recevez un code par email)' + googleTwoFactor_label: 'En utilisant une application de mot de passe à usage unique (ouvrez l’app, comme Google Authenticator, Authy or FreeOTP, pour obtenir un mot de passe à usage unique)' + table_method: Méthode + table_state: État + table_action: Action + state_enabled: Activé + state_disabled: Désactivé + action_email: Utiliser l'email + action_app: Utiliser une app OTP delete: title: "Supprimer mon compte (attention danger !)" 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é." @@ -165,6 +169,15 @@ config: and: "Une règle ET l’autre" matches: "Teste si un sujet correspond à une recherche (non sensible à la casse).
Exemple : title matches \"football\"" notmatches: "Teste si un sujet ne correspond pas à une recherche (non sensible à la casse).
Exemple : title notmatches \"football\"" + otp: + page_title: Authentification double-facteur + app: + two_factor_code_description_1: Vous venez d’activer l’authentification double-facteur, ouvrez votre application OTP pour configurer la génération du mot de passe à usage unique. Ces informations disparaîtront après un rechargement de la page. + two_factor_code_description_2: 'Vous pouvez scanner le QR code avec votre application :' + two_factor_code_description_3: 'N’oubliez pas de sauvegarder ces codes de secours dans un endroit sûr, vous pourrez les utiliser si vous ne pouvez plus accéder à votre application OTP :' + two_factor_code_description_4: 'Testez un code généré par votre application OTP :' + cancel: Annuler + enable: Activer entry: default_title: "Titre de l’article" @@ -585,6 +598,7 @@ flashes: tags_reset: "Tags supprimés" entries_reset: "Articles supprimés" archived_reset: "Articles archivés supprimés" + otp_enabled: "Authentification à double-facteur activée" entry: notice: entry_already_saved: "Article déjà sauvegardé le %date%" diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml index 333262312..274e5338a 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml @@ -102,12 +102,15 @@ config: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Nome' email_label: 'E-mail' - # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' - # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. - # two_factor_code_description_2: 'You can scan that QR Code with your app:' - # two_factor_code_description_3: 'Or use that code:' - # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # emailTwoFactor_label: 'Using email (receive a code by email)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # table_method: Method + # table_state: State + # table_action: Action + # state_enabled: Enabled + # state_disabled: Disabled + # action_email: Use email + # action_app: Use OTP App delete: title: Cancella il mio account (zona pericolosa) description: Rimuovendo il tuo account, TUTTI i tuoi articoli, TUTTE le tue etichette, TUTTE le tue annotazioni ed il tuo account verranno rimossi PERMANENTEMENTE (impossibile da ANNULLARE). Verrai poi disconnesso. @@ -165,6 +168,15 @@ config: and: "Una regola E un'altra" matches: 'Verifica che un oggetto risulti in una ricerca (case-insensitive).
Esempio: titolo contiene "football"' # notmatches: 'Tests that a subject doesn''t match match a search (case-insensitive).
Example: title notmatches "football"' + otp: + # page_title: Two-factor authentication + # app: + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # two_factor_code_description_4: 'Test an OTP code from your configured app:' + # cancel: Cancel + # enable: Enable entry: default_title: "Titolo del contenuto" diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml index 599490e1a..4e5370f98 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml @@ -102,12 +102,15 @@ config: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Nom' email_label: 'Adreça de corrièl' - # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' - # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. - # two_factor_code_description_2: 'You can scan that QR Code with your app:' - # two_factor_code_description_3: 'Or use that code:' - # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # emailTwoFactor_label: 'Using email (receive a code by email)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # table_method: Method + # table_state: State + # table_action: Action + # state_enabled: Enabled + # state_disabled: Disabled + # action_email: Use email + # action_app: Use OTP App delete: title: Suprimir mon compte (Mèfi zòna perilhosa) 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. @@ -165,6 +168,15 @@ config: and: "Una règla E l'autra" matches: 'Teste se un subjècte correspond a una recèrca (non sensibla a la cassa).
Exemple : title matches \"football\"' notmatches: 'Teste se subjècte correspond pas a una recèrca (sensibla a la cassa).
Example : title notmatches "football"' + otp: + # page_title: Two-factor authentication + # app: + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # two_factor_code_description_4: 'Test an OTP code from your configured app:' + # cancel: Cancel + # enable: Enable entry: default_title: "Títol de l'article" diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml index 89fd34dcd..a7a4d6c39 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml @@ -99,15 +99,18 @@ config: all: 'Wszystkie' rss_limit: 'Link do RSS' form_user: - two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." + # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Nazwa' email_label: 'Adres email' - # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' - # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. - # two_factor_code_description_2: 'You can scan that QR Code with your app:' - # two_factor_code_description_3: 'Or use that code:' - # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # emailTwoFactor_label: 'Using email (receive a code by email)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # table_method: Method + # table_state: State + # table_action: Action + # state_enabled: Enabled + # state_disabled: Disabled + # action_email: Use email + # action_app: Use OTP App delete: title: Usuń moje konto (niebezpieczna strefa !) description: Jeżeli usuniesz swoje konto, wszystkie twoje artykuły, tagi, adnotacje, oraz konto zostaną trwale usunięte (operacja jest NIEODWRACALNA). Następnie zostaniesz wylogowany. @@ -165,6 +168,15 @@ config: and: 'Jedna reguła I inna' matches: 'Sprawdź czy temat pasuje szukaj (duże lub małe litery).
Przykład: tytuł zawiera "piłka nożna"' notmatches: 'Sprawdź czy temat nie zawiera szukaj (duże lub małe litery).
Przykład: tytuł nie zawiera "piłka nożna"' + otp: + # page_title: Two-factor authentication + # app: + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # two_factor_code_description_4: 'Test an OTP code from your configured app:' + # cancel: Cancel + # enable: Enable entry: default_title: 'Tytuł wpisu' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml index f37aeb91f..a5483a6d3 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml @@ -102,12 +102,15 @@ config: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Nome' email_label: 'E-mail' - # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' - # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. - # two_factor_code_description_2: 'You can scan that QR Code with your app:' - # two_factor_code_description_3: 'Or use that code:' - # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # emailTwoFactor_label: 'Using email (receive a code by email)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # table_method: Method + # table_state: State + # table_action: Action + # state_enabled: Enabled + # state_disabled: Disabled + # action_email: Use email + # action_app: Use OTP App delete: # title: Delete my account (a.k.a danger zone) # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. @@ -165,6 +168,15 @@ config: and: 'Uma regra E outra' matches: 'Testa que um assunto corresponde a uma pesquisa (maiúscula ou minúscula).
Exemplo: título corresponde a "futebol"' # notmatches: 'Tests that a subject doesn''t match match a search (case-insensitive).
Example: title notmatches "football"' + otp: + # page_title: Two-factor authentication + # app: + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # two_factor_code_description_4: 'Test an OTP code from your configured app:' + # cancel: Cancel + # enable: Enable entry: default_title: 'Título da entrada' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml index c9d9500d2..3b7fbd691 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml @@ -102,12 +102,15 @@ config: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Nume' email_label: 'E-mail' - # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' - # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. - # two_factor_code_description_2: 'You can scan that QR Code with your app:' - # two_factor_code_description_3: 'Or use that code:' - # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # emailTwoFactor_label: 'Using email (receive a code by email)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # table_method: Method + # table_state: State + # table_action: Action + # state_enabled: Enabled + # state_disabled: Disabled + # action_email: Use email + # action_app: Use OTP App delete: # title: Delete my account (a.k.a danger zone) # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. @@ -165,6 +168,15 @@ config: # and: 'One rule AND another' # matches: 'Tests that a subject matches a search (case-insensitive).
Example: title matches "football"' # notmatches: 'Tests that a subject doesn''t match match a search (case-insensitive).
Example: title notmatches "football"' + otp: + # page_title: Two-factor authentication + # app: + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # two_factor_code_description_4: 'Test an OTP code from your configured app:' + # cancel: Cancel + # enable: Enable entry: # default_title: 'Title of the entry' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml index 62a078d40..927466314 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml @@ -99,12 +99,15 @@ config: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Имя' email_label: 'Email' - # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' - # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. - # two_factor_code_description_2: 'You can scan that QR Code with your app:' - # two_factor_code_description_3: 'Or use that code:' - # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # emailTwoFactor_label: 'Using email (receive a code by email)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # table_method: Method + # table_state: State + # table_action: Action + # state_enabled: Enabled + # state_disabled: Disabled + # action_email: Use email + # action_app: Use OTP App delete: title: "Удалить мой аккаунт (или опасная зона)" description: "Если Вы удалите ваш аккаунт, ВСЕ ваши записи, теги и другие данные, будут БЕЗВОЗВРАТНО удалены (операция не может быть отменена после). Затем Вы выйдете из системы." @@ -160,6 +163,15 @@ config: or: 'Одно правило ИЛИ другое' and: 'Одно правило И другое' matches: 'Тесты, в которых тема соответствует поиску (без учета регистра). Пример: title matches "футбол" ' + otp: + # page_title: Two-factor authentication + # app: + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # two_factor_code_description_4: 'Test an OTP code from your configured app:' + # cancel: Cancel + # enable: Enable entry: default_title: 'Название записи' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml index 78b5727a0..1fe4fa0ea 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml @@ -102,12 +102,15 @@ config: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'ชื่อ' email_label: 'อีเมล' - # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' - # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. - # two_factor_code_description_2: 'You can scan that QR Code with your app:' - # two_factor_code_description_3: 'Or use that code:' - # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # emailTwoFactor_label: 'Using email (receive a code by email)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # table_method: Method + # table_state: State + # table_action: Action + # state_enabled: Enabled + # state_disabled: Disabled + # action_email: Use email + # action_app: Use OTP App delete: title: ลบบัญชีของฉัน (โซนที่เป็นภัย!) description: ถ้าคุณลบบัญชีของคุณIf , รายการทั้งหมดของคุณ, แท็กทั้งหมดของคุณ, หมายเหตุทั้งหมดของคุณและบัญชีของคุณจะถูกลบอย่างถาวร (มันไม่สามารถยกเลิกได้) คุณจะต้องลงชื่อออก @@ -165,6 +168,15 @@ config: and: 'หนึ่งข้อบังคับและอื่นๆ' matches: 'ทดสอบว่า เรื่อง นี้ตรงกับ การต้นหา (กรณีไม่ทราบ).
ตัวอย่าง: หัวข้อที่ตรงกับ "football"' notmatches: 'ทดสอบว่า เรื่อง นี้ไม่ตรงกับ การต้นหา (กรณีไม่ทราบ).
ตัวอย่าง: หัวข้อทีไม่ตรงกับ "football"' + otp: + # page_title: Two-factor authentication + # app: + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # two_factor_code_description_4: 'Test an OTP code from your configured app:' + # cancel: Cancel + # enable: Enable entry: default_title: 'หัวข้อรายการ' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml index 9f4c01f79..3b8a0d599 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml @@ -102,12 +102,15 @@ config: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'İsim' email_label: 'E-posta' - # emailTwoFactor_label: 'Using email (receive a code by email)' - # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' - # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. - # two_factor_code_description_2: 'You can scan that QR Code with your app:' - # two_factor_code_description_3: 'Or use that code:' - # two_factor_code_description_4: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # emailTwoFactor_label: 'Using email (receive a code by email)' + # googleTwoFactor_label: 'Using an OTP app (open the app, like Google Authenticator, Authy or FreeOTP, to get a one time code)' + # table_method: Method + # table_state: State + # table_action: Action + # state_enabled: Enabled + # state_disabled: Disabled + # action_email: Use email + # action_app: Use OTP App delete: # title: Delete my account (a.k.a danger zone) # description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. @@ -165,6 +168,15 @@ config: and: 'Bir kural ve diğeri' # matches: 'Tests that a subject matches a search (case-insensitive).
Example: title matches "football"' # notmatches: 'Tests that a subject doesn''t match match a search (case-insensitive).
Example: title notmatches "football"' + otp: + # page_title: Two-factor authentication + # app: + # two_factor_code_description_1: You just enabled the OTP two factor authentication, open your OTP app and use that code to get a one time password. It'll disapear after a page reload. + # two_factor_code_description_2: 'You can scan that QR Code with your app:' + # two_factor_code_description_3: 'Also, save these backup codes in a safe place, you can use them in case you lose access to your OTP app:' + # two_factor_code_description_4: 'Test an OTP code from your configured app:' + # cancel: Cancel + # enable: Enable entry: default_title: 'Makalenin başlığı' 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 cf4394081..93f8ddf8a 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 @@ -168,48 +168,41 @@ + {{ form_widget(form.user.save) }} + {% if twofactor_auth %} +
{{ 'config.otp.page_title'|trans }}
+
{{ 'config.form_user.two_factor_description'|trans }}
-
-
- {{ form_label(form.user.emailTwoFactor) }} - {{ form_errors(form.user.emailTwoFactor) }} - {{ form_widget(form.user.emailTwoFactor) }} -
-
-
- {{ form_label(form.user.googleTwoFactor) }} - {{ form_widget(form.user.googleTwoFactor) }} - {{ form_errors(form.user.googleTwoFactor) }} -
- {% for OtpQrCode in app.session.flashbag.get('OtpQrCode') %} -
- {{ 'config.form_user.two_factor_code_description_1'|trans }} -
- {{ 'config.form_user.two_factor_code_description_2'|trans }} -

- - -

- {{ 'config.form_user.two_factor_code_description_3'|trans }} -

- {{ app.user.getGoogleAuthenticatorSecret }} -

- {{ 'config.form_user.two_factor_code_description_4'|trans }} -

- {{ app.user.getBackupCodes|join("\n")|nl2br }} -
- {% endfor %} -
+ + + + + + + + + + + + + + + + + + + + + +
{{ 'config.form_user.two_factor.table_method'|trans }}{{ 'config.form_user.two_factor.table_state'|trans }}{{ 'config.form_user.two_factor.table_action'|trans }}
{{ 'config.form_user.two_factor.emailTwoFactor_label'|trans }}{% if app.user.isEmailTwoFactor %}{{ 'config.form_user.two_factor.state_enabled'|trans }}{% else %}{{ 'config.form_user.two_factor.state_disabled'|trans }}{% endif %}{{ 'config.form_user.two_factor.action_email'|trans }}
{{ 'config.form_user.two_factor.googleTwoFactor_label'|trans }}{% if app.user.isGoogleTwoFactor %}{{ 'config.form_user.two_factor.state_enabled'|trans }}{% else %}{{ 'config.form_user.two_factor.state_disabled'|trans }}{% endif %}{{ 'config.form_user.two_factor.action_app'|trans }}
+ {% endif %} {{ form_widget(form.user._token) }} - {{ form_widget(form.user.save) }} {% if enabled_users > 1 %} diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/otp_app.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/otp_app.html.twig new file mode 100644 index 000000000..2e4442e36 --- /dev/null +++ b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/otp_app.html.twig @@ -0,0 +1,55 @@ +{% extends "WallabagCoreBundle::layout.html.twig" %} + +{% block title %}{{ 'config.page_title'|trans }} > {{ 'config.otp.page_title'|trans }}{% endblock %} + +{% block content %} +
{{ 'config.otp.page_title'|trans }}
+ +
    +
  1. +

    {{ 'config.otp.app.two_factor_code_description_1'|trans }}

    +

    {{ 'config.otp.app.two_factor_code_description_2'|trans }}

    + +

    + + +

    +
  2. +
  3. +

    {{ 'config.otp.app.two_factor_code_description_3'|trans }}

    + +

    {{ app.user.getBackupCodes|join("\n")|nl2br }}

    +
  4. +
  5. +

    {{ 'config.otp.app.two_factor_code_description_4'|trans }}

    + + {% for flashMessage in app.session.flashbag.get("two_factor") %} +
    + {{ flashMessage|trans }} +
    + {% endfor %} + +
    +
    +
    +
    + + +
    +
    +
    +
    + + {{ 'config.otp.app.cancel'|trans }} + + +
    +
    +
  6. +
+{% endblock %} 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 5b00eb7bd..412c18f49 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 @@ -196,45 +196,40 @@ - {% if twofactor_auth %} -
- {{ 'config.form_user.two_factor_description'|trans }} - -
- {{ form_widget(form.user.emailTwoFactor) }} - {{ form_label(form.user.emailTwoFactor) }} - {{ form_errors(form.user.emailTwoFactor) }} -
-
- {{ form_widget(form.user.googleTwoFactor) }} - {{ form_label(form.user.googleTwoFactor) }} - {{ form_errors(form.user.googleTwoFactor) }} -
-
- - {% for OtpQrCode in app.session.flashbag.get('OtpQrCode') %} -
- {{ 'config.form_user.two_factor_code_description_1'|trans }} -
- {{ 'config.form_user.two_factor_code_description_2'|trans }} -

- - -

- {{ 'config.form_user.two_factor_code_description_3'|trans }} -

- {{ app.user.getGoogleAuthenticatorSecret }} -

- {{ 'config.form_user.two_factor_code_description_4'|trans }} -

- {{ app.user.getBackupCodes|join("\n")|nl2br }} -
- {% endfor %} - {% endif %} - {{ form_widget(form.user.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }} + + {% if twofactor_auth %} +
+
+
+
{{ 'config.otp.page_title'|trans }}
+ +

{{ 'config.form_user.two_factor_description'|trans }}

+ + + + + + + + + + + + + + + + + + + + + + +
{{ 'config.form_user.two_factor.table_method'|trans }}{{ 'config.form_user.two_factor.table_state'|trans }}{{ 'config.form_user.two_factor.table_action'|trans }}
{{ 'config.form_user.two_factor.emailTwoFactor_label'|trans }}{% if app.user.isEmailTwoFactor %}{{ 'config.form_user.two_factor.state_enabled'|trans }}{% else %}{{ 'config.form_user.two_factor.state_disabled'|trans }}{% endif %}{{ 'config.form_user.two_factor.action_email'|trans }}
{{ 'config.form_user.two_factor.googleTwoFactor_label'|trans }}{% if app.user.isGoogleTwoFactor %}{{ 'config.form_user.two_factor.state_enabled'|trans }}{% else %}{{ 'config.form_user.two_factor.state_disabled'|trans }}{% endif %}{{ 'config.form_user.two_factor.action_app'|trans }}
+
+ {% endif %} {{ form_widget(form.user._token) }} diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/otp_app.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/otp_app.html.twig new file mode 100644 index 000000000..6aef355eb --- /dev/null +++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/otp_app.html.twig @@ -0,0 +1,63 @@ +{% extends "WallabagCoreBundle::layout.html.twig" %} + +{% block title %}{{ 'config.page_title'|trans }} > {{ 'config.otp.page_title'|trans }}{% endblock %} + +{% block content %} +
+
+
+
+
{{ 'config.otp.page_title'|trans }}
+ +
    +
  1. +

    {{ 'config.otp.app.two_factor_code_description_1'|trans }}

    +

    {{ 'config.otp.app.two_factor_code_description_2'|trans }}

    + +

    + + +

    +
  2. +
  3. +

    {{ 'config.otp.app.two_factor_code_description_3'|trans }}

    + +

    {{ app.user.getBackupCodes|join("\n")|nl2br }}

    +
  4. +
  5. +

    {{ 'config.otp.app.two_factor_code_description_4'|trans }}

    + + {% for flashMessage in app.session.flashbag.get("two_factor") %} +
    + {{ flashMessage|trans }} +
    + {% endfor %} + +
    +
    +
    +
    + + +
    +
    +
    +
    + + {{ 'config.otp.app.cancel'|trans }} + + +
    +
    +
  6. +
+
+
+
+
+{% endblock %} diff --git a/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php b/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php index 9ca52c643..1090a686b 100644 --- a/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php @@ -297,119 +297,6 @@ class ConfigControllerTest extends WallabagCoreTestCase $this->assertContains('flashes.config.notice.user_updated', $alert[0]); } - public function testUserEnable2faEmail() - { - $this->logInAs('admin'); - $client = $this->getClient(); - - $crawler = $client->request('GET', '/config'); - - $this->assertSame(200, $client->getResponse()->getStatusCode()); - - $form = $crawler->filter('button[id=update_user_save]')->form(); - - $data = [ - 'update_user[emailTwoFactor]' => '1', - ]; - - $client->submit($form, $data); - - $this->assertSame(302, $client->getResponse()->getStatusCode()); - - $crawler = $client->followRedirect(); - - $this->assertGreaterThan(1, $alert = $crawler->filter('body')->extract(['_text'])); - $this->assertContains('flashes.config.notice.user_updated', $alert[0]); - - // restore user - $em = $this->getEntityManager(); - $user = $em - ->getRepository('WallabagUserBundle:User') - ->findOneByUsername('admin'); - - $this->assertTrue($user->isEmailTwoFactor()); - - $user->setEmailTwoFactor(false); - $em->persist($user); - $em->flush(); - } - - public function testUserEnable2faGoogle() - { - $this->logInAs('admin'); - $client = $this->getClient(); - - $crawler = $client->request('GET', '/config'); - - $this->assertSame(200, $client->getResponse()->getStatusCode()); - - $form = $crawler->filter('button[id=update_user_save]')->form(); - - $data = [ - 'update_user[googleTwoFactor]' => '1', - ]; - - $client->submit($form, $data); - - $this->assertSame(302, $client->getResponse()->getStatusCode()); - - $crawler = $client->followRedirect(); - - $this->assertGreaterThan(1, $alert = $crawler->filter('body')->extract(['_text'])); - $this->assertContains('flashes.config.notice.user_updated', $alert[0]); - - // restore user - $em = $this->getEntityManager(); - $user = $em - ->getRepository('WallabagUserBundle:User') - ->findOneByUsername('admin'); - - $this->assertTrue($user->isGoogleAuthenticatorEnabled()); - - $user->setGoogleAuthenticatorSecret(null); - $em->persist($user); - $em->flush(); - } - - public function testUserEnable2faBoth() - { - $this->logInAs('admin'); - $client = $this->getClient(); - - $crawler = $client->request('GET', '/config'); - - $this->assertSame(200, $client->getResponse()->getStatusCode()); - - $form = $crawler->filter('button[id=update_user_save]')->form(); - - $data = [ - 'update_user[googleTwoFactor]' => '1', - 'update_user[emailTwoFactor]' => '1', - ]; - - $client->submit($form, $data); - - $this->assertSame(302, $client->getResponse()->getStatusCode()); - - $crawler = $client->followRedirect(); - - $this->assertGreaterThan(1, $alert = $crawler->filter('body')->extract(['_text'])); - $this->assertContains('flashes.config.notice.user_updated', $alert[0]); - - // restore user - $em = $this->getEntityManager(); - $user = $em - ->getRepository('WallabagUserBundle:User') - ->findOneByUsername('admin'); - - $this->assertTrue($user->isGoogleAuthenticatorEnabled()); - $this->assertFalse($user->isEmailTwoFactor()); - - $user->setGoogleAuthenticatorSecret(null); - $em->persist($user); - $em->flush(); - } - public function testRssUpdateResetToken() { $this->logInAs('admin'); @@ -1113,4 +1000,85 @@ class ConfigControllerTest extends WallabagCoreTestCase $this->assertNotSame('yuyuyuyu', $client->getRequest()->getLocale()); $this->assertNotSame('yuyuyuyu', $client->getContainer()->get('session')->get('_locale')); } + + public function testUserEnable2faEmail() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $crawler = $client->request('GET', '/config/otp/email'); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + + $crawler = $client->followRedirect(); + + $this->assertGreaterThan(1, $alert = $crawler->filter('body')->extract(['_text'])); + $this->assertContains('flashes.config.notice.otp_enabled', $alert[0]); + + // restore user + $em = $this->getEntityManager(); + $user = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUsername('admin'); + + $this->assertTrue($user->isEmailTwoFactor()); + + $user->setEmailTwoFactor(false); + $em->persist($user); + $em->flush(); + } + + public function testUserEnable2faGoogle() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $crawler = $client->request('GET', '/config/otp/app'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + // restore user + $em = $this->getEntityManager(); + $user = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUsername('admin'); + + $this->assertTrue($user->isGoogleTwoFactor()); + $this->assertGreaterThan(0, $user->getBackupCodes()); + + $user->setGoogleAuthenticatorSecret(false); + $user->setBackupCodes(null); + $em->persist($user); + $em->flush(); + } + + public function testUserEnable2faGoogleCancel() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $crawler = $client->request('GET', '/config/otp/app'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + // restore user + $em = $this->getEntityManager(); + $user = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUsername('admin'); + + $this->assertTrue($user->isGoogleTwoFactor()); + $this->assertGreaterThan(0, $user->getBackupCodes()); + + $crawler = $client->request('GET', '/config/otp/app/cancel'); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + + $user = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUsername('admin'); + + $this->assertFalse($user->isGoogleTwoFactor()); + $this->assertEmpty($user->getBackupCodes()); + } } From c416ed485fd318cafb8313679fa33cb65eb88e8e Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Sat, 19 Jan 2019 20:19:56 +0100 Subject: [PATCH 062/107] CS --- src/Wallabag/CoreBundle/Controller/ConfigController.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Wallabag/CoreBundle/Controller/ConfigController.php b/src/Wallabag/CoreBundle/Controller/ConfigController.php index 2643eed03..ed92c999a 100644 --- a/src/Wallabag/CoreBundle/Controller/ConfigController.php +++ b/src/Wallabag/CoreBundle/Controller/ConfigController.php @@ -161,11 +161,9 @@ class ConfigController extends Controller /** * Enable 2FA using email. * - * @param Request $request - * * @Route("/config/otp/email", name="config_otp_email") */ - public function otpEmailAction(Request $request) + public function otpEmailAction() { if (!$this->getParameter('twofactor_auth')) { return $this->createNotFoundException('two_factor not enabled'); From 7485a272ffbcc045e6002b4bf4ea289ce0a0f3b4 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Wed, 23 Jan 2019 13:47:51 +0100 Subject: [PATCH 063/107] Revert PG on Travis about drop/create the database --- .travis.yml | 4 +--- GNUmakefile | 6 ------ 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index c660bb5e5..393d00338 100644 --- a/.travis.yml +++ b/.travis.yml @@ -59,9 +59,7 @@ script: - travis_wait bash composer install -o --no-interaction --no-progress --prefer-dist - echo "travis_fold:start:prepare" - # custom "prepare" for PG because the database should be created with a different user (see "before_script") - - if [[ ! $DB = pgsql ]]; then make prepare DB=$DB; fi; - - if [[ $DB = pgsql ]]; then make prepare-travis-pg DB=$DB; fi; + - make prepare DB=$DB - echo "travis_fold:end:prepare" - make fixtures diff --git a/GNUmakefile b/GNUmakefile index d8c162028..a04468cb1 100755 --- a/GNUmakefile +++ b/GNUmakefile @@ -25,12 +25,6 @@ run: ## Run the wallabag built-in server build: ## Run webpack @npm run build:$(ENV) -prepare-travis-pg: ## Custom prepare for Travis & Postgres (do not drop/create the database) -ifdef DB - cp app/config/tests/parameters_test.$(DB).yml app/config/parameters_test.yml -endif - php bin/console doctrine:migrations:migrate --no-interaction --env=test - prepare: clean ## Prepare database for testsuite ifdef DB cp app/config/tests/parameters_test.$(DB).yml app/config/parameters_test.yml From 4654a83b6438b88e3b7062a21d18999d9df2fb8e Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Wed, 23 Jan 2019 14:43:39 +0100 Subject: [PATCH 064/107] Hash backup codes in the database using `password_hash` --- .../Controller/ConfigController.php | 21 ++++++++++------ .../themes/baggy/Config/otp_app.html.twig | 2 +- .../themes/material/Config/otp_app.html.twig | 2 +- src/Wallabag/UserBundle/Entity/User.php | 24 +++++++++++++++++-- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/Wallabag/CoreBundle/Controller/ConfigController.php b/src/Wallabag/CoreBundle/Controller/ConfigController.php index ed92c999a..9257ab18d 100644 --- a/src/Wallabag/CoreBundle/Controller/ConfigController.php +++ b/src/Wallabag/CoreBundle/Controller/ConfigController.php @@ -197,18 +197,25 @@ class ConfigController extends Controller } $user = $this->getUser(); + $secret = $this->get('scheb_two_factor.security.google_authenticator')->generateSecret(); - if (!$user->isGoogleTwoFactor()) { - $secret = $this->get('scheb_two_factor.security.google_authenticator')->generateSecret(); + $user->setGoogleAuthenticatorSecret($secret); + $user->setEmailTwoFactor(false); - $user->setGoogleAuthenticatorSecret($secret); - $user->setEmailTwoFactor(false); - $user->setBackupCodes((new BackupCodes())->toArray()); + $backupCodes = (new BackupCodes())->toArray(); + $backupCodesHashed = array_map( + function ($backupCode) { + return password_hash($backupCode, PASSWORD_DEFAULT); + }, + $backupCodes + ); - $this->container->get('fos_user.user_manager')->updateUser($user, true); - } + $user->setBackupCodes($backupCodesHashed); + + $this->container->get('fos_user.user_manager')->updateUser($user, true); return $this->render('WallabagCoreBundle:Config:otp_app.html.twig', [ + 'backupCodes' => $backupCodes, 'qr_code' => $this->get('scheb_two_factor.security.google_authenticator')->getQRContent($user), ]); } diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/otp_app.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/otp_app.html.twig index 2e4442e36..0919646ec 100644 --- a/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/otp_app.html.twig +++ b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/otp_app.html.twig @@ -20,7 +20,7 @@
  • {{ 'config.otp.app.two_factor_code_description_3'|trans }}

    -

    {{ app.user.getBackupCodes|join("\n")|nl2br }}

    +

    {{ backupCodes|join("\n")|nl2br }}

  • {{ 'config.otp.app.two_factor_code_description_4'|trans }}

    diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/otp_app.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/otp_app.html.twig index 6aef355eb..7875d7877 100644 --- a/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/otp_app.html.twig +++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/otp_app.html.twig @@ -24,7 +24,7 @@
  • {{ 'config.otp.app.two_factor_code_description_3'|trans }}

    -

    {{ app.user.getBackupCodes|join("\n")|nl2br }}

    +

    {{ backupCodes|join("\n")|nl2br }}

  • {{ 'config.otp.app.two_factor_code_description_4'|trans }}

    diff --git a/src/Wallabag/UserBundle/Entity/User.php b/src/Wallabag/UserBundle/Entity/User.php index ab34e2bfc..43fa6a80f 100644 --- a/src/Wallabag/UserBundle/Entity/User.php +++ b/src/Wallabag/UserBundle/Entity/User.php @@ -339,7 +339,7 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI */ public function isBackupCode(string $code): bool { - return \in_array($code, $this->backupCodes, true); + return false === $this->findBackupCode($code) ? false : true; } /** @@ -347,7 +347,7 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI */ public function invalidateBackupCode(string $code): void { - $key = array_search($code, $this->backupCodes, true); + $key = $this->findBackupCode($code); if (false !== $key) { unset($this->backupCodes[$key]); @@ -385,4 +385,24 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI return $this->clients->first(); } } + + /** + * Try to find a backup code from the list of backup codes of the current user. + * + * @param string $code Given code from the user + * + * @return string|false + */ + private function findBackupCode(string $code) + { + foreach ($this->backupCodes as $key => $backupCode) { + // backup code are hashed using `password_hash` + // see ConfigController->otpAppAction + if (password_verify($code, $backupCode)) { + return $key; + } + } + + return false; + } } From baa5ee2d4292460425ca7f6572425f1fb1100d9d Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Fri, 8 Feb 2019 15:03:52 +0100 Subject: [PATCH 065/107] Force default_protocol to generate an url input --- src/Wallabag/ApiBundle/Form/Type/ClientType.php | 1 + src/Wallabag/CoreBundle/Form/Type/EditEntryType.php | 2 ++ src/Wallabag/CoreBundle/Form/Type/NewEntryType.php | 1 + 3 files changed, 4 insertions(+) diff --git a/src/Wallabag/ApiBundle/Form/Type/ClientType.php b/src/Wallabag/ApiBundle/Form/Type/ClientType.php index fc22538f4..14dc5c44f 100644 --- a/src/Wallabag/ApiBundle/Form/Type/ClientType.php +++ b/src/Wallabag/ApiBundle/Form/Type/ClientType.php @@ -20,6 +20,7 @@ class ClientType extends AbstractType 'required' => false, 'label' => 'developer.client.form.redirect_uris_label', 'property_path' => 'redirectUris', + 'default_protocol' => null, ]) ->add('save', SubmitType::class, ['label' => 'developer.client.form.save_label']) ; diff --git a/src/Wallabag/CoreBundle/Form/Type/EditEntryType.php b/src/Wallabag/CoreBundle/Form/Type/EditEntryType.php index 083559286..2fc4c204b 100644 --- a/src/Wallabag/CoreBundle/Form/Type/EditEntryType.php +++ b/src/Wallabag/CoreBundle/Form/Type/EditEntryType.php @@ -22,11 +22,13 @@ class EditEntryType extends AbstractType 'disabled' => true, 'required' => false, 'label' => 'entry.edit.url_label', + 'default_protocol' => null, ]) ->add('origin_url', UrlType::class, [ 'required' => false, 'property_path' => 'originUrl', 'label' => 'entry.edit.origin_url_label', + 'default_protocol' => null, ]) ->add('save', SubmitType::class, [ 'label' => 'entry.edit.save_label', diff --git a/src/Wallabag/CoreBundle/Form/Type/NewEntryType.php b/src/Wallabag/CoreBundle/Form/Type/NewEntryType.php index 7d74fee34..7af1e5895 100644 --- a/src/Wallabag/CoreBundle/Form/Type/NewEntryType.php +++ b/src/Wallabag/CoreBundle/Form/Type/NewEntryType.php @@ -15,6 +15,7 @@ class NewEntryType extends AbstractType ->add('url', UrlType::class, [ 'required' => true, 'label' => 'entry.new.form_new.url_label', + 'default_protocol' => null, ]) ; } From 3784688a88230d9c3aec4ca518be52ea1c70aeb9 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Sat, 12 Jan 2019 13:09:36 +0100 Subject: [PATCH 066/107] Replace continue; with break; to avoid PHP 7.3 warnings Signed-off-by: Thomas Citharel --- .../GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php | 2 +- src/Wallabag/CoreBundle/Helper/DownloadImages.php | 2 +- src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php | 2 +- src/Wallabag/CoreBundle/Helper/TagsAssigner.php | 2 +- src/Wallabag/ImportBundle/Import/AbstractImport.php | 2 +- src/Wallabag/ImportBundle/Import/BrowserImport.php | 6 +++--- src/Wallabag/ImportBundle/Import/InstapaperImport.php | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php b/src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php index 90e00c62d..2e57aac8a 100644 --- a/src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php +++ b/src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php @@ -114,7 +114,7 @@ class GrabySiteConfigBuilder implements SiteConfigBuilder $extraFields = []; foreach ($extraFieldsStrings as $extraField) { if (false === strpos($extraField, '=')) { - continue; + break; } list($fieldName, $fieldValue) = explode('=', $extraField, 2); diff --git a/src/Wallabag/CoreBundle/Helper/DownloadImages.php b/src/Wallabag/CoreBundle/Helper/DownloadImages.php index cc3dcfceb..8c1c208f5 100644 --- a/src/Wallabag/CoreBundle/Helper/DownloadImages.php +++ b/src/Wallabag/CoreBundle/Helper/DownloadImages.php @@ -56,7 +56,7 @@ class DownloadImages $imagePath = $this->processSingleImage($entryId, $image, $url, $relativePath); if (false === $imagePath) { - continue; + break; } // if image contains "&" and we can't find it in the html it might be because it's encoded as & diff --git a/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php b/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php index fbdf2ac7a..d156df84e 100644 --- a/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php +++ b/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php @@ -37,7 +37,7 @@ class RuleBasedTagger foreach ($rules as $rule) { if (!$this->rulerz->satisfies($entry, $rule->getRule())) { - continue; + break; } $this->logger->info('Matching rule.', [ diff --git a/src/Wallabag/CoreBundle/Helper/TagsAssigner.php b/src/Wallabag/CoreBundle/Helper/TagsAssigner.php index e6b4989f8..519150f53 100644 --- a/src/Wallabag/CoreBundle/Helper/TagsAssigner.php +++ b/src/Wallabag/CoreBundle/Helper/TagsAssigner.php @@ -49,7 +49,7 @@ class TagsAssigner // avoid empty tag if (0 === \strlen($label)) { - continue; + break; } if (isset($tagsNotYetFlushed[$label])) { diff --git a/src/Wallabag/ImportBundle/Import/AbstractImport.php b/src/Wallabag/ImportBundle/Import/AbstractImport.php index d39d71b6c..5ae4aa8d6 100644 --- a/src/Wallabag/ImportBundle/Import/AbstractImport.php +++ b/src/Wallabag/ImportBundle/Import/AbstractImport.php @@ -169,7 +169,7 @@ abstract class AbstractImport implements ImportInterface $entry = $this->parseEntry($importedEntry); if (null === $entry) { - continue; + break; } // store each entry to be flushed so we can trigger the entry.saved event for each of them diff --git a/src/Wallabag/ImportBundle/Import/BrowserImport.php b/src/Wallabag/ImportBundle/Import/BrowserImport.php index 804bc6cd0..178ebe8e5 100644 --- a/src/Wallabag/ImportBundle/Import/BrowserImport.php +++ b/src/Wallabag/ImportBundle/Import/BrowserImport.php @@ -158,13 +158,13 @@ abstract class BrowserImport extends AbstractImport foreach ($entries as $importedEntry) { if ((array) $importedEntry !== $importedEntry) { - continue; + break; } $entry = $this->parseEntry($importedEntry); if (null === $entry) { - continue; + break; } // @see AbstractImport @@ -206,7 +206,7 @@ abstract class BrowserImport extends AbstractImport { foreach ($entries as $importedEntry) { if ((array) $importedEntry !== $importedEntry) { - continue; + break; } // set userId for the producer (it won't know which user is connected) diff --git a/src/Wallabag/ImportBundle/Import/InstapaperImport.php b/src/Wallabag/ImportBundle/Import/InstapaperImport.php index 439c978c7..6b6b35afa 100644 --- a/src/Wallabag/ImportBundle/Import/InstapaperImport.php +++ b/src/Wallabag/ImportBundle/Import/InstapaperImport.php @@ -65,7 +65,7 @@ class InstapaperImport extends AbstractImport $handle = fopen($this->filepath, 'r'); while (false !== ($data = fgetcsv($handle, 10240))) { if ('URL' === $data[0]) { - continue; + break; } // last element in the csv is the folder where the content belong From ea925bb112ab99efbb29d8e7113e80357a70bd18 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Wed, 27 Feb 2019 14:33:26 +0100 Subject: [PATCH 067/107] CS --- src/Wallabag/CoreBundle/Form/Type/EntryFilterType.php | 2 +- src/Wallabag/ImportBundle/Import/BrowserImport.php | 10 +++++----- src/Wallabag/ImportBundle/Import/ChromeImport.php | 2 +- src/Wallabag/ImportBundle/Import/FirefoxImport.php | 2 +- src/Wallabag/ImportBundle/Import/WallabagImport.php | 2 +- src/Wallabag/ImportBundle/Import/WallabagV1Import.php | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Wallabag/CoreBundle/Form/Type/EntryFilterType.php b/src/Wallabag/CoreBundle/Form/Type/EntryFilterType.php index 702c7f7aa..37d0640a6 100644 --- a/src/Wallabag/CoreBundle/Form/Type/EntryFilterType.php +++ b/src/Wallabag/CoreBundle/Form/Type/EntryFilterType.php @@ -108,7 +108,7 @@ class EntryFilterType extends AbstractType ->add('httpStatus', TextFilterType::class, [ 'apply_filter' => function (QueryInterface $filterQuery, $field, $values) { $value = $values['value']; - if (false === array_key_exists($value, Response::$statusTexts)) { + if (false === \array_key_exists($value, Response::$statusTexts)) { return; } diff --git a/src/Wallabag/ImportBundle/Import/BrowserImport.php b/src/Wallabag/ImportBundle/Import/BrowserImport.php index 178ebe8e5..99717beb8 100644 --- a/src/Wallabag/ImportBundle/Import/BrowserImport.php +++ b/src/Wallabag/ImportBundle/Import/BrowserImport.php @@ -77,7 +77,7 @@ abstract class BrowserImport extends AbstractImport */ public function parseEntry(array $importedEntry) { - if ((!array_key_exists('guid', $importedEntry) || (!array_key_exists('id', $importedEntry))) && \is_array(reset($importedEntry))) { + if ((!\array_key_exists('guid', $importedEntry) || (!\array_key_exists('id', $importedEntry))) && \is_array(reset($importedEntry))) { if ($this->producer) { $this->parseEntriesForProducer($importedEntry); @@ -89,7 +89,7 @@ abstract class BrowserImport extends AbstractImport return; } - if (array_key_exists('children', $importedEntry)) { + if (\array_key_exists('children', $importedEntry)) { if ($this->producer) { $this->parseEntriesForProducer($importedEntry['children']); @@ -101,11 +101,11 @@ abstract class BrowserImport extends AbstractImport return; } - if (!array_key_exists('uri', $importedEntry) && !array_key_exists('url', $importedEntry)) { + if (!\array_key_exists('uri', $importedEntry) && !\array_key_exists('url', $importedEntry)) { return; } - $url = array_key_exists('uri', $importedEntry) ? $importedEntry['uri'] : $importedEntry['url']; + $url = \array_key_exists('uri', $importedEntry) ? $importedEntry['uri'] : $importedEntry['url']; $existingEntry = $this->em ->getRepository('WallabagCoreBundle:Entry') @@ -126,7 +126,7 @@ abstract class BrowserImport extends AbstractImport // update entry with content (in case fetching failed, the given entry will be return) $this->fetchContent($entry, $data['url'], $data); - if (array_key_exists('tags', $data)) { + if (\array_key_exists('tags', $data)) { $this->tagsAssigner->assignTagsToEntry( $entry, $data['tags'] diff --git a/src/Wallabag/ImportBundle/Import/ChromeImport.php b/src/Wallabag/ImportBundle/Import/ChromeImport.php index eccee6986..4ae82ade8 100644 --- a/src/Wallabag/ImportBundle/Import/ChromeImport.php +++ b/src/Wallabag/ImportBundle/Import/ChromeImport.php @@ -57,7 +57,7 @@ class ChromeImport extends BrowserImport 'created_at' => substr($entry['date_added'], 0, 10), ]; - if (array_key_exists('tags', $entry) && '' !== $entry['tags']) { + if (\array_key_exists('tags', $entry) && '' !== $entry['tags']) { $data['tags'] = $entry['tags']; } diff --git a/src/Wallabag/ImportBundle/Import/FirefoxImport.php b/src/Wallabag/ImportBundle/Import/FirefoxImport.php index 8999e3f39..b3558f21e 100644 --- a/src/Wallabag/ImportBundle/Import/FirefoxImport.php +++ b/src/Wallabag/ImportBundle/Import/FirefoxImport.php @@ -57,7 +57,7 @@ class FirefoxImport extends BrowserImport 'created_at' => substr($entry['dateAdded'], 0, 10), ]; - if (array_key_exists('tags', $entry) && '' !== $entry['tags']) { + if (\array_key_exists('tags', $entry) && '' !== $entry['tags']) { $data['tags'] = $entry['tags']; } diff --git a/src/Wallabag/ImportBundle/Import/WallabagImport.php b/src/Wallabag/ImportBundle/Import/WallabagImport.php index c3a142b91..75a28fbf5 100644 --- a/src/Wallabag/ImportBundle/Import/WallabagImport.php +++ b/src/Wallabag/ImportBundle/Import/WallabagImport.php @@ -122,7 +122,7 @@ abstract class WallabagImport extends AbstractImport // update entry with content (in case fetching failed, the given entry will be return) $this->fetchContent($entry, $data['url'], $data); - if (array_key_exists('tags', $data)) { + if (\array_key_exists('tags', $data)) { $this->tagsAssigner->assignTagsToEntry( $entry, $data['tags'], diff --git a/src/Wallabag/ImportBundle/Import/WallabagV1Import.php b/src/Wallabag/ImportBundle/Import/WallabagV1Import.php index b9bb525ab..e05626117 100644 --- a/src/Wallabag/ImportBundle/Import/WallabagV1Import.php +++ b/src/Wallabag/ImportBundle/Import/WallabagV1Import.php @@ -61,7 +61,7 @@ class WallabagV1Import extends WallabagImport $data['html'] = $this->fetchingErrorMessage; } - if (array_key_exists('tags', $entry) && '' !== $entry['tags']) { + if (\array_key_exists('tags', $entry) && '' !== $entry['tags']) { $data['tags'] = $entry['tags']; } From 8c0ba953070dca22e9a06999cfe355ea01847c64 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Wed, 27 Feb 2019 14:59:50 +0100 Subject: [PATCH 068/107] Adding more tests --- .../GrabySiteConfigBuilder.php | 2 +- .../CoreBundle/Helper/DownloadImages.php | 2 +- .../CoreBundle/Helper/RuleBasedTagger.php | 2 +- .../CoreBundle/Helper/TagsAssigner.php | 2 +- .../ImportBundle/Import/AbstractImport.php | 2 +- .../ImportBundle/Import/BrowserImport.php | 6 +- .../ImportBundle/Import/InstapaperImport.php | 2 +- .../GrabySiteConfigBuilderTest.php | 68 +++++++++++++++++++ 8 files changed, 77 insertions(+), 9 deletions(-) diff --git a/src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php b/src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php index 2e57aac8a..90e00c62d 100644 --- a/src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php +++ b/src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php @@ -114,7 +114,7 @@ class GrabySiteConfigBuilder implements SiteConfigBuilder $extraFields = []; foreach ($extraFieldsStrings as $extraField) { if (false === strpos($extraField, '=')) { - break; + continue; } list($fieldName, $fieldValue) = explode('=', $extraField, 2); diff --git a/src/Wallabag/CoreBundle/Helper/DownloadImages.php b/src/Wallabag/CoreBundle/Helper/DownloadImages.php index 8c1c208f5..cc3dcfceb 100644 --- a/src/Wallabag/CoreBundle/Helper/DownloadImages.php +++ b/src/Wallabag/CoreBundle/Helper/DownloadImages.php @@ -56,7 +56,7 @@ class DownloadImages $imagePath = $this->processSingleImage($entryId, $image, $url, $relativePath); if (false === $imagePath) { - break; + continue; } // if image contains "&" and we can't find it in the html it might be because it's encoded as & diff --git a/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php b/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php index d156df84e..fbdf2ac7a 100644 --- a/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php +++ b/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php @@ -37,7 +37,7 @@ class RuleBasedTagger foreach ($rules as $rule) { if (!$this->rulerz->satisfies($entry, $rule->getRule())) { - break; + continue; } $this->logger->info('Matching rule.', [ diff --git a/src/Wallabag/CoreBundle/Helper/TagsAssigner.php b/src/Wallabag/CoreBundle/Helper/TagsAssigner.php index 519150f53..e6b4989f8 100644 --- a/src/Wallabag/CoreBundle/Helper/TagsAssigner.php +++ b/src/Wallabag/CoreBundle/Helper/TagsAssigner.php @@ -49,7 +49,7 @@ class TagsAssigner // avoid empty tag if (0 === \strlen($label)) { - break; + continue; } if (isset($tagsNotYetFlushed[$label])) { diff --git a/src/Wallabag/ImportBundle/Import/AbstractImport.php b/src/Wallabag/ImportBundle/Import/AbstractImport.php index 5ae4aa8d6..d39d71b6c 100644 --- a/src/Wallabag/ImportBundle/Import/AbstractImport.php +++ b/src/Wallabag/ImportBundle/Import/AbstractImport.php @@ -169,7 +169,7 @@ abstract class AbstractImport implements ImportInterface $entry = $this->parseEntry($importedEntry); if (null === $entry) { - break; + continue; } // store each entry to be flushed so we can trigger the entry.saved event for each of them diff --git a/src/Wallabag/ImportBundle/Import/BrowserImport.php b/src/Wallabag/ImportBundle/Import/BrowserImport.php index 99717beb8..3987e80f0 100644 --- a/src/Wallabag/ImportBundle/Import/BrowserImport.php +++ b/src/Wallabag/ImportBundle/Import/BrowserImport.php @@ -158,13 +158,13 @@ abstract class BrowserImport extends AbstractImport foreach ($entries as $importedEntry) { if ((array) $importedEntry !== $importedEntry) { - break; + continue; } $entry = $this->parseEntry($importedEntry); if (null === $entry) { - break; + continue; } // @see AbstractImport @@ -206,7 +206,7 @@ abstract class BrowserImport extends AbstractImport { foreach ($entries as $importedEntry) { if ((array) $importedEntry !== $importedEntry) { - break; + continue; } // set userId for the producer (it won't know which user is connected) diff --git a/src/Wallabag/ImportBundle/Import/InstapaperImport.php b/src/Wallabag/ImportBundle/Import/InstapaperImport.php index 6b6b35afa..439c978c7 100644 --- a/src/Wallabag/ImportBundle/Import/InstapaperImport.php +++ b/src/Wallabag/ImportBundle/Import/InstapaperImport.php @@ -65,7 +65,7 @@ class InstapaperImport extends AbstractImport $handle = fopen($this->filepath, 'r'); while (false !== ($data = fgetcsv($handle, 10240))) { if ('URL' === $data[0]) { - break; + continue; } // last element in the csv is the folder where the content belong diff --git a/tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php b/tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php index 1173fc3de..7beccd309 100644 --- a/tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php +++ b/tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php @@ -134,4 +134,72 @@ class GrabySiteConfigBuilderTest extends TestCase $this->assertCount(1, $records, 'One log was recorded'); } + + public function testBuildConfigWithBadExtraFields() + { + /* @var \Graby\SiteConfig\ConfigBuilder|\PHPUnit_Framework_MockObject_MockObject */ + $grabyConfigBuilderMock = $this->getMockBuilder('Graby\SiteConfig\ConfigBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $grabySiteConfig = new GrabySiteConfig(); + $grabySiteConfig->requires_login = true; + $grabySiteConfig->login_uri = 'http://www.example.com/login'; + $grabySiteConfig->login_username_field = 'login'; + $grabySiteConfig->login_password_field = 'password'; + $grabySiteConfig->login_extra_fields = ['field']; + $grabySiteConfig->not_logged_in_xpath = '//div[@class="need-login"]'; + + $grabyConfigBuilderMock + ->method('buildForHost') + ->with('example.com') + ->will($this->returnValue($grabySiteConfig)); + + $logger = new Logger('foo'); + $handler = new TestHandler(); + $logger->pushHandler($handler); + + $siteCrentialRepo = $this->getMockBuilder('Wallabag\CoreBundle\Repository\SiteCredentialRepository') + ->disableOriginalConstructor() + ->getMock(); + $siteCrentialRepo->expects($this->once()) + ->method('findOneByHostAndUser') + ->with('example.com', 1) + ->willReturn(['username' => 'foo', 'password' => 'bar']); + + $user = $this->getMockBuilder('Wallabag\UserBundle\Entity\User') + ->disableOriginalConstructor() + ->getMock(); + $user->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $token = new UsernamePasswordToken($user, 'pass', 'provider'); + + $tokenStorage = new TokenStorage(); + $tokenStorage->setToken($token); + + $this->builder = new GrabySiteConfigBuilder( + $grabyConfigBuilderMock, + $tokenStorage, + $siteCrentialRepo, + $logger + ); + + $config = $this->builder->buildForHost('www.example.com'); + + $this->assertSame('example.com', $config->getHost()); + $this->assertTrue($config->requiresLogin()); + $this->assertSame('http://www.example.com/login', $config->getLoginUri()); + $this->assertSame('login', $config->getUsernameField()); + $this->assertSame('password', $config->getPasswordField()); + $this->assertSame([], $config->getExtraFields()); + $this->assertSame('//div[@class="need-login"]', $config->getNotLoggedInXpath()); + $this->assertSame('foo', $config->getUsername()); + $this->assertSame('bar', $config->getPassword()); + + $records = $handler->getRecords(); + + $this->assertCount(1, $records, 'One log was recorded'); + } } From c2efb5a3069b59341667626beb0fd6223a98189d Mon Sep 17 00:00:00 2001 From: Nadrieril Date: Tue, 29 Jan 2019 14:24:38 +0100 Subject: [PATCH 069/107] Add missing entries in craue_config_setting. Should fix https://github.com/wallabag/wallabag/issues/3662 --- .../Version20190129120000.php | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 app/DoctrineMigrations/Version20190129120000.php diff --git a/app/DoctrineMigrations/Version20190129120000.php b/app/DoctrineMigrations/Version20190129120000.php new file mode 100644 index 000000000..61e0ef4cb --- /dev/null +++ b/app/DoctrineMigrations/Version20190129120000.php @@ -0,0 +1,74 @@ + "carrot", "value" => "1", "section" => "entry"), + array("name" => "share_diaspora", "value" => "1", "section" => "entry"), + array("name" => "diaspora_url", "value" => "http://diasporapod.com", "section" => "entry"), + array("name" => "share_shaarli", "value" => "1", "section" => "entry"), + array("name" => "shaarli_url", "value" => "http://myshaarli.com", "section" => "entry"), + array("name" => "share_mail", "value" => "1", "section" => "entry"), + array("name" => "share_twitter", "value" => "1", "section" => "entry"), + array("name" => "show_printlink", "value" => "1", "section" => "entry"), + array("name" => "export_epub", "value" => "1", "section" => "export"), + array("name" => "export_mobi", "value" => "1", "section" => "export"), + array("name" => "export_pdf", "value" => "1", "section" => "export"), + array("name" => "export_csv", "value" => "1", "section" => "export"), + array("name" => "export_json", "value" => "1", "section" => "export"), + array("name" => "export_txt", "value" => "1", "section" => "export"), + array("name" => "export_xml", "value" => "1", "section" => "export"), + array("name" => "piwik_enabled", "value" => "0", "section" => "analytics"), + array("name" => "piwik_host", "value" => "v2.wallabag.org", "section" => "analytics"), + array("name" => "piwik_site_id", "value" => "1", "section" => "analytics"), + array("name" => "demo_mode_enabled", "value" => "0", "section" => "misc"), + array("name" => "demo_mode_username", "value" => "wallabag", "section" => "misc"), + array("name" => "wallabag_support_url", "value" => "https://www.wallabag.org/pages/support.html", "section" => "misc"), + ); + + /** + * @param Schema $schema + */ + public function up(Schema $schema) + { + $piwikEnabled = $this->container + ->get('doctrine.orm.default_entity_manager') + ->getConnection() + ->fetchArray('SELECT * FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'piwik_enabled'"); + + $this->skipIf(false !== $piwikEnabled, 'It seems that you already played this migration, or user the wallabag:install command.'); + + foreach ($this->settings as $setting) { + $this->addSql(" + INSERT INTO " . $this->getTable('craue_config_setting') . " + (name, value, section) + VALUES ( + '" . $setting['name'] . "', + '" . $setting['value'] . "', + '" . $setting['section'] . "' + ); + "); + } + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + foreach ($this->settings as $setting) { + $this->addSql(" + DELETE FROM " . $this->getTable('craue_config_setting') . " + WHERE name = '" . $setting['name'] . "'; + "); + } + } +} From fcd54e2447332a681f5b1fde7e737ea5a8975823 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Wed, 27 Feb 2019 13:39:17 +0100 Subject: [PATCH 070/107] Test each internal settings before creating them --- .../Version20190129120000.php | 162 +++++++++++++----- 1 file changed, 120 insertions(+), 42 deletions(-) diff --git a/app/DoctrineMigrations/Version20190129120000.php b/app/DoctrineMigrations/Version20190129120000.php index 61e0ef4cb..5f25d67d0 100644 --- a/app/DoctrineMigrations/Version20190129120000.php +++ b/app/DoctrineMigrations/Version20190129120000.php @@ -8,54 +8,132 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; /** * Add missing entries in craue_config_setting. */ -class Version20190129120000 extends WallabagMigration +final class Version20190129120000 extends WallabagMigration { - var $settings = array( - array("name" => "carrot", "value" => "1", "section" => "entry"), - array("name" => "share_diaspora", "value" => "1", "section" => "entry"), - array("name" => "diaspora_url", "value" => "http://diasporapod.com", "section" => "entry"), - array("name" => "share_shaarli", "value" => "1", "section" => "entry"), - array("name" => "shaarli_url", "value" => "http://myshaarli.com", "section" => "entry"), - array("name" => "share_mail", "value" => "1", "section" => "entry"), - array("name" => "share_twitter", "value" => "1", "section" => "entry"), - array("name" => "show_printlink", "value" => "1", "section" => "entry"), - array("name" => "export_epub", "value" => "1", "section" => "export"), - array("name" => "export_mobi", "value" => "1", "section" => "export"), - array("name" => "export_pdf", "value" => "1", "section" => "export"), - array("name" => "export_csv", "value" => "1", "section" => "export"), - array("name" => "export_json", "value" => "1", "section" => "export"), - array("name" => "export_txt", "value" => "1", "section" => "export"), - array("name" => "export_xml", "value" => "1", "section" => "export"), - array("name" => "piwik_enabled", "value" => "0", "section" => "analytics"), - array("name" => "piwik_host", "value" => "v2.wallabag.org", "section" => "analytics"), - array("name" => "piwik_site_id", "value" => "1", "section" => "analytics"), - array("name" => "demo_mode_enabled", "value" => "0", "section" => "misc"), - array("name" => "demo_mode_username", "value" => "wallabag", "section" => "misc"), - array("name" => "wallabag_support_url", "value" => "https://www.wallabag.org/pages/support.html", "section" => "misc"), - ); + private $settings = [ + [ + 'name' => 'carrot', + 'value' => '1', + 'section' => 'entry', + ], + [ + 'name' => 'share_diaspora', + 'value' => '1', + 'section' => 'entry', + ], + [ + 'name' => 'diaspora_url', + 'value' => 'http://diasporapod.com', + 'section' => 'entry', + ], + [ + 'name' => 'share_shaarli', + 'value' => '1', + 'section' => 'entry', + ], + [ + 'name' => 'shaarli_url', + 'value' => 'http://myshaarli.com', + 'section' => 'entry', + ], + [ + 'name' => 'share_mail', + 'value' => '1', + 'section' => 'entry', + ], + [ + 'name' => 'share_twitter', + 'value' => '1', + 'section' => 'entry', + ], + [ + 'name' => 'show_printlink', + 'value' => '1', + 'section' => 'entry', + ], + [ + 'name' => 'export_epub', + 'value' => '1', + 'section' => 'export', + ], + [ + 'name' => 'export_mobi', + 'value' => '1', + 'section' => 'export', + ], + [ + 'name' => 'export_pdf', + 'value' => '1', + 'section' => 'export', + ], + [ + 'name' => 'export_csv', + 'value' => '1', + 'section' => 'export', + ], + [ + 'name' => 'export_json', + 'value' => '1', + 'section' => 'export', + ], + [ + 'name' => 'export_txt', + 'value' => '1', + 'section' => 'export', + ], + [ + 'name' => 'export_xml', + 'value' => '1', + 'section' => 'export', + ], + [ + 'name' => 'piwik_enabled', + 'value' => '0', + 'section' => 'analytics', + ], + [ + 'name' => 'piwik_host', + 'value' => 'v2.wallabag.org', + 'section' => 'analytics', + ], + [ + 'name' => 'piwik_site_id', + 'value' => '1', + 'section' => 'analytics', + ], + [ + 'name' => 'demo_mode_enabled', + 'value' => '0', + 'section' => 'misc', + ], + [ + 'name' => 'demo_mode_username', + 'value' => 'wallabag', + 'section' => 'misc', + ], + [ + 'name' => 'wallabag_support_url', + 'value' => 'https://www.wallabag.org/pages/support.html', + 'section' => 'misc', + ], + ]; /** * @param Schema $schema */ public function up(Schema $schema) { - $piwikEnabled = $this->container - ->get('doctrine.orm.default_entity_manager') - ->getConnection() - ->fetchArray('SELECT * FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'piwik_enabled'"); - - $this->skipIf(false !== $piwikEnabled, 'It seems that you already played this migration, or user the wallabag:install command.'); - foreach ($this->settings as $setting) { - $this->addSql(" - INSERT INTO " . $this->getTable('craue_config_setting') . " - (name, value, section) - VALUES ( - '" . $setting['name'] . "', - '" . $setting['value'] . "', - '" . $setting['section'] . "' - ); - "); + $settingEnabled = $this->container + ->get('doctrine.orm.default_entity_manager') + ->getConnection() + ->fetchArray('SELECT * FROM ' . $this->getTable('craue_config_setting') . " WHERE name = '" . $setting['name'] . "'"); + + if (false !== $settingEnabled) { + continue; + } + + $this->addSql('INSERT INTO ' . $this->getTable('craue_config_setting') . " (name, value, section) VALUES ('" . $setting['name'] . "', '" . $setting['value'] . "', '" . $setting['section'] . "');"); } } @@ -65,8 +143,8 @@ class Version20190129120000 extends WallabagMigration public function down(Schema $schema) { foreach ($this->settings as $setting) { - $this->addSql(" - DELETE FROM " . $this->getTable('craue_config_setting') . " + $this->addSql(' + DELETE FROM ' . $this->getTable('craue_config_setting') . " WHERE name = '" . $setting['name'] . "'; "); } From 85403dae0463660f86de68970dcd1a5cbe784a31 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Fri, 1 Mar 2019 20:26:51 +0100 Subject: [PATCH 071/107] Disable down for that migration --- app/DoctrineMigrations/Version20190129120000.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/DoctrineMigrations/Version20190129120000.php b/app/DoctrineMigrations/Version20190129120000.php index 5f25d67d0..3632e762d 100644 --- a/app/DoctrineMigrations/Version20190129120000.php +++ b/app/DoctrineMigrations/Version20190129120000.php @@ -142,11 +142,6 @@ final class Version20190129120000 extends WallabagMigration */ public function down(Schema $schema) { - foreach ($this->settings as $setting) { - $this->addSql(' - DELETE FROM ' . $this->getTable('craue_config_setting') . " - WHERE name = '" . $setting['name'] . "'; - "); - } + $this->skipIf(true, 'These settings are required and should not be removed.'); } } From bfe02a0b481055bb4e799200c8daa9a0ad987c71 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Sun, 28 May 2017 14:53:04 +0200 Subject: [PATCH 072/107] Hash the urls to check if they exist Signed-off-by: Thomas Citharel --- .../Controller/EntryRestController.php | 36 +++++-- .../Command/GenerateUrlHashesCommand.php | 95 ++++++++++++++++ .../CoreBundle/DataFixtures/EntryFixtures.php | 1 + src/Wallabag/CoreBundle/Entity/Entry.php | 30 +++++- .../CoreBundle/Helper/ContentProxy.php | 2 + .../Controller/EntryRestControllerTest.php | 55 +++++++++- .../Command/GenerateUrlHashesCommandTest.php | 101 ++++++++++++++++++ 7 files changed, 306 insertions(+), 14 deletions(-) create mode 100644 src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php create mode 100644 tests/Wallabag/CoreBundle/Command/GenerateUrlHashesCommandTest.php diff --git a/src/Wallabag/ApiBundle/Controller/EntryRestController.php b/src/Wallabag/ApiBundle/Controller/EntryRestController.php index 5c8500917..26746f7d8 100644 --- a/src/Wallabag/ApiBundle/Controller/EntryRestController.php +++ b/src/Wallabag/ApiBundle/Controller/EntryRestController.php @@ -29,6 +29,8 @@ class EntryRestController extends WallabagRestController * {"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"}, * {"name"="url", "dataType"="string", "required"=true, "format"="An url", "description"="Url to check if it exists"}, * {"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"} + * {"name"="hashedurl", "dataType"="string", "required"=true, "format"="An url", "description"="Md5 url to check if it exists"}, + * {"name"="hashedurls", "dataType"="string", "required"=false, "format"="An array of urls (?urls[]=http...&urls[]=http...)", "description"="Md5 urls (as an array) to check if it exists"} * } * ) * @@ -41,34 +43,46 @@ class EntryRestController extends WallabagRestController $returnId = (null === $request->query->get('return_id')) ? false : (bool) $request->query->get('return_id'); $urls = $request->query->get('urls', []); + $hashedUrls = $request->query->get('hashedurls', []); + // handle multiple urls first - if (!empty($urls)) { + if (!empty($hashedUrls)) { $results = []; - foreach ($urls as $url) { + foreach ($hashedUrls as $hashedUrl) { $res = $this->getDoctrine() ->getRepository('WallabagCoreBundle:Entry') - ->findByUrlAndUserId($url, $this->getUser()->getId()); + ->findOneBy([ + 'hashedUrl' => $hashedUrl, + 'user' => $this->getUser()->getId(), + ]); - $results[$url] = $this->returnExistInformation($res, $returnId); + // $results[$url] = $this->returnExistInformation($res, $returnId); + $results[$hashedUrl] = $this->returnExistInformation($res, $returnId); } return $this->sendResponse($results); } // let's see if it is a simple url? - $url = $request->query->get('url', ''); + $hashedUrl = $request->query->get('hashedurl', ''); - if (empty($url)) { - throw $this->createAccessDeniedException('URL is empty?, logged user id: ' . $this->getUser()->getId()); + // if (empty($url)) { + // throw $this->createAccessDeniedException('URL is empty?, logged user id: ' . $this->getUser()->getId()); + // } + + if (empty($hashedUrl)) { + throw $this->createAccessDeniedException('URL is empty?, logged user id: '.$this->getUser()->getId()); } $res = $this->getDoctrine() ->getRepository('WallabagCoreBundle:Entry') - ->findByUrlAndUserId($url, $this->getUser()->getId()); + // ->findByUrlAndUserId($url, $this->getUser()->getId()); + ->findOneBy([ + 'hashedUrl' => $hashedUrl, + 'user' => $this->getUser()->getId(), + ]); - $exists = $this->returnExistInformation($res, $returnId); - - return $this->sendResponse(['exists' => $exists]); + return $this->sendResponse(['exists' => $this->returnExistInformation($res, $returnId)]); } /** diff --git a/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php b/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php new file mode 100644 index 000000000..fe2644f28 --- /dev/null +++ b/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php @@ -0,0 +1,95 @@ +setName('wallabag:generate-hashed-urls') + ->setDescription('Generates hashed urls for each entry') + ->setHelp('This command helps you to generates hashes of the url of each entry, to check through API if an URL is already saved') + ->addArgument( + 'username', + InputArgument::OPTIONAL, + 'User to process entries' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->output = $output; + + $username = $input->getArgument('username'); + + if ($username) { + try { + $user = $this->getUser($username); + $this->generateHashedUrls($user); + } catch (NoResultException $e) { + $output->writeln(sprintf('User "%s" not found.', $username)); + + return 1; + } + } else { + $users = $this->getDoctrine()->getRepository('WallabagUserBundle:User')->findAll(); + + $output->writeln(sprintf('Generating hashed urls for the %d user account entries', count($users))); + + foreach ($users as $user) { + $output->writeln(sprintf('Processing user %s', $user->getUsername())); + $this->generateHashedUrls($user); + } + $output->writeln(sprintf('Finished generated hashed urls')); + } + + return 0; + } + + /** + * @param User $user + */ + private function generateHashedUrls(User $user) + { + $em = $this->getContainer()->get('doctrine.orm.entity_manager'); + $repo = $this->getDoctrine()->getRepository('WallabagCoreBundle:Entry'); + + $entries = $repo->findByUser($user->getId()); + + foreach ($entries as $entry) { + $entry->setHashedUrl(hash('sha512', $entry->getUrl())); + $em->persist($entry); + $em->flush(); + } + + $this->output->writeln(sprintf('Generated hashed urls for user %s', $user->getUserName())); + } + + /** + * Fetches a user from its username. + * + * @param string $username + * + * @return \Wallabag\UserBundle\Entity\User + */ + private function getUser($username) + { + return $this->getDoctrine()->getRepository('WallabagUserBundle:User')->findOneByUserName($username); + } + + private function getDoctrine() + { + return $this->getContainer()->get('doctrine'); + } +} diff --git a/src/Wallabag/CoreBundle/DataFixtures/EntryFixtures.php b/src/Wallabag/CoreBundle/DataFixtures/EntryFixtures.php index 024fcfdc8..9c10500d1 100644 --- a/src/Wallabag/CoreBundle/DataFixtures/EntryFixtures.php +++ b/src/Wallabag/CoreBundle/DataFixtures/EntryFixtures.php @@ -30,6 +30,7 @@ class EntryFixtures extends Fixture implements DependentFixtureInterface 'entry2' => [ 'user' => 'admin-user', 'url' => 'http://0.0.0.0/entry2', + 'hashed_url' => hash('md5', 'http://0.0.0.0/entry2'), 'reading_time' => 1, 'domain' => 'domain.io', 'mime' => 'text/html', diff --git a/src/Wallabag/CoreBundle/Entity/Entry.php b/src/Wallabag/CoreBundle/Entity/Entry.php index b3cfdc4a4..17a1ed58f 100644 --- a/src/Wallabag/CoreBundle/Entity/Entry.php +++ b/src/Wallabag/CoreBundle/Entity/Entry.php @@ -25,7 +25,8 @@ use Wallabag\UserBundle\Entity\User; * options={"collate"="utf8mb4_unicode_ci", "charset"="utf8mb4"}, * indexes={ * @ORM\Index(name="created_at", columns={"created_at"}), - * @ORM\Index(name="uid", columns={"uid"}) + * @ORM\Index(name="uid", columns={"uid"}), + * @ORM\Index(name="hashedurl", columns={"hashedurl"}) * } * ) * @ORM\HasLifecycleCallbacks() @@ -75,6 +76,13 @@ class Entry */ private $url; + /** + * @var string + * + * @ORM\Column(name="hashedurl", type="text", nullable=true) + */ + private $hashedUrl; + /** * @var bool * @@ -911,4 +919,24 @@ class Entry { return $this->originUrl; } + + /** + * @return string + */ + public function getHashedUrl() + { + return $this->hashedUrl; + } + + /** + * @param mixed $hashedUrl + * + * @return Entry + */ + public function setHashedUrl($hashedUrl) + { + $this->hashedUrl = $hashedUrl; + + return $this; + } } diff --git a/src/Wallabag/CoreBundle/Helper/ContentProxy.php b/src/Wallabag/CoreBundle/Helper/ContentProxy.php index 31953f12d..0534d27b2 100644 --- a/src/Wallabag/CoreBundle/Helper/ContentProxy.php +++ b/src/Wallabag/CoreBundle/Helper/ContentProxy.php @@ -248,6 +248,8 @@ class ContentProxy { $this->updateOriginUrl($entry, $content['url']); + $entry->setHashedUrl(hash('md5', $entry->getUrl())); + $this->setEntryDomainName($entry); if (!empty($content['title'])) { diff --git a/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php b/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php index 2151f587e..8d96d7b8c 100644 --- a/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php +++ b/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php @@ -987,6 +987,8 @@ class EntryRestControllerTest extends WallabagApiTestCase { $this->client->request('GET', '/api/entries/exists?url=http://0.0.0.0/entry2'); + $this->client->request('GET', '/api/entries/exists?hashedurl=' . hash('md5', 'http://0.0.0.0/entry2')); + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); $content = json_decode($this->client->getResponse()->getContent(), true); @@ -994,10 +996,22 @@ class EntryRestControllerTest extends WallabagApiTestCase $this->assertTrue($content['exists']); } + public function testGetEntriesExistsWithHash() + { + $this->client->request('GET', '/api/entries/exists?hashedurl=' . hash('md5', 'http://0.0.0.0/entry2')); + + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + + $content = json_decode($this->client->getResponse()->getContent(), true); + + $this->assertSame(2, $content['exists']); + } + public function testGetEntriesExistsWithManyUrls() { $url1 = 'http://0.0.0.0/entry2'; $url2 = 'http://0.0.0.0/entry10'; + $this->client->request('GET', '/api/entries/exists?urls[]=' . $url1 . '&urls[]=' . $url2 . '&return_id=1'); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); @@ -1027,9 +1041,46 @@ class EntryRestControllerTest extends WallabagApiTestCase $this->assertFalse($content[$url2]); } + public function testGetEntriesExistsWithManyUrlsHashed() + { + $url1 = 'http://0.0.0.0/entry2'; + $url2 = 'http://0.0.0.0/entry10'; + $this->client->request('GET', '/api/entries/exists?hashedurls[]='.hash('md5',$url1).'&hashedurls[]='.hash('md5',$url2) . '&return_id=1'); + + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + + $content = json_decode($this->client->getResponse()->getContent(), true); + + $this->assertArrayHasKey($url1, $content); + $this->assertArrayHasKey($url2, $content); + $this->assertSame(2, $content[$url1]); + $this->assertNull($content[$url2]); + + $this->assertArrayHasKey(hash('md5', $url1), $content); + $this->assertArrayHasKey(hash('md5', $url2), $content); + $this->assertEquals(2, $content[hash('md5', $url1)]); + $this->assertEquals(false, $content[hash('md5', $url2)]); + } + + public function testGetEntriesExistsWithManyUrlsHashedReturnBool() + { + $url1 = 'http://0.0.0.0/entry2'; + $url2 = 'http://0.0.0.0/entry10'; + $this->client->request('GET', '/api/entries/exists?hashedurls[]='.hash('md5',$url1).'&hashedurls[]='.hash('md5',$url2)); + + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + + $content = json_decode($this->client->getResponse()->getContent(), true); + + $this->assertArrayHasKey($url1, $content); + $this->assertArrayHasKey($url2, $content); + $this->assertTrue($content[$url1]); + $this->assertFalse($content[$url2]); + } + public function testGetEntriesExistsWhichDoesNotExists() { - $this->client->request('GET', '/api/entries/exists?url=http://google.com/entry2'); + $this->client->request('GET', '/api/entries/exists?hashedurl='.hash('md5','http://google.com/entry2')); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); @@ -1040,7 +1091,7 @@ class EntryRestControllerTest extends WallabagApiTestCase public function testGetEntriesExistsWithNoUrl() { - $this->client->request('GET', '/api/entries/exists?url='); + $this->client->request('GET', '/api/entries/exists?hashedurl='); $this->assertSame(403, $this->client->getResponse()->getStatusCode()); } diff --git a/tests/Wallabag/CoreBundle/Command/GenerateUrlHashesCommandTest.php b/tests/Wallabag/CoreBundle/Command/GenerateUrlHashesCommandTest.php new file mode 100644 index 000000000..8ca772cb9 --- /dev/null +++ b/tests/Wallabag/CoreBundle/Command/GenerateUrlHashesCommandTest.php @@ -0,0 +1,101 @@ +getClient()->getKernel()); + $application->add(new GenerateUrlHashesCommand()); + + $command = $application->find('wallabag:generate-hashed-urls'); + + $tester = new CommandTester($command); + $tester->execute([ + 'command' => $command->getName(), + ]); + + $this->assertContains('Generating hashed urls for the 3 user account entries', $tester->getDisplay()); + $this->assertContains('Finished generated hashed urls', $tester->getDisplay()); + } + + public function testRunGenerateUrlHashesCommandWithBadUsername() + { + $application = new Application($this->getClient()->getKernel()); + $application->add(new GenerateUrlHashesCommand()); + + $command = $application->find('wallabag:generate-hashed-urls'); + + $tester = new CommandTester($command); + $tester->execute([ + 'command' => $command->getName(), + 'username' => 'unknown', + ]); + + $this->assertContains('User "unknown" not found', $tester->getDisplay()); + } + + public function testRunGenerateUrlHashesCommandForUser() + { + $application = new Application($this->getClient()->getKernel()); + $application->add(new GenerateUrlHashesCommand()); + + $command = $application->find('wallabag:generate-hashed-urls'); + + $tester = new CommandTester($command); + $tester->execute([ + 'command' => $command->getName(), + 'username' => 'admin', + ]); + + $this->assertContains('Generated hashed urls for user admin', $tester->getDisplay()); + } + + public function testGenerateUrls() + { + $url = 'http://www.lemonde.fr/sport/visuel/2017/05/05/rondelle-prison-blanchissage-comprendre-le-hockey-sur-glace_5122587_3242.html'; + $client = $this->getClient(); + $em = $client->getContainer()->get('doctrine.orm.entity_manager'); + + $this->logInAs('admin'); + + $user = $em->getRepository('WallabagUserBundle:User')->findOneById($this->getLoggedInUserId()); + + $entry1 = new Entry($user); + $entry1->setUrl($url); + + $em->persist($entry1); + + $em->flush(); + + $this->assertNull($entry1->getHashedUrl()); + + $application = new Application($this->getClient()->getKernel()); + $application->add(new GenerateUrlHashesCommand()); + + $command = $application->find('wallabag:generate-hashed-urls'); + + $tester = new CommandTester($command); + $tester->execute([ + 'command' => $command->getName(), + 'username' => 'admin', + ]); + + $this->assertContains('Generated hashed urls for user admin', $tester->getDisplay()); + + $entry = $em->getRepository('WallabagCoreBundle:Entry')->findOneByUrl($url); + + $this->assertEquals($entry->getHashedUrl(), hash('sha512', $url)); + + $query = $em->createQuery('DELETE FROM Wallabag\CoreBundle\Entity\Entry e WHERE e.url = :url'); + $query->setParameter('url', $url); + $query->execute(); + } +} From 9c2b2aae70b06411336e6eb6ac43b3ebd30dc38c Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Mon, 1 Apr 2019 11:50:33 +0200 Subject: [PATCH 073/107] Keep url in exists endpoint - Add migration - Use md5 instead of sha512 (we don't need security here, just a hash) - Update tests --- .../Version20190401105353.php | 44 +++++++++++ .../Controller/EntryRestController.php | 57 +++++++------- .../Command/GenerateUrlHashesCommand.php | 19 +++-- .../CoreBundle/DataFixtures/EntryFixtures.php | 2 +- src/Wallabag/CoreBundle/Entity/Entry.php | 4 +- .../CoreBundle/Repository/EntryRepository.php | 24 ++++++ .../Controller/EntryRestControllerTest.php | 75 +++++++++---------- .../Command/GenerateUrlHashesCommandTest.php | 8 +- 8 files changed, 155 insertions(+), 78 deletions(-) create mode 100644 app/DoctrineMigrations/Version20190401105353.php diff --git a/app/DoctrineMigrations/Version20190401105353.php b/app/DoctrineMigrations/Version20190401105353.php new file mode 100644 index 000000000..4afc8b15d --- /dev/null +++ b/app/DoctrineMigrations/Version20190401105353.php @@ -0,0 +1,44 @@ +getTable($this->getTable('entry')); + + $this->skipIf($entryTable->hasColumn('hashed_url'), 'It seems that you already played this migration.'); + + $entryTable->addColumn('hashed_url', 'text', [ + 'length' => 32, + 'notnull' => false, + ]); + + // sqlite doesn't have the MD5 function by default + if ('sqlite' !== $this->connection->getDatabasePlatform()->getName()) { + $this->addSql('UPDATE ' . $this->getTable('entry') . ' SET hashed_url = MD5(url)'); + } + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + $entryTable = $schema->getTable($this->getTable('entry')); + + $this->skipIf(!$entryTable->hasColumn('hashed_url'), 'It seems that you already played this migration.'); + + $entryTable->dropColumn('hashed_url'); + } +} diff --git a/src/Wallabag/ApiBundle/Controller/EntryRestController.php b/src/Wallabag/ApiBundle/Controller/EntryRestController.php index 26746f7d8..0ecf1a0ea 100644 --- a/src/Wallabag/ApiBundle/Controller/EntryRestController.php +++ b/src/Wallabag/ApiBundle/Controller/EntryRestController.php @@ -27,10 +27,10 @@ class EntryRestController extends WallabagRestController * @ApiDoc( * parameters={ * {"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"}, - * {"name"="url", "dataType"="string", "required"=true, "format"="An url", "description"="Url to check if it exists"}, - * {"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"} - * {"name"="hashedurl", "dataType"="string", "required"=true, "format"="An url", "description"="Md5 url to check if it exists"}, - * {"name"="hashedurls", "dataType"="string", "required"=false, "format"="An array of urls (?urls[]=http...&urls[]=http...)", "description"="Md5 urls (as an array) to check if it exists"} + * {"name"="url", "dataType"="string", "required"=true, "format"="An url", "description"="DEPRECATED, use hashed_url instead"}, + * {"name"="urls", "dataType"="string", "required"=false, "format"="An array of urls (?urls[]=http...&urls[]=http...)", "description"="DEPRECATED, use hashed_urls instead"}, + * {"name"="hashed_url", "dataType"="string", "required"=true, "format"="An url", "description"="Md5 url to check if it exists"}, + * {"name"="hashed_urls", "dataType"="string", "required"=false, "format"="An array of urls (?urls[]=http...&urls[]=http...)", "description"="Md5 urls (as an array) to check if it exists"} * } * ) * @@ -39,22 +39,18 @@ class EntryRestController extends WallabagRestController public function getEntriesExistsAction(Request $request) { $this->validateAuthentication(); + $repo = $this->getDoctrine()->getRepository('WallabagCoreBundle:Entry'); $returnId = (null === $request->query->get('return_id')) ? false : (bool) $request->query->get('return_id'); - $urls = $request->query->get('urls', []); - $hashedUrls = $request->query->get('hashedurls', []); + $urls = $request->query->get('urls', []); + $hashedUrls = $request->query->get('hashed_urls', []); // handle multiple urls first if (!empty($hashedUrls)) { $results = []; foreach ($hashedUrls as $hashedUrl) { - $res = $this->getDoctrine() - ->getRepository('WallabagCoreBundle:Entry') - ->findOneBy([ - 'hashedUrl' => $hashedUrl, - 'user' => $this->getUser()->getId(), - ]); + $res = $repo->findByHashedUrlAndUserId($hashedUrl, $this->getUser()->getId()); // $results[$url] = $this->returnExistInformation($res, $returnId); $results[$hashedUrl] = $this->returnExistInformation($res, $returnId); @@ -63,24 +59,33 @@ class EntryRestController extends WallabagRestController return $this->sendResponse($results); } - // let's see if it is a simple url? - $hashedUrl = $request->query->get('hashedurl', ''); + // @deprecated, to be remove in 3.0 + if (!empty($urls)) { + $results = []; + foreach ($urls as $url) { + $res = $repo->findByUrlAndUserId($url, $this->getUser()->getId()); - // if (empty($url)) { - // throw $this->createAccessDeniedException('URL is empty?, logged user id: ' . $this->getUser()->getId()); - // } + $results[$url] = $this->returnExistInformation($res, $returnId); + } - if (empty($hashedUrl)) { - throw $this->createAccessDeniedException('URL is empty?, logged user id: '.$this->getUser()->getId()); + return $this->sendResponse($results); } - $res = $this->getDoctrine() - ->getRepository('WallabagCoreBundle:Entry') - // ->findByUrlAndUserId($url, $this->getUser()->getId()); - ->findOneBy([ - 'hashedUrl' => $hashedUrl, - 'user' => $this->getUser()->getId(), - ]); + // let's see if it is a simple url? + $url = $request->query->get('url', ''); + $hashedUrl = $request->query->get('hashed_url', ''); + + if (empty($url) && empty($hashedUrl)) { + throw $this->createAccessDeniedException('URL is empty?, logged user id: ' . $this->getUser()->getId()); + } + + $method = 'findByUrlAndUserId'; + if (!empty($hashedUrl)) { + $method = 'findByHashedUrlAndUserId'; + $url = $hashedUrl; + } + + $res = $repo->$method($url, $this->getUser()->getId()); return $this->sendResponse(['exists' => $this->returnExistInformation($res, $returnId)]); } diff --git a/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php b/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php index fe2644f28..fb5983902 100644 --- a/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php +++ b/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php @@ -45,13 +45,13 @@ class GenerateUrlHashesCommand extends ContainerAwareCommand } else { $users = $this->getDoctrine()->getRepository('WallabagUserBundle:User')->findAll(); - $output->writeln(sprintf('Generating hashed urls for the %d user account entries', count($users))); + $output->writeln(sprintf('Generating hashed urls for "%d" users', \count($users))); foreach ($users as $user) { - $output->writeln(sprintf('Processing user %s', $user->getUsername())); + $output->writeln(sprintf('Processing user: %s', $user->getUsername())); $this->generateHashedUrls($user); } - $output->writeln(sprintf('Finished generated hashed urls')); + $output->writeln('Finished generated hashed urls'); } return 0; @@ -67,13 +67,20 @@ class GenerateUrlHashesCommand extends ContainerAwareCommand $entries = $repo->findByUser($user->getId()); + $i = 1; foreach ($entries as $entry) { - $entry->setHashedUrl(hash('sha512', $entry->getUrl())); + $entry->setHashedUrl(hash('md5', $entry->getUrl())); $em->persist($entry); - $em->flush(); + + if (0 === ($i % 20)) { + $em->flush(); + } + ++$i; } - $this->output->writeln(sprintf('Generated hashed urls for user %s', $user->getUserName())); + $em->flush(); + + $this->output->writeln(sprintf('Generated hashed urls for user: %s', $user->getUserName())); } /** diff --git a/src/Wallabag/CoreBundle/DataFixtures/EntryFixtures.php b/src/Wallabag/CoreBundle/DataFixtures/EntryFixtures.php index 9c10500d1..1b18cad66 100644 --- a/src/Wallabag/CoreBundle/DataFixtures/EntryFixtures.php +++ b/src/Wallabag/CoreBundle/DataFixtures/EntryFixtures.php @@ -30,7 +30,6 @@ class EntryFixtures extends Fixture implements DependentFixtureInterface 'entry2' => [ 'user' => 'admin-user', 'url' => 'http://0.0.0.0/entry2', - 'hashed_url' => hash('md5', 'http://0.0.0.0/entry2'), 'reading_time' => 1, 'domain' => 'domain.io', 'mime' => 'text/html', @@ -90,6 +89,7 @@ class EntryFixtures extends Fixture implements DependentFixtureInterface foreach ($entries as $reference => $item) { $entry = new Entry($this->getReference($item['user'])); $entry->setUrl($item['url']); + $entry->setHashedUrl(hash('md5', $item['url'])); $entry->setReadingTime($item['reading_time']); $entry->setDomainName($item['domain']); $entry->setMimetype($item['mime']); diff --git a/src/Wallabag/CoreBundle/Entity/Entry.php b/src/Wallabag/CoreBundle/Entity/Entry.php index 17a1ed58f..a04f101f4 100644 --- a/src/Wallabag/CoreBundle/Entity/Entry.php +++ b/src/Wallabag/CoreBundle/Entity/Entry.php @@ -26,7 +26,7 @@ use Wallabag\UserBundle\Entity\User; * indexes={ * @ORM\Index(name="created_at", columns={"created_at"}), * @ORM\Index(name="uid", columns={"uid"}), - * @ORM\Index(name="hashedurl", columns={"hashedurl"}) + * @ORM\Index(name="hashed_url", columns={"hashed_url"}) * } * ) * @ORM\HasLifecycleCallbacks() @@ -79,7 +79,7 @@ class Entry /** * @var string * - * @ORM\Column(name="hashedurl", type="text", nullable=true) + * @ORM\Column(name="hashed_url", type="string", length=32, nullable=true) */ private $hashedUrl; diff --git a/src/Wallabag/CoreBundle/Repository/EntryRepository.php b/src/Wallabag/CoreBundle/Repository/EntryRepository.php index 45366623d..0c175abbb 100644 --- a/src/Wallabag/CoreBundle/Repository/EntryRepository.php +++ b/src/Wallabag/CoreBundle/Repository/EntryRepository.php @@ -346,6 +346,30 @@ class EntryRepository extends EntityRepository return false; } + /** + * Find an entry by its hashed url and its owner. + * If it exists, return the entry otherwise return false. + * + * @param $hashedUrl + * @param $userId + * + * @return Entry|bool + */ + public function findByHashedUrlAndUserId($hashedUrl, $userId) + { + $res = $this->createQueryBuilder('e') + ->where('e.hashedUrl = :hashed_url')->setParameter('hashed_url', urldecode($hashedUrl)) + ->andWhere('e.user = :user_id')->setParameter('user_id', $userId) + ->getQuery() + ->getResult(); + + if (\count($res)) { + return current($res); + } + + return false; + } + /** * Count all entries for a user. * diff --git a/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php b/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php index 8d96d7b8c..fc4dc9d92 100644 --- a/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php +++ b/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php @@ -971,40 +971,42 @@ class EntryRestControllerTest extends WallabagApiTestCase $this->assertGreaterThanOrEqual($now->getTimestamp(), (new \DateTime($content['starred_at']))->getTimestamp()); } - public function testGetEntriesExistsWithReturnId() + public function dataForEntriesExistWithUrl() { - $this->client->request('GET', '/api/entries/exists?url=http://0.0.0.0/entry2&return_id=1'); + $url = hash('md5', 'http://0.0.0.0/entry2'); - $this->assertSame(200, $this->client->getResponse()->getStatusCode()); - - $content = json_decode($this->client->getResponse()->getContent(), true); - - // it returns a database id, we don't know it, so we only check it's greater than the lowest possible value - $this->assertGreaterThan(1, $content['exists']); + return [ + 'with_id' => [ + 'url' => '/api/entries/exists?url=http://0.0.0.0/entry2&return_id=1', + 'expectedValue' => 2, + ], + 'without_id' => [ + 'url' => '/api/entries/exists?url=http://0.0.0.0/entry2', + 'expectedValue' => true, + ], + 'hashed_url_with_id' => [ + 'url' => '/api/entries/exists?hashed_url=' . $url . '&return_id=1', + 'expectedValue' => 2, + ], + 'hashed_url_without_id' => [ + 'url' => '/api/entries/exists?hashed_url=' . $url . '', + 'expectedValue' => true, + ], + ]; } - public function testGetEntriesExistsWithoutReturnId() + /** + * @dataProvider dataForEntriesExistWithUrl + */ + public function testGetEntriesExists($url, $expectedValue) { - $this->client->request('GET', '/api/entries/exists?url=http://0.0.0.0/entry2'); - - $this->client->request('GET', '/api/entries/exists?hashedurl=' . hash('md5', 'http://0.0.0.0/entry2')); + $this->client->request('GET', $url); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); $content = json_decode($this->client->getResponse()->getContent(), true); - $this->assertTrue($content['exists']); - } - - public function testGetEntriesExistsWithHash() - { - $this->client->request('GET', '/api/entries/exists?hashedurl=' . hash('md5', 'http://0.0.0.0/entry2')); - - $this->assertSame(200, $this->client->getResponse()->getStatusCode()); - - $content = json_decode($this->client->getResponse()->getContent(), true); - - $this->assertSame(2, $content['exists']); + $this->assertSame($expectedValue, $content['exists']); } public function testGetEntriesExistsWithManyUrls() @@ -1045,42 +1047,37 @@ class EntryRestControllerTest extends WallabagApiTestCase { $url1 = 'http://0.0.0.0/entry2'; $url2 = 'http://0.0.0.0/entry10'; - $this->client->request('GET', '/api/entries/exists?hashedurls[]='.hash('md5',$url1).'&hashedurls[]='.hash('md5',$url2) . '&return_id=1'); + $this->client->request('GET', '/api/entries/exists?hashed_urls[]=' . hash('md5', $url1) . '&hashed_urls[]=' . hash('md5', $url2) . '&return_id=1'); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); $content = json_decode($this->client->getResponse()->getContent(), true); - $this->assertArrayHasKey($url1, $content); - $this->assertArrayHasKey($url2, $content); - $this->assertSame(2, $content[$url1]); - $this->assertNull($content[$url2]); - $this->assertArrayHasKey(hash('md5', $url1), $content); $this->assertArrayHasKey(hash('md5', $url2), $content); - $this->assertEquals(2, $content[hash('md5', $url1)]); - $this->assertEquals(false, $content[hash('md5', $url2)]); + $this->assertSame(2, $content[hash('md5', $url1)]); + $this->assertNull($content[hash('md5', $url2)]); } public function testGetEntriesExistsWithManyUrlsHashedReturnBool() { $url1 = 'http://0.0.0.0/entry2'; $url2 = 'http://0.0.0.0/entry10'; - $this->client->request('GET', '/api/entries/exists?hashedurls[]='.hash('md5',$url1).'&hashedurls[]='.hash('md5',$url2)); + $this->client->request('GET', '/api/entries/exists?hashed_urls[]=' . hash('md5', $url1) . '&hashed_urls[]=' . hash('md5', $url2)); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); $content = json_decode($this->client->getResponse()->getContent(), true); - $this->assertArrayHasKey($url1, $content); - $this->assertArrayHasKey($url2, $content); - $this->assertTrue($content[$url1]); - $this->assertFalse($content[$url2]); + $this->assertArrayHasKey(hash('md5', $url1), $content); + $this->assertArrayHasKey(hash('md5', $url2), $content); + $this->assertTrue($content[hash('md5', $url1)]); + $this->assertFalse($content[hash('md5', $url2)]); } public function testGetEntriesExistsWhichDoesNotExists() { - $this->client->request('GET', '/api/entries/exists?hashedurl='.hash('md5','http://google.com/entry2')); + $this->client->request('GET', '/api/entries/exists?hashed_url=' . hash('md5', 'http://google.com/entry2')); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); @@ -1091,7 +1088,7 @@ class EntryRestControllerTest extends WallabagApiTestCase public function testGetEntriesExistsWithNoUrl() { - $this->client->request('GET', '/api/entries/exists?hashedurl='); + $this->client->request('GET', '/api/entries/exists?hashed_url='); $this->assertSame(403, $this->client->getResponse()->getStatusCode()); } diff --git a/tests/Wallabag/CoreBundle/Command/GenerateUrlHashesCommandTest.php b/tests/Wallabag/CoreBundle/Command/GenerateUrlHashesCommandTest.php index 8ca772cb9..cc1e3fbc7 100644 --- a/tests/Wallabag/CoreBundle/Command/GenerateUrlHashesCommandTest.php +++ b/tests/Wallabag/CoreBundle/Command/GenerateUrlHashesCommandTest.php @@ -22,7 +22,7 @@ class GenerateUrlHashesCommandTest extends WallabagCoreTestCase 'command' => $command->getName(), ]); - $this->assertContains('Generating hashed urls for the 3 user account entries', $tester->getDisplay()); + $this->assertContains('Generating hashed urls for "3" users', $tester->getDisplay()); $this->assertContains('Finished generated hashed urls', $tester->getDisplay()); } @@ -55,7 +55,7 @@ class GenerateUrlHashesCommandTest extends WallabagCoreTestCase 'username' => 'admin', ]); - $this->assertContains('Generated hashed urls for user admin', $tester->getDisplay()); + $this->assertContains('Generated hashed urls for user: admin', $tester->getDisplay()); } public function testGenerateUrls() @@ -88,11 +88,11 @@ class GenerateUrlHashesCommandTest extends WallabagCoreTestCase 'username' => 'admin', ]); - $this->assertContains('Generated hashed urls for user admin', $tester->getDisplay()); + $this->assertContains('Generated hashed urls for user: admin', $tester->getDisplay()); $entry = $em->getRepository('WallabagCoreBundle:Entry')->findOneByUrl($url); - $this->assertEquals($entry->getHashedUrl(), hash('sha512', $url)); + $this->assertSame($entry->getHashedUrl(), hash('md5', $url)); $query = $em->createQuery('DELETE FROM Wallabag\CoreBundle\Entity\Entry e WHERE e.url = :url'); $query->setParameter('url', $url); From 8a6456629814039cfc623cdb279bcba06dacff50 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Mon, 1 Apr 2019 13:51:57 +0200 Subject: [PATCH 074/107] Use a better index for hashed_url It'll most often be used in addition to the `user_id`. Also, automatically generate the hash when saving the url. Switch from `md5` to `sha1`. --- .../Version20190401105353.php | 5 +++- .../Command/GenerateUrlHashesCommand.php | 2 +- .../CoreBundle/DataFixtures/EntryFixtures.php | 1 - src/Wallabag/CoreBundle/Entity/Entry.php | 5 ++-- .../CoreBundle/Helper/ContentProxy.php | 2 -- .../Controller/EntryRestControllerTest.php | 24 +++++++++---------- .../Command/GenerateUrlHashesCommandTest.php | 5 +--- 7 files changed, 21 insertions(+), 23 deletions(-) diff --git a/app/DoctrineMigrations/Version20190401105353.php b/app/DoctrineMigrations/Version20190401105353.php index 4afc8b15d..c1c220537 100644 --- a/app/DoctrineMigrations/Version20190401105353.php +++ b/app/DoctrineMigrations/Version20190401105353.php @@ -20,7 +20,7 @@ class Version20190401105353 extends WallabagMigration $this->skipIf($entryTable->hasColumn('hashed_url'), 'It seems that you already played this migration.'); $entryTable->addColumn('hashed_url', 'text', [ - 'length' => 32, + 'length' => 40, 'notnull' => false, ]); @@ -28,6 +28,8 @@ class Version20190401105353 extends WallabagMigration if ('sqlite' !== $this->connection->getDatabasePlatform()->getName()) { $this->addSql('UPDATE ' . $this->getTable('entry') . ' SET hashed_url = MD5(url)'); } + + $entryTable->addIndex(['user_id', 'hashed_url'], 'hashed_url_user_id'); } /** @@ -39,6 +41,7 @@ class Version20190401105353 extends WallabagMigration $this->skipIf(!$entryTable->hasColumn('hashed_url'), 'It seems that you already played this migration.'); + $entryTable->dropIndex('hashed_url_user_id'); $entryTable->dropColumn('hashed_url'); } } diff --git a/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php b/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php index fb5983902..685e1672f 100644 --- a/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php +++ b/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php @@ -69,7 +69,7 @@ class GenerateUrlHashesCommand extends ContainerAwareCommand $i = 1; foreach ($entries as $entry) { - $entry->setHashedUrl(hash('md5', $entry->getUrl())); + $entry->setHashedUrl(hash('sha1', $entry->getUrl())); $em->persist($entry); if (0 === ($i % 20)) { diff --git a/src/Wallabag/CoreBundle/DataFixtures/EntryFixtures.php b/src/Wallabag/CoreBundle/DataFixtures/EntryFixtures.php index 1b18cad66..024fcfdc8 100644 --- a/src/Wallabag/CoreBundle/DataFixtures/EntryFixtures.php +++ b/src/Wallabag/CoreBundle/DataFixtures/EntryFixtures.php @@ -89,7 +89,6 @@ class EntryFixtures extends Fixture implements DependentFixtureInterface foreach ($entries as $reference => $item) { $entry = new Entry($this->getReference($item['user'])); $entry->setUrl($item['url']); - $entry->setHashedUrl(hash('md5', $item['url'])); $entry->setReadingTime($item['reading_time']); $entry->setDomainName($item['domain']); $entry->setMimetype($item['mime']); diff --git a/src/Wallabag/CoreBundle/Entity/Entry.php b/src/Wallabag/CoreBundle/Entity/Entry.php index a04f101f4..faf4d2598 100644 --- a/src/Wallabag/CoreBundle/Entity/Entry.php +++ b/src/Wallabag/CoreBundle/Entity/Entry.php @@ -26,7 +26,7 @@ use Wallabag\UserBundle\Entity\User; * indexes={ * @ORM\Index(name="created_at", columns={"created_at"}), * @ORM\Index(name="uid", columns={"uid"}), - * @ORM\Index(name="hashed_url", columns={"hashed_url"}) + * @ORM\Index(name="hashed_url_user_id", columns={"user_id", "hashed_url"}) * } * ) * @ORM\HasLifecycleCallbacks() @@ -79,7 +79,7 @@ class Entry /** * @var string * - * @ORM\Column(name="hashed_url", type="string", length=32, nullable=true) + * @ORM\Column(name="hashed_url", type="string", length=40, nullable=true) */ private $hashedUrl; @@ -324,6 +324,7 @@ class Entry public function setUrl($url) { $this->url = $url; + $this->hashedUrl = hash('sha1', $url); return $this; } diff --git a/src/Wallabag/CoreBundle/Helper/ContentProxy.php b/src/Wallabag/CoreBundle/Helper/ContentProxy.php index 0534d27b2..31953f12d 100644 --- a/src/Wallabag/CoreBundle/Helper/ContentProxy.php +++ b/src/Wallabag/CoreBundle/Helper/ContentProxy.php @@ -248,8 +248,6 @@ class ContentProxy { $this->updateOriginUrl($entry, $content['url']); - $entry->setHashedUrl(hash('md5', $entry->getUrl())); - $this->setEntryDomainName($entry); if (!empty($content['title'])) { diff --git a/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php b/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php index fc4dc9d92..34de8ec8c 100644 --- a/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php +++ b/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php @@ -973,7 +973,7 @@ class EntryRestControllerTest extends WallabagApiTestCase public function dataForEntriesExistWithUrl() { - $url = hash('md5', 'http://0.0.0.0/entry2'); + $url = hash('sha1', 'http://0.0.0.0/entry2'); return [ 'with_id' => [ @@ -1047,37 +1047,37 @@ class EntryRestControllerTest extends WallabagApiTestCase { $url1 = 'http://0.0.0.0/entry2'; $url2 = 'http://0.0.0.0/entry10'; - $this->client->request('GET', '/api/entries/exists?hashed_urls[]=' . hash('md5', $url1) . '&hashed_urls[]=' . hash('md5', $url2) . '&return_id=1'); + $this->client->request('GET', '/api/entries/exists?hashed_urls[]=' . hash('sha1', $url1) . '&hashed_urls[]=' . hash('sha1', $url2) . '&return_id=1'); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); $content = json_decode($this->client->getResponse()->getContent(), true); - $this->assertArrayHasKey(hash('md5', $url1), $content); - $this->assertArrayHasKey(hash('md5', $url2), $content); - $this->assertSame(2, $content[hash('md5', $url1)]); - $this->assertNull($content[hash('md5', $url2)]); + $this->assertArrayHasKey(hash('sha1', $url1), $content); + $this->assertArrayHasKey(hash('sha1', $url2), $content); + $this->assertSame(2, $content[hash('sha1', $url1)]); + $this->assertNull($content[hash('sha1', $url2)]); } public function testGetEntriesExistsWithManyUrlsHashedReturnBool() { $url1 = 'http://0.0.0.0/entry2'; $url2 = 'http://0.0.0.0/entry10'; - $this->client->request('GET', '/api/entries/exists?hashed_urls[]=' . hash('md5', $url1) . '&hashed_urls[]=' . hash('md5', $url2)); + $this->client->request('GET', '/api/entries/exists?hashed_urls[]=' . hash('sha1', $url1) . '&hashed_urls[]=' . hash('sha1', $url2)); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); $content = json_decode($this->client->getResponse()->getContent(), true); - $this->assertArrayHasKey(hash('md5', $url1), $content); - $this->assertArrayHasKey(hash('md5', $url2), $content); - $this->assertTrue($content[hash('md5', $url1)]); - $this->assertFalse($content[hash('md5', $url2)]); + $this->assertArrayHasKey(hash('sha1', $url1), $content); + $this->assertArrayHasKey(hash('sha1', $url2), $content); + $this->assertTrue($content[hash('sha1', $url1)]); + $this->assertFalse($content[hash('sha1', $url2)]); } public function testGetEntriesExistsWhichDoesNotExists() { - $this->client->request('GET', '/api/entries/exists?hashed_url=' . hash('md5', 'http://google.com/entry2')); + $this->client->request('GET', '/api/entries/exists?hashed_url=' . hash('sha1', 'http://google.com/entry2')); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); diff --git a/tests/Wallabag/CoreBundle/Command/GenerateUrlHashesCommandTest.php b/tests/Wallabag/CoreBundle/Command/GenerateUrlHashesCommandTest.php index cc1e3fbc7..17eed210b 100644 --- a/tests/Wallabag/CoreBundle/Command/GenerateUrlHashesCommandTest.php +++ b/tests/Wallabag/CoreBundle/Command/GenerateUrlHashesCommandTest.php @@ -72,11 +72,8 @@ class GenerateUrlHashesCommandTest extends WallabagCoreTestCase $entry1->setUrl($url); $em->persist($entry1); - $em->flush(); - $this->assertNull($entry1->getHashedUrl()); - $application = new Application($this->getClient()->getKernel()); $application->add(new GenerateUrlHashesCommand()); @@ -92,7 +89,7 @@ class GenerateUrlHashesCommandTest extends WallabagCoreTestCase $entry = $em->getRepository('WallabagCoreBundle:Entry')->findOneByUrl($url); - $this->assertSame($entry->getHashedUrl(), hash('md5', $url)); + $this->assertSame($entry->getHashedUrl(), hash('sha1', $url)); $query = $em->createQuery('DELETE FROM Wallabag\CoreBundle\Entity\Entry e WHERE e.url = :url'); $query->setParameter('url', $url); From c579ce2306297674c56376a2ab5c8ba66a272253 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Mon, 1 Apr 2019 14:34:20 +0200 Subject: [PATCH 075/107] Some cleanup Also, do not run the hashed_url migration into a Doctrine migration --- .../Version20190401105353.php | 5 ----- .../Controller/EntryRestController.php | 1 - .../Command/GenerateUrlHashesCommand.php | 8 ++------ .../CoreBundle/Repository/EntryRepository.php | 6 +++--- .../Controller/EntryRestControllerTest.php | 18 ++++++++++++++++++ 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/app/DoctrineMigrations/Version20190401105353.php b/app/DoctrineMigrations/Version20190401105353.php index c1c220537..94ebb1ab5 100644 --- a/app/DoctrineMigrations/Version20190401105353.php +++ b/app/DoctrineMigrations/Version20190401105353.php @@ -24,11 +24,6 @@ class Version20190401105353 extends WallabagMigration 'notnull' => false, ]); - // sqlite doesn't have the MD5 function by default - if ('sqlite' !== $this->connection->getDatabasePlatform()->getName()) { - $this->addSql('UPDATE ' . $this->getTable('entry') . ' SET hashed_url = MD5(url)'); - } - $entryTable->addIndex(['user_id', 'hashed_url'], 'hashed_url_user_id'); } diff --git a/src/Wallabag/ApiBundle/Controller/EntryRestController.php b/src/Wallabag/ApiBundle/Controller/EntryRestController.php index 0ecf1a0ea..ad43b1d46 100644 --- a/src/Wallabag/ApiBundle/Controller/EntryRestController.php +++ b/src/Wallabag/ApiBundle/Controller/EntryRestController.php @@ -52,7 +52,6 @@ class EntryRestController extends WallabagRestController foreach ($hashedUrls as $hashedUrl) { $res = $repo->findByHashedUrlAndUserId($hashedUrl, $this->getUser()->getId()); - // $results[$url] = $this->returnExistInformation($res, $returnId); $results[$hashedUrl] = $this->returnExistInformation($res, $returnId); } diff --git a/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php b/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php index 685e1672f..45bd8c5ff 100644 --- a/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php +++ b/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php @@ -20,18 +20,14 @@ class GenerateUrlHashesCommand extends ContainerAwareCommand ->setName('wallabag:generate-hashed-urls') ->setDescription('Generates hashed urls for each entry') ->setHelp('This command helps you to generates hashes of the url of each entry, to check through API if an URL is already saved') - ->addArgument( - 'username', - InputArgument::OPTIONAL, - 'User to process entries' - ); + ->addArgument('username', InputArgument::OPTIONAL, 'User to process entries'); } protected function execute(InputInterface $input, OutputInterface $output) { $this->output = $output; - $username = $input->getArgument('username'); + $username = (string) $input->getArgument('username'); if ($username) { try { diff --git a/src/Wallabag/CoreBundle/Repository/EntryRepository.php b/src/Wallabag/CoreBundle/Repository/EntryRepository.php index 0c175abbb..f50897296 100644 --- a/src/Wallabag/CoreBundle/Repository/EntryRepository.php +++ b/src/Wallabag/CoreBundle/Repository/EntryRepository.php @@ -350,15 +350,15 @@ class EntryRepository extends EntityRepository * Find an entry by its hashed url and its owner. * If it exists, return the entry otherwise return false. * - * @param $hashedUrl - * @param $userId + * @param string $hashedUrl Url hashed using sha1 + * @param int $userId * * @return Entry|bool */ public function findByHashedUrlAndUserId($hashedUrl, $userId) { $res = $this->createQueryBuilder('e') - ->where('e.hashedUrl = :hashed_url')->setParameter('hashed_url', urldecode($hashedUrl)) + ->where('e.hashedUrl = :hashed_url')->setParameter('hashed_url', $hashedUrl) ->andWhere('e.user = :user_id')->setParameter('user_id', $userId) ->getQuery() ->getResult(); diff --git a/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php b/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php index 34de8ec8c..8cc12ed37 100644 --- a/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php +++ b/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php @@ -1076,6 +1076,17 @@ class EntryRestControllerTest extends WallabagApiTestCase } public function testGetEntriesExistsWhichDoesNotExists() + { + $this->client->request('GET', '/api/entries/exists?url=http://google.com/entry2'); + + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + + $content = json_decode($this->client->getResponse()->getContent(), true); + + $this->assertFalse($content['exists']); + } + + public function testGetEntriesExistsWhichDoesNotExistsWithHashedUrl() { $this->client->request('GET', '/api/entries/exists?hashed_url=' . hash('sha1', 'http://google.com/entry2')); @@ -1087,6 +1098,13 @@ class EntryRestControllerTest extends WallabagApiTestCase } public function testGetEntriesExistsWithNoUrl() + { + $this->client->request('GET', '/api/entries/exists?url='); + + $this->assertSame(403, $this->client->getResponse()->getStatusCode()); + } + + public function testGetEntriesExistsWithNoHashedUrl() { $this->client->request('GET', '/api/entries/exists?hashed_url='); From 5cc0646e66f52448f83a7a458e0b60b4580e83e5 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Mon, 1 Apr 2019 15:45:17 +0200 Subject: [PATCH 076/107] Fix index on MySQL --- app/DoctrineMigrations/Version20190401105353.php | 2 +- src/Wallabag/CoreBundle/Entity/Entry.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/DoctrineMigrations/Version20190401105353.php b/app/DoctrineMigrations/Version20190401105353.php index 94ebb1ab5..d27962dbe 100644 --- a/app/DoctrineMigrations/Version20190401105353.php +++ b/app/DoctrineMigrations/Version20190401105353.php @@ -24,7 +24,7 @@ class Version20190401105353 extends WallabagMigration 'notnull' => false, ]); - $entryTable->addIndex(['user_id', 'hashed_url'], 'hashed_url_user_id'); + $entryTable->addIndex(['user_id', 'hashed_url'], 'hashed_url_user_id', [], ['lengths' => [null, 40]]); } /** diff --git a/src/Wallabag/CoreBundle/Entity/Entry.php b/src/Wallabag/CoreBundle/Entity/Entry.php index faf4d2598..c3fb87d21 100644 --- a/src/Wallabag/CoreBundle/Entity/Entry.php +++ b/src/Wallabag/CoreBundle/Entity/Entry.php @@ -26,7 +26,7 @@ use Wallabag\UserBundle\Entity\User; * indexes={ * @ORM\Index(name="created_at", columns={"created_at"}), * @ORM\Index(name="uid", columns={"uid"}), - * @ORM\Index(name="hashed_url_user_id", columns={"user_id", "hashed_url"}) + * @ORM\Index(name="hashed_url_user_id", columns={"user_id", "hashed_url"}, options={"lengths"={null, 40}}) * } * ) * @ORM\HasLifecycleCallbacks() From 76bc05ebc02408b213b536fec44e94b092889118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Benoist?= Date: Tue, 2 Apr 2019 22:59:50 +0200 Subject: [PATCH 077/107] Fix ApiDoc about md5/sha1 --- src/Wallabag/ApiBundle/Controller/EntryRestController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Wallabag/ApiBundle/Controller/EntryRestController.php b/src/Wallabag/ApiBundle/Controller/EntryRestController.php index ad43b1d46..06520af91 100644 --- a/src/Wallabag/ApiBundle/Controller/EntryRestController.php +++ b/src/Wallabag/ApiBundle/Controller/EntryRestController.php @@ -29,8 +29,8 @@ class EntryRestController extends WallabagRestController * {"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"}, * {"name"="url", "dataType"="string", "required"=true, "format"="An url", "description"="DEPRECATED, use hashed_url instead"}, * {"name"="urls", "dataType"="string", "required"=false, "format"="An array of urls (?urls[]=http...&urls[]=http...)", "description"="DEPRECATED, use hashed_urls instead"}, - * {"name"="hashed_url", "dataType"="string", "required"=true, "format"="An url", "description"="Md5 url to check if it exists"}, - * {"name"="hashed_urls", "dataType"="string", "required"=false, "format"="An array of urls (?urls[]=http...&urls[]=http...)", "description"="Md5 urls (as an array) to check if it exists"} + * {"name"="hashed_url", "dataType"="string", "required"=false, "format"="A hashed url", "description"="Hashed url using SHA1 to check if it exists"}, + * {"name"="hashed_urls", "dataType"="string", "required"=false, "format"="An array of hashed urls (?hashed_urls[]=xxx...&hashed_urls[]=xxx...)", "description"="An array of hashed urls using SHA1 to check if they exist"} * } * ) * From 531c8d0a5c55fa93438e227a7d349235fbd31d28 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Tue, 13 Jun 2017 18:48:10 +0200 Subject: [PATCH 078/107] Changed RSS to Atom feed and improve paging --- .../static/themes/baggy/css/article.scss | 6 +- .../static/themes/baggy/css/pictos.scss | 2 +- app/config/security.yml | 2 + .../Controller/ConfigController.php | 24 +- .../{RssController.php => FeedController.php} | 89 ++++--- .../WallabagCoreExtension.php | 2 +- src/Wallabag/CoreBundle/Entity/Config.php | 34 +-- .../Form/Type/{RssType.php => FeedType.php} | 10 +- .../Helper/PreparePagerForEntries.php | 2 +- ...ter.php => UsernameFeedTokenConverter.php} | 10 +- .../CoreBundle/Resources/config/services.yml | 6 +- .../Resources/translations/messages.da.yml | 16 +- .../Resources/translations/messages.de.yml | 24 +- .../Resources/translations/messages.en.yml | 20 +- .../Resources/translations/messages.es.yml | 16 +- .../Resources/translations/messages.fa.yml | 16 +- .../Resources/translations/messages.fr.yml | 20 +- .../Resources/translations/messages.it.yml | 16 +- .../Resources/translations/messages.oc.yml | 16 +- .../Resources/translations/messages.pl.yml | 16 +- .../Resources/translations/messages.pt.yml | 16 +- .../Resources/translations/messages.ro.yml | 16 +- .../Resources/translations/messages.tr.yml | 16 +- .../Resources/translations/validators.da.yml | 2 +- .../Resources/translations/validators.de.yml | 3 +- .../Resources/translations/validators.en.yml | 2 +- .../Resources/translations/validators.es.yml | 2 +- .../Resources/translations/validators.fa.yml | 2 +- .../Resources/translations/validators.fr.yml | 2 +- .../Resources/translations/validators.it.yml | 2 +- .../Resources/translations/validators.oc.yml | 2 +- .../Resources/translations/validators.pl.yml | 2 +- .../Resources/translations/validators.pt.yml | 2 +- .../Resources/translations/validators.ro.yml | 2 +- .../Resources/translations/validators.tr.yml | 2 +- .../views/themes/baggy/Config/index.html.twig | 42 ++-- .../themes/baggy/Entry/entries.html.twig | 8 +- .../views/themes/baggy/Tag/tags.html.twig | 2 +- .../themes/common/Entry/_feed_link.html.twig | 11 + .../themes/common/Entry/_rss_link.html.twig | 11 - .../themes/common/Entry/entries.xml.twig | 83 ++++--- .../themes/common/Static/quickstart.html.twig | 2 +- .../themes/material/Config/index.html.twig | 44 ++-- .../themes/material/Entry/entries.html.twig | 8 +- .../views/themes/material/Tag/tags.html.twig | 2 +- src/Wallabag/CoreBundle/Tools/Utils.php | 2 +- .../CoreBundle/Twig/WallabagExtension.php | 10 +- .../EventListener/CreateConfigListener.php | 8 +- .../UserBundle/Repository/UserRepository.php | 10 +- .../UserBundle/Resources/config/services.yml | 2 +- .../Controller/ConfigControllerTest.php | 32 +-- .../Controller/FeedControllerTest.php | 228 ++++++++++++++++++ .../Controller/RssControllerTest.php | 221 ----------------- .../Controller/SecurityControllerTest.php | 2 +- ...php => UsernameFeedTokenConverterTest.php} | 24 +- .../CoreBundle/Twig/WallabagExtensionTest.php | 25 ++ .../CreateConfigListenerTest.php | 2 +- 57 files changed, 635 insertions(+), 564 deletions(-) rename src/Wallabag/CoreBundle/Controller/{RssController.php => FeedController.php} (63%) rename src/Wallabag/CoreBundle/Form/Type/{RssType.php => FeedType.php} (77%) rename src/Wallabag/CoreBundle/ParamConverter/{UsernameRssTokenConverter.php => UsernameFeedTokenConverter.php} (88%) create mode 100644 src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/_feed_link.html.twig delete mode 100644 src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/_rss_link.html.twig create mode 100644 tests/Wallabag/CoreBundle/Controller/FeedControllerTest.php delete mode 100644 tests/Wallabag/CoreBundle/Controller/RssControllerTest.php rename tests/Wallabag/CoreBundle/ParamConverter/{UsernameRssTokenConverterTest.php => UsernameFeedTokenConverterTest.php} (90%) diff --git a/app/Resources/static/themes/baggy/css/article.scss b/app/Resources/static/themes/baggy/css/article.scss index 9094ad550..d203ce313 100644 --- a/app/Resources/static/themes/baggy/css/article.scss +++ b/app/Resources/static/themes/baggy/css/article.scss @@ -85,7 +85,7 @@ blockquote { color: #999; } -.icon-rss { +.icon-feed { background-color: #000; color: #fff; padding: 0.2em 0.5em; @@ -101,8 +101,8 @@ blockquote { margin-bottom: 0.5em; } - .icon-rss:hover, - .icon-rss:focus { + .icon-feed:hover, + .icon-feed:focus { background-color: #fff; color: #000; text-decoration: none; diff --git a/app/Resources/static/themes/baggy/css/pictos.scss b/app/Resources/static/themes/baggy/css/pictos.scss index 2ff019375..b6ebf3112 100644 --- a/app/Resources/static/themes/baggy/css/pictos.scss +++ b/app/Resources/static/themes/baggy/css/pictos.scss @@ -136,7 +136,7 @@ content: "\ea3a"; } -.icon-rss::before { +.icon-feed::before { content: "\e808"; } diff --git a/app/config/security.yml b/app/config/security.yml index 6a21b4e55..760b25503 100644 --- a/app/config/security.yml +++ b/app/config/security.yml @@ -72,6 +72,8 @@ security: - { path: /(unread|starred|archive|all).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/locale, role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: /tags/(.*).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/feed, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: /(unread|starred|archive).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY } # For backwards compatibility - { path: ^/share, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/settings, roles: ROLE_SUPER_ADMIN } - { path: ^/annotations, roles: ROLE_USER } diff --git a/src/Wallabag/CoreBundle/Controller/ConfigController.php b/src/Wallabag/CoreBundle/Controller/ConfigController.php index 9257ab18d..3b281d488 100644 --- a/src/Wallabag/CoreBundle/Controller/ConfigController.php +++ b/src/Wallabag/CoreBundle/Controller/ConfigController.php @@ -14,7 +14,7 @@ use Wallabag\CoreBundle\Entity\Config; use Wallabag\CoreBundle\Entity\TaggingRule; use Wallabag\CoreBundle\Form\Type\ChangePasswordType; use Wallabag\CoreBundle\Form\Type\ConfigType; -use Wallabag\CoreBundle\Form\Type\RssType; +use Wallabag\CoreBundle\Form\Type\FeedType; use Wallabag\CoreBundle\Form\Type\TaggingRuleType; use Wallabag\CoreBundle\Form\Type\UserInformationType; use Wallabag\CoreBundle\Tools\Utils; @@ -92,17 +92,17 @@ class ConfigController extends Controller return $this->redirect($this->generateUrl('config') . '#set3'); } - // handle rss information - $rssForm = $this->createForm(RssType::class, $config, ['action' => $this->generateUrl('config') . '#set2']); - $rssForm->handleRequest($request); + // handle feed information + $feedForm = $this->createForm(FeedType::class, $config, ['action' => $this->generateUrl('config') . '#set2']); + $feedForm->handleRequest($request); - if ($rssForm->isSubmitted() && $rssForm->isValid()) { + if ($feedForm->isSubmitted() && $feedForm->isValid()) { $em->persist($config); $em->flush(); $this->addFlash( 'notice', - 'flashes.config.notice.rss_updated' + 'flashes.config.notice.feed_updated' ); return $this->redirect($this->generateUrl('config') . '#set2'); @@ -143,14 +143,14 @@ class ConfigController extends Controller return $this->render('WallabagCoreBundle:Config:index.html.twig', [ 'form' => [ 'config' => $configForm->createView(), - 'rss' => $rssForm->createView(), + 'feed' => $feedForm->createView(), 'pwd' => $pwdForm->createView(), 'user' => $userForm->createView(), 'new_tagging_rule' => $newTaggingRule->createView(), ], - 'rss' => [ + 'feed' => [ 'username' => $user->getUsername(), - 'token' => $config->getRssToken(), + 'token' => $config->getFeedToken(), ], 'twofactor_auth' => $this->getParameter('twofactor_auth'), 'wallabag_url' => $this->getParameter('domain_name'), @@ -281,19 +281,19 @@ class ConfigController extends Controller public function generateTokenAction(Request $request) { $config = $this->getConfig(); - $config->setRssToken(Utils::generateToken()); + $config->setFeedToken(Utils::generateToken()); $em = $this->getDoctrine()->getManager(); $em->persist($config); $em->flush(); if ($request->isXmlHttpRequest()) { - return new JsonResponse(['token' => $config->getRssToken()]); + return new JsonResponse(['token' => $config->getFeedToken()]); } $this->addFlash( 'notice', - 'flashes.config.notice.rss_token_updated' + 'flashes.config.notice.feed_token_updated' ); return $this->redirect($this->generateUrl('config') . '#set2'); diff --git a/src/Wallabag/CoreBundle/Controller/RssController.php b/src/Wallabag/CoreBundle/Controller/FeedController.php similarity index 63% rename from src/Wallabag/CoreBundle/Controller/RssController.php rename to src/Wallabag/CoreBundle/Controller/FeedController.php index 1c831c039..9d55a9b7b 100644 --- a/src/Wallabag/CoreBundle/Controller/RssController.php +++ b/src/Wallabag/CoreBundle/Controller/FeedController.php @@ -15,56 +15,68 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Wallabag\CoreBundle\Entity\Tag; use Wallabag\UserBundle\Entity\User; -class RssController extends Controller +class FeedController extends Controller { /** * Shows unread entries for current user. * - * @Route("/{username}/{token}/unread.xml", name="unread_rss", defaults={"_format"="xml"}) - * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_rsstoken_converter") + * @Route("/feed/{username}/{token}/unread/{page}", name="unread_feed", defaults={"page": 1}) + * @Route("/{username}/{token}/unread.xml", defaults={"page": 1}) + * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_feed_token_converter") + * + * @param User $user + * @param $page * * @return \Symfony\Component\HttpFoundation\Response */ - public function showUnreadRSSAction(Request $request, User $user) + public function showUnreadFeedAction(User $user, $page) { - return $this->showEntries('unread', $user, $request->query->get('page', 1)); + return $this->showEntries('unread', $user, $page); } /** * Shows read entries for current user. * - * @Route("/{username}/{token}/archive.xml", name="archive_rss", defaults={"_format"="xml"}) - * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_rsstoken_converter") + * @Route("/feed/{username}/{token}/archive/{page}", name="archive_feed", defaults={"page": 1}) + * @Route("/{username}/{token}/archive.xml", defaults={"page": 1}) + * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_feed_token_converter") + * + * @param User $user + * @param $page * * @return \Symfony\Component\HttpFoundation\Response */ - public function showArchiveRSSAction(Request $request, User $user) + public function showArchiveFeedAction(User $user, $page) { - return $this->showEntries('archive', $user, $request->query->get('page', 1)); + return $this->showEntries('archive', $user, $page); } /** * Shows starred entries for current user. * - * @Route("/{username}/{token}/starred.xml", name="starred_rss", defaults={"_format"="xml"}) - * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_rsstoken_converter") + * @Route("/feed/{username}/{token}/starred/{page}", name="starred_feed", defaults={"page": 1}) + * @Route("/{username}/{token}/starred.xml", defaults={"page": 1}) + * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_feed_token_converter") + * + * @param User $user + * @param $page * * @return \Symfony\Component\HttpFoundation\Response */ - public function showStarredRSSAction(Request $request, User $user) + public function showStarredFeedAction(User $user, $page) { - return $this->showEntries('starred', $user, $request->query->get('page', 1)); + return $this->showEntries('starred', $user, $page); } /** * Shows all entries for current user. * - * @Route("/{username}/{token}/all.xml", name="all_rss", defaults={"_format"="xml"}) - * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_rsstoken_converter") + * @Route("/{username}/{token}/all.xml", name="all_feed", defaults={"_format"="xml"}) + * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_feed_token_converter") * * @return \Symfony\Component\HttpFoundation\Response */ - public function showAllRSSAction(Request $request, User $user) + public function showAllFeedAction(Request $request, User $user) { return $this->showEntries('all', $user, $request->query->get('page', 1)); } @@ -72,21 +84,21 @@ class RssController extends Controller /** * Shows entries associated to a tag for current user. * - * @Route("/{username}/{token}/tags/{slug}.xml", name="tag_rss", defaults={"_format"="xml"}) - * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_rsstoken_converter") + * @Route("/{username}/{token}/tags/{slug}.xml", name="tag_feed", defaults={"_format"="xml"}) + * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_feed_token_converter") * @ParamConverter("tag", options={"mapping": {"slug": "slug"}}) * * @return \Symfony\Component\HttpFoundation\Response */ - public function showTagsAction(Request $request, User $user, Tag $tag) + public function showTagsFeedAction(Request $request, User $user, Tag $tag) { $page = $request->query->get('page', 1); $url = $this->generateUrl( - 'tag_rss', + 'tag_feed', [ 'username' => $user->getUsername(), - 'token' => $user->getConfig()->getRssToken(), + 'token' => $user->getConfig()->getFeedToken(), 'slug' => $tag->getSlug(), ], UrlGeneratorInterface::ABSOLUTE_URL @@ -119,12 +131,15 @@ class RssController extends Controller return $this->render( '@WallabagCore/themes/common/Entry/entries.xml.twig', [ - 'url_html' => $this->generateUrl('tag_entries', ['slug' => $tag->getSlug()], UrlGeneratorInterface::ABSOLUTE_URL), - 'type' => 'tag (' . $tag->getLabel() . ')', + 'type' => 'tag', 'url' => $url, 'entries' => $entries, + 'user' => $user->getUsername(), + 'domainName' => $this->getParameter('domain_name'), + 'version' => $this->getParameter('wallabag_core.version'), + 'tag' => $tag->getSlug(), ], - new Response('', 200, ['Content-Type' => 'application/rss+xml']) + new Response('', 200, ['Content-Type' => 'application/atom+xml']) ); } @@ -162,14 +177,14 @@ class RssController extends Controller $pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false); $entries = new Pagerfanta($pagerAdapter); - $perPage = $user->getConfig()->getRssLimit() ?: $this->getParameter('wallabag_core.rss_limit'); + $perPage = $user->getConfig()->getFeedLimit() ?: $this->getParameter('wallabag_core.Feed_limit'); $entries->setMaxPerPage($perPage); $url = $this->generateUrl( - $type . '_rss', + $type . '_feed', [ 'username' => $user->getUsername(), - 'token' => $user->getConfig()->getRssToken(), + 'token' => $user->getConfig()->getFeedToken(), ], UrlGeneratorInterface::ABSOLUTE_URL ); @@ -178,19 +193,19 @@ class RssController extends Controller $entries->setCurrentPage((int) $page); } catch (OutOfRangeCurrentPageException $e) { if ($page > 1) { - return $this->redirect($url . '?page=' . $entries->getNbPages(), 302); + return $this->redirect($url . '/' . $entries->getNbPages()); } } - return $this->render( - '@WallabagCore/themes/common/Entry/entries.xml.twig', - [ - 'url_html' => $this->generateUrl($type, [], UrlGeneratorInterface::ABSOLUTE_URL), - 'type' => $type, - 'url' => $url, - 'entries' => $entries, - ], - new Response('', 200, ['Content-Type' => 'application/rss+xml']) + return $this->render('@WallabagCore/themes/common/Entry/entries.xml.twig', [ + 'type' => $type, + 'url' => $url, + 'entries' => $entries, + 'user' => $user->getUsername(), + 'domainName' => $this->getParameter('domain_name'), + 'version' => $this->getParameter('wallabag_core.version'), + ], + new Response('', 200, ['Content-Type' => 'application/atom+xml']) ); } } diff --git a/src/Wallabag/CoreBundle/DependencyInjection/WallabagCoreExtension.php b/src/Wallabag/CoreBundle/DependencyInjection/WallabagCoreExtension.php index a3ef2b53f..e9a1e9e05 100644 --- a/src/Wallabag/CoreBundle/DependencyInjection/WallabagCoreExtension.php +++ b/src/Wallabag/CoreBundle/DependencyInjection/WallabagCoreExtension.php @@ -18,7 +18,7 @@ class WallabagCoreExtension extends Extension $container->setParameter('wallabag_core.items_on_page', $config['items_on_page']); $container->setParameter('wallabag_core.theme', $config['theme']); $container->setParameter('wallabag_core.language', $config['language']); - $container->setParameter('wallabag_core.rss_limit', $config['rss_limit']); + $container->setParameter('wallabag_core.feed_limit', $config['rss_limit']); $container->setParameter('wallabag_core.reading_speed', $config['reading_speed']); $container->setParameter('wallabag_core.version', $config['version']); $container->setParameter('wallabag_core.paypal_url', $config['paypal_url']); diff --git a/src/Wallabag/CoreBundle/Entity/Config.php b/src/Wallabag/CoreBundle/Entity/Config.php index b902ae2cb..7458f7572 100644 --- a/src/Wallabag/CoreBundle/Entity/Config.php +++ b/src/Wallabag/CoreBundle/Entity/Config.php @@ -62,7 +62,7 @@ class Config * * @ORM\Column(name="rss_token", type="string", nullable=true) */ - private $rssToken; + private $feedToken; /** * @var int @@ -71,10 +71,10 @@ class Config * @Assert\Range( * min = 1, * max = 100000, - * maxMessage = "validator.rss_limit_too_high" + * maxMessage = "validator.feed_limit_too_high" * ) */ - private $rssLimit; + private $feedLimit; /** * @var float @@ -231,51 +231,51 @@ class Config } /** - * Set rssToken. + * Set feed Token. * - * @param string $rssToken + * @param string $feedToken * * @return Config */ - public function setRssToken($rssToken) + public function setFeedToken($feedToken) { - $this->rssToken = $rssToken; + $this->feedToken = $feedToken; return $this; } /** - * Get rssToken. + * Get feedToken. * * @return string */ - public function getRssToken() + public function getFeedToken() { - return $this->rssToken; + return $this->feedToken; } /** - * Set rssLimit. + * Set Feed Limit. * - * @param int $rssLimit + * @param int $feedLimit * * @return Config */ - public function setRssLimit($rssLimit) + public function setFeedLimit($feedLimit) { - $this->rssLimit = $rssLimit; + $this->feedLimit = $feedLimit; return $this; } /** - * Get rssLimit. + * Get Feed Limit. * * @return int */ - public function getRssLimit() + public function getFeedLimit() { - return $this->rssLimit; + return $this->feedLimit; } /** diff --git a/src/Wallabag/CoreBundle/Form/Type/RssType.php b/src/Wallabag/CoreBundle/Form/Type/FeedType.php similarity index 77% rename from src/Wallabag/CoreBundle/Form/Type/RssType.php rename to src/Wallabag/CoreBundle/Form/Type/FeedType.php index 49b31c1e2..9b34daf4c 100644 --- a/src/Wallabag/CoreBundle/Form/Type/RssType.php +++ b/src/Wallabag/CoreBundle/Form/Type/FeedType.php @@ -7,14 +7,14 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -class RssType extends AbstractType +class FeedType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder - ->add('rss_limit', null, [ - 'label' => 'config.form_rss.rss_limit', - 'property_path' => 'rssLimit', + ->add('feed_limit', null, [ + 'label' => 'config.form_feed.feed_limit', + 'property_path' => 'feedLimit', ]) ->add('save', SubmitType::class, [ 'label' => 'config.form.save', @@ -31,6 +31,6 @@ class RssType extends AbstractType public function getBlockPrefix() { - return 'rss_config'; + return 'feed_config'; } } diff --git a/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php b/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php index 183d394a0..04abc6d08 100644 --- a/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php +++ b/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php @@ -21,7 +21,7 @@ class PreparePagerForEntries /** * @param AdapterInterface $adapter - * @param User $user If user isn't logged in, we can force it (like for rss) + * @param User $user If user isn't logged in, we can force it (like for feed) * * @return Pagerfanta|null */ diff --git a/src/Wallabag/CoreBundle/ParamConverter/UsernameRssTokenConverter.php b/src/Wallabag/CoreBundle/ParamConverter/UsernameFeedTokenConverter.php similarity index 88% rename from src/Wallabag/CoreBundle/ParamConverter/UsernameRssTokenConverter.php rename to src/Wallabag/CoreBundle/ParamConverter/UsernameFeedTokenConverter.php index 4a2fcab5f..e220abfcf 100644 --- a/src/Wallabag/CoreBundle/ParamConverter/UsernameRssTokenConverter.php +++ b/src/Wallabag/CoreBundle/ParamConverter/UsernameFeedTokenConverter.php @@ -10,12 +10,12 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Wallabag\UserBundle\Entity\User; /** - * ParamConverter used in the RSS controller to retrieve the right user according to + * ParamConverter used in the Feed controller to retrieve the right user according to * username & token given in the url. * * @see http://stfalcon.com/en/blog/post/symfony2-custom-paramconverter */ -class UsernameRssTokenConverter implements ParamConverterInterface +class UsernameFeedTokenConverter implements ParamConverterInterface { private $registry; @@ -67,7 +67,7 @@ class UsernameRssTokenConverter implements ParamConverterInterface public function apply(Request $request, ParamConverter $configuration) { $username = $request->attributes->get('username'); - $rssToken = $request->attributes->get('token'); + $feedToken = $request->attributes->get('token'); if (!$request->attributes->has('username') || !$request->attributes->has('token')) { return false; @@ -78,8 +78,8 @@ class UsernameRssTokenConverter implements ParamConverterInterface $userRepository = $em->getRepository($configuration->getClass()); - // Try to find user by its username and config rss_token - $user = $userRepository->findOneByUsernameAndRsstoken($username, $rssToken); + // Try to find user by its username and config feed_token + $user = $userRepository->findOneByUsernameAndFeedtoken($username, $feedToken); if (null === $user || !($user instanceof User)) { throw new NotFoundHttpException(sprintf('%s not found.', $configuration->getClass())); diff --git a/src/Wallabag/CoreBundle/Resources/config/services.yml b/src/Wallabag/CoreBundle/Resources/config/services.yml index a27dd2108..280d779da 100644 --- a/src/Wallabag/CoreBundle/Resources/config/services.yml +++ b/src/Wallabag/CoreBundle/Resources/config/services.yml @@ -22,10 +22,10 @@ services: tags: - { name: form.type } - wallabag_core.param_converter.username_rsstoken_converter: - class: Wallabag\CoreBundle\ParamConverter\UsernameRssTokenConverter + wallabag_core.param_converter.username_feed_token_converter: + class: Wallabag\CoreBundle\ParamConverter\UsernameFeedTokenConverter tags: - - { name: request.param_converter, converter: username_rsstoken_converter } + - { name: request.param_converter, converter: username_feed_token_converter } arguments: - "@doctrine" diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml index 454f547de..61ef3b8f0 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml @@ -54,7 +54,7 @@ config: page_title: 'Opsætning' tab_menu: settings: 'Indstillinger' - rss: 'RSS' + feed: 'RSS' user_info: 'Brugeroplysninger' password: 'Adgangskode' # rules: 'Tagging rules' @@ -85,19 +85,19 @@ config: # 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." # help_language: "You can change the language of wallabag interface." # help_pocket_consumer_key: "Required for Pocket import. You can create it in your Pocket account." - form_rss: + form_feed: description: 'RSS-feeds fra wallabag gør det muligt at læse de artikler, der gemmes i wallabag, med din RSS-læser. Det kræver, at du genererer et token først.' token_label: 'RSS-Token' no_token: 'Intet token' token_create: 'Opret token' token_reset: 'Nulstil token' - rss_links: 'RSS-Links' - rss_link: + feed_links: 'RSS-Links' + feed_link: unread: 'Ulæst' starred: 'Favoritter' archive: 'Arkiv' # all: 'All' - # rss_limit: 'Number of items in the feed' + # feed_limit: 'Number of items in the feed' form_user: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Navn' @@ -372,7 +372,7 @@ quickstart: # title: 'Configure the application' # description: 'In order to have an application which suits you, have a look into the configuration of wallabag.' # language: 'Change language and design' - # rss: 'Enable RSS feeds' + # feed: 'Enable RSS feeds' # tagging_rules: 'Write rules to automatically tag your articles' # admin: # title: 'Administration' @@ -589,10 +589,10 @@ flashes: password_updated: 'Adgangskode opdateret' # password_not_updated_demo: "In demonstration mode, you can't change password for this user." user_updated: 'Oplysninger opdateret' - rss_updated: 'RSS-oplysninger opdateret' + feed_updated: 'RSS-oplysninger opdateret' # tagging_rules_updated: 'Tagging rules updated' # tagging_rules_deleted: 'Tagging rule deleted' - # rss_token_updated: 'RSS token updated' + # feed_token_updated: 'RSS token updated' # annotations_reset: Annotations reset # tags_reset: Tags reset # entries_reset: Entries reset diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml index dc1d4723f..991e00f12 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml @@ -54,7 +54,7 @@ config: page_title: 'Einstellungen' tab_menu: settings: 'Einstellungen' - rss: 'RSS' + feed: 'RSS' user_info: 'Benutzerinformation' password: 'Kennwort' rules: 'Tagging-Regeln' @@ -85,19 +85,19 @@ config: 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." help_language: "Du kannst die Sprache der wallabag-Oberfläche ändern." help_pocket_consumer_key: "Nötig für den Pocket-Import. Du kannst ihn in deinem Pocket account einrichten." - form_rss: + form_feed: description: 'Die RSS-Feeds von wallabag erlauben es dir, deine gespeicherten Artikel mit deinem bevorzugten RSS-Reader zu lesen. Vorher musst du jedoch einen Token erstellen.' token_label: 'RSS-Token' no_token: 'Kein Token' token_create: 'Token erstellen' token_reset: 'Token zurücksetzen' - rss_links: 'RSS-Links' - rss_link: + feed_links: 'RSS-Links' + feed_link: unread: 'Ungelesene' starred: 'Favoriten' archive: 'Archivierte' all: 'Alle' - rss_limit: 'Anzahl der Einträge pro Feed' + feed_limit: 'Anzahl der Einträge pro Feed' form_user: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Name' @@ -363,7 +363,7 @@ quickstart: title: 'Anwendung konfigurieren' description: 'Um die Applikation für dich anzupassen, schau in die Konfiguration von wallabag.' language: 'Sprache und Design ändern' - rss: 'RSS-Feeds aktivieren' + feed: 'RSS-Feeds aktivieren' tagging_rules: 'Schreibe Regeln, um deine Beiträge automatisch zu taggen (verschlagworten)' admin: title: 'Administration' @@ -580,14 +580,14 @@ flashes: password_updated: 'Kennwort aktualisiert' password_not_updated_demo: 'Im Testmodus kannst du das Kennwort nicht ändern.' user_updated: 'Information aktualisiert' - rss_updated: 'RSS-Informationen aktualisiert' + feed_updated: 'RSS-Informationen aktualisiert' tagging_rules_updated: 'Tagging-Regeln aktualisiert' tagging_rules_deleted: 'Tagging-Regel gelöscht' - rss_token_updated: 'RSS-Token aktualisiert' - annotations_reset: 'Anmerkungen zurücksetzen' - tags_reset: 'Tags zurücksetzen' - entries_reset: 'Einträge zurücksetzen' - archived_reset: 'Archiverte Einträge zurücksetzen' + feed_token_updated: 'RSS-Token aktualisiert' + annotations_reset: Anmerkungen zurücksetzen + tags_reset: Tags zurücksetzen + entries_reset: Einträge zurücksetzen + archived_reset: Archiverte Einträge zurücksetzen entry: notice: entry_already_saved: 'Eintrag bereits am %date% gespeichert' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml index 45145c806..5b8756523 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml @@ -54,7 +54,7 @@ config: page_title: 'Config' tab_menu: settings: 'Settings' - rss: 'RSS' + feed: 'Feeds' user_info: 'User information' password: 'Password' rules: 'Tagging rules' @@ -85,19 +85,19 @@ config: 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." help_language: "You can change the language of wallabag interface." help_pocket_consumer_key: "Required for Pocket import. You can create it in your Pocket account." - form_rss: - description: 'RSS feeds provided by wallabag allow you to read your saved articles with your favourite RSS reader. You need to generate a token first.' - token_label: 'RSS token' + form_feed: + description: 'Atom feeds provided by wallabag allow you to read your saved articles with your favourite Atom reader. You need to generate a token first.' + token_label: 'Feed token' no_token: 'No token' token_create: 'Create your token' token_reset: 'Regenerate your token' - rss_links: 'RSS links' - rss_link: + feed_links: 'Feed links' + feed_link: unread: 'Unread' starred: 'Starred' archive: 'Archived' all: 'All' - rss_limit: 'Number of items in the feed' + feed_limit: 'Number of items in the feed' form_user: two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Name' @@ -372,7 +372,7 @@ quickstart: title: 'Configure the application' description: 'In order to have an application which suits you, have a look into the configuration of wallabag.' language: 'Change language and design' - rss: 'Enable RSS feeds' + feed: 'Enable feeds' tagging_rules: 'Write rules to automatically tag your articles' admin: title: 'Administration' @@ -589,10 +589,10 @@ flashes: password_updated: 'Password updated' password_not_updated_demo: "In demonstration mode, you can't change password for this user." user_updated: 'Information updated' - rss_updated: 'RSS information updated' + feed_updated: 'Feed information updated' tagging_rules_updated: 'Tagging rules updated' tagging_rules_deleted: 'Tagging rule deleted' - rss_token_updated: 'RSS token updated' + feed_token_updated: 'Feed token updated' annotations_reset: Annotations reset tags_reset: Tags reset entries_reset: Entries reset diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml index c1047e55a..562b4191b 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml @@ -54,7 +54,7 @@ config: page_title: 'Configuración' tab_menu: settings: 'Configuración' - rss: 'RSS' + feed: 'RSS' user_info: 'Información de usuario' password: 'Contraseña' rules: 'Reglas de etiquetado automáticas' @@ -85,19 +85,19 @@ config: 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." help_language: "Puedes cambiar el idioma de la interfaz de wallabag." help_pocket_consumer_key: "Requerido para la importación desde Pocket. Puedes crearla en tu cuenta de Pocket." - form_rss: + form_feed: description: 'Los feeds RSS de wallabag permiten leer los artículos guardados con su lector RSS favorito. Primero necesitas generar un token.' token_label: 'Token RSS' no_token: 'Sin token' token_create: 'Crear token' token_reset: 'Reiniciar token' - rss_links: 'URLs de feeds RSS' - rss_link: + feed_links: 'URLs de feeds RSS' + feed_link: unread: 'sin leer' starred: 'favoritos' archive: 'archivados' # all: 'All' - rss_limit: 'Límite de artículos en feed RSS' + feed_limit: 'Límite de artículos en feed RSS' form_user: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Nombre' @@ -372,7 +372,7 @@ quickstart: title: 'Configure la aplicación' description: 'Para que la aplicación se ajuste a tus necesidades, echa un vistazo a la configuración de wallabag.' language: 'Cambie el idioma y el diseño' - rss: 'Activar los feeds RSS' + feed: 'Activar los feeds RSS' tagging_rules: 'Escribe reglas para etiquetar automáticamente tus artículos' admin: title: 'Administración' @@ -589,10 +589,10 @@ flashes: password_updated: 'Contraseña actualizada' password_not_updated_demo: "En el modo demo, no puede cambiar la contraseña del usuario." user_updated: 'Información actualizada' - rss_updated: 'Configuración RSS actualizada' + feed_updated: 'Configuración RSS actualizada' tagging_rules_updated: 'Regla de etiquetado actualizada' tagging_rules_deleted: 'Regla de etiquetado eliminada' - rss_token_updated: 'Token RSS actualizado' + feed_token_updated: 'Token RSS actualizado' annotations_reset: Anotaciones reiniciadas tags_reset: Etiquetas reiniciadas entries_reset: Artículos reiniciados diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml index 3042de2ef..f360e0d6d 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml @@ -54,7 +54,7 @@ config: page_title: 'پیکربندی' tab_menu: settings: 'تنظیمات' - rss: 'آر-اس-اس' + feed: 'آر-اس-اس' user_info: 'اطلاعات کاربر' password: 'رمز' rules: 'برچسب‌گذاری خودکار' @@ -85,19 +85,19 @@ config: # 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." # help_language: "You can change the language of wallabag interface." # help_pocket_consumer_key: "Required for Pocket import. You can create it in your Pocket account." - form_rss: + form_feed: description: 'با خوراک آر-اس-اس که wallabag در اختیارتان می‌گذارد، می‌توانید مقاله‌های ذخیره‌شده را در نرم‌افزار آر-اس-اس دلخواه خود بخوانید. برای این کار نخست باید یک کد بسازید.' token_label: 'کد آر-اس-اس' no_token: 'بدون کد' token_create: 'کد خود را بسازید' token_reset: 'بازنشانی کد' - rss_links: 'پیوند آر-اس-اس' - rss_link: + feed_links: 'پیوند آر-اس-اس' + feed_link: unread: 'خوانده‌نشده' starred: 'برگزیده' archive: 'بایگانی' # all: 'All' - rss_limit: 'محدودیت آر-اس-اس' + feed_limit: 'محدودیت آر-اس-اس' form_user: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'نام' @@ -372,7 +372,7 @@ quickstart: title: 'برنامه را تنظیم کنید' # description: 'In order to have an application which suits you, have a look into the configuration of wallabag.' language: 'زبان و نمای برنامه را تغییر دهید' - rss: 'خوراک آر-اس-اس را فعال کنید' + feed: 'خوراک آر-اس-اس را فعال کنید' tagging_rules: 'قانون‌های برچسب‌گذاری خودکار مقاله‌هایتان را تعریف کنید' admin: title: 'مدیریت' @@ -589,10 +589,10 @@ flashes: password_updated: 'رمز به‌روز شد' password_not_updated_demo: "در حالت نمایشی نمی‌توانید رمز کاربر را عوض کنید." user_updated: 'اطلاعات به‌روز شد' - rss_updated: 'اطلاعات آر-اس-اس به‌روز شد' + feed_updated: 'اطلاعات آر-اس-اس به‌روز شد' tagging_rules_updated: 'برچسب‌گذاری خودکار به‌روز شد' tagging_rules_deleted: 'قانون برچسب‌گذاری پاک شد' - rss_token_updated: 'کد آر-اس-اس به‌روز شد' + feed_token_updated: 'کد آر-اس-اس به‌روز شد' # annotations_reset: Annotations reset # tags_reset: Tags reset # entries_reset: Entries reset diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml index 57740ba23..79f15154a 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml @@ -54,7 +54,7 @@ config: page_title: "Configuration" tab_menu: settings: "Paramètres" - rss: "RSS" + feed: "Flux" user_info: "Mon compte" password: "Mot de passe" rules: "Règles de tag automatiques" @@ -85,19 +85,19 @@ config: 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." help_language: "Vous pouvez définir la langue de l’interface de wallabag." help_pocket_consumer_key: "Nécessaire pour l’import depuis Pocket. Vous pouvez le créer depuis votre compte Pocket." - form_rss: - 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." - token_label: "Jeton RSS" + form_feed: + description: "Les flux Atom 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." + token_label: "Jeton de flux" no_token: "Aucun jeton généré" token_create: "Créez votre jeton" token_reset: "Réinitialisez votre jeton" - rss_links: "Adresses de vos flux RSS" - rss_link: + feed_links: "Adresses de vos flux" + feed_link: unread: "Non lus" starred: "Favoris" archive: "Lus" all: "Tous" - rss_limit: "Nombre d’articles dans le flux" + feed_limit: "Nombre d’articles dans le flux" form_user: two_factor_description: "Activer l’authentification double-facteur veut dire que vous allez recevoir un code par courriel OU que vous devriez utiliser une application de mot de passe à usage unique (comme Google Authenticator, Authy or FreeOTP) pour obtenir un code temporaire à chaque nouvelle connexion non approuvée. Vous ne pouvez pas choisir les deux options." name_label: "Nom" @@ -372,7 +372,7 @@ quickstart: title: "Configurez l’application" description: "Pour voir une application qui vous correspond, allez voir du côté de la configuration de wallabag." language: "Changez la langue et le design de l’application" - rss: "Activez les flux RSS" + feed: "Activez les flux Atom" tagging_rules: "Écrivez des règles pour classer automatiquement vos articles" admin: title: "Administration" @@ -590,10 +590,10 @@ flashes: password_updated: "Votre mot de passe a bien été mis à jour" password_not_updated_demo: "En démo, vous ne pouvez pas changer le mot de passe de cet utilisateur." user_updated: "Vos informations personnelles ont bien été mises à jour" - rss_updated: "La configuration des flux RSS a bien été mise à jour" + feed_updated: "La configuration des flux a bien été mise à jour" tagging_rules_updated: "Règles mises à jour" tagging_rules_deleted: "Règle supprimée" - rss_token_updated: "Jeton RSS mis à jour" + feed_token_updated: "Jeton des flux mis à jour" annotations_reset: "Annotations supprimées" tags_reset: "Tags supprimés" entries_reset: "Articles supprimés" diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml index 274e5338a..daef359f2 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml @@ -54,7 +54,7 @@ config: page_title: 'Configurazione' tab_menu: settings: 'Impostazioni' - rss: 'RSS' + feed: 'RSS' user_info: 'Informazioni utente' password: 'Password' rules: 'Regole di etichettatura' @@ -85,19 +85,19 @@ config: help_reading_speed: "wallabag calcola un tempo di lettura per ogni articolo. Puoi definire qui, grazie a questa lista, se sei un lettore lento o veloce. wallabag ricalcolerà la velocità di lettura per ogni articolo." help_language: "Puoi cambiare la lingua dell'interfaccia di wallabag." help_pocket_consumer_key: "Richiesta per importare da Pocket. La puoi creare nel tuo account Pocket." - form_rss: + form_feed: description: 'I feed RSS generati da wallabag ti permettono di leggere i tuoi contenuti salvati con il tuo lettore di RSS preferito. Prima, devi generare un token.' token_label: 'Token RSS' no_token: 'Nessun token' token_create: 'Crea il tuo token' token_reset: 'Rigenera il tuo token' - rss_links: 'Collegamenti RSS' - rss_link: + feed_links: 'Collegamenti RSS' + feed_link: unread: 'Non letti' starred: 'Preferiti' archive: 'Archiviati' # all: 'All' - rss_limit: 'Numero di elementi nel feed' + feed_limit: 'Numero di elementi nel feed' form_user: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Nome' @@ -371,7 +371,7 @@ quickstart: title: "Configura l'applicazione" description: "Per avere un'applicazione che ti soddisfi, dai un'occhiata alla configurazione di wallabag." language: 'Cambia lingua e design' - rss: 'Abilita i feed RSS' + feed: 'Abilita i feed RSS' tagging_rules: 'Scrivi delle regole per taggare automaticamente i contenuti' admin: title: 'Amministrazione' @@ -588,10 +588,10 @@ flashes: password_updated: 'Password aggiornata' password_not_updated_demo: "In modalità demo, non puoi cambiare la password dell'utente." user_updated: 'Informazioni aggiornate' - rss_updated: 'Informazioni RSS aggiornate' + feed_updated: 'Informazioni RSS aggiornate' tagging_rules_updated: 'Regole di etichettatura aggiornate' tagging_rules_deleted: 'Regola di etichettatura eliminate' - rss_token_updated: 'RSS token aggiornato' + feed_token_updated: 'RSS token aggiornato' annotations_reset: Reset annotazioni tags_reset: Reset etichette entries_reset: Reset articoli diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml index 4e5370f98..980ddeb4a 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml @@ -54,7 +54,7 @@ config: page_title: 'Configuracion' tab_menu: settings: 'Paramètres' - rss: 'RSS' + feed: 'RSS' user_info: 'Mon compte' password: 'Senhal' rules: "Règlas d'etiquetas automaticas" @@ -85,19 +85,19 @@ config: 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." help_language: "Podètz cambiar la lenga de l'interfàcia de wallabag." help_pocket_consumer_key: "Requesida per l'importacion de Pocket. Podètz la crear dins vòstre compte Pocket." - form_rss: + form_feed: description: "Los fluxes RSS fornits per wallabag vos permeton de legir vòstres articles salvagardats dins vòstre lector de fluxes preferit. Per los poder emplegar, vos cal, d'en primièr crear un geton." token_label: 'Geton RSS' no_token: 'Pas cap de geton generat' token_create: 'Creatz vòstre geton' token_reset: 'Reïnicializatz vòstre geton' - rss_links: 'URLs de vòstres fluxes RSS' - rss_link: + feed_links: 'URLs de vòstres fluxes RSS' + feed_link: unread: 'Pas legits' starred: 'Favorits' archive: 'Legits' all: 'Totes' - rss_limit: "Nombre d'articles dins un flux RSS" + feed_limit: "Nombre d'articles dins un flux" form_user: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Nom' @@ -371,7 +371,7 @@ quickstart: title: "Configuratz l'aplicacion" description: "Per fin d'aver una aplicacion que vos va ben, anatz veire la configuracion de wallabag." language: "Cambiatz la lenga e l'estil de l'aplicacion" - rss: 'Activatz los fluxes RSS' + feed: 'Activatz los fluxes RSS' tagging_rules: 'Escrivètz de règlas per classar automaticament vòstres articles' admin: title: 'Administracion' @@ -588,10 +588,10 @@ flashes: password_updated: 'Vòstre senhal es ben estat mes a jorn' password_not_updated_demo: "En demostracion, podètz pas cambiar lo senhal d'aqueste utilizaire." user_updated: 'Vòstres informacions personnelas son ben estadas mesas a jorn' - rss_updated: 'La configuracion dels fluxes RSS es ben estada mesa a jorn' + feed_updated: 'La configuracion dels fluxes RSS es ben estada mesa a jorn' tagging_rules_updated: 'Règlas misa a jorn' tagging_rules_deleted: 'Règla suprimida' - rss_token_updated: 'Geton RSS mes a jorn' + feed_token_updated: 'Geton RSS mes a jorn' annotations_reset: Anotacions levadas tags_reset: Etiquetas levadas entries_reset: Articles levats diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml index a7a4d6c39..3813ac374 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml @@ -54,7 +54,7 @@ config: page_title: 'Konfiguracja' tab_menu: settings: 'Ustawienia' - rss: 'Kanał RSS' + feed: 'Kanał RSS' user_info: 'Informacje o użytkowniku' password: 'Hasło' rules: 'Zasady tagowania' @@ -85,19 +85,19 @@ config: 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." help_language: "Możesz zmienić język interfejsu wallabag." help_pocket_consumer_key: "Wymagane dla importu z Pocket. Możesz go stworzyć na swoim koncie Pocket." - form_rss: + form_feed: description: 'Kanały RSS prowadzone przez wallabag pozwalają Ci na czytanie twoich zapisanych artykułów w twoim ulubionym czytniku RSS. Musisz najpierw wynegenerować tokena.‌' token_label: 'Token RSS' no_token: 'Brak tokena' token_create: 'Stwórz tokena' token_reset: 'Zresetuj swojego tokena' - rss_links: 'RSS links' - rss_link: + feed_links: 'RSS links' + feed_link: unread: 'Nieprzeczytane' starred: 'Oznaczone gwiazdką' archive: 'Archiwum' all: 'Wszystkie' - rss_limit: 'Link do RSS' + feed_limit: 'Link do RSS' form_user: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Nazwa' @@ -371,7 +371,7 @@ quickstart: title: 'Konfiguruj aplikację' description: 'W celu dopasowania aplikacji do swoich upodobań, zobacz konfigurację aplikacji' language: 'Zmień język i wygląd' - rss: 'Włącz kanały RSS' + feed: 'Włącz kanały RSS' tagging_rules: 'Napisz reguły pozwalające na automatyczne otagowanie twoich artykułów' admin: title: 'Administracja' @@ -588,10 +588,10 @@ flashes: password_updated: 'Hasło zaktualizowane' password_not_updated_demo: "In demonstration mode, you can't change password for this user." user_updated: 'Informacje zaktualizowane' - rss_updated: 'Informacje RSS zaktualizowane' + feed_updated: 'Informacje RSS zaktualizowane' tagging_rules_updated: 'Reguły tagowania zaktualizowane' tagging_rules_deleted: 'Reguła tagowania usunięta' - rss_token_updated: 'Token kanału RSS zaktualizowany' + feed_token_updated: 'Token kanału RSS zaktualizowany' annotations_reset: Zresetuj adnotacje tags_reset: Zresetuj tagi entries_reset: Zresetuj wpisy diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml index a5483a6d3..96943c05a 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml @@ -54,7 +54,7 @@ config: page_title: 'Config' tab_menu: settings: 'Configurações' - rss: 'RSS' + feed: 'RSS' user_info: 'Informação do Usuário' password: 'Senha' rules: 'Regras de tags' @@ -85,19 +85,19 @@ config: # 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." # help_language: "You can change the language of wallabag interface." # help_pocket_consumer_key: "Required for Pocket import. You can create it in your Pocket account." - form_rss: + form_feed: description: 'Feeds RSS providos pelo wallabag permitem que você leia seus artigos salvos em seu leitor de RSS favorito. Você precisa gerar um token primeiro.' token_label: 'Token RSS' no_token: 'Nenhum Token' token_create: 'Criar seu token' token_reset: 'Gerar novamente seu token' - rss_links: 'Links RSS' - rss_link: + feed_links: 'Links RSS' + feed_link: unread: 'Não lido' starred: 'Destacado' archive: 'Arquivado' # all: 'All' - rss_limit: 'Número de itens no feed' + feed_limit: 'Número de itens no feed' form_user: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Nome' @@ -371,7 +371,7 @@ quickstart: title: 'Configurar a aplicação' description: 'Para ter uma aplicação que atende você, dê uma olhada na configuração do wallabag.' language: 'Alterar idioma e design' - rss: 'Habilitar feeds RSS' + feed: 'Habilitar feeds RSS' tagging_rules: 'Escrever regras para acrescentar tags automaticamente em seus artigos' admin: title: 'Administração' @@ -588,10 +588,10 @@ flashes: password_updated: 'Senha atualizada' password_not_updated_demo: 'Em modo de demonstração, você não pode alterar a senha deste usuário.' # user_updated: 'Information updated' - rss_updated: 'Informação de RSS atualizada' + feed_updated: 'Informação de RSS atualizada' tagging_rules_updated: 'Regras de tags atualizadas' tagging_rules_deleted: 'Regra de tag apagada' - rss_token_updated: 'Token RSS atualizado' + feed_token_updated: 'Token RSS atualizado' # annotations_reset: Annotations reset # tags_reset: Tags reset # entries_reset: Entries reset diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml index 3b7fbd691..0ce11e74e 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml @@ -54,7 +54,7 @@ config: page_title: 'Configurație' tab_menu: settings: 'Setări' - rss: 'RSS' + feed: 'RSS' user_info: 'Informații despre utilizator' password: 'Parolă' # rules: 'Tagging rules' @@ -85,19 +85,19 @@ config: # 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." # help_language: "You can change the language of wallabag interface." # help_pocket_consumer_key: "Required for Pocket import. You can create it in your Pocket account." - form_rss: + form_feed: description: 'Feed-urile RSS oferite de wallabag îți permit să-ți citești articolele salvate în reader-ul tău preferat RSS.' token_label: 'RSS-Token' no_token: 'Fără token' token_create: 'Crează-ți token' token_reset: 'Resetează-ți token-ul' - rss_links: 'Link-uri RSS' - rss_link: + feed_links: 'Link-uri RSS' + feed_link: unread: 'Unread' starred: 'Starred' archive: 'Archived' # all: 'All' - rss_limit: 'Limită RSS' + feed_limit: 'Limită RSS' form_user: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Nume' @@ -371,7 +371,7 @@ quickstart: # title: 'Configure the application' # description: 'In order to have an application which suits you, have a look into the configuration of wallabag.' # language: 'Change language and design' - # rss: 'Enable RSS feeds' + # feed: 'Enable RSS feeds' # tagging_rules: 'Write rules to automatically tag your articles' # admin: # title: 'Administration' @@ -588,10 +588,10 @@ flashes: password_updated: 'Parolă actualizată' password_not_updated_demo: "In demonstration mode, you can't change password for this user." user_updated: 'Informație actualizată' - rss_updated: 'Informație RSS actualizată' + feed_updated: 'Informație RSS actualizată' # tagging_rules_updated: 'Tagging rules updated' # tagging_rules_deleted: 'Tagging rule deleted' - # rss_token_updated: 'RSS token updated' + # feed_token_updated: 'RSS token updated' # annotations_reset: Annotations reset # tags_reset: Tags reset # entries_reset: Entries reset diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml index 3b8a0d599..2f86f25d7 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml @@ -54,7 +54,7 @@ config: page_title: 'Yapılandırma' tab_menu: settings: 'Ayarlar' - rss: 'RSS' + feed: 'RSS' user_info: 'Kullanıcı bilgileri' password: 'Şifre' rules: 'Etiketleme kuralları' @@ -85,19 +85,19 @@ config: # 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." # help_language: "You can change the language of wallabag interface." # help_pocket_consumer_key: "Required for Pocket import. You can create it in your Pocket account." - form_rss: + form_feed: description: 'wallabag RSS akışı kaydetmiş olduğunuz makalelerini favori RSS okuyucunuzda görüntülemenizi sağlar. Bunu yapabilmek için öncelikle belirteç (token) oluşturmalısınız.' token_label: 'RSS belirteci (token)' no_token: 'Belirteç (token) yok' token_create: 'Yeni belirteç (token) oluştur' token_reset: 'Belirteci (token) sıfırla' - rss_links: 'RSS akış bağlantıları' - rss_link: + feed_links: 'RSS akış bağlantıları' + feed_link: unread: 'Okunmayan' starred: 'Favoriler' archive: 'Arşiv' # all: 'All' - rss_limit: 'RSS içeriğinden talep edilecek makale limiti' + feed_limit: 'RSS içeriğinden talep edilecek makale limiti' form_user: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'İsim' @@ -369,7 +369,7 @@ quickstart: title: 'Uygulamayı Yapılandırma' # description: 'In order to have an application which suits you, have a look into the configuration of wallabag.' language: 'Dili ve tasarımı değiştirme' - rss: 'RSS akışını aktifleştirme' + feed: 'RSS akışını aktifleştirme' # tagging_rules: 'Write rules to automatically tag your articles' admin: # title: 'Administration' @@ -566,10 +566,10 @@ flashes: password_updated: 'Şifre güncellendi' password_not_updated_demo: "In demonstration mode, you can't change password for this user." user_updated: 'Bilgiler güncellendi' - rss_updated: 'RSS bilgiler güncellendi' + feed_updated: 'RSS bilgiler güncellendi' tagging_rules_updated: 'Tagging rules updated' tagging_rules_deleted: 'Tagging rule deleted' - rss_token_updated: 'RSS token updated' + feed_token_updated: 'RSS token updated' # annotations_reset: Annotations reset # tags_reset: Tags reset # entries_reset: Entries reset diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.da.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.da.yml index c6a842098..c04389788 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.da.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.da.yml @@ -3,5 +3,5 @@ validator: password_too_short: 'Adgangskoden skal være mindst 8 tegn' # password_wrong_value: 'Wrong value for your current password' # item_per_page_too_high: 'This will certainly kill the app' - # rss_limit_too_high: 'This will certainly kill the app' + # feed_limit_too_high: 'This will certainly kill the app' # 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 907b67a5d..4c675ef40 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.de.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.de.yml @@ -3,6 +3,5 @@ validator: password_too_short: 'Kennwort-Mindestlänge von acht Zeichen nicht erfüllt' password_wrong_value: 'Falscher Wert für dein aktuelles Kennwort' item_per_page_too_high: 'Dies wird die Anwendung möglicherweise beenden' - rss_limit_too_high: 'Dies wird die Anwendung möglicherweise beenden' + feed_limit_too_high: 'Dies wird die Anwendung möglicherweise beenden' quote_length_too_high: 'Das Zitat ist zu lang. Es sollte nicht mehr als {{ limit }} Zeichen enthalten.' - diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.en.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.en.yml index 8cc117fe0..89d4c68a5 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.en.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.en.yml @@ -3,5 +3,5 @@ validator: password_too_short: 'Password should by at least 8 chars long' password_wrong_value: 'Wrong value for your current password' item_per_page_too_high: 'This will certainly kill the app' - rss_limit_too_high: 'This will certainly kill the app' + feed_limit_too_high: 'This will certainly kill the app' 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 97a8edfa4..ba34ee764 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.es.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.es.yml @@ -3,5 +3,5 @@ validator: password_too_short: 'La contraseña debe tener al menos 8 carácteres' password_wrong_value: 'Entrada equivocada para su contraseña actual' item_per_page_too_high: 'Esto matará la aplicación' - rss_limit_too_high: 'Esto matará la aplicación' + feed_limit_too_high: 'Esto matará la aplicación' # 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 ef677525d..9b1a4af2b 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.fa.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.fa.yml @@ -3,5 +3,5 @@ validator: password_too_short: 'رمز شما باید ۸ حرف یا بیشتر باشد' password_wrong_value: 'رمز فعلی را اشتباه وارد کرده‌اید' item_per_page_too_high: 'با این تعداد برنامه به فنا می‌رود' - rss_limit_too_high: 'با این تعداد برنامه به فنا می‌رود' + feed_limit_too_high: 'با این تعداد برنامه به فنا می‌رود' # 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 f31b4ed27..92f69aa03 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.fr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.fr.yml @@ -3,5 +3,5 @@ validator: password_too_short: "Le mot de passe doit contenir au moins 8 caractères" password_wrong_value: "Votre mot de passe actuel est faux" item_per_page_too_high: "Ça ne va pas plaire à l’application" - rss_limit_too_high: "Ça ne va pas plaire à l’application" + feed_limit_too_high: "Ça ne va pas plaire à l’application" 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 d949cc3bd..b20d6f513 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.it.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.it.yml @@ -3,5 +3,5 @@ validator: password_too_short: 'La password deve essere lunga almeno 8 caratteri' password_wrong_value: 'Valore inserito per la password corrente errato' item_per_page_too_high: 'Questo valore è troppo alto' - rss_limit_too_high: 'Questo valore è troppo alto' + feed_limit_too_high: 'Questo valore è troppo alto' # 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 87f00f101..cb57844fe 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.oc.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.oc.yml @@ -3,5 +3,5 @@ validator: password_too_short: 'Lo senhal deu aver almens 8 caractèrs' password_wrong_value: 'Vòstre senhal actual es pas bon' item_per_page_too_high: "Aquò li agradarà pas a l'aplicacion" - rss_limit_too_high: "Aquò li agradarà pas a l'aplicacion" + feed_limit_too_high: "Aquò li agradarà pas a l'aplicacion" 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 e4165c148..94757cc50 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.pl.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.pl.yml @@ -3,5 +3,5 @@ validator: password_too_short: 'Hasło powinno mieć minimum 8 znaków długości' password_wrong_value: 'Twoje obecne hasło jest błędne' item_per_page_too_high: 'To może spowodować problemy z aplikacją' - rss_limit_too_high: 'To może spowodować problemy z aplikacją' + feed_limit_too_high: 'To może spowodować problemy z aplikacją' 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 a8c1f9de4..df2f3f353 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.pt.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.pt.yml @@ -3,5 +3,5 @@ validator: password_too_short: 'A senha deve ter pelo menos 8 caracteres' password_wrong_value: 'A senha atual informada está errada' item_per_page_too_high: 'Certamente isso pode matar a aplicação' - rss_limit_too_high: 'Certamente isso pode matar a aplicação' + feed_limit_too_high: 'Certamente isso pode matar a aplicação' # 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 6840cf115..e5c8a72f1 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.ro.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.ro.yml @@ -3,5 +3,5 @@ validator: password_too_short: 'Parola ar trebui să conțină cel puțin 8 caractere' # password_wrong_value: 'Wrong value for your current password' # item_per_page_too_high: 'This will certainly kill the app' - # rss_limit_too_high: 'This will certainly kill the app' + # feed_limit_too_high: 'This will certainly kill the app' # quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.tr.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.tr.yml index e1e7317f7..881ffd3be 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.tr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.tr.yml @@ -3,5 +3,5 @@ validator: # password_too_short: 'Password should by at least 8 chars long' # password_wrong_value: 'Wrong value for your current password' # item_per_page_too_high: 'This will certainly kill the app' - # rss_limit_too_high: 'This will certainly kill the app' + # feed_limit_too_high: 'This will certainly kill the app' # 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 93f8ddf8a..4ef6ab3cc 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 @@ -94,43 +94,43 @@ {{ form_rest(form.config) }} -

    {{ 'config.tab_menu.rss'|trans }}

    +

    {{ 'config.tab_menu.feed'|trans }}

    - {{ form_start(form.rss) }} - {{ form_errors(form.rss) }} + {{ form_start(form.feed) }} + {{ form_errors(form.feed) }}
    - {{ 'config.form_rss.description'|trans }} + {{ 'config.form_feed.description'|trans }}
    - - {% if rss.token %} - {{ rss.token }} + + {% if feed.token %} + {{ feed.token }} {% else %} - {{ 'config.form_rss.no_token'|trans }} + {{ 'config.form_feed.no_token'|trans }} {% endif %} – - {% if rss.token %} - {{ 'config.form_rss.token_reset'|trans }} + {% if feed.token %} + {{ 'config.form_feed.token_reset'|trans }} {% else %} - {{ 'config.form_rss.token_create'|trans }} + {{ 'config.form_feed.token_create'|trans }} {% endif %}
    - {% if rss.token %} + {% if feed.token %}
    @@ -138,13 +138,13 @@
    - {{ form_label(form.rss.rss_limit) }} - {{ form_errors(form.rss.rss_limit) }} - {{ form_widget(form.rss.rss_limit) }} + {{ form_label(form.feed.feed_limit) }} + {{ form_errors(form.feed.feed_limit) }} + {{ form_widget(form.feed.feed_limit) }}
    - {{ form_rest(form.rss) }} + {{ form_rest(form.feed) }}

    {{ 'config.tab_menu.user_info'|trans }}

    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 fb296c9d2..6c5d26016 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 @@ -2,8 +2,8 @@ {% block head %} {{ parent() }} - {% if tag is defined and app.user.config.rssToken %} - + {% if tag is defined and app.user.config.feedToken %} + {% endif %} {% endblock %} @@ -28,8 +28,8 @@
    {{ 'entry.list.number_on_the_page'|transchoice(entries.count) }}
  • {% endfor %} diff --git a/src/Wallabag/CoreBundle/Tools/Utils.php b/src/Wallabag/CoreBundle/Tools/Utils.php index e56e251e5..b7ad79664 100644 --- a/src/Wallabag/CoreBundle/Tools/Utils.php +++ b/src/Wallabag/CoreBundle/Tools/Utils.php @@ -5,7 +5,7 @@ namespace Wallabag\CoreBundle\Tools; class Utils { /** - * Generate a token used for RSS. + * Generate a token used for Feeds. * * @param int $length Length of the token * diff --git a/src/Wallabag/CoreBundle/Twig/WallabagExtension.php b/src/Wallabag/CoreBundle/Twig/WallabagExtension.php index 00b1e5954..61107ce72 100644 --- a/src/Wallabag/CoreBundle/Twig/WallabagExtension.php +++ b/src/Wallabag/CoreBundle/Twig/WallabagExtension.php @@ -28,6 +28,7 @@ class WallabagExtension extends \Twig_Extension implements \Twig_Extension_Globa { return [ new \Twig_SimpleFilter('removeWww', [$this, 'removeWww']), + new \Twig_SimpleFilter('removeScheme', [$this, 'removeScheme']), new \Twig_SimpleFilter('removeSchemeAndWww', [$this, 'removeSchemeAndWww']), ]; } @@ -46,11 +47,14 @@ class WallabagExtension extends \Twig_Extension implements \Twig_Extension_Globa return preg_replace('/^www\./i', '', $url); } + public function removeScheme($url) + { + return preg_replace('#^https?://#i', '', $url); + } + public function removeSchemeAndWww($url) { - return $this->removeWww( - preg_replace('@^https?://@i', '', $url) - ); + return $this->removeWww($this->removeScheme($url) } /** diff --git a/src/Wallabag/UserBundle/EventListener/CreateConfigListener.php b/src/Wallabag/UserBundle/EventListener/CreateConfigListener.php index 5cabfd35a..81954213f 100644 --- a/src/Wallabag/UserBundle/EventListener/CreateConfigListener.php +++ b/src/Wallabag/UserBundle/EventListener/CreateConfigListener.php @@ -18,19 +18,19 @@ class CreateConfigListener implements EventSubscriberInterface private $em; private $theme; private $itemsOnPage; - private $rssLimit; + private $feedLimit; private $language; private $readingSpeed; private $actionMarkAsRead; private $listMode; private $session; - public function __construct(EntityManager $em, $theme, $itemsOnPage, $rssLimit, $language, $readingSpeed, $actionMarkAsRead, $listMode, Session $session) + public function __construct(EntityManager $em, $theme, $itemsOnPage, $feedLimit, $language, $readingSpeed, $actionMarkAsRead, $listMode, Session $session) { $this->em = $em; $this->theme = $theme; $this->itemsOnPage = $itemsOnPage; - $this->rssLimit = $rssLimit; + $this->feedLimit = $feedLimit; $this->language = $language; $this->readingSpeed = $readingSpeed; $this->actionMarkAsRead = $actionMarkAsRead; @@ -54,7 +54,7 @@ class CreateConfigListener implements EventSubscriberInterface $config = new Config($event->getUser()); $config->setTheme($this->theme); $config->setItemsPerPage($this->itemsOnPage); - $config->setRssLimit($this->rssLimit); + $config->setFeedLimit($this->feedLimit); $config->setLanguage($this->session->get('_locale', $this->language)); $config->setReadingSpeed($this->readingSpeed); $config->setActionMarkAsRead($this->actionMarkAsRead); diff --git a/src/Wallabag/UserBundle/Repository/UserRepository.php b/src/Wallabag/UserBundle/Repository/UserRepository.php index be693d3b1..803911095 100644 --- a/src/Wallabag/UserBundle/Repository/UserRepository.php +++ b/src/Wallabag/UserBundle/Repository/UserRepository.php @@ -9,18 +9,18 @@ use Wallabag\UserBundle\Entity\User; class UserRepository extends EntityRepository { /** - * Find a user by its username and rss roken. + * Find a user by its username and Feed token. * * @param string $username - * @param string $rssToken + * @param string $feedToken * - * @return User|null + * @return null|User */ - public function findOneByUsernameAndRsstoken($username, $rssToken) + public function findOneByUsernameAndFeedtoken($username, $feedToken) { return $this->createQueryBuilder('u') ->leftJoin('u.config', 'c') - ->where('c.rssToken = :rss_token')->setParameter('rss_token', $rssToken) + ->where('c.feedToken = :feed_token')->setParameter('feed_token', $feedToken) ->andWhere('u.username = :username')->setParameter('username', $username) ->getQuery() ->getOneOrNullResult(); diff --git a/src/Wallabag/UserBundle/Resources/config/services.yml b/src/Wallabag/UserBundle/Resources/config/services.yml index 72cda3f8a..2dcf30111 100644 --- a/src/Wallabag/UserBundle/Resources/config/services.yml +++ b/src/Wallabag/UserBundle/Resources/config/services.yml @@ -28,7 +28,7 @@ services: - "@doctrine.orm.entity_manager" - "%wallabag_core.theme%" - "%wallabag_core.items_on_page%" - - "%wallabag_core.rss_limit%" + - "%wallabag_core.feed_limit%" - "%wallabag_core.language%" - "%wallabag_core.reading_speed%" - "%wallabag_core.action_mark_as_read%" diff --git a/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php b/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php index 1090a686b..d8478ce3e 100644 --- a/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php @@ -33,7 +33,7 @@ class ConfigControllerTest extends WallabagCoreTestCase $this->assertCount(1, $crawler->filter('button[id=config_save]')); $this->assertCount(1, $crawler->filter('button[id=change_passwd_save]')); $this->assertCount(1, $crawler->filter('button[id=update_user_save]')); - $this->assertCount(1, $crawler->filter('button[id=rss_config_save]')); + $this->assertCount(1, $crawler->filter('button[id=feed_config_save]')); } public function testUpdate() @@ -297,7 +297,7 @@ class ConfigControllerTest extends WallabagCoreTestCase $this->assertContains('flashes.config.notice.user_updated', $alert[0]); } - public function testRssUpdateResetToken() + public function testFeedUpdateResetToken() { $this->logInAs('admin'); $client = $this->getClient(); @@ -313,7 +313,7 @@ class ConfigControllerTest extends WallabagCoreTestCase } $config = $user->getConfig(); - $config->setRssToken(null); + $config->setFeedToken(null); $em->persist($config); $em->flush(); @@ -322,7 +322,7 @@ class ConfigControllerTest extends WallabagCoreTestCase $this->assertSame(200, $client->getResponse()->getStatusCode()); $this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text'])); - $this->assertContains('config.form_rss.no_token', $body[0]); + $this->assertContains('config.form_feed.no_token', $body[0]); $client->request('GET', '/generate-token'); $this->assertSame(302, $client->getResponse()->getStatusCode()); @@ -330,7 +330,7 @@ class ConfigControllerTest extends WallabagCoreTestCase $crawler = $client->followRedirect(); $this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text'])); - $this->assertNotContains('config.form_rss.no_token', $body[0]); + $this->assertNotContains('config.form_feed.no_token', $body[0]); } public function testGenerateTokenAjax() @@ -351,7 +351,7 @@ class ConfigControllerTest extends WallabagCoreTestCase $this->assertArrayHasKey('token', $content); } - public function testRssUpdate() + public function testFeedUpdate() { $this->logInAs('admin'); $client = $this->getClient(); @@ -360,10 +360,10 @@ class ConfigControllerTest extends WallabagCoreTestCase $this->assertSame(200, $client->getResponse()->getStatusCode()); - $form = $crawler->filter('button[id=rss_config_save]')->form(); + $form = $crawler->filter('button[id=feed_config_save]')->form(); $data = [ - 'rss_config[rss_limit]' => 12, + 'feed_config[feed_limit]' => 12, ]; $client->submit($form, $data); @@ -372,31 +372,31 @@ class ConfigControllerTest extends WallabagCoreTestCase $crawler = $client->followRedirect(); - $this->assertContains('flashes.config.notice.rss_updated', $crawler->filter('body')->extract(['_text'])[0]); + $this->assertContains('flashes.config.notice.feed_updated', $crawler->filter('body')->extract(['_text'])[0]); } - public function dataForRssFailed() + public function dataForFeedFailed() { return [ [ [ - 'rss_config[rss_limit]' => 0, + 'feed_config[feed_limit]' => 0, ], 'This value should be 1 or more.', ], [ [ - 'rss_config[rss_limit]' => 1000000000000, + 'feed_config[feed_limit]' => 1000000000000, ], - 'validator.rss_limit_too_high', + 'validator.feed_limit_too_high', ], ]; } /** - * @dataProvider dataForRssFailed + * @dataProvider dataForFeedFailed */ - public function testRssFailed($data, $expectedMessage) + public function testFeedFailed($data, $expectedMessage) { $this->logInAs('admin'); $client = $this->getClient(); @@ -405,7 +405,7 @@ class ConfigControllerTest extends WallabagCoreTestCase $this->assertSame(200, $client->getResponse()->getStatusCode()); - $form = $crawler->filter('button[id=rss_config_save]')->form(); + $form = $crawler->filter('button[id=feed_config_save]')->form(); $crawler = $client->submit($form, $data); diff --git a/tests/Wallabag/CoreBundle/Controller/FeedControllerTest.php b/tests/Wallabag/CoreBundle/Controller/FeedControllerTest.php new file mode 100644 index 000000000..7442e8a4f --- /dev/null +++ b/tests/Wallabag/CoreBundle/Controller/FeedControllerTest.php @@ -0,0 +1,228 @@ +loadXML($xml); + + $xpath = new \DOMXpath($doc); + $xpath->registerNamespace('a', 'http://www.w3.org/2005/Atom'); + + if (null === $nb) { + $this->assertGreaterThan(0, $xpath->query('//a:entry')->length); + } else { + $this->assertEquals($nb, $xpath->query('//a:entry')->length); + } + + $this->assertEquals(1, $xpath->query('/a:feed')->length); + + $this->assertEquals(1, $xpath->query('/a:feed/a:title')->length); + $this->assertContains('favicon.ico', $xpath->query('/a:feed/a:icon')->item(0)->nodeValue); + $this->assertContains('logo-square.png', $xpath->query('/a:feed/a:logo')->item(0)->nodeValue); + + $this->assertEquals(1, $xpath->query('/a:feed/a:updated')->length); + + $this->assertEquals(1, $xpath->query('/a:feed/a:generator')->length); + $this->assertEquals('wallabag', $xpath->query('/a:feed/a:generator')->item(0)->nodeValue); + $this->assertEquals('admin', $xpath->query('/a:feed/a:author/a:name')->item(0)->nodeValue); + + $this->assertEquals(1, $xpath->query('/a:feed/a:subtitle')->length); + if (null !== $tagValue && 0 === strpos($type, 'tag')) { + $this->assertEquals('wallabag — '.$type.' '.$tagValue.' feed', $xpath->query('/a:feed/a:title')->item(0)->nodeValue); + $this->assertEquals('Atom feed for entries tagged with ' . $tagValue, $xpath->query('/a:feed/a:subtitle')->item(0)->nodeValue); + } else { + $this->assertEquals('wallabag — '.$type.' feed', $xpath->query('/a:feed/a:title')->item(0)->nodeValue); + $this->assertEquals('Atom feed for ' . $type . ' entries', $xpath->query('/a:feed/a:subtitle')->item(0)->nodeValue); + } + + $this->assertEquals(1, $xpath->query('/a:feed/a:link[@rel="self"]')->length); + $this->assertContains($type, $xpath->query('/a:feed/a:link[@rel="self"]')->item(0)->getAttribute('href')); + + $this->assertEquals(1, $xpath->query('/a:feed/a:link[@rel="last"]')->length); + + foreach ($xpath->query('//a:entry') as $item) { + $this->assertEquals(1, $xpath->query('a:title', $item)->length); + $this->assertEquals(1, $xpath->query('a:link[@rel="via"]', $item)->length); + $this->assertEquals(1, $xpath->query('a:link[@rel="alternate"]', $item)->length); + $this->assertEquals(1, $xpath->query('a:id', $item)->length); + $this->assertEquals(1, $xpath->query('a:published', $item)->length); + $this->assertEquals(1, $xpath->query('a:content', $item)->length); + } + } + + public function dataForBadUrl() + { + return [ + [ + '/feed/admin/YZIOAUZIAO/unread', + ], + [ + '/feed/wallace/YZIOAUZIAO/starred', + ], + [ + '/feed/wallace/YZIOAUZIAO/archives', + ], + [ + '/feed/wallace/YZIOAUZIAO/all', + ], + ]; + } + + /** + * @dataProvider dataForBadUrl + */ + public function testBadUrl($url) + { + $client = $this->getClient(); + + $client->request('GET', $url); + + $this->assertSame(404, $client->getResponse()->getStatusCode()); + } + + public function testUnread() + { + $client = $this->getClient(); + $em = $client->getContainer()->get('doctrine.orm.entity_manager'); + $user = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUsername('admin'); + + $config = $user->getConfig(); + $config->setFeedToken('SUPERTOKEN'); + $config->setFeedLimit(2); + $em->persist($config); + $em->flush(); + + $client->request('GET', '/feed/admin/SUPERTOKEN/unread'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $this->validateDom($client->getResponse()->getContent(), 'unread', 2); + } + + public function testStarred() + { + $client = $this->getClient(); + $em = $client->getContainer()->get('doctrine.orm.entity_manager'); + $user = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUsername('admin'); + + $config = $user->getConfig(); + $config->setFeedToken('SUPERTOKEN'); + $config->setFeedLimit(1); + $em->persist($config); + $em->flush(); + + $client = $this->getClient(); + $client->request('GET', '/feed/admin/SUPERTOKEN/starred'); + + $this->assertSame(200, $client->getResponse()->getStatusCode(), 1); + + $this->validateDom($client->getResponse()->getContent(), 'starred'); + } + + public function testArchives() + { + $client = $this->getClient(); + $em = $client->getContainer()->get('doctrine.orm.entity_manager'); + $user = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUsername('admin'); + + $config = $user->getConfig(); + $config->setFeedToken('SUPERTOKEN'); + $config->setFeedLimit(null); + $em->persist($config); + $em->flush(); + + $client = $this->getClient(); + $client->request('GET', '/feed/admin/SUPERTOKEN/archive'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $this->validateDom($client->getResponse()->getContent(), 'archive'); + } + + public function testAll() + { + $client = $this->getClient(); + $em = $client->getContainer()->get('doctrine.orm.entity_manager'); + $user = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUsername('admin'); + + $config = $user->getConfig(); + $config->setFeedToken('SUPERTOKEN'); + $config->setFeedLimit(null); + $em->persist($config); + $em->flush(); + + $client = $this->getClient(); + $client->request('GET', '/feed/admin/SUPERTOKEN/all'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $this->validateDom($client->getResponse()->getContent(), 'all'); + } + + public function testPagination() + { + $client = $this->getClient(); + $em = $client->getContainer()->get('doctrine.orm.entity_manager'); + $user = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUsername('admin'); + + $config = $user->getConfig(); + $config->setFeedToken('SUPERTOKEN'); + $config->setFeedLimit(1); + $em->persist($config); + $em->flush(); + + $client = $this->getClient(); + + $client->request('GET', '/feed/admin/SUPERTOKEN/unread'); + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + $this->validateDom($client->getResponse()->getContent(), 'unread'); + + $client->request('GET', '/feed/admin/SUPERTOKEN/unread/2'); + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + $this->validateDom($client->getResponse()->getContent(), 'unread'); + + $client->request('GET', '/feed/admin/SUPERTOKEN/unread/3000'); + $this->assertEquals(302, $client->getResponse()->getStatusCode()); + } + + public function testTags() + { + $client = $this->getClient(); + $em = $client->getContainer()->get('doctrine.orm.entity_manager'); + $user = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUsername('admin'); + + $config = $user->getConfig(); + $config->setFeedToken('SUPERTOKEN'); + $config->setFeedLimit(null); + $em->persist($config); + $em->flush(); + + $client = $this->getClient(); + $client->request('GET', '/admin/SUPERTOKEN/tags/foo-bar.xml'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $this->validateDom($client->getResponse()->getContent(), 'tag', 2, 'foo-bar'); + + $client->request('GET', '/admin/SUPERTOKEN/tags/foo-bar.xml?page=3000'); + $this->assertSame(302, $client->getResponse()->getStatusCode()); + } +} diff --git a/tests/Wallabag/CoreBundle/Controller/RssControllerTest.php b/tests/Wallabag/CoreBundle/Controller/RssControllerTest.php deleted file mode 100644 index afa906210..000000000 --- a/tests/Wallabag/CoreBundle/Controller/RssControllerTest.php +++ /dev/null @@ -1,221 +0,0 @@ -loadXML($xml); - - $xpath = new \DOMXPath($doc); - - if (null === $nb) { - $this->assertGreaterThan(0, $xpath->query('//item')->length); - } else { - $this->assertSame($nb, $xpath->query('//item')->length); - } - - $this->assertSame(1, $xpath->query('/rss')->length); - $this->assertSame(1, $xpath->query('/rss/channel')->length); - - $this->assertSame(1, $xpath->query('/rss/channel/title')->length); - $this->assertSame('wallabag - ' . $type . ' feed', $xpath->query('/rss/channel/title')->item(0)->nodeValue); - - $this->assertSame(1, $xpath->query('/rss/channel/pubDate')->length); - - $this->assertSame(1, $xpath->query('/rss/channel/generator')->length); - $this->assertSame('wallabag', $xpath->query('/rss/channel/generator')->item(0)->nodeValue); - - $this->assertSame(1, $xpath->query('/rss/channel/description')->length); - $this->assertSame('wallabag ' . $type . ' elements', $xpath->query('/rss/channel/description')->item(0)->nodeValue); - - $this->assertSame(1, $xpath->query('/rss/channel/link[@rel="self"]')->length); - $this->assertContains($urlPagination . '.xml', $xpath->query('/rss/channel/link[@rel="self"]')->item(0)->getAttribute('href')); - - $this->assertSame(1, $xpath->query('/rss/channel/link[@rel="last"]')->length); - $this->assertContains($urlPagination . '.xml?page=', $xpath->query('/rss/channel/link[@rel="last"]')->item(0)->getAttribute('href')); - - foreach ($xpath->query('//item') as $item) { - $this->assertSame(1, $xpath->query('title', $item)->length); - $this->assertSame(1, $xpath->query('source', $item)->length); - $this->assertSame(1, $xpath->query('link', $item)->length); - $this->assertSame(1, $xpath->query('guid', $item)->length); - $this->assertSame(1, $xpath->query('pubDate', $item)->length); - $this->assertSame(1, $xpath->query('description', $item)->length); - } - } - - public function dataForBadUrl() - { - return [ - [ - '/admin/YZIOAUZIAO/unread.xml', - ], - [ - '/wallace/YZIOAUZIAO/starred.xml', - ], - [ - '/wallace/YZIOAUZIAO/archives.xml', - ], - [ - '/wallace/YZIOAUZIAO/all.xml', - ], - ]; - } - - /** - * @dataProvider dataForBadUrl - */ - public function testBadUrl($url) - { - $client = $this->getClient(); - - $client->request('GET', $url); - - $this->assertSame(404, $client->getResponse()->getStatusCode()); - } - - public function testUnread() - { - $client = $this->getClient(); - $em = $client->getContainer()->get('doctrine.orm.entity_manager'); - $user = $em - ->getRepository('WallabagUserBundle:User') - ->findOneByUsername('admin'); - - $config = $user->getConfig(); - $config->setRssToken('SUPERTOKEN'); - $config->setRssLimit(2); - $em->persist($config); - $em->flush(); - - $client->request('GET', '/admin/SUPERTOKEN/unread.xml'); - - $this->assertSame(200, $client->getResponse()->getStatusCode()); - - $this->validateDom($client->getResponse()->getContent(), 'unread', 'unread', 2); - } - - public function testStarred() - { - $client = $this->getClient(); - $em = $client->getContainer()->get('doctrine.orm.entity_manager'); - $user = $em - ->getRepository('WallabagUserBundle:User') - ->findOneByUsername('admin'); - - $config = $user->getConfig(); - $config->setRssToken('SUPERTOKEN'); - $config->setRssLimit(1); - $em->persist($config); - $em->flush(); - - $client = $this->getClient(); - $client->request('GET', '/admin/SUPERTOKEN/starred.xml'); - - $this->assertSame(200, $client->getResponse()->getStatusCode(), 1); - - $this->validateDom($client->getResponse()->getContent(), 'starred', 'starred'); - } - - public function testArchives() - { - $client = $this->getClient(); - $em = $client->getContainer()->get('doctrine.orm.entity_manager'); - $user = $em - ->getRepository('WallabagUserBundle:User') - ->findOneByUsername('admin'); - - $config = $user->getConfig(); - $config->setRssToken('SUPERTOKEN'); - $config->setRssLimit(null); - $em->persist($config); - $em->flush(); - - $client = $this->getClient(); - $client->request('GET', '/admin/SUPERTOKEN/archive.xml'); - - $this->assertSame(200, $client->getResponse()->getStatusCode()); - - $this->validateDom($client->getResponse()->getContent(), 'archive', 'archive'); - } - - public function testAll() - { - $client = $this->getClient(); - $em = $client->getContainer()->get('doctrine.orm.entity_manager'); - $user = $em - ->getRepository('WallabagUserBundle:User') - ->findOneByUsername('admin'); - - $config = $user->getConfig(); - $config->setRssToken('SUPERTOKEN'); - $config->setRssLimit(null); - $em->persist($config); - $em->flush(); - - $client = $this->getClient(); - $client->request('GET', '/admin/SUPERTOKEN/all.xml'); - - $this->assertSame(200, $client->getResponse()->getStatusCode()); - - $this->validateDom($client->getResponse()->getContent(), 'all', 'all'); - } - - public function testPagination() - { - $client = $this->getClient(); - $em = $client->getContainer()->get('doctrine.orm.entity_manager'); - $user = $em - ->getRepository('WallabagUserBundle:User') - ->findOneByUsername('admin'); - - $config = $user->getConfig(); - $config->setRssToken('SUPERTOKEN'); - $config->setRssLimit(1); - $em->persist($config); - $em->flush(); - - $client = $this->getClient(); - - $client->request('GET', '/admin/SUPERTOKEN/unread.xml'); - $this->assertSame(200, $client->getResponse()->getStatusCode()); - $this->validateDom($client->getResponse()->getContent(), 'unread', 'unread'); - - $client->request('GET', '/admin/SUPERTOKEN/unread.xml?page=2'); - $this->assertSame(200, $client->getResponse()->getStatusCode()); - $this->validateDom($client->getResponse()->getContent(), 'unread', 'unread'); - - $client->request('GET', '/admin/SUPERTOKEN/unread.xml?page=3000'); - $this->assertSame(302, $client->getResponse()->getStatusCode()); - } - - public function testTags() - { - $client = $this->getClient(); - $em = $client->getContainer()->get('doctrine.orm.entity_manager'); - $user = $em - ->getRepository('WallabagUserBundle:User') - ->findOneByUsername('admin'); - - $config = $user->getConfig(); - $config->setRssToken('SUPERTOKEN'); - $config->setRssLimit(null); - $em->persist($config); - $em->flush(); - - $client = $this->getClient(); - $client->request('GET', '/admin/SUPERTOKEN/tags/foo.xml'); - - $this->assertSame(200, $client->getResponse()->getStatusCode()); - - $this->validateDom($client->getResponse()->getContent(), 'tag (foo)', 'tags/foo'); - - $client->request('GET', '/admin/SUPERTOKEN/tags/foo.xml?page=3000'); - $this->assertSame(302, $client->getResponse()->getStatusCode()); - } -} diff --git a/tests/Wallabag/CoreBundle/Controller/SecurityControllerTest.php b/tests/Wallabag/CoreBundle/Controller/SecurityControllerTest.php index b03c7550d..3c3354d7f 100644 --- a/tests/Wallabag/CoreBundle/Controller/SecurityControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/SecurityControllerTest.php @@ -23,7 +23,7 @@ class SecurityControllerTest extends WallabagCoreTestCase $client->followRedirects(); $crawler = $client->request('GET', '/config'); - $this->assertContains('config.form_rss.description', $crawler->filter('body')->extract(['_text'])[0]); + $this->assertContains('config.form_feed.description', $crawler->filter('body')->extract(['_text'])[0]); } public function testLoginWith2FactorEmail() diff --git a/tests/Wallabag/CoreBundle/ParamConverter/UsernameRssTokenConverterTest.php b/tests/Wallabag/CoreBundle/ParamConverter/UsernameFeedTokenConverterTest.php similarity index 90% rename from tests/Wallabag/CoreBundle/ParamConverter/UsernameRssTokenConverterTest.php rename to tests/Wallabag/CoreBundle/ParamConverter/UsernameFeedTokenConverterTest.php index 800af5c9d..92fe38cd4 100644 --- a/tests/Wallabag/CoreBundle/ParamConverter/UsernameRssTokenConverterTest.php +++ b/tests/Wallabag/CoreBundle/ParamConverter/UsernameFeedTokenConverterTest.php @@ -5,15 +5,15 @@ namespace Tests\Wallabag\CoreBundle\ParamConverter; use PHPUnit\Framework\TestCase; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Symfony\Component\HttpFoundation\Request; -use Wallabag\CoreBundle\ParamConverter\UsernameRssTokenConverter; +use Wallabag\CoreBundle\ParamConverter\UsernameFeedTokenConverter; use Wallabag\UserBundle\Entity\User; -class UsernameRssTokenConverterTest extends TestCase +class UsernameFeedTokenConverterTest extends TestCase { public function testSupportsWithNoRegistry() { $params = new ParamConverter([]); - $converter = new UsernameRssTokenConverter(); + $converter = new UsernameFeedTokenConverter(); $this->assertFalse($converter->supports($params)); } @@ -29,7 +29,7 @@ class UsernameRssTokenConverterTest extends TestCase ->will($this->returnValue([])); $params = new ParamConverter([]); - $converter = new UsernameRssTokenConverter($registry); + $converter = new UsernameFeedTokenConverter($registry); $this->assertFalse($converter->supports($params)); } @@ -45,7 +45,7 @@ class UsernameRssTokenConverterTest extends TestCase ->will($this->returnValue(['default' => null])); $params = new ParamConverter([]); - $converter = new UsernameRssTokenConverter($registry); + $converter = new UsernameFeedTokenConverter($registry); $this->assertFalse($converter->supports($params)); } @@ -83,7 +83,7 @@ class UsernameRssTokenConverterTest extends TestCase ->will($this->returnValue($em)); $params = new ParamConverter(['class' => 'superclass']); - $converter = new UsernameRssTokenConverter($registry); + $converter = new UsernameFeedTokenConverter($registry); $this->assertFalse($converter->supports($params)); } @@ -121,7 +121,7 @@ class UsernameRssTokenConverterTest extends TestCase ->will($this->returnValue($em)); $params = new ParamConverter(['class' => 'WallabagUserBundle:User']); - $converter = new UsernameRssTokenConverter($registry); + $converter = new UsernameFeedTokenConverter($registry); $this->assertTrue($converter->supports($params)); } @@ -129,7 +129,7 @@ class UsernameRssTokenConverterTest extends TestCase public function testApplyEmptyRequest() { $params = new ParamConverter([]); - $converter = new UsernameRssTokenConverter(); + $converter = new UsernameFeedTokenConverter(); $res = $converter->apply(new Request(), $params); @@ -147,7 +147,7 @@ class UsernameRssTokenConverterTest extends TestCase ->getMock(); $repo->expects($this->once()) - ->method('findOneByUsernameAndRsstoken') + ->method('findOneByUsernameAndFeedToken') ->with('test', 'test') ->will($this->returnValue(null)); @@ -170,7 +170,7 @@ class UsernameRssTokenConverterTest extends TestCase ->will($this->returnValue($em)); $params = new ParamConverter(['class' => 'WallabagUserBundle:User']); - $converter = new UsernameRssTokenConverter($registry); + $converter = new UsernameFeedTokenConverter($registry); $request = new Request([], [], ['username' => 'test', 'token' => 'test']); $converter->apply($request, $params); @@ -185,7 +185,7 @@ class UsernameRssTokenConverterTest extends TestCase ->getMock(); $repo->expects($this->once()) - ->method('findOneByUsernameAndRsstoken') + ->method('findOneByUsernameAndFeedtoken') ->with('test', 'test') ->will($this->returnValue($user)); @@ -208,7 +208,7 @@ class UsernameRssTokenConverterTest extends TestCase ->will($this->returnValue($em)); $params = new ParamConverter(['class' => 'WallabagUserBundle:User', 'name' => 'user']); - $converter = new UsernameRssTokenConverter($registry); + $converter = new UsernameFeedTokenConverter($registry); $request = new Request([], [], ['username' => 'test', 'token' => 'test']); $converter->apply($request, $params); diff --git a/tests/Wallabag/CoreBundle/Twig/WallabagExtensionTest.php b/tests/Wallabag/CoreBundle/Twig/WallabagExtensionTest.php index bb92f7458..3fd90fda1 100644 --- a/tests/Wallabag/CoreBundle/Twig/WallabagExtensionTest.php +++ b/tests/Wallabag/CoreBundle/Twig/WallabagExtensionTest.php @@ -32,6 +32,31 @@ class WallabagExtensionTest extends TestCase $this->assertSame('gist.github.com', $extension->removeWww('gist.github.com')); } + public function testRemoveScheme() + { + $entryRepository = $this->getMockBuilder('Wallabag\CoreBundle\Repository\EntryRepository') + ->disableOriginalConstructor() + ->getMock(); + + $tagRepository = $this->getMockBuilder('Wallabag\CoreBundle\Repository\TagRepository') + ->disableOriginalConstructor() + ->getMock(); + + $tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface') + ->disableOriginalConstructor() + ->getMock(); + + $translator = $this->getMockBuilder('Symfony\Component\Translation\TranslatorInterface') + ->disableOriginalConstructor() + ->getMock(); + + $extension = new WallabagExtension($entryRepository, $tagRepository, $tokenStorage, 0, $translator); + + $this->assertEquals('lemonde.fr', $extension->removeScheme('lemonde.fr')); + $this->assertEquals('gist.github.com', $extension->removeScheme('gist.github.com')); + $this->assertEquals('gist.github.com', $extension->removeScheme('https://gist.github.com')); + } + public function testRemoveSchemeAndWww() { $entryRepository = $this->getMockBuilder('Wallabag\CoreBundle\Repository\EntryRepository') diff --git a/tests/Wallabag/UserBundle/EventListener/CreateConfigListenerTest.php b/tests/Wallabag/UserBundle/EventListener/CreateConfigListenerTest.php index c13bfbea9..fd32f3803 100644 --- a/tests/Wallabag/UserBundle/EventListener/CreateConfigListenerTest.php +++ b/tests/Wallabag/UserBundle/EventListener/CreateConfigListenerTest.php @@ -62,7 +62,7 @@ class CreateConfigListenerTest extends TestCase $config = new Config($user); $config->setTheme('baggy'); $config->setItemsPerPage(20); - $config->setRssLimit(50); + $config->setFeedLimit(50); $config->setLanguage('fr'); $config->setReadingSpeed(1); From f277bc042c8e805aab14b31b5b51e2878d80c6f4 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Thu, 25 Apr 2019 14:12:56 +0200 Subject: [PATCH 079/107] Fix tests & cs & migration --- .../Version20190425115043.php | 58 +++++++++++++++++++ .../CoreBundle/Command/InstallCommand.php | 2 +- .../CoreBundle/Controller/FeedController.php | 27 +++++---- src/Wallabag/CoreBundle/Entity/Config.php | 4 +- .../Resources/translations/messages.ru.yml | 16 ++--- .../Resources/translations/messages.th.yml | 16 ++--- .../views/themes/baggy/Tag/tags.html.twig | 2 +- .../views/themes/material/Tag/tags.html.twig | 2 +- .../CoreBundle/Twig/WallabagExtension.php | 2 +- .../UserBundle/Repository/UserRepository.php | 2 +- .../Controller/EntryControllerTest.php | 2 +- .../Controller/FeedControllerTest.php | 54 ++++++++--------- .../Controller/SecurityControllerTest.php | 2 +- .../CoreBundle/Twig/WallabagExtensionTest.php | 6 +- 14 files changed, 126 insertions(+), 69 deletions(-) create mode 100644 app/DoctrineMigrations/Version20190425115043.php diff --git a/app/DoctrineMigrations/Version20190425115043.php b/app/DoctrineMigrations/Version20190425115043.php new file mode 100644 index 000000000..4c5c49cc7 --- /dev/null +++ b/app/DoctrineMigrations/Version20190425115043.php @@ -0,0 +1,58 @@ +connection->getDatabasePlatform()->getName()) { + case 'sqlite': + $this->addSql('DROP INDEX UNIQ_87E64C53A76ED395'); + $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('config', true) . ' AS SELECT id, user_id, theme, items_per_page, language, rss_token, rss_limit, reading_speed, pocket_consumer_key, action_mark_as_read, list_mode FROM ' . $this->getTable('config', true)); + $this->addSql('DROP TABLE ' . $this->getTable('config', true)); + $this->addSql('CREATE TABLE ' . $this->getTable('config', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, theme VARCHAR(255) NOT NULL COLLATE BINARY, items_per_page INTEGER NOT NULL, language VARCHAR(255) NOT NULL COLLATE BINARY, reading_speed DOUBLE PRECISION DEFAULT NULL, pocket_consumer_key VARCHAR(255) DEFAULT NULL COLLATE BINARY, action_mark_as_read INTEGER DEFAULT 0, list_mode INTEGER DEFAULT NULL, feed_token VARCHAR(255) DEFAULT NULL, feed_limit INTEGER DEFAULT NULL, CONSTRAINT FK_87E64C53A76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO ' . $this->getTable('config', true) . ' (id, user_id, theme, items_per_page, language, feed_token, feed_limit, reading_speed, pocket_consumer_key, action_mark_as_read, list_mode) SELECT id, user_id, theme, items_per_page, language, rss_token, rss_limit, reading_speed, pocket_consumer_key, action_mark_as_read, list_mode FROM __temp__' . $this->getTable('config', true)); + $this->addSql('DROP TABLE __temp__' . $this->getTable('config', true)); + $this->addSql('CREATE UNIQUE INDEX UNIQ_87E64C53A76ED395 ON ' . $this->getTable('config', true) . ' (user_id)'); + break; + case 'mysql': + $this->addSql('ALTER TABLE ' . $this->getTable('config') . ' CHANGE rss_token feed_token VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE ' . $this->getTable('config') . ' CHANGE rss_limit feed_limit INT DEFAULT NULL'); + break; + case 'postgresql': + $this->addSql('ALTER TABLE ' . $this->getTable('config') . ' RENAME COLUMN rss_token TO feed_token'); + $this->addSql('ALTER TABLE ' . $this->getTable('config') . ' RENAME COLUMN rss_limit TO feed_limit'); + break; + } + } + + public function down(Schema $schema): void + { + switch ($this->connection->getDatabasePlatform()->getName()) { + case 'sqlite': + $this->addSql('DROP INDEX UNIQ_87E64C53A76ED395'); + $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('config', true) . ' AS SELECT id, user_id, theme, items_per_page, language, feed_token, feed_limit, reading_speed, pocket_consumer_key, action_mark_as_read, list_mode FROM "' . $this->getTable('config', true) . '"'); + $this->addSql('DROP TABLE "' . $this->getTable('config', true) . '"'); + $this->addSql('CREATE TABLE "' . $this->getTable('config', true) . '" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, theme VARCHAR(255) NOT NULL, items_per_page INTEGER NOT NULL, language VARCHAR(255) NOT NULL, reading_speed DOUBLE PRECISION DEFAULT NULL, pocket_consumer_key VARCHAR(255) DEFAULT NULL, action_mark_as_read INTEGER DEFAULT 0, list_mode INTEGER DEFAULT NULL, rss_token VARCHAR(255) DEFAULT NULL COLLATE BINARY, rss_limit INTEGER DEFAULT NULL)'); + $this->addSql('INSERT INTO "' . $this->getTable('config', true) . '" (id, user_id, theme, items_per_page, language, rss_token, rss_limit, reading_speed, pocket_consumer_key, action_mark_as_read, list_mode) SELECT id, user_id, theme, items_per_page, language, feed_token, feed_limit, reading_speed, pocket_consumer_key, action_mark_as_read, list_mode FROM __temp__' . $this->getTable('config', true)); + $this->addSql('DROP TABLE __temp__' . $this->getTable('config', true)); + $this->addSql('CREATE UNIQUE INDEX UNIQ_87E64C53A76ED395 ON "' . $this->getTable('config', true) . '" (user_id)'); + break; + case 'mysql': + $this->addSql('ALTER TABLE ' . $this->getTable('config') . ' CHANGE feed_token rss_token'); + $this->addSql('ALTER TABLE ' . $this->getTable('config') . ' CHANGE feed_limit rss_limit'); + break; + case 'postgresql': + $this->addSql('ALTER TABLE ' . $this->getTable('config') . ' RENAME COLUMN feed_token TO rss_token'); + $this->addSql('ALTER TABLE ' . $this->getTable('config') . ' RENAME COLUMN feed_limit TO rss_limit'); + break; + } + } +} diff --git a/src/Wallabag/CoreBundle/Command/InstallCommand.php b/src/Wallabag/CoreBundle/Command/InstallCommand.php index 49c84178f..c58ae2b5b 100644 --- a/src/Wallabag/CoreBundle/Command/InstallCommand.php +++ b/src/Wallabag/CoreBundle/Command/InstallCommand.php @@ -254,7 +254,7 @@ class InstallCommand extends ContainerAwareCommand $question->setHidden(true); $user->setPlainPassword($this->io->askQuestion($question)); - $user->setEmail($this->io->ask('Email', '')); + $user->setEmail($this->io->ask('Email', 'wallabag@wallabag.io')); $user->setEnabled(true); $user->addRole('ROLE_SUPER_ADMIN'); diff --git a/src/Wallabag/CoreBundle/Controller/FeedController.php b/src/Wallabag/CoreBundle/Controller/FeedController.php index 9d55a9b7b..8d422a906 100644 --- a/src/Wallabag/CoreBundle/Controller/FeedController.php +++ b/src/Wallabag/CoreBundle/Controller/FeedController.php @@ -8,7 +8,6 @@ use Pagerfanta\Exception\OutOfRangeCurrentPageException; use Pagerfanta\Pagerfanta; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Symfony\Bundle\FrameworkBundle\Controller\Controller; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -20,8 +19,8 @@ class FeedController extends Controller /** * Shows unread entries for current user. * - * @Route("/feed/{username}/{token}/unread/{page}", name="unread_feed", defaults={"page": 1}) - * @Route("/{username}/{token}/unread.xml", defaults={"page": 1}) + * @Route("/feed/{username}/{token}/unread/{page}", name="unread_feed", defaults={"page"=1, "_format"="xml"}) + * * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_feed_token_converter") * * @param User $user @@ -37,8 +36,8 @@ class FeedController extends Controller /** * Shows read entries for current user. * - * @Route("/feed/{username}/{token}/archive/{page}", name="archive_feed", defaults={"page": 1}) - * @Route("/{username}/{token}/archive.xml", defaults={"page": 1}) + * @Route("/feed/{username}/{token}/archive/{page}", name="archive_feed", defaults={"page"=1, "_format"="xml"}) + * * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_feed_token_converter") * * @param User $user @@ -54,8 +53,8 @@ class FeedController extends Controller /** * Shows starred entries for current user. * - * @Route("/feed/{username}/{token}/starred/{page}", name="starred_feed", defaults={"page": 1}) - * @Route("/{username}/{token}/starred.xml", defaults={"page": 1}) + * @Route("/feed/{username}/{token}/starred/{page}", name="starred_feed", defaults={"page"=1, "_format"="xml"}) + * * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_feed_token_converter") * * @param User $user @@ -71,29 +70,29 @@ class FeedController extends Controller /** * Shows all entries for current user. * - * @Route("/{username}/{token}/all.xml", name="all_feed", defaults={"_format"="xml"}) + * @Route("/feed/{username}/{token}/all/{page}", name="all_feed", defaults={"page"=1, "_format"="xml"}) + * * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_feed_token_converter") * * @return \Symfony\Component\HttpFoundation\Response */ - public function showAllFeedAction(Request $request, User $user) + public function showAllFeedAction(User $user, $page) { - return $this->showEntries('all', $user, $request->query->get('page', 1)); + return $this->showEntries('all', $user, $page); } /** * Shows entries associated to a tag for current user. * - * @Route("/{username}/{token}/tags/{slug}.xml", name="tag_feed", defaults={"_format"="xml"}) + * @Route("/feed/{username}/{token}/tags/{slug}/{page}", name="tag_feed", defaults={"page"=1, "_format"="xml"}) + * * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_feed_token_converter") * @ParamConverter("tag", options={"mapping": {"slug": "slug"}}) * * @return \Symfony\Component\HttpFoundation\Response */ - public function showTagsFeedAction(Request $request, User $user, Tag $tag) + public function showTagsFeedAction(User $user, Tag $tag, $page) { - $page = $request->query->get('page', 1); - $url = $this->generateUrl( 'tag_feed', [ diff --git a/src/Wallabag/CoreBundle/Entity/Config.php b/src/Wallabag/CoreBundle/Entity/Config.php index 7458f7572..c6e65d66e 100644 --- a/src/Wallabag/CoreBundle/Entity/Config.php +++ b/src/Wallabag/CoreBundle/Entity/Config.php @@ -60,14 +60,14 @@ class Config /** * @var string * - * @ORM\Column(name="rss_token", type="string", nullable=true) + * @ORM\Column(name="feed_token", type="string", nullable=true) */ private $feedToken; /** * @var int * - * @ORM\Column(name="rss_limit", type="integer", nullable=true) + * @ORM\Column(name="feed_limit", type="integer", nullable=true) * @Assert\Range( * min = 1, * max = 100000, diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml index 927466314..2ee2d83ab 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml @@ -53,7 +53,7 @@ config: page_title: 'Настройки' tab_menu: settings: 'Настройки' - rss: 'RSS' + feed: 'RSS' user_info: 'Информация о пользователе' password: 'Пароль' rules: 'Правила настройки простановки тегов' @@ -83,18 +83,18 @@ config: help_reading_speed: "wallabag посчитает сколько времени занимает чтение каждой записи. Вы можете определить здесь, как быстро вы читаете. wallabag пересчитает время чтения для каждой записи." help_language: "Вы можете изменить язык интерфейса wallabag." help_pocket_consumer_key: "Обязательно для импорта из Pocket. Вы можете создать это в Вашем аккаунте на Pocket." - form_rss: + form_feed: description: 'RSS фид созданный с помощью wallabag позволяет читать Ваши записи через Ваш любимый RSS агрегатор. Для начала Вам потребуется создать ключ.' token_label: 'RSS ключ' no_token: 'Ключ не задан' token_create: 'Создать ключ' token_reset: 'Пересоздать ключ' - rss_links: 'ссылка на RSS' - rss_link: + feed_links: 'ссылка на RSS' + feed_link: unread: 'непрочитанные' starred: 'помеченные' archive: 'архивные' - rss_limit: 'Количество записей в фиде' + feed_limit: 'Количество записей в фиде' form_user: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'Имя' @@ -359,7 +359,7 @@ quickstart: title: 'Настроить приложение' description: 'Чтобы иметь приложение, которое вам подходит, ознакомьтесь с конфигурацией wallabag.' language: 'Выбрать язык и дизайн' - rss: 'Включить RSS фид' + feed: 'Включить RSS фид' tagging_rules: 'Создать правило для автоматической установки тегов' admin: title: 'Администрирование' @@ -554,10 +554,10 @@ flashes: password_updated: 'Пароль обновлен' password_not_updated_demo: "В режиме демонстрации нельзя изменять пароль для этого пользователя." user_updated: 'Информация обновлена' - rss_updated: 'RSS информация обновлена' + feed_updated: 'RSS информация обновлена' tagging_rules_updated: 'Правила тегировния обновлены' tagging_rules_deleted: 'Правила тегировния удалены' - rss_token_updated: 'RSS ключ обновлен' + feed_token_updated: 'RSS ключ обновлен' annotations_reset: "Аннотации сброшены" tags_reset: "Теги сброшены" entries_reset: "Записи сброшены" diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml index 1fe4fa0ea..e04eee68f 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml @@ -54,7 +54,7 @@ config: page_title: 'กำหนดค่า' tab_menu: settings: 'ตั้งค่า' - rss: 'RSS' + feed: 'RSS' user_info: 'ข้อมูลผู้ใช้' password: 'รหัสผ่าน' rules: 'การแท็กข้อบังคับ' @@ -85,19 +85,19 @@ config: help_reading_speed: "wallabag จะคำนวณเวลาการอ่านในแต่ละรายการซึ่งคุณสามารถกำหนดได้ที่นี้,ต้องขอบคุณรายการนี้,หากคุณเป็นนักอ่านที่เร็วหรือช้า wallabag จะทำการคำนวณเวลาที่อ่านใหม่ในแต่ละรายการ" help_language: "คุณสามารถเปลี่ยภาษาของ wallabag interface ได้" help_pocket_consumer_key: "การ้องขอการเก็บการนำข้อมูลเข้า คุณสามารถสร้างบัญชีการเก็บของคุณ" - form_rss: + form_feed: description: 'RSS จะเก็บเงื่อนไขโดย wallabag ต้องยอมรับการอ่านรายการของคุณกับผู้อ่านที่ชอบ RSS คุณต้องทำเครื่องหมายก่อน' token_label: 'เครื่องหมาย RSS' no_token: 'ไม่มีเครื่องหมาย' token_create: 'สร้างเครื่องหมาย' token_reset: 'ทำเครื่องหมาย' - rss_links: 'ลิงค์ RSS' - rss_link: + feed_links: 'ลิงค์ RSS' + feed_link: unread: 'ยังไมได้่อ่าน' starred: 'ทำการแสดง' archive: 'เอกสาร' all: 'ทั้งหมด' - rss_limit: 'จำนวนไอเทมที่เก็บ' + feed_limit: 'จำนวนไอเทมที่เก็บ' form_user: # two_factor_description: "Enabling two factor authentication means you'll receive an email with a code OR need to use an OTP app (like Google Authenticator, Authy or FreeOTP) to get a one time code on every new untrusted connection. You can't choose both option." name_label: 'ชื่อ' @@ -369,7 +369,7 @@ quickstart: title: 'กำหนดค่าแอพพลิเคชั่น' description: 'ภายใน order จะมี application suit ของคุณ, จะมองหาองค์ประกอบของ wallabag' language: 'เปลี่ยนภาษาและออกแบบ' - rss: 'เปิดใช้ RSS' + feed: 'เปิดใช้ RSS' tagging_rules: 'เขียนข้อบังคับการแท็กอัตโนมัติของบทความของคุณ' admin: title: 'ผู้ดูแลระบบ' @@ -586,10 +586,10 @@ flashes: password_updated: 'อัปเดตรหัสผ่าน' password_not_updated_demo: "In demonstration mode, you can't change password for this user." user_updated: 'อัปเดตข้อมูล' - rss_updated: 'อัปเดตข้อมูล RSS' + feed_updated: 'อัปเดตข้อมูล RSS' tagging_rules_updated: 'อัปเดตการแท็กข้อบังคับ' tagging_rules_deleted: 'การลบข้อบังคับของแท็ก' - rss_token_updated: 'อัปเดตเครื่องหมาย RSS ' + feed_token_updated: 'อัปเดตเครื่องหมาย RSS ' annotations_reset: รีเซ็ตหมายเหตุ tags_reset: รีเซ็ตแท็ก entries_reset: รีเซ็ตรายการ 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 142668c0e..ae8403bd6 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 @@ -21,7 +21,7 @@ mode_edit {% endif %} - {% if app.user.config.rssToken %} + {% if app.user.config.feedToken %} rss_feed 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 737ef5fe4..79907bbb5 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 @@ -25,7 +25,7 @@ mode_edit {% endif %} - {% if app.user.config.rssToken %} + {% if app.user.config.feedToken %} rss_feed {% endif %} diff --git a/src/Wallabag/CoreBundle/Twig/WallabagExtension.php b/src/Wallabag/CoreBundle/Twig/WallabagExtension.php index 61107ce72..536185d44 100644 --- a/src/Wallabag/CoreBundle/Twig/WallabagExtension.php +++ b/src/Wallabag/CoreBundle/Twig/WallabagExtension.php @@ -54,7 +54,7 @@ class WallabagExtension extends \Twig_Extension implements \Twig_Extension_Globa public function removeSchemeAndWww($url) { - return $this->removeWww($this->removeScheme($url) + return $this->removeWww($this->removeScheme($url)); } /** diff --git a/src/Wallabag/UserBundle/Repository/UserRepository.php b/src/Wallabag/UserBundle/Repository/UserRepository.php index 803911095..4abd55f15 100644 --- a/src/Wallabag/UserBundle/Repository/UserRepository.php +++ b/src/Wallabag/UserBundle/Repository/UserRepository.php @@ -14,7 +14,7 @@ class UserRepository extends EntityRepository * @param string $username * @param string $feedToken * - * @return null|User + * @return User|null */ public function findOneByUsernameAndFeedtoken($username, $feedToken) { diff --git a/tests/Wallabag/CoreBundle/Controller/EntryControllerTest.php b/tests/Wallabag/CoreBundle/Controller/EntryControllerTest.php index 28291b5a0..caa8929d1 100644 --- a/tests/Wallabag/CoreBundle/Controller/EntryControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/EntryControllerTest.php @@ -166,7 +166,7 @@ class EntryControllerTest extends WallabagCoreTestCase $this->assertSame($this->url, $content->getUrl()); $this->assertContains('Google', $content->getTitle()); $this->assertSame('fr', $content->getLanguage()); - $this->assertSame('2016-04-07 19:01:35', $content->getPublishedAt()->format('Y-m-d H:i:s')); + $this->assertSame('2015-03-28 11:43:19', $content->getPublishedAt()->format('Y-m-d H:i:s')); $this->assertArrayHasKey('x-frame-options', $content->getHeaders()); $client->getContainer()->get('craue_config')->set('store_article_headers', 0); } diff --git a/tests/Wallabag/CoreBundle/Controller/FeedControllerTest.php b/tests/Wallabag/CoreBundle/Controller/FeedControllerTest.php index 7442e8a4f..70f33ebef 100644 --- a/tests/Wallabag/CoreBundle/Controller/FeedControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/FeedControllerTest.php @@ -11,48 +11,48 @@ class FeedControllerTest extends WallabagCoreTestCase $doc = new \DOMDocument(); $doc->loadXML($xml); - $xpath = new \DOMXpath($doc); + $xpath = new \DOMXPath($doc); $xpath->registerNamespace('a', 'http://www.w3.org/2005/Atom'); if (null === $nb) { $this->assertGreaterThan(0, $xpath->query('//a:entry')->length); } else { - $this->assertEquals($nb, $xpath->query('//a:entry')->length); + $this->assertSame($nb, $xpath->query('//a:entry')->length); } - $this->assertEquals(1, $xpath->query('/a:feed')->length); + $this->assertSame(1, $xpath->query('/a:feed')->length); - $this->assertEquals(1, $xpath->query('/a:feed/a:title')->length); + $this->assertSame(1, $xpath->query('/a:feed/a:title')->length); $this->assertContains('favicon.ico', $xpath->query('/a:feed/a:icon')->item(0)->nodeValue); $this->assertContains('logo-square.png', $xpath->query('/a:feed/a:logo')->item(0)->nodeValue); - $this->assertEquals(1, $xpath->query('/a:feed/a:updated')->length); + $this->assertSame(1, $xpath->query('/a:feed/a:updated')->length); - $this->assertEquals(1, $xpath->query('/a:feed/a:generator')->length); - $this->assertEquals('wallabag', $xpath->query('/a:feed/a:generator')->item(0)->nodeValue); - $this->assertEquals('admin', $xpath->query('/a:feed/a:author/a:name')->item(0)->nodeValue); + $this->assertSame(1, $xpath->query('/a:feed/a:generator')->length); + $this->assertSame('wallabag', $xpath->query('/a:feed/a:generator')->item(0)->nodeValue); + $this->assertSame('admin', $xpath->query('/a:feed/a:author/a:name')->item(0)->nodeValue); - $this->assertEquals(1, $xpath->query('/a:feed/a:subtitle')->length); + $this->assertSame(1, $xpath->query('/a:feed/a:subtitle')->length); if (null !== $tagValue && 0 === strpos($type, 'tag')) { - $this->assertEquals('wallabag — '.$type.' '.$tagValue.' feed', $xpath->query('/a:feed/a:title')->item(0)->nodeValue); - $this->assertEquals('Atom feed for entries tagged with ' . $tagValue, $xpath->query('/a:feed/a:subtitle')->item(0)->nodeValue); + $this->assertSame('wallabag — ' . $type . ' ' . $tagValue . ' feed', $xpath->query('/a:feed/a:title')->item(0)->nodeValue); + $this->assertSame('Atom feed for entries tagged with ' . $tagValue, $xpath->query('/a:feed/a:subtitle')->item(0)->nodeValue); } else { - $this->assertEquals('wallabag — '.$type.' feed', $xpath->query('/a:feed/a:title')->item(0)->nodeValue); - $this->assertEquals('Atom feed for ' . $type . ' entries', $xpath->query('/a:feed/a:subtitle')->item(0)->nodeValue); + $this->assertSame('wallabag — ' . $type . ' feed', $xpath->query('/a:feed/a:title')->item(0)->nodeValue); + $this->assertSame('Atom feed for ' . $type . ' entries', $xpath->query('/a:feed/a:subtitle')->item(0)->nodeValue); } - $this->assertEquals(1, $xpath->query('/a:feed/a:link[@rel="self"]')->length); + $this->assertSame(1, $xpath->query('/a:feed/a:link[@rel="self"]')->length); $this->assertContains($type, $xpath->query('/a:feed/a:link[@rel="self"]')->item(0)->getAttribute('href')); - $this->assertEquals(1, $xpath->query('/a:feed/a:link[@rel="last"]')->length); + $this->assertSame(1, $xpath->query('/a:feed/a:link[@rel="last"]')->length); foreach ($xpath->query('//a:entry') as $item) { - $this->assertEquals(1, $xpath->query('a:title', $item)->length); - $this->assertEquals(1, $xpath->query('a:link[@rel="via"]', $item)->length); - $this->assertEquals(1, $xpath->query('a:link[@rel="alternate"]', $item)->length); - $this->assertEquals(1, $xpath->query('a:id', $item)->length); - $this->assertEquals(1, $xpath->query('a:published', $item)->length); - $this->assertEquals(1, $xpath->query('a:content', $item)->length); + $this->assertSame(1, $xpath->query('a:title', $item)->length); + $this->assertSame(1, $xpath->query('a:link[@rel="via"]', $item)->length); + $this->assertSame(1, $xpath->query('a:link[@rel="alternate"]', $item)->length); + $this->assertSame(1, $xpath->query('a:id', $item)->length); + $this->assertSame(1, $xpath->query('a:published', $item)->length); + $this->assertSame(1, $xpath->query('a:content', $item)->length); } } @@ -190,15 +190,15 @@ class FeedControllerTest extends WallabagCoreTestCase $client = $this->getClient(); $client->request('GET', '/feed/admin/SUPERTOKEN/unread'); - $this->assertEquals(200, $client->getResponse()->getStatusCode()); + $this->assertSame(200, $client->getResponse()->getStatusCode()); $this->validateDom($client->getResponse()->getContent(), 'unread'); $client->request('GET', '/feed/admin/SUPERTOKEN/unread/2'); - $this->assertEquals(200, $client->getResponse()->getStatusCode()); + $this->assertSame(200, $client->getResponse()->getStatusCode()); $this->validateDom($client->getResponse()->getContent(), 'unread'); $client->request('GET', '/feed/admin/SUPERTOKEN/unread/3000'); - $this->assertEquals(302, $client->getResponse()->getStatusCode()); + $this->assertSame(302, $client->getResponse()->getStatusCode()); } public function testTags() @@ -216,13 +216,13 @@ class FeedControllerTest extends WallabagCoreTestCase $em->flush(); $client = $this->getClient(); - $client->request('GET', '/admin/SUPERTOKEN/tags/foo-bar.xml'); + $client->request('GET', '/feed/admin/SUPERTOKEN/tags/foo'); $this->assertSame(200, $client->getResponse()->getStatusCode()); - $this->validateDom($client->getResponse()->getContent(), 'tag', 2, 'foo-bar'); + $this->validateDom($client->getResponse()->getContent(), 'tag', 2, 'foo'); - $client->request('GET', '/admin/SUPERTOKEN/tags/foo-bar.xml?page=3000'); + $client->request('GET', '/feed/admin/SUPERTOKEN/tags/foo/3000'); $this->assertSame(302, $client->getResponse()->getStatusCode()); } } diff --git a/tests/Wallabag/CoreBundle/Controller/SecurityControllerTest.php b/tests/Wallabag/CoreBundle/Controller/SecurityControllerTest.php index 3c3354d7f..93019b1f2 100644 --- a/tests/Wallabag/CoreBundle/Controller/SecurityControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/SecurityControllerTest.php @@ -13,7 +13,7 @@ class SecurityControllerTest extends WallabagCoreTestCase $client->followRedirects(); $crawler = $client->request('GET', '/config'); - $this->assertContains('config.form_rss.description', $crawler->filter('body')->extract(['_text'])[0]); + $this->assertContains('config.form_feed.description', $crawler->filter('body')->extract(['_text'])[0]); } public function testLoginWithout2Factor() diff --git a/tests/Wallabag/CoreBundle/Twig/WallabagExtensionTest.php b/tests/Wallabag/CoreBundle/Twig/WallabagExtensionTest.php index 3fd90fda1..39fcec165 100644 --- a/tests/Wallabag/CoreBundle/Twig/WallabagExtensionTest.php +++ b/tests/Wallabag/CoreBundle/Twig/WallabagExtensionTest.php @@ -52,9 +52,9 @@ class WallabagExtensionTest extends TestCase $extension = new WallabagExtension($entryRepository, $tagRepository, $tokenStorage, 0, $translator); - $this->assertEquals('lemonde.fr', $extension->removeScheme('lemonde.fr')); - $this->assertEquals('gist.github.com', $extension->removeScheme('gist.github.com')); - $this->assertEquals('gist.github.com', $extension->removeScheme('https://gist.github.com')); + $this->assertSame('lemonde.fr', $extension->removeScheme('lemonde.fr')); + $this->assertSame('gist.github.com', $extension->removeScheme('gist.github.com')); + $this->assertSame('gist.github.com', $extension->removeScheme('https://gist.github.com')); } public function testRemoveSchemeAndWww() From 68a90821a305867e9b655da2dbfe558d37253990 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Fri, 26 Apr 2019 13:40:58 +0200 Subject: [PATCH 080/107] Handle redirection from previous feeds --- app/config/routing.yml | 36 +++++++++++++++++++ .../Controller/FeedControllerTest.php | 33 +++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/app/config/routing.yml b/app/config/routing.yml index a7c0f7e9d..d4defca02 100644 --- a/app/config/routing.yml +++ b/app/config/routing.yml @@ -59,3 +59,39 @@ fos_js_routing: 2fa_login_check: path: /2fa_check + +# redirect RSS feed to Atom +rss_to_atom_unread: + path: /{username}/{token}/unread.xml + defaults: + _controller: FrameworkBundle:Redirect:redirect + route: unread_feed + permanent: true + +rss_to_atom_archive: + path: /{username}/{token}/archive.xml + defaults: + _controller: FrameworkBundle:Redirect:redirect + route: archive_feed + permanent: true + +rss_to_atom_starred: + path: /{username}/{token}/starred.xml + defaults: + _controller: FrameworkBundle:Redirect:redirect + route: starred_feed + permanent: true + +rss_to_atom_all: + path: /{username}/{token}/all.xml + defaults: + _controller: FrameworkBundle:Redirect:redirect + route: all_feed + permanent: true + +rss_to_atom_tags: + path: /{username}/{token}/tags/{slug}.xml + defaults: + _controller: FrameworkBundle:Redirect:redirect + route: tag_feed + permanent: true diff --git a/tests/Wallabag/CoreBundle/Controller/FeedControllerTest.php b/tests/Wallabag/CoreBundle/Controller/FeedControllerTest.php index 70f33ebef..d52d7bb8a 100644 --- a/tests/Wallabag/CoreBundle/Controller/FeedControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/FeedControllerTest.php @@ -225,4 +225,37 @@ class FeedControllerTest extends WallabagCoreTestCase $client->request('GET', '/feed/admin/SUPERTOKEN/tags/foo/3000'); $this->assertSame(302, $client->getResponse()->getStatusCode()); } + + public function dataForRedirect() + { + return [ + [ + '/admin/YZIOAUZIAO/unread.xml', + ], + [ + '/admin/YZIOAUZIAO/starred.xml', + ], + [ + '/admin/YZIOAUZIAO/archive.xml', + ], + [ + '/admin/YZIOAUZIAO/all.xml', + ], + [ + '/admin/YZIOAUZIAO/tags/foo.xml', + ], + ]; + } + + /** + * @dataProvider dataForRedirect + */ + public function testRedirectFromRssToAtom($url) + { + $client = $this->getClient(); + + $client->request('GET', $url); + + $this->assertSame(301, $client->getResponse()->getStatusCode()); + } } From 9306c2a368cc7c7da577b6199440f4abc907af7d Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Fri, 10 May 2019 15:32:29 +0200 Subject: [PATCH 081/107] Use Imagick to keep GIF animation If Imagick is available, GIF will be saved using it to keep animation. Otherwise the previous method will be used and the animation won't be kept. --- composer.json | 3 +++ src/Wallabag/CoreBundle/Helper/DownloadImages.php | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b28404e3c..b1c144c72 100644 --- a/composer.json +++ b/composer.json @@ -103,6 +103,9 @@ "phpstan/phpstan-symfony": "^0.11.0", "phpstan/phpstan-doctrine": "^0.11.0" }, + "suggest": { + "ext-imagick": "To keep GIF animation when downloading image is enabled" + }, "scripts": { "post-cmd": [ "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters", diff --git a/src/Wallabag/CoreBundle/Helper/DownloadImages.php b/src/Wallabag/CoreBundle/Helper/DownloadImages.php index cc3dcfceb..bc2afc646 100644 --- a/src/Wallabag/CoreBundle/Helper/DownloadImages.php +++ b/src/Wallabag/CoreBundle/Helper/DownloadImages.php @@ -135,7 +135,16 @@ class DownloadImages switch ($ext) { case 'gif': - imagegif($im, $localPath); + // use Imagick if available to keep GIF animation + if (class_exists('\\Imagick')) { + $imagick = new \Imagick(); + $imagick->readImageBlob($res->getBody()); + $imagick->setImageFormat('gif'); + $imagick->writeImages($localPath, true); + } else { + imagegif($im, $localPath); + } + $this->logger->debug('DownloadImages: Re-creating gif'); break; case 'jpeg': From 1ca9310f5e2daf5904754125ba7da7d73d6587ce Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Fri, 10 May 2019 15:47:47 +0200 Subject: [PATCH 082/107] Setup Imagick for Travis To avoid error from phpstan about class not found --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 393d00338..8c1ec5cb3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,6 +54,10 @@ before_script: - echo "memory_limit=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini - phpenv config-rm xdebug.ini || echo "xdebug not available" - composer self-update --no-progress + # install imagick + - pear config-set preferred_state beta + - pecl channel-update pecl.php.net + - yes | pecl install imagick script: - travis_wait bash composer install -o --no-interaction --no-progress --prefer-dist From 77bd7f690db45be10a32fe4ceebccdd0edf7c24f Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Fri, 10 May 2019 15:49:39 +0200 Subject: [PATCH 083/107] CS --- .../GrabySiteConfigBuilderTest.php | 6 ++-- .../UsernameFeedTokenConverterTest.php | 32 +++++++++---------- .../CreateConfigListenerTest.php | 2 +- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php b/tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php index 7beccd309..277d80129 100644 --- a/tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php +++ b/tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php @@ -33,7 +33,7 @@ class GrabySiteConfigBuilderTest extends TestCase $grabyConfigBuilderMock ->method('buildForHost') ->with('example.com') - ->will($this->returnValue($grabySiteConfig)); + ->willReturn($grabySiteConfig); $logger = new Logger('foo'); $handler = new TestHandler(); @@ -93,7 +93,7 @@ class GrabySiteConfigBuilderTest extends TestCase $grabyConfigBuilderMock ->method('buildForHost') ->with('unknown.com') - ->will($this->returnValue(new GrabySiteConfig())); + ->willReturn(new GrabySiteConfig()); $logger = new Logger('foo'); $handler = new TestHandler(); @@ -153,7 +153,7 @@ class GrabySiteConfigBuilderTest extends TestCase $grabyConfigBuilderMock ->method('buildForHost') ->with('example.com') - ->will($this->returnValue($grabySiteConfig)); + ->willReturn($grabySiteConfig); $logger = new Logger('foo'); $handler = new TestHandler(); diff --git a/tests/Wallabag/CoreBundle/ParamConverter/UsernameFeedTokenConverterTest.php b/tests/Wallabag/CoreBundle/ParamConverter/UsernameFeedTokenConverterTest.php index 92fe38cd4..48c82ddec 100644 --- a/tests/Wallabag/CoreBundle/ParamConverter/UsernameFeedTokenConverterTest.php +++ b/tests/Wallabag/CoreBundle/ParamConverter/UsernameFeedTokenConverterTest.php @@ -26,7 +26,7 @@ class UsernameFeedTokenConverterTest extends TestCase $registry->expects($this->once()) ->method('getManagers') - ->will($this->returnValue([])); + ->willReturn([]); $params = new ParamConverter([]); $converter = new UsernameFeedTokenConverter($registry); @@ -42,7 +42,7 @@ class UsernameFeedTokenConverterTest extends TestCase $registry->expects($this->once()) ->method('getManagers') - ->will($this->returnValue(['default' => null])); + ->willReturn(['default' => null]); $params = new ParamConverter([]); $converter = new UsernameFeedTokenConverter($registry); @@ -58,7 +58,7 @@ class UsernameFeedTokenConverterTest extends TestCase $meta->expects($this->once()) ->method('getName') - ->will($this->returnValue('nothingrelated')); + ->willReturn('nothingrelated'); $em = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager') ->disableOriginalConstructor() @@ -67,7 +67,7 @@ class UsernameFeedTokenConverterTest extends TestCase $em->expects($this->once()) ->method('getClassMetadata') ->with('superclass') - ->will($this->returnValue($meta)); + ->willReturn($meta); $registry = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry') ->disableOriginalConstructor() @@ -75,12 +75,12 @@ class UsernameFeedTokenConverterTest extends TestCase $registry->expects($this->once()) ->method('getManagers') - ->will($this->returnValue(['default' => null])); + ->willReturn(['default' => null]); $registry->expects($this->once()) ->method('getManagerForClass') ->with('superclass') - ->will($this->returnValue($em)); + ->willReturn($em); $params = new ParamConverter(['class' => 'superclass']); $converter = new UsernameFeedTokenConverter($registry); @@ -96,7 +96,7 @@ class UsernameFeedTokenConverterTest extends TestCase $meta->expects($this->once()) ->method('getName') - ->will($this->returnValue('Wallabag\UserBundle\Entity\User')); + ->willReturn('Wallabag\UserBundle\Entity\User'); $em = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager') ->disableOriginalConstructor() @@ -105,7 +105,7 @@ class UsernameFeedTokenConverterTest extends TestCase $em->expects($this->once()) ->method('getClassMetadata') ->with('WallabagUserBundle:User') - ->will($this->returnValue($meta)); + ->willReturn($meta); $registry = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry') ->disableOriginalConstructor() @@ -113,12 +113,12 @@ class UsernameFeedTokenConverterTest extends TestCase $registry->expects($this->once()) ->method('getManagers') - ->will($this->returnValue(['default' => null])); + ->willReturn(['default' => null]); $registry->expects($this->once()) ->method('getManagerForClass') ->with('WallabagUserBundle:User') - ->will($this->returnValue($em)); + ->willReturn($em); $params = new ParamConverter(['class' => 'WallabagUserBundle:User']); $converter = new UsernameFeedTokenConverter($registry); @@ -149,7 +149,7 @@ class UsernameFeedTokenConverterTest extends TestCase $repo->expects($this->once()) ->method('findOneByUsernameAndFeedToken') ->with('test', 'test') - ->will($this->returnValue(null)); + ->willReturn(null); $em = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager') ->disableOriginalConstructor() @@ -158,7 +158,7 @@ class UsernameFeedTokenConverterTest extends TestCase $em->expects($this->once()) ->method('getRepository') ->with('WallabagUserBundle:User') - ->will($this->returnValue($repo)); + ->willReturn($repo); $registry = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry') ->disableOriginalConstructor() @@ -167,7 +167,7 @@ class UsernameFeedTokenConverterTest extends TestCase $registry->expects($this->once()) ->method('getManagerForClass') ->with('WallabagUserBundle:User') - ->will($this->returnValue($em)); + ->willReturn($em); $params = new ParamConverter(['class' => 'WallabagUserBundle:User']); $converter = new UsernameFeedTokenConverter($registry); @@ -187,7 +187,7 @@ class UsernameFeedTokenConverterTest extends TestCase $repo->expects($this->once()) ->method('findOneByUsernameAndFeedtoken') ->with('test', 'test') - ->will($this->returnValue($user)); + ->willReturn($user); $em = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager') ->disableOriginalConstructor() @@ -196,7 +196,7 @@ class UsernameFeedTokenConverterTest extends TestCase $em->expects($this->once()) ->method('getRepository') ->with('WallabagUserBundle:User') - ->will($this->returnValue($repo)); + ->willReturn($repo); $registry = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry') ->disableOriginalConstructor() @@ -205,7 +205,7 @@ class UsernameFeedTokenConverterTest extends TestCase $registry->expects($this->once()) ->method('getManagerForClass') ->with('WallabagUserBundle:User') - ->will($this->returnValue($em)); + ->willReturn($em); $params = new ParamConverter(['class' => 'WallabagUserBundle:User', 'name' => 'user']); $converter = new UsernameFeedTokenConverter($registry); diff --git a/tests/Wallabag/UserBundle/EventListener/CreateConfigListenerTest.php b/tests/Wallabag/UserBundle/EventListener/CreateConfigListenerTest.php index fd32f3803..d976c4ac6 100644 --- a/tests/Wallabag/UserBundle/EventListener/CreateConfigListenerTest.php +++ b/tests/Wallabag/UserBundle/EventListener/CreateConfigListenerTest.php @@ -68,7 +68,7 @@ class CreateConfigListenerTest extends TestCase $this->em->expects($this->once()) ->method('persist') - ->will($this->returnValue($config)); + ->willReturn($config); $this->em->expects($this->once()) ->method('flush'); From 844fd9fafc577faa8d6c8faa4e37b915be2389d9 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Fri, 10 May 2019 16:52:01 +0200 Subject: [PATCH 084/107] Fallback to default solution if Imagick fails --- src/Wallabag/CoreBundle/Helper/DownloadImages.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Wallabag/CoreBundle/Helper/DownloadImages.php b/src/Wallabag/CoreBundle/Helper/DownloadImages.php index bc2afc646..9a7e98285 100644 --- a/src/Wallabag/CoreBundle/Helper/DownloadImages.php +++ b/src/Wallabag/CoreBundle/Helper/DownloadImages.php @@ -137,10 +137,15 @@ class DownloadImages case 'gif': // use Imagick if available to keep GIF animation if (class_exists('\\Imagick')) { - $imagick = new \Imagick(); - $imagick->readImageBlob($res->getBody()); - $imagick->setImageFormat('gif'); - $imagick->writeImages($localPath, true); + try { + $imagick = new \Imagick(); + $imagick->readImageBlob($res->getBody()); + $imagick->setImageFormat('gif'); + $imagick->writeImages($localPath, true); + } catch (\Exception $e) { + // if Imagick fail, fallback to the default solution + imagegif($im, $localPath); + } } else { imagegif($im, $localPath); } From 637f0df9760b50b56c8ffb200719907032fdd885 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Fri, 10 May 2019 16:49:19 +0200 Subject: [PATCH 085/107] Cascade delete on oauth2 table when deleting a user --- .../Version20190510141130.php | 95 +++++++++++++++++++ src/Wallabag/ApiBundle/Entity/AccessToken.php | 1 + src/Wallabag/ApiBundle/Entity/AuthCode.php | 1 + .../ApiBundle/Entity/RefreshToken.php | 1 + 4 files changed, 98 insertions(+) create mode 100644 app/DoctrineMigrations/Version20190510141130.php diff --git a/app/DoctrineMigrations/Version20190510141130.php b/app/DoctrineMigrations/Version20190510141130.php new file mode 100644 index 000000000..adfe73a02 --- /dev/null +++ b/app/DoctrineMigrations/Version20190510141130.php @@ -0,0 +1,95 @@ +connection->getDatabasePlatform()->getName()) { + case 'sqlite': + $this->addSql('DROP INDEX IDX_368A4209A76ED395'); + $this->addSql('DROP INDEX IDX_368A420919EB6921'); + $this->addSql('DROP INDEX UNIQ_368A42095F37A13B'); + $this->addSql('DROP TABLE ' . $this->getTable('oauth2_access_tokens', true) . ''); + $this->addSql('CREATE TABLE ' . $this->getTable('oauth2_access_tokens', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, client_id INTEGER NOT NULL, user_id INTEGER DEFAULT NULL, expires_at INTEGER DEFAULT NULL, token VARCHAR(191) NOT NULL, scope VARCHAR(191) NOT NULL, CONSTRAINT FK_368A420919EB6921 FOREIGN KEY (client_id) REFERENCES ' . $this->getTable('oauth2_clients', true) . ' (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_368A4209A76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO ' . $this->getTable('oauth2_access_tokens', true) . ' (id, client_id, user_id, token, expires_at, scope) SELECT id, client_id, user_id, token, expires_at, scope FROM __temp__' . $this->getTable('oauth2_access_tokens', true) . ''); + $this->addSql('DROP TABLE __temp__' . $this->getTable('oauth2_access_tokens', true) . ''); + $this->addSql('CREATE INDEX IDX_368A4209A76ED395 ON ' . $this->getTable('oauth2_access_tokens', true) . ' (user_id)'); + $this->addSql('CREATE INDEX IDX_368A420919EB6921 ON ' . $this->getTable('oauth2_access_tokens', true) . ' (client_id)'); + + $this->addSql('DROP INDEX IDX_635D765EA76ED395'); + $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('oauth2_clients', true) . ' AS SELECT id, user_id, random_id, secret, redirect_uris, allowed_grant_types, name FROM ' . $this->getTable('oauth2_clients', true) . ''); + $this->addSql('DROP TABLE ' . $this->getTable('oauth2_clients', true) . ''); + $this->addSql('CREATE TABLE ' . $this->getTable('oauth2_clients', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, random_id VARCHAR(255) NOT NULL COLLATE BINARY, secret VARCHAR(255) NOT NULL COLLATE BINARY, name CLOB NOT NULL COLLATE BINARY, redirect_uris CLOB NOT NULL --(DC2Type:array), allowed_grant_types CLOB NOT NULL --(DC2Type:array), CONSTRAINT FK_635D765EA76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO ' . $this->getTable('oauth2_clients', true) . ' (id, user_id, random_id, secret, redirect_uris, allowed_grant_types, name) SELECT id, user_id, random_id, secret, redirect_uris, allowed_grant_types, name FROM __temp__' . $this->getTable('oauth2_clients', true) . ''); + $this->addSql('DROP TABLE __temp__' . $this->getTable('oauth2_clients', true) . ''); + $this->addSql('CREATE INDEX IDX_635D765EA76ED395 ON ' . $this->getTable('oauth2_clients', true) . ' (user_id)'); + + $this->addSql('DROP INDEX IDX_20C9FB24A76ED395'); + $this->addSql('DROP INDEX IDX_20C9FB2419EB6921'); + $this->addSql('DROP INDEX UNIQ_20C9FB245F37A13B'); + $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('oauth2_refresh_tokens', true) . ' AS SELECT id, client_id, user_id, token, expires_at, scope FROM ' . $this->getTable('oauth2_refresh_tokens', true) . ''); + $this->addSql('DROP TABLE ' . $this->getTable('oauth2_refresh_tokens', true) . ''); + $this->addSql('CREATE TABLE ' . $this->getTable('oauth2_refresh_tokens', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, client_id INTEGER NOT NULL, user_id INTEGER DEFAULT NULL, expires_at INTEGER DEFAULT NULL, token VARCHAR(191) NOT NULL, scope VARCHAR(191) NOT NULL, CONSTRAINT FK_20C9FB2419EB6921 FOREIGN KEY (client_id) REFERENCES ' . $this->getTable('oauth2_clients', true) . ' (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_20C9FB24A76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO ' . $this->getTable('oauth2_refresh_tokens', true) . ' (id, client_id, user_id, token, expires_at, scope) SELECT id, client_id, user_id, token, expires_at, scope FROM __temp__' . $this->getTable('oauth2_refresh_tokens', true) . ''); + $this->addSql('DROP TABLE __temp__' . $this->getTable('oauth2_refresh_tokens', true) . ''); + $this->addSql('CREATE INDEX IDX_20C9FB24A76ED395 ON ' . $this->getTable('oauth2_refresh_tokens', true) . ' (user_id)'); + $this->addSql('CREATE INDEX IDX_20C9FB2419EB6921 ON ' . $this->getTable('oauth2_refresh_tokens', true) . ' (client_id)'); + + $this->addSql('DROP INDEX IDX_EE52E3FAA76ED395'); + $this->addSql('DROP INDEX IDX_EE52E3FA19EB6921'); + $this->addSql('DROP INDEX UNIQ_EE52E3FA5F37A13B'); + $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('oauth2_auth_codes', true) . ' AS SELECT id, client_id, user_id, token, redirect_uri, expires_at, scope FROM ' . $this->getTable('oauth2_auth_codes', true) . ''); + $this->addSql('DROP TABLE ' . $this->getTable('oauth2_auth_codes', true) . ''); + $this->addSql('CREATE TABLE ' . $this->getTable('oauth2_auth_codes', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, client_id INTEGER NOT NULL, user_id INTEGER DEFAULT NULL, redirect_uri CLOB NOT NULL COLLATE BINARY, expires_at INTEGER DEFAULT NULL, token VARCHAR(191) NOT NULL, scope VARCHAR(191) NOT NULL, CONSTRAINT FK_EE52E3FA19EB6921 FOREIGN KEY (client_id) REFERENCES ' . $this->getTable('oauth2_clients', true) . ' (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EE52E3FAA76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO ' . $this->getTable('oauth2_auth_codes', true) . ' (id, client_id, user_id, token, redirect_uri, expires_at, scope) SELECT id, client_id, user_id, token, redirect_uri, expires_at, scope FROM __temp__' . $this->getTable('oauth2_auth_codes', true) . ''); + $this->addSql('DROP TABLE __temp__' . $this->getTable('oauth2_auth_codes', true) . ''); + $this->addSql('CREATE INDEX IDX_EE52E3FAA76ED395 ON ' . $this->getTable('oauth2_auth_codes', true) . ' (user_id)'); + $this->addSql('CREATE INDEX IDX_EE52E3FA19EB6921 ON ' . $this->getTable('oauth2_auth_codes', true) . ' (client_id)'); + break; + case 'mysql': + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_access_tokens') . ' DROP FOREIGN KEY FK_368A4209A76ED395'); + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_access_tokens') . ' ADD CONSTRAINT FK_368A4209A76ED395 FOREIGN KEY (user_id) REFERENCES `wallabag_user` (id) ON DELETE CASCADE'); + + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_clients') . ' DROP FOREIGN KEY IDX_user_oauth_client'); + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_clients') . ' ADD CONSTRAINT FK_635D765EA76ED395 FOREIGN KEY (user_id) REFERENCES `wallabag_user` (id)'); + + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_refresh_tokens') . ' DROP FOREIGN KEY FK_20C9FB24A76ED395'); + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_refresh_tokens') . ' ADD CONSTRAINT FK_20C9FB24A76ED395 FOREIGN KEY (user_id) REFERENCES `wallabag_user` (id) ON DELETE CASCADE'); + + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_auth_codes') . ' DROP FOREIGN KEY FK_EE52E3FAA76ED395'); + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_auth_codes') . ' ADD CONSTRAINT FK_EE52E3FAA76ED395 FOREIGN KEY (user_id) REFERENCES `wallabag_user` (id) ON DELETE CASCADE'); + break; + case 'postgresql': + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_access_tokens') . ' DROP CONSTRAINT FK_368A4209A76ED395'); + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_access_tokens') . ' ADD CONSTRAINT FK_368A4209A76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_clients') . ' DROP CONSTRAINT idx_user_oauth_client'); + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_clients') . ' ADD CONSTRAINT FK_635D765EA76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_refresh_tokens') . ' DROP CONSTRAINT FK_20C9FB24A76ED395'); + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_refresh_tokens') . ' ADD CONSTRAINT FK_20C9FB24A76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_auth_codes') . ' DROP CONSTRAINT FK_EE52E3FAA76ED395'); + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_auth_codes') . ' ADD CONSTRAINT FK_EE52E3FAA76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + break; + } + } + + public function down(Schema $schema): void + { + throw new SkipMigrationException('Too complex ...'); + } +} diff --git a/src/Wallabag/ApiBundle/Entity/AccessToken.php b/src/Wallabag/ApiBundle/Entity/AccessToken.php index 5e4099ddd..98e0af3e1 100644 --- a/src/Wallabag/ApiBundle/Entity/AccessToken.php +++ b/src/Wallabag/ApiBundle/Entity/AccessToken.php @@ -42,6 +42,7 @@ class AccessToken extends BaseAccessToken /** * @ORM\ManyToOne(targetEntity="Wallabag\UserBundle\Entity\User") + * @ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE") */ protected $user; } diff --git a/src/Wallabag/ApiBundle/Entity/AuthCode.php b/src/Wallabag/ApiBundle/Entity/AuthCode.php index 5fa205ac7..7c9c85396 100644 --- a/src/Wallabag/ApiBundle/Entity/AuthCode.php +++ b/src/Wallabag/ApiBundle/Entity/AuthCode.php @@ -42,6 +42,7 @@ class AuthCode extends BaseAuthCode /** * @ORM\ManyToOne(targetEntity="Wallabag\UserBundle\Entity\User") + * @ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE") */ protected $user; } diff --git a/src/Wallabag/ApiBundle/Entity/RefreshToken.php b/src/Wallabag/ApiBundle/Entity/RefreshToken.php index dd8e9c63e..55a507e13 100644 --- a/src/Wallabag/ApiBundle/Entity/RefreshToken.php +++ b/src/Wallabag/ApiBundle/Entity/RefreshToken.php @@ -42,6 +42,7 @@ class RefreshToken extends BaseRefreshToken /** * @ORM\ManyToOne(targetEntity="Wallabag\UserBundle\Entity\User") + * @ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE") */ protected $user; } From d2ef2d6df8db302dd53af5cf960a101b4301cdc8 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Fri, 10 May 2019 20:36:25 +0200 Subject: [PATCH 086/107] Fix SQLite migration --- .../Version20190510141130.php | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/app/DoctrineMigrations/Version20190510141130.php b/app/DoctrineMigrations/Version20190510141130.php index adfe73a02..5fc1dba48 100644 --- a/app/DoctrineMigrations/Version20190510141130.php +++ b/app/DoctrineMigrations/Version20190510141130.php @@ -22,40 +22,41 @@ final class Version20190510141130 extends WallabagMigration $this->addSql('DROP INDEX IDX_368A4209A76ED395'); $this->addSql('DROP INDEX IDX_368A420919EB6921'); $this->addSql('DROP INDEX UNIQ_368A42095F37A13B'); - $this->addSql('DROP TABLE ' . $this->getTable('oauth2_access_tokens', true) . ''); + $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('oauth2_access_tokens', true) . ' AS SELECT id, client_id, user_id, token, expires_at, scope FROM ' . $this->getTable('oauth2_access_tokens', true)); + $this->addSql('DROP TABLE ' . $this->getTable('oauth2_access_tokens', true)); $this->addSql('CREATE TABLE ' . $this->getTable('oauth2_access_tokens', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, client_id INTEGER NOT NULL, user_id INTEGER DEFAULT NULL, expires_at INTEGER DEFAULT NULL, token VARCHAR(191) NOT NULL, scope VARCHAR(191) NOT NULL, CONSTRAINT FK_368A420919EB6921 FOREIGN KEY (client_id) REFERENCES ' . $this->getTable('oauth2_clients', true) . ' (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_368A4209A76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); - $this->addSql('INSERT INTO ' . $this->getTable('oauth2_access_tokens', true) . ' (id, client_id, user_id, token, expires_at, scope) SELECT id, client_id, user_id, token, expires_at, scope FROM __temp__' . $this->getTable('oauth2_access_tokens', true) . ''); - $this->addSql('DROP TABLE __temp__' . $this->getTable('oauth2_access_tokens', true) . ''); + $this->addSql('INSERT INTO ' . $this->getTable('oauth2_access_tokens', true) . ' (id, client_id, user_id, token, expires_at, scope) SELECT id, client_id, user_id, token, expires_at, scope FROM __temp__' . $this->getTable('oauth2_access_tokens', true)); + $this->addSql('DROP TABLE __temp__' . $this->getTable('oauth2_access_tokens', true)); $this->addSql('CREATE INDEX IDX_368A4209A76ED395 ON ' . $this->getTable('oauth2_access_tokens', true) . ' (user_id)'); $this->addSql('CREATE INDEX IDX_368A420919EB6921 ON ' . $this->getTable('oauth2_access_tokens', true) . ' (client_id)'); $this->addSql('DROP INDEX IDX_635D765EA76ED395'); - $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('oauth2_clients', true) . ' AS SELECT id, user_id, random_id, secret, redirect_uris, allowed_grant_types, name FROM ' . $this->getTable('oauth2_clients', true) . ''); - $this->addSql('DROP TABLE ' . $this->getTable('oauth2_clients', true) . ''); - $this->addSql('CREATE TABLE ' . $this->getTable('oauth2_clients', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, random_id VARCHAR(255) NOT NULL COLLATE BINARY, secret VARCHAR(255) NOT NULL COLLATE BINARY, name CLOB NOT NULL COLLATE BINARY, redirect_uris CLOB NOT NULL --(DC2Type:array), allowed_grant_types CLOB NOT NULL --(DC2Type:array), CONSTRAINT FK_635D765EA76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); - $this->addSql('INSERT INTO ' . $this->getTable('oauth2_clients', true) . ' (id, user_id, random_id, secret, redirect_uris, allowed_grant_types, name) SELECT id, user_id, random_id, secret, redirect_uris, allowed_grant_types, name FROM __temp__' . $this->getTable('oauth2_clients', true) . ''); - $this->addSql('DROP TABLE __temp__' . $this->getTable('oauth2_clients', true) . ''); + $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('oauth2_clients', true) . ' AS SELECT id, user_id, random_id, secret, redirect_uris, allowed_grant_types, name FROM ' . $this->getTable('oauth2_clients', true)); + $this->addSql('DROP TABLE ' . $this->getTable('oauth2_clients', true)); + $this->addSql('CREATE TABLE ' . $this->getTable('oauth2_clients', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, random_id VARCHAR(255) NOT NULL COLLATE BINARY, secret VARCHAR(255) NOT NULL COLLATE BINARY, name CLOB NOT NULL COLLATE BINARY, redirect_uris CLOB NOT NULL, allowed_grant_types CLOB NOT NULL, CONSTRAINT FK_635D765EA76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO ' . $this->getTable('oauth2_clients', true) . ' (id, user_id, random_id, secret, redirect_uris, allowed_grant_types, name) SELECT id, user_id, random_id, secret, redirect_uris, allowed_grant_types, name FROM __temp__' . $this->getTable('oauth2_clients', true)); + $this->addSql('DROP TABLE __temp__' . $this->getTable('oauth2_clients', true)); $this->addSql('CREATE INDEX IDX_635D765EA76ED395 ON ' . $this->getTable('oauth2_clients', true) . ' (user_id)'); $this->addSql('DROP INDEX IDX_20C9FB24A76ED395'); $this->addSql('DROP INDEX IDX_20C9FB2419EB6921'); $this->addSql('DROP INDEX UNIQ_20C9FB245F37A13B'); - $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('oauth2_refresh_tokens', true) . ' AS SELECT id, client_id, user_id, token, expires_at, scope FROM ' . $this->getTable('oauth2_refresh_tokens', true) . ''); - $this->addSql('DROP TABLE ' . $this->getTable('oauth2_refresh_tokens', true) . ''); + $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('oauth2_refresh_tokens', true) . ' AS SELECT id, client_id, user_id, token, expires_at, scope FROM ' . $this->getTable('oauth2_refresh_tokens', true)); + $this->addSql('DROP TABLE ' . $this->getTable('oauth2_refresh_tokens', true)); $this->addSql('CREATE TABLE ' . $this->getTable('oauth2_refresh_tokens', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, client_id INTEGER NOT NULL, user_id INTEGER DEFAULT NULL, expires_at INTEGER DEFAULT NULL, token VARCHAR(191) NOT NULL, scope VARCHAR(191) NOT NULL, CONSTRAINT FK_20C9FB2419EB6921 FOREIGN KEY (client_id) REFERENCES ' . $this->getTable('oauth2_clients', true) . ' (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_20C9FB24A76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); - $this->addSql('INSERT INTO ' . $this->getTable('oauth2_refresh_tokens', true) . ' (id, client_id, user_id, token, expires_at, scope) SELECT id, client_id, user_id, token, expires_at, scope FROM __temp__' . $this->getTable('oauth2_refresh_tokens', true) . ''); - $this->addSql('DROP TABLE __temp__' . $this->getTable('oauth2_refresh_tokens', true) . ''); + $this->addSql('INSERT INTO ' . $this->getTable('oauth2_refresh_tokens', true) . ' (id, client_id, user_id, token, expires_at, scope) SELECT id, client_id, user_id, token, expires_at, scope FROM __temp__' . $this->getTable('oauth2_refresh_tokens', true)); + $this->addSql('DROP TABLE __temp__' . $this->getTable('oauth2_refresh_tokens', true)); $this->addSql('CREATE INDEX IDX_20C9FB24A76ED395 ON ' . $this->getTable('oauth2_refresh_tokens', true) . ' (user_id)'); $this->addSql('CREATE INDEX IDX_20C9FB2419EB6921 ON ' . $this->getTable('oauth2_refresh_tokens', true) . ' (client_id)'); $this->addSql('DROP INDEX IDX_EE52E3FAA76ED395'); $this->addSql('DROP INDEX IDX_EE52E3FA19EB6921'); $this->addSql('DROP INDEX UNIQ_EE52E3FA5F37A13B'); - $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('oauth2_auth_codes', true) . ' AS SELECT id, client_id, user_id, token, redirect_uri, expires_at, scope FROM ' . $this->getTable('oauth2_auth_codes', true) . ''); - $this->addSql('DROP TABLE ' . $this->getTable('oauth2_auth_codes', true) . ''); + $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('oauth2_auth_codes', true) . ' AS SELECT id, client_id, user_id, token, redirect_uri, expires_at, scope FROM ' . $this->getTable('oauth2_auth_codes', true)); + $this->addSql('DROP TABLE ' . $this->getTable('oauth2_auth_codes', true)); $this->addSql('CREATE TABLE ' . $this->getTable('oauth2_auth_codes', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, client_id INTEGER NOT NULL, user_id INTEGER DEFAULT NULL, redirect_uri CLOB NOT NULL COLLATE BINARY, expires_at INTEGER DEFAULT NULL, token VARCHAR(191) NOT NULL, scope VARCHAR(191) NOT NULL, CONSTRAINT FK_EE52E3FA19EB6921 FOREIGN KEY (client_id) REFERENCES ' . $this->getTable('oauth2_clients', true) . ' (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EE52E3FAA76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); - $this->addSql('INSERT INTO ' . $this->getTable('oauth2_auth_codes', true) . ' (id, client_id, user_id, token, redirect_uri, expires_at, scope) SELECT id, client_id, user_id, token, redirect_uri, expires_at, scope FROM __temp__' . $this->getTable('oauth2_auth_codes', true) . ''); - $this->addSql('DROP TABLE __temp__' . $this->getTable('oauth2_auth_codes', true) . ''); + $this->addSql('INSERT INTO ' . $this->getTable('oauth2_auth_codes', true) . ' (id, client_id, user_id, token, redirect_uri, expires_at, scope) SELECT id, client_id, user_id, token, redirect_uri, expires_at, scope FROM __temp__' . $this->getTable('oauth2_auth_codes', true)); + $this->addSql('DROP TABLE __temp__' . $this->getTable('oauth2_auth_codes', true)); $this->addSql('CREATE INDEX IDX_EE52E3FAA76ED395 ON ' . $this->getTable('oauth2_auth_codes', true) . ' (user_id)'); $this->addSql('CREATE INDEX IDX_EE52E3FA19EB6921 ON ' . $this->getTable('oauth2_auth_codes', true) . ' (client_id)'); break; From 754bf12e6724b1cd6c6d2da85a377cd96865131d Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Fri, 10 May 2019 21:15:46 +0200 Subject: [PATCH 087/107] Fix SQLite constraint --- app/DoctrineMigrations/Version20190510141130.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/DoctrineMigrations/Version20190510141130.php b/app/DoctrineMigrations/Version20190510141130.php index 5fc1dba48..524aa452b 100644 --- a/app/DoctrineMigrations/Version20190510141130.php +++ b/app/DoctrineMigrations/Version20190510141130.php @@ -24,7 +24,7 @@ final class Version20190510141130 extends WallabagMigration $this->addSql('DROP INDEX UNIQ_368A42095F37A13B'); $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('oauth2_access_tokens', true) . ' AS SELECT id, client_id, user_id, token, expires_at, scope FROM ' . $this->getTable('oauth2_access_tokens', true)); $this->addSql('DROP TABLE ' . $this->getTable('oauth2_access_tokens', true)); - $this->addSql('CREATE TABLE ' . $this->getTable('oauth2_access_tokens', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, client_id INTEGER NOT NULL, user_id INTEGER DEFAULT NULL, expires_at INTEGER DEFAULT NULL, token VARCHAR(191) NOT NULL, scope VARCHAR(191) NOT NULL, CONSTRAINT FK_368A420919EB6921 FOREIGN KEY (client_id) REFERENCES ' . $this->getTable('oauth2_clients', true) . ' (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_368A4209A76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('CREATE TABLE ' . $this->getTable('oauth2_access_tokens', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, client_id INTEGER NOT NULL, user_id INTEGER DEFAULT NULL, expires_at INTEGER DEFAULT NULL, token VARCHAR(191) NOT NULL, scope VARCHAR(191) NULL, CONSTRAINT FK_368A420919EB6921 FOREIGN KEY (client_id) REFERENCES ' . $this->getTable('oauth2_clients', true) . ' (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_368A4209A76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); $this->addSql('INSERT INTO ' . $this->getTable('oauth2_access_tokens', true) . ' (id, client_id, user_id, token, expires_at, scope) SELECT id, client_id, user_id, token, expires_at, scope FROM __temp__' . $this->getTable('oauth2_access_tokens', true)); $this->addSql('DROP TABLE __temp__' . $this->getTable('oauth2_access_tokens', true)); $this->addSql('CREATE INDEX IDX_368A4209A76ED395 ON ' . $this->getTable('oauth2_access_tokens', true) . ' (user_id)'); @@ -43,7 +43,7 @@ final class Version20190510141130 extends WallabagMigration $this->addSql('DROP INDEX UNIQ_20C9FB245F37A13B'); $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('oauth2_refresh_tokens', true) . ' AS SELECT id, client_id, user_id, token, expires_at, scope FROM ' . $this->getTable('oauth2_refresh_tokens', true)); $this->addSql('DROP TABLE ' . $this->getTable('oauth2_refresh_tokens', true)); - $this->addSql('CREATE TABLE ' . $this->getTable('oauth2_refresh_tokens', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, client_id INTEGER NOT NULL, user_id INTEGER DEFAULT NULL, expires_at INTEGER DEFAULT NULL, token VARCHAR(191) NOT NULL, scope VARCHAR(191) NOT NULL, CONSTRAINT FK_20C9FB2419EB6921 FOREIGN KEY (client_id) REFERENCES ' . $this->getTable('oauth2_clients', true) . ' (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_20C9FB24A76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('CREATE TABLE ' . $this->getTable('oauth2_refresh_tokens', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, client_id INTEGER NOT NULL, user_id INTEGER DEFAULT NULL, expires_at INTEGER DEFAULT NULL, token VARCHAR(191) NOT NULL, scope VARCHAR(191) NULL, CONSTRAINT FK_20C9FB2419EB6921 FOREIGN KEY (client_id) REFERENCES ' . $this->getTable('oauth2_clients', true) . ' (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_20C9FB24A76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); $this->addSql('INSERT INTO ' . $this->getTable('oauth2_refresh_tokens', true) . ' (id, client_id, user_id, token, expires_at, scope) SELECT id, client_id, user_id, token, expires_at, scope FROM __temp__' . $this->getTable('oauth2_refresh_tokens', true)); $this->addSql('DROP TABLE __temp__' . $this->getTable('oauth2_refresh_tokens', true)); $this->addSql('CREATE INDEX IDX_20C9FB24A76ED395 ON ' . $this->getTable('oauth2_refresh_tokens', true) . ' (user_id)'); @@ -54,7 +54,7 @@ final class Version20190510141130 extends WallabagMigration $this->addSql('DROP INDEX UNIQ_EE52E3FA5F37A13B'); $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('oauth2_auth_codes', true) . ' AS SELECT id, client_id, user_id, token, redirect_uri, expires_at, scope FROM ' . $this->getTable('oauth2_auth_codes', true)); $this->addSql('DROP TABLE ' . $this->getTable('oauth2_auth_codes', true)); - $this->addSql('CREATE TABLE ' . $this->getTable('oauth2_auth_codes', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, client_id INTEGER NOT NULL, user_id INTEGER DEFAULT NULL, redirect_uri CLOB NOT NULL COLLATE BINARY, expires_at INTEGER DEFAULT NULL, token VARCHAR(191) NOT NULL, scope VARCHAR(191) NOT NULL, CONSTRAINT FK_EE52E3FA19EB6921 FOREIGN KEY (client_id) REFERENCES ' . $this->getTable('oauth2_clients', true) . ' (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EE52E3FAA76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('CREATE TABLE ' . $this->getTable('oauth2_auth_codes', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, client_id INTEGER NOT NULL, user_id INTEGER DEFAULT NULL, redirect_uri CLOB NOT NULL COLLATE BINARY, expires_at INTEGER DEFAULT NULL, token VARCHAR(191) NOT NULL, scope VARCHAR(191) NULL, CONSTRAINT FK_EE52E3FA19EB6921 FOREIGN KEY (client_id) REFERENCES ' . $this->getTable('oauth2_clients', true) . ' (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EE52E3FAA76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); $this->addSql('INSERT INTO ' . $this->getTable('oauth2_auth_codes', true) . ' (id, client_id, user_id, token, redirect_uri, expires_at, scope) SELECT id, client_id, user_id, token, redirect_uri, expires_at, scope FROM __temp__' . $this->getTable('oauth2_auth_codes', true)); $this->addSql('DROP TABLE __temp__' . $this->getTable('oauth2_auth_codes', true)); $this->addSql('CREATE INDEX IDX_EE52E3FAA76ED395 ON ' . $this->getTable('oauth2_auth_codes', true) . ' (user_id)'); From b8b37ccdea6b453aa228cb79755752b5738b98b9 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Wed, 15 May 2019 14:58:40 +0200 Subject: [PATCH 088/107] CS --- .../GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php b/tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php index 44affde83..9e0a91365 100644 --- a/tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php +++ b/tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php @@ -12,6 +12,8 @@ use Wallabag\CoreBundle\GuzzleSiteAuthenticator\GrabySiteConfigBuilder; class GrabySiteConfigBuilderTest extends WallabagCoreTestCase { + private $builder; + public function testBuildConfigExists() { $grabyConfigBuilderMock = $this->getMockBuilder('Graby\SiteConfig\ConfigBuilder') @@ -157,8 +159,8 @@ class GrabySiteConfigBuilderTest extends WallabagCoreTestCase ->disableOriginalConstructor() ->getMock(); $siteCrentialRepo->expects($this->once()) - ->method('findOneByHostAndUser') - ->with('example.com', 1) + ->method('findOneByHostsAndUser') + ->with(['example.com', '.com'], 1) ->willReturn(['username' => 'foo', 'password' => 'bar']); $user = $this->getMockBuilder('Wallabag\UserBundle\Entity\User') From 2c290747cb0d235392f6e5d22205a706c6474168 Mon Sep 17 00:00:00 2001 From: Kevin Decherf Date: Sun, 12 May 2019 00:00:00 +0200 Subject: [PATCH 089/107] api/entries: add parameter detail to exclude or include content in response detail=metadata will nullify the content field of entries in order to make smaller responses. detail=full keeps the former behavior, it sends the content of entries. It's the default, for backward compatibility. Fixes #2817 Signed-off-by: Kevin Decherf --- .../Controller/EntryRestController.php | 6 +++++- .../CoreBundle/Repository/EntryRepository.php | 17 ++++++++++++++- .../Controller/EntryRestControllerTest.php | 21 +++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/Wallabag/ApiBundle/Controller/EntryRestController.php b/src/Wallabag/ApiBundle/Controller/EntryRestController.php index 06520af91..aff0534a0 100644 --- a/src/Wallabag/ApiBundle/Controller/EntryRestController.php +++ b/src/Wallabag/ApiBundle/Controller/EntryRestController.php @@ -103,6 +103,7 @@ class EntryRestController extends WallabagRestController * {"name"="tags", "dataType"="string", "required"=false, "format"="api,rest", "description"="a list of tags url encoded. Will returns entries that matches ALL tags."}, * {"name"="since", "dataType"="integer", "required"=false, "format"="default '0'", "description"="The timestamp since when you want entries updated."}, * {"name"="public", "dataType"="integer", "required"=false, "format"="1 or 0, all entries by default", "description"="filter by entries with a public link"}, + * {"name"="detail", "dataType"="string", "required"=false, "format"="metadata or full, metadata by default", "description"="include content field if 'full'. 'full' by default for backward compatibility."}, * } * ) * @@ -121,6 +122,7 @@ class EntryRestController extends WallabagRestController $perPage = (int) $request->query->get('perPage', 30); $tags = \is_array($request->query->get('tags')) ? '' : (string) $request->query->get('tags', ''); $since = $request->query->get('since', 0); + $detail = strtolower($request->query->get('detail', 'full')); try { /** @var \Pagerfanta\Pagerfanta $pager */ @@ -132,7 +134,8 @@ class EntryRestController extends WallabagRestController $sort, $order, $since, - $tags + $tags, + $detail ); } catch (\Exception $e) { throw new BadRequestHttpException($e->getMessage()); @@ -156,6 +159,7 @@ class EntryRestController extends WallabagRestController 'perPage' => $perPage, 'tags' => $tags, 'since' => $since, + 'detail' => $detail, ], true ) diff --git a/src/Wallabag/CoreBundle/Repository/EntryRepository.php b/src/Wallabag/CoreBundle/Repository/EntryRepository.php index f50897296..3990932e1 100644 --- a/src/Wallabag/CoreBundle/Repository/EntryRepository.php +++ b/src/Wallabag/CoreBundle/Repository/EntryRepository.php @@ -139,15 +139,30 @@ class EntryRepository extends EntityRepository * @param string $order * @param int $since * @param string $tags + * @param string $detail 'metadata' or 'full'. Include content field if 'full' + * + * @todo Breaking change: replace default detail=full by detail=metadata in a future version * * @return Pagerfanta */ - public function findEntries($userId, $isArchived = null, $isStarred = null, $isPublic = null, $sort = 'created', $order = 'asc', $since = 0, $tags = '') + public function findEntries($userId, $isArchived = null, $isStarred = null, $isPublic = null, $sort = 'created', $order = 'asc', $since = 0, $tags = '', $detail = 'full') { + if (!\in_array(strtolower($detail), ['full', 'metadata'], true)) { + throw new \Exception('Detail "' . $detail . '" parameter is wrong, allowed: full or metadata'); + } + $qb = $this->createQueryBuilder('e') ->leftJoin('e.tags', 't') ->where('e.user = :userId')->setParameter('userId', $userId); + if ('metadata' === $detail) { + $fieldNames = $this->getClassMetadata()->getFieldNames(); + $fields = array_filter($fieldNames, function ($k) { + return 'content' !== $k; + }); + $qb->select(sprintf('partial e.{%s}', implode(',', $fields))); + } + if (null !== $isArchived) { $qb->andWhere('e.isArchived = :isArchived')->setParameter('isArchived', (bool) $isArchived); } diff --git a/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php b/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php index 8cc12ed37..8b7898eea 100644 --- a/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php +++ b/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php @@ -133,6 +133,27 @@ class EntryRestControllerTest extends WallabagApiTestCase $this->assertSame(1, $content['page']); $this->assertGreaterThanOrEqual(1, $content['pages']); + $this->assertNotNull($content['_embedded']['items'][0]['content']); + + $this->assertSame('application/json', $this->client->getResponse()->headers->get('Content-Type')); + } + + public function testGetEntriesDetailMetadata() + { + $this->client->request('GET', '/api/entries?detail=metadata'); + + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + + $content = json_decode($this->client->getResponse()->getContent(), true); + + $this->assertGreaterThanOrEqual(1, \count($content)); + $this->assertNotEmpty($content['_embedded']['items']); + $this->assertGreaterThanOrEqual(1, $content['total']); + $this->assertSame(1, $content['page']); + $this->assertGreaterThanOrEqual(1, $content['pages']); + + $this->assertNull($content['_embedded']['items'][0]['content']); + $this->assertSame('application/json', $this->client->getResponse()->headers->get('Content-Type')); } From 423efadefc2459c7b4a2eabc32edaed918e1075d Mon Sep 17 00:00:00 2001 From: nicofrand Date: Fri, 10 May 2019 23:01:07 +0200 Subject: [PATCH 090/107] Set first picture as preview picture --- .../CoreBundle/Helper/ContentProxy.php | 20 ++++++++++--- .../CoreBundle/Helper/DownloadImages.php | 29 ++++++++++++++----- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/Wallabag/CoreBundle/Helper/ContentProxy.php b/src/Wallabag/CoreBundle/Helper/ContentProxy.php index bc257ffbc..ca01dec8d 100644 --- a/src/Wallabag/CoreBundle/Helper/ContentProxy.php +++ b/src/Wallabag/CoreBundle/Helper/ContentProxy.php @@ -12,8 +12,8 @@ use Wallabag\CoreBundle\Entity\Entry; use Wallabag\CoreBundle\Tools\Utils; /** - * This kind of proxy class take care of getting the content from an url - * and update the entry with what it found. + * This kind of proxy class takes care of getting the content from an url + * and updates the entry with what it found. */ class ContentProxy { @@ -289,13 +289,25 @@ class ContentProxy $this->updateLanguage($entry, $content['language']); } + $previewPictureUrl = ''; if (!empty($content['open_graph']['og_image'])) { - $this->updatePreviewPicture($entry, $content['open_graph']['og_image']); + $previewPictureUrl = $content['open_graph']['og_image']; } // if content is an image, define it as a preview too if (!empty($content['content_type']) && \in_array($this->mimeGuesser->guess($content['content_type']), ['jpeg', 'jpg', 'gif', 'png'], true)) { - $this->updatePreviewPicture($entry, $content['url']); + $previewPictureUrl = $content['url']; + } elseif (empty($previewPictureUrl)) { + $this->logger->debug('Extracting images from content to provide a default preview picture'); + $imagesUrls = DownloadImages::extractImagesUrlsFromHtml($content['html']); + $this->logger->debug(\count($imagesUrls) . ' pictures found'); + if (!empty($imagesUrls)) { + $previewPictureUrl = $imagesUrls[0]; + } + } + + if (!empty($previewPictureUrl)) { + $this->updatePreviewPicture($entry, $previewPictureUrl); } if (!empty($content['content_type'])) { diff --git a/src/Wallabag/CoreBundle/Helper/DownloadImages.php b/src/Wallabag/CoreBundle/Helper/DownloadImages.php index 9a7e98285..c1645e45a 100644 --- a/src/Wallabag/CoreBundle/Helper/DownloadImages.php +++ b/src/Wallabag/CoreBundle/Helper/DownloadImages.php @@ -30,6 +30,25 @@ class DownloadImages $this->setFolder(); } + /** + * Process the html and extract images URLs from it. + * + * @param string $html + * + * @return string[] + */ + public static function extractImagesUrlsFromHtml($html) + { + $crawler = new Crawler($html); + $imagesCrawler = $crawler + ->filterXpath('//img'); + $imagesUrls = $imagesCrawler + ->extract(['src']); + $imagesSrcsetUrls = self::getSrcsetUrls($imagesCrawler); + + return array_unique(array_merge($imagesUrls, $imagesSrcsetUrls)); + } + /** * Process the html and extract image from it, save them to local and return the updated html. * @@ -41,13 +60,7 @@ class DownloadImages */ public function processHtml($entryId, $html, $url) { - $crawler = new Crawler($html); - $imagesCrawler = $crawler - ->filterXpath('//img'); - $imagesUrls = $imagesCrawler - ->extract(['src']); - $imagesSrcsetUrls = $this->getSrcsetUrls($imagesCrawler); - $imagesUrls = array_unique(array_merge($imagesUrls, $imagesSrcsetUrls)); + $imagesUrls = self::extractImagesUrlsFromHtml($html); $relativePath = $this->getRelativePath($entryId); @@ -199,7 +212,7 @@ class DownloadImages * * @return array An array of urls */ - private function getSrcsetUrls(Crawler $imagesCrawler) + private static function getSrcsetUrls(Crawler $imagesCrawler) { $urls = []; $iterator = $imagesCrawler From 715fabf8f286177c7abb216f0c867a989de18c32 Mon Sep 17 00:00:00 2001 From: nicofrand Date: Mon, 13 May 2019 21:56:52 +0200 Subject: [PATCH 091/107] [tests] Set first picture as preview picture --- .../CoreBundle/Helper/ContentProxyTest.php | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php b/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php index 508adb1b6..8ed7fa5f8 100644 --- a/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php +++ b/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php @@ -214,6 +214,90 @@ class ContentProxyTest extends TestCase $this->assertSame('1.1.1.1', $entry->getDomainName()); } + public function testWithContentAndContentImage() + { + $tagger = $this->getTaggerMock(); + $tagger->expects($this->once()) + ->method('tag'); + + $graby = $this->getMockBuilder('Graby\Graby') + ->setMethods(['fetchContent']) + ->disableOriginalConstructor() + ->getMock(); + + $graby->expects($this->any()) + ->method('fetchContent') + ->willReturn([ + 'html' => "

    Test

    ", + 'title' => 'this is my title', + 'url' => 'http://1.1.1.1', + 'content_type' => 'text/html', + 'language' => 'fr', + 'status' => '200', + 'open_graph' => [ + 'og_title' => 'my OG title', + 'og_description' => 'OG desc', + 'og_image' => null, + ], + ]); + + $proxy = new ContentProxy($graby, $tagger, $this->getValidator(), $this->getLogger(), $this->fetchingErrorMessage); + $entry = new Entry(new User()); + $proxy->updateEntry($entry, 'http://0.0.0.0'); + + $this->assertSame('http://1.1.1.1', $entry->getUrl()); + $this->assertSame('this is my title', $entry->getTitle()); + $this->assertSame("

    Test

    ", $entry->getContent()); + $this->assertSame('http://3.3.3.3/cover.jpg', $entry->getPreviewPicture()); + $this->assertSame('text/html', $entry->getMimetype()); + $this->assertSame('fr', $entry->getLanguage()); + $this->assertSame('200', $entry->getHttpStatus()); + $this->assertSame(0.0, $entry->getReadingTime()); + $this->assertSame('1.1.1.1', $entry->getDomainName()); + } + + public function testWithContentImageAndOgImage() + { + $tagger = $this->getTaggerMock(); + $tagger->expects($this->once()) + ->method('tag'); + + $graby = $this->getMockBuilder('Graby\Graby') + ->setMethods(['fetchContent']) + ->disableOriginalConstructor() + ->getMock(); + + $graby->expects($this->any()) + ->method('fetchContent') + ->willReturn([ + 'html' => "

    Test

    ", + 'title' => 'this is my title', + 'url' => 'http://1.1.1.1', + 'content_type' => 'text/html', + 'language' => 'fr', + 'status' => '200', + 'open_graph' => [ + 'og_title' => 'my OG title', + 'og_description' => 'OG desc', + 'og_image' => 'http://3.3.3.3/cover.jpg', + ], + ]); + + $proxy = new ContentProxy($graby, $tagger, $this->getValidator(), $this->getLogger(), $this->fetchingErrorMessage); + $entry = new Entry(new User()); + $proxy->updateEntry($entry, 'http://0.0.0.0'); + + $this->assertSame('http://1.1.1.1', $entry->getUrl()); + $this->assertSame('this is my title', $entry->getTitle()); + $this->assertSame("

    Test

    ", $entry->getContent()); + $this->assertSame('http://3.3.3.3/cover.jpg', $entry->getPreviewPicture()); + $this->assertSame('text/html', $entry->getMimetype()); + $this->assertSame('fr', $entry->getLanguage()); + $this->assertSame('200', $entry->getHttpStatus()); + $this->assertSame(0.0, $entry->getReadingTime()); + $this->assertSame('1.1.1.1', $entry->getDomainName()); + } + public function testWithContentAndBadLanguage() { $tagger = $this->getTaggerMock(); From d99e6423f4bd54595a8a805dd1efd0bd94e8bb09 Mon Sep 17 00:00:00 2001 From: nicofrand Date: Tue, 21 May 2019 20:10:57 +0200 Subject: [PATCH 092/107] [tests] Fix pre-existing tests (preview now imported + records added) --- tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php | 2 +- .../ImportBundle/Controller/WallabagV1ControllerTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php b/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php index 8ed7fa5f8..c7caac1d3 100644 --- a/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php +++ b/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php @@ -499,7 +499,7 @@ class ContentProxyTest extends TestCase $records = $handler->getRecords(); - $this->assertCount(1, $records); + $this->assertCount(3, $records); $this->assertContains('Error while defining date', $records[0]['message']); } diff --git a/tests/Wallabag/ImportBundle/Controller/WallabagV1ControllerTest.php b/tests/Wallabag/ImportBundle/Controller/WallabagV1ControllerTest.php index 1f57939d9..2a8e7c899 100644 --- a/tests/Wallabag/ImportBundle/Controller/WallabagV1ControllerTest.php +++ b/tests/Wallabag/ImportBundle/Controller/WallabagV1ControllerTest.php @@ -121,7 +121,7 @@ class WallabagV1ControllerTest extends WallabagCoreTestCase $this->assertInstanceOf('Wallabag\CoreBundle\Entity\Entry', $content); $this->assertEmpty($content->getMimetype(), 'Mimetype for http://www.framablog.org is empty'); - $this->assertEmpty($content->getPreviewPicture(), 'Preview picture for http://www.framablog.org is empty'); + $this->assertSame($content->getPreviewPicture(), 'http://www.framablog.org/public/_img/framablog/wallaby_baby.jpg'); $this->assertEmpty($content->getLanguage(), 'Language for http://www.framablog.org is empty'); $tags = $content->getTags(); From c1a1c46e9ddc492606af9af82f1c4054e2c5ac20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Benoist?= Date: Thu, 23 May 2019 08:51:54 +0200 Subject: [PATCH 093/107] Force PHP version in Dockerfile --- docker/php/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index d0266ec74..b632cb8a2 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -1,4 +1,4 @@ -FROM php:fpm +FROM php:7.2-fpm # Default timezone. To change it, use the argument in the docker-compose.yml file ARG timezone='Europe/Paris' From 9ca670c801cddef0ba47adc3be02945164f6bc85 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Fri, 24 May 2019 14:37:54 +0200 Subject: [PATCH 094/107] Fix Instapaper import date --- src/Wallabag/ImportBundle/Import/InstapaperImport.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Wallabag/ImportBundle/Import/InstapaperImport.php b/src/Wallabag/ImportBundle/Import/InstapaperImport.php index 439c978c7..f7bee9ef0 100644 --- a/src/Wallabag/ImportBundle/Import/InstapaperImport.php +++ b/src/Wallabag/ImportBundle/Import/InstapaperImport.php @@ -93,6 +93,10 @@ class InstapaperImport extends AbstractImport return false; } + // most recent articles are first, which means we should create them at the end so they will show up first + // as Instapaper doesn't export the creation date of the article + $entries = array_reverse($entries); + if ($this->producer) { $this->parseEntriesForProducer($entries); From 31e276fc1636b41b03b7c29127681de257c16b06 Mon Sep 17 00:00:00 2001 From: Olivier Mehani Date: Thu, 2 May 2019 21:19:20 +1000 Subject: [PATCH 095/107] EntryRestController::getEntriesExistsAction: always find by hashed url Simplify the logic from #3158 by hashing all the urls from the request, and only doing a search by hash. This allows to get performance benefits from the new indexed hash column even when using older clients that do not hash the URL in the request. Fixes: #3158, #3919 Signed-off-by: Olivier Mehani --- .../Controller/EntryRestController.php | 79 +++++++++++-------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/src/Wallabag/ApiBundle/Controller/EntryRestController.php b/src/Wallabag/ApiBundle/Controller/EntryRestController.php index aff0534a0..17b53a018 100644 --- a/src/Wallabag/ApiBundle/Controller/EntryRestController.php +++ b/src/Wallabag/ApiBundle/Controller/EntryRestController.php @@ -43,50 +43,59 @@ class EntryRestController extends WallabagRestController $returnId = (null === $request->query->get('return_id')) ? false : (bool) $request->query->get('return_id'); - $urls = $request->query->get('urls', []); $hashedUrls = $request->query->get('hashed_urls', []); - - // handle multiple urls first - if (!empty($hashedUrls)) { - $results = []; - foreach ($hashedUrls as $hashedUrl) { - $res = $repo->findByHashedUrlAndUserId($hashedUrl, $this->getUser()->getId()); - - $results[$hashedUrl] = $this->returnExistInformation($res, $returnId); - } - - return $this->sendResponse($results); - } - - // @deprecated, to be remove in 3.0 - if (!empty($urls)) { - $results = []; - foreach ($urls as $url) { - $res = $repo->findByUrlAndUserId($url, $this->getUser()->getId()); - - $results[$url] = $this->returnExistInformation($res, $returnId); - } - - return $this->sendResponse($results); - } - - // let's see if it is a simple url? - $url = $request->query->get('url', ''); $hashedUrl = $request->query->get('hashed_url', ''); + if (!empty($hashedUrl)) { + $hashedUrls[] = $hashedUrl; + } - if (empty($url) && empty($hashedUrl)) { + $urls = $request->query->get('urls', []); + $url = $request->query->get('url', ''); + if (!empty($url)) { + $urls[] = $url; + } + + $urlHashMap = []; + foreach($urls as $urlToHash) { + $urlHash = hash('sha1', $urlToHash); // XXX: the hash logic would better be in a separate util to avoid duplication with GenerateUrlHashesCommand::generateHashedUrls + $hashedUrls[] = $urlHash; + $urlHashMap[$urlHash] = $urlToHash; + } + + if (empty($hashedUrls)) { throw $this->createAccessDeniedException('URL is empty?, logged user id: ' . $this->getUser()->getId()); } - $method = 'findByUrlAndUserId'; - if (!empty($hashedUrl)) { - $method = 'findByHashedUrlAndUserId'; - $url = $hashedUrl; + $results = []; + foreach ($hashedUrls as $hashedUrlToSearch) { + $res = $repo->findByHashedUrlAndUserId($hashedUrlToSearch, $this->getUser()->getId()); + + $results[$hashedUrlToSearch] = $this->returnExistInformation($res, $returnId); } - $res = $repo->$method($url, $this->getUser()->getId()); + $results = $this->replaceUrlHashes($results, $urlHashMap); - return $this->sendResponse(['exists' => $this->returnExistInformation($res, $returnId)]); + if (!empty($url) || !empty($hashedUrl)) { + $hu = array_keys($results)[0]; + return $this->sendResponse(['exists' => $results[$hu]]); + } + return $this->sendResponse($results); + } + + /** + * Replace the hashedUrl keys in $results with the unhashed URL from the + * request, as recorded in $urlHashMap. + */ + private function replaceUrlHashes(array $results, array $urlHashMap) { + $newResults = []; + foreach($results as $hash => $res) { + if (isset($urlHashMap[$hash])) { + $newResults[$urlHashMap[$hash]] = $res; + } else { + $newResults[$hash] = $res; + } + } + return $newResults; } /** From d5744bf0dfdbee4dbbe380d8a076d07b89fc76e6 Mon Sep 17 00:00:00 2001 From: Olivier Mehani Date: Fri, 3 May 2019 22:23:04 +1000 Subject: [PATCH 096/107] Delegate findByUrlAndUserId to findByHashedUrlAndUserId Signed-off-by: Olivier Mehani --- .../CoreBundle/Repository/EntryRepository.php | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/Wallabag/CoreBundle/Repository/EntryRepository.php b/src/Wallabag/CoreBundle/Repository/EntryRepository.php index 3990932e1..960b682df 100644 --- a/src/Wallabag/CoreBundle/Repository/EntryRepository.php +++ b/src/Wallabag/CoreBundle/Repository/EntryRepository.php @@ -348,17 +348,9 @@ class EntryRepository extends EntityRepository */ public function findByUrlAndUserId($url, $userId) { - $res = $this->createQueryBuilder('e') - ->where('e.url = :url')->setParameter('url', urldecode($url)) - ->andWhere('e.user = :user_id')->setParameter('user_id', $userId) - ->getQuery() - ->getResult(); - - if (\count($res)) { - return current($res); - } - - return false; + return $this->findByHashedUrlAndUserId( + hash('sha1', $url), // XXX: the hash logic would better be in a separate util to avoid duplication with GenerateUrlHashesCommand::generateHashedUrls + $userId); } /** From 4a5516376bf4c8b0cdc1e81d24ce1cca68425785 Mon Sep 17 00:00:00 2001 From: Olivier Mehani Date: Fri, 10 May 2019 22:07:55 +1000 Subject: [PATCH 097/107] Add Wallabag\CoreBundle\Helper\UrlHasher Signed-off-by: Olivier Mehani --- .../Controller/EntryRestController.php | 41 +++++++++++-------- .../Command/GenerateUrlHashesCommand.php | 5 ++- src/Wallabag/CoreBundle/Entity/Entry.php | 3 +- src/Wallabag/CoreBundle/Helper/UrlHasher.php | 22 ++++++++++ .../CoreBundle/Repository/EntryRepository.php | 29 ++++++++++++- 5 files changed, 79 insertions(+), 21 deletions(-) create mode 100644 src/Wallabag/CoreBundle/Helper/UrlHasher.php diff --git a/src/Wallabag/ApiBundle/Controller/EntryRestController.php b/src/Wallabag/ApiBundle/Controller/EntryRestController.php index 17b53a018..77eb489e6 100644 --- a/src/Wallabag/ApiBundle/Controller/EntryRestController.php +++ b/src/Wallabag/ApiBundle/Controller/EntryRestController.php @@ -14,6 +14,7 @@ use Wallabag\CoreBundle\Entity\Entry; use Wallabag\CoreBundle\Entity\Tag; use Wallabag\CoreBundle\Event\EntryDeletedEvent; use Wallabag\CoreBundle\Event\EntrySavedEvent; +use Wallabag\CoreBundle\Helper\UrlHasher; class EntryRestController extends WallabagRestController { @@ -56,8 +57,8 @@ class EntryRestController extends WallabagRestController } $urlHashMap = []; - foreach($urls as $urlToHash) { - $urlHash = hash('sha1', $urlToHash); // XXX: the hash logic would better be in a separate util to avoid duplication with GenerateUrlHashesCommand::generateHashedUrls + foreach ($urls as $urlToHash) { + $urlHash = UrlHasher::hashUrl($urlToHash); $hashedUrls[] = $urlHash; $urlHashMap[$urlHash] = $urlToHash; } @@ -77,25 +78,11 @@ class EntryRestController extends WallabagRestController if (!empty($url) || !empty($hashedUrl)) { $hu = array_keys($results)[0]; + return $this->sendResponse(['exists' => $results[$hu]]); } - return $this->sendResponse($results); - } - /** - * Replace the hashedUrl keys in $results with the unhashed URL from the - * request, as recorded in $urlHashMap. - */ - private function replaceUrlHashes(array $results, array $urlHashMap) { - $newResults = []; - foreach($results as $hash => $res) { - if (isset($urlHashMap[$hash])) { - $newResults[$urlHashMap[$hash]] = $res; - } else { - $newResults[$hash] = $res; - } - } - return $newResults; + return $this->sendResponse($results); } /** @@ -815,6 +802,24 @@ class EntryRestController extends WallabagRestController return $this->sendResponse($results); } + /** + * Replace the hashedUrl keys in $results with the unhashed URL from the + * request, as recorded in $urlHashMap. + */ + private function replaceUrlHashes(array $results, array $urlHashMap) + { + $newResults = []; + foreach ($results as $hash => $res) { + if (isset($urlHashMap[$hash])) { + $newResults[$urlHashMap[$hash]] = $res; + } else { + $newResults[$hash] = $res; + } + } + + return $newResults; + } + /** * Retrieve value from the request. * Used for POST & PATCH on a an entry. diff --git a/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php b/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php index 45bd8c5ff..775b04138 100644 --- a/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php +++ b/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php @@ -7,6 +7,7 @@ use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Wallabag\CoreBundle\Helper\UrlHasher; use Wallabag\UserBundle\Entity\User; class GenerateUrlHashesCommand extends ContainerAwareCommand @@ -65,7 +66,9 @@ class GenerateUrlHashesCommand extends ContainerAwareCommand $i = 1; foreach ($entries as $entry) { - $entry->setHashedUrl(hash('sha1', $entry->getUrl())); + $entry->setHashedUrl( + UrlHasher::hashUrl($entry->getUrl()) + ); $em->persist($entry); if (0 === ($i % 20)) { diff --git a/src/Wallabag/CoreBundle/Entity/Entry.php b/src/Wallabag/CoreBundle/Entity/Entry.php index c3fb87d21..1b4367fd7 100644 --- a/src/Wallabag/CoreBundle/Entity/Entry.php +++ b/src/Wallabag/CoreBundle/Entity/Entry.php @@ -13,6 +13,7 @@ use JMS\Serializer\Annotation\XmlRoot; use Symfony\Component\Validator\Constraints as Assert; use Wallabag\AnnotationBundle\Entity\Annotation; use Wallabag\CoreBundle\Helper\EntityTimestampsTrait; +use Wallabag\CoreBundle\Helper\UrlHasher; use Wallabag\UserBundle\Entity\User; /** @@ -324,7 +325,7 @@ class Entry public function setUrl($url) { $this->url = $url; - $this->hashedUrl = hash('sha1', $url); + $this->hashedUrl = UrlHasher::hashUrl($url); return $this; } diff --git a/src/Wallabag/CoreBundle/Helper/UrlHasher.php b/src/Wallabag/CoreBundle/Helper/UrlHasher.php new file mode 100644 index 000000000..e44f219a8 --- /dev/null +++ b/src/Wallabag/CoreBundle/Helper/UrlHasher.php @@ -0,0 +1,22 @@ +findByHashedUrlAndUserId( - hash('sha1', $url), // XXX: the hash logic would better be in a separate util to avoid duplication with GenerateUrlHashesCommand::generateHashedUrls + UrlHasher::hashUrl($url), $userId); } @@ -506,6 +507,32 @@ class EntryRepository extends EntityRepository return $this->find($randomId); } + /** + * Inject a UrlHasher. + * + * @param UrlHasher $hasher + */ + public function setUrlHasher(UrlHasher $hasher) + { + $this->urlHasher = $hasher; + } + + /** + * Get the UrlHasher, or create a default one if not injected. + * + * XXX: the default uses the default hash algorithm + * + * @return UrlHasher + */ + protected function getUrlHasher() + { + if (!isset($this->urlHasher)) { + $this->setUrlHasher(new UrlHasher()); + } + + return $this->urlHasher; + } + /** * Return a query builder to be used by other getBuilderFor* method. * From 0132ccd2a2e73a831fa198940c369bcdd5249e8b Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Fri, 24 May 2019 15:15:12 +0200 Subject: [PATCH 098/107] Change the way to define algorithm for hashing url --- .../Command/GenerateUrlHashesCommand.php | 6 ++---- src/Wallabag/CoreBundle/Helper/UrlHasher.php | 15 ++++++++------- .../CoreBundle/Repository/EntryRepository.php | 3 ++- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php b/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php index 775b04138..8f2bff114 100644 --- a/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php +++ b/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php @@ -66,9 +66,7 @@ class GenerateUrlHashesCommand extends ContainerAwareCommand $i = 1; foreach ($entries as $entry) { - $entry->setHashedUrl( - UrlHasher::hashUrl($entry->getUrl()) - ); + $entry->setHashedUrl(UrlHasher::hashUrl($entry->getUrl())); $em->persist($entry); if (0 === ($i % 20)) { @@ -87,7 +85,7 @@ class GenerateUrlHashesCommand extends ContainerAwareCommand * * @param string $username * - * @return \Wallabag\UserBundle\Entity\User + * @return User */ private function getUser($username) { diff --git a/src/Wallabag/CoreBundle/Helper/UrlHasher.php b/src/Wallabag/CoreBundle/Helper/UrlHasher.php index e44f219a8..d123eaba3 100644 --- a/src/Wallabag/CoreBundle/Helper/UrlHasher.php +++ b/src/Wallabag/CoreBundle/Helper/UrlHasher.php @@ -7,16 +7,17 @@ namespace Wallabag\CoreBundle\Helper; */ class UrlHasher { - /** @var string */ - const ALGORITHM = 'sha1'; - /** - * @param string $url + * Hash the given url using the given algorithm. + * Hashed url are faster to be retrieved in the database than the real url. * - * @return string hashed $url + * @param string $url + * @param string $algorithm + * + * @return string */ - public static function hashUrl(string $url) + public static function hashUrl(string $url, $algorithm = 'sha1') { - return hash(static::ALGORITHM, $url); + return hash($algorithm, urldecode($url)); } } diff --git a/src/Wallabag/CoreBundle/Repository/EntryRepository.php b/src/Wallabag/CoreBundle/Repository/EntryRepository.php index 37fc1000f..7c4a05ede 100644 --- a/src/Wallabag/CoreBundle/Repository/EntryRepository.php +++ b/src/Wallabag/CoreBundle/Repository/EntryRepository.php @@ -351,7 +351,8 @@ class EntryRepository extends EntityRepository { return $this->findByHashedUrlAndUserId( UrlHasher::hashUrl($url), - $userId); + $userId + ); } /** From 629a3797bcef33943df8ef5631328e05d12634ed Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Fri, 24 May 2019 15:43:30 +0200 Subject: [PATCH 099/107] Remove useless methods Also fix a phpdoc block --- .../Controller/EntryRestController.php | 4 +-- .../CoreBundle/Repository/EntryRepository.php | 26 ------------------- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/src/Wallabag/ApiBundle/Controller/EntryRestController.php b/src/Wallabag/ApiBundle/Controller/EntryRestController.php index 77eb489e6..bdd02129a 100644 --- a/src/Wallabag/ApiBundle/Controller/EntryRestController.php +++ b/src/Wallabag/ApiBundle/Controller/EntryRestController.php @@ -848,8 +848,8 @@ class EntryRestController extends WallabagRestController /** * Return information about the entry if it exist and depending on the id or not. * - * @param Entry|null $entry - * @param bool $returnId + * @param Entry|bool|null $entry + * @param bool $returnId * * @return bool|int */ diff --git a/src/Wallabag/CoreBundle/Repository/EntryRepository.php b/src/Wallabag/CoreBundle/Repository/EntryRepository.php index 7c4a05ede..880e7c65a 100644 --- a/src/Wallabag/CoreBundle/Repository/EntryRepository.php +++ b/src/Wallabag/CoreBundle/Repository/EntryRepository.php @@ -508,32 +508,6 @@ class EntryRepository extends EntityRepository return $this->find($randomId); } - /** - * Inject a UrlHasher. - * - * @param UrlHasher $hasher - */ - public function setUrlHasher(UrlHasher $hasher) - { - $this->urlHasher = $hasher; - } - - /** - * Get the UrlHasher, or create a default one if not injected. - * - * XXX: the default uses the default hash algorithm - * - * @return UrlHasher - */ - protected function getUrlHasher() - { - if (!isset($this->urlHasher)) { - $this->setUrlHasher(new UrlHasher()); - } - - return $this->urlHasher; - } - /** * Return a query builder to be used by other getBuilderFor* method. * From bf9ace0643f654e7ccd9c020b8b501ad56cd19de Mon Sep 17 00:00:00 2001 From: adev Date: Tue, 24 Oct 2017 22:55:40 +0200 Subject: [PATCH 100/107] Use httplug --- app/AppKernel.php | 1 + app/config/config.yml | 19 + composer.json | 12 +- .../CoreBundle/Helper/DownloadImages.php | 21 +- .../CoreBundle/Helper/HttpClientFactory.php | 59 +-- .../CoreBundle/Resources/config/services.yml | 11 +- .../ImportBundle/Import/PocketImport.php | 90 ++-- .../Resources/config/services.yml | 8 +- .../CoreBundle/Helper/DownloadImagesTest.php | 79 +--- .../ImportBundle/Import/PocketImportTest.php | 402 ++++++++---------- 10 files changed, 329 insertions(+), 373 deletions(-) diff --git a/app/AppKernel.php b/app/AppKernel.php index 7d19e9abc..4a54da298 100644 --- a/app/AppKernel.php +++ b/app/AppKernel.php @@ -34,6 +34,7 @@ class AppKernel extends Kernel new FOS\JsRoutingBundle\FOSJsRoutingBundle(), new BD\GuzzleSiteAuthenticatorBundle\BDGuzzleSiteAuthenticatorBundle(), new OldSound\RabbitMqBundle\OldSoundRabbitMqBundle(), + new Http\HttplugBundle\HttplugBundle(), // wallabag bundles new Wallabag\CoreBundle\WallabagCoreBundle(), diff --git a/app/config/config.yml b/app/config/config.yml index 078f277ac..309945c50 100644 --- a/app/config/config.yml +++ b/app/config/config.yml @@ -370,3 +370,22 @@ jms_serializer: sensio_framework_extra: router: annotations: false + +httplug: + clients: + wallabag_core: + factory: 'wallabag_core.http_client_factory' + plugins: ['httplug.plugin.logger'] + wallabag_core.entry.download_images: + factory: 'httplug.factory.auto' + plugins: ['httplug.plugin.logger'] + wallabag_import.pocket.client: + factory: 'httplug.factory.auto' + plugins: + - 'httplug.plugin.logger' + - header_defaults: + headers: + 'content-type': 'application/json' + 'X-Accept': 'application/json' + discovery: + client: false diff --git a/composer.json b/composer.json index b1c144c72..55e7f7651 100644 --- a/composer.json +++ b/composer.json @@ -66,8 +66,9 @@ "simplepie/simplepie": "~1.5", "willdurand/hateoas-bundle": "~1.3", "liip/theme-bundle": "^1.4.6", - "lexik/form-filter-bundle": "^5.0", - "j0k3r/graby": "^1.0", + "lexik/form-filter-bundle": "^5.0.4", + "j0k3r/graby": "^2.0", + "php-http/guzzle5-adapter": "^2.0", "friendsofsymfony/user-bundle": "2.0.*", "friendsofsymfony/oauth-server-bundle": "^1.5", "stof/doctrine-extensions-bundle": "^1.2", @@ -89,7 +90,8 @@ "bdunogier/guzzle-site-authenticator": "^1.0.0", "defuse/php-encryption": "^2.1", "html2text/html2text": "^4.1", - "pragmarx/recovery": "^0.1.0" + "pragmarx/recovery": "^0.1.0", + "php-http/httplug-bundle": "^1.14" }, "require-dev": { "doctrine/doctrine-fixtures-bundle": "~3.0", @@ -101,7 +103,9 @@ "phpstan/phpstan": "^0.11.0", "phpstan/phpstan-phpunit": "^0.11.0", "phpstan/phpstan-symfony": "^0.11.0", - "phpstan/phpstan-doctrine": "^0.11.0" + "phpstan/phpstan-doctrine": "^0.11.0", + "php-http/mock-client": "^1.0", + "guzzlehttp/psr7": "^1.0" }, "suggest": { "ext-imagick": "To keep GIF animation when downloading image is enabled" diff --git a/src/Wallabag/CoreBundle/Helper/DownloadImages.php b/src/Wallabag/CoreBundle/Helper/DownloadImages.php index c1645e45a..e57490608 100644 --- a/src/Wallabag/CoreBundle/Helper/DownloadImages.php +++ b/src/Wallabag/CoreBundle/Helper/DownloadImages.php @@ -2,8 +2,13 @@ namespace Wallabag\CoreBundle\Helper; -use GuzzleHttp\Client; -use GuzzleHttp\Message\Response; +use Http\Client\Common\HttpMethodsClient; +use Http\Client\Common\Plugin\ErrorPlugin; +use Http\Client\Common\PluginClient; +use Http\Client\HttpClient; +use Http\Discovery\MessageFactoryDiscovery; +use Http\Message\MessageFactory; +use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\Finder\Finder; @@ -19,9 +24,9 @@ class DownloadImages private $mimeGuesser; private $wallabagUrl; - public function __construct(Client $client, $baseFolder, $wallabagUrl, LoggerInterface $logger) + public function __construct(HttpClient $client, $baseFolder, $wallabagUrl, LoggerInterface $logger, MessageFactory $messageFactory = null) { - $this->client = $client; + $this->client = new HttpMethodsClient(new PluginClient($client, [new ErrorPlugin()]), $messageFactory ?: MessageFactoryDiscovery::find()); $this->baseFolder = $baseFolder; $this->wallabagUrl = rtrim($wallabagUrl, '/'); $this->logger = $logger; @@ -135,7 +140,7 @@ class DownloadImages $localPath = $folderPath . '/' . $hashImage . '.' . $ext; try { - $im = imagecreatefromstring($res->getBody()); + $im = imagecreatefromstring((string) $res->getBody()); } catch (\Exception $e) { $im = false; } @@ -306,14 +311,14 @@ class DownloadImages /** * Retrieve and validate the extension from the response of the url of the image. * - * @param Response $res Guzzle Response + * @param ResponseInterface $res Http Response * @param string $imagePath Path from the src image from the content (used for log only) * * @return string|false Extension name or false if validation failed */ - private function getExtensionFromResponse(Response $res, $imagePath) + private function getExtensionFromResponse(ResponseInterface $res, $imagePath) { - $ext = $this->mimeGuesser->guess($res->getHeader('content-type')); + $ext = $this->mimeGuesser->guess(current($res->getHeader('content-type'))); $this->logger->debug('DownloadImages: Checking extension', ['ext' => $ext, 'header' => $res->getHeader('content-type')]); // ok header doesn't have the extension, try a different way diff --git a/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php b/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php index 4602a6841..4899d3d41 100644 --- a/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php +++ b/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php @@ -2,16 +2,18 @@ namespace Wallabag\CoreBundle\Helper; -use Graby\Ring\Client\SafeCurlHandler; -use GuzzleHttp\Client; +use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Event\SubscriberInterface; +use Http\Adapter\Guzzle5\Client as GuzzleAdapter; use Psr\Log\LoggerInterface; +use Http\Client\HttpClient; +use Http\HttplugBundle\ClientFactory\ClientFactory; /** - * Builds and configures the Guzzle HTTP client. + * Builds and configures the HTTP client. */ -class HttpClientFactory +class HttpClientFactory implements ClientFactory { /** @var [\GuzzleHttp\Event\SubscriberInterface] */ private $subscribers = []; @@ -36,29 +38,6 @@ class HttpClientFactory $this->logger = $logger; } - /** - * @return \GuzzleHttp\Client|null - */ - public function buildHttpClient() - { - $this->logger->log('debug', 'Restricted access config enabled?', ['enabled' => (int) $this->restrictedAccess]); - - if (0 === (int) $this->restrictedAccess) { - return; - } - - // we clear the cookie to avoid websites who use cookies for analytics - $this->cookieJar->clear(); - // need to set the (shared) cookie jar - $client = new Client(['handler' => new SafeCurlHandler(), 'defaults' => ['cookies' => $this->cookieJar]]); - - foreach ($this->subscribers as $subscriber) { - $client->getEmitter()->attach($subscriber); - } - - return $client; - } - /** * Adds a subscriber to the HTTP client. * @@ -68,4 +47,30 @@ class HttpClientFactory { $this->subscribers[] = $subscriber; } + + /** + * Input an array of configuration to be able to create a HttpClient. + * + * @param array $config + * + * @return HttpClient + */ + public function createClient(array $config = []) + { + $this->logger->log('debug', 'Restricted access config enabled?', ['enabled' => (int) $this->restrictedAccess]); + + if (0 === (int) $this->restrictedAccess) { + return new GuzzleAdapter(new GuzzleClient()); + } + + // we clear the cookie to avoid websites who use cookies for analytics + $this->cookieJar->clear(); + // need to set the (shared) cookie jar + $guzzle = new GuzzleClient(['defaults' => ['cookies' => $this->cookieJar]]); + foreach ($this->subscribers as $subscriber) { + $guzzle->getEmitter()->attach($subscriber); + } + + return new GuzzleAdapter($guzzle); + } } diff --git a/src/Wallabag/CoreBundle/Resources/config/services.yml b/src/Wallabag/CoreBundle/Resources/config/services.yml index 280d779da..319869513 100644 --- a/src/Wallabag/CoreBundle/Resources/config/services.yml +++ b/src/Wallabag/CoreBundle/Resources/config/services.yml @@ -42,7 +42,7 @@ services: - error_message: '%wallabag_core.fetching_error_message%' error_message_title: '%wallabag_core.fetching_error_message_title%' - - "@wallabag_core.guzzle.http_client" + - "@wallabag_core.http_client" - "@wallabag_core.graby.config_builder" calls: - [ setLogger, [ "@logger" ] ] @@ -55,9 +55,8 @@ services: - {} - "@logger" - wallabag_core.guzzle.http_client: - class: GuzzleHttp\ClientInterface - factory: ["@wallabag_core.guzzle.http_client_factory", buildHttpClient] + wallabag_core.http_client: + alias: 'httplug.client.wallabag_core' wallabag_core.guzzle_authenticator.config_builder: class: Wallabag\CoreBundle\GuzzleSiteAuthenticator\GrabySiteConfigBuilder @@ -73,7 +72,7 @@ services: bd_guzzle_site_authenticator.site_config_builder: alias: wallabag_core.guzzle_authenticator.config_builder - wallabag_core.guzzle.http_client_factory: + wallabag_core.http_client_factory: class: Wallabag\CoreBundle\Helper\HttpClientFactory arguments: - "@wallabag_core.guzzle.cookie_jar" @@ -212,7 +211,7 @@ services: - "@logger" wallabag_core.entry.download_images.client: - class: GuzzleHttp\Client + alias: 'httplug.client.wallabag_core.entry.download_images' wallabag_core.helper.crypto_proxy: class: Wallabag\CoreBundle\Helper\CryptoProxy diff --git a/src/Wallabag/ImportBundle/Import/PocketImport.php b/src/Wallabag/ImportBundle/Import/PocketImport.php index a39d81568..9467fae24 100644 --- a/src/Wallabag/ImportBundle/Import/PocketImport.php +++ b/src/Wallabag/ImportBundle/Import/PocketImport.php @@ -2,13 +2,22 @@ namespace Wallabag\ImportBundle\Import; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\RequestException; +use Http\Client\Common\HttpMethodsClient; +use Http\Client\Common\Plugin\ErrorPlugin; +use Http\Client\Common\PluginClient; +use Http\Client\HttpClient; +use Http\Discovery\MessageFactoryDiscovery; +use Http\Message\MessageFactory; +use Http\Client\Exception\RequestException; use Wallabag\CoreBundle\Entity\Entry; +use Psr\Http\Message\ResponseInterface; class PocketImport extends AbstractImport { const NB_ELEMENTS = 5000; + /** + * @var HttpMethodsClient + */ private $client; private $accessToken; @@ -55,24 +64,18 @@ class PocketImport extends AbstractImport */ public function getRequestToken($redirectUri) { - $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/oauth/request', - [ - 'body' => json_encode([ - 'consumer_key' => $this->user->getConfig()->getPocketConsumerKey(), - 'redirect_uri' => $redirectUri, - ]), - ] - ); - try { - $response = $this->client->send($request); + $response = $this->client->post('https://getpocket.com/v3/oauth/request', [], json_encode([ + 'consumer_key' => $this->user->getConfig()->getPocketConsumerKey(), + 'redirect_uri' => $redirectUri, + ])); } catch (RequestException $e) { $this->logger->error(sprintf('PocketImport: Failed to request token: %s', $e->getMessage()), ['exception' => $e]); return false; } - return $response->json()['code']; + return $this->jsonDecode($response)['code']; } /** @@ -85,24 +88,19 @@ class PocketImport extends AbstractImport */ public function authorize($code) { - $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/oauth/authorize', - [ - 'body' => json_encode([ - 'consumer_key' => $this->user->getConfig()->getPocketConsumerKey(), - 'code' => $code, - ]), - ] - ); try { - $response = $this->client->send($request); + $response = $this->client->post('https://getpocket.com/v3/oauth/authorize', [], json_encode([ + 'consumer_key' => $this->user->getConfig()->getPocketConsumerKey(), + 'code' => $code, + ])); } catch (RequestException $e) { $this->logger->error(sprintf('PocketImport: Failed to authorize client: %s', $e->getMessage()), ['exception' => $e]); return false; } - $this->accessToken = $response->json()['access_token']; + $this->accessToken = $this->jsonDecode($response)['access_token']; return true; } @@ -114,29 +112,23 @@ class PocketImport extends AbstractImport { static $run = 0; - $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/get', - [ - 'body' => json_encode([ - 'consumer_key' => $this->user->getConfig()->getPocketConsumerKey(), - 'access_token' => $this->accessToken, - 'detailType' => 'complete', - 'state' => 'all', - 'sort' => 'newest', - 'count' => self::NB_ELEMENTS, - 'offset' => $offset, - ]), - ] - ); - try { - $response = $this->client->send($request); + $response = $this->client->post('https://getpocket.com/v3/get', [], json_encode([ + 'consumer_key' => $this->user->getConfig()->getPocketConsumerKey(), + 'access_token' => $this->accessToken, + 'detailType' => 'complete', + 'state' => 'all', + 'sort' => 'newest', + 'count' => self::NB_ELEMENTS, + 'offset' => $offset, + ])); } catch (RequestException $e) { $this->logger->error(sprintf('PocketImport: Failed to import: %s', $e->getMessage()), ['exception' => $e]); return false; } - $entries = $response->json(); + $entries = $this->jsonDecode($response); if ($this->producer) { $this->parseEntriesForProducer($entries['list']); @@ -159,13 +151,14 @@ class PocketImport extends AbstractImport } /** - * Set the Guzzle client. + * Set the Http client. * - * @param Client $client + * @param HttpClient $client + * @param MessageFactory|null $messageFactory */ - public function setClient(Client $client) + public function setClient(HttpClient $client, MessageFactory $messageFactory = null) { - $this->client = $client; + $this->client = new HttpMethodsClient(new PluginClient($client, [new ErrorPlugin()]), $messageFactory ?: MessageFactoryDiscovery::find()); } /** @@ -252,4 +245,15 @@ class PocketImport extends AbstractImport return $importedEntry; } + + protected function jsonDecode(ResponseInterface $response) + { + $data = \json_decode((string) $response->getBody(), true); + + if (JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException('Unable to parse JSON data: ' . json_last_error_msg()); + } + + return $data; + } } diff --git a/src/Wallabag/ImportBundle/Resources/config/services.yml b/src/Wallabag/ImportBundle/Resources/config/services.yml index 2dd7dff8c..973c0d03e 100644 --- a/src/Wallabag/ImportBundle/Resources/config/services.yml +++ b/src/Wallabag/ImportBundle/Resources/config/services.yml @@ -7,13 +7,7 @@ services: class: Wallabag\ImportBundle\Import\ImportChain wallabag_import.pocket.client: - class: GuzzleHttp\Client - arguments: - - - defaults: - headers: - content-type: "application/json" - X-Accept: "application/json" + alias: 'httplug.client.wallabag_import.pocket.client' wallabag_import.pocket.import: class: Wallabag\ImportBundle\Import\PocketImport diff --git a/tests/Wallabag/CoreBundle/Helper/DownloadImagesTest.php b/tests/Wallabag/CoreBundle/Helper/DownloadImagesTest.php index cda5f8431..537583647 100644 --- a/tests/Wallabag/CoreBundle/Helper/DownloadImagesTest.php +++ b/tests/Wallabag/CoreBundle/Helper/DownloadImagesTest.php @@ -3,9 +3,10 @@ namespace Tests\Wallabag\CoreBundle\Helper; use GuzzleHttp\Client; -use GuzzleHttp\Message\Response; use GuzzleHttp\Stream\Stream; use GuzzleHttp\Subscriber\Mock; +use Http\Mock\Client as HttpMockClient; +use GuzzleHttp\Psr7\Response; use Monolog\Handler\TestHandler; use Monolog\Logger; use PHPUnit\Framework\TestCase; @@ -32,18 +33,14 @@ class DownloadImagesTest extends TestCase */ public function testProcessHtml($html, $url) { - $client = new Client(); + $httpMockClient = new HttpMockClient(); - $mock = new Mock([ - new Response(200, ['content-type' => 'image/png'], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/unnamed.png'))), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient->addResponse(new Response(200, ['content-type' => 'image/png'], file_get_contents(__DIR__ . '/../fixtures/unnamed.png'))); $logHandler = new TestHandler(); $logger = new Logger('test', [$logHandler]); - $download = new DownloadImages($client, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); + $download = new DownloadImages($httpMockClient, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); $res = $download->processHtml(123, $html, $url); @@ -53,18 +50,13 @@ class DownloadImagesTest extends TestCase public function testProcessHtmlWithBadImage() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['content-type' => 'application/json'], Stream::factory('')), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['content-type' => 'application/json'], '')); $logHandler = new TestHandler(); $logger = new Logger('test', [$logHandler]); - $download = new DownloadImages($client, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); + $download = new DownloadImages($httpMockClient, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); $res = $download->processHtml(123, '
    ', 'http://imgur.com/gallery/WxtWY'); $this->assertContains('http://i.imgur.com/T9qgcHc.jpg', $res, 'Image were not replace because of content-type'); @@ -85,18 +77,13 @@ class DownloadImagesTest extends TestCase */ public function testProcessSingleImage($header, $extension) { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['content-type' => $header], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/unnamed.png'))), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['content-type' => $header], file_get_contents(__DIR__ . '/../fixtures/unnamed.png'))); $logHandler = new TestHandler(); $logger = new Logger('test', [$logHandler]); - $download = new DownloadImages($client, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); + $download = new DownloadImages($httpMockClient, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); $res = $download->processSingleImage(123, 'T9qgcHc.jpg', 'http://imgur.com/gallery/WxtWY'); $this->assertContains('/assets/images/9/b/9b0ead26/ebe60399.' . $extension, $res); @@ -104,18 +91,13 @@ class DownloadImagesTest extends TestCase public function testProcessSingleImageWithBadUrl() { - $client = new Client(); - - $mock = new Mock([ - new Response(404, []), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(404, [])); $logHandler = new TestHandler(); $logger = new Logger('test', [$logHandler]); - $download = new DownloadImages($client, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); + $download = new DownloadImages($httpMockClient, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); $res = $download->processSingleImage(123, 'T9qgcHc.jpg', 'http://imgur.com/gallery/WxtWY'); $this->assertFalse($res, 'Image can not be found, so it will not be replaced'); @@ -123,18 +105,13 @@ class DownloadImagesTest extends TestCase public function testProcessSingleImageWithBadImage() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['content-type' => 'image/png'], Stream::factory('')), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['content-type' => 'image/png'], '')); $logHandler = new TestHandler(); $logger = new Logger('test', [$logHandler]); - $download = new DownloadImages($client, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); + $download = new DownloadImages($httpMockClient, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); $res = $download->processSingleImage(123, 'http://i.imgur.com/T9qgcHc.jpg', 'http://imgur.com/gallery/WxtWY'); $this->assertFalse($res, 'Image can not be loaded, so it will not be replaced'); @@ -142,18 +119,13 @@ class DownloadImagesTest extends TestCase public function testProcessSingleImageFailAbsolute() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['content-type' => 'image/png'], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/unnamed.png'))), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['content-type' => 'image/png'], file_get_contents(__DIR__ . '/../fixtures/unnamed.png'))); $logHandler = new TestHandler(); $logger = new Logger('test', [$logHandler]); - $download = new DownloadImages($client, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); + $download = new DownloadImages($httpMockClient, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); $res = $download->processSingleImage(123, '/i.imgur.com/T9qgcHc.jpg', 'imgur.com/gallery/WxtWY'); $this->assertFalse($res, 'Absolute image can not be determined, so it will not be replaced'); @@ -161,18 +133,13 @@ class DownloadImagesTest extends TestCase public function testProcessRealImage() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['content-type' => null], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['content-type' => null], file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))); $logHandler = new TestHandler(); $logger = new Logger('test', [$logHandler]); - $download = new DownloadImages($client, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); + $download = new DownloadImages($httpMockClient, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); $res = $download->processSingleImage( 123, diff --git a/tests/Wallabag/ImportBundle/Import/PocketImportTest.php b/tests/Wallabag/ImportBundle/Import/PocketImportTest.php index 8083f1a88..ec76f1681 100644 --- a/tests/Wallabag/ImportBundle/Import/PocketImportTest.php +++ b/tests/Wallabag/ImportBundle/Import/PocketImportTest.php @@ -2,10 +2,8 @@ namespace Tests\Wallabag\ImportBundle\Import; -use GuzzleHttp\Client; -use GuzzleHttp\Message\Response; -use GuzzleHttp\Stream\Stream; -use GuzzleHttp\Subscriber\Mock; +use Http\Mock\Client as HttpMockClient; +use GuzzleHttp\Psr7\Response; use M6Web\Component\RedisMock\RedisMockFactory; use Monolog\Handler\TestHandler; use Monolog\Logger; @@ -38,16 +36,11 @@ class PocketImportTest extends TestCase public function testOAuthRequest() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['code' => 'wunderbar_code']))), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], json_encode(['code' => 'wunderbar_code']))); $pocketImport = $this->getPocketImport(); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $code = $pocketImport->getRequestToken('http://0.0.0.0/redirect'); @@ -56,16 +49,11 @@ class PocketImportTest extends TestCase public function testOAuthRequestBadResponse() { - $client = new Client(); - - $mock = new Mock([ - new Response(403), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(403)); $pocketImport = $this->getPocketImport(); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $code = $pocketImport->getRequestToken('http://0.0.0.0/redirect'); @@ -78,16 +66,11 @@ class PocketImportTest extends TestCase public function testOAuthAuthorize() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], json_encode(['access_token' => 'wunderbar_token']))); $pocketImport = $this->getPocketImport(); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $res = $pocketImport->authorize('wunderbar_code'); @@ -97,16 +80,11 @@ class PocketImportTest extends TestCase public function testOAuthAuthorizeBadResponse() { - $client = new Client(); - - $mock = new Mock([ - new Response(403), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(403)); $pocketImport = $this->getPocketImport(); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $res = $pocketImport->authorize('wunderbar_code'); @@ -122,94 +100,90 @@ class PocketImportTest extends TestCase */ public function testImport() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))), - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(' - { - "status": 1, - "list": { - "229279689": { - "item_id": "229279689", - "resolved_id": "229279689", - "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", - "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland", - "favorite": "1", - "status": "1", - "time_added": "1473020899", - "time_updated": "1473020899", - "time_read": "0", - "time_favorited": "0", - "sort_id": 0, - "resolved_title": "The Massive Ryder Cup Preview", - "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", - "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.", - "is_article": "1", - "is_index": "0", - "has_video": "1", - "has_image": "1", - "word_count": "3197", - "images": { - "1": { - "item_id": "229279689", - "image_id": "1", - "src": "http://a.espncdn.com/combiner/i?img=/photo/2012/0927/grant_g_ryder_cr_640.jpg&w=640&h=360", - "width": "0", - "height": "0", - "credit": "Jamie Squire/Getty Images", - "caption": "" - } - }, - "videos": { - "1": { - "item_id": "229279689", - "video_id": "1", - "src": "http://www.youtube.com/v/Er34PbFkVGk?version=3&hl=en_US&rel=0", - "width": "420", - "height": "315", - "type": "1", - "vid": "Er34PbFkVGk" - } - }, - "tags": { - "grantland": { - "item_id": "1147652870", - "tag": "grantland" - }, - "Ryder Cup": { - "item_id": "1147652870", - "tag": "Ryder Cup" - } + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], json_encode(['access_token' => 'wunderbar_token']))); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], <<<'JSON' + { + "status": 1, + "list": { + "229279689": { + "item_id": "229279689", + "resolved_id": "229279689", + "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", + "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland", + "favorite": "1", + "status": "1", + "time_added": "1473020899", + "time_updated": "1473020899", + "time_read": "0", + "time_favorited": "0", + "sort_id": 0, + "resolved_title": "The Massive Ryder Cup Preview", + "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", + "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.", + "is_article": "1", + "is_index": "0", + "has_video": "1", + "has_image": "1", + "word_count": "3197", + "images": { + "1": { + "item_id": "229279689", + "image_id": "1", + "src": "http://a.espncdn.com/combiner/i?img=/photo/2012/0927/grant_g_ryder_cr_640.jpg&w=640&h=360", + "width": "0", + "height": "0", + "credit": "Jamie Squire/Getty Images", + "caption": "" } }, - "229279690": { - "item_id": "229279689", - "resolved_id": "229279689", - "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", - "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland", - "favorite": "1", - "status": "1", - "time_added": "1473020899", - "time_updated": "1473020899", - "time_read": "0", - "time_favorited": "0", - "sort_id": 1, - "resolved_title": "The Massive Ryder Cup Preview", - "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", - "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.", - "is_article": "1", - "is_index": "0", - "has_video": "0", - "has_image": "0", - "word_count": "3197" + "videos": { + "1": { + "item_id": "229279689", + "video_id": "1", + "src": "http://www.youtube.com/v/Er34PbFkVGk?version=3&hl=en_US&rel=0", + "width": "420", + "height": "315", + "type": "1", + "vid": "Er34PbFkVGk" + } + }, + "tags": { + "grantland": { + "item_id": "1147652870", + "tag": "grantland" + }, + "Ryder Cup": { + "item_id": "1147652870", + "tag": "Ryder Cup" + } } + }, + "229279690": { + "item_id": "229279689", + "resolved_id": "229279689", + "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", + "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland", + "favorite": "1", + "status": "1", + "time_added": "1473020899", + "time_updated": "1473020899", + "time_read": "0", + "time_favorited": "0", + "sort_id": 1, + "resolved_title": "The Massive Ryder Cup Preview", + "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", + "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.", + "is_article": "1", + "is_index": "0", + "has_video": "0", + "has_image": "0", + "word_count": "3197" } } - ')), - ]); - - $client->getEmitter()->attach($mock); + } +JSON +)); $pocketImport = $this->getPocketImport('ConsumerKey', 1); @@ -240,7 +214,7 @@ class PocketImportTest extends TestCase ->method('updateEntry') ->willReturn($entry); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $pocketImport->authorize('wunderbar_code'); $res = $pocketImport->import(); @@ -254,56 +228,52 @@ class PocketImportTest extends TestCase */ public function testImportAndMarkAllAsRead() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))), - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(' - { - "status": 1, - "list": { - "229279689": { - "item_id": "229279689", - "resolved_id": "229279689", - "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", - "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland", - "favorite": "1", - "status": "1", - "time_added": "1473020899", - "time_updated": "1473020899", - "time_read": "0", - "time_favorited": "0", - "sort_id": 0, - "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.", - "is_article": "1", - "has_video": "1", - "has_image": "1", - "word_count": "3197" - }, - "229279690": { - "item_id": "229279689", - "resolved_id": "229279689", - "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview/2", - "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland", - "favorite": "1", - "status": "0", - "time_added": "1473020899", - "time_updated": "1473020899", - "time_read": "0", - "time_favorited": "0", - "sort_id": 1, - "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.", - "is_article": "1", - "has_video": "0", - "has_image": "0", - "word_count": "3197" - } + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], json_encode(['access_token' => 'wunderbar_token']))); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], <<<'JSON' + { + "status": 1, + "list": { + "229279689": { + "item_id": "229279689", + "resolved_id": "229279689", + "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", + "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland", + "favorite": "1", + "status": "1", + "time_added": "1473020899", + "time_updated": "1473020899", + "time_read": "0", + "time_favorited": "0", + "sort_id": 0, + "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.", + "is_article": "1", + "has_video": "1", + "has_image": "1", + "word_count": "3197" + }, + "229279690": { + "item_id": "229279689", + "resolved_id": "229279689", + "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview/2", + "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland", + "favorite": "1", + "status": "0", + "time_added": "1473020899", + "time_updated": "1473020899", + "time_read": "0", + "time_favorited": "0", + "sort_id": 1, + "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.", + "is_article": "1", + "has_video": "0", + "has_image": "0", + "word_count": "3197" } } - ')), - ]); - - $client->getEmitter()->attach($mock); + } +JSON +)); $pocketImport = $this->getPocketImport('ConsumerKey', 2); @@ -335,7 +305,7 @@ class PocketImportTest extends TestCase ->method('updateEntry') ->willReturn($entry); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $pocketImport->authorize('wunderbar_code'); $res = $pocketImport->setMarkAsRead(true)->import(); @@ -349,7 +319,7 @@ class PocketImportTest extends TestCase */ public function testImportWithRabbit() { - $client = new Client(); + $httpMockClient = new HttpMockClient(); $body = <<<'JSON' { @@ -374,19 +344,16 @@ class PocketImportTest extends TestCase } JSON; - $mock = new Mock([ - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))), - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(' - { - "status": 1, - "list": { - "229279690": ' . $body . ' - } + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], json_encode(['access_token' => 'wunderbar_token']))); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], <<getEmitter()->attach($mock); + } +JSON + )); $pocketImport = $this->getPocketImport(); @@ -420,7 +387,7 @@ JSON; ->method('publish') ->with(json_encode($bodyAsArray)); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $pocketImport->setProducer($producer); $pocketImport->authorize('wunderbar_code'); @@ -435,7 +402,7 @@ JSON; */ public function testImportWithRedis() { - $client = new Client(); + $httpMockClient = new HttpMockClient(); $body = <<<'JSON' { @@ -460,19 +427,16 @@ JSON; } JSON; - $mock = new Mock([ - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))), - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(' - { - "status": 1, - "list": { - "229279690": ' . $body . ' - } + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], json_encode(['access_token' => 'wunderbar_token']))); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], <<getEmitter()->attach($mock); + } +JSON + )); $pocketImport = $this->getPocketImport(); @@ -499,7 +463,7 @@ JSON; $queue = new RedisQueue($redisMock, 'pocket'); $producer = new Producer($queue); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $pocketImport->setProducer($producer); $pocketImport->authorize('wunderbar_code'); @@ -513,17 +477,13 @@ JSON; public function testImportBadResponse() { - $client = new Client(); + $httpMockClient = new HttpMockClient(); - $mock = new Mock([ - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))), - new Response(403), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], json_encode(['access_token' => 'wunderbar_token']))); + $httpMockClient->addResponse(new Response(403)); $pocketImport = $this->getPocketImport(); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $pocketImport->authorize('wunderbar_code'); $res = $pocketImport->import(); @@ -537,25 +497,23 @@ JSON; public function testImportWithExceptionFromGraby() { - $client = new Client(); + $httpMockClient = new HttpMockClient(); - $mock = new Mock([ - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))), - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(' - { - "status": 1, - "list": { - "229279689": { - "status": "1", - "favorite": "1", - "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview" - } + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], json_encode(['access_token' => 'wunderbar_token']))); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], <<<'JSON' + { + "status": 1, + "list": { + "229279689": { + "status": "1", + "favorite": "1", + "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview" } } - ')), - ]); - - $client->getEmitter()->attach($mock); + } + +JSON + )); $pocketImport = $this->getPocketImport('ConsumerKey', 1); @@ -579,7 +537,7 @@ JSON; ->method('updateEntry') ->will($this->throwException(new \Exception())); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $pocketImport->authorize('wunderbar_code'); $res = $pocketImport->import(); From 5f08426201c336f96d593954fb45b284d7e60f4a Mon Sep 17 00:00:00 2001 From: adev Date: Sat, 11 Nov 2017 20:04:15 +0100 Subject: [PATCH 101/107] Fix because of some breaking changes of Graby 2.0 --- .../Controller/EntryRestController.php | 4 +- .../CoreBundle/Helper/ContentProxy.php | 20 +++--- .../ImportBundle/Import/WallabagV2Import.php | 4 +- .../CoreBundle/Helper/ContentProxyTest.php | 70 ++++++++++--------- 4 files changed, 50 insertions(+), 48 deletions(-) diff --git a/src/Wallabag/ApiBundle/Controller/EntryRestController.php b/src/Wallabag/ApiBundle/Controller/EntryRestController.php index aff0534a0..d9d99c85b 100644 --- a/src/Wallabag/ApiBundle/Controller/EntryRestController.php +++ b/src/Wallabag/ApiBundle/Controller/EntryRestController.php @@ -369,9 +369,7 @@ class EntryRestController extends WallabagRestController 'language' => !empty($data['language']) ? $data['language'] : $entry->getLanguage(), 'date' => !empty($data['publishedAt']) ? $data['publishedAt'] : $entry->getPublishedAt(), // faking the open graph preview picture - 'open_graph' => [ - 'og_image' => !empty($data['picture']) ? $data['picture'] : $entry->getPreviewPicture(), - ], + 'image' => !empty($data['picture']) ? $data['picture'] : $entry->getPreviewPicture(), 'authors' => \is_string($data['authors']) ? explode(',', $data['authors']) : $entry->getPublishedBy(), ] ); diff --git a/src/Wallabag/CoreBundle/Helper/ContentProxy.php b/src/Wallabag/CoreBundle/Helper/ContentProxy.php index ca01dec8d..ac27e50a9 100644 --- a/src/Wallabag/CoreBundle/Helper/ContentProxy.php +++ b/src/Wallabag/CoreBundle/Helper/ContentProxy.php @@ -253,16 +253,14 @@ class ContentProxy if (!empty($content['title'])) { $entry->setTitle($content['title']); - } elseif (!empty($content['open_graph']['og_title'])) { - $entry->setTitle($content['open_graph']['og_title']); } if (empty($content['html'])) { $content['html'] = $this->fetchingErrorMessage; - if (!empty($content['open_graph']['og_description'])) { + if (!empty($content['description'])) { $content['html'] .= '

    But we found a short description:

    '; - $content['html'] .= $content['open_graph']['og_description']; + $content['html'] .= $content['description']; } } @@ -277,8 +275,8 @@ class ContentProxy $entry->setPublishedBy($content['authors']); } - if (!empty($content['all_headers']) && $this->storeArticleHeaders) { - $entry->setHeaders($content['all_headers']); + if (!empty($content['headers'])) { + $entry->setHeaders($content['headers']); } if (!empty($content['date'])) { @@ -290,12 +288,12 @@ class ContentProxy } $previewPictureUrl = ''; - if (!empty($content['open_graph']['og_image'])) { - $previewPictureUrl = $content['open_graph']['og_image']; + if (!empty($content['image'])) { + $previewPictureUrl = $content['image']; } // if content is an image, define it as a preview too - if (!empty($content['content_type']) && \in_array($this->mimeGuesser->guess($content['content_type']), ['jpeg', 'jpg', 'gif', 'png'], true)) { + if (!empty($content['headers']['content_type']) && \in_array($this->mimeGuesser->guess($content['headers']['content_type']), ['jpeg', 'jpg', 'gif', 'png'], true)) { $previewPictureUrl = $content['url']; } elseif (empty($previewPictureUrl)) { $this->logger->debug('Extracting images from content to provide a default preview picture'); @@ -310,8 +308,8 @@ class ContentProxy $this->updatePreviewPicture($entry, $previewPictureUrl); } - if (!empty($content['content_type'])) { - $entry->setMimetype($content['content_type']); + if (!empty($content['headers']['content-type'])) { + $entry->setMimetype($content['headers']['content-type']); } try { diff --git a/src/Wallabag/ImportBundle/Import/WallabagV2Import.php b/src/Wallabag/ImportBundle/Import/WallabagV2Import.php index 3e085ecff..2ba26003f 100644 --- a/src/Wallabag/ImportBundle/Import/WallabagV2Import.php +++ b/src/Wallabag/ImportBundle/Import/WallabagV2Import.php @@ -35,7 +35,9 @@ class WallabagV2Import extends WallabagImport { return [ 'html' => $entry['content'], - 'content_type' => $entry['mimetype'], + 'headers' => [ + 'content-type' => $entry['mimetype'], + ], 'is_archived' => (bool) ($entry['is_archived'] || $this->markAsRead), 'is_starred' => (bool) $entry['is_starred'], ] + $entry; diff --git a/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php b/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php index c7caac1d3..40a6cc802 100644 --- a/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php +++ b/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php @@ -104,15 +104,12 @@ class ContentProxyTest extends TestCase ->method('fetchContent') ->willReturn([ 'html' => false, - 'title' => '', + 'title' => 'my title', 'url' => '', 'content_type' => '', 'language' => '', 'status' => '', - 'open_graph' => [ - 'og_title' => 'my title', - 'og_description' => 'desc', - ], + 'description' => 'desc', ]); $proxy = new ContentProxy($graby, $tagger, $this->getValidator(), $this->getLogger(), $this->fetchingErrorMessage); @@ -147,13 +144,12 @@ class ContentProxyTest extends TestCase 'html' => str_repeat('this is my content', 325), 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', 'language' => 'fr', 'status' => '200', - 'open_graph' => [ - 'og_title' => 'my OG title', - 'og_description' => 'OG desc', - 'og_image' => 'http://3.3.3.3/cover.jpg', + 'description' => 'OG desc', + 'image' => 'http://3.3.3.3/cover.jpg', + 'headers' => [ + 'content-type' => 'text/html', ], ]); @@ -189,13 +185,12 @@ class ContentProxyTest extends TestCase 'html' => str_repeat('this is my content', 325), 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', 'language' => 'fr', 'status' => '200', - 'open_graph' => [ - 'og_title' => 'my OG title', - 'og_description' => 'OG desc', - 'og_image' => null, + 'description' => 'OG desc', + 'image' => null, + 'headers' => [ + 'content-type' => 'text/html', ], ]); @@ -320,9 +315,11 @@ class ContentProxyTest extends TestCase 'html' => str_repeat('this is my content', 325), 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', 'language' => 'dontexist', 'status' => '200', + 'headers' => [ + 'content-type' => 'text/html', + ], ]); $proxy = new ContentProxy($graby, $tagger, $validator, $this->getLogger(), $this->fetchingErrorMessage); @@ -367,10 +364,10 @@ class ContentProxyTest extends TestCase 'content_type' => 'text/html', 'language' => 'fr', 'status' => '200', - 'open_graph' => [ - 'og_title' => 'my OG title', - 'og_description' => 'OG desc', - 'og_image' => 'https://', + 'description' => 'OG desc', + 'image' => 'https://', + 'headers' => [ + 'content-type' => 'text/html', ], ]); @@ -404,12 +401,12 @@ class ContentProxyTest extends TestCase 'html' => str_repeat('this is my content', 325), 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', 'language' => 'fr', 'date' => '1395635872', 'authors' => ['Jeremy', 'Nico', 'Thomas'], - 'all_headers' => [ - 'Cache-Control' => 'no-cache', + 'headers' => [ + 'cache-control' => 'no-cache', + 'content-type' => 'text/html', ], ] ); @@ -447,9 +444,11 @@ class ContentProxyTest extends TestCase 'html' => str_repeat('this is my content', 325), 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', 'language' => 'fr', 'date' => '2016-09-08T11:55:58+0200', + 'headers' => [ + 'content-type' => 'text/html', + ], ] ); @@ -482,9 +481,11 @@ class ContentProxyTest extends TestCase 'html' => str_repeat('this is my content', 325), 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', 'language' => 'fr', 'date' => '01 02 2012', + 'headers' => [ + 'content-type' => 'text/html', + ], ] ); @@ -519,8 +520,10 @@ class ContentProxyTest extends TestCase 'html' => str_repeat('this is my content', 325), 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', 'language' => 'fr', + 'headers' => [ + 'content-type' => 'text/html', + ], ] ); @@ -559,13 +562,13 @@ class ContentProxyTest extends TestCase 'html' => $html, 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', 'language' => 'fr', 'status' => '200', - 'open_graph' => [ - 'og_title' => 'my OG title', - 'og_description' => 'OG desc', - 'og_image' => 'http://3.3.3.3/cover.jpg', + //'og_title' => 'my OG title', + 'description' => 'OG desc', + 'image' => 'http://3.3.3.3/cover.jpg', + 'headers' => [ + 'content-type' => 'text/html', ], ] ); @@ -597,9 +600,10 @@ class ContentProxyTest extends TestCase 'html' => '

    ', 'title' => 'this is my title', 'url' => 'http://1.1.1.1/image.jpg', - 'content_type' => 'image/jpeg', 'status' => '200', - 'open_graph' => [], + 'headers' => [ + 'content-type' => 'image/jpeg', + ], ]); $proxy = new ContentProxy($graby, $tagger, $this->getValidator(), $this->getLogger(), $this->fetchingErrorMessage); From 1048c9c4a811821b00cc04bfec905bebcc22bac4 Mon Sep 17 00:00:00 2001 From: adev Date: Sun, 12 Nov 2017 12:15:02 +0100 Subject: [PATCH 102/107] Configure timeout --- app/config/config.yml | 3 +++ src/Wallabag/CoreBundle/Helper/HttpClientFactory.php | 10 +++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/config/config.yml b/app/config/config.yml index 309945c50..bbcc682f9 100644 --- a/app/config/config.yml +++ b/app/config/config.yml @@ -375,6 +375,9 @@ httplug: clients: wallabag_core: factory: 'wallabag_core.http_client_factory' + config: + defaults: + timeout: 10 plugins: ['httplug.plugin.logger'] wallabag_core.entry.download_images: factory: 'httplug.factory.auto' diff --git a/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php b/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php index 4899d3d41..3e19a7be1 100644 --- a/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php +++ b/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php @@ -60,13 +60,17 @@ class HttpClientFactory implements ClientFactory $this->logger->log('debug', 'Restricted access config enabled?', ['enabled' => (int) $this->restrictedAccess]); if (0 === (int) $this->restrictedAccess) { - return new GuzzleAdapter(new GuzzleClient()); + return new GuzzleAdapter(new GuzzleClient($config)); } // we clear the cookie to avoid websites who use cookies for analytics $this->cookieJar->clear(); - // need to set the (shared) cookie jar - $guzzle = new GuzzleClient(['defaults' => ['cookies' => $this->cookieJar]]); + if (!isset($config['defaults']['cookies'])) { + // need to set the (shared) cookie jar + $config['defaults']['cookies'] = $this->cookieJar; + } + + $guzzle = new GuzzleClient($config); foreach ($this->subscribers as $subscriber) { $guzzle->getEmitter()->attach($subscriber); } From 448d99f84e93697ce49ec31224addb1da1a37a9f Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Mon, 28 Jan 2019 06:10:26 +0100 Subject: [PATCH 103/107] CS --- src/Wallabag/CoreBundle/Helper/DownloadImages.php | 2 +- src/Wallabag/CoreBundle/Helper/HttpClientFactory.php | 2 +- src/Wallabag/ImportBundle/Import/PocketImport.php | 7 +++---- tests/Wallabag/CoreBundle/Helper/DownloadImagesTest.php | 2 +- tests/Wallabag/ImportBundle/Import/PocketImportTest.php | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Wallabag/CoreBundle/Helper/DownloadImages.php b/src/Wallabag/CoreBundle/Helper/DownloadImages.php index e57490608..7a39a2e4a 100644 --- a/src/Wallabag/CoreBundle/Helper/DownloadImages.php +++ b/src/Wallabag/CoreBundle/Helper/DownloadImages.php @@ -312,7 +312,7 @@ class DownloadImages * Retrieve and validate the extension from the response of the url of the image. * * @param ResponseInterface $res Http Response - * @param string $imagePath Path from the src image from the content (used for log only) + * @param string $imagePath Path from the src image from the content (used for log only) * * @return string|false Extension name or false if validation failed */ diff --git a/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php b/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php index 3e19a7be1..b8e95381d 100644 --- a/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php +++ b/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php @@ -6,9 +6,9 @@ use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Event\SubscriberInterface; use Http\Adapter\Guzzle5\Client as GuzzleAdapter; -use Psr\Log\LoggerInterface; use Http\Client\HttpClient; use Http\HttplugBundle\ClientFactory\ClientFactory; +use Psr\Log\LoggerInterface; /** * Builds and configures the HTTP client. diff --git a/src/Wallabag/ImportBundle/Import/PocketImport.php b/src/Wallabag/ImportBundle/Import/PocketImport.php index 9467fae24..b35a561bb 100644 --- a/src/Wallabag/ImportBundle/Import/PocketImport.php +++ b/src/Wallabag/ImportBundle/Import/PocketImport.php @@ -5,12 +5,12 @@ namespace Wallabag\ImportBundle\Import; use Http\Client\Common\HttpMethodsClient; use Http\Client\Common\Plugin\ErrorPlugin; use Http\Client\Common\PluginClient; +use Http\Client\Exception\RequestException; use Http\Client\HttpClient; use Http\Discovery\MessageFactoryDiscovery; use Http\Message\MessageFactory; -use Http\Client\Exception\RequestException; -use Wallabag\CoreBundle\Entity\Entry; use Psr\Http\Message\ResponseInterface; +use Wallabag\CoreBundle\Entity\Entry; class PocketImport extends AbstractImport { @@ -88,7 +88,6 @@ class PocketImport extends AbstractImport */ public function authorize($code) { - try { $response = $this->client->post('https://getpocket.com/v3/oauth/authorize', [], json_encode([ 'consumer_key' => $this->user->getConfig()->getPocketConsumerKey(), @@ -153,7 +152,7 @@ class PocketImport extends AbstractImport /** * Set the Http client. * - * @param HttpClient $client + * @param HttpClient $client * @param MessageFactory|null $messageFactory */ public function setClient(HttpClient $client, MessageFactory $messageFactory = null) diff --git a/tests/Wallabag/CoreBundle/Helper/DownloadImagesTest.php b/tests/Wallabag/CoreBundle/Helper/DownloadImagesTest.php index 537583647..0199b3e44 100644 --- a/tests/Wallabag/CoreBundle/Helper/DownloadImagesTest.php +++ b/tests/Wallabag/CoreBundle/Helper/DownloadImagesTest.php @@ -3,10 +3,10 @@ namespace Tests\Wallabag\CoreBundle\Helper; use GuzzleHttp\Client; +use GuzzleHttp\Psr7\Response; use GuzzleHttp\Stream\Stream; use GuzzleHttp\Subscriber\Mock; use Http\Mock\Client as HttpMockClient; -use GuzzleHttp\Psr7\Response; use Monolog\Handler\TestHandler; use Monolog\Logger; use PHPUnit\Framework\TestCase; diff --git a/tests/Wallabag/ImportBundle/Import/PocketImportTest.php b/tests/Wallabag/ImportBundle/Import/PocketImportTest.php index ec76f1681..40e1626ba 100644 --- a/tests/Wallabag/ImportBundle/Import/PocketImportTest.php +++ b/tests/Wallabag/ImportBundle/Import/PocketImportTest.php @@ -2,8 +2,8 @@ namespace Tests\Wallabag\ImportBundle\Import; -use Http\Mock\Client as HttpMockClient; use GuzzleHttp\Psr7\Response; +use Http\Mock\Client as HttpMockClient; use M6Web\Component\RedisMock\RedisMockFactory; use Monolog\Handler\TestHandler; use Monolog\Logger; From a91a3150fbc4446e379cc23618db8f74e4044515 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Thu, 7 Feb 2019 17:30:38 +0100 Subject: [PATCH 104/107] CS --- src/Wallabag/ImportBundle/Import/PocketImport.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Wallabag/ImportBundle/Import/PocketImport.php b/src/Wallabag/ImportBundle/Import/PocketImport.php index b35a561bb..746120af1 100644 --- a/src/Wallabag/ImportBundle/Import/PocketImport.php +++ b/src/Wallabag/ImportBundle/Import/PocketImport.php @@ -247,7 +247,7 @@ class PocketImport extends AbstractImport protected function jsonDecode(ResponseInterface $response) { - $data = \json_decode((string) $response->getBody(), true); + $data = json_decode((string) $response->getBody(), true); if (JSON_ERROR_NONE !== json_last_error()) { throw new \InvalidArgumentException('Unable to parse JSON data: ' . json_last_error_msg()); From b6c1e1bacc59ba761d1b47ac6611d1db800f7252 Mon Sep 17 00:00:00 2001 From: Jeremy Benoist Date: Thu, 7 Feb 2019 17:56:05 +0100 Subject: [PATCH 105/107] Fix some tests --- .../CoreBundle/Helper/ContentProxy.php | 19 ++++---- .../CoreBundle/Helper/DownloadImagesTest.php | 44 ++++++------------- 2 files changed, 24 insertions(+), 39 deletions(-) diff --git a/src/Wallabag/CoreBundle/Helper/ContentProxy.php b/src/Wallabag/CoreBundle/Helper/ContentProxy.php index ac27e50a9..59465ad11 100644 --- a/src/Wallabag/CoreBundle/Helper/ContentProxy.php +++ b/src/Wallabag/CoreBundle/Helper/ContentProxy.php @@ -54,7 +54,11 @@ class ContentProxy if ((empty($content) || false === $this->validateContent($content)) && false === $disableContentUpdate) { $fetchedContent = $this->graby->fetchContent($url); - $fetchedContent['title'] = $this->sanitizeContentTitle($fetchedContent['title'], $fetchedContent['content_type']); + + $fetchedContent['title'] = $this->sanitizeContentTitle( + $fetchedContent['title'], + isset($fetchedContent['headers']['content-type']) ? $fetchedContent['headers']['content-type'] : '' + ); // when content is imported, we have information in $content // in case fetching content goes bad, we'll keep the imported information instead of overriding them @@ -188,8 +192,8 @@ class ContentProxy /** * Try to sanitize the title of the fetched content from wrong character encodings and invalid UTF-8 character. * - * @param $title - * @param $contentType + * @param string $title + * @param string $contentType * * @return string */ @@ -293,12 +297,15 @@ class ContentProxy } // if content is an image, define it as a preview too - if (!empty($content['headers']['content_type']) && \in_array($this->mimeGuesser->guess($content['headers']['content_type']), ['jpeg', 'jpg', 'gif', 'png'], true)) { + if (!empty($content['headers']['content-type']) && \in_array($this->mimeGuesser->guess($content['headers']['content-type']), ['jpeg', 'jpg', 'gif', 'png'], true)) { $previewPictureUrl = $content['url']; + + $entry->setMimetype($content['headers']['content-type']); } elseif (empty($previewPictureUrl)) { $this->logger->debug('Extracting images from content to provide a default preview picture'); $imagesUrls = DownloadImages::extractImagesUrlsFromHtml($content['html']); $this->logger->debug(\count($imagesUrls) . ' pictures found'); + if (!empty($imagesUrls)) { $previewPictureUrl = $imagesUrls[0]; } @@ -308,10 +315,6 @@ class ContentProxy $this->updatePreviewPicture($entry, $previewPictureUrl); } - if (!empty($content['headers']['content-type'])) { - $entry->setMimetype($content['headers']['content-type']); - } - try { $this->tagger->tag($entry); } catch (\Exception $e) { diff --git a/tests/Wallabag/CoreBundle/Helper/DownloadImagesTest.php b/tests/Wallabag/CoreBundle/Helper/DownloadImagesTest.php index 0199b3e44..3c720425a 100644 --- a/tests/Wallabag/CoreBundle/Helper/DownloadImagesTest.php +++ b/tests/Wallabag/CoreBundle/Helper/DownloadImagesTest.php @@ -2,10 +2,7 @@ namespace Tests\Wallabag\CoreBundle\Helper; -use GuzzleHttp\Client; use GuzzleHttp\Psr7\Response; -use GuzzleHttp\Stream\Stream; -use GuzzleHttp\Subscriber\Mock; use Http\Mock\Client as HttpMockClient; use Monolog\Handler\TestHandler; use Monolog\Logger; @@ -153,20 +150,15 @@ class DownloadImagesTest extends TestCase public function testProcessImageWithSrcset() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['content-type' => 'image/jpeg'], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))), - new Response(200, ['content-type' => 'image/jpeg'], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))), - new Response(200, ['content-type' => 'image/jpeg'], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['content-type' => null], file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))); + $httpMockClient->addResponse(new Response(200, ['content-type' => null], file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))); + $httpMockClient->addResponse(new Response(200, ['content-type' => null], file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))); $logHandler = new TestHandler(); $logger = new Logger('test', [$logHandler]); - $download = new DownloadImages($client, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); + $download = new DownloadImages($httpMockClient, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); $res = $download->processHtml(123, '

    ', 'http://piketty.blog.lemonde.fr/2017/10/12/budget-2018-la-jeunesse-sacrifiee/'); $this->assertNotContains('http://piketty.blog.lemonde.fr/', $res, 'Image srcset attribute were not replaced'); @@ -174,20 +166,15 @@ class DownloadImagesTest extends TestCase public function testProcessImageWithTrickySrcset() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['content-type' => 'image/jpeg'], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))), - new Response(200, ['content-type' => 'image/jpeg'], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))), - new Response(200, ['content-type' => 'image/jpeg'], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['content-type' => null], file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))); + $httpMockClient->addResponse(new Response(200, ['content-type' => null], file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))); + $httpMockClient->addResponse(new Response(200, ['content-type' => null], file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))); $logHandler = new TestHandler(); $logger = new Logger('test', [$logHandler]); - $download = new DownloadImages($client, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); + $download = new DownloadImages($httpMockClient, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); $res = $download->processHtml(123, '
    Test

    ", 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', + 'headers' => [ + 'content-type' => 'text/html', + ], 'language' => 'fr', 'status' => '200', - 'open_graph' => [ - 'og_title' => 'my OG title', - 'og_description' => 'OG desc', - 'og_image' => null, - ], + 'image' => null, ]); $proxy = new ContentProxy($graby, $tagger, $this->getValidator(), $this->getLogger(), $this->fetchingErrorMessage); @@ -274,14 +272,12 @@ class ContentProxyTest extends TestCase 'html' => "

    Test

    ", 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', + 'headers' => [ + 'content-type' => 'text/html', + ], 'language' => 'fr', 'status' => '200', - 'open_graph' => [ - 'og_title' => 'my OG title', - 'og_description' => 'OG desc', - 'og_image' => 'http://3.3.3.3/cover.jpg', - ], + 'image' => 'http://3.3.3.3/cover.jpg', ]); $proxy = new ContentProxy($graby, $tagger, $this->getValidator(), $this->getLogger(), $this->fetchingErrorMessage);