diff --git a/.editorconfig b/.editorconfig index 6553d30fd..140440443 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,5 +13,5 @@ insert_final_newline = true indent_style = space indent_size = 2 -[Makefile] +[*akefile] indent_style = tab diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 920f19055..d55992067 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,11 +1,41 @@ # How to contribute +## Test it locally + +### Using Docker + +- Clone the repository +- Ensure your Docker daemon is running +- Launch `docker-compose up` + +You'll then have: +- a web server (nginx) +- a PHP daemon (using FPM) +- a Redis database (to handle imports) +- a SQLite database to store articles + +You can now access your wallabag instance using that url: `http://127.0.0.1:8000` + +If you want to test using an other database than SQLite, uncomment the `postgres` or `mariadb` code from the `docker-compose.yml` file at the root of the repo. Also uncomment related line in the `php` section so the database will be linked to your PHP instance. + +### Using your own PHP server + +- Ensure you are running PHP > 7.1. +- Clone the repository +- Launch `composer install` +- If you got some errors, fix them (they might be related to some missing PHP extension from your machine) +- Then `php bin/console wallabag:install` +- If you got some errors, fix them (they might be related to some missing PHP extension from your machine) +- Run `php bin/console server:run` + +You can now access your wallabag instance using that url: `http://127.0.0.1:8000` + ## You found a bug Please [open a new issue](https://github.com/wallabag/wallabag/issues/new). To fix the bug quickly, we need some infos: please answer to the questions in the issue form. -If you have the skills, look for errors into php, server and application (see `var/logs`) logs. +If you have the skills, look for errors into PHP, server and application logs (see `var/logs`). Note : If you have large portions of text, use [Github's Gist service](https://gist.github.com/) or other pastebin-like. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..3081fc9de --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +# github: [nicosomb, j0k3r, tcitworld, Kdecherf] +liberapay: wallabag diff --git a/.gitignore b/.gitignore index 472c7ad94..797e0496c 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ web/uploads/ # Build /app/build /build +/coverage # Composer PHAR /composer.phar @@ -43,9 +44,6 @@ data/db/wallabag*.sqlite docker/logs/ docker/data/ -# To avoid crazy stuff on some PR, we must manually FORCE ADD IT on each new release -composer.lock - # assets stuff node_modules/ bin @@ -57,3 +55,6 @@ app/Resources/build/ admin-export.json specialexport.json /data/site-credentials-secret-key.txt + +# Custom CSS file +web/custom.css diff --git a/.travis.yml b/.travis.yml index 393063439..229e3e0ef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,11 @@ language: php services: + - mysql + - postgresql - rabbitmq - redis -# used for HHVM -addons: - apt: - packages: - - tidy - # cache vendor dirs cache: apt: true @@ -21,10 +17,10 @@ cache: - $HOME/.yarn-cache php: - - 5.6 - - 7.0 - 7.1 - 7.2 + - 7.3 + - 7.4 - nightly node_js: @@ -38,9 +34,10 @@ 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: 7.4 - php: nightly # exclude v1 branches @@ -58,31 +55,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; + # 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 f90cd8096..000000000 --- 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/CHANGELOG.md b/CHANGELOG.md index 52988f859..e418d3e27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,53 @@ # Changelog +## [2.3.8](https://github.com/wallabag/wallabag/tree/2.3.8) + [Full Changelog](https://github.com/wallabag/wallabag/compare/2.3.7...2.3.8) + +### Fixes + +- Jump to 2.3.8-dev [#3897](https://github.com/wallabag/wallabag/pull/3897) +- material: fix left padding on non-entry pages [#3901](https://github.com/wallabag/wallabag/pull/3901) +- Make dev/install/update script posix compatible [#3860](https://github.com/wallabag/wallabag/pull/3860) +- epub: fix exception when articles have the same title [#3908](https://github.com/wallabag/wallabag/pull/3908) +- Fix PHP warning [#3909](https://github.com/wallabag/wallabag/pull/3909) +- Add ability to match many domains for credentials [#3937](https://github.com/wallabag/wallabag/pull/3937) +- material: add metadata to list view [#3942](https://github.com/wallabag/wallabag/pull/3942) +- Enable no-referrer on img tags, enable strict-origin-when-cross-origin by default [#3943](https://github.com/wallabag/wallabag/pull/3943) +- Remove preview picture from share view page#3922 +- Fix Intl Locale issue [#3964](https://github.com/wallabag/wallabag/pull/3964) + +## [2.3.7](https://github.com/wallabag/wallabag/tree/2.3.7) + [Full Changelog](https://github.com/wallabag/wallabag/compare/2.3.6...2.3.7) + +### Fixes + +- Jump to 2.3.7-dev [#3837](https://github.com/wallabag/wallabag/pull/3837) +- Fix bad order parameter in the API [#3841](https://github.com/wallabag/wallabag/pull/3841) +- Update composer.json to add php-tidy (ext-tidy) [#3853](https://github.com/wallabag/wallabag/pull/3853) +- Add dedicated email for site config issue [#3861](https://github.com/wallabag/wallabag/pull/3861) +- Fix read & starred status in Pocket import [#3819](https://github.com/wallabag/wallabag/pull/3819) +- Fix broken 2 factor auth logo image [#3869](https://github.com/wallabag/wallabag/pull/3869) +- Fix CORS for API [#3882](https://github.com/wallabag/wallabag/pull/3882) +- Add support of expect parameter to change return object when deleting entry [#3887](https://github.com/wallabag/wallabag/pull/3887) +- epub export: fix missing cover image, only for exports of one article [#3886](https://github.com/wallabag/wallabag/pull/3886) +- Allow optional --ignore-root-warning [#3885](https://github.com/wallabag/wallabag/pull/3885) +- material: fix left padding of content on medium screens [#3893](https://github.com/wallabag/wallabag/pull/3893) +- material: hide creation date from card actions on specific sizes [#3894](https://github.com/wallabag/wallabag/pull/3894) + +## [2.3.6](https://github.com/wallabag/wallabag/tree/2.3.6) + [Full Changelog](https://github.com/wallabag/wallabag/compare/2.3.5...2.3.6) + +### Fixes + +- Jump to 2.3.6-dev and update release process [#3814](https://github.com/wallabag/wallabag/pull/3814) +- Fix tag API leak [#3823](https://github.com/wallabag/wallabag/pull/3823) +- Validate imported entry to avoid error on import [#3816](https://github.com/wallabag/wallabag/pull/3816) +- Fix incorrect reading time calculation for entries with CJK characters [#3820](https://github.com/wallabag/wallabag/pull/3820) +- EntriesExport/epub: replace epub identifier with unique urn [#3827](https://github.com/wallabag/wallabag/pull/3827) +- Fix settings field inverted [#3833](https://github.com/wallabag/wallabag/pull/3833) +- Cast client id to avoid PG error [#3831](https://github.com/wallabag/wallabag/pull/3831) +- Rework of EPUB/PDF exports [#3826](https://github.com/wallabag/wallabag/pull/3826) + ## [2.3.5](https://github.com/wallabag/wallabag/tree/2.3.5) [Full Changelog](https://github.com/wallabag/wallabag/compare/2.3.4...2.3.5) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..61bfbe323 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at hello@wallabag.org. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/COPYING.md b/COPYING.md index 6be863d32..72b9d5d0f 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/GNUmakefile b/GNUmakefile index a04468cb1..837f7103f 100755 --- a/GNUmakefile +++ b/GNUmakefile @@ -2,7 +2,14 @@ SHELL=bash TMP_FOLDER=/tmp RELEASE_FOLDER=wllbg-release -ENV ?= prod +# ensure the ENV variable is well defined +AVAILABLE_ENV := prod dev test +ifneq ($(filter $(ENV),$(AVAILABLE_ENV)),) + # all good +else + # not good, force it to "prod" + override ENV = prod +endif help: ## Display this help menu @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' @@ -10,21 +17,26 @@ help: ## Display this help menu clean: ## Clear the application cache rm -rf var/cache/* -install: ## Install wallabag with the latest version +install: customcss ## Install wallabag with the latest version @./scripts/install.sh $(ENV) update: ## Update the wallabag installation to the latest version @./scripts/update.sh $(ENV) -dev: ## Install the latest dev version +dev: ENV=dev +dev: build customcss ## Install the latest dev version @./scripts/dev.sh run: ## Run the wallabag built-in server @php bin/console server:run --env=dev build: ## Run webpack + @npm install @npm run build:$(ENV) +customcss: + @touch web/custom.css + prepare: clean ## Prepare database for testsuite ifdef DB cp app/config/tests/parameters_test.$(DB).yml app/config/parameters_test.yml diff --git a/README.md b/README.md index 57392da2d..e00c7ea06 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/RELEASE_PROCESS.md b/RELEASE_PROCESS.md index 829f8390c..881f07710 100644 --- a/RELEASE_PROCESS.md +++ b/RELEASE_PROCESS.md @@ -22,58 +22,42 @@ During this documentation, we assume the release is `$LAST_WALLABAG_RELEASE` (li git checkout master git pull origin master git checkout -b release-$LAST_WALLABAG_RELEASE -SYMFONY_ENV=prod composer up --no-dev -``` - -- Update `.travis.yml` file and replace the composer line with this one: - -```diff -script: -- - travis_wait bash composer install -o --no-interaction --no-progress --prefer-dist -+ - travis_wait bash composer update -o --no-interaction --no-progress --prefer-dist +composer up ``` - Then continue with these commands: ``` -git add --force composer.lock .travis.yml +git add composer.lock git commit -m "Release wallabag $LAST_WALLABAG_RELEASE" git push origin release-$LAST_WALLABAG_RELEASE ``` -- Create a new pull request with this title `DON'T MERGE Release wallabag $LAST_WALLABAG_RELEASE`. This pull request is used to launch builds on Travis-CI. -- Run these command to create the package: +- Create a new pull request with this title `Release wallabag $LAST_WALLABAG_RELEASE`. This pull request is used to launch builds on Travis-CI. +- Once PR is green, merge it and delete the branch. +- Run this command to create the package: ``` make release VERSION=$LAST_WALLABAG_RELEASE ``` -- [Create the new release on GitHub](https://github.com/wallabag/wallabag/releases/new) by targetting the `release-$LAST_WALLABAG_RELEASE` branch. You have to upload the package (generated previously). -- Close the previously created pull request (**DO NOT MERGE IT**) and delete the `release-$LAST_WALLABAG_RELEASE` branch. -- Update the URL shortener (used on `wllbg.org` to generate links like `https://wllbg.org/latest-v2-package` or `http://wllbg.org/latest-v2`) +- [Create the new release on GitHub](https://github.com/wallabag/wallabag/releases/new) by targetting the `master` branch or any appropriate branch (for instance backports). You have to upload the package (generated previously). +- Update the URL shortener (used on `wllbg.org` to update links like `https://wllbg.org/latest-v2-package` or `http://wllbg.org/latest-v2`) - Update Dockerfile https://github.com/wallabag/docker (and create a new tag) - Update wallabag.org website (downloads, MD5 sum, releases and new blog post) - Put the next patch version suffixed with `-dev` in `app/config/wallabag.yml` (`wallabag_core.version`) - Drink a :beer:! -### `composer.lock` -A release tag must contain a `composer.lock` file. It sets which dependencies were available at the time a release was done, -making it easier to fix issues after the release. It also speeds up `composer install` on stable versions a LOT, by skipping the -dependencies resolution part. - -Since `composer.lock` is ignored by default, either it must be removed from `.gitignore` _in the release branch_, -or it must be added using `git add --force composer.lock`. - ### Target PHP version `composer.lock` is _always_ built for a particular version, by default the one it is generated (with `composer update`). -If the PHP version used to generate the .lock isn't a widely available one (like PHP 7), a more common one should +If the PHP version used to generate the .lock isn't a widely available one (like PHP 8), a more common one should be locally specified in `composer.lock`: ```json "config": { "platform": { - "php": "5.5.9", + "php": "7.1.3", "ext-something": "4.0" } } diff --git a/app/AppKernel.php b/app/AppKernel.php index 40726f054..fcf929c65 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 +81,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 +98,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/Version20160401000000.php b/app/DoctrineMigrations/Version20160401000000.php index c80e3e1f5..9417935b7 100644 --- a/app/DoctrineMigrations/Version20160401000000.php +++ b/app/DoctrineMigrations/Version20160401000000.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20160401000000 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $this->skipIf($schema->hasTable($this->getTable('entry')), 'Database already initialized'); @@ -167,9 +164,6 @@ SQL } } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $this->addSql("DROP TABLE {$this->getTable('craue_config_setting')}"); diff --git a/app/DoctrineMigrations/Version20160410190541.php b/app/DoctrineMigrations/Version20160410190541.php index e1bd3e5c6..5b6d83dce 100644 --- a/app/DoctrineMigrations/Version20160410190541.php +++ b/app/DoctrineMigrations/Version20160410190541.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20160410190541 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); @@ -34,9 +31,6 @@ class Version20160410190541 extends WallabagMigration } } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); diff --git a/app/DoctrineMigrations/Version20160812120952.php b/app/DoctrineMigrations/Version20160812120952.php index d09aefa09..f5f90850d 100644 --- a/app/DoctrineMigrations/Version20160812120952.php +++ b/app/DoctrineMigrations/Version20160812120952.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20160812120952 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $clientsTable = $schema->getTable($this->getTable('oauth2_clients')); @@ -34,9 +31,6 @@ class Version20160812120952 extends WallabagMigration } } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $clientsTable = $schema->getTable($this->getTable('oauth2_clients')); diff --git a/app/DoctrineMigrations/Version20160911214952.php b/app/DoctrineMigrations/Version20160911214952.php index 9dc225fdc..4d7e0f7ee 100644 --- a/app/DoctrineMigrations/Version20160911214952.php +++ b/app/DoctrineMigrations/Version20160911214952.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20160911214952 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $redis = $this->container @@ -36,9 +33,6 @@ class Version20160911214952 extends WallabagMigration $this->skipIf(false !== $rabbitmq && false !== $redis, 'It seems that you already played this migration.'); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $this->addSql('DELETE FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'import_with_redis';"); diff --git a/app/DoctrineMigrations/Version20160916201049.php b/app/DoctrineMigrations/Version20160916201049.php index 13f99ce10..fc5e04aea 100644 --- a/app/DoctrineMigrations/Version20160916201049.php +++ b/app/DoctrineMigrations/Version20160916201049.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20160916201049 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $configTable = $schema->getTable($this->getTable('config')); @@ -23,9 +20,6 @@ class Version20160916201049 extends WallabagMigration $this->addSql('DELETE FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'pocket_consumer_key';"); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $configTable = $schema->getTable($this->getTable('config')); diff --git a/app/DoctrineMigrations/Version20161001072726.php b/app/DoctrineMigrations/Version20161001072726.php index 4e19a54a7..497cb2a16 100644 --- a/app/DoctrineMigrations/Version20161001072726.php +++ b/app/DoctrineMigrations/Version20161001072726.php @@ -11,9 +11,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20161001072726 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $this->skipIf('sqlite' === $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'mysql\' or \'postgresql\'.'); @@ -98,9 +95,6 @@ class Version20161001072726 extends WallabagMigration $this->addSql('ALTER TABLE ' . $this->getTable('annotation') . ' ADD CONSTRAINT FK_annotation_entry FOREIGN KEY (entry_id) REFERENCES ' . $this->getTable('entry') . ' (id) ON DELETE CASCADE'); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { throw new SkipMigrationException('Too complex ...'); diff --git a/app/DoctrineMigrations/Version20161022134138.php b/app/DoctrineMigrations/Version20161022134138.php index 231aada78..d993363c6 100644 --- a/app/DoctrineMigrations/Version20161022134138.php +++ b/app/DoctrineMigrations/Version20161022134138.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20161022134138 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration only apply to MySQL'); @@ -41,9 +38,6 @@ class Version20161022134138 extends WallabagMigration $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' CHANGE `name` `name` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration only apply to MySQL'); diff --git a/app/DoctrineMigrations/Version20161024212538.php b/app/DoctrineMigrations/Version20161024212538.php index a7e3c3c86..fa028ac0b 100644 --- a/app/DoctrineMigrations/Version20161024212538.php +++ b/app/DoctrineMigrations/Version20161024212538.php @@ -12,9 +12,6 @@ class Version20161024212538 extends WallabagMigration { private $constraintName = 'IDX_user_oauth_client'; - /** - * @param Schema $schema - */ public function up(Schema $schema) { $clientsTable = $schema->getTable($this->getTable('oauth2_clients')); @@ -32,9 +29,6 @@ class Version20161024212538 extends WallabagMigration ); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $clientsTable = $schema->getTable($this->getTable('oauth2_clients')); diff --git a/app/DoctrineMigrations/Version20161031132655.php b/app/DoctrineMigrations/Version20161031132655.php index 8d5768627..ec58cb2a8 100644 --- a/app/DoctrineMigrations/Version20161031132655.php +++ b/app/DoctrineMigrations/Version20161031132655.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20161031132655 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $images = $this->container @@ -25,9 +22,6 @@ class Version20161031132655 extends WallabagMigration $this->addSql('INSERT INTO ' . $this->getTable('craue_config_setting') . " (name, value, section) VALUES ('download_images_enabled', 0, 'misc')"); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $this->addSql('DELETE FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'download_images_enabled';"); diff --git a/app/DoctrineMigrations/Version20161104073720.php b/app/DoctrineMigrations/Version20161104073720.php index e2b18a460..e0289ec9a 100644 --- a/app/DoctrineMigrations/Version20161104073720.php +++ b/app/DoctrineMigrations/Version20161104073720.php @@ -12,9 +12,6 @@ class Version20161104073720 extends WallabagMigration { private $indexName = 'IDX_entry_created_at'; - /** - * @param Schema $schema - */ public function up(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); @@ -23,9 +20,6 @@ class Version20161104073720 extends WallabagMigration $entryTable->addIndex(['created_at'], $this->indexName); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); diff --git a/app/DoctrineMigrations/Version20161106113822.php b/app/DoctrineMigrations/Version20161106113822.php index 3a3c90dbf..5a4831f41 100644 --- a/app/DoctrineMigrations/Version20161106113822.php +++ b/app/DoctrineMigrations/Version20161106113822.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20161106113822 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $configTable = $schema->getTable($this->getTable('config')); @@ -25,9 +22,6 @@ class Version20161106113822 extends WallabagMigration ]); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $configTable = $schema->getTable($this->getTable('config')); diff --git a/app/DoctrineMigrations/Version20161117071626.php b/app/DoctrineMigrations/Version20161117071626.php index fc66e7e31..bafb70da1 100644 --- a/app/DoctrineMigrations/Version20161117071626.php +++ b/app/DoctrineMigrations/Version20161117071626.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20161117071626 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $share = $this->container @@ -36,9 +33,6 @@ class Version20161117071626 extends WallabagMigration $this->skipIf(false !== $share && false !== $unmark, 'It seems that you already played this migration.'); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $this->addSql('DELETE FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'share_unmark';"); diff --git a/app/DoctrineMigrations/Version20161118134328.php b/app/DoctrineMigrations/Version20161118134328.php index 8302408ec..2298447a2 100644 --- a/app/DoctrineMigrations/Version20161118134328.php +++ b/app/DoctrineMigrations/Version20161118134328.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20161118134328 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); @@ -25,9 +22,6 @@ class Version20161118134328 extends WallabagMigration ]); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); diff --git a/app/DoctrineMigrations/Version20161122144743.php b/app/DoctrineMigrations/Version20161122144743.php index 08a41f480..e628f0582 100644 --- a/app/DoctrineMigrations/Version20161122144743.php +++ b/app/DoctrineMigrations/Version20161122144743.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20161122144743 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $access = $this->container @@ -25,9 +22,6 @@ class Version20161122144743 extends WallabagMigration $this->addSql('INSERT INTO ' . $this->getTable('craue_config_setting') . " (name, value, section) VALUES ('restricted_access', 0, 'entry')"); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $this->addSql('DELETE FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'restricted_access';"); diff --git a/app/DoctrineMigrations/Version20161122203647.php b/app/DoctrineMigrations/Version20161122203647.php index 60ddeb087..27fe7d2c4 100644 --- a/app/DoctrineMigrations/Version20161122203647.php +++ b/app/DoctrineMigrations/Version20161122203647.php @@ -18,9 +18,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20161122203647 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $userTable = $schema->getTable($this->getTable('user')); @@ -31,9 +28,6 @@ class Version20161122203647 extends WallabagMigration $userTable->dropColumn('credentials_expired'); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $userTable = $schema->getTable($this->getTable('user')); diff --git a/app/DoctrineMigrations/Version20161128084725.php b/app/DoctrineMigrations/Version20161128084725.php index ef747154a..e22e842f1 100644 --- a/app/DoctrineMigrations/Version20161128084725.php +++ b/app/DoctrineMigrations/Version20161128084725.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20161128084725 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $configTable = $schema->getTable($this->getTable('config')); @@ -21,9 +18,6 @@ class Version20161128084725 extends WallabagMigration $configTable->addColumn('list_mode', 'integer', ['notnull' => false]); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $configTable = $schema->getTable($this->getTable('config')); diff --git a/app/DoctrineMigrations/Version20161128131503.php b/app/DoctrineMigrations/Version20161128131503.php index cd4346172..2a34d448f 100644 --- a/app/DoctrineMigrations/Version20161128131503.php +++ b/app/DoctrineMigrations/Version20161128131503.php @@ -16,9 +16,6 @@ class Version20161128131503 extends WallabagMigration 'expires_at' => 'datetime', ]; - /** - * @param Schema $schema - */ public function up(Schema $schema) { $userTable = $schema->getTable($this->getTable('user')); @@ -29,9 +26,6 @@ class Version20161128131503 extends WallabagMigration } } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $userTable = $schema->getTable($this->getTable('user')); diff --git a/app/DoctrineMigrations/Version20161214094402.php b/app/DoctrineMigrations/Version20161214094402.php index e9f1a3020..0240f5994 100644 --- a/app/DoctrineMigrations/Version20161214094402.php +++ b/app/DoctrineMigrations/Version20161214094402.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20161214094402 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); @@ -35,9 +32,6 @@ class Version20161214094402 extends WallabagMigration } } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); diff --git a/app/DoctrineMigrations/Version20161214094403.php b/app/DoctrineMigrations/Version20161214094403.php index 1f9e1f6be..c6003cb35 100644 --- a/app/DoctrineMigrations/Version20161214094403.php +++ b/app/DoctrineMigrations/Version20161214094403.php @@ -12,9 +12,6 @@ class Version20161214094403 extends WallabagMigration { private $indexName = 'IDX_entry_uid'; - /** - * @param Schema $schema - */ public function up(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); @@ -23,9 +20,6 @@ class Version20161214094403 extends WallabagMigration $entryTable->addIndex(['uid'], $this->indexName); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); diff --git a/app/DoctrineMigrations/Version20170127093841.php b/app/DoctrineMigrations/Version20170127093841.php index 491b9383f..d8eb34e57 100644 --- a/app/DoctrineMigrations/Version20170127093841.php +++ b/app/DoctrineMigrations/Version20170127093841.php @@ -13,9 +13,6 @@ class Version20170127093841 extends WallabagMigration private $indexStarredName = 'IDX_entry_starred'; private $indexArchivedName = 'IDX_entry_archived'; - /** - * @param Schema $schema - */ public function up(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); @@ -25,9 +22,6 @@ class Version20170127093841 extends WallabagMigration $entryTable->addIndex(['is_archived'], $this->indexArchivedName); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); diff --git a/app/DoctrineMigrations/Version20170327194233.php b/app/DoctrineMigrations/Version20170327194233.php index 1aa20eb5f..268f8cdea 100644 --- a/app/DoctrineMigrations/Version20170327194233.php +++ b/app/DoctrineMigrations/Version20170327194233.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20170327194233 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $scuttle = $this->container @@ -26,9 +23,6 @@ class Version20170327194233 extends WallabagMigration $this->addSql('INSERT INTO ' . $this->getTable('craue_config_setting') . " (name, value, section) VALUES ('scuttle_url', 'http://scuttle.org', 'entry')"); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $this->addSql('DELETE FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'share_scuttle';"); diff --git a/app/DoctrineMigrations/Version20170405182620.php b/app/DoctrineMigrations/Version20170405182620.php index c7752664a..798c72af9 100644 --- a/app/DoctrineMigrations/Version20170405182620.php +++ b/app/DoctrineMigrations/Version20170405182620.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20170405182620 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); @@ -30,9 +27,6 @@ class Version20170405182620 extends WallabagMigration ]); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); diff --git a/app/DoctrineMigrations/Version20170407200919.php b/app/DoctrineMigrations/Version20170407200919.php index ad05eadf5..d9fff6c38 100644 --- a/app/DoctrineMigrations/Version20170407200919.php +++ b/app/DoctrineMigrations/Version20170407200919.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20170407200919 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); @@ -21,9 +18,6 @@ class Version20170407200919 extends WallabagMigration $entryTable->dropColumn('is_public'); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); diff --git a/app/DoctrineMigrations/Version20170420134133.php b/app/DoctrineMigrations/Version20170420134133.php index c17325783..2bf053462 100644 --- a/app/DoctrineMigrations/Version20170420134133.php +++ b/app/DoctrineMigrations/Version20170420134133.php @@ -10,17 +10,11 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20170420134133 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $this->addSql('DELETE FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'download_pictures';"); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $downloadPictures = $this->container diff --git a/app/DoctrineMigrations/Version20170501115751.php b/app/DoctrineMigrations/Version20170501115751.php index fd238cef6..a879cd441 100644 --- a/app/DoctrineMigrations/Version20170501115751.php +++ b/app/DoctrineMigrations/Version20170501115751.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20170501115751 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $this->skipIf($schema->hasTable($this->getTable('site_credential')), 'It seems that you already played this migration.'); @@ -34,9 +31,6 @@ class Version20170501115751 extends WallabagMigration } } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $schema->dropTable($this->getTable('site_credential')); diff --git a/app/DoctrineMigrations/Version20170510082609.php b/app/DoctrineMigrations/Version20170510082609.php index 541ae1fdd..ddc894a65 100644 --- a/app/DoctrineMigrations/Version20170510082609.php +++ b/app/DoctrineMigrations/Version20170510082609.php @@ -17,9 +17,6 @@ class Version20170510082609 extends WallabagMigration 'email_canonical', ]; - /** - * @param Schema $schema - */ public function up(Schema $schema) { $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration only apply to MySQL'); @@ -29,9 +26,6 @@ class Version20170510082609 extends WallabagMigration } } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration only apply to MySQL'); diff --git a/app/DoctrineMigrations/Version20170511115400.php b/app/DoctrineMigrations/Version20170511115400.php index cb80dd451..9a89cfb8b 100644 --- a/app/DoctrineMigrations/Version20170511115400.php +++ b/app/DoctrineMigrations/Version20170511115400.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20170511115400 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); @@ -24,9 +21,6 @@ class Version20170511115400 extends WallabagMigration ]); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); diff --git a/app/DoctrineMigrations/Version20170511211659.php b/app/DoctrineMigrations/Version20170511211659.php index 00ce7b1f3..d0752bafe 100644 --- a/app/DoctrineMigrations/Version20170511211659.php +++ b/app/DoctrineMigrations/Version20170511211659.php @@ -13,19 +13,21 @@ class Version20170511211659 extends WallabagMigration { public function up(Schema $schema) { - $tableName = $this->getTable('annotation'); - switch ($this->connection->getDatabasePlatform()->getName()) { case 'sqlite': + $annotationTableName = $this->getTable('annotation', true); + $userTableName = $this->getTable('user', true); + $entryTableName = $this->getTable('entry', true); + $this->addSql(<<addSql('DROP TABLE ' . $tableName); + $this->addSql('DROP TABLE ' . $annotationTableName); $this->addSql(<<addSql(<<addSql('DROP TABLE __temp__wallabag_annotation'); break; case 'mysql': - $this->addSql('ALTER TABLE ' . $tableName . ' MODIFY quote TEXT NOT NULL'); + $this->addSql('ALTER TABLE ' . $this->getTable('annotation') . ' MODIFY quote TEXT NOT NULL'); break; case 'postgresql': - $this->addSql('ALTER TABLE ' . $tableName . ' ALTER COLUMN quote TYPE TEXT'); + $this->addSql('ALTER TABLE ' . $this->getTable('annotation') . ' ALTER COLUMN quote TYPE TEXT'); break; } } diff --git a/app/DoctrineMigrations/Version20170602075214.php b/app/DoctrineMigrations/Version20170602075214.php index 12997c715..f72839b20 100644 --- a/app/DoctrineMigrations/Version20170602075214.php +++ b/app/DoctrineMigrations/Version20170602075214.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20170602075214 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $apiUserRegistration = $this->container @@ -25,9 +22,6 @@ class Version20170602075214 extends WallabagMigration $this->addSql('INSERT INTO ' . $this->getTable('craue_config_setting') . " (name, value, section) VALUES ('api_user_registration', '0', 'api')"); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $this->addSql('DELETE FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'api_user_registration';"); diff --git a/app/DoctrineMigrations/Version20170606155640.php b/app/DoctrineMigrations/Version20170606155640.php index 153d31b88..099e53296 100644 --- a/app/DoctrineMigrations/Version20170606155640.php +++ b/app/DoctrineMigrations/Version20170606155640.php @@ -11,9 +11,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20170606155640 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $apiUserRegistration = $this->container @@ -26,9 +23,6 @@ class Version20170606155640 extends WallabagMigration $this->addSql('DELETE FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'wallabag_url'"); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $this->addSql('INSERT INTO ' . $this->getTable('craue_config_setting') . " (name, value, section) VALUES ('wallabag_url', 'wallabag.me', 'misc')"); diff --git a/app/DoctrineMigrations/Version20170719231144.php b/app/DoctrineMigrations/Version20170719231144.php index 93fe7f260..7a9731d43 100644 --- a/app/DoctrineMigrations/Version20170719231144.php +++ b/app/DoctrineMigrations/Version20170719231144.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20170719231144 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $this->skipIf('sqlite' === $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'mysql\' or \'postgresql\'.'); @@ -89,9 +86,6 @@ class Version20170719231144 extends WallabagMigration ); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { throw new SkipMigrationException('Too complex ...'); diff --git a/app/DoctrineMigrations/Version20170824113337.php b/app/DoctrineMigrations/Version20170824113337.php index 5fc31de9d..dc20f6d9c 100644 --- a/app/DoctrineMigrations/Version20170824113337.php +++ b/app/DoctrineMigrations/Version20170824113337.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20170824113337 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); @@ -37,9 +34,6 @@ class Version20170824113337 extends WallabagMigration ); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); diff --git a/app/DoctrineMigrations/Version20171008195606.php b/app/DoctrineMigrations/Version20171008195606.php index 6974232a0..60d8777f4 100644 --- a/app/DoctrineMigrations/Version20171008195606.php +++ b/app/DoctrineMigrations/Version20171008195606.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20171008195606 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $this->skipIf('sqlite' === $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'mysql\' or \'postgresql\'.'); @@ -29,9 +26,6 @@ class Version20171008195606 extends WallabagMigration } } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $this->skipIf('sqlite' === $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'mysql\' or \'postgresql\'.'); diff --git a/app/DoctrineMigrations/Version20171105202000.php b/app/DoctrineMigrations/Version20171105202000.php index 5ed787b99..5313a3368 100644 --- a/app/DoctrineMigrations/Version20171105202000.php +++ b/app/DoctrineMigrations/Version20171105202000.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20171105202000 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); @@ -24,9 +21,6 @@ class Version20171105202000 extends WallabagMigration ]); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $entryTable = $schema->getTable($this->getTable('entry')); diff --git a/app/DoctrineMigrations/Version20171120163128.php b/app/DoctrineMigrations/Version20171120163128.php index 48f10f1de..a6d126a50 100644 --- a/app/DoctrineMigrations/Version20171120163128.php +++ b/app/DoctrineMigrations/Version20171120163128.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20171120163128 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $storeArticleHeaders = $this->container @@ -25,9 +22,6 @@ class Version20171120163128 extends WallabagMigration $this->addSql('INSERT INTO ' . $this->getTable('craue_config_setting') . " (name, value, section) VALUES ('store_article_headers', '0', 'entry')"); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $this->addSql('DELETE FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'store_article_headers';"); diff --git a/app/DoctrineMigrations/Version20171125164500.php b/app/DoctrineMigrations/Version20171125164500.php index a72463f0c..2ee49d872 100644 --- a/app/DoctrineMigrations/Version20171125164500.php +++ b/app/DoctrineMigrations/Version20171125164500.php @@ -10,9 +10,6 @@ use Wallabag\CoreBundle\Doctrine\WallabagMigration; */ class Version20171125164500 extends WallabagMigration { - /** - * @param Schema $schema - */ public function up(Schema $schema) { $shaarliShareOriginUrl = $this->container @@ -25,9 +22,6 @@ class Version20171125164500 extends WallabagMigration $this->addSql('INSERT INTO ' . $this->getTable('craue_config_setting') . " (name, value, section) VALUES ('shaarli_share_origin_url', '0', 'entry')"); } - /** - * @param Schema $schema - */ public function down(Schema $schema) { $this->addSql('DELETE FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'shaarli_share_origin_url';"); diff --git a/app/DoctrineMigrations/Version20180405182455.php b/app/DoctrineMigrations/Version20180405182455.php new file mode 100755 index 000000000..1b8c3b0e1 --- /dev/null +++ b/app/DoctrineMigrations/Version20180405182455.php @@ -0,0 +1,45 @@ +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, + ] + ); + } + + 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 000000000..add161cdc --- /dev/null +++ b/app/DoctrineMigrations/Version20181128203230.php @@ -0,0 +1,42 @@ +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 000000000..5978291e8 --- /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 000000000..6548b9fa3 --- /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 000000000..8c5e28cab --- /dev/null +++ b/app/DoctrineMigrations/Version20190129120000.php @@ -0,0 +1,141 @@ + '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', + ], + ]; + + 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'] . "');"); + } + } + + 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 000000000..600fc1621 --- /dev/null +++ b/app/DoctrineMigrations/Version20190401105353.php @@ -0,0 +1,36 @@ +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]]); + } + + 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 000000000..5c6ae4941 --- /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 "' . $this->getTable('user', true) . '" (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 000000000..3c504e8a1 --- /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 "' . $this->getTable('oauth2_clients', true) . '" (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 "' . $this->getTable('oauth2_clients', true) . '" (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 "' . $this->getTable('oauth2_clients', true) . '" (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 "' . $this->getTable('oauth2_clients', true) . '" (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 ' . $this->getTable('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 ' . $this->getTable('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 ' . $this->getTable('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 ' . $this->getTable('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 ' . $this->getTable('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 ' . $this->getTable('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 ' . $this->getTable('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 ' . $this->getTable('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/DoctrineMigrations/Version20190511165128.php b/app/DoctrineMigrations/Version20190511165128.php new file mode 100644 index 000000000..7b6b1bec0 --- /dev/null +++ b/app/DoctrineMigrations/Version20190511165128.php @@ -0,0 +1,30 @@ +skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration only apply to MySQL'); + + $this->addSql('ALTER TABLE ' . $this->getTable('tag') . ' CHANGE `label` `label` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;'); + $this->addSql('ALTER TABLE ' . $this->getTable('tag') . ' CHANGE `slug` `slug` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;'); + } + + public function down(Schema $schema): void + { + $this->skipIf('mysql' !== $this->connection->getDatabasePlatform()->getName(), 'This migration only apply to MySQL'); + + $this->addSql('ALTER TABLE ' . $this->getTable('tag') . ' CHANGE `slug` `slug` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); + $this->addSql('ALTER TABLE ' . $this->getTable('tag') . ' CHANGE `label` `label` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); + } +} diff --git a/app/DoctrineMigrations/Version20190601125843.php b/app/DoctrineMigrations/Version20190601125843.php new file mode 100644 index 000000000..cbb92edc9 --- /dev/null +++ b/app/DoctrineMigrations/Version20190601125843.php @@ -0,0 +1,48 @@ +getTable($this->getTable('entry')); + + if (!$entryTable->hasColumn('given_url')) { + $entryTable->addColumn('given_url', 'text', [ + 'notnull' => false, + ]); + } + + if (!$entryTable->hasColumn('hashed_given_url')) { + $entryTable->addColumn('hashed_given_url', 'text', [ + 'length' => 40, + 'notnull' => false, + ]); + } + + // 40 = length of sha1 field hashed_given_url + $entryTable->addIndex(['user_id', 'hashed_given_url'], 'hashed_given_url_user_id', [], ['lengths' => [null, 40]]); + } + + public function down(Schema $schema) + { + $entryTable = $schema->getTable($this->getTable('entry')); + + if ($entryTable->hasColumn('given_url')) { + $entryTable->dropColumn('given_url'); + } + + if ($entryTable->hasColumn('hashed_given_url')) { + $entryTable->dropColumn('hashed_given_url'); + } + + $entryTable->dropIndex('hashed_given_url_user_id'); + } +} diff --git a/app/DoctrineMigrations/Version20190619093534.php b/app/DoctrineMigrations/Version20190619093534.php new file mode 100644 index 000000000..9a0625364 --- /dev/null +++ b/app/DoctrineMigrations/Version20190619093534.php @@ -0,0 +1,65 @@ +skipIf('sqlite' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'sqlite\'.'); + + $this->addSql('UPDATE ' . $this->getTable('entry', true) . ' SET reading_time = 0 WHERE reading_time IS NULL;'); + + $this->addSql('DROP INDEX hashed_given_url_user_id'); + $this->addSql('DROP INDEX IDX_entry_uid'); + $this->addSql('DROP INDEX IDX_F4D18282A76ED395'); + $this->addSql('DROP INDEX IDX_entry_created_at'); + $this->addSql('DROP INDEX IDX_entry_starred'); + $this->addSql('DROP INDEX IDX_entry_archived'); + $this->addSql('DROP INDEX hashed_url_user_id'); + $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('entry', true) . ' AS SELECT id, user_id, title, url, is_archived, is_starred, content, created_at, updated_at, mimetype, language, reading_time, domain_name, preview_picture, uid, http_status, published_at, published_by, headers, starred_at, origin_url, archived_at, hashed_url, given_url, hashed_given_url FROM ' . $this->getTable('entry', true) . ''); + $this->addSql('DROP TABLE ' . $this->getTable('entry', true) . ''); + $this->addSql('CREATE TABLE ' . $this->getTable('entry', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, title CLOB DEFAULT NULL COLLATE BINARY, url CLOB DEFAULT NULL COLLATE BINARY, is_archived BOOLEAN NOT NULL, is_starred BOOLEAN NOT NULL, content CLOB DEFAULT NULL COLLATE BINARY, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, mimetype CLOB DEFAULT NULL COLLATE BINARY, language CLOB DEFAULT NULL COLLATE BINARY, domain_name CLOB DEFAULT NULL COLLATE BINARY, preview_picture CLOB DEFAULT NULL COLLATE BINARY, uid VARCHAR(23) DEFAULT NULL COLLATE BINARY, http_status VARCHAR(3) DEFAULT NULL COLLATE BINARY, published_at DATETIME DEFAULT NULL, starred_at DATETIME DEFAULT NULL, origin_url CLOB DEFAULT NULL COLLATE BINARY, archived_at DATETIME DEFAULT NULL, given_url CLOB DEFAULT NULL COLLATE BINARY, reading_time INTEGER NOT NULL, published_by CLOB DEFAULT NULL --(DC2Type:array) + , headers CLOB DEFAULT NULL --(DC2Type:array) + , hashed_url VARCHAR(40) DEFAULT NULL, hashed_given_url VARCHAR(40) DEFAULT NULL, CONSTRAINT FK_F4D18282A76ED395 FOREIGN KEY (user_id) REFERENCES "' . $this->getTable('user', true) . '" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO ' . $this->getTable('entry', true) . ' (id, user_id, title, url, is_archived, is_starred, content, created_at, updated_at, mimetype, language, reading_time, domain_name, preview_picture, uid, http_status, published_at, published_by, headers, starred_at, origin_url, archived_at, hashed_url, given_url, hashed_given_url) SELECT id, user_id, title, url, is_archived, is_starred, content, created_at, updated_at, mimetype, language, reading_time, domain_name, preview_picture, uid, http_status, published_at, published_by, headers, starred_at, origin_url, archived_at, hashed_url, given_url, hashed_given_url FROM __temp__' . $this->getTable('entry', true) . ''); + $this->addSql('DROP TABLE __temp__' . $this->getTable('entry', true) . ''); + $this->addSql('CREATE INDEX hashed_given_url_user_id ON ' . $this->getTable('entry', true) . ' (user_id, hashed_given_url)'); + $this->addSql('CREATE INDEX IDX_F4D18282A76ED395 ON ' . $this->getTable('entry', true) . ' (user_id)'); + $this->addSql('CREATE INDEX hashed_url_user_id ON ' . $this->getTable('entry', true) . ' (user_id, hashed_url)'); + $this->addSql('CREATE INDEX created_at ON ' . $this->getTable('entry', true) . ' (created_at)'); + $this->addSql('CREATE INDEX uid ON ' . $this->getTable('entry', true) . ' (uid)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->skipIf('sqlite' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'sqlite\'.'); + + $this->addSql('DROP INDEX IDX_F4D18282A76ED395'); + $this->addSql('DROP INDEX created_at'); + $this->addSql('DROP INDEX uid'); + $this->addSql('DROP INDEX hashed_url_user_id'); + $this->addSql('DROP INDEX hashed_given_url_user_id'); + $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('entry', true) . ' AS SELECT id, user_id, uid, title, url, hashed_url, origin_url, given_url, hashed_given_url, is_archived, archived_at, is_starred, content, created_at, updated_at, published_at, published_by, starred_at, mimetype, language, reading_time, domain_name, preview_picture, http_status, headers FROM "' . $this->getTable('entry', true) . '"'); + $this->addSql('DROP TABLE "' . $this->getTable('entry', true) . '"'); + $this->addSql('CREATE TABLE "' . $this->getTable('entry', true) . '" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, uid VARCHAR(23) DEFAULT NULL, title CLOB DEFAULT NULL, url CLOB DEFAULT NULL, origin_url CLOB DEFAULT NULL, given_url CLOB DEFAULT NULL, is_archived BOOLEAN NOT NULL, archived_at DATETIME DEFAULT NULL, is_starred BOOLEAN NOT NULL, content CLOB DEFAULT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, published_at DATETIME DEFAULT NULL, starred_at DATETIME DEFAULT NULL, mimetype CLOB DEFAULT NULL, language CLOB DEFAULT NULL, domain_name CLOB DEFAULT NULL, preview_picture CLOB DEFAULT NULL, http_status VARCHAR(3) DEFAULT NULL, hashed_url CLOB DEFAULT NULL COLLATE BINARY, hashed_given_url CLOB DEFAULT NULL COLLATE BINARY, published_by CLOB DEFAULT NULL COLLATE BINARY, reading_time INTEGER DEFAULT NULL, headers CLOB DEFAULT NULL COLLATE BINARY)'); + $this->addSql('INSERT INTO "' . $this->getTable('entry', true) . '" (id, user_id, uid, title, url, hashed_url, origin_url, given_url, hashed_given_url, is_archived, archived_at, is_starred, content, created_at, updated_at, published_at, published_by, starred_at, mimetype, language, reading_time, domain_name, preview_picture, http_status, headers) SELECT id, user_id, uid, title, url, hashed_url, origin_url, given_url, hashed_given_url, is_archived, archived_at, is_starred, content, created_at, updated_at, published_at, published_by, starred_at, mimetype, language, reading_time, domain_name, preview_picture, http_status, headers FROM __temp__' . $this->getTable('entry', true) . ''); + $this->addSql('DROP TABLE __temp__' . $this->getTable('entry', true) . ''); + $this->addSql('CREATE INDEX IDX_F4D18282A76ED395 ON "' . $this->getTable('entry', true) . '" (user_id)'); + $this->addSql('CREATE INDEX hashed_url_user_id ON "' . $this->getTable('entry', true) . '" (user_id, hashed_url)'); + $this->addSql('CREATE INDEX hashed_given_url_user_id ON "' . $this->getTable('entry', true) . '" (user_id, hashed_given_url)'); + $this->addSql('CREATE INDEX IDX_entry_starred ON "' . $this->getTable('entry', true) . '" (is_starred)'); + $this->addSql('CREATE INDEX IDX_entry_archived ON "' . $this->getTable('entry', true) . '" (is_archived)'); + $this->addSql('CREATE INDEX IDX_entry_uid ON "' . $this->getTable('entry', true) . '" (uid)'); + $this->addSql('CREATE INDEX IDX_entry_created_at ON "' . $this->getTable('entry', true) . '" (created_at)'); + } +} diff --git a/app/DoctrineMigrations/Version20190708122957.php b/app/DoctrineMigrations/Version20190708122957.php new file mode 100644 index 000000000..9585e997a --- /dev/null +++ b/app/DoctrineMigrations/Version20190708122957.php @@ -0,0 +1,22 @@ +addSql('UPDATE ' . $this->getTable('config', true) . ' SET reading_speed = reading_speed*200'); + } + + public function down(Schema $schema): void + { + $this->addSql('UPDATE ' . $this->getTable('config', true) . ' SET reading_speed = reading_speed/200'); + } +} diff --git a/app/DoctrineMigrations/Version20190806130304.php b/app/DoctrineMigrations/Version20190806130304.php new file mode 100644 index 000000000..8b0271b1f --- /dev/null +++ b/app/DoctrineMigrations/Version20190806130304.php @@ -0,0 +1,116 @@ +connection->getDatabasePlatform()->getName()) { + case 'sqlite': + $this->addSql('DROP INDEX uid'); + $this->addSql('DROP INDEX created_at'); + $this->addSql('DROP INDEX hashed_url_user_id'); + $this->addSql('DROP INDEX IDX_F4D18282A76ED395'); + $this->addSql('DROP INDEX hashed_given_url_user_id'); + $this->addSql('CREATE TEMPORARY TABLE __temp__wallabag_entry AS SELECT id, user_id, title, url, is_archived, is_starred, content, created_at, updated_at, mimetype, language, domain_name, preview_picture, uid, http_status, published_at, starred_at, origin_url, archived_at, given_url, reading_time, published_by, headers, hashed_url, hashed_given_url FROM ' . $this->getTable('entry', true)); + $this->addSql('DROP TABLE ' . $this->getTable('entry', true)); + $this->addSql('CREATE TABLE ' . $this->getTable('entry', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, title CLOB DEFAULT NULL COLLATE BINARY, url CLOB DEFAULT NULL COLLATE BINARY, is_archived BOOLEAN NOT NULL, is_starred BOOLEAN NOT NULL, content CLOB DEFAULT NULL COLLATE BINARY, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, mimetype CLOB DEFAULT NULL COLLATE BINARY, domain_name CLOB DEFAULT NULL COLLATE BINARY, preview_picture CLOB DEFAULT NULL COLLATE BINARY, uid VARCHAR(23) DEFAULT NULL COLLATE BINARY, http_status VARCHAR(3) DEFAULT NULL COLLATE BINARY, published_at DATETIME DEFAULT NULL, starred_at DATETIME DEFAULT NULL, origin_url CLOB DEFAULT NULL COLLATE BINARY, archived_at DATETIME DEFAULT NULL, given_url CLOB DEFAULT NULL COLLATE BINARY, reading_time INTEGER NOT NULL, published_by CLOB DEFAULT NULL COLLATE BINARY --(DC2Type:array) + , headers CLOB DEFAULT NULL COLLATE BINARY --(DC2Type:array) + , hashed_url VARCHAR(40) DEFAULT NULL COLLATE BINARY, hashed_given_url VARCHAR(40) DEFAULT NULL COLLATE BINARY, language VARCHAR(20) DEFAULT NULL, CONSTRAINT FK_F4D18282A76ED395 FOREIGN KEY (user_id) REFERENCES "wallabag_user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO ' . $this->getTable('entry', true) . ' (id, user_id, title, url, is_archived, is_starred, content, created_at, updated_at, mimetype, language, domain_name, preview_picture, uid, http_status, published_at, starred_at, origin_url, archived_at, given_url, reading_time, published_by, headers, hashed_url, hashed_given_url) SELECT id, user_id, title, url, is_archived, is_starred, content, created_at, updated_at, mimetype, language, domain_name, preview_picture, uid, http_status, published_at, starred_at, origin_url, archived_at, given_url, reading_time, published_by, headers, hashed_url, hashed_given_url FROM __temp__wallabag_entry'); + $this->addSql('DROP TABLE __temp__wallabag_entry'); + $this->addSql('CREATE INDEX uid ON ' . $this->getTable('entry', true) . ' (uid)'); + $this->addSql('CREATE INDEX created_at ON ' . $this->getTable('entry', true) . ' (created_at)'); + $this->addSql('CREATE INDEX hashed_url_user_id ON ' . $this->getTable('entry', true) . ' (user_id, hashed_url)'); + $this->addSql('CREATE INDEX IDX_F4D18282A76ED395 ON ' . $this->getTable('entry', true) . ' (user_id)'); + $this->addSql('CREATE INDEX hashed_given_url_user_id ON ' . $this->getTable('entry', true) . ' (user_id, hashed_given_url)'); + $this->addSql('CREATE INDEX user_language ON ' . $this->getTable('entry', true) . ' (language, user_id)'); + $this->addSql('CREATE INDEX user_archived ON ' . $this->getTable('entry', true) . ' (user_id, is_archived, archived_at)'); + $this->addSql('CREATE INDEX user_created ON ' . $this->getTable('entry', true) . ' (user_id, created_at)'); + $this->addSql('CREATE INDEX user_starred ON ' . $this->getTable('entry', true) . ' (user_id, is_starred, starred_at)'); + $this->addSql('CREATE INDEX tag_label ON ' . $this->getTable('tag', true) . ' (label)'); + $this->addSql('CREATE INDEX config_feed_token ON ' . $this->getTable('config', true) . ' (feed_token)'); + break; + case 'mysql': + $this->addSql('ALTER TABLE ' . $this->getTable('entry') . ' MODIFY language VARCHAR(20) DEFAULT NULL'); + $this->addSql('CREATE INDEX user_language ON ' . $this->getTable('entry') . ' (language, user_id)'); + $this->addSql('CREATE INDEX user_archived ON ' . $this->getTable('entry') . ' (user_id, is_archived, archived_at)'); + $this->addSql('CREATE INDEX user_created ON ' . $this->getTable('entry') . ' (user_id, created_at)'); + $this->addSql('CREATE INDEX user_starred ON ' . $this->getTable('entry') . ' (user_id, is_starred, starred_at)'); + $this->addSql('CREATE INDEX tag_label ON ' . $this->getTable('tag') . ' (label (255))'); + $this->addSql('CREATE INDEX config_feed_token ON ' . $this->getTable('config') . ' (feed_token (255))'); + break; + case 'postgresql': + $this->addSql('ALTER TABLE ' . $this->getTable('entry') . ' ALTER language TYPE VARCHAR(20)'); + $this->addSql('CREATE INDEX user_language ON ' . $this->getTable('entry') . ' (language, user_id)'); + $this->addSql('CREATE INDEX user_archived ON ' . $this->getTable('entry') . ' (user_id, is_archived, archived_at)'); + $this->addSql('CREATE INDEX user_created ON ' . $this->getTable('entry') . ' (user_id, created_at)'); + $this->addSql('CREATE INDEX user_starred ON ' . $this->getTable('entry') . ' (user_id, is_starred, starred_at)'); + $this->addSql('CREATE INDEX tag_label ON ' . $this->getTable('tag') . ' (label)'); + $this->addSql('CREATE INDEX config_feed_token ON ' . $this->getTable('config') . ' (feed_token)'); + break; + } + } + + public function down(Schema $schema): void + { + switch ($this->connection->getDatabasePlatform()->getName()) { + case 'sqlite': + $this->addSql('DROP INDEX IDX_F4D18282A76ED395'); + $this->addSql('DROP INDEX created_at'); + $this->addSql('DROP INDEX uid'); + $this->addSql('DROP INDEX hashed_url_user_id'); + $this->addSql('DROP INDEX hashed_given_url_user_id'); + $this->addSql('DROP INDEX user_language'); + $this->addSql('DROP INDEX user_archived'); + $this->addSql('DROP INDEX user_created'); + $this->addSql('DROP INDEX user_starred'); + $this->addSql('DROP INDEX tag_label'); + $this->addSql('DROP INDEX config_feed_token'); + $this->addSql('CREATE TEMPORARY TABLE __temp__wallabag_entry AS SELECT id, user_id, uid, title, url, hashed_url, origin_url, given_url, hashed_given_url, is_archived, archived_at, is_starred, content, created_at, updated_at, published_at, published_by, starred_at, mimetype, language, reading_time, domain_name, preview_picture, http_status, headers FROM ' . $this->getTable('entry', true)); + $this->addSql('DROP TABLE ' . $this->getTable('entry', true)); + $this->addSql('CREATE TABLE ' . $this->getTable('entry', true) . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, uid VARCHAR(23) DEFAULT NULL, title CLOB DEFAULT NULL, url CLOB DEFAULT NULL, hashed_url VARCHAR(40) DEFAULT NULL, origin_url CLOB DEFAULT NULL, given_url CLOB DEFAULT NULL, hashed_given_url VARCHAR(40) DEFAULT NULL, is_archived BOOLEAN NOT NULL, archived_at DATETIME DEFAULT NULL, is_starred BOOLEAN NOT NULL, content CLOB DEFAULT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, published_at DATETIME DEFAULT NULL, published_by CLOB DEFAULT NULL --(DC2Type:array) + , starred_at DATETIME DEFAULT NULL, mimetype CLOB DEFAULT NULL, reading_time INTEGER NOT NULL, domain_name CLOB DEFAULT NULL, preview_picture CLOB DEFAULT NULL, http_status VARCHAR(3) DEFAULT NULL, headers CLOB DEFAULT NULL --(DC2Type:array) + , language CLOB DEFAULT NULL COLLATE BINARY)'); + $this->addSql('INSERT INTO ' . $this->getTable('entry', true) . ' (id, user_id, uid, title, url, hashed_url, origin_url, given_url, hashed_given_url, is_archived, archived_at, is_starred, content, created_at, updated_at, published_at, published_by, starred_at, mimetype, language, reading_time, domain_name, preview_picture, http_status, headers) SELECT id, user_id, uid, title, url, hashed_url, origin_url, given_url, hashed_given_url, is_archived, archived_at, is_starred, content, created_at, updated_at, published_at, published_by, starred_at, mimetype, language, reading_time, domain_name, preview_picture, http_status, headers FROM __temp__wallabag_entry'); + $this->addSql('DROP TABLE __temp__wallabag_entry'); + $this->addSql('CREATE INDEX IDX_F4D18282A76ED395 ON ' . $this->getTable('entry', true) . ' (user_id)'); + $this->addSql('CREATE INDEX created_at ON ' . $this->getTable('entry', true) . ' (created_at)'); + $this->addSql('CREATE INDEX uid ON ' . $this->getTable('entry', true) . ' (uid)'); + $this->addSql('CREATE INDEX hashed_url_user_id ON ' . $this->getTable('entry', true) . ' (user_id, hashed_url)'); + $this->addSql('CREATE INDEX hashed_given_url_user_id ON ' . $this->getTable('entry', true) . ' (user_id, hashed_given_url)'); + break; + case 'mysql': + $this->addSql('ALTER TABLE ' . $this->getTable('entry') . ' MODIFY language LONGTEXT DEFAULT NULL'); + $this->addSql('DROP INDEX user_language ON ' . $this->getTable('entry')); + $this->addSql('DROP INDEX user_archived ON ' . $this->getTable('entry')); + $this->addSql('DROP INDEX user_created ON ' . $this->getTable('entry')); + $this->addSql('DROP INDEX user_starred ON ' . $this->getTable('entry')); + $this->addSql('DROP INDEX tag_label ON ' . $this->getTable('tag')); + $this->addSql('DROP INDEX config_feed_token ON ' . $this->getTable('config')); + break; + case 'postgresql': + $this->addSql('ALTER TABLE ' . $this->getTable('entry') . ' ALTER language TYPE TEXT'); + $this->addSql('DROP INDEX user_language ON ' . $this->getTable('entry')); + $this->addSql('DROP INDEX user_archived ON ' . $this->getTable('entry')); + $this->addSql('DROP INDEX user_created ON ' . $this->getTable('entry')); + $this->addSql('DROP INDEX user_starred ON ' . $this->getTable('entry')); + $this->addSql('DROP INDEX tag_label ON ' . $this->getTable('tag')); + $this->addSql('DROP INDEX config_feed_token ON ' . $this->getTable('config')); + break; + } + } +} diff --git a/app/DoctrineMigrations/Version20190808124957.php b/app/DoctrineMigrations/Version20190808124957.php new file mode 100644 index 000000000..4e12e995e --- /dev/null +++ b/app/DoctrineMigrations/Version20190808124957.php @@ -0,0 +1,42 @@ +connection->getDatabasePlatform()->getName()) { + case 'sqlite': + $this->addSql('ALTER TABLE ' . $this->getTable('craue_config_setting', true) . ' RENAME TO ' . $this->getTable('internal_setting', true)); + break; + case 'mysql': + $this->addSql('ALTER TABLE ' . $this->getTable('craue_config_setting') . ' RENAME ' . $this->getTable('internal_setting')); + break; + case 'postgresql': + $this->addSql('ALTER TABLE ' . $this->getTable('craue_config_setting') . ' RENAME TO ' . $this->getTable('internal_setting')); + break; + } + } + + public function down(Schema $schema): void + { + switch ($this->connection->getDatabasePlatform()->getName()) { + case 'sqlite': + $this->addSql('ALTER TABLE ' . $this->getTable('internal_setting', true) . ' RENAME TO ' . $this->getTable('craue_config_setting', true)); + break; + case 'mysql': + $this->addSql('ALTER TABLE ' . $this->getTable('internal_setting') . ' RENAME ' . $this->getTable('craue_config_setting')); + break; + case 'postgresql': + $this->addSql('ALTER TABLE ' . $this->getTable('internal_setting') . ' RENAME TO ' . $this->getTable('craue_config_setting')); + break; + } + } +} diff --git a/app/Resources/static/themes/_global/index.js b/app/Resources/static/themes/_global/index.js index ae598e56e..88365270e 100644 --- a/app/Resources/static/themes/_global/index.js +++ b/app/Resources/static/themes/_global/index.js @@ -4,6 +4,9 @@ import $ from 'jquery'; /* Annotations */ import annotator from 'annotator'; +import ClipboardJS from 'clipboard'; +import 'mathjax/es5/tex-svg'; + /* Fonts */ import 'material-design-icons-iconfont/dist/material-design-icons.css'; import 'lato-font/css/lato-font.css'; @@ -70,4 +73,47 @@ $(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); + }); + + // handle copy to clipboard for developer stuff + const clipboard = new ClipboardJS('.btn'); + clipboard.on('success', (e) => { + e.clearSelection(); + }); }); diff --git a/app/Resources/static/themes/baggy/css/article.scss b/app/Resources/static/themes/baggy/css/article.scss index 9094ad550..d203ce313 100644 --- a/app/Resources/static/themes/baggy/css/article.scss +++ b/app/Resources/static/themes/baggy/css/article.scss @@ -85,7 +85,7 @@ blockquote { color: #999; } -.icon-rss { +.icon-feed { background-color: #000; color: #fff; padding: 0.2em 0.5em; @@ -101,8 +101,8 @@ blockquote { margin-bottom: 0.5em; } - .icon-rss:hover, - .icon-rss:focus { + .icon-feed:hover, + .icon-feed:focus { background-color: #fff; color: #000; text-decoration: none; diff --git a/app/Resources/static/themes/baggy/css/layout.scss b/app/Resources/static/themes/baggy/css/layout.scss index cb14e62d4..0293ebe57 100644 --- a/app/Resources/static/themes/baggy/css/layout.scss +++ b/app/Resources/static/themes/baggy/css/layout.scss @@ -295,6 +295,15 @@ div.pagination ul { } } -.hide { +.card-tag-form { + display: inline-block; +} + +.card-tag-form input[type="text"] { + min-width: 20em; +} + +.hide, +.hidden { display: none; } diff --git a/app/Resources/static/themes/baggy/css/media_queries.scss b/app/Resources/static/themes/baggy/css/media_queries.scss index c33db0b36..a08f369f4 100755 --- a/app/Resources/static/themes/baggy/css/media_queries.scss +++ b/app/Resources/static/themes/baggy/css/media_queries.scss @@ -170,3 +170,9 @@ width: 100%; } } + +@media only print { + header h1.logo { + display: none; + } +} diff --git a/app/Resources/static/themes/baggy/css/pictos.scss b/app/Resources/static/themes/baggy/css/pictos.scss index 2ff019375..b6ebf3112 100644 --- a/app/Resources/static/themes/baggy/css/pictos.scss +++ b/app/Resources/static/themes/baggy/css/pictos.scss @@ -136,7 +136,7 @@ content: "\ea3a"; } -.icon-rss::before { +.icon-feed::before { content: "\e808"; } diff --git a/app/Resources/static/themes/material/css/article.scss b/app/Resources/static/themes/material/css/article.scss index 75658a580..755372c95 100644 --- a/app/Resources/static/themes/material/css/article.scss +++ b/app/Resources/static/themes/material/css/article.scss @@ -5,6 +5,7 @@ #article { font-size: 20px; margin: 0 auto; + padding-bottom: 80px; max-width: 45em; article { @@ -172,18 +173,18 @@ &:hover { width: 260px !important; - .collapsible-body { - height: auto; - - li a i.material-icons { - margin: auto 5px auto -8px; - } - } - span { opacity: 1; } } + + .collapsible-body { + height: auto; + + li a i.material-icons { + margin: auto 5px auto -8px; + } + } } .progress { diff --git a/app/Resources/static/themes/material/css/cards.scss b/app/Resources/static/themes/material/css/cards.scss index 4f67e0385..9ae1be826 100644 --- a/app/Resources/static/themes/material/css/cards.scss +++ b/app/Resources/static/themes/material/css/cards.scss @@ -18,6 +18,24 @@ main { overflow: hidden; } +@mixin mixin-reading-time { + .reading-time { + display: inline-flex; + vertical-align: middle; + + .card-reading-time, + .card-created-at { + display: inline-flex; + } + + span { + margin-right: 5px; + } + + @content; + } +} + .card { .card-content .card-title, .card-reveal .card-title { @@ -98,14 +116,7 @@ main { margin-right: 5px !important; } - .reading-time { - display: inline-flex; - vertical-align: middle; - - span { - margin-right: 5px; - } - } + @include mixin-reading-time; } .card-image { @@ -186,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; } @@ -219,10 +241,18 @@ a.original:not(.waves-effect) { } div.metadata { + overflow: hidden; + height: 1.5em; + display: flex; + + ul.tags { + margin-left: 4px; + } + .chip { background-color: $blueAccentColor; padding: 0 7px; - margin: auto 2px; + margin: auto 1px; border-radius: 6px; line-height: 22px; height: 22px; @@ -239,6 +269,16 @@ a.original:not(.waves-effect) { padding-left: 8px; } } + + @include mixin-reading-time { + padding: 0 5px; + flex-wrap: wrap; + margin-left: auto; + + i.material-icons { + font-size: 20px; + } + } } div.card-content { @@ -272,9 +312,3 @@ a.original:not(.waves-effect) { .settings .div_tabs { padding-bottom: 15px; } - -@media only screen and (min-width: 992px) { - .card-tag-labels li { - max-width: 50%; - } -} diff --git a/app/Resources/static/themes/material/css/media_queries.scss b/app/Resources/static/themes/material/css/media_queries.scss index 725844269..4242ead2f 100644 --- a/app/Resources/static/themes/material/css/media_queries.scss +++ b/app/Resources/static/themes/material/css/media_queries.scss @@ -12,6 +12,16 @@ .pagination { margin-left: auto; } + + .card-tag-labels li { + max-width: 50%; + } +} + +@media screen and (min-width: 993px) { + body.entry main #content { + padding-left: 70px; + } } @media only screen and (max-width: 992px) { @@ -163,4 +173,23 @@ .row .col { padding: 0; } + + .card-stacked div.metadata .reading-time { + display: none; + } +} + +@media screen and (max-width: 310px), + screen and (min-width: 601px) and (max-width: 660px), + screen and (min-width: 993px) and (max-width: 1050px), + screen and (min-width: 1201px) and (max-width: 1250px) { + .card .card-action .reading-time .card-created-at { + display: none; + } +} + +@media only print { + body { + display: block; + } } diff --git a/app/Resources/static/themes/material/css/nav.scss b/app/Resources/static/themes/material/css/nav.scss index 147f163fd..b7288278c 100644 --- a/app/Resources/static/themes/material/css/nav.scss +++ b/app/Resources/static/themes/material/css/nav.scss @@ -131,6 +131,11 @@ nav { display: none; } +.entry-nav-top--sticky { + position: sticky; + top: 0; +} + @media (min-width: 993px) { .button-collapse { display: none; diff --git a/app/Resources/static/themes/material/index.js b/app/Resources/static/themes/material/index.js index 96310d81f..e808d75c2 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'; @@ -17,6 +17,36 @@ import './js/shortcuts/entry'; /* Theme style */ import './css/index.scss'; +const stickyNav = () => { + const nav = $('.js-entry-nav-top'); + $('[data-toggle="actions"]').click(() => { + nav.toggleClass('entry-nav-top--sticky'); + }); +}; + +const articleScroll = () => { + const articleEl = $('#article'); + if (articleEl.length > 0) { + $(window).scroll(() => { + const s = $(window).scrollTop(); + const d = $(document).height(); + const c = $(window).height(); + const articleElBottom = articleEl.offset().top + articleEl.height(); + const scrollPercent = (s / (d - c)) * 100; + $('.progress .determinate').css('width', `${scrollPercent}%`); + const fixedActionBtn = $('.js-fixed-action-btn'); + const toggleScrollDataName = 'toggle-auto'; + if ((s + c) > articleElBottom) { + fixedActionBtn.data(toggleScrollDataName, true); + fixedActionBtn.openFAB(); + } else if (fixedActionBtn.data(toggleScrollDataName) === true) { + fixedActionBtn.data(toggleScrollDataName, false); + fixedActionBtn.closeFAB(); + } + }); + } +}; + $(document).ready(() => { // sideNav $('.button-collapse').sideNav(); @@ -32,8 +62,12 @@ $(document).ready(() => { format: 'dd/mm/yyyy', container: 'body', }); + initFilters(); initExport(); + initRandom(); + stickyNav(); + articleScroll(); const toggleNav = (toShow, toFocus) => { $('.nav-panel-actions').hide(100); @@ -48,30 +82,27 @@ $(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(); - const c = $(window).height(); - const scrollPercent = (s / (d - c)) * 100; - $('.progress .determinate').css('width', `${scrollPercent}%`); - }); }); diff --git a/app/Resources/static/themes/material/js/tools.js b/app/Resources/static/themes/material/js/tools.js index 39398fd8a..0b3d30385 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 c5f664dc8..000000000 --- a/app/autoload.php +++ /dev/null @@ -1,13 +0,0 @@ -troubleshoot this issue. + wallabag can't retrieve contents for this article. Please troubleshoot this issue. api_limit_mass_actions: 10 encryption_key_path: "%kernel.project_dir%/data/site-credentials-secret-key.txt" default_internal_settings: @@ -44,7 +44,7 @@ wallabag_core: section: entry - name: diaspora_url - value: http://diasporapod.com + value: https://diasporapod.com section: entry - name: share_unmark @@ -64,11 +64,11 @@ wallabag_core: section: entry - name: shaarli_url - value: http://myshaarli.com + value: https://myshaarli.com section: entry - name: scuttle_url - value: http://scuttle.org + value: https://scuttle.org section: entry - name: share_mail diff --git a/bin/console b/bin/console index 49247c94d..8cef40b2c 100755 --- a/bin/console +++ b/bin/console @@ -6,19 +6,17 @@ use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Debug\Debug; // if you don't want to setup permissions the proper way, just uncomment the following PHP line -// read http://symfony.com/doc/current/book/installation.html#configuration-and-setup for more information +// read https://symfony.com/doc/current/setup.html#checking-symfony-application-configuration-and-setup +// for more information //umask(0000); set_time_limit(0); -/** - * @var Composer\Autoload\ClassLoader $loader - */ -$loader = require __DIR__.'/../app/autoload.php'; +require __DIR__.'/../vendor/autoload.php'; $input = new ArgvInput(); -$env = $input->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 68cfad05d..bfe287c43 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": "*", @@ -43,58 +46,70 @@ "ext-iconv": "*", "ext-tokenizer": "*", "ext-pdo": "*", - "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", + "ext-tidy": "*", + "symfony/symfony": "3.4.*", + "doctrine/orm": "^2.6", + "doctrine/doctrine-bundle": "^1.9", + "doctrine/doctrine-cache-bundle": "^1.3", + "doctrine/dbal": "2.9.2", + "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", - "simplepie/simplepie": "~1.5", + "wallabag/tcpdf": "^6.2.26", "willdurand/hateoas-bundle": "~1.3", "liip/theme-bundle": "^1.4.6", "lexik/form-filter-bundle": "^5.0.4", - "j0k3r/graby": "^1.0", + "j0k3r/graby": "^2.0", + "php-http/guzzle5-adapter": "^2.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": "^4.4", + "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", - "mnapoli/piwik-twig-extension": "^1.0", - "ocramius/proxy-manager": "^1.0.2", - "white-october/pagerfanta-bundle": "^1.1.0", + "craue/config-bundle": "^2.3.0", + "mnapoli/piwik-twig-extension": "^2.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" + "html2text/html2text": "^4.1", + "pragmarx/recovery": "^0.1.0", + "php-http/httplug-bundle": "^1.14", + "sentry/sentry-symfony": "^3.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": "3.4.x-dev", - "friendsofphp/php-cs-fixer": "~2.0", - "m6web/redis-mock": "^2.0", - "dama/doctrine-test-bundle": "^4.0" + "symfony/phpunit-bridge": "~4.3.8", + "friendsofphp/php-cs-fixer": "~2.13", + "m6web/redis-mock": "^5.0", + "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", + "php-http/mock-client": "^1.0", + "guzzlehttp/psr7": "^1.0" + }, + "suggest": { + "ext-imagick": "To keep GIF animation when downloading image is enabled" }, "scripts": { "post-cmd": [ @@ -123,18 +138,35 @@ } }, "autoload": { - "psr-4": { "Wallabag\\": "src/Wallabag/" }, - "classmap": [ "app/AppKernel.php", "app/AppCache.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" + } + ] } diff --git a/composer.lock b/composer.lock new file mode 100644 index 000000000..443c609d8 --- /dev/null +++ b/composer.lock @@ -0,0 +1,11547 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "4458551b4c0cddd988d7bbbc76dc38dc", + "packages": [ + { + "name": "bdunogier/guzzle-site-authenticator", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/wallabag/guzzle-site-authenticator.git", + "reference": "6649aca6f13c27088ef34e4202b31fc0da01d987" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wallabag/guzzle-site-authenticator/zipball/6649aca6f13c27088ef34e4202b31fc0da01d987", + "reference": "6649aca6f13c27088ef34e4202b31fc0da01d987", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^5.2.0", + "psr/log": "^1.0.0", + "symfony/expression-language": "^3.2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.0", + "monolog/monolog": "^1.13", + "php-coveralls/php-coveralls": "~1.0", + "symfony/phpunit-bridge": "^4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "BD\\GuzzleSiteAuthenticatorBundle\\": "bundle/", + "BD\\GuzzleSiteAuthenticator\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bertrand Dunogier", + "email": "bertrand.dunogier@gmail.com" + } + ], + "description": "A guzzle plugin that adds, if necessary, authentication data to requests. Uses credentials and cookies, with login requests to the sites.", + "time": "2018-12-13T21:06:29+00:00" + }, + { + "name": "beberlei/assert", + "version": "v3.2.7", + "source": { + "type": "git", + "url": "https://github.com/beberlei/assert.git", + "reference": "d63a6943fc4fd1a2aedb65994e3548715105abcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/beberlei/assert/zipball/d63a6943fc4fd1a2aedb65994e3548715105abcf", + "reference": "d63a6943fc4fd1a2aedb65994e3548715105abcf", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "php": "^7" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "*", + "phpstan/phpstan-shim": "*", + "phpunit/phpunit": ">=6.0.0 <8" + }, + "suggest": { + "ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles" + }, + "type": "library", + "autoload": { + "psr-4": { + "Assert\\": "lib/Assert" + }, + "files": [ + "lib/Assert/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de", + "role": "Lead Developer" + }, + { + "name": "Richard Quadling", + "email": "rquadling@gmail.com", + "role": "Collaborator" + } + ], + "description": "Thin assertion library for input validation in business models.", + "keywords": [ + "assert", + "assertion", + "validation" + ], + "time": "2019-12-19T17:51:41+00:00" + }, + { + "name": "behat/transliterator", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/Behat/Transliterator.git", + "reference": "826ce7e9c2a6664c0d1f381cbb38b1fb80a7ee2c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Transliterator/zipball/826ce7e9c2a6664c0d1f381cbb38b1fb80a7ee2c", + "reference": "826ce7e9c2a6664c0d1f381cbb38b1fb80a7ee2c", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "chuyskywalker/rolling-curl": "^3.1", + "php-yaoi/php-yaoi": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Transliterator": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Artistic-1.0" + ], + "description": "String transliterator", + "keywords": [ + "i18n", + "slug", + "transliterator" + ], + "time": "2017-04-04T11:38:05+00:00" + }, + { + "name": "clue/stream-filter", + "version": "v1.4.1", + "source": { + "type": "git", + "url": "https://github.com/clue/php-stream-filter.git", + "reference": "5a58cc30a8bd6a4eb8f856adf61dd3e013f53f71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/php-stream-filter/zipball/5a58cc30a8bd6a4eb8f856adf61dd3e013f53f71", + "reference": "5a58cc30a8bd6a4eb8f856adf61dd3e013f53f71", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^5.0 || ^4.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\StreamFilter\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian LÃŒck", + "email": "christian@lueck.tv" + } + ], + "description": "A simple and modern approach to stream filtering in PHP", + "homepage": "https://github.com/clue/php-stream-filter", + "keywords": [ + "bucket brigade", + "callback", + "filter", + "php_user_filter", + "stream", + "stream_filter_append", + "stream_filter_register" + ], + "time": "2019-04-09T12:31:48+00:00" + }, + { + "name": "craue/config-bundle", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/craue/CraueConfigBundle.git", + "reference": "ad474f63d6b51da0d346a7873b3a54f98fa96e32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/craue/CraueConfigBundle/zipball/ad474f63d6b51da0d346a7873b3a54f98fa96e32", + "reference": "ad474f63d6b51da0d346a7873b3a54f98fa96e32", + "shasum": "" + }, + "require": { + "doctrine/doctrine-bundle": "^1.5.1|~2.0", + "php": "~7.0", + "psr/simple-cache": "^1.0", + "symfony/cache": "~3.4|~4.2|~5.0", + "symfony/config": "~3.4|~4.2|~5.0", + "symfony/dependency-injection": "~3.4|~4.2|~5.0", + "symfony/form": "~3.4|~4.2|~5.0", + "symfony/framework-bundle": "~3.4|~4.2|~5.0", + "symfony/http-foundation": "~3.4|~4.2|~5.0", + "symfony/http-kernel": "~3.4|~4.2|~5.0", + "symfony/options-resolver": "~3.4|~4.2|~5.0", + "symfony/validator": "~3.4|~4.2|~5.0" + }, + "require-dev": { + "doctrine/instantiator": "^1.0.5", + "doctrine/orm": "^2.5.14", + "phpunit/phpunit": "^6.5.13|^7.5.1", + "symfony/phpunit-bridge": "~5.0", + "symfony/symfony": "~3.4|~4.2|~5.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Craue\\ConfigBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Raue", + "email": "christian.raue@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/craue/CraueConfigBundle/contributors" + } + ], + "description": "Database-stored settings made available via a service for your Symfony project.", + "homepage": "https://github.com/craue/CraueConfigBundle", + "keywords": [ + "config", + "symfony" + ], + "time": "2019-12-03T08:32:04+00:00" + }, + { + "name": "defuse/php-encryption", + "version": "v2.2.1", + "source": { + "type": "git", + "url": "https://github.com/defuse/php-encryption.git", + "reference": "0f407c43b953d571421e0020ba92082ed5fb7620" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/defuse/php-encryption/zipball/0f407c43b953d571421e0020ba92082ed5fb7620", + "reference": "0f407c43b953d571421e0020ba92082ed5fb7620", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "paragonie/random_compat": ">= 2", + "php": ">=5.4.0" + }, + "require-dev": { + "nikic/php-parser": "^2.0|^3.0|^4.0", + "phpunit/phpunit": "^4|^5" + }, + "bin": [ + "bin/generate-defuse-key" + ], + "type": "library", + "autoload": { + "psr-4": { + "Defuse\\Crypto\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Hornby", + "email": "taylor@defuse.ca", + "homepage": "https://defuse.ca/" + }, + { + "name": "Scott Arciszewski", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "Secure PHP Encryption Library", + "keywords": [ + "aes", + "authenticated encryption", + "cipher", + "crypto", + "cryptography", + "encrypt", + "encryption", + "openssl", + "security", + "symmetric key cryptography" + ], + "time": "2018-07-24T23:27:56+00:00" + }, + { + "name": "doctrine/annotations", + "version": "v1.8.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "904dca4eb10715b92569fbcd79e201d5c349b6bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/904dca4eb10715b92569fbcd79e201d5c349b6bc", + "reference": "904dca4eb10715b92569fbcd79e201d5c349b6bc", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "php": "^7.1" + }, + "require-dev": { + "doctrine/cache": "1.*", + "phpunit/phpunit": "^7.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "time": "2019-10-01T18:55:10+00:00" + }, + { + "name": "doctrine/cache", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "382e7f4db9a12dc6c19431743a2b096041bcdd62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/382e7f4db9a12dc6c19431743a2b096041bcdd62", + "reference": "382e7f4db9a12dc6c19431743a2b096041bcdd62", + "shasum": "" + }, + "require": { + "php": "~7.1" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "alcaeus/mongo-php-adapter": "^1.1", + "doctrine/coding-standard": "^6.0", + "mongodb/mongodb": "^1.1", + "phpunit/phpunit": "^7.0", + "predis/predis": "~1.0" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", + "keywords": [ + "abstraction", + "apcu", + "cache", + "caching", + "couchdb", + "memcached", + "php", + "redis", + "xcache" + ], + "time": "2019-11-29T15:36:20+00:00" + }, + { + "name": "doctrine/collections", + "version": "1.6.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "6b1e4b2b66f6d6e49983cebfe23a21b7ccc5b0d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/6b1e4b2b66f6d6e49983cebfe23a21b7ccc5b0d7", + "reference": "6b1e4b2b66f6d6e49983cebfe23a21b7ccc5b0d7", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan-shim": "^0.9.2", + "phpunit/phpunit": "^7.0", + "vimeo/psalm": "^3.2.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Collections\\": "lib/Doctrine/Common/Collections" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", + "homepage": "https://www.doctrine-project.org/projects/collections.html", + "keywords": [ + "array", + "collections", + "iterators", + "php" + ], + "time": "2019-11-13T13:07:11+00:00" + }, + { + "name": "doctrine/common", + "version": "2.12.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/common.git", + "reference": "2053eafdf60c2172ee1373d1b9289ba1db7f1fc6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/common/zipball/2053eafdf60c2172ee1373d1b9289ba1db7f1fc6", + "reference": "2053eafdf60c2172ee1373d1b9289ba1db7f1fc6", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0", + "doctrine/cache": "^1.0", + "doctrine/collections": "^1.0", + "doctrine/event-manager": "^1.0", + "doctrine/inflector": "^1.0", + "doctrine/lexer": "^1.0", + "doctrine/persistence": "^1.1", + "doctrine/reflection": "^1.0", + "php": "^7.1" + }, + "require-dev": { + "doctrine/coding-standard": "^1.0", + "phpstan/phpstan": "^0.11", + "phpstan/phpstan-phpunit": "^0.11", + "phpunit/phpunit": "^7.0", + "squizlabs/php_codesniffer": "^3.0", + "symfony/phpunit-bridge": "^4.0.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection support, persistence interfaces, proxies, event system and much more.", + "homepage": "https://www.doctrine-project.org/projects/common.html", + "keywords": [ + "common", + "doctrine", + "php" + ], + "time": "2020-01-10T15:49:25+00:00" + }, + { + "name": "doctrine/dbal", + "version": "v2.9.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9", + "reference": "22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9", + "shasum": "" + }, + "require": { + "doctrine/cache": "^1.0", + "doctrine/event-manager": "^1.0", + "ext-pdo": "*", + "php": "^7.1" + }, + "require-dev": { + "doctrine/coding-standard": "^5.0", + "jetbrains/phpstorm-stubs": "^2018.1.2", + "phpstan/phpstan": "^0.10.1", + "phpunit/phpunit": "^7.4", + "symfony/console": "^2.0.5|^3.0|^4.0", + "symfony/phpunit-bridge": "^3.4.5|^4.0.5" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.9.x-dev", + "dev-develop": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "lib/Doctrine/DBAL" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "dbal", + "mysql", + "persistence", + "pgsql", + "php", + "queryobject" + ], + "time": "2018-12-31T03:27:51+00:00" + }, + { + "name": "doctrine/doctrine-bundle", + "version": "1.12.7", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineBundle.git", + "reference": "18fb7d271676dcb8e882adb0157ac1445c8fe89c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/18fb7d271676dcb8e882adb0157ac1445c8fe89c", + "reference": "18fb7d271676dcb8e882adb0157ac1445c8fe89c", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^2.5.12", + "doctrine/doctrine-cache-bundle": "~1.2", + "doctrine/persistence": "^1.3.3", + "jdorn/sql-formatter": "^1.2.16", + "php": "^7.1", + "symfony/cache": "^3.4.30|^4.3.3", + "symfony/config": "^3.4.30|^4.3.3", + "symfony/console": "^3.4.30|^4.3.3", + "symfony/dependency-injection": "^3.4.30|^4.3.3", + "symfony/doctrine-bridge": "^3.4.30|^4.3.3", + "symfony/framework-bundle": "^3.4.30|^4.3.3", + "symfony/service-contracts": "^1.1.1|^2.0" + }, + "conflict": { + "doctrine/orm": "<2.6", + "twig/twig": "<1.34|>=2.0,<2.4" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "doctrine/orm": "^2.6", + "ocramius/proxy-manager": "^2.1", + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^7.5", + "symfony/phpunit-bridge": "^4.2", + "symfony/property-info": "^3.4.30|^4.3.3", + "symfony/proxy-manager-bridge": "^3.4|^4|^5", + "symfony/twig-bridge": "^3.4|^4.1", + "symfony/validator": "^3.4.30|^4.3.3", + "symfony/web-profiler-bundle": "^3.4.30|^4.3.3", + "symfony/yaml": "^3.4.30|^4.3.3", + "twig/twig": "^1.34|^2.12" + }, + "suggest": { + "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", + "symfony/web-profiler-bundle": "To use the data collector." + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "1.12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\DoctrineBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Doctrine Project", + "homepage": "http://www.doctrine-project.org/" + } + ], + "description": "Symfony DoctrineBundle", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "database", + "dbal", + "orm", + "persistence" + ], + "time": "2020-01-10T12:25:22+00:00" + }, + { + "name": "doctrine/doctrine-cache-bundle", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineCacheBundle.git", + "reference": "6bee2f9b339847e8a984427353670bad4e7bdccb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineCacheBundle/zipball/6bee2f9b339847e8a984427353670bad4e7bdccb", + "reference": "6bee2f9b339847e8a984427353670bad4e7bdccb", + "shasum": "" + }, + "require": { + "doctrine/cache": "^1.4.2", + "doctrine/inflector": "^1.0", + "php": "^7.1", + "symfony/doctrine-bridge": "^3.4|^4.0" + }, + "require-dev": { + "instaclick/coding-standard": "~1.1", + "instaclick/object-calisthenics-sniffs": "dev-master", + "instaclick/symfony2-coding-standard": "dev-remaster", + "phpunit/phpunit": "^7.0", + "predis/predis": "~0.8", + "satooshi/php-coveralls": "^1.0", + "squizlabs/php_codesniffer": "~1.5", + "symfony/console": "^3.4|^4.0", + "symfony/finder": "^3.4|^4.0", + "symfony/framework-bundle": "^3.4|^4.0", + "symfony/phpunit-bridge": "^3.4|^4.0", + "symfony/security-acl": "^2.8", + "symfony/validator": "^3.4|^4.0", + "symfony/yaml": "^3.4|^4.0" + }, + "suggest": { + "symfony/security-acl": "For using this bundle to cache ACLs" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\DoctrineCacheBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Fabio B. Silva", + "email": "fabio.bat.silva@gmail.com" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@hotmail.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Doctrine Project", + "homepage": "http://www.doctrine-project.org/" + } + ], + "description": "Symfony Bundle for Doctrine Cache", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "cache", + "caching" + ], + "time": "2019-11-29T11:22:01+00:00" + }, + { + "name": "doctrine/doctrine-migrations-bundle", + "version": "v1.3.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", + "reference": "49fa399181db4bf4f9f725126bd1cb65c4398dce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/49fa399181db4bf4f9f725126bd1cb65c4398dce", + "reference": "49fa399181db4bf4f9f725126bd1cb65c4398dce", + "shasum": "" + }, + "require": { + "doctrine/doctrine-bundle": "~1.0", + "doctrine/migrations": "^1.1", + "php": ">=5.4.0", + "symfony/framework-bundle": "~2.7|~3.3|~4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^7.4" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\MigrationsBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Doctrine Project", + "homepage": "http://www.doctrine-project.org" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Symfony DoctrineMigrationsBundle", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "dbal", + "migrations", + "schema" + ], + "time": "2018-12-03T11:55:33+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "629572819973f13486371cb611386eb17851e85c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/629572819973f13486371cb611386eb17851e85c", + "reference": "629572819973f13486371cb611386eb17851e85c", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "conflict": { + "doctrine/common": "<2.9@dev" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "time": "2019-11-10T09:48:07+00:00" + }, + { + "name": "doctrine/inflector", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "ec3a55242203ffa6a4b27c58176da97ff0a7aec1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/ec3a55242203ffa6a4b27c58176da97ff0a7aec1", + "reference": "ec3a55242203ffa6a4b27c58176da97ff0a7aec1", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Inflector\\": "lib/Doctrine/Common/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Common String Manipulations with regard to casing and singular/plural rules.", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "inflection", + "pluralize", + "singularize", + "string" + ], + "time": "2019-10-30T19:59:35+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "ae466f726242e637cebdd526a7d991b9433bacf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1", + "reference": "ae466f726242e637cebdd526a7d991b9433bacf1", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.13", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-shim": "^0.11", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2019-10-21T16:45:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "1febd6c3ef84253d7c815bed85fc622ad207a9f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/1febd6c3ef84253d7c815bed85fc622ad207a9f8", + "reference": "1febd6c3ef84253d7c815bed85fc622ad207a9f8", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "^4.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "time": "2019-06-08T11:03:04+00:00" + }, + { + "name": "doctrine/migrations", + "version": "v1.8.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/migrations.git", + "reference": "215438c0eef3e5f9b7da7d09c6b90756071b43e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/215438c0eef3e5f9b7da7d09c6b90756071b43e6", + "reference": "215438c0eef3e5f9b7da7d09c6b90756071b43e6", + "shasum": "" + }, + "require": { + "doctrine/dbal": "~2.6", + "ocramius/proxy-manager": "^1.0|^2.0", + "php": "^7.1", + "symfony/console": "~3.3|^4.0" + }, + "require-dev": { + "doctrine/coding-standard": "^1.0", + "doctrine/orm": "~2.5", + "jdorn/sql-formatter": "~1.1", + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "~7.0", + "squizlabs/php_codesniffer": "^3.0", + "symfony/yaml": "~3.3|^4.0" + }, + "suggest": { + "jdorn/sql-formatter": "Allows to generate formatted SQL with the diff command.", + "symfony/yaml": "Allows the use of yaml for migration configuration files." + }, + "bin": [ + "bin/doctrine-migrations" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "v1.8.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\Migrations\\": "lib/Doctrine/DBAL/Migrations", + "Doctrine\\Migrations\\": "lib/Doctrine/Migrations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Michael Simonson", + "email": "contact@mikesimonson.com" + } + ], + "description": "Database Schema migrations using Doctrine DBAL", + "homepage": "https://www.doctrine-project.org/projects/migrations.html", + "keywords": [ + "database", + "migrations" + ], + "time": "2018-06-06T21:00:30+00:00" + }, + { + "name": "doctrine/orm", + "version": "v2.6.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/orm.git", + "reference": "2d9b9351831d1230881c52f006011cbf72fe944e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/orm/zipball/2d9b9351831d1230881c52f006011cbf72fe944e", + "reference": "2d9b9351831d1230881c52f006011cbf72fe944e", + "shasum": "" + }, + "require": { + "doctrine/annotations": "~1.5", + "doctrine/cache": "~1.6", + "doctrine/collections": "^1.4", + "doctrine/common": "^2.7.1", + "doctrine/dbal": "^2.6", + "doctrine/instantiator": "~1.1", + "ext-pdo": "*", + "php": "^7.1", + "symfony/console": "~3.0|~4.0" + }, + "require-dev": { + "doctrine/coding-standard": "^5.0", + "phpunit/phpunit": "^7.5", + "symfony/yaml": "~3.4|~4.0" + }, + "suggest": { + "symfony/yaml": "If you want to use YAML Metadata Mapping Driver" + }, + "bin": [ + "bin/doctrine" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\ORM\\": "lib/Doctrine/ORM" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Object-Relational-Mapper for PHP", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "database", + "orm" + ], + "time": "2019-11-18T22:01:21+00:00" + }, + { + "name": "doctrine/persistence", + "version": "1.3.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/persistence.git", + "reference": "5dd3ac5eebef2d0b074daa4440bb18f93132dee4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/5dd3ac5eebef2d0b074daa4440bb18f93132dee4", + "reference": "5dd3ac5eebef2d0b074daa4440bb18f93132dee4", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0", + "doctrine/cache": "^1.0", + "doctrine/collections": "^1.0", + "doctrine/event-manager": "^1.0", + "doctrine/reflection": "^1.1", + "php": "^7.1" + }, + "conflict": { + "doctrine/common": "<2.10@dev" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan": "^0.11", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common", + "Doctrine\\Persistence\\": "lib/Doctrine/Persistence" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.", + "homepage": "https://doctrine-project.org/projects/persistence.html", + "keywords": [ + "mapper", + "object", + "odm", + "orm", + "persistence" + ], + "time": "2020-01-16T22:06:23+00:00" + }, + { + "name": "doctrine/reflection", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/reflection.git", + "reference": "bc420ead87fdfe08c03ecc3549db603a45b06d4c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/reflection/zipball/bc420ead87fdfe08c03ecc3549db603a45b06d4c", + "reference": "bc420ead87fdfe08c03ecc3549db603a45b06d4c", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0", + "ext-tokenizer": "*", + "php": "^7.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^5.0", + "doctrine/common": "^2.10", + "phpstan/phpstan": "^0.11.0", + "phpstan/phpstan-phpunit": "^0.11.0", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Reflection project is a simple library used by the various Doctrine projects which adds some additional functionality on top of the reflection functionality that comes with PHP. It allows you to get the reflection information about classes, methods and properties statically.", + "homepage": "https://www.doctrine-project.org/projects/reflection.html", + "keywords": [ + "reflection", + "static" + ], + "time": "2020-01-08T19:53:19+00:00" + }, + { + "name": "egulias/email-validator", + "version": "2.1.12", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "a6255605af39f2db7f5cb62e672bd8a7bad8d208" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/a6255605af39f2db7f5cb62e672bd8a7bad8d208", + "reference": "a6255605af39f2db7f5cb62e672bd8a7bad8d208", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1.0.1", + "php": ">= 5.5" + }, + "require-dev": { + "dominicsayers/isemail": "dev-master", + "phpunit/phpunit": "^4.8.35||^5.7||^6.0", + "satooshi/php-coveralls": "^1.0.1", + "symfony/phpunit-bridge": "^4.4@dev" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "EmailValidator" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "time": "2019-12-20T12:49:39+00:00" + }, + { + "name": "electrolinux/php-html5lib", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://github.com/electrolinux/php-html5lib.git", + "reference": "9f92154993c7ecb120d9f9c0e558660d32846721" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/electrolinux/php-html5lib/zipball/9f92154993c7ecb120d9f9c0e558660d32846721", + "reference": "9f92154993c7ecb120d9f9c0e558660d32846721", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "type": "library", + "autoload": { + "psr-0": { + "HTML5Lib": "src/", + "HTML5Lib\\Tests": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Multiple users", + "homepage": "https://code.google.com/p/html5lib/", + "role": "Original developers" + }, + { + "name": "Sébastien Lavoie", + "homepage": "http://blog.lavoie.sl", + "role": "Packager" + }, + { + "name": "didier Belot", + "role": "Packager" + } + ], + "description": "A PHP implementations of a HTML parser based on the WHATWG HTML5 specification.", + "homepage": "https://github.com/electrolinux/php-html5lib", + "keywords": [ + "HTML5", + "php" + ], + "time": "2013-03-18T18:32:30+00:00" + }, + { + "name": "fig/link-util", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/link-util.git", + "reference": "47f55860678a9e202206047bc02767556d298106" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/link-util/zipball/47f55860678a9e202206047bc02767556d298106", + "reference": "47f55860678a9e202206047bc02767556d298106", + "shasum": "" + }, + "require": { + "php": ">=5.5.0", + "psr/link": "~1.0@dev" + }, + "provide": { + "psr/link-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.1", + "squizlabs/php_codesniffer": "^2.3.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fig\\Link\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common utility implementations for HTTP links", + "keywords": [ + "http", + "http-link", + "link", + "psr", + "psr-13", + "rest" + ], + "time": "2019-12-18T15:40:05+00:00" + }, + { + "name": "fossar/htmlawed", + "version": "1.2.7", + "source": { + "type": "git", + "url": "https://github.com/fossar/HTMLawed.git", + "reference": "4f7505e9622a96242923895c3008719b2c2283d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fossar/HTMLawed/zipball/4f7505e9622a96242923895c3008719b2c2283d3", + "reference": "4f7505e9622a96242923895c3008719b2c2283d3", + "shasum": "" + }, + "require": { + "php": ">4.4.0" + }, + "replace": { + "htmlawed/htmlawed": "*" + }, + "type": "library", + "autoload": { + "files": [ + "htmLawed.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0+", + "LGPL-3.0" + ], + "authors": [ + { + "name": "Santosh Patnaik", + "homepage": "http://www.bioinformatics.org/people/index.php?user_hash=558b661f92d0ff7b", + "role": "Developer" + } + ], + "description": "htmLawed - Process text with HTML markup to make it more compliant with HTML standards and administrative policies", + "homepage": "http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed/", + "keywords": [ + "HTMLtidy", + "html", + "sanitize", + "strip", + "tags" + ], + "time": "2019-05-09T11:53:40+00:00" + }, + { + "name": "friendsofsymfony/jsrouting-bundle", + "version": "2.5.3", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfSymfony/FOSJsRoutingBundle.git", + "reference": "165b3f4ae1f798ec320b9942f6db921f7b568bed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfSymfony/FOSJsRoutingBundle/zipball/165b3f4ae1f798ec320b9942f6db921f7b568bed", + "reference": "165b3f4ae1f798ec320b9942f6db921f7b568bed", + "shasum": "" + }, + "require": { + "php": "^7.1", + "symfony/console": "~3.3|^4.0|^5.0", + "symfony/framework-bundle": "~3.3|^4.0|^5.0", + "symfony/serializer": "~3.3|^4.0|^5.0", + "willdurand/jsonp-callback-validator": "~1.0" + }, + "require-dev": { + "symfony/expression-language": "~3.3|^4.0|^5.0", + "symfony/phpunit-bridge": "^3.3|^4.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "FOS\\JsRoutingBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "William Durand", + "email": "will+git@drnd.me" + }, + { + "name": "FriendsOfSymfony Community", + "homepage": "https://github.com/friendsofsymfony/FOSJsRoutingBundle/contributors" + } + ], + "description": "A pretty nice way to expose your Symfony2 routing to client applications.", + "homepage": "http://friendsofsymfony.github.com", + "keywords": [ + "Js Routing", + "javascript", + "routing" + ], + "time": "2020-01-13T14:06:12+00:00" + }, + { + "name": "friendsofsymfony/oauth-server-bundle", + "version": "1.6.2", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfSymfony/FOSOAuthServerBundle.git", + "reference": "fcaa25cc49474bdb0db7894f880976fe76ffed23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfSymfony/FOSOAuthServerBundle/zipball/fcaa25cc49474bdb0db7894f880976fe76ffed23", + "reference": "fcaa25cc49474bdb0db7894f880976fe76ffed23", + "shasum": "" + }, + "require": { + "friendsofsymfony/oauth2-php": "~1.1", + "paragonie/random_compat": "^1|^2", + "php": "^5.5|^7.0", + "symfony/dependency-injection": "^2.8|~3.0|^4.0", + "symfony/framework-bundle": "~2.8|~3.0|^4.0", + "symfony/security-bundle": "~2.8|~3.0|^4.0" + }, + "require-dev": { + "doctrine/doctrine-bundle": "~1.0", + "doctrine/mongodb-odm": "~1.0", + "doctrine/orm": "~2.2", + "phing/phing": "~2.4", + "php-mock/php-mock-phpunit": "^1.1", + "phpunit/phpunit": "~4.8|~5.0", + "propel/propel1": "^1.6.5", + "symfony/class-loader": "~2.8|~3.0|^4.0", + "symfony/console": "~2.8|~3.0|^4.0", + "symfony/form": "~2.8|~3.0|^4.0", + "symfony/phpunit-bridge": "~2.8|~3.0|^4.0", + "symfony/templating": "~2.8|~3.0|^4.0", + "symfony/twig-bundle": "~2.8|~3.0|^4.0", + "symfony/yaml": "~2.8|~3.0|^4.0", + "willdurand/propel-typehintable-behavior": "^1.0.4" + }, + "suggest": { + "doctrine/doctrine-bundle": "*", + "doctrine/mongodb-odm-bundle": "*", + "propel/propel-bundle": "If you want to use Propel with Symfony2, then you will have to install the PropelBundle", + "symfony/console": "Needed to be able to use commands", + "symfony/form": "Needed to be able to use the AuthorizeFormType", + "willdurand/propel-typehintable-behavior": "The Typehintable behavior is useful to add type hints on generated methods, to be compliant with interfaces" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "psr-4": { + "FOS\\OAuthServerBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Arnaud Le Blanc", + "email": "arnaud.lb@gmail.com" + }, + { + "name": "FriendsOfSymfony Community", + "homepage": "https://github.com/FriendsOfSymfony/FOSOAuthServerBundle/contributors" + } + ], + "description": "Symfony2 OAuth Server Bundle", + "homepage": "http://friendsofsymfony.github.com", + "keywords": [ + "oauth", + "oauth2", + "server" + ], + "time": "2019-01-23T15:23:04+00:00" + }, + { + "name": "friendsofsymfony/oauth2-php", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfSymfony/oauth2-php.git", + "reference": "a41fef63f81ef2ef632350a6c7dc66d15baa9240" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfSymfony/oauth2-php/zipball/a41fef63f81ef2ef632350a6c7dc66d15baa9240", + "reference": "a41fef63f81ef2ef632350a6c7dc66d15baa9240", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/http-foundation": "~3.0|~4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "OAuth2\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Arnaud Le Blanc", + "email": "arnaud.lb@gmail.com" + }, + { + "name": "FriendsOfSymfony Community", + "homepage": "https://github.com/FriendsOfSymfony/oauth2-php/contributors" + } + ], + "description": "OAuth2 library", + "homepage": "https://github.com/FriendsOfSymfony/oauth2-php", + "keywords": [ + "oauth", + "oauth2" + ], + "time": "2018-01-30T19:58:25+00:00" + }, + { + "name": "friendsofsymfony/rest-bundle", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfSymfony/FOSRestBundle.git", + "reference": "42bdfed7db4e253fbd1b295ad06777f01a6d0450" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfSymfony/FOSRestBundle/zipball/42bdfed7db4e253fbd1b295ad06777f01a6d0450", + "reference": "42bdfed7db4e253fbd1b295ad06777f01a6d0450", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^1.0", + "php": "^7.1", + "psr/log": "^1.0", + "symfony/config": "^3.4|^4.3", + "symfony/debug": "^3.4|^4.3", + "symfony/dependency-injection": "^3.4|^4.3", + "symfony/event-dispatcher": "^3.4|^4.3", + "symfony/finder": "^3.4|^4.3", + "symfony/framework-bundle": "^3.4|^4.3", + "symfony/http-foundation": "^3.4|^4.3", + "symfony/http-kernel": "^3.4|^4.3", + "symfony/routing": "^3.4|^4.3", + "symfony/security-core": "^3.4|^4.3", + "willdurand/jsonp-callback-validator": "^1.0", + "willdurand/negotiation": "^2.0" + }, + "conflict": { + "jms/serializer": "<1.13.0", + "jms/serializer-bundle": "<2.0.0", + "sensio/framework-extra-bundle": "<3.0.13" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.0", + "jms/serializer": "^1.13|^2.0|^3.0", + "jms/serializer-bundle": "^2.3.1|^3.0", + "phpoption/phpoption": "^1.1", + "psr/http-message": "^1.0", + "sensio/framework-extra-bundle": "^3.0.13|^4.0|^5.0", + "symfony/asset": "^3.4|^4.3", + "symfony/browser-kit": "^3.4|^4.3", + "symfony/css-selector": "^3.4|^4.3", + "symfony/expression-language": "^3.4|^4.3", + "symfony/form": "^3.4|^4.3", + "symfony/phpunit-bridge": "^4.1.8", + "symfony/security-bundle": "^3.4|^4.3", + "symfony/serializer": "^3.4|^4.3", + "symfony/templating": "^3.4|^4.3", + "symfony/twig-bundle": "^3.4|^4.3", + "symfony/validator": "^3.4|^4.3", + "symfony/web-profiler-bundle": "^3.4|^4.3", + "symfony/yaml": "^3.4|^4.3" + }, + "suggest": { + "jms/serializer-bundle": "Add support for advanced serialization capabilities, recommended, requires ^2.0|^3.0", + "sensio/framework-extra-bundle": "Add support for the request body converter and the view response listener, requires ^3.0", + "symfony/expression-language": "Add support for using the expression language in the routing, requires ^2.7|^3.0", + "symfony/serializer": "Add support for basic serialization capabilities and xml decoding, requires ^2.7|^3.0", + "symfony/validator": "Add support for validation capabilities in the ParamFetcher, requires ^2.7|^3.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "FOS\\RestBundle\\": "" + }, + "exclude-from-classmap": [ + "Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lukas Kahwe Smith", + "email": "smith@pooteeweet.org" + }, + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com" + }, + { + "name": "FriendsOfSymfony Community", + "homepage": "https://github.com/friendsofsymfony/FOSRestBundle/contributors" + } + ], + "description": "This Bundle provides various tools to rapidly develop RESTful API's with Symfony", + "homepage": "http://friendsofsymfony.github.com", + "keywords": [ + "rest" + ], + "time": "2020-01-31T15:56:38+00:00" + }, + { + "name": "friendsofsymfony/user-bundle", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfSymfony/FOSUserBundle.git", + "reference": "2fc8a023d7ab482321cf7ec810ed49eab40eb50f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfSymfony/FOSUserBundle/zipball/2fc8a023d7ab482321cf7ec810ed49eab40eb50f", + "reference": "2fc8a023d7ab482321cf7ec810ed49eab40eb50f", + "shasum": "" + }, + "require": { + "paragonie/random_compat": "^1 || ^2", + "php": "^5.5.9 || ^7.0", + "symfony/form": "^2.7 || ^3.0", + "symfony/framework-bundle": "^2.7 || ^3.0", + "symfony/security-bundle": "^2.7 || ^3.0", + "symfony/templating": "^2.7 || ^3.0", + "symfony/twig-bundle": "^2.7 || ^3.0", + "twig/twig": "^1.28 || ^2.0" + }, + "conflict": { + "doctrine/doctrine-bundle": "<1.3", + "symfony/doctrine-bridge": "<2.7" + }, + "require-dev": { + "doctrine/doctrine-bundle": "^1.3", + "friendsofphp/php-cs-fixer": "^1.11", + "phpunit/phpunit": "~4.8|~5.0", + "swiftmailer/swiftmailer": "^4.3 || ^5.0 || ^6.0", + "symfony/console": "^2.7 || ^3.0", + "symfony/phpunit-bridge": "^2.7 || ^3.0", + "symfony/validator": "^2.7 || ^3.0", + "symfony/yaml": "^2.7 || ^3.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "FOS\\UserBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + }, + { + "name": "FriendsOfSymfony Community", + "homepage": "https://github.com/friendsofsymfony/FOSUserBundle/contributors" + }, + { + "name": "Thibault Duplessis", + "email": "thibault.duplessis@gmail.com" + } + ], + "description": "Symfony FOSUserBundle", + "homepage": "http://friendsofsymfony.github.com", + "keywords": [ + "User management" + ], + "time": "2017-11-29T17:01:21+00:00" + }, + { + "name": "gedmo/doctrine-extensions", + "version": "v2.4.38", + "source": { + "type": "git", + "url": "https://github.com/Atlantic18/DoctrineExtensions.git", + "reference": "81681364331b131518060e4776300a5346df1eb5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Atlantic18/DoctrineExtensions/zipball/81681364331b131518060e4776300a5346df1eb5", + "reference": "81681364331b131518060e4776300a5346df1eb5", + "shasum": "" + }, + "require": { + "behat/transliterator": "~1.2", + "doctrine/common": "~2.4", + "php": ">=5.3.2" + }, + "conflict": { + "doctrine/annotations": "<1.2", + "doctrine/mongodb-odm": ">=2.0" + }, + "require-dev": { + "doctrine/common": ">=2.5.0", + "doctrine/mongodb-odm": ">=1.0.2 <2.0", + "doctrine/orm": ">=2.5.0", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5", + "symfony/yaml": "~2.6 || ~3.0 || ~4.0" + }, + "suggest": { + "doctrine/mongodb-odm": "to use the extensions with the MongoDB ODM", + "doctrine/orm": "to use the extensions with the ORM" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Gedmo\\": "lib/Gedmo" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gediminas Morkevicius", + "email": "gediminas.morkevicius@gmail.com" + }, + { + "name": "Gustavo Falco", + "email": "comfortablynumb84@gmail.com" + }, + { + "name": "David Buchmann", + "email": "david@liip.ch" + } + ], + "description": "Doctrine2 behavioral extensions", + "homepage": "http://gediminasm.org/", + "keywords": [ + "Blameable", + "behaviors", + "doctrine2", + "extensions", + "gedmo", + "loggable", + "nestedset", + "sluggable", + "sortable", + "timestampable", + "translatable", + "tree", + "uploadable" + ], + "time": "2019-11-08T22:33:07+00:00" + }, + { + "name": "grandt/binstring", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/Grandt/PHPBinString.git", + "reference": "825fe2ac8a68190f651fc2dbc07b6edde18bc431" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Grandt/PHPBinString/zipball/825fe2ac8a68190f651fc2dbc07b6edde18bc431", + "reference": "825fe2ac8a68190f651fc2dbc07b6edde18bc431", + "shasum": "" + }, + "require": { + "php": ">=5.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "BinString.php", + "BinStringStatic.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "A. Grandt", + "email": "php@grandt.com", + "role": "Developer" + } + ], + "description": "A class for working around the use of mbstring.func_override", + "homepage": "https://github.com/Grandt/PHPBinString", + "keywords": [ + "binary strings", + "mbstring" + ], + "time": "2015-08-13T06:14:41+00:00" + }, + { + "name": "grandt/phpepub", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Daniel-KM/PHPePub.git", + "reference": "78546d9a49d59ad9a32b13f3d95cc2f548add1c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Daniel-KM/PHPePub/zipball/78546d9a49d59ad9a32b13f3d95cc2f548add1c3", + "reference": "78546d9a49d59ad9a32b13f3d95cc2f548add1c3", + "shasum": "" + }, + "require": { + "grandt/phpresizegif": "~1.0.3", + "grandt/relativepath": "~1.0.1", + "masterminds/html5": "~2.3", + "php": ">=5.3.0", + "phpzip/phpzip": "~2.0.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPePub\\": "src/PHPePub" + }, + "classmap": [ + "src/lib.uuid.php" + ] + }, + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "A. Grandt", + "email": "php@grandt.com", + "homepage": "http://grandt.com", + "role": "Developer" + }, + { + "name": "An, Hyeong-woo", + "email": "mail@mytory.net", + "homepage": "https://mytory.net", + "role": "Contributor" + }, + { + "name": "Daniel Berthereau", + "email": "daniel.composer@berthereau.net", + "role": "Contributor" + } + ], + "description": "Package to create and stream e-books in the ePub 2.0 and 3.0 formats.", + "homepage": "https://github.com/Grandt/PHPePub", + "keywords": [ + "e-book", + "epub" + ], + "support": { + "source": "https://github.com/Daniel-KM/PHPePub/tree/master" + }, + "time": "2018-10-21T22:00:00+00:00" + }, + { + "name": "grandt/phpresizegif", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/Grandt/PHPResizeGif.git", + "reference": "775f6810fcda2fd1d8ca881d44a80c8d310ae7fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Grandt/PHPResizeGif/zipball/775f6810fcda2fd1d8ca881d44a80c8d310ae7fe", + "reference": "775f6810fcda2fd1d8ca881d44a80c8d310ae7fe", + "shasum": "" + }, + "require": { + "grandt/binstring": ">=0.2.0", + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "grandt\\ResizeGif\\": "src/ResizeGif", + "grandt\\ResizeGif\\Files\\": "src/ResizeGif/Files", + "grandt\\ResizeGif\\Structure\\": "src/ResizeGif/Structure", + "grandt\\ResizeGif\\Debug\\": "src/ResizeGif/Debug" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "A. Grandt", + "email": "php@grandt.com", + "homepage": "http://grandt.com", + "role": "Developer" + } + ], + "description": "GIF89a compliant Gif resizer, including transparency and optimized gifs with sub sized elements.", + "homepage": "https://github.com/Grandt/PHPResizeGif", + "keywords": [ + "GIF89a", + "animated gif", + "gif", + "resize" + ], + "time": "2015-05-10T10:52:24+00:00" + }, + { + "name": "grandt/phpzipmerge", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/Grandt/PHPZipMerge.git", + "reference": "0b1273d3c2dbfe244904158b1dbd65a663264fb9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Grandt/PHPZipMerge/zipball/0b1273d3c2dbfe244904158b1dbd65a663264fb9", + "reference": "0b1273d3c2dbfe244904158b1dbd65a663264fb9", + "shasum": "" + }, + "require": { + "grandt/binstring": ">=1.0.0", + "grandt/relativepath": ">=1.0.1", + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipMerge\\": "src/ZipMerge" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "A. Grandt", + "email": "php@grandt.com", + "homepage": "http://grandt.com", + "role": "Developer" + }, + { + "name": "Greg Kappatos", + "homepage": "http://websiteconnect.com.au", + "role": "Developer" + } + ], + "description": "Merge and stream multiple Zip files on the fly.", + "homepage": "https://github.com/Grandt/PHPZipMerge", + "keywords": [ + "archive", + "compressed", + "compression", + "merge", + "phpzip", + "pkzip", + "stream", + "zip" + ], + "time": "2015-08-18T13:49:33+00:00" + }, + { + "name": "grandt/relativepath", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/Grandt/PHPRelativePath.git", + "reference": "19541133c24143b6295688472c54dd6ed15a5462" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Grandt/PHPRelativePath/zipball/19541133c24143b6295688472c54dd6ed15a5462", + "reference": "19541133c24143b6295688472c54dd6ed15a5462", + "shasum": "" + }, + "require": { + "php": ">=5.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "RelativePath.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "A. Grandt", + "email": "php@grandt.com", + "role": "Developer" + } + ], + "description": "A class for cleaning up/collapsing relative paths. Like real_path, but without the need for the path to exist on the filesystem.", + "homepage": "https://github.com/Grandt/PHPRelativePath", + "keywords": [ + "file path" + ], + "time": "2015-05-14T08:18:23+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "5.3.4", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b87eda7a7162f95574032da17e9323c9899cb6b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b87eda7a7162f95574032da17e9323c9899cb6b2", + "reference": "b87eda7a7162f95574032da17e9323c9899cb6b2", + "shasum": "" + }, + "require": { + "guzzlehttp/ringphp": "^1.1", + "php": ">=5.4.0", + "react/promise": "^2.2" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2019-10-30T09:32:00+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2016-12-20T10:07:11+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.6.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "239400de7a173fe9901b9ac7c06497751f00727a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a", + "reference": "239400de7a173fe9901b9ac7c06497751f00727a", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + }, + "suggest": { + "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2019-07-01T23:21:34+00:00" + }, + { + "name": "guzzlehttp/ringphp", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/RingPHP.git", + "reference": "5e2a174052995663dd68e6b5ad838afd47dd615b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/RingPHP/zipball/5e2a174052995663dd68e6b5ad838afd47dd615b", + "reference": "5e2a174052995663dd68e6b5ad838afd47dd615b", + "shasum": "" + }, + "require": { + "guzzlehttp/streams": "~3.0", + "php": ">=5.4.0", + "react/promise": "~2.0" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "ext-curl": "Guzzle will use specific adapters if cURL is present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Ring\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function.", + "abandoned": true, + "time": "2018-07-31T13:22:33+00:00" + }, + { + "name": "guzzlehttp/streams", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/streams.git", + "reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/streams/zipball/47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5", + "reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Provides a simple abstraction over streams of data", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "Guzzle", + "stream" + ], + "abandoned": true, + "time": "2014-10-12T19:18:40+00:00" + }, + { + "name": "hoa/compiler", + "version": "3.17.08.08", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Compiler.git", + "reference": "aa09caf0bf28adae6654ca6ee415ee2f522672de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Compiler/zipball/aa09caf0bf28adae6654ca6ee415ee2f522672de", + "reference": "aa09caf0bf28adae6654ca6ee415ee2f522672de", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0", + "hoa/file": "~1.0", + "hoa/iterator": "~2.0", + "hoa/math": "~1.0", + "hoa/protocol": "~1.0", + "hoa/regex": "~1.0", + "hoa/visitor": "~2.0" + }, + "require-dev": { + "hoa/json": "~2.0", + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Compiler\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Compiler library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "algebraic", + "ast", + "compiler", + "context-free", + "coverage", + "exhaustive", + "grammar", + "isotropic", + "language", + "lexer", + "library", + "ll1", + "llk", + "parser", + "pp", + "random", + "regular", + "rule", + "sampler", + "syntax", + "token", + "trace", + "uniform" + ], + "time": "2017-08-08T07:44:07+00:00" + }, + { + "name": "hoa/consistency", + "version": "1.17.05.02", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Consistency.git", + "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Consistency/zipball/fd7d0adc82410507f332516faf655b6ed22e4c2f", + "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f", + "shasum": "" + }, + "require": { + "hoa/exception": "~1.0", + "php": ">=5.5.0" + }, + "require-dev": { + "hoa/stream": "~1.0", + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Consistency\\": "." + }, + "files": [ + "Prelude.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Consistency library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "autoloader", + "callable", + "consistency", + "entity", + "flex", + "keyword", + "library" + ], + "time": "2017-05-02T12:18:12+00:00" + }, + { + "name": "hoa/event", + "version": "1.17.01.13", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Event.git", + "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Event/zipball/6c0060dced212ffa3af0e34bb46624f990b29c54", + "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Event\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Event library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "event", + "library", + "listener", + "observer" + ], + "time": "2017-01-13T15:30:50+00:00" + }, + { + "name": "hoa/exception", + "version": "1.17.01.16", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Exception.git", + "reference": "091727d46420a3d7468ef0595651488bfc3a458f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Exception/zipball/091727d46420a3d7468ef0595651488bfc3a458f", + "reference": "091727d46420a3d7468ef0595651488bfc3a458f", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Exception\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Exception library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "exception", + "library" + ], + "time": "2017-01-16T07:53:27+00:00" + }, + { + "name": "hoa/file", + "version": "1.17.07.11", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/File.git", + "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/File/zipball/35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", + "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0", + "hoa/exception": "~1.0", + "hoa/iterator": "~2.0", + "hoa/stream": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\File\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\File library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "Socket", + "directory", + "file", + "finder", + "library", + "link", + "temporary" + ], + "time": "2017-07-11T07:42:15+00:00" + }, + { + "name": "hoa/iterator", + "version": "2.17.01.10", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Iterator.git", + "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Iterator/zipball/d1120ba09cb4ccd049c86d10058ab94af245f0cc", + "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Iterator\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Iterator library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "iterator", + "library" + ], + "time": "2017-01-10T10:34:47+00:00" + }, + { + "name": "hoa/math", + "version": "1.17.05.16", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Math.git", + "reference": "7150785d30f5d565704912116a462e9f5bc83a0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Math/zipball/7150785d30f5d565704912116a462e9f5bc83a0c", + "reference": "7150785d30f5d565704912116a462e9f5bc83a0c", + "shasum": "" + }, + "require": { + "hoa/compiler": "~3.0", + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0", + "hoa/iterator": "~2.0", + "hoa/protocol": "~1.0", + "hoa/zformat": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Math\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Math library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "arrangement", + "combination", + "combinatorics", + "counting", + "library", + "math", + "permutation", + "sampler", + "set" + ], + "time": "2017-05-16T08:02:17+00:00" + }, + { + "name": "hoa/protocol", + "version": "1.17.01.14", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Protocol.git", + "reference": "5c2cf972151c45f373230da170ea015deecf19e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Protocol/zipball/5c2cf972151c45f373230da170ea015deecf19e2", + "reference": "5c2cf972151c45f373230da170ea015deecf19e2", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Protocol\\": "." + }, + "files": [ + "Wrapper.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Protocol library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "library", + "protocol", + "resource", + "stream", + "wrapper" + ], + "time": "2017-01-14T12:26:10+00:00" + }, + { + "name": "hoa/regex", + "version": "1.17.01.13", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Regex.git", + "reference": "7e263a61b6fb45c1d03d8e5ef77668518abd5bec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Regex/zipball/7e263a61b6fb45c1d03d8e5ef77668518abd5bec", + "reference": "7e263a61b6fb45c1d03d8e5ef77668518abd5bec", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0", + "hoa/math": "~1.0", + "hoa/protocol": "~1.0", + "hoa/ustring": "~4.0", + "hoa/visitor": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Regex\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Regex library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "compiler", + "library", + "regex" + ], + "time": "2017-01-13T16:10:24+00:00" + }, + { + "name": "hoa/ruler", + "version": "2.17.05.16", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Ruler.git", + "reference": "696835daf8336dfd490f032da7af444050e52dfc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Ruler/zipball/696835daf8336dfd490f032da7af444050e52dfc", + "reference": "696835daf8336dfd490f032da7af444050e52dfc", + "shasum": "" + }, + "require": { + "hoa/compiler": "~3.0", + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0", + "hoa/file": "~1.0", + "hoa/protocol": "~1.0", + "hoa/visitor": "~2.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Ruler\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Ruler library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "library", + "ruler" + ], + "time": "2017-05-16T07:52:21+00:00" + }, + { + "name": "hoa/stream", + "version": "1.17.02.21", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Stream.git", + "reference": "3293cfffca2de10525df51436adf88a559151d82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Stream/zipball/3293cfffca2de10525df51436adf88a559151d82", + "reference": "3293cfffca2de10525df51436adf88a559151d82", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0", + "hoa/exception": "~1.0", + "hoa/protocol": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Stream\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Stream library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "Context", + "bucket", + "composite", + "filter", + "in", + "library", + "out", + "protocol", + "stream", + "wrapper" + ], + "time": "2017-02-21T16:01:06+00:00" + }, + { + "name": "hoa/ustring", + "version": "4.17.01.16", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Ustring.git", + "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Ustring/zipball/e6326e2739178799b1fe3fdd92029f9517fa17a0", + "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "suggest": { + "ext-iconv": "ext/iconv must be present (or a third implementation) to use Hoa\\Ustring::transcode().", + "ext-intl": "To get a better Hoa\\Ustring::toAscii() and Hoa\\Ustring::compareTo()." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Ustring\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Ustring library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "library", + "search", + "string", + "unicode" + ], + "time": "2017-01-16T07:08:25+00:00" + }, + { + "name": "hoa/visitor", + "version": "2.17.01.16", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Visitor.git", + "reference": "c18fe1cbac98ae449e0d56e87469103ba08f224a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Visitor/zipball/c18fe1cbac98ae449e0d56e87469103ba08f224a", + "reference": "c18fe1cbac98ae449e0d56e87469103ba08f224a", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Visitor\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Visitor library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "library", + "structure", + "visit", + "visitor" + ], + "time": "2017-01-16T07:02:03+00:00" + }, + { + "name": "hoa/zformat", + "version": "1.17.01.10", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Zformat.git", + "reference": "522c381a2a075d4b9dbb42eb4592dd09520e4ac2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Zformat/zipball/522c381a2a075d4b9dbb42eb4592dd09520e4ac2", + "reference": "522c381a2a075d4b9dbb42eb4592dd09520e4ac2", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Zformat\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Zformat library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "library", + "parameter", + "zformat" + ], + "time": "2017-01-10T10:39:54+00:00" + }, + { + "name": "html2text/html2text", + "version": "4.2.1", + "source": { + "type": "git", + "url": "https://github.com/mtibben/html2text.git", + "reference": "f7555eaf271beea4e1098274d3ff37fbb7b21ea7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mtibben/html2text/zipball/f7555eaf271beea4e1098274d3ff37fbb7b21ea7", + "reference": "f7555eaf271beea4e1098274d3ff37fbb7b21ea7", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "~4" + }, + "suggest": { + "ext-mbstring": "For best performance", + "symfony/polyfill-mbstring": "If you can't install ext-mbstring" + }, + "type": "library", + "autoload": { + "psr-4": { + "Html2Text\\": [ + "src/", + "test/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Converts HTML to formatted plain text", + "time": "2018-08-13T12:05:08+00:00" + }, + { + "name": "http-interop/http-factory-guzzle", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/http-interop/http-factory-guzzle.git", + "reference": "34861658efb9899a6618cef03de46e2a52c80fc0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/http-interop/http-factory-guzzle/zipball/34861658efb9899a6618cef03de46e2a52c80fc0", + "reference": "34861658efb9899a6618cef03de46e2a52c80fc0", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^1.4.2", + "psr/http-factory": "^1.0" + }, + "provide": { + "psr/http-factory-implementation": "^1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.5", + "phpunit/phpunit": "^6.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Factory\\Guzzle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "An HTTP Factory using Guzzle PSR7", + "keywords": [ + "factory", + "http", + "psr-17", + "psr-7" + ], + "time": "2018-07-31T19:32:56+00:00" + }, + { + "name": "incenteev/composer-parameter-handler", + "version": "v2.1.3", + "source": { + "type": "git", + "url": "https://github.com/Incenteev/ParameterHandler.git", + "reference": "933c45a34814f27f2345c11c37d46b3ca7303550" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Incenteev/ParameterHandler/zipball/933c45a34814f27f2345c11c37d46b3ca7303550", + "reference": "933c45a34814f27f2345c11c37d46b3ca7303550", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/yaml": "^2.3 || ^3.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "^1.0@dev", + "symfony/filesystem": "^2.3 || ^3 || ^4", + "symfony/phpunit-bridge": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Incenteev\\ParameterHandler\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + } + ], + "description": "Composer script handling your ignored parameter file", + "homepage": "https://github.com/Incenteev/ParameterHandler", + "keywords": [ + "parameters management" + ], + "time": "2018-02-13T18:05:56+00:00" + }, + { + "name": "j0k3r/graby", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/j0k3r/graby.git", + "reference": "6b3e53b2dd3c85b8128dde3307b838885515764b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/j0k3r/graby/zipball/6b3e53b2dd3c85b8128dde3307b838885515764b", + "reference": "6b3e53b2dd3c85b8128dde3307b838885515764b", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-tidy": "*", + "fossar/htmlawed": "^1.2.4", + "guzzlehttp/psr7": "^1.5", + "j0k3r/graby-site-config": "^1.0", + "j0k3r/httplug-ssrf-plugin": "^2.0", + "j0k3r/php-readability": "^1.1", + "monolog/monolog": "^1.13.1", + "php": ">=7.1", + "php-http/client-common": "^2.0", + "php-http/discovery": "^1.0", + "php-http/httplug": "^2.0", + "php-http/message": "^1.7", + "psr/http-message": "^1.0", + "simplepie/simplepie": "^1.3.1", + "smalot/pdfparser": "~0.11", + "symfony/options-resolver": "~2.6|~3.0|~4.0", + "true/punycode": "~2.1", + "wallabag/tcpdf": "^6.2.26" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.14", + "php-coveralls/php-coveralls": "^2.0", + "php-http/guzzle6-adapter": "^2.0", + "php-http/mock-client": "^1.2", + "phpstan/phpstan": "^0.11", + "phpstan/phpstan-phpunit": "^0.11", + "symfony/phpunit-bridge": "~2.6|~3.0|~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.0": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Graby\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeremy Benoist", + "email": "jeremy.benoist@gmail.com", + "role": "Developer" + }, + { + "name": "FiveFilters.org", + "email": "fivefilters@fivefilters.org", + "role": "Developer (original version)" + } + ], + "description": "Graby helps you extract article content from web pages", + "time": "2020-01-23T08:59:01+00:00" + }, + { + "name": "j0k3r/graby-site-config", + "version": "1.0.100", + "source": { + "type": "git", + "url": "https://github.com/j0k3r/graby-site-config.git", + "reference": "d97cfd935302337604f5d778969bc94c12de086a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/j0k3r/graby-site-config/zipball/d97cfd935302337604f5d778969bc94c12de086a", + "reference": "d97cfd935302337604f5d778969bc94c12de086a", + "shasum": "" + }, + "require": { + "symfony/finder": "~2.6|~3.0|~4.0" + }, + "require-dev": { + "liip/rmt": "1.2.*", + "symfony/phpunit-bridge": "^4.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrabySiteConfig\\SiteConfig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "CC0-1.0" + ], + "authors": [ + { + "name": "Jeremy Benoist", + "email": "jeremy.benoist@gmail.com" + } + ], + "description": "Graby site config files", + "time": "2020-01-23T09:05:19+00:00" + }, + { + "name": "j0k3r/httplug-ssrf-plugin", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/j0k3r/httplug-ssrf-plugin.git", + "reference": "fb68804e3bace2b894d1b9f39ad2b59078f5eac2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/j0k3r/httplug-ssrf-plugin/zipball/fb68804e3bace2b894d1b9f39ad2b59078f5eac2", + "reference": "fb68804e3bace2b894d1b9f39ad2b59078f5eac2", + "shasum": "" + }, + "require": { + "php": "^7.1", + "php-http/client-common": "^2.0", + "php-http/discovery": "^1.5", + "php-http/message": "^1.7", + "php-http/message-factory": "^1.0.2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.14", + "guzzlehttp/psr7": "^1.0", + "php-http/guzzle6-adapter": "^2.0", + "php-http/mock-client": "^1.0", + "phpstan/phpstan": "^0.11", + "phpstan/phpstan-phpunit": "^0.11", + "symfony/phpunit-bridge": "~3.4.19|~4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Graby\\HttpClient\\Plugin\\ServerSideRequestForgeryProtection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeremy Benoist", + "email": "jeremy.benoist@gmail.com" + }, + { + "name": "aaa2000", + "email": "adev2000@gmail.com" + }, + { + "name": "Jack W", + "email": "jack@fin1te.net", + "role": "Developer (SafeCurl original version)" + } + ], + "description": "Server-Side Request Forgery (SSRF) protection plugin for HTTPlug", + "homepage": "https://github.com/j0k3r/httplug-ssrf-plugin", + "keywords": [ + "http", + "httplug", + "plugin", + "security", + "ssrf" + ], + "time": "2020-01-06T13:44:13+00:00" + }, + { + "name": "j0k3r/php-readability", + "version": "1.2.4", + "source": { + "type": "git", + "url": "https://github.com/j0k3r/php-readability.git", + "reference": "9306996b472fd3d4bc5a7928a301ccce38423793" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/j0k3r/php-readability/zipball/9306996b472fd3d4bc5a7928a301ccce38423793", + "reference": "9306996b472fd3d4bc5a7928a301ccce38423793", + "shasum": "" + }, + "require": { + "electrolinux/php-html5lib": "^0.1.0", + "ext-mbstring": "*", + "php": ">=5.6.0", + "psr/log": "^1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.14", + "monolog/monolog": "^1.24", + "php-coveralls/php-coveralls": "^2.1", + "symfony/phpunit-bridge": "^4.2.3" + }, + "suggest": { + "ext-tidy": "Used to clean up given HTML and to avoid problems with bad HTML structure." + }, + "type": "library", + "autoload": { + "psr-4": { + "Readability\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Keyvan Minoukadeh", + "email": "keyvan@keyvan.net", + "homepage": "http://keyvan.net", + "role": "Developer (ported original JS code to PHP)" + }, + { + "name": "Arc90", + "homepage": "http://arc90.com", + "role": "Developer (original JS version)" + }, + { + "name": "Jeremy Benoist", + "email": "jeremy.benoist@gmail.com", + "homepage": "http://www.j0k3r.net", + "role": "Developer" + }, + { + "name": "DitherSky", + "homepage": "https://github.com/Dither", + "role": "Developer (https://github.com/Dither/full-text-rss)" + } + ], + "description": "Automatic article extraction from HTML", + "keywords": [ + "article", + "article extraction", + "content", + "content extraction", + "extraction", + "html" + ], + "time": "2019-06-25T15:15:37+00:00" + }, + { + "name": "javibravo/simpleue", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/javibravo/simpleue.git", + "reference": "b12ccea0631cd3f6128ab91178fc5363f6e14ff6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/javibravo/simpleue/zipball/b12ccea0631cd3f6128ab91178fc5363f6e14ff6", + "reference": "b12ccea0631cd3f6128ab91178fc5363f6e14ff6", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "psr/log": "~1.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.9", + "pda/pheanstalk": "^3.1", + "phpunit/phpunit": "4.0.*", + "predis/predis": "^1.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow work with AWS Simple Queue Service (SQS) queues", + "ext-redis": "Allow work with Redis Locker", + "pda/pheanstalk": "Allow work with Beanstalkd queues", + "predis/predis": "Allow work with Redis queues" + }, + "type": "library", + "autoload": { + "psr-4": { + "Simpleue\\": "src/Simpleue" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Javier Bravo", + "email": "javibravo85@gmail.com" + } + ], + "description": "Php package to manage queue tasks in a simple way", + "homepage": "http://github.com/javibravo/simpleue", + "keywords": [ + "job", + "queue", + "redis", + "sqs", + "task" + ], + "time": "2017-11-15T13:41:13+00:00" + }, + { + "name": "jdorn/sql-formatter", + "version": "v1.2.17", + "source": { + "type": "git", + "url": "https://github.com/jdorn/sql-formatter.git", + "reference": "64990d96e0959dff8e059dfcdc1af130728d92bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jdorn/sql-formatter/zipball/64990d96e0959dff8e059dfcdc1af130728d92bc", + "reference": "64990d96e0959dff8e059dfcdc1af130728d92bc", + "shasum": "" + }, + "require": { + "php": ">=5.2.4" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "lib" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeremy Dorn", + "email": "jeremy@jeremydorn.com", + "homepage": "http://jeremydorn.com/" + } + ], + "description": "a PHP SQL highlighting library", + "homepage": "https://github.com/jdorn/sql-formatter/", + "keywords": [ + "highlight", + "sql" + ], + "time": "2014-01-12T16:20:24+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "1.2", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "75c7effcf3f77501d0e0caa75111aff4daa0dd48" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/75c7effcf3f77501d0e0caa75111aff4daa0dd48", + "reference": "75c7effcf3f77501d0e0caa75111aff4daa0dd48", + "shasum": "" + }, + "require": { + "ocramius/package-versions": "^1.2.0", + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A wrapper for ocramius/package-versions to get pretty versions strings", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "time": "2018-06-13T13:22:40+00:00" + }, + { + "name": "jms/metadata", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/metadata.git", + "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/e5854ab1aa643623dc64adde718a8eec32b957a8", + "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "doctrine/cache": "~1.0", + "symfony/cache": "~3.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5.x-dev" + } + }, + "autoload": { + "psr-0": { + "Metadata\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + }, + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Class/method/property metadata management in PHP", + "keywords": [ + "annotations", + "metadata", + "xml", + "yaml" + ], + "time": "2018-10-26T12:40:10+00:00" + }, + { + "name": "jms/parser-lib", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/parser-lib.git", + "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/parser-lib/zipball/c509473bc1b4866415627af0e1c6cc8ac97fa51d", + "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d", + "shasum": "" + }, + "require": { + "phpoption/phpoption": ">=0.9,<2.0-dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-0": { + "JMS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "description": "A library for easily creating recursive-descent parsers.", + "time": "2012-11-18T18:08:43+00:00" + }, + { + "name": "jms/serializer", + "version": "1.14.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/serializer.git", + "reference": "ee96d57024af9a7716d56fcbe3aa94b3d030f3ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/ee96d57024af9a7716d56fcbe3aa94b3d030f3ca", + "reference": "ee96d57024af9a7716d56fcbe3aa94b3d030f3ca", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0", + "doctrine/instantiator": "^1.0.3", + "jms/metadata": "^1.3", + "jms/parser-lib": "1.*", + "php": "^5.5|^7.0", + "phpcollection/phpcollection": "~0.1", + "phpoption/phpoption": "^1.1" + }, + "conflict": { + "twig/twig": "<1.12" + }, + "require-dev": { + "doctrine/orm": "~2.1", + "doctrine/phpcr-odm": "^1.3|^2.0", + "ext-pdo_sqlite": "*", + "jackalope/jackalope-doctrine-dbal": "^1.1.5", + "phpunit/phpunit": "^4.8|^5.0", + "propel/propel1": "~1.7", + "psr/container": "^1.0", + "symfony/dependency-injection": "^2.7|^3.3|^4.0", + "symfony/expression-language": "^2.6|^3.0", + "symfony/filesystem": "^2.1", + "symfony/form": "~2.1|^3.0", + "symfony/translation": "^2.1|^3.0", + "symfony/validator": "^2.2|^3.0", + "symfony/yaml": "^2.1|^3.0", + "twig/twig": "~1.12|~2.0" + }, + "suggest": { + "doctrine/cache": "Required if you like to use cache functionality.", + "doctrine/collections": "Required if you like to use doctrine collection types as ArrayCollection.", + "symfony/yaml": "Required if you'd like to serialize data to YAML format." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.14-dev" + } + }, + "autoload": { + "psr-0": { + "JMS\\Serializer": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + }, + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Library for (de-)serializing data of any complexity; supports XML, JSON, and YAML.", + "homepage": "http://jmsyst.com/libs/serializer", + "keywords": [ + "deserialization", + "jaxb", + "json", + "serialization", + "xml" + ], + "time": "2019-04-17T08:12:16+00:00" + }, + { + "name": "jms/serializer-bundle", + "version": "2.4.4", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/JMSSerializerBundle.git", + "reference": "92ee808c64c1c180775a0e57d00e3be0674668fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/JMSSerializerBundle/zipball/92ee808c64c1c180775a0e57d00e3be0674668fb", + "reference": "92ee808c64c1c180775a0e57d00e3be0674668fb", + "shasum": "" + }, + "require": { + "jms/serializer": "^1.10", + "php": "^5.4|^7.0", + "phpoption/phpoption": "^1.1.0", + "symfony/framework-bundle": "~2.3|~3.0|~4.0" + }, + "require-dev": { + "doctrine/orm": "*", + "phpunit/phpunit": "^4.8.35|^5.4.3|^6.0", + "symfony/expression-language": "~2.6|~3.0|~4.0", + "symfony/finder": "^2.3|^3.0|^4.0", + "symfony/form": "*", + "symfony/stopwatch": "*", + "symfony/twig-bundle": "*", + "symfony/validator": "*", + "symfony/yaml": "*" + }, + "suggest": { + "jms/di-extra-bundle": "Required to get lazy loading (de)serialization visitors, ~1.3", + "symfony/finder": "Required for cache warmup, supported versions ^2.3|^3.0|^4.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + }, + "autoload": { + "psr-4": { + "JMS\\SerializerBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + }, + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Allows you to easily serialize, and deserialize data of any complexity", + "homepage": "http://jmsyst.com/bundles/JMSSerializerBundle", + "keywords": [ + "deserialization", + "jaxb", + "json", + "serialization", + "xml" + ], + "time": "2019-03-30T10:26:09+00:00" + }, + { + "name": "kphoen/rulerz", + "version": "0.21.1", + "source": { + "type": "git", + "url": "https://github.com/K-Phoen/rulerz.git", + "reference": "d28a1bd59b4e66cc9fcdeee965f13f685eb9ce41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/K-Phoen/rulerz/zipball/d28a1bd59b4e66cc9fcdeee965f13f685eb9ce41", + "reference": "d28a1bd59b4e66cc9fcdeee965f13f685eb9ce41", + "shasum": "" + }, + "require": { + "hoa/ruler": "~2.0", + "php": ">=7.1", + "symfony/property-access": "~3.0|~4.0" + }, + "require-dev": { + "behat/behat": "~3.0", + "coduo/phpspec-data-provider-extension": "~1.0,!=1.0.2", + "doctrine/orm": "~2.4", + "elasticsearch/elasticsearch": "~1.0", + "illuminate/database": "~5.0", + "kphoen/rusty": "dev-master", + "liip/rmt": "^1.2", + "mikey179/vfsstream": "~1.4", + "phpspec/phpspec": "~2.0,>=2.4-dev", + "pomm-project/cli": "~2.0@dev", + "pomm-project/foundation": "~2.0@dev", + "pomm-project/model-manager": "~2.0.@dev", + "ruflin/elastica": "~1.0", + "solarium/solarium": "~3.0", + "vlucas/phpdotenv": "~2.1" + }, + "suggest": { + "doctrine/orm": "To execute rules as Doctrine queries", + "elasticsearch/elasticsearch": "To execute rules as Elasticsearch queries", + "kphoen/rulerz-spec-builder": "If you want a specification builder", + "pomm-project/model-manager": "To execute rules as Pomm queries", + "solarium/solarium": "To execute rules as Solr queries" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "RulerZ\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Gomez", + "email": "contact@kevingomez.fr" + } + ], + "description": "Powerful implementation of the Specification pattern", + "homepage": "https://github.com/K-Phoen/RulerZ", + "keywords": [ + "doctrine", + "specification" + ], + "time": "2018-09-18T15:15:42+00:00" + }, + { + "name": "kphoen/rulerz-bridge", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/K-Phoen/rulerz-bridge.git", + "reference": "fdad5856b669d59b5e4bda47c4e927a0485bf7a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/K-Phoen/rulerz-bridge/zipball/fdad5856b669d59b5e4bda47c4e927a0485bf7a0", + "reference": "fdad5856b669d59b5e4bda47c4e927a0485bf7a0", + "shasum": "" + }, + "require": { + "kphoen/rulerz": "~0.19,>=0.19.3|^1.0", + "php": ">=7.1", + "symfony/form": "^3.0|^4.0", + "symfony/validator": "^3.0|^4.0" + }, + "require-dev": { + "liip/rmt": "^1.2", + "phpunit/phpunit": "~7.1", + "symfony/phpunit-bridge": "^3.0|^4.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\RulerZ\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Gomez", + "email": "contact@kevingomez.fr" + } + ], + "description": "Symfony RulerZ Bridge", + "homepage": "https://github.com/K-Phoen/rulerz-bridge", + "keywords": [ + "Bridge", + "doctrine", + "ruler", + "rulerz", + "specification", + "symfony" + ], + "time": "2018-10-01T14:17:27+00:00" + }, + { + "name": "kphoen/rulerz-bundle", + "version": "0.15.0", + "source": { + "type": "git", + "url": "https://github.com/K-Phoen/RulerZBundle.git", + "reference": "f25f2eddfd311047f0f0ece2684b1f6463cc41ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/K-Phoen/RulerZBundle/zipball/f25f2eddfd311047f0f0ece2684b1f6463cc41ad", + "reference": "f25f2eddfd311047f0f0ece2684b1f6463cc41ad", + "shasum": "" + }, + "require": { + "kphoen/rulerz": "~0.17,>=0.19.3", + "kphoen/rulerz-bridge": "^1.0", + "php": ">=7.1", + "symfony/framework-bundle": "^3.0|^4.0" + }, + "require-dev": { + "liip/rmt": "^1.2", + "matthiasnoback/symfony-dependency-injection-test": "^3.0", + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "^7.1", + "symfony/phpunit-bridge": "^3.0|^4.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "KPhoen\\RulerZBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Gomez", + "email": "contact@kevingomez.fr" + } + ], + "description": "Symfony2 Bundle for RulerZ", + "homepage": "https://github.com/K-Phoen/RulerZBundle", + "keywords": [ + "doctrine", + "ruler", + "rulerz", + "specification" + ], + "time": "2018-09-17T09:02:32+00:00" + }, + { + "name": "lcobucci/jwt", + "version": "3.3.1", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/a11ec5f4b4d75d1fcd04e133dede4c317aac9e18", + "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-openssl": "*", + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "mikey179/vfsstream": "~1.5", + "phpmd/phpmd": "~2.2", + "phpunit/php-invoker": "~1.1", + "phpunit/phpunit": "^5.7 || ^7.3", + "squizlabs/php_codesniffer": "~2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Otávio Cobucci Oblonczyk", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "time": "2019-05-24T18:30:49+00:00" + }, + { + "name": "lexik/form-filter-bundle", + "version": "v5.0.10", + "source": { + "type": "git", + "url": "https://github.com/lexik/LexikFormFilterBundle.git", + "reference": "92df0638173979dc906bda7a33a10b98429d2057" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lexik/LexikFormFilterBundle/zipball/92df0638173979dc906bda7a33a10b98429d2057", + "reference": "92df0638173979dc906bda7a33a10b98429d2057", + "shasum": "" + }, + "require": { + "doctrine/orm": "^2.4.8", + "php": ">=5.5.9", + "symfony/form": "~2.8|~3.0|^4.0", + "symfony/framework-bundle": "~2.8|~3.0|^4.0" + }, + "require-dev": { + "doctrine/mongodb-odm-bundle": "^3.0", + "phpunit/phpunit": "~5.0|^7.5" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "Install this package if using the PHP 7 MongoDB Driver" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "5.x.x-dev" + } + }, + "autoload": { + "psr-4": { + "Lexik\\Bundle\\FormFilterBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dev Lexik", + "email": "dev@lexik.fr" + }, + { + "name": "Cedric Girard", + "email": "c.girard@lexik.fr" + } + ], + "description": "This bundle aim to provide classes to build some form filters and then build a doctrine query from this form filter.", + "homepage": "https://github.com/lexik/LexikFormFilterBundle", + "keywords": [ + "bundle", + "doctrine", + "filter", + "form", + "symfony" + ], + "time": "2019-04-17T17:58:44+00:00" + }, + { + "name": "liip/theme-bundle", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/liip/LiipThemeBundle.git", + "reference": "362394821ff8dcc90b06272e289e7dafe4dab52a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/liip/LiipThemeBundle/zipball/362394821ff8dcc90b06272e289e7dafe4dab52a", + "reference": "362394821ff8dcc90b06272e289e7dafe4dab52a", + "shasum": "" + }, + "require": { + "php": "^7.0", + "psr/log": "~1.0", + "symfony/finder": "^3.0|^4.0", + "symfony/framework-bundle": "^3.0|^4.0", + "symfony/templating": "^3.0|^4.0", + "symfony/twig-bundle": "^3.0|^4.0", + "twig/twig": "^1.34|^2.4" + }, + "conflict": { + "sebastian/comparator": "1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^6.0", + "symfony/console": "^3.0|^4.0", + "symfony/expression-language": "^3.0|^4.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "Liip\\ThemeBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Community contributions", + "homepage": "https://github.com/liip/LiipThemeBundle/contributors" + }, + { + "name": "Liip AG", + "homepage": "http://www.liip.ch/" + } + ], + "description": "Provides theming support for #Symfony2 Bundles", + "keywords": [ + "themes", + "theming" + ], + "time": "2019-06-19T12:53:08+00:00" + }, + { + "name": "masterminds/html5", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "104443ad663d15981225f99532ba73c2f1d6b6f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/104443ad663d15981225f99532ba73c2f1d6b6f2", + "reference": "104443ad663d15981225f99532ba73c2f1d6b6f2", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-dom": "*", + "ext-libxml": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35", + "sami/sami": "~2.0", + "satooshi/php-coveralls": "1.0.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "time": "2019-07-25T07:03:26+00:00" + }, + { + "name": "mgargano/simplehtmldom", + "version": "1.5", + "source": { + "type": "git", + "url": "https://github.com/matgargano/simplehtmldom.git", + "reference": "37fb0d7c1bda45c5a4cf14fdef56c1edf6aa42be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/matgargano/simplehtmldom/zipball/37fb0d7c1bda45c5a4cf14fdef56c1edf6aa42be", + "reference": "37fb0d7c1bda45c5a4cf14fdef56c1edf6aa42be", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "SimpleHtmlDom": "src/" + }, + "files": [ + "src/simple_html_dom.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "The MIT License" + ], + "authors": [ + { + "name": "S.C. Chen", + "email": "me578022@gmail.com" + } + ], + "description": "Composer package that gives you access to and (unlike all the others at this time) autoloads S.C. Chen's PHP Simple HTML DOM Parser Library", + "homepage": "http://simplehtmldom.sourceforge.net/", + "keywords": [ + "Simple", + "dom", + "html" + ], + "time": "2014-01-05T18:17:34+00:00" + }, + { + "name": "michelf/php-markdown", + "version": "1.9.0", + "source": { + "type": "git", + "url": "https://github.com/michelf/php-markdown.git", + "reference": "c83178d49e372ca967d1a8c77ae4e051b3a3c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/michelf/php-markdown/zipball/c83178d49e372ca967d1a8c77ae4e051b3a3c75c", + "reference": "c83178d49e372ca967d1a8c77ae4e051b3a3c75c", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": ">=4.3 <5.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Michelf\\": "Michelf/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Michel Fortin", + "email": "michel.fortin@michelf.ca", + "homepage": "https://michelf.ca/", + "role": "Developer" + }, + { + "name": "John Gruber", + "homepage": "https://daringfireball.net/" + } + ], + "description": "PHP Markdown", + "homepage": "https://michelf.ca/projects/php-markdown/", + "keywords": [ + "markdown" + ], + "time": "2019-12-02T02:32:27+00:00" + }, + { + "name": "mnapoli/piwik-twig-extension", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/mnapoli/PiwikTwigExtension.git", + "reference": "e1746d20730426d7098b4a9a3f5c6c99c6f04612" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mnapoli/PiwikTwigExtension/zipball/e1746d20730426d7098b4a9a3f5c6c99c6f04612", + "reference": "e1746d20730426d7098b4a9a3f5c6c99c6f04612", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "twig/twig": "~2.10" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.13", + "php-coveralls/php-coveralls": "^2.0", + "symfony/phpunit-bridge": "^4.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PiwikTwigExtension\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "time": "2019-06-21T10:58:04+00:00" + }, + { + "name": "monolog/monolog", + "version": "1.25.3", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "fa82921994db851a8becaf3787a9e73c5976b6f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fa82921994db851a8becaf3787a9e73c5976b6f1", + "reference": "fa82921994db851a8becaf3787a9e73c5976b6f1", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "graylog2/gelf-php": "~1.0", + "jakub-onderka/php-parallel-lint": "0.9", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "phpunit/phpunit": "~4.5", + "phpunit/phpunit-mock-objects": "2.3.0", + "ruflin/elastica": ">=0.90 <3.0", + "sentry/sentry": "^0.13", + "swiftmailer/swiftmailer": "^5.3|^6.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mongo": "Allow sending log messages to a MongoDB server", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "sentry/sentry": "Allow sending log messages to a Sentry server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "time": "2019-12-20T14:15:16+00:00" + }, + { + "name": "nelmio/api-doc-bundle", + "version": "2.13.4", + "target-dir": "Nelmio/ApiDocBundle", + "source": { + "type": "git", + "url": "https://github.com/nelmio/NelmioApiDocBundle.git", + "reference": "28802f2c44dbbf29aa7f5dc80a10f44d3558f580" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/28802f2c44dbbf29aa7f5dc80a10f44d3558f580", + "reference": "28802f2c44dbbf29aa7f5dc80a10f44d3558f580", + "shasum": "" + }, + "require": { + "michelf/php-markdown": "~1.4", + "php": ">=5.4", + "symfony/console": "~2.3|~3.0|~4.0", + "symfony/framework-bundle": "~2.3|~3.0|~4.0", + "symfony/twig-bundle": "~2.3|~3.0|~4.0" + }, + "conflict": { + "jms/serializer": "<0.12", + "jms/serializer-bundle": "<0.11", + "symfony/symfony": "~2.7.8", + "twig/twig": "<1.12" + }, + "require-dev": { + "doctrine/doctrine-bundle": "~1.5", + "doctrine/orm": "~2.3", + "dunglas/api-bundle": "~1.0", + "friendsofsymfony/rest-bundle": "~1.0|~2.0", + "jms/serializer-bundle": ">=0.11", + "sensio/framework-extra-bundle": "~3.0", + "symfony/browser-kit": "~2.3|~3.0|~4.0", + "symfony/css-selector": "~2.3|~3.0|~4.0", + "symfony/finder": "~2.3|~3.0|~4.0", + "symfony/form": "~2.3|~3.0|~4.0", + "symfony/phpunit-bridge": "~2.7|~3.0|~4.0", + "symfony/serializer": "~2.7|~3.0|~4.0", + "symfony/validator": "~2.3|~3.0|~4.0", + "symfony/yaml": "~2.3|~3.0|~4.0" + }, + "suggest": { + "dunglas/api-bundle": "For making use of resources definitions of DunglasApiBundle.", + "friendsofsymfony/rest-bundle": "For making use of REST information in the doc.", + "jms/serializer": "For making use of serializer information in the doc.", + "symfony/form": "For using form definitions as input.", + "symfony/validator": "For making use of validator information in the doc." + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-2.x": "2.13-dev" + } + }, + "autoload": { + "psr-0": { + "Nelmio\\ApiDocBundle": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nelmio", + "homepage": "http://nelm.io" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/nelmio/NelmioApiDocBundle/contributors" + } + ], + "description": "Generates documentation for your REST API from annotations", + "keywords": [ + "api", + "doc", + "documentation", + "rest" + ], + "time": "2019-06-01T13:34:59+00:00" + }, + { + "name": "nelmio/cors-bundle", + "version": "1.5.6", + "source": { + "type": "git", + "url": "https://github.com/nelmio/NelmioCorsBundle.git", + "reference": "10a24c10f242440211ed31075e74f81661c690d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/10a24c10f242440211ed31075e74f81661c690d9", + "reference": "10a24c10f242440211ed31075e74f81661c690d9", + "shasum": "" + }, + "require": { + "symfony/framework-bundle": "^2.7 || ^3.0 || ^4.0" + }, + "require-dev": { + "matthiasnoback/symfony-dependency-injection-test": "^1.0 || ^2.0", + "mockery/mockery": "^0.9 || ^1.0", + "symfony/phpunit-bridge": "^2.7 || ^3.0 || ^4.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "1.5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Nelmio\\CorsBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nelmio", + "homepage": "http://nelm.io" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/nelmio/NelmioCorsBundle/contributors" + } + ], + "description": "Adds CORS (Cross-Origin Resource Sharing) headers support in your Symfony2 application", + "keywords": [ + "api", + "cors", + "crossdomain" + ], + "time": "2019-06-17T08:53:14+00:00" + }, + { + "name": "ocramius/package-versions", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/Ocramius/PackageVersions.git", + "reference": "44af6f3a2e2e04f2af46bcb302ad9600cba41c7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ocramius/PackageVersions/zipball/44af6f3a2e2e04f2af46bcb302ad9600cba41c7d", + "reference": "44af6f3a2e2e04f2af46bcb302ad9600cba41c7d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0.0", + "php": "^7.1.0" + }, + "require-dev": { + "composer/composer": "^1.6.3", + "doctrine/coding-standard": "^5.0.1", + "ext-zip": "*", + "infection/infection": "^0.7.1", + "phpunit/phpunit": "^7.5.17" + }, + "type": "composer-plugin", + "extra": { + "class": "PackageVersions\\Installer", + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "PackageVersions\\": "src/PackageVersions" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", + "time": "2019-11-15T16:17:10+00:00" + }, + { + "name": "ocramius/proxy-manager", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Ocramius/ProxyManager.git", + "reference": "e18ac876b2e4819c76349de8f78ccc8ef1554cd7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ocramius/ProxyManager/zipball/e18ac876b2e4819c76349de8f78ccc8ef1554cd7", + "reference": "e18ac876b2e4819c76349de8f78ccc8ef1554cd7", + "shasum": "" + }, + "require": { + "ocramius/package-versions": "^1.1.1", + "php": "^7.1.0", + "zendframework/zend-code": "^3.1.0" + }, + "require-dev": { + "couscous/couscous": "^1.5.2", + "ext-phar": "*", + "humbug/humbug": "dev-master@DEV", + "nikic/php-parser": "^3.0.4", + "phpbench/phpbench": "^0.12.2", + "phpstan/phpstan": "^0.6.4", + "phpunit/phpunit": "^5.6.4", + "phpunit/phpunit-mock-objects": "^3.4.1", + "squizlabs/php_codesniffer": "^2.7.0" + }, + "suggest": { + "ocramius/generated-hydrator": "To have very fast object to array to object conversion for ghost objects", + "zendframework/zend-json": "To have the JsonRpc adapter (Remote Object feature)", + "zendframework/zend-soap": "To have the Soap adapter (Remote Object feature)", + "zendframework/zend-xmlrpc": "To have the XmlRpc adapter (Remote Object feature)" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "ProxyManager\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.io/" + } + ], + "description": "A library providing utilities to generate, instantiate and generally operate with Object Proxies", + "homepage": "https://github.com/Ocramius/ProxyManager", + "keywords": [ + "aop", + "lazy loading", + "proxy", + "proxy pattern", + "service proxies" + ], + "time": "2017-05-04T11:12:50+00:00" + }, + { + "name": "pagerfanta/pagerfanta", + "version": "v2.1.3", + "source": { + "type": "git", + "url": "https://github.com/whiteoctober/Pagerfanta.git", + "reference": "a53ff01d521648d9dbca19b93ac6bc75a59b0972" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/whiteoctober/Pagerfanta/zipball/a53ff01d521648d9dbca19b93ac6bc75a59b0972", + "reference": "a53ff01d521648d9dbca19b93ac6bc75a59b0972", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "doctrine/orm": "~2.3", + "doctrine/phpcr-odm": "1.*", + "jackalope/jackalope-doctrine-dbal": "1.*", + "jmikola/geojson": "~1.0", + "mandango/mandango": "~1.0@dev", + "mandango/mondator": "~1.0@dev", + "phpunit/phpunit": "^6.5", + "propel/propel": "~2.0@dev", + "propel/propel1": "~1.6", + "ruflin/elastica": "~1.3", + "solarium/solarium": "~3.1" + }, + "suggest": { + "doctrine/mongodb-odm": "To use the DoctrineODMMongoDBAdapter.", + "doctrine/orm": "To use the DoctrineORMAdapter.", + "doctrine/phpcr-odm": "To use the DoctrineODMPhpcrAdapter. >= 1.1.0", + "mandango/mandango": "To use the MandangoAdapter.", + "propel/propel": "To use the Propel2Adapter", + "propel/propel1": "To use the PropelAdapter", + "solarium/solarium": "To use the SolariumAdapter." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Pagerfanta\\": "src/Pagerfanta/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Pablo Díez", + "email": "pablodip@gmail.com" + } + ], + "description": "Pagination for PHP", + "keywords": [ + "page", + "pagination", + "paginator", + "paging" + ], + "time": "2019-07-17T20:56:16+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "47a1cedd2e4d52688eb8c96469c05ebc8fd28fa2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/47a1cedd2e4d52688eb8c96469c05ebc8fd28fa2", + "reference": "47a1cedd2e4d52688eb8c96469c05ebc8fd28fa2", + "shasum": "" + }, + "require": { + "php": "^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^6|^7", + "vimeo/psalm": "^1|^2|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "time": "2019-11-06T19:20:29+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v2.0.18", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "0a58ef6e3146256cc3dc7cc393927bcc7d1b72db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/0a58ef6e3146256cc3dc7cc393927bcc7d1b72db", + "reference": "0a58ef6e3146256cc3dc7cc393927bcc7d1b72db", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "autoload": { + "files": [ + "lib/random.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "time": "2019-01-03T20:59:08+00:00" + }, + { + "name": "php-amqplib/php-amqplib", + "version": "v2.11.0", + "source": { + "type": "git", + "url": "https://github.com/php-amqplib/php-amqplib.git", + "reference": "9ee212baced63442ca1ab029acde38e1144a00b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/9ee212baced63442ca1ab029acde38e1144a00b8", + "reference": "9ee212baced63442ca1ab029acde38e1144a00b8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-sockets": "*", + "php": ">=5.6.3", + "phpseclib/phpseclib": "^2.0.0" + }, + "replace": { + "videlalvaro/php-amqplib": "self.version" + }, + "require-dev": { + "ext-curl": "*", + "nategood/httpful": "^0.2.20", + "phpunit/phpunit": "^5.7|^6.5|^7.0", + "squizlabs/php_codesniffer": "^2.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.11-dev" + } + }, + "autoload": { + "psr-4": { + "PhpAmqpLib\\": "PhpAmqpLib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Alvaro Videla", + "role": "Original Maintainer" + }, + { + "name": "John Kelly", + "email": "johnmkelly86@gmail.com", + "role": "Maintainer" + }, + { + "name": "Raúl Araya", + "email": "nubeiro@gmail.com", + "role": "Maintainer" + }, + { + "name": "Luke Bakken", + "email": "luke@bakken.io", + "role": "Maintainer" + } + ], + "description": "Formerly videlalvaro/php-amqplib. This library is a pure PHP implementation of the AMQP protocol. It's been tested against RabbitMQ.", + "homepage": "https://github.com/php-amqplib/php-amqplib/", + "keywords": [ + "message", + "queue", + "rabbitmq" + ], + "time": "2019-11-19T15:15:19+00:00" + }, + { + "name": "php-amqplib/rabbitmq-bundle", + "version": "v1.14.4", + "source": { + "type": "git", + "url": "https://github.com/php-amqplib/RabbitMqBundle.git", + "reference": "cf67adaa4e306d8e9cb6855a72d79263b425ded8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-amqplib/RabbitMqBundle/zipball/cf67adaa4e306d8e9cb6855a72d79263b425ded8", + "reference": "cf67adaa4e306d8e9cb6855a72d79263b425ded8", + "shasum": "" + }, + "require": { + "php": "^5.3.9|^7.0", + "php-amqplib/php-amqplib": "^2.6", + "psr/log": "^1.0", + "symfony/config": "^2.7|^3.0|^4.0", + "symfony/console": "^2.7|^3.0|^4.0", + "symfony/dependency-injection": "^2.7|^3.0|^4.0", + "symfony/event-dispatcher": "^2.7|^3.0|^4.0", + "symfony/yaml": "^2.7|^3.0|^4.0" + }, + "replace": { + "oldsound/rabbitmq-bundle": "self.version" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|^5.4.3", + "symfony/debug": "^2.7|^3.0|^4.0", + "symfony/serializer": "^2.7|^3.0|^4.0" + }, + "suggest": { + "symfony/framework-bundle": "To use this lib as a full Symfony Bundle and to use the profiler data collector" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "1.10.x-dev" + } + }, + "autoload": { + "psr-4": { + "OldSound\\RabbitMqBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alvaro Videla" + } + ], + "description": "Integrates php-amqplib with Symfony & RabbitMq. Formerly oldsound/rabbitmq-bundle.", + "keywords": [ + "AMQP", + "Symfony2", + "message", + "queue", + "rabbitmq", + "symfony", + "symfony3", + "symfony4" + ], + "time": "2018-05-02T13:12:32+00:00" + }, + { + "name": "php-http/client-common", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/client-common.git", + "reference": "a8b29678d61556f45d6236b1667db16d998ceec5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/client-common/zipball/a8b29678d61556f45d6236b1667db16d998ceec5", + "reference": "a8b29678d61556f45d6236b1667db16d998ceec5", + "shasum": "" + }, + "require": { + "php": "^7.1", + "php-http/httplug": "^2.0", + "php-http/message": "^1.6", + "php-http/message-factory": "^1.0", + "symfony/options-resolver": " ^3.4.20 || ~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0" + }, + "require-dev": { + "doctrine/instantiator": "^1.1", + "guzzlehttp/psr7": "^1.4", + "phpspec/phpspec": "^5.1", + "phpspec/prophecy": "^1.8", + "sebastian/comparator": "^3.0" + }, + "suggest": { + "ext-json": "To detect JSON responses with the ContentTypePlugin", + "ext-libxml": "To detect XML responses with the ContentTypePlugin", + "php-http/cache-plugin": "PSR-6 Cache plugin", + "php-http/logger-plugin": "PSR-3 Logger plugin", + "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Client\\Common\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Common HTTP Client implementations and tools for HTTPlug", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "common", + "http", + "httplug" + ], + "time": "2019-11-18T08:58:18+00:00" + }, + { + "name": "php-http/curl-client", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/curl-client.git", + "reference": "9e79355af46d72e10da50be20b66f74b26143441" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/curl-client/zipball/9e79355af46d72e10da50be20b66f74b26143441", + "reference": "9e79355af46d72e10da50be20b66f74b26143441", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": "^7.1", + "php-http/discovery": "^1.6", + "php-http/httplug": "^2.0", + "php-http/message": "^1.2", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "symfony/options-resolver": "^3.4 || ^4.0 || ^5.0" + }, + "provide": { + "php-http/async-client-implementation": "1.0", + "php-http/client-implementation": "1.0", + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "guzzlehttp/psr7": "^1.0", + "php-http/client-integration-tests": "^2.0", + "phpunit/phpunit": "^7.5", + "zendframework/zend-diactoros": "^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Client\\Curl\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "МОхаОл КрасОльМОкПв", + "email": "m.krasilnikov@yandex.ru" + } + ], + "description": "PSR-18 and HTTPlug Async client with cURL", + "homepage": "http://php-http.org", + "keywords": [ + "curl", + "http", + "psr-18" + ], + "time": "2019-12-27T11:02:07+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.7.4", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "82dbef649ccffd8e4f22e1953c3a5265992b83c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82dbef649ccffd8e4f22e1953c3a5265992b83c0", + "reference": "82dbef649ccffd8e4f22e1953c3a5265992b83c0", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "conflict": { + "nyholm/psr7": "<1.0" + }, + "require-dev": { + "akeneo/phpspec-skip-example-extension": "^4.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1", + "puli/composer-plugin": "1.0.0-beta10" + }, + "suggest": { + "php-http/message": "Allow to use Guzzle, Diactoros or Slim Framework factories", + "puli/composer-plugin": "Sets up Puli which is recommended for Discovery to work. Check http://docs.php-http.org/en/latest/discovery.html for more details." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds installed HTTPlug implementations and PSR-7 message factories", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr7" + ], + "time": "2020-01-03T11:25:47+00:00" + }, + { + "name": "php-http/guzzle5-adapter", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/guzzle5-adapter.git", + "reference": "cce48360b1f8a3467bd94e853e6107aa4532008e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/guzzle5-adapter/zipball/cce48360b1f8a3467bd94e853e6107aa4532008e", + "reference": "cce48360b1f8a3467bd94e853e6107aa4532008e", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^5.1", + "php": "^7.0", + "php-http/discovery": "^1.0", + "php-http/httplug": "^2.0" + }, + "provide": { + "php-http/client-implementation": "1.0", + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "ext-curl": "*", + "guzzlehttp/ringphp": "^1.1", + "php-http/client-integration-tests": "^2.0", + "phpunit/phpunit": "^6.0 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Adapter\\Guzzle5\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Guzzle 5 HTTP Adapter", + "homepage": "http://httplug.io", + "keywords": [ + "Guzzle", + "http" + ], + "time": "2019-02-05T12:28:45+00:00" + }, + { + "name": "php-http/httplug", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "72d2b129a48f0490d55b7f89be0d6aa0597ffb06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/72d2b129a48f0490d55b7f89be0d6aa0597ffb06", + "reference": "72d2b129a48f0490d55b7f89be0d6aa0597ffb06", + "shasum": "" + }, + "require": { + "php": "^7.0", + "php-http/promise": "^1.0", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1", + "phpspec/phpspec": "^4.3.4|^5.0|^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "time": "2019-12-27T10:07:11+00:00" + }, + { + "name": "php-http/httplug-bundle", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/HttplugBundle.git", + "reference": "5044b655fcd3a43243383cd692a6bb6cd18af24f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/HttplugBundle/zipball/5044b655fcd3a43243383cd692a6bb6cd18af24f", + "reference": "5044b655fcd3a43243383cd692a6bb6cd18af24f", + "shasum": "" + }, + "require": { + "php": "^7.1", + "php-http/client-common": "^1.9 || ^2.0", + "php-http/client-implementation": "^1.0", + "php-http/discovery": "^1.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/logger-plugin": "^1.1", + "php-http/message": "^1.4", + "php-http/message-factory": "^1.0.2", + "php-http/stopwatch-plugin": "^1.2", + "psr/http-message": "^1.0", + "symfony/config": "^3.4.20 || ^4.2.1", + "symfony/dependency-injection": "^3.4.20 || ^4.2.1", + "symfony/event-dispatcher": "^3.4.20 || ^4.2.1", + "symfony/http-kernel": "^3.4.20 || ^4.2.1", + "symfony/options-resolver": "^3.4.20 || ^4.2.1" + }, + "conflict": { + "php-http/curl-client": "<2.0", + "php-http/guzzle6-adapter": "<1.1" + }, + "require-dev": { + "guzzlehttp/psr7": "^1.0", + "matthiasnoback/symfony-dependency-injection-test": "^3.0", + "nyholm/nsa": "^1.1", + "php-http/cache-plugin": "^1.6", + "php-http/guzzle6-adapter": "^1.1.1 || ^2.0.1", + "php-http/mock-client": "^1.2", + "php-http/promise": "^1.0", + "polishsymfonycommunity/symfony-mocker-container": "^1.0", + "symfony/browser-kit": "^3.4.20 || ^4.2.1", + "symfony/cache": "^3.4.20 || ^4.2.1", + "symfony/dom-crawler": "^3.4.20 || ^4.2.1", + "symfony/framework-bundle": "^3.4.0 || ^4.2", + "symfony/http-foundation": "^3.4.20 || ^4.2.1", + "symfony/phpunit-bridge": "^3.4 || ^4.2", + "symfony/stopwatch": "^3.4.20 || ^4.2.1", + "symfony/twig-bundle": "^3.4.20 || ^4.2.1", + "symfony/web-profiler-bundle": "^3.4.20 || ^4.2.1", + "twig/twig": "^1.36 || ^2.6" + }, + "suggest": { + "php-http/cache-plugin": "To configure clients that cache responses", + "php-http/mock-client": "Add this to your require-dev section to mock HTTP responses easily", + "twig/twig": "Add this to your require-dev section when using the WebProfilerBundle" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\HttplugBundle\\": "src/" + }, + "exclude-from-classmap": [ + "/Tests/Resources/MyPsr18TestClient.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "David Buchmann", + "email": "mail@davidbu.ch" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "Symfony integration for HTTPlug", + "homepage": "http://httplug.io", + "keywords": [ + "adapter", + "bundle", + "discovery", + "factory", + "http", + "httplug", + "message", + "php-http" + ], + "time": "2019-06-05T12:03:16+00:00" + }, + { + "name": "php-http/logger-plugin", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/logger-plugin.git", + "reference": "c1c6e90717ce350319b7b8bc489f1db35bb523fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/logger-plugin/zipball/c1c6e90717ce350319b7b8bc489f1db35bb523fd", + "reference": "c1c6e90717ce350319b7b8bc489f1db35bb523fd", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0", + "php-http/client-common": "^1.9 || ^2.0", + "php-http/message": "^1.0", + "psr/log": "^1.0" + }, + "require-dev": { + "henrikbjorn/phpspec-code-coverage": "^1.0", + "phpspec/phpspec": "^2.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Client\\Common\\Plugin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "PSR-3 Logger plugin for HTTPlug", + "homepage": "http://httplug.io", + "keywords": [ + "http", + "httplug", + "logger", + "plugin" + ], + "time": "2019-01-30T11:48:21+00:00" + }, + { + "name": "php-http/message", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/message.git", + "reference": "ce8f43ac1e294b54aabf5808515c3554a19c1e1c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message/zipball/ce8f43ac1e294b54aabf5808515c3554a19c1e1c", + "reference": "ce8f43ac1e294b54aabf5808515c3554a19c1e1c", + "shasum": "" + }, + "require": { + "clue/stream-filter": "^1.4", + "php": "^7.1", + "php-http/message-factory": "^1.0.2", + "psr/http-message": "^1.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0" + }, + "require-dev": { + "akeneo/phpspec-skip-example-extension": "^1.0", + "coduo/phpspec-data-provider-extension": "^1.0", + "ext-zlib": "*", + "guzzlehttp/psr7": "^1.0", + "henrikbjorn/phpspec-code-coverage": "^1.0", + "phpspec/phpspec": "^2.4", + "slim/slim": "^3.0", + "zendframework/zend-diactoros": "^1.0" + }, + "suggest": { + "ext-zlib": "Used with compressor/decompressor streams", + "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", + "slim/slim": "Used with Slim Framework PSR-7 implementation", + "zendframework/zend-diactoros": "Used with Diactoros Factories" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Message\\": "src/" + }, + "files": [ + "src/filters.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "HTTP Message related tools", + "homepage": "http://php-http.org", + "keywords": [ + "http", + "message", + "psr-7" + ], + "time": "2019-08-05T06:55:08+00:00" + }, + { + "name": "php-http/message-factory", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/message-factory.git", + "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message-factory/zipball/a478cb11f66a6ac48d8954216cfed9aa06a501a1", + "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Factory interfaces for PSR-7 HTTP Message", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "stream", + "uri" + ], + "time": "2015-12-19T14:08:53+00:00" + }, + { + "name": "php-http/promise", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "dc494cdc9d7160b9a09bd5573272195242ce7980" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/dc494cdc9d7160b9a09bd5573272195242ce7980", + "reference": "dc494cdc9d7160b9a09bd5573272195242ce7980", + "shasum": "" + }, + "require-dev": { + "henrikbjorn/phpspec-code-coverage": "^1.0", + "phpspec/phpspec": "^2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + }, + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "time": "2016-01-26T13:27:02+00:00" + }, + { + "name": "php-http/stopwatch-plugin", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/stopwatch-plugin.git", + "reference": "de6f39c96f57c60a43d535e27122de505e683f98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/stopwatch-plugin/zipball/de6f39c96f57c60a43d535e27122de505e683f98", + "reference": "de6f39c96f57c60a43d535e27122de505e683f98", + "shasum": "" + }, + "require": { + "php": "^7.1", + "php-http/client-common": "^1.9 || ^2.0", + "symfony/stopwatch": "^3.4 || ^4.0 || ^5.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.5 || ^3.0 || ^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Client\\Common\\Plugin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Symfony Stopwatch plugin for HTTPlug", + "homepage": "http://httplug.io", + "keywords": [ + "http", + "httplug", + "plugin", + "stopwatch" + ], + "time": "2019-11-18T08:10:48+00:00" + }, + { + "name": "phpcollection/phpcollection", + "version": "0.5.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-collection.git", + "reference": "f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-collection/zipball/f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6", + "reference": "f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6", + "shasum": "" + }, + "require": { + "phpoption/phpoption": "1.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.4-dev" + } + }, + "autoload": { + "psr-0": { + "PhpCollection": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "General-Purpose Collection Library for PHP", + "keywords": [ + "collection", + "list", + "map", + "sequence", + "set" + ], + "time": "2015-05-17T12:39:23+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.7.2", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "77f7c4d2e65413aff5b5a8cc8b3caf7a28d81959" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/77f7c4d2e65413aff5b5a8cc8b3caf7a28d81959", + "reference": "77f7c4d2e65413aff5b5a8cc8b3caf7a28d81959", + "shasum": "" + }, + "require": { + "php": "^5.5.9 || ^7.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.3", + "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "time": "2019-12-15T19:35:24+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "2.0.23", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "c78eb5058d5bb1a183133c36d4ba5b6675dfa099" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/c78eb5058d5bb1a183133c36d4ba5b6675dfa099", + "reference": "c78eb5058d5bb1a183133c36d4ba5b6675dfa099", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phing/phing": "~2.7", + "phpunit/phpunit": "^4.8.35|^5.7|^6.0", + "sami/sami": "~2.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "suggest": { + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-JÃŒrgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "time": "2019-09-17T03:41:22+00:00" + }, + { + "name": "phpzip/phpzip", + "version": "2.0.8", + "source": { + "type": "git", + "url": "https://github.com/Grandt/PHPZip.git", + "reference": "936f93d656f68e29c231a39e19fd59a636fe7e47" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Grandt/PHPZip/zipball/936f93d656f68e29c231a39e19fd59a636fe7e47", + "reference": "936f93d656f68e29c231a39e19fd59a636fe7e47", + "shasum": "" + }, + "require": { + "grandt/binstring": ">=1.0.0", + "grandt/phpzipmerge": ">=1.0.4", + "grandt/relativepath": ">=1.0.2", + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPZip\\Zip\\": "src/Zip" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "Adam Schmalhofer", + "email": "Adam.Schmalhofer@gmx.de", + "role": "Developer" + }, + { + "name": "A. Grandt", + "email": "php@grandt.com", + "homepage": "http://grandt.com", + "role": "Developer" + }, + { + "name": "Greg Kappatos", + "homepage": "http://websiteconnect.com.au", + "role": "Developer" + } + ], + "description": "Package to create and stream archives of compressed files in ZIP format with PHP 5.3+", + "homepage": "https://github.com/Grandt/PHPZip", + "keywords": [ + "archive", + "compressed", + "compression", + "phpzip", + "pkzip", + "stream", + "zip" + ], + "time": "2015-11-16T16:30:51+00:00" + }, + { + "name": "pragmarx/random", + "version": "v0.2.2", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/random.git", + "reference": "daf08a189c5d2d40d1a827db46364d3a741a51b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/random/zipball/daf08a189c5d2d40d1a827db46364d3a741a51b7", + "reference": "daf08a189c5d2d40d1a827db46364d3a741a51b7", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "fzaninotto/faker": "~1.7", + "phpunit/phpunit": "~6.4", + "pragmarx/trivia": "~0.1", + "squizlabs/php_codesniffer": "^2.3" + }, + "suggest": { + "fzaninotto/faker": "Allows you to get dozens of randomized types", + "pragmarx/trivia": "For the trivia database" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "PragmaRX\\Random\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "homepage": "https://antoniocarlosribeiro.com", + "role": "Developer" + } + ], + "description": "Create random chars, numbers, strings", + "homepage": "https://github.com/antonioribeiro/random", + "keywords": [ + "Randomize", + "faker", + "pragmarx", + "random", + "random number", + "random pattern", + "random string" + ], + "time": "2017-11-21T05:26:22+00:00" + }, + { + "name": "pragmarx/recovery", + "version": "v0.1.0", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/recovery.git", + "reference": "e16573a1ae5345cc3b100eec6d0296a1a15a90fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/recovery/zipball/e16573a1ae5345cc3b100eec6d0296a1a15a90fe", + "reference": "e16573a1ae5345cc3b100eec6d0296a1a15a90fe", + "shasum": "" + }, + "require": { + "php": "~7.0", + "pragmarx/random": "~0.1" + }, + "require-dev": { + "phpunit/phpunit": ">=5.4.3", + "squizlabs/php_codesniffer": "^2.3", + "tightenco/collect": "^5" + }, + "suggest": { + "tightenco/collect": "Allows to generate recovery codes as collections" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "PragmaRX\\Recovery\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "homepage": "https://antoniocarlosribeiro.com", + "role": "Developer" + } + ], + "description": "Create recovery codes for two factor auth", + "homepage": "https://github.com/antonioribeiro/recovery", + "keywords": [ + "2fa", + "account recovery", + "auth", + "backup codes", + "google2fa", + "pragmarx", + "recovery", + "recovery codes", + "two factor auth" + ], + "time": "2017-09-19T16:58:00+00:00" + }, + { + "name": "predis/predis", + "version": "v1.1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/nrk/predis.git", + "reference": "111d100ee389d624036b46b35ed0c9ac59c71313" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nrk/predis/zipball/111d100ee389d624036b46b35ed0c9ac59c71313", + "reference": "111d100ee389d624036b46b35ed0c9ac59c71313", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "suggest": { + "ext-curl": "Allows access to Webdis when paired with phpiredis", + "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol" + }, + "type": "library", + "autoload": { + "psr-4": { + "Predis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniele Alessandri", + "email": "suppakilla@gmail.com", + "homepage": "http://clorophilla.net" + } + ], + "description": "Flexible and feature-complete Redis client for PHP and HHVM", + "homepage": "http://github.com/nrk/predis", + "keywords": [ + "nosql", + "predis", + "redis" + ], + "time": "2017-07-12T14:39:17+00:00" + }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "time": "2016-08-06T20:24:11+00:00" + }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14T16:28:37+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "496a823ef742b632934724bf769560c2a5c7c44e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/496a823ef742b632934724bf769560c2a5c7c44e", + "reference": "496a823ef742b632934724bf769560c2a5c7c44e", + "shasum": "" + }, + "require": { + "php": "^7.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "time": "2018-10-30T23:29:13+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "time": "2019-04-30T12:38:16+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/link", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/link.git", + "reference": "eea8e8662d5cd3ae4517c9b864493f59fca95562" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/link/zipball/eea8e8662d5cd3ae4517c9b864493f59fca95562", + "reference": "eea8e8662d5cd3ae4517c9b864493f59fca95562", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Link\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for HTTP links", + "keywords": [ + "http", + "http-link", + "link", + "psr", + "psr-13", + "rest" + ], + "time": "2016-10-28T16:06:13+00:00" + }, + { + "name": "psr/log", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/446d54b4cb6bf489fc9d75f55843658e6f25d801", + "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2019-11-01T11:05:21+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "time": "2017-10-23T01:57:42+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/uuid", + "version": "3.9.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "7779489a47d443f845271badbdcedfe4df8e06fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/7779489a47d443f845271badbdcedfe4df8e06fb", + "reference": "7779489a47d443f845271badbdcedfe4df8e06fb", + "shasum": "" + }, + "require": { + "ext-json": "*", + "paragonie/random_compat": "^1 | ^2 | 9.99.99", + "php": "^5.4 | ^7 | ^8", + "symfony/polyfill-ctype": "^1.8" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "codeception/aspect-mock": "^1 | ^2", + "doctrine/annotations": "^1.2", + "goaop/framework": "1.0.0-alpha.2 | ^1 | ^2.1", + "jakub-onderka/php-parallel-lint": "^1", + "mockery/mockery": "^0.9.11 | ^1", + "moontoast/math": "^1.1", + "paragonie/random-lib": "^2", + "php-mock/php-mock-phpunit": "^0.3 | ^1.1", + "phpunit/phpunit": "^4.8 | ^5.4 | ^6.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "ext-ctype": "Provides support for PHP Ctype functions", + "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator", + "ext-openssl": "Provides the OpenSSL extension for use with the OpenSslGenerator", + "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator", + "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Uuid\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + }, + { + "name": "Marijn Huizendveld", + "email": "marijn.huizendveld@gmail.com" + }, + { + "name": "Thibaud Fabre", + "email": "thibaud@aztech.io" + } + ], + "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).", + "homepage": "https://github.com/ramsey/uuid", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "time": "2019-12-17T08:18:51+00:00" + }, + { + "name": "react/promise", + "version": "v2.7.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "31ffa96f8d2ed0341a57848cbb84d88b89dd664d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/31ffa96f8d2ed0341a57848cbb84d88b89dd664d", + "reference": "31ffa96f8d2ed0341a57848cbb84d88b89dd664d", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "time": "2019-01-07T21:25:54+00:00" + }, + { + "name": "scheb/two-factor-bundle", + "version": "v4.14.0", + "source": { + "type": "git", + "url": "https://github.com/scheb/two-factor-bundle.git", + "reference": "6393d304ee51a703711e5f5cc3e76f04ce4e238c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/scheb/two-factor-bundle/zipball/6393d304ee51a703711e5f5cc3e76f04ce4e238c", + "reference": "6393d304ee51a703711e5f5cc3e76f04ce4e238c", + "shasum": "" + }, + "require": { + "lcobucci/jwt": "^3.2", + "paragonie/constant_time_encoding": "^2.2", + "php": "^7.1.3", + "spomky-labs/otphp": "^9.1|^10.0", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/event-dispatcher": "^3.4|^4.0|^5.0", + "symfony/framework-bundle": "^3.4|^4.0|^5.0", + "symfony/http-foundation": "^3.4|^4.0|^5.0", + "symfony/http-kernel": "^3.4|^4.0|^5.0", + "symfony/property-access": "^3.4|^4.0|^5.0", + "symfony/security-bundle": "^3.4|^4.0|^5.0", + "symfony/twig-bundle": "^3.4|^4.0|^5.0" + }, + "require-dev": { + "doctrine/lexer": "^1.0.1", + "doctrine/orm": "^2.6", + "escapestudios/symfony2-coding-standard": "^3.9", + "phpunit/phpunit": "^7.0|^8.0", + "squizlabs/php_codesniffer": "^3.5", + "swiftmailer/swiftmailer": "^6.0", + "symfony/yaml": "^3.4|^4.0|^5.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Scheb\\TwoFactorBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Scheb", + "email": "me@christianscheb.de" + } + ], + "description": "Provides two-factor authentication for Symfony applications", + "homepage": "https://github.com/scheb/two-factor-bundle", + "keywords": [ + "Authentication", + "security", + "symfony", + "two-factor", + "two-step" + ], + "time": "2020-02-15T13:01:16+00:00" + }, + { + "name": "sensio/distribution-bundle", + "version": "v5.0.25", + "source": { + "type": "git", + "url": "https://github.com/sensiolabs/SensioDistributionBundle.git", + "reference": "80a38234bde8321fb92aa0b8c27978a272bb4baf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sensiolabs/SensioDistributionBundle/zipball/80a38234bde8321fb92aa0b8c27978a272bb4baf", + "reference": "80a38234bde8321fb92aa0b8c27978a272bb4baf", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "sensiolabs/security-checker": "~5.0|~6.0", + "symfony/class-loader": "~2.3|~3.0", + "symfony/config": "~2.3|~3.0", + "symfony/dependency-injection": "~2.3|~3.0", + "symfony/filesystem": "~2.3|~3.0", + "symfony/http-kernel": "~2.3|~3.0", + "symfony/process": "~2.3|~3.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sensio\\Bundle\\DistributionBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Base bundle for Symfony Distributions", + "keywords": [ + "configuration", + "distribution" + ], + "time": "2019-06-18T15:43:58+00:00" + }, + { + "name": "sensio/framework-extra-bundle", + "version": "v5.4.1", + "source": { + "type": "git", + "url": "https://github.com/sensiolabs/SensioFrameworkExtraBundle.git", + "reference": "585f4b3a1c54f24d1a8431c729fc8f5acca20c8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sensiolabs/SensioFrameworkExtraBundle/zipball/585f4b3a1c54f24d1a8431c729fc8f5acca20c8a", + "reference": "585f4b3a1c54f24d1a8431c729fc8f5acca20c8a", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0", + "doctrine/persistence": "^1.0", + "php": ">=7.1.3", + "symfony/config": "^3.4|^4.3", + "symfony/dependency-injection": "^3.4|^4.3", + "symfony/framework-bundle": "^3.4|^4.3", + "symfony/http-kernel": "^3.4|^4.3" + }, + "require-dev": { + "doctrine/doctrine-bundle": "^1.6", + "doctrine/orm": "^2.5", + "nyholm/psr7": "^1.1", + "symfony/browser-kit": "^3.4|^4.3", + "symfony/dom-crawler": "^3.4|^4.3", + "symfony/expression-language": "^3.4|^4.3", + "symfony/finder": "^3.4|^4.3", + "symfony/monolog-bridge": "^3.0|^4.0", + "symfony/monolog-bundle": "^3.2", + "symfony/phpunit-bridge": "^3.4.19|^4.1.8", + "symfony/psr-http-message-bridge": "^1.1", + "symfony/security-bundle": "^3.4|^4.3", + "symfony/twig-bundle": "^3.4|^4.3", + "symfony/yaml": "^3.4|^4.3", + "twig/twig": "~1.12|~2.0" + }, + "suggest": { + "symfony/expression-language": "", + "symfony/psr-http-message-bridge": "To use the PSR-7 converters", + "symfony/security-bundle": "" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "5.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sensio\\Bundle\\FrameworkExtraBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "This bundle provides a way to configure your controllers with annotations", + "keywords": [ + "annotations", + "controllers" + ], + "time": "2019-07-08T08:31:25+00:00" + }, + { + "name": "sensiolabs/security-checker", + "version": "v6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sensiolabs/security-checker.git", + "reference": "a576c01520d9761901f269c4934ba55448be4a54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sensiolabs/security-checker/zipball/a576c01520d9761901f269c4934ba55448be4a54", + "reference": "a576c01520d9761901f269c4934ba55448be4a54", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/console": "^2.8|^3.4|^4.2|^5.0", + "symfony/http-client": "^4.3|^5.0", + "symfony/mime": "^4.3|^5.0", + "symfony/polyfill-ctype": "^1.11" + }, + "bin": [ + "security-checker" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.0-dev" + } + }, + "autoload": { + "psr-4": { + "SensioLabs\\Security\\": "SensioLabs/Security" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien.potencier@gmail.com" + } + ], + "description": "A security checker for your composer.lock", + "time": "2019-11-01T13:20:14+00:00" + }, + { + "name": "sentry/sdk", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-php-sdk.git", + "reference": "4c115873c86ad5bd0ac6d962db70ca53bf8fb874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-php-sdk/zipball/4c115873c86ad5bd0ac6d962db70ca53bf8fb874", + "reference": "4c115873c86ad5bd0ac6d962db70ca53bf8fb874", + "shasum": "" + }, + "require": { + "http-interop/http-factory-guzzle": "^1.0", + "php-http/curl-client": "^1.0|^2.0", + "sentry/sentry": "^2.1.3" + }, + "type": "metapackage", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "This is a metapackage shipping sentry/sentry with a recommended http client.", + "time": "2019-09-09T19:54:44+00:00" + }, + { + "name": "sentry/sentry", + "version": "2.2.6", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-php.git", + "reference": "c9b253229b95f8e5bbf6a3661a170a0be0f80563" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/c9b253229b95f8e5bbf6a3661a170a0be0f80563", + "reference": "c9b253229b95f8e5bbf6a3661a170a0be0f80563", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/promises": "^1.3", + "jean85/pretty-package-versions": "^1.2", + "php": "^7.1", + "php-http/async-client-implementation": "^1.0", + "php-http/client-common": "^1.5|^2.0", + "php-http/discovery": "^1.6.1", + "php-http/httplug": "^1.1|^2.0", + "php-http/message": "^1.5", + "psr/http-message-implementation": "^1.0", + "ramsey/uuid": "^3.3", + "symfony/options-resolver": "^2.7|^3.0|^4.0|^5.0", + "zendframework/zend-diactoros": "^1.7.1|^2.0" + }, + "conflict": { + "php-http/client-common": "1.8.0", + "raven/raven": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.13", + "monolog/monolog": "^1.3|^2.0", + "php-http/mock-client": "^1.0", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.11", + "phpstan/phpstan-phpunit": "^0.11", + "phpunit/phpunit": "^7.5.18", + "symfony/phpunit-bridge": "^4.3|^5.0", + "vimeo/psalm": "^3.4" + }, + "suggest": { + "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-develop": "2.2-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Sentry\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "A PHP SDK for Sentry (http://sentry.io)", + "homepage": "http://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "log", + "logging", + "sentry" + ], + "time": "2019-12-18T08:56:34+00:00" + }, + { + "name": "sentry/sentry-symfony", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-symfony.git", + "reference": "620b90dc35c12ef96c6460df74e30441e359ce6d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-symfony/zipball/620b90dc35c12ef96c6460df74e30441e359ce6d", + "reference": "620b90dc35c12ef96c6460df74e30441e359ce6d", + "shasum": "" + }, + "require": { + "jean85/pretty-package-versions": "^1.0", + "php": "^7.1", + "sentry/sdk": "^2.0", + "symfony/config": "^2.8||^3.0||^4.0", + "symfony/console": "^2.8||^3.0||^4.0", + "symfony/dependency-injection": "^2.8||^3.0||^4.0", + "symfony/event-dispatcher": "^2.8||^3.0||^4.0", + "symfony/http-kernel": "^2.8||^3.0||^4.0", + "symfony/security-core": "^2.8||^3.0||^4.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.8", + "jangregor/phpstan-prophecy": "^0.3.0", + "monolog/monolog": "^1.11||^2.0", + "php-http/mock-client": "^1.0", + "phpstan/phpstan": "^0.11", + "phpstan/phpstan-phpunit": "^0.11", + "phpunit/phpunit": "^7.5", + "scrutinizer/ocular": "^1.4", + "symfony/expression-language": "^2.8||^3.0||^4.0" + }, + "suggest": { + "monolog/monolog": "Required to use the Monolog handler" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "master": "3.x-dev", + "releases/3.2.x": "3.2.x-dev", + "releases/2.x": "2.x-dev", + "releases/1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sentry\\SentryBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "David Cramer", + "email": "dcramer@gmail.com" + }, + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "Symfony integration for Sentry (http://getsentry.com)", + "homepage": "http://getsentry.com", + "keywords": [ + "errors", + "logging", + "sentry", + "symfony" + ], + "time": "2019-11-29T21:24:37+00:00" + }, + { + "name": "simplepie/simplepie", + "version": "1.5.4", + "source": { + "type": "git", + "url": "https://github.com/simplepie/simplepie.git", + "reference": "f4c8246511a38fc9d99a59fb42f61eeeafb31663" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/simplepie/simplepie/zipball/f4c8246511a38fc9d99a59fb42f61eeeafb31663", + "reference": "f4c8246511a38fc9d99a59fb42f61eeeafb31663", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "~5.4.3 || ~6.5" + }, + "suggest": { + "ext-curl": "", + "ext-iconv": "", + "ext-intl": "", + "ext-mbstring": "", + "mf2/mf2": "Microformat module that allows for parsing HTML for microformats" + }, + "type": "library", + "autoload": { + "psr-0": { + "SimplePie": "library" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ryan Parman", + "homepage": "http://ryanparman.com/", + "role": "Creator, alumnus developer" + }, + { + "name": "Geoffrey Sneddon", + "homepage": "http://gsnedders.com/", + "role": "Alumnus developer" + }, + { + "name": "Ryan McCue", + "email": "me@ryanmccue.info", + "homepage": "http://ryanmccue.info/", + "role": "Developer" + } + ], + "description": "A simple Atom/RSS parsing library for PHP", + "homepage": "http://simplepie.org/", + "keywords": [ + "atom", + "feeds", + "rss" + ], + "time": "2019-11-23T07:05:15+00:00" + }, + { + "name": "smalot/pdfparser", + "version": "v0.14.0", + "source": { + "type": "git", + "url": "https://github.com/smalot/pdfparser.git", + "reference": "ec72a99028ba5e21a0acad92047b85e128cbf81f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/smalot/pdfparser/zipball/ec72a99028ba5e21a0acad92047b85e128cbf81f", + "reference": "ec72a99028ba5e21a0acad92047b85e128cbf81f", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-zlib": "*", + "php": ">=5.3.0", + "tecnickcom/tcpdf": "~6.0" + }, + "require-dev": { + "atoum/atoum": "^2.8 | ^3.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Smalot\\PdfParser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "Sebastien Malot", + "email": "sebastien@malot.fr" + } + ], + "description": "Pdf parser library. Can read and extract information from pdf file.", + "homepage": "http://www.pdfparser.org", + "keywords": [ + "extract", + "parse", + "parser", + "pdf", + "text" + ], + "time": "2019-01-23T09:14:37+00:00" + }, + { + "name": "spomky-labs/otphp", + "version": "v9.1.4", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/otphp.git", + "reference": "48d463cf909320399fe08eab2e1cd18d899d5068" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/48d463cf909320399fe08eab2e1cd18d899d5068", + "reference": "48d463cf909320399fe08eab2e1cd18d899d5068", + "shasum": "" + }, + "require": { + "beberlei/assert": "^2.4|^3.0", + "paragonie/constant_time_encoding": "^2.0", + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.0", + "satooshi/php-coveralls": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "OTPHP\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/otphp/contributors" + } + ], + "description": "A PHP library for generating one time passwords according to RFC 4226 (HOTP Algorithm) and the RFC 6238 (TOTP Algorithm) and compatible with Google Authenticator", + "homepage": "https://github.com/Spomky-Labs/otphp", + "keywords": [ + "FreeOTP", + "RFC 4226", + "RFC 6238", + "google authenticator", + "hotp", + "otp", + "totp" + ], + "time": "2019-03-18T10:08:51+00:00" + }, + { + "name": "stof/doctrine-extensions-bundle", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/stof/StofDoctrineExtensionsBundle.git", + "reference": "46db71ec7ffee9122eca3cdddd4ef8d84bae269c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stof/StofDoctrineExtensionsBundle/zipball/46db71ec7ffee9122eca3cdddd4ef8d84bae269c", + "reference": "46db71ec7ffee9122eca3cdddd4ef8d84bae269c", + "shasum": "" + }, + "require": { + "gedmo/doctrine-extensions": "^2.3.4", + "php": ">=5.3.2", + "symfony/framework-bundle": "~2.7|~3.2|~4.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.0", + "symfony/security-bundle": "^2.7 || ^3.2 || ^4.0" + }, + "suggest": { + "doctrine/doctrine-bundle": "to use the ORM extensions", + "doctrine/mongodb-odm-bundle": "to use the MongoDB ODM extensions" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Stof\\DoctrineExtensionsBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + } + ], + "description": "Integration of the gedmo/doctrine-extensions with Symfony2", + "homepage": "https://github.com/stof/StofDoctrineExtensionsBundle", + "keywords": [ + "behaviors", + "doctrine2", + "extensions", + "gedmo", + "loggable", + "nestedset", + "sluggable", + "sortable", + "timestampable", + "translatable", + "tree" + ], + "time": "2017-12-24T16:06:50+00:00" + }, + { + "name": "swiftmailer/swiftmailer", + "version": "v6.2.3", + "source": { + "type": "git", + "url": "https://github.com/swiftmailer/swiftmailer.git", + "reference": "149cfdf118b169f7840bbe3ef0d4bc795d1780c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/149cfdf118b169f7840bbe3ef0d4bc795d1780c9", + "reference": "149cfdf118b169f7840bbe3ef0d4bc795d1780c9", + "shasum": "" + }, + "require": { + "egulias/email-validator": "~2.0", + "php": ">=7.0.0", + "symfony/polyfill-iconv": "^1.0", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "require-dev": { + "mockery/mockery": "~0.9.1", + "symfony/phpunit-bridge": "^3.4.19|^4.1.8" + }, + "suggest": { + "ext-intl": "Needed to support internationalized email addresses", + "true/punycode": "Needed to support internationalized email addresses, if ext-intl is not installed" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.2-dev" + } + }, + "autoload": { + "files": [ + "lib/swift_required.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Corbyn" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Swiftmailer, free feature-rich PHP mailer", + "homepage": "https://swiftmailer.symfony.com", + "keywords": [ + "email", + "mail", + "mailer" + ], + "time": "2019-11-12T09:31:26+00:00" + }, + { + "name": "symfony/http-client", + "version": "v4.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "9ebfc77b5018a05226b38642def82746f3e2ce0f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/9ebfc77b5018a05226b38642def82746f3e2ce0f", + "reference": "9ebfc77b5018a05226b38642def82746f3e2ce0f", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "psr/log": "^1.0", + "symfony/http-client-contracts": "^1.1.8|^2", + "symfony/polyfill-php73": "^1.11", + "symfony/service-contracts": "^1.0|^2" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "1.1" + }, + "require-dev": { + "guzzlehttp/promises": "^1.3.1", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/dependency-injection": "^4.3|^5.0", + "symfony/http-kernel": "^4.4", + "symfony/process": "^4.2|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony HttpClient component", + "homepage": "https://symfony.com", + "time": "2019-12-19T15:57:49+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v1.1.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "088bae75cfa2ec5eb6d33dce17dbd8613150ce6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/088bae75cfa2ec5eb6d33dce17dbd8613150ce6e", + "reference": "088bae75cfa2ec5eb6d33dce17dbd8613150ce6e", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "suggest": { + "symfony/http-client-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "time": "2019-11-07T12:44:51+00:00" + }, + { + "name": "symfony/mime", + "version": "v4.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "010cc488e56cafe5f7494dea70aea93100c234df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/010cc488e56cafe5f7494dea70aea93100c234df", + "reference": "010cc488e56cafe5f7494dea70aea93100c234df", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/mailer": "<4.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10", + "symfony/dependency-injection": "^3.4|^4.1|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A library to manipulate MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "time": "2019-11-30T08:27:26+00:00" + }, + { + "name": "symfony/monolog-bundle", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bundle.git", + "reference": "dd80460fcfe1fa2050a7103ad818e9d0686ce6fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/dd80460fcfe1fa2050a7103ad818e9d0686ce6fd", + "reference": "dd80460fcfe1fa2050a7103ad818e9d0686ce6fd", + "shasum": "" + }, + "require": { + "monolog/monolog": "~1.22 || ~2.0", + "php": ">=5.6", + "symfony/config": "~3.4 || ~4.0 || ^5.0", + "symfony/dependency-injection": "~3.4.10 || ^4.0.10 || ^5.0", + "symfony/http-kernel": "~3.4 || ~4.0 || ^5.0", + "symfony/monolog-bridge": "~3.4 || ~4.0 || ^5.0" + }, + "require-dev": { + "symfony/console": "~3.4 || ~4.0 || ^5.0", + "symfony/phpunit-bridge": "^3.4.19 || ^4.0 || ^5.0", + "symfony/yaml": "~3.4 || ~4.0 || ^5.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MonologBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "description": "Symfony MonologBundle", + "homepage": "http://symfony.com", + "keywords": [ + "log", + "logging" + ], + "time": "2019-11-13T13:11:14+00:00" + }, + { + "name": "symfony/polyfill-apcu", + "version": "v1.13.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-apcu.git", + "reference": "a8e961c841b9ec52927a87914f8820a1ad8f8116" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-apcu/zipball/a8e961c841b9ec52927a87914f8820a1ad8f8116", + "reference": "a8e961c841b9ec52927a87914f8820a1ad8f8116", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.13-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Apcu\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting apcu_* functions to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "apcu", + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2019-11-27T13:56:44+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.13.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", + "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.13-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2019-11-27T13:56:44+00:00" + }, + { + "name": "symfony/polyfill-iconv", + "version": "v1.13.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-iconv.git", + "reference": "a019efccc03f1a335af6b4f20c30f5ea8060be36" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/a019efccc03f1a335af6b4f20c30f5ea8060be36", + "reference": "a019efccc03f1a335af6b4f20c30f5ea8060be36", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-iconv": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.13-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Iconv\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Iconv extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "iconv", + "polyfill", + "portable", + "shim" + ], + "time": "2019-11-27T13:56:44+00:00" + }, + { + "name": "symfony/polyfill-intl-icu", + "version": "v1.13.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-icu.git", + "reference": "b3dffd68afa61ca70f2327f2dd9bbeb6aa53d70b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/b3dffd68afa61ca70f2327f2dd9bbeb6aa53d70b", + "reference": "b3dffd68afa61ca70f2327f2dd9bbeb6aa53d70b", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/intl": "~2.3|~3.0|~4.0|~5.0" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.13-dev" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's ICU-related data and classes", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "icu", + "intl", + "polyfill", + "portable", + "shim" + ], + "time": "2019-11-27T13:56:44+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.13.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "6f9c239e61e1b0c9229a28ff89a812dc449c3d46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/6f9c239e61e1b0c9229a28ff89a812dc449c3d46", + "reference": "6f9c239e61e1b0c9229a28ff89a812dc449c3d46", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/polyfill-mbstring": "^1.3", + "symfony/polyfill-php72": "^1.9" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.13-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "time": "2019-11-27T13:56:44+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.13.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7b4aab9743c30be783b73de055d24a39cf4b954f", + "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.13-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2019-11-27T14:18:11+00:00" + }, + { + "name": "symfony/polyfill-php56", + "version": "v1.13.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php56.git", + "reference": "53dd1cdf3cb986893ccf2b96665b25b3abb384f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/53dd1cdf3cb986893ccf2b96665b25b3abb384f4", + "reference": "53dd1cdf3cb986893ccf2b96665b25b3abb384f4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/polyfill-util": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.13-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php56\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 5.6+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2019-11-27T13:56:44+00:00" + }, + { + "name": "symfony/polyfill-php70", + "version": "v1.13.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php70.git", + "reference": "af23c7bb26a73b850840823662dda371484926c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/af23c7bb26a73b850840823662dda371484926c4", + "reference": "af23c7bb26a73b850840823662dda371484926c4", + "shasum": "" + }, + "require": { + "paragonie/random_compat": "~1.0|~2.0|~9.99", + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.13-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php70\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2019-11-27T13:56:44+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.13.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "66fea50f6cb37a35eea048d75a7d99a45b586038" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/66fea50f6cb37a35eea048d75a7d99a45b586038", + "reference": "66fea50f6cb37a35eea048d75a7d99a45b586038", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.13-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2019-11-27T13:56:44+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.13.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "4b0e2222c55a25b4541305a053013d5647d3a25f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/4b0e2222c55a25b4541305a053013d5647d3a25f", + "reference": "4b0e2222c55a25b4541305a053013d5647d3a25f", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.13-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2019-11-27T16:25:15+00:00" + }, + { + "name": "symfony/polyfill-util", + "version": "v1.13.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-util.git", + "reference": "964a67f293b66b95883a5ed918a65354fcd2258f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/964a67f293b66b95883a5ed918a65354fcd2258f", + "reference": "964a67f293b66b95883a5ed918a65354fcd2258f", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.13-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Util\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony utilities for portability of PHP codes", + "homepage": "https://symfony.com", + "keywords": [ + "compat", + "compatibility", + "polyfill", + "shim" + ], + "time": "2019-11-27T13:56:44+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v1.1.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "ffc7f5692092df31515df2a5ecf3b7302b3ddacf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/ffc7f5692092df31515df2a5ecf3b7302b3ddacf", + "reference": "ffc7f5692092df31515df2a5ecf3b7302b3ddacf", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "psr/container": "^1.0" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "time": "2019-10-14T12:27:06+00:00" + }, + { + "name": "symfony/swiftmailer-bundle", + "version": "v3.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/swiftmailer-bundle.git", + "reference": "defa9bdfc0191ed70b389cb93c550c6c82cf1745" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/swiftmailer-bundle/zipball/defa9bdfc0191ed70b389cb93c550c6c82cf1745", + "reference": "defa9bdfc0191ed70b389cb93c550c6c82cf1745", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "swiftmailer/swiftmailer": "^6.1.3", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/http-kernel": "^3.4|^4.0|^5.0" + }, + "conflict": { + "twig/twig": "<1.41|<2.10" + }, + "require-dev": { + "symfony/console": "^3.4|^4.0|^5.0", + "symfony/framework-bundle": "^3.4|^4.0|^5.0", + "symfony/phpunit-bridge": "^3.4.32|^4.3.5|^5.0", + "symfony/yaml": "^3.4|^4.0|^5.0" + }, + "suggest": { + "psr/log": "Allows logging" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\SwiftmailerBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "description": "Symfony SwiftmailerBundle", + "homepage": "http://symfony.com", + "time": "2019-11-07T21:01:35+00:00" + }, + { + "name": "symfony/symfony", + "version": "v3.4.37", + "source": { + "type": "git", + "url": "https://github.com/symfony/symfony.git", + "reference": "1bd873459b36cf505c7b515ba6e0e2ee40890b8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/symfony/zipball/1bd873459b36cf505c7b515ba6e0e2ee40890b8a", + "reference": "1bd873459b36cf505c7b515ba6e0e2ee40890b8a", + "shasum": "" + }, + "require": { + "doctrine/common": "~2.4", + "ext-xml": "*", + "fig/link-util": "^1.0", + "php": "^5.5.9|>=7.0.8", + "psr/cache": "~1.0", + "psr/container": "^1.0", + "psr/link": "^1.0", + "psr/log": "~1.0", + "psr/simple-cache": "^1.0", + "symfony/polyfill-apcu": "~1.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php56": "~1.0", + "symfony/polyfill-php70": "~1.6", + "twig/twig": "^1.41|^2.10" + }, + "conflict": { + "monolog/monolog": ">=2", + "phpdocumentor/reflection-docblock": "<3.0||>=3.2.0,<3.2.2", + "phpdocumentor/type-resolver": "<0.3.0", + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0" + }, + "provide": { + "psr/cache-implementation": "1.0", + "psr/container-implementation": "1.0", + "psr/log-implementation": "1.0", + "psr/simple-cache-implementation": "1.0" + }, + "replace": { + "symfony/asset": "self.version", + "symfony/browser-kit": "self.version", + "symfony/cache": "self.version", + "symfony/class-loader": "self.version", + "symfony/config": "self.version", + "symfony/console": "self.version", + "symfony/css-selector": "self.version", + "symfony/debug": "self.version", + "symfony/debug-bundle": "self.version", + "symfony/dependency-injection": "self.version", + "symfony/doctrine-bridge": "self.version", + "symfony/dom-crawler": "self.version", + "symfony/dotenv": "self.version", + "symfony/event-dispatcher": "self.version", + "symfony/expression-language": "self.version", + "symfony/filesystem": "self.version", + "symfony/finder": "self.version", + "symfony/form": "self.version", + "symfony/framework-bundle": "self.version", + "symfony/http-foundation": "self.version", + "symfony/http-kernel": "self.version", + "symfony/inflector": "self.version", + "symfony/intl": "self.version", + "symfony/ldap": "self.version", + "symfony/lock": "self.version", + "symfony/monolog-bridge": "self.version", + "symfony/options-resolver": "self.version", + "symfony/process": "self.version", + "symfony/property-access": "self.version", + "symfony/property-info": "self.version", + "symfony/proxy-manager-bridge": "self.version", + "symfony/routing": "self.version", + "symfony/security": "self.version", + "symfony/security-bundle": "self.version", + "symfony/security-core": "self.version", + "symfony/security-csrf": "self.version", + "symfony/security-guard": "self.version", + "symfony/security-http": "self.version", + "symfony/serializer": "self.version", + "symfony/stopwatch": "self.version", + "symfony/templating": "self.version", + "symfony/translation": "self.version", + "symfony/twig-bridge": "self.version", + "symfony/twig-bundle": "self.version", + "symfony/validator": "self.version", + "symfony/var-dumper": "self.version", + "symfony/web-link": "self.version", + "symfony/web-profiler-bundle": "self.version", + "symfony/web-server-bundle": "self.version", + "symfony/workflow": "self.version", + "symfony/yaml": "self.version" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/annotations": "~1.0", + "doctrine/cache": "~1.6", + "doctrine/data-fixtures": "1.0.*", + "doctrine/dbal": "~2.4", + "doctrine/doctrine-bundle": "~1.4", + "doctrine/orm": "~2.4,>=2.4.5", + "egulias/email-validator": "~1.2,>=1.2.8|~2.0", + "monolog/monolog": "~1.11", + "ocramius/proxy-manager": "~0.4|~1.0|~2.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0", + "predis/predis": "~1.0", + "symfony/phpunit-bridge": "^3.4.31|^4.3.4|~5.0", + "symfony/security-acl": "~2.8|~3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Doctrine\\": "src/Symfony/Bridge/Doctrine/", + "Symfony\\Bridge\\Monolog\\": "src/Symfony/Bridge/Monolog/", + "Symfony\\Bridge\\ProxyManager\\": "src/Symfony/Bridge/ProxyManager/", + "Symfony\\Bridge\\Twig\\": "src/Symfony/Bridge/Twig/", + "Symfony\\Bundle\\": "src/Symfony/Bundle/", + "Symfony\\Component\\": "src/Symfony/Component/" + }, + "classmap": [ + "src/Symfony/Component/Intl/Resources/stubs" + ], + "exclude-from-classmap": [ + "**/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "The Symfony PHP framework", + "homepage": "https://symfony.com", + "keywords": [ + "framework" + ], + "time": "2020-01-21T12:30:09+00:00" + }, + { + "name": "true/punycode", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/true/php-punycode.git", + "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/true/php-punycode/zipball/a4d0c11a36dd7f4e7cd7096076cab6d3378a071e", + "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.7", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "TrueBV\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Renan Gonçalves", + "email": "renan.saddam@gmail.com" + } + ], + "description": "A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA)", + "homepage": "https://github.com/true/php-punycode", + "keywords": [ + "idna", + "punycode" + ], + "time": "2016-11-16T10:37:54+00:00" + }, + { + "name": "twig/extensions", + "version": "v1.5.4", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig-extensions.git", + "reference": "57873c8b0c1be51caa47df2cdb824490beb16202" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig-extensions/zipball/57873c8b0c1be51caa47df2cdb824490beb16202", + "reference": "57873c8b0c1be51caa47df2cdb824490beb16202", + "shasum": "" + }, + "require": { + "twig/twig": "^1.27|^2.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^3.4", + "symfony/translation": "^2.7|^3.4" + }, + "suggest": { + "symfony/translation": "Allow the time_diff output to be translated" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "psr-0": { + "Twig_Extensions_": "lib/" + }, + "psr-4": { + "Twig\\Extensions\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Common additional features for Twig that do not directly belong in core", + "keywords": [ + "i18n", + "text" + ], + "time": "2018-12-05T18:34:18+00:00" + }, + { + "name": "twig/twig", + "version": "v2.12.3", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "97b6311585cae66a26833b14b33785f5797f7d39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/97b6311585cae66a26833b14b33785f5797f7d39", + "reference": "97b6311585cae66a26833b14b33785f5797f7d39", + "shasum": "" + }, + "require": { + "php": "^7.0", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "psr/container": "^1.0", + "symfony/phpunit-bridge": "^4.4|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.12-dev" + } + }, + "autoload": { + "psr-0": { + "Twig_": "lib/" + }, + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "time": "2019-12-28T07:12:03+00:00" + }, + { + "name": "wallabag/php-mobi", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/wallabag/php-mobi.git", + "reference": "bfe9c18d038e5ca24664cfdab107a645ac57c9b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wallabag/php-mobi/zipball/bfe9c18d038e5ca24664cfdab107a645ac57c9b5", + "reference": "bfe9c18d038e5ca24664cfdab107a645ac57c9b5", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "symfony/polyfill-mbstring": "^1.12" + }, + "type": "library", + "autoload": { + "files": [ + "MOBIClass/MOBI.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Sander Kromwijk", + "email": "s.kromwijk@gmail.co", + "role": "Original developer" + }, + { + "name": "Nicolas Lœuillet", + "email": "nicolas@loeuillet.org", + "homepage": "http://www.cdetc.fr" + } + ], + "description": "A Mobipocket file (.mobi) creator in PHP.", + "homepage": "https://github.com/wallabag/php-mobi", + "time": "2019-08-08T12:26:23+00:00" + }, + { + "name": "wallabag/tcpdf", + "version": "6.2.26", + "source": { + "type": "git", + "url": "https://github.com/wallabag/TCPDF.git", + "reference": "bf590f0604bcef1ae6fa3145649cf997f3564477" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wallabag/TCPDF/zipball/bf590f0604bcef1ae6fa3145649cf997f3564477", + "reference": "bf590f0604bcef1ae6fa3145649cf997f3564477", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "replace": { + "tecnickcom/tcpdf": "6.2.*" + }, + "type": "library", + "autoload": { + "classmap": [ + "fonts", + "config", + "include", + "tcpdf.php", + "tcpdf_parser.php", + "tcpdf_import.php", + "tcpdf_barcodes_1d.php", + "tcpdf_barcodes_2d.php", + "include/tcpdf_colors.php", + "include/tcpdf_filters.php", + "include/tcpdf_font_data.php", + "include/tcpdf_fonts.php", + "include/tcpdf_images.php", + "include/tcpdf_static.php", + "include/barcodes/datamatrix.php", + "include/barcodes/pdf417.php", + "include/barcodes/qrcode.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "Nicola Asuni", + "email": "info@tecnick.com", + "homepage": "http://nicolaasuni.tecnick.com", + "role": "Main developer" + }, + { + "name": "wallabag/core", + "homepage": "https://www.wallabag.org", + "role": "Developers" + } + ], + "description": "Keeping a working 6.2.x TCPDF version.", + "homepage": "https://www.wallabag.org/", + "keywords": [ + "PDFD32000-2008", + "TCPDF", + "barcodes", + "datamatrix", + "pdf", + "pdf417", + "qrcode" + ], + "time": "2018-10-25T06:56:14+00:00" + }, + { + "name": "white-october/pagerfanta-bundle", + "version": "v1.3.2", + "source": { + "type": "git", + "url": "https://github.com/whiteoctober/WhiteOctoberPagerfantaBundle.git", + "reference": "6df560869b5e09a3acf920890ab40598998b30ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/whiteoctober/WhiteOctoberPagerfantaBundle/zipball/6df560869b5e09a3acf920890ab40598998b30ae", + "reference": "6df560869b5e09a3acf920890ab40598998b30ae", + "shasum": "" + }, + "require": { + "pagerfanta/pagerfanta": "^1.1.0|^2.0.0", + "php": ">=5.3.3", + "symfony/framework-bundle": "~2.3|~3.0|~4.0", + "symfony/property-access": "~2.3|~3.0|~4.0", + "symfony/translation": "~2.3|~3.0|~4.0", + "symfony/twig-bundle": "~2.3|~3.0|~4.0" + }, + "conflict": { + "twig/twig": "<1.34|>=2.0,<2.4" + }, + "require-dev": { + "phpunit/phpunit": "~3.7|~4.0|^5.0", + "symfony/symfony": "~2.3|~3.0|~4.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "WhiteOctober\\PagerfantaBundle\\": "" + }, + "exclude-from-classmap": [ + "Tests/", + "TestsProject/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Pablo Díez", + "email": "pablodip@gmail.com" + } + ], + "description": "Bundle to use Pagerfanta with Symfony2", + "keywords": [ + "page", + "paging" + ], + "time": "2019-12-02T14:19:37+00:00" + }, + { + "name": "willdurand/hateoas", + "version": "2.12.0", + "source": { + "type": "git", + "url": "https://github.com/willdurand/Hateoas.git", + "reference": "71b1af62b398dc9a870ac0b16c84bdc23a76a5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/willdurand/Hateoas/zipball/71b1af62b398dc9a870ac0b16c84bdc23a76a5c5", + "reference": "71b1af62b398dc9a870ac0b16c84bdc23a76a5c5", + "shasum": "" + }, + "require": { + "doctrine/annotations": "~1.0", + "doctrine/common": "~2.0", + "jms/metadata": "~1.1", + "jms/serializer": "^1.7", + "php": "^5.5|^7.0", + "phpoption/phpoption": ">=1.1.0,<2.0-dev", + "symfony/expression-language": "~2.4 || ~3.0 || ~4.0" + }, + "require-dev": { + "pagerfanta/pagerfanta": "~1.0", + "phpunit/phpunit": "~5", + "symfony/dependency-injection": "~2.4 || ~3.0 || ~4.0", + "symfony/routing": "~2.4 || ~3.0 || ~4.0", + "symfony/yaml": "~2.4 || ~3.0 || ~4.0", + "twig/twig": "~1.12" + }, + "suggest": { + "symfony/routing": "To use the SymfonyRouteFactory.", + "symfony/yaml": "To use yaml based configuration.", + "twig/twig": "To use the Twig extensions." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.12-dev" + } + }, + "autoload": { + "psr-0": { + "Hateoas": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Adrien Brault", + "email": "adrien.brault@gmail.com" + }, + { + "name": "William DURAND", + "email": "william.durand1@gmail.com" + } + ], + "description": "A PHP library to support implementing representations for HATEOAS REST web services", + "time": "2018-02-23T17:05:31+00:00" + }, + { + "name": "willdurand/hateoas-bundle", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/willdurand/BazingaHateoasBundle.git", + "reference": "d1f915fd4f8a7cd43a88a0ce97ffb28abe3a94fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/willdurand/BazingaHateoasBundle/zipball/d1f915fd4f8a7cd43a88a0ce97ffb28abe3a94fa", + "reference": "d1f915fd4f8a7cd43a88a0ce97ffb28abe3a94fa", + "shasum": "" + }, + "require": { + "jms/serializer-bundle": "~1.0 || ^2.0", + "php": ">5.4 |^7.0", + "symfony/framework-bundle": "~2.3 || ~3.0 || ~4.0", + "willdurand/hateoas": "^2.10.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.5 || ~5.0", + "symfony/expression-language": "~2.4 || ~3.0 || ~4.0", + "symfony/stopwatch": "~2.4 || ~3.0 || ~4.0", + "twig/twig": "~1.12" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "Bazinga\\Bundle\\HateoasBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "William DURAND", + "email": "william.durand1@gmail.com" + } + ], + "description": "Integration of Hateoas into Symfony2.", + "keywords": [ + "HATEOAS", + "rest" + ], + "time": "2018-01-27T13:03:07+00:00" + }, + { + "name": "willdurand/jsonp-callback-validator", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/willdurand/JsonpCallbackValidator.git", + "reference": "1a7d388bb521959e612ef50c5c7b1691b097e909" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/willdurand/JsonpCallbackValidator/zipball/1a7d388bb521959e612ef50c5c7b1691b097e909", + "reference": "1a7d388bb521959e612ef50c5c7b1691b097e909", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "~3.7" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonpCallbackValidator": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "William Durand", + "email": "william.durand1@gmail.com", + "homepage": "http://www.willdurand.fr" + } + ], + "description": "JSONP callback validator.", + "time": "2014-01-20T22:35:06+00:00" + }, + { + "name": "willdurand/negotiation", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/willdurand/Negotiation.git", + "reference": "03436ededa67c6e83b9b12defac15384cb399dc9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/willdurand/Negotiation/zipball/03436ededa67c6e83b9b12defac15384cb399dc9", + "reference": "03436ededa67c6e83b9b12defac15384cb399dc9", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + }, + "autoload": { + "psr-4": { + "Negotiation\\": "src/Negotiation" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "William Durand", + "email": "will+git@drnd.me" + } + ], + "description": "Content Negotiation tools for PHP provided as a standalone library.", + "homepage": "http://williamdurand.fr/Negotiation/", + "keywords": [ + "accept", + "content", + "format", + "header", + "negotiation" + ], + "time": "2017-05-14T17:21:12+00:00" + }, + { + "name": "zendframework/zend-code", + "version": "3.4.1", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-code.git", + "reference": "268040548f92c2bfcba164421c1add2ba43abaaa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-code/zipball/268040548f92c2bfcba164421c1add2ba43abaaa", + "reference": "268040548f92c2bfcba164421c1add2ba43abaaa", + "shasum": "" + }, + "require": { + "php": "^7.1", + "zendframework/zend-eventmanager": "^2.6 || ^3.0" + }, + "conflict": { + "phpspec/prophecy": "<1.9.0" + }, + "require-dev": { + "doctrine/annotations": "^1.7", + "ext-phar": "*", + "phpunit/phpunit": "^7.5.16 || ^8.4", + "zendframework/zend-coding-standard": "^1.0", + "zendframework/zend-stdlib": "^2.7 || ^3.0" + }, + "suggest": { + "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features", + "zendframework/zend-stdlib": "Zend\\Stdlib component" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4.x-dev", + "dev-develop": "3.5.x-dev", + "dev-dev-4.0": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Code\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Extensions to the PHP Reflection API, static code scanning, and code generation", + "keywords": [ + "ZendFramework", + "code", + "zf" + ], + "abandoned": "laminas/laminas-code", + "time": "2019-12-10T19:21:15+00:00" + }, + { + "name": "zendframework/zend-diactoros", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-diactoros.git", + "reference": "de5847b068362a88684a55b0dbb40d85986cfa52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/de5847b068362a88684a55b0dbb40d85986cfa52", + "reference": "de5847b068362a88684a55b0dbb40d85986cfa52", + "shasum": "" + }, + "require": { + "php": "^7.1", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-curl": "*", + "ext-dom": "*", + "ext-libxml": "*", + "http-interop/http-factory-tests": "^0.5.0", + "php-http/psr7-integration-tests": "dev-master", + "phpunit/phpunit": "^7.0.2", + "zendframework/zend-coding-standard": "~1.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev", + "dev-develop": "2.2.x-dev", + "dev-release-1.8": "1.8.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/marshal_uri_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php" + ], + "psr-4": { + "Zend\\Diactoros\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "PSR HTTP Message implementations", + "keywords": [ + "http", + "psr", + "psr-7" + ], + "abandoned": "laminas/laminas-diactoros", + "time": "2019-11-13T19:16:13+00:00" + }, + { + "name": "zendframework/zend-eventmanager", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-eventmanager.git", + "reference": "a5e2583a211f73604691586b8406ff7296a946dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-eventmanager/zipball/a5e2583a211f73604691586b8406ff7296a946dd", + "reference": "a5e2583a211f73604691586b8406ff7296a946dd", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "athletic/athletic": "^0.1", + "container-interop/container-interop": "^1.1.0", + "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-stdlib": "^2.7.3 || ^3.0" + }, + "suggest": { + "container-interop/container-interop": "^1.1.0, to use the lazy listeners feature", + "zendframework/zend-stdlib": "^2.7.3 || ^3.0, to use the FilterChain feature" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev", + "dev-develop": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\EventManager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Trigger and listen to events within a PHP application", + "homepage": "https://github.com/zendframework/zend-eventmanager", + "keywords": [ + "event", + "eventmanager", + "events", + "zf2" + ], + "abandoned": "laminas/laminas-eventmanager", + "time": "2018-04-25T15:33:34+00:00" + } + ], + "packages-dev": [ + { + "name": "composer/semver", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/46d9139568ccb8d9e7cdd4539cab7347568a5e2e", + "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.5 || ^5.0.5", + "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "time": "2019-03-19T17:25:45+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "cbe23383749496fe0f373345208b79568e4bc248" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/cbe23383749496fe0f373345208b79568e4bc248", + "reference": "cbe23383749496fe0f373345208b79568e4bc248", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0", + "psr/log": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "time": "2019-11-06T16:40:04+00:00" + }, + { + "name": "dama/doctrine-test-bundle", + "version": "v5.0.3", + "source": { + "type": "git", + "url": "https://github.com/dmaicher/doctrine-test-bundle.git", + "reference": "29882b0d1a815f4819126ef714931bb24a31cbaa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/29882b0d1a815f4819126ef714931bb24a31cbaa", + "reference": "29882b0d1a815f4819126ef714931bb24a31cbaa", + "shasum": "" + }, + "require": { + "doctrine/dbal": "~2.5", + "doctrine/doctrine-bundle": "~1.4", + "php": "^7.1", + "symfony/framework-bundle": "~2.7|~3.0|~4.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0|~7.0|~8.0", + "symfony/phpunit-bridge": "~2.8|~3.0|~4.0", + "symfony/yaml": "~2.8|~3.0|~4.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "5.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "DAMA\\DoctrineTestBundle\\": "src/DAMA/DoctrineTestBundle" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "David Maicher", + "email": "mail@dmaicher.de" + } + ], + "description": "Symfony bundle to isolate doctrine database tests and improve test performance", + "keywords": [ + "Symfony 3", + "Symfony 4", + "doctrine", + "isolation", + "performance", + "symfony", + "symfony 2", + "tests" + ], + "time": "2019-03-22T10:34:17+00:00" + }, + { + "name": "doctrine/data-fixtures", + "version": "1.3.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/data-fixtures.git", + "reference": "f0ee99c64922fc3f863715232b615c478a61b0a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/f0ee99c64922fc3f863715232b615c478a61b0a3", + "reference": "f0ee99c64922fc3f863715232b615c478a61b0a3", + "shasum": "" + }, + "require": { + "doctrine/common": "~2.2", + "php": "^7.1" + }, + "conflict": { + "doctrine/phpcr-odm": "<1.3.0" + }, + "require-dev": { + "alcaeus/mongo-php-adapter": "^1.1", + "doctrine/dbal": "^2.5.4", + "doctrine/mongodb-odm": "^1.3.0", + "doctrine/orm": "^2.5.4", + "phpunit/phpunit": "^7.0" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "For using MongoDB ODM with PHP 7", + "doctrine/mongodb-odm": "For loading MongoDB ODM fixtures", + "doctrine/orm": "For loading ORM fixtures", + "doctrine/phpcr-odm": "For loading PHPCR ODM fixtures" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\DataFixtures\\": "lib/Doctrine/Common/DataFixtures" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Data Fixtures for all Doctrine Object Managers", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "database" + ], + "time": "2019-10-24T04:52:28+00:00" + }, + { + "name": "doctrine/doctrine-fixtures-bundle", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineFixturesBundle.git", + "reference": "8f07fcfdac7f3591f3c4bf13a50cbae05f65ed70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/8f07fcfdac7f3591f3c4bf13a50cbae05f65ed70", + "reference": "8f07fcfdac7f3591f3c4bf13a50cbae05f65ed70", + "shasum": "" + }, + "require": { + "doctrine/data-fixtures": "^1.3", + "doctrine/doctrine-bundle": "^1.11|^2.0", + "doctrine/orm": "^2.6.0", + "php": "^7.1", + "symfony/config": "^3.4|^4.3|^5.0", + "symfony/console": "^3.4|^4.3|^5.0", + "symfony/dependency-injection": "^3.4|^4.3|^5.0", + "symfony/doctrine-bridge": "^3.4|^4.1|^5.0", + "symfony/http-kernel": "^3.4|^4.3|^5.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpunit/phpunit": "^7.4", + "symfony/phpunit-bridge": "^4.1|^5.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\FixturesBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Doctrine Project", + "homepage": "http://www.doctrine-project.org" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "description": "Symfony DoctrineFixturesBundle", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "Fixture", + "persistence" + ], + "time": "2019-11-13T15:46:58+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v2.16.1", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", + "reference": "c8afb599858876e95e8ebfcd97812d383fa23f02" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/c8afb599858876e95e8ebfcd97812d383fa23f02", + "reference": "c8afb599858876e95e8ebfcd97812d383fa23f02", + "shasum": "" + }, + "require": { + "composer/semver": "^1.4", + "composer/xdebug-handler": "^1.2", + "doctrine/annotations": "^1.2", + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^5.6 || ^7.0", + "php-cs-fixer/diff": "^1.3", + "symfony/console": "^3.4.17 || ^4.1.6 || ^5.0", + "symfony/event-dispatcher": "^3.0 || ^4.0 || ^5.0", + "symfony/filesystem": "^3.0 || ^4.0 || ^5.0", + "symfony/finder": "^3.0 || ^4.0 || ^5.0", + "symfony/options-resolver": "^3.0 || ^4.0 || ^5.0", + "symfony/polyfill-php70": "^1.0", + "symfony/polyfill-php72": "^1.4", + "symfony/process": "^3.0 || ^4.0 || ^5.0", + "symfony/stopwatch": "^3.0 || ^4.0 || ^5.0" + }, + "require-dev": { + "johnkary/phpunit-speedtrap": "^1.1 || ^2.0 || ^3.0", + "justinrainbow/json-schema": "^5.0", + "keradus/cli-executor": "^1.2", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.1", + "php-cs-fixer/accessible-object": "^1.0", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.1", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.1", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.1", + "phpunitgoodpractices/traits": "^1.8", + "symfony/phpunit-bridge": "^4.3 || ^5.0", + "symfony/yaml": "^3.0 || ^4.0 || ^5.0" + }, + "suggest": { + "ext-mbstring": "For handling non-UTF8 characters in cache signature.", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "For IsIdenticalString constraint.", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "For XmlMatchesXsd constraint.", + "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "classmap": [ + "tests/Test/AbstractFixerTestCase.php", + "tests/Test/AbstractIntegrationCaseFactory.php", + "tests/Test/AbstractIntegrationTestCase.php", + "tests/Test/Assert/AssertTokensTrait.php", + "tests/Test/IntegrationCase.php", + "tests/Test/IntegrationCaseFactory.php", + "tests/Test/IntegrationCaseFactoryInterface.php", + "tests/Test/InternalIntegrationCaseFactory.php", + "tests/TestCase.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "time": "2019-11-25T22:10:32+00:00" + }, + { + "name": "m6web/redis-mock", + "version": "v5.0.0", + "source": { + "type": "git", + "url": "https://github.com/M6Web/RedisMock.git", + "reference": "1b7a1bcbb63d636b003643e88f785615e0a316bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/M6Web/RedisMock/zipball/1b7a1bcbb63d636b003643e88f785615e0a316bd", + "reference": "1b7a1bcbb63d636b003643e88f785615e0a316bd", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "atoum/atoum": "master-dev", + "predis/predis": "~1.1" + }, + "type": "library", + "autoload": { + "psr-0": { + "M6Web\\Component\\RedisMock": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "M6Web", + "email": "opensource@m6web.fr", + "homepage": "http://tech.m6web.fr/" + } + ], + "description": "Library providing a PHP mock for Redis", + "keywords": [ + "mock", + "redis" + ], + "time": "2020-01-08T08:03:01+00:00" + }, + { + "name": "nette/bootstrap", + "version": "v3.0.1", + "source": { + "type": "git", + "url": "https://github.com/nette/bootstrap.git", + "reference": "b45a1e33b6a44beb307756522396551e5a9ff249" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/bootstrap/zipball/b45a1e33b6a44beb307756522396551e5a9ff249", + "reference": "b45a1e33b6a44beb307756522396551e5a9ff249", + "shasum": "" + }, + "require": { + "nette/di": "^3.0", + "nette/utils": "^3.0", + "php": ">=7.1" + }, + "conflict": { + "tracy/tracy": "<2.6" + }, + "require-dev": { + "latte/latte": "^2.2", + "nette/application": "^3.0", + "nette/caching": "^3.0", + "nette/database": "^3.0", + "nette/forms": "^3.0", + "nette/http": "^3.0", + "nette/mail": "^3.0", + "nette/robot-loader": "^3.0", + "nette/safe-stream": "^2.2", + "nette/security": "^3.0", + "nette/tester": "^2.0", + "tracy/tracy": "^2.6" + }, + "suggest": { + "nette/robot-loader": "to use Configurator::createRobotLoader()", + "tracy/tracy": "to use Configurator::enableTracy()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🅱 Nette Bootstrap: the simple way to configure and bootstrap your Nette application.", + "homepage": "https://nette.org", + "keywords": [ + "bootstrapping", + "configurator", + "nette" + ], + "time": "2019-09-30T08:19:38+00:00" + }, + { + "name": "nette/di", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/nette/di.git", + "reference": "7ae47daa94b8dafbd0e8b6164e22e2d18d3e73ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/di/zipball/7ae47daa94b8dafbd0e8b6164e22e2d18d3e73ac", + "reference": "7ae47daa94b8dafbd0e8b6164e22e2d18d3e73ac", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "nette/neon": "^3.0", + "nette/php-generator": "^3.3", + "nette/robot-loader": "^3.2", + "nette/schema": "^1.0", + "nette/utils": "^3.0", + "php": ">=7.1" + }, + "conflict": { + "nette/bootstrap": "<3.0" + }, + "require-dev": { + "nette/tester": "^2.2", + "phpstan/phpstan": "^0.12", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ], + "files": [ + "src/compatibility.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "💎 Nette Dependency Injection Container: Flexible, compiled and full-featured DIC with perfectly usable autowiring and support for all new PHP 7.1 features.", + "homepage": "https://nette.org", + "keywords": [ + "compiled", + "di", + "dic", + "factory", + "ioc", + "nette", + "static" + ], + "time": "2019-12-17T04:03:21+00:00" + }, + { + "name": "nette/finder", + "version": "v2.5.1", + "source": { + "type": "git", + "url": "https://github.com/nette/finder.git", + "reference": "14164e1ddd69e9c5f627ff82a10874b3f5bba5fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/finder/zipball/14164e1ddd69e9c5f627ff82a10874b3f5bba5fe", + "reference": "14164e1ddd69e9c5f627ff82a10874b3f5bba5fe", + "shasum": "" + }, + "require": { + "nette/utils": "^2.4 || ~3.0.0", + "php": ">=7.1" + }, + "conflict": { + "nette/nette": "<2.2" + }, + "require-dev": { + "nette/tester": "^2.0", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.5-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🔍 Nette Finder: find files and directories with an intuitive API.", + "homepage": "https://nette.org", + "keywords": [ + "filesystem", + "glob", + "iterator", + "nette" + ], + "time": "2019-07-11T18:02:17+00:00" + }, + { + "name": "nette/neon", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/nette/neon.git", + "reference": "0a18fc88801a14d66587932de133eeca01f7ce8e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/neon/zipball/0a18fc88801a14d66587932de133eeca01f7ce8e", + "reference": "0a18fc88801a14d66587932de133eeca01f7ce8e", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "ext-json": "*", + "php": ">=7.1" + }, + "require-dev": { + "nette/tester": "^2.0", + "phpstan/phpstan": "^0.12", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🍞 Nette NEON: encodes and decodes NEON file format.", + "homepage": "http://ne-on.org", + "keywords": [ + "export", + "import", + "neon", + "nette", + "yaml" + ], + "time": "2019-12-27T04:00:04+00:00" + }, + { + "name": "nette/php-generator", + "version": "v3.3.1", + "source": { + "type": "git", + "url": "https://github.com/nette/php-generator.git", + "reference": "4240fd7adf499138c07b814ef9b9a6df9f6d7187" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/php-generator/zipball/4240fd7adf499138c07b814ef9b9a6df9f6d7187", + "reference": "4240fd7adf499138c07b814ef9b9a6df9f6d7187", + "shasum": "" + }, + "require": { + "nette/utils": "^2.4.2 || ~3.0.0", + "php": ">=7.1" + }, + "require-dev": { + "nette/tester": "^2.0", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 7.3 features.", + "homepage": "https://nette.org", + "keywords": [ + "code", + "nette", + "php", + "scaffolding" + ], + "time": "2019-11-22T11:12:11+00:00" + }, + { + "name": "nette/robot-loader", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/nette/robot-loader.git", + "reference": "d2a100e1f5cab390c78bc88709abbc91249c3993" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/robot-loader/zipball/d2a100e1f5cab390c78bc88709abbc91249c3993", + "reference": "d2a100e1f5cab390c78bc88709abbc91249c3993", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "nette/finder": "^2.5 || ^3.0", + "nette/utils": "^3.0", + "php": ">=7.1" + }, + "require-dev": { + "nette/tester": "^2.0", + "phpstan/phpstan": "^0.12", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🍀 Nette RobotLoader: high performance and comfortable autoloader that will search and autoload classes within your application.", + "homepage": "https://nette.org", + "keywords": [ + "autoload", + "class", + "interface", + "nette", + "trait" + ], + "time": "2019-12-26T22:32:02+00:00" + }, + { + "name": "nette/schema", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "337117df1dade22e2ba1fdc4a4b832c1e9b06b76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/337117df1dade22e2ba1fdc4a4b832c1e9b06b76", + "reference": "337117df1dade22e2ba1fdc4a4b832c1e9b06b76", + "shasum": "" + }, + "require": { + "nette/utils": "^3.0.1", + "php": ">=7.1" + }, + "require-dev": { + "nette/tester": "^2.2", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "time": "2019-10-31T20:52:19+00:00" + }, + { + "name": "nette/utils", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "f1b5ce0fae07f13e066e64f9a8f59e53b8f4982e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/f1b5ce0fae07f13e066e64f9a8f59e53b8f4982e", + "reference": "f1b5ce0fae07f13e066e64f9a8f59e53b8f4982e", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "nette/tester": "~2.0", + "phpstan/phpstan": "^0.12", + "tracy/tracy": "^2.3" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize() and toAscii()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()", + "ext-xml": "to use Strings::length() etc. when mbstring is not available" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "time": "2019-12-27T03:47:50+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "9a9981c347c5c49d6dfe5cf826bb882b824080dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9a9981c347c5c49d6dfe5cf826bb882b824080dc", + "reference": "9a9981c347c5c49d6dfe5cf826bb882b824080dc", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "0.0.5", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.3-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "time": "2019-11-08T13:50:10+00:00" + }, + { + "name": "php-cs-fixer/diff", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/diff.git", + "reference": "78bb099e9c16361126c86ce82ec4405ebab8e756" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/78bb099e9c16361126c86ce82ec4405ebab8e756", + "reference": "78bb099e9c16361126c86ce82ec4405ebab8e756", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "symfony/process": "^3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "SpacePossum" + } + ], + "description": "sebastian/diff v2 backport support for PHP5.6", + "homepage": "https://github.com/PHP-CS-Fixer", + "keywords": [ + "diff" + ], + "time": "2018-02-15T16:58:55+00:00" + }, + { + "name": "php-http/mock-client", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/mock-client.git", + "reference": "186547be76fe81cc0c0589f9285ef9c8dded9845" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/mock-client/zipball/186547be76fe81cc0c0589f9285ef9c8dded9845", + "reference": "186547be76fe81cc0c0589f9285ef9c8dded9845", + "shasum": "" + }, + "require": { + "php": "^5.5 || ^7.0", + "php-http/client-common": "^1.9 || ^2.0", + "php-http/discovery": "^1.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0" + }, + "provide": { + "php-http/async-client-implementation": "1.0", + "php-http/client-implementation": "1.0" + }, + "require-dev": { + "henrikbjorn/phpspec-code-coverage": "^1.0", + "phpspec/phpspec": "^2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Mock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "David de Boer", + "email": "david@ddeboer.nl" + } + ], + "description": "Mock HTTP client", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http", + "mock", + "psr7" + ], + "time": "2019-11-06T12:49:04+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "0.3.5", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "8c4ef2aefd9788238897b678a985e1d5c8df6db4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/8c4ef2aefd9788238897b678a985e1d5c8df6db4", + "reference": "8c4ef2aefd9788238897b678a985e1d5c8df6db4", + "shasum": "" + }, + "require": { + "php": "~7.1" + }, + "require-dev": { + "consistence/coding-standard": "^3.5", + "jakub-onderka/php-parallel-lint": "^0.9.2", + "phing/phing": "^2.16.0", + "phpstan/phpstan": "^0.10", + "phpunit/phpunit": "^6.3", + "slevomat/coding-standard": "^4.7.2", + "squizlabs/php_codesniffer": "^3.3.2", + "symfony/process": "^3.4 || ^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.3-dev" + } + }, + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "time": "2019-06-07T19:13:52+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "0.11.19", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "63cc502f6957b7f74efbac444b4cf219dcadffd7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/63cc502f6957b7f74efbac444b4cf219dcadffd7", + "reference": "63cc502f6957b7f74efbac444b4cf219dcadffd7", + "shasum": "" + }, + "require": { + "composer/xdebug-handler": "^1.3.0", + "jean85/pretty-package-versions": "^1.0.3", + "nette/bootstrap": "^2.4 || ^3.0", + "nette/di": "^2.4.7 || ^3.0", + "nette/neon": "^2.4.3 || ^3.0", + "nette/robot-loader": "^3.0.1", + "nette/schema": "^1.0", + "nette/utils": "^2.4.5 || ^3.0", + "nikic/php-parser": "^4.2.3", + "php": "~7.1", + "phpstan/phpdoc-parser": "^0.3.5", + "symfony/console": "~3.2 || ~4.0", + "symfony/finder": "~3.2 || ~4.0" + }, + "conflict": { + "symfony/console": "3.4.16 || 4.1.5" + }, + "require-dev": { + "brianium/paratest": "^2.0 || ^3.0", + "consistence/coding-standard": "^3.5", + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", + "ext-intl": "*", + "ext-mysqli": "*", + "ext-simplexml": "*", + "ext-soap": "*", + "ext-zip": "*", + "jakub-onderka/php-parallel-lint": "^1.0", + "localheinz/composer-normalize": "^1.1.0", + "phing/phing": "^2.16.0", + "phpstan/phpstan-deprecation-rules": "^0.11", + "phpstan/phpstan-php-parser": "^0.11", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-strict-rules": "^0.11", + "phpunit/phpunit": "^7.5.14 || ^8.0", + "slevomat/coding-standard": "^4.7.2", + "squizlabs/php_codesniffer": "^3.3.2" + }, + "bin": [ + "bin/phpstan" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.11-dev" + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "time": "2019-10-22T20:20:22+00:00" + }, + { + "name": "phpstan/phpstan-doctrine", + "version": "0.11.6", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-doctrine.git", + "reference": "77592865e167b32c7dcb4f39a35210e909a8854c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/77592865e167b32c7dcb4f39a35210e909a8854c", + "reference": "77592865e167b32c7dcb4f39a35210e909a8854c", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.0", + "php": "~7.1", + "phpstan/phpdoc-parser": "^0.3", + "phpstan/phpstan": "^0.11.7" + }, + "conflict": { + "doctrine/collections": "<1.0", + "doctrine/common": "<2.7", + "doctrine/mongodb-odm": "<1.2", + "doctrine/orm": "<2.5" + }, + "require-dev": { + "consistence/coding-standard": "^3.8", + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", + "doctrine/collections": "^1.0", + "doctrine/common": "^2.7", + "doctrine/mongodb-odm": "^1.2", + "doctrine/orm": "^2.5", + "jakub-onderka/php-parallel-lint": "^1.0", + "phing/phing": "^2.16.0", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-strict-rules": "^0.11", + "phpunit/phpunit": "^7.0", + "slevomat/coding-standard": "^5.0.4" + }, + "type": "phpstan-extension", + "extra": { + "branch-alias": { + "dev-master": "0.11-dev" + }, + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Doctrine extensions for PHPStan", + "time": "2019-09-13T08:40:06+00:00" + }, + { + "name": "phpstan/phpstan-phpunit", + "version": "0.11.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-phpunit.git", + "reference": "fbf2ad56c3b13189d29655e226c9b1da47c2fad9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/fbf2ad56c3b13189d29655e226c9b1da47c2fad9", + "reference": "fbf2ad56c3b13189d29655e226c9b1da47c2fad9", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.0", + "php": "~7.1", + "phpstan/phpdoc-parser": "^0.3", + "phpstan/phpstan": "^0.11.4" + }, + "conflict": { + "phpunit/phpunit": "<7.0" + }, + "require-dev": { + "consistence/coding-standard": "^3.0.1", + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", + "jakub-onderka/php-parallel-lint": "^1.0", + "phing/phing": "^2.16.0", + "phpstan/phpstan-strict-rules": "^0.11", + "phpunit/phpunit": "^7.0", + "satooshi/php-coveralls": "^1.0", + "slevomat/coding-standard": "^4.5.2" + }, + "type": "phpstan-extension", + "extra": { + "branch-alias": { + "dev-master": "0.11-dev" + }, + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPUnit extensions and rules for PHPStan", + "time": "2019-05-17T17:50:16+00:00" + }, + { + "name": "phpstan/phpstan-symfony", + "version": "0.11.6", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-symfony.git", + "reference": "c7be3054c21fd472a52b1c38eb129c3f93776084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/c7be3054c21fd472a52b1c38eb129c3f93776084", + "reference": "c7be3054c21fd472a52b1c38eb129c3f93776084", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "nikic/php-parser": "^4.0", + "php": "^7.1", + "phpstan/phpstan": "^0.11.7" + }, + "conflict": { + "symfony/framework-bundle": "<3.0" + }, + "require-dev": { + "consistence/coding-standard": "^3.0.1", + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", + "jakub-onderka/php-parallel-lint": "^1.0", + "nette/di": "^3.0-stable", + "phing/phing": "^2.16.0", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-strict-rules": "^0.11", + "phpunit/phpunit": "^7.0", + "slevomat/coding-standard": "^4.5.2", + "squizlabs/php_codesniffer": "^3.3.2", + "symfony/console": "^3.0 || ^4.0", + "symfony/framework-bundle": "^3.0 || ^4.0", + "symfony/messenger": "^4.2", + "symfony/serializer": "^3.0 || ^4.0" + }, + "type": "phpstan-extension", + "extra": { + "branch-alias": { + "dev-master": "0.11-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lukáš Unger", + "email": "looky.msc@gmail.com", + "homepage": "https://lookyman.net" + } + ], + "description": "Symfony Framework extensions and rules for PHPStan", + "time": "2019-05-19T17:40:25+00:00" + }, + { + "name": "sensio/generator-bundle", + "version": "v3.1.7", + "source": { + "type": "git", + "url": "https://github.com/sensiolabs/SensioGeneratorBundle.git", + "reference": "28cbaa244bd0816fd8908b93f90380bcd7b67a65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sensiolabs/SensioGeneratorBundle/zipball/28cbaa244bd0816fd8908b93f90380bcd7b67a65", + "reference": "28cbaa244bd0816fd8908b93f90380bcd7b67a65", + "shasum": "" + }, + "require": { + "symfony/console": "~2.7|~3.0", + "symfony/framework-bundle": "~2.7|~3.0", + "symfony/process": "~2.7|~3.0", + "symfony/yaml": "~2.7|~3.0", + "twig/twig": "^1.28.2|^2.0" + }, + "require-dev": { + "doctrine/orm": "~2.4", + "symfony/doctrine-bridge": "~2.7|~3.0", + "symfony/filesystem": "~2.7|~3.0", + "symfony/phpunit-bridge": "^3.3" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sensio\\Bundle\\GeneratorBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "This bundle generates code for you", + "abandoned": "symfony/maker-bundle", + "time": "2017-12-07T15:36:41+00:00" + }, + { + "name": "symfony/phpunit-bridge", + "version": "v4.3.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/phpunit-bridge.git", + "reference": "692a73a2e0b76123150709f5eb7e2ea9bf2bad43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/692a73a2e0b76123150709f5eb7e2ea9bf2bad43", + "reference": "692a73a2e0b76123150709f5eb7e2ea9bf2bad43", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "conflict": { + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0" + }, + "suggest": { + "symfony/debug": "For tracking deprecated interfaces usages at runtime with DebugClassLoader" + }, + "bin": [ + "bin/simple-phpunit" + ], + "type": "symfony-bridge", + "extra": { + "branch-alias": { + "dev-master": "4.3-dev" + }, + "thanks": { + "name": "phpunit/phpunit", + "url": "https://github.com/sebastianbergmann/phpunit" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Bridge\\PhpUnit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony PHPUnit Bridge", + "homepage": "https://symfony.com", + "time": "2020-01-31T09:56:21+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": { + "grandt/phpepub": 20, + "predis/predis": 20 + }, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=7.1.3", + "ext-pcre": "*", + "ext-dom": "*", + "ext-curl": "*", + "ext-gd": "*", + "ext-session": "*", + "ext-ctype": "*", + "ext-hash": "*", + "ext-simplexml": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-iconv": "*", + "ext-tokenizer": "*", + "ext-pdo": "*", + "ext-tidy": "*" + }, + "platform-dev": [], + "platform-overrides": { + "php": "7.1.3" + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 3c28f2f1a..34eb09560 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,9 @@ version: '2' services: nginx: - image: nginx + image: nginx:alpine ports: - - "8080:80" + - "8000:80" volumes: - ./docker/nginx/nginx.conf:/nginx.conf - ./docker/logs/nginx:/var/log/nginx @@ -16,7 +16,7 @@ services: build: context: docker/php args: - # Set here your timezone using one of this: http://php.net/manual/en/timezones.php + # Set here your timezone using one of this: https://php.net/manual/en/timezones.php timezone: 'Europe/Monaco' ports: - "9000:9000" @@ -32,8 +32,8 @@ services: # - ./docker/postgres/env # - ./docker/mariadb/env - #postgres: - # image: postgres:9 + # postgres: + # image: postgres:11-alpine # ports: # - "5432:5432" # volumes: @@ -41,7 +41,7 @@ services: # env_file: # - ./docker/postgres/env - #mariadb: + # mariadb: # image: mariadb:10 # ports: # - "3306:3306" @@ -50,12 +50,12 @@ services: # env_file: # - ./docker/mariadb/env - rabbitmq: - image: rabbitmq:3-management - ports: - - "15672:15672" + # rabbitmq: + # image: rabbitmq:3-management-alpine + # ports: + # - "15672:15672" redis: - image: redis + image: redis:4-alpine ports: - "6379:6379" diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index d0266ec74..171e12fc9 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -1,15 +1,39 @@ -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' +ARG memorylimit='512M' RUN apt-get update && apt-get install -y \ - libmcrypt-dev libicu-dev libpq-dev libxml2-dev libpng-dev libjpeg-dev \ - && /usr/local/bin/docker-php-ext-configure gd --with-jpeg-dir=/usr/include \ - && docker-php-ext-install \ - iconv mbstring intl pdo pdo_mysql pdo_pgsql gd + libmcrypt-dev \ + libicu-dev \ + libpq-dev \ + libxml2-dev \ + libpng-dev \ + libjpeg-dev \ + libsqlite3-dev \ + imagemagick \ + libmagickwand-dev \ + libtidy-dev \ + git +RUN docker-php-ext-install \ + iconv \ + mbstring \ + gd \ + intl \ + pdo \ + pdo_mysql \ + pdo_pgsql \ + pdo_sqlite \ + sockets \ + tidy \ + bcmath \ + zip + +RUN printf "\n" | pecl install imagick && docker-php-ext-enable imagick RUN echo "date.timezone="$timezone > /usr/local/etc/php/conf.d/date_timezone.ini +RUN echo "memory_limit ="$memorylimit > /usr/local/etc/php/conf.d/memory_limit.ini RUN usermod -u 1000 www-data diff --git a/docker/postgres/env b/docker/postgres/env index 80c78c2af..0dd1adb33 100644 --- a/docker/postgres/env +++ b/docker/postgres/env @@ -1,9 +1,9 @@ POSTGRES_USER=wallabag POSTGRES_PASSWORD=wallapass POSTGRES_DB=wallabag -export SYMFONY__ENV__DATABASE_DRIVER=pdo_pgsql -export SYMFONY__ENV__DATABASE_HOST=rdbms -export SYMFONY__ENV__DATABASE_PORT=5432 -export SYMFONY__ENV__DATABASE_NAME=wallabag -export SYMFONY__ENV__DATABASE_USER=wallabag -export SYMFONY__ENV__DATABASE_PASSWORD=wallapass +SYMFONY__ENV__DATABASE_HOST=rdbms +SYMFONY__ENV__DATABASE_PORT=5432 +SYMFONY__ENV__DATABASE_NAME=wallabag +SYMFONY__ENV__DATABASE_USER=wallabag +SYMFONY__ENV__DATABASE_PASSWORD=wallapass +SYMFONY__ENV__DATABASE_DRIVER=pdo_pgsql diff --git a/package.json b/package.json index ac894e797..7942eb5a1 100644 --- a/package.json +++ b/package.json @@ -64,14 +64,16 @@ }, "dependencies": { "annotator": "git://github.com/wallabag/annotator.git#0f076c7d371ed25eb0793346f46982d90f2c4c85", + "clipboard": "^2.0.4", "hammerjs": "^2.0.8", "highlight.js": "^9.12.0", "icomoon-free-npm": "^0.0.0", "jquery": "^2.1.4", "jquery.cookie": "^1.4.1", "jr-qrcode": "^1.0.7", - "material-design-icons-iconfont": "^3.0.3", + "material-design-icons-iconfont": "^5.0.1", "materialize-css": "^0.98.1", + "mathjax": "^3.0.0", "mousetrap": "^1.6.0", "ptsans-npm-webfont": "^0.0.4", "roboto-fontface": "^0.7.0", diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 000000000..d170e0e6f --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,15 @@ +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-7.4/vendor/autoload.php + + inferPrivatePropertyTypeFromConstructor: true diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 951b5a145..426a5e720 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/scripts/dev.sh b/scripts/dev.sh index 0703ced1e..cc465cf98 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -3,11 +3,18 @@ # eg: `sh dev.sh` COMPOSER_COMMAND='composer' +REQUIRE_FILE='scripts/require.sh' -DIR="${BASH_SOURCE}" -if [ ! -d "$DIR" ]; then DIR="$PWD/scripts"; fi -. "$DIR/require.sh" +if [ ! -f "$REQUIRE_FILE" ]; then + echo "Cannot find $REQUIRE_FILE" + exit 1 +fi + +. "$REQUIRE_FILE" $COMPOSER_COMMAND install -php bin/console wallabag:install -php bin/console server:run +if [ -z "$SKIP_WALLABAG_INITIALIZATION" ] +then + php bin/console wallabag:install +fi +php bin/console server:run $HOST diff --git a/scripts/install.sh b/scripts/install.sh index 8b7ea03f5..affa715ff 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -5,9 +5,17 @@ IGNORE_ROOT_ARG="--ignore-root-warning" IGNORE_ROOT=0 -if [ "$1" == "$IGNORE_ROOT_ARG" ]; then - IGNORE_ROOT=1 -fi +while :; do + case $1 in + $IGNORE_ROOT_ARG) IGNORE_ROOT=1 + ;; + *[a-zA-Z]) ENV=$1 + ;; + *) break + ;; + esac + shift +done # Abort running this script if root if [ "$IGNORE_ROOT" -eq 0 ] && [ "$EUID" == "0" ]; then @@ -17,12 +25,15 @@ if [ "$IGNORE_ROOT" -eq 0 ] && [ "$EUID" == "0" ]; then fi COMPOSER_COMMAND='composer' +REQUIRE_FILE='scripts/require.sh' -DIR="${BASH_SOURCE}" -if [ ! -d "$DIR" ]; then DIR="$PWD/scripts"; fi -. "$DIR/require.sh" +if [ ! -f "$REQUIRE_FILE" ]; then + echo "Cannot find $REQUIRE_FILE" + exit 1 +fi + +. "$REQUIRE_FILE" -ENV=$1 TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) git checkout $TAG diff --git a/scripts/release.sh b/scripts/release.sh index ee08a688f..e697e2200 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -9,8 +9,8 @@ ENV=$4 rm -rf $TMP_FOLDER/$RELEASE_FOLDER mkdir $TMP_FOLDER/$RELEASE_FOLDER -git clone git@github.com:wallabag/wallabag.git -b release-$VERSION $TMP_FOLDER/$RELEASE_FOLDER/$VERSION -cd $TMP_FOLDER/$RELEASE_FOLDER/$VERSION && SYMFONY_ENV=$ENV COMPOSER_MEMORY_LIMIT=-1 composer up -n --no-dev +git clone git@github.com:wallabag/wallabag.git $TMP_FOLDER/$RELEASE_FOLDER/$VERSION +cd $TMP_FOLDER/$RELEASE_FOLDER/$VERSION && SYMFONY_ENV=$ENV COMPOSER_MEMORY_LIMIT=-1 composer install -n --no-dev cd $TMP_FOLDER/$RELEASE_FOLDER/$VERSION && php bin/console wallabag:install --env=$ENV -n cd $TMP_FOLDER/$RELEASE_FOLDER/$VERSION && php bin/console assets:install --env=$ENV --symlink --relative cd $TMP_FOLDER/$RELEASE_FOLDER && tar czf wallabag-$VERSION.tar.gz --exclude="var/cache/*" --exclude="var/logs/*" --exclude="var/sessions/*" --exclude=".git" $VERSION diff --git a/scripts/require.sh b/scripts/require.sh index c48ab9368..63c381329 100755 --- a/scripts/require.sh +++ b/scripts/require.sh @@ -1,6 +1,6 @@ #! /usr/bin/env bash -# File used to check dependencies +# Check for composer if [ ! -f composer.phar ]; then echo "composer.phar not found, we'll see if composer is installed globally." command -v composer >/dev/null 2>&1 || { echo >&2 "wallabag requires composer but it's not installed (see http://doc.wallabag.org/en/master/user/installation.html). Aborting."; exit 1; } diff --git a/scripts/update.sh b/scripts/update.sh index c62d104a3..1f31d429c 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -5,9 +5,17 @@ IGNORE_ROOT_ARG="--ignore-root-warning" IGNORE_ROOT=0 -if [ "$1" == "$IGNORE_ROOT_ARG" ]; then - IGNORE_ROOT=1 -fi +while :; do + case $1 in + $IGNORE_ROOT_ARG) IGNORE_ROOT=1 + ;; + *[a-zA-Z]) ENV=$1 + ;; + *) break + ;; + esac + shift +done # Abort running this script if root if [ "$IGNORE_ROOT" -eq 0 ] && [ "$EUID" == "0" ]; then @@ -20,12 +28,20 @@ set -e set -u COMPOSER_COMMAND='composer' +REQUIRE_FILE='scripts/require.sh' -DIR="${BASH_SOURCE}" -if [ ! -d "$DIR" ]; then DIR="$PWD/scripts"; fi -. "$DIR/require.sh" +if [ ! -f "$REQUIRE_FILE" ]; then + echo "Cannot find $REQUIRE_FILE" + exit 1 +fi -ENV=$1 +. "$REQUIRE_FILE" + +# Check for wallabag .git folder +if [ ! -d .git ]; then + echo "Can not update because wallabag wasn't installed using git (see https://doc.wallabag.org/en/admin/upgrade.html#upgrade-on-a-shared-hosting). Aborting."; + exit 2; +fi rm -rf var/cache/* git fetch origin diff --git a/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php b/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php index 3a7421c74..883ce4a89 100644 --- a/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php +++ b/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php @@ -16,8 +16,6 @@ class WallabagAnnotationController extends FOSRestController /** * Retrieve annotations for an entry. * - * @param Entry $entry - * * @see Wallabag\ApiBundle\Controller\WallabagRestController * * @return JsonResponse @@ -39,9 +37,6 @@ class WallabagAnnotationController extends FOSRestController /** * Creates a new annotation. * - * @param Request $request - * @param Entry $entry - * * @return JsonResponse * * @see Wallabag\ApiBundle\Controller\WallabagRestController @@ -79,9 +74,6 @@ class WallabagAnnotationController extends FOSRestController * * @ParamConverter("annotation", class="WallabagAnnotationBundle:Annotation") * - * @param Annotation $annotation - * @param Request $request - * * @return JsonResponse */ public function putAnnotationAction(Annotation $annotation, Request $request) @@ -114,8 +106,6 @@ class WallabagAnnotationController extends FOSRestController * * @ParamConverter("annotation", class="WallabagAnnotationBundle:Annotation") * - * @param Annotation $annotation - * * @return JsonResponse */ public function deleteAnnotationAction(Annotation $annotation) 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 20e07fa31..ed46cea9b 100644 --- a/src/Wallabag/AnnotationBundle/DataFixtures/ORM/LoadAnnotationData.php +++ b/src/Wallabag/AnnotationBundle/DataFixtures/AnnotationFixtures.php @@ -1,13 +1,15 @@ '', ]) ->add('quote', null, [ - 'empty_data' => null, + 'empty_data' => '', + 'trim' => false, ]) ->add('ranges', CollectionType::class, [ 'entry_type' => RangeType::class, diff --git a/src/Wallabag/ApiBundle/Controller/AnnotationRestController.php b/src/Wallabag/ApiBundle/Controller/AnnotationRestController.php index 28d55ba90..66693189a 100644 --- a/src/Wallabag/ApiBundle/Controller/AnnotationRestController.php +++ b/src/Wallabag/ApiBundle/Controller/AnnotationRestController.php @@ -20,8 +20,6 @@ class AnnotationRestController extends WallabagRestController * } * ) * - * @param Entry $entry - * * @return JsonResponse */ public function getAnnotationsAction(Entry $entry) @@ -39,14 +37,11 @@ class AnnotationRestController extends WallabagRestController * @ApiDoc( * requirements={ * {"name"="ranges", "dataType"="array", "requirement"="\w+", "description"="The range array for the annotation"}, - * {"name"="quote", "dataType"="string", "required"=false, "description"="Optional, quote for the annotation"}, - * {"name"="text", "dataType"="string", "required"=true, "description"=""}, + * {"name"="quote", "dataType"="string", "description"="The annotated text"}, + * {"name"="text", "dataType"="string", "required"=true, "description"="Content of annotation"}, * } * ) * - * @param Request $request - * @param Entry $entry - * * @return JsonResponse */ public function postAnnotationAction(Request $request, Entry $entry) @@ -70,9 +65,6 @@ class AnnotationRestController extends WallabagRestController * * @ParamConverter("annotation", class="WallabagAnnotationBundle:Annotation") * - * @param Annotation $annotation - * @param Request $request - * * @return JsonResponse */ public function putAnnotationAction(Annotation $annotation, Request $request) @@ -96,8 +88,6 @@ class AnnotationRestController extends WallabagRestController * * @ParamConverter("annotation", class="WallabagAnnotationBundle:Annotation") * - * @param Annotation $annotation - * * @return JsonResponse */ public function deleteAnnotationAction(Annotation $annotation) diff --git a/src/Wallabag/ApiBundle/Controller/DeveloperController.php b/src/Wallabag/ApiBundle/Controller/DeveloperController.php index c7178017e..3224d7893 100644 --- a/src/Wallabag/ApiBundle/Controller/DeveloperController.php +++ b/src/Wallabag/ApiBundle/Controller/DeveloperController.php @@ -2,9 +2,9 @@ namespace Wallabag\ApiBundle\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\ApiBundle\Entity\Client; use Wallabag\ApiBundle\Form\Type\ClientType; @@ -29,8 +29,6 @@ class DeveloperController extends Controller /** * Create a client (an app). * - * @param Request $request - * * @Route("/developer/client/create", name="developer_create_client") * * @return \Symfony\Component\HttpFoundation\Response @@ -67,8 +65,6 @@ class DeveloperController extends Controller /** * Remove a client. * - * @param Client $client - * * @Route("/developer/client/delete/{id}", requirements={"id" = "\d+"}, name="developer_delete_client") * * @return \Symfony\Component\HttpFoundation\RedirectResponse diff --git a/src/Wallabag/ApiBundle/Controller/EntryRestController.php b/src/Wallabag/ApiBundle/Controller/EntryRestController.php index 0b4e74a0f..c09fdaebb 100644 --- a/src/Wallabag/ApiBundle/Controller/EntryRestController.php +++ b/src/Wallabag/ApiBundle/Controller/EntryRestController.php @@ -4,17 +4,17 @@ namespace Wallabag\ApiBundle\Controller; use Hateoas\Configuration\Route; use Hateoas\Representation\Factory\PagerfantaFactory; -use JMS\Serializer\SerializationContext; use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Wallabag\CoreBundle\Entity\Entry; use Wallabag\CoreBundle\Entity\Tag; use Wallabag\CoreBundle\Event\EntryDeletedEvent; use Wallabag\CoreBundle\Event\EntrySavedEvent; +use Wallabag\CoreBundle\Helper\UrlHasher; class EntryRestController extends WallabagRestController { @@ -28,8 +28,10 @@ class EntryRestController extends WallabagRestController * @ApiDoc( * parameters={ * {"name"="return_id", "dataType"="string", "required"=false, "format"="1 or 0", "description"="Set 1 if you want to retrieve ID in case entry(ies) exists, 0 by default"}, - * {"name"="url", "dataType"="string", "required"=true, "format"="An url", "description"="Url to check if it exists"}, - * {"name"="urls", "dataType"="string", "required"=false, "format"="An array of urls (?urls[]=http...&urls[]=http...)", "description"="Urls (as an array) to check if it exists"} + * {"name"="url", "dataType"="string", "required"=true, "format"="An url", "description"="DEPRECATED, use hashed_url instead"}, + * {"name"="urls", "dataType"="string", "required"=false, "format"="An array of urls (?urls[]=http...&urls[]=http...)", "description"="DEPRECATED, use hashed_urls instead"}, + * {"name"="hashed_url", "dataType"="string", "required"=false, "format"="A hashed url", "description"="Hashed url using SHA1 to check if it exists"}, + * {"name"="hashed_urls", "dataType"="string", "required"=false, "format"="An array of hashed urls (?hashed_urls[]=xxx...&hashed_urls[]=xxx...)", "description"="An array of hashed urls using SHA1 to check if they exist"} * } * ) * @@ -38,38 +40,49 @@ class EntryRestController extends WallabagRestController public function getEntriesExistsAction(Request $request) { $this->validateAuthentication(); + $repo = $this->getDoctrine()->getRepository('WallabagCoreBundle:Entry'); $returnId = (null === $request->query->get('return_id')) ? false : (bool) $request->query->get('return_id'); - $urls = $request->query->get('urls', []); - // handle multiple urls first - if (!empty($urls)) { - $results = []; - foreach ($urls as $url) { - $res = $this->getDoctrine() - ->getRepository('WallabagCoreBundle:Entry') - ->findByUrlAndUserId($url, $this->getUser()->getId()); - - $results[$url] = $this->returnExistInformation($res, $returnId); - } - - return $this->sendResponse($results); + $hashedUrls = $request->query->get('hashed_urls', []); + $hashedUrl = $request->query->get('hashed_url', ''); + if (!empty($hashedUrl)) { + $hashedUrls[] = $hashedUrl; } - // let's see if it is a simple url? + $urls = $request->query->get('urls', []); $url = $request->query->get('url', ''); + if (!empty($url)) { + $urls[] = $url; + } - if (empty($url)) { + $urlHashMap = []; + foreach ($urls as $urlToHash) { + $urlHash = UrlHasher::hashUrl($urlToHash); + $hashedUrls[] = $urlHash; + $urlHashMap[$urlHash] = $urlToHash; + } + + if (empty($hashedUrls)) { throw $this->createAccessDeniedException('URL is empty?, logged user id: ' . $this->getUser()->getId()); } - $res = $this->getDoctrine() - ->getRepository('WallabagCoreBundle:Entry') - ->findByUrlAndUserId($url, $this->getUser()->getId()); + $results = []; + foreach ($hashedUrls as $hashedUrlToSearch) { + $res = $repo->findByHashedUrlAndUserId($hashedUrlToSearch, $this->getUser()->getId()); - $exists = $this->returnExistInformation($res, $returnId); + $results[$hashedUrlToSearch] = $this->returnExistInformation($res, $returnId); + } - return $this->sendResponse(['exists' => $exists]); + $results = $this->replaceUrlHashes($results, $urlHashMap); + + if (!empty($url) || !empty($hashedUrl)) { + $hu = array_keys($results)[0]; + + return $this->sendResponse(['exists' => $results[$hu]]); + } + + return $this->sendResponse($results); } /** @@ -79,13 +92,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."}, * } * ) * @@ -98,24 +112,30 @@ class EntryRestController extends WallabagRestController $isArchived = (null === $request->query->get('archive')) ? null : (bool) $request->query->get('archive'); $isStarred = (null === $request->query->get('starred')) ? null : (bool) $request->query->get('starred'); $isPublic = (null === $request->query->get('public')) ? null : (bool) $request->query->get('public'); - $sort = $request->query->get('sort', 'created'); - $order = $request->query->get('order', 'desc'); + $sort = strtolower($request->query->get('sort', 'created')); + $order = strtolower($request->query->get('order', 'desc')); $page = (int) $request->query->get('page', 1); $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')); - /** @var \Pagerfanta\Pagerfanta $pager */ - $pager = $this->get('wallabag_core.entry_repository')->findEntries( - $this->getUser()->getId(), - $isArchived, - $isStarred, - $isPublic, - $sort, - $order, - $since, - $tags - ); + try { + /** @var \Pagerfanta\Pagerfanta $pager */ + $pager = $this->get('wallabag_core.entry_repository')->findEntries( + $this->getUser()->getId(), + $isArchived, + $isStarred, + $isPublic, + $sort, + $order, + $since, + $tags, + $detail + ); + } catch (\Exception $e) { + throw new BadRequestHttpException($e->getMessage()); + } $pager->setMaxPerPage($perPage); $pager->setCurrentPage($page); @@ -135,8 +155,9 @@ class EntryRestController extends WallabagRestController 'perPage' => $perPage, 'tags' => $tags, 'since' => $since, + 'detail' => $detail, ], - UrlGeneratorInterface::ABSOLUTE_URL + true ) ); @@ -344,9 +365,7 @@ class EntryRestController extends WallabagRestController 'language' => !empty($data['language']) ? $data['language'] : $entry->getLanguage(), 'date' => !empty($data['publishedAt']) ? $data['publishedAt'] : $entry->getPublishedAt(), // faking the open graph preview picture - 'open_graph' => [ - 'og_image' => !empty($data['picture']) ? $data['picture'] : $entry->getPreviewPicture(), - ], + 'image' => !empty($data['picture']) ? $data['picture'] : $entry->getPreviewPicture(), 'authors' => \is_string($data['authors']) ? explode(',', $data['authors']) : $entry->getPublishedBy(), ] ); @@ -358,7 +377,7 @@ class EntryRestController extends WallabagRestController } if (null !== $data['isArchived']) { - $entry->setArchived((bool) $data['isArchived']); + $entry->updateArchived((bool) $data['isArchived']); } if (null !== $data['isStarred']) { @@ -474,7 +493,7 @@ class EntryRestController extends WallabagRestController } if (null !== $data['isArchived']) { - $entry->setArchived((bool) $data['isArchived']); + $entry->updateArchived((bool) $data['isArchived']); } if (null !== $data['isStarred']) { @@ -545,7 +564,7 @@ class EntryRestController extends WallabagRestController } // if refreshing entry failed, don't save it - if ($this->getParameter('wallabag_core.fetching_error_message') === $entry->getContent()) { + if ($this->container->getParameter('wallabag_core.fetching_error_message') === $entry->getContent()) { return new JsonResponse([], 304); } @@ -565,18 +584,31 @@ class EntryRestController extends WallabagRestController * @ApiDoc( * requirements={ * {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"} + * }, + * parameters={ + * {"name"="expect", "dataType"="string", "required"=false, "format"="id or entry", "description"="Only returns the id instead of the deleted entry's full entity if 'id' is specified. Default to entry"}, * } * ) * * @return JsonResponse */ - public function deleteEntriesAction(Entry $entry) + public function deleteEntriesAction(Entry $entry, Request $request) { + $expect = $request->query->get('expect', 'entry'); + if (!\in_array($expect, ['id', 'entry'], true)) { + throw new BadRequestHttpException(sprintf("expect: 'id' or 'entry' expected, %s given", $expect)); + } $this->validateAuthentication(); $this->validateUserAccess($entry->getUser()->getId()); - // We copy $entry to keep id in returned object - $e = $entry; + $response = $this->sendResponse([ + 'id' => $entry->getId(), + ]); + // We clone $entry to keep id in returned object + if ('entry' === $expect) { + $e = clone $entry; + $response = $this->sendResponse($e); + } $em = $this->getDoctrine()->getManager(); $em->remove($entry); @@ -585,7 +617,7 @@ class EntryRestController extends WallabagRestController // entry deleted, dispatch event about it! $this->get('event_dispatcher')->dispatch(EntryDeletedEvent::NAME, new EntryDeletedEvent($entry)); - return $this->sendResponse($e); + return $response; } /** @@ -769,29 +801,27 @@ class EntryRestController extends WallabagRestController } /** - * Shortcut to send data serialized in json. - * - * @param mixed $data - * - * @return JsonResponse + * Replace the hashedUrl keys in $results with the unhashed URL from the + * request, as recorded in $urlHashMap. */ - private function sendResponse($data) + private function replaceUrlHashes(array $results, array $urlHashMap) { - // https://github.com/schmittjoh/JMSSerializerBundle/issues/293 - $context = new SerializationContext(); - $context->setSerializeNull(true); + $newResults = []; + foreach ($results as $hash => $res) { + if (isset($urlHashMap[$hash])) { + $newResults[$urlHashMap[$hash]] = $res; + } else { + $newResults[$hash] = $res; + } + } - $json = $this->get('jms_serializer')->serialize($data, 'json', $context); - - return (new JsonResponse())->setJson($json); + return $newResults; } /** * Retrieve value from the request. * Used for POST & PATCH on a an entry. * - * @param Request $request - * * @return array */ private function retrieveValueFromRequest(Request $request) @@ -814,8 +844,8 @@ class EntryRestController extends WallabagRestController /** * Return information about the entry if it exist and depending on the id or not. * - * @param Entry|null $entry - * @param bool $returnId + * @param Entry|bool|null $entry + * @param bool $returnId * * @return bool|int */ diff --git a/src/Wallabag/ApiBundle/Controller/SearchRestController.php b/src/Wallabag/ApiBundle/Controller/SearchRestController.php new file mode 100644 index 000000000..d9f99844c --- /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/TagRestController.php b/src/Wallabag/ApiBundle/Controller/TagRestController.php index c6d6df6a9..f3498f55a 100644 --- a/src/Wallabag/ApiBundle/Controller/TagRestController.php +++ b/src/Wallabag/ApiBundle/Controller/TagRestController.php @@ -46,12 +46,14 @@ class TagRestController extends WallabagRestController $this->validateAuthentication(); $label = $request->get('tag', ''); - $tag = $this->getDoctrine()->getRepository('WallabagCoreBundle:Tag')->findOneByLabel($label); + $tags = $this->getDoctrine()->getRepository('WallabagCoreBundle:Tag')->findByLabelsAndUser([$label], $this->getUser()->getId()); - if (empty($tag)) { + if (empty($tags)) { throw $this->createNotFoundException('Tag not found'); } + $tag = $tags[0]; + $this->getDoctrine() ->getRepository('WallabagCoreBundle:Entry') ->removeTag($this->getUser()->getId(), $tag); @@ -80,15 +82,7 @@ class TagRestController extends WallabagRestController $tagsLabels = $request->get('tags', ''); - $tags = []; - - foreach (explode(',', $tagsLabels) as $tagLabel) { - $tagEntity = $this->getDoctrine()->getRepository('WallabagCoreBundle:Tag')->findOneByLabel($tagLabel); - - if (!empty($tagEntity)) { - $tags[] = $tagEntity; - } - } + $tags = $this->getDoctrine()->getRepository('WallabagCoreBundle:Tag')->findByLabelsAndUser(explode(',', $tagsLabels), $this->getUser()->getId()); if (empty($tags)) { throw $this->createNotFoundException('Tags not found'); @@ -120,6 +114,12 @@ class TagRestController extends WallabagRestController { $this->validateAuthentication(); + $tagFromDb = $this->getDoctrine()->getRepository('WallabagCoreBundle:Tag')->findByLabelsAndUser([$tag->getLabel()], $this->getUser()->getId()); + + if (empty($tagFromDb)) { + throw $this->createNotFoundException('Tag not found'); + } + $this->getDoctrine() ->getRepository('WallabagCoreBundle:Entry') ->removeTag($this->getUser()->getId(), $tag); diff --git a/src/Wallabag/ApiBundle/Controller/TaggingRuleRestController.php b/src/Wallabag/ApiBundle/Controller/TaggingRuleRestController.php new file mode 100644 index 000000000..2496298aa --- /dev/null +++ b/src/Wallabag/ApiBundle/Controller/TaggingRuleRestController.php @@ -0,0 +1,39 @@ +validateAuthentication(); + + $data = SerializerBuilder::create()->build()->serialize( + $this->getUser()->getConfig()->getTaggingRules(), + 'json', + SerializationContext::create()->setGroups(['export_tagging_rule']) + ); + + return Response::create( + $data, + 200, + [ + 'Content-type' => 'application/json', + 'Content-Disposition' => 'attachment; filename="tagging_rules_' . $this->getUser()->getUsername() . '.json"', + 'Content-Transfer-Encoding' => 'UTF-8', + ] + ); + } +} diff --git a/src/Wallabag/ApiBundle/Controller/UserRestController.php b/src/Wallabag/ApiBundle/Controller/UserRestController.php index 3a4dafcd0..922ab7bbb 100644 --- a/src/Wallabag/ApiBundle/Controller/UserRestController.php +++ b/src/Wallabag/ApiBundle/Controller/UserRestController.php @@ -45,7 +45,7 @@ class UserRestController extends WallabagRestController */ public function putUserAction(Request $request) { - if (!$this->getParameter('fosuser_registration') || !$this->get('craue_config')->get('api_user_registration')) { + if (!$this->container->getParameter('fosuser_registration') || !$this->get('craue_config')->get('api_user_registration')) { $json = $this->get('jms_serializer')->serialize(['error' => "Server doesn't allow registrations"], 'json'); return (new JsonResponse()) @@ -119,7 +119,6 @@ class UserRestController extends WallabagRestController /** * Send user response. * - * @param User $user * @param string $group Used to define with serialized group might be used * @param int $status HTTP Status code to send * diff --git a/src/Wallabag/ApiBundle/Controller/WallabagRestController.php b/src/Wallabag/ApiBundle/Controller/WallabagRestController.php index 7d8cfbba2..44fd96835 100644 --- a/src/Wallabag/ApiBundle/Controller/WallabagRestController.php +++ b/src/Wallabag/ApiBundle/Controller/WallabagRestController.php @@ -2,18 +2,21 @@ namespace Wallabag\ApiBundle\Controller; -use FOS\RestBundle\Controller\FOSRestController; +use FOS\RestBundle\Controller\AbstractFOSRestController; +use JMS\Serializer\SerializationContext; use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Security\Core\Exception\AccessDeniedException; -class WallabagRestController extends FOSRestController +class WallabagRestController extends AbstractFOSRestController { /** * Retrieve version number. * * @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 c09a0c803..98e0af3e1 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 4d4b09fea..7c9c85396 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/Client.php b/src/Wallabag/ApiBundle/Entity/Client.php index e6f98f98c..78349820a 100644 --- a/src/Wallabag/ApiBundle/Entity/Client.php +++ b/src/Wallabag/ApiBundle/Entity/Client.php @@ -11,7 +11,7 @@ use Wallabag\UserBundle\Entity\User; /** * @ORM\Table("oauth2_clients") - * @ORM\Entity + * @ORM\Entity(repositoryClass="Wallabag\ApiBundle\Repository\ClientRepository") */ class Client extends BaseClient { diff --git a/src/Wallabag/ApiBundle/Entity/RefreshToken.php b/src/Wallabag/ApiBundle/Entity/RefreshToken.php index 822a02d8d..55a507e13 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 fc22538f4..14dc5c44f 100644 --- a/src/Wallabag/ApiBundle/Form/Type/ClientType.php +++ b/src/Wallabag/ApiBundle/Form/Type/ClientType.php @@ -20,6 +20,7 @@ class ClientType extends AbstractType 'required' => false, 'label' => 'developer.client.form.redirect_uris_label', 'property_path' => 'redirectUris', + 'default_protocol' => null, ]) ->add('save', SubmitType::class, ['label' => 'developer.client.form.save_label']) ; diff --git a/src/Wallabag/ApiBundle/Repository/ClientRepository.php b/src/Wallabag/ApiBundle/Repository/ClientRepository.php new file mode 100644 index 000000000..fc14262e2 --- /dev/null +++ b/src/Wallabag/ApiBundle/Repository/ClientRepository.php @@ -0,0 +1,19 @@ +getContainer()->get('doctrine.orm.entity_manager'); diff --git a/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php b/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php new file mode 100644 index 000000000..a0e9221e3 --- /dev/null +++ b/src/Wallabag/CoreBundle/Command/GenerateUrlHashesCommand.php @@ -0,0 +1,96 @@ +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; + } + + 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(UrlHasher::hashUrl($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 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 3c76545cd..3aa332f13 100644 --- a/src/Wallabag/CoreBundle/Command/InstallCommand.php +++ b/src/Wallabag/CoreBundle/Command/InstallCommand.php @@ -2,7 +2,6 @@ namespace Wallabag\CoreBundle\Command; -use Craue\ConfigBundle\Entity\Setting; use FOS\UserBundle\Event\UserEvent; use FOS\UserBundle\FOSUserEvents; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; @@ -13,6 +12,7 @@ use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Style\SymfonyStyle; +use Wallabag\CoreBundle\Entity\InternalSetting; class InstallCommand extends ContainerAwareCommand { @@ -54,7 +54,7 @@ class InstallCommand extends ContainerAwareCommand $this->io = new SymfonyStyle($input, $output); - $this->io->title('Wallabag installer'); + $this->io->title('wallabag installer'); $this ->checkRequirements() @@ -63,7 +63,7 @@ class InstallCommand extends ContainerAwareCommand ->setupConfig() ; - $this->io->success('Wallabag has been successfully installed.'); + $this->io->success('wallabag has been successfully installed.'); $this->io->success('You can now configure your web server, see https://doc.wallabag.org'); } @@ -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'); @@ -275,10 +276,10 @@ class InstallCommand extends ContainerAwareCommand $em = $this->getContainer()->get('doctrine.orm.entity_manager'); // cleanup before insert new stuff - $em->createQuery('DELETE FROM CraueConfigBundle:Setting')->execute(); + $em->createQuery('DELETE FROM WallabagCoreBundle:InternalSetting')->execute(); foreach ($this->getContainer()->getParameter('wallabag_core.default_internal_settings') as $setting) { - $newSetting = new Setting(); + $newSetting = new InternalSetting(); $newSetting->setName($setting['name']); $newSetting->setValue($setting['value']); $newSetting->setSection($setting['section']); @@ -325,9 +326,7 @@ class InstallCommand extends ContainerAwareCommand if (0 !== $exitCode) { $this->getApplication()->setAutoExit(true); - throw new \RuntimeException( - 'The command "' . $command . "\" generates some errors: \n\n" - . $output->fetch()); + throw new \RuntimeException('The command "' . $command . "\" generates some errors: \n\n" . $output->fetch()); } return $this; diff --git a/src/Wallabag/CoreBundle/Command/ShowUserCommand.php b/src/Wallabag/CoreBundle/Command/ShowUserCommand.php index a0184267e..87bccf718 100644 --- a/src/Wallabag/CoreBundle/Command/ShowUserCommand.php +++ b/src/Wallabag/CoreBundle/Command/ShowUserCommand.php @@ -46,9 +46,6 @@ class ShowUserCommand extends ContainerAwareCommand return 0; } - /** - * @param User $user - */ private function showUser(User $user) { $this->io->listing([ @@ -57,7 +54,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 b999c539f..6655ef93d 100644 --- a/src/Wallabag/CoreBundle/Controller/ConfigController.php +++ b/src/Wallabag/CoreBundle/Controller/ConfigController.php @@ -2,17 +2,23 @@ namespace Wallabag\CoreBundle\Controller; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; +use JMS\Serializer\SerializationContext; +use JMS\Serializer\SerializerBuilder; +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\HttpFoundation\Response; 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\TaggingRuleImportType; use Wallabag\CoreBundle\Form\Type\TaggingRuleType; use Wallabag\CoreBundle\Form\Type\UserInformationType; use Wallabag\CoreBundle\Tools\Utils; @@ -20,8 +26,6 @@ use Wallabag\CoreBundle\Tools\Utils; class ConfigController extends Controller { /** - * @param Request $request - * * @Route("/config", name="config") */ public function indexAction(Request $request) @@ -45,7 +49,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 +71,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 +86,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 +94,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 +134,7 @@ class ConfigController extends Controller $em->persist($taggingRule); $em->flush(); - $this->get('session')->getFlashBag()->add( + $this->addFlash( 'notice', 'flashes.config.notice.tagging_rules_updated' ); @@ -138,28 +142,168 @@ class ConfigController extends Controller return $this->redirect($this->generateUrl('config') . '#set5'); } + // handle tagging rules import + $taggingRulesImportform = $this->createForm(TaggingRuleImportType::class); + $taggingRulesImportform->handleRequest($request); + + if ($taggingRulesImportform->isSubmitted() && $taggingRulesImportform->isValid()) { + $message = 'flashes.config.notice.tagging_rules_not_imported'; + $file = $taggingRulesImportform->get('file')->getData(); + + if (null !== $file && $file->isValid() && \in_array($file->getClientMimeType(), ['application/json', 'application/octet-stream'], true)) { + $content = json_decode(file_get_contents($file->getPathname()), true); + + if (\is_array($content)) { + foreach ($content as $rule) { + $taggingRule = new TaggingRule(); + $taggingRule->setRule($rule['rule']); + $taggingRule->setTags($rule['tags']); + $taggingRule->setConfig($config); + $em->persist($taggingRule); + } + + $em->flush(); + + $message = 'flashes.config.notice.tagging_rules_imported'; + } + } + + $this->addFlash('notice', $message); + + return $this->redirect($this->generateUrl('config') . '#set5'); + } + 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(), + 'import_tagging_rule' => $taggingRulesImportform->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(), ]); } /** - * @param Request $request + * 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. + * + * @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')); + } + + /** * @Route("/generate-token", name="generate_token") * * @return RedirectResponse|JsonResponse @@ -167,19 +311,45 @@ 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'); + } + + /** + * @Route("/revoke-token", name="revoke_token") + * + * @return RedirectResponse|JsonResponse + */ + public function revokeTokenAction(Request $request) + { + $config = $this->getConfig(); + $config->setFeedToken(null); + + $em = $this->getDoctrine()->getManager(); + $em->persist($config); + $em->flush(); + + if ($request->isXmlHttpRequest()) { + return new JsonResponse(); + } + + $this->addFlash( + 'notice', + 'flashes.config.notice.feed_token_revoked' ); return $this->redirect($this->generateUrl('config') . '#set2'); @@ -188,8 +358,6 @@ class ConfigController extends Controller /** * Deletes a tagging rule and redirect to the config homepage. * - * @param TaggingRule $rule - * * @Route("/tagging-rule/delete/{id}", requirements={"id" = "\d+"}, name="delete_tagging_rule") * * @return RedirectResponse @@ -202,7 +370,7 @@ class ConfigController extends Controller $em->remove($rule); $em->flush(); - $this->get('session')->getFlashBag()->add( + $this->addFlash( 'notice', 'flashes.config.notice.tagging_rules_deleted' ); @@ -213,8 +381,6 @@ class ConfigController extends Controller /** * Edit a tagging rule. * - * @param TaggingRule $rule - * * @Route("/tagging-rule/edit/{id}", requirements={"id" = "\d+"}, name="edit_tagging_rule") * * @return RedirectResponse @@ -268,7 +434,7 @@ class ConfigController extends Controller break; } - $this->get('session')->getFlashBag()->add( + $this->addFlash( 'notice', 'flashes.config.notice.' . $type . '_reset' ); @@ -281,8 +447,6 @@ class ConfigController extends Controller * * @Route("/account/delete", name="delete_account") * - * @param Request $request - * * @throws AccessDeniedHttpException * * @return \Symfony\Component\HttpFoundation\RedirectResponse @@ -313,8 +477,6 @@ class ConfigController extends Controller * * @Route("/config/view-mode", name="switch_view_mode") * - * @param Request $request - * * @return \Symfony\Component\HttpFoundation\RedirectResponse */ public function changeViewModeAction(Request $request) @@ -329,6 +491,52 @@ class ConfigController extends Controller return $this->redirect($request->headers->get('referer')); } + /** + * Change the locale for the current user. + * + * @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'))); + } + + /** + * Export tagging rules for the logged in user. + * + * @Route("/tagging-rule/export", name="export_tagging_rule") + * + * @return Response + */ + public function exportTaggingRulesAction() + { + $data = SerializerBuilder::create()->build()->serialize( + $this->getUser()->getConfig()->getTaggingRules(), + 'json', + SerializationContext::create()->setGroups(['export_tagging_rule']) + ); + + return Response::create( + $data, + 200, + [ + 'Content-type' => 'application/json', + 'Content-Disposition' => 'attachment; filename="tagging_rules_' . $this->getUser()->getUsername() . '.json"', + 'Content-Transfer-Encoding' => 'UTF-8', + ] + ); + } + /** * Remove all tags for given tags and a given user and cleanup orphan tags. * @@ -395,8 +603,6 @@ class ConfigController extends Controller /** * Validate that a rule can be edited/deleted by the current user. - * - * @param TaggingRule $rule */ private function validateRuleAction(TaggingRule $rule) { diff --git a/src/Wallabag/CoreBundle/Controller/EntryController.php b/src/Wallabag/CoreBundle/Controller/EntryController.php index b7fdea279..ba5bfbe29 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; @@ -20,8 +21,7 @@ use Wallabag\CoreBundle\Form\Type\SearchEntryType; class EntryController extends Controller { /** - * @param Request $request - * @param int $page + * @param int $page * * @Route("/search/{page}", name="search", defaults={"page" = 1}) * @@ -52,8 +52,6 @@ class EntryController extends Controller } /** - * @param Request $request - * * @Route("/new-entry", name="new_entry") * * @return \Symfony\Component\HttpFoundation\Response @@ -96,8 +94,6 @@ class EntryController extends Controller } /** - * @param Request $request - * * @Route("/bookmarklet", name="bookmarklet") * * @return \Symfony\Component\HttpFoundation\Response @@ -134,9 +130,6 @@ class EntryController extends Controller /** * Edit an entry content. * - * @param Request $request - * @param Entry $entry - * * @Route("/edit/{id}", requirements={"id" = "\d+"}, name="edit") * * @return \Symfony\Component\HttpFoundation\Response @@ -170,8 +163,7 @@ class EntryController extends Controller /** * Shows all entries for current user. * - * @param Request $request - * @param int $page + * @param int $page * * @Route("/all/list/{page}", name="all", defaults={"page" = "1"}) * @@ -185,8 +177,7 @@ class EntryController extends Controller /** * Shows unread entries for current user. * - * @param Request $request - * @param int $page + * @param int $page * * @Route("/unread/list/{page}", name="unread", defaults={"page" = "1"}) * @@ -205,8 +196,7 @@ class EntryController extends Controller /** * Shows read entries for current user. * - * @param Request $request - * @param int $page + * @param int $page * * @Route("/archive/list/{page}", name="archive", defaults={"page" = "1"}) * @@ -220,8 +210,7 @@ class EntryController extends Controller /** * Shows starred entries for current user. * - * @param Request $request - * @param int $page + * @param int $page * * @Route("/starred/list/{page}", name="starred", defaults={"page" = "1"}) * @@ -233,9 +222,46 @@ class EntryController extends Controller } /** - * Shows entry content. + * Shows untagged articles for current user. * - * @param Entry $entry + * @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. * * @Route("/view/{id}", requirements={"id" = "\d+"}, name="view") * @@ -255,8 +281,6 @@ class EntryController extends Controller * Reload an entry. * Refetch content from the website and make it readable again. * - * @param Entry $entry - * * @Route("/reload/{id}", requirements={"id" = "\d+"}, name="reload_entry") * * @return \Symfony\Component\HttpFoundation\RedirectResponse @@ -289,9 +313,6 @@ class EntryController extends Controller /** * Changes read status for an entry. * - * @param Request $request - * @param Entry $entry - * * @Route("/archive/{id}", requirements={"id" = "\d+"}, name="archive_entry") * * @return \Symfony\Component\HttpFoundation\RedirectResponse @@ -321,9 +342,6 @@ class EntryController extends Controller /** * Changes starred status for an entry. * - * @param Request $request - * @param Entry $entry - * * @Route("/star/{id}", requirements={"id" = "\d+"}, name="star_entry") * * @return \Symfony\Component\HttpFoundation\RedirectResponse @@ -354,8 +372,6 @@ class EntryController extends Controller /** * Deletes entry and redirect to the homepage or the last viewed page. * - * @param Entry $entry - * * @Route("/delete/{id}", requirements={"id" = "\d+"}, name="delete_entry") * * @return \Symfony\Component\HttpFoundation\RedirectResponse @@ -396,8 +412,6 @@ class EntryController extends Controller /** * Get public URL for entry (and generate it if necessary). * - * @param Entry $entry - * * @Route("/share/{id}", requirements={"id" = "\d+"}, name="share") * * @return \Symfony\Component\HttpFoundation\Response @@ -422,8 +436,6 @@ class EntryController extends Controller /** * Disable public sharing for an entry. * - * @param Entry $entry - * * @Route("/share/delete/{id}", requirements={"id" = "\d+"}, name="delete_share") * * @return \Symfony\Component\HttpFoundation\Response @@ -446,8 +458,6 @@ class EntryController extends Controller /** * Ability to view a content publicly. * - * @param Entry $entry - * * @Route("/share/{uid}", requirements={"uid" = ".+"}, name="share_entry") * @Cache(maxage="25200", smaxage="25200", public=true) * @@ -465,61 +475,12 @@ 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. * - * @param string $type Entries type: unread, starred or archive - * @param Request $request - * @param int $page + * @param string $type Entries type: unread, starred or archive + * @param int $page * * @return \Symfony\Component\HttpFoundation\Response */ @@ -532,11 +493,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()); @@ -576,6 +535,9 @@ class EntryController extends Controller } } + $nbEntriesUntagged = $this->get('wallabag_core.entry_repository') + ->countUntaggedEntriesByUser($this->getUser()->getId()); + return $this->render( 'WallabagCoreBundle:Entry:entries.html.twig', [ 'form' => $form->createView(), @@ -583,14 +545,45 @@ class EntryController extends Controller 'currentPage' => $page, 'searchTerm' => $searchTerm, 'isFiltered' => $form->isSubmitted(), + 'nbEntriesUntagged' => $nbEntriesUntagged, ] ); } /** - * Check if the logged user can manage the given entry. + * 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. */ private function checkUserAction(Entry $entry) { @@ -602,8 +595,6 @@ class EntryController extends Controller /** * Check for existing entry, if it exists, redirect to it with a message. * - * @param Entry $entry - * * @return Entry|bool */ private function checkIfEntryAlreadyExists(Entry $entry) diff --git a/src/Wallabag/CoreBundle/Controller/ExportController.php b/src/Wallabag/CoreBundle/Controller/ExportController.php index 7ca892394..282fd733b 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; /** @@ -17,7 +17,6 @@ class ExportController extends Controller /** * Gets one entry content. * - * @param Entry $entry * @param string $format * * @Route("/export/{id}.{format}", name="export_entry", requirements={ @@ -58,6 +57,7 @@ class ExportController extends Controller $method = ucfirst($category); $methodBuilder = 'getBuilderFor' . $method . 'ByUser'; $repository = $this->get('wallabag_core.entry_repository'); + $title = $method; if ('tag_entries' === $category) { $tag = $this->get('wallabag_core.tag_repository')->findOneBySlug($request->query->get('tag')); @@ -66,6 +66,8 @@ class ExportController extends Controller $this->getUser()->getId(), $tag->getId() ); + + $title = 'Tag ' . $tag->getLabel(); } else { $entries = $repository ->$methodBuilder($this->getUser()->getId()) @@ -76,7 +78,7 @@ class ExportController extends Controller try { return $this->get('wallabag_core.helper.entries_export') ->setEntries($entries) - ->updateTitle($method) + ->updateTitle($title) ->updateAuthor($method) ->exportAs($format); } catch (\InvalidArgumentException $e) { diff --git a/src/Wallabag/CoreBundle/Controller/RssController.php b/src/Wallabag/CoreBundle/Controller/FeedController.php similarity index 63% rename from src/Wallabag/CoreBundle/Controller/RssController.php rename to src/Wallabag/CoreBundle/Controller/FeedController.php index 848bb8140..95c3427b8 100644 --- a/src/Wallabag/CoreBundle/Controller/RssController.php +++ b/src/Wallabag/CoreBundle/Controller/FeedController.php @@ -7,86 +7,94 @@ 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 $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 $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 $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 +127,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']) ); } @@ -133,7 +144,6 @@ class RssController extends Controller * It returns the response to be send. * * @param string $type Entries type: unread, starred or archive - * @param User $user * @param int $page * * @return \Symfony\Component\HttpFoundation\Response @@ -162,14 +172,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 +188,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 548de7448..4320c5ffe 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,10 +34,7 @@ class SiteCredentialController extends Controller /** * Creates a new site credential entity. * - * @Route("/new", name="site_credentials_new") - * @Method({"GET", "POST"}) - * - * @param Request $request + * @Route("/new", name="site_credentials_new", methods={"GET", "POST"}) * * @return \Symfony\Component\HttpFoundation\Response */ @@ -77,11 +72,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"}) - * - * @param Request $request - * @param SiteCredential $siteCredential + * @Route("/{id}/edit", name="site_credentials_edit", methods={"GET", "POST"}) * * @return \Symfony\Component\HttpFoundation\Response */ @@ -121,11 +112,7 @@ class SiteCredentialController extends Controller /** * Deletes a site credential entity. * - * @Route("/{id}", name="site_credentials_delete") - * @Method("DELETE") - * - * @param Request $request - * @param SiteCredential $siteCredential + * @Route("/{id}", name="site_credentials_delete", methods={"DELETE"}) * * @return \Symfony\Component\HttpFoundation\RedirectResponse */ diff --git a/src/Wallabag/CoreBundle/Controller/StaticController.php b/src/Wallabag/CoreBundle/Controller/StaticController.php index 318af303c..fa760c144 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 b6d28e59d..a6ad131ff 100644 --- a/src/Wallabag/CoreBundle/Controller/TagController.php +++ b/src/Wallabag/CoreBundle/Controller/TagController.php @@ -5,19 +5,17 @@ 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 { /** - * @param Request $request - * @param Entry $entry - * * @Route("/new-tag/{entry}", requirements={"entry" = "\d+"}, name="new_tag") * * @return \Symfony\Component\HttpFoundation\Response @@ -86,14 +84,22 @@ class TagController extends Controller { $tags = $this->get('wallabag_core.tag_repository') ->findAllFlatTagsWithNbEntries($this->getUser()->getId()); + $nbEntriesUntagged = $this->get('wallabag_core.entry_repository') + ->countUntaggedEntriesByUser($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, + 'nbEntriesUntagged' => $nbEntriesUntagged, ]); } /** - * @param Tag $tag * @param int $page * * @Route("/tag/list/{slug}/{page}", name="tag_entries", defaults={"page" = "1"}) @@ -130,4 +136,45 @@ 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. + * + * @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 75% rename from src/Wallabag/CoreBundle/DataFixtures/ORM/LoadConfigData.php rename to src/Wallabag/CoreBundle/DataFixtures/ConfigFixtures.php index 3d4d5def9..5e914965d 100644 --- a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadConfigData.php +++ b/src/Wallabag/CoreBundle/DataFixtures/ConfigFixtures.php @@ -1,13 +1,14 @@ setTheme('material'); $adminConfig->setItemsPerPage(30); - $adminConfig->setReadingSpeed(1); + $adminConfig->setReadingSpeed(200); $adminConfig->setLanguage('en'); $adminConfig->setPocketConsumerKey('xxxxx'); $adminConfig->setActionMarkAsRead(0); @@ -31,7 +32,7 @@ class LoadConfigData extends AbstractFixture implements OrderedFixtureInterface $bobConfig = new Config($this->getReference('bob-user')); $bobConfig->setTheme('default'); $bobConfig->setItemsPerPage(10); - $bobConfig->setReadingSpeed(1); + $bobConfig->setReadingSpeed(200); $bobConfig->setLanguage('fr'); $bobConfig->setPocketConsumerKey(null); $bobConfig->setActionMarkAsRead(1); @@ -44,7 +45,7 @@ class LoadConfigData extends AbstractFixture implements OrderedFixtureInterface $emptyConfig = new Config($this->getReference('empty-user')); $emptyConfig->setTheme('material'); $emptyConfig->setItemsPerPage(10); - $emptyConfig->setReadingSpeed(1); + $emptyConfig->setReadingSpeed(200); $emptyConfig->setLanguage('en'); $emptyConfig->setPocketConsumerKey(null); $emptyConfig->setActionMarkAsRead(0); @@ -60,8 +61,10 @@ class LoadConfigData extends AbstractFixture implements OrderedFixtureInterface /** * {@inheritdoc} */ - public function getOrder() + public function getDependencies() { - return 20; + return [ + UserFixtures::class, + ]; } } diff --git a/src/Wallabag/CoreBundle/DataFixtures/EntryFixtures.php b/src/Wallabag/CoreBundle/DataFixtures/EntryFixtures.php new file mode 100644 index 000000000..024fcfdc8 --- /dev/null +++ b/src/Wallabag/CoreBundle/DataFixtures/EntryFixtures.php @@ -0,0 +1,138 @@ + [ + 'user' => 'admin-user', + 'url' => 'http://0.0.0.0/entry1', + 'reading_time' => 11, + 'domain' => 'domain.io', + 'mime' => 'text/html', + 'title' => 'test title entry1', + 'content' => 'This is my content /o/', + 'language' => 'en', + 'tags' => ['foo-tag', 'baz-tag'], + ], + 'entry2' => [ + 'user' => 'admin-user', + 'url' => 'http://0.0.0.0/entry2', + 'reading_time' => 1, + 'domain' => 'domain.io', + 'mime' => 'text/html', + 'title' => 'test title entry2', + 'content' => 'This is my content /o/', + 'origin' => 'ftp://oneftp.tld', + 'language' => 'fr', + ], + 'entry3' => [ + 'user' => 'bob-user', + 'url' => 'http://0.0.0.0/entry3', + 'reading_time' => 1, + 'domain' => 'domain.io', + 'mime' => 'text/html', + 'title' => 'test title entry3', + 'content' => 'This is my content /o/', + 'language' => 'en', + 'tags' => ['foo-tag', 'bar-tag', 'bob-tag'], + ], + 'entry4' => [ + 'user' => 'admin-user', + 'url' => 'http://0.0.0.0/entry4', + 'reading_time' => 12, + 'domain' => 'domain.io', + 'mime' => 'text/html', + 'title' => 'test title entry4', + 'content' => 'This is my content /o/', + 'language' => 'en', + 'tags' => ['foo-tag', 'bar-tag'], + ], + 'entry5' => [ + 'user' => 'admin-user', + 'url' => 'http://0.0.0.0/entry5', + 'reading_time' => 12, + 'domain' => 'domain.io', + 'mime' => 'text/html', + 'title' => 'test title entry5', + 'content' => 'This is my content /o/', + 'language' => 'fr', + 'starred' => true, + 'preview' => 'http://0.0.0.0/image.jpg', + ], + 'entry6' => [ + 'user' => 'admin-user', + 'url' => 'http://0.0.0.0/entry6', + 'reading_time' => 12, + 'domain' => 'domain.io', + 'mime' => 'text/html', + 'title' => 'test title entry6', + 'content' => 'This is my content /o/', + 'language' => 'de', + 'archived' => true, + 'tags' => ['bar-tag'], + ], + ]; + + foreach ($entries as $reference => $item) { + $entry = new Entry($this->getReference($item['user'])); + $entry->setUrl($item['url']); + $entry->setReadingTime($item['reading_time']); + $entry->setDomainName($item['domain']); + $entry->setMimetype($item['mime']); + $entry->setTitle($item['title']); + $entry->setContent($item['content']); + $entry->setLanguage($item['language']); + + if (isset($item['tags'])) { + foreach ($item['tags'] as $tag) { + $entry->addTag($this->getReference($tag)); + } + } + + if (isset($item['origin'])) { + $entry->setOriginUrl($item['origin']); + } + + if (isset($item['starred'])) { + $entry->setStarred($item['starred']); + } + + if (isset($item['archived'])) { + $entry->setArchived($item['archived']); + } + + if (isset($item['preview'])) { + $entry->setPreviewPicture($item['preview']); + } + + $manager->persist($entry); + $this->addReference($reference, $entry); + } + + $manager->flush(); + } + + /** + * {@inheritdoc} + */ + public function getDependencies() + { + return [ + UserFixtures::class, + TagFixtures::class, + ]; + } +} diff --git a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadSettingData.php b/src/Wallabag/CoreBundle/DataFixtures/InternalSettingFixtures.php similarity index 65% rename from src/Wallabag/CoreBundle/DataFixtures/ORM/LoadSettingData.php rename to src/Wallabag/CoreBundle/DataFixtures/InternalSettingFixtures.php index 3fe88e7fd..b052d1d5e 100644 --- a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadSettingData.php +++ b/src/Wallabag/CoreBundle/DataFixtures/InternalSettingFixtures.php @@ -1,15 +1,14 @@ container->getParameter('wallabag_core.default_internal_settings') as $setting) { - $newSetting = new Setting(); + $newSetting = new InternalSetting(); $newSetting->setName($setting['name']); $newSetting->setValue($setting['value']); $newSetting->setSection($setting['section']); @@ -36,12 +35,4 @@ class LoadSettingData extends AbstractFixture implements OrderedFixtureInterface $manager->flush(); } - - /** - * {@inheritdoc} - */ - public function getOrder() - { - return 29; - } } diff --git a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadEntryData.php b/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadEntryData.php deleted file mode 100644 index 0e1510a29..000000000 --- a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadEntryData.php +++ /dev/null @@ -1,119 +0,0 @@ -getReference('admin-user')); - $entry1->setUrl('http://0.0.0.0/entry1'); - $entry1->setReadingTime(11); - $entry1->setDomainName('domain.io'); - $entry1->setMimetype('text/html'); - $entry1->setTitle('test title entry1'); - $entry1->setContent('This is my content /o/'); - $entry1->setLanguage('en'); - - $entry1->addTag($this->getReference('foo-tag')); - $entry1->addTag($this->getReference('baz-tag')); - - $manager->persist($entry1); - - $this->addReference('entry1', $entry1); - - $entry2 = new Entry($this->getReference('admin-user')); - $entry2->setUrl('http://0.0.0.0/entry2'); - $entry2->setReadingTime(1); - $entry2->setDomainName('domain.io'); - $entry2->setMimetype('text/html'); - $entry2->setTitle('test title entry2'); - $entry2->setContent('This is my content /o/'); - $entry2->setOriginUrl('ftp://oneftp.tld'); - $entry2->setLanguage('fr'); - - $manager->persist($entry2); - - $this->addReference('entry2', $entry2); - - $entry3 = new Entry($this->getReference('bob-user')); - $entry3->setUrl('http://0.0.0.0/entry3'); - $entry3->setReadingTime(1); - $entry3->setDomainName('domain.io'); - $entry3->setMimetype('text/html'); - $entry3->setTitle('test title entry3'); - $entry3->setContent('This is my content /o/'); - $entry3->setLanguage('en'); - - $entry3->addTag($this->getReference('foo-tag')); - $entry3->addTag($this->getReference('bar-tag')); - - $manager->persist($entry3); - - $this->addReference('entry3', $entry3); - - $entry4 = new Entry($this->getReference('admin-user')); - $entry4->setUrl('http://0.0.0.0/entry4'); - $entry4->setReadingTime(12); - $entry4->setDomainName('domain.io'); - $entry4->setMimetype('text/html'); - $entry4->setTitle('test title entry4'); - $entry4->setContent('This is my content /o/'); - $entry4->setLanguage('en'); - - $entry4->addTag($this->getReference('foo-tag')); - $entry4->addTag($this->getReference('bar-tag')); - - $manager->persist($entry4); - - $this->addReference('entry4', $entry4); - - $entry5 = new Entry($this->getReference('admin-user')); - $entry5->setUrl('http://0.0.0.0/entry5'); - $entry5->setReadingTime(12); - $entry5->setDomainName('domain.io'); - $entry5->setMimetype('text/html'); - $entry5->setTitle('test title entry5'); - $entry5->setContent('This is my content /o/'); - $entry5->setStarred(true); - $entry5->setLanguage('fr'); - $entry5->setPreviewPicture('http://0.0.0.0/image.jpg'); - - $manager->persist($entry5); - - $this->addReference('entry5', $entry5); - - $entry6 = new Entry($this->getReference('admin-user')); - $entry6->setUrl('http://0.0.0.0/entry6'); - $entry6->setReadingTime(12); - $entry6->setDomainName('domain.io'); - $entry6->setMimetype('text/html'); - $entry6->setTitle('test title entry6'); - $entry6->setContent('This is my content /o/'); - $entry6->setArchived(true); - $entry6->setLanguage('de'); - $entry6->addTag($this->getReference('bar-tag')); - - $manager->persist($entry6); - - $this->addReference('entry6', $entry6); - - $manager->flush(); - } - - /** - * {@inheritdoc} - */ - public function getOrder() - { - return 30; - } -} diff --git a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadSiteCredentialData.php b/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadSiteCredentialData.php deleted file mode 100644 index 866f55a40..000000000 --- a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadSiteCredentialData.php +++ /dev/null @@ -1,34 +0,0 @@ -getReference('admin-user')); - $credential->setHost('example.com'); - $credential->setUsername('foo'); - $credential->setPassword('bar'); - - $manager->persist($credential); - - $manager->flush(); - } - - /** - * {@inheritdoc} - */ - public function getOrder() - { - return 50; - } -} diff --git a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadTagData.php b/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadTagData.php deleted file mode 100644 index 0ecfd18b5..000000000 --- a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadTagData.php +++ /dev/null @@ -1,55 +0,0 @@ -setLabel('foo bar'); - - $manager->persist($tag1); - - $this->addReference('foo-bar-tag', $tag1); - - $tag2 = new Tag(); - $tag2->setLabel('bar'); - - $manager->persist($tag2); - - $this->addReference('bar-tag', $tag2); - - $tag3 = new Tag(); - $tag3->setLabel('baz'); - - $manager->persist($tag3); - - $this->addReference('baz-tag', $tag3); - - $tag4 = new Tag(); - $tag4->setLabel('foo'); - - $manager->persist($tag4); - - $this->addReference('foo-tag', $tag4); - - $manager->flush(); - } - - /** - * {@inheritdoc} - */ - public function getOrder() - { - return 25; - } -} diff --git a/src/Wallabag/CoreBundle/DataFixtures/SiteCredentialFixtures.php b/src/Wallabag/CoreBundle/DataFixtures/SiteCredentialFixtures.php new file mode 100644 index 000000000..9a7d116f6 --- /dev/null +++ b/src/Wallabag/CoreBundle/DataFixtures/SiteCredentialFixtures.php @@ -0,0 +1,56 @@ +container = $container; + } + + /** + * {@inheritdoc} + */ + public function load(ObjectManager $manager) + { + $credential = new SiteCredential($this->getReference('admin-user')); + $credential->setHost('.super.com'); + $credential->setUsername($this->container->get('wallabag_core.helper.crypto_proxy')->crypt('.super')); + $credential->setPassword($this->container->get('wallabag_core.helper.crypto_proxy')->crypt('bar')); + + $manager->persist($credential); + + $credential = new SiteCredential($this->getReference('admin-user')); + $credential->setHost('paywall.example.com'); + $credential->setUsername($this->container->get('wallabag_core.helper.crypto_proxy')->crypt('paywall.example')); + $credential->setPassword($this->container->get('wallabag_core.helper.crypto_proxy')->crypt('bar')); + + $manager->persist($credential); + + $manager->flush(); + } + + /** + * {@inheritdoc} + */ + public function getDependencies() + { + return [ + UserFixtures::class, + ]; + } +} diff --git a/src/Wallabag/CoreBundle/DataFixtures/TagFixtures.php b/src/Wallabag/CoreBundle/DataFixtures/TagFixtures.php new file mode 100644 index 000000000..58a0d799b --- /dev/null +++ b/src/Wallabag/CoreBundle/DataFixtures/TagFixtures.php @@ -0,0 +1,35 @@ + 'foo bar', //tag used for EntryControllerTest + 'bar-tag' => 'bar', + 'baz-tag' => 'baz', // tag used for ExportControllerTest + 'foo-tag' => 'foo', + 'bob-tag' => 'bob', // tag used for TagRestControllerTest + ]; + + foreach ($tags as $reference => $label) { + $tag = new Tag(); + $tag->setLabel($label); + + $manager->persist($tag); + + $this->addReference($reference, $tag); + } + + $manager->flush(); + } +} 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 55abd63cd..78ff314a8 100644 --- a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadTaggingRuleData.php +++ b/src/Wallabag/CoreBundle/DataFixtures/TaggingRuleFixtures.php @@ -1,13 +1,13 @@ defaultValue(50) ->end() ->integerNode('reading_speed') - ->defaultValue(1) + ->defaultValue(200) ->end() ->scalarNode('version') ->end() diff --git a/src/Wallabag/CoreBundle/DependencyInjection/WallabagCoreExtension.php b/src/Wallabag/CoreBundle/DependencyInjection/WallabagCoreExtension.php index a3ef2b53f..e9a1e9e05 100644 --- a/src/Wallabag/CoreBundle/DependencyInjection/WallabagCoreExtension.php +++ b/src/Wallabag/CoreBundle/DependencyInjection/WallabagCoreExtension.php @@ -18,7 +18,7 @@ class WallabagCoreExtension extends Extension $container->setParameter('wallabag_core.items_on_page', $config['items_on_page']); $container->setParameter('wallabag_core.theme', $config['theme']); $container->setParameter('wallabag_core.language', $config['language']); - $container->setParameter('wallabag_core.rss_limit', $config['rss_limit']); + $container->setParameter('wallabag_core.feed_limit', $config['rss_limit']); $container->setParameter('wallabag_core.reading_speed', $config['reading_speed']); $container->setParameter('wallabag_core.version', $config['version']); $container->setParameter('wallabag_core.paypal_url', $config['paypal_url']); diff --git a/src/Wallabag/CoreBundle/Doctrine/DBAL/Driver/CustomPostgreSQLDriver.php b/src/Wallabag/CoreBundle/Doctrine/DBAL/Driver/CustomPostgreSQLDriver.php deleted file mode 100644 index eb5b203fa..000000000 --- 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 7aa2409a1..4a3fef3b1 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 b902ae2cb..fe7942ee2 100644 --- a/src/Wallabag/CoreBundle/Entity/Config.php +++ b/src/Wallabag/CoreBundle/Entity/Config.php @@ -11,8 +11,12 @@ use Wallabag\UserBundle\Entity\User; * Config. * * @ORM\Entity(repositoryClass="Wallabag\CoreBundle\Repository\ConfigRepository") - * @ORM\Table(name="`config`") - * @ORM\Entity + * @ORM\Table( + * name="`config`", + * indexes={ + * @ORM\Index(name="config_feed_token", columns={"feed_token"}, options={"lengths"={255}}), + * } + * ) */ class Config { @@ -60,21 +64,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 +235,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; } /** @@ -367,8 +371,6 @@ class Config } /** - * @param TaggingRule $rule - * * @return Config */ public function addTaggingRule(TaggingRule $rule) diff --git a/src/Wallabag/CoreBundle/Entity/Entry.php b/src/Wallabag/CoreBundle/Entity/Entry.php index 2b1f2e050..190457983 100644 --- a/src/Wallabag/CoreBundle/Entity/Entry.php +++ b/src/Wallabag/CoreBundle/Entity/Entry.php @@ -13,6 +13,7 @@ use JMS\Serializer\Annotation\XmlRoot; use Symfony\Component\Validator\Constraints as Assert; use Wallabag\AnnotationBundle\Entity\Annotation; use Wallabag\CoreBundle\Helper\EntityTimestampsTrait; +use Wallabag\CoreBundle\Helper\UrlHasher; use Wallabag\UserBundle\Entity\User; /** @@ -25,7 +26,13 @@ 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\Index(name="hashed_given_url_user_id", columns={"user_id", "hashed_given_url"}, options={"lengths"={null, 40}}), + * @ORM\Index(name="user_language", columns={"language", "user_id"}), + * @ORM\Index(name="user_archived", columns={"user_id", "is_archived", "archived_at"}), + * @ORM\Index(name="user_created", columns={"user_id", "created_at"}), + * @ORM\Index(name="user_starred", columns={"user_id", "is_starred", "starred_at"}) * } * ) * @ORM\HasLifecycleCallbacks() @@ -66,6 +73,8 @@ class Entry private $title; /** + * Define the url fetched by wallabag (the final url after potential redirections). + * * @var string * * @Assert\NotBlank() @@ -75,6 +84,42 @@ class Entry */ private $url; + /** + * @var string + * + * @ORM\Column(name="hashed_url", type="string", length=40, nullable=true) + */ + private $hashedUrl; + + /** + * From where user retrieved/found the url (an other article, a twitter, or the given_url if non are provided). + * + * @var string + * + * @ORM\Column(name="origin_url", type="text", nullable=true) + * + * @Groups({"entries_for_user", "export_all"}) + */ + private $originUrl; + + /** + * Define the url entered by the user (without redirections). + * + * @var string + * + * @ORM\Column(name="given_url", type="text", nullable=true) + * + * @Groups({"entries_for_user", "export_all"}) + */ + private $givenUrl; + + /** + * @var string + * + * @ORM\Column(name="hashed_given_url", type="string", length=40, nullable=true) + */ + private $hashedGivenUrl; + /** * @var bool * @@ -86,6 +131,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 * @@ -171,7 +225,7 @@ class Entry /** * @var string * - * @ORM\Column(name="language", type="text", nullable=true) + * @ORM\Column(name="language", type="string", length=20, nullable=true) * * @Groups({"entries_for_user", "export_all"}) */ @@ -245,15 +299,6 @@ class Entry */ private $tags; - /** - * @var string - * - * @ORM\Column(name="origin_url", type="text", nullable=true) - * - * @Groups({"entries_for_user", "export_all"}) - */ - private $originUrl; - /* * @param User $user */ @@ -307,6 +352,7 @@ class Entry public function setUrl($url) { $this->url = $url; + $this->hashedUrl = UrlHasher::hashUrl($url); return $this; } @@ -335,6 +381,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 +441,7 @@ class Entry public function toggleArchive() { - $this->isArchived = $this->isArchived() ^ 1; + $this->updateArchived($this->isArchived() ^ 1); return $this; } @@ -466,8 +550,6 @@ class Entry * Set created_at. * Only used when importing data from an other service. * - * @param \DateTime $createdAt - * * @return Entry */ public function setCreatedAt(\DateTime $createdAt) @@ -539,9 +621,6 @@ class Entry return $this->annotations; } - /** - * @param Annotation $annotation - */ public function setAnnotation(Annotation $annotation) { $this->annotations[] = $annotation; @@ -618,9 +697,6 @@ class Entry return $data; } - /** - * @param Tag $tag - */ public function addTag(Tag $tag) { if ($this->tags->contains($tag)) { @@ -641,8 +717,6 @@ class Entry /** * Remove the given tag from the entry (if the tag is associated). - * - * @param Tag $tag */ public function removeTag(Tag $tag) { @@ -714,7 +788,20 @@ class Entry } /** - * @return string + * Format the entry language to a valid html lang attribute. + */ + public function getHTMLLanguage() + { + $parsedLocale = \Locale::parseLocale($this->getLanguage()); + $lang = ''; + $lang .= $parsedLocale['language'] ?? ''; + $lang .= isset($parsedLocale['region']) ? '-' . $parsedLocale['region'] : ''; + + return $lang; + } + + /** + * @return string|null */ public function getUid() { @@ -790,8 +877,6 @@ class Entry } /** - * @param \Datetime $publishedAt - * * @return Entry */ public function setPublishedAt(\Datetime $publishedAt) @@ -864,4 +949,49 @@ class Entry { return $this->originUrl; } + + /** + * Set given url. + * + * @param string $givenUrl + * + * @return Entry + */ + public function setGivenUrl($givenUrl) + { + $this->givenUrl = $givenUrl; + $this->hashedGivenUrl = UrlHasher::hashUrl($givenUrl); + + return $this; + } + + /** + * Get given url. + * + * @return string + */ + public function getGivenUrl() + { + return $this->givenUrl; + } + + /** + * @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/InternalSetting.php b/src/Wallabag/CoreBundle/Entity/InternalSetting.php new file mode 100644 index 000000000..df8bd3be2 --- /dev/null +++ b/src/Wallabag/CoreBundle/Entity/InternalSetting.php @@ -0,0 +1,36 @@ +createdAt; } + /** + * Get updatedAt. + * + * @return \DateTime + */ + public function getUpdatedAt() + { + return $this->updatedAt; + } + /** * @return User */ diff --git a/src/Wallabag/CoreBundle/Entity/Tag.php b/src/Wallabag/CoreBundle/Entity/Tag.php index a6dc8c509..9fb2f94fd 100644 --- a/src/Wallabag/CoreBundle/Entity/Tag.php +++ b/src/Wallabag/CoreBundle/Entity/Tag.php @@ -13,7 +13,13 @@ use JMS\Serializer\Annotation\XmlRoot; * Tag. * * @XmlRoot("tag") - * @ORM\Table(name="`tag`") + * @ORM\Table( + * name="`tag`", + * options={"collate"="utf8mb4_bin", "charset"="utf8mb4"}, + * indexes={ + * @ORM\Index(name="tag_label", columns={"label"}, options={"lengths"={255}}), + * } + * ) * @ORM\Entity(repositoryClass="Wallabag\CoreBundle\Repository\TagRepository") * @ExclusionPolicy("all") */ @@ -98,9 +104,6 @@ class Tag return $this->slug; } - /** - * @param Entry $entry - */ public function addEntry(Entry $entry) { if ($this->entries->contains($entry)) { @@ -111,9 +114,6 @@ class Tag $entry->addTag($this); } - /** - * @param Entry $entry - */ public function removeEntry(Entry $entry) { if (!$this->entries->contains($entry)) { diff --git a/src/Wallabag/CoreBundle/Entity/TaggingRule.php b/src/Wallabag/CoreBundle/Entity/TaggingRule.php index 84e11e261..f71660871 100644 --- a/src/Wallabag/CoreBundle/Entity/TaggingRule.php +++ b/src/Wallabag/CoreBundle/Entity/TaggingRule.php @@ -3,12 +3,16 @@ namespace Wallabag\CoreBundle\Entity; use Doctrine\ORM\Mapping as ORM; -use KPhoen\RulerZBundle\Validator\Constraints as RulerZAssert; +use JMS\Serializer\Annotation\Exclude; +use JMS\Serializer\Annotation\Groups; +use JMS\Serializer\Annotation\XmlRoot; +use Symfony\Bridge\RulerZ\Validator\Constraints as RulerZAssert; use Symfony\Component\Validator\Constraints as Assert; /** * Tagging rule. * + * @XmlRoot("tagging_rule") * @ORM\Entity(repositoryClass="Wallabag\CoreBundle\Repository\TaggingRuleRepository") * @ORM\Table(name="`tagging_rule`") * @ORM\Entity @@ -34,6 +38,8 @@ class TaggingRule * allowed_operators={">", "<", ">=", "<=", "=", "is", "!=", "and", "not", "or", "matches", "notmatches"} * ) * @ORM\Column(name="rule", type="string", nullable=false) + * + * @Groups({"export_tagging_rule"}) */ private $rule; @@ -42,10 +48,14 @@ class TaggingRule * * @Assert\NotBlank() * @ORM\Column(name="tags", type="simple_array", nullable=false) + * + * @Groups({"export_tagging_rule"}) */ private $tags = []; /** + * @Exclude + * * @ORM\ManyToOne(targetEntity="Wallabag\CoreBundle\Entity\Config", inversedBy="taggingRules") */ private $config; @@ -111,8 +121,6 @@ class TaggingRule /** * Set config. * - * @param Config $config - * * @return TaggingRule */ public function setConfig(Config $config) diff --git a/src/Wallabag/CoreBundle/Event/Listener/UserLocaleListener.php b/src/Wallabag/CoreBundle/Event/Listener/UserLocaleListener.php index 367cdfb00..1b5d61ada 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 */ @@ -23,14 +25,11 @@ class UserLocaleListener $this->session = $session; } - /** - * @param InteractiveLoginEvent $event - */ public function onInteractiveLogin(InteractiveLoginEvent $event) { $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/Event/Subscriber/CustomDoctrineORMSubscriber.php b/src/Wallabag/CoreBundle/Event/Subscriber/CustomDoctrineORMSubscriber.php index cabb3eca3..b8f6e1d6c 100644 --- a/src/Wallabag/CoreBundle/Event/Subscriber/CustomDoctrineORMSubscriber.php +++ b/src/Wallabag/CoreBundle/Event/Subscriber/CustomDoctrineORMSubscriber.php @@ -12,9 +12,6 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; */ class CustomDoctrineORMSubscriber extends DoctrineORMSubscriber implements EventSubscriberInterface { - /** - * @param GetFilterConditionEvent $event - */ public function filterDateRange(GetFilterConditionEvent $event) { $expr = $event->getFilterQuery()->getExpressionBuilder(); diff --git a/src/Wallabag/CoreBundle/Event/Subscriber/DownloadImagesSubscriber.php b/src/Wallabag/CoreBundle/Event/Subscriber/DownloadImagesSubscriber.php index 1dd0a1a42..ef8d7d3bc 100644 --- a/src/Wallabag/CoreBundle/Event/Subscriber/DownloadImagesSubscriber.php +++ b/src/Wallabag/CoreBundle/Event/Subscriber/DownloadImagesSubscriber.php @@ -35,8 +35,6 @@ class DownloadImagesSubscriber implements EventSubscriberInterface /** * Download images and updated the data into the entry. - * - * @param EntrySavedEvent $event */ public function onEntrySaved(EntrySavedEvent $event) { @@ -69,8 +67,6 @@ class DownloadImagesSubscriber implements EventSubscriberInterface /** * Remove images related to the entry. - * - * @param EntryDeletedEvent $event */ public function onEntryDeleted(EntryDeletedEvent $event) { @@ -88,8 +84,6 @@ class DownloadImagesSubscriber implements EventSubscriberInterface * * @todo If we want to add async download, it should be done in that method * - * @param Entry $entry - * * @return string|false False in case of async */ private function downloadImages(Entry $entry) @@ -106,8 +100,6 @@ class DownloadImagesSubscriber implements EventSubscriberInterface * * @todo If we want to add async download, it should be done in that method * - * @param Entry $entry - * * @return string|false False in case of async */ private function downloadPreviewImage(Entry $entry) diff --git a/src/Wallabag/CoreBundle/Event/Subscriber/SQLiteCascadeDeleteSubscriber.php b/src/Wallabag/CoreBundle/Event/Subscriber/SQLiteCascadeDeleteSubscriber.php index 9c1d8a1da..dcadeedfc 100644 --- a/src/Wallabag/CoreBundle/Event/Subscriber/SQLiteCascadeDeleteSubscriber.php +++ b/src/Wallabag/CoreBundle/Event/Subscriber/SQLiteCascadeDeleteSubscriber.php @@ -18,9 +18,6 @@ class SQLiteCascadeDeleteSubscriber implements EventSubscriber { private $doctrine; - /** - * @param \Doctrine\Bundle\DoctrineBundle\Registry $doctrine - */ public function __construct(Registry $doctrine) { $this->doctrine = $doctrine; @@ -39,8 +36,6 @@ class SQLiteCascadeDeleteSubscriber implements EventSubscriber /** * We removed everything related to the upcoming removed entry because SQLite can't handle it on it own. * We do it in the preRemove, because we can't retrieve tags in the postRemove (because the entry id is gone). - * - * @param LifecycleEventArgs $args */ public function preRemove(LifecycleEventArgs $args) { diff --git a/src/Wallabag/CoreBundle/Form/Type/ConfigType.php b/src/Wallabag/CoreBundle/Form/Type/ConfigType.php index 1714ce74a..6901fa08e 100644 --- a/src/Wallabag/CoreBundle/Form/Type/ConfigType.php +++ b/src/Wallabag/CoreBundle/Form/Type/ConfigType.php @@ -4,6 +4,7 @@ namespace Wallabag\CoreBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -37,19 +38,13 @@ class ConfigType extends AbstractType 'choices' => array_flip($this->themes), 'label' => 'config.form_settings.theme_label', ]) - ->add('items_per_page', null, [ + ->add('items_per_page', IntegerType::class, [ 'label' => 'config.form_settings.items_per_page_label', 'property_path' => 'itemsPerPage', ]) - ->add('reading_speed', ChoiceType::class, [ + ->add('reading_speed', IntegerType::class, [ 'label' => 'config.form_settings.reading_speed.label', 'property_path' => 'readingSpeed', - 'choices' => [ - 'config.form_settings.reading_speed.100_word' => '0.5', - 'config.form_settings.reading_speed.200_word' => '1', - 'config.form_settings.reading_speed.300_word' => '1.5', - 'config.form_settings.reading_speed.400_word' => '2', - ], ]) ->add('action_mark_as_read', ChoiceType::class, [ 'label' => 'config.form_settings.action_mark_as_read.label', diff --git a/src/Wallabag/CoreBundle/Form/Type/EditEntryType.php b/src/Wallabag/CoreBundle/Form/Type/EditEntryType.php index 083559286..2fc4c204b 100644 --- a/src/Wallabag/CoreBundle/Form/Type/EditEntryType.php +++ b/src/Wallabag/CoreBundle/Form/Type/EditEntryType.php @@ -22,11 +22,13 @@ class EditEntryType extends AbstractType 'disabled' => true, 'required' => false, 'label' => 'entry.edit.url_label', + 'default_protocol' => null, ]) ->add('origin_url', UrlType::class, [ 'required' => false, 'property_path' => 'originUrl', 'label' => 'entry.edit.origin_url_label', + 'default_protocol' => null, ]) ->add('save', SubmitType::class, [ 'label' => 'entry.edit.save_label', diff --git a/src/Wallabag/CoreBundle/Form/Type/EntryFilterType.php b/src/Wallabag/CoreBundle/Form/Type/EntryFilterType.php index 702c7f7aa..17070c598 100644 --- a/src/Wallabag/CoreBundle/Form/Type/EntryFilterType.php +++ b/src/Wallabag/CoreBundle/Form/Type/EntryFilterType.php @@ -23,9 +23,6 @@ class EntryFilterType extends AbstractType /** * Repository & user are used to get a list of language entries for this user. - * - * @param EntityRepository $entryRepository - * @param TokenStorageInterface $tokenStorage */ public function __construct(EntityRepository $entryRepository, TokenStorageInterface $tokenStorage) { @@ -54,8 +51,8 @@ class EntryFilterType extends AbstractType $lower = $values['value']['left_number'][0]; $upper = $values['value']['right_number'][0]; - $min = (int) ($lower * $this->user->getConfig()->getReadingSpeed()); - $max = (int) ($upper * $this->user->getConfig()->getReadingSpeed()); + $min = (int) ($lower * $this->user->getConfig()->getReadingSpeed() / 200); + $max = (int) ($upper * $this->user->getConfig()->getReadingSpeed() / 200); if (null === $lower && null === $upper) { // no value? no filter @@ -108,7 +105,7 @@ class EntryFilterType extends AbstractType ->add('httpStatus', TextFilterType::class, [ 'apply_filter' => function (QueryInterface $filterQuery, $field, $values) { $value = $values['value']; - if (false === array_key_exists($value, Response::$statusTexts)) { + if (false === \array_key_exists($value, Response::$statusTexts)) { return; } diff --git a/src/Wallabag/CoreBundle/Form/Type/RssType.php b/src/Wallabag/CoreBundle/Form/Type/FeedType.php similarity index 77% rename from src/Wallabag/CoreBundle/Form/Type/RssType.php rename to src/Wallabag/CoreBundle/Form/Type/FeedType.php index 49b31c1e2..9b34daf4c 100644 --- a/src/Wallabag/CoreBundle/Form/Type/RssType.php +++ b/src/Wallabag/CoreBundle/Form/Type/FeedType.php @@ -7,14 +7,14 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -class RssType extends AbstractType +class FeedType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder - ->add('rss_limit', null, [ - 'label' => 'config.form_rss.rss_limit', - 'property_path' => 'rssLimit', + ->add('feed_limit', null, [ + 'label' => 'config.form_feed.feed_limit', + 'property_path' => 'feedLimit', ]) ->add('save', SubmitType::class, [ 'label' => 'config.form.save', @@ -31,6 +31,6 @@ class RssType extends AbstractType public function getBlockPrefix() { - return 'rss_config'; + return 'feed_config'; } } diff --git a/src/Wallabag/CoreBundle/Form/Type/NewEntryType.php b/src/Wallabag/CoreBundle/Form/Type/NewEntryType.php index 7d74fee34..7af1e5895 100644 --- a/src/Wallabag/CoreBundle/Form/Type/NewEntryType.php +++ b/src/Wallabag/CoreBundle/Form/Type/NewEntryType.php @@ -15,6 +15,7 @@ class NewEntryType extends AbstractType ->add('url', UrlType::class, [ 'required' => true, 'label' => 'entry.new.form_new.url_label', + 'default_protocol' => null, ]) ; } diff --git a/src/Wallabag/CoreBundle/Form/Type/RenameTagType.php b/src/Wallabag/CoreBundle/Form/Type/RenameTagType.php new file mode 100644 index 000000000..e62700487 --- /dev/null +++ b/src/Wallabag/CoreBundle/Form/Type/RenameTagType.php @@ -0,0 +1,35 @@ +add('label', TextType::class, [ + 'required' => true, + 'attr' => [ + 'placeholder' => 'tag.rename.placeholder', + ], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => 'Wallabag\CoreBundle\Entity\Tag', + ]); + } + + public function getBlockPrefix() + { + return 'tag'; + } +} diff --git a/src/Wallabag/CoreBundle/Form/Type/TaggingRuleImportType.php b/src/Wallabag/CoreBundle/Form/Type/TaggingRuleImportType.php new file mode 100644 index 000000000..c6a8c0b81 --- /dev/null +++ b/src/Wallabag/CoreBundle/Form/Type/TaggingRuleImportType.php @@ -0,0 +1,29 @@ +add('file', FileType::class, [ + 'label' => 'config.form_rules.file_label', + 'required' => true, + ]) + ->add('import', SubmitType::class, [ + 'label' => 'config.form_rules.import_submit', + ]) + ; + } + + public function getBlockPrefix() + { + return 'upload_tagging_rule_file'; + } +} diff --git a/src/Wallabag/CoreBundle/Form/Type/UserInformationType.php b/src/Wallabag/CoreBundle/Form/Type/UserInformationType.php index 07c999496..6e4c9154c 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/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php b/src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php index 90e00c62d..b0be21763 100644 --- a/src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php +++ b/src/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilder.php @@ -34,11 +34,6 @@ class GrabySiteConfigBuilder implements SiteConfigBuilder /** * GrabySiteConfigBuilder constructor. - * - * @param ConfigBuilder $grabyConfigBuilder - * @param TokenStorage $token - * @param SiteCredentialRepository $credentialRepository - * @param LoggerInterface $logger */ public function __construct(ConfigBuilder $grabyConfigBuilder, TokenStorage $token, SiteCredentialRepository $credentialRepository, LoggerInterface $logger) { @@ -62,11 +57,24 @@ class GrabySiteConfigBuilder implements SiteConfigBuilder $host = substr($host, 4); } - $credentials = null; - if ($this->currentUser) { - $credentials = $this->credentialRepository->findOneByHostAndUser($host, $this->currentUser->getId()); + if (!$this->currentUser) { + $this->logger->debug('Auth: no current user defined.'); + + return false; } + $hosts = [$host]; + // will try to see for a host without the first subdomain (fr.example.org & .example.org) + $split = explode('.', $host); + + if (\count($split) > 1) { + // remove first subdomain + array_shift($split); + $hosts[] = '.' . implode('.', $split); + } + + $credentials = $this->credentialRepository->findOneByHostsAndUser($hosts, $this->currentUser->getId()); + if (null === $credentials) { $this->logger->debug('Auth: no credentials available for host.', ['host' => $host]); diff --git a/src/Wallabag/CoreBundle/Helper/ContentProxy.php b/src/Wallabag/CoreBundle/Helper/ContentProxy.php index d38811a27..9c6fa8db1 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 { @@ -47,13 +47,18 @@ class ContentProxy */ public function updateEntry(Entry $entry, $url, array $content = [], $disableContentUpdate = false) { + $this->graby->toggleImgNoReferrer(true); if (!empty($content['html'])) { $content['html'] = $this->graby->cleanupHtml($content['html'], $url); } if ((empty($content) || false === $this->validateContent($content)) && false === $disableContentUpdate) { $fetchedContent = $this->graby->fetchContent($url); - $fetchedContent['title'] = $this->sanitizeContentTitle($fetchedContent['title'], $fetchedContent['content_type']); + + $fetchedContent['title'] = $this->sanitizeContentTitle( + $fetchedContent['title'], + isset($fetchedContent['headers']['content-type']) ? $fetchedContent['headers']['content-type'] : '' + ); // when content is imported, we have information in $content // in case fetching content goes bad, we'll keep the imported information instead of overriding them @@ -73,13 +78,14 @@ class ContentProxy $entry->setUrl($url); } + $entry->setGivenUrl($url); + $this->stockEntry($entry, $content); } /** * Use a Symfony validator to ensure the language is well formatted. * - * @param Entry $entry * @param string $value Language to validate and save */ public function updateLanguage(Entry $entry, $value) @@ -105,7 +111,6 @@ class ContentProxy /** * Use a Symfony validator to ensure the preview picture is a real url. * - * @param Entry $entry * @param string $value URL to validate and save */ public function updatePreviewPicture(Entry $entry, $value) @@ -127,7 +132,6 @@ class ContentProxy /** * Update date. * - * @param Entry $entry * @param string $value Date to validate and save */ public function updatePublishedAt(Entry $entry, $value) @@ -154,8 +158,6 @@ class ContentProxy /** * Helper to extract and save host from entry url. - * - * @param Entry $entry */ public function setEntryDomainName(Entry $entry) { @@ -169,8 +171,6 @@ class ContentProxy * Helper to set a default title using: * - url basename, if applicable * - hostname. - * - * @param Entry $entry */ public function setDefaultEntryTitle(Entry $entry) { @@ -187,8 +187,8 @@ class ContentProxy /** * Try to sanitize the title of the fetched content from wrong character encodings and invalid UTF-8 character. * - * @param $title - * @param $contentType + * @param string $title + * @param string $contentType * * @return string */ @@ -252,22 +252,19 @@ class ContentProxy if (!empty($content['title'])) { $entry->setTitle($content['title']); - } elseif (!empty($content['open_graph']['og_title'])) { - $entry->setTitle($content['open_graph']['og_title']); } - $html = $content['html']; - if (false === $html) { - $html = $this->fetchingErrorMessage; + if (empty($content['html'])) { + $content['html'] = $this->fetchingErrorMessage; - if (!empty($content['open_graph']['og_description'])) { - $html .= '

But we found a short description:

'; - $html .= $content['open_graph']['og_description']; + if (!empty($content['description'])) { + $content['html'] .= '

But we found a short description:

'; + $content['html'] .= $content['description']; } } - $entry->setContent($html); - $entry->setReadingTime(Utils::getReadingTime($html)); + $entry->setContent($content['html']); + $entry->setReadingTime(Utils::getReadingTime($content['html'])); if (!empty($content['status'])) { $entry->setHttpStatus($content['status']); @@ -277,8 +274,8 @@ class ContentProxy $entry->setPublishedBy($content['authors']); } - if (!empty($content['all_headers']) && $this->storeArticleHeaders) { - $entry->setHeaders($content['all_headers']); + if (!empty($content['headers'])) { + $entry->setHeaders($content['headers']); } if (!empty($content['date'])) { @@ -289,17 +286,30 @@ class ContentProxy $this->updateLanguage($entry, $content['language']); } - if (!empty($content['open_graph']['og_image'])) { - $this->updatePreviewPicture($entry, $content['open_graph']['og_image']); + $previewPictureUrl = ''; + if (!empty($content['image'])) { + $previewPictureUrl = $content['image']; } // if content is an image, define it as a preview too - if (!empty($content['content_type']) && \in_array($this->mimeGuesser->guess($content['content_type']), ['jpeg', 'jpg', 'gif', 'png'], true)) { - $this->updatePreviewPicture($entry, $content['url']); + if (!empty($content['headers']['content-type']) && \in_array($this->mimeGuesser->guess($content['headers']['content-type']), ['jpeg', 'jpg', 'gif', 'png'], true)) { + $previewPictureUrl = $content['url']; + } elseif (empty($previewPictureUrl)) { + $this->logger->debug('Extracting images from content to provide a default preview picture'); + $imagesUrls = DownloadImages::extractImagesUrlsFromHtml($content['html']); + $this->logger->debug(\count($imagesUrls) . ' pictures found'); + + if (!empty($imagesUrls)) { + $previewPictureUrl = $imagesUrls[0]; + } } - if (!empty($content['content_type'])) { - $entry->setMimetype($content['content_type']); + if (!empty($content['headers']['content-type'])) { + $entry->setMimetype($content['headers']['content-type']); + } + + if (!empty($previewPictureUrl)) { + $this->updatePreviewPicture($entry, $previewPictureUrl); } try { @@ -316,7 +326,6 @@ class ContentProxy * Update the origin_url field when a redirection occurs * This field is set if it is empty and new url does not match ignore list. * - * @param Entry $entry * @param string $url */ private function updateOriginUrl(Entry $entry, $url) @@ -424,8 +433,6 @@ class ContentProxy /** * Validate that the given content has at least a title, an html and a url. * - * @param array $content - * * @return bool true if valid otherwise false */ private function validateContent(array $content) diff --git a/src/Wallabag/CoreBundle/Helper/DownloadImages.php b/src/Wallabag/CoreBundle/Helper/DownloadImages.php index cc3dcfceb..1d98fd1a9 100644 --- a/src/Wallabag/CoreBundle/Helper/DownloadImages.php +++ b/src/Wallabag/CoreBundle/Helper/DownloadImages.php @@ -2,8 +2,15 @@ namespace Wallabag\CoreBundle\Helper; -use GuzzleHttp\Client; -use GuzzleHttp\Message\Response; +use GuzzleHttp\Psr7\Uri; +use GuzzleHttp\Psr7\UriResolver; +use Http\Client\Common\HttpMethodsClient; +use Http\Client\Common\Plugin\ErrorPlugin; +use Http\Client\Common\PluginClient; +use Http\Client\HttpClient; +use Http\Discovery\MessageFactoryDiscovery; +use Http\Message\MessageFactory; +use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\Finder\Finder; @@ -19,9 +26,9 @@ class DownloadImages private $mimeGuesser; private $wallabagUrl; - public function __construct(Client $client, $baseFolder, $wallabagUrl, LoggerInterface $logger) + public function __construct(HttpClient $client, $baseFolder, $wallabagUrl, LoggerInterface $logger, MessageFactory $messageFactory = null) { - $this->client = $client; + $this->client = new HttpMethodsClient(new PluginClient($client, [new ErrorPlugin()]), $messageFactory ?: MessageFactoryDiscovery::find()); $this->baseFolder = $baseFolder; $this->wallabagUrl = rtrim($wallabagUrl, '/'); $this->logger = $logger; @@ -30,6 +37,23 @@ class DownloadImages $this->setFolder(); } + /** + * Process the html and extract images URLs from it. + * + * @param string $html + * + * @return string[] + */ + public static function extractImagesUrlsFromHtml($html) + { + $crawler = new Crawler($html); + $imagesCrawler = $crawler->filterXpath('//img'); + $imagesUrls = $imagesCrawler->extract(['src']); + $imagesSrcsetUrls = self::getSrcsetUrls($imagesCrawler); + + return array_unique(array_merge($imagesUrls, $imagesSrcsetUrls)); + } + /** * Process the html and extract image from it, save them to local and return the updated html. * @@ -41,13 +65,7 @@ class DownloadImages */ public function processHtml($entryId, $html, $url) { - $crawler = new Crawler($html); - $imagesCrawler = $crawler - ->filterXpath('//img'); - $imagesUrls = $imagesCrawler - ->extract(['src']); - $imagesSrcsetUrls = $this->getSrcsetUrls($imagesCrawler); - $imagesUrls = array_unique(array_merge($imagesUrls, $imagesSrcsetUrls)); + $imagesUrls = self::extractImagesUrlsFromHtml($html); $relativePath = $this->getRelativePath($entryId); @@ -122,7 +140,7 @@ class DownloadImages $localPath = $folderPath . '/' . $hashImage . '.' . $ext; try { - $im = imagecreatefromstring($res->getBody()); + $im = imagecreatefromstring((string) $res->getBody()); } catch (\Exception $e) { $im = false; } @@ -135,7 +153,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': @@ -181,29 +213,30 @@ class DownloadImages /** * Get images urls from the srcset image attribute. * - * @param Crawler $imagesCrawler - * * @return array An array of urls */ - private function getSrcsetUrls(Crawler $imagesCrawler) + private static function getSrcsetUrls(Crawler $imagesCrawler) { $urls = []; - $iterator = $imagesCrawler - ->getIterator(); + $iterator = $imagesCrawler->getIterator(); + while ($iterator->valid()) { $srcsetAttribute = $iterator->current()->getAttribute('srcset'); + if ('' !== $srcsetAttribute) { // Couldn't start with " OR ' OR a white space // Could be one or more white space // Must be one or more digits followed by w OR x $pattern = "/(?:[^\"'\s]+\s*(?:\d+[wx])+)/"; preg_match_all($pattern, $srcsetAttribute, $matches); + $srcset = \call_user_func_array('array_merge', $matches); $srcsetUrls = array_map(function ($src) { return trim(explode(' ', $src, 2)[0]); }, $srcset); $urls = array_merge($srcsetUrls, $urls); } + $iterator->next(); } @@ -260,33 +293,29 @@ class DownloadImages return $url; } - $base = new \SimplePie_IRI($base); + $base = new Uri($base); - // remove '//' in URL path (causes URLs not to resolve properly) - if (isset($base->ipath)) { - $base->ipath = preg_replace('!//+!', '/', $base->ipath); + // in case the url has no scheme & host + if ('' === $base->getAuthority() || '' === $base->getScheme()) { + $this->logger->error('DownloadImages: Can not make an absolute link', ['base' => $base, 'url' => $url]); + + return false; } - if ($absolute = \SimplePie_IRI::absolutize($base, $url)) { - return $absolute->get_uri(); - } - - $this->logger->error('DownloadImages: Can not make an absolute link', ['base' => $base, 'url' => $url]); - - return false; + return (string) UriResolver::resolve($base, new Uri($url)); } /** * Retrieve and validate the extension from the response of the url of the image. * - * @param Response $res Guzzle Response - * @param string $imagePath Path from the src image from the content (used for log only) + * @param ResponseInterface $res Http Response + * @param string $imagePath Path from the src image from the content (used for log only) * * @return string|false Extension name or false if validation failed */ - private function getExtensionFromResponse(Response $res, $imagePath) + private function getExtensionFromResponse(ResponseInterface $res, $imagePath) { - $ext = $this->mimeGuesser->guess($res->getHeader('content-type')); + $ext = $this->mimeGuesser->guess(current($res->getHeader('content-type'))); $this->logger->debug('DownloadImages: Checking extension', ['ext' => $ext, 'header' => $res->getHeader('content-type')]); // ok header doesn't have the extension, try a different way diff --git a/src/Wallabag/CoreBundle/Helper/EntriesExport.php b/src/Wallabag/CoreBundle/Helper/EntriesExport.php index cbf1037bf..f981ee50e 100644 --- a/src/Wallabag/CoreBundle/Helper/EntriesExport.php +++ b/src/Wallabag/CoreBundle/Helper/EntriesExport.php @@ -85,7 +85,7 @@ class EntriesExport public function updateAuthor($method) { if ('entry' !== $method) { - $this->author = $method . ' authors'; + $this->author = 'Various authors'; return $this; } @@ -150,8 +150,6 @@ class EntriesExport */ $book->setTitle($this->title); - // Could also be the ISBN number, prefered for published books, or a UUID. - $book->setIdentifier($this->title, EPub::IDENTIFIER_URI); // Not needed, but included for the example, Language is mandatory, but EPub defaults to "en". Use RFC3066 Language codes, such as "en", "da", "fr" etc. $book->setLanguage($this->language); $book->setDescription('Some articles saved on my wallabag'); @@ -167,12 +165,9 @@ class EntriesExport $book->addDublinCoreMetadata(DublinCore::CONTRIBUTOR, 'PHP'); $book->addDublinCoreMetadata(DublinCore::CONTRIBUTOR, 'wallabag'); - /* - * Front page - */ - if (file_exists($this->logoPath)) { - $book->setCoverImage('Cover.png', file_get_contents($this->logoPath), 'image/png'); - } + $entryIds = []; + $entryCount = \count($this->entries); + $i = 0; /* * Adding actual entries @@ -180,21 +175,48 @@ class EntriesExport // set tags as subjects foreach ($this->entries as $entry) { + ++$i; + + /* + * Front page + * Set if there's only one entry in the given set + */ + if (1 === $entryCount && null !== $entry->getPreviewPicture()) { + $book->setCoverImage($entry->getPreviewPicture()); + } + foreach ($entry->getTags() as $tag) { $book->setSubject($tag->getLabel()); } + $filename = sha1(sprintf('%s:%s', $entry->getUrl(), $entry->getTitle())); - // the reader in Kobo Devices doesn't likes special caracters - // in filenames, we limit to A-z/0-9 - $filename = preg_replace('/[^A-Za-z0-9\-]/', '', $entry->getTitle()); + $publishedBy = $entry->getPublishedBy(); + $authors = $this->translator->trans('export.unknown'); + if (!empty($publishedBy)) { + $authors = implode(',', $publishedBy); + } - $titlepage = $content_start . '

' . $entry->getTitle() . '

' . $this->getExportInformation('PHPePub') . $bookEnd; - $book->addChapter('Title', 'Title.html', $titlepage, true, EPub::EXTERNAL_REF_ADD); + $titlepage = $content_start . + '

' . $entry->getTitle() . '

' . + '
' . + '
' . $this->translator->trans('entry.view.published_by') . '
' . $authors . '
' . + '
' . $this->translator->trans('entry.metadata.reading_time') . '
' . $this->translator->trans('entry.metadata.reading_time_minutes_short', ['%readingTime%' => $entry->getReadingTime()]) . '
' . + '
' . $this->translator->trans('entry.metadata.added_on') . '
' . $entry->getCreatedAt()->format('Y-m-d') . '
' . + '
' . $this->translator->trans('entry.metadata.address') . '
' . $entry->getUrl() . '
' . + '
' . + $bookEnd; + $book->addChapter("Entry {$i} of {$entryCount}", "{$filename}_cover.html", $titlepage, true, EPub::EXTERNAL_REF_ADD); $chapter = $content_start . $entry->getContent() . $bookEnd; - $book->addChapter($entry->getTitle(), htmlspecialchars($filename) . '.html', $chapter, true, EPub::EXTERNAL_REF_ADD); + + $entryIds[] = $entry->getId(); + $book->addChapter($entry->getTitle(), "{$filename}.html", $chapter, true, EPub::EXTERNAL_REF_ADD); } - $book->buildTOC(); + $book->addChapter('Notices', 'Cover2.html', $content_start . $this->getExportInformation('PHPePub') . $bookEnd); + + // Could also be the ISBN number, prefered for published books, or a UUID. + $hash = sha1(sprintf('%s:%s', $this->wallabagUrl, implode(',', $entryIds))); + $book->setIdentifier(sprintf('urn:wallabag:%s', $hash), EPub::IDENTIFIER_URI); return Response::create( $book->getBook(), @@ -202,7 +224,7 @@ class EntriesExport [ 'Content-Description' => 'File Transfer', 'Content-type' => 'application/epub+zip', - 'Content-Disposition' => 'attachment; filename="' . $this->title . '.epub"', + 'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.epub"', 'Content-Transfer-Encoding' => 'binary', ] ); @@ -244,9 +266,6 @@ class EntriesExport } $mobi->setContentProvider($content); - // the browser inside Kindle Devices doesn't likes special caracters either, we limit to A-z/0-9 - $this->title = preg_replace('/[^A-Za-z0-9\-]/', '', $this->title); - return Response::create( $mobi->toString(), 200, @@ -254,7 +273,7 @@ class EntriesExport 'Accept-Ranges' => 'bytes', 'Content-Description' => 'File Transfer', 'Content-type' => 'application/x-mobipocket-ebook', - 'Content-Disposition' => 'attachment; filename="' . $this->title . '.mobi"', + 'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.mobi"', 'Content-Transfer-Encoding' => 'binary', ] ); @@ -278,14 +297,6 @@ class EntriesExport $pdf->SetSubject('Articles via wallabag'); $pdf->SetKeywords('wallabag'); - /* - * Front page - */ - $pdf->AddPage(); - $intro = '

' . $this->title . '

' . $this->getExportInformation('tcpdf'); - - $pdf->writeHTMLCell(0, 0, '', '', $intro, 0, 1, 0, true, '', true); - /* * Adding actual entries */ @@ -294,6 +305,22 @@ class EntriesExport $pdf->SetKeywords($tag->getLabel()); } + $publishedBy = $entry->getPublishedBy(); + $authors = $this->translator->trans('export.unknown'); + if (!empty($publishedBy)) { + $authors = implode(',', $publishedBy); + } + + $pdf->addPage(); + $html = '

' . $entry->getTitle() . '

' . + '
' . + '
' . $this->translator->trans('entry.view.published_by') . '
' . $authors . '
' . + '
' . $this->translator->trans('entry.metadata.reading_time') . '
' . $this->translator->trans('entry.metadata.reading_time_minutes_short', ['%readingTime%' => $entry->getReadingTime()]) . '
' . + '
' . $this->translator->trans('entry.metadata.added_on') . '
' . $entry->getCreatedAt()->format('Y-m-d') . '
' . + '
' . $this->translator->trans('entry.metadata.address') . '
' . $entry->getUrl() . '
' . + '
'; + $pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true); + $pdf->AddPage(); $html = '

' . $entry->getTitle() . '

'; $html .= $entry->getContent(); @@ -301,6 +328,14 @@ class EntriesExport $pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true); } + /* + * Last page + */ + $pdf->AddPage(); + $html = $this->getExportInformation('tcpdf'); + + $pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true); + // set image scale factor $pdf->setImageScale(PDF_IMAGE_SCALE_RATIO); @@ -310,7 +345,7 @@ class EntriesExport [ 'Content-Description' => 'File Transfer', 'Content-type' => 'application/pdf', - 'Content-Disposition' => 'attachment; filename="' . $this->title . '.pdf"', + 'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.pdf"', 'Content-Transfer-Encoding' => 'binary', ] ); @@ -356,7 +391,7 @@ class EntriesExport 200, [ 'Content-type' => 'application/csv', - 'Content-Disposition' => 'attachment; filename="' . $this->title . '.csv"', + 'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.csv"', 'Content-Transfer-Encoding' => 'UTF-8', ] ); @@ -374,7 +409,7 @@ class EntriesExport 200, [ 'Content-type' => 'application/json', - 'Content-Disposition' => 'attachment; filename="' . $this->title . '.json"', + 'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.json"', 'Content-Transfer-Encoding' => 'UTF-8', ] ); @@ -392,7 +427,7 @@ class EntriesExport 200, [ 'Content-type' => 'application/xml', - 'Content-Disposition' => 'attachment; filename="' . $this->title . '.xml"', + 'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.xml"', 'Content-Transfer-Encoding' => 'UTF-8', ] ); @@ -418,7 +453,7 @@ class EntriesExport 200, [ 'Content-type' => 'text/plain', - 'Content-Disposition' => 'attachment; filename="' . $this->title . '.txt"', + 'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.txt"', 'Content-Transfer-Encoding' => 'UTF-8', ] ); @@ -461,4 +496,15 @@ class EntriesExport return str_replace('%IMAGE%', '', $info); } + + /** + * Return a sanitized version of the title by applying translit iconv + * and removing non alphanumeric characters, - and space. + * + * @return string Sanitized filename + */ + private function getSanitizedFilename() + { + return preg_replace('/[^A-Za-z0-9\- \']/', '', iconv('utf-8', 'us-ascii//TRANSLIT', $this->title)); + } } diff --git a/src/Wallabag/CoreBundle/Helper/FileCookieJar.php b/src/Wallabag/CoreBundle/Helper/FileCookieJar.php new file mode 100644 index 000000000..9a63e9493 --- /dev/null +++ b/src/Wallabag/CoreBundle/Helper/FileCookieJar.php @@ -0,0 +1,91 @@ +logger = $logger; + } + + /** + * Saves the cookies to a file. + * + * @param string $filename File to save + * + * @throws \RuntimeException if the file cannot be found or created + */ + public function save($filename) + { + $json = []; + foreach ($this as $cookie) { + if ($cookie->getExpires() && !$cookie->getDiscard()) { + $json[] = $cookie->toArray(); + } + } + + if (false === file_put_contents($filename, json_encode($json), LOCK_EX)) { + // @codeCoverageIgnoreStart + throw new \RuntimeException("Unable to save file {$filename}"); + // @codeCoverageIgnoreEnd + } + } + + /** + * Load cookies from a JSON formatted file. + * + * Old cookies are kept unless overwritten by newly loaded ones. + * + * @param string $filename cookie file to load + * + * @throws \RuntimeException if the file cannot be loaded + */ + public function load($filename) + { + $json = file_get_contents($filename); + if (false === $json) { + // @codeCoverageIgnoreStart + throw new \RuntimeException("Unable to load file {$filename}"); + // @codeCoverageIgnoreEnd + } + + try { + $data = Utils::jsonDecode($json, true); + } catch (\InvalidArgumentException $e) { + $this->logger->error('JSON inside the cookie is broken', [ + 'json' => $json, + 'error_msg' => $e->getMessage(), + ]); + + // cookie file is invalid, just ignore the exception and it'll reset the whole cookie file + $data = ''; + } + + if (\is_array($data)) { + foreach (Utils::jsonDecode($json, true) as $cookie) { + $this->setCookie(new SetCookie($cookie)); + } + } elseif (\strlen($data)) { + throw new \RuntimeException("Invalid cookie file: {$filename}"); + } + } +} diff --git a/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php b/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php index 4602a6841..ea864acbb 100644 --- a/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php +++ b/src/Wallabag/CoreBundle/Helper/HttpClientFactory.php @@ -2,16 +2,18 @@ namespace Wallabag\CoreBundle\Helper; -use Graby\Ring\Client\SafeCurlHandler; -use GuzzleHttp\Client; +use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Event\SubscriberInterface; +use Http\Adapter\Guzzle5\Client as GuzzleAdapter; +use Http\Client\HttpClient; +use Http\HttplugBundle\ClientFactory\ClientFactory; use Psr\Log\LoggerInterface; /** - * Builds and configures the Guzzle HTTP client. + * Builds and configures the HTTP client. */ -class HttpClientFactory +class HttpClientFactory implements ClientFactory { /** @var [\GuzzleHttp\Event\SubscriberInterface] */ private $subscribers = []; @@ -25,9 +27,7 @@ class HttpClientFactory /** * HttpClientFactory constructor. * - * @param \GuzzleHttp\Cookie\CookieJar $cookieJar - * @param string $restrictedAccess This param is a kind of boolean. Values: 0 or 1 - * @param LoggerInterface $logger + * @param string $restrictedAccess This param is a kind of boolean. Values: 0 or 1 */ public function __construct(CookieJar $cookieJar, $restrictedAccess, LoggerInterface $logger) { @@ -36,36 +36,39 @@ class HttpClientFactory $this->logger = $logger; } - /** - * @return \GuzzleHttp\Client|null - */ - public function buildHttpClient() - { - $this->logger->log('debug', 'Restricted access config enabled?', ['enabled' => (int) $this->restrictedAccess]); - - if (0 === (int) $this->restrictedAccess) { - return; - } - - // we clear the cookie to avoid websites who use cookies for analytics - $this->cookieJar->clear(); - // need to set the (shared) cookie jar - $client = new Client(['handler' => new SafeCurlHandler(), 'defaults' => ['cookies' => $this->cookieJar]]); - - foreach ($this->subscribers as $subscriber) { - $client->getEmitter()->attach($subscriber); - } - - return $client; - } - /** * Adds a subscriber to the HTTP client. - * - * @param SubscriberInterface $subscriber */ public function addSubscriber(SubscriberInterface $subscriber) { $this->subscribers[] = $subscriber; } + + /** + * Input an array of configuration to be able to create a HttpClient. + * + * @return HttpClient + */ + public function createClient(array $config = []) + { + $this->logger->log('debug', 'Restricted access config enabled?', ['enabled' => (int) $this->restrictedAccess]); + + if (0 === (int) $this->restrictedAccess) { + return new GuzzleAdapter(new GuzzleClient($config)); + } + + // we clear the cookie to avoid websites who use cookies for analytics + $this->cookieJar->clear(); + if (!isset($config['defaults']['cookies'])) { + // need to set the (shared) cookie jar + $config['defaults']['cookies'] = $this->cookieJar; + } + + $guzzle = new GuzzleClient($config); + foreach ($this->subscribers as $subscriber) { + $guzzle->getEmitter()->attach($subscriber); + } + + return new GuzzleAdapter($guzzle); + } } diff --git a/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php b/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php index 1c2c50931..3d56a6d8e 100644 --- a/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php +++ b/src/Wallabag/CoreBundle/Helper/PreparePagerForEntries.php @@ -20,10 +20,9 @@ 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 null|Pagerfanta + * @return Pagerfanta|null */ public function prepare(AdapterInterface $adapter, User $user = null) { diff --git a/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php b/src/Wallabag/CoreBundle/Helper/RuleBasedTagger.php index 63f65067d..d48e2469a 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; @@ -55,8 +56,6 @@ class RuleBasedTagger /** * Apply all the tagging rules defined by a user on its entries. * - * @param User $user - * * @return array A list of modified entries */ public function tagAllForUser(User $user) @@ -108,8 +107,6 @@ class RuleBasedTagger /** * Retrieves the tagging rules for a given user. * - * @param User $user - * * @return array */ private function getRulesForUser(User $user) diff --git a/src/Wallabag/CoreBundle/Helper/TagsAssigner.php b/src/Wallabag/CoreBundle/Helper/TagsAssigner.php index e6b4989f8..433b09fef 100644 --- a/src/Wallabag/CoreBundle/Helper/TagsAssigner.php +++ b/src/Wallabag/CoreBundle/Helper/TagsAssigner.php @@ -21,7 +21,6 @@ class TagsAssigner /** * Assign some tags to an entry. * - * @param Entry $entry * @param array|string $tags An array of tag or a string coma separated of tag * @param array $entitiesReady Entities from the EntityManager which are persisted but not yet flushed * It is mostly to fix duplicate tag on import @see http://stackoverflow.com/a/7879164/569101 diff --git a/src/Wallabag/CoreBundle/Helper/UrlHasher.php b/src/Wallabag/CoreBundle/Helper/UrlHasher.php new file mode 100644 index 000000000..6753745f7 --- /dev/null +++ b/src/Wallabag/CoreBundle/Helper/UrlHasher.php @@ -0,0 +1,22 @@ +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 83379998d..bfd079377 100644 --- a/src/Wallabag/CoreBundle/Repository/EntryRepository.php +++ b/src/Wallabag/CoreBundle/Repository/EntryRepository.php @@ -3,11 +3,13 @@ namespace Wallabag\CoreBundle\Repository; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; use Pagerfanta\Adapter\DoctrineORMAdapter; use Pagerfanta\Pagerfanta; use Wallabag\CoreBundle\Entity\Entry; use Wallabag\CoreBundle\Entity\Tag; +use Wallabag\CoreBundle\Helper\UrlHasher; class EntryRepository extends EntityRepository { @@ -50,7 +52,7 @@ class EntryRepository extends EntityRepository public function getBuilderForArchiveByUser($userId) { return $this - ->getSortedQueryBuilderByUser($userId) + ->getSortedQueryBuilderByUser($userId, 'archivedAt', 'desc') ->andWhere('e.isArchived = true') ; } @@ -110,8 +112,7 @@ class EntryRepository extends EntityRepository */ public function getBuilderForUntaggedByUser($userId) { - return $this - ->sortQueryBuilder($this->getRawBuilderForUntaggedByUser($userId)); + return $this->sortQueryBuilder($this->getRawBuilderForUntaggedByUser($userId)); } /** @@ -128,6 +129,21 @@ class EntryRepository extends EntityRepository ->andWhere('t.id is null'); } + /** + * Retrieve the number of untagged entries for a user. + * + * @param int $userId + * + * @return int + */ + public function countUntaggedEntriesByUser($userId) + { + return (int) $this->getRawBuilderForUntaggedByUser($userId) + ->select('count(e.id)') + ->getQuery() + ->getSingleScalarResult(); + } + /** * Find Entries. * @@ -139,15 +155,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); } @@ -185,10 +216,16 @@ class EntryRepository extends EntityRepository } } + if (!\in_array(strtolower($order), ['asc', 'desc'], true)) { + throw new \Exception('Order "' . $order . '" parameter is wrong, allowed: asc or desc'); + } + if ('created' === $sort) { $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); @@ -269,7 +306,6 @@ class EntryRepository extends EntityRepository * DELETE et FROM entry_tag et WHERE et.entry_id IN ( SELECT e.id FROM entry e WHERE e.user_id = :userId ) AND et.tag_id = :tagId * * @param int $userId - * @param Tag $tag */ public function removeTag($userId, Tag $tag) { @@ -320,15 +356,44 @@ 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 + * @return Entry|false */ public function findByUrlAndUserId($url, $userId) { + return $this->findByHashedUrlAndUserId( + UrlHasher::hashUrl($url), + $userId + ); + } + + /** + * 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|false + */ + public function findByHashedUrlAndUserId($hashedUrl, $userId) + { + // try first using hashed_url (to use the database index) $res = $this->createQueryBuilder('e') - ->where('e.url = :url')->setParameter('url', urldecode($url)) + ->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); + } + + // then try using hashed_given_url (to use the database index) + $res = $this->createQueryBuilder('e') + ->where('e.hashedGivenUrl = :hashed_given_url')->setParameter('hashed_given_url', $hashedUrl) ->andWhere('e.user = :user_id')->setParameter('user_id', $userId) ->getQuery() ->getResult(); @@ -412,8 +477,8 @@ class EntryRepository extends EntityRepository /** * Find all entries by url and owner. * - * @param $url - * @param $userId + * @param string $url + * @param int $userId * * @return array */ @@ -426,6 +491,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. * @@ -456,15 +564,13 @@ class EntryRepository extends EntityRepository /** * Return the given QueryBuilder with an orderBy() call. * - * @param QueryBuilder $qb - * @param string $sortBy - * @param string $direction + * @param string $sortBy + * @param string $direction * * @return QueryBuilder */ 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/Repository/SiteCredentialRepository.php b/src/Wallabag/CoreBundle/Repository/SiteCredentialRepository.php index 369067612..aeadd7704 100644 --- a/src/Wallabag/CoreBundle/Repository/SiteCredentialRepository.php +++ b/src/Wallabag/CoreBundle/Repository/SiteCredentialRepository.php @@ -19,16 +19,16 @@ class SiteCredentialRepository extends \Doctrine\ORM\EntityRepository /** * Retrieve one username/password for the given host and userId. * - * @param string $host - * @param int $userId + * @param array $hosts An array of host to look for + * @param int $userId * - * @return null|array + * @return array|null */ - public function findOneByHostAndUser($host, $userId) + public function findOneByHostsAndUser($hosts, $userId) { $res = $this->createQueryBuilder('s') ->select('s.username', 's.password') - ->where('s.host = :hostname')->setParameter('hostname', $host) + ->where('s.host IN (:hosts)')->setParameter('hosts', $hosts) ->andWhere('s.user = :userId')->setParameter('userId', $userId) ->setMaxResults(1) ->getQuery() diff --git a/src/Wallabag/CoreBundle/Repository/TagRepository.php b/src/Wallabag/CoreBundle/Repository/TagRepository.php index 3ae9d4141..8464a6a55 100644 --- a/src/Wallabag/CoreBundle/Repository/TagRepository.php +++ b/src/Wallabag/CoreBundle/Repository/TagRepository.php @@ -3,6 +3,7 @@ namespace Wallabag\CoreBundle\Repository; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\QueryBuilder; use Wallabag\CoreBundle\Entity\Tag; class TagRepository extends EntityRepository @@ -45,12 +46,8 @@ class TagRepository extends EntityRepository */ public function findAllTags($userId) { - $ids = $this->createQueryBuilder('t') + $ids = $this->getQueryBuilderByUser($userId) ->select('t.id') - ->leftJoin('t.entries', 'e') - ->where('e.user = :userId')->setParameter('userId', $userId) - ->groupBy('t.id') - ->orderBy('t.slug') ->getQuery() ->getArrayResult(); @@ -71,18 +68,30 @@ class TagRepository extends EntityRepository */ public function findAllFlatTagsWithNbEntries($userId) { - return $this->createQueryBuilder('t') + return $this->getQueryBuilderByUser($userId) ->select('t.id, t.label, t.slug, count(e.id) as nbEntries') ->distinct(true) - ->leftJoin('t.entries', 'e') - ->where('e.user = :userId') - ->groupBy('t.id') - ->orderBy('t.slug') - ->setParameter('userId', $userId) ->getQuery() ->getArrayResult(); } + public function findByLabelsAndUser($labels, $userId) + { + $qb = $this->getQueryBuilderByUser($userId) + ->select('t.id'); + + $ids = $qb->andWhere($qb->expr()->in('t.label', $labels)) + ->getQuery() + ->getArrayResult(); + + $tags = []; + foreach ($ids as $id) { + $tags[] = $this->find($id); + } + + return $tags; + } + /** * Used only in test case to get a tag for our entry. * @@ -101,13 +110,9 @@ class TagRepository extends EntityRepository public function findForArchivedArticlesByUser($userId) { - $ids = $this->createQueryBuilder('t') + $ids = $this->getQueryBuilderByUser($userId) ->select('t.id') - ->leftJoin('t.entries', 'e') - ->where('e.user = :userId')->setParameter('userId', $userId) ->andWhere('e.isArchived = true') - ->groupBy('t.id') - ->orderBy('t.slug') ->getQuery() ->getArrayResult(); @@ -118,4 +123,20 @@ class TagRepository extends EntityRepository return $tags; } + + /** + * Retrieve a sorted list of tags used by a user. + * + * @param int $userId + * + * @return QueryBuilder + */ + private function getQueryBuilderByUser($userId) + { + return $this->createQueryBuilder('t') + ->leftJoin('t.entries', 'e') + ->where('e.user = :userId')->setParameter('userId', $userId) + ->groupBy('t.id') + ->orderBy('t.slug'); + } } diff --git a/src/Wallabag/CoreBundle/Resources/config/services.yml b/src/Wallabag/CoreBundle/Resources/config/services.yml index 85306276d..3f3d4de71 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" @@ -42,7 +42,7 @@ services: - error_message: '%wallabag_core.fetching_error_message%' error_message_title: '%wallabag_core.fetching_error_message_title%' - - "@wallabag_core.guzzle.http_client" + - "@wallabag_core.http_client" - "@wallabag_core.graby.config_builder" calls: - [ setLogger, [ "@logger" ] ] @@ -55,9 +55,8 @@ services: - {} - "@logger" - wallabag_core.guzzle.http_client: - class: GuzzleHttp\ClientInterface - factory: ["@wallabag_core.guzzle.http_client_factory", buildHttpClient] + wallabag_core.http_client: + alias: 'httplug.client.wallabag_core' wallabag_core.guzzle_authenticator.config_builder: class: Wallabag\CoreBundle\GuzzleSiteAuthenticator\GrabySiteConfigBuilder @@ -73,7 +72,7 @@ services: bd_guzzle_site_authenticator.site_config_builder: alias: wallabag_core.guzzle_authenticator.config_builder - wallabag_core.guzzle.http_client_factory: + wallabag_core.http_client_factory: class: Wallabag\CoreBundle\Helper\HttpClientFactory arguments: - "@wallabag_core.guzzle.cookie_jar" @@ -83,8 +82,10 @@ services: - ["addSubscriber", ["@bd_guzzle_site_authenticator.authenticator_subscriber"]] wallabag_core.guzzle.cookie_jar: - class: GuzzleHttp\Cookie\FileCookieJar - arguments: ["%kernel.cache_dir%/cookiejar.json"] + class: Wallabag\CoreBundle\Helper\FileCookieJar + arguments: + - "@logger" + - "%kernel.cache_dir%/cookiejar.json" wallabag_core.content_proxy: class: Wallabag\CoreBundle\Helper\ContentProxy @@ -181,6 +182,7 @@ services: wallabag_core.exception_controller: class: Wallabag\CoreBundle\Controller\ExceptionController + public: true arguments: - '@twig' - '%kernel.debug%' @@ -211,10 +213,38 @@ services: - "@logger" wallabag_core.entry.download_images.client: - class: GuzzleHttp\Client + alias: 'httplug.client.wallabag_core.entry.download_images' wallabag_core.helper.crypto_proxy: class: Wallabag\CoreBundle\Helper\CryptoProxy 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 e3ff21f14..47de066f9 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.da.yml @@ -33,10 +33,12 @@ menu: back_to_unread: 'Tilbage til de ulÊste artikler' # users_management: 'Users management' # site_credentials: 'Site credentials' + # quickstart: "Quickstart" top: 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 +55,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: @@ -65,12 +68,8 @@ config: items_per_page_label: 'Poster pr. side' language_label: 'Sprog' reading_speed: - # label: 'Reading speed' + # label: 'Reading speed (words per minute)' # help_message: 'You can use online tools to estimate your reading speed:' - # 100_word: 'I read ~100 words per minute' - # 200_word: 'I read ~200 words per minute' - # 300_word: 'I read ~300 words per minute' - # 400_word: 'I read ~400 words per minute' action_mark_as_read: # label: 'Where do you want to be redirected to after marking an article as read?' # redirect_homepage: 'To the homepage' @@ -83,25 +82,35 @@ 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: + # token_revoke: 'Revoke the token' + 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." + # login_label: 'Login (can not be changed)' 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. @@ -127,6 +136,15 @@ config: # edit_rule_label: 'edit' # rule_label: 'Rule' # tags_label: 'Tags' + # card: + # new_tagging_rule: Create a tagging rule + # import_tagging_rules: Import tagging rules + # import_tagging_rules_detail: You have to select the JSON file you previously exported. + # export_tagging_rules: Export tagging rules + # export_tagging_rules_detail: This will download a JSON file that you can use to import tagging rules elsewhere or to backup them. + # file_label: JSON file + # import_submit: Import + # export: Export # faq: # title: 'FAQ' # tagging_rules_definition_title: 'What does « tagging rules » mean?' @@ -159,6 +177,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' @@ -237,7 +264,7 @@ entry: # provided_by: 'Provided by' new: page_title: 'Gem ny artikel' - placeholder: 'http://website.com' + placeholder: 'https://website.dk' form_new: url_label: Url search: @@ -253,6 +280,11 @@ entry: confirm: # delete: "Are you sure you want to remove that article?" # delete_tag: "Are you sure you want to remove that tag from that article?" + metadata: + # reading_time: "Estimated reading time" + # reading_time_minutes_short: "%readingTime% min" + # address: "Address" + # added_on: "Added on" about: page_title: 'Om' @@ -348,7 +380,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' @@ -396,12 +428,16 @@ tag: list: # number_on_the_page: '{0} There is no tag.|{1} There is one tag.|]1,Inf[ There are %count% tags.' # see_untagged_entries: 'See untagged entries' + # no_untagged_entries: 'There are no untagged entries.' 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.

' +# unknown: 'Unknown' import: # page_title: 'Import' @@ -429,6 +465,9 @@ import: # wallabag_v2: # page_title: 'Import > Wallabag v2' # description: 'This importer will import all your wallabag v2 articles. Go to All articles, then, on the export sidebar, click on "JSON". You will have a "All articles.json" file.' + # elcurator: + # page_title: 'Import > elCurator' + # description: 'This importer will import all your elCurator articles. Go to your preferences in your elCurator account and then, export your content. You will have a JSON file.' # readability: # page_title: 'Import > Readability' # description: 'This importer will import all your Readability articles. On the tools (https://www.readability.com/tools/) page, click on "Export your data" in the "Data Export" section. You will received an email to download a json (which does not end with .json in fact).' @@ -482,6 +521,7 @@ developer: # redirect_uris_label: 'Redirect URIs' # save_label: 'Create a new client' # action_back: 'Back' + # copy_to_clipboard: Copy # client_parameter: # page_title: 'API clients management > Client parameters' # page_description: 'Here are your client parameters.' @@ -523,7 +563,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? @@ -544,7 +585,7 @@ site_credential: # create_new_one: Create a new credential # form: # username_label: 'Username' - # host_label: 'Host' + # host_label: 'Host (subdomain.example.org, .example.org, etc.)' # password_label: 'Password' # save: Save # delete: Delete @@ -561,14 +602,18 @@ 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' + # feed_token_revoked: 'RSS token revoked' # annotations_reset: Annotations reset # tags_reset: Tags reset # entries_reset: Entries reset # archived_reset: Archived entries deleted + # otp_enabled: Two-factor authentication enabled + # tagging_rules_imported: Tagging rules imported + # tagging_rules_not_imported: Error while importing tagging rules entry: notice: # entry_already_saved: 'Entry already saved on %date%' @@ -582,9 +627,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 c297ffb5d..50e67d47b 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.de.yml @@ -33,10 +33,12 @@ menu: back_to_unread: 'ZurÃŒck zu ungelesenen Artikeln' users_management: 'Benutzerverwaltung' site_credentials: 'Zugangsdaten' + quickstart: "Schnelleinstieg" top: 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 +55,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: @@ -65,12 +68,8 @@ config: items_per_page_label: 'EintrÀge pro Seite' language_label: 'Sprache' reading_speed: - label: 'Lesegeschwindigkeit' + label: 'Lesegeschwindigkeit (Wörter pro Minute)' help_message: 'Du kannst Online-Tools nutzen, um deine Lesegeschwindigkeit herauszufinden.' - 100_word: 'Ich lese ~100 Wörter pro Minute' - 200_word: 'Ich lese ~200 Wörter pro Minute' - 300_word: 'Ich lese ~300 Wörter pro Minute' - 400_word: 'Ich lese ~400 Wörter pro Minute' action_mark_as_read: label: 'Wohin soll nach dem Gelesenmarkieren eines Artikels weitergeleitet werden?' redirect_homepage: 'Zur Homepage' @@ -83,25 +82,35 @@ 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: + # token_revoke: 'Revoke the token' + 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." + # login_label: 'Login (can not be changed)' 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.' @@ -127,6 +136,15 @@ config: edit_rule_label: 'bearbeiten' rule_label: 'Regel' tags_label: 'Tags' + # card: + # new_tagging_rule: Create a tagging rule + # import_tagging_rules: Import tagging rules + # import_tagging_rules_detail: You have to select the JSON file you previously exported. + # export_tagging_rules: Export tagging rules + # export_tagging_rules_detail: This will download a JSON file that you can use to import tagging rules elsewhere or to backup them. + # file_label: JSON file + # import_submit: Import + # export: Export faq: title: 'FAQ' tagging_rules_definition_title: 'Was bedeuten die "Tagging-Regeln"?' @@ -159,6 +177,15 @@ config: and: 'Eine Regel UND eine andere' matches: 'Testet, ob eine Variable auf eine Suche zutrifft (Groß- und Kleinschreibung wird nicht berÃŒcksichtigt).
Beispiel: title matches "Fußball"' notmatches: 'Testet, ob ein Titel nicht auf eine Suche zutrifft (Groß- und Kleinschreibung wird nicht berÃŒcksichtigt).
Beispiel: title notmatches "Fußball"' + 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: 'Titel des Eintrags' @@ -253,6 +280,11 @@ entry: confirm: delete: 'Bist du sicher, dass du diesen Artikel löschen möchtest?' delete_tag: 'Bist du sicher, dass du diesen Tag vom Artikel entfernen möchtest?' + metadata: + # reading_time: "Estimated reading time" + # reading_time_minutes_short: "%readingTime% min" + # address: "Address" + # added_on: "Added on" about: page_title: 'Über' @@ -348,7 +380,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' @@ -396,12 +428,16 @@ tag: list: number_on_the_page: '{0} Es gibt keine Tags.|{1} Es gibt einen Tag.|]1,Inf[ Es gibt %count% Tags.' see_untagged_entries: 'Zeige nicht getaggte EintrÀge' + # no_untagged_entries: 'There are no untagged entries.' 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.

' + # unknown: 'Unknown' import: page_title: 'Importieren' @@ -429,6 +465,9 @@ import: wallabag_v2: page_title: 'Aus wallabag v2 importieren' description: 'Dieser Import wird all deine Artikel aus wallabag v2 importieren. Gehe auf "Alle Artikel" und dann, in der Exportieren-Seitenleiste auf "JSON". Dabei erhÀltst du eine "All articles.json"-Datei.' + # elcurator: + # page_title: 'Import > elCurator' + # description: 'This importer will import all your elCurator articles. Go to your preferences in your elCurator account and then, export your content. You will have a JSON file.' readability: page_title: 'Aus Readability importieren' description: 'Dieser Import wird all deine Artikel aus Readability importieren. Auf der Tools Seite (https://www.readability.com/tools/) klickst du auf "Exportiere deine Daten" in dem Abschnitt "Datenexport". Du wirst eine E-Mail mit einem Downloadlink zu einer json Datei, die aber nicht auf .json endet, erhalten' @@ -482,6 +521,7 @@ developer: redirect_uris_label: 'Weiterleitungs-URIs' save_label: 'Neuen Client erstellen' action_back: 'ZurÌck' + # copy_to_clipboard: Copy client_parameter: page_title: 'API-Client-Verwaltung > Client-Parameter' page_description: 'Dies sind deine Client-Parameter.' @@ -523,7 +563,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?' @@ -544,7 +585,7 @@ site_credential: create_new_one: 'Einen neuen Seitenzugang anlegen' form: username_label: 'Benutzername' - host_label: 'Host' + host_label: 'Host (subdomain.example.org, .example.org, etc.)' password_label: 'Passwort' save: 'Speichern' delete: 'Löschen' @@ -561,14 +602,18 @@ 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' + # feed_token_revoked: 'RSS token revoked' + annotations_reset: Anmerkungen zurÌcksetzen + tags_reset: Tags zurÌcksetzen + entries_reset: EintrÀge zurÌcksetzen + archived_reset: Archiverte EintrÀge zurÌcksetzen + # otp_enabled: Two-factor authentication enabled + # tagging_rules_imported: Tagging rules imported + # tagging_rules_not_imported: Error while importing tagging rules entry: notice: entry_already_saved: 'Eintrag bereits am %date% gespeichert' @@ -582,9 +627,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 169ae728e..27585f775 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml @@ -5,7 +5,7 @@ security: forgot_password: 'Forgot your password?' submit: 'Login' register: 'Register' - username: 'Username' + username: 'Login' password: 'Password' cancel: 'Cancel' resetting: @@ -33,10 +33,12 @@ menu: back_to_unread: 'Back to unread articles' users_management: 'Users management' site_credentials: 'Site credentials' + quickstart: "Quickstart" top: 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 +55,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: @@ -65,12 +68,8 @@ config: items_per_page_label: 'Items per page' language_label: 'Language' reading_speed: - label: 'Reading speed' + label: 'Reading speed (words per minute)' help_message: 'You can use online tools to estimate your reading speed:' - 100_word: 'I read ~100 words per minute' - 200_word: 'I read ~200 words per minute' - 300_word: 'I read ~300 words per minute' - 400_word: 'I read ~400 words per minute' action_mark_as_read: label: 'What to do after removing, starring or marking as read an article?' redirect_homepage: 'Go to the homepage' @@ -83,25 +82,35 @@ 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: + token_revoke: 'Revoke the token' + 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." + login_label: 'Login (can not be changed)' 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. @@ -127,6 +136,15 @@ config: edit_rule_label: 'edit' rule_label: 'Rule' tags_label: 'Tags' + card: + new_tagging_rule: Create a tagging rule + import_tagging_rules: Import tagging rules + import_tagging_rules_detail: You have to select the JSON file you previously exported. + export_tagging_rules: Export tagging rules + export_tagging_rules_detail: This will download a JSON file that you can use to import tagging rules elsewhere or to backup them. + file_label: JSON file + import_submit: Import + export: Export faq: title: 'FAQ' tagging_rules_definition_title: 'What does « tagging rules » mean?' @@ -159,6 +177,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' @@ -237,7 +264,7 @@ entry: provided_by: 'Provided by' new: page_title: 'Save new entry' - placeholder: 'http://website.com' + placeholder: 'https://website.com' form_new: url_label: Url search: @@ -253,6 +280,11 @@ entry: confirm: delete: "Are you sure you want to remove that article?" delete_tag: "Are you sure you want to remove that tag from that article?" + metadata: + reading_time: "Estimated reading time" + reading_time_minutes_short: "%readingTime% min" + address: "Address" + added_on: "Added on" about: page_title: 'About' @@ -348,7 +380,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' @@ -396,12 +428,16 @@ tag: list: number_on_the_page: '{0} There are no tags.|{1} There is one tag.|]1,Inf[ There are %count% tags.' see_untagged_entries: 'See untagged entries' + no_untagged_entries: 'There are no untagged entries.' 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.

' + unknown: 'Unknown' import: page_title: 'Import' @@ -429,6 +465,9 @@ import: wallabag_v2: page_title: 'Import > Wallabag v2' description: 'This importer will import all your wallabag v2 articles. Go to All articles, then, on the export sidebar, click on "JSON". You will have a "All articles.json" file.' + elcurator: + page_title: 'Import > elCurator' + description: 'This importer will import all your elCurator articles. Go to your preferences in your elCurator account and then, export your content. You will have a JSON file.' readability: page_title: 'Import > Readability' description: 'This importer will import all your Readability articles. On the tools (https://www.readability.com/tools/) page, click on "Export your data" in the "Data Export" section. You will received an email to download a json (which does not end with .json in fact).' @@ -482,6 +521,7 @@ developer: redirect_uris_label: 'Redirect URIs (optional)' save_label: 'Create a new client' action_back: 'Back' + copy_to_clipboard: Copy client_parameter: page_title: 'API clients management > Client parameters' page_description: 'Here are your client parameters.' @@ -515,7 +555,7 @@ user: no: No create_new_one: Create a new user form: - username_label: 'Username' + username_label: 'Login' name_label: 'Name' password_label: 'Password' repeat_new_password_label: 'Repeat new password' @@ -523,13 +563,14 @@ 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? back_to_list: Back to list search: - placeholder: Filter by username or email + placeholder: Filter by login or email site_credential: page_title: Site credentials management @@ -543,8 +584,8 @@ site_credential: no: No create_new_one: Create a new credential form: - username_label: 'Username' - host_label: 'Host' + username_label: 'Login' + host_label: 'Host (subdomain.example.org, .example.org, etc.)' password_label: 'Password' save: Save delete: Delete @@ -561,14 +602,18 @@ 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' + feed_token_revoked: 'RSS token revoked' annotations_reset: Annotations reset tags_reset: Tags reset entries_reset: Entries reset archived_reset: Archived entries deleted + otp_enabled: Two-factor authentication enabled + tagging_rules_imported: Tagging rules imported + tagging_rules_not_imported: Error while importing tagging rules entry: notice: entry_already_saved: 'Entry already saved on %date%' @@ -582,9 +627,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 039a1867c..2cf67176b 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.es.yml @@ -32,11 +32,13 @@ menu: save_link: 'Guardar un enlace' back_to_unread: 'Volver a los artículos sin leer' users_management: 'Configuración de usuarios' - # site_credentials: 'Site credentials' + site_credentials: 'Credenciales del sitio' + quickstart: "Inicio rápido" top: add_new_entry: 'Añadir un nuevo artículo' search: 'Buscar' filter_entries: 'Filtrar los artículos' + random_entry: 'Ir a un artículo aleatório de esta lista' export: 'Exportar' search_form: input_label: 'Introduzca su búsqueda aquí' @@ -45,7 +47,7 @@ footer: wallabag: elsewhere: 'Lleva wallabag contigo' social: 'Social' - powered_by: 'funciona con' + powered_by: 'impulsado por' about: 'Acerca de' stats: Desde el %user_creation% has leído %nb_archives% artículos. ¡Eso hace unos %per_day% por día! @@ -53,11 +55,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: @@ -65,43 +68,49 @@ config: items_per_page_label: 'Número de artículos por página' language_label: 'Idioma' reading_speed: - label: 'Velocidad de lectura' + label: 'Velocidad de lectura (palabras por minuto)' help_message: 'Puede utilizar herramientas en línea para calcular su velocidad de lectura:' - 100_word: 'Leo ~100 palabras por minuto' - 200_word: 'Leo ~200 palabras por minuto' - 300_word: 'Leo ~300 palabras por minuto' - 400_word: 'Leo ~400 palabras por minuto' action_mark_as_read: label: '¿Dónde quieres ser redirigido después de marcar un artículo como leído?' redirect_homepage: 'A la página de inicio' redirect_current_page: 'A la página actual' pocket_consumer_key_label: Clave de consumidor para importar contenidos de Pocket android_configuration: Configura tu aplicación Android - # android_instruction: "Touch here to prefill your Android application" + android_instruction: "Toca aquí para prellenar tu aplicación Android" help_theme: "wallabag es personalizable. Puedes elegir tu tema preferido aquí." help_items_per_page: "Puedes cambiar el número de artículos mostrados en cada página." 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: - description: 'Los feeds RSS de wallabag permiten leer los artículos guardados con su lector RSS favorito. Primero necesitas generar un token.' + form_feed: + description: 'Los feeds RSS de wallabag permiten leer los artículos guardados con tu 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: - unread: 'sin leer' - starred: 'favoritos' - archive: 'archivados' - # all: 'All' - rss_limit: 'Límite de artículos en feed RSS' + token_revoke: 'Revocar token' + feed_links: 'URLs de feeds RSS' + feed_link: + unread: 'Sin leer' + starred: 'Favoritos' + archive: 'Archivados' + all: 'Todos' + 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: "Activar la autenticación de dos pasos significa que recibirás un correo electrónico con un código o que necesitarás usar una aplicación OTP (como Google Authenticator, Authy or FreeOTP) para conseguir un código de utilización única en cada nueva conexión no confiable. No puedes usar los dos métodos." + login_label: 'Nombre de usuario (no se puede cambiar)' 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." + email_label: 'Dirección de correo electrónico' + two_factor: + emailTwoFactor_label: 'Usando el correo electrónico (recibe un código por correo electrónico)' + googleTwoFactor_label: 'Usando una aplicación OTP (abre la aplicación, por ejemplo Google Authenticator, Authy o FreeOTP, para conseguir un código de utilización única)' + table_method: 'Método' + table_state: 'Estado' + table_action: 'Acción' + state_enabled: 'Activado' + state_disabled: 'Desactivado' + action_email: 'Usar correo electrónico' + action_app: 'Usar aplicación OTP' 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. @@ -113,7 +122,7 @@ config: annotations: Eliminar TODAS las anotaciones tags: Eliminar TODAS las etiquetas entries: Eliminar TODOS los artículos - # archived: Remove ALL archived entries + archived: Eliminar TODOS los artículos archivados confirm: ¿Estás completamente seguro? (NO SE PUEDE DESHACER) form_password: description: "Puedes cambiar la contraseña aquí. Tu nueva contraseña debe tener al menos 8 caracteres." @@ -127,6 +136,15 @@ config: edit_rule_label: 'editar' rule_label: 'Regla' tags_label: 'Etiquetas' + card: + new_tagging_rule: Crear una regla de etiquetado + import_tagging_rules: Importar reglas de etiquetado + import_tagging_rules_detail: Debes seleccionar un archivo JSON exportado previamente. + export_tagging_rules: Exportar reglas de etiquetado + export_tagging_rules_detail: Un archivo JSON será descargado y este podrá ser utilizado para volver a importar las reglas de etiquetado o como copia de seguridad. + file_label: Archivo JSON + import_submit: Importar + export: Exportar faq: title: 'Preguntas frecuentes' tagging_rules_definition_title: '¿Qué significa « reglas de etiquetado automático »?' @@ -157,8 +175,17 @@ config: not_equal_to: 'Diferente de
' or: 'Una regla U otra' 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"' + matches: 'Prueba si un sujeto corresponde a una búsqueda (insensible a mayúsculas).
Ejemplo : title matches "fútbol"' + notmatches: 'Prueba si subject no corresponde a una búsqueda (insensible a mayúsculas).
Example: title notmatches "fútbol"' + otp: + page_title: Autenticación de dos pasos + app: + two_factor_code_description_1: Acabas de activar la autenticación en dos factores con OTP, abre tu aplicación OTP y consigue un código de utilización única. Desaparecerá al volver a cargar la página. + two_factor_code_description_2: 'Puedes escanear el código QR con tu aplicación:' + two_factor_code_description_3: 'No olvides guardar los códigos de seguridad en un lugar seguro, los puedes utilizar en caso de que pierdas el accesso a tu aplicación OTP:' + two_factor_code_description_4: 'Prueba un código generado por tu aplicación OTP:' + cancel: Cancelar + enable: Activar entry: default_title: 'Título del artículo' @@ -192,8 +219,8 @@ entry: unread_label: 'Sin leer' preview_picture_label: 'Tiene imagen de previsualización' preview_picture_help: 'Imagen de previsualización' - # is_public_label: 'Has a public link' - # is_public_help: 'Public link' + is_public_label: 'Tiene un enlace público' + is_public_help: 'Enlace público' language_label: 'Idioma' http_status_label: 'Código de estado HTTP' reading_time: @@ -220,7 +247,7 @@ entry: delete: 'Eliminar' add_a_tag: 'Añadir una etiqueta' share_content: 'Compartir' - share_email_label: 'e-mail' + share_email_label: 'Correo electrónico' public_link: 'enlace público' delete_public_link: 'eliminar enlace público' export: 'Exportar' @@ -232,12 +259,12 @@ entry: original_article: 'original' annotations_on_the_entry: '{0} Sin anotaciones|{1} Una anotación|]1,Inf[ %count% anotaciones' created_at: 'Fecha de creación' - # published_at: 'Publication date' - # published_by: 'Published by' - # provided_by: 'Provided by' + published_at: 'Fecha de publicación' + published_by: 'Publicado por' + provided_by: 'Proporcionado por' new: page_title: 'Guardar un nuevo artículo' - placeholder: 'http://sitioweb.com' + placeholder: 'https://sitioweb.es' form_new: url_label: URL search: @@ -246,13 +273,18 @@ entry: page_title: 'Editar un artículo' title_label: 'Título' url_label: 'URL' - # origin_url_label: 'Origin url (from where you found that entry)' + origin_url_label: 'URL de origen (de dónde has encontrado este artículo)' save_label: 'Guardar' public: shared_by_wallabag: "Este artículo se ha compartido con wallabag" confirm: - # delete: "Are you sure you want to remove that article?" - # delete_tag: "Are you sure you want to remove that tag from that article?" + delete: "¿Estás seguro de que quieres eliminar este artículo?" + delete_tag: "¿Estás seguro de que quieres eliminar esta etiqueta de este artículo?" + metadata: + reading_time: "Tiempo de lectura estimado" + reading_time_minutes_short: "%readingTime% min" + address: "Dirección" + added_on: "Añadido el" about: page_title: 'Acerca de' @@ -274,14 +306,14 @@ about: bug_reports: 'Reporte de errores' support: 'en GitHub' helping: - description: 'wallabag es software libre y gratuito. Usted puede ayudarnos :' + description: 'wallabag es software libre y gratuito. Usted puede ayudarnos:' by_contributing: 'contribuyendo al proyecto :' by_contributing_2: 'nuestras necesidades están en un ticket' by_paypal: 'vía Paypal' contributors: description: 'Gracias a los colaboradores de la aplicación web de wallabag' third_party: - description: 'Aquí está la lista de bibliotecas de terceros utilizadas por wallabag (con sus licencias) :' + description: 'Aquí está la lista de bibliotecas de terceros utilizadas por wallabag (con sus licencias):' package: 'Paquete' license: 'Licencia' @@ -329,7 +361,7 @@ howto: article_title: Atajos de teclado disponibles en el artículo open_original: Abrir la URL original de un artículo toggle_favorite: Marcar como favorito / no favorito el artículo - toggle_archive: marcar como leído / no leído el artículo + toggle_archive: Marcar como leído / no leído el artículo delete: Borrar el artículo material_title: Atajos de teclado disponibles solo en el tema Material add_link: Añadir un nuevo artículo @@ -348,7 +380,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' @@ -388,7 +420,7 @@ quickstart: title: 'Apoyo' description: 'Si necesitas ayuda, estamos a tu disposición.' github: 'En GitHub' - email: 'Por e-mail' + email: 'Por correo electrónico' gitter: 'En Gitter' tag: @@ -396,12 +428,16 @@ tag: list: number_on_the_page: '{0} No hay ninguna etiqueta.|{1} Hay una etiqueta.|]1,Inf[ Hay %count% etiquetas.' see_untagged_entries: 'Ver artículos sin etiquetas' + no_untagged_entries: 'No hay artículos sin etiquetas.' new: add: 'Añadir' placeholder: 'Puedes añadir varias etiquetas, separadas por una coma.' + rename: + placeholder: 'Puedes actualizar el nombre de la etiqueta.' -# 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.

' +export: + footer_template: '

Producido por wallabag con %method%

Por favor abre un ticket si tienes algún problema con la visualización de este E-Book en tu dispositivo.

' + unknown: 'Desconocido' import: page_title: 'Importar' @@ -421,7 +457,7 @@ import: admin_message: 'Debe definir %keyurls%una clave del API Pocket%keyurle%.' user_message: 'El administrador de su servidor debe definir una clave del API Pocket.' authorize_message: 'Puede importar sus datos desde su cuenta de Pocket. Sólo tiene que hacer clic el botón para autorizar que wallabag se conecte a getpocket.com.' - connect_to_pocket: 'Conectar a Pocket e importar los datos' + connect_to_pocket: 'Conectarse a Pocket e importar los datos' wallabag_v1: page_title: 'Importar > Wallabag v1' description: 'Importa todos tus artículos de wallabag v1. En la configuración de wallabag v1, haga clic en "Exportar JSON" dentro de la sección "Exportar datos de wallabag". Obtendrás un archivo llamado "wallabag-export-1-xxxx-xx-xx.json".' @@ -429,13 +465,16 @@ import: wallabag_v2: page_title: 'Importar > Wallabag v2' description: 'Importa todos tus artículos de wallabag v2. En la sección Todos los artículos, en la barra lateral, haga clic en "JSON". Obtendrás un archivo llamado "All articles.json".' + # elcurator: + # page_title: 'Import > elCurator' + # description: 'This importer will import all your elCurator articles. Go to your preferences in your elCurator account and then, export your content. You will have a JSON file.' readability: page_title: 'Importar > Readability' - description: 'Importa todos tus artículos de Readability. En la página de herramientas (https://www.readability.com/tools/), haga clic en "Exportar tus datos" en la sección "Exportar datos". Recibirás un e-mail para descargar un JSON (que no tiene extensión .json).' + description: 'Importa todos tus artículos de Readability. En la página de herramientas (https://www.readability.com/tools/), haga clic en "Exportar tus datos" en la sección "Exportar datos". Recibirás un correo electrónico para descargar un JSON (que no tiene extensión .json).' how_to: 'Seleccione el archivo exportado de Readability y haga clic en el botón para subirlo e importarlo.' worker: enabled: "La importación se realiza de forma asíncrona. Una vez que la tarea de importación ha comenzado, un trabajador externo se encargará de los artículos uno a uno. El servicio actual es:" - download_images_warning: "Tienes activado descargar imágenes de los artículos. Esto justo con la importación clásica de artículos puede tardar mucho tiempo en ser procesado (o incluso fallar). Recomendamos encarecidamente habilitar la importación asíncrona para evitar errores." + download_images_warning: "Tienes activado descargar imágenes de los artículos. Esto junto con la importación clásica de artículos puede tardar mucho tiempo en ser procesado (o incluso fallar). Recomendamos encarecidamente habilitar la importación asíncrona para evitar errores." firefox: page_title: 'Importar > Firefox' description: "Importa todos tus marcadores de Firefox. En la ventana de marcadores (Ctrl+Mayus+O), en \"Importar y respaldar\", elige \"Copiar...\". Obtendrás un archivo .json." @@ -482,6 +521,7 @@ developer: redirect_uris_label: 'URIs de redirección' save_label: 'Crear un nuevo cliente' action_back: 'Volver' + copy_to_clipboard: 'Copiar' client_parameter: page_title: 'Gestión de clientes API > Parámetros del cliente' page_description: 'Aquí están los parámetros del cliente.' @@ -493,14 +533,14 @@ developer: howto: page_title: 'Gestión de clientes API > Cómo crear mi primera aplicación' description: - paragraph_1: 'Los siguientes comandos hacen uso de la biblioteca HTTPie. Compruebe que está instalada en su sistema antes de usarla.' + paragraph_1: 'Los siguientes comandos hacen uso de la biblioteca HTTPie. Comprueba que está instalada en tu sistema antes de usarla.' paragraph_2: 'Necesitas un token para establecer la comunicación entre una aplicación de terceros y la API de wallabag.' paragraph_3: 'Para crear este token, necesitas crear un nuevo cliente.' - paragraph_4: 'Ahora crea tu token (reemplace client_id, client_secret, username y password con los valores generados):' + paragraph_4: 'Ahora crea tu token (reemplaza client_id, client_secret, username y password con los valores generados):' paragraph_5: 'Este API devolverá una respuesta como esta:' paragraph_6: 'El access_token es útil para llamar a los métodos del API. Por ejemplo:' paragraph_7: 'Esta llamada devolverá todos los artículos de tu usuario.' - paragraph_8: 'Si quiere ver todos los métodos del API, puede verlos en nuestra documentación del API.' + paragraph_8: 'Si quieres ver todos los métodos del API, puedes verlos en nuestra documentación del API.' back: 'Volver' user: @@ -520,36 +560,37 @@ user: password_label: 'Contraseña' repeat_new_password_label: 'Confirmar la contraseña' plain_password_label: '????' - email_label: 'E-mail' + email_label: 'Correo electrónico' enabled_label: 'Activado' last_login_label: 'Último inicio de sesión' - twofactor_label: Autenticación en dos pasos + twofactor_email_label: 'Autenticación de dos pasos por correo electrónico' + twofactor_google_label: 'Autenticación de dos pasos por aplicación OTP' save: Guardar delete: Eliminar delete_confirm: ¿Estás seguro? back_to_list: Volver a la lista search: - # placeholder: Filter by username or email + placeholder: 'Filtrar por nombre de usuario o correo electrónico' site_credential: - # page_title: Site credentials management - # new_site_credential: Create a credential - # edit_site_credential: Edit an existing credential - # description: "Here you can manage all credentials for sites which required them (create, edit and delete), like a paywall, an authentication, etc." - # list: - # actions: Actions - # edit_action: Edit - # yes: Yes - # no: No - # create_new_one: Create a new credential - # form: - # username_label: 'Username' - # host_label: 'Host' - # password_label: 'Password' - # save: Save - # delete: Delete - # delete_confirm: Are you sure? - # back_to_list: Back to list + page_title: 'Gestión de credenciales del sitio' + new_site_credential: 'Crear una credencial' + edit_site_credential: 'Editar una credencial existente' + description: "Aquí puedes gestionar todas las credenciales para los sitios que las necesiten (crear, editar y borrar), como un paywall, una autenticación, etc." + list: + actions: 'Acciones' + edit_action: 'Editar' + yes: 'Sí' + no: 'No' + create_new_one: 'Crear una nueva credencial' + form: + username_label: 'Nombre de usuario' + host_label: 'Host (subdominio.ejemplo.org, .ejemplo.org, etc.)' + password_label: 'Contraseña' + save: 'Guardar' + delete: 'Borrar' + delete_confirm: '¿Estás seguro?' + back_to_list: 'Volver a la lista' error: page_title: Ha ocurrido un error @@ -561,14 +602,18 @@ 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' + feed_token_revoked: 'Token RSS revocado' annotations_reset: Anotaciones reiniciadas tags_reset: Etiquetas reiniciadas entries_reset: Artículos reiniciados - # archived_reset: Archived entries deleted + archived_reset: Artículos archivados borrados + otp_enabled: Autenticación de dos pasos activada + tagging_rules_imported: Reglas de etiquetado importadas + tagging_rules_not_imported: Un error se ha producico en la importación de las reglas de etiquetado entry: notice: entry_already_saved: 'Artículo ya guardado el %fecha%' @@ -582,9 +627,11 @@ flashes: entry_starred: 'Artículo marcado como favorito' entry_unstarred: 'Artículo desmarcado como favorito' entry_deleted: 'Artículo eliminado' + no_random_entry: 'Ningún artículo con esos criterios fue encontrado' tag: notice: tag_added: 'Etiqueta añadida' + tag_renamed: 'Etiqueta renombrada' import: notice: failed: 'Importación fallida, por favor, inténtelo de nuevo.' @@ -596,15 +643,15 @@ flashes: rabbit_enabled_not_installed: RabbitMQ está activado para gestionar la importación asíncrona pero parece que no se puede conectar. Por favor, comprueba la configuración de RabbitMQ. developer: notice: - client_created: 'Creado el cliente %name%.' - client_deleted: 'Eliminado el cliente %name%' + client_created: 'El cliente %name% ha sido creado.' + client_deleted: 'El cliente %name% ha sido eliminado' user: notice: - added: 'Añadido el usuario "%username%"' - updated: 'Actualizado el usuario "%username%"' - deleted: 'Eliminado el usuario "%username%"' + added: 'El usuario "%username%" ha sido añadido' + updated: 'El usuario "%username%" ha sido actualizado' + deleted: 'El usuario "%username%" ha sido eliminado' site_credential: notice: - # added: 'Site credential for "%host%" added' - # updated: 'Site credential for "%host%" updated' - # deleted: 'Site credential for "%host%" deleted' + added: 'Credenciales del sitio añadidas para "%host%"' + updated: 'Credenciales del sitio actualizadas para "%host%"' + deleted: 'Credenciales del sitio eliminadas para "%host%"' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml index b3f2eb584..c1fb74d37 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml @@ -33,10 +33,12 @@ menu: back_to_unread: 'ؚازگ؎ت ØšÙ‡ خوانده‌ن؎ده‌ها' # users_management: 'Users management' # site_credentials: 'Site credentials' + quickstart: "Quickstart" top: add_new_entry: 'افزودن مقالهٔ تازه' search: 'جستجو' filter_entries: 'فیلترکردن مقاله‌ها' + # random_entry: Jump to a random entry from that list export: 'ؚرون‌ؚری' search_form: input_label: 'جستجوی خود را این‌جا ؚنویسید:' @@ -53,11 +55,12 @@ config: page_title: 'ٟیکرؚندی' tab_menu: settings: 'تن؞یمات' - rss: 'آر-اس-اس' + feed: 'آر-اس-اس' user_info: 'اطلاعات کارؚر' password: 'رمز' rules: 'ؚرچسؚ‌گذاری خودکار' new_user: 'افزودن کارؚر' + # reset: 'Reset area' form: save: 'ذخیره' form_settings: @@ -65,12 +68,8 @@ config: items_per_page_label: 'تعداد مقاله در هر صفحه' language_label: 'زؚان' reading_speed: - label: 'سرعت خواندن' + # label: 'Reading speed (words per minute)' help_message: 'سرعت خواندن‌تان را ؚا اؚزارهای آنلاین تخمین ؚزنید:' - 100_word: 'من تقریؚاً Û±Û°Û° واژه را در دقیقه می‌خوانم' - 200_word: 'من تقریؚاً Û²Û°Û° واژه را در دقیقه می‌خوانم' - 300_word: 'من تقریؚاً Û³Û°Û° واژه را در دقیقه می‌خوانم' - 400_word: 'من تقریؚاً ÛŽÛ°Û° واژه را در دقیقه می‌خوانم' action_mark_as_read: # label: 'Where do you want to be redirected to after marking an article as read?' # redirect_homepage: 'To the homepage' @@ -83,25 +82,35 @@ 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: + # token_revoke: 'Revoke the token' + 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." + # login_label: 'Login (can not be changed)' 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. @@ -127,6 +136,15 @@ config: # edit_rule_label: 'edit' rule_label: 'قانون' tags_label: 'ؚرچسؚ‌ها' + # card: + # new_tagging_rule: Create a tagging rule + # import_tagging_rules: Import tagging rules + # import_tagging_rules_detail: You have to select the JSON file you previously exported. + # export_tagging_rules: Export tagging rules + # export_tagging_rules_detail: This will download a JSON file that you can use to import tagging rules elsewhere or to backup them. + # file_label: JSON file + # import_submit: Import + # export: Export faq: title: 'ٟرس؎‌های متداول' tagging_rules_definition_title: 'ؚرچسؚ‌گذاری خودکار یعنی چه؟' @@ -159,6 +177,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' @@ -237,7 +264,7 @@ entry: # provided_by: 'Provided by' new: page_title: 'ذخیرهٔ مقالهٔ تازه' - placeholder: 'http://website.com' + placeholder: 'https://website.ir' form_new: url_label: ن؎انی search: @@ -253,6 +280,11 @@ entry: confirm: # delete: "Are you sure you want to remove that article?" # delete_tag: "Are you sure you want to remove that tag from that article?" + metadata: + # reading_time: "Estimated reading time" + # reading_time_minutes_short: "%readingTime% min" + # address: "Address" + # added_on: "Added on" about: page_title: 'درؚاره' @@ -348,7 +380,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: 'مدیریت' @@ -396,12 +428,16 @@ tag: list: number_on_the_page: '{0} هیچ ؚرچسؚی نیست.|{1} یک ؚرچسؚ هست.|]1,Inf[ %count% ؚرچسؚ هست.' # see_untagged_entries: 'See untagged entries' + # no_untagged_entries: 'There are no untagged entries.' 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.

' +# unknown: 'Unknown' import: page_title: 'درون‌ریزی' @@ -429,6 +465,9 @@ import: wallabag_v2: page_title: 'درون‌ریزی > Wallabag v2' description: 'این ؚرنامه همهٔ داده‌های ؎ما را در نسخهٔ Û² wallabag درون‌ریزی می‌کند. ØšÙ‡ ؚخ؎ «همهٔ مقاله‌ها» ؚروید و در ؚخ؎ «ؚرون‌ریزی» روی "JSON" کلیک کنید. ؚا این کار ؎ما ٟرونده‌ای ØšÙ‡ ØŽÚ©Ù„ "All articles.json" دریافت خواهید کرد.' + # elcurator: + # page_title: 'Import > elCurator' + # description: 'This importer will import all your elCurator articles. Go to your preferences in your elCurator account and then, export your content. You will have a JSON file.' readability: page_title: 'درون‌ریزی > Readability' # description: 'This importer will import all your Readability articles. On the tools (https://www.readability.com/tools/) page, click on "Export your data" in the "Data Export" section. You will received an email to download a json (which does not end with .json in fact).' @@ -482,6 +521,7 @@ developer: # redirect_uris_label: 'Redirect URIs' # save_label: 'Create a new client' # action_back: 'ؚازگ؎ت' + # copy_to_clipboard: Copy # client_parameter: # page_title: 'API clients management > Client parameters' # page_description: 'Here are your client parameters.' @@ -523,7 +563,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? @@ -544,7 +585,7 @@ site_credential: # create_new_one: Create a new credential # form: # username_label: 'Username' - # host_label: 'Host' + # host_label: 'Host (subdomain.example.org, .example.org, etc.)' # password_label: 'Password' # save: Save # delete: Delete @@ -561,14 +602,18 @@ 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: 'کد آر-اس-اس ؚه‌روز ؎د' + # feed_token_revoked: 'RSS token revoked' # annotations_reset: Annotations reset # tags_reset: Tags reset # entries_reset: Entries reset # archived_reset: Archived entries deleted + # otp_enabled: Two-factor authentication enabled + # tagging_rules_imported: Tagging rules imported + # tagging_rules_not_imported: Error while importing tagging rules entry: notice: entry_already_saved: 'این مقاله در تاریخ %date% ذخیره ؎ده ؚود' @@ -582,9 +627,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 5cdd836ea..2b8bb0922 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml @@ -5,7 +5,7 @@ security: forgot_password: "Mot de passe oublié ?" submit: "Se connecter" register: "Créer un compte" - username: "Nom d’utilisateur" + username: "Identifiant" password: "Mot de passe" cancel: "Annuler" resetting: @@ -33,10 +33,12 @@ menu: back_to_unread: "Retour aux articles non lus" users_management: "Gestion des utilisateurs" site_credentials: 'AccÚs aux sites' + quickstart: "Pour bien débuter" top: 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 +55,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: @@ -65,14 +68,10 @@ config: items_per_page_label: "Nombre d’articles par page" language_label: "Langue" reading_speed: - label: "Vitesse de lecture" + label: "Vitesse de lecture (mots par minute)" help_message: "Vous pouvez utiliser un outil en ligne pour estimer votre vitesse de lecture :" - 100_word: "Je lis environ 100 mots par minute" - 200_word: "Je lis environ 200 mots par minute" - 300_word: "Je lis environ 300 mots par minute" - 400_word: "Je lis environ 400 mots par minute" action_mark_as_read: - label: "Que faire lorsqu'un article est supprimé, marqué comme lu ou marqué comme favoris ?" + label: "Que faire lorsqu'un article est supprimé, marqué comme lu ou marqué comme favori ?" redirect_homepage: "Retourner à la page d’accueil" redirect_current_page: "Rester sur la page actuelle" pocket_consumer_key_label: "Clé d’authentification Pocket pour importer les données" @@ -83,25 +82,35 @@ 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: + token_revoke: 'Supprimer le jeton' + 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." + login_label: 'Identifiant' 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é." @@ -127,6 +136,15 @@ config: edit_rule_label: "éditer" rule_label: "RÚgle" tags_label: "Tags" + card: + new_tagging_rule: Créer une rÚgle + import_tagging_rules: Importer des rÚgles + import_tagging_rules_detail: Vous devez sélectionné un fichier JSON que vous avez précédemment exporté. + export_tagging_rules: Exporter les rÚgles + export_tagging_rules_detail: Un fichier JSON sera téléchargé et vous pourrez l'utiliser pour ré-importer les rÚgles ou comme sauvegarde. + file_label: Fichier JSON + import_submit: Importer + export: Export faq: title: "FAQ" tagging_rules_definition_title: "Que signifient les rÚgles de tag automatiques ?" @@ -159,6 +177,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" @@ -237,7 +264,7 @@ entry: provided_by: "Fourni par" new: page_title: "Sauvegarder un nouvel article" - placeholder: "http://website.com" + placeholder: "https://website.fr" form_new: url_label: "Adresse" search: @@ -253,6 +280,11 @@ entry: confirm: delete: "Voulez-vous vraiment supprimer cet article ?" delete_tag: "Voulez-vous vraiment supprimer ce tag de cet article ?" + metadata: + reading_time: "Durée de lecture estimée" + reading_time_minutes_short: "%readingTime% min" + address: "Adresse" + added_on: "Ajouté le" about: page_title: "À propos" @@ -290,7 +322,7 @@ howto: tab_menu: add_link: "Ajouter un lien" shortcuts: "Utiliser les raccourcis" - page_description: "Il y a plusieurs façon d’enregistrer un article :" + page_description: "Il y a plusieurs façons d’enregistrer un article :" top_menu: browser_addons: "Extensions de navigateur" mobile_apps: "Applications smartphone" @@ -348,7 +380,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" @@ -396,12 +428,16 @@ tag: list: number_on_the_page: "{0} Il n’y a pas de tag.|{1} Il y a un tag.|]1,Inf[ Il y a %count% tags." see_untagged_entries: "Voir les articles sans tag" + no_untagged_entries: 'Aucun article sans 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.

' + unknown: 'Inconnu' import: page_title: "Importer" @@ -429,6 +465,9 @@ import: wallabag_v2: page_title: "Importer > wallabag v2" description: "Cet outil va importer tous vos articles d’une autre instance de wallabag v2. Allez dans tous vos articles, puis, sur la barre latérale, cliquez sur « JSON ». Vous allez récupérer un fichier « All articles.json »" + # elcurator: + # page_title: 'Import > elCurator' + # description: 'This importer will import all your elCurator articles. Go to your preferences in your elCurator account and then, export your content. You will have a JSON file.' readability: page_title: "Importer > Readability" description: "Cet outil va importer toutes vos données de Readability. Sur la page des outils (https://www.readability.com/tools/), cliquez sur « Export your data » dans la section « Data Export ». Vous allez recevoir un courriel avec un lien pour télécharger le json." @@ -482,6 +521,7 @@ developer: redirect_uris_label: "Adresses de redirection (optionnel)" save_label: "Créer un nouveau client" action_back: "Retour" + copy_to_clipboard: Copier client_parameter: page_title: "Gestion des clients API > Les paramÚtres de votre client" page_description: "Voilà les paramÚtres de votre client" @@ -515,7 +555,7 @@ user: no: "Non" create_new_one: "Créer un nouvel utilisateur" form: - username_label: "Nom d’utilisateur" + username_label: "Identifiant (ne peut être changé)" name_label: "Nom" password_label: "Mot de passe" repeat_new_password_label: "Confirmez votre nouveau mot de passe" @@ -524,12 +564,14 @@ 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 ?" back_to_list: "Revenir à la liste" search: - placeholder: "Filtrer par nom d’utilisateur ou email" + placeholder: "Filtrer par identifiant ou email" site_credential: page_title: Gestion des accÚs aux sites @@ -544,7 +586,7 @@ site_credential: create_new_one: Créer un nouvel accÚs à un site form: username_label: 'Identifiant' - host_label: 'Domaine' + host_label: 'Domaine (subdomain.example.org, .example.org, etc.)' password_label: 'Mot de passe' save: "Sauvegarder" delete: "Supprimer" @@ -561,14 +603,18 @@ 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" + feed_token_revoked: 'Jeton des flux supprimé' 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" + tagging_rules_imported: RÚgles bien importées + tagging_rules_not_imported: Impossible d'importer les rÚgles entry: notice: entry_already_saved: "Article déjà sauvegardé le %date%" @@ -582,9 +628,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 83b3edcdd..8cee3e522 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.it.yml @@ -33,10 +33,12 @@ menu: back_to_unread: 'Torna ai contenuti non letti' users_management: 'Gestione utenti' site_credentials: 'Credenziali sito' + quickstart: "Introduzione" top: 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 +55,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: @@ -65,12 +68,8 @@ config: items_per_page_label: 'Elementi per pagina' language_label: 'Lingua' reading_speed: - label: 'Velocità di lettura' + label: 'Velocità di lettura (parole al minuto)' help_message: 'Puoi utilizzare degli strumenti online per valutare la tua velocità di lettura:' - 100_word: 'Leggo ~100 parole al minuto' - 200_word: 'Leggo ~200 parole al minuto' - 300_word: 'Leggo ~300 parole al minuto' - 400_word: 'Leggo ~400 parole al minuto' action_mark_as_read: label: "Dove vuoi essere reindirizzato dopo aver segnato l'articolo come già letto?" redirect_homepage: 'Alla homepage' @@ -83,25 +82,35 @@ 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: + # token_revoke: 'Revoke the token' + 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." + # login_label: 'Login (can not be changed)' 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." + 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: 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. @@ -127,6 +136,15 @@ config: edit_rule_label: 'modifica' rule_label: 'Regola' tags_label: 'Etichetta' + # card: + # new_tagging_rule: Create a tagging rule + # import_tagging_rules: Import tagging rules + # import_tagging_rules_detail: You have to select the JSON file you previously exported. + # export_tagging_rules: Export tagging rules + # export_tagging_rules_detail: This will download a JSON file that you can use to import tagging rules elsewhere or to backup them. + # file_label: JSON file + # import_submit: Import + # export: Export faq: title: 'FAQ' tagging_rules_definition_title: 'Cosa significa « regole di etichettatura » ?' @@ -159,6 +177,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" @@ -237,7 +264,7 @@ entry: # provided_by: 'Provided by' new: page_title: 'Salva un nuovo contenuto' - placeholder: 'http://website.com' + placeholder: 'https://website.it' form_new: url_label: Url search: @@ -253,6 +280,11 @@ entry: confirm: delete: "Vuoi veramente rimuovere quell'articolo?" delete_tag: "Vuoi veramente rimuovere quell'etichetta da quell'articolo?" + metadata: + # reading_time: "Estimated reading time" + # reading_time_minutes_short: "%readingTime% min" + # address: "Address" + # added_on: "Added on" about: page_title: 'A proposito' @@ -348,7 +380,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' @@ -396,12 +428,16 @@ tag: list: number_on_the_page: "{0} Non ci sono etichette.|{1} C'Ú un'etichetta.|]1,Inf[ ci sono %count% etichette." see_untagged_entries: 'Vedi articoli non etichettati' + # no_untagged_entries: 'There are no untagged entries.' 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.

' +# unknown: 'Unknown' import: page_title: 'Importa' @@ -429,6 +465,9 @@ import: wallabag_v2: page_title: 'Importa da > Wallabag v2' description: 'Questo importatore copierà tutti i tuoi dati da un wallabag v2. Vai in "Tutti i contenuti", e, nella barra laterale di esportazione, clicca su "JSON". Otterrai un file "Tutti i contenuti.json' + # elcurator: + # page_title: 'Import > elCurator' + # description: 'This importer will import all your elCurator articles. Go to your preferences in your elCurator account and then, export your content. You will have a JSON file.' readability: page_title: 'Importa da > Readability' description: 'Questo importatore copierà tutti i tuoi articoli da Readability. Nella pagina strumenti (https://www.readability.com/tools/), clicca su "Export your data" nella sezione "Data Export". Riceverai una E-mail per scaricare un file json (che tuttavia non termina con .json).' @@ -482,6 +521,7 @@ developer: redirect_uris_label: 'Redirect URI' save_label: 'Crea un nuovo client' action_back: 'Indietro' + # copy_to_clipboard: Copy client_parameter: page_title: 'Gestione client API > Parametri Client' page_description: 'Questi sono i tuoi parametri del client.' @@ -523,7 +563,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? @@ -544,7 +585,7 @@ site_credential: # create_new_one: Create a new credential # form: # username_label: 'Username' - # host_label: 'Host' + # host_label: 'Host (subdomain.example.org, .example.org, etc.)' # password_label: 'Password' # save: Save # delete: Delete @@ -561,14 +602,18 @@ 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' + # feed_token_revoked: 'RSS token revoked' annotations_reset: Reset annotazioni tags_reset: Reset etichette entries_reset: Reset articoli # archived_reset: Archived entries deleted + # otp_enabled: Two-factor authentication enabled + # tagging_rules_imported: Tagging rules imported + # tagging_rules_not_imported: Error while importing tagging rules entry: notice: entry_already_saved: 'Contenuto già salvato in data %date%' @@ -582,9 +627,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 95bc9560f..052582ab8 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml @@ -33,10 +33,12 @@ menu: back_to_unread: 'Tornar als articles pas legits' users_management: 'Gestion dels utilizaires' site_credentials: 'Identificants del site' + quickstart: "Per ben començar" top: add_new_entry: 'Enregistrar un novÚl article' search: 'Cercar' filter_entries: 'Filtrar los articles' + random_entry: "Sautar a un article a l'azard" export: 'Exportar' search_form: input_label: 'Picatz vòstre mot-clau a cercar aquí' @@ -53,11 +55,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: @@ -65,12 +68,8 @@ config: items_per_page_label: "Nombre d'articles per pagina" language_label: 'Lenga' reading_speed: - label: 'Velocitat de lectura' + label: 'Velocitat de lectura (mots per minuta)' help_message: 'PodÚtz utilizar una aisina en linha per estimar vòstra velocitat de lectura :' - 100_word: "Legissi a l'entorn de 100 mots per minuta" - 200_word: "Legissi a l'entorn de 200 mots per minuta" - 300_word: "Legissi a l'entorn de 300 mots per minuta" - 400_word: "Legissi a l'entorn de 400 mots per minuta" action_mark_as_read: label: 'Ont volÚtz Ússer menat aprÚp aver marcat un article coma legit ?' redirect_homepage: "A la pagina d’acuÚlh" @@ -83,25 +82,35 @@ 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: + oken_revoke: 'Revocar lo geton' + 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: "L’activacion de l’autentificacion en dos temps indica que recebretz un còdi per corriÚl amb un còdi O que vos cal utilizar una aplicacion OTP (coma Google Authenticator, Authy o FreeOTP) per obténer un còdi a usatge unic cada còp qu’i a una connexion pas fisabla. PodÚtz pas causir las doas opcions." + login_label: 'Identificant (se pòt pas cambiar)' 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." + two_factor: + emailTwoFactor_label: 'En utilizar lo corriÚl (recebre un còdi per corriÚl)' + googleTwoFactor_label: 'En utilizar una aplicacion OTP (Dobrir l’aplicacion, coma Google Authenticator, Authy o FreeOTP, per obténer un còdi a usatge unic)' + table_method: Metòde + table_state: Estat + table_action: Accion + state_enabled: Activada + state_disabled: Desactivada + action_email: Utilizar lo corriÚl + action_app: Utilizar una aplicacion OTP 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. @@ -127,6 +136,15 @@ config: edit_rule_label: 'modificar' rule_label: 'RÚgla' tags_label: 'Etiquetas' + card: + new_tagging_rule: Crear una rÚgla d’etiquetatge + import_tagging_rules: Importar de rÚglas d’etiquetatge + import_tagging_rules_detail: Vos cal causir un fichiÚr JSON qu’importÚretz per abans. + export_tagging_rules: Exportar las rÚglas d’etiquetatge + export_tagging_rules_detail: Telecargarà un fichiÚr JSON que podÚtz utilizar per importar las rÚglas d’etiquetatge endacòm mai o per las salvagardar. + file_label: fichiÚr JSON + import_submit: Importar + export: Exportar faq: title: 'FAQ' tagging_rules_definition_title: "Qué significa las rÚglas d'etiquetas automaticas ?" @@ -159,6 +177,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: Autentificacion en dos temps + app: + two_factor_code_description_1: AvÚtz pas qu’activat l’autentificacion en dos temps, dobrissÚtz l’aplicacion OTP e utilizatz aqueste còdi per obténer un senhal unic. Apareisserà aprÚp un recargament de pagina. + two_factor_code_description_2: 'PodÚtz numerizar aqueste còdi QR amb l’aplicacion :' + two_factor_code_description_3: 'Amai, enregistratz aquestes còdis de recuperacion dins un lòc segur, los podÚtz utilizar se per cas perdatz l’accÚs a l’aplicacion OTP :' + two_factor_code_description_4: 'Ensajatz un còdi de vòstra aplicacion configurada app :' + cancel: Anullar + enable: Activar entry: default_title: "Títol de l'article" @@ -186,7 +213,7 @@ entry: export_title: 'Exportar' filters: title: 'Filtres' - status_label: 'Estatus' + status_label: 'Estat' archived_label: 'Legits' starred_label: 'Favorits' unread_label: 'Pas legits' @@ -195,7 +222,7 @@ entry: is_public_label: 'Ten un ligam public' is_public_help: 'Ligam public' language_label: 'Lenga' - http_status_label: 'Estatut HTTP' + http_status_label: 'Estat HTTP' reading_time: label: 'Durada de lectura en minutas' from: 'de' @@ -237,7 +264,7 @@ entry: provided_by: 'Provesit per' new: page_title: 'Enregistrar un novÚl article' - placeholder: 'http://website.com' + placeholder: 'https://website.cat' form_new: url_label: Url search: @@ -253,6 +280,11 @@ entry: confirm: delete: "SÚtz segur de voler suprimir aqueste article ?" delete_tag: "SÚtz segur de voler levar aquesta etiqueta de l'article ?" + metadata: + reading_time: "Temps de lectura estimat" + reading_time_minutes_short: "%readingTime% min" + address: "Adreça" + added_on: "Ajustat a" about: page_title: 'A prepaus' @@ -348,7 +380,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' @@ -396,12 +428,16 @@ tag: list: number_on_the_page: "{0} I a pas cap d'etiquetas.|{1} I a una etiqueta.|]1,Inf[ I a %count% etiquetas." see_untagged_entries: "Afichar las entradas sens etiquetas" + no_untagged_entries: 'I a pas cap d’article pas etiquetat.' new: add: 'Ajustar' placeholder: "PodÚtz ajustar mai qu'una etiqueta, separadas per de virgula." + rename: + placeholder: 'PodÚtz actualizar lo nom de l’etiqueta.' 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.

' + unknown: 'Desconegut' import: page_title: 'Importar' @@ -429,6 +465,9 @@ import: wallabag_v2: page_title: 'Importar > Wallabag v2' description: "Aquesta aisina importarà totas vòstras donadas d'una instància mai de wallabag v2. Anatz dins totes vòstres articles, puÚi, sus la barra laterala, clicatz sus \"JSON\". Traparetz un fichiÚr \"All articles.json\"." + # elcurator: + # page_title: 'Import > elCurator' + # description: 'This importer will import all your elCurator articles. Go to your preferences in your elCurator account and then, export your content. You will have a JSON file.' readability: page_title: 'Importar > Readability' description: "Aquesta aisina importarà totas vòstres articles de Readability. Sus la pagina de l'aisina (https://www.readability.com/tools/), clicatz sus \"Export your data\" dins la seccion \"Data Export\". Recebretz un corriÚl per telecargar un json (qu'acaba pas amb un .json de fach)." @@ -482,6 +521,7 @@ developer: redirect_uris_label: 'URLs de redireccion' save_label: 'Crear un novÚl client' action_back: 'Retorn' + copy_to_clipboard: Copiar client_parameter: page_title: 'Gestion dels clients API > Los paramÚtres de vòstre client' page_description: 'Vaquí los paramÚtres de vòstre client.' @@ -523,7 +563,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: Autentificacion en dos temps per corriÚl + twofactor_google_label: Autentificacion en dos temps per aplicacion OTP save: 'Enregistrar' delete: 'Suprimir' delete_confirm: 'SÚtz segur ?' @@ -544,7 +585,7 @@ site_credential: create_new_one: Crear un novÚl identificant form: username_label: "Nom d'utilizaire" - host_label: 'Òste' + host_label: 'Òste (subdomain.example.org, .example.org, etc.)' password_label: 'Senhal' save: 'Enregistrar' delete: 'Suprimir' @@ -561,14 +602,18 @@ 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' + feed_token_revoked: 'Geton RSS revocat' annotations_reset: Anotacions levadas tags_reset: Etiquetas levadas entries_reset: Articles levats archived_reset: Articles archivat suprimits + otp_enabled: Autentificacion en dos temps activada + tagging_rules_imported: RÚglas d’etiquetatge importadas + tagging_rules_not_imported: Error en important las rÚglas d’etiquetatge entry: notice: entry_already_saved: 'Article ja salvagardat lo %date%' @@ -582,9 +627,11 @@ flashes: entry_starred: 'Article ajustat dins los favorits' entry_unstarred: 'Article quitat dels favorits' entry_deleted: 'Article suprimit' + no_random_entry: 'Cap d’article pas trobat amb aquestes critÚris' tag: notice: tag_added: 'Etiqueta ajustada' + tag_renamed: 'Etiqueta renomenada' import: notice: failed: "L'importacion a fracassat, mercés de tornar ensajar." diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml index a64e60b01..93e8f8526 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml @@ -33,10 +33,12 @@ menu: back_to_unread: 'Powrót do nieprzeczytanych artykułów' users_management: 'Zarządzanie uÅŒytkownikami' site_credentials: 'Poświadczenia strony' + quickstart: "Szybki start" top: 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 +55,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: @@ -65,12 +68,8 @@ config: items_per_page_label: 'Ilość elementów na stronie' language_label: 'Język' reading_speed: - label: 'Prędkość czytania' + label: 'Prędkość czytania (słów na minutę)' help_message: 'MoÅŒesz skorzystać z narzędzi online do określenia twojej prędkości czytania:' - 100_word: 'Czytam ~100 słów na minutę' - 200_word: 'Czytam ~200 słów na minutę' - 300_word: 'Czytam ~300 słów na minutę' - 400_word: 'Czytam ~400 słów na minutę' action_mark_as_read: label: 'Gdzie zostaniesz przekierowany po oznaczeniu artukuły jako przeczytanego' redirect_homepage: 'do strony głównej' @@ -83,25 +82,35 @@ 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: + # token_revoke: 'Revoke the token' + 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." + # login_label: 'Login (can not be changed)' 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." + 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: 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. @@ -127,6 +136,15 @@ config: edit_rule_label: 'edytuj' rule_label: 'Reguła' tags_label: 'Tagi' + # card: + # new_tagging_rule: Create a tagging rule + # import_tagging_rules: Import tagging rules + # import_tagging_rules_detail: You have to select the JSON file you previously exported. + # export_tagging_rules: Export tagging rules + # export_tagging_rules_detail: This will download a JSON file that you can use to import tagging rules elsewhere or to backup them. + # file_label: JSON file + # import_submit: Import + # export: Export faq: title: 'FAQ' tagging_rules_definition_title: 'Co oznaczają « reguły tagowania » ?' @@ -159,6 +177,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' @@ -237,7 +264,7 @@ entry: provided_by: 'Dostarczony przez' new: page_title: 'Zapisz nowy wpis' - placeholder: 'http://website.com' + placeholder: 'https://website.pl' form_new: url_label: Url search: @@ -253,6 +280,11 @@ entry: confirm: delete: "Czy jesteś pewien, ÅŒe chcesz usunąć ten artykuł?" delete_tag: "Czy jesteś pewien, ÅŒe chcesz usunąć ten tag, z tego artykułu?" + metadata: + # reading_time: "Estimated reading time" + # reading_time_minutes_short: "%readingTime% min" + # address: "Address" + # added_on: "Added on" about: page_title: 'O nas' @@ -348,7 +380,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' @@ -396,12 +428,16 @@ tag: list: number_on_the_page: '{0} Nie ma tagów.|{1} Jest jeden tag.|]1,Inf[ Są %count% tagi.' see_untagged_entries: 'Zobacz nieotagowane wpisy' + # no_untagged_entries: 'There are no untagged entries.' 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.

' + 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.

' + # unknown: 'Unknown' import: page_title: 'Import' @@ -429,6 +465,9 @@ import: wallabag_v2: page_title: 'Import > Wallabag v2' description: 'Ten importer, zaimportuje wszystkie twoje artykułu z wallabag v2. Idź do wszystkich artykułów, a następnie na panelu exportu kliknij na "JSON". Otrzymasz plik "All articles.json".' + # elcurator: + # page_title: 'Import > elCurator' + # description: 'This importer will import all your elCurator articles. Go to your preferences in your elCurator account and then, export your content. You will have a JSON file.' readability: page_title: 'Import > Readability' description: 'Ten importer, zaimportuje wszystkie twoje artykuły z Readability. Na stronie narzędzi (https://www.readability.com/tools/), kliknij na "Export your data" w sekcji "Data Export". Otrzymach email z plikiem JSON (plik nie będzie zawierał rozszerzenia .json).' @@ -482,6 +521,7 @@ developer: redirect_uris_label: 'Przekieruj adresy URI' save_label: 'Stwórz nowego klienta' action_back: 'Cofnij' + # copy_to_clipboard: Copy client_parameter: page_title: 'Zarządzanie klientami API > Parametry klienta' page_description: 'Tutaj znajdują się parametry klienta.' @@ -523,7 +563,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? @@ -544,7 +585,7 @@ site_credential: create_new_one: Stwórz nowe poświadczenie form: username_label: 'Nazwa uÅŒytkownika' - host_label: 'Host' + host_label: 'Host (subdomain.example.org, .example.org, etc.)' password_label: 'Hasło' save: Zapisz delete: Usuń @@ -561,14 +602,18 @@ 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' + # feed_token_revoked: 'RSS token revoked' annotations_reset: Zresetuj adnotacje tags_reset: Zresetuj tagi entries_reset: Zresetuj wpisy archived_reset: Zarchiwizowane wpisy usunięte + # otp_enabled: Two-factor authentication enabled + # tagging_rules_imported: Tagging rules imported + # tagging_rules_not_imported: Error while importing tagging rules entry: notice: entry_already_saved: 'Wpis juÅŒ został dodany %date%' @@ -582,9 +627,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 58d2d0587..7159569e6 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml @@ -19,24 +19,26 @@ menu: unread: 'Não lido' starred: 'Destacado' archive: 'Arquivo' - all_articles: 'Todas as entradas' + all_articles: 'Todos os artigos' config: 'Configurações' tags: 'Tags' internal_settings: 'Configurações Internas' import: 'Importar' - howto: 'How to' - # developer: 'API clients management' + howto: 'Ajuda' + developer: 'Gestão dos clientes API' logout: 'Sair' about: 'Sobre' search: 'Pesquisa' save_link: 'Salvar um link' back_to_unread: 'Voltar para os artigos não lidos' users_management: 'Gestão de Usuários' - # site_credentials: 'Site credentials' + site_credentials: 'Credenciais do site' + quickstart: "Começo Rápido" top: - add_new_entry: 'Adicionar uma nova entrada' + add_new_entry: 'Adicionar um novo artigo' search: 'Pesquisa' - filter_entries: 'Filtrar entradas' + filter_entries: 'Filtrar os artigos' + random_entry: Ir para um artigo aleatório desta lista export: 'Exportar' search_form: input_label: 'Digite aqui sua pesquisa' @@ -50,14 +52,15 @@ footer: stats: 'Desde %user_creation% você leu %nb_archives% artigos. Isso é %per_day% por dia!' config: - page_title: 'Config' + page_title: 'Configurações' 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: 'Reiniciar minha conta' form: save: 'Salvar' form_settings: @@ -65,68 +68,83 @@ config: items_per_page_label: 'Itens por página' language_label: 'Idioma' reading_speed: - label: 'Velocidade de leitura' + label: 'Velocidade de leitura (palavras por minuto)' help_message: 'Você pode usar ferramentas online para estimar sua velocidade de leitura:' - 100_word: 'Posso ler ~100 palavras por minuto' - 200_word: 'Posso ler ~200 palavras por minuto' - 300_word: 'Posso ler ~300 palavras por minuto' - 400_word: 'Posso ler ~400 palavras por minuto' action_mark_as_read: - # label: 'Where do you want to be redirected to after marking an article as read?' - # redirect_homepage: 'To the homepage' - # redirect_current_page: 'To the current page' + label: 'Para onde você deseja ser redirecionado após marcar um artigo como lido?' + redirect_homepage: 'Para a página inicial' + redirect_current_page: 'Ficar na página atual' pocket_consumer_key_label: 'Chave do consumidor do Pocket para importar conteúdo' - # android_configuration: Configure your Android application - # android_instruction: "Touch here to prefill your Android application" - # help_theme: "wallabag is customizable. You can choose your prefered theme here." - # help_items_per_page: "You can change the number of articles displayed on each page." - # 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: + android_configuration: Configure seu aplicativo Android + android_instruction: "Toque aqui para preencher seu aplicativo Android" + help_theme: "wallabag é personalizável. Você pode escolher o seu tema preferido aqui." + help_items_per_page: "Você pode alterar o número de artigos exibidos em cada página." + help_reading_speed: "wallabag calcula um tempo de leitura para cada artigo. Você pode definir aqui, graças a esta lista, se você é um leitor rápido ou lento. O wallabag recalcula o tempo de leitura de cada artigo." + help_language: "Você pode alterar o idioma da interface de wallabag." + help_pocket_consumer_key: "Necessário para importação desde Pocket. Você pode creá-lo na sua conta de Pocket." + 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: + token_revoke: 'Revocar token' + feed_links: 'Links RSS' + feed_link: unread: 'Não lido' starred: 'Destacado' archive: 'Arquivado' - # all: 'All' - rss_limit: 'Número de itens no feed' + all: 'Todos' + 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: "Ativar a autenticação de dois fatores significa que você vai receber um e-mail com um código OU que você vai precisar usar um aplicativo OTP (como Google Authenticator, Authy ou FreeOTP) para conseguir um código de utilização única em cada nova conexão não confiável. Você não pode escolher as duas opções." + login_label: 'Nome de usuário (não pode ser mudado)' 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." + two_factor: + emailTwoFactor_label: 'Usando e-mail (receber um código por e-mail)' + googleTwoFactor_label: 'Usando um aplicativo OTP (abra o aplicativo, como Google Authenticator, Authy ou FreeOTP, para conseguir um código de usagem único)' + table_method: Método + table_state: Estado + table_action: Ação + state_enabled: Ativado + state_disabled: Desativado + action_email: Usar e-mail + action_app: Usar aplicação OTP 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. - # confirm: Are you really sure? (THIS CAN'T BE UNDONE) - # button: Delete my account + title: Apagar minha conta (Zona de perigo) + description: Se você apaga sua conta, TODOS os seus artigos, TODOS os seus tags, TODAS suas anotações e sua conta serão PERMANENTEMENTE removidos (NÃO pode ser DESFEITO). Depois da operação você será desconectado. + confirm: Tem certeza? (ISSO NÃO PODE SER DESFEITO) + button: Apagar minha conta reset: - # title: Reset area (a.k.a danger zone) - # description: By hiting buttons below you'll have ability to remove some informations from your account. Be aware that these actions are IRREVERSIBLE. - # annotations: Remove ALL annotations - # tags: Remove ALL tags - # entries: Remove ALL entries - # archived: Remove ALL archived entries - # confirm: Are you really really sure? (THIS CAN'T BE UNDONE) + title: Reiniciar minha conta (Zona de perigo) + description: Apertando os botões aqui em baixo você poderá remover informações da sua conta. Saiba que eessas ações sao IRREVERSIVEIS. + annotations: Remover TODAS as anotações + tags: Remover TODOS os tags + entries: Remover TODOS os artigos + archived: Remover TODOS os artigos arquivados + confirm: Tem certeza? (ISSO NÃO PODE SER DESFEITO) form_password: - # description: "You can change your password here. Your new password should by at least 8 characters long." + description: "Você pode mudar a sua senha aqui. A nova senha deve ter pelo menos 8 caracteres." old_password_label: 'Senha atual' new_password_label: 'Nova senha' repeat_new_password_label: 'Repita a nova senha' form_rules: - if_label: 'if' + if_label: 'se' then_tag_as_label: 'então coloque a tag' delete_rule_label: 'apagar' edit_rule_label: 'editar' rule_label: 'Regras' tags_label: 'Tags' + card: + new_tagging_rule: Criar uma regra de tag + import_tagging_rules: Importar regras de tags + import_tagging_rules_detail: Você precisa selecionar o arquivo JSON exportado previamente. + export_tagging_rules: Exportar regras de tags + export_tagging_rules_detail: Isso vai baixar um arquivo JSON que você poderá usar para importar regras de marcação em outro local ou fazer uma cópia de segurança delas. + file_label: Arquivo JSON + import_submit: Importar + export: Exportar faq: title: 'FAQ' tagging_rules_definition_title: 'O que as « regras de tags » significam?' @@ -135,18 +153,18 @@ config: how_to_use_them_description: 'Vamos dizer que você deseja adicionar a tag « leitura rápida » quando o tempo de leitura for menor que 3 minutos.
Neste caso, você deve « readingTime <= 3 » no campo Regra e « leitura rápida » no campo Tags.
Diversas tags podem ser adicionadas simultâneamente separando-as com vírgula: « leitura rápida, precisa ser lido »
Regras complexas podem ser escritas usando os seguintes operadores pré-definidos: if « readingTime >= 5 AND domainName = "github.com" » então adicione a tag « leitura longa, github »' variables_available_title: 'Quais variáveis e operadores eu posso usar para escrever regras?' variables_available_description: 'As seguintes variáveis e operadores podem ser usados para criar regras de tags:' - meaning: 'Meaning' + meaning: 'Significado' variable_description: label: 'Variável' - title: 'Título da entrada' - url: 'URL da entrada' - isArchived: 'Se a entrada está arquivada ou não' - isDestacado: 'Se a entrada está destacada ou não' - content: 'O conteúdo da entrada' - language: 'O idioma da entrada' - mimetype: 'O mime-type da entrada' - readingTime: 'O tempo estimado de leitura da entrada, em minutos' - domainName: 'O domínio da entrada' + title: 'Título do artigo' + url: 'URL do artigo' + isArchived: 'Se o artigo está arquivado ou não' + isDestacado: 'Se o artigo está destacado ou não' + content: 'O conteúdo do artigo' + language: 'O idioma do artigo' + mimetype: 'O mime-type do artigo' + readingTime: 'O tempo estimado de leitura do artigo, em minutos' + domainName: 'O domínio do artigo' operator_description: label: 'Operador' less_than: 'Menor que...' @@ -157,22 +175,31 @@ config: not_equal_to: 'Diferente de...' or: 'Uma regra OU outra' 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"' + matches: 'Testa que um assunto corresponde a uma pesquisa (maiúscula ou minúscula).
Exemplo: title matches "futebol"' + notmatches: 'Testa que um assunto não corresponde a uma search (maiúscula ou minúscula).
Exemplo: title notmatches "futebol"' + otp: + page_title: Autenticação de dois fatores + app: + two_factor_code_description_1: Você acaba de ativar a autenticação de dois fatores com aplicativo OTP, abra seu aplicativo OTP e consegua um código de usagem único. Vai desaparecer ao recargar a página. + two_factor_code_description_2: 'Você pode escanear este código QR com seu aplicativo:' + two_factor_code_description_3: 'Não esqueça de guardar os códigos de segurança em um lugar seguro, você poderá usá-los se você perder o acesso ao seu aplicativo OTP:' + two_factor_code_description_4: 'Teste um código gerado pelo seu aplicativo OTP:' + cancel: Cancelar + enable: Ativar entry: - default_title: 'Título da entrada' + default_title: 'Título do artigo' page_titles: - unread: 'Entradas não lidas' - starred: 'Entradas destacadas' - archived: 'Entradas arquivadas' - filtered: 'Entradas filtradas' + unread: 'Artigos não lidos' + starred: 'Artigos destacados' + archived: 'Artigos arquivados' + filtered: 'Artigos filtrados' filtered_tags: 'Filtrar por tags:' - # filtered_search: 'Filtered by search:' + filtered_search: 'Filtrar por busca:' untagged: 'Entradas sem tags' - # all: 'All entries' + all: 'Todos os artigos' list: - number_on_the_page: '{0} Não existem entradas.|{1} Existe uma entrada.|]1,Inf[ Existem %count% entradas.' + number_on_the_page: '{0} Não existem artigos.|{1} Existe um artigo.|]1,Inf[ Existem %count% artigos.' reading_time: 'tempo estimado de leitura' reading_time_minutes: 'tempo estimado de leitura: %readingTime% min' reading_time_less_one_minute: 'tempo estimado de leitura: < 1 min' @@ -190,12 +217,12 @@ entry: archived_label: 'Arquivado' starred_label: 'Destacado' unread_label: 'Não Lido' - preview_picture_label: 'Possui uma imagem de preview' - preview_picture_help: 'Imagem de preview' - # is_public_label: 'Has a public link' - # is_public_help: 'Public link' + preview_picture_label: 'Possui uma imagem de pré-visualização' + preview_picture_help: 'Imagem de pré-visualização' + is_public_label: 'Tem um link público' + is_public_help: 'Link público' language_label: 'Idioma' - # http_status_label: 'HTTP status' + http_status_label: 'Código de estado HTTP' reading_time: label: 'Tempo de leitura em minutos' from: 'de' @@ -214,11 +241,11 @@ entry: back_to_homepage: 'Voltar' set_as_read: 'Marcar como lido' set_as_unread: 'Marcar como não lido' - set_as_starred: 'Alternar destaque' + set_as_starred: 'Marcar como destacado' view_original_article: 'Artigo original' re_fetch_content: 'Recapturar o conteúdo' delete: 'Apagar' - add_a_tag: 'Adicionar uma tag' + add_a_tag: 'Adicionar um tag' share_content: 'Compartilhar' share_email_label: 'E-mail' public_link: 'link público' @@ -232,27 +259,32 @@ entry: original_article: 'original' annotations_on_the_entry: '{0} Sem anotações|{1} Uma anotação|]1,Inf[ %nbAnnotations% anotações' created_at: 'Data de criação' - # published_at: 'Publication date' - # published_by: 'Published by' - # provided_by: 'Provided by' + published_at: 'Data de publicação' + published_by: 'Publicado por' + provided_by: 'Fornecido por' new: - page_title: 'Salvar nova entrada' - placeholder: 'http://website.com' + page_title: 'Salvar novo artigo' + placeholder: 'https://website.pt' form_new: url_label: Url search: - # placeholder: 'What are you looking for?' + placeholder: 'O que você está procurando?' edit: - page_title: 'Editar uma entrada' + page_title: 'Editar um artigo' title_label: 'Título' url_label: 'Url' - # origin_url_label: 'Origin url (from where you found that entry)' + origin_url_label: 'URL de origem url (onde você encontrou este artigo)' save_label: 'Salvar' public: shared_by_wallabag: "Este artigo foi compartilhado pelo wallabag" confirm: - # delete: "Are you sure you want to remove that article?" - # delete_tag: "Are you sure you want to remove that tag from that article?" + delete: Tem certeza de que deseja remover este artigo?" + delete_tag: "Tem certeza de que deseja remover este tag deste artigo?" + metadata: + reading_time: "Tempo estimado de leitura" + reading_time_minutes_short: "%readingTime% min" + address: "Endereço" + added_on: "Adicionado o" about: page_title: 'Sobre' @@ -274,7 +306,7 @@ about: bug_reports: 'Informar bugs' support: 'no GitHub' helping: - description: 'wallabag é livre e software livre. Você pode nos ajudar:' + description: 'wallabag é um software livre e gratuito. Você pode nos ajudar:' by_contributing: 'contribuindo com o projeto:' by_contributing_2: 'uma lista de todas as nossas necessidades' by_paypal: 'via Paypal' @@ -289,11 +321,11 @@ howto: page_title: 'How to' page_description: 'Existem diferentes formas de salvar um artigo:' tab_menu: - # add_link: "Add a link" - # shortcuts: "Use shortcuts" + add_link: "Adicionar um link" + shortcuts: "Usar atalhos" top_menu: browser_addons: 'Extensões de navegadores' - mobile_apps: "App's móveis" + mobile_apps: "Aplicativos móveis" bookmarklet: 'Bookmarklet' form: description: 'Obrigado por este formulário' @@ -308,34 +340,34 @@ howto: ios: 'na iTunes Store' windows: 'na Microsoft Store' bookmarklet: - description: 'Arraste e solve este link na sua barra de favoritos:' + description: 'Arraste e solte este link na sua barra de favoritos:' shortcuts: - # page_description: Here are the shortcuts available in wallabag. - # shortcut: Shortcut - # action: Action - # all_pages_title: Shortcuts available in all pages - # go_unread: Go to unread - # go_starred: Go to starred - # go_archive: Go to archive - # go_all: Go to all entries - # go_tags: Go to tags - # go_config: Go to config - # go_import: Go to import - # go_developers: Go to developers - # go_howto: Go to howto (this page!) - # go_logout: Logout - # list_title: Shortcuts available in listing pages - # search: Display the search form - # article_title: Shortcuts available in entry view - # open_original: Open original URL of the entry - # toggle_favorite: Toggle star status for the entry - # toggle_archive: Toggle read status for the entry - # delete: Delete the entry - # material_title: Shortcuts available with Material theme only - # add_link: Add a new link - # hide_form: Hide the current form (search or new link) - # arrows_navigation: Navigate through articles - # open_article: Display the selected entry + page_description: Aqui estão os atalhos disponíveis no wallabag. + shortcut: Atalho + action: Ação + all_pages_title: Atalhos disponíveis em todas as páginas + go_unread: Ir para não lidos + go_starred: Ir para favoritos + go_archive: Ir para arquivados + go_all: Ir a todos os artigos + go_tags: Ir para tags + go_config: Ir para configuração + go_import: Ir para importar + go_developers: Ir para desenvolvedores + go_howto: Ir para ajuda (esta página) + go_logout: Ir para sair + list_title: Atalhos disponíveis em páginas de listagem + search: Exibir o formulário de pesquisa + article_title: Atalhos disponíveis no artigo + open_original: Abrir URL original do artigo + toggle_favorite: Marcar o artigo como destacado / não destacado + toggle_archive: Marcar artigo como lido / não lido + delete: Apagar o artigo + material_title: Atalhos disponíveis apenas com o tema Material + add_link: Adicionar um novo artigo + hide_form: Ocultar o formulário atual (pesquisa ou novo artigo) + arrows_navigation: Navegar pelo artigos + open_article: Exibir o artigo selecionado quickstart: page_title: 'Começo Rápido' @@ -348,7 +380,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' @@ -369,8 +401,8 @@ quickstart: pocket: 'Migrar do Pocket' wallabag_v1: 'Migrar do wallabag v1' wallabag_v2: 'Migrar do wallabag v2' - readability: 'Migrate from Readability' - instapaper: 'Migrate from Instapaper' + readability: 'Migrar do Readability' + instapaper: 'Migrar do Instapaper' developer: title: 'Desenvolvedores' description: 'Nós também agradecemos os desenvolvedores: Docker, API, traduções, etc.' @@ -381,7 +413,7 @@ quickstart: description: "Existem muitas funcionalidades no wallabag. Não hesite em ler o manual para conhecê-las e aprender como usá-las." annotate: 'Anotar seu artigo' export: 'Converter seu artigo em ePUB ou PDF' - search_filters: 'veja coo você pode encontrar um artigo usanndo o motor de busca e filtros' + search_filters: 'veja como você pode encontrar um artigo usando o motor de busca e filtros' fetching_errors: 'O que eu posso fazer quando um artigo encontra erros na recuperação?' all_docs: 'E outros muitos artigos!' support: @@ -396,21 +428,25 @@ tag: list: number_on_the_page: '{0} Não existem tags.|{1} Uma tag.|]1,Inf[ Existem %count% tags.' see_untagged_entries: 'Ver entradas sem tags' + no_untagged_entries: 'Não há entradas sem tags.' new: - # add: 'Add' - # placeholder: 'You can add several tags, separated by a comma.' + add: 'Adicionar' + placeholder: 'Você pode adicionar varios tags, separados por vírgulas.' + rename: + placeholder: 'Você pode atualizar o nome do tag.' -# 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.

' +export: + footer_template: '

Producido por wallabag com %method%

Por favor abra um bolheto se você tiver problemas com a exibição deste E-Book no seu dispositivo.

' + unknown: 'Desconhecido' import: page_title: 'Importar' - page_description: 'Bem-vindo ao importador do wallabag. Por favo selecione o serviço do qual deseja migrar.' + page_description: 'Bem-vindo ao importador do wallabag. Por favor selecione o serviço do qual deseja migrar.' action: import_contents: 'Importar conteúdos' form: mark_as_read_title: 'Marcar todos como lidos?' - mark_as_read_label: 'Marcar todas as entradas importadas como lidas' + mark_as_read_label: 'Marcar todos os artigos importados como lidos' file_label: 'Arquivo' save_label: 'Carregar arquivo' pocket: @@ -429,13 +465,16 @@ import: wallabag_v2: page_title: 'Importar > Wallabag v2' description: 'Com este importador você importa todos os seus artigos do wallabag v2. Vá em Todos os artigos e então, na barra lateral de exportação, clique em "JSON". Você irá criar um arquivo "All articles.json".' + # elcurator: + # page_title: 'Import > elCurator' + # description: 'This importer will import all your elCurator articles. Go to your preferences in your elCurator account and then, export your content. You will have a JSON file.' readability: page_title: 'Importar > Readability' description: 'Este importador pode importar todos os artigos do Readability. Nas página ferramentas (https://www.readability.com/tools/), clique em "Export your data" na seção "Data Export". Você receberá um e-mail para fazer o download de um json (que de fato não termina com .json).' how_to: 'Por favor, selecione sua exportação do Readability e clique no botão abaixo para importá-la.' worker: enabled: "A importação é feita assíncronamente. Uma vez que a tarefa de importação é iniciada, um trabalho externo pode executar tarefas uma por vez. O serviço atual é:" - # download_images_warning: "You enabled downloading images for your articles. Combined with classic import it can take ages to proceed (or maybe failed). We strongly recommend to enable asynchronous import to avoid errors." + download_images_warning: "Você ativou o donwload de imagens para os seus artigos. Combinado com a importação clásica isso pode demorar muito tempo (ou mesmo falhar). Nós recomendamos fortemente que você ative a importação assíncrona para evitar erros." firefox: page_title: 'Importar > Firefox' description: "Com este importador você importa todos os favoritos de seu Firefox. Somente vá até seus favoritos (Ctrl+Maj+O), e em \"Importar e Backup\" e escolha \"Backup...\". Você terá então um arquivo .json." @@ -449,12 +488,12 @@ import: description: 'Este importador pode importar todos os artigos do seu Instapaper. Nas página de configurações (https://www.instapaper.com/user), clique em "Download .CSV file" na seção "Export". Um arquivo CSV será baixado (algo como "instapaper-export.csv").' how_to: 'Por favor, selecione sua exportação do seu Instapaper e clique no botão abaixo para importá-la.' pinboard: - # page_title: "Import > Pinboard" - # description: 'This importer will import all your Instapaper articles. On the backup (https://pinboard.in/settings/backup) page, click on "JSON" in the "Bookmarks" section. A JSON file will be downloaded (like "pinboard_export").' - # how_to: 'Please select your Pinboard export and click on the below button to upload and import it.' + page_title: "Importar > Pinboard" + description: 'Este importador pode importar todos os artigos do seu Pinboard. Na página de cópia de segurança (https://pinboard.in/settings/backup), clique em "JSON" na seção "Bookmarks". Um arquivo JSON será baixado (algo como "pinboard_export").' + how_to: 'Por favor selecione a sua exportação Pinboard e clique no botão em baixo para carregá-lo e importá-lo.' developer: - # page_title: 'API clients management' + page_title: 'Gestão dos clientes API' welcome_message: 'Bem-vindo a API do wallabag' documentation: 'Documentação' how_to_first_app: 'Como criar minha primeira aplicação' @@ -475,23 +514,24 @@ developer: warn_message_2: 'Se você remover isso, todo o aplicativo configurado com este cliente não poderá se autenticar no seu wallabag.' action: 'Remover este cliente' client: - # page_title: 'API clients management > Novo cliente' + page_title: 'Gestão de clientes API > Novo cliente' page_description: 'Você está prestes a criar um novo cliente. Por favor preencha o campo abaixo para a URI de redirecionamento de sua aplicação.' form: name_label: 'Nome do cliente' redirect_uris_label: 'URIs de redirecionamento' save_label: 'Criar um novo cliente' action_back: 'Voltar' + copy_to_clipboard: Copiar client_parameter: - # page_title: 'API clients management > Parâmetros de clientes' + page_title: 'Gestão de clientes API > Parâmetros de clientes' page_description: 'Aqui estão os parâmetros de seus clientes.' field_name: 'Nome do cliente' field_id: 'ID do cliente' field_secret: 'Chave do cliente' back: 'Voltar' - read_howto: 'Leia o how-to "Criar minha primeira aplicação"' + read_howto: 'Leia o guia "Criar minha primeira aplicação"' howto: - # page_title: 'API clients management > Criar minha primeira aplicação' + page_title: 'Gestão de clientes API > Criar minha primeira aplicação' description: paragraph_1: 'Os seguintes comandos fazem uso da biblioteca HTTPie. Tenha certeza que ela está instalada em seu servidor antes de usá-la.' paragraph_2: 'Você precisa de um token para a comunicação entre sua aplicação terceira e a API do wallabag.' @@ -523,36 +563,37 @@ user: email_label: 'E-mail' enabled_label: 'Habilitado' last_login_label: 'Último login' - twofactor_label: 'Autenticação de dois passos' + twofactor_email_label: Autenticação de dois fatores por e-mail + twofactor_google_label: Autenticação de dois fatores por aplicativo OTP save: 'Salvar' delete: 'Apagar' delete_confirm: 'Tem certeza?' back_to_list: 'Voltar para a lista' search: - # placeholder: Filter by username or email + placeholder: Filtrar por nome de usuário ou e-mail site_credential: - # page_title: Site credentials management - # new_site_credential: Create a credential - # edit_site_credential: Edit an existing credential - # description: "Here you can manage all credentials for sites which required them (create, edit and delete), like a paywall, an authentication, etc." + page_title: Gerenciamento de credenciais do site + new_site_credential: Criar uma credencial + edit_site_credential: Editar uma credencial existente + description: "Aqui você pode gerenciar todas as credenciais para os sites que precisam delas (criar, editar e apagar), como um paywall, uma autenticação, etc." list: actions: 'Ações' edit_action: 'Editar' yes: 'Sim' no: 'Não' - # create_new_one: Create a new credential + create_new_one: Criar uma nova credencial form: - # username_label: 'Username' - # host_label: 'Host' - # password_label: 'Password' + username_label: 'Nome de usuário' + host_label: 'Host (subdominio.exemplo.org, .exemplo.org, etc.)' + password_label: 'Senha' save: 'Salvar' delete: 'Apagar' delete_confirm: 'Tem certeza?' back_to_list: 'Voltar para a lista' error: - # page_title: An error occurred + page_title: Um erro ocorreu flashes: config: @@ -560,31 +601,37 @@ flashes: config_saved: 'Configiração salva.' 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' + user_updated: 'Informação 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' - # annotations_reset: Annotations reset - # tags_reset: Tags reset - # entries_reset: Entries reset - # archived_reset: Archived entries deleted + feed_token_updated: 'Token RSS atualizado' + feed_token_revoked: 'Token RSS revogado' + annotations_reset: Anotações reinicializadas + tags_reset: Tags reinicializados + entries_reset: Artigos reinicializados + archived_reset: Artigos arquivados apagados + otp_enabled: Autenticação de dois fatores ativada + tagging_rules_imported: Regras de tags importadas + tagging_rules_not_imported: Erro ao importar regras de tags entry: notice: - entry_already_saved: 'Entrada já foi salva em %date%' - entry_saved: 'Entrada salva' - entry_saved_failed: 'Failed to save entry' - entry_updated: 'Entrada atualizada' - entry_reloaded: 'Entrada recarregada' - entry_reloaded_failed: 'Falha em recarregar a entrada' - entry_archived: 'Entrada arquivada' - entry_unarchived: 'Entrada desarquivada' - entry_starred: 'Entrada destacada' - entry_unstarred: 'Entrada não destacada' - entry_deleted: 'Entrada apagada' + entry_already_saved: 'Artigo já foi salvo em %date%' + entry_saved: 'Artigo salvo' + entry_saved_failed: 'Artigo salvo mas falha na recuperação do conteúdo' + entry_updated: 'Artigo atualizado' + entry_reloaded: 'Artigo recarregado' + entry_reloaded_failed: 'Artigo em recarregar o artigo' + entry_archived: 'Artigo arquivado' + entry_unarchived: 'Artigo desarquivado' + entry_starred: 'Artigo destacado' + entry_unstarred: 'Artigo não destacado' + entry_deleted: 'Artigo apagado' + no_random_entry: 'Nehum artigo com esses criterios foi encontrado' tag: notice: tag_added: 'Tag adicionada' + tag_renamed: 'Tag renomeado' import: notice: failed: 'Importação falhou, por favor tente novamente.' @@ -605,6 +652,6 @@ flashes: deleted: 'Usuário "%username%" removido' site_credential: notice: - # added: 'Site credential for "%host%" added' - # updated: 'Site credential for "%host%" updated' - # deleted: 'Site credential for "%host%" deleted' + added: 'Credencial do site para "%host%" foi adicionada' + updated: 'Credencial do site pa "%host%" atualizada' + deleted: 'Credencial do site pa "%host%" removida' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml index 564fed945..f671da8d3 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml @@ -33,10 +33,12 @@ menu: back_to_unread: 'Înapoi la articolele necitite' # users_management: 'Users management' # site_credentials: 'Site credentials' + # quickstart: "Quickstart" top: 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 +55,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: @@ -65,12 +68,8 @@ config: items_per_page_label: 'Articole pe pagină' language_label: 'Limbă' reading_speed: - # label: 'Reading speed' + # label: 'Reading speed (words per minute)' # help_message: 'You can use online tools to estimate your reading speed:' - # 100_word: 'I read ~100 words per minute' - # 200_word: 'I read ~200 words per minute' - # 300_word: 'I read ~300 words per minute' - # 400_word: 'I read ~400 words per minute' action_mark_as_read: # label: 'Where do you want to be redirected to after marking an article as read?' # redirect_homepage: 'To the homepage' @@ -83,25 +82,35 @@ 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: + # token_revoke: 'Revoke the token' + 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." + # login_label: 'Login (can not be changed)' 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." + 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. @@ -127,6 +136,15 @@ config: # edit_rule_label: 'edit' # rule_label: 'Rule' # tags_label: 'Tags' + # card: + # new_tagging_rule: Create a tagging rule + # import_tagging_rules: Import tagging rules + # import_tagging_rules_detail: You have to select the JSON file you previously exported. + # export_tagging_rules: Export tagging rules + # export_tagging_rules_detail: This will download a JSON file that you can use to import tagging rules elsewhere or to backup them. + # file_label: JSON file + # import_submit: Import + # export: Export # faq: # title: 'FAQ' # tagging_rules_definition_title: 'What does « tagging rules » mean?' @@ -159,6 +177,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' @@ -237,7 +264,7 @@ entry: # provided_by: 'Provided by' new: page_title: 'Salvează un nou articol' - placeholder: 'http://website.com' + placeholder: 'https://website.ro' form_new: url_label: Url search: @@ -253,6 +280,11 @@ entry: confirm: # delete: "Are you sure you want to remove that article?" # delete_tag: "Are you sure you want to remove that tag from that article?" + metadata: + # reading_time: "Estimated reading time" + # reading_time_minutes_short: "%readingTime% min" + # address: "Address" + # added_on: "Added on" about: page_title: 'Despre' @@ -348,7 +380,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' @@ -396,12 +428,16 @@ tag: list: # number_on_the_page: '{0} There is no tag.|{1} There is one tag.|]1,Inf[ There are %count% tags.' # see_untagged_entries: 'See untagged entries' + # no_untagged_entries: 'There are no untagged entries.' 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.

' +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.

' + # unknown: 'Unknown' import: # page_title: 'Import' @@ -429,6 +465,9 @@ import: # wallabag_v2: # page_title: 'Import > Wallabag v2' # description: 'This importer will import all your wallabag v2 articles. Go to All articles, then, on the export sidebar, click on "JSON". You will have a "All articles.json" file.' + # elcurator: + # page_title: 'Import > elCurator' + # description: 'This importer will import all your elCurator articles. Go to your preferences in your elCurator account and then, export your content. You will have a JSON file.' # readability: # page_title: 'Import > Readability' # description: 'This importer will import all your Readability articles. On the tools (https://www.readability.com/tools/) page, click on "Export your data" in the "Data Export" section. You will received an email to download a json (which does not end with .json in fact).' @@ -482,6 +521,7 @@ developer: # redirect_uris_label: 'Redirect URIs' # save_label: 'Create a new client' # action_back: 'Back' + # copy_to_clipboard: Copy # client_parameter: # page_title: 'API clients management > Client parameters' # page_description: 'Here are your client parameters.' @@ -523,7 +563,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? @@ -544,7 +585,7 @@ site_credential: # create_new_one: Create a new credential # form: # username_label: 'Username' - # host_label: 'Host' + # host_label: 'Host (subdomain.example.org, .example.org, etc.)' # password_label: 'Password' # save: Save # delete: Delete @@ -561,14 +602,18 @@ 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' + # feed_token_revoked: 'RSS token revoked' # annotations_reset: Annotations reset # tags_reset: Tags reset # entries_reset: Entries reset # archived_reset: Archived entries deleted + # otp_enabled: Two-factor authentication enabled + # tagging_rules_imported: Tagging rules imported + # tagging_rules_not_imported: Error while importing tagging rules entry: notice: # entry_already_saved: 'Entry already saved on %date%' @@ -582,9 +627,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 5f210c934..7d45df1cb 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.ru.yml @@ -32,10 +32,13 @@ menu: save_link: 'СПхраМОть ссылку' back_to_unread: 'НазаЎ к МепрПчОтаММыЌ запОсяЌ' users_management: 'УправлеМОе пПльзПвателяЌО' + site_credentials: 'Site credentials' + quickstart: "Быстрый старт" top: add_new_entry: 'ДПбавОть МПвую запОсь' search: 'ППОск' filter_entries: 'ЀОльтр запОсей' + # random_entry: Jump to a random entry from that list export: 'ЭкспПрт' search_form: input_label: 'ВвеЎОте текст Ўля пПОска' @@ -52,11 +55,12 @@ config: page_title: 'НастрПйкО' tab_menu: settings: 'НастрПйкО' - rss: 'RSS' + feed: 'RSS' user_info: 'ИМфПрЌацОя П пПльзПвателе' password: 'ПарПль' rules: 'ПравОла МастрПйкО прПстаМПвкО тегПв' new_user: 'ДПбавОть пПльзПвателя' + reset: 'СбрПс ЎаММых' form: save: 'СПхраМОть' form_settings: @@ -64,41 +68,49 @@ config: items_per_page_label: 'ЗапОсей Ма страМОце' language_label: 'Язык' reading_speed: - label: 'СкПрПсть чтеМОя' + label: 'СкПрПсть чтеМОя (слПв в ЌОМуту)' help_message: 'Вы ЌПжете ОспПльзПвать ПМлайМ-ОМструЌеМты Ўля ПцеМкО скПрПстО чтеМОя:' - 100_word: 'Я чОтаю ~100 слПв в ЌОМуту' - 200_word: 'Я чОтаю ~200 слПв в ЌОМуту' - 300_word: 'Я чОтаю ~300 слПв в ЌОМуту' - 400_word: 'Я чОтаю ~400 слПв в ЌОМуту' action_mark_as_read: label: 'КуЎа Вы хПтОте быть переМаправлеМы, пПсле пПЌеткО запОсО, как прПчОтаММая?' redirect_homepage: 'На ЎПЌашМюю страМОцу' redirect_current_page: 'На текущую страМОцу' pocket_consumer_key_label: "Ключ Пт Pocket Ўля ОЌпПрта кПМтеМта" android_configuration: "НастрПйте Ваше Android прОлПжеМОе" + # android_instruction: "Touch here to prefill your Android application" help_theme: "wallabag МастраОваеЌый, зЎесь Вы ЌПжете выбрать теЌу." help_items_per_page: "Вы ЌПжете выбрать кПлОчествП ПтПбражаеЌых запОсей Ма страМОце." 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: + # token_revoke: 'Revoke the token' + feed_links: 'ссылка Ма RSS' + feed_link: unread: 'МепрПчОтаММые' starred: 'пПЌечеММые' archive: 'архОвМые' - rss_limit: 'КПлОчествП запОсей в фОЎе' + # all: 'All' + 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." + # login_label: 'Login (can not be changed)' name_label: 'ИЌя' email_label: 'Email' - twoFactorAuthentication_label: 'ДвухфактПрМая аутеМтОфОкацОя' - help_twoFactorAuthentication: "ЕслО Вы включОте ЎвухфактПрМую аутеМтОфОкацОю, тП Вы буЎете пПлучать кПЎ Ма указаММый раМее email, кажЎый раз прО вхПЎе в wallabag." + 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: "УЎалОть ЌПй аккауМт (ОлО ПпасМая зПМа)" description: "ЕслО Вы уЎалОте ваш аккауМт, ВСЕ вашО запОсО, тегО О ЎругОе ЎаММые, буЎут БЕЗВОЗВРАТНО уЎалеМы (ПперацОя Ме ЌПжет быть ПтЌеМеМа пПсле). ЗатеЌ Вы выйЎете Оз сОстеЌы." @@ -110,6 +122,7 @@ config: annotations: "УЎалОть все аММПтацОО" tags: "УЎалОть все тегО" entries: "УЎалОть все запОсО" + # archived: Remove ALL archived entries confirm: "Вы увереМы? (ДаММые буЎут БЕЗВОЗВРАТНО уЎалеМы, этО ЎействОя МеПбратОЌы)" form_password: description: "ЗЎесь Вы ЌПжете пПЌеМять свПя парПль. Ваш парПль ЎПлжеМ быть ЎлОММее 8 сОЌвПлПв." @@ -123,6 +136,15 @@ config: edit_rule_label: 'ОзЌеМОть' rule_label: 'ПравОлП' tags_label: 'тегО' + # card: + # new_tagging_rule: Create a tagging rule + # import_tagging_rules: Import tagging rules + # import_tagging_rules_detail: You have to select the JSON file you previously exported. + # export_tagging_rules: Export tagging rules + # export_tagging_rules_detail: This will download a JSON file that you can use to import tagging rules elsewhere or to backup them. + # file_label: JSON file + # import_submit: Import + # export: Export faq: title: 'FAQ' tagging_rules_definition_title: 'ЧтП зМачОт "правОлП тегОрПваМОя"?' @@ -154,6 +176,16 @@ config: or: 'ОЎМП правОлП ИЛИ ЎругПе' and: 'ОЎМП правОлП И ЎругПе' matches: 'Тесты, в кПтПрых теЌа сППтветствует пПОску (без учета регОстра). ПрОЌер: title matches "футбПл" ' + # 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: 'НазваМОе запОсО' @@ -165,6 +197,7 @@ entry: filtered_tags: 'ОтфОльтрПваММые пП тегу:' filtered_search: 'ОтфОльтрПваММые пП пПОску:' untagged: 'ЗапОсО без тегПв' + # all: 'All entries' list: number_on_the_page: '{0} ЗапОсей Ме ПбМаружеМП.|{1} ОЎМа запОсь.|]1,Inf[ НайЎеМП %count% запОсей.' reading_time: 'расчетМПе вреЌя чтеМОя' @@ -186,6 +219,8 @@ entry: unread_label: 'НепрПчОтаММая' preview_picture_label: 'Есть картОМка преЎварОтельМПгП прПсЌПтра' preview_picture_help: 'КартОМка преЎварОтельМПгП прПсЌПтра' + # is_public_label: 'Has a public link' + # is_public_help: 'Public link' language_label: 'Язык' http_status_label: 'статус HTTP' reading_time: @@ -224,10 +259,12 @@ entry: original_article: 'ПрОгОМал' annotations_on_the_entry: '{0} Нет аММПтацОО|{1} ОЎМа аММПтацОя|]1,Inf[ %count% аММПтацОй' created_at: 'Дата сПзЎаМОя' + # published_at: 'Publication date' + # published_by: 'Published by' # provided_by: 'Provided by' new: page_title: 'СПхраМОть МПвую запОсь' - placeholder: 'http://website.com' + placeholder: 'https://website.ru' form_new: url_label: Ссылка search: @@ -237,10 +274,17 @@ entry: title_label: 'НазваМОе' url_label: 'Ссылка' # origin_url_label: 'Origin url (from where you found that entry)' - is_public_label: 'ПублОчМая' save_label: 'СПхраМОть' public: shared_by_wallabag: "ЗапОсь была ПпублОкПваМа wallabag" + confirm: + # delete: "Are you sure you want to remove that article?" + # delete_tag: "Are you sure you want to remove that tag from that article?" + metadata: + # reading_time: "Estimated reading time" + # reading_time_minutes_short: "%readingTime% min" + # address: "Address" + # added_on: "Added on" about: page_title: 'О' @@ -336,7 +380,7 @@ quickstart: title: 'НастрПОть прОлПжеМОе' description: 'ЧтПбы ОЌеть прОлПжеМОе, кПтПрПе ваЌ пПЎхПЎОт, ПзМакПЌьтесь с кПМфОгурацОей wallabag.' language: 'Выбрать язык О ЎОзайМ' - rss: 'ВключОть RSS фОЎ' + feed: 'ВключОть RSS фОЎ' tagging_rules: 'СПзЎать правОлП Ўля автПЌатОческПй устаМПвкО тегПв' admin: title: 'АЎЌОМОстрОрПваМОе' @@ -384,9 +428,16 @@ tag: list: number_on_the_page: '{0} ТегО Ме МайЎеМы.|{1} НайЎеМ ПЎОМ тег.|]1,Inf[ НайЎеМП %count% тегПв.' see_untagged_entries: 'ПрПсЌПтреть запОсО без тегПв' + # no_untagged_entries: 'There are no untagged entries.' 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.

' + # unknown: 'Unknown' import: page_title: 'ИЌпПрт' @@ -414,6 +465,9 @@ import: wallabag_v2: page_title: 'ИЌпПрт > Wallabag v2' description: 'ЀуМкцОя ОЌпПрта ЎПбавОт все вашО запОсО wallabag v2. ПерейЎОте кП всеЌ статьяЌ, затеЌ Ма бПкПвПй паМелО экспПрта МажЌОте "JSON". У вас пПявОтся файл сП всеЌО запОсяЌО "All articles.json".' + # elcurator: + # page_title: 'Import > elCurator' + # description: 'This importer will import all your elCurator articles. Go to your preferences in your elCurator account and then, export your content. You will have a JSON file.' readability: page_title: 'ИЌпПрт > Readability' description: 'ЀуМкцОя ОЌпПрта ЎПбавОт все вашО запОсО Ўля чтеМОя. На страМОце ОМструЌеМтПв (https://www.readability.com/tools/) МажЌОте "ЭкспПрт вашОх ЎаММых" в разЎеле "ЭкспПрт ЎаММых". Вы пПлучОте электрПММПе пОсьЌП Ўля загрузкО json (чтП Ме закаМчОвается тПлькП .json файлПЌ).' @@ -467,6 +521,7 @@ developer: redirect_uris_label: 'Ссылка переМаправлеМОя (ПпцОПМальМП)' save_label: 'СПзЎать МПвПгП клОеМта' action_back: 'НазаЎ' + # copy_to_clipboard: Copy client_parameter: page_title: 'УправлеМОе клОеМтскОЌ API > ПараЌетры клОеМта' page_description: 'ЗЎесь вашО параЌетры клОеМта.' @@ -508,11 +563,34 @@ 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: "Вы увереМы?" back_to_list: "НазаЎ к спОску" + search: + # placeholder: Filter by login or email + +site_credential: + # page_title: Site credentials management + # new_site_credential: Create a credential + # edit_site_credential: Edit an existing credential + # description: "Here you can manage all credentials for sites which required them (create, edit and delete), like a paywall, an authentication, etc." + # list: + # actions: Actions + # edit_action: Edit + # yes: Yes + # no: No + # create_new_one: Create a new credential + # form: + # username_label: 'Login' + # host_label: 'Host (subdomain.example.org, .example.org, etc.)' + # password_label: 'Password' + # save: Save + # delete: Delete + # delete_confirm: Are you sure? + # back_to_list: Back to list error: page_title: "ПрПОзПшла ПшОбка" @@ -524,13 +602,18 @@ 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 ключ ПбМПвлеМ' + # feed_token_revoked: 'RSS token revoked' annotations_reset: "АММПтацОО сбрПшеМы" tags_reset: "ТегО сбрПшеМы" entries_reset: "ЗапОсО сбрПшеМы" + # archived_reset: Archived entries deleted + # otp_enabled: Two-factor authentication enabled + # tagging_rules_imported: Tagging rules imported + # tagging_rules_not_imported: Error while importing tagging rules entry: notice: entry_already_saved: 'ЗапОсь была сПхраМеМа раМее %date%' @@ -544,9 +627,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: 'ВП вреЌя ОЌпПрта прПОзПшла ПшОбка, пПвтПрОте пПпытку.' @@ -564,4 +649,9 @@ flashes: notice: added: 'ППльзПватель "%username%" ЎПбавлеМ' updated: 'ППльзПватель "%username%" ПбМПвлеМ' - deleted: 'ППльзПватель "%username%" уЎалеМ' \ No newline at end of file + deleted: 'ППльзПватель "%username%" уЎалеМ' + site_credential: + notice: + # added: 'Site credential for "%host%" added' + # updated: 'Site credential for "%host%" updated' + # deleted: 'Site credential for "%host%" deleted' diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml index 9d22f90d3..03f8fa9a9 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.th.yml @@ -33,10 +33,12 @@ menu: back_to_unread: 'àžàž¥àž±àžšà¹„àž›àž¢àž±àž‡àž£àž²àž¢àžàž²àž£àž—àžµà¹ˆà¹„àž¡à¹ˆà¹„àž”à¹‰àž­à¹ˆàž²àž™' users_management: 'àžàž²àž£àžˆàž±àž”àžàž²àž£àžœàž¹à¹‰à¹ƒàžŠà¹‰' site_credentials: 'àžàž²àž£àž£àž±àžšàž£àž­àž‡à¹„àž‹àž•à¹Œ' + quickstart: "à¹€àž£àžŽà¹ˆàž¡à¹àžšàžšàž”à¹ˆàž§àž™" top: add_new_entry: 'à¹€àžžàžŽà¹ˆàž¡àž£àž²àž¢àžàž²àž£à¹ƒàž«àž¡à¹ˆ' search: 'àž„à¹‰àž™àž«àž²' filter_entries: 'àž•àž±àž§àžàž£àž­àž‡àž£àž²àž¢àžàž²àž£' + # random_entry: Jump to a random entry from that list export: 'àž™àž³àž‚à¹‰àž­àž¡àž¹àž¥àž­àž­àž' search_form: input_label: 'àž„à¹‰àž™àž«àž²àž—àžµà¹ˆàž™àžµà¹‰' @@ -53,11 +55,12 @@ config: page_title: 'àžàž³àž«àž™àž”àž„à¹ˆàž²' tab_menu: settings: 'àž•àž±à¹‰àž‡àž„à¹ˆàž²' - rss: 'RSS' + feed: 'RSS' user_info: 'àž‚à¹‰àž­àž¡àž¹àž¥àžœàž¹à¹‰à¹ƒàžŠà¹‰' password: 'àž£àž«àž±àžªàžœà¹ˆàž²àž™' rules: 'àžàž²àž£à¹àž—à¹‡àžàž‚à¹‰àž­àžšàž±àž‡àž„àž±àžš' new_user: 'à¹€àžžàžŽà¹ˆàž¡àžœàž¹à¹‰à¹ƒàžŠà¹‰' + reset: 'àž£àžµà¹€àž‹à¹‡àž•àžžàž·à¹‰àž™àž—àžµà¹ˆ ' form: save: 'àžšàž±àž™àž—àž¶àž' form_settings: @@ -65,12 +68,8 @@ config: items_per_page_label: 'à¹„àž­à¹€àž—àž¡àž•à¹ˆàž­àž«àž™à¹‰àž²' language_label: 'àž àž²àž©àž²' reading_speed: - label: 'àžàž²àž£àž­à¹ˆàž²àž™à¹àžšàžšàž”à¹ˆàž§àž™' + label: 'àžàž²àž£àž­à¹ˆàž²àž™à¹àžšàžšàž”à¹ˆàž§àž™ (àž„àž³àž•à¹ˆàž­àž™àž²àž—àžµ)' help_message: 'àž„àžžàž“àžªàž²àž¡àž²àž£àž–à¹ƒàžŠà¹‰à¹€àž„àž£àž·à¹ˆàž­àž‡àž¡àž·àž­àž­àž­àž™à¹„àž¥àž™à¹Œà¹€àžžàž·à¹ˆàž­àž›àž£àž°à¹€àž¡àžŽàž™àžàž²àž£àž­à¹ˆàž²àž™à¹àžšàžšàž”à¹ˆàž§àž™:' - 100_word: 'àž‰àž±àž™àž­à¹ˆàž²àž™ ~100 àž„àž³àž•à¹ˆàž­àž™àž²àž—àžµ' - 200_word: 'àž‰àž±àž™àž­à¹ˆàž²àž™ ~200 àž„àž³àž•à¹ˆàž­àž™àž²àž—' - 300_word: 'àž‰àž±àž™àž­à¹ˆàž²àž™ ~300 àž„àž³àž•à¹ˆàž­àž™àž²àž—' - 400_word: 'àž‰àž±àž™àž­à¹ˆàž²àž™ ~400 àž„àž³àž•à¹ˆàž­àž™àž²àž—' action_mark_as_read: label: 'àž„àžžàž“àž•à¹‰àž­àž‡àžàž²àž£à¹€àž›àž¥àžµà¹ˆàž¢àž™àž—àžŽàžšàž—àž²àž‡àž«àž¥àž±àž‡àžˆàž²àžàž£àž°àžšàžžà¹€àž„àž£àž·à¹ˆàž­àž‡àž«àž¡àž²àž¢àž£àž²àž¢àžàž²àž£àž­à¹ˆàž²àž™àž—àžµà¹ˆà¹„àž«àž™?' redirect_homepage: 'à¹„àž›àž¢àž±àž‡à¹‚àž®àž¡à¹€àžžàžˆ' @@ -83,25 +82,35 @@ 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: + # token_revoke: 'Revoke the token' + 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." + # login_label: 'Login (can not be changed)' name_label: 'àžŠàž·à¹ˆàž­' email_label: 'àž­àžµà¹€àž¡àž¥' - twoFactorAuthentication_label: 'Two factor authentication' - help_twoFactorAuthentication: "àž–à¹‰àž²àž„àžžàž“à¹€àž›àžŽàž” 2FA, à¹ƒàž™à¹àž•à¹ˆàž¥àž°àžŠà¹ˆàž§àž‡à¹€àž§àž¥àž²àž—àžµà¹ˆàž„àžžàž“àž•à¹‰àž­àž‡àžàž²àž£àž¥àž‡àžŠàž·à¹ˆàž­à¹€àž‚à¹‰àž²à¹ƒàžŠ wallabag, àž„àžžàž“àžˆàž°àž•à¹‰àž­àž‡à¹„àž”à¹‰àž£àž±àžš code àžˆàž²àžàž­àžµà¹€àž¡àž¥" + 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: àž¥àžšàžšàž±àžàžŠàžµàž‚àž­àž‡àž‰àž±àž™ (à¹‚àž‹àž™àž—àžµà¹ˆà¹€àž›à¹‡àž™àž àž±àž¢!) description: àž–à¹‰àž²àž„àžžàž“àž¥àžšàžšàž±àžàžŠàžµàž‚àž­àž‡àž„àžžàž“If , àž£àž²àž¢àžàž²àž£àž—àž±à¹‰àž‡àž«àž¡àž”àž‚àž­àž‡àž„àžžàž“, à¹àž—à¹‡àžàž—àž±à¹‰àž‡àž«àž¡àž”àž‚àž­àž‡àž„àžžàž“, àž«àž¡àž²àž¢à¹€àž«àž•àžžàž—àž±à¹‰àž‡àž«àž¡àž”àž‚àž­àž‡àž„àžžàž“à¹àž¥àž°àžšàž±àžàžŠàžµàž‚àž­àž‡àž„àžžàž“àžˆàž°àž–àž¹àžàž¥àžšàž­àž¢à¹ˆàž²àž‡àž–àž²àž§àž£ (àž¡àž±àž™à¹„àž¡à¹ˆàžªàž²àž¡àž²àž£àž–àž¢àžà¹€àž¥àžŽàžà¹„àž”à¹‰) àž„àžžàž“àžˆàž°àž•à¹‰àž­àž‡àž¥àž‡àžŠàž·à¹ˆàž­àž­àž­àž @@ -127,6 +136,15 @@ config: edit_rule_label: 'àž›àž£àž±àžšà¹àžà¹‰' rule_label: 'àž‚à¹‰àž­àžšàž±àž‡àž„àž±àžš' tags_label: 'à¹àž—à¹‡àž' + # card: + # new_tagging_rule: Create a tagging rule + # import_tagging_rules: Import tagging rules + # import_tagging_rules_detail: You have to select the JSON file you previously exported. + # export_tagging_rules: Export tagging rules + # export_tagging_rules_detail: This will download a JSON file that you can use to import tagging rules elsewhere or to backup them. + # file_label: JSON file + # import_submit: Import + # export: Export faq: title: 'FAQ' tagging_rules_definition_title: 'àž‚à¹‰àž­àžšàž±àž‡àž„àž±àžšàžàž²àž£à¹àž—à¹‡àžàž„àž·àž­àž­àž°à¹„àž£?' @@ -159,6 +177,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: 'àž«àž±àž§àž‚à¹‰àž­àž£àž²àž¢àžàž²àž£' @@ -234,9 +261,10 @@ entry: created_at: 'àž§àž±àž™àž—àžµà¹ˆàžªàž£à¹‰àž²àž‡' published_at: 'àž§àž±àž™àž—àžµà¹ˆàž›àž£àž°àžàž²àžš' published_by: 'àž›àž£àž°àžàž²àžšà¹‚àž”àž¢' + # provided_by: 'Provided by' new: page_title: 'àžšàž±àž™àž—àž¶àžàž£àž²àž¢àžàž²àž£à¹ƒàž«àž¡à¹ˆ' - placeholder: 'http://website.com' + placeholder: 'https://website.th' form_new: url_label: Url search: @@ -245,12 +273,18 @@ entry: page_title: 'à¹àžà¹‰à¹„àž‚àž£àž²àž¢àžàž²àž£' title_label: 'àž«àž±àž§àž‚à¹‰àž­' url_label: 'Url' + # origin_url_label: 'Origin url (from where you found that entry)' save_label: 'àžšàž±àž™àž—àž¶àž' public: shared_by_wallabag: "àžšàž—àž„àž§àž²àž¡àž™àžµà¹‰àžˆàž°àž¡àžµàžàž²àž£à¹àžŠàž£à¹Œà¹‚àž”àž¢ %username% àžàž±àžš wallabag" confirm: delete: "àž„àžžàž“à¹àž™à¹ˆà¹ƒàžˆàž«àž£àž·àž­à¹„àž¡à¹ˆàž§à¹ˆàž²àž„àžžàž“àž•à¹‰àž­àž‡àžàž²àž£àž¥àžšàžšàž—àž„àž§àž²àž¡àž™àžµà¹‰?" delete_tag: "àž„àžžàž“à¹àž™à¹ˆà¹ƒàžˆàž«àž£àž·àž­à¹„àž¡à¹ˆàž§à¹ˆàž²àž„àžžàž“àž•à¹‰àž­àž‡àžàž²àž£àž¥àžšà¹àž—à¹‡àžàžˆàž²àžàžšàž—àž„àž§àž²àž¡àž™àžµà¹‰?" + metadata: + # reading_time: "Estimated reading time" + # reading_time_minutes_short: "%readingTime% min" + # address: "Address" + # added_on: "Added on" about: page_title: 'à¹€àžàžµà¹ˆàž¢àž§àžàž±àžš' @@ -346,7 +380,7 @@ quickstart: title: 'àžàž³àž«àž™àž”àž„à¹ˆàž²à¹àž­àžžàžžàž¥àžŽà¹€àž„àžŠàž±à¹ˆàž™' description: 'àž àž²àž¢à¹ƒàž™ order àžˆàž°àž¡àžµ application suit àž‚àž­àž‡àž„àžžàž“, àžˆàž°àž¡àž­àž‡àž«àž²àž­àž‡àž„à¹Œàž›àž£àž°àžàž­àžšàž‚àž­àž‡ wallabag' language: 'à¹€àž›àž¥àžµà¹ˆàž¢àž™àž àž²àž©àž²à¹àž¥àž°àž­àž­àžà¹àžšàžš' - rss: 'à¹€àž›àžŽàž”à¹ƒàžŠà¹‰ RSS' + feed: 'à¹€àž›àžŽàž”à¹ƒàžŠà¹‰ RSS' tagging_rules: 'à¹€àž‚àžµàž¢àž™àž‚à¹‰àž­àžšàž±àž‡àž„àž±àžšàžàž²àž£à¹àž—à¹‡àžàž­àž±àž•à¹‚àž™àž¡àž±àž•àžŽàž‚àž­àž‡àžšàž—àž„àž§àž²àž¡àž‚àž­àž‡àž„àžžàž“' admin: title: 'àžœàž¹à¹‰àž”àž¹à¹àž¥àž£àž°àžšàžš' @@ -394,12 +428,16 @@ tag: list: number_on_the_page: '{0} à¹„àž¡à¹ˆàž¡àžµàžàž²àž£à¹àž—à¹‡àž|{1} àž¡àžµàž«àž™àž¶à¹ˆàž‡à¹àž—à¹‡àž|]1,Inf[ àž¡àžµ %count% à¹àž—à¹‡àž' see_untagged_entries: 'àžžàžšàž£àž²àž¢àžàž²àž£àž—àžµà¹ˆà¹„àž¡à¹ˆà¹„àž”à¹‰à¹àž—à¹‡àž' + # no_untagged_entries: 'There are no untagged entries.' 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.

' + # unknown: 'Unknown' import: page_title: 'àž™àž³àž‚à¹‰àž­àž¡àž¹àž¥à¹€àžŠà¹‰àž²' @@ -427,6 +465,9 @@ import: wallabag_v2: page_title: 'àž™àž³à¹€àž‚à¹‰àž²àž‚à¹‰àž­àž¡àž¹àž¥ > Wallabag v2' description: 'àžªàž³àž«àž£àž±àžšàžœàž¹à¹‰àž™àž³à¹€àž‚à¹‰àž²àž‚à¹‰àž­àž¡àž¹àž¥àžˆàž° import àžšàž—àž„àž§àž²àž¡ wallabag v2 àž—àž±à¹‰àž‡àž«àž¡àž”àž‚àž­àž‡àž„àžžàž“ à¹„àž›àž¢àž±àž‡àžšàž—àž„àž§àž²àž¡àž—àž±à¹‰àž‡àž«àž¡àž”, àž”àž±àž‡àž™àž±à¹‰àž™, àžšàž™ export sidebar, click àž—àžµà¹ˆ "JSON" àž„àžžàž“àžˆàž°àž¡àžµà¹„àžŸàž¥à¹Œ "All articles.json"' + # elcurator: + # page_title: 'Import > elCurator' + # description: 'This importer will import all your elCurator articles. Go to your preferences in your elCurator account and then, export your content. You will have a JSON file.' readability: page_title: 'àž™àž³à¹€àž‚à¹‰àž²àž‚à¹‰àž­àž¡àž¹àž¥ > Readability' description: 'àžªàž³àž«àž£àž±àžšàžœàž¹à¹‰àž™àž³à¹€àž‚à¹‰àž²àž‚à¹‰àž­àž¡àž¹àž¥àžˆàž° import àžšàž—àž„àž§àž²àž¡ Readability àž—àž±à¹‰àž‡àž«àž¡àž”àž‚àž­àž‡àž„àžžàž“ à¹„àž›àž—àžµà¹ˆà¹€àž„àž£àž·à¹ˆàž­àž‡àž¡àž·àž­ (https://www.readability.com/tools/) àž‚àž­àž‡àž«àž™à¹‰àž²àž™àž±à¹‰àž™, click àž—àžµà¹ˆ "Export your data" à¹ƒàž™àžªà¹ˆàž§àž™ "Data Export" àž„àžžàž“àžˆàž°à¹„àž”à¹‰àž£àž±àžš email à¹„àž› download json (which does not end with .json in fact).' @@ -480,6 +521,7 @@ developer: redirect_uris_label: 'à¹€àžªà¹‰àž™àž—àž²àž‡à¹ƒàž«àž¡à¹ˆàž‚àž­àž‡ URIs (à¹ƒàž«à¹‰à¹€àž¥àž·àž­àžà¹„àž”à¹‰)' save_label: 'àžªàž£à¹ˆà¹‰àž²àž‡àž¥àž¹àžàž‚à¹ˆàž²àž¢à¹ƒàž«àž¡' action_back: 'àžàž¥àž±àžš' + # copy_to_clipboard: Copy client_parameter: page_title: 'àžàž²àž£àžˆàž±àž”àžàž²àž£àž¥àž¹àžàž‚à¹ˆàž²àž¢àž‚àž­àž‡ API > àžžàž²àž£àž²àž¡àžŽà¹€àž•àž­àž£à¹Œàž‚àž­àž‡àž¥àž¹àžàž‚à¹ˆàž²àž¢' page_description: 'àž—àžµà¹ˆàž™àžµà¹‰à¹€àž›à¹‡àž™àžžàž²àž£àž²àž¡àžŽà¹€àž•àž­àž£à¹Œàž‚àž­àž‡àž¥àž¹àžàž‚à¹ˆàž²àž¢àž‚àž­àž‡àž„àžžàž“' @@ -521,7 +563,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: àž•àžžàž“à¹àž™à¹ˆà¹ƒàžˆàž«àž£àž·àž­à¹„àž¡à¹ˆ? @@ -542,7 +585,7 @@ site_credential: create_new_one: àžªàž£à¹‰àž²àž‡àž‚à¹‰àž­àž¡àž¹àž¥àžªà¹ˆàž§àž™àž•àž±àž§à¹ƒàž«àž¡à¹ˆ form: username_label: 'àžŠàž·à¹ˆàž­àžœàž¹à¹‰à¹ƒàžŠà¹‰' - host_label: 'à¹‚àž®àžª' + host_label: 'à¹‚àž®àžª (subdomain.example.org, .example.org, etc.)' password_label: 'àž£àž«àž±àžªàžœà¹ˆàž²àž™' save: àžšàž±àž™àž—àž¶àž delete: àž¥àžš @@ -559,14 +602,18 @@ 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 ' + # feed_token_revoked: 'RSS token revoked' annotations_reset: àž£àžµà¹€àž‹à¹‡àž•àž«àž¡àž²àž¢à¹€àž«àž•àžž tags_reset: àž£àžµà¹€àž‹à¹‡àž•à¹àž—à¹‡àž entries_reset: àž£àžµà¹€àž‹à¹‡àž•àž£àž²àž¢àžàž²àž£ archived_reset: àžàž²àž£àž¥àžšà¹€àž­àžàžªàž²àž£àž‚àž­àž‡àž£àž²àž¢àžàž²àž£ + # otp_enabled: Two-factor authentication enabled + # tagging_rules_imported: Tagging rules imported + # tagging_rules_not_imported: Error while importing tagging rules entry: notice: entry_already_saved: 'àž£àž²àž¢àžàž²àž£àžžàž£à¹‰àž­àž¡àžšàž±àž™àž—àž¶àžàž—àžµà¹ˆ %date%' @@ -580,9 +627,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 4c71f0b96..05cfbbfc9 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml @@ -33,10 +33,12 @@ menu: back_to_unread: 'Okunmayan makalelere geri dön' # users_management: 'Users management' # site_credentials: 'Site credentials' + quickstart: "Hızlı başlangıç" top: 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 +55,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: @@ -65,12 +68,8 @@ config: items_per_page_label: 'Sayfa başına makale sayısı' language_label: 'Dil' reading_speed: - # label: 'Reading speed' + # label: 'Reading speed (words per minute)' # help_message: 'You can use online tools to estimate your reading speed:' - # 100_word: 'I read ~100 words per minute' - # 200_word: 'I read ~200 words per minute' - # 300_word: 'I read ~300 words per minute' - # 400_word: 'I read ~400 words per minute' action_mark_as_read: # label: 'Where do you want to be redirected to after marking an article as read?' # redirect_homepage: 'To the homepage' @@ -83,25 +82,35 @@ 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: + # token_revoke: 'Revoke the token' + 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." + # login_label: 'Login (can not be changed)' 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." + 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. @@ -127,6 +136,15 @@ config: # edit_rule_label: 'edit' rule_label: 'Kural' tags_label: 'Etiketler' + # card: + # new_tagging_rule: Create a tagging rule + # import_tagging_rules: Import tagging rules + # import_tagging_rules_detail: You have to select the JSON file you previously exported. + # export_tagging_rules: Export tagging rules + # export_tagging_rules_detail: This will download a JSON file that you can use to import tagging rules elsewhere or to backup them. + # file_label: JSON file + # import_submit: Import + # export: Export faq: title: 'S.S.S.' tagging_rules_definition_title: '« etiketleme kuralları » ne anlama geliyor?' @@ -159,6 +177,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ığı' @@ -192,6 +219,8 @@ entry: unread_label: 'Okunmayan' preview_picture_label: 'Resim önizlemesi varsa' preview_picture_help: 'Resim önizlemesi' + # is_public_label: 'Has a public link' + # is_public_help: 'Public link' language_label: 'Dil' # http_status_label: 'HTTP status' reading_time: @@ -235,7 +264,7 @@ entry: # provided_by: 'Provided by' new: page_title: 'Yeni makaleyi kaydet' - placeholder: 'http://website.com' + placeholder: 'https://website.tr' form_new: url_label: Url search: @@ -251,6 +280,11 @@ entry: confirm: # delete: "Are you sure you want to remove that article?" # delete_tag: "Are you sure you want to remove that tag from that article?" + metadata: + # reading_time: "Estimated reading time" + # reading_time_minutes_short: "%readingTime% min" + # address: "Address" + # added_on: "Added on" about: page_title: 'Hakkımızda' @@ -346,7 +380,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' @@ -394,12 +428,16 @@ tag: list: number_on_the_page: '{0} Herhangi bir etiket yok.|{1} Burada bir adet etiket var.|]1,Inf[ Burada %count% adet etiket var.' # see_untagged_entries: 'See untagged entries' + # no_untagged_entries: 'There are no untagged entries.' 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.

' +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.

' + # unknown: 'Unknown' import: page_title: 'İçe Aktar' @@ -427,6 +465,9 @@ import: wallabag_v2: page_title: 'İçe Aktar > Wallabag v2' # description: 'This importer will import all your wallabag v2 articles. Go to All articles, then, on the export sidebar, click on "JSON". You will have a "All articles.json" file.' + # elcurator: + # page_title: 'Import > elCurator' + # description: 'This importer will import all your elCurator articles. Go to your preferences in your elCurator account and then, export your content. You will have a JSON file.' readability: page_title: 'İçe Aktar > Readability' # description: 'This importer will import all your Readability articles. On the tools (https://www.readability.com/tools/) page, click on "Export your data" in the "Data Export" section. You will received an email to download a json (which does not end with .json in fact).' @@ -480,6 +521,7 @@ developer: # redirect_uris_label: 'Redirect URIs' # save_label: 'Create a new client' # action_back: 'Back' + # copy_to_clipboard: Copy # client_parameter: # page_title: 'API clients management > Client parameters' # page_description: 'Here are your client parameters.' @@ -521,7 +563,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? @@ -529,6 +572,26 @@ user: search: # placeholder: Filter by username or email +site_credential: + # page_title: Site credentials management + # new_site_credential: Create a credential + # edit_site_credential: Edit an existing credential + # description: "Here you can manage all credentials for sites which required them (create, edit and delete), like a paywall, an authentication, etc." + # list: + # actions: Actions + # edit_action: Edit + # yes: Yes + # no: No + # create_new_one: Create a new credential + # form: + # username_label: 'Login' + # host_label: 'Host (subdomain.example.org, .example.org, etc.)' + # password_label: 'Password' + # save: Save + # delete: Delete + # delete_confirm: Are you sure? + # back_to_list: Back to list + error: # page_title: An error occurred @@ -539,14 +602,18 @@ 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' - tagging_rules_updated: 'Tagging rules updated' - tagging_rules_deleted: 'Tagging rule deleted' - rss_token_updated: 'RSS token updated' + feed_updated: 'RSS bilgiler gÃŒncellendi' + # tagging_rules_updated: 'Tagging rules updated' + # tagging_rules_deleted: 'Tagging rule deleted' + # feed_token_updated: 'RSS token updated' + # feed_token_revoked: 'RSS token revoked' # annotations_reset: Annotations reset # tags_reset: Tags reset # entries_reset: Entries reset # archived_reset: Archived entries deleted + # otp_enabled: Two-factor authentication enabled + # tagging_rules_imported: Tagging rules imported + # tagging_rules_not_imported: Error while importing tagging rules entry: notice: entry_already_saved: 'Entry already saved on %date%' @@ -560,9 +627,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 c6a842098..c04389788 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.da.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.da.yml @@ -3,5 +3,5 @@ validator: password_too_short: 'Adgangskoden skal vÊre mindst 8 tegn' # password_wrong_value: 'Wrong value for your current password' # item_per_page_too_high: 'This will certainly kill the app' - # rss_limit_too_high: 'This will certainly kill the app' + # feed_limit_too_high: 'This will certainly kill the app' # quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.de.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.de.yml index 907b67a5d..4c675ef40 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.de.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.de.yml @@ -3,6 +3,5 @@ validator: password_too_short: 'Kennwort-MindestlÀnge von acht Zeichen nicht erfÃŒllt' password_wrong_value: 'Falscher Wert fÃŒr dein aktuelles Kennwort' item_per_page_too_high: 'Dies wird die Anwendung möglicherweise beenden' - rss_limit_too_high: 'Dies wird die Anwendung möglicherweise beenden' + feed_limit_too_high: 'Dies wird die Anwendung möglicherweise beenden' quote_length_too_high: 'Das Zitat ist zu lang. Es sollte nicht mehr als {{ limit }} Zeichen enthalten.' - diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.en.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.en.yml index 8cc117fe0..89d4c68a5 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.en.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.en.yml @@ -3,5 +3,5 @@ validator: password_too_short: 'Password should by at least 8 chars long' password_wrong_value: 'Wrong value for your current password' item_per_page_too_high: 'This will certainly kill the app' - rss_limit_too_high: 'This will certainly kill the app' + feed_limit_too_high: 'This will certainly kill the app' quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.es.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.es.yml index 97a8edfa4..ea6575eb2 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.es.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.es.yml @@ -1,7 +1,7 @@ validator: password_must_match: 'Las contraseñas no coinciden' - password_too_short: 'La contraseña debe tener al menos 8 carácteres' - password_wrong_value: 'Entrada equivocada para su contraseña actual' + password_too_short: 'La contraseña debe tener al menos 8 caracteres' + password_wrong_value: 'La contraseña actual es incorrecta' item_per_page_too_high: 'Esto matará la aplicación' - rss_limit_too_high: 'Esto matará la aplicación' - # quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' + feed_limit_too_high: 'Esto matará la aplicación' + quote_length_too_high: 'La cita es demasiado larga. Debe tener {{ limit }} caracteres o menos.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.fa.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.fa.yml index ef677525d..9b1a4af2b 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.fa.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.fa.yml @@ -3,5 +3,5 @@ validator: password_too_short: 'رمز ؎ما ؚاید Ûž حرف یا ؚی؎تر ؚا؎د' password_wrong_value: 'رمز فعلی را ا؎تؚاه وارد کرده‌اید' item_per_page_too_high: 'ؚا این تعداد ؚرنامه ØšÙ‡ فنا می‌رود' - rss_limit_too_high: 'ؚا این تعداد ؚرنامه ØšÙ‡ فنا می‌رود' + feed_limit_too_high: 'ؚا این تعداد ؚرنامه ØšÙ‡ فنا می‌رود' # quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.fr.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.fr.yml index f31b4ed27..92f69aa03 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.fr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.fr.yml @@ -3,5 +3,5 @@ validator: password_too_short: "Le mot de passe doit contenir au moins 8 caractÚres" password_wrong_value: "Votre mot de passe actuel est faux" item_per_page_too_high: "Ça ne va pas plaire à l’application" - rss_limit_too_high: "Ça ne va pas plaire à l’application" + feed_limit_too_high: "Ça ne va pas plaire à l’application" quote_length_too_high: "La citation est trop longue. Elle doit avoir au maximum {{ limit }} caractÚres." diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.it.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.it.yml index d949cc3bd..b20d6f513 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.it.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.it.yml @@ -3,5 +3,5 @@ validator: password_too_short: 'La password deve essere lunga almeno 8 caratteri' password_wrong_value: 'Valore inserito per la password corrente errato' item_per_page_too_high: 'Questo valore Ú troppo alto' - rss_limit_too_high: 'Questo valore Ú troppo alto' + feed_limit_too_high: 'Questo valore Ú troppo alto' # quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.oc.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.oc.yml index 87f00f101..cb57844fe 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.oc.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.oc.yml @@ -3,5 +3,5 @@ validator: password_too_short: 'Lo senhal deu aver almens 8 caractÚrs' password_wrong_value: 'Vòstre senhal actual es pas bon' item_per_page_too_high: "Aquò li agradarà pas a l'aplicacion" - rss_limit_too_high: "Aquò li agradarà pas a l'aplicacion" + feed_limit_too_high: "Aquò li agradarà pas a l'aplicacion" quote_length_too_high: 'Aquesta citacion es tròpa longa. Cal que faga {{ limit }} caractÚrs o mens.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.pl.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.pl.yml index e4165c148..94757cc50 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.pl.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.pl.yml @@ -3,5 +3,5 @@ validator: password_too_short: 'Hasło powinno mieć minimum 8 znaków długości' password_wrong_value: 'Twoje obecne hasło jest błędne' item_per_page_too_high: 'To moÅŒe spowodować problemy z aplikacją' - rss_limit_too_high: 'To moÅŒe spowodować problemy z aplikacją' + feed_limit_too_high: 'To moÅŒe spowodować problemy z aplikacją' quote_length_too_high: 'Cytat jest zbyt długi. powinien mieć {{ limit }} znaków lub mniej.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.pt.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.pt.yml index a8c1f9de4..1d69af97d 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' - # quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' + feed_limit_too_high: 'Certamente isso pode matar a aplicação' + quote_length_too_high: 'A citaçãpo é muito longa. Ela deve ter {{ limit }} caracteres ou menos.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.ro.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.ro.yml index 6840cf115..e5c8a72f1 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.ro.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.ro.yml @@ -3,5 +3,5 @@ validator: password_too_short: 'Parola ar trebui să conțină cel puțin 8 caractere' # password_wrong_value: 'Wrong value for your current password' # item_per_page_too_high: 'This will certainly kill the app' - # rss_limit_too_high: 'This will certainly kill the app' + # feed_limit_too_high: 'This will certainly kill the app' # quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' diff --git a/src/Wallabag/CoreBundle/Resources/translations/validators.tr.yml b/src/Wallabag/CoreBundle/Resources/translations/validators.tr.yml index e1e7317f7..881ffd3be 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/validators.tr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/validators.tr.yml @@ -3,5 +3,5 @@ validator: # password_too_short: 'Password should by at least 8 chars long' # password_wrong_value: 'Wrong value for your current password' # item_per_page_too_high: 'This will certainly kill the app' - # rss_limit_too_high: 'This will certainly kill the app' + # feed_limit_too_high: 'This will certainly kill the app' # quote_length_too_high: 'The quote is too long. It should have {{ limit }} characters or less.' diff --git a/src/Wallabag/CoreBundle/Resources/views/base.html.twig b/src/Wallabag/CoreBundle/Resources/views/base.html.twig index aa388bcbc..496b3fb6b 100644 --- a/src/Wallabag/CoreBundle/Resources/views/base.html.twig +++ b/src/Wallabag/CoreBundle/Resources/views/base.html.twig @@ -1,13 +1,15 @@ - - - - - +{% set lang = app.request.locale|default('') -%} + + + + + {% block head %} + @@ -42,6 +44,7 @@ {% block css %} {% endblock %} + {% block scripts %} 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 bcc57dace..f719bea25 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,42 @@ {{ 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 feed.token %} + – {{ 'config.form_feed.token_reset'|trans }} + – {{ 'config.form_feed.token_revoke'|trans }} + {% else %} + – {{ 'config.form_feed.token_create'|trans }} {% endif %} - – - - {% if rss.token %} - {{ 'config.form_rss.token_reset'|trans }} - {% else %} - {{ 'config.form_rss.token_create'|trans }} - {% endif %} -
- {% if rss.token %} + {% if feed.token %}
@@ -139,19 +137,25 @@
- {{ 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 }}

{{ form_start(form.user) }} {{ form_errors(form.user) }} +
+
+ + {{ app.user.username }} +
+
@@ -169,52 +173,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 - -
+ + + + + + + + + + + + + + + + + + + + + +
{{ '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 %} -

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

-
-

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

- -
- {{ form_widget(form.user._token) }} - {{ form_widget(form.user.save) }} {% if enabled_users > 1 %} @@ -277,7 +270,7 @@ {% endfor %} - {{ form_start(form.new_tagging_rule) }} + {{ form_start(form.new_tagging_rule) }} {{ form_errors(form.new_tagging_rule) }}
@@ -298,6 +291,34 @@ {{ form_rest(form.new_tagging_rule) }} + +
+

{{ 'config.form_rules.card.import_tagging_rules'|trans }}

+

{{ 'config.form_rules.card.import_tagging_rules_detail'|trans }}

+
+ + {{ form_start(form.import_tagging_rule) }} + {{ form_errors(form.import_tagging_rule) }} + +
+
+ {{ form_label(form.import_tagging_rule.file) }} + {{ form_errors(form.import_tagging_rule.file) }} + {{ form_widget(form.import_tagging_rule.file) }} +
+
+ + {{ form_rest(form.import_tagging_rule) }} + + + {% if app.user.config.taggingRules is not empty %} +
+

{{ 'config.form_rules.card.export_tagging_rules'|trans }}

+

{{ 'config.form_rules.card.export_tagging_rules_detail'|trans }}

+

{{ 'config.form_rules.export'|trans }}

+
+ {% endif %} +

{{ 'config.form_rules.faq.title'|trans }}

@@ -382,4 +403,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 000000000..1d3685ae6 --- /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 cfc6644b8..b747ed84a 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) }}
- {{ 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 }} + – {{ 'config.form_feed.token_revoke'|trans }} {% else %} - {{ 'config.form_rss.token_create'|trans }} - {% endif %} + – {{ 'config.form_feed.token_create'|trans }} + {% endif %}
- {% if rss.token %} + {% if feed.token %} @@ -165,14 +166,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) }}
@@ -180,6 +181,15 @@ {{ form_start(form.user) }} {{ form_errors(form.user) }} +
+
+
{{ 'config.form_user.login_label'|trans }}
+
+ {{ app.user.username }} +
+
+
+
{{ form_label(form.user.name) }} @@ -196,59 +206,42 @@
- {% if twofactor_auth %} -
-
- {{ 'config.form_user.two_factor_description'|trans }} - -
- - {{ form_widget(form.user.twoFactorAuthentication) }} - {{ form_label(form.user.twoFactorAuthentication) }} - {{ form_errors(form.user.twoFactorAuthentication) }} -
- -
- {% endif %} - {{ form_widget(form.user.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }} + + {% if twofactor_auth %} +
+
+
+
{{ 'config.otp.page_title'|trans }}
+ +

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

+ + + + + + + + + + + + + + + + + + + + + + +
{{ 'config.form_user.two_factor.table_method'|trans }}{{ 'config.form_user.two_factor.table_state'|trans }}{{ 'config.form_user.two_factor.table_action'|trans }}
{{ 'config.form_user.two_factor.emailTwoFactor_label'|trans }}{% if app.user.isEmailTwoFactor %}{{ 'config.form_user.two_factor.state_enabled'|trans }}{% else %}{{ 'config.form_user.two_factor.state_disabled'|trans }}{% endif %}{{ 'config.form_user.two_factor.action_email'|trans }}
{{ 'config.form_user.two_factor.googleTwoFactor_label'|trans }}{% if app.user.isGoogleTwoFactor %}{{ 'config.form_user.two_factor.state_enabled'|trans }}{% else %}{{ 'config.form_user.two_factor.state_disabled'|trans }}{% endif %}{{ 'config.form_user.two_factor.action_app'|trans }}
+
+ {% endif %} {{ form_widget(form.user._token) }} - -


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


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

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

- -
- {% endif %}
@@ -314,28 +307,77 @@
{% endif %} - {{ form_start(form.new_tagging_rule) }} - {{ form_errors(form.new_tagging_rule) }} +
    +
  • +
    +
    + {{ 'config.form_rules.card.new_tagging_rule'|trans }} -
    -
    - {{ form_label(form.new_tagging_rule.rule) }} - {{ form_errors(form.new_tagging_rule.rule) }} - {{ form_widget(form.new_tagging_rule.rule) }} + {{ form_start(form.new_tagging_rule) }} + {{ form_errors(form.new_tagging_rule) }} + +
    +
    + {{ form_label(form.new_tagging_rule.rule) }} + {{ form_errors(form.new_tagging_rule.rule) }} + {{ form_widget(form.new_tagging_rule.rule) }} +
    +
    + +
    +
    + {{ form_label(form.new_tagging_rule.tags) }} + {{ form_errors(form.new_tagging_rule.tags) }} + {{ form_widget(form.new_tagging_rule.tags) }} +
    +
    + + {{ form_widget(form.new_tagging_rule.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }} + {{ form_rest(form.new_tagging_rule) }} + +
    -
    +
  • +
  • +
    +
    + {{ 'config.form_rules.card.import_tagging_rules'|trans }} +

    {{ 'config.form_rules.card.import_tagging_rules_detail'|trans }}

    + {{ form_start(form.import_tagging_rule) }} + {{ form_errors(form.import_tagging_rule) }} +
    +
    + {{ form_errors(form.import_tagging_rule.file) }} +
    + {{ form.import_tagging_rule.file.vars.label|trans }} + {{ form_widget(form.import_tagging_rule.file) }} +
    +
    + +
    +
    +
    -
    -
    - {{ form_label(form.new_tagging_rule.tags) }} - {{ form_errors(form.new_tagging_rule.tags) }} - {{ form_widget(form.new_tagging_rule.tags) }} + {{ form_widget(form.import_tagging_rule.import, { 'attr': {'class': 'btn waves-effect waves-light'} }) }} + + {{ form_rest(form.import_tagging_rule) }} + +
    -
    - - {{ form_widget(form.new_tagging_rule.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }} - {{ form_rest(form.new_tagging_rule) }} - +
  • + {% if app.user.config.taggingRules is not empty %} +
  • +
    +
    + {{ 'config.form_rules.card.export_tagging_rules'|trans }} +

    {{ 'config.form_rules.card.export_tagging_rules_detail'|trans }}

    +
    +

    {{ 'config.form_rules.export'|trans }}

    +
    +
    +
  • + {% endif %} +
@@ -422,6 +464,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 000000000..6f405d7f3 --- /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/Card/_content.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/Card/_content.html.twig index 1f3cd1a72..1102a0bdb 100644 --- a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/Card/_content.html.twig +++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/Card/_content.html.twig @@ -8,8 +8,11 @@
{{ entry.domainName|removeWww }} - {% if withTags is defined %} + {% if withMetadata is defined %} {% include "@WallabagCore/themes/material/Entry/_tags.html.twig" with {'tags': entry.tags | slice(0, 3), 'entryId': entry.id, 'listClass': ' hide-on-med-and-down'} only %} +
+
{% include "@WallabagCore/themes/material/Entry/_reading_time.html.twig" with {'entry': entry} only %}
+
{% endif %}
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_actions.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_actions.html.twig index 827f09d97..be764e108 100644 --- a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_actions.html.twig +++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_actions.html.twig @@ -1,9 +1,11 @@
- - {% include "@WallabagCore/themes/material/Entry/_reading_time.html.twig" with {'entry': entry} only %} - today -  {{ entry.createdAt|date('Y-m-d') }} - +
+
{% include "@WallabagCore/themes/material/Entry/_reading_time.html.twig" with {'entry': entry} only %}
+
+ today +  {{ entry.createdAt|date('Y-m-d') }} +
+
  • diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_list.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_list.html.twig index 1c00f2fa3..6a0950355 100644 --- a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_list.html.twig +++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_list.html.twig @@ -5,7 +5,7 @@
- {% include "@WallabagCore/themes/material/Entry/Card/_content.html.twig" with {'entry': entry, 'withTags': true, 'subClass': 'metadata'} only %} + {% include "@WallabagCore/themes/material/Entry/Card/_content.html.twig" with {'entry': entry, 'withMetadata': true, 'subClass': 'metadata'} only %}
  • {% if entry.isArchived == 0 %}done{% else %}unarchive{% endif %} diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_reading_time.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_reading_time.html.twig index 6ba187684..b7167e958 100644 --- a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_reading_time.html.twig +++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_reading_time.html.twig @@ -1,4 +1,4 @@ -{% set readingTime = entry.readingTime / app.user.config.readingSpeed %} +{% set readingTime = entry.readingTime / app.user.config.readingSpeed * 200 %} timer {% if readingTime > 0 %} {{ 'entry.list.reading_time_minutes_short'|trans({'%readingTime%': readingTime|round}) }} 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 a137f3c34..3906e1e0e 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,17 +62,14 @@ {% set currentTag = null %} {% if tag is defined %} {% set currentTag = tag.slug %} - {% endif %} - {% if currentRoute == 'homepage' %} - {% set currentRoute = 'unread' %} {% endif %}

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

      {% if craue_setting('export_epub') %}
    • EPUB
    • {% endif %} {% if craue_setting('export_mobi') %}
    • MOBI
    • {% endif %} {% if craue_setting('export_pdf') %}
    • PDF
    • {% endif %} - {% if craue_setting('export_csv') %}
    • JSON
    • {% endif %} - {% if craue_setting('export_json') %}
    • CSV
    • {% endif %} + {% if craue_setting('export_json') %}
    • JSON
    • {% endif %} + {% if craue_setting('export_csv') %}
    • CSV
    • {% endif %} {% if craue_setting('export_txt') %}
    • TXT
    • {% endif %} {% if craue_setting('export_xml') %}
    • XML
    • {% endif %}
    @@ -83,6 +83,12 @@

    {{ 'entry.filters.title'|trans }}

    + {% if currentRoute != 'untagged' and nbEntriesUntagged != 0 %} + + {% endif %} +
    diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/entry.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/entry.html.twig index c6c19de6b..e23fa0e19 100644 --- a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/entry.html.twig +++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/entry.html.twig @@ -8,7 +8,7 @@
    -
  • + {% if activeRoute %} +
  • + + casino + +
  • + {% endif %}
  • filter_list @@ -125,7 +137,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 46bb1dc5c..b7ad79664 100644 --- a/src/Wallabag/CoreBundle/Tools/Utils.php +++ b/src/Wallabag/CoreBundle/Tools/Utils.php @@ -5,7 +5,7 @@ namespace Wallabag\CoreBundle\Tools; class Utils { /** - * Generate a token used for RSS. + * Generate a token used for Feeds. * * @param int $length Length of the token * @@ -20,15 +20,14 @@ class Utils } /** - * For a given text, we calculate reading time for an article - * based on 200 words per minute. + * For a given text, we calculate reading time for an article based on 200 words per minute. * - * @param $text + * @param string $text * * @return float */ public static function getReadingTime($text) { - return floor(\count(preg_split('~[^\p{L}\p{N}\']+~u', strip_tags($text))) / 200); + return floor(\count(preg_split('~([^\p{L}\p{N}\']+|(\p{Han}|\p{Hiragana}|\p{Katakana}|\p{Hangul}){1,2})~u', strip_tags($text))) / 200); } } diff --git a/src/Wallabag/CoreBundle/Twig/WallabagExtension.php b/src/Wallabag/CoreBundle/Twig/WallabagExtension.php index 00b1e5954..02f17f50a 100644 --- a/src/Wallabag/CoreBundle/Twig/WallabagExtension.php +++ b/src/Wallabag/CoreBundle/Twig/WallabagExtension.php @@ -4,10 +4,14 @@ namespace Wallabag\CoreBundle\Twig; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Translation\TranslatorInterface; +use Twig\Extension\AbstractExtension; +use Twig\Extension\GlobalsInterface; +use Twig\TwigFilter; +use Twig\TwigFunction; use Wallabag\CoreBundle\Repository\EntryRepository; use Wallabag\CoreBundle\Repository\TagRepository; -class WallabagExtension extends \Twig_Extension implements \Twig_Extension_GlobalsInterface +class WallabagExtension extends AbstractExtension implements GlobalsInterface { private $tokenStorage; private $entryRepository; @@ -24,20 +28,26 @@ class WallabagExtension extends \Twig_Extension implements \Twig_Extension_Globa $this->translator = $translator; } + public function getGlobals() + { + return []; + } + public function getFilters() { return [ - new \Twig_SimpleFilter('removeWww', [$this, 'removeWww']), - new \Twig_SimpleFilter('removeSchemeAndWww', [$this, 'removeSchemeAndWww']), + new TwigFilter('removeWww', [$this, 'removeWww']), + new TwigFilter('removeScheme', [$this, 'removeScheme']), + new TwigFilter('removeSchemeAndWww', [$this, 'removeSchemeAndWww']), ]; } public function getFunctions() { return [ - new \Twig_SimpleFunction('count_entries', [$this, 'countEntries']), - new \Twig_SimpleFunction('count_tags', [$this, 'countTags']), - new \Twig_SimpleFunction('display_stats', [$this, 'displayStats']), + new TwigFunction('count_entries', [$this, 'countEntries']), + new TwigFunction('count_tags', [$this, 'countTags']), + new TwigFunction('display_stats', [$this, 'displayStats']), ]; } @@ -46,11 +56,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/Consumer/AbstractConsumer.php b/src/Wallabag/ImportBundle/Consumer/AbstractConsumer.php index b035f5cc5..e4bfbdf03 100644 --- a/src/Wallabag/ImportBundle/Consumer/AbstractConsumer.php +++ b/src/Wallabag/ImportBundle/Consumer/AbstractConsumer.php @@ -52,6 +52,13 @@ abstract class AbstractConsumer $this->import->setUser($user); + if (false === $this->import->validateEntry($storedEntry)) { + $this->logger->warning('Entry is invalid', ['entry' => $storedEntry]); + + // return true to skip message + return true; + } + $entry = $this->import->parseEntry($storedEntry); if (null === $entry) { diff --git a/src/Wallabag/ImportBundle/Controller/BrowserController.php b/src/Wallabag/ImportBundle/Controller/BrowserController.php index 6418925c0..8c2bdfe58 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 @@ -13,8 +13,6 @@ abstract class BrowserController extends Controller /** * @Route("/browser", name="import_browser") * - * @param Request $request - * * @return Response */ public function indexAction(Request $request) diff --git a/src/Wallabag/ImportBundle/Controller/ChromeController.php b/src/Wallabag/ImportBundle/Controller/ChromeController.php index 0cb418a1a..6628cdb0b 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/ElcuratorController.php b/src/Wallabag/ImportBundle/Controller/ElcuratorController.php new file mode 100644 index 000000000..174c2c963 --- /dev/null +++ b/src/Wallabag/ImportBundle/Controller/ElcuratorController.php @@ -0,0 +1,41 @@ +get('wallabag_import.elcurator.import'); + + if ($this->get('craue_config')->get('import_with_rabbitmq')) { + $service->setProducer($this->get('old_sound_rabbit_mq.import_elcurator_producer')); + } elseif ($this->get('craue_config')->get('import_with_redis')) { + $service->setProducer($this->get('wallabag_import.producer.redis.elcurator')); + } + + return $service; + } + + /** + * {@inheritdoc} + */ + protected function getImportTemplate() + { + return 'WallabagImportBundle:Elcurator:index.html.twig'; + } +} diff --git a/src/Wallabag/ImportBundle/Controller/FirefoxController.php b/src/Wallabag/ImportBundle/Controller/FirefoxController.php index 88697f9dc..dce8455f3 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 7e4fd1744..5a7e53d61 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 { @@ -43,6 +43,7 @@ class ImportController extends Controller + $this->getTotalMessageInRabbitQueue('chrome') + $this->getTotalMessageInRabbitQueue('instapaper') + $this->getTotalMessageInRabbitQueue('pinboard') + + $this->getTotalMessageInRabbitQueue('elcurator') ; } catch (\Exception $e) { $rabbitNotInstalled = true; @@ -59,6 +60,7 @@ class ImportController extends Controller + $redis->llen('wallabag.import.chrome') + $redis->llen('wallabag.import.instapaper') + $redis->llen('wallabag.import.pinboard') + + $redis->llen('wallabag.import.elcurator') ; } catch (\Exception $e) { $redisNotInstalled = true; diff --git a/src/Wallabag/ImportBundle/Controller/InstapaperController.php b/src/Wallabag/ImportBundle/Controller/InstapaperController.php index f184baf91..faed3b728 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 6f54c69a0..cc6fae798 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 9f28819aa..71ceb4276 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 729a97a3b..b120ef967 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/WallabagController.php b/src/Wallabag/ImportBundle/Controller/WallabagController.php index d182dd2ca..5180006d3 100644 --- a/src/Wallabag/ImportBundle/Controller/WallabagController.php +++ b/src/Wallabag/ImportBundle/Controller/WallabagController.php @@ -16,8 +16,6 @@ abstract class WallabagController extends Controller /** * Handle import request. * - * @param Request $request - * * @return Response|RedirectResponse */ public function indexAction(Request $request) diff --git a/src/Wallabag/ImportBundle/Controller/WallabagV1Controller.php b/src/Wallabag/ImportBundle/Controller/WallabagV1Controller.php index d700d8a88..e1c353438 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 ab26400cf..c4116c1d0 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/AbstractImport.php b/src/Wallabag/ImportBundle/Import/AbstractImport.php index 58a234f46..1b073e99a 100644 --- a/src/Wallabag/ImportBundle/Import/AbstractImport.php +++ b/src/Wallabag/ImportBundle/Import/AbstractImport.php @@ -46,8 +46,6 @@ abstract class AbstractImport implements ImportInterface /** * Set RabbitMQ/Redis Producer to send each entry to a queue. * This method should be called when user has enabled RabbitMQ. - * - * @param ProducerInterface $producer */ public function setProducer(ProducerInterface $producer) { @@ -57,8 +55,6 @@ abstract class AbstractImport implements ImportInterface /** * Set current user. * Could the current *connected* user or one retrieve by the consumer. - * - * @param User $user */ public function setUser(User $user) { @@ -112,12 +108,17 @@ abstract class AbstractImport implements ImportInterface /** * Parse one entry. * - * @param array $importedEntry - * * @return Entry */ abstract public function parseEntry(array $importedEntry); + /** + * Validate that an entry is valid (like has some required keys, etc.). + * + * @return bool + */ + abstract public function validateEntry(array $importedEntry); + /** * Fetch content from the ContentProxy (using graby). * If it fails return the given entry to be saved in all case (to avoid user to loose the content). @@ -140,10 +141,8 @@ abstract class AbstractImport implements ImportInterface /** * Parse and insert all given entries. - * - * @param $entries */ - protected function parseEntries($entries) + protected function parseEntries(array $entries) { $i = 1; $entryToBeFlushed = []; @@ -153,6 +152,10 @@ abstract class AbstractImport implements ImportInterface $importedEntry = $this->setEntryAsRead($importedEntry); } + if (false === $this->validateEntry($importedEntry)) { + continue; + } + $entry = $this->parseEntry($importedEntry); if (null === $entry) { @@ -197,8 +200,6 @@ abstract class AbstractImport implements ImportInterface * * Faster parse entries for Producer. * We don't care to make check at this time. They'll be done by the consumer. - * - * @param array $entries */ protected function parseEntriesForProducer(array $entries) { @@ -220,8 +221,6 @@ abstract class AbstractImport implements ImportInterface * Set current imported entry to archived / read. * Implementation is different accross all imports. * - * @param array $importedEntry - * * @return array */ abstract protected function setEntryAsRead(array $importedEntry); diff --git a/src/Wallabag/ImportBundle/Import/BrowserImport.php b/src/Wallabag/ImportBundle/Import/BrowserImport.php index 225f1791f..ea7afd3dd 100644 --- a/src/Wallabag/ImportBundle/Import/BrowserImport.php +++ b/src/Wallabag/ImportBundle/Import/BrowserImport.php @@ -77,7 +77,7 @@ abstract class BrowserImport extends AbstractImport */ public function parseEntry(array $importedEntry) { - if ((!array_key_exists('guid', $importedEntry) || (!array_key_exists('id', $importedEntry))) && \is_array(reset($importedEntry))) { + if ((!\array_key_exists('guid', $importedEntry) || (!\array_key_exists('id', $importedEntry))) && \is_array(reset($importedEntry))) { if ($this->producer) { $this->parseEntriesForProducer($importedEntry); @@ -89,7 +89,7 @@ abstract class BrowserImport extends AbstractImport return; } - if (array_key_exists('children', $importedEntry)) { + if (\array_key_exists('children', $importedEntry)) { if ($this->producer) { $this->parseEntriesForProducer($importedEntry['children']); @@ -101,11 +101,11 @@ abstract class BrowserImport extends AbstractImport return; } - if (!array_key_exists('uri', $importedEntry) && !array_key_exists('url', $importedEntry)) { + if (!\array_key_exists('uri', $importedEntry) && !\array_key_exists('url', $importedEntry)) { return; } - $url = array_key_exists('uri', $importedEntry) ? $importedEntry['uri'] : $importedEntry['url']; + $url = \array_key_exists('uri', $importedEntry) ? $importedEntry['uri'] : $importedEntry['url']; $existingEntry = $this->em ->getRepository('WallabagCoreBundle:Entry') @@ -126,14 +126,14 @@ abstract class BrowserImport extends AbstractImport // update entry with content (in case fetching failed, the given entry will be return) $this->fetchContent($entry, $data['url'], $data); - if (array_key_exists('tags', $data)) { + if (\array_key_exists('tags', $data)) { $this->tagsAssigner->assignTagsToEntry( $entry, $data['tags'] ); } - $entry->setArchived($data['is_archived']); + $entry->updateArchived($data['is_archived']); if (!empty($data['created_at'])) { $dt = new \DateTime(); @@ -148,10 +148,8 @@ abstract class BrowserImport extends AbstractImport /** * Parse and insert all given entries. - * - * @param $entries */ - protected function parseEntries($entries) + protected function parseEntries(array $entries) { $i = 1; $entryToBeFlushed = []; @@ -199,8 +197,6 @@ abstract class BrowserImport extends AbstractImport * * Faster parse entries for Producer. * We don't care to make check at this time. They'll be done by the consumer. - * - * @param array $entries */ protected function parseEntriesForProducer(array $entries) { diff --git a/src/Wallabag/ImportBundle/Import/ChromeImport.php b/src/Wallabag/ImportBundle/Import/ChromeImport.php index 09183abe4..4ae82ade8 100644 --- a/src/Wallabag/ImportBundle/Import/ChromeImport.php +++ b/src/Wallabag/ImportBundle/Import/ChromeImport.php @@ -30,6 +30,18 @@ class ChromeImport extends BrowserImport return 'import.chrome.description'; } + /** + * {@inheritdoc} + */ + public function validateEntry(array $importedEntry) + { + if (empty($importedEntry['url'])) { + return false; + } + + return true; + } + /** * {@inheritdoc} */ @@ -45,7 +57,7 @@ class ChromeImport extends BrowserImport 'created_at' => substr($entry['date_added'], 0, 10), ]; - if (array_key_exists('tags', $entry) && '' !== $entry['tags']) { + if (\array_key_exists('tags', $entry) && '' !== $entry['tags']) { $data['tags'] = $entry['tags']; } diff --git a/src/Wallabag/ImportBundle/Import/ElcuratorImport.php b/src/Wallabag/ImportBundle/Import/ElcuratorImport.php new file mode 100644 index 000000000..d12816139 --- /dev/null +++ b/src/Wallabag/ImportBundle/Import/ElcuratorImport.php @@ -0,0 +1,54 @@ + $entry['url'], + 'title' => $entry['title'], + 'created_at' => $entry['created_at'], + 'is_archived' => 0, + 'is_starred' => $entry['is_saved'], + ] + $entry; + } + + /** + * {@inheritdoc} + */ + protected function setEntryAsRead(array $importedEntry) + { + $importedEntry['is_archived'] = 1; + + return $importedEntry; + } +} diff --git a/src/Wallabag/ImportBundle/Import/FirefoxImport.php b/src/Wallabag/ImportBundle/Import/FirefoxImport.php index 73269fe16..b3558f21e 100644 --- a/src/Wallabag/ImportBundle/Import/FirefoxImport.php +++ b/src/Wallabag/ImportBundle/Import/FirefoxImport.php @@ -30,6 +30,18 @@ class FirefoxImport extends BrowserImport return 'import.firefox.description'; } + /** + * {@inheritdoc} + */ + public function validateEntry(array $importedEntry) + { + if (empty($importedEntry['uri'])) { + return false; + } + + return true; + } + /** * {@inheritdoc} */ @@ -45,7 +57,7 @@ class FirefoxImport extends BrowserImport 'created_at' => substr($entry['dateAdded'], 0, 10), ]; - if (array_key_exists('tags', $entry) && '' !== $entry['tags']) { + if (\array_key_exists('tags', $entry) && '' !== $entry['tags']) { $data['tags'] = $entry['tags']; } diff --git a/src/Wallabag/ImportBundle/Import/ImportChain.php b/src/Wallabag/ImportBundle/Import/ImportChain.php index 9dd779569..e1b5867de 100644 --- a/src/Wallabag/ImportBundle/Import/ImportChain.php +++ b/src/Wallabag/ImportBundle/Import/ImportChain.php @@ -14,8 +14,7 @@ class ImportChain /** * Add an import to the chain. * - * @param ImportInterface $import - * @param string $alias + * @param string $alias */ public function addImport(ImportInterface $import, $alias) { diff --git a/src/Wallabag/ImportBundle/Import/InstapaperImport.php b/src/Wallabag/ImportBundle/Import/InstapaperImport.php index e4f0970c0..f7bee9ef0 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); @@ -105,6 +108,18 @@ class InstapaperImport extends AbstractImport return true; } + /** + * {@inheritdoc} + */ + public function validateEntry(array $importedEntry) + { + if (empty($importedEntry['url'])) { + return false; + } + + return true; + } + /** * {@inheritdoc} */ @@ -135,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 110b04642..202eb1b3e 100644 --- a/src/Wallabag/ImportBundle/Import/PinboardImport.php +++ b/src/Wallabag/ImportBundle/Import/PinboardImport.php @@ -80,6 +80,18 @@ class PinboardImport extends AbstractImport return true; } + /** + * {@inheritdoc} + */ + public function validateEntry(array $importedEntry) + { + if (empty($importedEntry['href'])) { + return false; + } + + return true; + } + /** * {@inheritdoc} */ @@ -119,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 c1b35b7ef..24fdaa2b8 100644 --- a/src/Wallabag/ImportBundle/Import/PocketImport.php +++ b/src/Wallabag/ImportBundle/Import/PocketImport.php @@ -2,13 +2,22 @@ namespace Wallabag\ImportBundle\Import; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\RequestException; +use Http\Client\Common\HttpMethodsClient; +use Http\Client\Common\Plugin\ErrorPlugin; +use Http\Client\Common\PluginClient; +use Http\Client\Exception\RequestException; +use Http\Client\HttpClient; +use Http\Discovery\MessageFactoryDiscovery; +use Http\Message\MessageFactory; +use Psr\Http\Message\ResponseInterface; use Wallabag\CoreBundle\Entity\Entry; class PocketImport extends AbstractImport { const NB_ELEMENTS = 5000; + /** + * @var HttpMethodsClient + */ private $client; private $accessToken; @@ -55,24 +64,18 @@ class PocketImport extends AbstractImport */ public function getRequestToken($redirectUri) { - $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/oauth/request', - [ - 'body' => json_encode([ - 'consumer_key' => $this->user->getConfig()->getPocketConsumerKey(), - 'redirect_uri' => $redirectUri, - ]), - ] - ); - try { - $response = $this->client->send($request); + $response = $this->client->post('https://getpocket.com/v3/oauth/request', [], json_encode([ + 'consumer_key' => $this->user->getConfig()->getPocketConsumerKey(), + 'redirect_uri' => $redirectUri, + ])); } catch (RequestException $e) { $this->logger->error(sprintf('PocketImport: Failed to request token: %s', $e->getMessage()), ['exception' => $e]); return false; } - return $response->json()['code']; + return $this->jsonDecode($response)['code']; } /** @@ -85,24 +88,18 @@ class PocketImport extends AbstractImport */ public function authorize($code) { - $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/oauth/authorize', - [ - 'body' => json_encode([ - 'consumer_key' => $this->user->getConfig()->getPocketConsumerKey(), - 'code' => $code, - ]), - ] - ); - try { - $response = $this->client->send($request); + $response = $this->client->post('https://getpocket.com/v3/oauth/authorize', [], json_encode([ + 'consumer_key' => $this->user->getConfig()->getPocketConsumerKey(), + 'code' => $code, + ])); } catch (RequestException $e) { $this->logger->error(sprintf('PocketImport: Failed to authorize client: %s', $e->getMessage()), ['exception' => $e]); return false; } - $this->accessToken = $response->json()['access_token']; + $this->accessToken = $this->jsonDecode($response)['access_token']; return true; } @@ -114,29 +111,23 @@ class PocketImport extends AbstractImport { static $run = 0; - $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/get', - [ - 'body' => json_encode([ - 'consumer_key' => $this->user->getConfig()->getPocketConsumerKey(), - 'access_token' => $this->accessToken, - 'detailType' => 'complete', - 'state' => 'all', - 'sort' => 'newest', - 'count' => self::NB_ELEMENTS, - 'offset' => $offset, - ]), - ] - ); - try { - $response = $this->client->send($request); + $response = $this->client->post('https://getpocket.com/v3/get', [], json_encode([ + 'consumer_key' => $this->user->getConfig()->getPocketConsumerKey(), + 'access_token' => $this->accessToken, + 'detailType' => 'complete', + 'state' => 'all', + 'sort' => 'newest', + 'count' => self::NB_ELEMENTS, + 'offset' => $offset, + ])); } catch (RequestException $e) { $this->logger->error(sprintf('PocketImport: Failed to import: %s', $e->getMessage()), ['exception' => $e]); return false; } - $entries = $response->json(); + $entries = $this->jsonDecode($response); if ($this->producer) { $this->parseEntriesForProducer($entries['list']); @@ -159,13 +150,23 @@ class PocketImport extends AbstractImport } /** - * Set the Guzzle client. - * - * @param Client $client + * Set the Http client. */ - public function setClient(Client $client) + public function setClient(HttpClient $client, MessageFactory $messageFactory = null) { - $this->client = $client; + $this->client = new HttpMethodsClient(new PluginClient($client, [new ErrorPlugin()]), $messageFactory ?: MessageFactoryDiscovery::find()); + } + + /** + * {@inheritdoc} + */ + public function validateEntry(array $importedEntry) + { + if (empty($importedEntry['resolved_url']) && empty($importedEntry['given_url'])) { + return false; + } + + return true; } /** @@ -194,10 +195,10 @@ class PocketImport extends AbstractImport $this->fetchContent($entry, $url); // 0, 1, 2 - 1 if the item is archived - 2 if the item should be deleted - $entry->setArchived(1 === $importedEntry['status'] || $this->markAsRead); + $entry->updateArchived(1 === (int) $importedEntry['status'] || $this->markAsRead); - // 0 or 1 - 1 If the item is starred - $entry->setStarred(1 === $importedEntry['favorite']); + // 0 or 1 - 1 if the item is starred + $entry->setStarred(1 === (int) $importedEntry['favorite']); $title = 'Untitled'; if (isset($importedEntry['resolved_title']) && '' !== $importedEntry['resolved_title']) { @@ -240,4 +241,15 @@ class PocketImport extends AbstractImport return $importedEntry; } + + protected function jsonDecode(ResponseInterface $response) + { + $data = json_decode((string) $response->getBody(), true); + + if (JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException('Unable to parse JSON data: ' . json_last_error_msg()); + } + + return $data; + } } diff --git a/src/Wallabag/ImportBundle/Import/ReadabilityImport.php b/src/Wallabag/ImportBundle/Import/ReadabilityImport.php index 002b27f46..c5abf1892 100644 --- a/src/Wallabag/ImportBundle/Import/ReadabilityImport.php +++ b/src/Wallabag/ImportBundle/Import/ReadabilityImport.php @@ -80,6 +80,18 @@ class ReadabilityImport extends AbstractImport return true; } + /** + * {@inheritdoc} + */ + public function validateEntry(array $importedEntry) + { + if (empty($importedEntry['article__url'])) { + return false; + } + + return true; + } + /** * {@inheritdoc} */ @@ -111,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 c64ccd64d..75a28fbf5 100644 --- a/src/Wallabag/ImportBundle/Import/WallabagImport.php +++ b/src/Wallabag/ImportBundle/Import/WallabagImport.php @@ -86,6 +86,18 @@ abstract class WallabagImport extends AbstractImport return $this; } + /** + * {@inheritdoc} + */ + public function validateEntry(array $importedEntry) + { + if (empty($importedEntry['url'])) { + return false; + } + + return true; + } + /** * {@inheritdoc} */ @@ -110,7 +122,7 @@ abstract class WallabagImport extends AbstractImport // update entry with content (in case fetching failed, the given entry will be return) $this->fetchContent($entry, $data['url'], $data); - if (array_key_exists('tags', $data)) { + if (\array_key_exists('tags', $data)) { $this->tagsAssigner->assignTagsToEntry( $entry, $data['tags'], @@ -122,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/Import/WallabagV1Import.php b/src/Wallabag/ImportBundle/Import/WallabagV1Import.php index b9bb525ab..e05626117 100644 --- a/src/Wallabag/ImportBundle/Import/WallabagV1Import.php +++ b/src/Wallabag/ImportBundle/Import/WallabagV1Import.php @@ -61,7 +61,7 @@ class WallabagV1Import extends WallabagImport $data['html'] = $this->fetchingErrorMessage; } - if (array_key_exists('tags', $entry) && '' !== $entry['tags']) { + if (\array_key_exists('tags', $entry) && '' !== $entry['tags']) { $data['tags'] = $entry['tags']; } diff --git a/src/Wallabag/ImportBundle/Import/WallabagV2Import.php b/src/Wallabag/ImportBundle/Import/WallabagV2Import.php index 3e085ecff..2ba26003f 100644 --- a/src/Wallabag/ImportBundle/Import/WallabagV2Import.php +++ b/src/Wallabag/ImportBundle/Import/WallabagV2Import.php @@ -35,7 +35,9 @@ class WallabagV2Import extends WallabagImport { return [ 'html' => $entry['content'], - 'content_type' => $entry['mimetype'], + 'headers' => [ + 'content-type' => $entry['mimetype'], + ], 'is_archived' => (bool) ($entry['is_archived'] || $this->markAsRead), 'is_starred' => (bool) $entry['is_starred'], ] + $entry; diff --git a/src/Wallabag/ImportBundle/Resources/config/rabbit.yml b/src/Wallabag/ImportBundle/Resources/config/rabbit.yml index e9ecb8467..0bf0e761c 100644 --- a/src/Wallabag/ImportBundle/Resources/config/rabbit.yml +++ b/src/Wallabag/ImportBundle/Resources/config/rabbit.yml @@ -48,6 +48,14 @@ services: - "@wallabag_import.wallabag_v2.import" - "@event_dispatcher" - "@logger" + wallabag_import.consumer.amqp.elcurator: + class: Wallabag\ImportBundle\Consumer\AMQPEntryConsumer + arguments: + - "@doctrine.orm.entity_manager" + - "@wallabag_user.user_repository" + - "@wallabag_import.elcurator.import" + - "@event_dispatcher" + - "@logger" wallabag_import.consumer.amqp.firefox: class: Wallabag\ImportBundle\Consumer\AMQPEntryConsumer arguments: diff --git a/src/Wallabag/ImportBundle/Resources/config/redis.yml b/src/Wallabag/ImportBundle/Resources/config/redis.yml index 091cdba00..40a6e2240 100644 --- a/src/Wallabag/ImportBundle/Resources/config/redis.yml +++ b/src/Wallabag/ImportBundle/Resources/config/redis.yml @@ -126,6 +126,27 @@ services: - "@event_dispatcher" - "@logger" + # elcurator + wallabag_import.queue.redis.elcurator: + class: Simpleue\Queue\RedisQueue + arguments: + - "@wallabag_core.redis.client" + - "wallabag.import.elcurator" + + wallabag_import.producer.redis.elcurator: + class: Wallabag\ImportBundle\Redis\Producer + arguments: + - "@wallabag_import.queue.redis.elcurator" + + wallabag_import.consumer.redis.elcurator: + class: Wallabag\ImportBundle\Consumer\RedisEntryConsumer + arguments: + - "@doctrine.orm.entity_manager" + - "@wallabag_user.user_repository" + - "@wallabag_import.elcurator.import" + - "@event_dispatcher" + - "@logger" + # firefox wallabag_import.queue.redis.firefox: class: Simpleue\Queue\RedisQueue diff --git a/src/Wallabag/ImportBundle/Resources/config/services.yml b/src/Wallabag/ImportBundle/Resources/config/services.yml index b224a6a23..d824da4ab 100644 --- a/src/Wallabag/ImportBundle/Resources/config/services.yml +++ b/src/Wallabag/ImportBundle/Resources/config/services.yml @@ -7,13 +7,7 @@ services: class: Wallabag\ImportBundle\Import\ImportChain wallabag_import.pocket.client: - class: GuzzleHttp\Client - arguments: - - - defaults: - headers: - content-type: "application/json" - X-Accept: "application/json" + alias: 'httplug.client.wallabag_import.pocket.client' wallabag_import.pocket.import: class: Wallabag\ImportBundle\Import\PocketImport @@ -54,6 +48,18 @@ services: tags: - { name: wallabag_import.import, alias: wallabag_v2 } + wallabag_import.elcurator.import: + class: Wallabag\ImportBundle\Import\ElcuratorImport + arguments: + - "@doctrine.orm.entity_manager" + - "@wallabag_core.content_proxy" + - "@wallabag_core.tags_assigner" + - "@event_dispatcher" + calls: + - [ setLogger, [ "@logger" ]] + tags: + - { name: wallabag_import.import, alias: elcurator } + wallabag_import.readability.import: class: Wallabag\ImportBundle\Import\ReadabilityImport arguments: @@ -112,3 +118,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/ImportBundle/Resources/views/Elcurator/index.html.twig b/src/Wallabag/ImportBundle/Resources/views/Elcurator/index.html.twig new file mode 100644 index 000000000..e3a0d709f --- /dev/null +++ b/src/Wallabag/ImportBundle/Resources/views/Elcurator/index.html.twig @@ -0,0 +1,3 @@ +{% extends "WallabagImportBundle:WallabagV1:index.html.twig" %} + +{% block title %}{{ 'import.elcurator.page_title'|trans }}{% endblock %} diff --git a/src/Wallabag/UserBundle/Controller/ManageController.php b/src/Wallabag/UserBundle/Controller/ManageController.php index f3de656f9..1122f8f07 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) { - $deleteForm = $this->createDeleteForm($user); - $editForm = $this->createForm('Wallabag\UserBundle\Form\UserType', $user); - $editForm->handleRequest($request); + $userManager = $this->container->get('fos_user.user_manager'); - if ($editForm->isSubmitted() && $editForm->isValid()) { - $em = $this->getDoctrine()->getManager(); - $em->persist($user); - $em->flush(); + $deleteForm = $this->createDeleteForm($user); + $form = $this->createForm('Wallabag\UserBundle\Form\UserType', $user); + $form->handleRequest($request); + + // `googleTwoFactor` isn't a field within the User entity, we need to define it's value in a different way + if ($this->getParameter('twofactor_auth') && true === $user->isGoogleAuthenticatorEnabled() && false === $form->isSubmitted()) { + $form->get('googleTwoFactor')->setData(true); + } + + if ($form->isSubmitted() && $form->isValid()) { + // 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) { @@ -116,8 +127,7 @@ class ManageController extends Controller } /** - * @param Request $request - * @param int $page + * @param int $page * * @Route("/list/{page}", name="user_index", defaults={"page" = 1}) * @@ -135,8 +145,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 +169,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 26dbda3b2..1e375e09e 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 48446e3c1..aeab761db 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() { @@ -182,8 +188,6 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf } /** - * @param Entry $entry - * * @return User */ public function addEntry(Entry $entry) @@ -233,54 +237,122 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf /** * @return bool */ - public function isTwoFactorAuthentication() + public function isEmailTwoFactor() { - return $this->twoFactorAuthentication; + return $this->emailTwoFactor; } /** - * @param bool $twoFactorAuthentication + * @param bool $emailTwoFactor */ - public function setTwoFactorAuthentication($twoFactorAuthentication) + public function setEmailTwoFactor($emailTwoFactor) { - $this->twoFactorAuthentication = $twoFactorAuthentication; + $this->emailTwoFactor = $emailTwoFactor; } - public function isEmailAuthEnabled() + /** + * Used in the user config form to be "like" the email option. + */ + public function isGoogleTwoFactor() { - return $this->twoFactorAuthentication; + return $this->isGoogleAuthenticatorEnabled(); } - public function getEmailAuthCode() + /** + * {@inheritdoc} + */ + public function isEmailAuthEnabled(): bool + { + return $this->emailTwoFactor; + } + + /** + * {@inheritdoc} + */ + public function getEmailAuthCode(): string { return $this->authCode; } - public function setEmailAuthCode($authCode) + /** + * {@inheritdoc} + */ + public function setEmailAuthCode(string $authCode): void { $this->authCode = $authCode; } - public function addTrustedComputer($token, \DateTime $validUntil) + /** + * {@inheritdoc} + */ + public function getEmailAuthRecipient(): string { - $this->trusted[$token] = $validUntil->format('r'); - } - - public function isTrustedComputer($token) - { - if (isset($this->trusted[$token])) { - $now = new \DateTime(); - $validUntil = new \DateTime($this->trusted[$token]); - - return $now < $validUntil; - } - - return false; + return $this->email; + } + + /** + * {@inheritdoc} + */ + public function isGoogleAuthenticatorEnabled(): bool + { + return $this->googleAuthenticatorSecret ? true : false; + } + + /** + * {@inheritdoc} + */ + public function getGoogleAuthenticatorUsername(): string + { + return $this->username; + } + + /** + * {@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]); + } } /** - * @param Client $client - * * @return User */ public function addClient(Client $client) @@ -309,4 +381,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 e4d55c197..81954213f 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 56fea640b..03fad9717 100644 --- a/src/Wallabag/UserBundle/Form/UserType.php +++ b/src/Wallabag/UserBundle/Form/UserType.php @@ -12,10 +12,6 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class UserType extends AbstractType { - /** - * @param FormBuilderInterface $builder - * @param array $options - */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder @@ -35,9 +31,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', @@ -45,9 +46,6 @@ class UserType extends AbstractType ; } - /** - * @param OptionsResolver $resolver - */ public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ diff --git a/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php b/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php index aed805c95..4eea444f2 100644 --- a/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php +++ b/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php @@ -4,6 +4,7 @@ namespace Wallabag\UserBundle\Mailer; use Scheb\TwoFactorBundle\Mailer\AuthCodeMailerInterface; use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface; +use Twig\Environment; /** * Custom mailer for TwoFactorBundle email. @@ -21,7 +22,7 @@ class AuthCodeMailer implements AuthCodeMailerInterface /** * Twig to render the html's email. * - * @var \Twig_Environment + * @var Environment */ private $twig; @@ -56,14 +57,12 @@ class AuthCodeMailer implements AuthCodeMailerInterface /** * Initialize the auth code mailer with the SwiftMailer object. * - * @param \Swift_Mailer $mailer - * @param \Twig_Environment $twig - * @param string $senderEmail - * @param string $senderName - * @param string $supportUrl wallabag support url - * @param string $wallabagUrl wallabag instance url + * @param string $senderEmail + * @param string $senderName + * @param string $supportUrl wallabag support url + * @param string $wallabagUrl wallabag instance url */ - public function __construct(\Swift_Mailer $mailer, \Twig_Environment $twig, $senderEmail, $senderName, $supportUrl, $wallabagUrl) + public function __construct(\Swift_Mailer $mailer, Environment $twig, $senderEmail, $senderName, $supportUrl, $wallabagUrl) { $this->mailer = $mailer; $this->twig = $twig; @@ -75,10 +74,8 @@ class AuthCodeMailer implements AuthCodeMailerInterface /** * Send the auth code to the user via email. - * - * @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 +94,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 be693d3b1..4abd55f15 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 d3925de3b..2dcf30111 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 c8471bdda..e15ed255b 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,15 +10,20 @@

{{ flashMessage|trans }}

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

{{ authenticationError|trans(authenticationErrorData, 'SchebTwoFactorBundle') }}

+ {% endif %} +
- - + +
- {% if useTrustedOption %} + {% if displayTrustedOption %}
- - + +
{% endif %}
@@ -25,7 +31,7 @@
{{ 'security.login.cancel'|trans }}
diff --git a/src/Wallabag/UserBundle/Resources/views/Manage/edit.html.twig b/src/Wallabag/UserBundle/Resources/views/Manage/edit.html.twig index 3ffd15f5d..2de8f3a55 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 d0a85fc74..85cd4f0d3 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'}}) }}
- {{ form_widget(form._token) }} {% for flashMessage in app.session.flashbag.get('notice') %} diff --git a/src/Wallabag/UserBundle/Resources/views/TwoFactor/email_auth_code.html.twig b/src/Wallabag/UserBundle/Resources/views/TwoFactor/email_auth_code.html.twig index ecc1d79a2..cd5aaf407 100644 --- a/src/Wallabag/UserBundle/Resources/views/TwoFactor/email_auth_code.html.twig +++ b/src/Wallabag/UserBundle/Resources/views/TwoFactor/email_auth_code.html.twig @@ -74,7 +74,7 @@ - +
logologo

wallabag

{{ "auth_code.on"|trans({}, 'wallabag_user') }} {{ wallabag_url }}
diff --git a/src/Wallabag/UserBundle/Resources/views/layout.html.twig b/src/Wallabag/UserBundle/Resources/views/layout.html.twig index f97e98703..a47b31d04 100644 --- a/src/Wallabag/UserBundle/Resources/views/layout.html.twig +++ b/src/Wallabag/UserBundle/Resources/views/layout.html.twig @@ -15,6 +15,11 @@ {% block fos_user_content %} {% endblock fos_user_content %} +
+ Deutsch – + English – + Français +
{% endblock %} diff --git a/tests/Wallabag/AnnotationBundle/Controller/AnnotationControllerTest.php b/tests/Wallabag/AnnotationBundle/Controller/AnnotationControllerTest.php index 964744684..260edd770 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); $this->assertSame('my annotation', $annotation->getText()); } + public function testAllowEmptyQuote() + { + $em = $this->client->getContainer()->get('doctrine.orm.entity_manager'); + + /** @var Entry $entry */ + $entry = $em + ->getRepository('WallabagCoreBundle:Entry') + ->findOneByUsernameAndNotArchived('admin'); + + $headers = ['CONTENT_TYPE' => 'application/json']; + $content = json_encode([ + 'text' => 'my annotation', + 'quote' => null, + 'ranges' => [ + ['start' => '', 'startOffset' => 24, 'end' => '', 'endOffset' => 31], + ], + ]); + $this->client->request('POST', '/api/annotations/' . $entry->getId() . '.json', [], [], $headers, $content); + + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + + $content = json_decode($this->client->getResponse()->getContent(), true); + + $this->assertSame('Big boss', $content['user']); + $this->assertSame('v1.0', $content['annotator_schema_version']); + $this->assertSame('my annotation', $content['text']); + $this->assertSame('', $content['quote']); + } + + public function testAllowOmmittedQuote() + { + $em = $this->client->getContainer()->get('doctrine.orm.entity_manager'); + + /** @var Entry $entry */ + $entry = $em + ->getRepository('WallabagCoreBundle:Entry') + ->findOneByUsernameAndNotArchived('admin'); + + $headers = ['CONTENT_TYPE' => 'application/json']; + $content = json_encode([ + 'text' => 'my new annotation', + 'ranges' => [ + ['start' => '', 'startOffset' => 25, 'end' => '', 'endOffset' => 32], + ], + ]); + $this->client->request('POST', '/api/annotations/' . $entry->getId() . '.json', [], [], $headers, $content); + + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + + $content = json_decode($this->client->getResponse()->getContent(), true); + + $this->assertSame('Big boss', $content['user']); + $this->assertSame('v1.0', $content['annotator_schema_version']); + $this->assertSame('my new annotation', $content['text']); + $this->assertSame('', $content['quote']); + } + /** * @dataProvider dataForEachAnnotations */ diff --git a/tests/Wallabag/AnnotationBundle/WallabagAnnotationTestCase.php b/tests/Wallabag/AnnotationBundle/WallabagAnnotationTestCase.php index 105e8add2..9c7aba6bd 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 f58d1c120..e3d692906 100644 --- a/tests/Wallabag/ApiBundle/Controller/DeveloperControllerTest.php +++ b/tests/Wallabag/ApiBundle/Controller/DeveloperControllerTest.php @@ -30,7 +30,7 @@ class DeveloperControllerTest extends WallabagCoreTestCase $newNbClients = $em->getRepository('WallabagApiBundle:Client')->findAll(); $this->assertGreaterThan(\count($nbClients), \count($newNbClients)); - $this->assertGreaterThan(1, $alert = $crawler->filter('.settings ul li strong')->extract(['_text'])); + $this->assertGreaterThan(1, $alert = $crawler->filter('.settings table strong')->extract(['_text'])); $this->assertContains('My app', $alert[0]); } @@ -56,6 +56,20 @@ class DeveloperControllerTest extends WallabagCoreTestCase $this->assertArrayHasKey('refresh_token', $data); } + public function testCreateTokenWithBadClientId() + { + $client = $this->getClient(); + $client->request('POST', '/oauth/v2/token', [ + 'grant_type' => 'password', + 'client_id' => '$WALLABAG_CLIENT_ID', + 'client_secret' => 'secret', + 'username' => 'admin', + 'password' => 'mypassword', + ]); + + $this->assertSame(400, $client->getResponse()->getStatusCode()); + } + public function testListingClient() { $this->logInAs('admin'); @@ -121,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 58b617f3d..8b7898eea 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.'); @@ -242,6 +263,15 @@ class EntryRestControllerTest extends WallabagApiTestCase $this->assertSame(2, $content['limit']); } + public function testGetStarredEntriesWithBadSort() + { + $this->client->request('GET', '/api/entries', ['starred' => 1, 'sort' => 'updated', 'order' => 'unknown']); + + $this->assertSame(400, $this->client->getResponse()->getStatusCode()); + + $this->assertSame('application/json', $this->client->getResponse()->headers->get('Content-Type')); + } + public function testGetStarredEntries() { $this->client->request('GET', '/api/entries', ['starred' => 1, 'sort' => 'updated']); @@ -391,29 +421,71 @@ class EntryRestControllerTest extends WallabagApiTestCase public function testDeleteEntry() { - $entry = $this->client->getContainer() - ->get('doctrine.orm.entity_manager') - ->getRepository('WallabagCoreBundle:Entry') - ->findOneByUser(1, ['id' => 'asc']); + $em = $this->client->getContainer()->get('doctrine.orm.entity_manager'); + $entry = new Entry($em->getReference(User::class, 1)); + $entry->setUrl('http://0.0.0.0/test-delete-entry'); + $entry->setTitle('Test delete entry'); + $em->persist($entry); + $em->flush(); - if (!$entry) { - $this->markTestSkipped('No content found in db.'); - } + $em->clear(); - $this->client->request('DELETE', '/api/entries/' . $entry->getId() . '.json'); + $e = [ + 'title' => $entry->getTitle(), + 'url' => $entry->getUrl(), + 'id' => $entry->getId(), + ]; + + $this->client->request('DELETE', '/api/entries/' . $e['id'] . '.json'); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); $content = json_decode($this->client->getResponse()->getContent(), true); - $this->assertSame($entry->getTitle(), $content['title']); - $this->assertSame($entry->getUrl(), $content['url']); - $this->assertSame($entry->getId(), $content['id']); + $this->assertSame($e['title'], $content['title']); + $this->assertSame($e['url'], $content['url']); + $this->assertSame($e['id'], $content['id']); // We'll try to delete this entry again - $this->client->request('DELETE', '/api/entries/' . $entry->getId() . '.json'); + $client = $this->createAuthorizedClient(); + $client->request('DELETE', '/api/entries/' . $e['id'] . '.json'); - $this->assertSame(404, $this->client->getResponse()->getStatusCode()); + $this->assertSame(404, $client->getResponse()->getStatusCode()); + } + + public function testDeleteEntryExpectId() + { + $em = $this->client->getContainer()->get('doctrine.orm.entity_manager'); + $entry = new Entry($em->getReference(User::class, 1)); + $entry->setUrl('http://0.0.0.0/test-delete-entry-id'); + $em->persist($entry); + $em->flush(); + + $em->clear(); + + $id = $entry->getId(); + + $this->client->request('DELETE', '/api/entries/' . $id . '.json?expect=id'); + + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + + $content = json_decode($this->client->getResponse()->getContent(), true); + + $this->assertSame($id, $content['id']); + $this->assertArrayNotHasKey('url', $content); + + // We'll try to delete this entry again + $client = $this->createAuthorizedClient(); + $client->request('DELETE', '/api/entries/' . $id . '.json'); + + $this->assertSame(404, $client->getResponse()->getStatusCode()); + } + + public function testDeleteEntryExpectBadRequest() + { + $this->client->request('DELETE', '/api/entries/1.json?expect=badrequest'); + + $this->assertSame(400, $this->client->getResponse()->getStatusCode()); } public function testPostEntry() @@ -438,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']); @@ -454,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')); @@ -533,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() @@ -582,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.'); @@ -609,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']); @@ -624,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.'); @@ -658,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.'); @@ -689,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.'); @@ -721,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.'); @@ -766,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.'); @@ -783,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') @@ -823,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() @@ -831,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.'); @@ -853,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.'); @@ -875,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.'); @@ -901,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.'); @@ -920,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', @@ -929,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, + ], ]; } @@ -950,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()); @@ -958,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]); } @@ -978,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'); @@ -989,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='); @@ -996,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.'); @@ -1036,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(); @@ -1060,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); @@ -1080,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')); @@ -1148,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(); @@ -1206,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 000000000..fd5246393 --- /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/TagRestControllerTest.php b/tests/Wallabag/ApiBundle/Controller/TagRestControllerTest.php index 430e548d2..9daa94cd5 100644 --- a/tests/Wallabag/ApiBundle/Controller/TagRestControllerTest.php +++ b/tests/Wallabag/ApiBundle/Controller/TagRestControllerTest.php @@ -7,6 +7,8 @@ use Wallabag\CoreBundle\Entity\Tag; class TagRestControllerTest extends WallabagApiTestCase { + private $otherUserTagLabel = 'bob'; + public function testGetUserTags() { $this->client->request('GET', '/api/tags.json'); @@ -19,17 +21,33 @@ class TagRestControllerTest extends WallabagApiTestCase $this->assertArrayHasKey('id', $content[0]); $this->assertArrayHasKey('label', $content[0]); + $tagLabels = array_map(function ($i) { + return $i['label']; + }, $content); + + $this->assertNotContains($this->otherUserTagLabel, $tagLabels, 'There is a possible tag leak'); + return end($content); } public function testDeleteUserTag() { + $em = $this->client->getContainer()->get('doctrine.orm.entity_manager'); + $entry = $this->client->getContainer() + ->get('doctrine.orm.entity_manager') + ->getRepository('WallabagCoreBundle:Entry') + ->findOneWithTags($this->user->getId()); + + $entry = $entry[0]; + $tagLabel = 'tagtest'; $tag = new Tag(); $tag->setLabel($tagLabel); - - $em = $this->client->getContainer()->get('doctrine.orm.entity_manager'); $em->persist($tag); + + $entry->addTag($tag); + + $em->persist($entry); $em->flush(); $em->clear(); @@ -53,6 +71,16 @@ class TagRestControllerTest extends WallabagApiTestCase $this->assertNull($tag, $tagLabel . ' was removed because it begun an orphan tag'); } + public function testDeleteOtherUserTag() + { + $em = $this->client->getContainer()->get('doctrine.orm.entity_manager'); + $tag = $em->getRepository('WallabagCoreBundle:Tag')->findOneByLabel($this->otherUserTagLabel); + + $this->client->request('DELETE', '/api/tags/' . $tag->getId() . '.json'); + + $this->assertSame(404, $this->client->getResponse()->getStatusCode()); + } + public function dataForDeletingTagByLabel() { return [ @@ -112,6 +140,13 @@ class TagRestControllerTest extends WallabagApiTestCase $this->assertSame(404, $this->client->getResponse()->getStatusCode()); } + public function testDeleteTagByLabelOtherUser() + { + $this->client->request('DELETE', '/api/tag/label.json', ['tag' => $this->otherUserTagLabel]); + + $this->assertSame(404, $this->client->getResponse()->getStatusCode()); + } + /** * @dataProvider dataForDeletingTagByLabel */ @@ -180,4 +215,11 @@ class TagRestControllerTest extends WallabagApiTestCase $this->assertSame(404, $this->client->getResponse()->getStatusCode()); } + + public function testDeleteTagsByLabelOtherUser() + { + $this->client->request('DELETE', '/api/tags/label.json', ['tags' => $this->otherUserTagLabel]); + + $this->assertSame(404, $this->client->getResponse()->getStatusCode()); + } } diff --git a/tests/Wallabag/ApiBundle/Controller/TaggingRuleRestControllerTest.php b/tests/Wallabag/ApiBundle/Controller/TaggingRuleRestControllerTest.php new file mode 100644 index 000000000..b6477256e --- /dev/null +++ b/tests/Wallabag/ApiBundle/Controller/TaggingRuleRestControllerTest.php @@ -0,0 +1,15 @@ +client->request('GET', '/api/taggingrule/export'); + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + $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 ac4d6cdcf..8b49c0ae0 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 8a188e1c9..fd2e113e1 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 000000000..17eed210b --- /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 bd351b187..d89284511 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 b13f6519a..c4bd6dace 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 9b34f2a08..ed383a2c9 100644 --- a/tests/Wallabag/CoreBundle/Command/ShowUserCommandTest.php +++ b/tests/Wallabag/CoreBundle/Command/ShowUserCommandTest.php @@ -59,7 +59,8 @@ class ShowUserCommandTest extends WallabagCoreTestCase $this->assertContains('Username: admin', $tester->getDisplay()); $this->assertContains('Email: bigboss@wallabag.org', $tester->getDisplay()); $this->assertContains('Display name: Big boss', $tester->getDisplay()); - $this->assertContains('2FA activated: no', $tester->getDisplay()); + $this->assertContains('2FA (email) activated', $tester->getDisplay()); + $this->assertContains('2FA (OTP) activated', $tester->getDisplay()); } public function testShowUser() diff --git a/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php b/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php index e07c57dd3..fa93c9c21 100644 --- a/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php @@ -1,7 +1,8 @@ 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() @@ -50,7 +51,7 @@ class ConfigControllerTest extends WallabagCoreTestCase $data = [ 'config[theme]' => 'baggy', 'config[items_per_page]' => '30', - 'config[reading_speed]' => '0.5', + 'config[reading_speed]' => '100', 'config[action_mark_as_read]' => '0', 'config[language]' => 'en', ]; @@ -91,7 +92,7 @@ class ConfigControllerTest extends WallabagCoreTestCase $crawler = $client->request('GET', '/config'); $form = $crawler->filter('button[id=config_save]')->form(); $data = [ - 'config[reading_speed]' => '2', + 'config[reading_speed]' => '400', ]; $client->submit($form, $data); @@ -105,7 +106,7 @@ class ConfigControllerTest extends WallabagCoreTestCase $crawler = $client->request('GET', '/config'); $form = $crawler->filter('button[id=config_save]')->form(); $data = [ - 'config[reading_speed]' => '0.5', + 'config[reading_speed]' => '100', ]; $client->submit($form, $data); } @@ -297,7 +298,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 +314,7 @@ class ConfigControllerTest extends WallabagCoreTestCase } $config = $user->getConfig(); - $config->setRssToken(null); + $config->setFeedToken(null); $em->persist($config); $em->flush(); @@ -322,7 +323,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 +331,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->assertContains('config.form_feed.token_reset', $body[0]); } public function testGenerateTokenAjax() @@ -351,7 +352,23 @@ class ConfigControllerTest extends WallabagCoreTestCase $this->assertArrayHasKey('token', $content); } - public function testRssUpdate() + public function testRevokeTokenAjax() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $client->request( + 'GET', + '/revoke-token', + [], + [], + ['HTTP_X-Requested-With' => 'XMLHttpRequest'] + ); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + } + + public function testFeedUpdate() { $this->logInAs('admin'); $client = $this->getClient(); @@ -360,10 +377,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 +389,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 +422,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); @@ -678,7 +695,7 @@ class ConfigControllerTest extends WallabagCoreTestCase $config->setTheme('material'); $config->setItemsPerPage(30); - $config->setReadingSpeed(1); + $config->setReadingSpeed(200); $config->setLanguage('en'); $config->setPocketConsumerKey('xxxxx'); @@ -849,7 +866,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 +982,183 @@ 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()); + } + + public function testExportTaggingRule() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + ob_start(); + $crawler = $client->request('GET', '/tagging-rule/export'); + ob_end_clean(); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $headers = $client->getResponse()->headers; + $this->assertSame('application/json', $headers->get('content-type')); + $this->assertSame('attachment; filename="tagging_rules_admin.json"', $headers->get('content-disposition')); + $this->assertSame('UTF-8', $headers->get('content-transfer-encoding')); + + $content = json_decode($client->getResponse()->getContent(), true); + + $this->assertCount(4, $content); + $this->assertSame('content matches "spurs"', $content[0]['rule']); + $this->assertSame('sport', $content[0]['tags'][0]); + } + + public function testImportTagginfRuleBadFile() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $crawler = $client->request('GET', '/config'); + $form = $crawler->filter('form[name=upload_tagging_rule_file] > button[type=submit]')->form(); + + $data = [ + 'upload_tagging_rule_file[file]' => '', + ]; + + $client->submit($form, $data); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + } + + public function testImportTagginfRuleFile() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $crawler = $client->request('GET', '/config'); + $form = $crawler->filter('form[name=upload_tagging_rule_file] > button[type=submit]')->form(); + + $file = new UploadedFile(__DIR__ . '/../fixtures/tagging_rules_admin.json', 'tagging_rules_admin.json'); + + $data = [ + 'upload_tagging_rule_file[file]' => $file, + ]; + + $client->submit($form, $data); + $this->assertSame(302, $client->getResponse()->getStatusCode()); + + $user = $client->getContainer()->get('fos_user.user_manager.test')->findUserBy(['username' => 'admin']); + $taggingRules = $user->getConfig()->getTaggingRules()->toArray(); + $this->assertCount(5, $taggingRules); + $this->assertSame('title matches "football"', $taggingRules[4]->getRule()); + } } diff --git a/tests/Wallabag/CoreBundle/Controller/EntryControllerTest.php b/tests/Wallabag/CoreBundle/Controller/EntryControllerTest.php index 479e07000..3a8f92e7c 100644 --- a/tests/Wallabag/CoreBundle/Controller/EntryControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/EntryControllerTest.php @@ -13,7 +13,7 @@ class EntryControllerTest extends WallabagCoreTestCase { const AN_URL_CONTAINING_AN_ARTICLE_WITH_IMAGE = 'https://www.lemonde.fr/judo/article/2017/11/11/judo-la-decima-de-teddy-riner_5213605_1556020.html'; public $downloadImagesEnabled = false; - public $url = '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'; + public $url = 'https://www.lemonde.fr/pixels/article/2019/06/18/ce-qu-il-faut-savoir-sur-le-libra-la-cryptomonnaie-de-facebook_5477887_4408996.html'; /** * @after @@ -164,9 +164,8 @@ class EntryControllerTest extends WallabagCoreTestCase $this->assertInstanceOf('Wallabag\CoreBundle\Entity\Entry', $content); $this->assertSame($this->url, $content->getUrl()); - $this->assertContains('Google', $content->getTitle()); + $this->assertContains('la cryptomonnaie de Facebook', $content->getTitle()); $this->assertSame('fr', $content->getLanguage()); - $this->assertSame('2016-04-07 19:01:35', $content->getPublishedAt()->format('Y-m-d H:i:s')); $this->assertArrayHasKey('x-frame-options', $content->getHeaders()); $client->getContainer()->get('craue_config')->set('store_article_headers', 0); } @@ -200,8 +199,8 @@ class EntryControllerTest extends WallabagCoreTestCase $authors = $content->getPublishedBy(); $this->assertSame('2017-04-05 19:26:13', $content->getPublishedAt()->format('Y-m-d H:i:s')); $this->assertSame('fr', $content->getLanguage()); - $this->assertSame('Raphaël Balenieri, correspondant à Pékin', $authors[0]); - $this->assertSame('Frédéric Autran, correspondant à New York', $authors[1]); + $this->assertSame('Raphaël Balenieri', $authors[0]); + $this->assertSame('Frédéric Autran', $authors[1]); } public function testPostNewOkUrlExist() @@ -236,7 +235,45 @@ class EntryControllerTest extends WallabagCoreTestCase $this->logInAs('admin'); $client = $this->getClient(); - $url = 'http://www.aritylabs.com/post/106091708292/des-contr%C3%B4leurs-optionnels-gr%C3%A2ce-%C3%A0-constmissing'; + $url = 'https://www.aritylabs.com/post/106091708292/des-contr%C3%B4leurs-optionnels-gr%C3%A2ce-%C3%A0-constmissing'; + + $crawler = $client->request('GET', '/new'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $form = $crawler->filter('form[name=entry]')->form(); + + $data = [ + 'entry[url]' => $url, + ]; + + $client->submit($form, $data); + + $crawler = $client->request('GET', '/new'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $form = $crawler->filter('form[name=entry]')->form(); + + $data = [ + 'entry[url]' => $url, + ]; + + $client->submit($form, $data); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + $this->assertContains('/view/', $client->getResponse()->getTargetUrl()); + } + + /** + * This test will require an internet connection. + */ + public function testPostNewOkUrlExistWithRedirection() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $url = 'https://wllbg.org/test-redirect/c51c'; $crawler = $client->request('GET', '/new'); @@ -522,9 +559,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 +660,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 +813,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 +1024,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 +1046,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 +1286,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 +1316,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 +1367,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', @@ -1339,7 +1377,7 @@ class EntryControllerTest extends WallabagCoreTestCase ], 'es-ES' => [ 'https://www.20minutos.es/noticia/3360685/0/gobierno-sanchez-primero-historia-mas-mujeres-que-hombres/', - 'es', + 'es_ES', ], ]; } @@ -1494,4 +1532,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 6f3308e56..d7ce7c45a 100644 --- a/tests/Wallabag/CoreBundle/Controller/ExportControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/ExportControllerTest.php @@ -98,7 +98,7 @@ class ExportControllerTest extends WallabagCoreTestCase $headers = $client->getResponse()->headers; $this->assertSame('application/x-mobipocket-ebook', $headers->get('content-type')); - $this->assertSame('attachment; filename="' . preg_replace('/[^A-Za-z0-9\-]/', '', $content->getTitle()) . '.mobi"', $headers->get('content-disposition')); + $this->assertSame('attachment; filename="' . $this->getSanitizedFilename($content->getTitle()) . '.mobi"', $headers->get('content-disposition')); $this->assertSame('binary', $headers->get('content-transfer-encoding')); } @@ -126,7 +126,7 @@ class ExportControllerTest extends WallabagCoreTestCase $headers = $client->getResponse()->headers; $this->assertSame('application/pdf', $headers->get('content-type')); - $this->assertSame('attachment; filename="Tag_entries articles.pdf"', $headers->get('content-disposition')); + $this->assertSame('attachment; filename="Tag foo bar articles.pdf"', $headers->get('content-disposition')); $this->assertSame('binary', $headers->get('content-transfer-encoding')); } @@ -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]); @@ -212,7 +212,7 @@ class ExportControllerTest extends WallabagCoreTestCase $headers = $client->getResponse()->headers; $this->assertSame('application/json', $headers->get('content-type')); - $this->assertSame('attachment; filename="' . $contentInDB->getTitle() . '.json"', $headers->get('content-disposition')); + $this->assertSame('attachment; filename="' . $this->getSanitizedFilename($contentInDB->getTitle()) . '.json"', $headers->get('content-disposition')); $this->assertSame('UTF-8', $headers->get('content-transfer-encoding')); $content = json_decode($client->getResponse()->getContent(), true); @@ -281,4 +281,9 @@ class ExportControllerTest extends WallabagCoreTestCase $this->assertNotEmpty('created_at', (string) $content->entry[0]->created_at); $this->assertNotEmpty('updated_at', (string) $content->entry[0]->updated_at); } + + private function getSanitizedFilename($title) + { + return preg_replace('/[^A-Za-z0-9\- \']/', '', iconv('utf-8', 'us-ascii//TRANSLIT', $title)); + } } diff --git a/tests/Wallabag/CoreBundle/Controller/FeedControllerTest.php b/tests/Wallabag/CoreBundle/Controller/FeedControllerTest.php new file mode 100644 index 000000000..d52d7bb8a --- /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 2af6e14f3..000000000 --- a/tests/Wallabag/CoreBundle/Controller/RssControllerTest.php +++ /dev/null @@ -1,221 +0,0 @@ -loadXML($xml); - - $xpath = new \DOMXpath($doc); - - if (null === $nb) { - $this->assertGreaterThan(0, $xpath->query('//item')->length); - } else { - $this->assertSame($nb, $xpath->query('//item')->length); - } - - $this->assertSame(1, $xpath->query('/rss')->length); - $this->assertSame(1, $xpath->query('/rss/channel')->length); - - $this->assertSame(1, $xpath->query('/rss/channel/title')->length); - $this->assertSame('wallabag - ' . $type . ' feed', $xpath->query('/rss/channel/title')->item(0)->nodeValue); - - $this->assertSame(1, $xpath->query('/rss/channel/pubDate')->length); - - $this->assertSame(1, $xpath->query('/rss/channel/generator')->length); - $this->assertSame('wallabag', $xpath->query('/rss/channel/generator')->item(0)->nodeValue); - - $this->assertSame(1, $xpath->query('/rss/channel/description')->length); - $this->assertSame('wallabag ' . $type . ' elements', $xpath->query('/rss/channel/description')->item(0)->nodeValue); - - $this->assertSame(1, $xpath->query('/rss/channel/link[@rel="self"]')->length); - $this->assertContains($urlPagination . '.xml', $xpath->query('/rss/channel/link[@rel="self"]')->item(0)->getAttribute('href')); - - $this->assertSame(1, $xpath->query('/rss/channel/link[@rel="last"]')->length); - $this->assertContains($urlPagination . '.xml?page=', $xpath->query('/rss/channel/link[@rel="last"]')->item(0)->getAttribute('href')); - - foreach ($xpath->query('//item') as $item) { - $this->assertSame(1, $xpath->query('title', $item)->length); - $this->assertSame(1, $xpath->query('source', $item)->length); - $this->assertSame(1, $xpath->query('link', $item)->length); - $this->assertSame(1, $xpath->query('guid', $item)->length); - $this->assertSame(1, $xpath->query('pubDate', $item)->length); - $this->assertSame(1, $xpath->query('description', $item)->length); - } - } - - public function dataForBadUrl() - { - return [ - [ - '/admin/YZIOAUZIAO/unread.xml', - ], - [ - '/wallace/YZIOAUZIAO/starred.xml', - ], - [ - '/wallace/YZIOAUZIAO/archives.xml', - ], - [ - '/wallace/YZIOAUZIAO/all.xml', - ], - ]; - } - - /** - * @dataProvider dataForBadUrl - */ - public function testBadUrl($url) - { - $client = $this->getClient(); - - $client->request('GET', $url); - - $this->assertSame(404, $client->getResponse()->getStatusCode()); - } - - public function testUnread() - { - $client = $this->getClient(); - $em = $client->getContainer()->get('doctrine.orm.entity_manager'); - $user = $em - ->getRepository('WallabagUserBundle:User') - ->findOneByUsername('admin'); - - $config = $user->getConfig(); - $config->setRssToken('SUPERTOKEN'); - $config->setRssLimit(2); - $em->persist($config); - $em->flush(); - - $client->request('GET', '/admin/SUPERTOKEN/unread.xml'); - - $this->assertSame(200, $client->getResponse()->getStatusCode()); - - $this->validateDom($client->getResponse()->getContent(), 'unread', 'unread', 2); - } - - public function testStarred() - { - $client = $this->getClient(); - $em = $client->getContainer()->get('doctrine.orm.entity_manager'); - $user = $em - ->getRepository('WallabagUserBundle:User') - ->findOneByUsername('admin'); - - $config = $user->getConfig(); - $config->setRssToken('SUPERTOKEN'); - $config->setRssLimit(1); - $em->persist($config); - $em->flush(); - - $client = $this->getClient(); - $client->request('GET', '/admin/SUPERTOKEN/starred.xml'); - - $this->assertSame(200, $client->getResponse()->getStatusCode(), 1); - - $this->validateDom($client->getResponse()->getContent(), 'starred', 'starred'); - } - - public function testArchives() - { - $client = $this->getClient(); - $em = $client->getContainer()->get('doctrine.orm.entity_manager'); - $user = $em - ->getRepository('WallabagUserBundle:User') - ->findOneByUsername('admin'); - - $config = $user->getConfig(); - $config->setRssToken('SUPERTOKEN'); - $config->setRssLimit(null); - $em->persist($config); - $em->flush(); - - $client = $this->getClient(); - $client->request('GET', '/admin/SUPERTOKEN/archive.xml'); - - $this->assertSame(200, $client->getResponse()->getStatusCode()); - - $this->validateDom($client->getResponse()->getContent(), 'archive', 'archive'); - } - - public function testAll() - { - $client = $this->getClient(); - $em = $client->getContainer()->get('doctrine.orm.entity_manager'); - $user = $em - ->getRepository('WallabagUserBundle:User') - ->findOneByUsername('admin'); - - $config = $user->getConfig(); - $config->setRssToken('SUPERTOKEN'); - $config->setRssLimit(null); - $em->persist($config); - $em->flush(); - - $client = $this->getClient(); - $client->request('GET', '/admin/SUPERTOKEN/all.xml'); - - $this->assertSame(200, $client->getResponse()->getStatusCode()); - - $this->validateDom($client->getResponse()->getContent(), 'all', 'all'); - } - - public function testPagination() - { - $client = $this->getClient(); - $em = $client->getContainer()->get('doctrine.orm.entity_manager'); - $user = $em - ->getRepository('WallabagUserBundle:User') - ->findOneByUsername('admin'); - - $config = $user->getConfig(); - $config->setRssToken('SUPERTOKEN'); - $config->setRssLimit(1); - $em->persist($config); - $em->flush(); - - $client = $this->getClient(); - - $client->request('GET', '/admin/SUPERTOKEN/unread.xml'); - $this->assertSame(200, $client->getResponse()->getStatusCode()); - $this->validateDom($client->getResponse()->getContent(), 'unread', 'unread'); - - $client->request('GET', '/admin/SUPERTOKEN/unread.xml?page=2'); - $this->assertSame(200, $client->getResponse()->getStatusCode()); - $this->validateDom($client->getResponse()->getContent(), 'unread', 'unread'); - - $client->request('GET', '/admin/SUPERTOKEN/unread.xml?page=3000'); - $this->assertSame(302, $client->getResponse()->getStatusCode()); - } - - public function testTags() - { - $client = $this->getClient(); - $em = $client->getContainer()->get('doctrine.orm.entity_manager'); - $user = $em - ->getRepository('WallabagUserBundle:User') - ->findOneByUsername('admin'); - - $config = $user->getConfig(); - $config->setRssToken('SUPERTOKEN'); - $config->setRssLimit(null); - $em->persist($config); - $em->flush(); - - $client = $this->getClient(); - $client->request('GET', '/admin/SUPERTOKEN/tags/foo.xml'); - - $this->assertSame(200, $client->getResponse()->getStatusCode()); - - $this->validateDom($client->getResponse()->getContent(), 'tag (foo)', 'tags/foo'); - - $client->request('GET', '/admin/SUPERTOKEN/tags/foo.xml?page=3000'); - $this->assertSame(302, $client->getResponse()->getStatusCode()); - } -} diff --git a/tests/Wallabag/CoreBundle/Controller/SecurityControllerTest.php b/tests/Wallabag/CoreBundle/Controller/SecurityControllerTest.php index 395208a2f..6b51f4035 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,24 +42,24 @@ class SecurityControllerTest extends WallabagCoreTestCase $user = $em ->getRepository('WallabagUserBundle:User') ->findOneByUsername('admin'); - $user->setTwoFactorAuthentication(true); + $user->setEmailTwoFactor(true); $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]); + $this->assertContains('trusted', $crawler->filter('body')->extract(['_text'])[0]); // restore user $user = $em ->getRepository('WallabagUserBundle:User') ->findOneByUsername('admin'); - $user->setTwoFactorAuthentication(false); + $user->setEmailTwoFactor(false); $em->persist($user); $em->flush(); } - public function testTrustedComputer() + public function testLoginWith2FactorGoogle() { $client = $this->getClient(); @@ -69,15 +69,27 @@ class SecurityControllerTest extends WallabagCoreTestCase return; } + $client->followRedirects(); + $em = $client->getContainer()->get('doctrine.orm.entity_manager'); $user = $em ->getRepository('WallabagUserBundle:User') ->findOneByUsername('admin'); + $user->setGoogleAuthenticatorSecret('26LDIHYGHNELOQEM'); + $em->persist($user); + $em->flush(); - $date = new \DateTime(); - $user->addTrustedComputer('ABCDEF', $date->add(new \DateInterval('P1M'))); - $this->assertTrue($user->isTrustedComputer('ABCDEF')); - $this->assertFalse($user->isTrustedComputer('FEDCBA')); + $this->logInAsUsingHttp('admin'); + $crawler = $client->request('GET', '/config'); + $this->assertContains('trusted', $crawler->filter('body')->extract(['_text'])[0]); + + // restore user + $user = $em + ->getRepository('WallabagUserBundle:User') + ->findOneByUsername('admin'); + $user->setGoogleAuthenticatorSecret(null); + $em->persist($user); + $em->flush(); } public function testEnabledRegistration() diff --git a/tests/Wallabag/CoreBundle/Controller/TagControllerTest.php b/tests/Wallabag/CoreBundle/Controller/TagControllerTest.php index 768f4c078..47c83a7ba 100644 --- a/tests/Wallabag/CoreBundle/Controller/TagControllerTest.php +++ b/tests/Wallabag/CoreBundle/Controller/TagControllerTest.php @@ -176,4 +176,95 @@ 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.'); + } + + public function testAddUnicodeTagLabel() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $entry = new Entry($this->getLoggedInUser()); + $entry->setUrl('http://0.0.0.0/tag-caché'); + $this->getEntityManager()->persist($entry); + $this->getEntityManager()->flush(); + $this->getEntityManager()->clear(); + + $crawler = $client->request('GET', '/view/' . $entry->getId()); + + $form = $crawler->filter('form[name=tag]')->form(); + + $data = [ + 'tag[label]' => 'cache', + ]; + + $client->submit($form, $data); + + $crawler = $client->request('GET', '/view/' . $entry->getId()); + + $form = $crawler->filter('form[name=tag]')->form(); + + $data = [ + 'tag[label]' => 'caché', + ]; + + $client->submit($form, $data); + + $newEntry = $client->getContainer() + ->get('doctrine.orm.entity_manager') + ->getRepository('WallabagCoreBundle:Entry') + ->find($entry->getId()); + + $tags = $newEntry->getTags()->toArray(); + foreach ($tags as $key => $tag) { + $tags[$key] = $tag->getLabel(); + } + + $this->assertGreaterThanOrEqual(2, \count($tags)); + $this->assertNotFalse(array_search('cache', $tags, true), 'Tag cache is assigned to the entry'); + $this->assertNotFalse(array_search('caché', $tags, true), 'Tag caché is assigned to the entry'); + } } diff --git a/tests/Wallabag/CoreBundle/Entity/EntryTest.php b/tests/Wallabag/CoreBundle/Entity/EntryTest.php new file mode 100644 index 000000000..d400636e7 --- /dev/null +++ b/tests/Wallabag/CoreBundle/Entity/EntryTest.php @@ -0,0 +1,28 @@ +logInAs('admin'); + $entry = new Entry($this->getLoggedInUser()); + $languages = [ + 'en_GB' => 'en-GB', + 'en_US' => 'en-US', + 'en-gb' => 'en-GB', + 'en-US' => 'en-US', + 'fr' => 'fr', + 'fr_FR' => 'fr-FR', + 'ja' => 'ja', + ]; + foreach ($languages as $entryLang => $lang) { + $entry->setLanguage($entryLang); + $this->assertSame($lang, $entry->getHTMLLanguage()); + } + } +} diff --git a/tests/Wallabag/CoreBundle/Event/Listener/UserLocaleListenerTest.php b/tests/Wallabag/CoreBundle/Event/Listener/UserLocaleListenerTest.php index 93edfde8c..ff0a9602b 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 1173fc3de..9e0a91365 100644 --- a/tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php +++ b/tests/Wallabag/CoreBundle/GuzzleSiteAuthenticator/GrabySiteConfigBuilderTest.php @@ -5,26 +5,24 @@ namespace Tests\Wallabag\CoreBundle\GuzzleSiteAuthenticator; use Graby\SiteConfig\SiteConfig as GrabySiteConfig; use Monolog\Handler\TestHandler; use Monolog\Logger; -use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Tests\Wallabag\CoreBundle\WallabagCoreTestCase; use Wallabag\CoreBundle\GuzzleSiteAuthenticator\GrabySiteConfigBuilder; -class GrabySiteConfigBuilderTest extends TestCase +class GrabySiteConfigBuilderTest extends WallabagCoreTestCase { - /** @var \Wallabag\CoreBundle\GuzzleSiteAuthenticator\GrabySiteConfigBuilder */ - protected $builder; + private $builder; public function testBuildConfigExists() { - /* @var \Graby\SiteConfig\ConfigBuilder|\PHPUnit_Framework_MockObject_MockObject */ $grabyConfigBuilderMock = $this->getMockBuilder('Graby\SiteConfig\ConfigBuilder') ->disableOriginalConstructor() ->getMock(); $grabySiteConfig = new GrabySiteConfig(); $grabySiteConfig->requires_login = true; - $grabySiteConfig->login_uri = 'http://www.example.com/login'; + $grabySiteConfig->login_uri = 'http://api.example.com/login'; $grabySiteConfig->login_username_field = 'login'; $grabySiteConfig->login_password_field = 'password'; $grabySiteConfig->login_extra_fields = ['field=value']; @@ -32,8 +30,8 @@ class GrabySiteConfigBuilderTest extends TestCase $grabyConfigBuilderMock ->method('buildForHost') - ->with('example.com') - ->will($this->returnValue($grabySiteConfig)); + ->with('api.example.com') + ->willReturn($grabySiteConfig); $logger = new Logger('foo'); $handler = new TestHandler(); @@ -43,8 +41,126 @@ class GrabySiteConfigBuilderTest extends TestCase ->disableOriginalConstructor() ->getMock(); $siteCrentialRepo->expects($this->once()) - ->method('findOneByHostAndUser') - ->with('example.com', 1) + ->method('findOneByHostsAndUser') + ->with(['api.example.com', '.example.com'], 1) + ->willReturn(['username' => 'foo', 'password' => 'bar']); + + $user = $this->getMockBuilder('Wallabag\UserBundle\Entity\User') + ->disableOriginalConstructor() + ->getMock(); + $user->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $token = new UsernamePasswordToken($user, 'pass', 'provider'); + + $tokenStorage = new TokenStorage(); + $tokenStorage->setToken($token); + + $builder = new GrabySiteConfigBuilder( + $grabyConfigBuilderMock, + $tokenStorage, + $siteCrentialRepo, + $logger + ); + + $config = $builder->buildForHost('api.example.com'); + + $this->assertSame('api.example.com', $config->getHost()); + $this->assertTrue($config->requiresLogin()); + $this->assertSame('http://api.example.com/login', $config->getLoginUri()); + $this->assertSame('login', $config->getUsernameField()); + $this->assertSame('password', $config->getPasswordField()); + $this->assertSame(['field' => 'value'], $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 testBuildConfigDoesntExist() + { + $grabyConfigBuilderMock = $this->getMockBuilder('\Graby\SiteConfig\ConfigBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $grabyConfigBuilderMock + ->method('buildForHost') + ->with('unknown.com') + ->willReturn(new 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(['unknown.com', '.com'], 1) + ->willReturn(null); + + $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); + + $builder = new GrabySiteConfigBuilder( + $grabyConfigBuilderMock, + $tokenStorage, + $siteCrentialRepo, + $logger + ); + + $config = $builder->buildForHost('unknown.com'); + + $this->assertFalse($config); + + $records = $handler->getRecords(); + + $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') @@ -73,7 +189,7 @@ class GrabySiteConfigBuilderTest extends TestCase $this->assertSame('http://www.example.com/login', $config->getLoginUri()); $this->assertSame('login', $config->getUsernameField()); $this->assertSame('password', $config->getPasswordField()); - $this->assertSame(['field' => 'value'], $config->getExtraFields()); + $this->assertSame([], $config->getExtraFields()); $this->assertSame('//div[@class="need-login"]', $config->getNotLoggedInXpath()); $this->assertSame('foo', $config->getUsername()); $this->assertSame('bar', $config->getPassword()); @@ -83,9 +199,8 @@ class GrabySiteConfigBuilderTest extends TestCase $this->assertCount(1, $records, 'One log was recorded'); } - public function testBuildConfigDoesntExist() + public function testBuildConfigUserNotDefined() { - /* @var \Graby\SiteConfig\ConfigBuilder|\PHPUnit_Framework_MockObject_MockObject */ $grabyConfigBuilderMock = $this->getMockBuilder('\Graby\SiteConfig\ConfigBuilder') ->disableOriginalConstructor() ->getMock(); @@ -93,7 +208,7 @@ class GrabySiteConfigBuilderTest extends TestCase $grabyConfigBuilderMock ->method('buildForHost') ->with('unknown.com') - ->will($this->returnValue(new GrabySiteConfig())); + ->willReturn(new GrabySiteConfig()); $logger = new Logger('foo'); $handler = new TestHandler(); @@ -102,10 +217,69 @@ class GrabySiteConfigBuilderTest extends TestCase $siteCrentialRepo = $this->getMockBuilder('Wallabag\CoreBundle\Repository\SiteCredentialRepository') ->disableOriginalConstructor() ->getMock(); - $siteCrentialRepo->expects($this->once()) - ->method('findOneByHostAndUser') - ->with('unknown.com', 1) - ->willReturn(null); + + $tokenStorage = new TokenStorage(); + + $builder = new GrabySiteConfigBuilder( + $grabyConfigBuilderMock, + $tokenStorage, + $siteCrentialRepo, + $logger + ); + + $config = $builder->buildForHost('unknown.com'); + + $this->assertFalse($config); + } + + public function dataProviderCredentials() + { + return [ + [ + 'host' => 'example.com', + ], + [ + 'host' => 'other.example.com', + ], + [ + 'host' => 'paywall.example.com', + 'expectedUsername' => 'paywall.example', + 'expectedPassword' => 'bar', + ], + [ + 'host' => 'api.super.com', + 'expectedUsername' => '.super', + 'expectedPassword' => 'bar', + ], + [ + 'host' => '.super.com', + 'expectedUsername' => '.super', + 'expectedPassword' => 'bar', + ], + ]; + } + + /** + * @dataProvider dataProviderCredentials + */ + public function testBuildConfigWithDbAccess($host, $expectedUsername = null, $expectedPassword = null) + { + $grabyConfigBuilderMock = $this->getMockBuilder('Graby\SiteConfig\ConfigBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $grabySiteConfig = new GrabySiteConfig(); + $grabySiteConfig->requires_login = true; + $grabySiteConfig->login_uri = 'http://api.example.com/login'; + $grabySiteConfig->login_username_field = 'login'; + $grabySiteConfig->login_password_field = 'password'; + $grabySiteConfig->login_extra_fields = ['field=value']; + $grabySiteConfig->not_logged_in_xpath = '//div[@class="need-login"]'; + + $grabyConfigBuilderMock + ->method('buildForHost') + ->with($host) + ->willReturn($grabySiteConfig); $user = $this->getMockBuilder('Wallabag\UserBundle\Entity\User') ->disableOriginalConstructor() @@ -119,19 +293,26 @@ class GrabySiteConfigBuilderTest extends TestCase $tokenStorage = new TokenStorage(); $tokenStorage->setToken($token); - $this->builder = new GrabySiteConfigBuilder( + $logger = new Logger('foo'); + $handler = new TestHandler(); + $logger->pushHandler($handler); + + $builder = new GrabySiteConfigBuilder( $grabyConfigBuilderMock, $tokenStorage, - $siteCrentialRepo, + $this->getClient()->getContainer()->get('wallabag_core.site_credential_repository'), $logger ); - $config = $this->builder->buildForHost('unknown.com'); + $config = $builder->buildForHost($host); - $this->assertFalse($config); + if (null === $expectedUsername && null === $expectedPassword) { + $this->assertFalse($config); - $records = $handler->getRecords(); + return; + } - $this->assertCount(1, $records, 'One log was recorded'); + $this->assertSame($expectedUsername, $config->getUsername()); + $this->assertSame($expectedPassword, $config->getPassword()); } } diff --git a/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php b/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php index 3dd9273c8..9ce72c79c 100644 --- a/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php +++ b/tests/Wallabag/CoreBundle/Helper/ContentProxyTest.php @@ -36,7 +36,9 @@ class ContentProxyTest extends TestCase 'html' => false, 'title' => '', 'url' => '', - 'content_type' => '', + 'headers' => [ + 'content-type' => '', + ], 'language' => '', ]); @@ -71,7 +73,9 @@ class ContentProxyTest extends TestCase 'html' => false, 'title' => '', 'url' => '', - 'content_type' => '', + 'headers' => [ + 'content-type' => '', + ], 'language' => '', ]); @@ -104,15 +108,14 @@ class ContentProxyTest extends TestCase ->method('fetchContent') ->willReturn([ 'html' => false, - 'title' => '', + 'title' => 'my title', 'url' => '', - 'content_type' => '', + 'headers' => [ + 'content-type' => '', + ], 'language' => '', 'status' => '', - 'open_graph' => [ - 'og_title' => 'my title', - 'og_description' => 'desc', - ], + 'description' => 'desc', ]); $proxy = new ContentProxy($graby, $tagger, $this->getValidator(), $this->getLogger(), $this->fetchingErrorMessage); @@ -147,13 +150,12 @@ class ContentProxyTest extends TestCase 'html' => str_repeat('this is my content', 325), 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', 'language' => 'fr', 'status' => '200', - 'open_graph' => [ - 'og_title' => 'my OG title', - 'og_description' => 'OG desc', - 'og_image' => 'http://3.3.3.3/cover.jpg', + 'description' => 'OG desc', + 'image' => 'http://3.3.3.3/cover.jpg', + 'headers' => [ + 'content-type' => 'text/html', ], ]); @@ -163,7 +165,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()); @@ -189,13 +191,12 @@ class ContentProxyTest extends TestCase 'html' => str_repeat('this is my content', 325), 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', 'language' => 'fr', 'status' => '200', - 'open_graph' => [ - 'og_title' => 'my OG title', - 'og_description' => 'OG desc', - 'og_image' => null, + 'description' => 'OG desc', + 'image' => null, + 'headers' => [ + 'content-type' => 'text/html', ], ]); @@ -205,7 +206,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 +215,86 @@ 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', + 'headers' => [ + 'content-type' => 'text/html', + ], + 'language' => 'fr', + 'status' => '200', + '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', + 'headers' => [ + 'content-type' => 'text/html', + ], + 'language' => 'fr', + 'status' => '200', + '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(); @@ -236,9 +317,11 @@ class ContentProxyTest extends TestCase 'html' => str_repeat('this is my content', 325), 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', 'language' => 'dontexist', 'status' => '200', + 'headers' => [ + 'content-type' => 'text/html', + ], ]); $proxy = new ContentProxy($graby, $tagger, $validator, $this->getLogger(), $this->fetchingErrorMessage); @@ -247,7 +330,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()); @@ -280,14 +363,13 @@ class ContentProxyTest extends TestCase 'html' => str_repeat('this is my content', 325), 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', + 'headers' => [ + 'content-type' => 'text/html', + ], 'language' => 'fr', 'status' => '200', - 'open_graph' => [ - 'og_title' => 'my OG title', - 'og_description' => 'OG desc', - 'og_image' => 'https://', - ], + 'description' => 'OG desc', + 'image' => 'https://', ]); $proxy = new ContentProxy($graby, $tagger, $validator, $this->getLogger(), $this->fetchingErrorMessage); @@ -296,7 +378,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()); @@ -320,19 +402,19 @@ class ContentProxyTest extends TestCase 'html' => str_repeat('this is my content', 325), 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', 'language' => 'fr', 'date' => '1395635872', 'authors' => ['Jeremy', 'Nico', 'Thomas'], - 'all_headers' => [ - 'Cache-Control' => 'no-cache', + 'headers' => [ + 'cache-control' => 'no-cache', + 'content-type' => 'text/html', ], ] ); $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()); @@ -363,15 +445,17 @@ class ContentProxyTest extends TestCase 'html' => str_repeat('this is my content', 325), 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', 'language' => 'fr', 'date' => '2016-09-08T11:55:58+0200', + 'headers' => [ + 'content-type' => 'text/html', + ], ] ); $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()); @@ -398,15 +482,17 @@ class ContentProxyTest extends TestCase 'html' => str_repeat('this is my content', 325), 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', 'language' => 'fr', 'date' => '01 02 2012', + 'headers' => [ + 'content-type' => 'text/html', + ], ] ); $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 +501,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']); } @@ -435,8 +521,10 @@ class ContentProxyTest extends TestCase 'html' => str_repeat('this is my content', 325), 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', 'language' => 'fr', + 'headers' => [ + 'content-type' => 'text/html', + ], ] ); @@ -475,13 +563,13 @@ class ContentProxyTest extends TestCase 'html' => $html, 'title' => 'this is my title', 'url' => 'http://1.1.1.1', - 'content_type' => 'text/html', 'language' => 'fr', 'status' => '200', - 'open_graph' => [ - 'og_title' => 'my OG title', - 'og_description' => 'OG desc', - 'og_image' => 'http://3.3.3.3/cover.jpg', + //'og_title' => 'my OG title', + 'description' => 'OG desc', + 'image' => 'http://3.3.3.3/cover.jpg', + 'headers' => [ + 'content-type' => 'text/html', ], ] ); @@ -513,9 +601,10 @@ class ContentProxyTest extends TestCase 'html' => '

', 'title' => 'this is my title', 'url' => 'http://1.1.1.1/image.jpg', - 'content_type' => 'image/jpeg', 'status' => '200', - 'open_graph' => [], + 'headers' => [ + 'content-type' => 'image/jpeg', + ], ]); $proxy = new ContentProxy($graby, $tagger, $this->getValidator(), $this->getLogger(), $this->fetchingErrorMessage); @@ -553,7 +642,9 @@ class ContentProxyTest extends TestCase 'html' => false, 'title' => $actualTitle, 'url' => '', - 'content_type' => 'text/html', + 'headers' => [ + 'content-type' => 'text/html', + ], 'language' => '', ]); @@ -588,7 +679,9 @@ class ContentProxyTest extends TestCase 'html' => false, 'title' => $actualTitle, 'url' => '', - 'content_type' => 'text/html', + 'headers' => [ + 'content-type' => 'text/html', + ], 'language' => '', ]); @@ -622,7 +715,9 @@ class ContentProxyTest extends TestCase 'html' => false, 'title' => $actualTitle, 'url' => '', - 'content_type' => 'application/pdf', + 'headers' => [ + 'content-type' => 'application/pdf', + ], 'language' => '', ]); @@ -656,7 +751,9 @@ class ContentProxyTest extends TestCase 'html' => false, 'title' => $actualTitle, 'url' => '', - 'content_type' => 'application/pdf', + 'headers' => [ + 'content-type' => 'application/pdf', + ], 'language' => '', ]); @@ -690,7 +787,9 @@ class ContentProxyTest extends TestCase 'html' => false, 'title' => $actualTitle, 'url' => '', - 'content_type' => 'application/pdf', + 'headers' => [ + 'content-type' => 'application/pdf', + ], 'language' => '', ]); @@ -725,7 +824,9 @@ class ContentProxyTest extends TestCase 'html' => false, 'title' => $actualTitle, 'url' => '', - 'content_type' => 'application/pdf', + 'headers' => [ + 'content-type' => 'application/pdf', + ], 'language' => '', ]); @@ -855,7 +956,9 @@ class ContentProxyTest extends TestCase 'html' => false, 'title' => '', 'url' => $content_url, - 'content_type' => '', + 'headers' => [ + 'content-type' => '', + ], 'language' => '', ], true @@ -886,7 +989,9 @@ class ContentProxyTest extends TestCase } /** - * https://stackoverflow.com/a/18506801. + * Convert hex to string. + * + * @see https://stackoverflow.com/a/18506801 * * @param $hex * diff --git a/tests/Wallabag/CoreBundle/Helper/DownloadImagesTest.php b/tests/Wallabag/CoreBundle/Helper/DownloadImagesTest.php index cda5f8431..3c720425a 100644 --- a/tests/Wallabag/CoreBundle/Helper/DownloadImagesTest.php +++ b/tests/Wallabag/CoreBundle/Helper/DownloadImagesTest.php @@ -2,10 +2,8 @@ namespace Tests\Wallabag\CoreBundle\Helper; -use GuzzleHttp\Client; -use GuzzleHttp\Message\Response; -use GuzzleHttp\Stream\Stream; -use GuzzleHttp\Subscriber\Mock; +use GuzzleHttp\Psr7\Response; +use Http\Mock\Client as HttpMockClient; use Monolog\Handler\TestHandler; use Monolog\Logger; use PHPUnit\Framework\TestCase; @@ -32,18 +30,14 @@ class DownloadImagesTest extends TestCase */ public function testProcessHtml($html, $url) { - $client = new Client(); + $httpMockClient = new HttpMockClient(); - $mock = new Mock([ - new Response(200, ['content-type' => 'image/png'], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/unnamed.png'))), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient->addResponse(new Response(200, ['content-type' => 'image/png'], file_get_contents(__DIR__ . '/../fixtures/unnamed.png'))); $logHandler = new TestHandler(); $logger = new Logger('test', [$logHandler]); - $download = new DownloadImages($client, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); + $download = new DownloadImages($httpMockClient, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); $res = $download->processHtml(123, $html, $url); @@ -53,18 +47,13 @@ class DownloadImagesTest extends TestCase public function testProcessHtmlWithBadImage() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['content-type' => 'application/json'], Stream::factory('')), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['content-type' => 'application/json'], '')); $logHandler = new TestHandler(); $logger = new Logger('test', [$logHandler]); - $download = new DownloadImages($client, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); + $download = new DownloadImages($httpMockClient, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); $res = $download->processHtml(123, '
', 'http://imgur.com/gallery/WxtWY'); $this->assertContains('http://i.imgur.com/T9qgcHc.jpg', $res, 'Image were not replace because of content-type'); @@ -85,18 +74,13 @@ class DownloadImagesTest extends TestCase */ public function testProcessSingleImage($header, $extension) { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['content-type' => $header], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/unnamed.png'))), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['content-type' => $header], file_get_contents(__DIR__ . '/../fixtures/unnamed.png'))); $logHandler = new TestHandler(); $logger = new Logger('test', [$logHandler]); - $download = new DownloadImages($client, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); + $download = new DownloadImages($httpMockClient, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); $res = $download->processSingleImage(123, 'T9qgcHc.jpg', 'http://imgur.com/gallery/WxtWY'); $this->assertContains('/assets/images/9/b/9b0ead26/ebe60399.' . $extension, $res); @@ -104,18 +88,13 @@ class DownloadImagesTest extends TestCase public function testProcessSingleImageWithBadUrl() { - $client = new Client(); - - $mock = new Mock([ - new Response(404, []), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(404, [])); $logHandler = new TestHandler(); $logger = new Logger('test', [$logHandler]); - $download = new DownloadImages($client, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); + $download = new DownloadImages($httpMockClient, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); $res = $download->processSingleImage(123, 'T9qgcHc.jpg', 'http://imgur.com/gallery/WxtWY'); $this->assertFalse($res, 'Image can not be found, so it will not be replaced'); @@ -123,18 +102,13 @@ class DownloadImagesTest extends TestCase public function testProcessSingleImageWithBadImage() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['content-type' => 'image/png'], Stream::factory('')), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['content-type' => 'image/png'], '')); $logHandler = new TestHandler(); $logger = new Logger('test', [$logHandler]); - $download = new DownloadImages($client, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); + $download = new DownloadImages($httpMockClient, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); $res = $download->processSingleImage(123, 'http://i.imgur.com/T9qgcHc.jpg', 'http://imgur.com/gallery/WxtWY'); $this->assertFalse($res, 'Image can not be loaded, so it will not be replaced'); @@ -142,18 +116,13 @@ class DownloadImagesTest extends TestCase public function testProcessSingleImageFailAbsolute() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['content-type' => 'image/png'], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/unnamed.png'))), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['content-type' => 'image/png'], file_get_contents(__DIR__ . '/../fixtures/unnamed.png'))); $logHandler = new TestHandler(); $logger = new Logger('test', [$logHandler]); - $download = new DownloadImages($client, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); + $download = new DownloadImages($httpMockClient, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); $res = $download->processSingleImage(123, '/i.imgur.com/T9qgcHc.jpg', 'imgur.com/gallery/WxtWY'); $this->assertFalse($res, 'Absolute image can not be determined, so it will not be replaced'); @@ -161,18 +130,13 @@ class DownloadImagesTest extends TestCase public function testProcessRealImage() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['content-type' => null], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['content-type' => null], file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))); $logHandler = new TestHandler(); $logger = new Logger('test', [$logHandler]); - $download = new DownloadImages($client, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); + $download = new DownloadImages($httpMockClient, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); $res = $download->processSingleImage( 123, @@ -186,20 +150,15 @@ class DownloadImagesTest extends TestCase public function testProcessImageWithSrcset() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['content-type' => 'image/jpeg'], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))), - new Response(200, ['content-type' => 'image/jpeg'], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))), - new Response(200, ['content-type' => 'image/jpeg'], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['content-type' => null], file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))); + $httpMockClient->addResponse(new Response(200, ['content-type' => null], file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))); + $httpMockClient->addResponse(new Response(200, ['content-type' => null], file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))); $logHandler = new TestHandler(); $logger = new Logger('test', [$logHandler]); - $download = new DownloadImages($client, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); + $download = new DownloadImages($httpMockClient, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); $res = $download->processHtml(123, '

', 'http://piketty.blog.lemonde.fr/2017/10/12/budget-2018-la-jeunesse-sacrifiee/'); $this->assertNotContains('http://piketty.blog.lemonde.fr/', $res, 'Image srcset attribute were not replaced'); @@ -207,20 +166,15 @@ class DownloadImagesTest extends TestCase public function testProcessImageWithTrickySrcset() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['content-type' => 'image/jpeg'], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))), - new Response(200, ['content-type' => 'image/jpeg'], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))), - new Response(200, ['content-type' => 'image/jpeg'], Stream::factory(file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['content-type' => null], file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))); + $httpMockClient->addResponse(new Response(200, ['content-type' => null], file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))); + $httpMockClient->addResponse(new Response(200, ['content-type' => null], file_get_contents(__DIR__ . '/../fixtures/image-no-content-type.jpg'))); $logHandler = new TestHandler(); $logger = new Logger('test', [$logHandler]); - $download = new DownloadImages($client, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); + $download = new DownloadImages($httpMockClient, sys_get_temp_dir() . '/wallabag_test', 'http://wallabag.io/', $logger); $res = $download->processHtml(123, '
getRelativePathname() . '" as wrong definition, see README.'); + } + + $examples[] = [ + $file->getRelativePathname(), + $match[1], // content + $match[2], // reading time + ]; } return $examples; diff --git a/tests/Wallabag/CoreBundle/Tools/samples/README b/tests/Wallabag/CoreBundle/Tools/samples/README new file mode 100644 index 000000000..e8f946c03 --- /dev/null +++ b/tests/Wallabag/CoreBundle/Tools/samples/README @@ -0,0 +1,5 @@ +Defined language sample should use the following structure: + +-----CONTENT----- + +-----READING_TIME----- diff --git a/tests/Wallabag/CoreBundle/Tools/samples/chinese.txt b/tests/Wallabag/CoreBundle/Tools/samples/chinese.txt new file mode 100644 index 000000000..864603cb2 --- /dev/null +++ b/tests/Wallabag/CoreBundle/Tools/samples/chinese.txt @@ -0,0 +1,10 @@ +-----CONTENT----- +职然问讲念谷月挂倧报䜏本読胜圕芁耐蟌。料士纞朚陈䞎兎组静终図问有。今芳深蜊盞环孊俳健越増职県県倚刞报。雪月批富掲皿家猝城闎真䞭厩図人连。前担写治芞面毎䜜䌌氎州皿泚球戊頃。枈方宮安目垣匷入料䌚先呌略。蚈定蚭負財䜜芧経己員事田事球岡瀺差孊。最院曞暡婚金回犁朝船教任分犁怜理慮宿。 + +倉送调指匏真気亀现䞊様女限宅倍。犁䞚皿者普视想来朚残止者枈断匏安。䞇臎盞领鉄再改界逮由竹匏元最台倉。枈问掻助库脳郚风政京転诎区倉。文図化仙政垞地里芞䞊耒前読望误记枩政信土。惑育候圓人䞇郚逮重申結暙番業望般。断瀬埌瀟倩打日資亀献秀䞖芧第。補圓線里身瀟蚘利件郚倜䞭心掲倧。 + +时倧栗倜测眲垂芁纯京挙化枈莟品。倩最场情算掲攟故手茚指岛然枡掻民幎。第纯亀䞀特问明宀试賛际者建。论铜所垞瞄䞀広気特秋提公茶可満猖旅盞倉暩。 + +兵线枈来先决暡入䟛定树垌逮技鉄倚连写塩。着刊犁浩歩人仕讟谢争关呚埒今高。十育幕桂球闚蜜任快毎瀟掋着道育纞栌幻末。关机高害通方纳狱瀟州芁北盞持䞭衚。郎垂真提里过䜕连地曎重郜山割呚。 +-----READING_TIME----- +1 diff --git a/tests/Wallabag/CoreBundle/Tools/samples/cyrillic.txt b/tests/Wallabag/CoreBundle/Tools/samples/cyrillic.txt index 7b904da40..90906d04a 100644 --- a/tests/Wallabag/CoreBundle/Tools/samples/cyrillic.txt +++ b/tests/Wallabag/CoreBundle/Tools/samples/cyrillic.txt @@ -1,7 +1,10 @@ +-----CONTENT----- ЛПреЌ ОпсуЌ ЎПлПр сОт аЌет, ех цуЌ ОллуЎ ЎелеМОт, пер регОПМе фацОлОс те. ЕО Ќел вОЎОт саепе ОМтеллегаЌ, яуас ЌаОестатОс цПМстОтуаЌ яуП ат, цОвОбус рефПрЌОЎаМс МецессОтатОбус ОЎ яуО. ИЌпетус тацОЌатес пертОМах аЎ еуЌ. Усу еу легере блаМЎОт. АМ Ќеа трОтаМО Оуварет, ОллуЌ сцаевПла легеМЎПс ат Ќеа, ЎебОтОс ОЌпеЎОт МусяуаЌ ест аЎ. Не ЌаОПруЌ ЌПлестОе цПтОЎОеяуе вОс. ИОсяуе цПМцлуЎатуряуе ЌеО еу, татОПМ цПМсецтетуер еО прП. ЛОбер рОЎеМс ОЎ хас, ОЎ цПМсул сеМсерОт пертОМацОа Ќеа. Ѐацер ЌПлестОае цПЌпрехеМсаЌ аЎ еуЌ, ОМ хОс апеОрОаМ вОвеМЎуЌ. ЯуО ауЎОре епОцуреО ОуЎОцабОт ат, верП хабеП вертереЌ аЎ Оус. БПМПруЌ плацерат ОМ вОс, сеа МП Пцурререт прОМцОпес ОМтерессет, хас ет ЎОцерет ЎОспутаМЎП. ЯуП цу цлОта Пцурререт. СПМет ЌеМаМЎрО ОМ сеа. ЕуЌ те МПМуЌы вертереЌ. ВОрОс еяуОЎеЌ фацОлОсО ет вОЌ, ЎелОцата ОМтеллегат Оус ОМ. ИЎ ЎОцат суЌЌП вОтае вел, алОяуОп Ўелецтус те ЎуП, цу вОх хОМц ЎуОс вОЎОссе. Нец цу фацОлОс урбаМОтас, алОа ОМсПлеМс ассуеверОт прО ут. -Яуаеяуе абхПрреаМт ОМцПррупте Ме сеа, еу еОрЌПЎ еруЎОтО вОх. Вел ПптОПМ трОтаМО цПрруЌпОт те. ППссе сусцОпОт губергреМ ут Ќел, ет еПс ОрОуре ЌеМаМЎрО еффОцОеМЎО. Те сале Мулла цПМсецтетуер сеа, Ќеа Ме прОЌа алОеМуЌ еффОцОаМтур. ПрО ет вПцОбус рефПрЌОЎаМс, теЌпПр албуцОус сеЎ аМ. ЕО утрПяуе вПлуЌус Оус, атяуО цПМгуе МП ЌеО. \ No newline at end of file +Яуаеяуе абхПрреаМт ОМцПррупте Ме сеа, еу еОрЌПЎ еруЎОтО вОх. Вел ПптОПМ трОтаМО цПрруЌпОт те. ППссе сусцОпОт губергреМ ут Ќел, ет еПс ОрОуре ЌеМаМЎрО еффОцОеМЎО. Те сале Мулла цПМсецтетуер сеа, Ќеа Ме прОЌа алОеМуЌ еффОцОаМтур. ПрО ет вПцОбус рефПрЌОЎаМс, теЌпПр албуцОус сеЎ аМ. ЕО утрПяуе вПлуЌус Оус, атяуО цПМгуе МП ЌеО. +-----READING_TIME----- +1 diff --git a/tests/Wallabag/CoreBundle/Tools/samples/greek.txt b/tests/Wallabag/CoreBundle/Tools/samples/greek.txt index 59f15b8bc..f8ade0d7e 100644 --- a/tests/Wallabag/CoreBundle/Tools/samples/greek.txt +++ b/tests/Wallabag/CoreBundle/Tools/samples/greek.txt @@ -1,3 +1,4 @@ +-----CONTENT----- ΛορεΌ ιπσΞΌ Ύολορ σιτ αΌετ, ηασ Μο ΞταΌΞρ qΞαεqΞε ρεπρεηεΜΎΞΜτ. ΝαΌ λατιΜε προΌπτα qΞαερεΜΎΞΌ ιΎ. Νεc ει φαcερ cοΜcλΞΎατΞρqΞε, vολΞπτΞα vολΞπταρια εφφιcιεΜΎι αΎ προ, Με σεα ασσεΜτιορ ΎεφιΜιεβασ. Μεα αγαΌ ειΞσ Ύολορε ετ, ηισ ει cορπορα περφεcτο. VιΟ cιβο ΎελεΜιτ Με, jΞστο ριΎεΜσ οπορτερε σεΎ ιΎ. Ηισ Μισλ ιΞvαρετ γΞβεργρεΜ εΟ. ΕΞΌ ιΌπεΎιτ ΎετραΟιτ ιΜιΌιcΞσ ατ, αλια βλαΜΎιτ ΎΞο εα, Όεα ιλλΞΎ επιcΞρι cοΜσετετΞρ αΎ. ΙλλΞΎ γραεcε ΎελεΜιτι ηισ Μο. Νεc ιΎ ριΎεΜσ εΞισΌοΎ περιcΞλισ, vισ αΎ λαβοραΌΞσ περσεcΞτι. ΙΞσ εα λΞπτατΞΌ αλιqΞαΜΎο ΎισπΞταΜΎο. @@ -6,4 +7,6 @@ CΞ σεΎ αλβΞcιΞσ ποστΞλαΜτ. VιΟ ιΎ ηοΌερο περcιπιτ cοΜcεπταΌ. ΙΜ vιΌ λιβρισ vιΎερερ, εΟ vισ αλιι ερρορ. VιΟ λοβορτισ ασσεΜτιορ cοΜτεΜτιοΜεσ τε, Με ηασ Ύεcορε περcιπιτΞρ. Εστ εΟ ΎισπΞτατιοΜι ΎεφιΜιτιοΜεΌ, qΞοΎ πηαεΎρΞΌ προ εΞ, εΟ ηασ ιΜτεγρε ελιγεΜΎι cοΜσεcτετΞερ. -ΙΞσ Όολλισ ειρΌοΎ Μο, vιΟ ΜοστρΞΌ cοΜσετετΞρ ει. ΙΞΎιcο vερτερεΌ λΞcιλιΞσ qΞι τε, Με προΌπτα ΞτροqΞε αccοΌΌοΎαρε περ. Ίαcετε ΌαΜΎαΌΞσ ηασ εΟ, λιβερ Ύεβετ εΞΌ εΟ, vιΟ ιΎ Ύιcερετ σιγΜιφερΞΌqΞε. Εξ vιΟ vοcεΜτ. \ No newline at end of file +ΙΞσ Όολλισ ειρΌοΎ Μο, vιΟ ΜοστρΞΌ cοΜσετετΞρ ει. ΙΞΎιcο vερτερεΌ λΞcιλιΞσ qΞι τε, Με προΌπτα ΞτροqΞε αccοΌΌοΎαρε περ. Ίαcετε ΌαΜΎαΌΞσ ηασ εΟ, λιβερ Ύεβετ εΞΌ εΟ, vιΟ ιΎ Ύιcερετ σιγΜιφερΞΌqΞε. Εξ vιΟ vοcεΜτ. +-----READING_TIME----- +1 diff --git a/tests/Wallabag/CoreBundle/Tools/samples/japanese.txt b/tests/Wallabag/CoreBundle/Tools/samples/japanese.txt new file mode 100644 index 000000000..013a8d748 --- /dev/null +++ b/tests/Wallabag/CoreBundle/Tools/samples/japanese.txt @@ -0,0 +1,10 @@ +-----CONTENT----- +聞7配なク時初かきぎ觊敎ペ囜鎚芧女ミ将増3郚ゅ芋荷や蚀䌁たげやラ千第ロル䌁族リた期寄け。戊ト理茉コミチヒ芞面だ䌚入テヒロ゜䞀期ナトヒ詊鮮せお倩出䞊ぞる䜓森ヘツノ決垂ね地各ナク匷町ず前目ずたなを掻盎オ携握湯りよ。 + +流ムワ䜜倧犁ヒフ断日ヱ断千ね消諞もずぐろ䞭勧リ配幎リ文7茅ろぞりめ蟺枡フ䞉負安が囜撮ラむム以逃めじット州67棋うきゃ。催キケ者乗フヒ゜ツ染64厎ク捉瀺よぎふら道䞖ぞび属品おく西捕ニレ亀重むフ匏買散ル展五めづっむ鎧属ざごび数開キハツ聞続衚クシタ補球゜り犁源蚗ひれも。 + +季手ッがふ挙思メ勢1䜿すけねげ日熱争らあふか䜍矩゚コ望桑安く決管ヌひ広間キヱ皇北ょはこ逊山ミ攟芋負さぞお故携蚃畑枯ひわン。著支にふみ意豊ラだ球監トクナ銬惚が抱審リヒ劎厚ゅぜひ継貞ミノ果疑文キダ闘府兌ナカシト倚䞍っあ財責゚速蚎埄猶げすぜ。 + +了摘芋いぶころ䌚料ぞゆぱ法利コツハリ統財千りむ䌝幎りぜ提瀟ロ片远ごヌ合䜜むカシニ感山よち真噚敗銙レれさ。芖シ探倧む什69真ケトヱ䟿郜ケホワナ境号ヱカオハ䞀助む関念ろんび幌脚芁だ客投ヱハむ針教ヒノりラ階担うスりね袖陞ょげけ同講ノ料党ダ催宮補ゆ埳就画圧愛め。 +-----READING_TIME----- +1 diff --git a/tests/Wallabag/CoreBundle/Tools/samples/korean.txt b/tests/Wallabag/CoreBundle/Tools/samples/korean.txt new file mode 100644 index 000000000..e3ef2af6b --- /dev/null +++ b/tests/Wallabag/CoreBundle/Tools/samples/korean.txt @@ -0,0 +1,10 @@ +-----CONTENT----- +국군은 국가의 안전볎장곌 국토방위의 신성한 의묎륌 수행핚을 사명윌로 하며, 대통령읎 임시회의 집회륌 요구할 때에는 Ʞ간곌 집회요구의 읎유륌 명시하여알 한닀. 정당의 목적읎나 활동읎 믌죌적 Ʞ볞질서에 위배될 때에는 정부는 헌법재판소에 ê·ž 핎산을 제소할 수 있고. 감사위원은 원장의 제청윌로 대통령읎 임명하고. + +대한믌국의 죌권은 국믌에게 있고, 국회는 국믌의 볎통·평등·직접·비밀선거에 의하여 선출된 국회의원윌로 구성한닀. 국가는 농업 및 얎업을 볎혞·육성하Ʞ 위하여 농·얎쎌종합개발곌 ê·ž 지원등 필요한 계획을 수늜·시행하여알 한닀. 대통령의 임Ʞ연장 또는 쀑임변겜을 위한 헌법개정은 ê·ž 헌법개정 제안 당시의 대통령에 대하여는 횚력읎 없닀. + +국회가 재적의원 곌반수의 찬성윌로 계엄의 핎제륌 요구한 때에는 대통령은 읎륌 핎제하여알 한닀, 선거에 ꎀ한 겜비는 법률읎 정하는 겜우륌 제왞하고는 정당 또는 후볎자에게 부닎시킬 수 없닀. ê·ž 정치적 쀑늜성은 쀀수된닀. 헌법개정안은 국회가 의결한 후 30음 읎낎에 국믌투표에 붙여 국회의원선거권자 곌반수의 투표와 투표자 곌반수의 찬성을 얻얎알 한닀. + +낎부규윚곌 사묎처늬에 ꎀ한 규칙을 제정할 수 있닀. 대통령에 대한 탄핵소추는 국회재적의원 곌반수의 발의와 국회재적의원 3분의 2 읎상의 찬성읎 있얎알 한닀. 대통령은 국가의 원수읎며. 대통령읎 궐위된 때 또는 대통령 당선자가 사망하거나 판결 Ʞ타의 사유로 ê·ž 자격을 상싀한 때에는 60음 읎낎에 후임자륌 선거한닀. +-----READING_TIME----- +2 diff --git a/tests/Wallabag/CoreBundle/Tools/samples/latin.txt b/tests/Wallabag/CoreBundle/Tools/samples/latin.txt index 605cc40ea..279885970 100644 --- a/tests/Wallabag/CoreBundle/Tools/samples/latin.txt +++ b/tests/Wallabag/CoreBundle/Tools/samples/latin.txt @@ -1,3 +1,4 @@ +-----CONTENT----- Lorem ipsum dolor sit amet, pro vivendo oporteat pertinacia ei. Vim fabellas molestiae cu, vel nibh legimus ea, in qui atomorum democritum. Ius ne agam soluta ignota, his sale aperiri complectitur te, omnis volumus accusam an eos. Ut mentitum appetere mel, minim temporibus eloquentiam sea ea. Tation nominati pro ad. Pri eros eloquentiam reformidans ea, et liber epicurei erroribus pro, pri patrioque repudiandae et. Cetero perfecto at eam. Eros hendrerit constituto vix at, brute aperiri adolescens pro eu. Vix lucilius consulatu ei, ullum tantas munere vel in, regione feugiat eligendi at eam. @@ -6,4 +7,6 @@ Eam an lucilius iracundia, audire diceret facilisi his in, ex paulo pertinacia p Nec ut quod probo eligendi, cu dico iriure aperiam vis. Augue causae abhorreant per ut, iriure repudiandae no nam, exerci equidem deleniti nam te. Et duo saperet debitis adipiscing, quo odio audiam no, ex iudico delenit propriae duo. Eu eum eros abhorreant, an tractatos expetendis est. -Vix. \ No newline at end of file +Vix. +-----READING_TIME----- +1 diff --git a/tests/Wallabag/CoreBundle/Twig/WallabagExtensionTest.php b/tests/Wallabag/CoreBundle/Twig/WallabagExtensionTest.php index bb92f7458..39fcec165 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 6e1163c56..816d22f4d 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/CoreBundle/fixtures/tagging_rules_admin.json b/tests/Wallabag/CoreBundle/fixtures/tagging_rules_admin.json new file mode 100644 index 000000000..a54824e26 --- /dev/null +++ b/tests/Wallabag/CoreBundle/fixtures/tagging_rules_admin.json @@ -0,0 +1,4 @@ +[{ + "rule": "title matches \"football\"", + "tags": ["football"] +}] diff --git a/tests/Wallabag/ImportBundle/Command/ImportCommandTest.php b/tests/Wallabag/ImportBundle/Command/ImportCommandTest.php index f95320a4a..8e1c528de 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 b2141c040..b7f6192d3 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/ElcuratorControllerTest.php b/tests/Wallabag/ImportBundle/Controller/ElcuratorControllerTest.php new file mode 100644 index 000000000..b9919f8f5 --- /dev/null +++ b/tests/Wallabag/ImportBundle/Controller/ElcuratorControllerTest.php @@ -0,0 +1,132 @@ +logInAs('admin'); + $client = $this->getClient(); + + $crawler = $client->request('GET', '/import/elcurator'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + $this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count()); + $this->assertSame(1, $crawler->filter('input[type=file]')->count()); + } + + public function testImportElcuratorWithRabbitEnabled() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $client->getContainer()->get('craue_config')->set('import_with_rabbitmq', 1); + + $crawler = $client->request('GET', '/import/elcurator'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + $this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count()); + $this->assertSame(1, $crawler->filter('input[type=file]')->count()); + + $client->getContainer()->get('craue_config')->set('import_with_rabbitmq', 0); + } + + public function testImportElcuratorBadFile() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $crawler = $client->request('GET', '/import/elcurator'); + $form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form(); + + $data = [ + 'upload_import_file[file]' => '', + ]; + + $client->submit($form, $data); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + } + + public function testImportElcuratorWithRedisEnabled() + { + $this->checkRedis(); + $this->logInAs('admin'); + $client = $this->getClient(); + + $client->getContainer()->get('craue_config')->set('import_with_redis', 1); + + $crawler = $client->request('GET', '/import/elcurator'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + $this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count()); + $this->assertSame(1, $crawler->filter('input[type=file]')->count()); + + $form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form(); + + $file = new UploadedFile(__DIR__ . '/../fixtures/elcurator.json', 'elcurator.json'); + + $data = [ + 'upload_import_file[file]' => $file, + ]; + + $client->submit($form, $data); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + + $crawler = $client->followRedirect(); + + $this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text'])); + $this->assertContains('flashes.import.notice.summary', $body[0]); + + $this->assertNotEmpty($client->getContainer()->get('wallabag_core.redis.client')->lpop('wallabag.import.elcurator')); + + $client->getContainer()->get('craue_config')->set('import_with_redis', 0); + } + + public function testImportElcuratorWithFile() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $crawler = $client->request('GET', '/import/elcurator'); + $form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form(); + + $file = new UploadedFile(__DIR__ . '/../fixtures/elcurator.json', 'elcurator.json'); + + $data = [ + 'upload_import_file[file]' => $file, + ]; + + $client->submit($form, $data); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + + $crawler = $client->followRedirect(); + + $this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text'])); + $this->assertContains('flashes.import.notice.summary', $body[0]); + + $content = $client->getContainer() + ->get('doctrine.orm.entity_manager') + ->getRepository('WallabagCoreBundle:Entry') + ->findByUrlAndUserId( + 'https://devblog.lexik.fr/git/qualite-de-code-integration-de-php-git-hooks-dans-symfony2-2842', + $this->getLoggedInUserId() + ); + + $this->assertInstanceOf('Wallabag\CoreBundle\Entity\Entry', $content); + + $this->assertSame('Qualité de code - Intégration de php-git-hooks dans Symfony2 - Experts Symfony et Drupal - Lexik', $content->getTitle()); + $this->assertSame('2015-09-09', $content->getCreatedAt()->format('Y-m-d')); + $this->assertTrue($content->isStarred(), 'Entry is starred'); + + $tags = $content->getTags(); + $this->assertContains('tag1', $tags, 'It includes the "tag1" tag'); + $this->assertContains('tag2', $tags, 'It includes the "tag2" tag'); + } +} diff --git a/tests/Wallabag/ImportBundle/Controller/FirefoxControllerTest.php b/tests/Wallabag/ImportBundle/Controller/FirefoxControllerTest.php index dc5ed6d0c..3e64f2e56 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/ImportControllerTest.php b/tests/Wallabag/ImportBundle/Controller/ImportControllerTest.php index e1ec7c657..f0edb78de 100644 --- a/tests/Wallabag/ImportBundle/Controller/ImportControllerTest.php +++ b/tests/Wallabag/ImportBundle/Controller/ImportControllerTest.php @@ -24,6 +24,6 @@ class ImportControllerTest extends WallabagCoreTestCase $crawler = $client->request('GET', '/import/'); $this->assertSame(200, $client->getResponse()->getStatusCode()); - $this->assertSame(8, $crawler->filter('blockquote')->count()); + $this->assertSame(9, $crawler->filter('blockquote')->count()); } } diff --git a/tests/Wallabag/ImportBundle/Controller/InstapaperControllerTest.php b/tests/Wallabag/ImportBundle/Controller/InstapaperControllerTest.php index 7390fa884..05347767d 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 80819f457..963759b1e 100644 --- a/tests/Wallabag/ImportBundle/Controller/PinboardControllerTest.php +++ b/tests/Wallabag/ImportBundle/Controller/PinboardControllerTest.php @@ -121,13 +121,13 @@ class PinboardControllerTest extends WallabagCoreTestCase $this->assertInstanceOf('Wallabag\CoreBundle\Entity\Entry', $content); $this->assertNotEmpty($content->getMimetype(), 'Mimetype for https://ma.ttias.be is ok'); $this->assertNotEmpty($content->getPreviewPicture(), 'Preview picture for https://ma.ttias.be is ok'); - $this->assertNotEmpty($content->getLanguage(), 'Language for https://ma.ttias.be is ok'); + $this->assertNull($content->getLanguage(), 'Language for https://ma.ttias.be is null'); $tags = $content->getTags(); $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 5619659ad..4f2f40536 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 c67941a71..2a8e7c899 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 822656ba4..b606e26ae 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/ImportBundle/Import/PocketImportTest.php b/tests/Wallabag/ImportBundle/Import/PocketImportTest.php index baa5d9057..40e1626ba 100644 --- a/tests/Wallabag/ImportBundle/Import/PocketImportTest.php +++ b/tests/Wallabag/ImportBundle/Import/PocketImportTest.php @@ -2,10 +2,8 @@ namespace Tests\Wallabag\ImportBundle\Import; -use GuzzleHttp\Client; -use GuzzleHttp\Message\Response; -use GuzzleHttp\Stream\Stream; -use GuzzleHttp\Subscriber\Mock; +use GuzzleHttp\Psr7\Response; +use Http\Mock\Client as HttpMockClient; use M6Web\Component\RedisMock\RedisMockFactory; use Monolog\Handler\TestHandler; use Monolog\Logger; @@ -38,16 +36,11 @@ class PocketImportTest extends TestCase public function testOAuthRequest() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['code' => 'wunderbar_code']))), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], json_encode(['code' => 'wunderbar_code']))); $pocketImport = $this->getPocketImport(); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $code = $pocketImport->getRequestToken('http://0.0.0.0/redirect'); @@ -56,16 +49,11 @@ class PocketImportTest extends TestCase public function testOAuthRequestBadResponse() { - $client = new Client(); - - $mock = new Mock([ - new Response(403), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(403)); $pocketImport = $this->getPocketImport(); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $code = $pocketImport->getRequestToken('http://0.0.0.0/redirect'); @@ -78,16 +66,11 @@ class PocketImportTest extends TestCase public function testOAuthAuthorize() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], json_encode(['access_token' => 'wunderbar_token']))); $pocketImport = $this->getPocketImport(); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $res = $pocketImport->authorize('wunderbar_code'); @@ -97,16 +80,11 @@ class PocketImportTest extends TestCase public function testOAuthAuthorizeBadResponse() { - $client = new Client(); - - $mock = new Mock([ - new Response(403), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(403)); $pocketImport = $this->getPocketImport(); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $res = $pocketImport->authorize('wunderbar_code'); @@ -122,94 +100,90 @@ class PocketImportTest extends TestCase */ public function testImport() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))), - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(' - { - "status": 1, - "list": { - "229279689": { - "item_id": "229279689", - "resolved_id": "229279689", - "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", - "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland", - "favorite": "1", - "status": "1", - "time_added": "1473020899", - "time_updated": "1473020899", - "time_read": "0", - "time_favorited": "0", - "sort_id": 0, - "resolved_title": "The Massive Ryder Cup Preview", - "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", - "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.", - "is_article": "1", - "is_index": "0", - "has_video": "1", - "has_image": "1", - "word_count": "3197", - "images": { - "1": { - "item_id": "229279689", - "image_id": "1", - "src": "http://a.espncdn.com/combiner/i?img=/photo/2012/0927/grant_g_ryder_cr_640.jpg&w=640&h=360", - "width": "0", - "height": "0", - "credit": "Jamie Squire/Getty Images", - "caption": "" - } - }, - "videos": { - "1": { - "item_id": "229279689", - "video_id": "1", - "src": "http://www.youtube.com/v/Er34PbFkVGk?version=3&hl=en_US&rel=0", - "width": "420", - "height": "315", - "type": "1", - "vid": "Er34PbFkVGk" - } - }, - "tags": { - "grantland": { - "item_id": "1147652870", - "tag": "grantland" - }, - "Ryder Cup": { - "item_id": "1147652870", - "tag": "Ryder Cup" - } + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], json_encode(['access_token' => 'wunderbar_token']))); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], <<<'JSON' + { + "status": 1, + "list": { + "229279689": { + "item_id": "229279689", + "resolved_id": "229279689", + "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", + "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland", + "favorite": "1", + "status": "1", + "time_added": "1473020899", + "time_updated": "1473020899", + "time_read": "0", + "time_favorited": "0", + "sort_id": 0, + "resolved_title": "The Massive Ryder Cup Preview", + "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", + "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.", + "is_article": "1", + "is_index": "0", + "has_video": "1", + "has_image": "1", + "word_count": "3197", + "images": { + "1": { + "item_id": "229279689", + "image_id": "1", + "src": "http://a.espncdn.com/combiner/i?img=/photo/2012/0927/grant_g_ryder_cr_640.jpg&w=640&h=360", + "width": "0", + "height": "0", + "credit": "Jamie Squire/Getty Images", + "caption": "" } }, - "229279690": { - "item_id": "229279689", - "resolved_id": "229279689", - "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", - "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland", - "favorite": "1", - "status": "1", - "time_added": "1473020899", - "time_updated": "1473020899", - "time_read": "0", - "time_favorited": "0", - "sort_id": 1, - "resolved_title": "The Massive Ryder Cup Preview", - "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", - "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.", - "is_article": "1", - "is_index": "0", - "has_video": "0", - "has_image": "0", - "word_count": "3197" + "videos": { + "1": { + "item_id": "229279689", + "video_id": "1", + "src": "http://www.youtube.com/v/Er34PbFkVGk?version=3&hl=en_US&rel=0", + "width": "420", + "height": "315", + "type": "1", + "vid": "Er34PbFkVGk" + } + }, + "tags": { + "grantland": { + "item_id": "1147652870", + "tag": "grantland" + }, + "Ryder Cup": { + "item_id": "1147652870", + "tag": "Ryder Cup" + } } + }, + "229279690": { + "item_id": "229279689", + "resolved_id": "229279689", + "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", + "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland", + "favorite": "1", + "status": "1", + "time_added": "1473020899", + "time_updated": "1473020899", + "time_read": "0", + "time_favorited": "0", + "sort_id": 1, + "resolved_title": "The Massive Ryder Cup Preview", + "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", + "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.", + "is_article": "1", + "is_index": "0", + "has_video": "0", + "has_image": "0", + "word_count": "3197" } } - ')), - ]); - - $client->getEmitter()->attach($mock); + } +JSON +)); $pocketImport = $this->getPocketImport('ConsumerKey', 1); @@ -226,6 +200,13 @@ class PocketImportTest extends TestCase ->method('getRepository') ->willReturn($entryRepo); + $this->em + ->expects($this->any()) + ->method('persist') + ->with($this->callback(function ($persistedEntry) { + return $persistedEntry->isArchived() && $persistedEntry->isStarred(); + })); + $entry = new Entry($this->user); $this->contentProxy @@ -233,7 +214,7 @@ class PocketImportTest extends TestCase ->method('updateEntry') ->willReturn($entry); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $pocketImport->authorize('wunderbar_code'); $res = $pocketImport->import(); @@ -247,56 +228,52 @@ class PocketImportTest extends TestCase */ public function testImportAndMarkAllAsRead() { - $client = new Client(); - - $mock = new Mock([ - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))), - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(' - { - "status": 1, - "list": { - "229279689": { - "item_id": "229279689", - "resolved_id": "229279689", - "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", - "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland", - "favorite": "1", - "status": "1", - "time_added": "1473020899", - "time_updated": "1473020899", - "time_read": "0", - "time_favorited": "0", - "sort_id": 0, - "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.", - "is_article": "1", - "has_video": "1", - "has_image": "1", - "word_count": "3197" - }, - "229279690": { - "item_id": "229279689", - "resolved_id": "229279689", - "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview/2", - "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland", - "favorite": "1", - "status": "0", - "time_added": "1473020899", - "time_updated": "1473020899", - "time_read": "0", - "time_favorited": "0", - "sort_id": 1, - "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.", - "is_article": "1", - "has_video": "0", - "has_image": "0", - "word_count": "3197" - } + $httpMockClient = new HttpMockClient(); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], json_encode(['access_token' => 'wunderbar_token']))); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], <<<'JSON' + { + "status": 1, + "list": { + "229279689": { + "item_id": "229279689", + "resolved_id": "229279689", + "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview", + "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland", + "favorite": "1", + "status": "1", + "time_added": "1473020899", + "time_updated": "1473020899", + "time_read": "0", + "time_favorited": "0", + "sort_id": 0, + "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.", + "is_article": "1", + "has_video": "1", + "has_image": "1", + "word_count": "3197" + }, + "229279690": { + "item_id": "229279689", + "resolved_id": "229279689", + "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview/2", + "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland", + "favorite": "1", + "status": "0", + "time_added": "1473020899", + "time_updated": "1473020899", + "time_read": "0", + "time_favorited": "0", + "sort_id": 1, + "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.", + "is_article": "1", + "has_video": "0", + "has_image": "0", + "word_count": "3197" } } - ')), - ]); - - $client->getEmitter()->attach($mock); + } +JSON +)); $pocketImport = $this->getPocketImport('ConsumerKey', 2); @@ -328,7 +305,7 @@ class PocketImportTest extends TestCase ->method('updateEntry') ->willReturn($entry); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $pocketImport->authorize('wunderbar_code'); $res = $pocketImport->setMarkAsRead(true)->import(); @@ -342,7 +319,7 @@ class PocketImportTest extends TestCase */ public function testImportWithRabbit() { - $client = new Client(); + $httpMockClient = new HttpMockClient(); $body = <<<'JSON' { @@ -367,19 +344,16 @@ class PocketImportTest extends TestCase } JSON; - $mock = new Mock([ - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))), - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(' - { - "status": 1, - "list": { - "229279690": ' . $body . ' - } + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], json_encode(['access_token' => 'wunderbar_token']))); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], <<getEmitter()->attach($mock); + } +JSON + )); $pocketImport = $this->getPocketImport(); @@ -413,7 +387,7 @@ JSON; ->method('publish') ->with(json_encode($bodyAsArray)); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $pocketImport->setProducer($producer); $pocketImport->authorize('wunderbar_code'); @@ -428,7 +402,7 @@ JSON; */ public function testImportWithRedis() { - $client = new Client(); + $httpMockClient = new HttpMockClient(); $body = <<<'JSON' { @@ -453,19 +427,16 @@ JSON; } JSON; - $mock = new Mock([ - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))), - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(' - { - "status": 1, - "list": { - "229279690": ' . $body . ' - } + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], json_encode(['access_token' => 'wunderbar_token']))); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], <<getEmitter()->attach($mock); + } +JSON + )); $pocketImport = $this->getPocketImport(); @@ -492,7 +463,7 @@ JSON; $queue = new RedisQueue($redisMock, 'pocket'); $producer = new Producer($queue); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $pocketImport->setProducer($producer); $pocketImport->authorize('wunderbar_code'); @@ -506,17 +477,13 @@ JSON; public function testImportBadResponse() { - $client = new Client(); + $httpMockClient = new HttpMockClient(); - $mock = new Mock([ - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))), - new Response(403), - ]); - - $client->getEmitter()->attach($mock); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], json_encode(['access_token' => 'wunderbar_token']))); + $httpMockClient->addResponse(new Response(403)); $pocketImport = $this->getPocketImport(); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $pocketImport->authorize('wunderbar_code'); $res = $pocketImport->import(); @@ -530,25 +497,23 @@ JSON; public function testImportWithExceptionFromGraby() { - $client = new Client(); + $httpMockClient = new HttpMockClient(); - $mock = new Mock([ - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))), - new Response(200, ['Content-Type' => 'application/json'], Stream::factory(' - { - "status": 1, - "list": { - "229279689": { - "status": "1", - "favorite": "1", - "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview" - } + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], json_encode(['access_token' => 'wunderbar_token']))); + $httpMockClient->addResponse(new Response(200, ['Content-Type' => 'application/json'], <<<'JSON' + { + "status": 1, + "list": { + "229279689": { + "status": "1", + "favorite": "1", + "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview" } } - ')), - ]); - - $client->getEmitter()->attach($mock); + } + +JSON + )); $pocketImport = $this->getPocketImport('ConsumerKey', 1); @@ -572,7 +537,7 @@ JSON; ->method('updateEntry') ->will($this->throwException(new \Exception())); - $pocketImport->setClient($client); + $pocketImport->setClient($httpMockClient); $pocketImport->authorize('wunderbar_code'); $res = $pocketImport->import(); diff --git a/tests/Wallabag/ImportBundle/fixtures/elcurator.json b/tests/Wallabag/ImportBundle/fixtures/elcurator.json new file mode 100644 index 000000000..f6fb2dfb6 --- /dev/null +++ b/tests/Wallabag/ImportBundle/fixtures/elcurator.json @@ -0,0 +1,13 @@ +[ + { + "created_at": "2015-09-09 11:10:32 UTC", + "title": "Qualité de code - Intégration de php-git-hooks dans Symfony2 - Experts Symfony et Drupal - Lexik", + "url": "https://devblog.lexik.fr/git/qualite-de-code-integration-de-php-git-hooks-dans-symfony2-2842", + "description": null, + "tags": [ + "tag1", + "tag2" + ], + "is_saved": true + } +] diff --git a/tests/Wallabag/UserBundle/Controller/ManageControllerTest.php b/tests/Wallabag/UserBundle/Controller/ManageControllerTest.php index adc2cf093..f44e6fbf6 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); + $config->setReadingSpeed(200); $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 aa1760688..4f93a92ca 100644 --- a/tests/Wallabag/UserBundle/Mailer/AuthCodeMailerTest.php +++ b/tests/Wallabag/UserBundle/Mailer/AuthCodeMailerTest.php @@ -3,25 +3,11 @@ namespace Tests\Wallabag\UserBundle\Mailer; use PHPUnit\Framework\TestCase; +use Twig\Environment; +use Twig\Loader\ArrayLoader; 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; @@ -43,13 +29,13 @@ class AuthCodeMailerTest extends TestCase {% block body_text %}text body {{ support_url }}{% endblock %} TWIG; - $this->twig = new \Twig_Environment(new \Twig_Loader_Array(['WallabagUserBundle:TwoFactor:email_auth_code.html.twig' => $twigTemplate])); + $this->twig = new Environment(new ArrayLoader(['WallabagUserBundle:TwoFactor:email_auth_code.html.twig' => $twigTemplate])); } 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 000000000..53f240a1d --- /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 4c2c4650e..3427e133e 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 8456754d5..57e1a433f 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 8c1ed86bd..4ebd522d4 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;font-display:block;src:url(fonts/MaterialIcons-Regular.eot);src:local("\263A"),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"}.material-icons._10k:before{content:"\E951"}.material-icons._10mp:before{content:"\E952"}.material-icons._11mp:before{content:"\E953"}.material-icons._12mp:before{content:"\E954"}.material-icons._13mp:before{content:"\E955"}.material-icons._14mp:before{content:"\E956"}.material-icons._15mp:before{content:"\E957"}.material-icons._16mp:before{content:"\E958"}.material-icons._17mp:before{content:"\E959"}.material-icons._18mp:before{content:"\E95A"}.material-icons._19mp:before{content:"\E95B"}.material-icons._1k:before{content:"\E95C"}.material-icons._1k_plus:before{content:"\E95D"}.material-icons._20mp:before{content:"\E95E"}.material-icons._21mp:before{content:"\E95F"}.material-icons._22mp:before{content:"\E960"}.material-icons._23mp:before{content:"\E961"}.material-icons._24mp:before{content:"\E962"}.material-icons._2k:before{content:"\E963"}.material-icons._2k_plus:before{content:"\E964"}.material-icons._2mp:before{content:"\E965"}.material-icons._360:before{content:"\E577"}.material-icons._3d_rotation:before{content:"\E84D"}.material-icons._3k:before{content:"\E966"}.material-icons._3k_plus:before{content:"\E967"}.material-icons._3mp:before{content:"\E968"}.material-icons._4k:before{content:"\E072"}.material-icons._4k_plus:before{content:"\E969"}.material-icons._4mp:before{content:"\E96A"}.material-icons._5k:before{content:"\E96B"}.material-icons._5k_plus:before{content:"\E96C"}.material-icons._5mp:before{content:"\E96D"}.material-icons._6k:before{content:"\E96E"}.material-icons._6k_plus:before{content:"\E96F"}.material-icons._6mp:before{content:"\E970"}.material-icons._7k:before{content:"\E971"}.material-icons._7k_plus:before{content:"\E972"}.material-icons._7mp:before{content:"\E973"}.material-icons._8k:before{content:"\E974"}.material-icons._8k_plus:before{content:"\E975"}.material-icons._8mp:before{content:"\E976"}.material-icons._9k:before{content:"\E977"}.material-icons._9k_plus:before{content:"\E978"}.material-icons._9mp:before{content:"\E979"}.material-icons.ac_unit:before{content:"\EB3B"}.material-icons.access_alarm:before{content:"\E190"}.material-icons.access_alarms:before{content:"\E191"}.material-icons.access_time:before{content:"\E192"}.material-icons.accessibility:before{content:"\E84E"}.material-icons.accessibility_new:before{content:"\E92C"}.material-icons.accessible:before{content:"\E914"}.material-icons.accessible_forward:before{content:"\E934"}.material-icons.account_balance:before{content:"\E84F"}.material-icons.account_balance_wallet:before{content:"\E850"}.material-icons.account_box:before{content:"\E851"}.material-icons.account_circle:before{content:"\E853"}.material-icons.account_tree:before{content:"\E97A"}.material-icons.adb:before{content:"\E60E"}.material-icons.add:before{content:"\E145"}.material-icons.add_a_photo:before{content:"\E439"}.material-icons.add_alarm:before{content:"\E193"}.material-icons.add_alert:before{content:"\E003"}.material-icons.add_box:before{content:"\E146"}.material-icons.add_call:before{content:"\E0E8"}.material-icons.add_chart:before{content:"\E97B"}.material-icons.add_circle:before{content:"\E147"}.material-icons.add_circle_outline:before{content:"\E148"}.material-icons.add_comment:before{content:"\E266"}.material-icons.add_ic_call:before{content:"\E97C"}.material-icons.add_link:before{content:"\E178"}.material-icons.add_location:before{content:"\E567"}.material-icons.add_moderator:before{content:"\E97D"}.material-icons.add_photo_alternate:before{content:"\E43E"}.material-icons.add_shopping_cart:before{content:"\E854"}.material-icons.add_to_home_screen:before{content:"\E1FE"}.material-icons.add_to_photos:before{content:"\E39D"}.material-icons.add_to_queue:before{content:"\E05C"}.material-icons.adjust:before{content:"\E39E"}.material-icons.airline_seat_flat:before{content:"\E630"}.material-icons.airline_seat_flat_angled:before{content:"\E631"}.material-icons.airline_seat_individual_suite:before{content:"\E632"}.material-icons.airline_seat_legroom_extra:before{content:"\E633"}.material-icons.airline_seat_legroom_normal:before{content:"\E634"}.material-icons.airline_seat_legroom_reduced:before{content:"\E635"}.material-icons.airline_seat_recline_extra:before{content:"\E636"}.material-icons.airline_seat_recline_normal:before{content:"\E637"}.material-icons.airplanemode_active:before{content:"\E195"}.material-icons.airplanemode_inactive:before,.material-icons.airplanemode_off:before{content:"\E194"}.material-icons.airplanemode_on:before{content:"\E195"}.material-icons.airplay:before{content:"\E055"}.material-icons.airport_shuttle:before{content:"\EB3C"}.material-icons.alarm:before{content:"\E855"}.material-icons.alarm_add:before{content:"\E856"}.material-icons.alarm_off:before{content:"\E857"}.material-icons.alarm_on:before{content:"\E858"}.material-icons.album:before{content:"\E019"}.material-icons.all_inbox:before{content:"\E97F"}.material-icons.all_inclusive:before{content:"\EB3D"}.material-icons.all_out:before{content:"\E90B"}.material-icons.alternate_email:before{content:"\E0E6"}.material-icons.amp_stories:before{content:"\EA13"}.material-icons.android:before{content:"\E859"}.material-icons.announcement:before{content:"\E85A"}.material-icons.apartment:before{content:"\EA40"}.material-icons.approval:before{content:"\E982"}.material-icons.apps:before{content:"\E5C3"}.material-icons.archive:before{content:"\E149"}.material-icons.arrow_back:before{content:"\E5C4"}.material-icons.arrow_back_ios:before{content:"\E5E0"}.material-icons.arrow_downward:before{content:"\E5DB"}.material-icons.arrow_drop_down:before{content:"\E5C5"}.material-icons.arrow_drop_down_circle:before{content:"\E5C6"}.material-icons.arrow_drop_up:before{content:"\E5C7"}.material-icons.arrow_forward:before{content:"\E5C8"}.material-icons.arrow_forward_ios:before{content:"\E5E1"}.material-icons.arrow_left:before{content:"\E5DE"}.material-icons.arrow_right:before{content:"\E5DF"}.material-icons.arrow_right_alt:before{content:"\E941"}.material-icons.arrow_upward:before{content:"\E5D8"}.material-icons.art_track:before{content:"\E060"}.material-icons.aspect_ratio:before{content:"\E85B"}.material-icons.assessment:before{content:"\E85C"}.material-icons.assignment:before{content:"\E85D"}.material-icons.assignment_ind:before{content:"\E85E"}.material-icons.assignment_late:before{content:"\E85F"}.material-icons.assignment_return:before{content:"\E860"}.material-icons.assignment_returned:before{content:"\E861"}.material-icons.assignment_turned_in:before{content:"\E862"}.material-icons.assistant:before{content:"\E39F"}.material-icons.assistant_direction:before{content:"\E988"}.material-icons.assistant_navigation:before{content:"\E989"}.material-icons.assistant_photo:before{content:"\E3A0"}.material-icons.atm:before{content:"\E573"}.material-icons.attach_file:before{content:"\E226"}.material-icons.attach_money:before{content:"\E227"}.material-icons.attachment:before{content:"\E2BC"}.material-icons.attractions:before{content:"\EA52"}.material-icons.audiotrack:before{content:"\E3A1"}.material-icons.autorenew:before{content:"\E863"}.material-icons.av_timer:before{content:"\E01B"}.material-icons.backspace:before{content:"\E14A"}.material-icons.backup:before{content:"\E864"}.material-icons.badge:before{content:"\EA67"}.material-icons.bakery_dining:before{content:"\EA53"}.material-icons.ballot:before{content:"\E172"}.material-icons.bar_chart:before{content:"\E26B"}.material-icons.bathtub:before{content:"\EA41"}.material-icons.battery_alert:before{content:"\E19C"}.material-icons.battery_charging_full:before{content:"\E1A3"}.material-icons.battery_full:before{content:"\E1A4"}.material-icons.battery_std:before{content:"\E1A5"}.material-icons.battery_unknown:before{content:"\E1A6"}.material-icons.beach_access:before{content:"\EB3E"}.material-icons.beenhere:before{content:"\E52D"}.material-icons.block:before{content:"\E14B"}.material-icons.bluetooth:before{content:"\E1A7"}.material-icons.bluetooth_audio:before{content:"\E60F"}.material-icons.bluetooth_connected:before{content:"\E1A8"}.material-icons.bluetooth_disabled:before{content:"\E1A9"}.material-icons.bluetooth_searching:before{content:"\E1AA"}.material-icons.blur_circular:before{content:"\E3A2"}.material-icons.blur_linear:before{content:"\E3A3"}.material-icons.blur_off:before{content:"\E3A4"}.material-icons.blur_on:before{content:"\E3A5"}.material-icons.bolt:before{content:"\EA0B"}.material-icons.book:before{content:"\E865"}.material-icons.bookmark:before{content:"\E866"}.material-icons.bookmark_border:before,.material-icons.bookmark_outline:before{content:"\E867"}.material-icons.bookmarks:before{content:"\E98B"}.material-icons.border_all:before{content:"\E228"}.material-icons.border_bottom:before{content:"\E229"}.material-icons.border_clear:before{content:"\E22A"}.material-icons.border_color:before{content:"\E22B"}.material-icons.border_horizontal:before{content:"\E22C"}.material-icons.border_inner:before{content:"\E22D"}.material-icons.border_left:before{content:"\E22E"}.material-icons.border_outer:before{content:"\E22F"}.material-icons.border_right:before{content:"\E230"}.material-icons.border_style:before{content:"\E231"}.material-icons.border_top:before{content:"\E232"}.material-icons.border_vertical:before{content:"\E233"}.material-icons.branding_watermark:before{content:"\E06B"}.material-icons.breakfast_dining:before{content:"\EA54"}.material-icons.brightness_1:before{content:"\E3A6"}.material-icons.brightness_2:before{content:"\E3A7"}.material-icons.brightness_3:before{content:"\E3A8"}.material-icons.brightness_4:before{content:"\E3A9"}.material-icons.brightness_5:before{content:"\E3AA"}.material-icons.brightness_6:before{content:"\E3AB"}.material-icons.brightness_7:before{content:"\E3AC"}.material-icons.brightness_auto:before{content:"\E1AB"}.material-icons.brightness_high:before{content:"\E1AC"}.material-icons.brightness_low:before{content:"\E1AD"}.material-icons.brightness_medium:before{content:"\E1AE"}.material-icons.broken_image:before{content:"\E3AD"}.material-icons.brunch_dining:before{content:"\EA73"}.material-icons.brush:before{content:"\E3AE"}.material-icons.bubble_chart:before{content:"\E6DD"}.material-icons.bug_report:before{content:"\E868"}.material-icons.build:before{content:"\E869"}.material-icons.burst_mode:before{content:"\E43C"}.material-icons.bus_alert:before{content:"\E98F"}.material-icons.business:before{content:"\E0AF"}.material-icons.business_center:before{content:"\EB3F"}.material-icons.cached:before{content:"\E86A"}.material-icons.cake:before{content:"\E7E9"}.material-icons.calendar_today:before{content:"\E935"}.material-icons.calendar_view_day:before{content:"\E936"}.material-icons.call:before{content:"\E0B0"}.material-icons.call_end:before{content:"\E0B1"}.material-icons.call_made:before{content:"\E0B2"}.material-icons.call_merge:before{content:"\E0B3"}.material-icons.call_missed:before{content:"\E0B4"}.material-icons.call_missed_outgoing:before{content:"\E0E4"}.material-icons.call_received:before{content:"\E0B5"}.material-icons.call_split:before{content:"\E0B6"}.material-icons.call_to_action:before{content:"\E06C"}.material-icons.camera:before{content:"\E3AF"}.material-icons.camera_alt:before{content:"\E3B0"}.material-icons.camera_enhance:before{content:"\E8FC"}.material-icons.camera_front:before{content:"\E3B1"}.material-icons.camera_rear:before{content:"\E3B2"}.material-icons.camera_roll:before{content:"\E3B3"}.material-icons.cancel:before{content:"\E5C9"}.material-icons.cancel_presentation:before{content:"\E0E9"}.material-icons.cancel_schedule_send:before{content:"\EA39"}.material-icons.car_rental:before{content:"\EA55"}.material-icons.car_repair:before{content:"\EA56"}.material-icons.card_giftcard:before{content:"\E8F6"}.material-icons.card_membership:before{content:"\E8F7"}.material-icons.card_travel:before{content:"\E8F8"}.material-icons.cases:before{content:"\E992"}.material-icons.casino:before{content:"\EB40"}.material-icons.cast:before{content:"\E307"}.material-icons.cast_connected:before{content:"\E308"}.material-icons.category:before{content:"\E574"}.material-icons.celebration:before{content:"\EA65"}.material-icons.cell_wifi:before{content:"\E0EC"}.material-icons.center_focus_strong:before{content:"\E3B4"}.material-icons.center_focus_weak:before{content:"\E3B5"}.material-icons.change_history:before{content:"\E86B"}.material-icons.chat:before{content:"\E0B7"}.material-icons.chat_bubble:before{content:"\E0CA"}.material-icons.chat_bubble_outline:before{content:"\E0CB"}.material-icons.check:before{content:"\E5CA"}.material-icons.check_box:before{content:"\E834"}.material-icons.check_box_outline_blank:before{content:"\E835"}.material-icons.check_circle:before{content:"\E86C"}.material-icons.check_circle_outline:before{content:"\E92D"}.material-icons.chevron_left:before{content:"\E5CB"}.material-icons.chevron_right:before{content:"\E5CC"}.material-icons.child_care:before{content:"\EB41"}.material-icons.child_friendly:before{content:"\EB42"}.material-icons.chrome_reader_mode:before{content:"\E86D"}.material-icons.circle_notifications:before{content:"\E994"}.material-icons.class:before{content:"\E86E"}.material-icons.clear:before{content:"\E14C"}.material-icons.clear_all:before{content:"\E0B8"}.material-icons.close:before{content:"\E5CD"}.material-icons.closed_caption:before{content:"\E01C"}.material-icons.closed_caption_off:before{content:"\E996"}.material-icons.cloud:before{content:"\E2BD"}.material-icons.cloud_circle:before{content:"\E2BE"}.material-icons.cloud_done:before{content:"\E2BF"}.material-icons.cloud_download:before{content:"\E2C0"}.material-icons.cloud_off:before{content:"\E2C1"}.material-icons.cloud_queue:before{content:"\E2C2"}.material-icons.cloud_upload:before{content:"\E2C3"}.material-icons.code:before{content:"\E86F"}.material-icons.collections:before{content:"\E3B6"}.material-icons.collections_bookmark:before{content:"\E431"}.material-icons.color_lens:before{content:"\E3B7"}.material-icons.colorize:before{content:"\E3B8"}.material-icons.comment:before{content:"\E0B9"}.material-icons.commute:before{content:"\E940"}.material-icons.compare:before{content:"\E3B9"}.material-icons.compare_arrows:before{content:"\E915"}.material-icons.compass_calibration:before{content:"\E57C"}.material-icons.compress:before{content:"\E94D"}.material-icons.computer:before{content:"\E30A"}.material-icons.confirmation_num:before,.material-icons.confirmation_number:before{content:"\E638"}.material-icons.connected_tv:before{content:"\E998"}.material-icons.contact_mail:before{content:"\E0D0"}.material-icons.contact_phone:before{content:"\E0CF"}.material-icons.contact_support:before{content:"\E94C"}.material-icons.contactless:before{content:"\EA71"}.material-icons.contacts:before{content:"\E0BA"}.material-icons.content_copy:before{content:"\E14D"}.material-icons.content_cut:before{content:"\E14E"}.material-icons.content_paste:before{content:"\E14F"}.material-icons.control_camera:before{content:"\E074"}.material-icons.control_point:before{content:"\E3BA"}.material-icons.control_point_duplicate:before{content:"\E3BB"}.material-icons.copyright:before{content:"\E90C"}.material-icons.create:before{content:"\E150"}.material-icons.create_new_folder:before{content:"\E2CC"}.material-icons.credit_card:before{content:"\E870"}.material-icons.crop:before{content:"\E3BE"}.material-icons.crop_16_9:before{content:"\E3BC"}.material-icons.crop_3_2:before{content:"\E3BD"}.material-icons.crop_5_4:before{content:"\E3BF"}.material-icons.crop_7_5:before{content:"\E3C0"}.material-icons.crop_din:before{content:"\E3C1"}.material-icons.crop_free:before{content:"\E3C2"}.material-icons.crop_landscape:before{content:"\E3C3"}.material-icons.crop_original:before{content:"\E3C4"}.material-icons.crop_portrait:before{content:"\E3C5"}.material-icons.crop_rotate:before{content:"\E437"}.material-icons.crop_square:before{content:"\E3C6"}.material-icons.dangerous:before{content:"\E99A"}.material-icons.dashboard:before{content:"\E871"}.material-icons.dashboard_customize:before{content:"\E99B"}.material-icons.data_usage:before{content:"\E1AF"}.material-icons.date_range:before{content:"\E916"}.material-icons.deck:before{content:"\EA42"}.material-icons.dehaze:before{content:"\E3C7"}.material-icons.delete:before{content:"\E872"}.material-icons.delete_forever:before{content:"\E92B"}.material-icons.delete_outline:before{content:"\E92E"}.material-icons.delete_sweep:before{content:"\E16C"}.material-icons.delivery_dining:before{content:"\EA72"}.material-icons.departure_board:before{content:"\E576"}.material-icons.description:before{content:"\E873"}.material-icons.desktop_access_disabled:before{content:"\E99D"}.material-icons.desktop_mac:before{content:"\E30B"}.material-icons.desktop_windows:before{content:"\E30C"}.material-icons.details:before{content:"\E3C8"}.material-icons.developer_board:before{content:"\E30D"}.material-icons.developer_mode:before{content:"\E1B0"}.material-icons.device_hub:before{content:"\E335"}.material-icons.device_thermostat:before{content:"\E1FF"}.material-icons.device_unknown:before{content:"\E339"}.material-icons.devices:before{content:"\E1B1"}.material-icons.devices_other:before{content:"\E337"}.material-icons.dialer_sip:before{content:"\E0BB"}.material-icons.dialpad:before{content:"\E0BC"}.material-icons.dinner_dining:before{content:"\EA57"}.material-icons.directions:before{content:"\E52E"}.material-icons.directions_bike:before{content:"\E52F"}.material-icons.directions_boat:before{content:"\E532"}.material-icons.directions_bus:before{content:"\E530"}.material-icons.directions_car:before{content:"\E531"}.material-icons.directions_ferry:before{content:"\E532"}.material-icons.directions_railway:before{content:"\E534"}.material-icons.directions_run:before{content:"\E566"}.material-icons.directions_subway:before{content:"\E533"}.material-icons.directions_train:before{content:"\E534"}.material-icons.directions_transit:before{content:"\E535"}.material-icons.directions_walk:before{content:"\E536"}.material-icons.disc_full:before{content:"\E610"}.material-icons.dnd_forwardslash:before{content:"\E611"}.material-icons.dns:before{content:"\E875"}.material-icons.do_not_disturb:before{content:"\E612"}.material-icons.do_not_disturb_alt:before{content:"\E611"}.material-icons.do_not_disturb_off:before{content:"\E643"}.material-icons.do_not_disturb_on:before{content:"\E644"}.material-icons.dock:before{content:"\E30E"}.material-icons.domain:before{content:"\E7EE"}.material-icons.domain_disabled:before{content:"\E0EF"}.material-icons.done:before{content:"\E876"}.material-icons.done_all:before{content:"\E877"}.material-icons.done_outline:before{content:"\E92F"}.material-icons.donut_large:before{content:"\E917"}.material-icons.donut_small:before{content:"\E918"}.material-icons.double_arrow:before{content:"\EA50"}.material-icons.drafts:before{content:"\E151"}.material-icons.drag_handle:before{content:"\E25D"}.material-icons.drag_indicator:before{content:"\E945"}.material-icons.drive_eta:before{content:"\E613"}.material-icons.drive_file_move_outline:before{content:"\E9A1"}.material-icons.drive_file_rename_outline:before{content:"\E9A2"}.material-icons.drive_folder_upload:before{content:"\E9A3"}.material-icons.dry_cleaning:before{content:"\EA58"}.material-icons.duo:before{content:"\E9A5"}.material-icons.dvr:before{content:"\E1B2"}.material-icons.dynamic_feed:before{content:"\EA14"}.material-icons.eco:before{content:"\EA35"}.material-icons.edit:before{content:"\E3C9"}.material-icons.edit_attributes:before{content:"\E578"}.material-icons.edit_location:before{content:"\E568"}.material-icons.edit_off:before{content:"\E950"}.material-icons.eject:before{content:"\E8FB"}.material-icons.email:before{content:"\E0BE"}.material-icons.emoji_emotions:before{content:"\EA22"}.material-icons.emoji_events:before{content:"\EA23"}.material-icons.emoji_flags:before{content:"\EA1A"}.material-icons.emoji_food_beverage:before{content:"\EA1B"}.material-icons.emoji_nature:before{content:"\EA1C"}.material-icons.emoji_objects:before{content:"\EA24"}.material-icons.emoji_people:before{content:"\EA1D"}.material-icons.emoji_symbols:before{content:"\EA1E"}.material-icons.emoji_transportation:before{content:"\EA1F"}.material-icons.enhance_photo_translate:before{content:"\E8FC"}.material-icons.enhanced_encryption:before{content:"\E63F"}.material-icons.equalizer:before{content:"\E01D"}.material-icons.error:before{content:"\E000"}.material-icons.error_outline:before{content:"\E001"}.material-icons.euro:before{content:"\EA15"}.material-icons.euro_symbol:before{content:"\E926"}.material-icons.ev_station:before{content:"\E56D"}.material-icons.event:before{content:"\E878"}.material-icons.event_available:before{content:"\E614"}.material-icons.event_busy:before{content:"\E615"}.material-icons.event_note:before{content:"\E616"}.material-icons.event_seat:before{content:"\E903"}.material-icons.exit_to_app:before{content:"\E879"}.material-icons.expand:before{content:"\E94F"}.material-icons.expand_less:before{content:"\E5CE"}.material-icons.expand_more:before{content:"\E5CF"}.material-icons.explicit:before{content:"\E01E"}.material-icons.explore:before{content:"\E87A"}.material-icons.explore_off:before{content:"\E9A8"}.material-icons.exposure:before{content:"\E3CA"}.material-icons.exposure_minus_1:before{content:"\E3CB"}.material-icons.exposure_minus_2:before{content:"\E3CC"}.material-icons.exposure_neg_1:before{content:"\E3CB"}.material-icons.exposure_neg_2:before{content:"\E3CC"}.material-icons.exposure_plus_1:before{content:"\E3CD"}.material-icons.exposure_plus_2:before{content:"\E3CE"}.material-icons.exposure_zero:before{content:"\E3CF"}.material-icons.extension:before{content:"\E87B"}.material-icons.face:before{content:"\E87C"}.material-icons.fast_forward:before{content:"\E01F"}.material-icons.fast_rewind:before{content:"\E020"}.material-icons.fastfood:before{content:"\E57A"}.material-icons.favorite:before{content:"\E87D"}.material-icons.favorite_border:before,.material-icons.favorite_outline:before{content:"\E87E"}.material-icons.featured_play_list:before{content:"\E06D"}.material-icons.featured_video:before{content:"\E06E"}.material-icons.feedback:before{content:"\E87F"}.material-icons.festival:before{content:"\EA68"}.material-icons.fiber_dvr:before{content:"\E05D"}.material-icons.fiber_manual_record:before{content:"\E061"}.material-icons.fiber_new:before{content:"\E05E"}.material-icons.fiber_pin:before{content:"\E06A"}.material-icons.fiber_smart_record:before{content:"\E062"}.material-icons.file_copy:before{content:"\E173"}.material-icons.file_download:before{content:"\E2C4"}.material-icons.file_download_done:before{content:"\E9AA"}.material-icons.file_present:before{content:"\EA0E"}.material-icons.file_upload:before{content:"\E2C6"}.material-icons.filter:before{content:"\E3D3"}.material-icons.filter_1:before{content:"\E3D0"}.material-icons.filter_2:before{content:"\E3D1"}.material-icons.filter_3:before{content:"\E3D2"}.material-icons.filter_4:before{content:"\E3D4"}.material-icons.filter_5:before{content:"\E3D5"}.material-icons.filter_6:before{content:"\E3D6"}.material-icons.filter_7:before{content:"\E3D7"}.material-icons.filter_8:before{content:"\E3D8"}.material-icons.filter_9:before{content:"\E3D9"}.material-icons.filter_9_plus:before{content:"\E3DA"}.material-icons.filter_b_and_w:before{content:"\E3DB"}.material-icons.filter_center_focus:before{content:"\E3DC"}.material-icons.filter_drama:before{content:"\E3DD"}.material-icons.filter_frames:before{content:"\E3DE"}.material-icons.filter_hdr:before{content:"\E3DF"}.material-icons.filter_list:before{content:"\E152"}.material-icons.filter_list_alt:before{content:"\E94E"}.material-icons.filter_none:before{content:"\E3E0"}.material-icons.filter_tilt_shift:before{content:"\E3E2"}.material-icons.filter_vintage:before{content:"\E3E3"}.material-icons.find_in_page:before{content:"\E880"}.material-icons.find_replace:before{content:"\E881"}.material-icons.fingerprint:before{content:"\E90D"}.material-icons.fireplace:before{content:"\EA43"}.material-icons.first_page:before{content:"\E5DC"}.material-icons.fit_screen:before{content:"\EA10"}.material-icons.fitness_center:before{content:"\EB43"}.material-icons.flag:before{content:"\E153"}.material-icons.flare:before{content:"\E3E4"}.material-icons.flash_auto:before{content:"\E3E5"}.material-icons.flash_off:before{content:"\E3E6"}.material-icons.flash_on:before{content:"\E3E7"}.material-icons.flight:before{content:"\E539"}.material-icons.flight_land:before{content:"\E904"}.material-icons.flight_takeoff:before{content:"\E905"}.material-icons.flip:before{content:"\E3E8"}.material-icons.flip_camera_android:before{content:"\EA37"}.material-icons.flip_camera_ios:before{content:"\EA38"}.material-icons.flip_to_back:before{content:"\E882"}.material-icons.flip_to_front:before{content:"\E883"}.material-icons.folder:before{content:"\E2C7"}.material-icons.folder_open:before{content:"\E2C8"}.material-icons.folder_shared:before{content:"\E2C9"}.material-icons.folder_special:before{content:"\E617"}.material-icons.font_download:before{content:"\E167"}.material-icons.format_align_center:before{content:"\E234"}.material-icons.format_align_justify:before{content:"\E235"}.material-icons.format_align_left:before{content:"\E236"}.material-icons.format_align_right:before{content:"\E237"}.material-icons.format_bold:before{content:"\E238"}.material-icons.format_clear:before{content:"\E239"}.material-icons.format_color_fill:before{content:"\E23A"}.material-icons.format_color_reset:before{content:"\E23B"}.material-icons.format_color_text:before{content:"\E23C"}.material-icons.format_indent_decrease:before{content:"\E23D"}.material-icons.format_indent_increase:before{content:"\E23E"}.material-icons.format_italic:before{content:"\E23F"}.material-icons.format_line_spacing:before{content:"\E240"}.material-icons.format_list_bulleted:before{content:"\E241"}.material-icons.format_list_numbered:before{content:"\E242"}.material-icons.format_list_numbered_rtl:before{content:"\E267"}.material-icons.format_paint:before{content:"\E243"}.material-icons.format_quote:before{content:"\E244"}.material-icons.format_shapes:before{content:"\E25E"}.material-icons.format_size:before{content:"\E245"}.material-icons.format_strikethrough:before{content:"\E246"}.material-icons.format_textdirection_l_to_r:before{content:"\E247"}.material-icons.format_textdirection_r_to_l:before{content:"\E248"}.material-icons.format_underline:before,.material-icons.format_underlined:before{content:"\E249"}.material-icons.forum:before{content:"\E0BF"}.material-icons.forward:before{content:"\E154"}.material-icons.forward_10:before{content:"\E056"}.material-icons.forward_30:before{content:"\E057"}.material-icons.forward_5:before{content:"\E058"}.material-icons.free_breakfast:before{content:"\EB44"}.material-icons.fullscreen:before{content:"\E5D0"}.material-icons.fullscreen_exit:before{content:"\E5D1"}.material-icons.functions:before{content:"\E24A"}.material-icons.g_translate:before{content:"\E927"}.material-icons.gamepad:before{content:"\E30F"}.material-icons.games:before{content:"\E021"}.material-icons.gavel:before{content:"\E90E"}.material-icons.gesture:before{content:"\E155"}.material-icons.get_app:before{content:"\E884"}.material-icons.gif:before{content:"\E908"}.material-icons.goat:before{content:"\DBFF"}.material-icons.golf_course:before{content:"\EB45"}.material-icons.gps_fixed:before{content:"\E1B3"}.material-icons.gps_not_fixed:before{content:"\E1B4"}.material-icons.gps_off:before{content:"\E1B5"}.material-icons.grade:before{content:"\E885"}.material-icons.gradient:before{content:"\E3E9"}.material-icons.grain:before{content:"\E3EA"}.material-icons.graphic_eq:before{content:"\E1B8"}.material-icons.grid_off:before{content:"\E3EB"}.material-icons.grid_on:before{content:"\E3EC"}.material-icons.grid_view:before{content:"\E9B0"}.material-icons.group:before{content:"\E7EF"}.material-icons.group_add:before{content:"\E7F0"}.material-icons.group_work:before{content:"\E886"}.material-icons.hail:before{content:"\E9B1"}.material-icons.hardware:before{content:"\EA59"}.material-icons.hd:before{content:"\E052"}.material-icons.hdr_off:before{content:"\E3ED"}.material-icons.hdr_on:before{content:"\E3EE"}.material-icons.hdr_strong:before{content:"\E3F1"}.material-icons.hdr_weak:before{content:"\E3F2"}.material-icons.headset:before{content:"\E310"}.material-icons.headset_mic:before{content:"\E311"}.material-icons.headset_off:before{content:"\E33A"}.material-icons.healing:before{content:"\E3F3"}.material-icons.hearing:before{content:"\E023"}.material-icons.height:before{content:"\EA16"}.material-icons.help:before{content:"\E887"}.material-icons.help_outline:before{content:"\E8FD"}.material-icons.high_quality:before{content:"\E024"}.material-icons.highlight:before{content:"\E25F"}.material-icons.highlight_off:before,.material-icons.highlight_remove:before{content:"\E888"}.material-icons.history:before{content:"\E889"}.material-icons.home:before{content:"\E88A"}.material-icons.home_filled:before{content:"\E9B2"}.material-icons.home_work:before{content:"\EA09"}.material-icons.horizontal_split:before{content:"\E947"}.material-icons.hot_tub:before{content:"\EB46"}.material-icons.hotel:before{content:"\E53A"}.material-icons.hourglass_empty:before{content:"\E88B"}.material-icons.hourglass_full:before{content:"\E88C"}.material-icons.house:before{content:"\EA44"}.material-icons.how_to_reg:before{content:"\E174"}.material-icons.how_to_vote:before{content:"\E175"}.material-icons.http:before{content:"\E902"}.material-icons.https:before{content:"\E88D"}.material-icons.icecream:before{content:"\EA69"}.material-icons.image:before{content:"\E3F4"}.material-icons.image_aspect_ratio:before{content:"\E3F5"}.material-icons.image_search:before{content:"\E43F"}.material-icons.imagesearch_roller:before{content:"\E9B4"}.material-icons.import_contacts:before{content:"\E0E0"}.material-icons.import_export:before{content:"\E0C3"}.material-icons.important_devices:before{content:"\E912"}.material-icons.inbox:before{content:"\E156"}.material-icons.indeterminate_check_box:before{content:"\E909"}.material-icons.info:before{content:"\E88E"}.material-icons.info_outline:before{content:"\E88F"}.material-icons.input:before{content:"\E890"}.material-icons.insert_chart:before{content:"\E24B"}.material-icons.insert_chart_outlined:before{content:"\E26A"}.material-icons.insert_comment:before{content:"\E24C"}.material-icons.insert_drive_file:before{content:"\E24D"}.material-icons.insert_emoticon:before{content:"\E24E"}.material-icons.insert_invitation:before{content:"\E24F"}.material-icons.insert_link:before{content:"\E250"}.material-icons.insert_photo:before{content:"\E251"}.material-icons.inventory:before{content:"\E179"}.material-icons.invert_colors:before{content:"\E891"}.material-icons.invert_colors_off:before{content:"\E0C4"}.material-icons.invert_colors_on:before{content:"\E891"}.material-icons.iso:before{content:"\E3F6"}.material-icons.keyboard:before{content:"\E312"}.material-icons.keyboard_arrow_down:before{content:"\E313"}.material-icons.keyboard_arrow_left:before{content:"\E314"}.material-icons.keyboard_arrow_right:before{content:"\E315"}.material-icons.keyboard_arrow_up:before{content:"\E316"}.material-icons.keyboard_backspace:before{content:"\E317"}.material-icons.keyboard_capslock:before{content:"\E318"}.material-icons.keyboard_control:before{content:"\E5D3"}.material-icons.keyboard_hide:before{content:"\E31A"}.material-icons.keyboard_return:before{content:"\E31B"}.material-icons.keyboard_tab:before{content:"\E31C"}.material-icons.keyboard_voice:before{content:"\E31D"}.material-icons.king_bed:before{content:"\EA45"}.material-icons.kitchen:before{content:"\EB47"}.material-icons.label:before{content:"\E892"}.material-icons.label_important:before{content:"\E937"}.material-icons.label_important_outline:before{content:"\E948"}.material-icons.label_off:before{content:"\E9B6"}.material-icons.label_outline:before{content:"\E893"}.material-icons.landscape:before{content:"\E3F7"}.material-icons.language:before{content:"\E894"}.material-icons.laptop:before{content:"\E31E"}.material-icons.laptop_chromebook:before{content:"\E31F"}.material-icons.laptop_mac:before{content:"\E320"}.material-icons.laptop_windows:before{content:"\E321"}.material-icons.last_page:before{content:"\E5DD"}.material-icons.launch:before{content:"\E895"}.material-icons.layers:before{content:"\E53B"}.material-icons.layers_clear:before{content:"\E53C"}.material-icons.leak_add:before{content:"\E3F8"}.material-icons.leak_remove:before{content:"\E3F9"}.material-icons.lens:before{content:"\E3FA"}.material-icons.library_add:before{content:"\E02E"}.material-icons.library_add_check:before{content:"\E9B7"}.material-icons.library_books:before{content:"\E02F"}.material-icons.library_music:before{content:"\E030"}.material-icons.lightbulb:before{content:"\E0F0"}.material-icons.lightbulb_outline:before{content:"\E90F"}.material-icons.line_style:before{content:"\E919"}.material-icons.line_weight:before{content:"\E91A"}.material-icons.linear_scale:before{content:"\E260"}.material-icons.link:before{content:"\E157"}.material-icons.link_off:before{content:"\E16F"}.material-icons.linked_camera:before{content:"\E438"}.material-icons.liquor:before{content:"\EA60"}.material-icons.list:before{content:"\E896"}.material-icons.list_alt:before{content:"\E0EE"}.material-icons.live_help:before{content:"\E0C6"}.material-icons.live_tv:before{content:"\E639"}.material-icons.local_activity:before{content:"\E53F"}.material-icons.local_airport:before{content:"\E53D"}.material-icons.local_atm:before{content:"\E53E"}.material-icons.local_attraction:before{content:"\E53F"}.material-icons.local_bar:before{content:"\E540"}.material-icons.local_cafe:before{content:"\E541"}.material-icons.local_car_wash:before{content:"\E542"}.material-icons.local_convenience_store:before{content:"\E543"}.material-icons.local_dining:before{content:"\E556"}.material-icons.local_drink:before{content:"\E544"}.material-icons.local_florist:before{content:"\E545"}.material-icons.local_gas_station:before{content:"\E546"}.material-icons.local_grocery_store:before{content:"\E547"}.material-icons.local_hospital:before{content:"\E548"}.material-icons.local_hotel:before{content:"\E549"}.material-icons.local_laundry_service:before{content:"\E54A"}.material-icons.local_library:before{content:"\E54B"}.material-icons.local_mall:before{content:"\E54C"}.material-icons.local_movies:before{content:"\E54D"}.material-icons.local_offer:before{content:"\E54E"}.material-icons.local_parking:before{content:"\E54F"}.material-icons.local_pharmacy:before{content:"\E550"}.material-icons.local_phone:before{content:"\E551"}.material-icons.local_pizza:before{content:"\E552"}.material-icons.local_play:before{content:"\E553"}.material-icons.local_post_office:before{content:"\E554"}.material-icons.local_print_shop:before,.material-icons.local_printshop:before{content:"\E555"}.material-icons.local_restaurant:before{content:"\E556"}.material-icons.local_see:before{content:"\E557"}.material-icons.local_shipping:before{content:"\E558"}.material-icons.local_taxi:before{content:"\E559"}.material-icons.location_city:before{content:"\E7F1"}.material-icons.location_disabled:before{content:"\E1B6"}.material-icons.location_history:before{content:"\E55A"}.material-icons.location_off:before{content:"\E0C7"}.material-icons.location_on:before{content:"\E0C8"}.material-icons.location_searching:before{content:"\E1B7"}.material-icons.lock:before{content:"\E897"}.material-icons.lock_open:before{content:"\E898"}.material-icons.lock_outline:before{content:"\E899"}.material-icons.logout:before{content:"\E9BA"}.material-icons.looks:before{content:"\E3FC"}.material-icons.looks_3:before{content:"\E3FB"}.material-icons.looks_4:before{content:"\E3FD"}.material-icons.looks_5:before{content:"\E3FE"}.material-icons.looks_6:before{content:"\E3FF"}.material-icons.looks_one:before{content:"\E400"}.material-icons.looks_two:before{content:"\E401"}.material-icons.loop:before{content:"\E028"}.material-icons.loupe:before{content:"\E402"}.material-icons.low_priority:before{content:"\E16D"}.material-icons.loyalty:before{content:"\E89A"}.material-icons.lunch_dining:before{content:"\EA61"}.material-icons.mail:before{content:"\E158"}.material-icons.mail_outline:before{content:"\E0E1"}.material-icons.map:before{content:"\E55B"}.material-icons.margin:before{content:"\E9BB"}.material-icons.mark_as_unread:before{content:"\E9BC"}.material-icons.markunread:before{content:"\E159"}.material-icons.markunread_mailbox:before{content:"\E89B"}.material-icons.maximize:before{content:"\E930"}.material-icons.meeting_room:before{content:"\EB4F"}.material-icons.memory:before{content:"\E322"}.material-icons.menu:before{content:"\E5D2"}.material-icons.menu_book:before{content:"\EA19"}.material-icons.menu_open:before{content:"\E9BD"}.material-icons.merge_type:before{content:"\E252"}.material-icons.message:before{content:"\E0C9"}.material-icons.messenger:before{content:"\E0CA"}.material-icons.messenger_outline:before{content:"\E0CB"}.material-icons.mic:before{content:"\E029"}.material-icons.mic_none:before{content:"\E02A"}.material-icons.mic_off:before{content:"\E02B"}.material-icons.minimize:before{content:"\E931"}.material-icons.missed_video_call:before{content:"\E073"}.material-icons.mms:before{content:"\E618"}.material-icons.mobile_friendly:before{content:"\E200"}.material-icons.mobile_off:before{content:"\E201"}.material-icons.mobile_screen_share:before{content:"\E0E7"}.material-icons.mode_comment:before{content:"\E253"}.material-icons.mode_edit:before{content:"\E254"}.material-icons.monetization_on:before{content:"\E263"}.material-icons.money:before{content:"\E57D"}.material-icons.money_off:before{content:"\E25C"}.material-icons.monochrome_photos:before{content:"\E403"}.material-icons.mood:before{content:"\E7F2"}.material-icons.mood_bad:before{content:"\E7F3"}.material-icons.more:before{content:"\E619"}.material-icons.more_horiz:before{content:"\E5D3"}.material-icons.more_vert:before{content:"\E5D4"}.material-icons.motorcycle:before{content:"\E91B"}.material-icons.mouse:before{content:"\E323"}.material-icons.move_to_inbox:before{content:"\E168"}.material-icons.movie:before{content:"\E02C"}.material-icons.movie_creation:before{content:"\E404"}.material-icons.movie_filter:before{content:"\E43A"}.material-icons.mp:before{content:"\E9C3"}.material-icons.multiline_chart:before{content:"\E6DF"}.material-icons.multitrack_audio:before{content:"\E1B8"}.material-icons.museum:before{content:"\EA36"}.material-icons.music_note:before{content:"\E405"}.material-icons.music_off:before{content:"\E440"}.material-icons.music_video:before{content:"\E063"}.material-icons.my_library_add:before{content:"\E02E"}.material-icons.my_library_books:before{content:"\E02F"}.material-icons.my_library_music:before{content:"\E030"}.material-icons.my_location:before{content:"\E55C"}.material-icons.nature:before{content:"\E406"}.material-icons.nature_people:before{content:"\E407"}.material-icons.navigate_before:before{content:"\E408"}.material-icons.navigate_next:before{content:"\E409"}.material-icons.navigation:before{content:"\E55D"}.material-icons.near_me:before{content:"\E569"}.material-icons.network_cell:before{content:"\E1B9"}.material-icons.network_check:before{content:"\E640"}.material-icons.network_locked:before{content:"\E61A"}.material-icons.network_wifi:before{content:"\E1BA"}.material-icons.new_releases:before{content:"\E031"}.material-icons.next_week:before{content:"\E16A"}.material-icons.nfc:before{content:"\E1BB"}.material-icons.nightlife:before{content:"\EA62"}.material-icons.nights_stay:before{content:"\EA46"}.material-icons.no_encryption:before{content:"\E641"}.material-icons.no_meeting_room:before{content:"\EB4E"}.material-icons.no_sim:before{content:"\E0CC"}.material-icons.not_interested:before{content:"\E033"}.material-icons.not_listed_location:before{content:"\E575"}.material-icons.note:before{content:"\E06F"}.material-icons.note_add:before{content:"\E89C"}.material-icons.notes:before{content:"\E26C"}.material-icons.notification_important:before{content:"\E004"}.material-icons.notifications:before{content:"\E7F4"}.material-icons.notifications_active:before{content:"\E7F7"}.material-icons.notifications_none:before{content:"\E7F5"}.material-icons.notifications_off:before{content:"\E7F6"}.material-icons.notifications_on:before{content:"\E7F7"}.material-icons.notifications_paused:before{content:"\E7F8"}.material-icons.now_wallpaper:before{content:"\E1BC"}.material-icons.now_widgets:before{content:"\E1BD"}.material-icons.offline_bolt:before{content:"\E932"}.material-icons.offline_pin:before{content:"\E90A"}.material-icons.offline_share:before{content:"\E9C5"}.material-icons.ondemand_video:before{content:"\E63A"}.material-icons.opacity:before{content:"\E91C"}.material-icons.open_in_browser:before{content:"\E89D"}.material-icons.open_in_new:before{content:"\E89E"}.material-icons.open_with:before{content:"\E89F"}.material-icons.outdoor_grill:before{content:"\EA47"}.material-icons.outlined_flag:before{content:"\E16E"}.material-icons.padding:before{content:"\E9C8"}.material-icons.pages:before{content:"\E7F9"}.material-icons.pageview:before{content:"\E8A0"}.material-icons.palette:before{content:"\E40A"}.material-icons.pan_tool:before{content:"\E925"}.material-icons.panorama:before{content:"\E40B"}.material-icons.panorama_fish_eye:before,.material-icons.panorama_fisheye:before{content:"\E40C"}.material-icons.panorama_horizontal:before{content:"\E40D"}.material-icons.panorama_photosphere:before{content:"\E9C9"}.material-icons.panorama_photosphere_select:before{content:"\E9CA"}.material-icons.panorama_vertical:before{content:"\E40E"}.material-icons.panorama_wide_angle:before{content:"\E40F"}.material-icons.park:before{content:"\EA63"}.material-icons.party_mode:before{content:"\E7FA"}.material-icons.pause:before{content:"\E034"}.material-icons.pause_circle_filled:before{content:"\E035"}.material-icons.pause_circle_outline:before{content:"\E036"}.material-icons.pause_presentation:before{content:"\E0EA"}.material-icons.payment:before{content:"\E8A1"}.material-icons.people:before{content:"\E7FB"}.material-icons.people_alt:before{content:"\EA21"}.material-icons.people_outline:before{content:"\E7FC"}.material-icons.perm_camera_mic:before{content:"\E8A2"}.material-icons.perm_contact_cal:before,.material-icons.perm_contact_calendar:before{content:"\E8A3"}.material-icons.perm_data_setting:before{content:"\E8A4"}.material-icons.perm_device_info:before,.material-icons.perm_device_information:before{content:"\E8A5"}.material-icons.perm_identity:before{content:"\E8A6"}.material-icons.perm_media:before{content:"\E8A7"}.material-icons.perm_phone_msg:before{content:"\E8A8"}.material-icons.perm_scan_wifi:before{content:"\E8A9"}.material-icons.person:before{content:"\E7FD"}.material-icons.person_add:before{content:"\E7FE"}.material-icons.person_add_disabled:before{content:"\E9CB"}.material-icons.person_outline:before{content:"\E7FF"}.material-icons.person_pin:before{content:"\E55A"}.material-icons.person_pin_circle:before{content:"\E56A"}.material-icons.personal_video:before{content:"\E63B"}.material-icons.pets:before{content:"\E91D"}.material-icons.phone:before{content:"\E0CD"}.material-icons.phone_android:before{content:"\E324"}.material-icons.phone_bluetooth_speaker:before{content:"\E61B"}.material-icons.phone_callback:before{content:"\E649"}.material-icons.phone_disabled:before{content:"\E9CC"}.material-icons.phone_enabled:before{content:"\E9CD"}.material-icons.phone_forwarded:before{content:"\E61C"}.material-icons.phone_in_talk:before{content:"\E61D"}.material-icons.phone_iphone:before{content:"\E325"}.material-icons.phone_locked:before{content:"\E61E"}.material-icons.phone_missed:before{content:"\E61F"}.material-icons.phone_paused:before{content:"\E620"}.material-icons.phonelink:before{content:"\E326"}.material-icons.phonelink_erase:before{content:"\E0DB"}.material-icons.phonelink_lock:before{content:"\E0DC"}.material-icons.phonelink_off:before{content:"\E327"}.material-icons.phonelink_ring:before{content:"\E0DD"}.material-icons.phonelink_setup:before{content:"\E0DE"}.material-icons.photo:before{content:"\E410"}.material-icons.photo_album:before{content:"\E411"}.material-icons.photo_camera:before{content:"\E412"}.material-icons.photo_filter:before{content:"\E43B"}.material-icons.photo_library:before{content:"\E413"}.material-icons.photo_size_select_actual:before{content:"\E432"}.material-icons.photo_size_select_large:before{content:"\E433"}.material-icons.photo_size_select_small:before{content:"\E434"}.material-icons.picture_as_pdf:before{content:"\E415"}.material-icons.picture_in_picture:before{content:"\E8AA"}.material-icons.picture_in_picture_alt:before{content:"\E911"}.material-icons.pie_chart:before{content:"\E6C4"}.material-icons.pie_chart_outlined:before{content:"\E6C5"}.material-icons.pin_drop:before{content:"\E55E"}.material-icons.pivot_table_chart:before{content:"\E9CE"}.material-icons.place:before{content:"\E55F"}.material-icons.play_arrow:before{content:"\E037"}.material-icons.play_circle_fill:before,.material-icons.play_circle_filled:before{content:"\E038"}.material-icons.play_circle_outline:before{content:"\E039"}.material-icons.play_for_work:before{content:"\E906"}.material-icons.playlist_add:before{content:"\E03B"}.material-icons.playlist_add_check:before{content:"\E065"}.material-icons.playlist_play:before{content:"\E05F"}.material-icons.plus_one:before{content:"\E800"}.material-icons.policy:before{content:"\EA17"}.material-icons.poll:before{content:"\E801"}.material-icons.polymer:before{content:"\E8AB"}.material-icons.pool:before{content:"\EB48"}.material-icons.portable_wifi_off:before{content:"\E0CE"}.material-icons.portrait:before{content:"\E416"}.material-icons.post_add:before{content:"\EA20"}.material-icons.power:before{content:"\E63C"}.material-icons.power_input:before{content:"\E336"}.material-icons.power_off:before{content:"\E646"}.material-icons.power_settings_new:before{content:"\E8AC"}.material-icons.pregnant_woman:before{content:"\E91E"}.material-icons.present_to_all:before{content:"\E0DF"}.material-icons.print:before{content:"\E8AD"}.material-icons.print_disabled:before{content:"\E9CF"}.material-icons.priority_high:before{content:"\E645"}.material-icons.public:before{content:"\E80B"}.material-icons.publish:before{content:"\E255"}.material-icons.query_builder:before{content:"\E8AE"}.material-icons.question_answer:before{content:"\E8AF"}.material-icons.queue:before{content:"\E03C"}.material-icons.queue_music:before{content:"\E03D"}.material-icons.queue_play_next:before{content:"\E066"}.material-icons.quick_contacts_dialer:before{content:"\E0CF"}.material-icons.quick_contacts_mail:before{content:"\E0D0"}.material-icons.radio:before{content:"\E03E"}.material-icons.radio_button_checked:before{content:"\E837"}.material-icons.radio_button_off:before{content:"\E836"}.material-icons.radio_button_on:before{content:"\E837"}.material-icons.radio_button_unchecked:before{content:"\E836"}.material-icons.railway_alert:before{content:"\E9D1"}.material-icons.ramen_dining:before{content:"\EA64"}.material-icons.rate_review:before{content:"\E560"}.material-icons.receipt:before{content:"\E8B0"}.material-icons.recent_actors:before{content:"\E03F"}.material-icons.recommend:before{content:"\E9D2"}.material-icons.record_voice_over:before{content:"\E91F"}.material-icons.redeem:before{content:"\E8B1"}.material-icons.redo:before{content:"\E15A"}.material-icons.refresh:before{content:"\E5D5"}.material-icons.remove:before{content:"\E15B"}.material-icons.remove_circle:before{content:"\E15C"}.material-icons.remove_circle_outline:before{content:"\E15D"}.material-icons.remove_done:before{content:"\E9D3"}.material-icons.remove_from_queue:before{content:"\E067"}.material-icons.remove_moderator:before{content:"\E9D4"}.material-icons.remove_red_eye:before{content:"\E417"}.material-icons.remove_shopping_cart:before{content:"\E928"}.material-icons.reorder:before{content:"\E8FE"}.material-icons.repeat:before{content:"\E040"}.material-icons.repeat_on:before{content:"\E9D6"}.material-icons.repeat_one:before{content:"\E041"}.material-icons.repeat_one_on:before{content:"\E9D7"}.material-icons.replay:before{content:"\E042"}.material-icons.replay_10:before{content:"\E059"}.material-icons.replay_30:before{content:"\E05A"}.material-icons.replay_5:before{content:"\E05B"}.material-icons.replay_circle_filled:before{content:"\E9D8"}.material-icons.reply:before{content:"\E15E"}.material-icons.reply_all:before{content:"\E15F"}.material-icons.report:before{content:"\E160"}.material-icons.report_off:before{content:"\E170"}.material-icons.report_problem:before{content:"\E8B2"}.material-icons.reset_tv:before{content:"\E9D9"}.material-icons.restaurant:before{content:"\E56C"}.material-icons.restaurant_menu:before{content:"\E561"}.material-icons.restore:before{content:"\E8B3"}.material-icons.restore_from_trash:before{content:"\E938"}.material-icons.restore_page:before{content:"\E929"}.material-icons.ring_volume:before{content:"\E0D1"}.material-icons.room:before{content:"\E8B4"}.material-icons.room_service:before{content:"\EB49"}.material-icons.rotate_90_degrees_ccw:before{content:"\E418"}.material-icons.rotate_left:before{content:"\E419"}.material-icons.rotate_right:before{content:"\E41A"}.material-icons.rounded_corner:before{content:"\E920"}.material-icons.router:before{content:"\E328"}.material-icons.rowing:before{content:"\E921"}.material-icons.rss_feed:before{content:"\E0E5"}.material-icons.rtt:before{content:"\E9AD"}.material-icons.rv_hookup:before{content:"\E642"}.material-icons.satellite:before{content:"\E562"}.material-icons.save:before{content:"\E161"}.material-icons.save_alt:before{content:"\E171"}.material-icons.saved_search:before{content:"\EA11"}.material-icons.scanner:before{content:"\E329"}.material-icons.scatter_plot:before{content:"\E268"}.material-icons.schedule:before{content:"\E8B5"}.material-icons.schedule_send:before{content:"\EA0A"}.material-icons.school:before{content:"\E80C"}.material-icons.score:before{content:"\E269"}.material-icons.screen_lock_landscape:before{content:"\E1BE"}.material-icons.screen_lock_portrait:before{content:"\E1BF"}.material-icons.screen_lock_rotation:before{content:"\E1C0"}.material-icons.screen_rotation:before{content:"\E1C1"}.material-icons.screen_share:before{content:"\E0E2"}.material-icons.sd:before{content:"\E9DD"}.material-icons.sd_card:before{content:"\E623"}.material-icons.sd_storage:before{content:"\E1C2"}.material-icons.search:before{content:"\E8B6"}.material-icons.security:before{content:"\E32A"}.material-icons.segment:before{content:"\E94B"}.material-icons.select_all:before{content:"\E162"}.material-icons.send:before{content:"\E163"}.material-icons.send_and_archive:before{content:"\EA0C"}.material-icons.sentiment_dissatisfied:before{content:"\E811"}.material-icons.sentiment_neutral:before{content:"\E812"}.material-icons.sentiment_satisfied:before{content:"\E813"}.material-icons.sentiment_satisfied_alt:before{content:"\E0ED"}.material-icons.sentiment_very_dissatisfied:before{content:"\E814"}.material-icons.sentiment_very_satisfied:before{content:"\E815"}.material-icons.settings:before{content:"\E8B8"}.material-icons.settings_applications:before{content:"\E8B9"}.material-icons.settings_backup_restore:before{content:"\E8BA"}.material-icons.settings_bluetooth:before{content:"\E8BB"}.material-icons.settings_brightness:before{content:"\E8BD"}.material-icons.settings_cell:before{content:"\E8BC"}.material-icons.settings_display:before{content:"\E8BD"}.material-icons.settings_ethernet:before{content:"\E8BE"}.material-icons.settings_input_antenna:before{content:"\E8BF"}.material-icons.settings_input_component:before{content:"\E8C0"}.material-icons.settings_input_composite:before{content:"\E8C1"}.material-icons.settings_input_hdmi:before{content:"\E8C2"}.material-icons.settings_input_svideo:before{content:"\E8C3"}.material-icons.settings_overscan:before{content:"\E8C4"}.material-icons.settings_phone:before{content:"\E8C5"}.material-icons.settings_power:before{content:"\E8C6"}.material-icons.settings_remote:before{content:"\E8C7"}.material-icons.settings_system_daydream:before{content:"\E1C3"}.material-icons.settings_voice:before{content:"\E8C8"}.material-icons.share:before{content:"\E80D"}.material-icons.shield:before{content:"\E9E0"}.material-icons.shop:before{content:"\E8C9"}.material-icons.shop_two:before{content:"\E8CA"}.material-icons.shopping_basket:before{content:"\E8CB"}.material-icons.shopping_cart:before{content:"\E8CC"}.material-icons.short_text:before{content:"\E261"}.material-icons.show_chart:before{content:"\E6E1"}.material-icons.shuffle:before{content:"\E043"}.material-icons.shuffle_on:before{content:"\E9E1"}.material-icons.shutter_speed:before{content:"\E43D"}.material-icons.signal_cellular_4_bar:before{content:"\E1C8"}.material-icons.signal_cellular_alt:before{content:"\E202"}.material-icons.signal_cellular_connected_no_internet_4_bar:before{content:"\E1CD"}.material-icons.signal_cellular_no_sim:before{content:"\E1CE"}.material-icons.signal_cellular_null:before{content:"\E1CF"}.material-icons.signal_cellular_off:before{content:"\E1D0"}.material-icons.signal_wifi_4_bar:before{content:"\E1D8"}.material-icons.signal_wifi_4_bar_lock:before{content:"\E1D9"}.material-icons.signal_wifi_off:before{content:"\E1DA"}.material-icons.sim_card:before{content:"\E32B"}.material-icons.sim_card_alert:before{content:"\E624"}.material-icons.single_bed:before{content:"\EA48"}.material-icons.skip_next:before{content:"\E044"}.material-icons.skip_previous:before{content:"\E045"}.material-icons.slideshow:before{content:"\E41B"}.material-icons.slow_motion_video:before{content:"\E068"}.material-icons.smartphone:before{content:"\E32C"}.material-icons.smoke_free:before{content:"\EB4A"}.material-icons.smoking_rooms:before{content:"\EB4B"}.material-icons.sms:before{content:"\E625"}.material-icons.sms_failed:before{content:"\E626"}.material-icons.snooze:before{content:"\E046"}.material-icons.sort:before{content:"\E164"}.material-icons.sort_by_alpha:before{content:"\E053"}.material-icons.spa:before{content:"\EB4C"}.material-icons.space_bar:before{content:"\E256"}.material-icons.speaker:before{content:"\E32D"}.material-icons.speaker_group:before{content:"\E32E"}.material-icons.speaker_notes:before{content:"\E8CD"}.material-icons.speaker_notes_off:before{content:"\E92A"}.material-icons.speaker_phone:before{content:"\E0D2"}.material-icons.speed:before{content:"\E9E4"}.material-icons.spellcheck:before{content:"\E8CE"}.material-icons.sports:before{content:"\EA30"}.material-icons.sports_baseball:before{content:"\EA51"}.material-icons.sports_basketball:before{content:"\EA26"}.material-icons.sports_cricket:before{content:"\EA27"}.material-icons.sports_esports:before{content:"\EA28"}.material-icons.sports_football:before{content:"\EA29"}.material-icons.sports_golf:before{content:"\EA2A"}.material-icons.sports_handball:before{content:"\EA33"}.material-icons.sports_hockey:before{content:"\EA2B"}.material-icons.sports_kabaddi:before{content:"\EA34"}.material-icons.sports_mma:before{content:"\EA2C"}.material-icons.sports_motorsports:before{content:"\EA2D"}.material-icons.sports_rugby:before{content:"\EA2E"}.material-icons.sports_soccer:before{content:"\EA2F"}.material-icons.sports_tennis:before{content:"\EA32"}.material-icons.sports_volleyball:before{content:"\EA31"}.material-icons.square_foot:before{content:"\EA49"}.material-icons.stacked_bar_chart:before{content:"\E9E6"}.material-icons.star:before{content:"\E838"}.material-icons.star_border:before{content:"\E83A"}.material-icons.star_half:before{content:"\E839"}.material-icons.star_outline:before{content:"\E83A"}.material-icons.stars:before{content:"\E8D0"}.material-icons.stay_current_landscape:before{content:"\E0D3"}.material-icons.stay_current_portrait:before{content:"\E0D4"}.material-icons.stay_primary_landscape:before{content:"\E0D5"}.material-icons.stay_primary_portrait:before{content:"\E0D6"}.material-icons.stop:before{content:"\E047"}.material-icons.stop_screen_share:before{content:"\E0E3"}.material-icons.storage:before{content:"\E1DB"}.material-icons.store:before{content:"\E8D1"}.material-icons.store_mall_directory:before{content:"\E563"}.material-icons.storefront:before{content:"\EA12"}.material-icons.straighten:before{content:"\E41C"}.material-icons.stream:before{content:"\E9E9"}.material-icons.streetview:before{content:"\E56E"}.material-icons.strikethrough_s:before{content:"\E257"}.material-icons.style:before{content:"\E41D"}.material-icons.subdirectory_arrow_left:before{content:"\E5D9"}.material-icons.subdirectory_arrow_right:before{content:"\E5DA"}.material-icons.subject:before{content:"\E8D2"}.material-icons.subscriptions:before{content:"\E064"}.material-icons.subtitles:before{content:"\E048"}.material-icons.subway:before{content:"\E56F"}.material-icons.supervised_user_circle:before{content:"\E939"}.material-icons.supervisor_account:before{content:"\E8D3"}.material-icons.surround_sound:before{content:"\E049"}.material-icons.swap_calls:before{content:"\E0D7"}.material-icons.swap_horiz:before{content:"\E8D4"}.material-icons.swap_horizontal_circle:before{content:"\E933"}.material-icons.swap_vert:before{content:"\E8D5"}.material-icons.swap_vert_circle:before,.material-icons.swap_vertical_circle:before{content:"\E8D6"}.material-icons.swipe:before{content:"\E9EC"}.material-icons.switch_account:before{content:"\E9ED"}.material-icons.switch_camera:before{content:"\E41E"}.material-icons.switch_video:before{content:"\E41F"}.material-icons.sync:before{content:"\E627"}.material-icons.sync_alt:before{content:"\EA18"}.material-icons.sync_disabled:before{content:"\E628"}.material-icons.sync_problem:before{content:"\E629"}.material-icons.system_update:before{content:"\E62A"}.material-icons.system_update_alt:before,.material-icons.system_update_tv:before{content:"\E8D7"}.material-icons.tab:before{content:"\E8D8"}.material-icons.tab_unselected:before{content:"\E8D9"}.material-icons.table_chart:before{content:"\E265"}.material-icons.tablet:before{content:"\E32F"}.material-icons.tablet_android:before{content:"\E330"}.material-icons.tablet_mac:before{content:"\E331"}.material-icons.tag:before{content:"\E9EF"}.material-icons.tag_faces:before{content:"\E420"}.material-icons.takeout_dining:before{content:"\EA74"}.material-icons.tap_and_play:before{content:"\E62B"}.material-icons.terrain:before{content:"\E564"}.material-icons.text_fields:before{content:"\E262"}.material-icons.text_format:before{content:"\E165"}.material-icons.text_rotate_up:before{content:"\E93A"}.material-icons.text_rotate_vertical:before{content:"\E93B"}.material-icons.text_rotation_angledown:before{content:"\E93C"}.material-icons.text_rotation_angleup:before{content:"\E93D"}.material-icons.text_rotation_down:before{content:"\E93E"}.material-icons.text_rotation_none:before{content:"\E93F"}.material-icons.textsms:before{content:"\E0D8"}.material-icons.texture:before{content:"\E421"}.material-icons.theater_comedy:before{content:"\EA66"}.material-icons.theaters:before{content:"\E8DA"}.material-icons.thumb_down:before{content:"\E8DB"}.material-icons.thumb_down_alt:before{content:"\E816"}.material-icons.thumb_down_off_alt:before{content:"\E9F2"}.material-icons.thumb_up:before{content:"\E8DC"}.material-icons.thumb_up_alt:before{content:"\E817"}.material-icons.thumb_up_off_alt:before{content:"\E9F3"}.material-icons.thumbs_up_down:before{content:"\E8DD"}.material-icons.time_to_leave:before{content:"\E62C"}.material-icons.timelapse:before{content:"\E422"}.material-icons.timeline:before{content:"\E922"}.material-icons.timer:before{content:"\E425"}.material-icons.timer_10:before{content:"\E423"}.material-icons.timer_3:before{content:"\E424"}.material-icons.timer_off:before{content:"\E426"}.material-icons.title:before{content:"\E264"}.material-icons.toc:before{content:"\E8DE"}.material-icons.today:before{content:"\E8DF"}.material-icons.toggle_off:before{content:"\E9F5"}.material-icons.toggle_on:before{content:"\E9F6"}.material-icons.toll:before{content:"\E8E0"}.material-icons.tonality:before{content:"\E427"}.material-icons.touch_app:before{content:"\E913"}.material-icons.toys:before{content:"\E332"}.material-icons.track_changes:before{content:"\E8E1"}.material-icons.traffic:before{content:"\E565"}.material-icons.train:before{content:"\E570"}.material-icons.tram:before{content:"\E571"}.material-icons.transfer_within_a_station:before{content:"\E572"}.material-icons.transform:before{content:"\E428"}.material-icons.transit_enterexit:before{content:"\E579"}.material-icons.translate:before{content:"\E8E2"}.material-icons.trending_down:before{content:"\E8E3"}.material-icons.trending_flat:before,.material-icons.trending_neutral:before{content:"\E8E4"}.material-icons.trending_up:before{content:"\E8E5"}.material-icons.trip_origin:before{content:"\E57B"}.material-icons.tune:before{content:"\E429"}.material-icons.turned_in:before{content:"\E8E6"}.material-icons.turned_in_not:before{content:"\E8E7"}.material-icons.tv:before{content:"\E333"}.material-icons.tv_off:before{content:"\E647"}.material-icons.two_wheeler:before{content:"\E9F9"}.material-icons.unarchive:before{content:"\E169"}.material-icons.undo:before{content:"\E166"}.material-icons.unfold_less:before{content:"\E5D6"}.material-icons.unfold_more:before{content:"\E5D7"}.material-icons.unsubscribe:before{content:"\E0EB"}.material-icons.update:before{content:"\E923"}.material-icons.upload_file:before{content:"\E9FC"}.material-icons.usb:before{content:"\E1E0"}.material-icons.verified_user:before{content:"\E8E8"}.material-icons.vertical_align_bottom:before{content:"\E258"}.material-icons.vertical_align_center:before{content:"\E259"}.material-icons.vertical_align_top:before{content:"\E25A"}.material-icons.vertical_split:before{content:"\E949"}.material-icons.vibration:before{content:"\E62D"}.material-icons.video_call:before{content:"\E070"}.material-icons.video_collection:before{content:"\E04A"}.material-icons.video_label:before{content:"\E071"}.material-icons.video_library:before{content:"\E04A"}.material-icons.videocam:before{content:"\E04B"}.material-icons.videocam_off:before{content:"\E04C"}.material-icons.videogame_asset:before{content:"\E338"}.material-icons.view_agenda:before{content:"\E8E9"}.material-icons.view_array:before{content:"\E8EA"}.material-icons.view_carousel:before{content:"\E8EB"}.material-icons.view_column:before{content:"\E8EC"}.material-icons.view_comfortable:before,.material-icons.view_comfy:before{content:"\E42A"}.material-icons.view_compact:before{content:"\E42B"}.material-icons.view_day:before{content:"\E8ED"}.material-icons.view_headline:before{content:"\E8EE"}.material-icons.view_in_ar:before{content:"\E9FE"}.material-icons.view_list:before{content:"\E8EF"}.material-icons.view_module:before{content:"\E8F0"}.material-icons.view_quilt:before{content:"\E8F1"}.material-icons.view_stream:before{content:"\E8F2"}.material-icons.view_week:before{content:"\E8F3"}.material-icons.vignette:before{content:"\E435"}.material-icons.visibility:before{content:"\E8F4"}.material-icons.visibility_off:before{content:"\E8F5"}.material-icons.voice_chat:before{content:"\E62E"}.material-icons.voice_over_off:before{content:"\E94A"}.material-icons.voicemail:before{content:"\E0D9"}.material-icons.volume_down:before{content:"\E04D"}.material-icons.volume_mute:before{content:"\E04E"}.material-icons.volume_off:before{content:"\E04F"}.material-icons.volume_up:before{content:"\E050"}.material-icons.volunteer_activism:before{content:"\EA70"}.material-icons.vpn_key:before{content:"\E0DA"}.material-icons.vpn_lock:before{content:"\E62F"}.material-icons.wallet_giftcard:before{content:"\E8F6"}.material-icons.wallet_membership:before{content:"\E8F7"}.material-icons.wallet_travel:before{content:"\E8F8"}.material-icons.wallpaper:before{content:"\E1BC"}.material-icons.warning:before{content:"\E002"}.material-icons.watch:before{content:"\E334"}.material-icons.watch_later:before{content:"\E924"}.material-icons.waterfall_chart:before{content:"\EA00"}.material-icons.waves:before{content:"\E176"}.material-icons.wb_auto:before{content:"\E42C"}.material-icons.wb_cloudy:before{content:"\E42D"}.material-icons.wb_incandescent:before{content:"\E42E"}.material-icons.wb_iridescent:before{content:"\E436"}.material-icons.wb_shade:before{content:"\EA01"}.material-icons.wb_sunny:before{content:"\E430"}.material-icons.wb_twighlight:before{content:"\EA02"}.material-icons.wc:before{content:"\E63D"}.material-icons.web:before{content:"\E051"}.material-icons.web_asset:before{content:"\E069"}.material-icons.weekend:before{content:"\E16B"}.material-icons.whatshot:before{content:"\E80E"}.material-icons.where_to_vote:before{content:"\E177"}.material-icons.widgets:before{content:"\E1BD"}.material-icons.wifi:before{content:"\E63E"}.material-icons.wifi_lock:before{content:"\E1E1"}.material-icons.wifi_off:before{content:"\E648"}.material-icons.wifi_tethering:before{content:"\E1E2"}.material-icons.work:before{content:"\E8F9"}.material-icons.work_off:before{content:"\E942"}.material-icons.work_outline:before{content:"\E943"}.material-icons.workspaces_filled:before{content:"\EA0D"}.material-icons.workspaces_outline:before{content:"\EA0F"}.material-icons.wrap_text:before{content:"\E25B"}.material-icons.youtube_searched_for:before{content:"\E8FA"}.material-icons.zoom_in:before{content:"\E8FF"}.material-icons.zoom_out:before{content:"\E900"}.material-icons.zoom_out_map:before{content:"\E56B"}@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-feed{background-color:#000;color:#fff;padding:.2em .5em}.icon-feed:before{position:relative;top:2px}.list-tags li{margin-bottom:.5em}.list-tags .icon-feed:focus,.list-tags .icon-feed: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-feed: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 only print{header h1.logo{display:none}}@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 436294d31..77249f12b 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("