]> git.immae.eu Git - github/wallabag/wallabag.git/commitdiff
Merge remote-tracking branch 'origin/master' into 2.2
authorJeremy Benoist <jeremy.benoist@gmail.com>
Mon, 24 Oct 2016 10:03:17 +0000 (12:03 +0200)
committerJeremy Benoist <jeremy.benoist@gmail.com>
Mon, 24 Oct 2016 10:03:17 +0000 (12:03 +0200)
47 files changed:
app/DoctrineMigrations/Version20160812120952.php
app/DoctrineMigrations/Version20161001072726.php [new file with mode: 0644]
app/DoctrineMigrations/Version20161022134138.php [new file with mode: 0644]
app/config/config.yml
app/config/config_test.yml
app/config/parameters.yml.dist
app/config/parameters_test.yml
app/config/routing_rest.yml
app/config/tests/parameters_test.mysql.yml
app/config/tests/parameters_test.pgsql.yml
app/config/tests/parameters_test.sqlite.yml
composer.json
docs/en/user/configuration.rst
docs/en/user/parameters.rst
docs/fr/user/configuration.rst
src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php
src/Wallabag/AnnotationBundle/Entity/Annotation.php
src/Wallabag/AnnotationBundle/Repository/AnnotationRepository.php
src/Wallabag/ApiBundle/Controller/WallabagRestController.php
src/Wallabag/ApiBundle/Resources/config/routing_rest.yml
src/Wallabag/CoreBundle/Command/InstallCommand.php
src/Wallabag/CoreBundle/Controller/ConfigController.php
src/Wallabag/CoreBundle/Controller/TagController.php
src/Wallabag/CoreBundle/Entity/Entry.php
src/Wallabag/CoreBundle/Repository/EntryRepository.php
src/Wallabag/CoreBundle/Repository/TagRepository.php
src/Wallabag/CoreBundle/Resources/config/services.yml
src/Wallabag/CoreBundle/Resources/translations/messages.da.yml
src/Wallabag/CoreBundle/Resources/translations/messages.de.yml
src/Wallabag/CoreBundle/Resources/translations/messages.en.yml
src/Wallabag/CoreBundle/Resources/translations/messages.es.yml
src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml
src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml
src/Wallabag/CoreBundle/Resources/translations/messages.it.yml
src/Wallabag/CoreBundle/Resources/translations/messages.oc.yml
src/Wallabag/CoreBundle/Resources/translations/messages.pl.yml
src/Wallabag/CoreBundle/Resources/translations/messages.ro.yml
src/Wallabag/CoreBundle/Resources/translations/messages.tr.yml
src/Wallabag/CoreBundle/Resources/views/themes/baggy/Config/index.html.twig
src/Wallabag/CoreBundle/Resources/views/themes/material/Config/index.html.twig
src/Wallabag/CoreBundle/Subscriber/SQLiteCascadeDeleteSubscriber.php [new file with mode: 0644]
src/Wallabag/UserBundle/Repository/UserRepository.php
src/Wallabag/UserBundle/Resources/config/services.yml
tests/Wallabag/AnnotationBundle/Controller/AnnotationControllerTest.php
tests/Wallabag/AnnotationBundle/WallabagAnnotationTestCase.php
tests/Wallabag/ApiBundle/Controller/WallabagRestControllerTest.php
tests/Wallabag/CoreBundle/Controller/ConfigControllerTest.php

index 3aafea644ed1ac3c83338617d4a9cabafc24a269..39423e2f56d1a9eb8004bc01bb82f373435bb8c2 100644 (file)
@@ -33,9 +33,11 @@ class Version20160812120952 extends AbstractMigration implements ContainerAwareI
             case 'sqlite':
                 $this->addSql('ALTER TABLE '.$this->getTable('oauth2_clients').' ADD name longtext DEFAULT NULL');
                 break;
+
             case 'mysql':
                 $this->addSql('ALTER TABLE '.$this->getTable('oauth2_clients').' ADD name longtext COLLATE \'utf8_unicode_ci\' DEFAULT NULL');
                 break;
+
             case 'postgresql':
                 $this->addSql('ALTER TABLE '.$this->getTable('oauth2_clients').' ADD name text DEFAULT NULL');
         }
diff --git a/app/DoctrineMigrations/Version20161001072726.php b/app/DoctrineMigrations/Version20161001072726.php
new file mode 100644 (file)
index 0000000..237db93
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+
+namespace Application\Migrations;
+
+use Doctrine\DBAL\Migrations\AbstractMigration;
+use Doctrine\DBAL\Schema\Schema;
+use Symfony\Component\DependencyInjection\ContainerAwareInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class Version20161001072726 extends AbstractMigration implements ContainerAwareInterface
+{
+    /**
+     * @var ContainerInterface
+     */
+    private $container;
+
+    public function setContainer(ContainerInterface $container = null)
+    {
+        $this->container = $container;
+    }
+
+    private function getTable($tableName)
+    {
+        return $this->container->getParameter('database_table_prefix') . $tableName;
+    }
+
+    /**
+     * @param Schema $schema
+     */
+    public function up(Schema $schema)
+    {
+        $this->skipIf($this->connection->getDatabasePlatform()->getName() == 'sqlite', 'Migration can only be executed safely on \'mysql\' or \'postgresql\'.');
+
+        // remove all FK from entry_tag
+        $query = $this->connection->query("SELECT CONSTRAINT_NAME FROM information_schema.key_column_usage WHERE TABLE_NAME = '".$this->getTable('entry_tag')."' AND CONSTRAINT_NAME LIKE 'FK_%' AND TABLE_SCHEMA = '".$this->connection->getDatabase()."'");
+        $query->execute();
+
+        foreach ($query->fetchAll() as $fk) {
+            $this->addSql('ALTER TABLE '.$this->getTable('entry_tag').' DROP FOREIGN KEY '.$fk['CONSTRAINT_NAME']);
+        }
+
+        $this->addSql('ALTER TABLE '.$this->getTable('entry_tag').' ADD CONSTRAINT FK_entry_tag_entry FOREIGN KEY (entry_id) REFERENCES '.$this->getTable('entry').' (id) ON DELETE CASCADE');
+        $this->addSql('ALTER TABLE '.$this->getTable('entry_tag').' ADD CONSTRAINT FK_entry_tag_tag FOREIGN KEY (tag_id) REFERENCES '.$this->getTable('tag').' (id) ON DELETE CASCADE');
+
+        // remove entry FK from annotation
+        $query = $this->connection->query("SELECT CONSTRAINT_NAME FROM information_schema.key_column_usage WHERE TABLE_NAME = '".$this->getTable('annotation')."' AND CONSTRAINT_NAME LIKE 'FK_%' and COLUMN_NAME = 'entry_id' AND TABLE_SCHEMA = '".$this->connection->getDatabase()."'");
+        $query->execute();
+
+        foreach ($query->fetchAll() as $fk) {
+            $this->addSql('ALTER TABLE '.$this->getTable('annotation').' DROP FOREIGN KEY '.$fk['CONSTRAINT_NAME']);
+        }
+
+        $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
new file mode 100644 (file)
index 0000000..5cce55a
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+
+namespace Application\Migrations;
+
+use Doctrine\DBAL\Migrations\AbstractMigration;
+use Doctrine\DBAL\Schema\Schema;
+use Symfony\Component\DependencyInjection\ContainerAwareInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class Version20161022134138 extends AbstractMigration implements ContainerAwareInterface
+{
+    /**
+     * @var ContainerInterface
+     */
+    private $container;
+
+    public function setContainer(ContainerInterface $container = null)
+    {
+        $this->container = $container;
+    }
+
+    private function getTable($tableName)
+    {
+        return $this->container->getParameter('database_table_prefix') . $tableName;
+    }
+
+    /**
+     * @param Schema $schema
+     */
+    public function up(Schema $schema)
+    {
+        $this->skipIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'This migration only apply to MySQL');
+
+        $this->addSql('ALTER DATABASE '.$this->container->getParameter('database_name').' CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;');
+
+        $this->addSql('ALTER TABLE '.$this->getTable('annotation').' CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
+        $this->addSql('ALTER TABLE '.$this->getTable('entry').' CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
+        $this->addSql('ALTER TABLE '.$this->getTable('tag').' CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
+        $this->addSql('ALTER TABLE '.$this->getTable('user').' CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
+
+        $this->addSql('ALTER TABLE '.$this->getTable('annotation').' CHANGE `text` `text` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
+        $this->addSql('ALTER TABLE '.$this->getTable('annotation').' CHANGE `quote` `quote` VARCHAR(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
+
+        $this->addSql('ALTER TABLE '.$this->getTable('entry').' CHANGE `title` `title` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
+        $this->addSql('ALTER TABLE '.$this->getTable('entry').' CHANGE `content` `content` longtext 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;');
+
+        $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($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'This migration only apply to MySQL');
+
+        $this->addSql('ALTER DATABASE '.$this->container->getParameter('database_name').' CHARACTER SET = utf8 COLLATE = utf8_unicode_ci;');
+
+        $this->addSql('ALTER TABLE '.$this->getTable('annotation').' CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;');
+        $this->addSql('ALTER TABLE '.$this->getTable('entry').' CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;');
+        $this->addSql('ALTER TABLE '.$this->getTable('tag').' CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;');
+        $this->addSql('ALTER TABLE '.$this->getTable('user').' CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;');
+
+        $this->addSql('ALTER TABLE '.$this->getTable('annotation').' CHANGE `text` `text` longtext CHARACTER SET utf8 COLLATE utf8_unicode_ci;');
+        $this->addSql('ALTER TABLE '.$this->getTable('annotation').' CHANGE `quote` `quote` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci;');
+
+        $this->addSql('ALTER TABLE '.$this->getTable('entry').' CHANGE `title` `title` longtext CHARACTER SET utf8 COLLATE utf8_unicode_ci;');
+        $this->addSql('ALTER TABLE '.$this->getTable('entry').' CHANGE `content` `content` longtext CHARACTER SET utf8 COLLATE utf8_unicode_ci;');
+
+        $this->addSql('ALTER TABLE '.$this->getTable('tag').' CHANGE `label` `label` longtext CHARACTER SET utf8 COLLATE utf8_unicode_ci;');
+
+        $this->addSql('ALTER TABLE '.$this->getTable('user').' CHANGE `name` `name` longtext CHARACTER SET utf8 COLLATE utf8_unicode_ci;');
+
+    }
+}
index a56cbdd9e01d2afab2947e3273cd62831991bdda..dfb0e3b27a181b4a313c248b95226a2a18400b3f 100644 (file)
@@ -76,7 +76,7 @@ doctrine:
         dbname: "%database_name%"
         user: "%database_user%"
         password: "%database_password%"
-        charset: UTF8
+        charset: "%database_charset%"
         path: "%database_path%"
         unix_socket: "%database_socket%"
         server_version: 5.6
@@ -113,12 +113,26 @@ swiftmailer:
 fos_rest:
     param_fetcher_listener: true
     body_listener: true
-    format_listener: true
     view:
+        mime_types:
+            csv:
+                - 'text/csv'
+                - 'text/plain'
+            pdf:
+                - 'application/pdf'
+            epub:
+                - 'application/epub+zip'
+            mobi:
+                - 'application/x-mobipocket-ebook'
         view_response_listener: 'force'
         formats:
             xml: true
-            json : true
+            json: true
+            txt: true
+            csv: true
+            pdf: true
+            epub: true
+            mobi: true
         templating_formats:
             html: true
         force_redirects:
@@ -127,10 +141,21 @@ fos_rest:
         default_engine: twig
     routing_loader:
         default_format: json
+    format_listener:
+        enabled: true
+        rules:
+            - { path: "^/api/entries/([0-9]+)/export.(.*)", priorities: ['epub', 'mobi', 'pdf', 'txt', 'csv'], fallback_format: false, prefer_extension: false }
+            - { path: "^/api", priorities: ['json', 'xml'], fallback_format: false, prefer_extension: false }
+            - { path: "^/annotations", priorities: ['json', 'xml'], fallback_format: false, prefer_extension: false }
+            # for an unknown reason, EACH REQUEST goes to FOS\RestBundle\EventListener\FormatListener
+            # so we need to add custom rule for custom api export but also for all other routes of the application...
+            - { path: '^/', priorities: ['text/html', '*/*'], fallback_format: html, prefer_extension: false }
 
 nelmio_api_doc:
     sandbox:
         enabled: false
+    cache:
+        enabled: true
     name: wallabag API documentation
 
 nelmio_cors:
index 3eab6fb2848f5afccf9c8b6b76ded8a8b845aa93..f5e2c25ead1bca8ccb1b9980fa48657f5c864c76 100644 (file)
@@ -28,7 +28,7 @@ doctrine:
         dbname: "%test_database_name%"
         user: "%test_database_user%"
         password: "%test_database_password%"
-        charset: UTF8
+        charset: "%test_database_charset%"
         path: "%test_database_path%"
     orm:
         metadata_cache_driver:
index ece4903a186ba8a3de077466fbac84a02e27b658..7a22cb9870fbe10faf13ff69062fca37a597ee61 100644 (file)
@@ -19,16 +19,18 @@ parameters:
     database_path: "%kernel.root_dir%/../data/db/wallabag.sqlite"
     database_table_prefix: wallabag_
     database_socket: null
+    # with MySQL, use "utf8mb4" if you got problem with content with emojis
+    database_charset: utf8
 
-    mailer_transport:  smtp
-    mailer_host:       127.0.0.1
-    mailer_user:       ~
-    mailer_password:   ~
+    mailer_transport: smtp
+    mailer_host: 127.0.0.1
+    mailer_user: ~
+    mailer_password: ~
 
-    locale:            en
+    locale: en
 
     # A secret key that's used to generate certain security-related tokens
-    secret:            ovmpmAWXRCabNlMgzlzFXDYmCFfzGv
+    secret: ovmpmAWXRCabNlMgzlzFXDYmCFfzGv
 
     # two factor stuff
     twofactor_auth: true
index 2943b27a75338643eac98b59b1e48d21146475ee..5f2e25bb9bb13ebe021a6e3ca6c00a7d11f51e6b 100644 (file)
@@ -6,3 +6,4 @@ parameters:
     test_database_user: null
     test_database_password: null
     test_database_path: '%kernel.root_dir%/../data/db/wallabag_test.sqlite'
+    test_database_charset: utf8
index 52d395dd9029066c18bf1e9483057a15d2601f47..29f4ab14c3ca09f35f201b611acfd6cc42b9d9f6 100644 (file)
@@ -1,4 +1,3 @@
 Rest_Wallabag:
-  type : rest
-  resource: "@WallabagApiBundle/Resources/config/routing_rest.yml"
-
+    type : rest
+    resource: "@WallabagApiBundle/Resources/config/routing_rest.yml"
index d8512845fa6ca4f462dbe5a764ec20b7ed85d6fc..bca2d466364588c62dd13a4d2649d3708a2403d8 100644 (file)
@@ -6,3 +6,4 @@ parameters:
     test_database_user: root
     test_database_password: ~
     test_database_path: ~
+    test_database_charset: utf8mb4
index 41383868d5be06b2ffe9a931d61a9ee8cf957b61..3e18d4a0395f41fedced20f96fe9ee6241cb9e64 100644 (file)
@@ -6,3 +6,4 @@ parameters:
     test_database_user: travis
     test_database_password: ~
     test_database_path: ~
+    test_database_charset: utf8
index 1952e3a61135340aa66c461c53c53e0fbe1b3332..b8a5f41a709020ae1e59a3d47f15dd10f47732a5 100644 (file)
@@ -6,3 +6,4 @@ parameters:
     test_database_user: ~
     test_database_password: ~
     test_database_path: "%kernel.root_dir%/../data/db/wallabag_test.sqlite"
+    test_database_charset: utf8
index 79de337bada8f38cbd5a9e69c393f5be993a0bfc..ebc0a7dc555de13f281d62a4ba23b4760adaa777 100644 (file)
         "sensio/framework-extra-bundle": "^3.0.2",
         "incenteev/composer-parameter-handler": "^2.0",
         "nelmio/cors-bundle": "~1.4.0",
-        "friendsofsymfony/rest-bundle": "~1.4",
-        "jms/serializer-bundle": "~1.0",
+        "friendsofsymfony/rest-bundle": "~2.1",
+        "jms/serializer-bundle": "~1.1",
         "nelmio/api-doc-bundle": "~2.7",
         "mgargano/simplehtmldom": "~1.5",
-        "tecnickcom/tcpdf": "~6.2",
+        "tecnickcom/tc-lib-pdf": "dev-master",
         "simplepie/simplepie": "~1.3.1",
         "willdurand/hateoas-bundle": "~1.0",
         "htmlawed/htmlawed": "~1.1.19",
index f74924dfe39da2c845a3312036881f0c4a80903b..e7055a145abb7261ce7a0703a6e84b810e418216 100644 (file)
@@ -50,6 +50,8 @@ User information
 
 You can change your name, your email address and enable ``Two factor authentication``.
 
+If the wallabag instance has more than one enabled user, you can delete your account here. **Take care, we delete all your data**.
+
 Two factor authentication
 ~~~~~~~~~~~~~~~~~~~~~~~~~
 
index 6cbd5ae4ba3dea4c7e6947eb48bb3276289c4a80..c35cf3b8ae6026423738ef1302e3b0efe4472a64 100644 (file)
@@ -12,6 +12,7 @@ What is the meaning of the parameters?
    "database_path", "``""%kernel.root_dir%/../data/db/wallabag.sqlite""``", "only for SQLite, define where to put the database file. Leave it empty for other database"
    "database_table_prefix", "wallabag_", "all wallabag's tables will be prefixed with that string. You can include a ``_`` for clarity"
    "database_socket", "null", "If your database is using a socket instead of tcp, put the path of the socket (other connection parameters will then be ignored)"
+   "database_charset", "utf8mb4", "For PostgreSQL & SQLite you should use utf8, for MySQL use utf8mb4 which handle emoji"
 
 .. csv-table:: Configuration to send emails from wallabag
    :header: "name", "default", "description"
index 8bfe66f54d1501916224a8b586e78ceaeda697db..90eece112c5fa5960b217a0b788507621d988049 100644 (file)
@@ -51,6 +51,8 @@ Mon compte
 
 Vous pouvez ici modifier votre nom, votre adresse email et activer la ``Double authentification``.
 
+Si l'instance de wallabag compte plus d'un utilisateur actif, vous pouvez supprimer ici votre compte. **Attention, nous supprimons toutes vos données**.
+
 Double authentification (2FA)
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
index ad083e31c6fa2a29d6852cc6f001b2961e68f815..c13a034ffe58f27c2f7fab0a388ef0e686fed4b6 100644 (file)
@@ -3,9 +3,8 @@
 namespace Wallabag\AnnotationBundle\Controller;
 
 use FOS\RestBundle\Controller\FOSRestController;
-use Nelmio\ApiDocBundle\Annotation\ApiDoc;
+use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
 use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
 use Wallabag\AnnotationBundle\Entity\Annotation;
 use Wallabag\CoreBundle\Entity\Entry;
@@ -15,42 +14,35 @@ class WallabagAnnotationController extends FOSRestController
     /**
      * Retrieve annotations for an entry.
      *
-     * @ApiDoc(
-     *      requirements={
-     *          {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
-     *      }
-     * )
+     * @param Entry $entry
+     *
+     * @see Wallabag\ApiBundle\Controller\WallabagRestController
      *
-     * @return Response
+     * @return JsonResponse
      */
     public function getAnnotationsAction(Entry $entry)
     {
         $annotationRows = $this
-                ->getDoctrine()
-                ->getRepository('WallabagAnnotationBundle:Annotation')
-                ->findAnnotationsByPageId($entry->getId(), $this->getUser()->getId());
+            ->getDoctrine()
+            ->getRepository('WallabagAnnotationBundle:Annotation')
+            ->findAnnotationsByPageId($entry->getId(), $this->getUser()->getId());
         $total = count($annotationRows);
         $annotations = ['total' => $total, 'rows' => $annotationRows];
 
         $json = $this->get('serializer')->serialize($annotations, 'json');
 
-        return $this->renderJsonResponse($json);
+        return (new JsonResponse())->setJson($json);
     }
 
     /**
      * Creates a new annotation.
      *
-     * @param Entry $entry
+     * @param Request $request
+     * @param Entry   $entry
      *
-     * @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"=""},
-     *      }
-     * )
+     * @return JsonResponse
      *
-     * @return Response
+     * @see Wallabag\ApiBundle\Controller\WallabagRestController
      */
     public function postAnnotationAction(Request $request, Entry $entry)
     {
@@ -75,21 +67,20 @@ class WallabagAnnotationController extends FOSRestController
 
         $json = $this->get('serializer')->serialize($annotation, 'json');
 
-        return $this->renderJsonResponse($json);
+        return (new JsonResponse())->setJson($json);
     }
 
     /**
      * Updates an annotation.
      *
-     * @ApiDoc(
-     *      requirements={
-     *          {"name"="annotation", "dataType"="string", "requirement"="\w+", "description"="The annotation ID"}
-     *      }
-     * )
+     * @see Wallabag\ApiBundle\Controller\WallabagRestController
      *
      * @ParamConverter("annotation", class="WallabagAnnotationBundle:Annotation")
      *
-     * @return Response
+     * @param Annotation $annotation
+     * @param Request    $request
+     *
+     * @return JsonResponse
      */
     public function putAnnotationAction(Annotation $annotation, Request $request)
     {
@@ -104,21 +95,19 @@ class WallabagAnnotationController extends FOSRestController
 
         $json = $this->get('serializer')->serialize($annotation, 'json');
 
-        return $this->renderJsonResponse($json);
+        return (new JsonResponse())->setJson($json);
     }
 
     /**
      * Removes an annotation.
      *
-     * @ApiDoc(
-     *      requirements={
-     *          {"name"="annotation", "dataType"="string", "requirement"="\w+", "description"="The annotation ID"}
-     *      }
-     * )
+     * @see Wallabag\ApiBundle\Controller\WallabagRestController
      *
      * @ParamConverter("annotation", class="WallabagAnnotationBundle:Annotation")
      *
-     * @return Response
+     * @param Annotation $annotation
+     *
+     * @return JsonResponse
      */
     public function deleteAnnotationAction(Annotation $annotation)
     {
@@ -128,19 +117,6 @@ class WallabagAnnotationController extends FOSRestController
 
         $json = $this->get('serializer')->serialize($annotation, 'json');
 
-        return $this->renderJsonResponse($json);
-    }
-
-    /**
-     * Send a JSON Response.
-     * We don't use the Symfony JsonRespone, because it takes an array as parameter instead of a JSON string.
-     *
-     * @param string $json
-     *
-     * @return Response
-     */
-    private function renderJsonResponse($json, $code = 200)
-    {
-        return new Response($json, $code, ['application/json']);
+        return (new JsonResponse())->setJson($json);
     }
 }
index c48d873100e2a38a25fa37e06a04a465ea49f0c1..0838f5aa9153cb2a4c84f53fdd67779ee9828cad 100644 (file)
@@ -82,7 +82,7 @@ class Annotation
      * @Exclude
      *
      * @ORM\ManyToOne(targetEntity="Wallabag\CoreBundle\Entity\Entry", inversedBy="annotations")
-     * @ORM\JoinColumn(name="entry_id", referencedColumnName="id")
+     * @ORM\JoinColumn(name="entry_id", referencedColumnName="id", onDelete="cascade")
      */
     private $entry;
 
index 5f7da70ecfa4cb1d375fdbc94f68d9a019c2c4ce..8d3f07eef350f2eb8ebf12a05b80deb5f6d026fb 100644 (file)
@@ -50,7 +50,8 @@ class AnnotationRepository extends EntityRepository
     {
         return $this->createQueryBuilder('a')
             ->andWhere('a.id = :annotationId')->setParameter('annotationId', $annotationId)
-            ->getQuery()->getSingleResult()
+            ->getQuery()
+            ->getSingleResult()
         ;
     }
 
@@ -67,7 +68,8 @@ class AnnotationRepository extends EntityRepository
         return $this->createQueryBuilder('a')
             ->where('a.entry = :entryId')->setParameter('entryId', $entryId)
             ->andwhere('a.user = :userId')->setParameter('userId', $userId)
-            ->getQuery()->getResult()
+            ->getQuery()
+            ->getResult()
         ;
     }
 
@@ -106,4 +108,18 @@ class AnnotationRepository extends EntityRepository
             ->getQuery()
             ->getSingleResult();
     }
+
+    /**
+     * Remove all annotations for a user id.
+     * Used when a user want to reset all informations.
+     *
+     * @param int $userId
+     */
+    public function removeAllByUserId($userId)
+    {
+        $this->getEntityManager()
+            ->createQuery('DELETE FROM Wallabag\AnnotationBundle\Entity\Annotation a WHERE a.user = :userId')
+            ->setParameter('userId', $userId)
+            ->execute();
+    }
 }
index 9997913d2afefbd7c0f03143b17640470f1a42ae..a73d44ca2bc35f79be45dbe004df022b552baa22 100644 (file)
@@ -3,15 +3,17 @@
 namespace Wallabag\ApiBundle\Controller;
 
 use FOS\RestBundle\Controller\FOSRestController;
-use Hateoas\Configuration\Route;
+use Hateoas\Configuration\Route as HateoasRoute;
 use Hateoas\Representation\Factory\PagerfantaFactory;
 use Nelmio\ApiDocBundle\Annotation\ApiDoc;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 use Symfony\Component\Security\Core\Exception\AccessDeniedException;
 use Wallabag\CoreBundle\Entity\Entry;
 use Wallabag\CoreBundle\Entity\Tag;
+use Wallabag\AnnotationBundle\Entity\Annotation;
 
 class WallabagRestController extends FOSRestController
 {
@@ -115,7 +117,7 @@ class WallabagRestController extends FOSRestController
         $pagerfantaFactory = new PagerfantaFactory('page', 'perPage');
         $paginatedCollection = $pagerfantaFactory->createRepresentation(
             $pager,
-            new Route(
+            new HateoasRoute(
                 'api_get_entries',
                 [
                     'archive' => $isArchived,
@@ -157,6 +159,28 @@ class WallabagRestController extends FOSRestController
         return (new JsonResponse())->setJson($json);
     }
 
+    /**
+     * Retrieve a single entry as a predefined format.
+     *
+     * @ApiDoc(
+     *      requirements={
+     *          {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
+     *      }
+     * )
+     *
+     * @return Response
+     */
+    public function getEntryExportAction(Entry $entry, Request $request)
+    {
+        $this->validateAuthentication();
+        $this->validateUserAccess($entry->getUser()->getId());
+
+        return $this->get('wallabag_core.helper.entries_export')
+            ->setEntries($entry)
+            ->updateTitle('entry')
+            ->exportAs($request->attributes->get('_format'));
+    }
+
     /**
      * Create an entry.
      *
@@ -495,6 +519,104 @@ class WallabagRestController extends FOSRestController
         return (new JsonResponse())->setJson($json);
     }
 
+    /**
+     * Retrieve annotations for an entry.
+     *
+     * @ApiDoc(
+     *      requirements={
+     *          {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
+     *      }
+     * )
+     *
+     * @param Entry $entry
+     *
+     * @return JsonResponse
+     */
+    public function getAnnotationsAction(Entry $entry)
+    {
+        $this->validateAuthentication();
+
+        return $this->forward('WallabagAnnotationBundle:WallabagAnnotation:getAnnotations', [
+            'entry' => $entry,
+        ]);
+    }
+
+    /**
+     * Creates a new annotation.
+     *
+     * @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"=""},
+     *      }
+     * )
+     *
+     * @param Request $request
+     * @param Entry   $entry
+     *
+     * @return JsonResponse
+     */
+    public function postAnnotationAction(Request $request, Entry $entry)
+    {
+        $this->validateAuthentication();
+
+        return $this->forward('WallabagAnnotationBundle:WallabagAnnotation:postAnnotation', [
+            'request' => $request,
+            'entry' => $entry,
+        ]);
+    }
+
+    /**
+     * Updates an annotation.
+     *
+     * @ApiDoc(
+     *      requirements={
+     *          {"name"="annotation", "dataType"="string", "requirement"="\w+", "description"="The annotation ID"}
+     *      }
+     * )
+     *
+     * @ParamConverter("annotation", class="WallabagAnnotationBundle:Annotation")
+     *
+     * @param Annotation $annotation
+     * @param Request    $request
+     *
+     * @return JsonResponse
+     */
+    public function putAnnotationAction(Annotation $annotation, Request $request)
+    {
+        $this->validateAuthentication();
+
+        return $this->forward('WallabagAnnotationBundle:WallabagAnnotation:putAnnotation', [
+            'annotation' => $annotation,
+            'request' => $request,
+        ]);
+    }
+
+    /**
+     * Removes an annotation.
+     *
+     * @ApiDoc(
+     *      requirements={
+     *          {"name"="annotation", "dataType"="string", "requirement"="\w+", "description"="The annotation ID"}
+     *      }
+     * )
+     *
+     * @ParamConverter("annotation", class="WallabagAnnotationBundle:Annotation")
+     *
+     * @param Annotation $annotation
+     *
+     * @return JsonResponse
+     */
+    public function deleteAnnotationAction(Annotation $annotation)
+    {
+        $this->validateAuthentication();
+
+        return $this->forward('WallabagAnnotationBundle:WallabagAnnotation:deleteAnnotation', [
+            'annotation' => $annotation,
+        ]);
+    }
+
     /**
      * Retrieve version number.
      *
index 5f43f9716a46fb31b206196658da0de9e45cc954..35f8b2c1106c3525a720670910fb560a1b58ce3b 100644 (file)
@@ -1,4 +1,4 @@
-entries:
-  type: rest
-  resource:     "WallabagApiBundle:WallabagRest"
-  name_prefix:  api_
+api:
+    type: rest
+    resource: "WallabagApiBundle:WallabagRest"
+    name_prefix:  api_
index 857a8b4cfb2abed26a449d8ee270b8a4b9a224ab..277f852422f6d14afea83df85bbd2023dae31cfd 100644 (file)
@@ -40,7 +40,7 @@ class InstallCommand extends ContainerAwareCommand
     {
         $this
             ->setName('wallabag:install')
-            ->setDescription('Wallabag installer.')
+            ->setDescription('wallabag installer.')
             ->addOption(
                'reset',
                null,
@@ -55,7 +55,7 @@ class InstallCommand extends ContainerAwareCommand
         $this->defaultInput = $input;
         $this->defaultOutput = $output;
 
-        $output->writeln('<info>Installing Wallabag...</info>');
+        $output->writeln('<info>Installing wallabag...</info>');
         $output->writeln('');
 
         $this
@@ -65,7 +65,7 @@ class InstallCommand extends ContainerAwareCommand
             ->setupConfig()
         ;
 
-        $output->writeln('<info>Wallabag has been successfully installed.</info>');
+        $output->writeln('<info>wallabag has been successfully installed.</info>');
         $output->writeln('<comment>Just execute `php bin/console server:run --env=prod` for using wallabag: http://localhost:8000</comment>');
     }
 
@@ -77,7 +77,7 @@ class InstallCommand extends ContainerAwareCommand
 
         // testing if database driver exists
         $fulfilled = true;
-        $label = '<comment>PDO Driver</comment>';
+        $label = '<comment>PDO Driver (%s)</comment>';
         $status = '<info>OK!</info>';
         $help = '';
 
@@ -87,7 +87,7 @@ class InstallCommand extends ContainerAwareCommand
             $help = 'Database driver "'.$this->getContainer()->getParameter('database_driver').'" is not installed.';
         }
 
-        $rows[] = [$label, $status, $help];
+        $rows[] = [sprintf($label, $this->getContainer()->getParameter('database_driver')), $status, $help];
 
         // testing if connection to the database can be etablished
         $label = '<comment>Database connection</comment>';
@@ -95,7 +95,8 @@ class InstallCommand extends ContainerAwareCommand
         $help = '';
 
         try {
-            $this->getContainer()->get('doctrine')->getManager()->getConnection()->connect();
+            $conn = $this->getContainer()->get('doctrine')->getManager()->getConnection();
+            $conn->connect();
         } catch (\Exception $e) {
             if (false === strpos($e->getMessage(), 'Unknown database')
                 && false === strpos($e->getMessage(), 'database "'.$this->getContainer()->getParameter('database_name').'" does not exist')) {
@@ -107,6 +108,21 @@ class InstallCommand extends ContainerAwareCommand
 
         $rows[] = [$label, $status, $help];
 
+        // now check if MySQL isn't too old to handle utf8mb4
+        if ($conn->isConnected() && $conn->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\MySqlPlatform) {
+            $version = $conn->query('select version()')->fetchColumn();
+            $minimalVersion = '5.5.4';
+
+            if (false === version_compare($version, $minimalVersion, '>')) {
+                $fulfilled = false;
+                $rows[] = [
+                    '<comment>Database version</comment>',
+                    '<error>ERROR!</error>',
+                    'Your MySQL version ('.$version.') is too old, consider upgrading ('.$minimalVersion.'+).',
+                ];
+            }
+        }
+
         foreach ($this->functionExists as $functionRequired) {
             $label = '<comment>'.$functionRequired.'</comment>';
             $status = '<info>OK!</info>';
@@ -131,7 +147,7 @@ class InstallCommand extends ContainerAwareCommand
             throw new \RuntimeException('Some system requirements are not fulfilled. Please check output messages and fix them.');
         }
 
-        $this->defaultOutput->writeln('<info>Success! Your system can run Wallabag properly.</info>');
+        $this->defaultOutput->writeln('<info>Success! Your system can run wallabag properly.</info>');
 
         $this->defaultOutput->writeln('');
 
index 91cdcae506fe75f2176f84fbe3c31911c29d3a66..8d391917ad83016fc2adf2cdc63fb38fc29fc875 100644 (file)
@@ -7,6 +7,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 use Wallabag\CoreBundle\Entity\Config;
 use Wallabag\CoreBundle\Entity\TaggingRule;
 use Wallabag\CoreBundle\Form\Type\ConfigType;
@@ -148,6 +149,9 @@ class ConfigController extends Controller
                 'token' => $config->getRssToken(),
             ],
             'twofactor_auth' => $this->getParameter('twofactor_auth'),
+            'enabled_users' => $this->getDoctrine()
+                ->getRepository('WallabagUserBundle:User')
+                ->getSumEnabledUsers(),
         ]);
     }
 
@@ -220,6 +224,80 @@ class ConfigController extends Controller
         return $this->redirect($this->generateUrl('config').'?tagging-rule='.$rule->getId().'#set5');
     }
 
+    /**
+     * Remove all annotations OR tags OR entries for the current user.
+     *
+     * @Route("/reset/{type}", requirements={"id" = "annotations|tags|entries"}, name="config_reset")
+     *
+     * @return RedirectResponse
+     */
+    public function resetAction($type)
+    {
+        $em = $this->getDoctrine()->getManager();
+
+        switch ($type) {
+            case 'annotations':
+                $this->getDoctrine()
+                    ->getRepository('WallabagAnnotationBundle:Annotation')
+                    ->removeAllByUserId($this->getUser()->getId());
+                break;
+
+            case 'tags':
+                $this->removeAllTagsByUserId($this->getUser()->getId());
+                break;
+
+            case 'entries':
+                // SQLite doesn't care about cascading remove, so we need to manually remove associated stuf
+                // otherwise they won't be removed ...
+                if ($this->get('doctrine')->getConnection()->getDriver() instanceof \Doctrine\DBAL\Driver\PDOSqlite\Driver) {
+                    $this->getDoctrine()->getRepository('WallabagAnnotationBundle:Annotation')->removeAllByUserId($this->getUser()->getId());
+                }
+
+                // manually remove tags to avoid orphan tag
+                $this->removeAllTagsByUserId($this->getUser()->getId());
+
+                $this->getDoctrine()
+                    ->getRepository('WallabagCoreBundle:Entry')
+                    ->removeAllByUserId($this->getUser()->getId());
+        }
+
+        $this->get('session')->getFlashBag()->add(
+            'notice',
+            'flashes.config.notice.'.$type.'_reset'
+        );
+
+        return $this->redirect($this->generateUrl('config').'#set3');
+    }
+
+    /**
+     * Remove all tags for a given user and cleanup orphan tags.
+     *
+     * @param int $userId
+     */
+    private function removeAllTagsByUserId($userId)
+    {
+        $tags = $this->getDoctrine()->getRepository('WallabagCoreBundle:Tag')->findAllTags($userId);
+
+        if (empty($tags)) {
+            return;
+        }
+
+        $this->getDoctrine()
+            ->getRepository('WallabagCoreBundle:Entry')
+            ->removeTags($userId, $tags);
+
+        // cleanup orphan tags
+        $em = $this->getDoctrine()->getManager();
+
+        foreach ($tags as $tag) {
+            if (count($tag->getEntries()) === 0) {
+                $em->remove($tag);
+            }
+        }
+
+        $em->flush();
+    }
+
     /**
      * Validate that a rule can be edited/deleted by the current user.
      *
@@ -251,4 +329,37 @@ class ConfigController extends Controller
 
         return $config;
     }
+
+    /**
+     * Delete account for current user.
+     *
+     * @Route("/account/delete", name="delete_account")
+     *
+     * @param Request $request
+     *
+     * @throws AccessDeniedHttpException
+     *
+     * @return \Symfony\Component\HttpFoundation\RedirectResponse
+     */
+    public function deleteAccountAction(Request $request)
+    {
+        $enabledUsers = $this->getDoctrine()
+            ->getRepository('WallabagUserBundle:User')
+            ->getSumEnabledUsers();
+
+        if ($enabledUsers <= 1) {
+            throw new AccessDeniedHttpException();
+        }
+
+        $user = $this->getUser();
+
+        // logout current user
+        $this->get('security.token_storage')->setToken(null);
+        $request->getSession()->invalidate();
+
+        $em = $this->get('fos_user.user_manager');
+        $em->deleteUser($user);
+
+        return $this->redirect($this->generateUrl('fos_user_security_login'));
+    }
 }
index 5acc685281a205a57cf88b92e72125cc040abc0b..4542d484c3a2a9e9aaf4b003109e0d068860c2ec 100644 (file)
@@ -90,15 +90,15 @@ class TagController extends Controller
 
         $flatTags = [];
 
-        foreach ($tags as $key => $tag) {
+        foreach ($tags as $tag) {
             $nbEntries = $this->getDoctrine()
                 ->getRepository('WallabagCoreBundle:Entry')
-                ->countAllEntriesByUserIdAndTagId($this->getUser()->getId(), $tag['id']);
+                ->countAllEntriesByUserIdAndTagId($this->getUser()->getId(), $tag->getId());
 
             $flatTags[] = [
-                'id' => $tag['id'],
-                'label' => $tag['label'],
-                'slug' => $tag['slug'],
+                'id' => $tag->getId(),
+                'label' => $tag->getLabel(),
+                'slug' => $tag->getSlug(),
                 'nbEntries' => $nbEntries,
             ];
         }
index f2da3f4def670d5a0e08af123a1617a2cb3fa509..dd0f7e67227b2f7d677888de72e30c57b90d0c04 100644 (file)
@@ -19,7 +19,7 @@ use Wallabag\AnnotationBundle\Entity\Annotation;
  *
  * @XmlRoot("entry")
  * @ORM\Entity(repositoryClass="Wallabag\CoreBundle\Repository\EntryRepository")
- * @ORM\Table(name="`entry`")
+ * @ORM\Table(name="`entry`", options={"collate"="utf8mb4_unicode_ci", "charset"="utf8mb4"})
  * @ORM\HasLifecycleCallbacks()
  * @Hateoas\Relation("self", href = "expr('/api/entries/' ~ object.getId())")
  */
@@ -190,10 +190,10 @@ class Entry
      * @ORM\JoinTable(
      *  name="entry_tag",
      *  joinColumns={
-     *      @ORM\JoinColumn(name="entry_id", referencedColumnName="id")
+     *      @ORM\JoinColumn(name="entry_id", referencedColumnName="id", onDelete="cascade")
      *  },
      *  inverseJoinColumns={
-     *      @ORM\JoinColumn(name="tag_id", referencedColumnName="id")
+     *      @ORM\JoinColumn(name="tag_id", referencedColumnName="id", onDelete="cascade")
      *  }
      * )
      */
index cd2b47b9f7c32b737d35910effe1cf53398adac8..14616d8889e7dc2f2d60d93b60beb73e9e87e15f 100644 (file)
@@ -329,4 +329,18 @@ class EntryRepository extends EntityRepository
 
         return $qb->getQuery()->getSingleScalarResult();
     }
+
+    /**
+     * Remove all entries for a user id.
+     * Used when a user want to reset all informations.
+     *
+     * @param int $userId
+     */
+    public function removeAllByUserId($userId)
+    {
+        $this->getEntityManager()
+            ->createQuery('DELETE FROM Wallabag\CoreBundle\Entity\Entry e WHERE e.user = :userId')
+            ->setParameter('userId', $userId)
+            ->execute();
+    }
 }
index e76878d49d6e7cc5bb06c0f2a1d8227ef2bec4bc..81445989b71c97fc06a13e22f8aaada1b81d871d 100644 (file)
@@ -34,6 +34,9 @@ class TagRepository extends EntityRepository
 
     /**
      * Find all tags per user.
+     * Instead of just left joined on the Entry table, we select only id and group by id to avoid tag multiplication in results.
+     * Once we have all tags id, we can safely request them one by one.
+     * This'll still be fastest than the previous query.
      *
      * @param int $userId
      *
@@ -41,15 +44,20 @@ class TagRepository extends EntityRepository
      */
     public function findAllTags($userId)
     {
-        return $this->createQueryBuilder('t')
-            ->select('t.slug', 't.label', 't.id')
+        $ids = $this->createQueryBuilder('t')
+            ->select('t.id')
             ->leftJoin('t.entries', 'e')
             ->where('e.user = :userId')->setParameter('userId', $userId)
-            ->groupBy('t.slug')
-            ->addGroupBy('t.label')
-            ->addGroupBy('t.id')
+            ->groupBy('t.id')
             ->getQuery()
             ->getArrayResult();
+
+        $tags = [];
+        foreach ($ids as $id) {
+            $tags[] = $this->find($id);
+        }
+
+        return $tags;
     }
 
     /**
index 614488a64fd92596243a19ba5bea04d81c989c1a..cc5f9e9ade8d30f1a7e1a4d86ab2ef6639c7fe41 100644 (file)
@@ -129,3 +129,10 @@ services:
         arguments:
             - '@twig'
             - '%kernel.debug%'
+
+    wallabag_core.subscriber.sqlite_cascade_delete:
+        class:  Wallabag\CoreBundle\Subscriber\SQLiteCascadeDeleteSubscriber
+        arguments:
+            - "@doctrine"
+        tags:
+            - { name: doctrine.event_subscriber }
index 714ced14d1a42566a5fdfd933100e75725aadcd8..c05955833c5b597ece9aa544b0ba4642fca95cb9 100644 (file)
@@ -88,6 +88,18 @@ config:
         name_label: 'Navn'
         email_label: 'Emailadresse'
         # twoFactorAuthentication_label: 'Two factor authentication'
+        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
+    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
+        # confirm: Are you really really sure? (THIS CAN'T BE UNDONE)
     form_password:
         old_password_label: 'Gammel adgangskode'
         new_password_label: 'Ny adgangskode'
@@ -460,6 +472,9 @@ flashes:
             # tagging_rules_deleted: 'Tagging rule deleted'
             # user_added: 'User "%username%" added'
             # rss_token_updated: 'RSS token updated'
+            # annotations_reset: Annotations reset
+            # tags_reset: Tags reset
+            # entries_reset: Entries reset
     entry:
         notice:
             # entry_already_saved: 'Entry already saved on %date%'
index 57e49f84ec61330d7ea6087ab8836219fba8c483..0051da2f47294dd3ec62d2e0039000170ad8abc6 100644 (file)
@@ -88,6 +88,18 @@ config:
         name_label: 'Name'
         email_label: 'E-Mail-Adresse'
         twoFactorAuthentication_label: 'Zwei-Faktor-Authentifizierung'
+        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
+    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
+        # confirm: Are you really really sure? (THIS CAN'T BE UNDONE)
     form_password:
         old_password_label: 'Altes Kennwort'
         new_password_label: 'Neues Kennwort'
@@ -460,6 +472,9 @@ flashes:
             tagging_rules_deleted: 'Tagging-Regel gelöscht'
             user_added: 'Benutzer "%username%" erstellt'
             rss_token_updated: 'RSS-Token aktualisiert'
+            # annotations_reset: Annotations reset
+            # tags_reset: Tags reset
+            # entries_reset: Entries reset
     entry:
         notice:
             entry_already_saved: 'Eintrag bereits am %date% gespeichert'
index 4a59c75eb3c568a786b94c2b09b53e82599f7903..462be5562ea3dd7f9a5bd2c884344793849a9c13 100644 (file)
@@ -88,6 +88,18 @@ config:
         name_label: 'Name'
         email_label: 'Email'
         twoFactorAuthentication_label: 'Two factor authentication'
+        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
+    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
+        confirm: Are you really really sure? (THIS CAN'T BE UNDONE)
     form_password:
         old_password_label: 'Current password'
         new_password_label: 'New password'
@@ -459,6 +471,9 @@ flashes:
             tagging_rules_updated: 'Tagging rules updated'
             tagging_rules_deleted: 'Tagging rule deleted'
             rss_token_updated: 'RSS token updated'
+            annotations_reset: Annotations reset
+            tags_reset: Tags reset
+            entries_reset: Entries reset
     entry:
         notice:
             entry_already_saved: 'Entry already saved on %date%'
index 1b1e0cb1056318d536717774776db5782330b062..cfabe09f28936867393bb9a10b93c8a5f2192af4 100644 (file)
@@ -88,6 +88,18 @@ config:
         name_label: 'Nombre'
         email_label: 'Direccion e-mail'
         twoFactorAuthentication_label: 'Autentificación de dos factores'
+        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
+    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
+        # confirm: Are you really really sure? (THIS CAN'T BE UNDONE)
     form_password:
         old_password_label: 'Contraseña actual'
         new_password_label: 'Nueva contraseña'
@@ -460,6 +472,9 @@ flashes:
             tagging_rules_deleted: 'Regla de etiquetado actualizada'
             user_added: 'Usuario "%username%" añadido'
             rss_token_updated: 'RSS token actualizado'
+            # annotations_reset: Annotations reset
+            # tags_reset: Tags reset
+            # entries_reset: Entries reset
     entry:
         notice:
             entry_already_saved: 'Entrada ya guardada por %fecha%'
index 41dc8acf41c9ad963d9e3ca212aaf9f4f8d3636b..07b5bee7c01a02e287e03bf3fa483f3aa8aec92e 100644 (file)
@@ -88,6 +88,18 @@ config:
         name_label: 'نام'
         email_label: 'نشانی ایمیل'
         twoFactorAuthentication_label: 'تأیید ۲مرحله‌ای'
+        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
+    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
+        # confirm: Are you really really sure? (THIS CAN'T BE UNDONE)
     form_password:
         old_password_label: 'رمز قدیمی'
         new_password_label: 'رمز تازه'
@@ -459,6 +471,9 @@ flashes:
             tagging_rules_deleted: 'قانون برچسب‌گذاری پاک شد'
             user_added: 'کابر "%username%" افزوده شد'
             rss_token_updated: 'کد آر-اس-اس به‌روز شد'
+            # annotations_reset: Annotations reset
+            # tags_reset: Tags reset
+            # entries_reset: Entries reset
     entry:
         notice:
             entry_already_saved: 'این مقاله در تاریخ %date% ذخیره شده بود'
index 7fb9681d97ed75141bd34400098c5878818f1831..db6f9f7e261d170788f2ab8bc1329bec34b914fb 100644 (file)
@@ -88,6 +88,18 @@ config:
         name_label: 'Nom'
         email_label: 'Adresse e-mail'
         twoFactorAuthentication_label: 'Double authentification'
+        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é.
+            confirm: Vous êtes vraiment sûr ? (C'EST IRRÉVERSIBLE)
+            button: 'Supprimer mon compte'
+    reset:
+        title: Réinitialisation (attention danger !)
+        description: En cliquant sur les boutons ci-dessous vous avez la possibilité de supprimer certaines informations de votre compte. Attention, ces actions sont IRRÉVERSIBLES !
+        annotations: Supprimer TOUTES les annotations
+        tags: Supprimer TOUS les tags
+        entries: Supprimer TOUS les articles
+        confirm: Êtes-vous vraiment vraiment sûr ? (C'EST IRRÉVERSIBLE)
     form_password:
         old_password_label: 'Mot de passe actuel'
         new_password_label: 'Nouveau mot de passe'
@@ -386,7 +398,7 @@ developer:
         field_grant_types: 'Type de privilège accordé'
         no_client: 'Aucun client pour le moment'
     remove:
-        warn_message_1: 'Vous avez la possibilité de supprimer le client %name%. Cette action est IRREVERSIBLE !'
+        warn_message_1: 'Vous avez la possibilité de supprimer le client %name%. Cette action est IRRÉVERSIBLE !'
         warn_message_2: "Si vous supprimez le client %name%, toutes les applications qui l'utilisaient ne fonctionneront plus avec votre compte wallabag."
         action: 'Supprimer le client %name%'
     client:
@@ -460,9 +472,12 @@ flashes:
             tagging_rules_deleted: 'Règle supprimée'
             user_added: 'Utilisateur "%username%" ajouté'
             rss_token_updated: 'Jeton RSS mis à jour'
+            annotations_reset: Annotations supprimées
+            tags_reset: Tags supprimés
+            entries_reset: Articles supprimés
     entry:
         notice:
-            entry_already_saved: 'Article déjà sauvergardé le %date%'
+            entry_already_saved: 'Article déjà sauvegardé le %date%'
             entry_saved: 'Article enregistré'
             entry_saved_failed: 'Article enregistré mais impossible de récupérer le contenu'
             entry_updated: 'Article mis à jour'
index b279ae40e056195a38409fc28721c840dd6b5d0c..f1aff51a884f6102360e00df51ffcd404015d77e 100644 (file)
@@ -88,6 +88,18 @@ config:
         name_label: 'Nome'
         email_label: 'E-mail'
         twoFactorAuthentication_label: 'Two factor authentication'
+        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
+    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
+        # confirm: Are you really really sure? (THIS CAN'T BE UNDONE)
     form_password:
         old_password_label: 'Password corrente'
         new_password_label: 'Nuova password'
@@ -460,6 +472,9 @@ flashes:
             tagging_rules_deleted: 'Regola di tagging aggiornate'
             user_added: 'Utente "%username%" aggiunto'
             rss_token_updated: 'RSS token aggiornato'
+            # annotations_reset: Annotations reset
+            # tags_reset: Tags reset
+            # entries_reset: Entries reset
     entry:
         notice:
             entry_already_saved: 'Contenuto già salvato in data %date%'
index a4659620b31c78a9728419fa3c72145a4daf76bf..e0567d7e1b74d71a2892b211db3143cb3fb66f35 100644 (file)
@@ -25,13 +25,13 @@ menu:
         internal_settings: 'Configuracion interna'
         import: 'Importar'
         howto: 'Ajuda'
-        developer: 'Desvolopador'
+        developer: 'Desvolopaire'
         logout: 'Desconnexion'
         about: 'A prepaus'
         search: 'Cercar'
         save_link: 'Enregistrar un novèl article'
         back_to_unread: 'Tornar als articles pas legits'
-        # users_management: 'Users management'
+        users_management: 'Gestion dels utilizaires'
     top:
         add_new_entry: 'Enregistrar un novèl article'
         search: 'Cercar'
@@ -46,7 +46,7 @@ footer:
         social: 'Social'
         powered_by: 'propulsat per'
         about: 'A prepaus'
-    # stats: Since %user_creation% you read %nb_archives% articles. That is about %per_day% a day!
+    stats: "Dempuèi %user_creation% avètz legit %nb_archives% articles. Es a l'entorn de %per_day% per jorn !"
 
 config:
     page_title: 'Configuracion'
@@ -88,6 +88,18 @@ config:
         name_label: 'Nom'
         email_label: 'Adreça de corrièl'
         twoFactorAuthentication_label: 'Dobla autentificacion'
+        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
+    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
+        # confirm: Are you really really sure? (THIS CAN'T BE UNDONE)
     form_password:
         old_password_label: 'Senhal actual'
         new_password_label: 'Senhal novèl'
@@ -96,7 +108,7 @@ config:
         if_label: 'se'
         then_tag_as_label: 'alara atribuir las etiquetas'
         delete_rule_label: 'suprimir'
-        # edit_rule_label: 'edit'
+        edit_rule_label: 'modificar'
         rule_label: 'Règla'
         tags_label: 'Etiquetas'
         faq:
@@ -209,7 +221,7 @@ entry:
         is_public_label: 'Public'
         save_label: 'Enregistrar'
     public:
-        # shared_by_wallabag: "This article has been shared by <a href='%wallabag_instance%'>wallabag</a>"
+        shared_by_wallabag: "Aqueste article es estat partejat per <a href='%wallabag_instance%'>wallabag</a>"
 
 about:
     page_title: 'A prepaus'
@@ -265,14 +277,14 @@ howto:
 
 quickstart:
     page_title: 'Per ben començar'
-    # more: 'More…'
+    more: 'Mai…'
     intro:
         title: 'Benvenguda sus wallabag !'
         paragraph_1: "Anem vos guidar per far lo torn de la proprietat e vos presentar unas fonccionalitats que vos poirián interessar per vos apropriar aquesta aisina."
         paragraph_2: 'Seguètz-nos '
     configure:
-        title: "Configuratz l'aplicacio"
-        # description: 'In order to have an application which suits you, have a look into the configuration of wallabag.'
+        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'
         tagging_rules: 'Escrivètz de règlas per classar automaticament vòstres articles'
@@ -286,7 +298,7 @@ quickstart:
         import: 'Configurar los impòrt'
     first_steps:
         title: 'Primièrs passes'
-        # description: "Now wallabag is well configured, it's time to archive the web. You can click on the top right sign + to add a link."
+        description: "Ara wallabag es ben configurat, es lo moment d'archivar lo web. Podètz clicar sul signe + a man drecha amont per ajustar un ligam."
         new_article: 'Ajustatz vòstre primièr article'
         unread_articles: 'E racaptatz-lo !'
     migrate:
@@ -298,14 +310,14 @@ quickstart:
         readability: 'Migrar dempuèi Readability'
         instapaper: 'Migrar dempuèi Instapaper'
     developer:
-        title: 'Pels desvolopadors'
-        # description: 'We also thought to the developers: Docker, API, translations, etc.'
+        title: 'Pels desvolopaires'
+        description: 'Avèm tanben pensat als desvolopaires : Docker, API, traduccions, etc.'
         create_application: 'Crear vòstra aplicacion tèrça'
-        # use_docker: 'Use Docker to install wallabag'
+        use_docker: 'Utilizar Docker per installar wallabag'
     docs:
         title: 'Documentacion complèta'
-        # description: "There are so much features in wallabag. Don't hesitate to read the manual to know them and to learn how to use them."
-        annotate: 'Anotatar vòstre article'
+        description: "I a un fum de fonccionalitats dins wallabag. Esitetz pas a legir lo manual per las conéisser e aprendre a las utilizar."
+        annotate: 'Anotar vòstre article'
         export: 'Convertissètz vòstres articles en ePub o en PDF'
         search_filters: "Aprenètz a utilizar lo motor de recèrca e los filtres per retrobar l'article que vos interèssa"
         fetching_errors: "Qué far se mon article es pas recuperat coma cal ?"
@@ -390,7 +402,7 @@ developer:
         warn_message_2: "Se suprimissètz un client, totas las aplicacions que l'emplegan foncionaràn pas mai amb vòstre compte wallabag."
         action: 'Suprimir aqueste client'
     client:
-        page_title: 'Desvlopador > Novèl client'
+        page_title: 'Desvolopaire > Novèl client'
         page_description: "Anatz crear un novèl client. Mercés de cumplir l'url de redireccion cap a vòstra aplicacion."
         form:
             name_label: "Nom del client"
@@ -398,7 +410,7 @@ developer:
             save_label: 'Crear un novèl client'
         action_back: 'Retorn'
     client_parameter:
-        page_title: 'Desvolopador > Los paramètres de vòstre client'
+        page_title: 'Desvolopaire > Los paramètres de vòstre client'
         page_description: 'Vaquí los paramètres de vòstre client'
         field_name: 'Nom del client'
         field_id: 'ID Client'
@@ -406,7 +418,7 @@ developer:
         back: 'Retour'
         read_howto: 'Legir "cossí crear ma primièra aplicacion"'
     howto:
-        page_title: 'Desvolopador > Cossí crear ma primièra aplicacion'
+        page_title: 'Desvolopaire > Cossí crear ma primièra aplicacion'
         description:
             paragraph_1: "Las comandas seguentas utilizan la <a href=\"https://github.com/jkbrzt/httpie\">bibliotèca HTTPie</a>. Asseguratz-vos que siasqueòu installadas abans de l'utilizar."
             paragraph_2: "Vos cal un geton per escambiar entre vòstra aplicacion e l'API de wallabar."
@@ -419,31 +431,31 @@ developer:
         back: 'Retorn'
 
 user:
-    # page_title: Users management
-    # new_user: Create a new user
-    # edit_user: Edit an existing user
-    # description: "Here you can manage all users (create, edit and delete)"
-    list:
-    #     actions: Actions
-    #     edit_action: Edit
-    #     yes: Yes
-    #     no: No
-    #     create_new_one: Create a new user
+    page_title: 'Gestion dels utilizaires'
+    new_user: 'Crear un novèl utilizaire'
+    edit_user: 'Modificar un utilizaire existent'
+    description: "Aquí podètz gerir totes los utilizaires (crear, modificar e suprimir)"
+    list:
+         actions: 'Accions'
+         edit_action: 'Modificar'
+         yes: 'Òc'
+         no: 'Non'
+         create_new_one: 'Crear un novèl utilizaire'
     form:
         username_label: "Nom d'utilizaire"
-        # name_label: 'Name'
+        name_label: 'Nom'
         password_label: 'Senhal'
         repeat_new_password_label: 'Confirmatz vòstre novèl senhal'
         plain_password_label: 'Senhal en clar'
         email_label: 'Adreça de corrièl'
-        # enabled_label: 'Enabled'
-        # locked_label: 'Locked'
-        # last_login_label: 'Last login'
-        # twofactor_label: Two factor authentication
-        # save: Save
-        # delete: Delete
-        # delete_confirm: Are you sure?
-        # back_to_list: Back to list
+        enabled_label: 'Actiu'
+        locked_label: 'Varrolhat'
+        last_login_label: 'Darrièra connexion'
+        twofactor_label: 'Autentificacion doble-factor'
+        save: 'Enregistrar'
+        delete: 'Suprimir'
+        delete_confirm: 'Sètz segur ?'
+        back_to_list: 'Tornar a la lista'
 
 error:
     # page_title: An error occurred
@@ -458,8 +470,11 @@ flashes:
             rss_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'
-            user_added: 'Utilizaire "%username%" apondut'
+            user_added: 'Utilizaire "%username%" ajustat'
             rss_token_updated: 'Geton RSS mes a jorn'
+            # annotations_reset: Annotations reset
+            # tags_reset: Tags reset
+            # entries_reset: Entries reset
     entry:
         notice:
             entry_already_saved: 'Article ja salvargardat lo %date%'
@@ -470,12 +485,12 @@ flashes:
             entry_reloaded_failed: "L'article es estat cargat de nòu mai la recuperacion del contengut a fracassat"
             entry_archived: 'Article marcat coma legit'
             entry_unarchived: 'Article marcat coma pas legit'
-            entry_starred: 'Article apondut dins los favorits'
+            entry_starred: 'Article ajustat dins los favorits'
             entry_unstarred: 'Article quitat dels favorits'
             entry_deleted: 'Article suprimit'
     tag:
         notice:
-            tag_added: 'Etiqueta aponduda'
+            tag_added: 'Etiqueta ajustada'
     import:
         notice:
             failed: "L'importacion a fracassat, mercés de tornar ensajar"
index 798b39c22ca761f1fdad09fe3edc3555a75e08b8..8eef998b69ff8b225f09b44c9e1ff9cb04c8321a 100644 (file)
@@ -88,6 +88,18 @@ config:
         name_label: 'Nazwa'
         email_label: 'Adres email'
         twoFactorAuthentication_label: 'Autoryzacja dwuetapowa'
+        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.
+            confirm: Jesteś pewien? (tej operacji NIE MOŻNA cofnąć)
+            button: Usuń moje konto
+    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
+        # confirm: Are you really really sure? (THIS CAN'T BE UNDONE)
     form_password:
         old_password_label: 'Stare hasło'
         new_password_label: 'Nowe hasło'
@@ -460,6 +472,9 @@ flashes:
             tagging_rules_deleted: 'Reguła tagowania usunięta'
             user_added: 'Użytkownik "%username%" dodany'
             rss_token_updated: 'Token kanału RSS zaktualizowany'
+            # annotations_reset: Annotations reset
+            # tags_reset: Tags reset
+            # entries_reset: Entries reset
     entry:
         notice:
             entry_already_saved: 'Wpis już został dodany %date%'
index 21f27e0880f4e14ed973bb57ac609abd55121a36..6e4813e58aecbad389420bc887044141e63f95c7 100644 (file)
@@ -88,6 +88,18 @@ config:
         name_label: 'Nume'
         email_label: 'E-mail'
         # twoFactorAuthentication_label: 'Two factor authentication'
+        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
+    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
+        # confirm: Are you really really sure? (THIS CAN'T BE UNDONE)
     form_password:
         old_password_label: 'Parola veche'
         new_password_label: 'Parola nouă'
@@ -460,6 +472,9 @@ flashes:
             # tagging_rules_deleted: 'Tagging rule deleted'
             # user_added: 'User "%username%" added'
             # rss_token_updated: 'RSS token updated'
+            # annotations_reset: Annotations reset
+            # tags_reset: Tags reset
+            # entries_reset: Entries reset
     entry:
         notice:
             # entry_already_saved: 'Entry already saved on %date%'
index f137ec996c321474687dd7e2af40f986fc9cb97a..769031023d3235b57b7861a4673f217f8bd91abb 100644 (file)
@@ -88,6 +88,18 @@ config:
         name_label: 'İsim'
         email_label: 'E-posta'
         twoFactorAuthentication_label: 'İki adımlı doğrulama'
+        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
+    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
+        # confirm: Are you really really sure? (THIS CAN'T BE UNDONE)
     form_password:
         old_password_label: 'Eski şifre'
         new_password_label: 'Yeni şifre'
@@ -459,6 +471,9 @@ flashes:
             tagging_rules_deleted: 'Tagging rule deleted'
             user_added: 'User "%username%" added'
             rss_token_updated: 'RSS token updated'
+            # annotations_reset: Annotations reset
+            # tags_reset: Tags reset
+            # entries_reset: Entries reset
     entry:
         notice:
             entry_already_saved: 'Entry already saved on %date%'
index ff7ef73a81509ac971aac10e47061aecc611b9b6..455d02950e0f4f4717af39df66740d5e7afa478f 100644 (file)
         </fieldset>
         {% endif %}
 
+        <h2>{{ 'config.reset.title'|trans }}</h2>
+        <fieldset class="w500p inline">
+            <p>{{ 'config.reset.description'|trans }}</p>
+            <ul>
+                <li>
+                    <a href="{{ path('config_reset', { type: 'annotations'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
+                        {{ 'config.reset.annotations'|trans }}
+                    </a>
+                </li>
+                <li>
+                    <a href="{{ path('config_reset', { type: 'tags'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
+                        {{ 'config.reset.tags'|trans }}
+                    </a>
+                </li>
+                <li>
+                    <a href="{{ path('config_reset', { type: 'entries'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
+                        {{ 'config.reset.entries'|trans }}
+                    </a>
+                </li>
+            </ul>
+        </fieldset>
+
         {{ form_widget(form.user._token) }}
         {{ form_widget(form.user.save) }}
     </form>
 
+    {% if enabled_users > 1 %}
+        <h2>{{ 'config.form_user.delete.title'|trans }}</h2>
+
+        <p>{{ 'config.form_user.delete.description'|trans }}</p>
+        <a href="{{ path('delete_account') }}" onclick="return confirm('{{ 'config.form_user.delete.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red delete-account">
+            {{ 'config.form_user.delete.button'|trans }}
+        </a>
+    {% endif %}
+
     <h2>{{ 'config.tab_menu.password'|trans }}</h2>
 
     {{ form_start(form.pwd) }}
index 19faddc05b5a900eb6b98657c26a2395d4fc3fa8..b53ae2fef48be6eabc2b43146f01000443db9cc6 100644 (file)
                             {{ form_widget(form.user.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }}
                             {{ form_widget(form.user._token) }}
                         </form>
+
+                        <br /><hr /><br />
+
+                        <div class="row">
+                            <h5>{{ 'config.reset.title'|trans }}</h5>
+                            <p>{{ 'config.reset.description'|trans }}</p>
+                            <a href="{{ path('config_reset', { type: 'annotations'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
+                                {{ 'config.reset.annotations'|trans }}
+                            </a>
+                            <a href="{{ path('config_reset', { type: 'tags'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
+                                {{ 'config.reset.tags'|trans }}
+                            </a>
+                            <a href="{{ path('config_reset', { type: 'entries'}) }}" onclick="return confirm('{{ 'config.reset.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red">
+                                {{ 'config.reset.entries'|trans }}
+                            </a>
+                        </div>
+
+                        {% if enabled_users > 1 %}
+                            <br /><hr /><br />
+
+                            <div class="row">
+                                <h5>{{ 'config.form_user.delete.title'|trans }}</h5>
+                                <p>{{ 'config.form_user.delete.description'|trans }}</p>
+                                <a href="{{ path('delete_account') }}" onclick="return confirm('{{ 'config.form_user.delete.confirm'|trans|escape('js') }}')" class="waves-effect waves-light btn red delete-account">
+                                    {{ 'config.form_user.delete.button'|trans }}
+                                </a>
+                            </div>
+                        {% endif %}
                     </div>
 
                     <div id="set4" class="col s12">
diff --git a/src/Wallabag/CoreBundle/Subscriber/SQLiteCascadeDeleteSubscriber.php b/src/Wallabag/CoreBundle/Subscriber/SQLiteCascadeDeleteSubscriber.php
new file mode 100644 (file)
index 0000000..f7210bd
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+
+namespace Wallabag\CoreBundle\Subscriber;
+
+use Doctrine\Common\EventSubscriber;
+use Doctrine\ORM\Event\LifecycleEventArgs;
+use Wallabag\CoreBundle\Entity\Entry;
+use Doctrine\Bundle\DoctrineBundle\Registry;
+
+/**
+ * SQLite doesn't care about cascading remove, so we need to manually remove associated stuf for an Entry.
+ * Foreign Key Support can be enabled by running `PRAGMA foreign_keys = ON;` at runtime (AT RUNTIME !).
+ * But it needs a compilation flag that not all SQLite instance has ...
+ *
+ * @see https://www.sqlite.org/foreignkeys.html#fk_enable
+ */
+class SQLiteCascadeDeleteSubscriber implements EventSubscriber
+{
+    private $doctrine;
+
+    /**
+     * @param \Doctrine\Bundle\DoctrineBundle\Registry $doctrine
+     */
+    public function __construct(Registry $doctrine)
+    {
+        $this->doctrine = $doctrine;
+    }
+
+    /**
+     * @return array
+     */
+    public function getSubscribedEvents()
+    {
+        return [
+            'preRemove',
+        ];
+    }
+
+    /**
+     * 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)
+    {
+        $entity = $args->getEntity();
+
+        if (!$this->doctrine->getConnection()->getDriver() instanceof \Doctrine\DBAL\Driver\PDOSqlite\Driver ||
+            !$entity instanceof Entry) {
+            return;
+        }
+
+        $em = $this->doctrine->getManager();
+
+        if (null !== $entity->getTags()) {
+            foreach ($entity->getTags() as $tag) {
+                $entity->removeTag($tag);
+            }
+        }
+
+        if (null !== $entity->getAnnotations()) {
+            foreach ($entity->getAnnotations() as $annotation) {
+                $em->remove($annotation);
+            }
+        }
+
+        $em->flush();
+    }
+}
index 009c4881d0ea8945379e159894f00b4ca51a1f2d..445edb3c1077cb2826e2937cb7fccdff2ae4e2be 100644 (file)
@@ -38,4 +38,18 @@ class UserRepository extends EntityRepository
             ->getQuery()
             ->getSingleResult();
     }
+
+    /**
+     * Count how many users are enabled.
+     *
+     * @return int
+     */
+    public function getSumEnabledUsers()
+    {
+        return $this->createQueryBuilder('u')
+            ->select('count(u)')
+            ->andWhere('u.expired = false')
+            ->getQuery()
+            ->getSingleScalarResult();
+    }
 }
index eb9c8e676e0cbd03b4f92eda2b502a1f5c88ad32..8062e53f12fd1a76c3a0b1ad3690c0e5e9468473 100644 (file)
@@ -21,7 +21,7 @@ services:
         arguments:
             - WallabagUserBundle:User
 
-    wallabag_user.create_config:
+    wallabag_user.listener.create_config:
         class: Wallabag\UserBundle\EventListener\CreateConfigListener
         arguments:
             - "@doctrine.orm.entity_manager"
index 70849f74129000ca787a0a414396feb478516f81..cee0b8473dae5c6a766b33ed3e0bd8127493bfc6 100644 (file)
@@ -3,35 +3,80 @@
 namespace Tests\AnnotationBundle\Controller;
 
 use Tests\Wallabag\AnnotationBundle\WallabagAnnotationTestCase;
+use Wallabag\AnnotationBundle\Entity\Annotation;
+use Wallabag\CoreBundle\Entity\Entry;
 
 class AnnotationControllerTest extends WallabagAnnotationTestCase
 {
-    public function testGetAnnotations()
+    /**
+     * This data provider allow to tests annotation from the :
+     *     - API POV (when user use the api to manage annotations)
+     *     - and User POV (when user use the web interface - using javascript - to manage annotations)
+     */
+    public function dataForEachAnnotations()
     {
-        $annotation = $this->client->getContainer()
-            ->get('doctrine.orm.entity_manager')
-            ->getRepository('WallabagAnnotationBundle:Annotation')
-            ->findOneByUsername('admin');
+        return [
+            ['/api/annotations'],
+            ['annotations'],
+        ];
+    }
+
+    /**
+     * Test fetching annotations for an entry.
+     *
+     * @dataProvider dataForEachAnnotations
+     */
+    public function testGetAnnotations($prefixUrl)
+    {
+        $em = $this->client->getContainer()->get('doctrine.orm.entity_manager');
+
+        $user = $em
+            ->getRepository('WallabagUserBundle:User')
+            ->findOneByUserName('admin');
+        $entry = $em
+            ->getRepository('WallabagCoreBundle:Entry')
+            ->findOneByUsernameAndNotArchived('admin');
 
-        if (!$annotation) {
-            $this->markTestSkipped('No content found in db.');
+        $annotation = new Annotation($user);
+        $annotation->setEntry($entry);
+        $annotation->setText('This is my annotation /o/');
+        $annotation->setQuote('content');
+
+        $em->persist($annotation);
+        $em->flush();
+
+        if ('annotations' === $prefixUrl) {
+            $this->logInAs('admin');
         }
 
-        $this->logInAs('admin');
-        $crawler = $this->client->request('GET', 'annotations/'.$annotation->getEntry()->getId().'.json');
+        $this->client->request('GET', $prefixUrl.'/'.$entry->getId().'.json');
         $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
 
         $content = json_decode($this->client->getResponse()->getContent(), true);
-        $this->assertEquals(1, $content['total']);
+        $this->assertGreaterThanOrEqual(1, $content['total']);
         $this->assertEquals($annotation->getText(), $content['rows'][0]['text']);
+
+        // we need to re-fetch the annotation becase after the flush, it has been "detached" from the entity manager
+        $annotation = $em->getRepository('WallabagAnnotationBundle:Annotation')->findAnnotationById($annotation->getId());
+        $em->remove($annotation);
+        $em->flush();
     }
 
-    public function testSetAnnotation()
+    /**
+     * Test creating an annotation for an entry.
+     *
+     * @dataProvider dataForEachAnnotations
+     */
+    public function testSetAnnotation($prefixUrl)
     {
-        $this->logInAs('admin');
+        $em = $this->client->getContainer()->get('doctrine.orm.entity_manager');
 
-        $entry = $this->client->getContainer()
-            ->get('doctrine.orm.entity_manager')
+        if ('annotations' === $prefixUrl) {
+            $this->logInAs('admin');
+        }
+
+        /** @var Entry $entry */
+        $entry = $em
             ->getRepository('WallabagCoreBundle:Entry')
             ->findOneByUsernameAndNotArchived('admin');
 
@@ -41,7 +86,7 @@ class AnnotationControllerTest extends WallabagAnnotationTestCase
             'quote' => 'my quote',
             'ranges' => ['start' => '', 'startOffset' => 24, 'end' => '', 'endOffset' => 31],
         ]);
-        $crawler = $this->client->request('POST', 'annotations/'.$entry->getId().'.json', [], [], $headers, $content);
+        $this->client->request('POST', $prefixUrl.'/'.$entry->getId().'.json', [], [], $headers, $content);
 
         $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
 
@@ -52,6 +97,7 @@ class AnnotationControllerTest extends WallabagAnnotationTestCase
         $this->assertEquals('my annotation', $content['text']);
         $this->assertEquals('my quote', $content['quote']);
 
+        /** @var Annotation $annotation */
         $annotation = $this->client->getContainer()
             ->get('doctrine.orm.entity_manager')
             ->getRepository('WallabagAnnotationBundle:Annotation')
@@ -60,20 +106,35 @@ class AnnotationControllerTest extends WallabagAnnotationTestCase
         $this->assertEquals('my annotation', $annotation->getText());
     }
 
-    public function testEditAnnotation()
+    /**
+     * Test editing an existing annotation.
+     *
+     * @dataProvider dataForEachAnnotations
+     */
+    public function testEditAnnotation($prefixUrl)
     {
-        $annotation = $this->client->getContainer()
-            ->get('doctrine.orm.entity_manager')
-            ->getRepository('WallabagAnnotationBundle:Annotation')
-            ->findOneByUsername('admin');
+        $em = $this->client->getContainer()->get('doctrine.orm.entity_manager');
 
-        $this->logInAs('admin');
+        $user = $em
+            ->getRepository('WallabagUserBundle:User')
+            ->findOneByUserName('admin');
+        $entry = $em
+            ->getRepository('WallabagCoreBundle:Entry')
+            ->findOneByUsernameAndNotArchived('admin');
+
+        $annotation = new Annotation($user);
+        $annotation->setEntry($entry);
+        $annotation->setText('This is my annotation /o/');
+        $annotation->setQuote('my quote');
+
+        $em->persist($annotation);
+        $em->flush();
 
         $headers = ['CONTENT_TYPE' => 'application/json'];
         $content = json_encode([
             'text' => 'a modified annotation',
         ]);
-        $crawler = $this->client->request('PUT', 'annotations/'.$annotation->getId().'.json', [], [], $headers, $content);
+        $this->client->request('PUT', $prefixUrl.'/'.$annotation->getId().'.json', [], [], $headers, $content);
         $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
 
         $content = json_decode($this->client->getResponse()->getContent(), true);
@@ -83,35 +144,56 @@ class AnnotationControllerTest extends WallabagAnnotationTestCase
         $this->assertEquals('a modified annotation', $content['text']);
         $this->assertEquals('my quote', $content['quote']);
 
-        $annotationUpdated = $this->client->getContainer()
-            ->get('doctrine.orm.entity_manager')
+        /** @var Annotation $annotationUpdated */
+        $annotationUpdated = $em
             ->getRepository('WallabagAnnotationBundle:Annotation')
             ->findOneById($annotation->getId());
         $this->assertEquals('a modified annotation', $annotationUpdated->getText());
+
+        $em->remove($annotationUpdated);
+        $em->flush();
     }
 
-    public function testDeleteAnnotation()
+    /**
+     * Test deleting an annotation.
+     *
+     * @dataProvider dataForEachAnnotations
+     */
+    public function testDeleteAnnotation($prefixUrl)
     {
-        $annotation = $this->client->getContainer()
-            ->get('doctrine.orm.entity_manager')
-            ->getRepository('WallabagAnnotationBundle:Annotation')
-            ->findOneByUsername('admin');
+        $em = $this->client->getContainer()->get('doctrine.orm.entity_manager');
 
-        $this->logInAs('admin');
+        $user = $em
+            ->getRepository('WallabagUserBundle:User')
+            ->findOneByUserName('admin');
+        $entry = $em
+            ->getRepository('WallabagCoreBundle:Entry')
+            ->findOneByUsernameAndNotArchived('admin');
+
+        $annotation = new Annotation($user);
+        $annotation->setEntry($entry);
+        $annotation->setText('This is my annotation /o/');
+        $annotation->setQuote('my quote');
+
+        $em->persist($annotation);
+        $em->flush();
+
+        if ('annotations' === $prefixUrl) {
+            $this->logInAs('admin');
+        }
 
         $headers = ['CONTENT_TYPE' => 'application/json'];
         $content = json_encode([
             'text' => 'a modified annotation',
         ]);
-        $crawler = $this->client->request('DELETE', 'annotations/'.$annotation->getId().'.json', [], [], $headers, $content);
+        $this->client->request('DELETE', $prefixUrl.'/'.$annotation->getId().'.json', [], [], $headers, $content);
         $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
 
         $content = json_decode($this->client->getResponse()->getContent(), true);
 
-        $this->assertEquals('a modified annotation', $content['text']);
+        $this->assertEquals('This is my annotation /o/', $content['text']);
 
-        $annotationDeleted = $this->client->getContainer()
-            ->get('doctrine.orm.entity_manager')
+        $annotationDeleted = $em
             ->getRepository('WallabagAnnotationBundle:Annotation')
             ->findOneById($annotation->getId());
 
index 82790a5c43c350d7bca672d3a19380b85b525421..ef3f1324eb3937fe2f76c0e370dc976844543232 100644 (file)
@@ -8,7 +8,7 @@ use Symfony\Component\BrowserKit\Cookie;
 abstract class WallabagAnnotationTestCase extends WebTestCase
 {
     /**
-     * @var Client
+     * @var \Symfony\Bundle\FrameworkBundle\Client
      */
     protected $client = null;
 
@@ -35,7 +35,7 @@ abstract class WallabagAnnotationTestCase extends WebTestCase
     }
 
     /**
-     * @return Client
+     * @return \Symfony\Bundle\FrameworkBundle\Client
      */
     protected function createAuthorizedClient()
     {
@@ -49,7 +49,7 @@ abstract class WallabagAnnotationTestCase extends WebTestCase
         $firewallName = $container->getParameter('fos_user.firewall_name');
 
         $this->user = $userManager->findUserBy(['username' => 'admin']);
-        $loginManager->loginUser($firewallName, $this->user);
+        $loginManager->logInUser($firewallName, $this->user);
 
         // save the login token into the session and put it in a cookie
         $container->get('session')->set('_security_'.$firewallName, serialize($container->get('security.token_storage')->getToken()));
index 5dcb3e000dac75d68f0026851856dcdc0692973b..6bca3c8bb9b1931b84bc16394bd5e33eb6f6485b 100644 (file)
@@ -32,12 +32,55 @@ class WallabagRestControllerTest extends WallabagApiTestCase
         $this->assertEquals($entry->getUserEmail(), $content['user_email']);
         $this->assertEquals($entry->getUserId(), $content['user_id']);
 
-        $this->assertTrue(
-            $this->client->getResponse()->headers->contains(
-                'Content-Type',
-                'application/json'
-            )
-        );
+        $this->assertEquals('application/json', $this->client->getResponse()->headers->get('Content-Type'));
+    }
+
+    public function testExportEntry()
+    {
+        $entry = $this->client->getContainer()
+            ->get('doctrine.orm.entity_manager')
+            ->getRepository('WallabagCoreBundle:Entry')
+            ->findOneBy(['user' => 1, 'isArchived' => false]);
+
+        if (!$entry) {
+            $this->markTestSkipped('No content found in db.');
+        }
+
+        $this->client->request('GET', '/api/entries/'.$entry->getId().'/export.epub');
+        $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
+
+        // epub format got the content type in the content
+        $this->assertContains('application/epub', $this->client->getResponse()->getContent());
+        $this->assertEquals('application/epub+zip', $this->client->getResponse()->headers->get('Content-Type'));
+
+        // re-auth client for mobi
+        $client = $this->createAuthorizedClient();
+        $client->request('GET', '/api/entries/'.$entry->getId().'/export.mobi');
+        $this->assertEquals(200, $client->getResponse()->getStatusCode());
+
+        $this->assertEquals('application/x-mobipocket-ebook', $client->getResponse()->headers->get('Content-Type'));
+
+        // re-auth client for pdf
+        $client = $this->createAuthorizedClient();
+        $client->request('GET', '/api/entries/'.$entry->getId().'/export.pdf');
+        $this->assertEquals(200, $client->getResponse()->getStatusCode());
+
+        $this->assertContains('PDF-', $client->getResponse()->getContent());
+        $this->assertEquals('application/pdf', $client->getResponse()->headers->get('Content-Type'));
+
+        // re-auth client for pdf
+        $client = $this->createAuthorizedClient();
+        $client->request('GET', '/api/entries/'.$entry->getId().'/export.txt');
+        $this->assertEquals(200, $client->getResponse()->getStatusCode());
+
+        $this->assertContains('text/plain', $client->getResponse()->headers->get('Content-Type'));
+
+        // re-auth client for pdf
+        $client = $this->createAuthorizedClient();
+        $client->request('GET', '/api/entries/'.$entry->getId().'/export.csv');
+        $this->assertEquals(200, $client->getResponse()->getStatusCode());
+
+        $this->assertContains('application/csv', $client->getResponse()->headers->get('Content-Type'));
     }
 
     public function testGetOneEntryWrongUser()
@@ -70,12 +113,7 @@ class WallabagRestControllerTest extends WallabagApiTestCase
         $this->assertEquals(1, $content['page']);
         $this->assertGreaterThanOrEqual(1, $content['pages']);
 
-        $this->assertTrue(
-            $this->client->getResponse()->headers->contains(
-                'Content-Type',
-                'application/json'
-            )
-        );
+        $this->assertEquals('application/json', $this->client->getResponse()->headers->get('Content-Type'));
     }
 
     public function testGetEntriesWithFullOptions()
@@ -117,12 +155,7 @@ class WallabagRestControllerTest extends WallabagApiTestCase
             $this->assertContains('since=1443274283', $content['_links'][$link]['href']);
         }
 
-        $this->assertTrue(
-            $this->client->getResponse()->headers->contains(
-                'Content-Type',
-                'application/json'
-            )
-        );
+        $this->assertEquals('application/json', $this->client->getResponse()->headers->get('Content-Type'));
     }
 
     public function testGetStarredEntries()
@@ -150,12 +183,7 @@ class WallabagRestControllerTest extends WallabagApiTestCase
             $this->assertContains('sort=updated', $content['_links'][$link]['href']);
         }
 
-        $this->assertTrue(
-            $this->client->getResponse()->headers->contains(
-                'Content-Type',
-                'application/json'
-            )
-        );
+        $this->assertEquals('application/json', $this->client->getResponse()->headers->get('Content-Type'));
     }
 
     public function testGetArchiveEntries()
@@ -182,12 +210,7 @@ class WallabagRestControllerTest extends WallabagApiTestCase
             $this->assertContains('archive=1', $content['_links'][$link]['href']);
         }
 
-        $this->assertTrue(
-            $this->client->getResponse()->headers->contains(
-                'Content-Type',
-                'application/json'
-            )
-        );
+        $this->assertEquals('application/json', $this->client->getResponse()->headers->get('Content-Type'));
     }
 
     public function testGetTaggedEntries()
@@ -214,12 +237,7 @@ class WallabagRestControllerTest extends WallabagApiTestCase
             $this->assertContains('tags='.urlencode('foo,bar'), $content['_links'][$link]['href']);
         }
 
-        $this->assertTrue(
-            $this->client->getResponse()->headers->contains(
-                'Content-Type',
-                'application/json'
-            )
-        );
+        $this->assertEquals('application/json', $this->client->getResponse()->headers->get('Content-Type'));
     }
 
     public function testGetDatedEntries()
@@ -246,12 +264,7 @@ class WallabagRestControllerTest extends WallabagApiTestCase
             $this->assertContains('since=1443274283', $content['_links'][$link]['href']);
         }
 
-        $this->assertTrue(
-            $this->client->getResponse()->headers->contains(
-                'Content-Type',
-                'application/json'
-            )
-        );
+        $this->assertEquals('application/json', $this->client->getResponse()->headers->get('Content-Type'));
     }
 
     public function testGetDatedSupEntries()
@@ -279,12 +292,7 @@ class WallabagRestControllerTest extends WallabagApiTestCase
             $this->assertContains('since='.($future->getTimestamp() + 1000), $content['_links'][$link]['href']);
         }
 
-        $this->assertTrue(
-            $this->client->getResponse()->headers->contains(
-                'Content-Type',
-                'application/json'
-            )
-        );
+        $this->assertEquals('application/json', $this->client->getResponse()->headers->get('Content-Type'));
     }
 
     public function testDeleteEntry()
index 1954c654a53fba9f7882d62ad524907fcd6fa069..8d0644d1c46ea9f9ea2cf8fb2298f7de18dd9867 100644 (file)
@@ -3,6 +3,11 @@
 namespace Tests\Wallabag\CoreBundle\Controller;
 
 use Tests\Wallabag\CoreBundle\WallabagCoreTestCase;
+use Wallabag\CoreBundle\Entity\Config;
+use Wallabag\UserBundle\Entity\User;
+use Wallabag\CoreBundle\Entity\Entry;
+use Wallabag\CoreBundle\Entity\Tag;
+use Wallabag\AnnotationBundle\Entity\Annotation;
 
 class ConfigControllerTest extends WallabagCoreTestCase
 {
@@ -570,4 +575,264 @@ class ConfigControllerTest extends WallabagCoreTestCase
         $config->set('demo_mode_enabled', 0);
         $config->set('demo_mode_username', 'wallabag');
     }
+
+    public function testDeleteUserButtonVisibility()
+    {
+        $this->logInAs('admin');
+        $client = $this->getClient();
+
+        $crawler = $client->request('GET', '/config');
+
+        $this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
+        $this->assertContains('config.form_user.delete.button', $body[0]);
+
+        $em = $client->getContainer()->get('doctrine.orm.entity_manager');
+
+        $user = $em
+            ->getRepository('WallabagUserBundle:User')
+            ->findOneByUsername('empty');
+        $user->setExpired(1);
+        $em->persist($user);
+
+        $user = $em
+            ->getRepository('WallabagUserBundle:User')
+            ->findOneByUsername('bob');
+        $user->setExpired(1);
+        $em->persist($user);
+
+        $em->flush();
+
+        $crawler = $client->request('GET', '/config');
+
+        $this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
+        $this->assertNotContains('config.form_user.delete.button', $body[0]);
+
+        $client->request('GET', '/account/delete');
+        $this->assertEquals(403, $client->getResponse()->getStatusCode());
+
+        $user = $em
+            ->getRepository('WallabagUserBundle:User')
+            ->findOneByUsername('empty');
+        $user->setExpired(0);
+        $em->persist($user);
+
+        $user = $em
+            ->getRepository('WallabagUserBundle:User')
+            ->findOneByUsername('bob');
+        $user->setExpired(0);
+        $em->persist($user);
+
+        $em->flush();
+    }
+
+    public function testDeleteAccount()
+    {
+        $client = $this->getClient();
+        $em = $client->getContainer()->get('doctrine.orm.entity_manager');
+
+        $user = new User();
+        $user->setName('Wallace');
+        $user->setEmail('wallace@wallabag.org');
+        $user->setUsername('wallace');
+        $user->setPlainPassword('wallace');
+        $user->setEnabled(true);
+        $user->addRole('ROLE_SUPER_ADMIN');
+
+        $em->persist($user);
+
+        $config = new Config($user);
+
+        $config->setTheme('material');
+        $config->setItemsPerPage(30);
+        $config->setReadingSpeed(1);
+        $config->setLanguage('en');
+        $config->setPocketConsumerKey('xxxxx');
+
+        $em->persist($config);
+        $em->flush();
+
+        $this->logInAs('wallace');
+        $loggedInUserId = $this->getLoggedInUserId();
+
+        // create entry to check after user deletion
+        // that this entry is also deleted
+        $crawler = $client->request('GET', '/new');
+
+        $this->assertEquals(200, $client->getResponse()->getStatusCode());
+
+        $form = $crawler->filter('form[name=entry]')->form();
+        $data = [
+            'entry[url]' => $url = 'https://github.com/wallabag/wallabag',
+        ];
+
+        $client->submit($form, $data);
+        $this->assertEquals(302, $client->getResponse()->getStatusCode());
+
+        $crawler = $client->request('GET', '/config');
+
+        $deleteLink = $crawler->filter('.delete-account')->last()->link();
+
+        $client->click($deleteLink);
+        $this->assertEquals(302, $client->getResponse()->getStatusCode());
+
+        $em = $client->getContainer()->get('doctrine.orm.entity_manager');
+        $user = $em
+            ->getRepository('WallabagUserBundle:User')
+            ->createQueryBuilder('u')
+            ->where('u.username = :username')->setParameter('username', 'wallace')
+            ->getQuery()
+            ->getOneOrNullResult()
+        ;
+
+        $this->assertNull($user);
+
+        $entries = $client->getContainer()
+            ->get('doctrine.orm.entity_manager')
+            ->getRepository('WallabagCoreBundle:Entry')
+            ->findByUser($loggedInUserId);
+
+        $this->assertEmpty($entries);
+    }
+
+    public function testReset()
+    {
+        $this->logInAs('empty');
+        $client = $this->getClient();
+
+        $em = $client->getContainer()->get('doctrine.orm.entity_manager');
+
+        $user = static::$kernel->getContainer()->get('security.token_storage')->getToken()->getUser();
+
+        $tag = new Tag();
+        $tag->setLabel('super');
+        $em->persist($tag);
+
+        $entry = new Entry($user);
+        $entry->setUrl('http://www.lemonde.fr/europe/article/2016/10/01/pour-le-psoe-chaque-election-s-est-transformee-en-une-agonie_5006476_3214.html');
+        $entry->setContent('Youhou');
+        $entry->setTitle('Youhou');
+        $entry->addTag($tag);
+        $em->persist($entry);
+
+        $entry2 = new Entry($user);
+        $entry2->setUrl('http://www.lemonde.de/europe/article/2016/10/01/pour-le-psoe-chaque-election-s-est-transformee-en-une-agonie_5006476_3214.html');
+        $entry2->setContent('Youhou');
+        $entry2->setTitle('Youhou');
+        $entry2->addTag($tag);
+        $em->persist($entry2);
+
+        $annotation = new Annotation($user);
+        $annotation->setText('annotated');
+        $annotation->setQuote('annotated');
+        $annotation->setRanges([]);
+        $annotation->setEntry($entry);
+        $em->persist($annotation);
+
+        $em->flush();
+
+        // reset annotations
+        $crawler = $client->request('GET', '/config#set3');
+
+        $this->assertEquals(200, $client->getResponse()->getStatusCode());
+
+        $crawler = $client->click($crawler->selectLink('config.reset.annotations')->link());
+
+        $this->assertEquals(302, $client->getResponse()->getStatusCode());
+        $this->assertContains('flashes.config.notice.annotations_reset', $client->getContainer()->get('session')->getFlashBag()->get('notice')[0]);
+
+        $annotationsReset = $em
+            ->getRepository('WallabagAnnotationBundle:Annotation')
+            ->findAnnotationsByPageId($entry->getId(), $user->getId());
+
+        $this->assertEmpty($annotationsReset, 'Annotations were reset');
+
+        // reset tags
+        $crawler = $client->request('GET', '/config#set3');
+
+        $this->assertEquals(200, $client->getResponse()->getStatusCode());
+
+        $crawler = $client->click($crawler->selectLink('config.reset.tags')->link());
+
+        $this->assertEquals(302, $client->getResponse()->getStatusCode());
+        $this->assertContains('flashes.config.notice.tags_reset', $client->getContainer()->get('session')->getFlashBag()->get('notice')[0]);
+
+        $tagReset = $em
+            ->getRepository('WallabagCoreBundle:Tag')
+            ->countAllTags($user->getId());
+
+        $this->assertEquals(0, $tagReset, 'Tags were reset');
+
+        // reset entries
+        $crawler = $client->request('GET', '/config#set3');
+
+        $this->assertEquals(200, $client->getResponse()->getStatusCode());
+
+        $crawler = $client->click($crawler->selectLink('config.reset.entries')->link());
+
+        $this->assertEquals(302, $client->getResponse()->getStatusCode());
+        $this->assertContains('flashes.config.notice.entries_reset', $client->getContainer()->get('session')->getFlashBag()->get('notice')[0]);
+
+        $entryReset = $em
+            ->getRepository('WallabagCoreBundle:Entry')
+            ->countAllEntriesByUsername($user->getId());
+
+        $this->assertEquals(0, $entryReset, 'Entries were reset');
+    }
+
+    public function testResetEntriesCascade()
+    {
+        $this->logInAs('empty');
+        $client = $this->getClient();
+
+        $em = $client->getContainer()->get('doctrine.orm.entity_manager');
+
+        $user = static::$kernel->getContainer()->get('security.token_storage')->getToken()->getUser();
+
+        $tag = new Tag();
+        $tag->setLabel('super');
+        $em->persist($tag);
+
+        $entry = new Entry($user);
+        $entry->setUrl('http://www.lemonde.fr/europe/article/2016/10/01/pour-le-psoe-chaque-election-s-est-transformee-en-une-agonie_5006476_3214.html');
+        $entry->setContent('Youhou');
+        $entry->setTitle('Youhou');
+        $entry->addTag($tag);
+        $em->persist($entry);
+
+        $annotation = new Annotation($user);
+        $annotation->setText('annotated');
+        $annotation->setQuote('annotated');
+        $annotation->setRanges([]);
+        $annotation->setEntry($entry);
+        $em->persist($annotation);
+
+        $em->flush();
+
+        $crawler = $client->request('GET', '/config#set3');
+
+        $this->assertEquals(200, $client->getResponse()->getStatusCode());
+
+        $crawler = $client->click($crawler->selectLink('config.reset.entries')->link());
+
+        $this->assertEquals(302, $client->getResponse()->getStatusCode());
+        $this->assertContains('flashes.config.notice.entries_reset', $client->getContainer()->get('session')->getFlashBag()->get('notice')[0]);
+
+        $entryReset = $em
+            ->getRepository('WallabagCoreBundle:Entry')
+            ->countAllEntriesByUsername($user->getId());
+
+        $this->assertEquals(0, $entryReset, 'Entries were reset');
+
+        $tagReset = $em
+            ->getRepository('WallabagCoreBundle:Tag')
+            ->countAllTags($user->getId());
+
+        $this->assertEquals(0, $tagReset, 'Tags were reset');
+
+        $annotationsReset = $em
+            ->getRepository('WallabagAnnotationBundle:Annotation')
+            ->findAnnotationsByPageId($entry->getId(), $user->getId());
+
+        $this->assertEmpty($annotationsReset, 'Annotations were reset');
+    }
 }