aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--src/Wallabag/CoreBundle/Tools/Utils.php12
-rw-r--r--src/Wallabag/ImportBundle/Controller/ImportController.php48
-rw-r--r--src/Wallabag/ImportBundle/Controller/PocketController.php45
-rw-r--r--src/Wallabag/ImportBundle/Import/ImportInterface.php29
-rw-r--r--src/Wallabag/ImportBundle/Import/PocketImport.php154
-rw-r--r--src/Wallabag/ImportBundle/Resources/config/services.yml21
-rw-r--r--src/Wallabag/ImportBundle/Resources/views/Import/index.html.twig28
-rw-r--r--src/Wallabag/ImportBundle/Resources/views/Pocket/index.html.twig2
-rw-r--r--src/Wallabag/ImportBundle/Tests/Import/PocketImportTest.php228
9 files changed, 357 insertions, 210 deletions
diff --git a/src/Wallabag/CoreBundle/Tools/Utils.php b/src/Wallabag/CoreBundle/Tools/Utils.php
index b501ce65..a16baca9 100644
--- a/src/Wallabag/CoreBundle/Tools/Utils.php
+++ b/src/Wallabag/CoreBundle/Tools/Utils.php
@@ -27,16 +27,6 @@ class Utils
27 } 27 }
28 28
29 /** 29 /**
30 * @param $words
31 *
32 * @return float
33 */
34 public static function convertWordsToMinutes($words)
35 {
36 return floor($words / 200);
37 }
38
39 /**
40 * For a given text, we calculate reading time for an article 30 * For a given text, we calculate reading time for an article
41 * based on 200 words per minute. 31 * based on 200 words per minute.
42 * 32 *
@@ -46,6 +36,6 @@ class Utils
46 */ 36 */
47 public static function getReadingTime($text) 37 public static function getReadingTime($text)
48 { 38 {
49 return self::convertWordsToMinutes(str_word_count(strip_tags($text))); 39 return floor(str_word_count(strip_tags($text)) / 200);
50 } 40 }
51} 41}
diff --git a/src/Wallabag/ImportBundle/Controller/ImportController.php b/src/Wallabag/ImportBundle/Controller/ImportController.php
index 6ebd6a0a..2a0d6ab5 100644
--- a/src/Wallabag/ImportBundle/Controller/ImportController.php
+++ b/src/Wallabag/ImportBundle/Controller/ImportController.php
@@ -4,58 +4,14 @@ namespace Wallabag\ImportBundle\Controller;
4 4
5use Symfony\Bundle\FrameworkBundle\Controller\Controller; 5use Symfony\Bundle\FrameworkBundle\Controller\Controller;
6use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 6use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
7use Symfony\Component\Console\Input\ArrayInput;
8use Symfony\Component\Console\Output\NullOutput;
9use Symfony\Component\HttpFoundation\Request;
10use Wallabag\ImportBundle\Command\ImportCommand;
11use Wallabag\ImportBundle\Form\Type\UploadImportType;
12 7
13class ImportController extends Controller 8class ImportController extends Controller
14{ 9{
15 /** 10 /**
16 * @Route("/import", name="import") 11 * @Route("/import", name="import")
17 */ 12 */
18 public function importAction(Request $request) 13 public function importAction()
19 { 14 {
20 $importForm = $this->createForm(new UploadImportType()); 15 return $this->render('WallabagImportBundle:Import:index.html.twig', []);
21 $importForm->handleRequest($request);
22 $user = $this->getUser();
23
24 if ($importForm->isValid()) {
25 $file = $importForm->get('file')->getData();
26 $name = $user->getId().'.json';
27 $dir = __DIR__.'/../../../../web/uploads/import';
28
29 if (in_array($file->getMimeType(), $this->getParameter('wallabag_import.allow_mimetypes')) && $file->move($dir, $name)) {
30 $command = new ImportCommand();
31 $command->setContainer($this->container);
32 $input = new ArrayInput(array('userId' => $user->getId()));
33 $return = $command->run($input, new NullOutput());
34
35 if ($return == 0) {
36 $this->get('session')->getFlashBag()->add(
37 'notice',
38 'Import successful'
39 );
40 } else {
41 $this->get('session')->getFlashBag()->add(
42 'notice',
43 'Import failed'
44 );
45 }
46
47 return $this->redirect('/');
48 } else {
49 $this->get('session')->getFlashBag()->add(
50 'notice',
51 'Error while processing import. Please verify your import file.'
52 );
53 }
54 }
55
56 return $this->render('WallabagImportBundle:Import:index.html.twig', array(
57 'form' => array(
58 'import' => $importForm->createView(), ),
59 ));
60 } 16 }
61} 17}
diff --git a/src/Wallabag/ImportBundle/Controller/PocketController.php b/src/Wallabag/ImportBundle/Controller/PocketController.php
index 2ab062e7..61eeba43 100644
--- a/src/Wallabag/ImportBundle/Controller/PocketController.php
+++ b/src/Wallabag/ImportBundle/Controller/PocketController.php
@@ -8,35 +8,56 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
8class PocketController extends Controller 8class PocketController extends Controller
9{ 9{
10 /** 10 /**
11 * @Route("/import/pocket", name="pocket_import") 11 * @Route("/import/pocket", name="import_pocket")
12 */ 12 */
13 public function indexAction() 13 public function indexAction()
14 { 14 {
15 return $this->render('WallabagImportBundle:Pocket:index.html.twig', array()); 15 return $this->render('WallabagImportBundle:Pocket:index.html.twig', []);
16 } 16 }
17 17
18 /** 18 /**
19 * @Route("/import/pocket/auth", name="pocket_auth") 19 * @Route("/import/pocket/auth", name="import_pocket_auth")
20 */ 20 */
21 public function authAction() 21 public function authAction()
22 { 22 {
23 $pocket = $this->get('wallabag_import.pocket.import'); 23 $requestToken = $this->get('wallabag_import.pocket.import')
24 $authUrl = $pocket->oAuthRequest( 24 ->getRequestToken($this->generateUrl('import', [], true));
25 $this->generateUrl('import', array(), true), 25
26 $this->generateUrl('pocket_callback', array(), true) 26 $this->get('session')->set('import.pocket.code', $requestToken);
27 );
28 27
29 return $this->redirect($authUrl, 301); 28 return $this->redirect(
29 'https://getpocket.com/auth/authorize?request_token='.$requestToken.'&redirect_uri='.$this->generateUrl('import_pocket_callback', [], true),
30 301
31 );
30 } 32 }
31 33
32 /** 34 /**
33 * @Route("/import/pocket/callback", name="pocket_callback") 35 * @Route("/import/pocket/callback", name="import_pocket_callback")
34 */ 36 */
35 public function callbackAction() 37 public function callbackAction()
36 { 38 {
39 $message = 'Import failed, please try again.';
37 $pocket = $this->get('wallabag_import.pocket.import'); 40 $pocket = $this->get('wallabag_import.pocket.import');
38 $accessToken = $pocket->oAuthAuthorize(); 41
39 $pocket->import($accessToken); 42 // something bad happend on pocket side
43 if (false === $pocket->authorize($this->get('session')->get('import.pocket.code'))) {
44 $this->get('session')->getFlashBag()->add(
45 'notice',
46 $message
47 );
48
49 return $this->redirect($this->generateUrl('import_pocket'));
50 }
51
52 if (true === $pocket->import()) {
53 $summary = $pocket->getSummary();
54 $message = $summary['imported'].' entrie(s) imported, '.$summary['skipped'].' already saved.';
55 }
56
57 $this->get('session')->getFlashBag()->add(
58 'notice',
59 $message
60 );
40 61
41 return $this->redirect($this->generateUrl('homepage')); 62 return $this->redirect($this->generateUrl('homepage'));
42 } 63 }
diff --git a/src/Wallabag/ImportBundle/Import/ImportInterface.php b/src/Wallabag/ImportBundle/Import/ImportInterface.php
index 0f9b3256..8cf238aa 100644
--- a/src/Wallabag/ImportBundle/Import/ImportInterface.php
+++ b/src/Wallabag/ImportBundle/Import/ImportInterface.php
@@ -2,7 +2,9 @@
2 2
3namespace Wallabag\ImportBundle\Import; 3namespace Wallabag\ImportBundle\Import;
4 4
5interface ImportInterface 5use Psr\Log\LoggerAwareInterface;
6
7interface ImportInterface extends LoggerAwareInterface
6{ 8{
7 /** 9 /**
8 * Name of the import. 10 * Name of the import.
@@ -19,27 +21,18 @@ interface ImportInterface
19 public function getDescription(); 21 public function getDescription();
20 22
21 /** 23 /**
22 * Return the oauth url to authenticate the client. 24 * Import content using the user token.
23 *
24 * @param string $redirectUri Redirect url in case of error
25 * @param string $callbackUri Url when the authentication is complete
26 *
27 * @return string
28 */
29 public function oAuthRequest($redirectUri, $callbackUri);
30
31 /**
32 * Usually called by the previous callback to authorize the client.
33 * Then it return a token that can be used for next requests.
34 * 25 *
35 * @return string 26 * @return bool
36 */ 27 */
37 public function oAuthAuthorize(); 28 public function import();
38 29
39 /** 30 /**
40 * Import content using the user token. 31 * Return an array with summary info about the import, with keys:
32 * - skipped
33 * - imported.
41 * 34 *
42 * @param string $accessToken User access token 35 * @return array
43 */ 36 */
44 public function import($accessToken); 37 public function getSummary();
45} 38}
diff --git a/src/Wallabag/ImportBundle/Import/PocketImport.php b/src/Wallabag/ImportBundle/Import/PocketImport.php
index e5c86f07..1710d9d3 100644
--- a/src/Wallabag/ImportBundle/Import/PocketImport.php
+++ b/src/Wallabag/ImportBundle/Import/PocketImport.php
@@ -2,29 +2,39 @@
2 2
3namespace Wallabag\ImportBundle\Import; 3namespace Wallabag\ImportBundle\Import;
4 4
5use Psr\Log\LoggerInterface;
6use Psr\Log\NullLogger;
5use Doctrine\ORM\EntityManager; 7use Doctrine\ORM\EntityManager;
6use GuzzleHttp\Client; 8use GuzzleHttp\Client;
7use Symfony\Component\HttpFoundation\Session\Session; 9use GuzzleHttp\Exception\RequestException;
8use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; 10use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
9use Wallabag\CoreBundle\Entity\Entry; 11use Wallabag\CoreBundle\Entity\Entry;
10use Wallabag\CoreBundle\Entity\Tag; 12use Wallabag\CoreBundle\Entity\Tag;
11use Wallabag\CoreBundle\Tools\Utils; 13use Wallabag\CoreBundle\Helper\ContentProxy;
12 14
13class PocketImport implements ImportInterface 15class PocketImport implements ImportInterface
14{ 16{
15 private $user; 17 private $user;
16 private $session;
17 private $em; 18 private $em;
19 private $contentProxy;
20 private $logger;
18 private $consumerKey; 21 private $consumerKey;
19 private $skippedEntries = 0; 22 private $skippedEntries = 0;
20 private $importedEntries = 0; 23 private $importedEntries = 0;
24 protected $accessToken;
21 25
22 public function __construct(TokenStorageInterface $tokenStorage, Session $session, EntityManager $em, $consumerKey) 26 public function __construct(TokenStorageInterface $tokenStorage, EntityManager $em, ContentProxy $contentProxy, $consumerKey)
23 { 27 {
24 $this->user = $tokenStorage->getToken()->getUser(); 28 $this->user = $tokenStorage->getToken()->getUser();
25 $this->session = $session;
26 $this->em = $em; 29 $this->em = $em;
30 $this->contentProxy = $contentProxy;
27 $this->consumerKey = $consumerKey; 31 $this->consumerKey = $consumerKey;
32 $this->logger = new NullLogger();
33 }
34
35 public function setLogger(LoggerInterface $logger)
36 {
37 $this->logger = $logger;
28 } 38 }
29 39
30 /** 40 /**
@@ -44,9 +54,13 @@ class PocketImport implements ImportInterface
44 } 54 }
45 55
46 /** 56 /**
47 * {@inheritdoc} 57 * Return the oauth url to authenticate the client.
58 *
59 * @param string $redirectUri Redirect url in case of error
60 *
61 * @return string request_token for callback method
48 */ 62 */
49 public function oAuthRequest($redirectUri, $callbackUri) 63 public function getRequestToken($redirectUri)
50 { 64 {
51 $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/oauth/request', 65 $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/oauth/request',
52 [ 66 [
@@ -57,44 +71,59 @@ class PocketImport implements ImportInterface
57 ] 71 ]
58 ); 72 );
59 73
60 $response = $this->client->send($request); 74 try {
61 $values = $response->json(); 75 $response = $this->client->send($request);
76 } catch (RequestException $e) {
77 $this->logger->error(sprintf('PocketImport: Failed to request token: %s', $e->getMessage()), ['exception' => $e]);
62 78
63 // store code in session for callback method 79 return false;
64 $this->session->set('pocketCode', $values['code']); 80 }
65 81
66 return 'https://getpocket.com/auth/authorize?request_token='.$values['code'].'&redirect_uri='.$callbackUri; 82 return $response->json()['code'];
67 } 83 }
68 84
69 /** 85 /**
70 * {@inheritdoc} 86 * Usually called by the previous callback to authorize the client.
87 * Then it return a token that can be used for next requests.
88 *
89 * @param string $code request_token from getRequestToken
90 *
91 * @return bool
71 */ 92 */
72 public function oAuthAuthorize() 93 public function authorize($code)
73 { 94 {
74 $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/oauth/authorize', 95 $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/oauth/authorize',
75 [ 96 [
76 'body' => json_encode([ 97 'body' => json_encode([
77 'consumer_key' => $this->consumerKey, 98 'consumer_key' => $this->consumerKey,
78 'code' => $this->session->get('pocketCode'), 99 'code' => $code,
79 ]), 100 ]),
80 ] 101 ]
81 ); 102 );
82 103
83 $response = $this->client->send($request); 104 try {
105 $response = $this->client->send($request);
106 } catch (RequestException $e) {
107 $this->logger->error(sprintf('PocketImport: Failed to authorize client: %s', $e->getMessage()), ['exception' => $e]);
84 108
85 return $response->json()['access_token']; 109 return false;
110 }
111
112 $this->accessToken = $response->json()['access_token'];
113
114 return true;
86 } 115 }
87 116
88 /** 117 /**
89 * {@inheritdoc} 118 * {@inheritdoc}
90 */ 119 */
91 public function import($accessToken) 120 public function import()
92 { 121 {
93 $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/get', 122 $request = $this->client->createRequest('POST', 'https://getpocket.com/v3/get',
94 [ 123 [
95 'body' => json_encode([ 124 'body' => json_encode([
96 'consumer_key' => $this->consumerKey, 125 'consumer_key' => $this->consumerKey,
97 'access_token' => $accessToken, 126 'access_token' => $this->accessToken,
98 'detailType' => 'complete', 127 'detailType' => 'complete',
99 'state' => 'all', 128 'state' => 'all',
100 'sort' => 'oldest', 129 'sort' => 'oldest',
@@ -102,61 +131,45 @@ class PocketImport implements ImportInterface
102 ] 131 ]
103 ); 132 );
104 133
105 $response = $this->client->send($request); 134 try {
135 $response = $this->client->send($request);
136 } catch (RequestException $e) {
137 $this->logger->error(sprintf('PocketImport: Failed to import: %s', $e->getMessage()), ['exception' => $e]);
138
139 return false;
140 }
141
106 $entries = $response->json(); 142 $entries = $response->json();
107 143
108 $this->parsePocketEntries($entries['list']); 144 $this->parsePocketEntries($entries['list']);
109 145
110 $this->session->getFlashBag()->add( 146 return true;
111 'notice',
112 $this->importedEntries.' entries imported, '.$this->skippedEntries.' already saved.'
113 );
114 } 147 }
115 148
116 /** 149 /**
117 * Set the Guzzle client. 150 * {@inheritdoc}
118 *
119 * @param Client $client
120 */ 151 */
121 public function setClient(Client $client) 152 public function getSummary()
122 { 153 {
123 $this->client = $client; 154 return [
155 'skipped' => $this->skippedEntries,
156 'imported' => $this->importedEntries,
157 ];
124 } 158 }
125 159
126 /** 160 /**
127 * Returns the good title for current entry. 161 * Set the Guzzle client.
128 *
129 * @param $pocketEntry
130 * 162 *
131 * @return string 163 * @param Client $client
132 */ 164 */
133 private function guessTitle($pocketEntry) 165 public function setClient(Client $client)
134 { 166 {
135 if (isset($pocketEntry['resolved_title']) && $pocketEntry['resolved_title'] != '') { 167 $this->client = $client;
136 return $pocketEntry['resolved_title'];
137 } elseif (isset($pocketEntry['given_title']) && $pocketEntry['given_title'] != '') {
138 return $pocketEntry['given_title'];
139 }
140
141 return 'Untitled';
142 } 168 }
143 169
144 /** 170 /**
145 * Returns the good URL for current entry. 171 * @todo move that in a more global place
146 *
147 * @param $pocketEntry
148 *
149 * @return string
150 */ 172 */
151 private function guessURL($pocketEntry)
152 {
153 if (isset($pocketEntry['resolved_url']) && $pocketEntry['resolved_url'] != '') {
154 return $pocketEntry['resolved_url'];
155 }
156
157 return $pocketEntry['given_url'];
158 }
159
160 private function assignTagsToEntry(Entry $entry, $tags) 173 private function assignTagsToEntry(Entry $entry, $tags)
161 { 174 {
162 foreach ($tags as $tag) { 175 foreach ($tags as $tag) {
@@ -177,13 +190,16 @@ class PocketImport implements ImportInterface
177 } 190 }
178 191
179 /** 192 /**
193 * @see https://getpocket.com/developer/docs/v3/retrieve
194 *
180 * @param $entries 195 * @param $entries
181 */ 196 */
182 private function parsePocketEntries($entries) 197 private function parsePocketEntries($entries)
183 { 198 {
184 foreach ($entries as $pocketEntry) { 199 foreach ($entries as $pocketEntry) {
185 $entry = new Entry($this->user); 200 $entry = new Entry($this->user);
186 $url = $this->guessURL($pocketEntry); 201
202 $url = isset($pocketEntry['resolved_url']) && $pocketEntry['resolved_url'] != '' ? $pocketEntry['resolved_url'] : $pocketEntry['given_url'];
187 203
188 $existingEntry = $this->em 204 $existingEntry = $this->em
189 ->getRepository('WallabagCoreBundle:Entry') 205 ->getRepository('WallabagCoreBundle:Entry')
@@ -194,31 +210,33 @@ class PocketImport implements ImportInterface
194 continue; 210 continue;
195 } 211 }
196 212
197 $entry->setUrl($url); 213 $entry = $this->contentProxy->updateEntry($entry, $url);
198 $entry->setDomainName(parse_url($url, PHP_URL_HOST));
199 214
215 // 0, 1, 2 - 1 if the item is archived - 2 if the item should be deleted
200 if ($pocketEntry['status'] == 1) { 216 if ($pocketEntry['status'] == 1) {
201 $entry->setArchived(true); 217 $entry->setArchived(true);
202 } 218 }
219
220 // 0 or 1 - 1 If the item is favorited
203 if ($pocketEntry['favorite'] == 1) { 221 if ($pocketEntry['favorite'] == 1) {
204 $entry->setStarred(true); 222 $entry->setStarred(true);
205 } 223 }
206 224
207 $entry->setTitle($this->guessTitle($pocketEntry)); 225 $title = 'Untitled';
208 226 if (isset($pocketEntry['resolved_title']) && $pocketEntry['resolved_title'] != '') {
209 if (isset($pocketEntry['excerpt'])) { 227 $title = $pocketEntry['resolved_title'];
210 $entry->setContent($pocketEntry['excerpt']); 228 } elseif (isset($pocketEntry['given_title']) && $pocketEntry['given_title'] != '') {
229 $title = $pocketEntry['given_title'];
211 } 230 }
212 231
213 if (isset($pocketEntry['has_image']) && $pocketEntry['has_image'] > 0) { 232 $entry->setTitle($title);
214 $entry->setPreviewPicture($pocketEntry['image']['src']);
215 }
216 233
217 if (isset($pocketEntry['word_count'])) { 234 // 0, 1, or 2 - 1 if the item has images in it - 2 if the item is an image
218 $entry->setReadingTime(Utils::convertWordsToMinutes($pocketEntry['word_count'])); 235 if (isset($pocketEntry['has_image']) && $pocketEntry['has_image'] > 0 && isset($pocketEntry['images'][1])) {
236 $entry->setPreviewPicture($pocketEntry['images'][1]['src']);
219 } 237 }
220 238
221 if (!empty($pocketEntry['tags'])) { 239 if (isset($pocketEntry['tags']) && !empty($pocketEntry['tags'])) {
222 $this->assignTagsToEntry($entry, $pocketEntry['tags']); 240 $this->assignTagsToEntry($entry, $pocketEntry['tags']);
223 } 241 }
224 242
diff --git a/src/Wallabag/ImportBundle/Resources/config/services.yml b/src/Wallabag/ImportBundle/Resources/config/services.yml
index ab516ca5..f421821c 100644
--- a/src/Wallabag/ImportBundle/Resources/config/services.yml
+++ b/src/Wallabag/ImportBundle/Resources/config/services.yml
@@ -1,14 +1,4 @@
1services: 1services:
2 wallabag_import.pocket.import:
3 class: Wallabag\ImportBundle\Import\PocketImport
4 arguments:
5 - "@security.token_storage"
6 - "@session"
7 - "@doctrine.orm.entity_manager"
8 - %pocket_consumer_key%
9 calls:
10 - [ setClient, [ "@wallabag_import.pocket.client" ] ]
11
12 wallabag_import.pocket.client: 2 wallabag_import.pocket.client:
13 class: GuzzleHttp\Client 3 class: GuzzleHttp\Client
14 arguments: 4 arguments:
@@ -17,3 +7,14 @@ services:
17 headers: 7 headers:
18 content-type: "application/json" 8 content-type: "application/json"
19 X-Accept: "application/json" 9 X-Accept: "application/json"
10
11 wallabag_import.pocket.import:
12 class: Wallabag\ImportBundle\Import\PocketImport
13 arguments:
14 - "@security.token_storage"
15 - "@doctrine.orm.entity_manager"
16 - "@wallabag_core.content_proxy"
17 - %pocket_consumer_key%
18 calls:
19 - [ setClient, [ "@wallabag_import.pocket.client" ] ]
20 - [ setLogger, [ "@logger" ]]
diff --git a/src/Wallabag/ImportBundle/Resources/views/Import/index.html.twig b/src/Wallabag/ImportBundle/Resources/views/Import/index.html.twig
index ee759a52..b068283a 100644
--- a/src/Wallabag/ImportBundle/Resources/views/Import/index.html.twig
+++ b/src/Wallabag/ImportBundle/Resources/views/Import/index.html.twig
@@ -8,35 +8,9 @@
8 <div class="card-panel settings"> 8 <div class="card-panel settings">
9 {% trans %}Welcome on wallabag importer. Please select your previous service that you want to migrate.{% endtrans %} 9 {% trans %}Welcome on wallabag importer. Please select your previous service that you want to migrate.{% endtrans %}
10 <ul> 10 <ul>
11 <li><a href="{{ path('pocket_import') }}">Pocket</a></li> 11 <li><a href="{{ path('import_pocket') }}">Pocket</a></li>
12 </ul> 12 </ul>
13 </div> 13 </div>
14 </div> 14 </div>
15</div> 15</div>
16
17
18<div class="row">
19 <div class="col s12">
20 <div class="card-panel settings">
21 <div class="row">
22 <div class="col s12">
23 <form action="{{ path('import') }}" method="post" {{ form_enctype(form.import) }}>
24 {{ form_errors(form.import) }}
25 <div class="row">
26 <div class="input-field col s12">
27 <p>{% trans %}Please select your wallabag export and click on the below button to upload and import it.{% endtrans %}</p>
28 {{ form_errors(form.import.file) }}
29 {{ form_widget(form.import.file) }}
30 </div>
31 </div>
32 <div class="hidden">{{ form_rest(form.import) }}</div>
33 <button class="btn waves-effect waves-light" type="submit" name="action">
34 {% trans %}Upload file{% endtrans %}
35 </button>
36 </form>
37 </div>
38 </div>
39 </div>
40 </div>
41</div>
42{% endblock %} 16{% endblock %}
diff --git a/src/Wallabag/ImportBundle/Resources/views/Pocket/index.html.twig b/src/Wallabag/ImportBundle/Resources/views/Pocket/index.html.twig
index df64e472..940fe4cc 100644
--- a/src/Wallabag/ImportBundle/Resources/views/Pocket/index.html.twig
+++ b/src/Wallabag/ImportBundle/Resources/views/Pocket/index.html.twig
@@ -7,7 +7,7 @@
7 <div class="col s12"> 7 <div class="col s12">
8 <div class="card-panel settings"> 8 <div class="card-panel settings">
9 {% 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 %} 9 {% 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 %}
10 <form method="post" action="{{ path('pocket_auth') }}"> 10 <form method="post" action="{{ path('import_pocket_auth') }}">
11 <input type="submit" value="Connect to Pocket and import data" /> 11 <input type="submit" value="Connect to Pocket and import data" />
12 </form> 12 </form>
13 </div> 13 </div>
diff --git a/src/Wallabag/ImportBundle/Tests/Import/PocketImportTest.php b/src/Wallabag/ImportBundle/Tests/Import/PocketImportTest.php
index 4c718fa3..cf706fa9 100644
--- a/src/Wallabag/ImportBundle/Tests/Import/PocketImportTest.php
+++ b/src/Wallabag/ImportBundle/Tests/Import/PocketImportTest.php
@@ -4,19 +4,28 @@ namespace Wallabag\ImportBundle\Tests\Import;
4 4
5use Wallabag\UserBundle\Entity\User; 5use Wallabag\UserBundle\Entity\User;
6use Wallabag\ImportBundle\Import\PocketImport; 6use Wallabag\ImportBundle\Import\PocketImport;
7use Symfony\Component\HttpFoundation\Session\Session;
8use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
9use GuzzleHttp\Client; 7use GuzzleHttp\Client;
10use GuzzleHttp\Subscriber\Mock; 8use GuzzleHttp\Subscriber\Mock;
11use GuzzleHttp\Message\Response; 9use GuzzleHttp\Message\Response;
12use GuzzleHttp\Stream\Stream; 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}
13 21
14class PocketImportTest extends \PHPUnit_Framework_TestCase 22class PocketImportTest extends \PHPUnit_Framework_TestCase
15{ 23{
16 protected $token; 24 protected $token;
17 protected $user; 25 protected $user;
18 protected $session;
19 protected $em; 26 protected $em;
27 protected $contentProxy;
28 protected $logHandler;
20 29
21 private function getPocketImport($consumerKey = 'ConsumerKey') 30 private function getPocketImport($consumerKey = 'ConsumerKey')
22 { 31 {
@@ -30,6 +39,10 @@ class PocketImportTest extends \PHPUnit_Framework_TestCase
30 ->disableOriginalConstructor() 39 ->disableOriginalConstructor()
31 ->getMock(); 40 ->getMock();
32 41
42 $this->contentProxy = $this->getMockBuilder('Wallabag\CoreBundle\Helper\ContentProxy')
43 ->disableOriginalConstructor()
44 ->getMock();
45
33 $token->expects($this->once()) 46 $token->expects($this->once())
34 ->method('getUser') 47 ->method('getUser')
35 ->willReturn($this->user); 48 ->willReturn($this->user);
@@ -38,18 +51,22 @@ class PocketImportTest extends \PHPUnit_Framework_TestCase
38 ->method('getToken') 51 ->method('getToken')
39 ->willReturn($token); 52 ->willReturn($token);
40 53
41 $this->session = new Session(new MockArraySessionStorage());
42
43 $this->em = $this->getMockBuilder('Doctrine\ORM\EntityManager') 54 $this->em = $this->getMockBuilder('Doctrine\ORM\EntityManager')
44 ->disableOriginalConstructor() 55 ->disableOriginalConstructor()
45 ->getMock(); 56 ->getMock();
46 57
47 return new PocketImport( 58 $pocket = new PocketImportMock(
48 $this->tokenStorage, 59 $this->tokenStorage,
49 $this->session,
50 $this->em, 60 $this->em,
61 $this->contentProxy,
51 $consumerKey 62 $consumerKey
52 ); 63 );
64
65 $this->logHandler = new TestHandler();
66 $logger = new Logger('test', array($this->logHandler));
67 $pocket->setLogger($logger);
68
69 return $pocket;
53 } 70 }
54 71
55 public function testInit() 72 public function testInit()
@@ -65,7 +82,7 @@ class PocketImportTest extends \PHPUnit_Framework_TestCase
65 $client = new Client(); 82 $client = new Client();
66 83
67 $mock = new Mock([ 84 $mock = new Mock([
68 new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['code' => 'wunderbar']))), 85 new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['code' => 'wunderbar_code']))),
69 ]); 86 ]);
70 87
71 $client->getEmitter()->attach($mock); 88 $client->getEmitter()->attach($mock);
@@ -73,10 +90,31 @@ class PocketImportTest extends \PHPUnit_Framework_TestCase
73 $pocketImport = $this->getPocketImport(); 90 $pocketImport = $this->getPocketImport();
74 $pocketImport->setClient($client); 91 $pocketImport->setClient($client);
75 92
76 $url = $pocketImport->oAuthRequest('http://0.0.0.0./redirect', 'http://0.0.0.0./callback'); 93 $code = $pocketImport->getRequestToken('http://0.0.0.0/redirect');
77 94
78 $this->assertEquals('https://getpocket.com/auth/authorize?request_token=wunderbar&redirect_uri=http://0.0.0.0./callback', $url); 95 $this->assertEquals('wunderbar_code', $code);
79 $this->assertEquals('wunderbar', $this->session->get('pocketCode')); 96 }
97
98 public function testOAuthRequestBadResponse()
99 {
100 $client = new Client();
101
102 $mock = new Mock([
103 new Response(403),
104 ]);
105
106 $client->getEmitter()->attach($mock);
107
108 $pocketImport = $this->getPocketImport();
109 $pocketImport->setClient($client);
110
111 $code = $pocketImport->getRequestToken('http://0.0.0.0/redirect');
112
113 $this->assertFalse($code);
114
115 $records = $this->logHandler->getRecords();
116 $this->assertContains('PocketImport: Failed to request token', $records[0]['message']);
117 $this->assertEquals('ERROR', $records[0]['level_name']);
80 } 118 }
81 119
82 public function testOAuthAuthorize() 120 public function testOAuthAuthorize()
@@ -84,7 +122,7 @@ class PocketImportTest extends \PHPUnit_Framework_TestCase
84 $client = new Client(); 122 $client = new Client();
85 123
86 $mock = new Mock([ 124 $mock = new Mock([
87 new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar']))), 125 new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))),
88 ]); 126 ]);
89 127
90 $client->getEmitter()->attach($mock); 128 $client->getEmitter()->attach($mock);
@@ -92,26 +130,182 @@ class PocketImportTest extends \PHPUnit_Framework_TestCase
92 $pocketImport = $this->getPocketImport(); 130 $pocketImport = $this->getPocketImport();
93 $pocketImport->setClient($client); 131 $pocketImport->setClient($client);
94 132
95 $accessToken = $pocketImport->oAuthAuthorize(); 133 $res = $pocketImport->authorize('wunderbar_code');
96 134
97 $this->assertEquals('wunderbar', $accessToken); 135 $this->assertTrue($res);
136 $this->assertEquals('wunderbar_token', $pocketImport->getAccessToken());
98 } 137 }
99 138
139 public function testOAuthAuthorizeBadResponse()
140 {
141 $client = new Client();
142
143 $mock = new Mock([
144 new Response(403),
145 ]);
146
147 $client->getEmitter()->attach($mock);
148
149 $pocketImport = $this->getPocketImport();
150 $pocketImport->setClient($client);
151
152 $res = $pocketImport->authorize('wunderbar_code');
153
154 $this->assertFalse($res);
155
156 $records = $this->logHandler->getRecords();
157 $this->assertContains('PocketImport: Failed to authorize client', $records[0]['message']);
158 $this->assertEquals('ERROR', $records[0]['level_name']);
159 }
160
161 /**
162 * Will sample results from https://getpocket.com/developer/docs/v3/retrieve.
163 */
100 public function testImport() 164 public function testImport()
101 { 165 {
102 $client = new Client(); 166 $client = new Client();
103 167
104 $mock = new Mock([ 168 $mock = new Mock([
105 new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['list' => []]))), 169 new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))),
170 new Response(200, ['Content-Type' => 'application/json'], Stream::factory('
171 {
172 "status": 1,
173 "list": {
174 "229279689": {
175 "item_id": "229279689",
176 "resolved_id": "229279689",
177 "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
178 "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland",
179 "favorite": "1",
180 "status": "1",
181 "resolved_title": "The Massive Ryder Cup Preview",
182 "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
183 "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.",
184 "is_article": "1",
185 "has_video": "1",
186 "has_image": "1",
187 "word_count": "3197",
188 "images": {
189 "1": {
190 "item_id": "229279689",
191 "image_id": "1",
192 "src": "http://a.espncdn.com/combiner/i?img=/photo/2012/0927/grant_g_ryder_cr_640.jpg&w=640&h=360",
193 "width": "0",
194 "height": "0",
195 "credit": "Jamie Squire/Getty Images",
196 "caption": ""
197 }
198 },
199 "videos": {
200 "1": {
201 "item_id": "229279689",
202 "video_id": "1",
203 "src": "http://www.youtube.com/v/Er34PbFkVGk?version=3&hl=en_US&rel=0",
204 "width": "420",
205 "height": "315",
206 "type": "1",
207 "vid": "Er34PbFkVGk"
208 }
209 },
210 "tags": {
211 "grantland": {
212 "item_id": "1147652870",
213 "tag": "grantland"
214 },
215 "Ryder Cup": {
216 "item_id": "1147652870",
217 "tag": "Ryder Cup"
218 }
219 }
220 },
221 "229279690": {
222 "item_id": "229279689",
223 "resolved_id": "229279689",
224 "given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
225 "given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland",
226 "favorite": "1",
227 "status": "1",
228 "resolved_title": "The Massive Ryder Cup Preview",
229 "resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
230 "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.",
231 "is_article": "1",
232 "has_video": "0",
233 "has_image": "0",
234 "word_count": "3197"
235 }
236 }
237 }
238 ')),
239 ]);
240
241 $client->getEmitter()->attach($mock);
242
243 $pocketImport = $this->getPocketImport();
244
245 $entryRepo = $this->getMockBuilder('Wallabag\CoreBundle\Repository\EntryRepository')
246 ->disableOriginalConstructor()
247 ->getMock();
248
249 $entryRepo->expects($this->exactly(2))
250 ->method('existByUrlAndUserId')
251 ->will($this->onConsecutiveCalls(false, true));
252
253 $tag = $this->getMockBuilder('Wallabag\CoreBundle\Entity\Tag')
254 ->disableOriginalConstructor()
255 ->getMock();
256
257 $tagRepo = $this->getMockBuilder('Wallabag\CoreBundle\Repository\TagRepository')
258 ->disableOriginalConstructor()
259 ->getMock();
260
261 $tagRepo->expects($this->exactly(2))
262 ->method('findOneByLabelAndUserId')
263 ->will($this->onConsecutiveCalls(false, $tag));
264
265 $this->em
266 ->expects($this->any())
267 ->method('getRepository')
268 ->will($this->onConsecutiveCalls($entryRepo, $tagRepo, $tagRepo, $entryRepo));
269
270 $entry = $this->getMockBuilder('Wallabag\CoreBundle\Entity\Entry')
271 ->disableOriginalConstructor()
272 ->getMock();
273
274 $this->contentProxy
275 ->expects($this->once())
276 ->method('updateEntry')
277 ->willReturn($entry);
278
279 $pocketImport->setClient($client);
280 $pocketImport->authorize('wunderbar_code');
281
282 $res = $pocketImport->import();
283
284 $this->assertTrue($res);
285 $this->assertEquals(['skipped' => 1, 'imported' => 1], $pocketImport->getSummary());
286 }
287
288 public function testImportBadResponse()
289 {
290 $client = new Client();
291
292 $mock = new Mock([
293 new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))),
294 new Response(403),
106 ]); 295 ]);
107 296
108 $client->getEmitter()->attach($mock); 297 $client->getEmitter()->attach($mock);
109 298
110 $pocketImport = $this->getPocketImport(); 299 $pocketImport = $this->getPocketImport();
111 $pocketImport->setClient($client); 300 $pocketImport->setClient($client);
301 $pocketImport->authorize('wunderbar_code');
302
303 $res = $pocketImport->import();
112 304
113 $pocketImport->import('wunderbar'); 305 $this->assertFalse($res);
114 306
115 $this->assertEquals('0 entries imported, 0 already saved.', $this->session->getFlashBag()->get('notice')[0]); 307 $records = $this->logHandler->getRecords();
308 $this->assertContains('PocketImport: Failed to import', $records[0]['message']);
309 $this->assertEquals('ERROR', $records[0]['level_name']);
116 } 310 }
117} 311}