]> git.immae.eu Git - github/wallabag/wallabag.git/commitdiff
Merge pull request #1493 from wallabag/v2-pocket-import 2.0.0-alpha.1
authorJeremy Benoist <j0k3r@users.noreply.github.com>
Thu, 7 Jan 2016 21:15:08 +0000 (22:15 +0100)
committerJeremy Benoist <j0k3r@users.noreply.github.com>
Thu, 7 Jan 2016 21:15:08 +0000 (22:15 +0100)
v2 – 1st draft for Pocket import via API & Wallabag v1 import

51 files changed:
app/AppKernel.php
app/config/config.yml
app/config/parameters.yml.dist
app/config/routing.yml
app/config/tests/parameters.yml.dist.mysql
app/config/tests/parameters.yml.dist.pgsql
app/config/tests/parameters.yml.dist.sqlite
composer.json
composer.lock
docs/en/index.rst
docs/en/user/import.rst [new file with mode: 0644]
docs/img/user/export_wllbg_1.png [new file with mode: 0644]
docs/img/user/import_wllbg.png [new file with mode: 0644]
src/Wallabag/ApiBundle/Controller/WallabagRestController.php
src/Wallabag/CoreBundle/Command/InstallCommand.php
src/Wallabag/CoreBundle/Controller/EntryController.php
src/Wallabag/CoreBundle/Entity/Config.php
src/Wallabag/CoreBundle/Entity/Entry.php
src/Wallabag/CoreBundle/Repository/EntryRepository.php
src/Wallabag/CoreBundle/Resources/config/services.yml
src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml
src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig
src/Wallabag/CoreBundle/Resources/views/themes/material/public/css/main.css
src/Wallabag/ImportBundle/Command/ImportCommand.php [new file with mode: 0644]
src/Wallabag/ImportBundle/Controller/ImportController.php [new file with mode: 0644]
src/Wallabag/ImportBundle/Controller/PocketController.php [new file with mode: 0644]
src/Wallabag/ImportBundle/Controller/WallabagV1Controller.php [new file with mode: 0644]
src/Wallabag/ImportBundle/DependencyInjection/Configuration.php [new file with mode: 0644]
src/Wallabag/ImportBundle/DependencyInjection/WallabagImportExtension.php [new file with mode: 0644]
src/Wallabag/ImportBundle/Form/Type/UploadImportType.php [new file with mode: 0644]
src/Wallabag/ImportBundle/Import/ImportChain.php [new file with mode: 0644]
src/Wallabag/ImportBundle/Import/ImportCompilerPass.php [new file with mode: 0644]
src/Wallabag/ImportBundle/Import/ImportInterface.php [new file with mode: 0644]
src/Wallabag/ImportBundle/Import/PocketImport.php [new file with mode: 0644]
src/Wallabag/ImportBundle/Import/WallabagV1Import.php [new file with mode: 0644]
src/Wallabag/ImportBundle/Resources/config/services.yml [new file with mode: 0644]
src/Wallabag/ImportBundle/Resources/views/Import/index.html.twig [new file with mode: 0644]
src/Wallabag/ImportBundle/Resources/views/Pocket/index.html.twig [new file with mode: 0644]
src/Wallabag/ImportBundle/Resources/views/WallabagV1/index.html.twig [new file with mode: 0644]
src/Wallabag/ImportBundle/Tests/Controller/ImportControllerTest.php [new file with mode: 0644]
src/Wallabag/ImportBundle/Tests/Controller/PocketControllerTest.php [new file with mode: 0644]
src/Wallabag/ImportBundle/Tests/Controller/WallabagV1ControllerTest.php [new file with mode: 0644]
src/Wallabag/ImportBundle/Tests/Import/ImportChainTest.php [new file with mode: 0644]
src/Wallabag/ImportBundle/Tests/Import/ImportCompilerPassTest.php [new file with mode: 0644]
src/Wallabag/ImportBundle/Tests/Import/PocketImportTest.php [new file with mode: 0644]
src/Wallabag/ImportBundle/Tests/Import/WallabagV1ImportTest.php [new file with mode: 0644]
src/Wallabag/ImportBundle/Tests/fixtures/test.html [new file with mode: 0644]
src/Wallabag/ImportBundle/Tests/fixtures/test.txt [new file with mode: 0644]
src/Wallabag/ImportBundle/Tests/fixtures/wallabag-v1.json [new file with mode: 0644]
src/Wallabag/ImportBundle/WallabagImportBundle.php [new file with mode: 0644]
web/uploads/import/.gitkeep [new file with mode: 0644]

index 85edc14affea612128abe6a8ea1214393c4eff84..93b0201a5844219c06d196d61847b782dfce6dba 100644 (file)
@@ -31,6 +31,7 @@ class AppKernel extends Kernel
             new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),
             new Scheb\TwoFactorBundle\SchebTwoFactorBundle(),
             new KPhoen\RulerZBundle\KPhoenRulerZBundle(),
+            new Wallabag\ImportBundle\WallabagImportBundle(),
         );
 
         if (in_array($this->getEnvironment(), array('dev', 'test'))) {
index 8403a458f69916a11f2745825de010a2627413c2..e50f9b52e41614eb342fbc06a37c6b32a53c1539 100644 (file)
@@ -31,6 +31,10 @@ wallabag_core:
         fr: 'Français'
         de: 'Deutsch'
 
+wallabag_import:
+    allow_mimetypes: ['application/octet-stream', 'application/json', 'text/plain']
+    resource_dir: "%kernel.root_dir%/../web/uploads/import"
+
 # Twig Configuration
 twig:
     debug:            "%kernel.debug%"
index 149179c2e8afa849aa8b79795a1dae9a121003d9..a769bc6638a2a6897f754f2268dbe94a287c4b9c 100644 (file)
@@ -60,3 +60,6 @@ parameters:
     language: en
     from_email: no-reply@wallabag.org
     rss_limit: 50
+
+    # pocket import
+    pocket_consumer_key: xxxxxxxx
index 0f7b61fb3427991b7fad329357ae82688a57dc3f..1ca2f677bba6f29340c45d2588c8bb377f002808 100644 (file)
@@ -1,3 +1,8 @@
+wallabag_import:
+    resource: "@WallabagImportBundle/Controller/"
+    type:     annotation
+    prefix:   /import
+
 wallabag_api:
     resource: "@WallabagApiBundle/Resources/config/routing.yml"
     prefix: /
index 096ad8c767fc41aeeaa0ea90d01c36baf0b27576..88b1d2b40afcd8203e3a70d99da012aef14d85b6 100644 (file)
@@ -60,3 +60,6 @@ parameters:
     language: en_US
     from_email: no-reply@wallabag.org
     rss_limit: 50
+
+    # pocket import
+    pocket_consumer_key: xxxxxxxx
index ca3f6ea20ab59a3347a649203bb0590e9f7d9dd6..3c61142dd0c8987298f8f802d56721c35b8ec0ac 100644 (file)
@@ -60,3 +60,6 @@ parameters:
     language: en_US
     from_email: no-reply@wallabag.org
     rss_limit: 50
+
+    # pocket import
+    pocket_consumer_key: xxxxxxxx
index 92460bcfc1b12bcf6543d27becfebe7edcc341c5..2f7699b5795fb5f436f460503bea7e50c24bbd37 100644 (file)
@@ -60,3 +60,6 @@ parameters:
     language: en_US
     from_email: no-reply@wallabag.org
     rss_limit: 50
+
+    # pocket import
+    pocket_consumer_key: xxxxxxxx
index bf519faf229f0391c99018f08f00a4f44f9f96db..0ba42a3e29bef3a19bb00217472e401506848f56 100644 (file)
@@ -59,7 +59,8 @@
         "scheb/two-factor-bundle": "~1.4.0",
         "grandt/phpepub": "~4.0",
         "wallabag/php-mobi": "~1.0.0",
-        "kphoen/rulerz-bundle": "~0.10"
+        "kphoen/rulerz-bundle": "~0.10",
+        "guzzlehttp/guzzle": "^5.2.0"
     },
     "require-dev": {
         "doctrine/doctrine-fixtures-bundle": "~2.2.0",
index aee96198641628616de82039f061c7addc039956..858a125c10b3c3f928fe69f0f4a18bd395e5fcf0 100644 (file)
@@ -4,8 +4,8 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "91da706ef4b39a73704c3e2154c1a227",
-    "content-hash": "81a3c2c84d78471bfb526b2b572182f7",
+    "hash": "fdba142656b2089b0e4cbddb45e2ad1f",
+    "content-hash": "a233f851c52683783b6a42be707c52b1",
     "packages": [
         {
             "name": "behat/transliterator",
         },
         {
             "name": "doctrine/cache",
-            "version": "v1.5.4",
+            "version": "v1.6.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/cache.git",
-                "reference": "47cdc76ceb95cc591d9c79a36dc3794975b5d136"
+                "reference": "f8af318d14bdb0eff0336795b428b547bd39ccb6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/cache/zipball/47cdc76ceb95cc591d9c79a36dc3794975b5d136",
-                "reference": "47cdc76ceb95cc591d9c79a36dc3794975b5d136",
+                "url": "https://api.github.com/repos/doctrine/cache/zipball/f8af318d14bdb0eff0336795b428b547bd39ccb6",
+                "reference": "f8af318d14bdb0eff0336795b428b547bd39ccb6",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.2"
+                "php": "~5.5|~7.0"
             },
             "conflict": {
                 "doctrine/common": ">2.2,<2.4"
             },
             "require-dev": {
-                "phpunit/phpunit": ">=3.7",
+                "phpunit/phpunit": "~4.8|~5.0",
                 "predis/predis": "~1.0",
                 "satooshi/php-coveralls": "~0.6"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.5.x-dev"
+                    "dev-master": "1.6.x-dev"
                 }
             },
             "autoload": {
                 "cache",
                 "caching"
             ],
-            "time": "2015-12-19 05:03:47"
+            "time": "2015-12-31 16:37:02"
         },
         {
             "name": "doctrine/collections",
         },
         {
             "name": "friendsofsymfony/rest-bundle",
-            "version": "1.7.6",
+            "version": "1.7.7",
             "target-dir": "FOS/RestBundle",
             "source": {
                 "type": "git",
                 "url": "https://github.com/FriendsOfSymfony/FOSRestBundle.git",
-                "reference": "f95b2f141748e9a5e2ddae833f60c38417aee8c3"
+                "reference": "c79b7e5df96e5581591ceb6a026bd4e5f9346de0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/FriendsOfSymfony/FOSRestBundle/zipball/f95b2f141748e9a5e2ddae833f60c38417aee8c3",
-                "reference": "f95b2f141748e9a5e2ddae833f60c38417aee8c3",
+                "url": "https://api.github.com/repos/FriendsOfSymfony/FOSRestBundle/zipball/c79b7e5df96e5581591ceb6a026bd4e5f9346de0",
+                "reference": "c79b7e5df96e5581591ceb6a026bd4e5f9346de0",
                 "shasum": ""
             },
             "require": {
             "keywords": [
                 "rest"
             ],
-            "time": "2015-12-20 13:45:30"
+            "time": "2015-12-29 16:02:50"
         },
         {
             "name": "friendsofsymfony/user-bundle",
             "source": {
                 "type": "git",
                 "url": "https://github.com/FriendsOfSymfony/FOSUserBundle.git",
-                "reference": "e39b040e272c72f0a090c67d802e1d3b2d0b0313"
+                "reference": "e5e7a2b8984da8dfedaf44adc7e5f60a62ad280c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/FriendsOfSymfony/FOSUserBundle/zipball/e39b040e272c72f0a090c67d802e1d3b2d0b0313",
-                "reference": "e39b040e272c72f0a090c67d802e1d3b2d0b0313",
+                "url": "https://api.github.com/repos/FriendsOfSymfony/FOSUserBundle/zipball/e5e7a2b8984da8dfedaf44adc7e5f60a62ad280c",
+                "reference": "e5e7a2b8984da8dfedaf44adc7e5f60a62ad280c",
                 "shasum": ""
             },
             "require": {
             "keywords": [
                 "User management"
             ],
-            "time": "2015-12-05 09:38:57"
+            "time": "2015-12-28 18:02:43"
         },
         {
             "name": "gedmo/doctrine-extensions",
index 6ccfd44c7a1871db7516cd5f8abb0e15f7e7113a..8cb1b47904e06e26ef54c73de2ef1a98027bdced 100644 (file)
@@ -24,6 +24,7 @@ The main documentation for the site is organized into a couple sections:
    user/login
    user/configuration
    user/first_article
+   user/import
 
 user/organize
 user/filters
diff --git a/docs/en/user/import.rst b/docs/en/user/import.rst
new file mode 100644 (file)
index 0000000..d326b06
--- /dev/null
@@ -0,0 +1,39 @@
+Migrate to wallabag
+===================
+
+From wallabag 1.x
+-----------------
+
+Export your data from your wallabag 1.x
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+On your config page, click on ``JSON export`` in the ``Export your wallabag data`` section.
+
+.. image:: ../../img/user/export_wllbg_1.png
+   :alt: Export from wallabag 1.x
+   :align: center
+
+You will have a ``wallabag-export-1-1970-01-01.json`` file.
+
+Import your data into wallabag 2.x
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Click on  ``Import`` link in the menu, select your export file on your computer and import it.
+
+.. image:: ../../img/user/import_wllbg.png
+   :alt: Import from wallabag 1.x
+   :align: center
+
+All your wallabag 1.x articles will be imported.
+
+From Pocket
+-----------
+
+From Instapaper
+---------------
+
+From Readability
+----------------
+
+From HTML or JSON file
+----------------------
diff --git a/docs/img/user/export_wllbg_1.png b/docs/img/user/export_wllbg_1.png
new file mode 100644 (file)
index 0000000..f9d2451
Binary files /dev/null and b/docs/img/user/export_wllbg_1.png differ
diff --git a/docs/img/user/import_wllbg.png b/docs/img/user/import_wllbg.png
new file mode 100644 (file)
index 0000000..6eec07e
Binary files /dev/null and b/docs/img/user/import_wllbg.png differ
index 459c41729d8a8b9543a98c64cc6147267da3ce87..354a6f8e7a0b750c63ec06b26fb68a4f6506a81f 100644 (file)
@@ -60,7 +60,7 @@ class WallabagRestController extends FOSRestController
      *       }
      * )
      *
-     * @return Entry
+     * @return Response
      */
     public function getEntriesAction(Request $request)
     {
@@ -101,7 +101,7 @@ class WallabagRestController extends FOSRestController
      *      }
      * )
      *
-     * @return Entry
+     * @return Response
      */
     public function getEntryAction(Entry $entry)
     {
@@ -124,7 +124,7 @@ class WallabagRestController extends FOSRestController
      *       }
      * )
      *
-     * @return Entry
+     * @return Response
      */
     public function postEntriesAction(Request $request)
     {
@@ -166,7 +166,7 @@ class WallabagRestController extends FOSRestController
      *      }
      * )
      *
-     * @return Entry
+     * @return Response
      */
     public function patchEntriesAction(Entry $entry, Request $request)
     {
@@ -211,7 +211,7 @@ class WallabagRestController extends FOSRestController
      *      }
      * )
      *
-     * @return Entry
+     * @return Response
      */
     public function deleteEntriesAction(Entry $entry)
     {
@@ -235,6 +235,8 @@ class WallabagRestController extends FOSRestController
      *          {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
      *      }
      * )
+     *
+     * @return Response
      */
     public function getEntriesTagsAction(Entry $entry)
     {
@@ -257,6 +259,8 @@ class WallabagRestController extends FOSRestController
      *          {"name"="tags", "dataType"="string", "required"=false, "format"="tag1,tag2,tag3", "description"="a comma-separated list of tags."},
      *       }
      * )
+     *
+     * @return Response
      */
     public function postEntriesTagsAction(Request $request, Entry $entry)
     {
@@ -286,6 +290,8 @@ class WallabagRestController extends FOSRestController
      *          {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
      *      }
      * )
+     *
+     * @return Response
      */
     public function deleteEntriesTagsAction(Entry $entry, Tag $tag)
     {
@@ -306,6 +312,8 @@ class WallabagRestController extends FOSRestController
      * Retrieve all tags.
      *
      * @ApiDoc()
+     *
+     * @return Response
      */
     public function getTagsAction()
     {
@@ -328,6 +336,8 @@ class WallabagRestController extends FOSRestController
      *          {"name"="tag", "dataType"="integer", "requirement"="\w+", "description"="The tag"}
      *      }
      * )
+     *
+     * @return Response
      */
     public function deleteTagAction(Tag $tag)
     {
index 85c4ee902ee2ad308e6f6bfcc93671710bf3886a..e791d4dda5b462a7262e350b75bdb7165064f5e2 100644 (file)
@@ -11,7 +11,6 @@ use Symfony\Component\Console\Output\NullOutput;
 use Symfony\Component\Console\Question\Question;
 use Symfony\Component\Console\Question\ConfirmationQuestion;
 use Symfony\Component\Console\Helper\Table;
-use Wallabag\UserBundle\Entity\User;
 use Wallabag\CoreBundle\Entity\Config;
 
 class InstallCommand extends ContainerAwareCommand
index fa58013312a74169207f4795c2e8c4dd253f474f..37f7ab60a6822df11733d6d5366ecde021b4d289 100644 (file)
@@ -48,6 +48,19 @@ class EntryController extends Controller
         $form->handleRequest($request);
 
         if ($form->isValid()) {
+            // check for existing entry, if it exists, redirect to it with a message
+            $existingEntry = $this->get('wallabag_core.entry_repository')
+                ->existByUrlAndUserId($entry->getUrl(), $this->getUser()->getId());
+
+            if (false !== $existingEntry) {
+                $this->get('session')->getFlashBag()->add(
+                    'notice',
+                    'Entry already saved on '.$existingEntry['createdAt']->format('d-m-Y')
+                );
+
+                return $this->redirect($this->generateUrl('view', array('id' => $existingEntry['id'])));
+            }
+
             $this->updateEntry($entry);
             $this->get('session')->getFlashBag()->add(
                 'notice',
index 2ca4182e6700da790f8f6f02e1fa44701bbee1ed..d3590f35f0945495ef6f07697ebb83a96afe44e1 100644 (file)
@@ -5,6 +5,7 @@ namespace Wallabag\CoreBundle\Entity;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\ORM\Mapping as ORM;
 use Symfony\Component\Validator\Constraints as Assert;
+use Wallabag\UserBundle\Entity\User;
 
 /**
  * Config.
@@ -86,7 +87,7 @@ class Config
     /*
      * @param User     $user
      */
-    public function __construct(\Wallabag\UserBundle\Entity\User $user)
+    public function __construct(User $user)
     {
         $this->user = $user;
         $this->taggingRules = new ArrayCollection();
@@ -181,7 +182,7 @@ class Config
      *
      * @return Config
      */
-    public function setUser(\Wallabag\UserBundle\Entity\User $user = null)
+    public function setUser(User $user = null)
     {
         $this->user = $user;
 
@@ -225,7 +226,7 @@ class Config
     /**
      * Set rssLimit.
      *
-     * @param string $rssLimit
+     * @param int $rssLimit
      *
      * @return Config
      */
@@ -239,7 +240,7 @@ class Config
     /**
      * Get rssLimit.
      *
-     * @return string
+     * @return int
      */
     public function getRssLimit()
     {
index b413c489c7c9291922a4b9a8fc6fea1860711011..f11a77860450105624623e5eae91869fbac09849 100644 (file)
@@ -245,7 +245,7 @@ class Entry
     /**
      * Set isArchived.
      *
-     * @param string $isArchived
+     * @param bool $isArchived
      *
      * @return Entry
      */
@@ -259,7 +259,7 @@ class Entry
     /**
      * Get isArchived.
      *
-     * @return string
+     * @return bool
      */
     public function isArchived()
     {
@@ -276,7 +276,7 @@ class Entry
     /**
      * Set isStarred.
      *
-     * @param string $isStarred
+     * @param bool $isStarred
      *
      * @return Entry
      */
@@ -290,7 +290,7 @@ class Entry
     /**
      * Get isStarred.
      *
-     * @return string
+     * @return bool
      */
     public function isStarred()
     {
index ca71970bcbaf9b83936da34a93bde1694c16824a..c6763a40caa26d81febc8083bb4c4c267f94acf3 100644 (file)
@@ -223,4 +223,29 @@ class EntryRepository extends EntityRepository
             ->getQuery()
             ->getResult();
     }
+
+    /**
+     * Find an entry by its url and its owner.
+     * If it exists, return the entry otherwise return false.
+     *
+     * @param $url
+     * @param $userId
+     *
+     * @return array|bool
+     */
+    public function existByUrlAndUserId($url, $userId)
+    {
+        $res = $this->createQueryBuilder('e')
+            ->select('e.id, e.createdAt')
+            ->where('e.url = :url')->setParameter('url', $url)
+            ->andWhere('e.user = :user_id')->setParameter('user_id', $userId)
+            ->getQuery()
+            ->getResult();
+
+        if (count($res)) {
+            return current($res);
+        }
+
+        return false;
+    }
 }
index c92b4eb37c843d49ca5ec12b1b68d08dffd7cfc4..96b1c931339eb37760a6eb04cbd114c317d42f8d 100644 (file)
@@ -63,6 +63,7 @@ services:
             - @wallabag_core.tag_repository
             - @wallabag_core.entry_repository
 
+    # repository as a service
     wallabag_core.entry_repository:
         class: Wallabag\CoreBundle\Repository\EntryRepository
         factory: [ @doctrine.orm.default_entity_manager, getRepository ]
index 7b10dea11ef2d414c9efbafa5ef3aa0b44a026af..067465842d4906a5cddd650d808db3aafd8e5677 100644 (file)
@@ -13,6 +13,7 @@ archive: 'Lus'
 all: 'Tous les articles'
 tags: 'Tags'
 config: 'Configuration'
+import: 'Importer'
 howto: 'Aide'
 logout: 'Déconnexion'
 Filtered: 'Articles filtrés'
@@ -128,3 +129,14 @@ Download: 'Télécharger'
 Does this article appear wrong?: "Est-ce que cet article s'affiche mal ?"
 Problems?: 'Un problème ?'
 Edit title: "Modifier le titre"
+
+# Import
+Welcome on wallabag importer. Please select your previous service that you want to migrate.: "Bienvenue dans l'outil de migration de wallabag. Choisissez ci-dessous le service depuis lequel vous souhaitez migrer."
+"This importer will import all your Pocket data. Pocket doesn't allow us to retrieve content from their service, so the readable content of each article will be re-fetched by wallabag.": "Cet outil va importer toutes vos données de Pocket. Pocket ne nous autorise pas à récupérer le contenu depuis leur service, donc wallabag doit reparcourir chaque article pour récupérer son contenu."
+"This importer will import all your wallabag v1 articles. On your config page, click on \"JSON export\" in the \"Export your wallabag data\" section. You will have a \"wallabag-export-1-xxxx-xx-xx.json\" file.": "Cet outil va importer toutes vos données de wallabag v1. Sur votre page de configuration de wallabag v1, cliquez sur \"Export JSON\" dans la section \"Exporter vos données de wallabag\". Vous allez récupérer un fichier \"wallabag-export-1-xxxx-xx-xx.json\"."
+"You can import your data from your Pocket account. You just have to click on the below button and authorize the application to connect to getpocket.com.": "Vous pouvez importer vos données depuis votre compte Pocket. Vous n'avez qu'à cliquer sur le bouton ci-dessous et à autoriser wallabag à se connecter à getpocket.com."
+Connect to Pocket and import data: Se connecter à Pocket et importer les données.
+Please select your wallabag export and click on the below button to upload and import it.: Choisissez le fichier de votre export wallabag v1 et cliquez sur le bouton ci-dessous pour l'importer.
+File: Fichier
+Upload file: Importer le fichier
+Import contents: "Importer les contenus"
\ No newline at end of file
index f426e25b97addb1a0fea2410b97cb10abac89f9f..6b8d7adf4a1db8a090dd276dba6655f8e53e682f 100644 (file)
@@ -45,6 +45,7 @@
             <li class="bold border-bottom {% if currentRoute == 'all' %}active{% endif %}"><a class="waves-effect" href="{{ path('all') }}">{% trans %}all{% endtrans %}</a></li>
             <li class="bold border-bottom {% if currentRoute == 'tags' %}active{% endif %}"><a class="waves-effect" href="{{ path('tag') }}">{% trans %}tags{% endtrans %}</a></li>
             <li class="bold {% if currentRoute == 'config' %}active{% endif %}"><a class="waves-effect" href="{{ path('config') }}">{% trans %}config{% endtrans %}</a></li>
+            <li class="bold {% if currentRoute == 'import' %}active{% endif %}"><a class="waves-effect" href="{{ path('import') }}">{% trans %}import{% endtrans %}</a></li>
             <li class="bold {% if currentRoute == 'howto' %}active{% endif %}"><a class="waves-effect" href="{{ path('howto') }}">{% trans %}howto{% endtrans %}</a></li>
             <li class="bold"><a class="waves-effect" class="icon icon-power" href="{{ path('fos_user_security_logout') }}" title="{% trans %}logout{% endtrans %}">{% trans %}logout{% endtrans %}</a></li>
         </ul>
index 7396557148a894df062d075abdae8df1ddd5e04a..0ce334a319212ac0a1a8e12b6547cd36c161d5ed 100755 (executable)
@@ -500,4 +500,8 @@ footer [class^="icon-"]:hover, footer [class*=" icon-"]:hover {
 /* force height on non-input field in the settings page */
 div.settings div.input-field div, div.settings div.input-field ul {
     margin-top: 40px;
-}
\ No newline at end of file
+}
+/* but avoid to kill all file input */
+div.settings div.file-field div {
+    margin-top: inherit;
+}
diff --git a/src/Wallabag/ImportBundle/Command/ImportCommand.php b/src/Wallabag/ImportBundle/Command/ImportCommand.php
new file mode 100644 (file)
index 0000000..dfbfc2f
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+
+namespace Wallabag\ImportBundle\Command;
+
+use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
+use Symfony\Component\Config\Definition\Exception\Exception;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ImportCommand extends ContainerAwareCommand
+{
+    protected function configure()
+    {
+        $this
+            ->setName('wallabag:import-v1')
+            ->setDescription('Import entries from a JSON export from a wallabag v1 instance')
+            ->addArgument('userId', InputArgument::REQUIRED, 'User ID to populate')
+            ->addArgument('filepath', InputArgument::REQUIRED, 'Path to the JSON file')
+        ;
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $output->writeln('Start : '.(new \DateTime())->format('d-m-Y G:i:s').' ---');
+
+        $em = $this->getContainer()->get('doctrine')->getManager();
+        // Turning off doctrine default logs queries for saving memory
+        $em->getConnection()->getConfiguration()->setSQLLogger(null);
+
+        $user = $em->getRepository('WallabagUserBundle:User')->findOneById($input->getArgument('userId'));
+
+        if (!is_object($user)) {
+            throw new Exception(sprintf('User with id "%s" not found', $input->getArgument('userId')));
+        }
+
+        $wallabag = $this->getContainer()->get('wallabag_import.wallabag_v1.import');
+        $res = $wallabag
+            ->setUser($user)
+            ->setFilepath($input->getArgument('filepath'))
+            ->import();
+
+        if (true === $res) {
+            $summary = $wallabag->getSummary();
+            $output->writeln('<info>'.$summary['imported'].' imported</info>');
+            $output->writeln('<comment>'.$summary['skipped'].' already saved</comment>');
+        }
+
+        $em->clear();
+
+        $output->writeln('End : '.(new \DateTime())->format('d-m-Y G:i:s').' ---');
+    }
+}
diff --git a/src/Wallabag/ImportBundle/Controller/ImportController.php b/src/Wallabag/ImportBundle/Controller/ImportController.php
new file mode 100644 (file)
index 0000000..c1486e3
--- /dev/null
@@ -0,0 +1,19 @@
+<?php
+
+namespace Wallabag\ImportBundle\Controller;
+
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
+
+class ImportController extends Controller
+{
+    /**
+     * @Route("/", name="import")
+     */
+    public function importAction()
+    {
+        return $this->render('WallabagImportBundle:Import:index.html.twig', [
+            'imports' => $this->get('wallabag_import.chain')->getAll(),
+        ]);
+    }
+}
diff --git a/src/Wallabag/ImportBundle/Controller/PocketController.php b/src/Wallabag/ImportBundle/Controller/PocketController.php
new file mode 100644 (file)
index 0000000..a085338
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+namespace Wallabag\ImportBundle\Controller;
+
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
+
+class PocketController extends Controller
+{
+    /**
+     * @Route("/pocket", name="import_pocket")
+     */
+    public function indexAction()
+    {
+        return $this->render('WallabagImportBundle:Pocket:index.html.twig', [
+            'import' => $this->get('wallabag_import.pocket.import'),
+        ]);
+    }
+
+    /**
+     * @Route("/pocket/auth", name="import_pocket_auth")
+     */
+    public function authAction()
+    {
+        $requestToken = $this->get('wallabag_import.pocket.import')
+            ->getRequestToken($this->generateUrl('import', [], true));
+
+        $this->get('session')->set('import.pocket.code', $requestToken);
+
+        return $this->redirect(
+            'https://getpocket.com/auth/authorize?request_token='.$requestToken.'&redirect_uri='.$this->generateUrl('import_pocket_callback', [], true),
+            301
+        );
+    }
+
+    /**
+     * @Route("/pocket/callback", name="import_pocket_callback")
+     */
+    public function callbackAction()
+    {
+        $message = 'Import failed, please try again.';
+        $pocket = $this->get('wallabag_import.pocket.import');
+
+        // something bad happend on pocket side
+        if (false === $pocket->authorize($this->get('session')->get('import.pocket.code'))) {
+            $this->get('session')->getFlashBag()->add(
+                'notice',
+                $message
+            );
+
+            return $this->redirect($this->generateUrl('import_pocket'));
+        }
+
+        if (true === $pocket->import()) {
+            $summary = $pocket->getSummary();
+            $message = 'Import summary: '.$summary['imported'].' imported, '.$summary['skipped'].' already saved.';
+        }
+
+        $this->get('session')->getFlashBag()->add(
+            'notice',
+            $message
+        );
+
+        return $this->redirect($this->generateUrl('homepage'));
+    }
+}
diff --git a/src/Wallabag/ImportBundle/Controller/WallabagV1Controller.php b/src/Wallabag/ImportBundle/Controller/WallabagV1Controller.php
new file mode 100644 (file)
index 0000000..e50a6c3
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+
+namespace Wallabag\ImportBundle\Controller;
+
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
+use Symfony\Component\HttpFoundation\Request;
+use Wallabag\ImportBundle\Form\Type\UploadImportType;
+
+class WallabagV1Controller extends Controller
+{
+    /**
+     * @Route("/wallabag-v1", name="import_wallabag_v1")
+     */
+    public function indexAction(Request $request)
+    {
+        $form = $this->createForm(new UploadImportType());
+        $form->handleRequest($request);
+
+        $wallabag = $this->get('wallabag_import.wallabag_v1.import');
+
+        if ($form->isValid()) {
+            $file = $form->get('file')->getData();
+            $name = $this->getUser()->getId().'.json';
+
+            if (in_array($file->getClientMimeType(), $this->getParameter('wallabag_import.allow_mimetypes')) && $file->move($this->getParameter('wallabag_import.resource_dir'), $name)) {
+                $res = $wallabag
+                    ->setUser($this->getUser())
+                    ->setFilepath($this->getParameter('wallabag_import.resource_dir').'/'.$name)
+                    ->import();
+
+                $message = 'Import failed, please try again.';
+                if (true === $res) {
+                    $summary = $wallabag->getSummary();
+                    $message = 'Import summary: '.$summary['imported'].' imported, '.$summary['skipped'].' already saved.';
+
+                    unlink($this->getParameter('wallabag_import.resource_dir').'/'.$name);
+                }
+
+                $this->get('session')->getFlashBag()->add(
+                    'notice',
+                    $message
+                );
+
+                return $this->redirect($this->generateUrl('homepage'));
+            } else {
+                $this->get('session')->getFlashBag()->add(
+                    'notice',
+                    'Error while processing import. Please verify your import file.'
+                );
+            }
+        }
+
+        return $this->render('WallabagImportBundle:WallabagV1:index.html.twig', [
+            'form' => $form->createView(),
+            'import' => $wallabag,
+        ]);
+    }
+}
diff --git a/src/Wallabag/ImportBundle/DependencyInjection/Configuration.php b/src/Wallabag/ImportBundle/DependencyInjection/Configuration.php
new file mode 100644 (file)
index 0000000..39df9d3
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+
+namespace Wallabag\ImportBundle\DependencyInjection;
+
+use Symfony\Component\Config\Definition\Builder\TreeBuilder;
+use Symfony\Component\Config\Definition\ConfigurationInterface;
+
+class Configuration implements ConfigurationInterface
+{
+    public function getConfigTreeBuilder()
+    {
+        $treeBuilder = new TreeBuilder();
+        $rootNode = $treeBuilder->root('wallabag_import');
+
+        $rootNode
+            ->children()
+                ->arrayNode('allow_mimetypes')
+                    ->prototype('scalar')->end()
+                ->end()
+                ->scalarNode('resource_dir')
+                ->end()
+            ->end()
+        ;
+
+        return $treeBuilder;
+    }
+}
diff --git a/src/Wallabag/ImportBundle/DependencyInjection/WallabagImportExtension.php b/src/Wallabag/ImportBundle/DependencyInjection/WallabagImportExtension.php
new file mode 100644 (file)
index 0000000..3f23c36
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+
+namespace Wallabag\ImportBundle\DependencyInjection;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\Config\FileLocator;
+use Symfony\Component\HttpKernel\DependencyInjection\Extension;
+use Symfony\Component\DependencyInjection\Loader;
+
+class WallabagImportExtension extends Extension
+{
+    public function load(array $configs, ContainerBuilder $container)
+    {
+        $configuration = new Configuration();
+        $config = $this->processConfiguration($configuration, $configs);
+        $container->setParameter('wallabag_import.allow_mimetypes', $config['allow_mimetypes']);
+        $container->setParameter('wallabag_import.resource_dir', $config['resource_dir']);
+
+        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
+        $loader->load('services.yml');
+    }
+
+    public function getAlias()
+    {
+        return 'wallabag_import';
+    }
+}
diff --git a/src/Wallabag/ImportBundle/Form/Type/UploadImportType.php b/src/Wallabag/ImportBundle/Form/Type/UploadImportType.php
new file mode 100644 (file)
index 0000000..415890f
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+
+namespace Wallabag\ImportBundle\Form\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+
+class UploadImportType extends AbstractType
+{
+    public function buildForm(FormBuilderInterface $builder, array $options)
+    {
+        $builder
+            ->add('file', 'file')
+            ->add('save', 'submit')
+        ;
+    }
+
+    public function getName()
+    {
+        return 'upload_import_file';
+    }
+}
diff --git a/src/Wallabag/ImportBundle/Import/ImportChain.php b/src/Wallabag/ImportBundle/Import/ImportChain.php
new file mode 100644 (file)
index 0000000..9dd7795
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+namespace Wallabag\ImportBundle\Import;
+
+class ImportChain
+{
+    private $imports;
+
+    public function __construct()
+    {
+        $this->imports = [];
+    }
+
+    /**
+     * Add an import to the chain.
+     *
+     * @param ImportInterface $import
+     * @param string          $alias
+     */
+    public function addImport(ImportInterface $import, $alias)
+    {
+        $this->imports[$alias] = $import;
+    }
+
+    /**
+     * Get all imports.
+     *
+     * @return array<ImportInterface>
+     */
+    public function getAll()
+    {
+        return $this->imports;
+    }
+}
diff --git a/src/Wallabag/ImportBundle/Import/ImportCompilerPass.php b/src/Wallabag/ImportBundle/Import/ImportCompilerPass.php
new file mode 100644 (file)
index 0000000..a363a56
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+namespace Wallabag\ImportBundle\Import;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\Reference;
+
+class ImportCompilerPass implements CompilerPassInterface
+{
+    public function process(ContainerBuilder $container)
+    {
+        if (!$container->hasDefinition('wallabag_import.chain')) {
+            return;
+        }
+
+        $definition = $container->getDefinition(
+            'wallabag_import.chain'
+        );
+
+        $taggedServices = $container->findTaggedServiceIds(
+            'wallabag_import.import'
+        );
+        foreach ($taggedServices as $id => $tagAttributes) {
+            foreach ($tagAttributes as $attributes) {
+                $definition->addMethodCall(
+                    'addImport',
+                    [new Reference($id), $attributes['alias']]
+                );
+            }
+        }
+    }
+}
diff --git a/src/Wallabag/ImportBundle/Import/ImportInterface.php b/src/Wallabag/ImportBundle/Import/ImportInterface.php
new file mode 100644 (file)
index 0000000..25dc0d8
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+namespace Wallabag\ImportBundle\Import;
+
+use Psr\Log\LoggerAwareInterface;
+
+interface ImportInterface extends LoggerAwareInterface
+{
+    /**
+     * Name of the import.
+     *
+     * @return string
+     */
+    public function getName();
+
+    /**
+     * Url to start the import.
+     *
+     * @return string
+     */
+    public function getUrl();
+
+    /**
+     * Description of the import.
+     *
+     * @return string
+     */
+    public function getDescription();
+
+    /**
+     * Import content using the user token.
+     *
+     * @return bool
+     */
+    public function import();
+
+    /**
+     * Return an array with summary info about the import, with keys:
+     *     - skipped
+     *     - imported.
+     *
+     * @return array
+     */
+    public function getSummary();
+}
diff --git a/src/Wallabag/ImportBundle/Import/PocketImport.php b/src/Wallabag/ImportBundle/Import/PocketImport.php
new file mode 100644 (file)
index 0000000..cdcec1e
--- /dev/null
@@ -0,0 +1,267 @@
+<?php
+
+namespace Wallabag\ImportBundle\Import;
+
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use Doctrine\ORM\EntityManager;
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\RequestException;
+use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
+use Wallabag\CoreBundle\Entity\Entry;
+use Wallabag\CoreBundle\Entity\Tag;
+use Wallabag\CoreBundle\Helper\ContentProxy;
+
+class PocketImport implements ImportInterface
+{
+    private $user;
+    private $em;
+    private $contentProxy;
+    private $logger;
+    private $client;
+    private $consumerKey;
+    private $skippedEntries = 0;
+    private $importedEntries = 0;
+    protected $accessToken;
+    private $translator;
+
+    public function __construct(TokenStorageInterface $tokenStorage, EntityManager $em, ContentProxy $contentProxy, $consumerKey)
+    {
+        $this->user = $tokenStorage->getToken()->getUser();
+        $this->em = $em;
+        $this->contentProxy = $contentProxy;
+        $this->consumerKey = $consumerKey;
+        $this->logger = new NullLogger();
+    }
+
+    public function setLogger(LoggerInterface $logger)
+    {
+        $this->logger = $logger;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getName()
+    {
+        return 'Pocket';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getUrl()
+    {
+        return 'import_pocket';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDescription()
+    {
+        return 'This importer will import all your Pocket data. Pocket doesn\'t allow us to retrieve content from their service, so the readable content of each article will be re-fetched by wallabag.';
+    }
+
+    /**
+     * Return the oauth url to authenticate the client.
+     *
+     * @param string $redirectUri Redirect url in case of error
+     *
+     * @return string request_token for callback method
+     */
+    public function getRequestToken($redirectUri)
+    {
+        $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/oauth/request',
+            [
+                'body' => json_encode([
+                    'consumer_key' => $this->consumerKey,
+                    'redirect_uri' => $redirectUri,
+                ]),
+            ]
+        );
+
+        try {
+            $response = $this->client->send($request);
+        } catch (RequestException $e) {
+            $this->logger->error(sprintf('PocketImport: Failed to request token: %s', $e->getMessage()), ['exception' => $e]);
+
+            return false;
+        }
+
+        return $response->json()['code'];
+    }
+
+    /**
+     * Usually called by the previous callback to authorize the client.
+     * Then it return a token that can be used for next requests.
+     *
+     * @param string $code request_token from getRequestToken
+     *
+     * @return bool
+     */
+    public function authorize($code)
+    {
+        $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/oauth/authorize',
+            [
+                'body' => json_encode([
+                    'consumer_key' => $this->consumerKey,
+                    'code' => $code,
+                ]),
+            ]
+        );
+
+        try {
+            $response = $this->client->send($request);
+        } catch (RequestException $e) {
+            $this->logger->error(sprintf('PocketImport: Failed to authorize client: %s', $e->getMessage()), ['exception' => $e]);
+
+            return false;
+        }
+
+        $this->accessToken = $response->json()['access_token'];
+
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function import()
+    {
+        $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/get',
+            [
+                'body' => json_encode([
+                    'consumer_key' => $this->consumerKey,
+                    'access_token' => $this->accessToken,
+                    'detailType' => 'complete',
+                    'state' => 'all',
+                    'sort' => 'oldest',
+                ]),
+            ]
+        );
+
+        try {
+            $response = $this->client->send($request);
+        } catch (RequestException $e) {
+            $this->logger->error(sprintf('PocketImport: Failed to import: %s', $e->getMessage()), ['exception' => $e]);
+
+            return false;
+        }
+
+        $entries = $response->json();
+
+        $this->parseEntries($entries['list']);
+
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSummary()
+    {
+        return [
+            'skipped' => $this->skippedEntries,
+            'imported' => $this->importedEntries,
+        ];
+    }
+
+    /**
+     * Set the Guzzle client.
+     *
+     * @param Client $client
+     */
+    public function setClient(Client $client)
+    {
+        $this->client = $client;
+    }
+
+    /**
+     * @todo move that in a more global place
+     */
+    private function assignTagsToEntry(Entry $entry, $tags)
+    {
+        foreach ($tags as $tag) {
+            $label = trim($tag['tag']);
+            $tagEntity = $this->em
+                ->getRepository('WallabagCoreBundle:Tag')
+                ->findOneByLabel($label);
+
+            if (is_object($tagEntity)) {
+                $entry->addTag($tagEntity);
+            } else {
+                $newTag = new Tag();
+                $newTag->setLabel($label);
+
+                $entry->addTag($newTag);
+            }
+            $this->em->flush();
+        }
+    }
+
+    /**
+     * @see https://getpocket.com/developer/docs/v3/retrieve
+     *
+     * @param $entries
+     */
+    private function parseEntries($entries)
+    {
+        $i = 1;
+
+        foreach ($entries as $pocketEntry) {
+            $url = isset($pocketEntry['resolved_url']) && $pocketEntry['resolved_url'] != '' ? $pocketEntry['resolved_url'] : $pocketEntry['given_url'];
+
+            $existingEntry = $this->em
+                ->getRepository('WallabagCoreBundle:Entry')
+                ->existByUrlAndUserId($url, $this->user->getId());
+
+            if (false !== $existingEntry) {
+                ++$this->skippedEntries;
+                continue;
+            }
+
+            $entry = new Entry($this->user);
+            $entry = $this->contentProxy->updateEntry($entry, $url);
+
+            // 0, 1, 2 - 1 if the item is archived - 2 if the item should be deleted
+            if ($pocketEntry['status'] == 1) {
+                $entry->setArchived(true);
+            }
+
+            // 0 or 1 - 1 If the item is favorited
+            if ($pocketEntry['favorite'] == 1) {
+                $entry->setStarred(true);
+            }
+
+            $title = 'Untitled';
+            if (isset($pocketEntry['resolved_title']) && $pocketEntry['resolved_title'] != '') {
+                $title = $pocketEntry['resolved_title'];
+            } elseif (isset($pocketEntry['given_title']) && $pocketEntry['given_title'] != '') {
+                $title = $pocketEntry['given_title'];
+            }
+
+            $entry->setTitle($title);
+
+            // 0, 1, or 2 - 1 if the item has images in it - 2 if the item is an image
+            if (isset($pocketEntry['has_image']) && $pocketEntry['has_image'] > 0 && isset($pocketEntry['images'][1])) {
+                $entry->setPreviewPicture($pocketEntry['images'][1]['src']);
+            }
+
+            if (isset($pocketEntry['tags']) && !empty($pocketEntry['tags'])) {
+                $this->assignTagsToEntry($entry, $pocketEntry['tags']);
+            }
+
+            $this->em->persist($entry);
+            ++$this->importedEntries;
+
+            // flush every 20 entries
+            if (($i % 20) === 0) {
+                $this->em->flush();
+            }
+            ++$i;
+        }
+
+        $this->em->flush();
+    }
+}
diff --git a/src/Wallabag/ImportBundle/Import/WallabagV1Import.php b/src/Wallabag/ImportBundle/Import/WallabagV1Import.php
new file mode 100644 (file)
index 0000000..393089d
--- /dev/null
@@ -0,0 +1,159 @@
+<?php
+
+namespace Wallabag\ImportBundle\Import;
+
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use Doctrine\ORM\EntityManager;
+use Wallabag\CoreBundle\Entity\Entry;
+use Wallabag\UserBundle\Entity\User;
+use Wallabag\CoreBundle\Tools\Utils;
+
+class WallabagV1Import implements ImportInterface
+{
+    private $user;
+    private $em;
+    private $logger;
+    private $skippedEntries = 0;
+    private $importedEntries = 0;
+    private $filepath;
+
+    public function __construct(EntityManager $em)
+    {
+        $this->em = $em;
+        $this->logger = new NullLogger();
+    }
+
+    public function setLogger(LoggerInterface $logger)
+    {
+        $this->logger = $logger;
+    }
+
+    /**
+     * We define the user in a custom call because on the import command there is no logged in user.
+     * So we can't retrieve user from the `security.token_storage` service.
+     *
+     * @param User $user
+     */
+    public function setUser(User $user)
+    {
+        $this->user = $user;
+
+        return $this;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getName()
+    {
+        return 'wallabag v1';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getUrl()
+    {
+        return 'import_wallabag_v1';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDescription()
+    {
+        return 'This importer will import all your wallabag v1 articles. On your config page, click on "JSON export" in the "Export your wallabag data" section. You will have a "wallabag-export-1-xxxx-xx-xx.json" file.';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function import()
+    {
+        if (!$this->user) {
+            $this->logger->error('WallabagV1Import: user is not defined');
+
+            return false;
+        }
+
+        if (!file_exists($this->filepath) || !is_readable($this->filepath)) {
+            $this->logger->error('WallabagV1Import: unable to read file', array('filepath' => $this->filepath));
+
+            return false;
+        }
+
+        $data = json_decode(file_get_contents($this->filepath), true);
+
+        if (empty($data)) {
+            return false;
+        }
+
+        $this->parseEntries($data);
+
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSummary()
+    {
+        return [
+            'skipped' => $this->skippedEntries,
+            'imported' => $this->importedEntries,
+        ];
+    }
+
+    /**
+     * Set file path to the json file.
+     *
+     * @param string $filepath
+     */
+    public function setFilepath($filepath)
+    {
+        $this->filepath = $filepath;
+
+        return $this;
+    }
+
+    /**
+     * @param $entries
+     */
+    private function parseEntries($entries)
+    {
+        $i = 1;
+
+        foreach ($entries as $importedEntry) {
+            $existingEntry = $this->em
+                ->getRepository('WallabagCoreBundle:Entry')
+                ->existByUrlAndUserId($importedEntry['url'], $this->user->getId());
+
+            if (false !== $existingEntry) {
+                ++$this->skippedEntries;
+                continue;
+            }
+
+            // @see ContentProxy->updateEntry
+            $entry = new Entry($this->user);
+            $entry->setUrl($importedEntry['url']);
+            $entry->setTitle($importedEntry['title']);
+            $entry->setArchived($importedEntry['is_read']);
+            $entry->setStarred($importedEntry['is_fav']);
+            $entry->setContent($importedEntry['content']);
+            $entry->setReadingTime(Utils::getReadingTime($importedEntry['content']));
+            $entry->setDomainName(parse_url($importedEntry['url'], PHP_URL_HOST));
+
+            $this->em->persist($entry);
+            ++$this->importedEntries;
+
+            // flush every 20 entries
+            if (($i % 20) === 0) {
+                $this->em->flush();
+            }
+            ++$i;
+        }
+
+        $this->em->flush();
+    }
+}
diff --git a/src/Wallabag/ImportBundle/Resources/config/services.yml b/src/Wallabag/ImportBundle/Resources/config/services.yml
new file mode 100644 (file)
index 0000000..e4dde10
--- /dev/null
@@ -0,0 +1,34 @@
+services:
+    wallabag_import.chain:
+        class: Wallabag\ImportBundle\Import\ImportChain
+
+    wallabag_import.pocket.client:
+        class: GuzzleHttp\Client
+        arguments:
+            -
+                defaults:
+                    headers:
+                        content-type: "application/json"
+                        X-Accept: "application/json"
+
+    wallabag_import.pocket.import:
+        class: Wallabag\ImportBundle\Import\PocketImport
+        arguments:
+            - "@security.token_storage"
+            - "@doctrine.orm.entity_manager"
+            - "@wallabag_core.content_proxy"
+            - %pocket_consumer_key%
+        calls:
+            - [ setClient, [ "@wallabag_import.pocket.client" ] ]
+            - [ setLogger, [ "@logger" ]]
+        tags:
+            -  { name: wallabag_import.import, alias: pocket }
+
+    wallabag_import.wallabag_v1.import:
+        class: Wallabag\ImportBundle\Import\WallabagV1Import
+        arguments:
+            - "@doctrine.orm.entity_manager"
+        calls:
+            - [ setLogger, [ "@logger" ]]
+        tags:
+            -  { name: wallabag_import.import, alias: wallabag_v1 }
diff --git a/src/Wallabag/ImportBundle/Resources/views/Import/index.html.twig b/src/Wallabag/ImportBundle/Resources/views/Import/index.html.twig
new file mode 100644 (file)
index 0000000..303e6cb
--- /dev/null
@@ -0,0 +1,21 @@
+{% extends "WallabagCoreBundle::layout.html.twig" %}
+{% block title %}{% trans %}Import{% endtrans %}{% endblock %}
+
+{% block content %}
+<div class="row">
+    <div class="col s12">
+        <div class="card-panel settings">
+            {% trans %}Welcome on wallabag importer. Please select your previous service that you want to migrate.{% endtrans %}
+            <ul>
+                {% for import in imports %}
+                    <li>
+                        <h5>{{ import.name }}</h5>
+                        <blockquote>{{ import.description|trans }}</blockquote>
+                        <p><a class="waves-effect waves-light btn" href="{{ path(import.url) }}">{% trans %}Import contents{% endtrans %}</a></p>
+                    </li>
+                {% endfor %}
+            </ul>
+        </div>
+    </div>
+</div>
+{% endblock %}
diff --git a/src/Wallabag/ImportBundle/Resources/views/Pocket/index.html.twig b/src/Wallabag/ImportBundle/Resources/views/Pocket/index.html.twig
new file mode 100644 (file)
index 0000000..643ad77
--- /dev/null
@@ -0,0 +1,18 @@
+{% extends "WallabagCoreBundle::layout.html.twig" %}
+{% block title %}{% trans %}Import > Pocket{% endtrans %}{% endblock %}
+
+{% block content %}
+<div class="row">
+    <div class="col s12">
+        <div class="card-panel settings">
+            <blockquote>{{ import.description|trans }}</blockquote>
+            <p>{% trans %}You can import your data from your Pocket account. You just have to click on the below button and authorize the application to connect to getpocket.com.{% endtrans %}</p>
+            <form method="post" action="{{ path('import_pocket_auth') }}">
+                <button class="btn waves-effect waves-light" type="submit" name="action">
+                    {% trans %}Connect to Pocket and import data{% endtrans %}
+                </button>
+            </form>
+        </div>
+    </div>
+</div>
+{% endblock %}
diff --git a/src/Wallabag/ImportBundle/Resources/views/WallabagV1/index.html.twig b/src/Wallabag/ImportBundle/Resources/views/WallabagV1/index.html.twig
new file mode 100644 (file)
index 0000000..1359f2e
--- /dev/null
@@ -0,0 +1,36 @@
+{% extends "WallabagCoreBundle::layout.html.twig" %}
+{% block title %}{% trans %}Import > Wallabag v1{% endtrans %}{% endblock %}
+
+{% block content %}
+<div class="row">
+    <div class="col s12">
+        <div class="card-panel settings">
+            <div class="row">
+                <blockquote>{{ import.description|trans }}</blockquote>
+                <p>{% trans %}Please select your wallabag export and click on the below button to upload and import it.{% endtrans %}</p>
+                <div class="col s12">
+                    {{ form_start(form, {'method': 'POST'}) }}
+                        {{ form_errors(form) }}
+                        <div class="row">
+                            <div class="file-field input-field col s12">
+                                {{ form_errors(form.file) }}
+                                <div class="btn">
+                                    <span>{% trans %}File{% endtrans %}</span>
+                                    {{ form_widget(form.file) }}
+                                </div>
+                                <div class="file-path-wrapper">
+                                    <input class="file-path validate" type="text">
+                                </div>
+                            </div>
+                        </div>
+                        <div class="hidden">{{ form_rest(form) }}</div>
+                        <button class="btn waves-effect waves-light" type="submit" name="action">
+                            {% trans %}Upload file{% endtrans %}
+                        </button>
+                    </form>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %}
diff --git a/src/Wallabag/ImportBundle/Tests/Controller/ImportControllerTest.php b/src/Wallabag/ImportBundle/Tests/Controller/ImportControllerTest.php
new file mode 100644 (file)
index 0000000..30009af
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+
+namespace Wallabag\ImportBundle\Tests\Controller;
+
+use Wallabag\CoreBundle\Tests\WallabagCoreTestCase;
+
+class ImportControllerTest extends WallabagCoreTestCase
+{
+    public function testLogin()
+    {
+        $client = $this->getClient();
+
+        $client->request('GET', '/import/');
+
+        $this->assertEquals(302, $client->getResponse()->getStatusCode());
+        $this->assertContains('login', $client->getResponse()->headers->get('location'));
+    }
+
+    public function testImportList()
+    {
+        $this->logInAs('admin');
+        $client = $this->getClient();
+
+        $crawler = $client->request('GET', '/import/');
+
+        $this->assertEquals(200, $client->getResponse()->getStatusCode());
+        $this->assertEquals(2, $crawler->filter('blockquote')->count());
+    }
+}
diff --git a/src/Wallabag/ImportBundle/Tests/Controller/PocketControllerTest.php b/src/Wallabag/ImportBundle/Tests/Controller/PocketControllerTest.php
new file mode 100644 (file)
index 0000000..c2acd68
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+
+namespace Wallabag\ImportBundle\Tests\Controller;
+
+use Wallabag\CoreBundle\Tests\WallabagCoreTestCase;
+
+class PocketControllerTest extends WallabagCoreTestCase
+{
+    public function testImportPocket()
+    {
+        $this->logInAs('admin');
+        $client = $this->getClient();
+
+        $crawler = $client->request('GET', '/import/pocket');
+
+        $this->assertEquals(200, $client->getResponse()->getStatusCode());
+        $this->assertEquals(1, $crawler->filter('button[type=submit]')->count());
+    }
+
+    public function testImportPocketAuth()
+    {
+        $this->logInAs('admin');
+        $client = $this->getClient();
+
+        $crawler = $client->request('GET', '/import/pocket/auth');
+
+        $this->assertEquals(301, $client->getResponse()->getStatusCode());
+        $this->assertContains('getpocket.com/auth/authorize', $client->getResponse()->headers->get('location'));
+    }
+
+    public function testImportPocketCallbackWithBadToken()
+    {
+        $this->logInAs('admin');
+        $client = $this->getClient();
+
+        $crawler = $client->request('GET', '/import/pocket/callback');
+
+        $this->assertEquals(302, $client->getResponse()->getStatusCode());
+        $this->assertContains('import/pocket', $client->getResponse()->headers->get('location'));
+        $this->assertEquals('Import failed, please try again.', $client->getContainer()->get('session')->getFlashBag()->peek('notice')[0]);
+    }
+}
diff --git a/src/Wallabag/ImportBundle/Tests/Controller/WallabagV1ControllerTest.php b/src/Wallabag/ImportBundle/Tests/Controller/WallabagV1ControllerTest.php
new file mode 100644 (file)
index 0000000..e12ea42
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+namespace Wallabag\ImportBundle\Tests\Controller;
+
+use Wallabag\CoreBundle\Tests\WallabagCoreTestCase;
+use Symfony\Component\HttpFoundation\File\UploadedFile;
+
+class WallabagV1ControllerTest extends WallabagCoreTestCase
+{
+    public function testImportWallabag()
+    {
+        $this->logInAs('admin');
+        $client = $this->getClient();
+
+        $crawler = $client->request('GET', '/import/wallabag-v1');
+
+        $this->assertEquals(200, $client->getResponse()->getStatusCode());
+        $this->assertEquals(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count());
+        $this->assertEquals(1, $crawler->filter('input[type=file]')->count());
+    }
+
+    public function testImportWallabagWithFile()
+    {
+        $this->logInAs('admin');
+        $client = $this->getClient();
+
+        $crawler = $client->request('GET', '/import/wallabag-v1');
+        $form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
+
+        $file = new UploadedFile(__DIR__.'/../fixtures/wallabag-v1.json', 'wallabag-v1.json');
+
+        $data = array(
+            'upload_import_file[file]' => $file,
+        );
+
+        $client->submit($form, $data);
+
+        $this->assertEquals(302, $client->getResponse()->getStatusCode());
+
+        $crawler = $client->followRedirect();
+
+        $this->assertGreaterThan(1, $alert = $crawler->filter('div.messages.success')->extract(array('_text')));
+        $this->assertContains('Import summary', $alert[0]);
+    }
+
+    public function testImportWallabagWithEmptyFile()
+    {
+        $this->logInAs('admin');
+        $client = $this->getClient();
+
+        $crawler = $client->request('GET', '/import/wallabag-v1');
+        $form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
+
+        $file = new UploadedFile(__DIR__.'/../fixtures/test.txt', 'test.txt');
+
+        $data = array(
+            'upload_import_file[file]' => $file,
+        );
+
+        $client->submit($form, $data);
+
+        $this->assertEquals(302, $client->getResponse()->getStatusCode());
+
+        $crawler = $client->followRedirect();
+
+        $this->assertGreaterThan(1, $alert = $crawler->filter('div.messages.success')->extract(array('_text')));
+        $this->assertContains('Import failed, please try again', $alert[0]);
+    }
+}
diff --git a/src/Wallabag/ImportBundle/Tests/Import/ImportChainTest.php b/src/Wallabag/ImportBundle/Tests/Import/ImportChainTest.php
new file mode 100644 (file)
index 0000000..702d2a9
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+namespace Wallabag\ImportBundle\Tests\Import;
+
+use Wallabag\ImportBundle\Import\ImportChain;
+
+class ImportChainTest extends \PHPUnit_Framework_TestCase
+{
+    public function testGetAll()
+    {
+        $import = $this->getMockBuilder('Wallabag\ImportBundle\Import\ImportInterface')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $importChain = new ImportChain();
+        $importChain->addImport($import, 'alias');
+
+        $this->assertCount(1, $importChain->getAll());
+        $this->assertEquals($import, $importChain->getAll()['alias']);
+    }
+}
diff --git a/src/Wallabag/ImportBundle/Tests/Import/ImportCompilerPassTest.php b/src/Wallabag/ImportBundle/Tests/Import/ImportCompilerPassTest.php
new file mode 100644 (file)
index 0000000..bd62ab3
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+
+namespace Wallabag\ImportBundle\Tests\Import;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Wallabag\ImportBundle\Import\ImportCompilerPass;
+
+class ImportCompilerPassTest extends \PHPUnit_Framework_TestCase
+{
+    public function testProcessNoDefinition()
+    {
+        $container = new ContainerBuilder();
+        $res = $this->process($container);
+
+        $this->assertNull($res);
+    }
+
+    public function testProcess()
+    {
+        $container = new ContainerBuilder();
+        $container
+            ->register('wallabag_import.chain')
+            ->setPublic(false)
+        ;
+
+        $container
+            ->register('foo')
+            ->addTag('wallabag_import.import', array('alias' => 'pocket'))
+        ;
+
+        $this->process($container);
+
+        $this->assertTrue($container->hasDefinition('wallabag_import.chain'));
+
+        $definition = $container->getDefinition('wallabag_import.chain');
+        $this->assertTrue($definition->hasMethodCall('addImport'));
+
+        $calls = $definition->getMethodCalls();
+        $this->assertEquals('pocket', $calls[0][1][1]);
+    }
+
+    protected function process(ContainerBuilder $container)
+    {
+        $repeatedPass = new ImportCompilerPass();
+        $repeatedPass->process($container);
+    }
+}
diff --git a/src/Wallabag/ImportBundle/Tests/Import/PocketImportTest.php b/src/Wallabag/ImportBundle/Tests/Import/PocketImportTest.php
new file mode 100644 (file)
index 0000000..043b211
--- /dev/null
@@ -0,0 +1,314 @@
+<?php
+
+namespace Wallabag\ImportBundle\Tests\Import;
+
+use Wallabag\UserBundle\Entity\User;
+use Wallabag\ImportBundle\Import\PocketImport;
+use GuzzleHttp\Client;
+use GuzzleHttp\Subscriber\Mock;
+use GuzzleHttp\Message\Response;
+use GuzzleHttp\Stream\Stream;
+use Monolog\Logger;
+use Monolog\Handler\TestHandler;
+
+class PocketImportMock extends PocketImport
+{
+    public function getAccessToken()
+    {
+        return $this->accessToken;
+    }
+}
+
+class PocketImportTest extends \PHPUnit_Framework_TestCase
+{
+    protected $token;
+    protected $user;
+    protected $em;
+    protected $contentProxy;
+    protected $logHandler;
+
+    private function getPocketImport($consumerKey = 'ConsumerKey')
+    {
+        $this->user = new User();
+
+        $this->tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $this->contentProxy = $this->getMockBuilder('Wallabag\CoreBundle\Helper\ContentProxy')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $token->expects($this->once())
+            ->method('getUser')
+            ->willReturn($this->user);
+
+        $this->tokenStorage->expects($this->once())
+            ->method('getToken')
+            ->willReturn($token);
+
+        $this->em = $this->getMockBuilder('Doctrine\ORM\EntityManager')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $pocket = new PocketImportMock(
+            $this->tokenStorage,
+            $this->em,
+            $this->contentProxy,
+            $consumerKey
+        );
+
+        $this->logHandler = new TestHandler();
+        $logger = new Logger('test', array($this->logHandler));
+        $pocket->setLogger($logger);
+
+        return $pocket;
+    }
+
+    public function testInit()
+    {
+        $pocketImport = $this->getPocketImport();
+
+        $this->assertEquals('Pocket', $pocketImport->getName());
+        $this->assertNotEmpty($pocketImport->getUrl());
+        $this->assertContains('This importer will import all your Pocket data.', $pocketImport->getDescription());
+    }
+
+    public function testOAuthRequest()
+    {
+        $client = new Client();
+
+        $mock = new Mock([
+            new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['code' => 'wunderbar_code']))),
+        ]);
+
+        $client->getEmitter()->attach($mock);
+
+        $pocketImport = $this->getPocketImport();
+        $pocketImport->setClient($client);
+
+        $code = $pocketImport->getRequestToken('http://0.0.0.0/redirect');
+
+        $this->assertEquals('wunderbar_code', $code);
+    }
+
+    public function testOAuthRequestBadResponse()
+    {
+        $client = new Client();
+
+        $mock = new Mock([
+            new Response(403),
+        ]);
+
+        $client->getEmitter()->attach($mock);
+
+        $pocketImport = $this->getPocketImport();
+        $pocketImport->setClient($client);
+
+        $code = $pocketImport->getRequestToken('http://0.0.0.0/redirect');
+
+        $this->assertFalse($code);
+
+        $records = $this->logHandler->getRecords();
+        $this->assertContains('PocketImport: Failed to request token', $records[0]['message']);
+        $this->assertEquals('ERROR', $records[0]['level_name']);
+    }
+
+    public function testOAuthAuthorize()
+    {
+        $client = new Client();
+
+        $mock = new Mock([
+            new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))),
+        ]);
+
+        $client->getEmitter()->attach($mock);
+
+        $pocketImport = $this->getPocketImport();
+        $pocketImport->setClient($client);
+
+        $res = $pocketImport->authorize('wunderbar_code');
+
+        $this->assertTrue($res);
+        $this->assertEquals('wunderbar_token', $pocketImport->getAccessToken());
+    }
+
+    public function testOAuthAuthorizeBadResponse()
+    {
+        $client = new Client();
+
+        $mock = new Mock([
+            new Response(403),
+        ]);
+
+        $client->getEmitter()->attach($mock);
+
+        $pocketImport = $this->getPocketImport();
+        $pocketImport->setClient($client);
+
+        $res = $pocketImport->authorize('wunderbar_code');
+
+        $this->assertFalse($res);
+
+        $records = $this->logHandler->getRecords();
+        $this->assertContains('PocketImport: Failed to authorize client', $records[0]['message']);
+        $this->assertEquals('ERROR', $records[0]['level_name']);
+    }
+
+    /**
+     * Will sample results from https://getpocket.com/developer/docs/v3/retrieve.
+     */
+    public function testImport()
+    {
+        $client = new Client();
+
+        $mock = new Mock([
+            new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))),
+            new Response(200, ['Content-Type' => 'application/json'], Stream::factory('
+                {
+                    "status": 1,
+                    "list": {
+                        "229279689": {
+                            "item_id": "229279689",
+                            "resolved_id": "229279689",
+                            "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
+                            "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland",
+                            "favorite": "1",
+                            "status": "1",
+                            "resolved_title": "The Massive Ryder Cup Preview",
+                            "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
+                            "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.",
+                            "is_article": "1",
+                            "has_video": "1",
+                            "has_image": "1",
+                            "word_count": "3197",
+                            "images": {
+                                "1": {
+                                    "item_id": "229279689",
+                                    "image_id": "1",
+                                    "src": "http://a.espncdn.com/combiner/i?img=/photo/2012/0927/grant_g_ryder_cr_640.jpg&w=640&h=360",
+                                    "width": "0",
+                                    "height": "0",
+                                    "credit": "Jamie Squire/Getty Images",
+                                    "caption": ""
+                                }
+                            },
+                            "videos": {
+                                "1": {
+                                    "item_id": "229279689",
+                                    "video_id": "1",
+                                    "src": "http://www.youtube.com/v/Er34PbFkVGk?version=3&hl=en_US&rel=0",
+                                    "width": "420",
+                                    "height": "315",
+                                    "type": "1",
+                                    "vid": "Er34PbFkVGk"
+                                }
+                            },
+                            "tags": {
+                                "grantland": {
+                                    "item_id": "1147652870",
+                                    "tag": "grantland"
+                                },
+                                "Ryder Cup": {
+                                    "item_id": "1147652870",
+                                    "tag": "Ryder Cup"
+                                }
+                            }
+                        },
+                        "229279690": {
+                            "item_id": "229279689",
+                            "resolved_id": "229279689",
+                            "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
+                            "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland",
+                            "favorite": "1",
+                            "status": "1",
+                            "resolved_title": "The Massive Ryder Cup Preview",
+                            "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
+                            "excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.",
+                            "is_article": "1",
+                            "has_video": "0",
+                            "has_image": "0",
+                            "word_count": "3197"
+                        }
+                    }
+                }
+            ')),
+        ]);
+
+        $client->getEmitter()->attach($mock);
+
+        $pocketImport = $this->getPocketImport();
+
+        $entryRepo = $this->getMockBuilder('Wallabag\CoreBundle\Repository\EntryRepository')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $entryRepo->expects($this->exactly(2))
+            ->method('existByUrlAndUserId')
+            ->will($this->onConsecutiveCalls(false, true));
+
+        $tag = $this->getMockBuilder('Wallabag\CoreBundle\Entity\Tag')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $tagRepo = $this->getMockBuilder('Wallabag\CoreBundle\Repository\TagRepository')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $tagRepo->expects($this->exactly(2))
+            // the method `findOneByLabel` doesn't exist, EntityRepository will then call `_call` method
+            // to magically call the `findOneBy` with ['label' => 'foo']
+            ->method('__call')
+            ->will($this->onConsecutiveCalls(false, $tag));
+
+        $this->em
+            ->expects($this->any())
+            ->method('getRepository')
+            ->will($this->onConsecutiveCalls($entryRepo, $tagRepo, $tagRepo, $entryRepo));
+
+        $entry = $this->getMockBuilder('Wallabag\CoreBundle\Entity\Entry')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $this->contentProxy
+            ->expects($this->once())
+            ->method('updateEntry')
+            ->willReturn($entry);
+
+        $pocketImport->setClient($client);
+        $pocketImport->authorize('wunderbar_code');
+
+        $res = $pocketImport->import();
+
+        $this->assertTrue($res);
+        $this->assertEquals(['skipped' => 1, 'imported' => 1], $pocketImport->getSummary());
+    }
+
+    public function testImportBadResponse()
+    {
+        $client = new Client();
+
+        $mock = new Mock([
+            new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))),
+            new Response(403),
+        ]);
+
+        $client->getEmitter()->attach($mock);
+
+        $pocketImport = $this->getPocketImport();
+        $pocketImport->setClient($client);
+        $pocketImport->authorize('wunderbar_code');
+
+        $res = $pocketImport->import();
+
+        $this->assertFalse($res);
+
+        $records = $this->logHandler->getRecords();
+        $this->assertContains('PocketImport: Failed to import', $records[0]['message']);
+        $this->assertEquals('ERROR', $records[0]['level_name']);
+    }
+}
diff --git a/src/Wallabag/ImportBundle/Tests/Import/WallabagV1ImportTest.php b/src/Wallabag/ImportBundle/Tests/Import/WallabagV1ImportTest.php
new file mode 100644 (file)
index 0000000..d5b4177
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+
+namespace Wallabag\ImportBundle\Tests\Import;
+
+use Wallabag\UserBundle\Entity\User;
+use Wallabag\ImportBundle\Import\WallabagV1Import;
+use Monolog\Logger;
+use Monolog\Handler\TestHandler;
+
+class WallabagV1ImportTest extends \PHPUnit_Framework_TestCase
+{
+    protected $user;
+    protected $em;
+    protected $logHandler;
+
+    private function getWallabagV1Import($unsetUser = false)
+    {
+        $this->user = new User();
+
+        $this->em = $this->getMockBuilder('Doctrine\ORM\EntityManager')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $pocket = new WallabagV1Import($this->em);
+
+        $this->logHandler = new TestHandler();
+        $logger = new Logger('test', array($this->logHandler));
+        $pocket->setLogger($logger);
+
+        if (false === $unsetUser) {
+            $pocket->setUser($this->user);
+        }
+
+        return $pocket;
+    }
+
+    public function testInit()
+    {
+        $wallabagV1Import = $this->getWallabagV1Import();
+
+        $this->assertEquals('wallabag v1', $wallabagV1Import->getName());
+        $this->assertNotEmpty($wallabagV1Import->getUrl());
+        $this->assertContains('This importer will import all your wallabag v1 articles.', $wallabagV1Import->getDescription());
+    }
+
+    public function testImport()
+    {
+        $wallabagV1Import = $this->getWallabagV1Import();
+        $wallabagV1Import->setFilepath(__DIR__.'/../fixtures/wallabag-v1.json');
+
+        $entryRepo = $this->getMockBuilder('Wallabag\CoreBundle\Repository\EntryRepository')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $entryRepo->expects($this->exactly(3))
+            ->method('existByUrlAndUserId')
+            ->will($this->onConsecutiveCalls(false, true, false));
+
+        $this->em
+            ->expects($this->any())
+            ->method('getRepository')
+            ->willReturn($entryRepo);
+
+        $res = $wallabagV1Import->import();
+
+        $this->assertTrue($res);
+        $this->assertEquals(['skipped' => 1, 'imported' => 2], $wallabagV1Import->getSummary());
+    }
+
+    public function testImportBadFile()
+    {
+        $wallabagV1Import = $this->getWallabagV1Import();
+        $wallabagV1Import->setFilepath(__DIR__.'/../fixtures/wallabag-v1.jsonx');
+
+        $res = $wallabagV1Import->import();
+
+        $this->assertFalse($res);
+
+        $records = $this->logHandler->getRecords();
+        $this->assertContains('WallabagV1Import: unable to read file', $records[0]['message']);
+        $this->assertEquals('ERROR', $records[0]['level_name']);
+    }
+
+    public function testImportUserNotDefined()
+    {
+        $wallabagV1Import = $this->getWallabagV1Import(true);
+        $wallabagV1Import->setFilepath(__DIR__.'/../fixtures/wallabag-v1.json');
+
+        $res = $wallabagV1Import->import();
+
+        $this->assertFalse($res);
+
+        $records = $this->logHandler->getRecords();
+        $this->assertContains('WallabagV1Import: user is not defined', $records[0]['message']);
+        $this->assertEquals('ERROR', $records[0]['level_name']);
+    }
+}
diff --git a/src/Wallabag/ImportBundle/Tests/fixtures/test.html b/src/Wallabag/ImportBundle/Tests/fixtures/test.html
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/Wallabag/ImportBundle/Tests/fixtures/test.txt b/src/Wallabag/ImportBundle/Tests/fixtures/test.txt
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/Wallabag/ImportBundle/Tests/fixtures/wallabag-v1.json b/src/Wallabag/ImportBundle/Tests/fixtures/wallabag-v1.json
new file mode 100644 (file)
index 0000000..534343f
--- /dev/null
@@ -0,0 +1,50 @@
+[
+    {
+        "0": "1",
+        "1": "Framabag, un nouveau service libre et gratuit",
+        "2": "http://www.framablog.org/index.php/post/2014/02/05/Framabag-service-libre-gratuit-interview-developpeur",
+        "3": "0",
+        "4": "0",
+        "5": "\n<h2>Une interview de Nicolas, son développeur.</h2>\n<p><em>Il ne vous a sûrement pas échappé que notre consommation de contenus du Web est terriblement chronophage et particulièrement frustrante tout à la fois : non seulement nous passons beaucoup (trop ?) de temps en ligne à explorer les mines aurifères de la toile, y détectant pépites et filons, mais nous sommes surtout constamment en manque. Même si nous ne sommes pas dans le zapping frénétique si facilement dénoncé par les doctes psychologues qui pontifient sur les dangers du numérique pour les jeunes cervelles, il nous vient souvent le goût amer de l’inachevé : pas le temps de tout lire (<a href=\"http://fr.wiktionary.org/wiki/TLDR\">TL;DR</a> est devenu le clin d’œil mi-figue mi-raisin d’une génération de lecteurs pressés), pas trop le temps de réfléchir non plus hélas, pas le temps de suivre la ribambelle de liens associés à un article…<br /></em></p>\n<p><em>Pour nous donner bonne conscience, nous rangeons scrupuleusement un marque-page de plus dans un sous-dossier qui en comporte déjà 256, nous notons un élément de plus dans la toujours ridiculement longue toudouliste, bref nous remettons à plus tard, c’est-à-dire le plus souvent aux introuvables calendes grecques, le soin de lire vraiment un article jugé intéressant, de regarder une vidéo signalée par les rézossocios, de lire un chapitre entier d’un ouvrage disponible en ligne…<br /></em></p>\n<p><em>Alors bien sûr, à défaut de nous donner tout le temps qui serait nécessaire, des solutions existent pour nous permettre de « lire plus tard » en sauvegardant le précieux pollen de nos butinages de site en site, et d’en faire ultérieurement votre miel ; c’est bel et bon mais les ruches sont un peu distantes, ça s’appelle le cloud (nos amis techies m’ont bien expliqué mais j’ai seulement compris que des trucs à moi sont sur des machines lointaines, ça ne me rassure pas trop) et elles sont souvent propriétaires, ne laissant entrer que les <s>utilisateurs</s> consommateurs payants et qui consentent à leurs conditions. Sans compter que de gros bourdons viennent profiter plus ou moins discrètement de toutes ces traces de nous-mêmes qui permettent de monétiser notre profil : si je collecte sur ces services (ne les nommons pas, justement) une série d’articles sur l’idée de Nature chez Diderot, je recevrai diverses sollicitations pour devenir client de la boutique Nature &amp; Découverte du boulevard Diderot. Et si d’aventure les programmes de la NSA moulinent sur le service, je serai peut-être un jour dans une liste des militants naturistes indésirables sur les vols de la PanAm (je ne sais plus trop si je plaisante là, finalement…)<br /></em></p>\n<p><em>La bonne idée : « se constituer un réservoir de documents sélectionnés à parcourir plus tard » appelait donc une autre bonne idée, celle d’avoir le contrôle de ce réservoir, de notre collection personnelle. C’est Nicolas Lœuillet, ci-dessous interviewé, qui s’y est collé avec une belle application appelée euh… oui, appelée Wallabag.<br /></em></p>\n<p><em>Framasoft soutient d’autant plus son initiative qu’<a href=\"http://www.framablog.org/index.php/post/2014/01/31/Geektionnerd-Wallabag\">on lui a cherché des misères pour une histoire de nom</a> et qu’il est possible d’installer soi-même une copie de Wallabag sur son propre site.<br /></em></p>\n<p><em>Le petit plus de Framasoft, réseau toujours désireux de vous proposer des alternatives libératrices, c’est de vous proposer (sur inscription préalable) un accès au Framabag, autrement dit votre Wallabag sur un serveur Frama* avec notre garantie de confidentialité. Comme pour le Framanews, nous vous accueillons volontiers dans la limite de nos capacités, en vous invitant à vous lancer dans votre auto-hébergement de Wallabag.<br />Cet article est trop long ? Mettez-le dans <a href=\"http://www.framabag.org/index.php\">votre Framabag</a> et hop.<br /></em></p>\n<p><em>Framablog : Salut Nicolas… Tu peux te présenter brièvement ?<br /></em></p>\n<p>Salut ! Développeur PHP depuis quelques années maintenant (10 ans), j’ai voulu me remettre à niveau techniquement parlant (depuis 3 ans, j’ai pas mal lâché le clavier). Pour mes besoins persos, j’ai donc créé un petit projet pour remplacer une solution propriétaire existante. Sans aucune prétention, j’ai hébergé ce projet sur Github et comme c’est la seule solution <em>open source</em> de ce type, le nombre de personnes intéressées a augmenté …</p>\n<p><em>Les utilisateurs de services Framasoft ne le savent pas forcément, mais tu as déjà pas mal participé à la FramaGalaxie, non ?<br /></em></p>\n<p>En effet. J’ai commencé un plugin pour Framanews, <a href=\"https://github.com/nicosomb/ttrss-purge-accounts\">ttrss-purge-accounts</a>, qui permet de nettoyer la base de données de comptes plus utilisés. Mais ce <em>plugin</em> a besoin d’être terminé à 100% pour être intégré au sein de Framanews (et donc de Tiny Tiny RSS), si quelqu’un souhaite m’aider, il n’y a aucun souci.<br />J’ai aussi fait 1 ou 2 apparitions dans des traductions pour Framablog. Rien d’extraordinaire, je ne suis pas bilingue, ça me permet de m’entraîner.</p>\n<p><em>Parlons de suite de ce qui fâche : ton application Wallabag, elle s’appellait pas “Poche”, avant ? Tu nous racontes l’histoire ?<br /></em></p>\n<p>Euh en effet … Déjà, pourquoi <em>poche</em> ? Parce que l’un des trois « ténors » sur le marché s’appelle <em>Pocket</em>. Comme mon appli n’était destinée qu’à mon usage personnel au départ, je ne me suis pas torturé bien longtemps.</p>\n<p>Cet été, on a failli changer de nom, quand il y a eu de plus en plus d’utilisateurs. Et puis on s’est dit que poche, c’était pas mal, ça sonnait bien français et puis avec les quelques dizaines d’utilisateurs, on ne gênerait personne.</p>\n<p>C’est sans compter avec les sociétés américaines et leur fâcheuse manie de vouloir envoyer leurs avocats à tout bout de champ. Le 23 janvier, j’ai reçu un email de la part du cabinet d’avocats de Pocket me demandant de changer le nom, le logo, de ne plus utiliser le terme “read-it-later” (« lisez le plus tard ») et de ne plus dire que Pocket n’est pas gratuit (tout est parti d’<a href=\"https://twitter.com/wallabagapp/status/423786365944225792\">un tweet</a> où je qualifie Pocket de « non free » à savoir non libre). Bref, même si je semblais dans mon droit, j’ai quand même pris la décision de changer de nom et Wallabag est né, suite aux <a href=\"http://framadate.org/studs.php?sondage=llcp6ojpyc9pklha\">dizaines de propositions de nom reçues</a>. C’est un mélange entre le wallaby (de la famille des kangourous, qui stockent dans leur poche ce qui leur est cher) et <em>bag</em> (les termes sac / sacoche / besace sont énormément revenus). Mais maintenant, on va de l’avant, plus de temps à perdre avec ça, on a du pain sur la planche.<br /><img src=\"http://www.framablog.org/public/_img/framablog/wallaby_baby.jpg\" alt=\"wallaby avec bébé dans sa poche\" class=\"c1\" title=\"wallaby avec bébé dans sa poche\" /> crédit photo <a href=\"http://www.flickr.com/photos/26782864@N00/5027202234/in/photolist-8EeJ5A-h1TL6v-NEL81-cnNkSo-9YM1tv-7Kcg6b-8zpAoa-a1ZLMN-9YM39r-7h5SAD-8EeHfL-8EeFwu-dtVwnM-8uoME1-JEzXe-Gq4qy-92VJPR-Cxe1v-8H3D2J-a1ZFNs-9Y72K6-8EeGxL-5L53Fx-5NkENs-5U8CTY-5Nkssh-nkavF-9CrgwP-7sdCAa-duf2Kh-hZepzy-hZdU1e-hZeofF-hZekDg-hZegAY-hZeMZn-jaHgAf-8P87D2-5NgqRv-aT48QB-hZdV4Y-hZeC64-ERgps-5VYGGd-5VYJB1-5NkrFk-6Jxh7h-7h9PuQ-ERfMx-h1U1ih-h1USBx\">William Warby</a> qui autorise explicitement toute réutilisation.</p>\n<p><em>Bon, alors explique-moi ce que je vais pouvoir faire avec Framabag…<br /></em></p>\n<p>Alors Framabag, ça te permet de te créer un compte gratuitement et librement pour pouvoir utiliser Wallabag. Seule ton adresse email est nécessaire, on se charge d’installer et de mettre à jour Wallabag pour toi. Tu peux d’ailleurs profiter <a href=\"http://www.framasoft.net/#topPgCloud\">d’autres services proposés par Framasoft ici</a>.</p>\n<p>À ce jour, il y a 834 comptes créés sur Framabag.</p>\n<p><em>Vous avez vraiment conçu ce service afin qu’on puisse l’utiliser avec un maximum d’outils, non ?<br /></em></p>\n<p>Autour de l’application web, il existe déjà des applications pour smartphones (Android et Windows Phone), des extensions Firefox et Google Chrome.</p>\n<p>Comme Wallabag possède des flux RSS, c’est facile de lire les articles sauvegardés sur sa liseuse (si celle-ci permet de lire des flux RSS). Calibre (« logiciel de lecture, de gestion de bibliothèques et de conversion de fichiers numériques de type ebook ou livre électronique »,nous dit ubuntu-fr.org) intègre depuis quelques semaines maintenant la possibilité de récupérer les articles non lus, pratique pour faire un fichier ePub !</p>\n<p>D’autres applications web permettent l’intégration avec Wallabag (FreshRSS, Leed et Tiny Tiny RSS pour les agrégateurs de flux). L’API qui sera disponible dans la prochaine version de Wallabag permettra encore plus d’interactivité.</p>\n<p><em>Y a-t-il un mode de lecture hors ligne ou est-ce que c’est prévu pour les prochaines versions ?<br /></em></p>\n<p>Il y a un pseudo mode hors ligne, disponible avec l’application Android. On peut récupérer (via un flux RSS) les articles non lus que l’on a sauvegardés. Une fois déconnecté, on peut continuer à lire sur son smartphone ou sa tablette les articles. Par contre, il manque des fonctionnalités : quand tu marques un article comme lu, ce n’est pas synchronisé avec la version web de Wallabag. J’espère que je suis presque clair dans mes explications.</p>\n<p>Pour la v2, qui est déjà en cours de développement, où je suis bien aidé par Vincent Jousse, on aura la possibilité d’avoir un vrai mode hors ligne.</p>\n<p><em>Alors si on veut aider / participer / trifouiller le code / vous envoyer des retours, on fait comment ?<br /></em></p>\n<p>On peut aider de plusieurs façons :</p>\n<ul><li>utiliser wallabag et nous remonter les problèmes rencontrés ;</li>\n<li>participer au développement de l’application https://github.com/wallabag/wallabag Si Silex / Symfony2 / HTML5 / etc. te parlent, n’hésite pas !</li>\n<li>comme tout projet, le gros point noir est le manque de documentation. <a href=\"http://doc.wallabag.org\">Elle est dispo ici</a> mais il manque plein de choses et tout n’est pas à jour ;</li>\n<li>parler de Wallabag autour de vous ;</li>\n<li>il existe <a href=\"https://flattr.com/thing/1265480/poche-a-read-it-later-open-source-system\">un compte Flattr</a>.</li>\n</ul><p><em>Le mot de la fin…?<br /></em></p>\n<p>Merci à Framasoft d’accueillir et de soutenir Wallabag !</p>\n<p>La route est encore bien longue pour ne plus utiliser de solutions propriétaires, mais on devrait y arriver, non ?</p>\n<p><img src=\"http://www.framablog.org/public/_img/framablog/pleinLesPoches.png\" alt=\"framasoft plein les poches\" class=\"c1\" title=\"framasoft plein les poches\" /><br /><a href=\"http://framalab.org/gknd-creator/\">hackez Gégé !</a></p>\n",
+        "6": "1",
+        "id": "1",
+        "title": "Framabag, un nouveau service libre et gratuit",
+        "url": "http://www.framablog.org/index.php/post/2014/02/05/Framabag-service-libre-gratuit-interview-developpeur",
+        "is_read": "0",
+        "is_fav": "0",
+        "content": "\n<h2>Une interview de Nicolas, son développeur.</h2>\n<p><em>Il ne vous a sûrement pas échappé que notre consommation de contenus du Web est terriblement chronophage et particulièrement frustrante tout à la fois : non seulement nous passons beaucoup (trop ?) de temps en ligne à explorer les mines aurifères de la toile, y détectant pépites et filons, mais nous sommes surtout constamment en manque. Même si nous ne sommes pas dans le zapping frénétique si facilement dénoncé par les doctes psychologues qui pontifient sur les dangers du numérique pour les jeunes cervelles, il nous vient souvent le goût amer de l’inachevé : pas le temps de tout lire (<a href=\"http://fr.wiktionary.org/wiki/TLDR\">TL;DR</a> est devenu le clin d’œil mi-figue mi-raisin d’une génération de lecteurs pressés), pas trop le temps de réfléchir non plus hélas, pas le temps de suivre la ribambelle de liens associés à un article…<br /></em></p>\n<p><em>Pour nous donner bonne conscience, nous rangeons scrupuleusement un marque-page de plus dans un sous-dossier qui en comporte déjà 256, nous notons un élément de plus dans la toujours ridiculement longue toudouliste, bref nous remettons à plus tard, c’est-à-dire le plus souvent aux introuvables calendes grecques, le soin de lire vraiment un article jugé intéressant, de regarder une vidéo signalée par les rézossocios, de lire un chapitre entier d’un ouvrage disponible en ligne…<br /></em></p>\n<p><em>Alors bien sûr, à défaut de nous donner tout le temps qui serait nécessaire, des solutions existent pour nous permettre de « lire plus tard » en sauvegardant le précieux pollen de nos butinages de site en site, et d’en faire ultérieurement votre miel ; c’est bel et bon mais les ruches sont un peu distantes, ça s’appelle le cloud (nos amis techies m’ont bien expliqué mais j’ai seulement compris que des trucs à moi sont sur des machines lointaines, ça ne me rassure pas trop) et elles sont souvent propriétaires, ne laissant entrer que les <s>utilisateurs</s> consommateurs payants et qui consentent à leurs conditions. Sans compter que de gros bourdons viennent profiter plus ou moins discrètement de toutes ces traces de nous-mêmes qui permettent de monétiser notre profil : si je collecte sur ces services (ne les nommons pas, justement) une série d’articles sur l’idée de Nature chez Diderot, je recevrai diverses sollicitations pour devenir client de la boutique Nature &amp; Découverte du boulevard Diderot. Et si d’aventure les programmes de la NSA moulinent sur le service, je serai peut-être un jour dans une liste des militants naturistes indésirables sur les vols de la PanAm (je ne sais plus trop si je plaisante là, finalement…)<br /></em></p>\n<p><em>La bonne idée : « se constituer un réservoir de documents sélectionnés à parcourir plus tard » appelait donc une autre bonne idée, celle d’avoir le contrôle de ce réservoir, de notre collection personnelle. C’est Nicolas Lœuillet, ci-dessous interviewé, qui s’y est collé avec une belle application appelée euh… oui, appelée Wallabag.<br /></em></p>\n<p><em>Framasoft soutient d’autant plus son initiative qu’<a href=\"http://www.framablog.org/index.php/post/2014/01/31/Geektionnerd-Wallabag\">on lui a cherché des misères pour une histoire de nom</a> et qu’il est possible d’installer soi-même une copie de Wallabag sur son propre site.<br /></em></p>\n<p><em>Le petit plus de Framasoft, réseau toujours désireux de vous proposer des alternatives libératrices, c’est de vous proposer (sur inscription préalable) un accès au Framabag, autrement dit votre Wallabag sur un serveur Frama* avec notre garantie de confidentialité. Comme pour le Framanews, nous vous accueillons volontiers dans la limite de nos capacités, en vous invitant à vous lancer dans votre auto-hébergement de Wallabag.<br />Cet article est trop long ? Mettez-le dans <a href=\"http://www.framabag.org/index.php\">votre Framabag</a> et hop.<br /></em></p>\n<p><em>Framablog : Salut Nicolas… Tu peux te présenter brièvement ?<br /></em></p>\n<p>Salut ! Développeur PHP depuis quelques années maintenant (10 ans), j’ai voulu me remettre à niveau techniquement parlant (depuis 3 ans, j’ai pas mal lâché le clavier). Pour mes besoins persos, j’ai donc créé un petit projet pour remplacer une solution propriétaire existante. Sans aucune prétention, j’ai hébergé ce projet sur Github et comme c’est la seule solution <em>open source</em> de ce type, le nombre de personnes intéressées a augmenté …</p>\n<p><em>Les utilisateurs de services Framasoft ne le savent pas forcément, mais tu as déjà pas mal participé à la FramaGalaxie, non ?<br /></em></p>\n<p>En effet. J’ai commencé un plugin pour Framanews, <a href=\"https://github.com/nicosomb/ttrss-purge-accounts\">ttrss-purge-accounts</a>, qui permet de nettoyer la base de données de comptes plus utilisés. Mais ce <em>plugin</em> a besoin d’être terminé à 100% pour être intégré au sein de Framanews (et donc de Tiny Tiny RSS), si quelqu’un souhaite m’aider, il n’y a aucun souci.<br />J’ai aussi fait 1 ou 2 apparitions dans des traductions pour Framablog. Rien d’extraordinaire, je ne suis pas bilingue, ça me permet de m’entraîner.</p>\n<p><em>Parlons de suite de ce qui fâche : ton application Wallabag, elle s’appellait pas “Poche”, avant ? Tu nous racontes l’histoire ?<br /></em></p>\n<p>Euh en effet … Déjà, pourquoi <em>poche</em> ? Parce que l’un des trois « ténors » sur le marché s’appelle <em>Pocket</em>. Comme mon appli n’était destinée qu’à mon usage personnel au départ, je ne me suis pas torturé bien longtemps.</p>\n<p>Cet été, on a failli changer de nom, quand il y a eu de plus en plus d’utilisateurs. Et puis on s’est dit que poche, c’était pas mal, ça sonnait bien français et puis avec les quelques dizaines d’utilisateurs, on ne gênerait personne.</p>\n<p>C’est sans compter avec les sociétés américaines et leur fâcheuse manie de vouloir envoyer leurs avocats à tout bout de champ. Le 23 janvier, j’ai reçu un email de la part du cabinet d’avocats de Pocket me demandant de changer le nom, le logo, de ne plus utiliser le terme “read-it-later” (« lisez le plus tard ») et de ne plus dire que Pocket n’est pas gratuit (tout est parti d’<a href=\"https://twitter.com/wallabagapp/status/423786365944225792\">un tweet</a> où je qualifie Pocket de « non free » à savoir non libre). Bref, même si je semblais dans mon droit, j’ai quand même pris la décision de changer de nom et Wallabag est né, suite aux <a href=\"http://framadate.org/studs.php?sondage=llcp6ojpyc9pklha\">dizaines de propositions de nom reçues</a>. C’est un mélange entre le wallaby (de la famille des kangourous, qui stockent dans leur poche ce qui leur est cher) et <em>bag</em> (les termes sac / sacoche / besace sont énormément revenus). Mais maintenant, on va de l’avant, plus de temps à perdre avec ça, on a du pain sur la planche.<br /><img src=\"http://www.framablog.org/public/_img/framablog/wallaby_baby.jpg\" alt=\"wallaby avec bébé dans sa poche\" class=\"c1\" title=\"wallaby avec bébé dans sa poche\" /> crédit photo <a href=\"http://www.flickr.com/photos/26782864@N00/5027202234/in/photolist-8EeJ5A-h1TL6v-NEL81-cnNkSo-9YM1tv-7Kcg6b-8zpAoa-a1ZLMN-9YM39r-7h5SAD-8EeHfL-8EeFwu-dtVwnM-8uoME1-JEzXe-Gq4qy-92VJPR-Cxe1v-8H3D2J-a1ZFNs-9Y72K6-8EeGxL-5L53Fx-5NkENs-5U8CTY-5Nkssh-nkavF-9CrgwP-7sdCAa-duf2Kh-hZepzy-hZdU1e-hZeofF-hZekDg-hZegAY-hZeMZn-jaHgAf-8P87D2-5NgqRv-aT48QB-hZdV4Y-hZeC64-ERgps-5VYGGd-5VYJB1-5NkrFk-6Jxh7h-7h9PuQ-ERfMx-h1U1ih-h1USBx\">William Warby</a> qui autorise explicitement toute réutilisation.</p>\n<p><em>Bon, alors explique-moi ce que je vais pouvoir faire avec Framabag…<br /></em></p>\n<p>Alors Framabag, ça te permet de te créer un compte gratuitement et librement pour pouvoir utiliser Wallabag. Seule ton adresse email est nécessaire, on se charge d’installer et de mettre à jour Wallabag pour toi. Tu peux d’ailleurs profiter <a href=\"http://www.framasoft.net/#topPgCloud\">d’autres services proposés par Framasoft ici</a>.</p>\n<p>À ce jour, il y a 834 comptes créés sur Framabag.</p>\n<p><em>Vous avez vraiment conçu ce service afin qu’on puisse l’utiliser avec un maximum d’outils, non ?<br /></em></p>\n<p>Autour de l’application web, il existe déjà des applications pour smartphones (Android et Windows Phone), des extensions Firefox et Google Chrome.</p>\n<p>Comme Wallabag possède des flux RSS, c’est facile de lire les articles sauvegardés sur sa liseuse (si celle-ci permet de lire des flux RSS). Calibre (« logiciel de lecture, de gestion de bibliothèques et de conversion de fichiers numériques de type ebook ou livre électronique »,nous dit ubuntu-fr.org) intègre depuis quelques semaines maintenant la possibilité de récupérer les articles non lus, pratique pour faire un fichier ePub !</p>\n<p>D’autres applications web permettent l’intégration avec Wallabag (FreshRSS, Leed et Tiny Tiny RSS pour les agrégateurs de flux). L’API qui sera disponible dans la prochaine version de Wallabag permettra encore plus d’interactivité.</p>\n<p><em>Y a-t-il un mode de lecture hors ligne ou est-ce que c’est prévu pour les prochaines versions ?<br /></em></p>\n<p>Il y a un pseudo mode hors ligne, disponible avec l’application Android. On peut récupérer (via un flux RSS) les articles non lus que l’on a sauvegardés. Une fois déconnecté, on peut continuer à lire sur son smartphone ou sa tablette les articles. Par contre, il manque des fonctionnalités : quand tu marques un article comme lu, ce n’est pas synchronisé avec la version web de Wallabag. J’espère que je suis presque clair dans mes explications.</p>\n<p>Pour la v2, qui est déjà en cours de développement, où je suis bien aidé par Vincent Jousse, on aura la possibilité d’avoir un vrai mode hors ligne.</p>\n<p><em>Alors si on veut aider / participer / trifouiller le code / vous envoyer des retours, on fait comment ?<br /></em></p>\n<p>On peut aider de plusieurs façons :</p>\n<ul><li>utiliser wallabag et nous remonter les problèmes rencontrés ;</li>\n<li>participer au développement de l’application https://github.com/wallabag/wallabag Si Silex / Symfony2 / HTML5 / etc. te parlent, n’hésite pas !</li>\n<li>comme tout projet, le gros point noir est le manque de documentation. <a href=\"http://doc.wallabag.org\">Elle est dispo ici</a> mais il manque plein de choses et tout n’est pas à jour ;</li>\n<li>parler de Wallabag autour de vous ;</li>\n<li>il existe <a href=\"https://flattr.com/thing/1265480/poche-a-read-it-later-open-source-system\">un compte Flattr</a>.</li>\n</ul><p><em>Le mot de la fin…?<br /></em></p>\n<p>Merci à Framasoft d’accueillir et de soutenir Wallabag !</p>\n<p>La route est encore bien longue pour ne plus utiliser de solutions propriétaires, mais on devrait y arriver, non ?</p>\n<p><img src=\"http://www.framablog.org/public/_img/framablog/pleinLesPoches.png\" alt=\"framasoft plein les poches\" class=\"c1\" title=\"framasoft plein les poches\" /><br /><a href=\"http://framalab.org/gknd-creator/\">hackez Gégé !</a></p>\n",
+        "user_id": "1"
+    },
+    {
+        "0": "2",
+        "1": "wallabag/wallabag",
+        "2": "https://github.com/wallabag/wallabag",
+        "3": "1",
+        "4": "0",
+        "5": "<span class=\"name\">README.md</span><p>wallabag is a self hostable application allowing you to not miss any content anymore. Click, save, read it when you can. It extracts content so that you can read it when you have time.</p>\n<p>More informations on our website: <a href=\"http://wallabag.org\">wallabag.org</a></p>\n<h2><a class=\"anchor\" href=\"https://github.com/wallabag/wallabag#license\"></a>License</h2>\n<p>Copyright © 2010-2014 Nicolas Lœuillet <a href=\"mailto:nicolas@loeuillet.org\">nicolas@loeuillet.org</a> This work is free. You can redistribute it and/or modify it under the terms of the Do What The Fuck You Want To Public License, Version 2, as published by Sam Hocevar. See the COPYING file for more details.</p>\n",
+        "6": "1",
+        "id": "2",
+        "title": "wallabag/wallabag",
+        "url": "https://github.com/wallabag/wallabag",
+        "is_read": "1",
+        "is_fav": "0",
+        "content": "<span class=\"name\">README.md</span><p>wallabag is a self hostable application allowing you to not miss any content anymore. Click, save, read it when you can. It extracts content so that you can read it when you have time.</p>\n<p>More informations on our website: <a href=\"http://wallabag.org\">wallabag.org</a></p>\n<h2><a class=\"anchor\" href=\"https://github.com/wallabag/wallabag#license\"></a>License</h2>\n<p>Copyright © 2010-2014 Nicolas Lœuillet <a href=\"mailto:nicolas@loeuillet.org\">nicolas@loeuillet.org</a> This work is free. You can redistribute it and/or modify it under the terms of the Do What The Fuck You Want To Public License, Version 2, as published by Sam Hocevar. See the COPYING file for more details.</p>\n",
+        "user_id": "1"
+    },
+    {
+        "0": "3",
+        "1": "a self hostable application for saving web pages | wallabag",
+        "2": "https://www.wallabag.org/",
+        "3": "1",
+        "4": "0",
+        "5": "\n<div class=\"row\">\n<div class=\"col-lg-8 col-md-12 col-xs-12 col-sm-12\">\n<p>wallabag (formerly poche) is a <strong>self hostable application for saving web pages</strong>. Unlike other services, wallabag is free (as in freedom) and open source.</p>\n</div>\n\n</div>\n<div class=\"row\">\n<div class=\"col-lg-8 col-md-12 col-xs-12 col-sm-12\">\n<p>With this application you will not miss content anymore. <strong>Click, save, read it when you want</strong>. It saves the content you select so that you can read it when you have time.</p>\n</div>\n\n</div>\n<div class=\"row\">\n<div class=\"col-lg-6 col-md-12 col-xs-12 col-sm-12\">\n<h2>How it works</h2>\n<p>Thanks to the bookmarklet or <a title=\"Downloads\" href=\"http://www.wallabag.org/downloads/\">third-party applications</a>, you save an article in your wallabag to read it later. Then, when you open your wallabag, <strong>you can comfortably read your articles</strong>.</p>\n<h2>How to use wallabag</h2>\n<p>There are two ways to use wallabag: you can <a href=\"http://www.wallabag.org/frequently-asked-questions/#How_can_I_install_wallabag_and_what_are_the_requirements\">install it</a> on your web server or you can <a href=\"http://app.inthepoche.com\">create an account</a> at Framabag (we install and upgrade wallabag for you).</p>\n</div>\n\n</div>\n",
+        "6": "1",
+        "id": "3",
+        "title": "a self hostable application for saving web pages | wallabag",
+        "url": "https://www.wallabag.org/",
+        "is_read": "1",
+        "is_fav": "0",
+        "content": "\n<div class=\"row\">\n<div class=\"col-lg-8 col-md-12 col-xs-12 col-sm-12\">\n<p>wallabag (formerly poche) is a <strong>self hostable application for saving web pages</strong>. Unlike other services, wallabag is free (as in freedom) and open source.</p>\n</div>\n\n</div>\n<div class=\"row\">\n<div class=\"col-lg-8 col-md-12 col-xs-12 col-sm-12\">\n<p>With this application you will not miss content anymore. <strong>Click, save, read it when you want</strong>. It saves the content you select so that you can read it when you have time.</p>\n</div>\n\n</div>\n<div class=\"row\">\n<div class=\"col-lg-6 col-md-12 col-xs-12 col-sm-12\">\n<h2>How it works</h2>\n<p>Thanks to the bookmarklet or <a title=\"Downloads\" href=\"http://www.wallabag.org/downloads/\">third-party applications</a>, you save an article in your wallabag to read it later. Then, when you open your wallabag, <strong>you can comfortably read your articles</strong>.</p>\n<h2>How to use wallabag</h2>\n<p>There are two ways to use wallabag: you can <a href=\"http://www.wallabag.org/frequently-asked-questions/#How_can_I_install_wallabag_and_what_are_the_requirements\">install it</a> on your web server or you can <a href=\"http://app.inthepoche.com\">create an account</a> at Framabag (we install and upgrade wallabag for you).</p>\n</div>\n\n</div>\n",
+        "user_id": "1"
+    }
+]
diff --git a/src/Wallabag/ImportBundle/WallabagImportBundle.php b/src/Wallabag/ImportBundle/WallabagImportBundle.php
new file mode 100644 (file)
index 0000000..a5ddc1b
--- /dev/null
@@ -0,0 +1,17 @@
+<?php
+
+namespace Wallabag\ImportBundle;
+
+use Symfony\Component\HttpKernel\Bundle\Bundle;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Wallabag\ImportBundle\Import\ImportCompilerPass;
+
+class WallabagImportBundle extends Bundle
+{
+    public function build(ContainerBuilder $container)
+    {
+        parent::build($container);
+
+        $container->addCompilerPass(new ImportCompilerPass());
+    }
+}
diff --git a/web/uploads/import/.gitkeep b/web/uploads/import/.gitkeep
new file mode 100644 (file)
index 0000000..e69de29