From: Jeremy Benoist Date: Mon, 27 May 2019 08:46:01 +0000 (+0200) Subject: Merge remote-tracking branch 'origin/master' into 2.4 X-Git-Url: https://git.immae.eu/?a=commitdiff_plain;h=92a66835624acf6fd14f5adc5f8aab399658592e;hp=2ba365c7c49556cd23b444dc3bb8d4a8cf08809d;p=github%2Fwallabag%2Fwallabag.git Merge remote-tracking branch 'origin/master' into 2.4 --- diff --git a/.editorconfig b/.editorconfig index 6553d30f..14044044 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 39306343..8c1ec5cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,12 +4,6 @@ services: - rabbitmq - redis -# used for HHVM -addons: - apt: - packages: - - tidy - # cache vendor dirs cache: apt: true @@ -21,10 +15,9 @@ cache: - $HOME/.yarn-cache php: - - 5.6 - - 7.0 - 7.1 - 7.2 + - 7.3 - nightly node_js: @@ -38,7 +31,7 @@ env: matrix: fast_finish: true include: - - php: 7.0 + - php: 7.2 env: CS_FIXER=run VALIDATE_TRANSLATION_FILE=run ASSETS=build DB=sqlite allow_failures: - php: nightly @@ -58,31 +51,26 @@ 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 - - if [[ $DB = pgsql ]]; then psql -c 'create database wallabag_test;' -U postgres; fi; - # increase swap to avoid "proc_open(): fork failed - Cannot allocate memory" - # this should be removed when no more PHP 5 build will be defined - - sudo swapon -s - - sudo fallocate -l 4G /swapfile - - sudo chmod 600 /swapfile - - sudo mkswap /swapfile - - sudo swapon /swapfile - - sudo swapon -s + # 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 + - echo "travis_fold:start:prepare" - make prepare DB=$DB - echo "travis_fold:end:prepare" - - echo "travis_fold:start:fixtures" - - php bin/console doctrine:fixtures:load --no-interaction --env=test - - echo "travis_fold:end:fixtures" + - make fixtures - - if [[ $VALIDATE_TRANSLATION_FILE = '' ]]; then ./bin/simple-phpunit -v ; fi; + - if [[ $VALIDATE_TRANSLATION_FILE = '' ]]; then SYMFONY_PHPUNIT_VERSION=6.5 ./bin/simple-phpunit -v ; fi; + # PHPStan needs PHPUnit to be installed and cache app to be generated + - if [[ $VALIDATE_TRANSLATION_FILE = '' ]]; then php bin/phpstan analyse src tests --no-progress --level 1 ; fi; - if [[ $CS_FIXER = run ]]; then php bin/php-cs-fixer fix --verbose --dry-run ; fi; - if [[ $VALIDATE_TRANSLATION_FILE = run ]]; then php bin/console lint:yaml src/Wallabag/CoreBundle/Resources/translations -v ; fi; - if [[ $VALIDATE_TRANSLATION_FILE = run ]]; then php bin/console lint:yaml app/Resources/CraueConfigBundle/translations -v ; fi; diff --git a/.zappr.yaml b/.zappr.yaml deleted file mode 100644 index f90cd809..00000000 --- a/.zappr.yaml +++ /dev/null @@ -1,26 +0,0 @@ -# see https://zappr.opensource.zalan.do/ -autobranch: false -commit: false -approvals: - minimum: 1 - ignore: pr_opener - pattern: "^(:\\+1:|👍)$" - veto: - pattern: "^(:\\-1:|👎)$" - from: - orgs: - - wallabag - collaborators: true -specification: - title: - minimum-length: - enabled: true - length: 8 - body: - minimum-length: - enabled: true - length: 8 - contains-url: false - contains-issue-number: false - template: - differs-from-body: true diff --git a/COPYING.md b/COPYING.md index 6be863d3..72b9d5d0 100644 --- a/COPYING.md +++ b/COPYING.md @@ -1,4 +1,4 @@ -Copyright (c) 2013-2017 Nicolas Lœuillet +Copyright (c) 2013-current Nicolas Lœuillet Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 57392da2..e00c7ea0 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,10 @@ Then you can install wallabag by executing the following commands: ``` git clone https://github.com/wallabag/wallabag.git -cd wallabag && make install +cd wallabag && make install ``` -Now, [configure a virtual host](https://doc.wallabag.org/en/admin/installation/virtualhosts.html) to use your wallabag. +Now, [configure a virtual host](https://doc.wallabag.org/en/admin/installation/virtualhosts.html) to use your wallabag. # Run on YunoHost [![Install Wallabag with YunoHost](https://install-app.yunohost.org/install-with-yunohost.png)](https://install-app.yunohost.org/?app=wallabag2) @@ -30,6 +30,6 @@ Now, [configure a virtual host](https://doc.wallabag.org/en/admin/installation/v Wallabag app for [YunoHost](https://yunohost.org). See [here](https://github.com/YunoHost-Apps/wallabag2_ynh) # License -Copyright © 2013-2018 Nicolas Lœuillet +Copyright © 2013-current Nicolas Lœuillet This work is free. You can redistribute it and/or modify it under the terms of the MIT License. See the COPYING file for more details. diff --git a/app/AppKernel.php b/app/AppKernel.php index 40726f05..7d19e9ab 100644 --- a/app/AppKernel.php +++ b/app/AppKernel.php @@ -1,6 +1,7 @@ getEnvironment(), ['dev', 'test'], true)) { $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle(); $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle(); - $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle(); $bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(); - $bundles[] = new Symfony\Bundle\WebServerBundle\WebServerBundle(); if ('test' === $this->getEnvironment()) { $bundles[] = new DAMA\DoctrineTestBundle\DAMADoctrineTestBundle(); } + + if ('dev' === $this->getEnvironment()) { + $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle(); + $bundles[] = new Symfony\Bundle\WebServerBundle\WebServerBundle(); + } } return $bundles; } + public function getRootDir() + { + return __DIR__; + } + public function getCacheDir() { return dirname(__DIR__) . '/var/cache/' . $this->getEnvironment(); @@ -70,7 +79,8 @@ class AppKernel extends Kernel public function registerContainerConfiguration(LoaderInterface $loader) { - $loader->load($this->getProjectDir() . '/app/config/config_' . $this->getEnvironment() . '.yml'); + $loader->load($this->getRootDir() . '/config/config_' . $this->getEnvironment() . '.yml'); + $loader->load(function ($container) { if ($container->getParameter('use_webpack_dev_server')) { $container->loadFromExtension('framework', [ @@ -86,5 +96,11 @@ class AppKernel extends Kernel ]); } }); + + $loader->load(function (ContainerBuilder $container) { + // $container->setParameter('container.autowiring.strict_mode', true); + // $container->setParameter('container.dumper.inline_class_loader', true); + $container->addObjectResource($this); + }); } } diff --git a/app/DoctrineMigrations/Version20180405182455.php b/app/DoctrineMigrations/Version20180405182455.php new file mode 100755 index 00000000..50fe97c7 --- /dev/null +++ b/app/DoctrineMigrations/Version20180405182455.php @@ -0,0 +1,51 @@ +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'); + } +} diff --git a/app/DoctrineMigrations/Version20181128203230.php b/app/DoctrineMigrations/Version20181128203230.php new file mode 100644 index 00000000..d1b09fc7 --- /dev/null +++ b/app/DoctrineMigrations/Version20181128203230.php @@ -0,0 +1,45 @@ +skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration can only be applied on \'mysql\'.'); + + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_access_tokens') . ' CHANGE `token` `token` varchar(191) NOT NULL'); + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_access_tokens') . ' CHANGE `scope` `scope` varchar(191)'); + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_auth_codes') . ' CHANGE `token` `token` varchar(191) NOT NULL'); + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_auth_codes') . ' CHANGE `scope` `scope` varchar(191)'); + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_refresh_tokens') . ' CHANGE `token` `token` varchar(191) NOT NULL'); + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_refresh_tokens') . ' CHANGE `scope` `scope` varchar(191)'); + $this->addSql('ALTER TABLE ' . $this->getTable('craue_config_setting') . ' CHANGE `name` `name` varchar(191)'); + $this->addSql('ALTER TABLE ' . $this->getTable('craue_config_setting') . ' CHANGE `section` `section` varchar(191)'); + $this->addSql('ALTER TABLE ' . $this->getTable('craue_config_setting') . ' CHANGE `value` `value` varchar(191)'); + } + + public function down(Schema $schema) + { + $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration can only be applied on \'mysql\'.'); + + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_access_tokens') . ' CHANGE `token` `token` varchar(255) NOT NULL'); + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_access_tokens') . ' CHANGE `scope` `scope` varchar(255)'); + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_auth_codes') . ' CHANGE `token` `token` varchar(255) NOT NULL'); + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_auth_codes') . ' CHANGE `scope` `scope` varchar(255)'); + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_refresh_tokens') . ' CHANGE `token` `token` varchar(255) NOT NULL'); + $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_refresh_tokens') . ' CHANGE `scope` `scope` varchar(255)'); + $this->addSql('ALTER TABLE ' . $this->getTable('craue_config_setting') . ' CHANGE `name` `name` varchar(255)'); + $this->addSql('ALTER TABLE ' . $this->getTable('craue_config_setting') . ' CHANGE `section` `section` varchar(255)'); + $this->addSql('ALTER TABLE ' . $this->getTable('craue_config_setting') . ' CHANGE `value` `value` varchar(255)'); + } +} diff --git a/app/DoctrineMigrations/Version20181202073750.php b/app/DoctrineMigrations/Version20181202073750.php new file mode 100644 index 00000000..5978291e --- /dev/null +++ b/app/DoctrineMigrations/Version20181202073750.php @@ -0,0 +1,76 @@ +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'); + $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; + } + } + + public function down(Schema $schema): void + { + 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'); + $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; + } + } +} diff --git a/app/DoctrineMigrations/Version20190117131816.php b/app/DoctrineMigrations/Version20190117131816.php new file mode 100644 index 00000000..6548b9fa --- /dev/null +++ b/app/DoctrineMigrations/Version20190117131816.php @@ -0,0 +1,32 @@ +getTable($this->getTable('site_credential')); + + $this->skipIf($siteCredentialTable->hasColumn('updated_at'), 'It seems that you already played this migration.'); + + $siteCredentialTable->addColumn('updated_at', 'datetime', [ + 'notnull' => false, + ]); + } + + public function down(Schema $schema): void + { + $siteCredentialTable = $schema->getTable($this->getTable('site_credential')); + + $this->skipIf(!$siteCredentialTable->hasColumn('updated_at'), 'It seems that you already played this migration.'); + + $siteCredentialTable->dropColumn('updated_at'); + } +} diff --git a/app/DoctrineMigrations/Version20190129120000.php b/app/DoctrineMigrations/Version20190129120000.php new file mode 100644 index 00000000..3632e762 --- /dev/null +++ b/app/DoctrineMigrations/Version20190129120000.php @@ -0,0 +1,147 @@ + '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) + { + foreach ($this->settings as $setting) { + $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'] . "');"); + } + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + $this->skipIf(true, 'These settings are required and should not be removed.'); + } +} diff --git a/app/DoctrineMigrations/Version20190401105353.php b/app/DoctrineMigrations/Version20190401105353.php new file mode 100644 index 00000000..d27962db --- /dev/null +++ b/app/DoctrineMigrations/Version20190401105353.php @@ -0,0 +1,42 @@ +getTable($this->getTable('entry')); + + $this->skipIf($entryTable->hasColumn('hashed_url'), 'It seems that you already played this migration.'); + + $entryTable->addColumn('hashed_url', 'text', [ + 'length' => 40, + 'notnull' => false, + ]); + + $entryTable->addIndex(['user_id', 'hashed_url'], 'hashed_url_user_id', [], ['lengths' => [null, 40]]); + } + + /** + * @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->dropIndex('hashed_url_user_id'); + $entryTable->dropColumn('hashed_url'); + } +} diff --git a/app/DoctrineMigrations/Version20190425115043.php b/app/DoctrineMigrations/Version20190425115043.php new file mode 100644 index 00000000..4c5c49cc --- /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/app/DoctrineMigrations/Version20190510141130.php b/app/DoctrineMigrations/Version20190510141130.php new file mode 100644 index 00000000..524aa452 --- /dev/null +++ b/app/DoctrineMigrations/Version20190510141130.php @@ -0,0 +1,96 @@ +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('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) 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, 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 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)'); + $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) 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/app/Resources/static/themes/_global/index.js b/app/Resources/static/themes/_global/index.js index ae598e56..9ad96fc0 100644 --- a/app/Resources/static/themes/_global/index.js +++ b/app/Resources/static/themes/_global/index.js @@ -70,4 +70,41 @@ $(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(); + } + }; + }); + + // mimic radio button because emailTwoFactor is a boolean + $('#update_user_googleTwoFactor').on('change', () => { + $('#update_user_emailTwoFactor').prop('checked', false); + }); + + $('#update_user_emailTwoFactor').on('change', () => { + $('#update_user_googleTwoFactor').prop('checked', false); + }); + + // same mimic for super admin + $('#user_googleTwoFactor').on('change', () => { + $('#user_emailTwoFactor').prop('checked', false); + }); + + $('#user_emailTwoFactor').on('change', () => { + $('#user_googleTwoFactor').prop('checked', false); + }); }); diff --git a/app/Resources/static/themes/baggy/css/article.scss b/app/Resources/static/themes/baggy/css/article.scss index 9094ad55..d203ce31 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/layout.scss b/app/Resources/static/themes/baggy/css/layout.scss index cb14e62d..0293ebe5 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/baggy/css/pictos.scss b/app/Resources/static/themes/baggy/css/pictos.scss index 2ff01937..b6ebf311 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/Resources/static/themes/material/css/cards.scss b/app/Resources/static/themes/material/css/cards.scss index 0cdc7457..9ae1be82 100644 --- a/app/Resources/static/themes/material/css/cards.scss +++ b/app/Resources/static/themes/material/css/cards.scss @@ -197,6 +197,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/app/Resources/static/themes/material/index.js b/app/Resources/static/themes/material/index.js index 96310d81..2926cad1 100755 --- a/app/Resources/static/themes/material/index.js +++ b/app/Resources/static/themes/material/index.js @@ -8,7 +8,7 @@ import 'materialize-css/dist/js/materialize'; import '../_global/index'; /* Tools */ -import { initExport, initFilters } from './js/tools'; +import { initExport, initFilters, initRandom } from './js/tools'; /* Import shortcuts */ import './js/shortcuts/main'; @@ -32,8 +32,10 @@ $(document).ready(() => { format: 'dd/mm/yyyy', container: 'body', }); + initFilters(); initExport(); + initRandom(); const toggleNav = (toShow, toFocus) => { $('.nav-panel-actions').hide(100); @@ -48,25 +50,30 @@ $(document).ready(() => { $('#tag_label').focus(); return false; }); + $('#nav-btn-add').on('click', () => { toggleNav('.nav-panel-add', '#entry_url'); return false; }); + const materialAddForm = $('.nav-panel-add'); materialAddForm.on('submit', () => { materialAddForm.addClass('disabled'); $('input#entry_url', materialAddForm).prop('readonly', true).trigger('blur'); }); + $('#nav-btn-search').on('click', () => { toggleNav('.nav-panel-search', '#search_entry_term'); return false; }); + $('.close').on('click', (e) => { $(e.target).parent('.nav-panel-item').hide(100); $('.nav-panel-actions').show(100); $('.nav-panels').css('background', 'transparent'); return false; }); + $(window).scroll(() => { const s = $(window).scrollTop(); const d = $(document).height(); diff --git a/app/Resources/static/themes/material/js/tools.js b/app/Resources/static/themes/material/js/tools.js index 39398fd8..0b3d3038 100644 --- a/app/Resources/static/themes/material/js/tools.js +++ b/app/Resources/static/themes/material/js/tools.js @@ -8,6 +8,7 @@ function initFilters() { $('#clear_form_filters').on('click', () => { $('#filters input').val(''); $('#filters :checked').removeAttr('checked'); + return false; }); } @@ -21,4 +22,15 @@ function initExport() { } } -export { initExport, initFilters }; +function initRandom() { + // no display if export (ie: entries) not available + if ($('div').is('#export')) { + $('#button_random').show(); + } +} + +export { + initExport, + initFilters, + initRandom, +}; diff --git a/app/autoload.php b/app/autoload.php deleted file mode 100644 index c5f664dc..00000000 --- a/app/autoload.php +++ /dev/null @@ -1,13 +0,0 @@ -getParameterOption(['--env', '-e'], getenv('SYMFONY_ENV') ?: 'dev'); -$debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption(['--no-debug', '']) && $env !== 'prod'; +$env = $input->getParameterOption(['--env', '-e'], getenv('SYMFONY_ENV') ?: 'dev', true); +$debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption('--no-debug', true) && $env !== 'prod'; if ($debug) { Debug::enable(); diff --git a/composer.json b/composer.json index 0483da1d..b1c144c7 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,10 @@ "name": "wallabag/wallabag", "type": "project", "description": "open source self hostable read-it-later web application", - "keywords": ["read-it-later","read it later"], + "keywords": [ + "read-it-later", + "read it later" + ], "homepage": "https://github.com/wallabag/wallabag", "license": "MIT", "authors": [ @@ -28,7 +31,7 @@ "issues": "https://github.com/wallabag/wallabag/issues" }, "require": { - "php": ">=5.6.0", + "php": ">=7.1.3", "ext-pcre": "*", "ext-dom": "*", "ext-curl": "*", @@ -44,59 +47,64 @@ "ext-tokenizer": "*", "ext-pdo": "*", "ext-tidy": "*", - "symfony/symfony": "~3.3.13", - "doctrine/orm": "^2.5.12", - "doctrine/doctrine-bundle": "^1.8.0", - "doctrine/doctrine-cache-bundle": "^1.3.2", - "twig/extensions": "^1.5.1", - "symfony/swiftmailer-bundle": "^2.6.7", - "symfony/monolog-bundle": "^3.1.2", - "sensio/distribution-bundle": "^5.0.21", - "sensio/framework-extra-bundle": "^3.0.28", - "incenteev/composer-parameter-handler": "^2.1.2", + "symfony/symfony": "3.4.*", + "doctrine/orm": "^2.6", + "doctrine/doctrine-bundle": "^1.9", + "doctrine/doctrine-cache-bundle": "^1.3", + "twig/extensions": "^1.5", + "symfony/swiftmailer-bundle": "^3.2", + "symfony/monolog-bundle": "^3.1", + "sensio/distribution-bundle": "^5.0", + "sensio/framework-extra-bundle": "^5.2", + "incenteev/composer-parameter-handler": "^2.1", "nelmio/cors-bundle": "~1.5", "friendsofsymfony/rest-bundle": "~2.1", "jms/serializer-bundle": "~2.2", "nelmio/api-doc-bundle": "^2.13.2", "mgargano/simplehtmldom": "~1.5", - "wallabag/tcpdf": "^6.2.15", + "wallabag/tcpdf": "^6.2.26", "simplepie/simplepie": "~1.5", "willdurand/hateoas-bundle": "~1.3", "liip/theme-bundle": "^1.4.6", - "lexik/form-filter-bundle": "^5.0.4", + "lexik/form-filter-bundle": "^5.0", "j0k3r/graby": "^1.0", "friendsofsymfony/user-bundle": "2.0.*", - "friendsofsymfony/oauth-server-bundle": "^1.5.2", + "friendsofsymfony/oauth-server-bundle": "^1.5", "stof/doctrine-extensions-bundle": "^1.2", - "scheb/two-factor-bundle": "^2.14.0", - "grandt/phpepub": "^4.0.7", - "wallabag/php-mobi": "~1.0.0", + "scheb/two-factor-bundle": "^3.0", + "grandt/phpepub": "dev-master", + "wallabag/php-mobi": "~1.0", "kphoen/rulerz-bundle": "~0.13", "guzzlehttp/guzzle": "^5.3.1", "doctrine/doctrine-migrations-bundle": "^1.3", - "paragonie/random_compat": "^2.0.11", - "craue/config-bundle": "~2.0", + "craue/config-bundle": "dev-utf8mb4", "mnapoli/piwik-twig-extension": "^1.0", - "ocramius/proxy-manager": "^1.0.2", - "white-october/pagerfanta-bundle": "^1.1.0", + "ocramius/proxy-manager": "^2.1.1", + "white-october/pagerfanta-bundle": "^1.1", "php-amqplib/rabbitmq-bundle": "^1.14", - "predis/predis": "^1.1.1", + "predis/predis": "v1.1.x-dev", "javibravo/simpleue": "^2.0", - "symfony/dom-crawler": "^3.3.13", - "friendsofsymfony/jsrouting-bundle": "^1.6.3", + "symfony/dom-crawler": "^3.4", + "friendsofsymfony/jsrouting-bundle": "^2.2", "bdunogier/guzzle-site-authenticator": "^1.0.0", "defuse/php-encryption": "^2.1", "html2text/html2text": "^4.1", - "sulu/symfony-intl-fix": "^1.0" + "pragmarx/recovery": "^0.1.0" }, "require-dev": { - "doctrine/doctrine-fixtures-bundle": "~2.2", - "doctrine/data-fixtures": "~1.1", + "doctrine/doctrine-fixtures-bundle": "~3.0", "sensio/generator-bundle": "^3.0", "symfony/phpunit-bridge": "^4.2", - "friendsofphp/php-cs-fixer": "~2.0", - "m6web/redis-mock": "^2.0", - "dama/doctrine-test-bundle": "^4.0" + "friendsofphp/php-cs-fixer": "~2.13", + "m6web/redis-mock": "^4.1", + "dama/doctrine-test-bundle": "^5.0", + "phpstan/phpstan": "^0.11.0", + "phpstan/phpstan-phpunit": "^0.11.0", + "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": [ @@ -125,22 +133,40 @@ } }, "autoload": { - "psr-4": { "Wallabag\\": "src/Wallabag/" }, - "classmap": [ "app/AppKernel.php", "app/AppCache.php" ], - "exclude-from-classmap": [ - "vendor/symfony/intl/Locale.php", - "vendor/symfony/symfony/src/Symfony/Component/Intl/Locale.php" + "psr-4": { + "Wallabag\\": "src/Wallabag/" + }, + "classmap": [ + "app/AppKernel.php", + "app/AppCache.php" ] }, "autoload-dev": { - "psr-4": { "Tests\\": "tests/" } + "psr-4": { + "Tests\\": "tests/" + }, + "files": [ + "vendor/symfony/symfony/src/Symfony/Component/VarDumper/Resources/functions/dump.php" + ] }, "config": { "bin-dir": "bin", "platform": { - "php": "5.6.0" + "php": "7.1.3" } }, "minimum-stability": "dev", - "prefer-stable": true + "prefer-stable": true, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/Daniel-KM/PHPePub", + "comment": "The most up-to-date PHPePub as of now" + }, + { + "type": "vcs", + "url": "https://github.com/wallabag/CraueConfigBundle", + "comment": "To handle utf8mb4 field size" + } + ] } diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index d0266ec7..b632cb8a 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' diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..dfbc97ac --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,13 @@ +includes: + - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-symfony/extension.neon + - vendor/phpstan/phpstan-doctrine/extension.neon + - vendor/phpstan/phpstan-phpunit/rules.neon + +parameters: + symfony: + container_xml_path: %rootDir%/../../../var/cache/test/appTestDebugProjectContainer.xml + + # https://github.com/phpstan/phpstan/issues/694#issuecomment-350724288 + autoload_files: + - vendor/bin/.phpunit/phpunit-6.5/vendor/autoload.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 951b5a14..426a5e72 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -4,7 +4,7 @@ xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.8/phpunit.xsd" backupGlobals="false" colors="true" - bootstrap="app/autoload.php" + bootstrap="vendor/autoload.php" > @@ -15,7 +15,7 @@ - + diff --git a/src/Wallabag/AnnotationBundle/DataFixtures/ORM/LoadAnnotationData.php b/src/Wallabag/AnnotationBundle/DataFixtures/AnnotationFixtures.php similarity index 67% rename from src/Wallabag/AnnotationBundle/DataFixtures/ORM/LoadAnnotationData.php rename to src/Wallabag/AnnotationBundle/DataFixtures/AnnotationFixtures.php index 20e07fa3..ed46cea9 100644 --- a/src/Wallabag/AnnotationBundle/DataFixtures/ORM/LoadAnnotationData.php +++ b/src/Wallabag/AnnotationBundle/DataFixtures/AnnotationFixtures.php @@ -1,13 +1,15 @@ 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('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 = $this->getDoctrine() - ->getRepository('WallabagCoreBundle:Entry') - ->findByUrlAndUserId($url, $this->getUser()->getId()); + $res = $repo->findByUrlAndUserId($url, $this->getUser()->getId()); $results[$url] = $this->returnExistInformation($res, $returnId); } @@ -59,18 +72,21 @@ class EntryRestController extends WallabagRestController // let's see if it is a simple url? $url = $request->query->get('url', ''); + $hashedUrl = $request->query->get('hashed_url', ''); - if (empty($url)) { + if (empty($url) && 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()); + $method = 'findByUrlAndUserId'; + if (!empty($hashedUrl)) { + $method = 'findByHashedUrlAndUserId'; + $url = $hashedUrl; + } - $exists = $this->returnExistInformation($res, $returnId); + $res = $repo->$method($url, $this->getUser()->getId()); - return $this->sendResponse(['exists' => $exists]); + return $this->sendResponse(['exists' => $this->returnExistInformation($res, $returnId)]); } /** @@ -80,13 +96,14 @@ 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."}, * {"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."}, * } * ) * @@ -105,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 */ @@ -116,7 +134,8 @@ class EntryRestController extends WallabagRestController $sort, $order, $since, - $tags + $tags, + $detail ); } catch (\Exception $e) { throw new BadRequestHttpException($e->getMessage()); @@ -140,8 +159,9 @@ class EntryRestController extends WallabagRestController 'perPage' => $perPage, 'tags' => $tags, 'since' => $since, + 'detail' => $detail, ], - UrlGeneratorInterface::ABSOLUTE_URL + true ) ); @@ -363,7 +383,7 @@ class EntryRestController extends WallabagRestController } if (null !== $data['isArchived']) { - $entry->setArchived((bool) $data['isArchived']); + $entry->updateArchived((bool) $data['isArchived']); } if (null !== $data['isStarred']) { @@ -479,7 +499,7 @@ class EntryRestController extends WallabagRestController } if (null !== $data['isArchived']) { - $entry->setArchived((bool) $data['isArchived']); + $entry->updateArchived((bool) $data['isArchived']); } if (null !== $data['isStarred']) { @@ -786,24 +806,6 @@ class EntryRestController extends WallabagRestController return $this->sendResponse($results); } - /** - * Shortcut to send data serialized in json. - * - * @param mixed $data - * - * @return JsonResponse - */ - private function sendResponse($data) - { - // https://github.com/schmittjoh/JMSSerializerBundle/issues/293 - $context = new SerializationContext(); - $context->setSerializeNull(true); - - $json = $this->get('jms_serializer')->serialize($data, 'json', $context); - - return (new JsonResponse())->setJson($json); - } - /** * Retrieve value from the request. * Used for POST & PATCH on a an entry. diff --git a/src/Wallabag/ApiBundle/Controller/SearchRestController.php b/src/Wallabag/ApiBundle/Controller/SearchRestController.php new file mode 100644 index 00000000..d9f99844 --- /dev/null +++ b/src/Wallabag/ApiBundle/Controller/SearchRestController.php @@ -0,0 +1,65 @@ +validateAuthentication(); + + $term = $request->query->get('term'); + $page = (int) $request->query->get('page', 1); + $perPage = (int) $request->query->get('perPage', 30); + + $qb = $this->get('wallabag_core.entry_repository') + ->getBuilderForSearchByUser( + $this->getUser()->getId(), + $term, + null + ); + + $pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false); + $pager = new Pagerfanta($pagerAdapter); + + $pager->setMaxPerPage($perPage); + $pager->setCurrentPage($page); + + $pagerfantaFactory = new PagerfantaFactory('page', 'perPage'); + $paginatedCollection = $pagerfantaFactory->createRepresentation( + $pager, + new Route( + 'api_get_search', + [ + 'term' => $term, + 'page' => $page, + 'perPage' => $perPage, + ], + true + ) + ); + + return $this->sendResponse($paginatedCollection); + } +} diff --git a/src/Wallabag/ApiBundle/Controller/WallabagRestController.php b/src/Wallabag/ApiBundle/Controller/WallabagRestController.php index 7d8cfbba..f18b0910 100644 --- a/src/Wallabag/ApiBundle/Controller/WallabagRestController.php +++ b/src/Wallabag/ApiBundle/Controller/WallabagRestController.php @@ -3,6 +3,7 @@ namespace Wallabag\ApiBundle\Controller; use FOS\RestBundle\Controller\FOSRestController; +use JMS\Serializer\SerializationContext; use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Security\Core\Exception\AccessDeniedException; @@ -14,6 +15,8 @@ class WallabagRestController extends FOSRestController * * @ApiDoc() * + * @deprecated Should use info endpoint instead + * * @return JsonResponse */ public function getVersionAction() @@ -24,6 +27,24 @@ class WallabagRestController extends FOSRestController return (new JsonResponse())->setJson($json); } + /** + * Retrieve information about the wallabag instance. + * + * @ApiDoc() + * + * @return JsonResponse + */ + public function getInfoAction() + { + $info = [ + 'appname' => 'wallabag', + 'version' => $this->container->getParameter('wallabag_core.version'), + 'allowed_registration' => $this->container->getParameter('wallabag_user.registration_enabled'), + ]; + + return (new JsonResponse())->setJson($this->get('jms_serializer')->serialize($info, 'json')); + } + protected function validateAuthentication() { if (false === $this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) { @@ -44,4 +65,22 @@ class WallabagRestController extends FOSRestController throw $this->createAccessDeniedException('Access forbidden. Entry user id: ' . $requestUserId . ', logged user id: ' . $user->getId()); } } + + /** + * Shortcut to send data serialized in json. + * + * @param mixed $data + * + * @return JsonResponse + */ + protected function sendResponse($data) + { + // https://github.com/schmittjoh/JMSSerializerBundle/issues/293 + $context = new SerializationContext(); + $context->setSerializeNull(true); + + $json = $this->get('jms_serializer')->serialize($data, 'json', $context); + + return (new JsonResponse())->setJson($json); + } } diff --git a/src/Wallabag/ApiBundle/Entity/AccessToken.php b/src/Wallabag/ApiBundle/Entity/AccessToken.php index c09a0c80..98e0af3e 100644 --- a/src/Wallabag/ApiBundle/Entity/AccessToken.php +++ b/src/Wallabag/ApiBundle/Entity/AccessToken.php @@ -8,6 +8,22 @@ use FOS\OAuthServerBundle\Entity\AccessToken as BaseAccessToken; /** * @ORM\Table("oauth2_access_tokens") * @ORM\Entity + * @ORM\AttributeOverrides({ + * @ORM\AttributeOverride(name="token", + * column=@ORM\Column( + * name = "token", + * type = "string", + * length = 191 + * ) + * ), + * @ORM\AttributeOverride(name="scope", + * column=@ORM\Column( + * name = "scope", + * type = "string", + * length = 191 + * ) + * ) + * }) */ class AccessToken extends BaseAccessToken { @@ -26,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 4d4b09fe..7c9c8539 100644 --- a/src/Wallabag/ApiBundle/Entity/AuthCode.php +++ b/src/Wallabag/ApiBundle/Entity/AuthCode.php @@ -8,6 +8,22 @@ use FOS\OAuthServerBundle\Entity\AuthCode as BaseAuthCode; /** * @ORM\Table("oauth2_auth_codes") * @ORM\Entity + * @ORM\AttributeOverrides({ + * @ORM\AttributeOverride(name="token", + * column=@ORM\Column( + * name = "token", + * type = "string", + * length = 191 + * ) + * ), + * @ORM\AttributeOverride(name="scope", + * column=@ORM\Column( + * name = "scope", + * type = "string", + * length = 191 + * ) + * ) + * }) */ class AuthCode extends BaseAuthCode { @@ -26,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 822a02d8..55a507e1 100644 --- a/src/Wallabag/ApiBundle/Entity/RefreshToken.php +++ b/src/Wallabag/ApiBundle/Entity/RefreshToken.php @@ -8,6 +8,22 @@ use FOS\OAuthServerBundle\Entity\RefreshToken as BaseRefreshToken; /** * @ORM\Table("oauth2_refresh_tokens") * @ORM\Entity + * @ORM\AttributeOverrides({ + * @ORM\AttributeOverride(name="token", + * column=@ORM\Column( + * name = "token", + * type = "string", + * length = 191 + * ) + * ), + * @ORM\AttributeOverride(name="scope", + * column=@ORM\Column( + * name = "scope", + * type = "string", + * length = 191 + * ) + * ) + * }) */ class RefreshToken extends BaseRefreshToken { @@ -26,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; } diff --git a/src/Wallabag/ApiBundle/Form/Type/ClientType.php b/src/Wallabag/ApiBundle/Form/Type/ClientType.php index fc22538f..14dc5c44 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/ApiBundle/Resources/config/routing_rest.yml b/src/Wallabag/ApiBundle/Resources/config/routing_rest.yml index c0283e71..06e62c37 100644 --- a/src/Wallabag/ApiBundle/Resources/config/routing_rest.yml +++ b/src/Wallabag/ApiBundle/Resources/config/routing_rest.yml @@ -3,6 +3,11 @@ entry: resource: "WallabagApiBundle:EntryRest" name_prefix: api_ +search: + type: rest + resource: "WallabagApiBundle:SearchRest" + name_prefix: api_ + tag: type: rest resource: "WallabagApiBundle:TagRest" diff --git a/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php b/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php new file mode 100644 index 00000000..45bd8c5f --- /dev/null +++ b/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php @@ -0,0 +1,98 @@ +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 = (string) $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 "%d" users', \count($users))); + + foreach ($users as $user) { + $output->writeln(sprintf('Processing user: %s', $user->getUsername())); + $this->generateHashedUrls($user); + } + $output->writeln('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()); + + $i = 1; + foreach ($entries as $entry) { + $entry->setHashedUrl(hash('sha1', $entry->getUrl())); + $em->persist($entry); + + if (0 === ($i % 20)) { + $em->flush(); + } + ++$i; + } + + $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/Command/InstallCommand.php b/src/Wallabag/CoreBundle/Command/InstallCommand.php index 3c76545c..c58ae2b5 100644 --- a/src/Wallabag/CoreBundle/Command/InstallCommand.php +++ b/src/Wallabag/CoreBundle/Command/InstallCommand.php @@ -94,8 +94,9 @@ class InstallCommand extends ContainerAwareCommand $status = 'OK!'; $help = ''; + $conn = $this->getContainer()->get('doctrine')->getManager()->getConnection(); + try { - $conn = $this->getContainer()->get('doctrine')->getManager()->getConnection(); $conn->connect(); } catch (\Exception $e) { if (false === strpos($e->getMessage(), 'Unknown database') @@ -253,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/Command/ShowUserCommand.php b/src/Wallabag/CoreBundle/Command/ShowUserCommand.php index a0184267..c95efbf3 100644 --- a/src/Wallabag/CoreBundle/Command/ShowUserCommand.php +++ b/src/Wallabag/CoreBundle/Command/ShowUserCommand.php @@ -57,7 +57,8 @@ class ShowUserCommand extends ContainerAwareCommand sprintf('Display name: %s', $user->getName()), sprintf('Creation date: %s', $user->getCreatedAt()->format('Y-m-d H:i:s')), sprintf('Last login: %s', null !== $user->getLastLogin() ? $user->getLastLogin()->format('Y-m-d H:i:s') : 'never'), - sprintf('2FA activated: %s', $user->isTwoFactorAuthentication() ? 'yes' : 'no'), + sprintf('2FA (email) activated: %s', $user->isEmailTwoFactor() ? 'yes' : 'no'), + sprintf('2FA (OTP) activated: %s', $user->isGoogleAuthenticatorEnabled() ? 'yes' : 'no'), ]); } diff --git a/src/Wallabag/CoreBundle/Controller/ConfigController.php b/src/Wallabag/CoreBundle/Controller/ConfigController.php index b999c539..3b281d48 100644 --- a/src/Wallabag/CoreBundle/Controller/ConfigController.php +++ b/src/Wallabag/CoreBundle/Controller/ConfigController.php @@ -2,17 +2,19 @@ namespace Wallabag\CoreBundle\Controller; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; +use PragmaRX\Recovery\Recovery as BackupCodes; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Validator\Constraints\Locale as LocaleConstraint; 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; @@ -45,7 +47,7 @@ class ConfigController extends Controller $activeTheme = $this->get('liip_theme.active_theme'); $activeTheme->setName($config->getTheme()); - $this->get('session')->getFlashBag()->add( + $this->addFlash( 'notice', 'flashes.config.notice.config_saved' ); @@ -67,7 +69,7 @@ class ConfigController extends Controller $userManager->updateUser($user, true); } - $this->get('session')->getFlashBag()->add('notice', $message); + $this->addFlash('notice', $message); return $this->redirect($this->generateUrl('config') . '#set4'); } @@ -82,7 +84,7 @@ class ConfigController extends Controller if ($userForm->isSubmitted() && $userForm->isValid()) { $userManager->updateUser($user, true); - $this->get('session')->getFlashBag()->add( + $this->addFlash( 'notice', 'flashes.config.notice.user_updated' ); @@ -90,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->get('session')->getFlashBag()->add( + $this->addFlash( 'notice', - 'flashes.config.notice.rss_updated' + 'flashes.config.notice.feed_updated' ); return $this->redirect($this->generateUrl('config') . '#set2'); @@ -130,7 +132,7 @@ class ConfigController extends Controller $em->persist($taggingRule); $em->flush(); - $this->get('session')->getFlashBag()->add( + $this->addFlash( 'notice', 'flashes.config.notice.tagging_rules_updated' ); @@ -141,22 +143,134 @@ 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'), - 'enabled_users' => $this->get('wallabag_user.user_repository') - ->getSumEnabledUsers(), + 'enabled_users' => $this->get('wallabag_user.user_repository')->getSumEnabledUsers(), ]); } + /** + * Enable 2FA using email. + * + * @Route("/config/otp/email", name="config_otp_email") + */ + public function otpEmailAction() + { + 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(); + $secret = $this->get('scheb_two_factor.security.google_authenticator')->generateSecret(); + + $user->setGoogleAuthenticatorSecret($secret); + $user->setEmailTwoFactor(false); + + $backupCodes = (new BackupCodes())->toArray(); + $backupCodesHashed = array_map( + function ($backupCode) { + return password_hash($backupCode, PASSWORD_DEFAULT); + }, + $backupCodes + ); + + $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), + ]); + } + + /** + * 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 * @@ -167,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->get('session')->getFlashBag()->add( + $this->addFlash( 'notice', - 'flashes.config.notice.rss_token_updated' + 'flashes.config.notice.feed_token_updated' ); return $this->redirect($this->generateUrl('config') . '#set2'); @@ -202,7 +316,7 @@ class ConfigController extends Controller $em->remove($rule); $em->flush(); - $this->get('session')->getFlashBag()->add( + $this->addFlash( 'notice', 'flashes.config.notice.tagging_rules_deleted' ); @@ -268,7 +382,7 @@ class ConfigController extends Controller break; } - $this->get('session')->getFlashBag()->add( + $this->addFlash( 'notice', 'flashes.config.notice.' . $type . '_reset' ); @@ -329,6 +443,27 @@ class ConfigController extends Controller return $this->redirect($request->headers->get('referer')); } + /** + * Change the locale for the current user. + * + * @param Request $request + * @param string $language + * + * @Route("/locale/{language}", name="changeLocale") + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + */ + public function setLocaleAction(Request $request, $language = null) + { + $errors = $this->get('validator')->validate($language, (new LocaleConstraint())); + + if (0 === \count($errors)) { + $request->getSession()->set('_locale', $language); + } + + return $this->redirect($request->headers->get('referer', $this->generateUrl('homepage'))); + } + /** * Remove all tags for given tags and a given user and cleanup orphan tags. * diff --git a/src/Wallabag/CoreBundle/Controller/EntryController.php b/src/Wallabag/CoreBundle/Controller/EntryController.php index b7fdea27..5c8ecb40 100644 --- a/src/Wallabag/CoreBundle/Controller/EntryController.php +++ b/src/Wallabag/CoreBundle/Controller/EntryController.php @@ -2,12 +2,13 @@ namespace Wallabag\CoreBundle\Controller; +use Doctrine\ORM\NoResultException; use Pagerfanta\Adapter\DoctrineORMAdapter; use Pagerfanta\Exception\OutOfRangeCurrentPageException; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Wallabag\CoreBundle\Entity\Entry; use Wallabag\CoreBundle\Event\EntryDeletedEvent; @@ -232,6 +233,46 @@ class EntryController extends Controller return $this->showEntries('starred', $request, $page); } + /** + * Shows untagged articles for current user. + * + * @param Request $request + * @param int $page + * + * @Route("/untagged/list/{page}", name="untagged", defaults={"page" = "1"}) + * + * @return \Symfony\Component\HttpFoundation\Response + */ + public function showUntaggedEntriesAction(Request $request, $page) + { + return $this->showEntries('untagged', $request, $page); + } + + /** + * Shows random entry depending on the given type. + * + * @param string $type + * + * @Route("/{type}/random", name="random_entry", requirements={"type": "unread|starred|archive|untagged|all"}) + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + */ + public function redirectRandomEntryAction($type = 'all') + { + try { + $entry = $this->get('wallabag_core.entry_repository') + ->getRandomEntry($this->getUser()->getId(), $type); + } catch (NoResultException $e) { + $bag = $this->get('session')->getFlashBag(); + $bag->clear(); + $bag->add('notice', 'flashes.entry.notice.no_random_entry'); + + return $this->redirect($this->generateUrl($type)); + } + + return $this->redirect($this->generateUrl('view', ['id' => $entry->getId()])); + } + /** * Shows entry content. * @@ -465,54 +506,6 @@ class EntryController extends Controller ); } - /** - * Shows untagged articles for current user. - * - * @param Request $request - * @param int $page - * - * @Route("/untagged/list/{page}", name="untagged", defaults={"page" = "1"}) - * - * @return \Symfony\Component\HttpFoundation\Response - */ - public function showUntaggedEntriesAction(Request $request, $page) - { - return $this->showEntries('untagged', $request, $page); - } - - /** - * Fetch content and update entry. - * In case it fails, $entry->getContent will return an error message. - * - * @param Entry $entry - * @param string $prefixMessage Should be the translation key: entry_saved or entry_reloaded - */ - private function updateEntry(Entry $entry, $prefixMessage = 'entry_saved') - { - $message = 'flashes.entry.notice.' . $prefixMessage; - - try { - $this->get('wallabag_core.content_proxy')->updateEntry($entry, $entry->getUrl()); - } catch (\Exception $e) { - $this->get('logger')->error('Error while saving an entry', [ - 'exception' => $e, - 'entry' => $entry, - ]); - - $message = 'flashes.entry.notice.' . $prefixMessage . '_failed'; - } - - if (empty($entry->getDomainName())) { - $this->get('wallabag_core.content_proxy')->setEntryDomainName($entry); - } - - if (empty($entry->getTitle())) { - $this->get('wallabag_core.content_proxy')->setDefaultEntryTitle($entry); - } - - $this->get('session')->getFlashBag()->add('notice', $message); - } - /** * Global method to retrieve entries depending on the given type * It returns the response to be send. @@ -532,11 +525,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()); @@ -587,6 +578,39 @@ class EntryController extends Controller ); } + /** + * Fetch content and update entry. + * In case it fails, $entry->getContent will return an error message. + * + * @param Entry $entry + * @param string $prefixMessage Should be the translation key: entry_saved or entry_reloaded + */ + private function updateEntry(Entry $entry, $prefixMessage = 'entry_saved') + { + $message = 'flashes.entry.notice.' . $prefixMessage; + + try { + $this->get('wallabag_core.content_proxy')->updateEntry($entry, $entry->getUrl()); + } catch (\Exception $e) { + $this->get('logger')->error('Error while saving an entry', [ + 'exception' => $e, + 'entry' => $entry, + ]); + + $message = 'flashes.entry.notice.' . $prefixMessage . '_failed'; + } + + if (empty($entry->getDomainName())) { + $this->get('wallabag_core.content_proxy')->setEntryDomainName($entry); + } + + if (empty($entry->getTitle())) { + $this->get('wallabag_core.content_proxy')->setDefaultEntryTitle($entry); + } + + $this->get('session')->getFlashBag()->add('notice', $message); + } + /** * Check if the logged user can manage the given entry. * diff --git a/src/Wallabag/CoreBundle/Controller/ExportController.php b/src/Wallabag/CoreBundle/Controller/ExportController.php index 9e9dbe49..9ff35ff5 100644 --- a/src/Wallabag/CoreBundle/Controller/ExportController.php +++ b/src/Wallabag/CoreBundle/Controller/ExportController.php @@ -2,10 +2,10 @@ namespace Wallabag\CoreBundle\Controller; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Routing\Annotation\Route; use Wallabag\CoreBundle\Entity\Entry; /** diff --git a/src/Wallabag/CoreBundle/Controller/RssController.php b/src/Wallabag/CoreBundle/Controller/FeedController.php similarity index 62% rename from src/Wallabag/CoreBundle/Controller/RssController.php rename to src/Wallabag/CoreBundle/Controller/FeedController.php index 848bb814..8d422a90 100644 --- a/src/Wallabag/CoreBundle/Controller/RssController.php +++ b/src/Wallabag/CoreBundle/Controller/FeedController.php @@ -7,86 +7,97 @@ use Pagerfanta\Adapter\DoctrineORMAdapter; use Pagerfanta\Exception\OutOfRangeCurrentPageException; use Pagerfanta\Pagerfanta; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 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; 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, "_format"="xml"}) + * + * @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, "_format"="xml"}) + * + * @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, "_format"="xml"}) + * + * @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("/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 showAllRSSAction(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_rss", defaults={"_format"="xml"}) - * @ParamConverter("user", class="WallabagUserBundle:User", converter="username_rsstoken_converter") + * @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 showTagsAction(Request $request, User $user, Tag $tag) + public function showTagsFeedAction(User $user, Tag $tag, $page) { - $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 +130,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 +176,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 +192,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/Controller/SiteCredentialController.php b/src/Wallabag/CoreBundle/Controller/SiteCredentialController.php index 548de744..51bc1d94 100644 --- a/src/Wallabag/CoreBundle/Controller/SiteCredentialController.php +++ b/src/Wallabag/CoreBundle/Controller/SiteCredentialController.php @@ -2,10 +2,9 @@ namespace Wallabag\CoreBundle\Controller; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; use Wallabag\CoreBundle\Entity\SiteCredential; use Wallabag\UserBundle\Entity\User; @@ -19,8 +18,7 @@ class SiteCredentialController extends Controller /** * Lists all User entities. * - * @Route("/", name="site_credentials_index") - * @Method("GET") + * @Route("/", name="site_credentials_index", methods={"GET"}) */ public function indexAction() { @@ -36,8 +34,7 @@ class SiteCredentialController extends Controller /** * Creates a new site credential entity. * - * @Route("/new", name="site_credentials_new") - * @Method({"GET", "POST"}) + * @Route("/new", name="site_credentials_new", methods={"GET", "POST"}) * * @param Request $request * @@ -77,8 +74,7 @@ class SiteCredentialController extends Controller /** * Displays a form to edit an existing site credential entity. * - * @Route("/{id}/edit", name="site_credentials_edit") - * @Method({"GET", "POST"}) + * @Route("/{id}/edit", name="site_credentials_edit", methods={"GET", "POST"}) * * @param Request $request * @param SiteCredential $siteCredential @@ -121,8 +117,7 @@ class SiteCredentialController extends Controller /** * Deletes a site credential entity. * - * @Route("/{id}", name="site_credentials_delete") - * @Method("DELETE") + * @Route("/{id}", name="site_credentials_delete", methods={"DELETE"}) * * @param Request $request * @param SiteCredential $siteCredential diff --git a/src/Wallabag/CoreBundle/Controller/StaticController.php b/src/Wallabag/CoreBundle/Controller/StaticController.php index 318af303..fa760c14 100644 --- a/src/Wallabag/CoreBundle/Controller/StaticController.php +++ b/src/Wallabag/CoreBundle/Controller/StaticController.php @@ -2,8 +2,8 @@ namespace Wallabag\CoreBundle\Controller; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\Routing\Annotation\Route; class StaticController extends Controller { diff --git a/src/Wallabag/CoreBundle/Controller/TagController.php b/src/Wallabag/CoreBundle/Controller/TagController.php index b6d28e59..d0155c60 100644 --- a/src/Wallabag/CoreBundle/Controller/TagController.php +++ b/src/Wallabag/CoreBundle/Controller/TagController.php @@ -5,12 +5,13 @@ namespace Wallabag\CoreBundle\Controller; use Pagerfanta\Adapter\ArrayAdapter; use Pagerfanta\Exception\OutOfRangeCurrentPageException; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; 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 { @@ -87,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, ]); } @@ -130,4 +137,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); + } } diff --git a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadConfigData.php b/src/Wallabag/CoreBundle/DataFixtures/ConfigFixtures.php similarity index 81% rename from src/Wallabag/CoreBundle/DataFixtures/ORM/LoadConfigData.php rename to src/Wallabag/CoreBundle/DataFixtures/ConfigFixtures.php index 3d4d5def..c54e9f2c 100644 --- a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadConfigData.php +++ b/src/Wallabag/CoreBundle/DataFixtures/ConfigFixtures.php @@ -1,13 +1,14 @@ flush(); } - - /** - * {@inheritdoc} - */ - public function getOrder() - { - return 29; - } } diff --git a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadSiteCredentialData.php b/src/Wallabag/CoreBundle/DataFixtures/SiteCredentialFixtures.php similarity index 77% rename from src/Wallabag/CoreBundle/DataFixtures/ORM/LoadSiteCredentialData.php rename to src/Wallabag/CoreBundle/DataFixtures/SiteCredentialFixtures.php index faf29da6..9a7d116f 100644 --- a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadSiteCredentialData.php +++ b/src/Wallabag/CoreBundle/DataFixtures/SiteCredentialFixtures.php @@ -1,15 +1,16 @@ flush(); } - - /** - * {@inheritdoc} - */ - public function getOrder() - { - return 25; - } } diff --git a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadTaggingRuleData.php b/src/Wallabag/CoreBundle/DataFixtures/TaggingRuleFixtures.php similarity index 77% rename from src/Wallabag/CoreBundle/DataFixtures/ORM/LoadTaggingRuleData.php rename to src/Wallabag/CoreBundle/DataFixtures/TaggingRuleFixtures.php index 55abd63c..78ff314a 100644 --- a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadTaggingRuleData.php +++ b/src/Wallabag/CoreBundle/DataFixtures/TaggingRuleFixtures.php @@ -1,13 +1,13 @@ 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/Doctrine/DBAL/Driver/CustomPostgreSQLDriver.php b/src/Wallabag/CoreBundle/Doctrine/DBAL/Driver/CustomPostgreSQLDriver.php deleted file mode 100644 index eb5b203f..00000000 --- a/src/Wallabag/CoreBundle/Doctrine/DBAL/Driver/CustomPostgreSQLDriver.php +++ /dev/null @@ -1,25 +0,0 @@ -_platform->quoteIdentifier($sequenceName); - - // the `method_exists` is only to avoid test to fail: - // DAMA\DoctrineTestBundle\Doctrine\DBAL\StaticConnection doesn't support the `getServerVersion` - if (method_exists($this->_conn->getWrappedConnection(), 'getServerVersion') && (float) ($this->_conn->getWrappedConnection()->getServerVersion()) >= 10) { - $query = "SELECT min_value, increment_by FROM pg_sequences WHERE schemaname = 'public' AND sequencename = " . $this->_conn->quote($sequenceName); - } - - $data = $this->_conn->fetchAll($query); - - return new Sequence($sequenceName, $data[0]['increment_by'], $data[0]['min_value']); - } -} diff --git a/src/Wallabag/CoreBundle/Doctrine/WallabagMigration.php b/src/Wallabag/CoreBundle/Doctrine/WallabagMigration.php index 7aa2409a..4a3fef3b 100644 --- a/src/Wallabag/CoreBundle/Doctrine/WallabagMigration.php +++ b/src/Wallabag/CoreBundle/Doctrine/WallabagMigration.php @@ -2,8 +2,8 @@ namespace Wallabag\CoreBundle\Doctrine; -use Doctrine\DBAL\Migrations\AbstractMigration; use Doctrine\DBAL\Schema\Schema; +use Doctrine\Migrations\AbstractMigration; use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerInterface; diff --git a/src/Wallabag/CoreBundle/Entity/Config.php b/src/Wallabag/CoreBundle/Entity/Config.php index b902ae2c..c6e65d66 100644 --- a/src/Wallabag/CoreBundle/Entity/Config.php +++ b/src/Wallabag/CoreBundle/Entity/Config.php @@ -60,21 +60,21 @@ class Config /** * @var string * - * @ORM\Column(name="rss_token", type="string", nullable=true) + * @ORM\Column(name="feed_token", type="string", nullable=true) */ - private $rssToken; + 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, - * 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/Entity/Entry.php b/src/Wallabag/CoreBundle/Entity/Entry.php index 2b1f2e05..c3fb87d2 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="hashed_url_user_id", columns={"user_id", "hashed_url"}, options={"lengths"={null, 40}}) * } * ) * @ORM\HasLifecycleCallbacks() @@ -75,6 +76,13 @@ class Entry */ private $url; + /** + * @var string + * + * @ORM\Column(name="hashed_url", type="string", length=40, nullable=true) + */ + private $hashedUrl; + /** * @var bool * @@ -86,6 +94,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 * @@ -307,6 +324,7 @@ class Entry public function setUrl($url) { $this->url = $url; + $this->hashedUrl = hash('sha1', $url); return $this; } @@ -335,6 +353,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 +413,7 @@ class Entry public function toggleArchive() { - $this->isArchived = $this->isArchived() ^ 1; + $this->updateArchived($this->isArchived() ^ 1); return $this; } @@ -864,4 +920,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/Entity/SiteCredential.php b/src/Wallabag/CoreBundle/Entity/SiteCredential.php index ac714359..dee48fd5 100644 --- a/src/Wallabag/CoreBundle/Entity/SiteCredential.php +++ b/src/Wallabag/CoreBundle/Entity/SiteCredential.php @@ -59,6 +59,13 @@ class SiteCredential */ private $createdAt; + /** + * @var \DateTime + * + * @ORM\Column(name="updated_at", type="datetime") + */ + private $updatedAt; + /** * @ORM\ManyToOne(targetEntity="Wallabag\UserBundle\Entity\User", inversedBy="siteCredentials") */ @@ -178,6 +185,16 @@ class SiteCredential return $this->createdAt; } + /** + * Get updatedAt. + * + * @return \DateTime + */ + public function getUpdatedAt() + { + return $this->updatedAt; + } + /** * @return User */ diff --git a/src/Wallabag/CoreBundle/Entity/TaggingRule.php b/src/Wallabag/CoreBundle/Entity/TaggingRule.php index 84e11e26..c1be3165 100644 --- a/src/Wallabag/CoreBundle/Entity/TaggingRule.php +++ b/src/Wallabag/CoreBundle/Entity/TaggingRule.php @@ -3,7 +3,7 @@ namespace Wallabag\CoreBundle\Entity; use Doctrine\ORM\Mapping as ORM; -use KPhoen\RulerZBundle\Validator\Constraints as RulerZAssert; +use Symfony\Bridge\RulerZ\Validator\Constraints as RulerZAssert; use Symfony\Component\Validator\Constraints as Assert; /** diff --git a/src/Wallabag/CoreBundle/Event/Listener/UserLocaleListener.php b/src/Wallabag/CoreBundle/Event/Listener/UserLocaleListener.php index 367cdfb0..dc1db5c7 100644 --- a/src/Wallabag/CoreBundle/Event/Listener/UserLocaleListener.php +++ b/src/Wallabag/CoreBundle/Event/Listener/UserLocaleListener.php @@ -6,8 +6,10 @@ use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; /** - * Stores the locale of the user in the session after the - * login. This can be used by the LocaleListener afterwards. + * Stores the locale of the user in the session after the login. + * If no locale are defined (if user doesn't change it from the login screen), override it with the user's config one. + * + * This can be used by the LocaleListener afterwards. * * @see http://symfony.com/doc/master/cookbook/session/locale_sticky_session.html */ @@ -30,7 +32,7 @@ class UserLocaleListener { $user = $event->getAuthenticationToken()->getUser(); - if (null !== $user->getConfig()->getLanguage()) { + if (null !== $user->getConfig()->getLanguage() && null === $this->session->get('_locale')) { $this->session->set('_locale', $user->getConfig()->getLanguage()); } } diff --git a/src/Wallabag/CoreBundle/Form/Type/EditEntryType.php b/src/Wallabag/CoreBundle/Form/Type/EditEntryType.php index 08355928..2fc4c204 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/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 49b31c1e..9b34daf4 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/Form/Type/NewEntryType.php b/src/Wallabag/CoreBundle/Form/Type/NewEntryType.php index 7d74fee3..7af1e589 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, ]) ; } diff --git a/src/Wallabag/CoreBundle/Form/Type/RenameTagType.php b/src/Wallabag/CoreBundle/Form/Type/RenameTagType.php new file mode 100644 index 00000000..e6270048 --- /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'; + } +} diff --git a/src/Wallabag/CoreBundle/Form/Type/UserInformationType.php b/src/Wallabag/CoreBundle/Form/Type/UserInformationType.php index 07c99949..6e4c9154 100644 --- a/src/Wallabag/CoreBundle/Form/Type/UserInformationType.php +++ b/src/Wallabag/CoreBundle/Form/Type/UserInformationType.php @@ -21,9 +21,14 @@ class UserInformationType extends AbstractType ->add('email', EmailType::class, [ 'label' => 'config.form_user.email_label', ]) - ->add('twoFactorAuthentication', CheckboxType::class, [ + ->add('emailTwoFactor', CheckboxType::class, [ 'required' => false, - 'label' => 'config.form_user.twoFactorAuthentication_label', + 'label' => 'config.form_user.emailTwoFactor_label', + ]) + ->add('googleTwoFactor', CheckboxType::class, [ + 'required' => false, + 'label' => 'config.form_user.googleTwoFactor_label', + 'mapped' => false, ]) ->add('save', SubmitType::class, [ 'label' => 'config.form.save', diff --git a/src/Wallabag/CoreBundle/Helper/ContentProxy.php b/src/Wallabag/CoreBundle/Helper/ContentProxy.php index bc257ffb..ca01dec8 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 cc3dcfce..c1645e45 100644 --- a/src/Wallabag/CoreBundle/Helper/DownloadImages.php +++ b/src/Wallabag/CoreBundle/Helper/DownloadImages.php @@ -31,23 +31,36 @@ class DownloadImages } /** - * Process the html and extract image from it, save them to local and return the updated html. + * Process the html and extract images URLs from it. * - * @param int $entryId ID of the entry * @param string $html - * @param string $url Used as a base path for relative image and folder * - * @return string + * @return string[] */ - public function processHtml($entryId, $html, $url) + public static function extractImagesUrlsFromHtml($html) { $crawler = new Crawler($html); $imagesCrawler = $crawler ->filterXpath('//img'); $imagesUrls = $imagesCrawler ->extract(['src']); - $imagesSrcsetUrls = $this->getSrcsetUrls($imagesCrawler); - $imagesUrls = array_unique(array_merge($imagesUrls, $imagesSrcsetUrls)); + $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. + * + * @param int $entryId ID of the entry + * @param string $html + * @param string $url Used as a base path for relative image and folder + * + * @return string + */ + public function processHtml($entryId, $html, $url) + { + $imagesUrls = self::extractImagesUrlsFromHtml($html); $relativePath = $this->getRelativePath($entryId); @@ -135,7 +148,21 @@ class DownloadImages switch ($ext) { case 'gif': - imagegif($im, $localPath); + // use Imagick if available to keep GIF animation + if (class_exists('\\Imagick')) { + 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); + } + $this->logger->debug('DownloadImages: Re-creating gif'); break; case 'jpeg': @@ -185,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 diff --git a/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php b/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php index 183d394a..04abc6d0 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/Helper/RuleBasedTagger.php b/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php index 63f65067..fbdf2ac7 100644 --- a/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php +++ b/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php @@ -6,6 +6,7 @@ use Psr\Log\LoggerInterface; use RulerZ\RulerZ; use Wallabag\CoreBundle\Entity\Entry; use Wallabag\CoreBundle\Entity\Tag; +use Wallabag\CoreBundle\Entity\TaggingRule; use Wallabag\CoreBundle\Repository\EntryRepository; use Wallabag\CoreBundle\Repository\TagRepository; use Wallabag\UserBundle\Entity\User; 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 4a2fcab5..e220abfc 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/Repository/EntryRepository.php b/src/Wallabag/CoreBundle/Repository/EntryRepository.php index cebce714..3990932e 100644 --- a/src/Wallabag/CoreBundle/Repository/EntryRepository.php +++ b/src/Wallabag/CoreBundle/Repository/EntryRepository.php @@ -3,6 +3,7 @@ namespace Wallabag\CoreBundle\Repository; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; use Pagerfanta\Adapter\DoctrineORMAdapter; use Pagerfanta\Pagerfanta; @@ -50,7 +51,7 @@ class EntryRepository extends EntityRepository public function getBuilderForArchiveByUser($userId) { return $this - ->getSortedQueryBuilderByUser($userId) + ->getSortedQueryBuilderByUser($userId, 'archivedAt', 'desc') ->andWhere('e.isArchived = true') ; } @@ -110,8 +111,7 @@ class EntryRepository extends EntityRepository */ public function getBuilderForUntaggedByUser($userId) { - return $this - ->sortQueryBuilder($this->getRawBuilderForUntaggedByUser($userId)); + return $this->sortQueryBuilder($this->getRawBuilderForUntaggedByUser($userId)); } /** @@ -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); } @@ -193,6 +208,8 @@ class EntryRepository extends EntityRepository $qb->orderBy('e.id', $order); } elseif ('updated' === $sort) { $qb->orderBy('e.updatedAt', $order); + } elseif ('archived' === $sort) { + $qb->orderBy('e.archivedAt', $order); } $pagerAdapter = new DoctrineORMAdapter($qb, true, false); @@ -324,8 +341,8 @@ class EntryRepository extends EntityRepository * Find an entry by its url and its owner. * If it exists, return the entry otherwise return false. * - * @param $url - * @param $userId + * @param string $url + * @param int $userId * * @return Entry|bool */ @@ -344,6 +361,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 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', $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. * @@ -416,8 +457,8 @@ class EntryRepository extends EntityRepository /** * Find all entries by url and owner. * - * @param $url - * @param $userId + * @param string $url + * @param int $userId * * @return array */ @@ -430,6 +471,49 @@ class EntryRepository extends EntityRepository ->getResult(); } + /** + * Returns a random entry, filtering by status. + * + * @param int $userId + * @param string $type Can be unread, archive, starred, etc + * + * @throws NoResultException + * + * @return Entry + */ + public function getRandomEntry($userId, $type = '') + { + $qb = $this->getQueryBuilderByUser($userId) + ->select('e.id'); + + switch ($type) { + case 'unread': + $qb->andWhere('e.isArchived = false'); + break; + case 'archive': + $qb->andWhere('e.isArchived = true'); + break; + case 'starred': + $qb->andWhere('e.isStarred = true'); + break; + case 'untagged': + $qb->leftJoin('e.tags', 't'); + $qb->andWhere('t.id is null'); + break; + } + + $ids = $qb->getQuery()->getArrayResult(); + + if (empty($ids)) { + throw new NoResultException(); + } + + // random select one in the list + $randomId = $ids[mt_rand(0, \count($ids) - 1)]['id']; + + return $this->find($randomId); + } + /** * Return a query builder to be used by other getBuilderFor* method. * @@ -468,7 +552,6 @@ class EntryRepository extends EntityRepository */ private function sortQueryBuilder(QueryBuilder $qb, $sortBy = 'createdAt', $direction = 'desc') { - return $qb - ->orderBy(sprintf('e.%s', $sortBy), $direction); + return $qb->orderBy(sprintf('e.%s', $sortBy), $direction); } } diff --git a/src/Wallabag/CoreBundle/Resources/config/services.yml b/src/Wallabag/CoreBundle/Resources/config/services.yml index 85306276..280d779d 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" @@ -181,6 +181,7 @@ services: wallabag_core.exception_controller: class: Wallabag\CoreBundle\Controller\ExceptionController + public: true arguments: - '@twig' - '%kernel.debug%' @@ -218,3 +219,31 @@ services: arguments: - "%wallabag_core.site_credentials.encryption_key_path%" - "@logger" + + wallabag_core.command.clean_duplicates: + class: Wallabag\CoreBundle\Command\CleanDuplicatesCommand + tags: ['console.command'] + + wallabag_core.command.export: + class: Wallabag\CoreBundle\Command\ExportCommand + tags: ['console.command'] + + wallabag_core.command.install: + class: Wallabag\CoreBundle\Command\InstallCommand + tags: ['console.command'] + + wallabag_core.command.list_user: + class: Wallabag\CoreBundle\Command\ListUserCommand + tags: ['console.command'] + + wallabag_core.command.reload_entry: + class: Wallabag\CoreBundle\Command\ReloadEntryCommand + tags: ['console.command'] + + wallabag_core.command.show_user: + class: Wallabag\CoreBundle\Command\ShowUserCommand + tags: ['console.command'] + + wallabag_core.command.tag_all: + class: Wallabag\CoreBundle\Command\TagAllCommand + tags: ['console.command'] diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml index 6f842534..e04c2ff1 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml @@ -37,6 +37,7 @@ menu: add_new_entry: 'Tilføj ny artikel' search: 'Søg' filter_entries: 'Filtrer artikler' + # random_entry: Jump to a random entry from that list # export: 'Export' search_form: input_label: 'Indtast søgning' @@ -53,11 +54,12 @@ config: page_title: 'Opsætning' tab_menu: settings: 'Indstillinger' - rss: 'RSS' + feed: 'RSS' user_info: 'Brugeroplysninger' password: 'Adgangskode' # rules: 'Tagging rules' new_user: 'Tilføj bruger' + # reset: 'Reset area' form: save: 'Gem' form_settings: @@ -83,25 +85,33 @@ 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 on every new untrusted connexion" + # 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' - # twoFactorAuthentication_label: 'Two factor authentication' - # help_twoFactorAuthentication: "If you enable 2FA, each time you want to login to wallabag, you'll receive a code by email." + 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. @@ -159,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' @@ -353,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' @@ -404,6 +423,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.

' @@ -529,7 +550,8 @@ user: email_label: 'Emailadresse' # enabled_label: 'Enabled' # last_login_label: 'Last login' - # twofactor_label: Two factor authentication + # twofactor_email_label: Two factor authentication by email + # twofactor_google_label: Two factor authentication by OTP app # save: Save # delete: Delete # delete_confirm: Are you sure? @@ -567,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 @@ -588,9 +610,11 @@ flashes: entry_starred: 'Artikel markeret som favorit' entry_unstarred: 'Artikel ikke længere markeret som favorit' entry_deleted: 'Artikel slettet' + # no_random_entry: 'No article with these criterias was found' 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 874908b9..5a9668a9 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml @@ -37,6 +37,7 @@ menu: add_new_entry: 'Neuen Artikel hinzufügen' search: 'Suche' filter_entries: 'Artikel filtern' + # random_entry: Jump to a random entry from that list export: 'Exportieren' search_form: input_label: 'Suchbegriff hier eingeben' @@ -53,11 +54,12 @@ config: page_title: 'Einstellungen' tab_menu: settings: 'Einstellungen' - rss: 'RSS' + feed: 'RSS' user_info: 'Benutzerinformation' password: 'Kennwort' rules: 'Tagging-Regeln' new_user: 'Benutzer hinzufügen' + reset: 'Zurücksetzen' form: save: 'Speichern' form_settings: @@ -83,25 +85,33 @@ 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: "Wenn du die Zwei-Faktor-Authentifizierung aktivierst, erhältst du eine E-Mail mit einem Code bei jeder nicht vertrauenswürdigen Verbindung" + # 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' - twoFactorAuthentication_label: 'Zwei-Faktor-Authentifizierung' - help_twoFactorAuthentication: "Wenn du 2FA aktivierst, wirst du bei jedem Login einen Code per E-Mail bekommen." + 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.' @@ -353,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' @@ -404,6 +414,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.

' @@ -529,7 +541,8 @@ user: email_label: 'E-Mail-Adresse' enabled_label: 'Aktiviert' last_login_label: 'Letzter Login' - twofactor_label: 'Zwei-Faktor-Authentifizierung' + # twofactor_email_label: Two factor authentication by email + # twofactor_google_label: Two factor authentication by OTP app save: 'Speichern' delete: 'Löschen' delete_confirm: 'Bist du sicher?' @@ -567,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' @@ -588,9 +601,11 @@ flashes: entry_starred: 'Eintrag favorisiert' entry_unstarred: 'Eintrag defavorisiert' entry_deleted: 'Eintrag gelöscht' + # no_random_entry: 'No article with these criterias was found' 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 598ad58d..e2994f53 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml @@ -37,6 +37,7 @@ menu: add_new_entry: 'Add a new entry' search: 'Search' filter_entries: 'Filter entries' + random_entry: Jump to a random entry from that list export: 'Export' search_form: input_label: 'Enter your search here' @@ -53,11 +54,12 @@ config: page_title: 'Config' tab_menu: settings: 'Settings' - rss: 'RSS' + feed: 'Feeds' user_info: 'User information' password: 'Password' rules: 'Tagging rules' new_user: 'Add a user' + reset: 'Reset area' form: save: 'Save' form_settings: @@ -83,25 +85,33 @@ 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 on every new untrusted connection." + 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' - twoFactorAuthentication_label: 'Two factor authentication' - help_twoFactorAuthentication: "If you enable 2FA, each time you want to login to wallabag, you'll receive a code by email." + 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. @@ -159,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' @@ -353,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' @@ -404,6 +423,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.

' @@ -529,7 +550,8 @@ user: email_label: 'Email' enabled_label: 'Enabled' last_login_label: 'Last login' - twofactor_label: Two factor authentication + twofactor_email_label: Two factor authentication by email + twofactor_google_label: Two factor authentication by OTP app save: Save delete: Delete delete_confirm: Are you sure? @@ -567,14 +589,15 @@ 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 archived_reset: Archived entries deleted + otp_enabled: Two-factor authentication enabled entry: notice: entry_already_saved: 'Entry already saved on %date%' @@ -588,9 +611,11 @@ flashes: entry_starred: 'Entry starred' entry_unstarred: 'Entry unstarred' entry_deleted: 'Entry deleted' + no_random_entry: 'No article with these criterias was found' 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 f8aa4109..d1ccfc81 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml @@ -37,6 +37,7 @@ menu: add_new_entry: 'Añadir un nuevo artículo' search: 'Buscar' filter_entries: 'Filtrar los artículos' + # random_entry: Jump to a random entry from that list export: 'Exportar' search_form: input_label: 'Introduzca su búsqueda aquí' @@ -53,11 +54,12 @@ 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' new_user: 'Añadir un usuario' + reset: 'Reiniciar mi cuenta' form: save: 'Guardar' form_settings: @@ -83,25 +85,33 @@ 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: "Con la autenticación en dos pasos recibirá código por e-mail en cada nueva conexión que no sea de confianza." + # 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' - twoFactorAuthentication_label: 'Autenticación en dos pasos' - help_twoFactorAuthentication: "Si activas la autenticación en dos pasos, cada vez que quieras iniciar sesión en wallabag recibirás un código por e-mail." + 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. @@ -159,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' @@ -353,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' @@ -404,6 +423,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.

' @@ -529,7 +550,8 @@ user: email_label: 'E-mail' enabled_label: 'Activado' last_login_label: 'Último inicio de sesión' - twofactor_label: Autenticación en dos pasos + # twofactor_email_label: Two factor authentication by email + # twofactor_google_label: Two factor authentication by OTP app save: Guardar delete: Eliminar delete_confirm: ¿Estás seguro? @@ -567,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 @@ -588,9 +610,11 @@ flashes: entry_starred: 'Artículo marcado como favorito' entry_unstarred: 'Artículo desmarcado como favorito' entry_deleted: 'Artículo eliminado' + # no_random_entry: 'No article with these criterias was found' 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 785e39ee..e5d36bd3 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml @@ -37,6 +37,7 @@ menu: add_new_entry: 'افزودن مقالهٔ تازه' search: 'جستجو' filter_entries: 'فیلترکردن مقاله‌ها' + # random_entry: Jump to a random entry from that list export: 'برون‌بری' search_form: input_label: 'جستجوی خود را این‌جا بنویسید:' @@ -53,11 +54,12 @@ config: page_title: 'پیکربندی' tab_menu: settings: 'تنظیمات' - rss: 'آر-اس-اس' + feed: 'آر-اس-اس' user_info: 'اطلاعات کاربر' password: 'رمز' rules: 'برچسب‌گذاری خودکار' new_user: 'افزودن کاربر' + # reset: 'Reset area' form: save: 'ذخیره' form_settings: @@ -83,25 +85,33 @@ 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: "با فعال‌کردن تأیید ۲مرحله‌ای هر بار که اتصال تأییدنشده‌ای برقرار شد، به شما یک کد از راه ایمیل فرستاده می‌شود" + # 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: 'نشانی ایمیل' - twoFactorAuthentication_label: 'تأیید ۲مرحله‌ای' - # help_twoFactorAuthentication: "If you enable 2FA, each time you want to login to wallabag, you'll receive a code by email." + 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. @@ -159,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' @@ -353,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: 'مدیریت' @@ -404,6 +423,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.

' @@ -529,7 +550,8 @@ user: email_label: 'نشانی ایمیل' # enabled_label: 'Enabled' # last_login_label: 'Last login' - # twofactor_label: Two factor authentication + # twofactor_email_label: Two factor authentication by email + # twofactor_google_label: Two factor authentication by OTP app # save: Save # delete: Delete # delete_confirm: Are you sure? @@ -567,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 @@ -588,9 +610,11 @@ flashes: entry_starred: 'مقاله برگزیده شد' entry_unstarred: 'مقاله نابرگزیده شد' entry_deleted: 'مقاله پاک شد' + # no_random_entry: 'No article with these criterias was found' 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 b2fa1c50..0b1853a4 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml @@ -37,6 +37,7 @@ menu: add_new_entry: "Sauvegarder un nouvel article" search: "Rechercher" filter_entries: "Filtrer les articles" + random_entry: Aller à un article aléatoire de cette liste export: "Exporter" search_form: input_label: "Saisissez votre terme de recherche" @@ -53,11 +54,12 @@ 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" new_user: "Créer un compte" + reset: "Réinitialisation" form: save: "Enregistrer" form_settings: @@ -83,25 +85,33 @@ 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 à chaque nouvelle connexion non approuvée." + 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" - twoFactorAuthentication_label: "Double authentification" - help_twoFactorAuthentication: "Si vous activez 2FA, à chaque tentative de connexion à wallabag, vous recevrez un code par email." + 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é." @@ -159,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" @@ -353,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" @@ -404,6 +423,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.

' @@ -530,6 +551,8 @@ user: enabled_label: "Activé" last_login_label: "Dernière connexion" twofactor_label: "Double authentification" + twofactor_email_label: Double authentification par email + twofactor_google_label: Double authentification par OTP app save: "Sauvegarder" delete: "Supprimer" delete_confirm: "Êtes-vous sûr ?" @@ -567,14 +590,15 @@ 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" 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%" @@ -588,9 +612,11 @@ flashes: entry_starred: "Article ajouté dans les favoris" entry_unstarred: "Article retiré des favoris" entry_deleted: "Article supprimé" + no_random_entry: "Aucun article correspond aux critères n'a été trouvé" 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 ecaa3b60..0474d2bc 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml @@ -37,6 +37,7 @@ menu: add_new_entry: 'Aggiungi un nuovo contenuto' search: 'Cerca' filter_entries: 'Filtra contenuti' + # random_entry: Jump to a random entry from that list export: 'Esporta' search_form: input_label: 'Inserisci qui la tua ricerca' @@ -53,11 +54,12 @@ config: page_title: 'Configurazione' tab_menu: settings: 'Impostazioni' - rss: 'RSS' + feed: 'RSS' user_info: 'Informazioni utente' password: 'Password' rules: 'Regole di etichettatura' new_user: 'Aggiungi utente' + reset: 'Area di reset' form: save: 'Salva' form_settings: @@ -83,25 +85,32 @@ 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: "Abilitando l'autenticazione a due fattori riceverai una e-mail con un codice per ogni nuova connesione non verificata" + # 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' - twoFactorAuthentication_label: 'Autenticazione a due fattori' - help_twoFactorAuthentication: "Se abiliti l'autenticazione a due fattori, ogni volta che vorrai connetterti a wallabag, riceverai un codice via 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)' + # 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. @@ -159,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" @@ -353,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' @@ -404,6 +422,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.

' @@ -529,7 +549,8 @@ user: email_label: 'E-mail' enabled_label: 'Abilitato' last_login_label: 'Ultima connessione' - twofactor_label: Autenticazione a due fattori + # twofactor_email_label: Two factor authentication by email + # twofactor_google_label: Two factor authentication by OTP app save: Salva delete: Cancella delete_confirm: Sei sicuro? @@ -567,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 @@ -588,9 +609,11 @@ flashes: entry_starred: 'Contenuto segnato come preferito' entry_unstarred: 'Contenuto rimosso dai preferiti' entry_deleted: 'Contenuto eliminato' + # no_random_entry: 'No article with these criterias was found' 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 848c88d2..e761832e 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml @@ -37,6 +37,7 @@ menu: add_new_entry: 'Enregistrar un novèl article' search: 'Cercar' filter_entries: 'Filtrar los articles' + # random_entry: Jump to a random entry from that list export: 'Exportar' search_form: input_label: 'Picatz vòstre mot-clau a cercar aquí' @@ -53,11 +54,12 @@ 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" new_user: 'Crear un compte' + reset: 'Zòna de reïnicializacion' form: save: 'Enregistrar' form_settings: @@ -83,25 +85,32 @@ 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: "Activar l'autentificacion en dos temps vòl dire que recebretz un còdi per corrièl per cada novèla connexion pas aprovada." + # 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' - twoFactorAuthentication_label: 'Dobla autentificacion' - help_twoFactorAuthentication: "S'avètz activat l'autentificacion en dos temps, cada còp que volètz vos connectar a wallabag, recebretz un còdi per corrièl." + # 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. @@ -159,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" @@ -353,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' @@ -404,6 +422,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.

' @@ -529,7 +549,8 @@ user: email_label: 'Adreça de corrièl' enabled_label: 'Actiu' last_login_label: 'Darrièra connexion' - twofactor_label: 'Autentificacion doble-factor' + # twofactor_email_label: Two factor authentication by email + # twofactor_google_label: Two factor authentication by OTP app save: 'Enregistrar' delete: 'Suprimir' delete_confirm: 'Sètz segur ?' @@ -567,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 @@ -588,9 +609,11 @@ flashes: entry_starred: 'Article ajustat dins los favorits' entry_unstarred: 'Article quitat dels favorits' entry_deleted: 'Article suprimit' + # no_random_entry: 'No article with these criterias was found' 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 a0032fe8..f3d506e5 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml @@ -37,6 +37,7 @@ menu: add_new_entry: 'Dodaj nowy wpis' search: 'Szukaj' filter_entries: 'Filtruj wpisy' + # random_entry: Jump to a random entry from that list export: 'Eksportuj' search_form: input_label: 'Wpisz swoje zapytanie tutaj' @@ -53,11 +54,12 @@ 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' new_user: 'Dodaj użytkownika' + reset: 'Reset' form: save: 'Zapisz' form_settings: @@ -83,25 +85,32 @@ 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: "Włączenie autoryzacji dwuetapowej oznacza, że będziesz otrzymywał maile z kodem przy każdym nowym, niezaufanym połączeniu" + # 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' - twoFactorAuthentication_label: 'Autoryzacja dwuetapowa' - help_twoFactorAuthentication: "Jeżeli włączysz autoryzację dwuetapową. Za każdym razem, kiedy będziesz chciał się zalogować, dostaniesz kod na swój 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)' + # 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. @@ -159,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' @@ -353,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' @@ -404,6 +422,8 @@ tag: new: add: 'Dodaj' placeholder: 'Możesz dodać kilka tagów, oddzielając je przecinkami.' + rename: + placeholder: 'Możesz zaktualizować nazwę taga.' 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.

' @@ -529,7 +549,8 @@ user: email_label: 'Adres email' enabled_label: 'Włączony' last_login_label: 'Ostatnie logowanie' - twofactor_label: Autoryzacja dwuetapowa + # twofactor_email_label: Two factor authentication by email + # twofactor_google_label: Two factor authentication by OTP app save: Zapisz delete: Usuń delete_confirm: Jesteś pewien? @@ -567,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 @@ -588,9 +609,11 @@ flashes: entry_starred: 'Wpis oznaczony gwiazdką' entry_unstarred: 'Wpis odznaczony gwiazdką' entry_deleted: 'Wpis usunięty' + # no_random_entry: 'No article with these criterias was found' tag: notice: tag_added: 'Tag dodany' + tag_renamed: 'Nazwa taga zmieniona' 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 292fad61..6ddc1fc1 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml @@ -37,6 +37,7 @@ menu: add_new_entry: 'Adicionar uma nova entrada' search: 'Pesquisa' filter_entries: 'Filtrar entradas' + # random_entry: Jump to a random entry from that list export: 'Exportar' search_form: input_label: 'Digite aqui sua pesquisa' @@ -53,11 +54,12 @@ 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' new_user: 'Adicionar um usuário' + # reset: 'Reset area' form: save: 'Salvar' form_settings: @@ -83,25 +85,32 @@ 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: 'Habilitar autenticação de dois passos significa que você receberá um e-mail com um código a cada nova conexão desconhecida.' + # 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' - twoFactorAuthentication_label: 'Autenticação de dois passos' - # help_twoFactorAuthentication: "If you enable 2FA, each time you want to login to wallabag, you'll receive a code by 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)' + # 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. @@ -159,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' @@ -353,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' @@ -404,6 +422,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.

' @@ -529,7 +549,8 @@ user: email_label: 'E-mail' enabled_label: 'Habilitado' last_login_label: 'Último login' - twofactor_label: 'Autenticação de dois passos' + # twofactor_email_label: Two factor authentication by email + # twofactor_google_label: Two factor authentication by OTP app save: 'Salvar' delete: 'Apagar' delete_confirm: 'Tem certeza?' @@ -567,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 @@ -588,9 +609,11 @@ flashes: entry_starred: 'Entrada destacada' entry_unstarred: 'Entrada não destacada' entry_deleted: 'Entrada apagada' + # no_random_entry: 'No article with these criterias was found' 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 9e8d68b3..8c0791f0 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml @@ -37,6 +37,7 @@ menu: add_new_entry: 'Introdu un nou articol' search: 'Căutare' filter_entries: 'Filtrează articolele' + # random_entry: Jump to a random entry from that list # export: 'Export' search_form: input_label: 'Introdu căutarea ta' @@ -53,11 +54,12 @@ 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' new_user: 'Crează un utilizator' + # reset: 'Reset area' form: save: 'Salvează' form_settings: @@ -83,25 +85,32 @@ 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 on every new untrusted connexion" + # 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' - # twoFactorAuthentication_label: 'Two factor authentication' - # help_twoFactorAuthentication: "If you enable 2FA, each time you want to login to wallabag, you'll receive a code by 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)' + # 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. @@ -159,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' @@ -353,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' @@ -404,6 +422,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.

' @@ -529,7 +549,8 @@ user: email_label: 'E-mail' # enabled_label: 'Enabled' # last_login_label: 'Last login' - # twofactor_label: Two factor authentication + # twofactor_email_label: Two factor authentication by email + # twofactor_google_label: Two factor authentication by OTP app # save: Save # delete: Delete # delete_confirm: Are you sure? @@ -567,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 @@ -588,9 +609,11 @@ flashes: entry_starred: 'Articol adăugat la favorite' entry_unstarred: 'Articol șters de la favorite' entry_deleted: 'Articol șters' + # no_random_entry: 'No article with these criterias was found' 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 48753b55..2ee2d83a 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml @@ -36,6 +36,7 @@ menu: add_new_entry: 'Добавить новую запись' search: 'Поиск' filter_entries: 'Фильтр записей' + # random_entry: Jump to a random entry from that list export: 'Экспорт' search_form: input_label: 'Введите текст для поиска' @@ -52,11 +53,12 @@ config: page_title: 'Настройки' tab_menu: settings: 'Настройки' - rss: 'RSS' + feed: 'RSS' user_info: 'Информация о пользователе' password: 'Пароль' rules: 'Правила настройки простановки тегов' new_user: 'Добавить пользователя' + reset: 'Сброс данных' form: save: 'Сохранить' form_settings: @@ -81,24 +83,31 @@ 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: "Включить двухфакторную аутентификацию, Вы получите сообщение на указанный email с кодом, при каждом новом непроверенном подключении." + # 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' - twoFactorAuthentication_label: 'Двухфакторная аутентификация' - help_twoFactorAuthentication: "Если Вы включите двухфакторную аутентификацию, то Вы будете получать код на указанный ранее email, каждый раз при входе в wallabag." + # 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: "Если Вы удалите ваш аккаунт, ВСЕ ваши записи, теги и другие данные, будут БЕЗВОЗВРАТНО удалены (операция не может быть отменена после). Затем Вы выйдете из системы." @@ -154,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: 'Название записи' @@ -341,7 +359,7 @@ quickstart: title: 'Настроить приложение' description: 'Чтобы иметь приложение, которое вам подходит, ознакомьтесь с конфигурацией wallabag.' language: 'Выбрать язык и дизайн' - rss: 'Включить RSS фид' + feed: 'Включить RSS фид' tagging_rules: 'Создать правило для автоматической установки тегов' admin: title: 'Администрирование' @@ -392,6 +410,8 @@ tag: new: add: 'Добавить' placeholder: 'Вы можете добавить несколько тегов, разделенных запятой.' + 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.

' @@ -517,7 +537,8 @@ user: email_label: 'Email' enabled_label: 'Включить' last_login_label: 'Последний вход' - twofactor_label: "Двухфакторная аутентификация" + # twofactor_email_label: Two factor authentication by email + # twofactor_google_label: Two factor authentication by OTP app save: "Сохранить" delete: "Удалить" delete_confirm: "Вы уверены?" @@ -533,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: "Записи сброшены" @@ -553,9 +574,11 @@ flashes: entry_starred: 'Запись помечена звездочкой' entry_unstarred: 'Пометка звездочкой у записи убрана' entry_deleted: 'Запись удалена' + # no_random_entry: 'No article with these criterias was found' tag: notice: tag_added: 'Тег добавлен' + # tag_renamed: 'Tag renamed' import: notice: failed: 'Во время импорта произошла ошибка, повторите попытку.' @@ -573,4 +596,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 cb3b0f23..967ae427 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml @@ -37,6 +37,7 @@ menu: add_new_entry: 'เพิ่มรายการใหม่' search: 'ค้นหา' filter_entries: 'ตัวกรองรายการ' + # random_entry: Jump to a random entry from that list export: 'นำข้อมูลออก' search_form: input_label: 'ค้นหาที่นี้' @@ -53,11 +54,12 @@ config: page_title: 'กำหนดค่า' tab_menu: settings: 'ตั้งค่า' - rss: 'RSS' + feed: 'RSS' user_info: 'ข้อมูลผู้ใช้' password: 'รหัสผ่าน' rules: 'การแท็กข้อบังคับ' new_user: 'เพิ่มผู้ใช้' + reset: 'รีเซ็ตพื้นที่ ' form: save: 'บันทึก' form_settings: @@ -83,25 +85,32 @@ 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: "การเปิดใช้งาน two factor authentication คือคุณจะต้องได้รับอีเมลกับ code ที่ยังไม่ตรวจสอบในการเชื่อมต่อ" + # 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: 'อีเมล' - twoFactorAuthentication_label: 'Two factor authentication' - help_twoFactorAuthentication: "ถ้าคุณเปิด 2FA, ในแต่ละช่วงเวลาที่คุณต้องการลงชื่อเข้าใช wallabag, คุณจะต้องได้รับ code จากอีเมล" + # 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 , รายการทั้งหมดของคุณ, แท็กทั้งหมดของคุณ, หมายเหตุทั้งหมดของคุณและบัญชีของคุณจะถูกลบอย่างถาวร (มันไม่สามารถยกเลิกได้) คุณจะต้องลงชื่อออก @@ -159,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: 'หัวข้อรายการ' @@ -351,7 +369,7 @@ quickstart: title: 'กำหนดค่าแอพพลิเคชั่น' description: 'ภายใน order จะมี application suit ของคุณ, จะมองหาองค์ประกอบของ wallabag' language: 'เปลี่ยนภาษาและออกแบบ' - rss: 'เปิดใช้ RSS' + feed: 'เปิดใช้ RSS' tagging_rules: 'เขียนข้อบังคับการแท็กอัตโนมัติของบทความของคุณ' admin: title: 'ผู้ดูแลระบบ' @@ -402,6 +420,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.

' @@ -527,7 +547,8 @@ user: email_label: 'อีเมล' enabled_label: 'เปิดใช้งาน' last_login_label: 'ลงชื้อเข้าใช้ครั้งสุดท้าย' - twofactor_label: Two factor authentication + # twofactor_email_label: Two factor authentication by email + # twofactor_google_label: Two factor authentication by OTP app save: บันทึก delete: ลบ delete_confirm: ตุณแน่ใจหรือไม่? @@ -565,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: รีเซ็ตรายการ @@ -586,9 +607,11 @@ flashes: entry_starred: 'รายการที่แสดง' entry_unstarred: 'รายการที่ไม่ได้แสดง' entry_deleted: 'รายการที่ถูกลบ' + # no_random_entry: 'No article with these criterias was found' 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 e2156d47..2f86f25d 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml @@ -37,6 +37,7 @@ menu: add_new_entry: 'Yeni bir makale ekle' search: 'Ara' filter_entries: 'Filtrele' + # random_entry: Jump to a random entry from that list export: 'Dışa Aktar' search_form: input_label: 'Aramak istediğiniz herhangi bir şey yazın' @@ -53,11 +54,12 @@ config: page_title: 'Yapılandırma' tab_menu: settings: 'Ayarlar' - rss: 'RSS' + feed: 'RSS' user_info: 'Kullanıcı bilgileri' password: 'Şifre' rules: 'Etiketleme kuralları' new_user: 'Bir kullanıcı ekle' + # reset: 'Reset area' form: save: 'Kaydet' form_settings: @@ -83,25 +85,32 @@ 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: "İki adımlı doğrulamayı aktifleştirdiğinizde, her yeni güvenilmeyen bağlantılarda size e-posta ile bir kod alacaksınız." + # 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' - twoFactorAuthentication_label: 'İki adımlı doğrulama' - # help_twoFactorAuthentication: "If you enable 2FA, each time you want to login to wallabag, you'll receive a code by 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)' + # 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. @@ -159,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ığı' @@ -351,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' @@ -402,6 +420,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.

' @@ -527,7 +547,8 @@ user: email_label: 'E-posta' # enabled_label: 'Enabled' # last_login_label: 'Last login' - # twofactor_label: Two factor authentication + # twofactor_email_label: Two factor authentication by email + # twofactor_google_label: Two factor authentication by OTP app # save: Save # delete: Delete # delete_confirm: Are you sure? @@ -545,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 @@ -566,9 +587,11 @@ flashes: entry_starred: 'Makale favorilere eklendi' entry_unstarred: 'Makale favorilerden çıkartıldı' entry_deleted: 'Makale silindi' + # no_random_entry: 'No article with these criterias was found' tag: notice: tag_added: 'Etiket eklendi' + # tag_renamed: 'Tag renamed' import: notice: # failed: 'Import failed, please try again.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.da.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.da.yml index c6a84209..c0438978 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 907b67a5..4c675ef4 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 8cc117fe..89d4c68a 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 97a8edfa..ba34ee76 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 ef677525..9b1a4af2 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 f31b4ed2..92f69aa0 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 d949cc3b..b20d6f51 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 87f00f10..cb57844f 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 e4165c14..94757cc5 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 a8c1f9de..df2f3f35 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 6840cf11..e5c8a72f 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 e1e7317f..881ffd3b 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 bcc57dac..4ef6ab3c 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 @@
@@ -95,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 %}
@@ -139,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 }}

@@ -169,52 +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.twoFactorAuthentication) }} - {{ form_errors(form.user.twoFactorAuthentication) }} - {{ form_widget(form.user.twoFactorAuthentication) }} -
- - live_help - -
- {% endif %} + + + + + + + + -

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

-
-

{{ 'config.reset.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) }} - {{ form_widget(form.user.save) }} {% if enabled_users > 1 %} @@ -277,7 +265,7 @@ {% endfor %} - {{ form_start(form.new_tagging_rule) }} + {{ form_start(form.new_tagging_rule) }} {{ form_errors(form.new_tagging_rule) }}
@@ -382,4 +370,31 @@ + +

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

+
+

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

+ +
{% endblock %} 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 00000000..0919646e --- /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 }}

    + +

    {{ backupCodes|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/baggy/Entry/entries.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/baggy/Entry/entries.html.twig index 832112be..6c5d2601 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 %} @@ -20,13 +20,19 @@ {% block content %} {% set currentRoute = app.request.attributes.get('_route') %} + {% if currentRoute == 'homepage' %} + {% set currentRoute = 'unread' %} + {% endif %} {% set listMode = app.user.config.listMode %}
{{ 'entry.list.number_on_the_page'|transchoice(entries.count) }}
@@ -122,42 +122,42 @@
- {{ 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 }}
-
{{ 'config.form_rss.token_label'|trans }}
+
{{ 'config.form_feed.token_label'|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 %} @@ -165,14 +165,14 @@
- {{ 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_widget(form.rss.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }} - {{ form_rest(form.rss) }} + {{ form_widget(form.feed.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }} + {{ form_rest(form.feed) }}
@@ -196,59 +196,42 @@ - {% if twofactor_auth %} -
-
- {{ 'config.form_user.two_factor_description'|trans }} - -
+ {{ form_widget(form.user.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }} - {{ form_widget(form.user.twoFactorAuthentication) }} - {{ form_label(form.user.twoFactorAuthentication) }} - {{ form_errors(form.user.twoFactorAuthentication) }} -
-
- - live_help - + {% 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.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }} {{ form_widget(form.user._token) }} - -


- - - - {% if enabled_users > 1 %} -


- -
-
{{ 'config.form_user.delete.title'|trans }}
-

{{ 'config.form_user.delete.description'|trans }}

- -
- {% endif %}
@@ -422,6 +405,37 @@
+ +
+ + + {% if enabled_users > 1 %} +


+ +
+
{{ 'config.form_user.delete.title'|trans }}
+

{{ 'config.form_user.delete.description'|trans }}

+ +
+ {% endif %} +
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 00000000..7875d787 --- /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 }}

    + +

    {{ backupCodes|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/Entry/entries.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/entries.html.twig index 742dd330..476d7403 100644 --- a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/entries.html.twig +++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/entries.html.twig @@ -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 %} @@ -21,12 +21,15 @@ {% block content %} {% set listMode = app.user.config.listMode %} {% set currentRoute = app.request.attributes.get('_route') %} + {% if currentRoute == 'homepage' %} + {% set currentRoute = 'unread' %} + {% endif %}
{{ 'entry.list.number_on_the_page'|transchoice(entries.count) }} {% if listMode == 0 %}view_list{% else %}view_module{% endif %} - {% if app.user.config.rssToken %} - {% include "@WallabagCore/themes/common/Entry/_rss_link.html.twig" %} + {% if app.user.config.feedToken %} + {% include "@WallabagCore/themes/common/Entry/_feed_link.html.twig" %} {% endif %}
{% if entries.getNbPages > 1 %} @@ -59,9 +62,6 @@ {% set currentTag = null %} {% if tag is defined %} {% set currentTag = tag.slug %} - {% endif %} - {% if currentRoute == 'homepage' %} - {% set currentRoute = 'unread' %} {% endif %}

{{ 'entry.list.export_title'|trans }}

    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 c15b5146..79907bbb 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,9 +13,20 @@
      {% for tag in tags %}
    • - {{tag.label}} ({{ tag.nbEntries }}) - {% if app.user.config.rssToken %} - rss_feed + + {{ tag.label }} ({{ tag.nbEntries }}) + + {% if renameForms is defined and renameForms[tag.id] is defined %} + + + mode_edit + + {% endif %} + {% if app.user.config.feedToken %} + rss_feed {% endif %}
    • {% endfor %} diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig index 052a8c01..b9c45567 100644 --- a/src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig +++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig @@ -46,6 +46,8 @@ {% set activeRoute = 'starred' %} {% elseif currentRoute == 'unread' or currentRoute == 'homepage' or currentRouteFromQueryParams == 'unread' %} {% set activeRoute = 'unread' %} + {% elseif currentRoute == 'untagged' %} + {% set activeRoute = 'untagged' %} {% endif %}
    • @@ -113,6 +115,13 @@ search
    • + {% if activeRoute %} +
    • + + casino + +
    • + {% endif %}
    • filter_list @@ -125,7 +134,7 @@
- {{ render(controller("WallabagCoreBundle:Entry:searchForm", {'currentRoute': app.request.attributes.get('_route')})) }} + {{ render(controller("WallabagCoreBundle:Entry:searchForm", {'currentRoute': currentRoute})) }} {{ render(controller("WallabagCoreBundle:Entry:addEntryForm")) }} diff --git a/src/Wallabag/CoreBundle/Tools/Utils.php b/src/Wallabag/CoreBundle/Tools/Utils.php index e56e251e..b7ad7966 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 00b1e595..536185d4 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/ImportBundle/Controller/BrowserController.php b/src/Wallabag/ImportBundle/Controller/BrowserController.php index 6418925c..58d2a730 100644 --- a/src/Wallabag/ImportBundle/Controller/BrowserController.php +++ b/src/Wallabag/ImportBundle/Controller/BrowserController.php @@ -2,10 +2,10 @@ namespace Wallabag\ImportBundle\Controller; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; use Wallabag\ImportBundle\Form\Type\UploadImportType; abstract class BrowserController extends Controller diff --git a/src/Wallabag/ImportBundle/Controller/ChromeController.php b/src/Wallabag/ImportBundle/Controller/ChromeController.php index 0cb418a1..6628cdb0 100644 --- a/src/Wallabag/ImportBundle/Controller/ChromeController.php +++ b/src/Wallabag/ImportBundle/Controller/ChromeController.php @@ -2,8 +2,8 @@ namespace Wallabag\ImportBundle\Controller; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; class ChromeController extends BrowserController { diff --git a/src/Wallabag/ImportBundle/Controller/FirefoxController.php b/src/Wallabag/ImportBundle/Controller/FirefoxController.php index 88697f9d..dce8455f 100644 --- a/src/Wallabag/ImportBundle/Controller/FirefoxController.php +++ b/src/Wallabag/ImportBundle/Controller/FirefoxController.php @@ -2,8 +2,8 @@ namespace Wallabag\ImportBundle\Controller; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; class FirefoxController extends BrowserController { diff --git a/src/Wallabag/ImportBundle/Controller/ImportController.php b/src/Wallabag/ImportBundle/Controller/ImportController.php index 7e4fd174..fbd7434e 100644 --- a/src/Wallabag/ImportBundle/Controller/ImportController.php +++ b/src/Wallabag/ImportBundle/Controller/ImportController.php @@ -2,8 +2,8 @@ namespace Wallabag\ImportBundle\Controller; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\Routing\Annotation\Route; class ImportController extends Controller { diff --git a/src/Wallabag/ImportBundle/Controller/InstapaperController.php b/src/Wallabag/ImportBundle/Controller/InstapaperController.php index f184baf9..faed3b72 100644 --- a/src/Wallabag/ImportBundle/Controller/InstapaperController.php +++ b/src/Wallabag/ImportBundle/Controller/InstapaperController.php @@ -2,9 +2,9 @@ namespace Wallabag\ImportBundle\Controller; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; use Wallabag\ImportBundle\Form\Type\UploadImportType; class InstapaperController extends Controller diff --git a/src/Wallabag/ImportBundle/Controller/PinboardController.php b/src/Wallabag/ImportBundle/Controller/PinboardController.php index 6f54c69a..cc6fae79 100644 --- a/src/Wallabag/ImportBundle/Controller/PinboardController.php +++ b/src/Wallabag/ImportBundle/Controller/PinboardController.php @@ -2,9 +2,9 @@ namespace Wallabag\ImportBundle\Controller; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; use Wallabag\ImportBundle\Form\Type\UploadImportType; class PinboardController extends Controller diff --git a/src/Wallabag/ImportBundle/Controller/PocketController.php b/src/Wallabag/ImportBundle/Controller/PocketController.php index 9f28819a..71ceb427 100644 --- a/src/Wallabag/ImportBundle/Controller/PocketController.php +++ b/src/Wallabag/ImportBundle/Controller/PocketController.php @@ -2,10 +2,10 @@ namespace Wallabag\ImportBundle\Controller; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; class PocketController extends Controller diff --git a/src/Wallabag/ImportBundle/Controller/ReadabilityController.php b/src/Wallabag/ImportBundle/Controller/ReadabilityController.php index 729a97a3..b120ef96 100644 --- a/src/Wallabag/ImportBundle/Controller/ReadabilityController.php +++ b/src/Wallabag/ImportBundle/Controller/ReadabilityController.php @@ -2,9 +2,9 @@ namespace Wallabag\ImportBundle\Controller; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; use Wallabag\ImportBundle\Form\Type\UploadImportType; class ReadabilityController extends Controller diff --git a/src/Wallabag/ImportBundle/Controller/WallabagV1Controller.php b/src/Wallabag/ImportBundle/Controller/WallabagV1Controller.php index d700d8a8..e1c35343 100644 --- a/src/Wallabag/ImportBundle/Controller/WallabagV1Controller.php +++ b/src/Wallabag/ImportBundle/Controller/WallabagV1Controller.php @@ -2,8 +2,8 @@ namespace Wallabag\ImportBundle\Controller; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; class WallabagV1Controller extends WallabagController { diff --git a/src/Wallabag/ImportBundle/Controller/WallabagV2Controller.php b/src/Wallabag/ImportBundle/Controller/WallabagV2Controller.php index ab26400c..c4116c1d 100644 --- a/src/Wallabag/ImportBundle/Controller/WallabagV2Controller.php +++ b/src/Wallabag/ImportBundle/Controller/WallabagV2Controller.php @@ -2,8 +2,8 @@ namespace Wallabag\ImportBundle\Controller; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; class WallabagV2Controller extends WallabagController { diff --git a/src/Wallabag/ImportBundle/Import/BrowserImport.php b/src/Wallabag/ImportBundle/Import/BrowserImport.php index 811e3fb8..3987e80f 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 5a18c7c0..f7bee9ef 100644 --- a/src/Wallabag/ImportBundle/Import/InstapaperImport.php +++ b/src/Wallabag/ImportBundle/Import/InstapaperImport.php @@ -62,7 +62,7 @@ class InstapaperImport extends AbstractImport } $entries = []; - $handle = fopen($this->filepath, 'rb'); + $handle = fopen($this->filepath, 'r'); while (false !== ($data = fgetcsv($handle, 10240))) { if ('URL' === $data[0]) { continue; @@ -79,7 +79,6 @@ class InstapaperImport extends AbstractImport $entries[] = [ 'url' => $data[0], 'title' => $data[1], - 'status' => $data[3], 'is_archived' => 'Archive' === $data[3] || 'Starred' === $data[3], 'is_starred' => 'Starred' === $data[3], 'html' => false, @@ -94,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); @@ -147,7 +150,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 995d1f2c..202eb1b3 100644 --- a/src/Wallabag/ImportBundle/Import/PinboardImport.php +++ b/src/Wallabag/ImportBundle/Import/PinboardImport.php @@ -131,7 +131,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 5737928d..a39d8156 100644 --- a/src/Wallabag/ImportBundle/Import/PocketImport.php +++ b/src/Wallabag/ImportBundle/Import/PocketImport.php @@ -206,7 +206,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 === (int) $importedEntry['status'] || $this->markAsRead); + $entry->updateArchived(1 === (int) $importedEntry['status'] || $this->markAsRead); // 0 or 1 - 1 if the item is starred $entry->setStarred(1 === (int) $importedEntry['favorite']); diff --git a/src/Wallabag/ImportBundle/Import/ReadabilityImport.php b/src/Wallabag/ImportBundle/Import/ReadabilityImport.php index a5f3798e..c5abf189 100644 --- a/src/Wallabag/ImportBundle/Import/ReadabilityImport.php +++ b/src/Wallabag/ImportBundle/Import/ReadabilityImport.php @@ -123,7 +123,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 58b6a970..75a28fbf 100644 --- a/src/Wallabag/ImportBundle/Import/WallabagImport.php +++ b/src/Wallabag/ImportBundle/Import/WallabagImport.php @@ -134,7 +134,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/src/Wallabag/ImportBundle/Resources/config/services.yml b/src/Wallabag/ImportBundle/Resources/config/services.yml index b224a6a2..2dd7dff8 100644 --- a/src/Wallabag/ImportBundle/Resources/config/services.yml +++ b/src/Wallabag/ImportBundle/Resources/config/services.yml @@ -112,3 +112,11 @@ services: - [ setLogger, [ "@logger" ]] tags: - { name: wallabag_import.import, alias: chrome } + + wallabag_import.command.import: + class: Wallabag\ImportBundle\Command\ImportCommand + tags: ['console.command'] + + wallabag_import.command.redis_worker: + class: Wallabag\ImportBundle\Command\RedisWorkerCommand + tags: ['console.command'] diff --git a/src/Wallabag/UserBundle/Controller/ManageController.php b/src/Wallabag/UserBundle/Controller/ManageController.php index f3de656f..63a06206 100644 --- a/src/Wallabag/UserBundle/Controller/ManageController.php +++ b/src/Wallabag/UserBundle/Controller/ManageController.php @@ -7,10 +7,9 @@ use FOS\UserBundle\FOSUserEvents; use Pagerfanta\Adapter\DoctrineORMAdapter; use Pagerfanta\Exception\OutOfRangeCurrentPageException; use Pagerfanta\Pagerfanta; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; use Wallabag\UserBundle\Entity\User; use Wallabag\UserBundle\Form\SearchUserType; @@ -22,8 +21,7 @@ class ManageController extends Controller /** * Creates a new User entity. * - * @Route("/new", name="user_new") - * @Method({"GET", "POST"}) + * @Route("/new", name="user_new", methods={"GET", "POST"}) */ public function newAction(Request $request) { @@ -60,19 +58,33 @@ class ManageController extends Controller /** * Displays a form to edit an existing User entity. * - * @Route("/{id}/edit", name="user_edit") - * @Method({"GET", "POST"}) + * @Route("/{id}/edit", name="user_edit", methods={"GET", "POST"}) */ public function editAction(Request $request, User $user) { + $userManager = $this->container->get('fos_user.user_manager'); + $deleteForm = $this->createDeleteForm($user); - $editForm = $this->createForm('Wallabag\UserBundle\Form\UserType', $user); - $editForm->handleRequest($request); + $form = $this->createForm('Wallabag\UserBundle\Form\UserType', $user); + $form->handleRequest($request); - if ($editForm->isSubmitted() && $editForm->isValid()) { - $em = $this->getDoctrine()->getManager(); - $em->persist($user); - $em->flush(); + // `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()) { + // 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( 'notice', @@ -84,7 +96,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'), ]); @@ -93,8 +105,7 @@ class ManageController extends Controller /** * Deletes a User entity. * - * @Route("/{id}", name="user_delete") - * @Method("DELETE") + * @Route("/{id}", name="user_delete", methods={"DELETE"}) */ public function deleteAction(Request $request, User $user) { @@ -135,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); @@ -161,7 +170,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 * diff --git a/src/Wallabag/UserBundle/DataFixtures/ORM/LoadUserData.php b/src/Wallabag/UserBundle/DataFixtures/UserFixtures.php similarity index 79% rename from src/Wallabag/UserBundle/DataFixtures/ORM/LoadUserData.php rename to src/Wallabag/UserBundle/DataFixtures/UserFixtures.php index 26dbda3b..1e375e09 100644 --- a/src/Wallabag/UserBundle/DataFixtures/ORM/LoadUserData.php +++ b/src/Wallabag/UserBundle/DataFixtures/UserFixtures.php @@ -1,13 +1,12 @@ flush(); } - - /** - * {@inheritdoc} - */ - public function getOrder() - { - return 10; - } } diff --git a/src/Wallabag/UserBundle/Entity/User.php b/src/Wallabag/UserBundle/Entity/User.php index 48446e3c..43fa6a80 100644 --- a/src/Wallabag/UserBundle/Entity/User.php +++ b/src/Wallabag/UserBundle/Entity/User.php @@ -8,8 +8,9 @@ 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\BackupCodeInterface; +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 +29,7 @@ use Wallabag\CoreBundle\Helper\EntityTimestampsTrait; * @UniqueEntity("email") * @UniqueEntity("username") */ -class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterface +class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorInterface, BackupCodeInterface { use EntityTimestampsTrait; @@ -123,16 +124,21 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf private $authCode; /** - * @var bool - * - * @ORM\Column(type="boolean") + * @ORM\Column(name="googleAuthenticatorSecret", type="string", nullable=true) */ - private $twoFactorAuthentication = false; + private $googleAuthenticatorSecret; /** * @ORM\Column(type="json_array", nullable=true) */ - private $trusted; + private $backupCodes; + + /** + * @var bool + * + * @ORM\Column(type="boolean") + */ + private $emailTwoFactor = false; public function __construct() { @@ -233,49 +239,119 @@ 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->isGoogleAuthenticatorEnabled(); + } + + /** + * {@inheritdoc} + */ + public function isEmailAuthEnabled(): bool { - return $this->twoFactorAuthentication; + return $this->emailTwoFactor; } - public function getEmailAuthCode() + /** + * {@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; + } + + public function setBackupCodes(array $codes = null) + { + $this->backupCodes = $codes; + } + + public function getBackupCodes() + { + return $this->backupCodes; + } + + /** + * {@inheritdoc} + */ + public function isBackupCode(string $code): bool + { + return false === $this->findBackupCode($code) ? false : true; + } + + /** + * {@inheritdoc} + */ + public function invalidateBackupCode(string $code): void + { + $key = $this->findBackupCode($code); + + if (false !== $key) { + unset($this->backupCodes[$key]); + } } /** @@ -309,4 +385,24 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf 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; + } } diff --git a/src/Wallabag/UserBundle/EventListener/CreateConfigListener.php b/src/Wallabag/UserBundle/EventListener/CreateConfigListener.php index e4d55c19..81954213 100644 --- a/src/Wallabag/UserBundle/EventListener/CreateConfigListener.php +++ b/src/Wallabag/UserBundle/EventListener/CreateConfigListener.php @@ -6,6 +6,7 @@ use Doctrine\ORM\EntityManager; use FOS\UserBundle\Event\UserEvent; use FOS\UserBundle\FOSUserEvents; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Session\Session; use Wallabag\CoreBundle\Entity\Config; /** @@ -17,22 +18,24 @@ 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) + 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; $this->listMode = $listMode; + $this->session = $session; } public static function getSubscribedEvents() @@ -51,8 +54,8 @@ class CreateConfigListener implements EventSubscriberInterface $config = new Config($event->getUser()); $config->setTheme($this->theme); $config->setItemsPerPage($this->itemsOnPage); - $config->setRssLimit($this->rssLimit); - $config->setLanguage($this->language); + $config->setFeedLimit($this->feedLimit); + $config->setLanguage($this->session->get('_locale', $this->language)); $config->setReadingSpeed($this->readingSpeed); $config->setActionMarkAsRead($this->actionMarkAsRead); $config->setListMode($this->listMode); diff --git a/src/Wallabag/UserBundle/Form/UserType.php b/src/Wallabag/UserBundle/Form/UserType.php index 56fea640..026db9a2 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 aed805c9..2797efde 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'); @@ -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') diff --git a/src/Wallabag/UserBundle/Repository/UserRepository.php b/src/Wallabag/UserBundle/Repository/UserRepository.php index be693d3b..4abd55f1 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 */ - 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 d3925de3..2dcf3011 100644 --- a/src/Wallabag/UserBundle/Resources/config/services.yml +++ b/src/Wallabag/UserBundle/Resources/config/services.yml @@ -28,11 +28,12 @@ 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%" - "%wallabag_core.list_mode%" + - "@session" tags: - { name: kernel.event_subscriber } diff --git a/src/Wallabag/UserBundle/Resources/views/Authentication/form.html.twig b/src/Wallabag/UserBundle/Resources/views/Authentication/form.html.twig index c8471bdd..47a5cb78 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 3ffd15f5..2de8f3a5 100644 --- a/src/Wallabag/UserBundle/Resources/views/Manage/edit.html.twig +++ b/src/Wallabag/UserBundle/Resources/views/Manage/edit.html.twig @@ -50,9 +50,14 @@ {% 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) }}
{% endif %} diff --git a/src/Wallabag/UserBundle/Resources/views/Registration/register_content.html.twig b/src/Wallabag/UserBundle/Resources/views/Registration/register_content.html.twig index d0a85fc7..85cd4f0d 100644 --- a/src/Wallabag/UserBundle/Resources/views/Registration/register_content.html.twig +++ b/src/Wallabag/UserBundle/Resources/views/Registration/register_content.html.twig @@ -3,7 +3,6 @@ {{ form_start(form, {'method': 'post', 'action': path('fos_user_registration_register'), 'attr': {'class': 'fos_user_registration_register'}}) }}
{% endblock %} diff --git a/tests/Wallabag/AnnotationBundle/Controller/AnnotationControllerTest.php b/tests/Wallabag/AnnotationBundle/Controller/AnnotationControllerTest.php index 96474468..2c46e0a1 100644 --- a/tests/Wallabag/AnnotationBundle/Controller/AnnotationControllerTest.php +++ b/tests/Wallabag/AnnotationBundle/Controller/AnnotationControllerTest.php @@ -1,6 +1,6 @@ assertSame('my quote', $content['quote']); /** @var Annotation $annotation */ - $annotation = $this->client->getContainer() - ->get('doctrine.orm.entity_manager') + $annotation = $em ->getRepository('WallabagAnnotationBundle:Annotation') ->findLastAnnotationByPageId($entry->getId(), 1); diff --git a/tests/Wallabag/AnnotationBundle/WallabagAnnotationTestCase.php b/tests/Wallabag/AnnotationBundle/WallabagAnnotationTestCase.php index 105e8add..9c7aba6b 100644 --- a/tests/Wallabag/AnnotationBundle/WallabagAnnotationTestCase.php +++ b/tests/Wallabag/AnnotationBundle/WallabagAnnotationTestCase.php @@ -43,9 +43,9 @@ abstract class WallabagAnnotationTestCase extends WebTestCase $container = $client->getContainer(); /** @var $userManager \FOS\UserBundle\Doctrine\UserManager */ - $userManager = $container->get('fos_user.user_manager'); + $userManager = $container->get('fos_user.user_manager.test'); /** @var $loginManager \FOS\UserBundle\Security\LoginManager */ - $loginManager = $container->get('fos_user.security.login_manager'); + $loginManager = $container->get('fos_user.security.login_manager.test'); $firewallName = $container->getParameter('fos_user.firewall_name'); $this->user = $userManager->findUserBy(['username' => 'admin']); diff --git a/tests/Wallabag/ApiBundle/Controller/DeveloperControllerTest.php b/tests/Wallabag/ApiBundle/Controller/DeveloperControllerTest.php index e1a0ac7e..5586c70d 100644 --- a/tests/Wallabag/ApiBundle/Controller/DeveloperControllerTest.php +++ b/tests/Wallabag/ApiBundle/Controller/DeveloperControllerTest.php @@ -135,7 +135,7 @@ class DeveloperControllerTest extends WallabagCoreTestCase { $client = $this->getClient(); $em = $client->getContainer()->get('doctrine.orm.entity_manager'); - $userManager = $client->getContainer()->get('fos_user.user_manager'); + $userManager = $client->getContainer()->get('fos_user.user_manager.test'); $user = $userManager->findUserBy(['username' => $username]); $apiClient = new Client($user); $apiClient->setName('My app'); diff --git a/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php b/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php index 46b5f460..8b7898ee 100644 --- a/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php +++ b/tests/Wallabag/ApiBundle/Controller/EntryRestControllerTest.php @@ -15,7 +15,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $entry = $this->client->getContainer() ->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findOneBy(['user' => 1, 'isArchived' => false]); + ->findOneBy(['user' => $this->getUserId(), 'isArchived' => false]); if (!$entry) { $this->markTestSkipped('No content found in db.'); @@ -41,7 +41,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $entry = $this->client->getContainer() ->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findOneBy(['user' => 1, 'url' => 'http://0.0.0.0/entry2']); + ->findOneBy(['user' => $this->getUserId(), 'url' => 'http://0.0.0.0/entry2']); if (!$entry) { $this->markTestSkipped('No content found in db.'); @@ -60,7 +60,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $entry = $this->client->getContainer() ->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findOneBy(['user' => 1, 'isArchived' => false]); + ->findOneBy(['user' => $this->getUserId(), 'isArchived' => false]); if (!$entry) { $this->markTestSkipped('No content found in db.'); @@ -108,7 +108,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $entry = $this->client->getContainer() ->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findOneBy(['user' => 2, 'isArchived' => false]); + ->findOneBy(['user' => $this->getUserId('bob'), 'isArchived' => false]); if (!$entry) { $this->markTestSkipped('No content found in db.'); @@ -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')); } @@ -185,7 +206,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $entry = $this->client->getContainer() ->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findOneByUser(1); + ->findOneByUser($this->getUserId()); if (!$entry) { $this->markTestSkipped('No content found in db.'); @@ -489,8 +510,9 @@ 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->assertSame($this->getUserId(), $content['user_id']); $this->assertCount(2, $content['tags']); $this->assertNull($content['origin_url']); $this->assertSame('my content', $content['content']); @@ -505,7 +527,7 @@ class EntryRestControllerTest extends WallabagApiTestCase public function testPostSameEntry() { $em = $this->client->getContainer()->get('doctrine.orm.entity_manager'); - $entry = new Entry($em->getReference(User::class, 1)); + $entry = new Entry($em->getReference(User::class, $this->getUserId())); $entry->setUrl('https://www.lemonde.fr/pixels/article/2015/03/28/plongee-dans-l-univers-d-ingress-le-jeu-de-google-aux-frontieres-du-reel_4601155_4408996.html'); $entry->setArchived(true); $entry->addTag((new Tag())->setLabel('google')); @@ -584,7 +606,8 @@ 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->assertSame(1, $content['user_id']); + $this->assertGreaterThanOrEqual($now->getTimestamp(), (new \DateTime($content['archived_at']))->getTimestamp()); + $this->assertSame($this->getUserId(), $content['user_id']); } public function testPostArchivedAndStarredEntryWithoutQuotes() @@ -633,7 +656,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $entry = $this->client->getContainer() ->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findOneByUser(1); + ->findOneByUser($this->getUserId()); if (!$entry) { $this->markTestSkipped('No content found in db.'); @@ -660,7 +683,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $this->assertSame($entry->getUrl(), $content['url']); $this->assertSame('New awesome title', $content['title']); $this->assertGreaterThanOrEqual(1, \count($content['tags']), 'We force only one tag'); - $this->assertSame(1, $content['user_id']); + $this->assertSame($this->getUserId(), $content['user_id']); $this->assertSame('de_AT', $content['language']); $this->assertSame('http://preview.io/picture.jpg', $content['preview_picture']); $this->assertContains('sponge', $content['published_by']); @@ -675,7 +698,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $entry = $this->client->getContainer() ->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findOneByUser(1); + ->findOneByUser($this->getUserId()); if (!$entry) { $this->markTestSkipped('No content found in db.'); @@ -709,7 +732,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $entry = $this->client->getContainer() ->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findOneByUser(1); + ->findOneByUser($this->getUserId()); if (!$entry) { $this->markTestSkipped('No content found in db.'); @@ -740,7 +763,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $entry = $this->client->getContainer() ->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findOneByUser(1); + ->findOneByUser($this->getUserId()); if (!$entry) { $this->markTestSkipped('No content found in db.'); @@ -772,7 +795,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $entry = $this->client->getContainer() ->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findOneByUser(1); + ->findOneByUser($this->getUserId()); if (!$entry) { $this->markTestSkipped('No content found in db.'); @@ -817,7 +840,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $entry = $this->client->getContainer() ->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findOneByUser(1); + ->findOneByUser($this->getUserId()); if (!$entry) { $this->markTestSkipped('No content found in db.'); @@ -834,7 +857,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $content = json_decode($this->client->getResponse()->getContent(), true); $this->assertArrayHasKey('tags', $content); - $this->assertSame($nbTags + 3, \count($content['tags'])); + $this->assertCount($nbTags + 3, $content['tags']); $entryDB = $this->client->getContainer() ->get('doctrine.orm.entity_manager') @@ -874,7 +897,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $content = json_decode($this->client->getResponse()->getContent(), true); $this->assertArrayHasKey('tags', $content); - $this->assertSame($nbTags - 1, \count($content['tags'])); + $this->assertCount($nbTags - 1, $content['tags']); } public function testSaveIsArchivedAfterPost() @@ -882,7 +905,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $entry = $this->client->getContainer() ->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findOneBy(['user' => 1, 'isArchived' => true]); + ->findOneBy(['user' => $this->getUserId(), 'isArchived' => true]); if (!$entry) { $this->markTestSkipped('No content found in db.'); @@ -904,7 +927,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $entry = $this->client->getContainer() ->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findOneBy(['user' => 1, 'isStarred' => true]); + ->findOneBy(['user' => $this->getUserId(), 'isStarred' => true]); if (!$entry) { $this->markTestSkipped('No content found in db.'); @@ -926,7 +949,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $entry = $this->client->getContainer() ->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findOneBy(['user' => 1, 'isArchived' => true]); + ->findOneBy(['user' => $this->getUserId(), 'isArchived' => true]); if (!$entry) { $this->markTestSkipped('No content found in db.'); @@ -952,7 +975,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $entry = $this->client->getContainer() ->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findOneBy(['user' => 1, 'isStarred' => true]); + ->findOneBy(['user' => $this->getUserId(), 'isStarred' => true]); if (!$entry) { $this->markTestSkipped('No content found in db.'); @@ -971,6 +994,8 @@ class EntryRestControllerTest extends WallabagApiTestCase public function dataForEntriesExistWithUrl() { + $url = hash('sha1', 'http://0.0.0.0/entry2'); + return [ 'with_id' => [ 'url' => '/api/entries/exists?url=http://0.0.0.0/entry2&return_id=1', @@ -980,6 +1005,14 @@ class EntryRestControllerTest extends WallabagApiTestCase '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, + ], ]; } @@ -1001,6 +1034,7 @@ 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?urls[]=' . $url1 . '&urls[]=' . $url2 . '&return_id=1'); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); @@ -1009,7 +1043,8 @@ class EntryRestControllerTest extends WallabagApiTestCase $this->assertArrayHasKey($url1, $content); $this->assertArrayHasKey($url2, $content); - $this->assertSame(2, $content[$url1]); + // 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[$url1]); $this->assertNull($content[$url2]); } @@ -1029,6 +1064,38 @@ 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?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('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('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('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?url=http://google.com/entry2'); @@ -1040,6 +1107,17 @@ class EntryRestControllerTest extends WallabagApiTestCase $this->assertFalse($content['exists']); } + public function testGetEntriesExistsWhichDoesNotExistsWithHashedUrl() + { + $this->client->request('GET', '/api/entries/exists?hashed_url=' . hash('sha1', '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 testGetEntriesExistsWithNoUrl() { $this->client->request('GET', '/api/entries/exists?url='); @@ -1047,11 +1125,18 @@ class EntryRestControllerTest extends WallabagApiTestCase $this->assertSame(403, $this->client->getResponse()->getStatusCode()); } + public function testGetEntriesExistsWithNoHashedUrl() + { + $this->client->request('GET', '/api/entries/exists?hashed_url='); + + $this->assertSame(403, $this->client->getResponse()->getStatusCode()); + } + public function testReloadEntryErrorWhileFetching() { $entry = $this->client->getContainer()->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findByUrlAndUserId('http://0.0.0.0/entry4', 1); + ->findByUrlAndUserId('http://0.0.0.0/entry4', $this->getUserId()); if (!$entry) { $this->markTestSkipped('No content found in db.'); @@ -1087,7 +1172,7 @@ class EntryRestControllerTest extends WallabagApiTestCase { $entry = $this->client->getContainer()->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findByUrlAndUserId('http://0.0.0.0/entry4', 1); + ->findByUrlAndUserId('http://0.0.0.0/entry4', $this->getUserId()); $tags = $entry->getTags(); @@ -1111,7 +1196,7 @@ class EntryRestControllerTest extends WallabagApiTestCase $entry = $this->client->getContainer()->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findByUrlAndUserId('http://0.0.0.0/entry4', 1); + ->findByUrlAndUserId('http://0.0.0.0/entry4', $this->getUserId()); $tags = $entry->getTags(); $this->assertCount(4, $tags); @@ -1131,7 +1216,7 @@ class EntryRestControllerTest extends WallabagApiTestCase public function testDeleteEntriesTagsListAction() { $em = $this->client->getContainer()->get('doctrine.orm.entity_manager'); - $entry = new Entry($em->getReference(User::class, 1)); + $entry = new Entry($em->getReference(User::class, $this->getUserId())); $entry->setUrl('http://0.0.0.0/test-entry'); $entry->addTag((new Tag())->setLabel('foo-tag')); $entry->addTag((new Tag())->setLabel('bar-tag')); @@ -1199,7 +1284,7 @@ class EntryRestControllerTest extends WallabagApiTestCase public function testDeleteEntriesListAction() { $em = $this->client->getContainer()->get('doctrine.orm.entity_manager'); - $em->persist((new Entry($em->getReference(User::class, 1)))->setUrl('http://0.0.0.0/test-entry1')); + $em->persist((new Entry($em->getReference(User::class, $this->getUserId())))->setUrl('http://0.0.0.0/test-entry1')); $em->flush(); $em->clear(); @@ -1257,7 +1342,7 @@ class EntryRestControllerTest extends WallabagApiTestCase public function testRePostEntryAndReUsePublishedAt() { $em = $this->client->getContainer()->get('doctrine.orm.entity_manager'); - $entry = new Entry($em->getReference(User::class, 1)); + $entry = new Entry($em->getReference(User::class, $this->getUserId())); $entry->setTitle('Antoine de Caunes : « Je veux avoir le droit de tâtonner »'); $entry->setContent('hihi'); $entry->setUrl('https://www.lemonde.fr/m-perso/article/2017/06/25/antoine-de-caunes-je-veux-avoir-le-droit-de-tatonner_5150728_4497916.html'); diff --git a/tests/Wallabag/ApiBundle/Controller/SearchRestControllerTest.php b/tests/Wallabag/ApiBundle/Controller/SearchRestControllerTest.php new file mode 100644 index 00000000..fd524639 --- /dev/null +++ b/tests/Wallabag/ApiBundle/Controller/SearchRestControllerTest.php @@ -0,0 +1,69 @@ +client->request('GET', '/api/search', [ + 'page' => 1, + 'perPage' => 2, + 'term' => 'entry', // 6 results + ]); + + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + + $content = json_decode($this->client->getResponse()->getContent(), true); + + $this->assertGreaterThanOrEqual(1, \count($content)); + $this->assertArrayHasKey('items', $content['_embedded']); + $this->assertGreaterThanOrEqual(0, $content['total']); + $this->assertSame(1, $content['page']); + $this->assertSame(2, $content['limit']); + $this->assertGreaterThanOrEqual(1, $content['pages']); + + $this->assertArrayHasKey('_links', $content); + $this->assertArrayHasKey('self', $content['_links']); + $this->assertArrayHasKey('first', $content['_links']); + $this->assertArrayHasKey('last', $content['_links']); + + foreach (['self', 'first', 'last'] as $link) { + $this->assertArrayHasKey('href', $content['_links'][$link]); + $this->assertContains('term=entry', $content['_links'][$link]['href']); + } + + $this->assertSame('application/json', $this->client->getResponse()->headers->get('Content-Type')); + } + + public function testGetSearchWithNoLimit() + { + $this->client->request('GET', '/api/search', [ + 'term' => 'entry', + ]); + + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + + $content = json_decode($this->client->getResponse()->getContent(), true); + + $this->assertGreaterThanOrEqual(1, \count($content)); + $this->assertArrayHasKey('items', $content['_embedded']); + $this->assertGreaterThanOrEqual(0, $content['total']); + $this->assertSame(1, $content['page']); + $this->assertGreaterThanOrEqual(1, $content['pages']); + + $this->assertArrayHasKey('_links', $content); + $this->assertArrayHasKey('self', $content['_links']); + $this->assertArrayHasKey('first', $content['_links']); + $this->assertArrayHasKey('last', $content['_links']); + + foreach (['self', 'first', 'last'] as $link) { + $this->assertArrayHasKey('href', $content['_links'][$link]); + $this->assertContains('term=entry', $content['_links'][$link]['href']); + } + + $this->assertSame('application/json', $this->client->getResponse()->headers->get('Content-Type')); + } +} diff --git a/tests/Wallabag/ApiBundle/Controller/WallabagRestControllerTest.php b/tests/Wallabag/ApiBundle/Controller/WallabagRestControllerTest.php index ac4d6cdc..8b49c0ae 100644 --- a/tests/Wallabag/ApiBundle/Controller/WallabagRestControllerTest.php +++ b/tests/Wallabag/ApiBundle/Controller/WallabagRestControllerTest.php @@ -18,4 +18,21 @@ class WallabagRestControllerTest extends WallabagApiTestCase $this->assertSame($client->getContainer()->getParameter('wallabag_core.version'), $content); } + + public function testGetInfo() + { + // create a new client instead of using $this->client to be sure client isn't authenticated + $client = static::createClient(); + $client->request('GET', '/api/info'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $content = json_decode($client->getResponse()->getContent(), true); + + $this->assertArrayHasKey('appname', $content); + $this->assertArrayHasKey('version', $content); + $this->assertArrayHasKey('allowed_registration', $content); + + $this->assertSame('wallabag', $content['appname']); + } } diff --git a/tests/Wallabag/ApiBundle/WallabagApiTestCase.php b/tests/Wallabag/ApiBundle/WallabagApiTestCase.php index 8a188e1c..fd2e113e 100644 --- a/tests/Wallabag/ApiBundle/WallabagApiTestCase.php +++ b/tests/Wallabag/ApiBundle/WallabagApiTestCase.php @@ -31,9 +31,9 @@ abstract class WallabagApiTestCase extends WebTestCase $container = $client->getContainer(); /** @var $userManager \FOS\UserBundle\Doctrine\UserManager */ - $userManager = $container->get('fos_user.user_manager'); + $userManager = $container->get('fos_user.user_manager.test'); /** @var $loginManager \FOS\UserBundle\Security\LoginManager */ - $loginManager = $container->get('fos_user.security.login_manager'); + $loginManager = $container->get('fos_user.security.login_manager.test'); $firewallName = $container->getParameter('fos_user.firewall_name'); $this->user = $userManager->findUserBy(['username' => 'admin']); @@ -48,4 +48,23 @@ abstract class WallabagApiTestCase extends WebTestCase return $client; } + + /** + * Return the ID for the user admin. + * Used because on heavy testing we don't want to re-create the database on each run. + * Which means "admin" user won't have id 1 all the time. + * + * @param string $username + * + * @return int + */ + protected function getUserId($username = 'admin') + { + return $this->client + ->getContainer() + ->get('doctrine.orm.entity_manager') + ->getRepository('WallabagUserBundle:User') + ->findOneByUserName($username) + ->getId(); + } } diff --git a/tests/Wallabag/CoreBundle/Command/GenerateUrlHashesCommandTest.php b/tests/Wallabag/CoreBundle/Command/GenerateUrlHashesCommandTest.php new file mode 100644 index 00000000..17eed210 --- /dev/null +++ b/tests/Wallabag/CoreBundle/Command/GenerateUrlHashesCommandTest.php @@ -0,0 +1,98 @@ +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 "3" users', $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(); + + $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->assertSame($entry->getHashedUrl(), hash('sha1', $url)); + + $query = $em->createQuery('DELETE FROM Wallabag\CoreBundle\Entity\Entry e WHERE e.url = :url'); + $query->setParameter('url', $url); + $query->execute(); + } +} diff --git a/tests/Wallabag/CoreBundle/Command/InstallCommandTest.php b/tests/Wallabag/CoreBundle/Command/InstallCommandTest.php index bd351b18..d8928451 100644 --- a/tests/Wallabag/CoreBundle/Command/InstallCommandTest.php +++ b/tests/Wallabag/CoreBundle/Command/InstallCommandTest.php @@ -18,6 +18,18 @@ use Wallabag\CoreBundle\Command\InstallCommand; class InstallCommandTest extends WallabagCoreTestCase { + public static function setUpBeforeClass() + { + // disable doctrine-test-bundle + StaticDriver::setKeepStaticConnections(false); + } + + public static function tearDownAfterClass() + { + // enable doctrine-test-bundle + StaticDriver::setKeepStaticConnections(true); + } + public function setUp() { parent::setUp(); @@ -51,9 +63,6 @@ class InstallCommandTest extends WallabagCoreTestCase parent::setUp(); } - // disable doctrine-test-bundle - StaticDriver::setKeepStaticConnections(false); - $this->resetDatabase($this->getClient()); } @@ -62,6 +71,7 @@ class InstallCommandTest extends WallabagCoreTestCase $databasePath = getenv('TEST_DATABASE_PATH'); // Remove variable environnement putenv('TEST_DATABASE_PATH'); + if ($databasePath && file_exists($databasePath)) { unlink($databasePath); } else { @@ -71,8 +81,6 @@ class InstallCommandTest extends WallabagCoreTestCase $this->resetDatabase($client); } - // enable doctrine-test-bundle - StaticDriver::setKeepStaticConnections(true); parent::tearDown(); } diff --git a/tests/Wallabag/CoreBundle/Command/ReloadEntryCommandTest.php b/tests/Wallabag/CoreBundle/Command/ReloadEntryCommandTest.php index b13f6519..c4bd6dac 100644 --- a/tests/Wallabag/CoreBundle/Command/ReloadEntryCommandTest.php +++ b/tests/Wallabag/CoreBundle/Command/ReloadEntryCommandTest.php @@ -26,7 +26,7 @@ class ReloadEntryCommandTest extends WallabagCoreTestCase { parent::setUp(); - $userRepository = $this->getClient()->getContainer()->get('wallabag_user.user_repository'); + $userRepository = $this->getClient()->getContainer()->get('wallabag_user.user_repository.test'); $user = $userRepository->findOneByUserName('admin'); $this->adminEntry = new Entry($user); @@ -60,7 +60,7 @@ class ReloadEntryCommandTest extends WallabagCoreTestCase $reloadedEntries = $this->getClient() ->getContainer() - ->get('wallabag_core.entry_repository') + ->get('wallabag_core.entry_repository.test') ->findById([$this->adminEntry->getId(), $this->bobEntry->getId()]); foreach ($reloadedEntries as $reloadedEntry) { @@ -84,7 +84,7 @@ class ReloadEntryCommandTest extends WallabagCoreTestCase 'interactive' => false, ]); - $entryRepository = $this->getClient()->getContainer()->get('wallabag_core.entry_repository'); + $entryRepository = $this->getClient()->getContainer()->get('wallabag_core.entry_repository.test'); $reloadedAdminEntry = $entryRepository->find($this->adminEntry->getId()); $this->assertNotEmpty($reloadedAdminEntry->getContent()); diff --git a/tests/Wallabag/CoreBundle/Command/ShowUserCommandTest.php b/tests/Wallabag/CoreBundle/Command/ShowUserCommandTest.php index 9b34f2a0..ed383a2c 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 e07c57dd..d8478ce3 100644 --- a/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php @@ -1,6 +1,6 @@ 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); @@ -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); @@ -965,4 +965,120 @@ class ConfigControllerTest extends WallabagCoreTestCase $client->request('GET', '/config/view-mode'); } + + public function testChangeLocaleWithoutReferer() + { + $client = $this->getClient(); + + $client->request('GET', '/locale/de'); + $client->followRedirect(); + + $this->assertSame('de', $client->getRequest()->getLocale()); + $this->assertSame('de', $client->getContainer()->get('session')->get('_locale')); + } + + public function testChangeLocaleWithReferer() + { + $client = $this->getClient(); + + $client->request('GET', '/login'); + $client->request('GET', '/locale/de'); + $client->followRedirect(); + + $this->assertSame('de', $client->getRequest()->getLocale()); + $this->assertSame('de', $client->getContainer()->get('session')->get('_locale')); + } + + public function testChangeLocaleToBadLocale() + { + $client = $this->getClient(); + + $client->request('GET', '/login'); + $client->request('GET', '/locale/yuyuyuyu'); + $client->followRedirect(); + + $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()); + } } diff --git a/tests/Wallabag/CoreBundle/Controller/EntryControllerTest.php b/tests/Wallabag/CoreBundle/Controller/EntryControllerTest.php index 265e4205..9dee9891 100644 --- a/tests/Wallabag/CoreBundle/Controller/EntryControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/EntryControllerTest.php @@ -522,9 +522,12 @@ class EntryControllerTest extends WallabagCoreTestCase $crawler = $client->followRedirect(); - $this->assertGreaterThan(1, $title = $crawler->filter('div[id=article] h1')->extract(['_text'])); + $title = $crawler->filter('div[id=article] h1')->extract(['_text']); + $this->assertGreaterThan(1, $title); $this->assertContains('My updated title hehe :)', $title[0]); - $this->assertSame(1, \count($stats = $crawler->filter('div[class=tools] ul[class=stats] li a[class=tool]')->extract(['_text']))); + + $stats = $crawler->filter('div[class=tools] ul[class=stats] li a[class=tool]')->extract(['_text']); + $this->assertCount(1, $stats); $this->assertNotContains('example.io', trim($stats[0])); } @@ -620,7 +623,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); @@ -773,7 +776,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(); @@ -984,8 +987,13 @@ class EntryControllerTest extends WallabagCoreTestCase $client->request('GET', '/share/' . $content->getId()); $this->assertSame(302, $client->getResponse()->getStatusCode()); - // follow link with uid - $crawler = $client->followRedirect(); + $shareUrl = $client->getResponse()->getTargetUrl(); + + // use a new client to have a fresh empty session (instead of a logged one from the previous client) + $client->restart(); + + $client->request('GET', $shareUrl); + $this->assertSame(200, $client->getResponse()->getStatusCode()); $this->assertContains('max-age=25200', $client->getResponse()->headers->get('cache-control')); $this->assertContains('public', $client->getResponse()->headers->get('cache-control')); @@ -1001,9 +1009,6 @@ class EntryControllerTest extends WallabagCoreTestCase $client->request('GET', '/share/' . $content->getUid()); $this->assertSame(404, $client->getResponse()->getStatusCode()); - $client->request('GET', '/view/' . $content->getId()); - $this->assertContains('no-cache', $client->getResponse()->headers->get('cache-control')); - // removing the share $client->request('GET', '/share/delete/' . $content->getId()); $this->assertSame(302, $client->getResponse()->getStatusCode()); @@ -1244,7 +1249,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(); @@ -1274,7 +1279,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(); @@ -1325,10 +1330,6 @@ class EntryControllerTest extends WallabagCoreTestCase 'http://www.hao123.com/shequ?__noscript__-=1', 'zh_CN', ], - 'ru' => [ - 'https://www.kp.ru/daily/26879.7/3921982/', - 'ru', - ], 'pt_BR' => [ 'https://politica.estadao.com.br/noticias/eleicoes,campanha-catatonica,70002491983', 'pt_BR', @@ -1494,4 +1495,30 @@ class EntryControllerTest extends WallabagCoreTestCase $this->assertSame(sprintf('/remove-tag/%s/%s', $entry->getId(), $tag->getId()), $link); } + + public function testRandom() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $client->request('GET', '/unread/random'); + $this->assertSame(302, $client->getResponse()->getStatusCode()); + $this->assertContains('/view/', $client->getResponse()->getTargetUrl(), 'Unread random'); + + $client->request('GET', '/starred/random'); + $this->assertSame(302, $client->getResponse()->getStatusCode()); + $this->assertContains('/view/', $client->getResponse()->getTargetUrl(), 'Starred random'); + + $client->request('GET', '/archive/random'); + $this->assertSame(302, $client->getResponse()->getStatusCode()); + $this->assertContains('/view/', $client->getResponse()->getTargetUrl(), 'Archive random'); + + $client->request('GET', '/untagged/random'); + $this->assertSame(302, $client->getResponse()->getStatusCode()); + $this->assertContains('/view/', $client->getResponse()->getTargetUrl(), 'Untagged random'); + + $client->request('GET', '/all/random'); + $this->assertSame(302, $client->getResponse()->getStatusCode()); + $this->assertContains('/view/', $client->getResponse()->getTargetUrl(), 'All random'); + } } diff --git a/tests/Wallabag/CoreBundle/Controller/ExportControllerTest.php b/tests/Wallabag/CoreBundle/Controller/ExportControllerTest.php index 0c3d4c83..d7ce7c45 100644 --- a/tests/Wallabag/CoreBundle/Controller/ExportControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/ExportControllerTest.php @@ -180,7 +180,7 @@ class ExportControllerTest extends WallabagCoreTestCase $this->assertGreaterThan(1, $csv); // +1 for title line - $this->assertSame(\count($contentInDB) + 1, \count($csv)); + $this->assertCount(\count($contentInDB) + 1, $csv); $this->assertSame('Title;URL;Content;Tags;"MIME Type";Language;"Creation date"', $csv[0]); $this->assertContains($contentInDB[0]['title'], $csv[1]); $this->assertContains($contentInDB[0]['url'], $csv[1]); diff --git a/tests/Wallabag/CoreBundle/Controller/FeedControllerTest.php b/tests/Wallabag/CoreBundle/Controller/FeedControllerTest.php new file mode 100644 index 00000000..d52d7bb8 --- /dev/null +++ b/tests/Wallabag/CoreBundle/Controller/FeedControllerTest.php @@ -0,0 +1,261 @@ +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->assertSame($nb, $xpath->query('//a:entry')->length); + } + + $this->assertSame(1, $xpath->query('/a:feed')->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->assertSame(1, $xpath->query('/a:feed/a:updated')->length); + + $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->assertSame(1, $xpath->query('/a:feed/a:subtitle')->length); + if (null !== $tagValue && 0 === strpos($type, 'tag')) { + $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->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->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->assertSame(1, $xpath->query('/a:feed/a:link[@rel="last"]')->length); + + foreach ($xpath->query('//a:entry') as $item) { + $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); + } + } + + 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->assertSame(200, $client->getResponse()->getStatusCode()); + $this->validateDom($client->getResponse()->getContent(), 'unread'); + + $client->request('GET', '/feed/admin/SUPERTOKEN/unread/2'); + $this->assertSame(200, $client->getResponse()->getStatusCode()); + $this->validateDom($client->getResponse()->getContent(), 'unread'); + + $client->request('GET', '/feed/admin/SUPERTOKEN/unread/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->setFeedToken('SUPERTOKEN'); + $config->setFeedLimit(null); + $em->persist($config); + $em->flush(); + + $client = $this->getClient(); + $client->request('GET', '/feed/admin/SUPERTOKEN/tags/foo'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $this->validateDom($client->getResponse()->getContent(), 'tag', 2, 'foo'); + + $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()); + } +} diff --git a/tests/Wallabag/CoreBundle/Controller/RssControllerTest.php b/tests/Wallabag/CoreBundle/Controller/RssControllerTest.php deleted file mode 100644 index 2af6e14f..00000000 --- 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 395208a2..93019b1f 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() @@ -23,10 +23,10 @@ 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 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(); + + $this->logInAsUsingHttp('admin'); + $crawler = $client->request('GET', '/config'); + $this->assertContains('scheb_two_factor.trusted', $crawler->filter('body')->extract(['_text'])[0]); - $date = new \DateTime(); - $user->addTrustedComputer('ABCDEF', $date->add(new \DateInterval('P1M'))); - $this->assertTrue($user->isTrustedComputer('ABCDEF')); - $this->assertFalse($user->isTrustedComputer('FEDCBA')); + // 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/CoreBundle/Controller/TagControllerTest.php b/tests/Wallabag/CoreBundle/Controller/TagControllerTest.php index 768f4c07..be17dcf5 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.'); + } } diff --git a/tests/Wallabag/CoreBundle/Event/Listener/UserLocaleListenerTest.php b/tests/Wallabag/CoreBundle/Event/Listener/UserLocaleListenerTest.php index 93edfde8..ff0a9602 100644 --- a/tests/Wallabag/CoreBundle/Event/Listener/UserLocaleListenerTest.php +++ b/tests/Wallabag/CoreBundle/Event/Listener/UserLocaleListenerTest.php @@ -56,4 +56,27 @@ class UserLocaleListenerTest extends TestCase $this->assertNull($session->get('_locale')); } + + public function testWithLanguageFromSession() + { + $session = new Session(new MockArraySessionStorage()); + $listener = new UserLocaleListener($session); + $session->set('_locale', 'de'); + + $user = new User(); + $user->setEnabled(true); + + $config = new Config($user); + $config->setLanguage('fr'); + + $user->setConfig($config); + + $userToken = new UsernamePasswordToken($user, '', 'test'); + $request = Request::create('/'); + $event = new InteractiveLoginEvent($request, $userToken); + + $listener->onInteractiveLogin($event); + + $this->assertSame('de', $session->get('_locale')); + } } diff --git a/tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php b/tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php index 845762dc..9e0a9136 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') @@ -29,7 +31,7 @@ class GrabySiteConfigBuilderTest extends WallabagCoreTestCase $grabyConfigBuilderMock ->method('buildForHost') ->with('api.example.com') - ->will($this->returnValue($grabySiteConfig)); + ->willReturn($grabySiteConfig); $logger = new Logger('foo'); $handler = new TestHandler(); @@ -88,7 +90,7 @@ class GrabySiteConfigBuilderTest extends WallabagCoreTestCase $grabyConfigBuilderMock ->method('buildForHost') ->with('unknown.com') - ->will($this->returnValue(new GrabySiteConfig())); + ->willReturn(new GrabySiteConfig()); $logger = new Logger('foo'); $handler = new TestHandler(); @@ -130,6 +132,73 @@ class GrabySiteConfigBuilderTest extends WallabagCoreTestCase $this->assertCount(1, $records, 'One log was recorded'); } + public function testBuildConfigWithBadExtraFields() + { + $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') + ->willReturn($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('findOneByHostsAndUser') + ->with(['example.com', '.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'); + } + public function testBuildConfigUserNotDefined() { $grabyConfigBuilderMock = $this->getMockBuilder('\Graby\SiteConfig\ConfigBuilder') @@ -139,7 +208,7 @@ class GrabySiteConfigBuilderTest extends WallabagCoreTestCase $grabyConfigBuilderMock ->method('buildForHost') ->with('unknown.com') - ->will($this->returnValue(new GrabySiteConfig())); + ->willReturn(new GrabySiteConfig()); $logger = new Logger('foo'); $handler = new TestHandler(); @@ -210,7 +279,7 @@ class GrabySiteConfigBuilderTest extends WallabagCoreTestCase $grabyConfigBuilderMock ->method('buildForHost') ->with($host) - ->will($this->returnValue($grabySiteConfig)); + ->willReturn($grabySiteConfig); $user = $this->getMockBuilder('Wallabag\UserBundle\Entity\User') ->disableOriginalConstructor() diff --git a/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php b/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php index 3dd9273c..c7caac1d 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()); @@ -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(); @@ -247,7 +331,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 +380,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 +416,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 +455,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 +490,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()); @@ -415,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/CoreBundle/Helper/RedirectTest.php b/tests/Wallabag/CoreBundle/Helper/RedirectTest.php index 04e1a59c..29e12cbe 100644 --- a/tests/Wallabag/CoreBundle/Helper/RedirectTest.php +++ b/tests/Wallabag/CoreBundle/Helper/RedirectTest.php @@ -17,6 +17,9 @@ class RedirectTest extends TestCase /** @var Redirect */ private $redirect; + /** @var UsernamePasswordToken */ + private $token; + public function setUp() { $this->routerMock = $this->getMockBuilder('Symfony\Component\Routing\Router') diff --git a/tests/Wallabag/CoreBundle/ParamConverter/UsernameRssTokenConverterTest.php b/tests/Wallabag/CoreBundle/ParamConverter/UsernameFeedTokenConverterTest.php similarity index 78% rename from tests/Wallabag/CoreBundle/ParamConverter/UsernameRssTokenConverterTest.php rename to tests/Wallabag/CoreBundle/ParamConverter/UsernameFeedTokenConverterTest.php index b044a700..48c82dde 100644 --- a/tests/Wallabag/CoreBundle/ParamConverter/UsernameRssTokenConverterTest.php +++ b/tests/Wallabag/CoreBundle/ParamConverter/UsernameFeedTokenConverterTest.php @@ -1,19 +1,19 @@ assertFalse($converter->supports($params)); } @@ -26,10 +26,10 @@ class UsernameRssTokenConverterTest extends TestCase $registry->expects($this->once()) ->method('getManagers') - ->will($this->returnValue([])); + ->willReturn([]); $params = new ParamConverter([]); - $converter = new UsernameRssTokenConverter($registry); + $converter = new UsernameFeedTokenConverter($registry); $this->assertFalse($converter->supports($params)); } @@ -42,10 +42,10 @@ class UsernameRssTokenConverterTest extends TestCase $registry->expects($this->once()) ->method('getManagers') - ->will($this->returnValue(['default' => null])); + ->willReturn(['default' => null]); $params = new ParamConverter([]); - $converter = new UsernameRssTokenConverter($registry); + $converter = new UsernameFeedTokenConverter($registry); $this->assertFalse($converter->supports($params)); } @@ -58,7 +58,7 @@ class UsernameRssTokenConverterTest 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 UsernameRssTokenConverterTest 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,15 +75,15 @@ class UsernameRssTokenConverterTest 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 UsernameRssTokenConverter($registry); + $converter = new UsernameFeedTokenConverter($registry); $this->assertFalse($converter->supports($params)); } @@ -96,7 +96,7 @@ class UsernameRssTokenConverterTest 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 UsernameRssTokenConverterTest 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,15 +113,15 @@ class UsernameRssTokenConverterTest 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 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,9 +147,9 @@ class UsernameRssTokenConverterTest extends TestCase ->getMock(); $repo->expects($this->once()) - ->method('findOneByUsernameAndRsstoken') + ->method('findOneByUsernameAndFeedToken') ->with('test', 'test') - ->will($this->returnValue(null)); + ->willReturn(null); $em = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager') ->disableOriginalConstructor() @@ -158,7 +158,7 @@ class UsernameRssTokenConverterTest 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,10 +167,10 @@ class UsernameRssTokenConverterTest 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 UsernameRssTokenConverter($registry); + $converter = new UsernameFeedTokenConverter($registry); $request = new Request([], [], ['username' => 'test', 'token' => 'test']); $converter->apply($request, $params); @@ -185,9 +185,9 @@ class UsernameRssTokenConverterTest extends TestCase ->getMock(); $repo->expects($this->once()) - ->method('findOneByUsernameAndRsstoken') + ->method('findOneByUsernameAndFeedtoken') ->with('test', 'test') - ->will($this->returnValue($user)); + ->willReturn($user); $em = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager') ->disableOriginalConstructor() @@ -196,7 +196,7 @@ class UsernameRssTokenConverterTest 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,10 +205,10 @@ class UsernameRssTokenConverterTest 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 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 bb92f745..39fcec16 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->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() { $entryRepository = $this->getMockBuilder('Wallabag\CoreBundle\Repository\EntryRepository') diff --git a/tests/Wallabag/CoreBundle/WallabagCoreTestCase.php b/tests/Wallabag/CoreBundle/WallabagCoreTestCase.php index 6e1163c5..816d22f4 100644 --- a/tests/Wallabag/CoreBundle/WallabagCoreTestCase.php +++ b/tests/Wallabag/CoreBundle/WallabagCoreTestCase.php @@ -84,8 +84,8 @@ abstract class WallabagCoreTestCase extends WebTestCase $container = $this->client->getContainer(); $session = $container->get('session'); - $userManager = $container->get('fos_user.user_manager'); - $loginManager = $container->get('fos_user.security.login_manager'); + $userManager = $container->get('fos_user.user_manager.test'); + $loginManager = $container->get('fos_user.security.login_manager.test'); $firewallName = $container->getParameter('fos_user.firewall_name'); $user = $userManager->findUserBy(['username' => $username]); diff --git a/tests/Wallabag/ImportBundle/Command/ImportCommandTest.php b/tests/Wallabag/ImportBundle/Command/ImportCommandTest.php index f95320a4..8e1c528d 100644 --- a/tests/Wallabag/ImportBundle/Command/ImportCommandTest.php +++ b/tests/Wallabag/ImportBundle/Command/ImportCommandTest.php @@ -84,6 +84,8 @@ class ImportCommandTest extends WallabagCoreTestCase public function testRunImportCommandWithUserId() { + $this->logInAs('admin'); + $application = new Application($this->getClient()->getKernel()); $application->add(new ImportCommand()); @@ -92,7 +94,7 @@ class ImportCommandTest extends WallabagCoreTestCase $tester = new CommandTester($command); $tester->execute([ 'command' => $command->getName(), - 'username' => 1, + 'username' => $this->getLoggedInUserId(), 'filepath' => $application->getKernel()->getContainer()->getParameter('kernel.project_dir') . '/tests/Wallabag/ImportBundle/fixtures/wallabag-v2-read.json', '--useUserId' => true, '--importer' => 'v2', diff --git a/tests/Wallabag/ImportBundle/Consumer/AMQPEntryConsumerTest.php b/tests/Wallabag/ImportBundle/Consumer/AMQPEntryConsumerTest.php index b2141c04..b7f6192d 100644 --- a/tests/Wallabag/ImportBundle/Consumer/AMQPEntryConsumerTest.php +++ b/tests/Wallabag/ImportBundle/Consumer/AMQPEntryConsumerTest.php @@ -1,6 +1,6 @@ assertInstanceOf('Wallabag\CoreBundle\Entity\Entry', $content); $this->assertNotEmpty($content->getPreviewPicture(), 'Preview picture for http://www.usinenouvelle.com is ok'); $this->assertNotEmpty($content->getLanguage(), 'Language for http://www.usinenouvelle.com is ok'); - $this->assertSame(1, \count($content->getTags())); + $this->assertCount(1, $content->getTags()); $createdAt = $content->getCreatedAt(); $this->assertSame('2011', $createdAt->format('Y')); diff --git a/tests/Wallabag/ImportBundle/Controller/FirefoxControllerTest.php b/tests/Wallabag/ImportBundle/Controller/FirefoxControllerTest.php index dc5ed6d0..3e64f2e5 100644 --- a/tests/Wallabag/ImportBundle/Controller/FirefoxControllerTest.php +++ b/tests/Wallabag/ImportBundle/Controller/FirefoxControllerTest.php @@ -122,7 +122,7 @@ class FirefoxControllerTest extends WallabagCoreTestCase $this->assertNotEmpty($content->getMimetype(), 'Mimetype for http://lexpansion.lexpress.fr is ok'); $this->assertNotEmpty($content->getPreviewPicture(), 'Preview picture for http://lexpansion.lexpress.fr is ok'); $this->assertNotEmpty($content->getLanguage(), 'Language for http://lexpansion.lexpress.fr is ok'); - $this->assertSame(3, \count($content->getTags())); + $this->assertCount(3, $content->getTags()); $content = $client->getContainer() ->get('doctrine.orm.entity_manager') diff --git a/tests/Wallabag/ImportBundle/Controller/InstapaperControllerTest.php b/tests/Wallabag/ImportBundle/Controller/InstapaperControllerTest.php index 7390fa88..05347767 100644 --- a/tests/Wallabag/ImportBundle/Controller/InstapaperControllerTest.php +++ b/tests/Wallabag/ImportBundle/Controller/InstapaperControllerTest.php @@ -124,7 +124,7 @@ class InstapaperControllerTest extends WallabagCoreTestCase $this->assertNotEmpty($content->getPreviewPicture(), 'Preview picture for https://www.liberation.fr is ok'); $this->assertNotEmpty($content->getLanguage(), 'Language for https://www.liberation.fr is ok'); $this->assertContains('foot', $content->getTags(), 'It includes the "foot" tag'); - $this->assertSame(1, \count($content->getTags())); + $this->assertCount(1, $content->getTags()); $this->assertInstanceOf(\DateTime::class, $content->getCreatedAt()); $content = $client->getContainer() @@ -138,7 +138,7 @@ class InstapaperControllerTest extends WallabagCoreTestCase $this->assertContains('foot', $content->getTags()); $this->assertContains('test_tag', $content->getTags()); - $this->assertSame(2, \count($content->getTags())); + $this->assertCount(2, $content->getTags()); } public function testImportInstapaperWithFileAndMarkAllAsRead() diff --git a/tests/Wallabag/ImportBundle/Controller/PinboardControllerTest.php b/tests/Wallabag/ImportBundle/Controller/PinboardControllerTest.php index 80819f45..15646d55 100644 --- a/tests/Wallabag/ImportBundle/Controller/PinboardControllerTest.php +++ b/tests/Wallabag/ImportBundle/Controller/PinboardControllerTest.php @@ -127,7 +127,7 @@ class PinboardControllerTest extends WallabagCoreTestCase $this->assertContains('foot', $tags, 'It includes the "foot" tag'); $this->assertContains('varnish', $tags, 'It includes the "varnish" tag'); $this->assertContains('php', $tags, 'It includes the "php" tag'); - $this->assertSame(3, \count($tags)); + $this->assertCount(3, $tags); $this->assertInstanceOf(\DateTime::class, $content->getCreatedAt()); $this->assertSame('2016-10-26', $content->getCreatedAt()->format('Y-m-d')); diff --git a/tests/Wallabag/ImportBundle/Controller/ReadabilityControllerTest.php b/tests/Wallabag/ImportBundle/Controller/ReadabilityControllerTest.php index 5619659a..4f2f4053 100644 --- a/tests/Wallabag/ImportBundle/Controller/ReadabilityControllerTest.php +++ b/tests/Wallabag/ImportBundle/Controller/ReadabilityControllerTest.php @@ -125,7 +125,7 @@ class ReadabilityControllerTest extends WallabagCoreTestCase $tags = $content->getTags(); $this->assertContains('foot', $tags, 'It includes the "foot" tag'); - $this->assertSame(1, \count($tags)); + $this->assertCount(1, $tags); $this->assertInstanceOf(\DateTime::class, $content->getCreatedAt()); $this->assertSame('2016-09-08', $content->getCreatedAt()->format('Y-m-d')); diff --git a/tests/Wallabag/ImportBundle/Controller/WallabagV1ControllerTest.php b/tests/Wallabag/ImportBundle/Controller/WallabagV1ControllerTest.php index c67941a7..2a8e7c89 100644 --- a/tests/Wallabag/ImportBundle/Controller/WallabagV1ControllerTest.php +++ b/tests/Wallabag/ImportBundle/Controller/WallabagV1ControllerTest.php @@ -121,13 +121,13 @@ 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(); $this->assertContains('foot', $tags, 'It includes the "foot" tag'); $this->assertContains('framabag', $tags, 'It includes the "framabag" tag'); - $this->assertSame(2, \count($tags)); + $this->assertCount(2, $tags); $this->assertInstanceOf(\DateTime::class, $content->getCreatedAt()); } diff --git a/tests/Wallabag/ImportBundle/Controller/WallabagV2ControllerTest.php b/tests/Wallabag/ImportBundle/Controller/WallabagV2ControllerTest.php index 822656ba..b606e26a 100644 --- a/tests/Wallabag/ImportBundle/Controller/WallabagV2ControllerTest.php +++ b/tests/Wallabag/ImportBundle/Controller/WallabagV2ControllerTest.php @@ -128,7 +128,7 @@ class WallabagV2ControllerTest extends WallabagCoreTestCase $tags = $content->getTags(); $this->assertContains('foot', $tags, 'It includes the "foot" tag'); - $this->assertSame(1, \count($tags)); + $this->assertCount(1, $tags); $content = $client->getContainer() ->get('doctrine.orm.entity_manager') @@ -147,7 +147,7 @@ class WallabagV2ControllerTest extends WallabagCoreTestCase $this->assertContains('foot', $tags, 'It includes the "foot" tag'); $this->assertContains('mediapart', $tags, 'It includes the "mediapart" tag'); $this->assertContains('blog', $tags, 'It includes the "blog" tag'); - $this->assertSame(3, \count($tags)); + $this->assertCount(3, $tags); $this->assertInstanceOf(\DateTime::class, $content->getCreatedAt()); $this->assertSame('2016-09-08', $content->getCreatedAt()->format('Y-m-d')); diff --git a/tests/Wallabag/UserBundle/Controller/ManageControllerTest.php b/tests/Wallabag/UserBundle/Controller/ManageControllerTest.php index adc2cf09..f44e6fbf 100644 --- a/tests/Wallabag/UserBundle/Controller/ManageControllerTest.php +++ b/tests/Wallabag/UserBundle/Controller/ManageControllerTest.php @@ -1,6 +1,6 @@ em = $this->getMockBuilder('Doctrine\ORM\EntityManager') ->disableOriginalConstructor() ->getMock(); @@ -34,7 +37,8 @@ class CreateConfigListenerTest extends TestCase 'fr', 1, 1, - 1 + 1, + $session ); $this->dispatcher = new EventDispatcher(); @@ -58,13 +62,13 @@ 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); $this->em->expects($this->once()) ->method('persist') - ->will($this->returnValue($config)); + ->willReturn($config); $this->em->expects($this->once()) ->method('flush'); diff --git a/tests/Wallabag/UserBundle/Mailer/AuthCodeMailerTest.php b/tests/Wallabag/UserBundle/Mailer/AuthCodeMailerTest.php index aa176068..1713c10c 100644 --- a/tests/Wallabag/UserBundle/Mailer/AuthCodeMailerTest.php +++ b/tests/Wallabag/UserBundle/Mailer/AuthCodeMailerTest.php @@ -6,22 +6,6 @@ use PHPUnit\Framework\TestCase; use Wallabag\UserBundle\Entity\User; use Wallabag\UserBundle\Mailer\AuthCodeMailer; -/** - * @see https://www.pmg.com/blog/integration-testing-swift-mailer/ - */ -final class CountableMemorySpool extends \Swift_MemorySpool implements \Countable -{ - public function count() - { - return \count($this->messages); - } - - public function getMessages() - { - return $this->messages; - } -} - class AuthCodeMailerTest extends TestCase { protected $mailer; @@ -49,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'); diff --git a/tests/Wallabag/UserBundle/Mailer/CountableMemorySpool.php b/tests/Wallabag/UserBundle/Mailer/CountableMemorySpool.php new file mode 100644 index 00000000..53f240a1 --- /dev/null +++ b/tests/Wallabag/UserBundle/Mailer/CountableMemorySpool.php @@ -0,0 +1,19 @@ +messages); + } + + public function getMessages() + { + return $this->messages; + } +} diff --git a/web/app.php b/web/app.php index 4c2c4650..3427e133 100644 --- a/web/app.php +++ b/web/app.php @@ -2,14 +2,9 @@ use Symfony\Component\HttpFoundation\Request; -/** - * @var Composer\Autoload\ClassLoader - */ -$loader = require __DIR__.'/../app/autoload.php'; -include_once __DIR__.'/../var/bootstrap.php.cache'; +require __DIR__.'/../vendor/autoload.php'; $kernel = new AppKernel('prod', false); -$kernel->loadClassCache(); //$kernel = new AppCache($kernel); // When using the HttpCache, you need to call the method in your front controller instead of relying on the configuration parameter diff --git a/web/app_dev.php b/web/app_dev.php index 8456754d..57e1a433 100644 --- a/web/app_dev.php +++ b/web/app_dev.php @@ -1,10 +1,10 @@ loadClassCache(); $request = Request::createFromGlobals(); $response = $kernel->handle($request); $response->send(); diff --git a/web/wallassets/baggy.css b/web/wallassets/baggy.css index 8c1ed86b..fb808467 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 436294d3..a157f0ed 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=242)}([function(e,t,n){var r,a;!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=g.type(e);return"function"!==n&&!g.isWindow(e)&&("array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e)}function winnow(e,t,n){if(g.isFunction(t))return g.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return g.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(v.test(t))return g.filter(t,e,n);t=g.filter(t,e)}return g.grep(e,function(e){return d.call(t,e)>-1!==n})}function sibling(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}function createOptions(e){var t={};return g.each(e.match(x)||[],function(e,n){t[n]=!0}),t}function completed(){s.removeEventListener("DOMContentLoaded",completed),n.removeEventListener("load",completed),g.ready()}function Data(){this.expando=g.expando+Data.uid++}function dataAttr(e,t,n){var r;if(void 0===n&&1===e.nodeType)if(r="data-"+t.replace(F,"-$&").toLowerCase(),"string"==typeof(n=e.getAttribute(r))){try{n="true"===n||"false"!==n&&("null"===n?null:+n+""===n?+n:U.test(n)?g.parseJSON(n):n)}catch(e){}k.set(e,t,n)}else n=void 0;return n}function adjustCSS(e,t,n,r){var a,i=1,o=20,s=r?function(){return r.cur()}:function(){return g.css(e,t,"")},l=s(),c=n&&n[3]||(g.cssNumber[t]?"":"px"),_=(g.cssNumber[t]||"px"!==c&&+l)&&G.exec(g.css(e,t));if(_&&_[3]!==c){c=c||_[3],n=n||[],_=+l||1;do{i=i||".5",_/=i,g.style(e,t,_+c)}while(i!==(i=s()/l)&&1!==i&&--o)}return n&&(_=+_||+l||0,a=n[1]?_+(n[1]+1)*n[2]:+n[2],r&&(r.unit=c,r.start=_,r.end=a)),a}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&&g.nodeName(e,t)?g.merge([e],n):n}function setGlobalEval(e,t){for(var n=0,r=e.length;n-1)a&&a.push(i);else if(c=g.contains(i.ownerDocument,i),o=getAll(d.appendChild(i),"script"),c&&setGlobalEval(o),n)for(_=0;i=o[_++];)z.test(i.type||"")&&n.push(i);return d}function returnTrue(){return!0}function returnFalse(){return!1}function safeActiveElement(){try{return s.activeElement}catch(e){}}function on(e,t,n,r,a,i){var o,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==a?(a=n,r=n=void 0):null==a&&("string"==typeof n?(a=r,r=void 0):(a=r,r=n,n=void 0)),!1===a)a=returnFalse;else if(!a)return e;return 1===i&&(o=a,a=function(e){return g().off(e),o.apply(this,arguments)},a.guid=o.guid||(o.guid=g.guid++)),e.each(function(){g.event.add(this,t,a,r,n)})}function manipulationTarget(e,t){return g.nodeName(e,"table")&&g.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,a,i,o,s,l,c;if(1===t.nodeType){if(P.hasData(e)&&(i=P.access(e),o=P.set(t,i),c=i.events)){delete o.handle,o.events={};for(a in c)for(n=0,r=c[a].length;n1&&"string"==typeof m&&!E.checkClone&&J.test(m))return e.each(function(a){var i=e.eq(a);S&&(t[0]=m.call(this,a,i.html())),domManip(i,t,n,r)});if(u&&(a=buildFragment(t,e[0].ownerDocument,!1,e,r),i=a.firstChild,1===a.childNodes.length&&(a=i),i||r)){for(o=g.map(getAll(a,"script"),disableScript),s=o.length;d")).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,a,i,o,s=e.style;return n=n||oe(e),o=n?n.getPropertyValue(t)||n[t]:void 0,""!==o&&void 0!==o||g.contains(e.ownerDocument,e)||(o=g.style(e,t)),n&&!E.pixelMarginRight()&&ie.test(o)&&ae.test(t)&&(r=s.width,a=s.minWidth,i=s.maxWidth,s.minWidth=s.maxWidth=s.width=o,o=n.width,s.width=r,s.minWidth=a,s.maxWidth=i),void 0!==o?o+"":o}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 pe)return e;for(var t=e[0].toUpperCase()+e.slice(1),n=ue.length;n--;)if((e=ue[n]+t)in pe)return e}function setPositiveNumber(e,t,n){var r=G.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):t}function augmentWidthOrHeight(e,t,n,r,a){for(var i=n===(r?"border":"content")?4:"width"===t?1:0,o=0;i<4;i+=2)"margin"===n&&(o+=g.css(e,n+Y[i],!0,a)),r?("content"===n&&(o-=g.css(e,"padding"+Y[i],!0,a)),"margin"!==n&&(o-=g.css(e,"border"+Y[i]+"Width",!0,a))):(o+=g.css(e,"padding"+Y[i],!0,a),"padding"!==n&&(o+=g.css(e,"border"+Y[i]+"Width",!0,a)));return o}function getWidthOrHeight(e,t,n){var r=!0,a="width"===t?e.offsetWidth:e.offsetHeight,i=oe(e),o="border-box"===g.css(e,"boxSizing",!1,i);if(a<=0||null==a){if(a=curCSS(e,t,i),(a<0||null==a)&&(a=e.style[t]),ie.test(a))return a;r=o&&(E.boxSizingReliable()||a===e.style[t]),a=parseFloat(a)||0}return a+augmentWidthOrHeight(e,t,n||(o?"border":"content"),r,i)+"px"}function showHide(e,t){for(var n,r,a,i=[],o=0,s=e.length;o=0&&n=0},isPlainObject:function(e){var t;if("object"!==g.type(e)||e.nodeType||g.isWindow(e))return!1;if(e.constructor&&!m.call(e,"constructor")&&!m.call(e.constructor.prototype||{},"isPrototypeOf"))return!1;for(t in e);return void 0===t||m.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?u[p.call(e)]||"object":typeof e},globalEval:function(e){var t,n=eval;(e=g.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(f,"ms-").replace(T,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=p.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("|"),a=n.length;a--;)r.attrHandle[n[a]]=t}function siblingCheck(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||I)-(~e.sourceIndex||I);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 a,i=e([],n.length,t),o=i.length;o--;)n[a=i[o]]&&(n[a]=!(r[a]=n[a]))})})}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 a=e.length;a--;)if(!e[a](t,n,r))return!1;return!0}:e[0]}function multipleContexts(e,t,n){for(var r=0,a=t.length;r-1&&(i[c]=!(o[c]=d))}}else S=condense(S===o?S.splice(m,S.length):S),a?a(null,o,S,l):M.apply(o,S)})}function matcherFromTokens(e){for(var t,n,a,i=e.length,o=r.relative[e[0].type],s=o||r.relative[" "],l=o?1:0,_=addCombinator(function(e){return e===t},s,!0),d=addCombinator(function(e){return P(t,e)>-1},s,!0),u=[function(e,n,r){var a=!o&&(r||n!==c)||((t=n).nodeType?_(e,n,r):d(e,n,r));return t=null,a}];l1&&elementMatcher(u),l>1&&toSelector(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(H,"$1"),n,l0,a=e.length>0,i=function(i,o,s,l,_){var d,m,g,S=0,f="0",T=i&&[],b=[],h=c,N=i||a&&r.find.TAG("*",_),O=C+=null==h?1:Math.random()||.1,R=N.length;for(_&&(c=o===p||o||_);f!==R&&null!=(d=N[f]);f++){if(a&&d){for(m=0,o||d.ownerDocument===p||(u(d),s=!E);g=e[m++];)if(g(d,o||p,s)){l.push(d);break}_&&(C=O)}n&&((d=!g&&d)&&S--,i&&T.push(d))}if(S+=f,n&&f!==S){for(m=0;g=t[m++];)g(T,b,o,s);if(i){if(S>0)for(;f--;)T[f]||b[f]||(b[f]=x.call(l));b=condense(b)}M.apply(l,b),_&&!i&&b.length>0&&S+t.length>1&&Sizzle.uniqueSort(l)}return _&&(C=O,c=h),T};return n?markFunction(i):i}var t,n,r,a,i,o,s,l,c,_,d,u,p,m,E,g,S,f,T,b="sizzle"+1*new Date,h=e.document,C=0,N=0,O=createCache(),R=createCache(),v=createCache(),y=function(e,t){return e===t&&(d=!0),0},I=1<<31,A={}.hasOwnProperty,D=[],x=D.pop,w=D.push,M=D.push,L=D.slice,P=function(e,t){for(var n=0,r=e.length;n+~]|"+U+")"+U+"*"),z=new RegExp("="+U+"*([^\\]'\"]*?)"+U+"*\\]","g"),W=new RegExp(G),K=new RegExp("^"+F+"$"),Q={ID:new RegExp("^#("+F+")"),CLASS:new RegExp("^\\.("+F+")"),TAG:new RegExp("^("+F+"|[*])"),ATTR:new RegExp("^"+B),PSEUDO:new RegExp("^"+G),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+U+"*(even|odd|(([+-]|)(\\d*)n|)"+U+"*(?:([+-]|)"+U+"*(\\d+)|))"+U+"*\\)|)","i"),bool:new RegExp("^(?:"+k+")$","i"),needsContext:new RegExp("^"+U+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+U+"*((?:-\\d)?\\d*)"+U+"*\\)|)(?=[^-]|$)","i")},$=/^(?:input|select|textarea|button)$/i,j=/^h\d$/i,X=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,J=/[+~]/,ee=/'|\\/g,te=new RegExp("\\\\([\\da-f]{1,6}"+U+"?|("+U+")|.)","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(){u()};try{M.apply(D=L.call(h.childNodes),h.childNodes),D[h.childNodes.length].nodeType}catch(e){M={apply:D.length?function(e,t){w.apply(e,L.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},u=Sizzle.setDocument=function(e){var t,a,o=e?e.ownerDocument||e:h;return o!==p&&9===o.nodeType&&o.documentElement?(p=o,m=p.documentElement,E=!i(p),(a=p.defaultView)&&a.top!==a&&(a.addEventListener?a.addEventListener("unload",re,!1):a.attachEvent&&a.attachEvent("onunload",re)),n.attributes=assert(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=assert(function(e){return e.appendChild(p.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=X.test(p.getElementsByClassName),n.getById=assert(function(e){return m.appendChild(e).id=b,!p.getElementsByName||!p.getElementsByName(b).length}),n.getById?(r.find.ID=function(e,t){if(void 0!==t.getElementById&&E){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=[],a=0,i=t.getElementsByTagName(e);if("*"===e){for(;n=i[a++];)1===n.nodeType&&r.push(n);return r}return i},r.find.CLASS=n.getElementsByClassName&&function(e,t){if(void 0!==t.getElementsByClassName&&E)return t.getElementsByClassName(e)},S=[],g=[],(n.qsa=X.test(p.querySelectorAll))&&(assert(function(e){m.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&g.push("[*^$]="+U+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||g.push("\\["+U+"*(?:value|"+k+")"),e.querySelectorAll("[id~="+b+"-]").length||g.push("~="),e.querySelectorAll(":checked").length||g.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||g.push(".#.+[+~]")}),assert(function(e){var t=p.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&g.push("name"+U+"*[*^$|!~]?="),e.querySelectorAll(":enabled").length||g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(n.matchesSelector=X.test(f=m.matches||m.webkitMatchesSelector||m.mozMatchesSelector||m.oMatchesSelector||m.msMatchesSelector))&&assert(function(e){n.disconnectedMatch=f.call(e,"div"),f.call(e,"[s!='']:x"),S.push("!=",G)}),g=g.length&&new RegExp(g.join("|")),S=S.length&&new RegExp(S.join("|")),t=X.test(m.compareDocumentPosition),T=t||X.test(m.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},y=t?function(e,t){if(e===t)return d=!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===p||e.ownerDocument===h&&T(h,e)?-1:t===p||t.ownerDocument===h&&T(h,t)?1:_?P(_,e)-P(_,t):0:4&r?-1:1)}:function(e,t){if(e===t)return d=!0,0;var n,r=0,a=e.parentNode,i=t.parentNode,o=[e],s=[t];if(!a||!i)return e===p?-1:t===p?1:a?-1:i?1:_?P(_,e)-P(_,t):0;if(a===i)return siblingCheck(e,t);for(n=e;n=n.parentNode;)o.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;o[r]===s[r];)r++;return r?siblingCheck(o[r],s[r]):o[r]===h?-1:s[r]===h?1:0},p):p},Sizzle.matches=function(e,t){return Sizzle(e,null,null,t)},Sizzle.matchesSelector=function(e,t){if((e.ownerDocument||e)!==p&&u(e),t=t.replace(z,"='$1']"),n.matchesSelector&&E&&!v[t+" "]&&(!S||!S.test(t))&&(!g||!g.test(t)))try{var r=f.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return Sizzle(t,p,null,[e]).length>0},Sizzle.contains=function(e,t){return(e.ownerDocument||e)!==p&&u(e),T(e,t)},Sizzle.attr=function(e,t){(e.ownerDocument||e)!==p&&u(e);var a=r.attrHandle[t.toLowerCase()],i=a&&A.call(r.attrHandle,t.toLowerCase())?a(e,t,!E):void 0;return void 0!==i?i:n.attributes||!E?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=[],a=0,i=0;if(d=!n.detectDuplicates,_=!n.sortStable&&e.slice(0),e.sort(y),d){for(;t=e[i++];)t===e[i]&&(a=r.push(i));for(;a--;)e.splice(r[a],1)}return _=null,e},a=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+=a(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r++];)n+=a(t);return n},r=Sizzle.selectors={cacheLength:50,createPseudo:markFunction,match:Q,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 Q.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&W.test(n)&&(t=o(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=O[e+" "];return t||(t=new RegExp("(^|"+U+")"+e+"("+U+"|$)"))&&O(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 a=Sizzle.attr(r,e);return null==a?"!="===t:!t||(a+="","="===t?a===n:"!="===t?a!==n:"^="===t?n&&0===a.indexOf(n):"*="===t?n&&a.indexOf(n)>-1:"$="===t?n&&a.slice(-n.length)===n:"~="===t?(" "+a.replace(Y," ")+" ").indexOf(n)>-1:"|="===t&&(a===n||a.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,a){var i="nth"!==e.slice(0,3),o="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===a?function(e){return!!e.parentNode}:function(t,n,l){var c,_,d,u,p,m,E=i!==o?"nextSibling":"previousSibling",g=t.parentNode,S=s&&t.nodeName.toLowerCase(),f=!l&&!s,T=!1;if(g){if(i){for(;E;){for(u=t;u=u[E];)if(s?u.nodeName.toLowerCase()===S:1===u.nodeType)return!1;m=E="only"===e&&!m&&"nextSibling"}return!0}if(m=[o?g.firstChild:g.lastChild],o&&f){for(u=g,d=u[b]||(u[b]={}),_=d[u.uniqueID]||(d[u.uniqueID]={}),c=_[e]||[],p=c[0]===C&&c[1],T=p&&c[2],u=p&&g.childNodes[p];u=++p&&u&&u[E]||(T=p=0)||m.pop();)if(1===u.nodeType&&++T&&u===t){_[e]=[C,p,T];break}}else if(f&&(u=t,d=u[b]||(u[b]={}),_=d[u.uniqueID]||(d[u.uniqueID]={}),c=_[e]||[],p=c[0]===C&&c[1],T=p),!1===T)for(;(u=++p&&u&&u[E]||(T=p=0)||m.pop())&&((s?u.nodeName.toLowerCase()!==S:1!==u.nodeType)||!++T||(f&&(d=u[b]||(u[b]={}),_=d[u.uniqueID]||(d[u.uniqueID]={}),_[e]=[C,T]),u!==t)););return(T-=a)===r||T%r==0&&T/r>=0}}},PSEUDO:function(e,t){var n,a=r.pseudos[e]||r.setFilters[e.toLowerCase()]||Sizzle.error("unsupported pseudo: "+e);return a[b]?a(t):a.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?markFunction(function(e,n){for(var r,i=a(e,t),o=i.length;o--;)r=P(e,i[o]),e[r]=!(n[r]=i[o])}):function(e){return a(e,0,n)}):a}},pseudos:{not:markFunction(function(e){var t=[],n=[],r=s(e.replace(H,"$1"));return r[b]?markFunction(function(e,t,n,a){for(var i,o=r(e,null,a,[]),s=e.length;s--;)(i=o[s])&&(e[s]=!(t[s]=i))}):function(e,a,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||a(t)).indexOf(e)>-1}}),lang:markFunction(function(e){return K.test(e||"")||Sizzle.error("unsupported lang: "+e),e=e.replace(te,ne).toLowerCase(),function(t){var n;do{if(n=E?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===m},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.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 j.test(e.nodeName)},input:function(e){return $.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[0]).type&&n.getById&&9===t.nodeType&&E&&r.relative[c[1].type]){if(!(t=(r.find.ID(_.matches[0].replace(te,ne),t)||[])[0]))return a;p&&(t=t.parentNode),e=e.slice(c.shift().value.length)}for(l=Q.needsContext.test(e)?0:c.length;l--&&(_=c[l],!r.relative[d=_.type]);)if((u=r.find[d])&&(i=u(_.matches[0].replace(te,ne),J.test(c[0].type)&&testContext(t.parentNode)||t))){if(c.splice(l,1),!(e=i.length&&toSelector(c)))return M.apply(a,i),a;break}}return(p||s(e,m))(i,t,!E,a,!t||J.test(e)&&testContext(t.parentNode)||t),a},n.sortStable=b.split("").sort(y).join("")===b,n.detectDuplicates=!!d,u(),n.sortDetached=assert(function(e){return 1&e.compareDocumentPosition(p.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(k,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);g.find=h,g.expr=h.selectors,g.expr[":"]=g.expr.pseudos,g.uniqueSort=g.unique=h.uniqueSort,g.text=h.getText,g.isXMLDoc=h.isXML,g.contains=h.contains;var C=function(e,t,n){for(var r=[],a=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(a&&g(e).is(n))break;r.push(e)}return r},N=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},O=g.expr.match.needsContext,R=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;g.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?g.find.matchesSelector(r,e)?[r]:[]:g.find.matches(e,g.grep(t,function(e){return 1===e.nodeType}))},g.fn.extend({find:function(e){var t,n=this.length,r=[],a=this;if("string"!=typeof e)return this.pushStack(g(e).filter(function(){for(t=0;t1?g.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&&O.test(e)?g(e):e||[],!1).length}});var y,I=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/;(g.fn.init=function(e,t,n){var r,a;if(!e)return this;if(n=n||y,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:I.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 g?t[0]:t,g.merge(this,g.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:s,!0)),R.test(r[1])&&g.isPlainObject(t))for(r in t)g.isFunction(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return a=s.getElementById(r[2]),a&&a.parentNode&&(this.length=1,this[0]=a),this.context=s,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):g.isFunction(e)?void 0!==n.ready?n.ready(e):e(g):(void 0!==e.selector&&(this.selector=e.selector,this.context=e.context),g.makeArray(e,this))}).prototype=g.fn,y=g(s);var A=/^(?:parents|prev(?:Until|All))/,D={children:!0,contents:!0,next:!0,prev:!0};g.fn.extend({has:function(e){var t=g(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&g.find.matchesSelector(n,e))){i.push(n);break}return this.pushStack(i.length>1?g.uniqueSort(i):i)},index:function(e){return e?"string"==typeof e?d.call(g(e),this[0]):d.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(g.uniqueSort(g.merge(this.get(),g(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),g.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return C(e,"parentNode")},parentsUntil:function(e,t,n){return C(e,"parentNode",n)},next:function(e){return sibling(e,"nextSibling")},prev:function(e){return sibling(e,"previousSibling")},nextAll:function(e){return C(e,"nextSibling")},prevAll:function(e){return C(e,"previousSibling")},nextUntil:function(e,t,n){return C(e,"nextSibling",n)},prevUntil:function(e,t,n){return C(e,"previousSibling",n)},siblings:function(e){return N((e.parentNode||{}).firstChild,e)},children:function(e){return N(e.firstChild)},contents:function(e){return e.contentDocument||g.merge([],e.childNodes)}},function(e,t){g.fn[e]=function(n,r){var a=g.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(a=g.filter(r,a)),this.length>1&&(D[e]||g.uniqueSort(a),A.test(e)&&a.reverse()),this.pushStack(a)}});var x=/\S+/g;g.Callbacks=function(e){e="string"==typeof e?createOptions(e):g.extend({},e);var t,n,r,a,i=[],o=[],s=-1,l=function(){for(a=e.once,r=t=!0;o.length;s=-1)for(n=o.shift();++s-1;)i.splice(n,1),n<=s&&s--}),this},has:function(e){return e?g.inArray(e,i)>-1:i.length>0},empty:function(){return i&&(i=[]),this},disable:function(){return a=o=[],i=n="",this},disabled:function(){return!i},lock:function(){return a=o=[],n||(i=n=""),this},locked:function(){return!!a},fireWith:function(e,n){return a||(n=n||[],n=[e,n.slice?n.slice():n],o.push(n),t||l()),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!r}};return c},g.extend({Deferred:function(e){var t=[["resolve","done",g.Callbacks("once memory"),"resolved"],["reject","fail",g.Callbacks("once memory"),"rejected"],["notify","progress",g.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return a.done(arguments).fail(arguments),this},then:function(){var e=arguments;return g.Deferred(function(n){g.each(t,function(t,i){var o=g.isFunction(e[t])&&e[t];a[i[1]](function(){var e=o&&o.apply(this,arguments);e&&g.isFunction(e.promise)?e.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[i[0]+"With"](this===r?n.promise():this,o?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?g.extend(e,r):r}},a={};return r.pipe=r.then,g.each(t,function(e,i){var o=i[2],s=i[3];r[i[1]]=o.add,s&&o.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),a[i[0]]=function(){return a[i[0]+"With"](this===a?r:this,arguments),this},a[i[0]+"With"]=o.fireWith}),r.promise(a),e&&e.call(a,a),a},when:function(e){var t,n,r,a=0,i=l.call(arguments),o=i.length,s=1!==o||e&&g.isFunction(e.promise)?o:0,c=1===s?e:g.Deferred(),_=function(e,n,r){return function(a){n[e]=this,r[e]=arguments.length>1?l.call(arguments):a,r===t?c.notifyWith(n,r):--s||c.resolveWith(n,r)}};if(o>1)for(t=new Array(o),n=new Array(o),r=new Array(o);a0||(w.resolveWith(s,[g]),g.fn.triggerHandler&&(g(s).triggerHandler("ready"),g(s).off("ready"))))}}),g.ready.promise=function(e){return w||(w=g.Deferred(),"complete"===s.readyState||"loading"!==s.readyState&&!s.documentElement.doScroll?n.setTimeout(g.ready):(s.addEventListener("DOMContentLoaded",completed),n.addEventListener("load",completed))),w.promise(e)},g.ready.promise();var M=function(e,t,n,r,a,i,o){var s=0,l=e.length,c=null==n;if("object"===g.type(n)){a=!0;for(s in n)M(e,t,s,n[s],!0,i,o)}else if(void 0!==r&&(a=!0,g.isFunction(r)||(o=!0),c&&(o?(t.call(e,r),t=null):(c=t,t=function(e,t,n){return c.call(g(e),n)})),t))for(;s-1&&void 0!==n&&k.set(this,e,t)})},null,t,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){k.remove(this,e)})}}),g.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=P.get(e,t),n&&(!r||g.isArray(n)?r=P.access(e,t,g.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=g.queue(e,t),r=n.length,a=n.shift(),i=g._queueHooks(e,t),o=function(){g.dequeue(e,t)};"inprogress"===a&&(a=n.shift(),r--),a&&("fx"===t&&n.unshift("inprogress"),delete i.stop,a.call(e,o,i)),!r&&i&&i.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return P.get(e,n)||P.access(e,n,{empty:g.Callbacks("once memory").add(function(){P.remove(e,[t+"queue",n])})})}}),g.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,"",""]};W.optgroup=W.option,W.tbody=W.tfoot=W.colgroup=W.caption=W.thead,W.th=W.td;var K=/<|&#?\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),E.checkClone=t.cloneNode(!0).cloneNode(!0).lastChild.checked,t.innerHTML="",E.noCloneChecked=!!t.cloneNode(!0).lastChild.defaultValue}();var Q=/^key/,$=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,j=/^([^.]*)(?:\.(.+)|)/;g.event={global:{},add:function(e,t,n,r,a){var i,o,s,l,c,_,d,u,p,m,E,S=P.get(e);if(S)for(n.handler&&(i=n,n=i.handler,a=i.selector),n.guid||(n.guid=g.guid++),(l=S.events)||(l=S.events={}),(o=S.handle)||(o=S.handle=function(t){return void 0!==g&&g.event.triggered!==t.type?g.event.dispatch.apply(e,arguments):void 0}),t=(t||"").match(x)||[""],c=t.length;c--;)s=j.exec(t[c])||[],p=E=s[1],m=(s[2]||"").split(".").sort(),p&&(d=g.event.special[p]||{},p=(a?d.delegateType:d.bindType)||p,d=g.event.special[p]||{},_=g.extend({type:p,origType:E,data:r,handler:n,guid:n.guid,selector:a,needsContext:a&&g.expr.match.needsContext.test(a),namespace:m.join(".")},i),(u=l[p])||(u=l[p]=[],u.delegateCount=0,d.setup&&!1!==d.setup.call(e,r,m,o)||e.addEventListener&&e.addEventListener(p,o)),d.add&&(d.add.call(e,_),_.handler.guid||(_.handler.guid=n.guid)),a?u.splice(u.delegateCount++,0,_):u.push(_),g.event.global[p]=!0)},remove:function(e,t,n,r,a){var i,o,s,l,c,_,d,u,p,m,E,S=P.hasData(e)&&P.get(e);if(S&&(l=S.events)){for(t=(t||"").match(x)||[""],c=t.length;c--;)if(s=j.exec(t[c])||[],p=E=s[1],m=(s[2]||"").split(".").sort(),p){for(d=g.event.special[p]||{},p=(r?d.delegateType:d.bindType)||p,u=l[p]||[],s=s[2]&&new RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"),o=i=u.length;i--;)_=u[i],!a&&E!==_.origType||n&&n.guid!==_.guid||s&&!s.test(_.namespace)||r&&r!==_.selector&&("**"!==r||!_.selector)||(u.splice(i,1),_.selector&&u.delegateCount--,d.remove&&d.remove.call(e,_));o&&!u.length&&(d.teardown&&!1!==d.teardown.call(e,m,S.handle)||g.removeEvent(e,p,S.handle),delete l[p])}else for(p in l)g.event.remove(e,p+t[c],n,r,!0);g.isEmptyObject(l)&&P.remove(e,"handle events")}},dispatch:function(e){e=g.event.fix(e);var t,n,r,a,i,o=[],s=l.call(arguments),c=(P.get(this,"events")||{})[e.type]||[],_=g.event.special[e.type]||{};if(s[0]=e,e.delegateTarget=this,!_.preDispatch||!1!==_.preDispatch.call(this,e)){for(o=g.event.handlers.call(this,e,c),t=0;(a=o[t++])&&!e.isPropagationStopped();)for(e.currentTarget=a.elem,n=0;(i=a.handlers[n++])&&!e.isImmediatePropagationStopped();)e.rnamespace&&!e.rnamespace.test(i.namespace)||(e.handleObj=i,e.data=i.data,void 0!==(r=((g.event.special[i.origType]||{}).handle||i.handler).apply(a.elem,s))&&!1===(e.result=r)&&(e.preventDefault(),e.stopPropagation()));return _.postDispatch&&_.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,a,i,o=[],s=t.delegateCount,l=e.target;if(s&&l.nodeType&&("click"!==e.type||isNaN(e.button)||e.button<1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&(!0!==l.disabled||"click"!==e.type)){for(r=[],n=0;n-1:g.find(a,this,null,[l]).length),r[a]&&r.push(i);r.length&&o.push({elem:l,handlers:r})}return s]*)\/>/gi,Z=/\s*$/g;g.extend({htmlPrefilter:function(e){return e.replace(X,"<$1>")},clone:function(e,t,n){var r,a,i,o,s=e.cloneNode(!0),l=g.contains(e.ownerDocument,e);if(!(E.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||g.isXMLDoc(e)))for(o=getAll(s),i=getAll(e),r=0,a=i.length;r0&&setGlobalEval(o,!l&&getAll(e,"script")),s},cleanData:function(e){for(var t,n,r,a=g.event.special,i=0;void 0!==(n=e[i]);i++)if(L(n)){if(t=n[P.expando]){if(t.events)for(r in t.events)a[r]?g.event.remove(n,r):g.removeEvent(n,r,t.handle);n[P.expando]=void 0}n[k.expando]&&(n[k.expando]=void 0)}}}),g.fn.extend({domManip:domManip,detach:function(e){return remove(this,e,!0)},remove:function(e){return remove(this,e)},text:function(e){return M(this,function(e){return void 0===e?g.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&&(g.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 g.clone(this,e,t)})},html:function(e){return M(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&&!Z.test(e)&&!W[(V.exec(e)||["",""])[1].toLowerCase()]){e=g.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(){H(this)?g(this).show():g(this).hide()})}}),g.Tween=Tween,Tween.prototype={constructor:Tween,init:function(e,t,n,r,a,i){this.elem=e,this.prop=n,this.easing=a||g.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=i||(g.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=g.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=g.css(e.elem,e.prop,""),t&&"auto"!==t?t:0)},set:function(e){g.fx.step[e.prop]?g.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[g.cssProps[e.prop]]&&!g.cssHooks[e.prop]?e.elem[e.prop]=e.now:g.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)}},g.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},g.fx=Tween.prototype.init,g.fx.step={};var me,Ee,ge=/^(?:toggle|show|hide)$/,Se=/queueHooks$/;g.Animation=g.extend(Animation,{tweeners:{"*":[function(e,t){var n=this.createTween(e,t);return adjustCSS(n.elem,e,G.exec(t),n),n}]},tweener:function(e,t){g.isFunction(e)?(t=e,e=["*"]):e=e.match(x);for(var n,r=0,a=e.length;r1)},removeAttr:function(e){return this.each(function(){g.removeAttr(this,e)})}}),g.extend({attr:function(e,t,n){var r,a,i=e.nodeType;if(3!==i&&8!==i&&2!==i)return void 0===e.getAttribute?g.prop(e,t,n):(1===i&&g.isXMLDoc(e)||(t=t.toLowerCase(),a=g.attrHooks[t]||(g.expr.match.bool.test(t)?fe:void 0)),void 0!==n?null===n?void g.removeAttr(e,t):a&&"set"in a&&void 0!==(r=a.set(e,n,t))?r:(e.setAttribute(t,n+""),n):a&&"get"in a&&null!==(r=a.get(e,t))?r:(r=g.find.attr(e,t),null==r?void 0:r))},attrHooks:{type:{set:function(e,t){if(!E.radioValue&&"radio"===t&&g.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r,a=0,i=t&&t.match(x);if(i&&1===e.nodeType)for(;n=i[a++];)r=g.propFix[n]||n,g.expr.match.bool.test(n)&&(e[r]=!1),e.removeAttribute(n)}}),fe={set:function(e,t,n){return!1===t?g.removeAttr(e,n):e.setAttribute(n,n),n}},g.each(g.expr.match.bool.source.match(/\w+/g),function(e,t){var n=Te[t]||g.find.attr;Te[t]=function(e,t,r){var a,i;return r||(i=Te[t],Te[t]=a,a=null!=n(e,t,r)?t.toLowerCase():null,Te[t]=i),a}});var be=/^(?:input|select|textarea|button)$/i,he=/^(?:a|area)$/i;g.fn.extend({prop:function(e,t){return M(this,g.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[g.propFix[e]||e]})}}),g.extend({prop:function(e,t,n){var r,a,i=e.nodeType;if(3!==i&&8!==i&&2!==i)return 1===i&&g.isXMLDoc(e)||(t=g.propFix[t]||t,a=g.propHooks[t]),void 0!==n?a&&"set"in a&&void 0!==(r=a.set(e,n,t))?r:e[t]=n:a&&"get"in a&&null!==(r=a.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=g.find.attr(e,"tabindex");return t?parseInt(t,10):be.test(e.nodeName)||he.test(e.nodeName)&&e.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),E.optSelected||(g.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)}}),g.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){g.propFix[this.toLowerCase()]=this});var Ce=/[\t\r\n\f]/g;g.fn.extend({addClass:function(e){var t,n,r,a,i,o,s,l=0;if(g.isFunction(e))return this.each(function(t){g(this).addClass(e.call(this,t,getClass(this)))});if("string"==typeof e&&e)for(t=e.match(x)||[];n=this[l++];)if(a=getClass(n),r=1===n.nodeType&&(" "+a+" ").replace(Ce," ")){for(o=0;i=t[o++];)r.indexOf(" "+i+" ")<0&&(r+=i+" ");s=g.trim(r),a!==s&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,a,i,o,s,l=0;if(g.isFunction(e))return this.each(function(t){g(this).removeClass(e.call(this,t,getClass(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof e&&e)for(t=e.match(x)||[];n=this[l++];)if(a=getClass(n),r=1===n.nodeType&&(" "+a+" ").replace(Ce," ")){for(o=0;i=t[o++];)for(;r.indexOf(" "+i+" ")>-1;)r=r.replace(" "+i+" "," ");s=g.trim(r),a!==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):g.isFunction(e)?this.each(function(n){g(this).toggleClass(e.call(this,n,getClass(this),t),t)}):this.each(function(){var t,r,a,i;if("string"===n)for(r=0,a=g(this),i=e.match(x)||[];t=i[r++];)a.hasClass(t)?a.removeClass(t):a.addClass(t);else void 0!==e&&"boolean"!==n||(t=getClass(this),t&&P.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":P.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;for(t=" "+e+" ";n=this[r++];)if(1===n.nodeType&&(" "+getClass(n)+" ").replace(Ce," ").indexOf(t)>-1)return!0;return!1}});var Ne=/\r/g,Oe=/[\x20\t\r\n\f]+/g;g.fn.extend({val:function(e){var t,n,r,a=this[0];{if(arguments.length)return r=g.isFunction(e),this.each(function(n){var a;1===this.nodeType&&(a=r?e.call(this,n,g(this).val()):e,null==a?a="":"number"==typeof a?a+="":g.isArray(a)&&(a=g.map(a,function(e){return null==e?"":e+""})),(t=g.valHooks[this.type]||g.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,a,"value")||(this.value=a))});if(a)return(t=g.valHooks[a.type]||g.valHooks[a.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(a,"value"))?n:(n=a.value,"string"==typeof n?n.replace(Ne,""):null==n?"":n)}}}),g.extend({valHooks:{option:{get:function(e){var t=g.find.attr(e,"value");return null!=t?t:g.trim(g.text(e)).replace(Oe," ")}},select:{get:function(e){for(var t,n,r=e.options,a=e.selectedIndex,i="select-one"===e.type||a<0,o=i?null:[],s=i?a+1:r.length,l=a<0?s:i?a:0;l-1)&&(n=!0);return n||(e.selectedIndex=-1),i}}}}),g.each(["radio","checkbox"],function(){g.valHooks[this]={set:function(e,t){if(g.isArray(t))return e.checked=g.inArray(g(e).val(),t)>-1}},E.checkOn||(g.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Re=/^(?:focusinfocus|focusoutblur)$/;g.extend(g.event,{trigger:function(e,t,r,a){var i,o,l,c,_,d,u,p=[r||s],E=m.call(e,"type")?e.type:e,S=m.call(e,"namespace")?e.namespace.split("."):[];if(o=l=r=r||s,3!==r.nodeType&&8!==r.nodeType&&!Re.test(E+g.event.triggered)&&(E.indexOf(".")>-1&&(S=E.split("."),E=S.shift(),S.sort()),_=E.indexOf(":")<0&&"on"+E,e=e[g.expando]?e:new g.Event(E,"object"==typeof e&&e),e.isTrigger=a?2:3,e.namespace=S.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+S.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=r),t=null==t?[e]:g.makeArray(t,[e]),u=g.event.special[E]||{},a||!u.trigger||!1!==u.trigger.apply(r,t))){if(!a&&!u.noBubble&&!g.isWindow(r)){for(c=u.delegateType||E,Re.test(c+E)||(o=o.parentNode);o;o=o.parentNode)p.push(o),l=o;l===(r.ownerDocument||s)&&p.push(l.defaultView||l.parentWindow||n)}for(i=0;(o=p[i++])&&!e.isPropagationStopped();)e.type=i>1?c:u.bindType||E,d=(P.get(o,"events")||{})[e.type]&&P.get(o,"handle"),d&&d.apply(o,t),(d=_&&o[_])&&d.apply&&L(o)&&(e.result=d.apply(o,t),!1===e.result&&e.preventDefault());return e.type=E,a||e.isDefaultPrevented()||u._default&&!1!==u._default.apply(p.pop(),t)||!L(r)||_&&g.isFunction(r[E])&&!g.isWindow(r)&&(l=r[_],l&&(r[_]=null),g.event.triggered=E,r[E](),g.event.triggered=void 0,l&&(r[_]=l)),e.result}},simulate:function(e,t,n){var r=g.extend(new g.Event,n,{type:e,isSimulated:!0});g.event.trigger(r,null,t)}}),g.fn.extend({trigger:function(e,t){return this.each(function(){g.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return g.event.trigger(e,t,n,!0)}}),g.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){g.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),g.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),E.focusin="onfocusin"in n,E.focusin||g.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){g.event.simulate(t,e.target,g.event.fix(e))};g.event.special[t]={setup:function(){var r=this.ownerDocument||this,a=P.access(r,t);a||r.addEventListener(e,n,!0),P.access(r,t,(a||0)+1)},teardown:function(){var r=this.ownerDocument||this,a=P.access(r,t)-1;a?P.access(r,t,a):(r.removeEventListener(e,n,!0),P.remove(r,t))}}});var ve=n.location,ye=g.now(),Ie=/\?/;g.parseJSON=function(e){return JSON.parse(e+"")},g.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||g.error("Invalid XML: "+e),t};var Ae=/#.*$/,De=/([?&])_=[^&]*/,xe=/^(.*?):[ \t]*([^\r\n]*)$/gm,we=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Me=/^(?:GET|HEAD)$/,Le=/^\/\//,Pe={},ke={},Ue="*/".concat("*"),Fe=s.createElement("a");Fe.href=ve.href,g.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:ve.href,type:"GET",isLocal:we.test(ve.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Ue,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":g.parseJSON,"text xml":g.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?ajaxExtend(ajaxExtend(e,g.ajaxSettings),t):ajaxExtend(g.ajaxSettings,e)},ajaxPrefilter:addToPrefiltersOrTransports(Pe),ajaxTransport:addToPrefiltersOrTransports(ke),ajax:function(e,t){function done(e,t,o,s){var c,d,T,b,C,O=t;2!==h&&(h=2,l&&n.clearTimeout(l),r=void 0,i=s||"",N.readyState=e>0?4:0,c=e>=200&&e<300||304===e,o&&(b=ajaxHandleResponses(u,N,o)),b=ajaxConvert(u,b,N,c),c?(u.ifModified&&(C=N.getResponseHeader("Last-Modified"),C&&(g.lastModified[a]=C),(C=N.getResponseHeader("etag"))&&(g.etag[a]=C)),204===e||"HEAD"===u.type?O="nocontent":304===e?O="notmodified":(O=b.state,d=b.data,T=b.error,c=!T)):(T=O,!e&&O||(O="error",e<0&&(e=0))),N.status=e,N.statusText=(t||O)+"",c?E.resolveWith(p,[d,O,N]):E.rejectWith(p,[N,O,T]),N.statusCode(f),f=void 0,_&&m.trigger(c?"ajaxSuccess":"ajaxError",[N,u,c?d:T]),S.fireWith(p,[N,O]),_&&(m.trigger("ajaxComplete",[N,u]),--g.active||g.event.trigger("ajaxStop")))}"object"==typeof e&&(t=e,e=void 0),t=t||{};var r,a,i,o,l,c,_,d,u=g.ajaxSetup({},t),p=u.context||u,m=u.context&&(p.nodeType||p.jquery)?g(p):g.event,E=g.Deferred(),S=g.Callbacks("once memory"),f=u.statusCode||{},T={},b={},h=0,C="canceled",N={readyState:0,getResponseHeader:function(e){var t;if(2===h){if(!o)for(o={};t=xe.exec(i);)o[t[1].toLowerCase()]=t[2];t=o[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===h?i:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return h||(e=b[n]=b[n]||e,T[e]=t),this},overrideMimeType:function(e){return h||(u.mimeType=e),this},statusCode:function(e){var t;if(e)if(h<2)for(t in e)f[t]=[f[t],e[t]];else N.always(e[N.status]);return this},abort:function(e){var t=e||C;return r&&r.abort(t),done(0,t),this}};if(E.promise(N).complete=S.add,N.success=N.done,N.error=N.fail,u.url=((e||u.url||ve.href)+"").replace(Ae,"").replace(Le,ve.protocol+"//"),u.type=t.method||t.type||u.method||u.type,u.dataTypes=g.trim(u.dataType||"*").toLowerCase().match(x)||[""],null==u.crossDomain){c=s.createElement("a");try{c.href=u.url,c.href=c.href,u.crossDomain=Fe.protocol+"//"+Fe.host!=c.protocol+"//"+c.host}catch(e){u.crossDomain=!0}}if(u.data&&u.processData&&"string"!=typeof u.data&&(u.data=g.param(u.data,u.traditional)),inspectPrefiltersOrTransports(Pe,u,t,N),2===h)return N;_=g.event&&u.global,_&&0==g.active++&&g.event.trigger("ajaxStart"),u.type=u.type.toUpperCase(),u.hasContent=!Me.test(u.type),a=u.url,u.hasContent||(u.data&&(a=u.url+=(Ie.test(a)?"&":"?")+u.data,delete u.data),!1===u.cache&&(u.url=De.test(a)?a.replace(De,"$1_="+ye++):a+(Ie.test(a)?"&":"?")+"_="+ye++)),u.ifModified&&(g.lastModified[a]&&N.setRequestHeader("If-Modified-Since",g.lastModified[a]),g.etag[a]&&N.setRequestHeader("If-None-Match",g.etag[a])),(u.data&&u.hasContent&&!1!==u.contentType||t.contentType)&&N.setRequestHeader("Content-Type",u.contentType),N.setRequestHeader("Accept",u.dataTypes[0]&&u.accepts[u.dataTypes[0]]?u.accepts[u.dataTypes[0]]+("*"!==u.dataTypes[0]?", "+Ue+"; q=0.01":""):u.accepts["*"]);for(d in u.headers)N.setRequestHeader(d,u.headers[d]);if(u.beforeSend&&(!1===u.beforeSend.call(p,N,u)||2===h))return N.abort();C="abort";for(d in{success:1,error:1,complete:1})N[d](u[d]);if(r=inspectPrefiltersOrTransports(ke,u,t,N)){if(N.readyState=1,_&&m.trigger("ajaxSend",[N,u]),2===h)return N;u.async&&u.timeout>0&&(l=n.setTimeout(function(){N.abort("timeout")},u.timeout));try{h=1,r.send(T,done)}catch(e){if(!(h<2))throw e;done(-1,e)}}else done(-1,"No Transport");return N},getJSON:function(e,t,n){return g.get(e,t,n,"json")},getScript:function(e,t){return g.get(e,void 0,t,"script")}}),g.each(["get","post"],function(e,t){g[t]=function(e,n,r,a){return g.isFunction(n)&&(a=a||r,r=n,n=void 0),g.ajax(g.extend({url:e,type:t,dataType:a,data:n,success:r},g.isPlainObject(e)&&e))}}),g._evalUrl=function(e){return g.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,throws:!0})},g.fn.extend({wrapAll:function(e){var t;return g.isFunction(e)?this.each(function(t){g(this).wrapAll(e.call(this,t))}):(this[0]&&(t=g(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 g.isFunction(e)?this.each(function(t){g(this).wrapInner(e.call(this,t))}):this.each(function(){var t=g(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g.isFunction(e);return this.each(function(n){g(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){g.nodeName(this,"body")||g(this).replaceWith(this.childNodes)}).end()}}),g.expr.filters.hidden=function(e){return!g.expr.filters.visible(e)},g.expr.filters.visible=function(e){return e.offsetWidth>0||e.offsetHeight>0||e.getClientRects().length>0};var Be=/%20/g,Ge=/\[\]$/,Ye=/\r?\n/g,He=/^(?:submit|button|image|reset|file)$/i,qe=/^(?:input|select|textarea|keygen)/i;g.param=function(e,t){var n,r=[],a=function(e,t){t=g.isFunction(t)?t():null==t?"":t,r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(void 0===t&&(t=g.ajaxSettings&&g.ajaxSettings.traditional),g.isArray(e)||e.jquery&&!g.isPlainObject(e))g.each(e,function(){a(this.name,this.value)});else for(n in e)buildParams(n,e[n],t,a);return r.join("&").replace(Be,"+")},g.fn.extend({serialize:function(){return g.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=g.prop(this,"elements");return e?g.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!g(this).is(":disabled")&&qe.test(this.nodeName)&&!He.test(e)&&(this.checked||!q.test(e))}).map(function(e,t){var n=g(this).val();return null==n?null:g.isArray(n)?g.map(n,function(e){return{name:t.name,value:e.replace(Ye,"\r\n")}}):{name:t.name,value:n.replace(Ye,"\r\n")}}).get()}}),g.ajaxSettings.xhr=function(){try{return new n.XMLHttpRequest}catch(e){}};var Ve={0:200,1223:204},ze=g.ajaxSettings.xhr();E.cors=!!ze&&"withCredentials"in ze,E.ajax=ze=!!ze,g.ajaxTransport(function(e){var t,r;if(E.cors||ze&&!e.crossDomain)return{send:function(a,i){var o,s=e.xhr();if(s.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(o in e.xhrFields)s[o]=e.xhrFields[o];e.mimeType&&s.overrideMimeType&&s.overrideMimeType(e.mimeType),e.crossDomain||a["X-Requested-With"]||(a["X-Requested-With"]="XMLHttpRequest");for(o in a)s.setRequestHeader(o,a[o]);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(Ve[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()}}}),g.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 g.globalEval(e),e}}}),g.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),g.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(r,a){t=g("