aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJeremy Benoist <j0k3r@users.noreply.github.com>2016-01-07 22:15:08 +0100
committerJeremy Benoist <j0k3r@users.noreply.github.com>2016-01-07 22:15:08 +0100
commit39643c6b76d92d509b1af0228b6379d7fdce8a1c (patch)
tree931dceef7dbc8ae9911d01ded709d558417a6cdd
parent488a468e3e11ff0ab6284afe232bf0f7fa68a8eb (diff)
parentb88cf91fc8371194df78e690983c61ea94f266cd (diff)
downloadwallabag-2.0.0-alpha.1.tar.gz
wallabag-2.0.0-alpha.1.tar.zst
wallabag-2.0.0-alpha.1.zip
Merge pull request #1493 from wallabag/v2-pocket-import2.0.0-alpha.1
v2 – 1st draft for Pocket import via API & Wallabag v1 import
-rw-r--r--app/AppKernel.php1
-rw-r--r--app/config/config.yml4
-rw-r--r--app/config/parameters.yml.dist3
-rw-r--r--app/config/routing.yml5
-rw-r--r--app/config/tests/parameters.yml.dist.mysql3
-rw-r--r--app/config/tests/parameters.yml.dist.pgsql3
-rw-r--r--app/config/tests/parameters.yml.dist.sqlite3
-rw-r--r--composer.json3
-rw-r--r--composer.lock38
-rw-r--r--docs/en/index.rst1
-rw-r--r--docs/en/user/import.rst39
-rw-r--r--docs/img/user/export_wllbg_1.pngbin0 -> 10954 bytes
-rw-r--r--docs/img/user/import_wllbg.pngbin0 -> 11803 bytes
-rw-r--r--src/Wallabag/ApiBundle/Controller/WallabagRestController.php20
-rw-r--r--src/Wallabag/CoreBundle/Command/InstallCommand.php1
-rw-r--r--src/Wallabag/CoreBundle/Controller/EntryController.php13
-rw-r--r--src/Wallabag/CoreBundle/Entity/Config.php9
-rw-r--r--src/Wallabag/CoreBundle/Entity/Entry.php8
-rw-r--r--src/Wallabag/CoreBundle/Repository/EntryRepository.php25
-rw-r--r--src/Wallabag/CoreBundle/Resources/config/services.yml1
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml12
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig1
-rwxr-xr-xsrc/Wallabag/CoreBundle/Resources/views/themes/material/public/css/main.css6
-rw-r--r--src/Wallabag/ImportBundle/Command/ImportCommand.php53
-rw-r--r--src/Wallabag/ImportBundle/Controller/ImportController.php19
-rw-r--r--src/Wallabag/ImportBundle/Controller/PocketController.php66
-rw-r--r--src/Wallabag/ImportBundle/Controller/WallabagV1Controller.php59
-rw-r--r--src/Wallabag/ImportBundle/DependencyInjection/Configuration.php27
-rw-r--r--src/Wallabag/ImportBundle/DependencyInjection/WallabagImportExtension.php27
-rw-r--r--src/Wallabag/ImportBundle/Form/Type/UploadImportType.php22
-rw-r--r--src/Wallabag/ImportBundle/Import/ImportChain.php34
-rw-r--r--src/Wallabag/ImportBundle/Import/ImportCompilerPass.php33
-rw-r--r--src/Wallabag/ImportBundle/Import/ImportInterface.php45
-rw-r--r--src/Wallabag/ImportBundle/Import/PocketImport.php267
-rw-r--r--src/Wallabag/ImportBundle/Import/WallabagV1Import.php159
-rw-r--r--src/Wallabag/ImportBundle/Resources/config/services.yml34
-rw-r--r--src/Wallabag/ImportBundle/Resources/views/Import/index.html.twig21
-rw-r--r--src/Wallabag/ImportBundle/Resources/views/Pocket/index.html.twig18
-rw-r--r--src/Wallabag/ImportBundle/Resources/views/WallabagV1/index.html.twig36
-rw-r--r--src/Wallabag/ImportBundle/Tests/Controller/ImportControllerTest.php29
-rw-r--r--src/Wallabag/ImportBundle/Tests/Controller/PocketControllerTest.php42
-rw-r--r--src/Wallabag/ImportBundle/Tests/Controller/WallabagV1ControllerTest.php69
-rw-r--r--src/Wallabag/ImportBundle/Tests/Import/ImportChainTest.php21
-rw-r--r--src/Wallabag/ImportBundle/Tests/Import/ImportCompilerPassTest.php47
-rw-r--r--src/Wallabag/ImportBundle/Tests/Import/PocketImportTest.php314
-rw-r--r--src/Wallabag/ImportBundle/Tests/Import/WallabagV1ImportTest.php97
-rw-r--r--src/Wallabag/ImportBundle/Tests/fixtures/test.html0
-rw-r--r--src/Wallabag/ImportBundle/Tests/fixtures/test.txt0
-rw-r--r--src/Wallabag/ImportBundle/Tests/fixtures/wallabag-v1.json50
-rw-r--r--src/Wallabag/ImportBundle/WallabagImportBundle.php17
-rw-r--r--web/uploads/import/.gitkeep0
51 files changed, 1770 insertions, 35 deletions
diff --git a/app/AppKernel.php b/app/AppKernel.php
index 85edc14a..93b0201a 100644
--- a/app/AppKernel.php
+++ b/app/AppKernel.php
@@ -31,6 +31,7 @@ class AppKernel extends Kernel
31 new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(), 31 new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),
32 new Scheb\TwoFactorBundle\SchebTwoFactorBundle(), 32 new Scheb\TwoFactorBundle\SchebTwoFactorBundle(),
33 new KPhoen\RulerZBundle\KPhoenRulerZBundle(), 33 new KPhoen\RulerZBundle\KPhoenRulerZBundle(),
34 new Wallabag\ImportBundle\WallabagImportBundle(),
34 ); 35 );
35 36
36 if (in_array($this->getEnvironment(), array('dev', 'test'))) { 37 if (in_array($this->getEnvironment(), array('dev', 'test'))) {
diff --git a/app/config/config.yml b/app/config/config.yml
index 8403a458..e50f9b52 100644
--- a/app/config/config.yml
+++ b/app/config/config.yml
@@ -31,6 +31,10 @@ wallabag_core:
31 fr: 'Français' 31 fr: 'Français'
32 de: 'Deutsch' 32 de: 'Deutsch'
33 33
34wallabag_import:
35 allow_mimetypes: ['application/octet-stream', 'application/json', 'text/plain']
36 resource_dir: "%kernel.root_dir%/../web/uploads/import"
37
34# Twig Configuration 38# Twig Configuration
35twig: 39twig:
36 debug: "%kernel.debug%" 40 debug: "%kernel.debug%"
diff --git a/app/config/parameters.yml.dist b/app/config/parameters.yml.dist
index 149179c2..a769bc66 100644
--- a/app/config/parameters.yml.dist
+++ b/app/config/parameters.yml.dist
@@ -60,3 +60,6 @@ parameters:
60 language: en 60 language: en
61 from_email: no-reply@wallabag.org 61 from_email: no-reply@wallabag.org
62 rss_limit: 50 62 rss_limit: 50
63
64 # pocket import
65 pocket_consumer_key: xxxxxxxx
diff --git a/app/config/routing.yml b/app/config/routing.yml
index 0f7b61fb..1ca2f677 100644
--- a/app/config/routing.yml
+++ b/app/config/routing.yml
@@ -1,3 +1,8 @@
1wallabag_import:
2 resource: "@WallabagImportBundle/Controller/"
3 type: annotation
4 prefix: /import
5
1wallabag_api: 6wallabag_api:
2 resource: "@WallabagApiBundle/Resources/config/routing.yml" 7 resource: "@WallabagApiBundle/Resources/config/routing.yml"
3 prefix: / 8 prefix: /
diff --git a/app/config/tests/parameters.yml.dist.mysql b/app/config/tests/parameters.yml.dist.mysql
index 096ad8c7..88b1d2b4 100644
--- a/app/config/tests/parameters.yml.dist.mysql
+++ b/app/config/tests/parameters.yml.dist.mysql
@@ -60,3 +60,6 @@ parameters:
60 language: en_US 60 language: en_US
61 from_email: no-reply@wallabag.org 61 from_email: no-reply@wallabag.org
62 rss_limit: 50 62 rss_limit: 50
63
64 # pocket import
65 pocket_consumer_key: xxxxxxxx
diff --git a/app/config/tests/parameters.yml.dist.pgsql b/app/config/tests/parameters.yml.dist.pgsql
index ca3f6ea2..3c61142d 100644
--- a/app/config/tests/parameters.yml.dist.pgsql
+++ b/app/config/tests/parameters.yml.dist.pgsql
@@ -60,3 +60,6 @@ parameters:
60 language: en_US 60 language: en_US
61 from_email: no-reply@wallabag.org 61 from_email: no-reply@wallabag.org
62 rss_limit: 50 62 rss_limit: 50
63
64 # pocket import
65 pocket_consumer_key: xxxxxxxx
diff --git a/app/config/tests/parameters.yml.dist.sqlite b/app/config/tests/parameters.yml.dist.sqlite
index 92460bcf..2f7699b5 100644
--- a/app/config/tests/parameters.yml.dist.sqlite
+++ b/app/config/tests/parameters.yml.dist.sqlite
@@ -60,3 +60,6 @@ parameters:
60 language: en_US 60 language: en_US
61 from_email: no-reply@wallabag.org 61 from_email: no-reply@wallabag.org
62 rss_limit: 50 62 rss_limit: 50
63
64 # pocket import
65 pocket_consumer_key: xxxxxxxx
diff --git a/composer.json b/composer.json
index bf519faf..0ba42a3e 100644
--- a/composer.json
+++ b/composer.json
@@ -59,7 +59,8 @@
59 "scheb/two-factor-bundle": "~1.4.0", 59 "scheb/two-factor-bundle": "~1.4.0",
60 "grandt/phpepub": "~4.0", 60 "grandt/phpepub": "~4.0",
61 "wallabag/php-mobi": "~1.0.0", 61 "wallabag/php-mobi": "~1.0.0",
62 "kphoen/rulerz-bundle": "~0.10" 62 "kphoen/rulerz-bundle": "~0.10",
63 "guzzlehttp/guzzle": "^5.2.0"
63 }, 64 },
64 "require-dev": { 65 "require-dev": {
65 "doctrine/doctrine-fixtures-bundle": "~2.2.0", 66 "doctrine/doctrine-fixtures-bundle": "~2.2.0",
diff --git a/composer.lock b/composer.lock
index aee96198..858a125c 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,8 @@
4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
5 "This file is @generated automatically" 5 "This file is @generated automatically"
6 ], 6 ],
7 "hash": "91da706ef4b39a73704c3e2154c1a227", 7 "hash": "fdba142656b2089b0e4cbddb45e2ad1f",
8 "content-hash": "81a3c2c84d78471bfb526b2b572182f7", 8 "content-hash": "a233f851c52683783b6a42be707c52b1",
9 "packages": [ 9 "packages": [
10 { 10 {
11 "name": "behat/transliterator", 11 "name": "behat/transliterator",
@@ -117,33 +117,33 @@
117 }, 117 },
118 { 118 {
119 "name": "doctrine/cache", 119 "name": "doctrine/cache",
120 "version": "v1.5.4", 120 "version": "v1.6.0",
121 "source": { 121 "source": {
122 "type": "git", 122 "type": "git",
123 "url": "https://github.com/doctrine/cache.git", 123 "url": "https://github.com/doctrine/cache.git",
124 "reference": "47cdc76ceb95cc591d9c79a36dc3794975b5d136" 124 "reference": "f8af318d14bdb0eff0336795b428b547bd39ccb6"
125 }, 125 },
126 "dist": { 126 "dist": {
127 "type": "zip", 127 "type": "zip",
128 "url": "https://api.github.com/repos/doctrine/cache/zipball/47cdc76ceb95cc591d9c79a36dc3794975b5d136", 128 "url": "https://api.github.com/repos/doctrine/cache/zipball/f8af318d14bdb0eff0336795b428b547bd39ccb6",
129 "reference": "47cdc76ceb95cc591d9c79a36dc3794975b5d136", 129 "reference": "f8af318d14bdb0eff0336795b428b547bd39ccb6",
130 "shasum": "" 130 "shasum": ""
131 }, 131 },
132 "require": { 132 "require": {
133 "php": ">=5.3.2" 133 "php": "~5.5|~7.0"
134 }, 134 },
135 "conflict": { 135 "conflict": {
136 "doctrine/common": ">2.2,<2.4" 136 "doctrine/common": ">2.2,<2.4"
137 }, 137 },
138 "require-dev": { 138 "require-dev": {
139 "phpunit/phpunit": ">=3.7", 139 "phpunit/phpunit": "~4.8|~5.0",
140 "predis/predis": "~1.0", 140 "predis/predis": "~1.0",
141 "satooshi/php-coveralls": "~0.6" 141 "satooshi/php-coveralls": "~0.6"
142 }, 142 },
143 "type": "library", 143 "type": "library",
144 "extra": { 144 "extra": {
145 "branch-alias": { 145 "branch-alias": {
146 "dev-master": "1.5.x-dev" 146 "dev-master": "1.6.x-dev"
147 } 147 }
148 }, 148 },
149 "autoload": { 149 "autoload": {
@@ -183,7 +183,7 @@
183 "cache", 183 "cache",
184 "caching" 184 "caching"
185 ], 185 ],
186 "time": "2015-12-19 05:03:47" 186 "time": "2015-12-31 16:37:02"
187 }, 187 },
188 { 188 {
189 "name": "doctrine/collections", 189 "name": "doctrine/collections",
@@ -981,17 +981,17 @@
981 }, 981 },
982 { 982 {
983 "name": "friendsofsymfony/rest-bundle", 983 "name": "friendsofsymfony/rest-bundle",
984 "version": "1.7.6", 984 "version": "1.7.7",
985 "target-dir": "FOS/RestBundle", 985 "target-dir": "FOS/RestBundle",
986 "source": { 986 "source": {
987 "type": "git", 987 "type": "git",
988 "url": "https://github.com/FriendsOfSymfony/FOSRestBundle.git", 988 "url": "https://github.com/FriendsOfSymfony/FOSRestBundle.git",
989 "reference": "f95b2f141748e9a5e2ddae833f60c38417aee8c3" 989 "reference": "c79b7e5df96e5581591ceb6a026bd4e5f9346de0"
990 }, 990 },
991 "dist": { 991 "dist": {
992 "type": "zip", 992 "type": "zip",
993 "url": "https://api.github.com/repos/FriendsOfSymfony/FOSRestBundle/zipball/f95b2f141748e9a5e2ddae833f60c38417aee8c3", 993 "url": "https://api.github.com/repos/FriendsOfSymfony/FOSRestBundle/zipball/c79b7e5df96e5581591ceb6a026bd4e5f9346de0",
994 "reference": "f95b2f141748e9a5e2ddae833f60c38417aee8c3", 994 "reference": "c79b7e5df96e5581591ceb6a026bd4e5f9346de0",
995 "shasum": "" 995 "shasum": ""
996 }, 996 },
997 "require": { 997 "require": {
@@ -1063,7 +1063,7 @@
1063 "keywords": [ 1063 "keywords": [
1064 "rest" 1064 "rest"
1065 ], 1065 ],
1066 "time": "2015-12-20 13:45:30" 1066 "time": "2015-12-29 16:02:50"
1067 }, 1067 },
1068 { 1068 {
1069 "name": "friendsofsymfony/user-bundle", 1069 "name": "friendsofsymfony/user-bundle",
@@ -1071,12 +1071,12 @@
1071 "source": { 1071 "source": {
1072 "type": "git", 1072 "type": "git",
1073 "url": "https://github.com/FriendsOfSymfony/FOSUserBundle.git", 1073 "url": "https://github.com/FriendsOfSymfony/FOSUserBundle.git",
1074 "reference": "e39b040e272c72f0a090c67d802e1d3b2d0b0313" 1074 "reference": "e5e7a2b8984da8dfedaf44adc7e5f60a62ad280c"
1075 }, 1075 },
1076 "dist": { 1076 "dist": {
1077 "type": "zip", 1077 "type": "zip",
1078 "url": "https://api.github.com/repos/FriendsOfSymfony/FOSUserBundle/zipball/e39b040e272c72f0a090c67d802e1d3b2d0b0313", 1078 "url": "https://api.github.com/repos/FriendsOfSymfony/FOSUserBundle/zipball/e5e7a2b8984da8dfedaf44adc7e5f60a62ad280c",
1079 "reference": "e39b040e272c72f0a090c67d802e1d3b2d0b0313", 1079 "reference": "e5e7a2b8984da8dfedaf44adc7e5f60a62ad280c",
1080 "shasum": "" 1080 "shasum": ""
1081 }, 1081 },
1082 "require": { 1082 "require": {
@@ -1132,7 +1132,7 @@
1132 "keywords": [ 1132 "keywords": [
1133 "User management" 1133 "User management"
1134 ], 1134 ],
1135 "time": "2015-12-05 09:38:57" 1135 "time": "2015-12-28 18:02:43"
1136 }, 1136 },
1137 { 1137 {
1138 "name": "gedmo/doctrine-extensions", 1138 "name": "gedmo/doctrine-extensions",
diff --git a/docs/en/index.rst b/docs/en/index.rst
index 6ccfd44c..8cb1b479 100644
--- a/docs/en/index.rst
+++ b/docs/en/index.rst
@@ -24,6 +24,7 @@ The main documentation for the site is organized into a couple sections:
24 user/login 24 user/login
25 user/configuration 25 user/configuration
26 user/first_article 26 user/first_article
27 user/import
27 28
28user/organize 29user/organize
29user/filters 30user/filters
diff --git a/docs/en/user/import.rst b/docs/en/user/import.rst
new file mode 100644
index 00000000..d326b06e
--- /dev/null
+++ b/docs/en/user/import.rst
@@ -0,0 +1,39 @@
1Migrate to wallabag
2===================
3
4From wallabag 1.x
5-----------------
6
7Export your data from your wallabag 1.x
8~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
9
10On your config page, click on ``JSON export`` in the ``Export your wallabag data`` section.
11
12.. image:: ../../img/user/export_wllbg_1.png
13 :alt: Export from wallabag 1.x
14 :align: center
15
16You will have a ``wallabag-export-1-1970-01-01.json`` file.
17
18Import your data into wallabag 2.x
19~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
20
21Click on ``Import`` link in the menu, select your export file on your computer and import it.
22
23.. image:: ../../img/user/import_wllbg.png
24 :alt: Import from wallabag 1.x
25 :align: center
26
27All your wallabag 1.x articles will be imported.
28
29From Pocket
30-----------
31
32From Instapaper
33---------------
34
35From Readability
36----------------
37
38From HTML or JSON file
39----------------------
diff --git a/docs/img/user/export_wllbg_1.png b/docs/img/user/export_wllbg_1.png
new file mode 100644
index 00000000..f9d24517
--- /dev/null
+++ b/docs/img/user/export_wllbg_1.png
Binary files differ
diff --git a/docs/img/user/import_wllbg.png b/docs/img/user/import_wllbg.png
new file mode 100644
index 00000000..6eec07e4
--- /dev/null
+++ b/docs/img/user/import_wllbg.png
Binary files differ
diff --git a/src/Wallabag/ApiBundle/Controller/WallabagRestController.php b/src/Wallabag/ApiBundle/Controller/WallabagRestController.php
index 459c4172..354a6f8e 100644
--- a/src/Wallabag/ApiBundle/Controller/WallabagRestController.php
+++ b/src/Wallabag/ApiBundle/Controller/WallabagRestController.php
@@ -60,7 +60,7 @@ class WallabagRestController extends FOSRestController
60 * } 60 * }
61 * ) 61 * )
62 * 62 *
63 * @return Entry 63 * @return Response
64 */ 64 */
65 public function getEntriesAction(Request $request) 65 public function getEntriesAction(Request $request)
66 { 66 {
@@ -101,7 +101,7 @@ class WallabagRestController extends FOSRestController
101 * } 101 * }
102 * ) 102 * )
103 * 103 *
104 * @return Entry 104 * @return Response
105 */ 105 */
106 public function getEntryAction(Entry $entry) 106 public function getEntryAction(Entry $entry)
107 { 107 {
@@ -124,7 +124,7 @@ class WallabagRestController extends FOSRestController
124 * } 124 * }
125 * ) 125 * )
126 * 126 *
127 * @return Entry 127 * @return Response
128 */ 128 */
129 public function postEntriesAction(Request $request) 129 public function postEntriesAction(Request $request)
130 { 130 {
@@ -166,7 +166,7 @@ class WallabagRestController extends FOSRestController
166 * } 166 * }
167 * ) 167 * )
168 * 168 *
169 * @return Entry 169 * @return Response
170 */ 170 */
171 public function patchEntriesAction(Entry $entry, Request $request) 171 public function patchEntriesAction(Entry $entry, Request $request)
172 { 172 {
@@ -211,7 +211,7 @@ class WallabagRestController extends FOSRestController
211 * } 211 * }
212 * ) 212 * )
213 * 213 *
214 * @return Entry 214 * @return Response
215 */ 215 */
216 public function deleteEntriesAction(Entry $entry) 216 public function deleteEntriesAction(Entry $entry)
217 { 217 {
@@ -235,6 +235,8 @@ class WallabagRestController extends FOSRestController
235 * {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"} 235 * {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
236 * } 236 * }
237 * ) 237 * )
238 *
239 * @return Response
238 */ 240 */
239 public function getEntriesTagsAction(Entry $entry) 241 public function getEntriesTagsAction(Entry $entry)
240 { 242 {
@@ -257,6 +259,8 @@ class WallabagRestController extends FOSRestController
257 * {"name"="tags", "dataType"="string", "required"=false, "format"="tag1,tag2,tag3", "description"="a comma-separated list of tags."}, 259 * {"name"="tags", "dataType"="string", "required"=false, "format"="tag1,tag2,tag3", "description"="a comma-separated list of tags."},
258 * } 260 * }
259 * ) 261 * )
262 *
263 * @return Response
260 */ 264 */
261 public function postEntriesTagsAction(Request $request, Entry $entry) 265 public function postEntriesTagsAction(Request $request, Entry $entry)
262 { 266 {
@@ -286,6 +290,8 @@ class WallabagRestController extends FOSRestController
286 * {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"} 290 * {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
287 * } 291 * }
288 * ) 292 * )
293 *
294 * @return Response
289 */ 295 */
290 public function deleteEntriesTagsAction(Entry $entry, Tag $tag) 296 public function deleteEntriesTagsAction(Entry $entry, Tag $tag)
291 { 297 {
@@ -306,6 +312,8 @@ class WallabagRestController extends FOSRestController
306 * Retrieve all tags. 312 * Retrieve all tags.
307 * 313 *
308 * @ApiDoc() 314 * @ApiDoc()
315 *
316 * @return Response
309 */ 317 */
310 public function getTagsAction() 318 public function getTagsAction()
311 { 319 {
@@ -328,6 +336,8 @@ class WallabagRestController extends FOSRestController
328 * {"name"="tag", "dataType"="integer", "requirement"="\w+", "description"="The tag"} 336 * {"name"="tag", "dataType"="integer", "requirement"="\w+", "description"="The tag"}
329 * } 337 * }
330 * ) 338 * )
339 *
340 * @return Response
331 */ 341 */
332 public function deleteTagAction(Tag $tag) 342 public function deleteTagAction(Tag $tag)
333 { 343 {
diff --git a/src/Wallabag/CoreBundle/Command/InstallCommand.php b/src/Wallabag/CoreBundle/Command/InstallCommand.php
index 85c4ee90..e791d4dd 100644
--- a/src/Wallabag/CoreBundle/Command/InstallCommand.php
+++ b/src/Wallabag/CoreBundle/Command/InstallCommand.php
@@ -11,7 +11,6 @@ use Symfony\Component\Console\Output\NullOutput;
11use Symfony\Component\Console\Question\Question; 11use Symfony\Component\Console\Question\Question;
12use Symfony\Component\Console\Question\ConfirmationQuestion; 12use Symfony\Component\Console\Question\ConfirmationQuestion;
13use Symfony\Component\Console\Helper\Table; 13use Symfony\Component\Console\Helper\Table;
14use Wallabag\UserBundle\Entity\User;
15use Wallabag\CoreBundle\Entity\Config; 14use Wallabag\CoreBundle\Entity\Config;
16 15
17class InstallCommand extends ContainerAwareCommand 16class InstallCommand extends ContainerAwareCommand
diff --git a/src/Wallabag/CoreBundle/Controller/EntryController.php b/src/Wallabag/CoreBundle/Controller/EntryController.php
index fa580133..37f7ab60 100644
--- a/src/Wallabag/CoreBundle/Controller/EntryController.php
+++ b/src/Wallabag/CoreBundle/Controller/EntryController.php
@@ -48,6 +48,19 @@ class EntryController extends Controller
48 $form->handleRequest($request); 48 $form->handleRequest($request);
49 49
50 if ($form->isValid()) { 50 if ($form->isValid()) {
51 // check for existing entry, if it exists, redirect to it with a message
52 $existingEntry = $this->get('wallabag_core.entry_repository')
53 ->existByUrlAndUserId($entry->getUrl(), $this->getUser()->getId());
54
55 if (false !== $existingEntry) {
56 $this->get('session')->getFlashBag()->add(
57 'notice',
58 'Entry already saved on '.$existingEntry['createdAt']->format('d-m-Y')
59 );
60
61 return $this->redirect($this->generateUrl('view', array('id' => $existingEntry['id'])));
62 }
63
51 $this->updateEntry($entry); 64 $this->updateEntry($entry);
52 $this->get('session')->getFlashBag()->add( 65 $this->get('session')->getFlashBag()->add(
53 'notice', 66 'notice',
diff --git a/src/Wallabag/CoreBundle/Entity/Config.php b/src/Wallabag/CoreBundle/Entity/Config.php
index 2ca4182e..d3590f35 100644
--- a/src/Wallabag/CoreBundle/Entity/Config.php
+++ b/src/Wallabag/CoreBundle/Entity/Config.php
@@ -5,6 +5,7 @@ namespace Wallabag\CoreBundle\Entity;
5use Doctrine\Common\Collections\ArrayCollection; 5use Doctrine\Common\Collections\ArrayCollection;
6use Doctrine\ORM\Mapping as ORM; 6use Doctrine\ORM\Mapping as ORM;
7use Symfony\Component\Validator\Constraints as Assert; 7use Symfony\Component\Validator\Constraints as Assert;
8use Wallabag\UserBundle\Entity\User;
8 9
9/** 10/**
10 * Config. 11 * Config.
@@ -86,7 +87,7 @@ class Config
86 /* 87 /*
87 * @param User $user 88 * @param User $user
88 */ 89 */
89 public function __construct(\Wallabag\UserBundle\Entity\User $user) 90 public function __construct(User $user)
90 { 91 {
91 $this->user = $user; 92 $this->user = $user;
92 $this->taggingRules = new ArrayCollection(); 93 $this->taggingRules = new ArrayCollection();
@@ -181,7 +182,7 @@ class Config
181 * 182 *
182 * @return Config 183 * @return Config
183 */ 184 */
184 public function setUser(\Wallabag\UserBundle\Entity\User $user = null) 185 public function setUser(User $user = null)
185 { 186 {
186 $this->user = $user; 187 $this->user = $user;
187 188
@@ -225,7 +226,7 @@ class Config
225 /** 226 /**
226 * Set rssLimit. 227 * Set rssLimit.
227 * 228 *
228 * @param string $rssLimit 229 * @param int $rssLimit
229 * 230 *
230 * @return Config 231 * @return Config
231 */ 232 */
@@ -239,7 +240,7 @@ class Config
239 /** 240 /**
240 * Get rssLimit. 241 * Get rssLimit.
241 * 242 *
242 * @return string 243 * @return int
243 */ 244 */
244 public function getRssLimit() 245 public function getRssLimit()
245 { 246 {
diff --git a/src/Wallabag/CoreBundle/Entity/Entry.php b/src/Wallabag/CoreBundle/Entity/Entry.php
index b413c489..f11a7786 100644
--- a/src/Wallabag/CoreBundle/Entity/Entry.php
+++ b/src/Wallabag/CoreBundle/Entity/Entry.php
@@ -245,7 +245,7 @@ class Entry
245 /** 245 /**
246 * Set isArchived. 246 * Set isArchived.
247 * 247 *
248 * @param string $isArchived 248 * @param bool $isArchived
249 * 249 *
250 * @return Entry 250 * @return Entry
251 */ 251 */
@@ -259,7 +259,7 @@ class Entry
259 /** 259 /**
260 * Get isArchived. 260 * Get isArchived.
261 * 261 *
262 * @return string 262 * @return bool
263 */ 263 */
264 public function isArchived() 264 public function isArchived()
265 { 265 {
@@ -276,7 +276,7 @@ class Entry
276 /** 276 /**
277 * Set isStarred. 277 * Set isStarred.
278 * 278 *
279 * @param string $isStarred 279 * @param bool $isStarred
280 * 280 *
281 * @return Entry 281 * @return Entry
282 */ 282 */
@@ -290,7 +290,7 @@ class Entry
290 /** 290 /**
291 * Get isStarred. 291 * Get isStarred.
292 * 292 *
293 * @return string 293 * @return bool
294 */ 294 */
295 public function isStarred() 295 public function isStarred()
296 { 296 {
diff --git a/src/Wallabag/CoreBundle/Repository/EntryRepository.php b/src/Wallabag/CoreBundle/Repository/EntryRepository.php
index ca71970b..c6763a40 100644
--- a/src/Wallabag/CoreBundle/Repository/EntryRepository.php
+++ b/src/Wallabag/CoreBundle/Repository/EntryRepository.php
@@ -223,4 +223,29 @@ class EntryRepository extends EntityRepository
223 ->getQuery() 223 ->getQuery()
224 ->getResult(); 224 ->getResult();
225 } 225 }
226
227 /**
228 * Find an entry by its url and its owner.
229 * If it exists, return the entry otherwise return false.
230 *
231 * @param $url
232 * @param $userId
233 *
234 * @return array|bool
235 */
236 public function existByUrlAndUserId($url, $userId)
237 {
238 $res = $this->createQueryBuilder('e')
239 ->select('e.id, e.createdAt')
240 ->where('e.url = :url')->setParameter('url', $url)
241 ->andWhere('e.user = :user_id')->setParameter('user_id', $userId)
242 ->getQuery()
243 ->getResult();
244
245 if (count($res)) {
246 return current($res);
247 }
248
249 return false;
250 }
226} 251}
diff --git a/src/Wallabag/CoreBundle/Resources/config/services.yml b/src/Wallabag/CoreBundle/Resources/config/services.yml
index c92b4eb3..96b1c931 100644
--- a/src/Wallabag/CoreBundle/Resources/config/services.yml
+++ b/src/Wallabag/CoreBundle/Resources/config/services.yml
@@ -63,6 +63,7 @@ services:
63 - @wallabag_core.tag_repository 63 - @wallabag_core.tag_repository
64 - @wallabag_core.entry_repository 64 - @wallabag_core.entry_repository
65 65
66 # repository as a service
66 wallabag_core.entry_repository: 67 wallabag_core.entry_repository:
67 class: Wallabag\CoreBundle\Repository\EntryRepository 68 class: Wallabag\CoreBundle\Repository\EntryRepository
68 factory: [ @doctrine.orm.default_entity_manager, getRepository ] 69 factory: [ @doctrine.orm.default_entity_manager, getRepository ]
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml
index 7b10dea1..06746584 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml
@@ -13,6 +13,7 @@ archive: 'Lus'
13all: 'Tous les articles' 13all: 'Tous les articles'
14tags: 'Tags' 14tags: 'Tags'
15config: 'Configuration' 15config: 'Configuration'
16import: 'Importer'
16howto: 'Aide' 17howto: 'Aide'
17logout: 'Déconnexion' 18logout: 'Déconnexion'
18Filtered: 'Articles filtrés' 19Filtered: 'Articles filtrés'
@@ -128,3 +129,14 @@ Download: 'Télécharger'
128Does this article appear wrong?: "Est-ce que cet article s'affiche mal ?" 129Does this article appear wrong?: "Est-ce que cet article s'affiche mal ?"
129Problems?: 'Un problème ?' 130Problems?: 'Un problème ?'
130Edit title: "Modifier le titre" 131Edit title: "Modifier le titre"
132
133# Import
134Welcome 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."
135"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."
136"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\"."
137"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."
138Connect to Pocket and import data: Se connecter à Pocket et importer les données.
139Please 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.
140File: Fichier
141Upload file: Importer le fichier
142Import contents: "Importer les contenus" \ No newline at end of file
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig
index f426e25b..6b8d7adf 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig
@@ -45,6 +45,7 @@
45 <li class="bold border-bottom {% if currentRoute == 'all' %}active{% endif %}"><a class="waves-effect" href="{{ path('all') }}">{% trans %}all{% endtrans %}</a></li> 45 <li class="bold border-bottom {% if currentRoute == 'all' %}active{% endif %}"><a class="waves-effect" href="{{ path('all') }}">{% trans %}all{% endtrans %}</a></li>
46 <li class="bold border-bottom {% if currentRoute == 'tags' %}active{% endif %}"><a class="waves-effect" href="{{ path('tag') }}">{% trans %}tags{% endtrans %}</a></li> 46 <li class="bold border-bottom {% if currentRoute == 'tags' %}active{% endif %}"><a class="waves-effect" href="{{ path('tag') }}">{% trans %}tags{% endtrans %}</a></li>
47 <li class="bold {% if currentRoute == 'config' %}active{% endif %}"><a class="waves-effect" href="{{ path('config') }}">{% trans %}config{% endtrans %}</a></li> 47 <li class="bold {% if currentRoute == 'config' %}active{% endif %}"><a class="waves-effect" href="{{ path('config') }}">{% trans %}config{% endtrans %}</a></li>
48 <li class="bold {% if currentRoute == 'import' %}active{% endif %}"><a class="waves-effect" href="{{ path('import') }}">{% trans %}import{% endtrans %}</a></li>
48 <li class="bold {% if currentRoute == 'howto' %}active{% endif %}"><a class="waves-effect" href="{{ path('howto') }}">{% trans %}howto{% endtrans %}</a></li> 49 <li class="bold {% if currentRoute == 'howto' %}active{% endif %}"><a class="waves-effect" href="{{ path('howto') }}">{% trans %}howto{% endtrans %}</a></li>
49 <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> 50 <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>
50 </ul> 51 </ul>
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/public/css/main.css b/src/Wallabag/CoreBundle/Resources/views/themes/material/public/css/main.css
index 73965571..0ce334a3 100755
--- a/src/Wallabag/CoreBundle/Resources/views/themes/material/public/css/main.css
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/public/css/main.css
@@ -500,4 +500,8 @@ footer [class^="icon-"]:hover, footer [class*=" icon-"]:hover {
500/* force height on non-input field in the settings page */ 500/* force height on non-input field in the settings page */
501div.settings div.input-field div, div.settings div.input-field ul { 501div.settings div.input-field div, div.settings div.input-field ul {
502 margin-top: 40px; 502 margin-top: 40px;
503} \ No newline at end of file 503}
504/* but avoid to kill all file input */
505div.settings div.file-field div {
506 margin-top: inherit;
507}
diff --git a/src/Wallabag/ImportBundle/Command/ImportCommand.php b/src/Wallabag/ImportBundle/Command/ImportCommand.php
new file mode 100644
index 00000000..dfbfc2f7
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Command/ImportCommand.php
@@ -0,0 +1,53 @@
1<?php
2
3namespace Wallabag\ImportBundle\Command;
4
5use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
6use Symfony\Component\Config\Definition\Exception\Exception;
7use Symfony\Component\Console\Input\InputArgument;
8use Symfony\Component\Console\Input\InputInterface;
9use Symfony\Component\Console\Output\OutputInterface;
10
11class ImportCommand extends ContainerAwareCommand
12{
13 protected function configure()
14 {
15 $this
16 ->setName('wallabag:import-v1')
17 ->setDescription('Import entries from a JSON export from a wallabag v1 instance')
18 ->addArgument('userId', InputArgument::REQUIRED, 'User ID to populate')
19 ->addArgument('filepath', InputArgument::REQUIRED, 'Path to the JSON file')
20 ;
21 }
22
23 protected function execute(InputInterface $input, OutputInterface $output)
24 {
25 $output->writeln('Start : '.(new \DateTime())->format('d-m-Y G:i:s').' ---');
26
27 $em = $this->getContainer()->get('doctrine')->getManager();
28 // Turning off doctrine default logs queries for saving memory
29 $em->getConnection()->getConfiguration()->setSQLLogger(null);
30
31 $user = $em->getRepository('WallabagUserBundle:User')->findOneById($input->getArgument('userId'));
32
33 if (!is_object($user)) {
34 throw new Exception(sprintf('User with id "%s" not found', $input->getArgument('userId')));
35 }
36
37 $wallabag = $this->getContainer()->get('wallabag_import.wallabag_v1.import');
38 $res = $wallabag
39 ->setUser($user)
40 ->setFilepath($input->getArgument('filepath'))
41 ->import();
42
43 if (true === $res) {
44 $summary = $wallabag->getSummary();
45 $output->writeln('<info>'.$summary['imported'].' imported</info>');
46 $output->writeln('<comment>'.$summary['skipped'].' already saved</comment>');
47 }
48
49 $em->clear();
50
51 $output->writeln('End : '.(new \DateTime())->format('d-m-Y G:i:s').' ---');
52 }
53}
diff --git a/src/Wallabag/ImportBundle/Controller/ImportController.php b/src/Wallabag/ImportBundle/Controller/ImportController.php
new file mode 100644
index 00000000..c1486e38
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Controller/ImportController.php
@@ -0,0 +1,19 @@
1<?php
2
3namespace Wallabag\ImportBundle\Controller;
4
5use Symfony\Bundle\FrameworkBundle\Controller\Controller;
6use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
7
8class ImportController extends Controller
9{
10 /**
11 * @Route("/", name="import")
12 */
13 public function importAction()
14 {
15 return $this->render('WallabagImportBundle:Import:index.html.twig', [
16 'imports' => $this->get('wallabag_import.chain')->getAll(),
17 ]);
18 }
19}
diff --git a/src/Wallabag/ImportBundle/Controller/PocketController.php b/src/Wallabag/ImportBundle/Controller/PocketController.php
new file mode 100644
index 00000000..a0853383
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Controller/PocketController.php
@@ -0,0 +1,66 @@
1<?php
2
3namespace Wallabag\ImportBundle\Controller;
4
5use Symfony\Bundle\FrameworkBundle\Controller\Controller;
6use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
7
8class PocketController extends Controller
9{
10 /**
11 * @Route("/pocket", name="import_pocket")
12 */
13 public function indexAction()
14 {
15 return $this->render('WallabagImportBundle:Pocket:index.html.twig', [
16 'import' => $this->get('wallabag_import.pocket.import'),
17 ]);
18 }
19
20 /**
21 * @Route("/pocket/auth", name="import_pocket_auth")
22 */
23 public function authAction()
24 {
25 $requestToken = $this->get('wallabag_import.pocket.import')
26 ->getRequestToken($this->generateUrl('import', [], true));
27
28 $this->get('session')->set('import.pocket.code', $requestToken);
29
30 return $this->redirect(
31 'https://getpocket.com/auth/authorize?request_token='.$requestToken.'&redirect_uri='.$this->generateUrl('import_pocket_callback', [], true),
32 301
33 );
34 }
35
36 /**
37 * @Route("/pocket/callback", name="import_pocket_callback")
38 */
39 public function callbackAction()
40 {
41 $message = 'Import failed, please try again.';
42 $pocket = $this->get('wallabag_import.pocket.import');
43
44 // something bad happend on pocket side
45 if (false === $pocket->authorize($this->get('session')->get('import.pocket.code'))) {
46 $this->get('session')->getFlashBag()->add(
47 'notice',
48 $message
49 );
50
51 return $this->redirect($this->generateUrl('import_pocket'));
52 }
53
54 if (true === $pocket->import()) {
55 $summary = $pocket->getSummary();
56 $message = 'Import summary: '.$summary['imported'].' imported, '.$summary['skipped'].' already saved.';
57 }
58
59 $this->get('session')->getFlashBag()->add(
60 'notice',
61 $message
62 );
63
64 return $this->redirect($this->generateUrl('homepage'));
65 }
66}
diff --git a/src/Wallabag/ImportBundle/Controller/WallabagV1Controller.php b/src/Wallabag/ImportBundle/Controller/WallabagV1Controller.php
new file mode 100644
index 00000000..e50a6c35
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Controller/WallabagV1Controller.php
@@ -0,0 +1,59 @@
1<?php
2
3namespace Wallabag\ImportBundle\Controller;
4
5use Symfony\Bundle\FrameworkBundle\Controller\Controller;
6use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
7use Symfony\Component\HttpFoundation\Request;
8use Wallabag\ImportBundle\Form\Type\UploadImportType;
9
10class WallabagV1Controller extends Controller
11{
12 /**
13 * @Route("/wallabag-v1", name="import_wallabag_v1")
14 */
15 public function indexAction(Request $request)
16 {
17 $form = $this->createForm(new UploadImportType());
18 $form->handleRequest($request);
19
20 $wallabag = $this->get('wallabag_import.wallabag_v1.import');
21
22 if ($form->isValid()) {
23 $file = $form->get('file')->getData();
24 $name = $this->getUser()->getId().'.json';
25
26 if (in_array($file->getClientMimeType(), $this->getParameter('wallabag_import.allow_mimetypes')) && $file->move($this->getParameter('wallabag_import.resource_dir'), $name)) {
27 $res = $wallabag
28 ->setUser($this->getUser())
29 ->setFilepath($this->getParameter('wallabag_import.resource_dir').'/'.$name)
30 ->import();
31
32 $message = 'Import failed, please try again.';
33 if (true === $res) {
34 $summary = $wallabag->getSummary();
35 $message = 'Import summary: '.$summary['imported'].' imported, '.$summary['skipped'].' already saved.';
36
37 unlink($this->getParameter('wallabag_import.resource_dir').'/'.$name);
38 }
39
40 $this->get('session')->getFlashBag()->add(
41 'notice',
42 $message
43 );
44
45 return $this->redirect($this->generateUrl('homepage'));
46 } else {
47 $this->get('session')->getFlashBag()->add(
48 'notice',
49 'Error while processing import. Please verify your import file.'
50 );
51 }
52 }
53
54 return $this->render('WallabagImportBundle:WallabagV1:index.html.twig', [
55 'form' => $form->createView(),
56 'import' => $wallabag,
57 ]);
58 }
59}
diff --git a/src/Wallabag/ImportBundle/DependencyInjection/Configuration.php b/src/Wallabag/ImportBundle/DependencyInjection/Configuration.php
new file mode 100644
index 00000000..39df9d3f
--- /dev/null
+++ b/src/Wallabag/ImportBundle/DependencyInjection/Configuration.php
@@ -0,0 +1,27 @@
1<?php
2
3namespace Wallabag\ImportBundle\DependencyInjection;
4
5use Symfony\Component\Config\Definition\Builder\TreeBuilder;
6use Symfony\Component\Config\Definition\ConfigurationInterface;
7
8class Configuration implements ConfigurationInterface
9{
10 public function getConfigTreeBuilder()
11 {
12 $treeBuilder = new TreeBuilder();
13 $rootNode = $treeBuilder->root('wallabag_import');
14
15 $rootNode
16 ->children()
17 ->arrayNode('allow_mimetypes')
18 ->prototype('scalar')->end()
19 ->end()
20 ->scalarNode('resource_dir')
21 ->end()
22 ->end()
23 ;
24
25 return $treeBuilder;
26 }
27}
diff --git a/src/Wallabag/ImportBundle/DependencyInjection/WallabagImportExtension.php b/src/Wallabag/ImportBundle/DependencyInjection/WallabagImportExtension.php
new file mode 100644
index 00000000..3f23c36b
--- /dev/null
+++ b/src/Wallabag/ImportBundle/DependencyInjection/WallabagImportExtension.php
@@ -0,0 +1,27 @@
1<?php
2
3namespace Wallabag\ImportBundle\DependencyInjection;
4
5use Symfony\Component\DependencyInjection\ContainerBuilder;
6use Symfony\Component\Config\FileLocator;
7use Symfony\Component\HttpKernel\DependencyInjection\Extension;
8use Symfony\Component\DependencyInjection\Loader;
9
10class WallabagImportExtension extends Extension
11{
12 public function load(array $configs, ContainerBuilder $container)
13 {
14 $configuration = new Configuration();
15 $config = $this->processConfiguration($configuration, $configs);
16 $container->setParameter('wallabag_import.allow_mimetypes', $config['allow_mimetypes']);
17 $container->setParameter('wallabag_import.resource_dir', $config['resource_dir']);
18
19 $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
20 $loader->load('services.yml');
21 }
22
23 public function getAlias()
24 {
25 return 'wallabag_import';
26 }
27}
diff --git a/src/Wallabag/ImportBundle/Form/Type/UploadImportType.php b/src/Wallabag/ImportBundle/Form/Type/UploadImportType.php
new file mode 100644
index 00000000..415890f3
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Form/Type/UploadImportType.php
@@ -0,0 +1,22 @@
1<?php
2
3namespace Wallabag\ImportBundle\Form\Type;
4
5use Symfony\Component\Form\AbstractType;
6use Symfony\Component\Form\FormBuilderInterface;
7
8class UploadImportType extends AbstractType
9{
10 public function buildForm(FormBuilderInterface $builder, array $options)
11 {
12 $builder
13 ->add('file', 'file')
14 ->add('save', 'submit')
15 ;
16 }
17
18 public function getName()
19 {
20 return 'upload_import_file';
21 }
22}
diff --git a/src/Wallabag/ImportBundle/Import/ImportChain.php b/src/Wallabag/ImportBundle/Import/ImportChain.php
new file mode 100644
index 00000000..9dd77956
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Import/ImportChain.php
@@ -0,0 +1,34 @@
1<?php
2
3namespace Wallabag\ImportBundle\Import;
4
5class ImportChain
6{
7 private $imports;
8
9 public function __construct()
10 {
11 $this->imports = [];
12 }
13
14 /**
15 * Add an import to the chain.
16 *
17 * @param ImportInterface $import
18 * @param string $alias
19 */
20 public function addImport(ImportInterface $import, $alias)
21 {
22 $this->imports[$alias] = $import;
23 }
24
25 /**
26 * Get all imports.
27 *
28 * @return array<ImportInterface>
29 */
30 public function getAll()
31 {
32 return $this->imports;
33 }
34}
diff --git a/src/Wallabag/ImportBundle/Import/ImportCompilerPass.php b/src/Wallabag/ImportBundle/Import/ImportCompilerPass.php
new file mode 100644
index 00000000..a363a566
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Import/ImportCompilerPass.php
@@ -0,0 +1,33 @@
1<?php
2
3namespace Wallabag\ImportBundle\Import;
4
5use Symfony\Component\DependencyInjection\ContainerBuilder;
6use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
7use Symfony\Component\DependencyInjection\Reference;
8
9class ImportCompilerPass implements CompilerPassInterface
10{
11 public function process(ContainerBuilder $container)
12 {
13 if (!$container->hasDefinition('wallabag_import.chain')) {
14 return;
15 }
16
17 $definition = $container->getDefinition(
18 'wallabag_import.chain'
19 );
20
21 $taggedServices = $container->findTaggedServiceIds(
22 'wallabag_import.import'
23 );
24 foreach ($taggedServices as $id => $tagAttributes) {
25 foreach ($tagAttributes as $attributes) {
26 $definition->addMethodCall(
27 'addImport',
28 [new Reference($id), $attributes['alias']]
29 );
30 }
31 }
32 }
33}
diff --git a/src/Wallabag/ImportBundle/Import/ImportInterface.php b/src/Wallabag/ImportBundle/Import/ImportInterface.php
new file mode 100644
index 00000000..25dc0d85
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Import/ImportInterface.php
@@ -0,0 +1,45 @@
1<?php
2
3namespace Wallabag\ImportBundle\Import;
4
5use Psr\Log\LoggerAwareInterface;
6
7interface ImportInterface extends LoggerAwareInterface
8{
9 /**
10 * Name of the import.
11 *
12 * @return string
13 */
14 public function getName();
15
16 /**
17 * Url to start the import.
18 *
19 * @return string
20 */
21 public function getUrl();
22
23 /**
24 * Description of the import.
25 *
26 * @return string
27 */
28 public function getDescription();
29
30 /**
31 * Import content using the user token.
32 *
33 * @return bool
34 */
35 public function import();
36
37 /**
38 * Return an array with summary info about the import, with keys:
39 * - skipped
40 * - imported.
41 *
42 * @return array
43 */
44 public function getSummary();
45}
diff --git a/src/Wallabag/ImportBundle/Import/PocketImport.php b/src/Wallabag/ImportBundle/Import/PocketImport.php
new file mode 100644
index 00000000..cdcec1e2
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Import/PocketImport.php
@@ -0,0 +1,267 @@
1<?php
2
3namespace Wallabag\ImportBundle\Import;
4
5use Psr\Log\LoggerInterface;
6use Psr\Log\NullLogger;
7use Doctrine\ORM\EntityManager;
8use GuzzleHttp\Client;
9use GuzzleHttp\Exception\RequestException;
10use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
11use Wallabag\CoreBundle\Entity\Entry;
12use Wallabag\CoreBundle\Entity\Tag;
13use Wallabag\CoreBundle\Helper\ContentProxy;
14
15class PocketImport implements ImportInterface
16{
17 private $user;
18 private $em;
19 private $contentProxy;
20 private $logger;
21 private $client;
22 private $consumerKey;
23 private $skippedEntries = 0;
24 private $importedEntries = 0;
25 protected $accessToken;
26 private $translator;
27
28 public function __construct(TokenStorageInterface $tokenStorage, EntityManager $em, ContentProxy $contentProxy, $consumerKey)
29 {
30 $this->user = $tokenStorage->getToken()->getUser();
31 $this->em = $em;
32 $this->contentProxy = $contentProxy;
33 $this->consumerKey = $consumerKey;
34 $this->logger = new NullLogger();
35 }
36
37 public function setLogger(LoggerInterface $logger)
38 {
39 $this->logger = $logger;
40 }
41
42 /**
43 * {@inheritdoc}
44 */
45 public function getName()
46 {
47 return 'Pocket';
48 }
49
50 /**
51 * {@inheritdoc}
52 */
53 public function getUrl()
54 {
55 return 'import_pocket';
56 }
57
58 /**
59 * {@inheritdoc}
60 */
61 public function getDescription()
62 {
63 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.';
64 }
65
66 /**
67 * Return the oauth url to authenticate the client.
68 *
69 * @param string $redirectUri Redirect url in case of error
70 *
71 * @return string request_token for callback method
72 */
73 public function getRequestToken($redirectUri)
74 {
75 $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/oauth/request',
76 [
77 'body' => json_encode([
78 'consumer_key' => $this->consumerKey,
79 'redirect_uri' => $redirectUri,
80 ]),
81 ]
82 );
83
84 try {
85 $response = $this->client->send($request);
86 } catch (RequestException $e) {
87 $this->logger->error(sprintf('PocketImport: Failed to request token: %s', $e->getMessage()), ['exception' => $e]);
88
89 return false;
90 }
91
92 return $response->json()['code'];
93 }
94
95 /**
96 * Usually called by the previous callback to authorize the client.
97 * Then it return a token that can be used for next requests.
98 *
99 * @param string $code request_token from getRequestToken
100 *
101 * @return bool
102 */
103 public function authorize($code)
104 {
105 $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/oauth/authorize',
106 [
107 'body' => json_encode([
108 'consumer_key' => $this->consumerKey,
109 'code' => $code,
110 ]),
111 ]
112 );
113
114 try {
115 $response = $this->client->send($request);
116 } catch (RequestException $e) {
117 $this->logger->error(sprintf('PocketImport: Failed to authorize client: %s', $e->getMessage()), ['exception' => $e]);
118
119 return false;
120 }
121
122 $this->accessToken = $response->json()['access_token'];
123
124 return true;
125 }
126
127 /**
128 * {@inheritdoc}
129 */
130 public function import()
131 {
132 $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/get',
133 [
134 'body' => json_encode([
135 'consumer_key' => $this->consumerKey,
136 'access_token' => $this->accessToken,
137 'detailType' => 'complete',
138 'state' => 'all',
139 'sort' => 'oldest',
140 ]),
141 ]
142 );
143
144 try {
145 $response = $this->client->send($request);
146 } catch (RequestException $e) {
147 $this->logger->error(sprintf('PocketImport: Failed to import: %s', $e->getMessage()), ['exception' => $e]);
148
149 return false;
150 }
151
152 $entries = $response->json();
153
154 $this->parseEntries($entries['list']);
155
156 return true;
157 }
158
159 /**
160 * {@inheritdoc}
161 */
162 public function getSummary()
163 {
164 return [
165 'skipped' => $this->skippedEntries,
166 'imported' => $this->importedEntries,
167 ];
168 }
169
170 /**
171 * Set the Guzzle client.
172 *
173 * @param Client $client
174 */
175 public function setClient(Client $client)
176 {
177 $this->client = $client;
178 }
179
180 /**
181 * @todo move that in a more global place
182 */
183 private function assignTagsToEntry(Entry $entry, $tags)
184 {
185 foreach ($tags as $tag) {
186 $label = trim($tag['tag']);
187 $tagEntity = $this->em
188 ->getRepository('WallabagCoreBundle:Tag')
189 ->findOneByLabel($label);
190
191 if (is_object($tagEntity)) {
192 $entry->addTag($tagEntity);
193 } else {
194 $newTag = new Tag();
195 $newTag->setLabel($label);
196
197 $entry->addTag($newTag);
198 }
199 $this->em->flush();
200 }
201 }
202
203 /**
204 * @see https://getpocket.com/developer/docs/v3/retrieve
205 *
206 * @param $entries
207 */
208 private function parseEntries($entries)
209 {
210 $i = 1;
211
212 foreach ($entries as $pocketEntry) {
213 $url = isset($pocketEntry['resolved_url']) && $pocketEntry['resolved_url'] != '' ? $pocketEntry['resolved_url'] : $pocketEntry['given_url'];
214
215 $existingEntry = $this->em
216 ->getRepository('WallabagCoreBundle:Entry')
217 ->existByUrlAndUserId($url, $this->user->getId());
218
219 if (false !== $existingEntry) {
220 ++$this->skippedEntries;
221 continue;
222 }
223
224 $entry = new Entry($this->user);
225 $entry = $this->contentProxy->updateEntry($entry, $url);
226
227 // 0, 1, 2 - 1 if the item is archived - 2 if the item should be deleted
228 if ($pocketEntry['status'] == 1) {
229 $entry->setArchived(true);
230 }
231
232 // 0 or 1 - 1 If the item is favorited
233 if ($pocketEntry['favorite'] == 1) {
234 $entry->setStarred(true);
235 }
236
237 $title = 'Untitled';
238 if (isset($pocketEntry['resolved_title']) && $pocketEntry['resolved_title'] != '') {
239 $title = $pocketEntry['resolved_title'];
240 } elseif (isset($pocketEntry['given_title']) && $pocketEntry['given_title'] != '') {
241 $title = $pocketEntry['given_title'];
242 }
243
244 $entry->setTitle($title);
245
246 // 0, 1, or 2 - 1 if the item has images in it - 2 if the item is an image
247 if (isset($pocketEntry['has_image']) && $pocketEntry['has_image'] > 0 && isset($pocketEntry['images'][1])) {
248 $entry->setPreviewPicture($pocketEntry['images'][1]['src']);
249 }
250
251 if (isset($pocketEntry['tags']) && !empty($pocketEntry['tags'])) {
252 $this->assignTagsToEntry($entry, $pocketEntry['tags']);
253 }
254
255 $this->em->persist($entry);
256 ++$this->importedEntries;
257
258 // flush every 20 entries
259 if (($i % 20) === 0) {
260 $this->em->flush();
261 }
262 ++$i;
263 }
264
265 $this->em->flush();
266 }
267}
diff --git a/src/Wallabag/ImportBundle/Import/WallabagV1Import.php b/src/Wallabag/ImportBundle/Import/WallabagV1Import.php
new file mode 100644
index 00000000..393089d6
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Import/WallabagV1Import.php
@@ -0,0 +1,159 @@
1<?php
2
3namespace Wallabag\ImportBundle\Import;
4
5use Psr\Log\LoggerInterface;
6use Psr\Log\NullLogger;
7use Doctrine\ORM\EntityManager;
8use Wallabag\CoreBundle\Entity\Entry;
9use Wallabag\UserBundle\Entity\User;
10use Wallabag\CoreBundle\Tools\Utils;
11
12class WallabagV1Import implements ImportInterface
13{
14 private $user;
15 private $em;
16 private $logger;
17 private $skippedEntries = 0;
18 private $importedEntries = 0;
19 private $filepath;
20
21 public function __construct(EntityManager $em)
22 {
23 $this->em = $em;
24 $this->logger = new NullLogger();
25 }
26
27 public function setLogger(LoggerInterface $logger)
28 {
29 $this->logger = $logger;
30 }
31
32 /**
33 * We define the user in a custom call because on the import command there is no logged in user.
34 * So we can't retrieve user from the `security.token_storage` service.
35 *
36 * @param User $user
37 */
38 public function setUser(User $user)
39 {
40 $this->user = $user;
41
42 return $this;
43 }
44
45 /**
46 * {@inheritdoc}
47 */
48 public function getName()
49 {
50 return 'wallabag v1';
51 }
52
53 /**
54 * {@inheritdoc}
55 */
56 public function getUrl()
57 {
58 return 'import_wallabag_v1';
59 }
60
61 /**
62 * {@inheritdoc}
63 */
64 public function getDescription()
65 {
66 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.';
67 }
68
69 /**
70 * {@inheritdoc}
71 */
72 public function import()
73 {
74 if (!$this->user) {
75 $this->logger->error('WallabagV1Import: user is not defined');
76
77 return false;
78 }
79
80 if (!file_exists($this->filepath) || !is_readable($this->filepath)) {
81 $this->logger->error('WallabagV1Import: unable to read file', array('filepath' => $this->filepath));
82
83 return false;
84 }
85
86 $data = json_decode(file_get_contents($this->filepath), true);
87
88 if (empty($data)) {
89 return false;
90 }
91
92 $this->parseEntries($data);
93
94 return true;
95 }
96
97 /**
98 * {@inheritdoc}
99 */
100 public function getSummary()
101 {
102 return [
103 'skipped' => $this->skippedEntries,
104 'imported' => $this->importedEntries,
105 ];
106 }
107
108 /**
109 * Set file path to the json file.
110 *
111 * @param string $filepath
112 */
113 public function setFilepath($filepath)
114 {
115 $this->filepath = $filepath;
116
117 return $this;
118 }
119
120 /**
121 * @param $entries
122 */
123 private function parseEntries($entries)
124 {
125 $i = 1;
126
127 foreach ($entries as $importedEntry) {
128 $existingEntry = $this->em
129 ->getRepository('WallabagCoreBundle:Entry')
130 ->existByUrlAndUserId($importedEntry['url'], $this->user->getId());
131
132 if (false !== $existingEntry) {
133 ++$this->skippedEntries;
134 continue;
135 }
136
137 // @see ContentProxy->updateEntry
138 $entry = new Entry($this->user);
139 $entry->setUrl($importedEntry['url']);
140 $entry->setTitle($importedEntry['title']);
141 $entry->setArchived($importedEntry['is_read']);
142 $entry->setStarred($importedEntry['is_fav']);
143 $entry->setContent($importedEntry['content']);
144 $entry->setReadingTime(Utils::getReadingTime($importedEntry['content']));
145 $entry->setDomainName(parse_url($importedEntry['url'], PHP_URL_HOST));
146
147 $this->em->persist($entry);
148 ++$this->importedEntries;
149
150 // flush every 20 entries
151 if (($i % 20) === 0) {
152 $this->em->flush();
153 }
154 ++$i;
155 }
156
157 $this->em->flush();
158 }
159}
diff --git a/src/Wallabag/ImportBundle/Resources/config/services.yml b/src/Wallabag/ImportBundle/Resources/config/services.yml
new file mode 100644
index 00000000..e4dde100
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Resources/config/services.yml
@@ -0,0 +1,34 @@
1services:
2 wallabag_import.chain:
3 class: Wallabag\ImportBundle\Import\ImportChain
4
5 wallabag_import.pocket.client:
6 class: GuzzleHttp\Client
7 arguments:
8 -
9 defaults:
10 headers:
11 content-type: "application/json"
12 X-Accept: "application/json"
13
14 wallabag_import.pocket.import:
15 class: Wallabag\ImportBundle\Import\PocketImport
16 arguments:
17 - "@security.token_storage"
18 - "@doctrine.orm.entity_manager"
19 - "@wallabag_core.content_proxy"
20 - %pocket_consumer_key%
21 calls:
22 - [ setClient, [ "@wallabag_import.pocket.client" ] ]
23 - [ setLogger, [ "@logger" ]]
24 tags:
25 - { name: wallabag_import.import, alias: pocket }
26
27 wallabag_import.wallabag_v1.import:
28 class: Wallabag\ImportBundle\Import\WallabagV1Import
29 arguments:
30 - "@doctrine.orm.entity_manager"
31 calls:
32 - [ setLogger, [ "@logger" ]]
33 tags:
34 - { 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
index 00000000..303e6cbf
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Resources/views/Import/index.html.twig
@@ -0,0 +1,21 @@
1{% extends "WallabagCoreBundle::layout.html.twig" %}
2{% block title %}{% trans %}Import{% endtrans %}{% endblock %}
3
4{% block content %}
5<div class="row">
6 <div class="col s12">
7 <div class="card-panel settings">
8 {% trans %}Welcome on wallabag importer. Please select your previous service that you want to migrate.{% endtrans %}
9 <ul>
10 {% for import in imports %}
11 <li>
12 <h5>{{ import.name }}</h5>
13 <blockquote>{{ import.description|trans }}</blockquote>
14 <p><a class="waves-effect waves-light btn" href="{{ path(import.url) }}">{% trans %}Import contents{% endtrans %}</a></p>
15 </li>
16 {% endfor %}
17 </ul>
18 </div>
19 </div>
20</div>
21{% 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
index 00000000..643ad775
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Resources/views/Pocket/index.html.twig
@@ -0,0 +1,18 @@
1{% extends "WallabagCoreBundle::layout.html.twig" %}
2{% block title %}{% trans %}Import > Pocket{% endtrans %}{% endblock %}
3
4{% block content %}
5<div class="row">
6 <div class="col s12">
7 <div class="card-panel settings">
8 <blockquote>{{ import.description|trans }}</blockquote>
9 <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>
10 <form method="post" action="{{ path('import_pocket_auth') }}">
11 <button class="btn waves-effect waves-light" type="submit" name="action">
12 {% trans %}Connect to Pocket and import data{% endtrans %}
13 </button>
14 </form>
15 </div>
16 </div>
17</div>
18{% 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
index 00000000..1359f2e4
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Resources/views/WallabagV1/index.html.twig
@@ -0,0 +1,36 @@
1{% extends "WallabagCoreBundle::layout.html.twig" %}
2{% block title %}{% trans %}Import > Wallabag v1{% endtrans %}{% endblock %}
3
4{% block content %}
5<div class="row">
6 <div class="col s12">
7 <div class="card-panel settings">
8 <div class="row">
9 <blockquote>{{ import.description|trans }}</blockquote>
10 <p>{% trans %}Please select your wallabag export and click on the below button to upload and import it.{% endtrans %}</p>
11 <div class="col s12">
12 {{ form_start(form, {'method': 'POST'}) }}
13 {{ form_errors(form) }}
14 <div class="row">
15 <div class="file-field input-field col s12">
16 {{ form_errors(form.file) }}
17 <div class="btn">
18 <span>{% trans %}File{% endtrans %}</span>
19 {{ form_widget(form.file) }}
20 </div>
21 <div class="file-path-wrapper">
22 <input class="file-path validate" type="text">
23 </div>
24 </div>
25 </div>
26 <div class="hidden">{{ form_rest(form) }}</div>
27 <button class="btn waves-effect waves-light" type="submit" name="action">
28 {% trans %}Upload file{% endtrans %}
29 </button>
30 </form>
31 </div>
32 </div>
33 </div>
34 </div>
35</div>
36{% endblock %}
diff --git a/src/Wallabag/ImportBundle/Tests/Controller/ImportControllerTest.php b/src/Wallabag/ImportBundle/Tests/Controller/ImportControllerTest.php
new file mode 100644
index 00000000..30009af4
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Tests/Controller/ImportControllerTest.php
@@ -0,0 +1,29 @@
1<?php
2
3namespace Wallabag\ImportBundle\Tests\Controller;
4
5use Wallabag\CoreBundle\Tests\WallabagCoreTestCase;
6
7class ImportControllerTest extends WallabagCoreTestCase
8{
9 public function testLogin()
10 {
11 $client = $this->getClient();
12
13 $client->request('GET', '/import/');
14
15 $this->assertEquals(302, $client->getResponse()->getStatusCode());
16 $this->assertContains('login', $client->getResponse()->headers->get('location'));
17 }
18
19 public function testImportList()
20 {
21 $this->logInAs('admin');
22 $client = $this->getClient();
23
24 $crawler = $client->request('GET', '/import/');
25
26 $this->assertEquals(200, $client->getResponse()->getStatusCode());
27 $this->assertEquals(2, $crawler->filter('blockquote')->count());
28 }
29}
diff --git a/src/Wallabag/ImportBundle/Tests/Controller/PocketControllerTest.php b/src/Wallabag/ImportBundle/Tests/Controller/PocketControllerTest.php
new file mode 100644
index 00000000..c2acd68c
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Tests/Controller/PocketControllerTest.php
@@ -0,0 +1,42 @@
1<?php
2
3namespace Wallabag\ImportBundle\Tests\Controller;
4
5use Wallabag\CoreBundle\Tests\WallabagCoreTestCase;
6
7class PocketControllerTest extends WallabagCoreTestCase
8{
9 public function testImportPocket()
10 {
11 $this->logInAs('admin');
12 $client = $this->getClient();
13
14 $crawler = $client->request('GET', '/import/pocket');
15
16 $this->assertEquals(200, $client->getResponse()->getStatusCode());
17 $this->assertEquals(1, $crawler->filter('button[type=submit]')->count());
18 }
19
20 public function testImportPocketAuth()
21 {
22 $this->logInAs('admin');
23 $client = $this->getClient();
24
25 $crawler = $client->request('GET', '/import/pocket/auth');
26
27 $this->assertEquals(301, $client->getResponse()->getStatusCode());
28 $this->assertContains('getpocket.com/auth/authorize', $client->getResponse()->headers->get('location'));
29 }
30
31 public function testImportPocketCallbackWithBadToken()
32 {
33 $this->logInAs('admin');
34 $client = $this->getClient();
35
36 $crawler = $client->request('GET', '/import/pocket/callback');
37
38 $this->assertEquals(302, $client->getResponse()->getStatusCode());
39 $this->assertContains('import/pocket', $client->getResponse()->headers->get('location'));
40 $this->assertEquals('Import failed, please try again.', $client->getContainer()->get('session')->getFlashBag()->peek('notice')[0]);
41 }
42}
diff --git a/src/Wallabag/ImportBundle/Tests/Controller/WallabagV1ControllerTest.php b/src/Wallabag/ImportBundle/Tests/Controller/WallabagV1ControllerTest.php
new file mode 100644
index 00000000..e12ea429
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Tests/Controller/WallabagV1ControllerTest.php
@@ -0,0 +1,69 @@
1<?php
2
3namespace Wallabag\ImportBundle\Tests\Controller;
4
5use Wallabag\CoreBundle\Tests\WallabagCoreTestCase;
6use Symfony\Component\HttpFoundation\File\UploadedFile;
7
8class WallabagV1ControllerTest extends WallabagCoreTestCase
9{
10 public function testImportWallabag()
11 {
12 $this->logInAs('admin');
13 $client = $this->getClient();
14
15 $crawler = $client->request('GET', '/import/wallabag-v1');
16
17 $this->assertEquals(200, $client->getResponse()->getStatusCode());
18 $this->assertEquals(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count());
19 $this->assertEquals(1, $crawler->filter('input[type=file]')->count());
20 }
21
22 public function testImportWallabagWithFile()
23 {
24 $this->logInAs('admin');
25 $client = $this->getClient();
26
27 $crawler = $client->request('GET', '/import/wallabag-v1');
28 $form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
29
30 $file = new UploadedFile(__DIR__.'/../fixtures/wallabag-v1.json', 'wallabag-v1.json');
31
32 $data = array(
33 'upload_import_file[file]' => $file,
34 );
35
36 $client->submit($form, $data);
37
38 $this->assertEquals(302, $client->getResponse()->getStatusCode());
39
40 $crawler = $client->followRedirect();
41
42 $this->assertGreaterThan(1, $alert = $crawler->filter('div.messages.success')->extract(array('_text')));
43 $this->assertContains('Import summary', $alert[0]);
44 }
45
46 public function testImportWallabagWithEmptyFile()
47 {
48 $this->logInAs('admin');
49 $client = $this->getClient();
50
51 $crawler = $client->request('GET', '/import/wallabag-v1');
52 $form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
53
54 $file = new UploadedFile(__DIR__.'/../fixtures/test.txt', 'test.txt');
55
56 $data = array(
57 'upload_import_file[file]' => $file,
58 );
59
60 $client->submit($form, $data);
61
62 $this->assertEquals(302, $client->getResponse()->getStatusCode());
63
64 $crawler = $client->followRedirect();
65
66 $this->assertGreaterThan(1, $alert = $crawler->filter('div.messages.success')->extract(array('_text')));
67 $this->assertContains('Import failed, please try again', $alert[0]);
68 }
69}
diff --git a/src/Wallabag/ImportBundle/Tests/Import/ImportChainTest.php b/src/Wallabag/ImportBundle/Tests/Import/ImportChainTest.php
new file mode 100644
index 00000000..702d2a9b
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Tests/Import/ImportChainTest.php
@@ -0,0 +1,21 @@
1<?php
2
3namespace Wallabag\ImportBundle\Tests\Import;
4
5use Wallabag\ImportBundle\Import\ImportChain;
6
7class ImportChainTest extends \PHPUnit_Framework_TestCase
8{
9 public function testGetAll()
10 {
11 $import = $this->getMockBuilder('Wallabag\ImportBundle\Import\ImportInterface')
12 ->disableOriginalConstructor()
13 ->getMock();
14
15 $importChain = new ImportChain();
16 $importChain->addImport($import, 'alias');
17
18 $this->assertCount(1, $importChain->getAll());
19 $this->assertEquals($import, $importChain->getAll()['alias']);
20 }
21}
diff --git a/src/Wallabag/ImportBundle/Tests/Import/ImportCompilerPassTest.php b/src/Wallabag/ImportBundle/Tests/Import/ImportCompilerPassTest.php
new file mode 100644
index 00000000..bd62ab3b
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Tests/Import/ImportCompilerPassTest.php
@@ -0,0 +1,47 @@
1<?php
2
3namespace Wallabag\ImportBundle\Tests\Import;
4
5use Symfony\Component\DependencyInjection\ContainerBuilder;
6use Wallabag\ImportBundle\Import\ImportCompilerPass;
7
8class ImportCompilerPassTest extends \PHPUnit_Framework_TestCase
9{
10 public function testProcessNoDefinition()
11 {
12 $container = new ContainerBuilder();
13 $res = $this->process($container);
14
15 $this->assertNull($res);
16 }
17
18 public function testProcess()
19 {
20 $container = new ContainerBuilder();
21 $container
22 ->register('wallabag_import.chain')
23 ->setPublic(false)
24 ;
25
26 $container
27 ->register('foo')
28 ->addTag('wallabag_import.import', array('alias' => 'pocket'))
29 ;
30
31 $this->process($container);
32
33 $this->assertTrue($container->hasDefinition('wallabag_import.chain'));
34
35 $definition = $container->getDefinition('wallabag_import.chain');
36 $this->assertTrue($definition->hasMethodCall('addImport'));
37
38 $calls = $definition->getMethodCalls();
39 $this->assertEquals('pocket', $calls[0][1][1]);
40 }
41
42 protected function process(ContainerBuilder $container)
43 {
44 $repeatedPass = new ImportCompilerPass();
45 $repeatedPass->process($container);
46 }
47}
diff --git a/src/Wallabag/ImportBundle/Tests/Import/PocketImportTest.php b/src/Wallabag/ImportBundle/Tests/Import/PocketImportTest.php
new file mode 100644
index 00000000..043b2114
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Tests/Import/PocketImportTest.php
@@ -0,0 +1,314 @@
1<?php
2
3namespace Wallabag\ImportBundle\Tests\Import;
4
5use Wallabag\UserBundle\Entity\User;
6use Wallabag\ImportBundle\Import\PocketImport;
7use GuzzleHttp\Client;
8use GuzzleHttp\Subscriber\Mock;
9use GuzzleHttp\Message\Response;
10use GuzzleHttp\Stream\Stream;
11use Monolog\Logger;
12use Monolog\Handler\TestHandler;
13
14class PocketImportMock extends PocketImport
15{
16 public function getAccessToken()
17 {
18 return $this->accessToken;
19 }
20}
21
22class PocketImportTest extends \PHPUnit_Framework_TestCase
23{
24 protected $token;
25 protected $user;
26 protected $em;
27 protected $contentProxy;
28 protected $logHandler;
29
30 private function getPocketImport($consumerKey = 'ConsumerKey')
31 {
32 $this->user = new User();
33
34 $this->tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')
35 ->disableOriginalConstructor()
36 ->getMock();
37
38 $token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')
39 ->disableOriginalConstructor()
40 ->getMock();
41
42 $this->contentProxy = $this->getMockBuilder('Wallabag\CoreBundle\Helper\ContentProxy')
43 ->disableOriginalConstructor()
44 ->getMock();
45
46 $token->expects($this->once())
47 ->method('getUser')
48 ->willReturn($this->user);
49
50 $this->tokenStorage->expects($this->once())
51 ->method('getToken')
52 ->willReturn($token);
53
54 $this->em = $this->getMockBuilder('Doctrine\ORM\EntityManager')
55 ->disableOriginalConstructor()
56 ->getMock();
57
58 $pocket = new PocketImportMock(
59 $this->tokenStorage,
60 $this->em,
61 $this->contentProxy,
62 $consumerKey
63 );
64
65 $this->logHandler = new TestHandler();
66 $logger = new Logger('test', array($this->logHandler));
67 $pocket->setLogger($logger);
68
69 return $pocket;
70 }
71
72 public function testInit()
73 {
74 $pocketImport = $this->getPocketImport();
75
76 $this->assertEquals('Pocket', $pocketImport->getName());
77 $this->assertNotEmpty($pocketImport->getUrl());
78 $this->assertContains('This importer will import all your Pocket data.', $pocketImport->getDescription());
79 }
80
81 public function testOAuthRequest()
82 {
83 $client = new Client();
84
85 $mock = new Mock([
86 new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['code' => 'wunderbar_code']))),
87 ]);
88
89 $client->getEmitter()->attach($mock);
90
91 $pocketImport = $this->getPocketImport();
92 $pocketImport->setClient($client);
93
94 $code = $pocketImport->getRequestToken('http://0.0.0.0/redirect');
95
96 $this->assertEquals('wunderbar_code', $code);
97 }
98
99 public function testOAuthRequestBadResponse()
100 {
101 $client = new Client();
102
103 $mock = new Mock([
104 new Response(403),
105 ]);
106
107 $client->getEmitter()->attach($mock);
108
109 $pocketImport = $this->getPocketImport();
110 $pocketImport->setClient($client);
111
112 $code = $pocketImport->getRequestToken('http://0.0.0.0/redirect');
113
114 $this->assertFalse($code);
115
116 $records = $this->logHandler->getRecords();
117 $this->assertContains('PocketImport: Failed to request token', $records[0]['message']);
118 $this->assertEquals('ERROR', $records[0]['level_name']);
119 }
120
121 public function testOAuthAuthorize()
122 {
123 $client = new Client();
124
125 $mock = new Mock([
126 new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))),
127 ]);
128
129 $client->getEmitter()->attach($mock);
130
131 $pocketImport = $this->getPocketImport();
132 $pocketImport->setClient($client);
133
134 $res = $pocketImport->authorize('wunderbar_code');
135
136 $this->assertTrue($res);
137 $this->assertEquals('wunderbar_token', $pocketImport->getAccessToken());
138 }
139
140 public function testOAuthAuthorizeBadResponse()
141 {
142 $client = new Client();
143
144 $mock = new Mock([
145 new Response(403),
146 ]);
147
148 $client->getEmitter()->attach($mock);
149
150 $pocketImport = $this->getPocketImport();
151 $pocketImport->setClient($client);
152
153 $res = $pocketImport->authorize('wunderbar_code');
154
155 $this->assertFalse($res);
156
157 $records = $this->logHandler->getRecords();
158 $this->assertContains('PocketImport: Failed to authorize client', $records[0]['message']);
159 $this->assertEquals('ERROR', $records[0]['level_name']);
160 }
161
162 /**
163 * Will sample results from https://getpocket.com/developer/docs/v3/retrieve.
164 */
165 public function testImport()
166 {
167 $client = new Client();
168
169 $mock = new Mock([
170 new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))),
171 new Response(200, ['Content-Type' => 'application/json'], Stream::factory('
172 {
173 "status": 1,
174 "list": {
175 "229279689": {
176 "item_id": "229279689",
177 "resolved_id": "229279689",
178 "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
179 "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland",
180 "favorite": "1",
181 "status": "1",
182 "resolved_title": "The Massive Ryder Cup Preview",
183 "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
184 "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.",
185 "is_article": "1",
186 "has_video": "1",
187 "has_image": "1",
188 "word_count": "3197",
189 "images": {
190 "1": {
191 "item_id": "229279689",
192 "image_id": "1",
193 "src": "http://a.espncdn.com/combiner/i?img=/photo/2012/0927/grant_g_ryder_cr_640.jpg&w=640&h=360",
194 "width": "0",
195 "height": "0",
196 "credit": "Jamie Squire/Getty Images",
197 "caption": ""
198 }
199 },
200 "videos": {
201 "1": {
202 "item_id": "229279689",
203 "video_id": "1",
204 "src": "http://www.youtube.com/v/Er34PbFkVGk?version=3&hl=en_US&rel=0",
205 "width": "420",
206 "height": "315",
207 "type": "1",
208 "vid": "Er34PbFkVGk"
209 }
210 },
211 "tags": {
212 "grantland": {
213 "item_id": "1147652870",
214 "tag": "grantland"
215 },
216 "Ryder Cup": {
217 "item_id": "1147652870",
218 "tag": "Ryder Cup"
219 }
220 }
221 },
222 "229279690": {
223 "item_id": "229279689",
224 "resolved_id": "229279689",
225 "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
226 "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland",
227 "favorite": "1",
228 "status": "1",
229 "resolved_title": "The Massive Ryder Cup Preview",
230 "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
231 "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.",
232 "is_article": "1",
233 "has_video": "0",
234 "has_image": "0",
235 "word_count": "3197"
236 }
237 }
238 }
239 ')),
240 ]);
241
242 $client->getEmitter()->attach($mock);
243
244 $pocketImport = $this->getPocketImport();
245
246 $entryRepo = $this->getMockBuilder('Wallabag\CoreBundle\Repository\EntryRepository')
247 ->disableOriginalConstructor()
248 ->getMock();
249
250 $entryRepo->expects($this->exactly(2))
251 ->method('existByUrlAndUserId')
252 ->will($this->onConsecutiveCalls(false, true));
253
254 $tag = $this->getMockBuilder('Wallabag\CoreBundle\Entity\Tag')
255 ->disableOriginalConstructor()
256 ->getMock();
257
258 $tagRepo = $this->getMockBuilder('Wallabag\CoreBundle\Repository\TagRepository')
259 ->disableOriginalConstructor()
260 ->getMock();
261
262 $tagRepo->expects($this->exactly(2))
263 // the method `findOneByLabel` doesn't exist, EntityRepository will then call `_call` method
264 // to magically call the `findOneBy` with ['label' => 'foo']
265 ->method('__call')
266 ->will($this->onConsecutiveCalls(false, $tag));
267
268 $this->em
269 ->expects($this->any())
270 ->method('getRepository')
271 ->will($this->onConsecutiveCalls($entryRepo, $tagRepo, $tagRepo, $entryRepo));
272
273 $entry = $this->getMockBuilder('Wallabag\CoreBundle\Entity\Entry')
274 ->disableOriginalConstructor()
275 ->getMock();
276
277 $this->contentProxy
278 ->expects($this->once())
279 ->method('updateEntry')
280 ->willReturn($entry);
281
282 $pocketImport->setClient($client);
283 $pocketImport->authorize('wunderbar_code');
284
285 $res = $pocketImport->import();
286
287 $this->assertTrue($res);
288 $this->assertEquals(['skipped' => 1, 'imported' => 1], $pocketImport->getSummary());
289 }
290
291 public function testImportBadResponse()
292 {
293 $client = new Client();
294
295 $mock = new Mock([
296 new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))),
297 new Response(403),
298 ]);
299
300 $client->getEmitter()->attach($mock);
301
302 $pocketImport = $this->getPocketImport();
303 $pocketImport->setClient($client);
304 $pocketImport->authorize('wunderbar_code');
305
306 $res = $pocketImport->import();
307
308 $this->assertFalse($res);
309
310 $records = $this->logHandler->getRecords();
311 $this->assertContains('PocketImport: Failed to import', $records[0]['message']);
312 $this->assertEquals('ERROR', $records[0]['level_name']);
313 }
314}
diff --git a/src/Wallabag/ImportBundle/Tests/Import/WallabagV1ImportTest.php b/src/Wallabag/ImportBundle/Tests/Import/WallabagV1ImportTest.php
new file mode 100644
index 00000000..d5b41777
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Tests/Import/WallabagV1ImportTest.php
@@ -0,0 +1,97 @@
1<?php
2
3namespace Wallabag\ImportBundle\Tests\Import;
4
5use Wallabag\UserBundle\Entity\User;
6use Wallabag\ImportBundle\Import\WallabagV1Import;
7use Monolog\Logger;
8use Monolog\Handler\TestHandler;
9
10class WallabagV1ImportTest extends \PHPUnit_Framework_TestCase
11{
12 protected $user;
13 protected $em;
14 protected $logHandler;
15
16 private function getWallabagV1Import($unsetUser = false)
17 {
18 $this->user = new User();
19
20 $this->em = $this->getMockBuilder('Doctrine\ORM\EntityManager')
21 ->disableOriginalConstructor()
22 ->getMock();
23
24 $pocket = new WallabagV1Import($this->em);
25
26 $this->logHandler = new TestHandler();
27 $logger = new Logger('test', array($this->logHandler));
28 $pocket->setLogger($logger);
29
30 if (false === $unsetUser) {
31 $pocket->setUser($this->user);
32 }
33
34 return $pocket;
35 }
36
37 public function testInit()
38 {
39 $wallabagV1Import = $this->getWallabagV1Import();
40
41 $this->assertEquals('wallabag v1', $wallabagV1Import->getName());
42 $this->assertNotEmpty($wallabagV1Import->getUrl());
43 $this->assertContains('This importer will import all your wallabag v1 articles.', $wallabagV1Import->getDescription());
44 }
45
46 public function testImport()
47 {
48 $wallabagV1Import = $this->getWallabagV1Import();
49 $wallabagV1Import->setFilepath(__DIR__.'/../fixtures/wallabag-v1.json');
50
51 $entryRepo = $this->getMockBuilder('Wallabag\CoreBundle\Repository\EntryRepository')
52 ->disableOriginalConstructor()
53 ->getMock();
54
55 $entryRepo->expects($this->exactly(3))
56 ->method('existByUrlAndUserId')
57 ->will($this->onConsecutiveCalls(false, true, false));
58
59 $this->em
60 ->expects($this->any())
61 ->method('getRepository')
62 ->willReturn($entryRepo);
63
64 $res = $wallabagV1Import->import();
65
66 $this->assertTrue($res);
67 $this->assertEquals(['skipped' => 1, 'imported' => 2], $wallabagV1Import->getSummary());
68 }
69
70 public function testImportBadFile()
71 {
72 $wallabagV1Import = $this->getWallabagV1Import();
73 $wallabagV1Import->setFilepath(__DIR__.'/../fixtures/wallabag-v1.jsonx');
74
75 $res = $wallabagV1Import->import();
76
77 $this->assertFalse($res);
78
79 $records = $this->logHandler->getRecords();
80 $this->assertContains('WallabagV1Import: unable to read file', $records[0]['message']);
81 $this->assertEquals('ERROR', $records[0]['level_name']);
82 }
83
84 public function testImportUserNotDefined()
85 {
86 $wallabagV1Import = $this->getWallabagV1Import(true);
87 $wallabagV1Import->setFilepath(__DIR__.'/../fixtures/wallabag-v1.json');
88
89 $res = $wallabagV1Import->import();
90
91 $this->assertFalse($res);
92
93 $records = $this->logHandler->getRecords();
94 $this->assertContains('WallabagV1Import: user is not defined', $records[0]['message']);
95 $this->assertEquals('ERROR', $records[0]['level_name']);
96 }
97}
diff --git a/src/Wallabag/ImportBundle/Tests/fixtures/test.html b/src/Wallabag/ImportBundle/Tests/fixtures/test.html
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Tests/fixtures/test.html
diff --git a/src/Wallabag/ImportBundle/Tests/fixtures/test.txt b/src/Wallabag/ImportBundle/Tests/fixtures/test.txt
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Tests/fixtures/test.txt
diff --git a/src/Wallabag/ImportBundle/Tests/fixtures/wallabag-v1.json b/src/Wallabag/ImportBundle/Tests/fixtures/wallabag-v1.json
new file mode 100644
index 00000000..534343f8
--- /dev/null
+++ b/src/Wallabag/ImportBundle/Tests/fixtures/wallabag-v1.json
@@ -0,0 +1,50 @@
1[
2 {
3 "0": "1",
4 "1": "Framabag, un nouveau service libre et gratuit",
5 "2": "http://www.framablog.org/index.php/post/2014/02/05/Framabag-service-libre-gratuit-interview-developpeur",
6 "3": "0",
7 "4": "0",
8 "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",
9 "6": "1",
10 "id": "1",
11 "title": "Framabag, un nouveau service libre et gratuit",
12 "url": "http://www.framablog.org/index.php/post/2014/02/05/Framabag-service-libre-gratuit-interview-developpeur",
13 "is_read": "0",
14 "is_fav": "0",
15 "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",
16 "user_id": "1"
17 },
18 {
19 "0": "2",
20 "1": "wallabag/wallabag",
21 "2": "https://github.com/wallabag/wallabag",
22 "3": "1",
23 "4": "0",
24 "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",
25 "6": "1",
26 "id": "2",
27 "title": "wallabag/wallabag",
28 "url": "https://github.com/wallabag/wallabag",
29 "is_read": "1",
30 "is_fav": "0",
31 "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",
32 "user_id": "1"
33 },
34 {
35 "0": "3",
36 "1": "a self hostable application for saving web pages | wallabag",
37 "2": "https://www.wallabag.org/",
38 "3": "1",
39 "4": "0",
40 "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",
41 "6": "1",
42 "id": "3",
43 "title": "a self hostable application for saving web pages | wallabag",
44 "url": "https://www.wallabag.org/",
45 "is_read": "1",
46 "is_fav": "0",
47 "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",
48 "user_id": "1"
49 }
50]
diff --git a/src/Wallabag/ImportBundle/WallabagImportBundle.php b/src/Wallabag/ImportBundle/WallabagImportBundle.php
new file mode 100644
index 00000000..a5ddc1b4
--- /dev/null
+++ b/src/Wallabag/ImportBundle/WallabagImportBundle.php
@@ -0,0 +1,17 @@
1<?php
2
3namespace Wallabag\ImportBundle;
4
5use Symfony\Component\HttpKernel\Bundle\Bundle;
6use Symfony\Component\DependencyInjection\ContainerBuilder;
7use Wallabag\ImportBundle\Import\ImportCompilerPass;
8
9class WallabagImportBundle extends Bundle
10{
11 public function build(ContainerBuilder $container)
12 {
13 parent::build($container);
14
15 $container->addCompilerPass(new ImportCompilerPass());
16 }
17}
diff --git a/web/uploads/import/.gitkeep b/web/uploads/import/.gitkeep
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/web/uploads/import/.gitkeep