Merge pull request #1493 from wallabag/v2-pocket-import

v2 – 1st draft for Pocket import via API & Wallabag v1 import
This commit is contained in:
Jeremy Benoist 2016-01-07 22:15:08 +01:00
commit 39643c6b76
51 changed files with 1770 additions and 35 deletions

View file

@ -31,6 +31,7 @@ class AppKernel extends Kernel
new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),
new Scheb\TwoFactorBundle\SchebTwoFactorBundle(),
new KPhoen\RulerZBundle\KPhoenRulerZBundle(),
new Wallabag\ImportBundle\WallabagImportBundle(),
);
if (in_array($this->getEnvironment(), array('dev', 'test'))) {

View file

@ -31,6 +31,10 @@ wallabag_core:
fr: 'Français'
de: 'Deutsch'
wallabag_import:
allow_mimetypes: ['application/octet-stream', 'application/json', 'text/plain']
resource_dir: "%kernel.root_dir%/../web/uploads/import"
# Twig Configuration
twig:
debug: "%kernel.debug%"

View file

@ -60,3 +60,6 @@ parameters:
language: en
from_email: no-reply@wallabag.org
rss_limit: 50
# pocket import
pocket_consumer_key: xxxxxxxx

View file

@ -1,3 +1,8 @@
wallabag_import:
resource: "@WallabagImportBundle/Controller/"
type: annotation
prefix: /import
wallabag_api:
resource: "@WallabagApiBundle/Resources/config/routing.yml"
prefix: /

View file

@ -60,3 +60,6 @@ parameters:
language: en_US
from_email: no-reply@wallabag.org
rss_limit: 50
# pocket import
pocket_consumer_key: xxxxxxxx

View file

@ -60,3 +60,6 @@ parameters:
language: en_US
from_email: no-reply@wallabag.org
rss_limit: 50
# pocket import
pocket_consumer_key: xxxxxxxx

View file

@ -60,3 +60,6 @@ parameters:
language: en_US
from_email: no-reply@wallabag.org
rss_limit: 50
# pocket import
pocket_consumer_key: xxxxxxxx

View file

@ -59,7 +59,8 @@
"scheb/two-factor-bundle": "~1.4.0",
"grandt/phpepub": "~4.0",
"wallabag/php-mobi": "~1.0.0",
"kphoen/rulerz-bundle": "~0.10"
"kphoen/rulerz-bundle": "~0.10",
"guzzlehttp/guzzle": "^5.2.0"
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "~2.2.0",

38
composer.lock generated
View file

@ -4,8 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "91da706ef4b39a73704c3e2154c1a227",
"content-hash": "81a3c2c84d78471bfb526b2b572182f7",
"hash": "fdba142656b2089b0e4cbddb45e2ad1f",
"content-hash": "a233f851c52683783b6a42be707c52b1",
"packages": [
{
"name": "behat/transliterator",
@ -117,33 +117,33 @@
},
{
"name": "doctrine/cache",
"version": "v1.5.4",
"version": "v1.6.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/cache.git",
"reference": "47cdc76ceb95cc591d9c79a36dc3794975b5d136"
"reference": "f8af318d14bdb0eff0336795b428b547bd39ccb6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/cache/zipball/47cdc76ceb95cc591d9c79a36dc3794975b5d136",
"reference": "47cdc76ceb95cc591d9c79a36dc3794975b5d136",
"url": "https://api.github.com/repos/doctrine/cache/zipball/f8af318d14bdb0eff0336795b428b547bd39ccb6",
"reference": "f8af318d14bdb0eff0336795b428b547bd39ccb6",
"shasum": ""
},
"require": {
"php": ">=5.3.2"
"php": "~5.5|~7.0"
},
"conflict": {
"doctrine/common": ">2.2,<2.4"
},
"require-dev": {
"phpunit/phpunit": ">=3.7",
"phpunit/phpunit": "~4.8|~5.0",
"predis/predis": "~1.0",
"satooshi/php-coveralls": "~0.6"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.5.x-dev"
"dev-master": "1.6.x-dev"
}
},
"autoload": {
@ -183,7 +183,7 @@
"cache",
"caching"
],
"time": "2015-12-19 05:03:47"
"time": "2015-12-31 16:37:02"
},
{
"name": "doctrine/collections",
@ -981,17 +981,17 @@
},
{
"name": "friendsofsymfony/rest-bundle",
"version": "1.7.6",
"version": "1.7.7",
"target-dir": "FOS/RestBundle",
"source": {
"type": "git",
"url": "https://github.com/FriendsOfSymfony/FOSRestBundle.git",
"reference": "f95b2f141748e9a5e2ddae833f60c38417aee8c3"
"reference": "c79b7e5df96e5581591ceb6a026bd4e5f9346de0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/FriendsOfSymfony/FOSRestBundle/zipball/f95b2f141748e9a5e2ddae833f60c38417aee8c3",
"reference": "f95b2f141748e9a5e2ddae833f60c38417aee8c3",
"url": "https://api.github.com/repos/FriendsOfSymfony/FOSRestBundle/zipball/c79b7e5df96e5581591ceb6a026bd4e5f9346de0",
"reference": "c79b7e5df96e5581591ceb6a026bd4e5f9346de0",
"shasum": ""
},
"require": {
@ -1063,7 +1063,7 @@
"keywords": [
"rest"
],
"time": "2015-12-20 13:45:30"
"time": "2015-12-29 16:02:50"
},
{
"name": "friendsofsymfony/user-bundle",
@ -1071,12 +1071,12 @@
"source": {
"type": "git",
"url": "https://github.com/FriendsOfSymfony/FOSUserBundle.git",
"reference": "e39b040e272c72f0a090c67d802e1d3b2d0b0313"
"reference": "e5e7a2b8984da8dfedaf44adc7e5f60a62ad280c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/FriendsOfSymfony/FOSUserBundle/zipball/e39b040e272c72f0a090c67d802e1d3b2d0b0313",
"reference": "e39b040e272c72f0a090c67d802e1d3b2d0b0313",
"url": "https://api.github.com/repos/FriendsOfSymfony/FOSUserBundle/zipball/e5e7a2b8984da8dfedaf44adc7e5f60a62ad280c",
"reference": "e5e7a2b8984da8dfedaf44adc7e5f60a62ad280c",
"shasum": ""
},
"require": {
@ -1132,7 +1132,7 @@
"keywords": [
"User management"
],
"time": "2015-12-05 09:38:57"
"time": "2015-12-28 18:02:43"
},
{
"name": "gedmo/doctrine-extensions",

View file

@ -24,6 +24,7 @@ The main documentation for the site is organized into a couple sections:
user/login
user/configuration
user/first_article
user/import
user/organize
user/filters

39
docs/en/user/import.rst Normal file
View file

@ -0,0 +1,39 @@
Migrate to wallabag
===================
From wallabag 1.x
-----------------
Export your data from your wallabag 1.x
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
On your config page, click on ``JSON export`` in the ``Export your wallabag data`` section.
.. image:: ../../img/user/export_wllbg_1.png
:alt: Export from wallabag 1.x
:align: center
You will have a ``wallabag-export-1-1970-01-01.json`` file.
Import your data into wallabag 2.x
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Click on ``Import`` link in the menu, select your export file on your computer and import it.
.. image:: ../../img/user/import_wllbg.png
:alt: Import from wallabag 1.x
:align: center
All your wallabag 1.x articles will be imported.
From Pocket
-----------
From Instapaper
---------------
From Readability
----------------
From HTML or JSON file
----------------------

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -60,7 +60,7 @@ class WallabagRestController extends FOSRestController
* }
* )
*
* @return Entry
* @return Response
*/
public function getEntriesAction(Request $request)
{
@ -101,7 +101,7 @@ class WallabagRestController extends FOSRestController
* }
* )
*
* @return Entry
* @return Response
*/
public function getEntryAction(Entry $entry)
{
@ -124,7 +124,7 @@ class WallabagRestController extends FOSRestController
* }
* )
*
* @return Entry
* @return Response
*/
public function postEntriesAction(Request $request)
{
@ -166,7 +166,7 @@ class WallabagRestController extends FOSRestController
* }
* )
*
* @return Entry
* @return Response
*/
public function patchEntriesAction(Entry $entry, Request $request)
{
@ -211,7 +211,7 @@ class WallabagRestController extends FOSRestController
* }
* )
*
* @return Entry
* @return Response
*/
public function deleteEntriesAction(Entry $entry)
{
@ -235,6 +235,8 @@ class WallabagRestController extends FOSRestController
* {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
* }
* )
*
* @return Response
*/
public function getEntriesTagsAction(Entry $entry)
{
@ -257,6 +259,8 @@ class WallabagRestController extends FOSRestController
* {"name"="tags", "dataType"="string", "required"=false, "format"="tag1,tag2,tag3", "description"="a comma-separated list of tags."},
* }
* )
*
* @return Response
*/
public function postEntriesTagsAction(Request $request, Entry $entry)
{
@ -286,6 +290,8 @@ class WallabagRestController extends FOSRestController
* {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
* }
* )
*
* @return Response
*/
public function deleteEntriesTagsAction(Entry $entry, Tag $tag)
{
@ -306,6 +312,8 @@ class WallabagRestController extends FOSRestController
* Retrieve all tags.
*
* @ApiDoc()
*
* @return Response
*/
public function getTagsAction()
{
@ -328,6 +336,8 @@ class WallabagRestController extends FOSRestController
* {"name"="tag", "dataType"="integer", "requirement"="\w+", "description"="The tag"}
* }
* )
*
* @return Response
*/
public function deleteTagAction(Tag $tag)
{

View file

@ -11,7 +11,6 @@ use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Helper\Table;
use Wallabag\UserBundle\Entity\User;
use Wallabag\CoreBundle\Entity\Config;
class InstallCommand extends ContainerAwareCommand

View file

@ -48,6 +48,19 @@ class EntryController extends Controller
$form->handleRequest($request);
if ($form->isValid()) {
// check for existing entry, if it exists, redirect to it with a message
$existingEntry = $this->get('wallabag_core.entry_repository')
->existByUrlAndUserId($entry->getUrl(), $this->getUser()->getId());
if (false !== $existingEntry) {
$this->get('session')->getFlashBag()->add(
'notice',
'Entry already saved on '.$existingEntry['createdAt']->format('d-m-Y')
);
return $this->redirect($this->generateUrl('view', array('id' => $existingEntry['id'])));
}
$this->updateEntry($entry);
$this->get('session')->getFlashBag()->add(
'notice',

View file

@ -5,6 +5,7 @@ namespace Wallabag\CoreBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Wallabag\UserBundle\Entity\User;
/**
* Config.
@ -86,7 +87,7 @@ class Config
/*
* @param User $user
*/
public function __construct(\Wallabag\UserBundle\Entity\User $user)
public function __construct(User $user)
{
$this->user = $user;
$this->taggingRules = new ArrayCollection();
@ -181,7 +182,7 @@ class Config
*
* @return Config
*/
public function setUser(\Wallabag\UserBundle\Entity\User $user = null)
public function setUser(User $user = null)
{
$this->user = $user;
@ -225,7 +226,7 @@ class Config
/**
* Set rssLimit.
*
* @param string $rssLimit
* @param int $rssLimit
*
* @return Config
*/
@ -239,7 +240,7 @@ class Config
/**
* Get rssLimit.
*
* @return string
* @return int
*/
public function getRssLimit()
{

View file

@ -245,7 +245,7 @@ class Entry
/**
* Set isArchived.
*
* @param string $isArchived
* @param bool $isArchived
*
* @return Entry
*/
@ -259,7 +259,7 @@ class Entry
/**
* Get isArchived.
*
* @return string
* @return bool
*/
public function isArchived()
{
@ -276,7 +276,7 @@ class Entry
/**
* Set isStarred.
*
* @param string $isStarred
* @param bool $isStarred
*
* @return Entry
*/
@ -290,7 +290,7 @@ class Entry
/**
* Get isStarred.
*
* @return string
* @return bool
*/
public function isStarred()
{

View file

@ -223,4 +223,29 @@ class EntryRepository extends EntityRepository
->getQuery()
->getResult();
}
/**
* Find an entry by its url and its owner.
* If it exists, return the entry otherwise return false.
*
* @param $url
* @param $userId
*
* @return array|bool
*/
public function existByUrlAndUserId($url, $userId)
{
$res = $this->createQueryBuilder('e')
->select('e.id, e.createdAt')
->where('e.url = :url')->setParameter('url', $url)
->andWhere('e.user = :user_id')->setParameter('user_id', $userId)
->getQuery()
->getResult();
if (count($res)) {
return current($res);
}
return false;
}
}

View file

@ -63,6 +63,7 @@ services:
- @wallabag_core.tag_repository
- @wallabag_core.entry_repository
# repository as a service
wallabag_core.entry_repository:
class: Wallabag\CoreBundle\Repository\EntryRepository
factory: [ @doctrine.orm.default_entity_manager, getRepository ]

View file

@ -13,6 +13,7 @@ archive: 'Lus'
all: 'Tous les articles'
tags: 'Tags'
config: 'Configuration'
import: 'Importer'
howto: 'Aide'
logout: 'Déconnexion'
Filtered: 'Articles filtrés'
@ -128,3 +129,14 @@ Download: 'Télécharger'
Does this article appear wrong?: "Est-ce que cet article s'affiche mal ?"
Problems?: 'Un problème ?'
Edit title: "Modifier le titre"
# Import
Welcome on wallabag importer. Please select your previous service that you want to migrate.: "Bienvenue dans l'outil de migration de wallabag. Choisissez ci-dessous le service depuis lequel vous souhaitez migrer."
"This importer will import all your Pocket data. Pocket doesn't allow us to retrieve content from their service, so the readable content of each article will be re-fetched by wallabag.": "Cet outil va importer toutes vos données de Pocket. Pocket ne nous autorise pas à récupérer le contenu depuis leur service, donc wallabag doit reparcourir chaque article pour récupérer son contenu."
"This importer will import all your wallabag v1 articles. On your config page, click on \"JSON export\" in the \"Export your wallabag data\" section. You will have a \"wallabag-export-1-xxxx-xx-xx.json\" file.": "Cet outil va importer toutes vos données de wallabag v1. Sur votre page de configuration de wallabag v1, cliquez sur \"Export JSON\" dans la section \"Exporter vos données de wallabag\". Vous allez récupérer un fichier \"wallabag-export-1-xxxx-xx-xx.json\"."
"You can import your data from your Pocket account. You just have to click on the below button and authorize the application to connect to getpocket.com.": "Vous pouvez importer vos données depuis votre compte Pocket. Vous n'avez qu'à cliquer sur le bouton ci-dessous et à autoriser wallabag à se connecter à getpocket.com."
Connect to Pocket and import data: Se connecter à Pocket et importer les données.
Please select your wallabag export and click on the below button to upload and import it.: Choisissez le fichier de votre export wallabag v1 et cliquez sur le bouton ci-dessous pour l'importer.
File: Fichier
Upload file: Importer le fichier
Import contents: "Importer les contenus"

View file

@ -45,6 +45,7 @@
<li class="bold border-bottom {% if currentRoute == 'all' %}active{% endif %}"><a class="waves-effect" href="{{ path('all') }}">{% trans %}all{% endtrans %}</a></li>
<li class="bold border-bottom {% if currentRoute == 'tags' %}active{% endif %}"><a class="waves-effect" href="{{ path('tag') }}">{% trans %}tags{% endtrans %}</a></li>
<li class="bold {% if currentRoute == 'config' %}active{% endif %}"><a class="waves-effect" href="{{ path('config') }}">{% trans %}config{% endtrans %}</a></li>
<li class="bold {% if currentRoute == 'import' %}active{% endif %}"><a class="waves-effect" href="{{ path('import') }}">{% trans %}import{% endtrans %}</a></li>
<li class="bold {% if currentRoute == 'howto' %}active{% endif %}"><a class="waves-effect" href="{{ path('howto') }}">{% trans %}howto{% endtrans %}</a></li>
<li class="bold"><a class="waves-effect" class="icon icon-power" href="{{ path('fos_user_security_logout') }}" title="{% trans %}logout{% endtrans %}">{% trans %}logout{% endtrans %}</a></li>
</ul>

View file

@ -500,4 +500,8 @@ footer [class^="icon-"]:hover, footer [class*=" icon-"]:hover {
/* force height on non-input field in the settings page */
div.settings div.input-field div, div.settings div.input-field ul {
margin-top: 40px;
}
}
/* but avoid to kill all file input */
div.settings div.file-field div {
margin-top: inherit;
}

View file

@ -0,0 +1,53 @@
<?php
namespace Wallabag\ImportBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ImportCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
->setName('wallabag:import-v1')
->setDescription('Import entries from a JSON export from a wallabag v1 instance')
->addArgument('userId', InputArgument::REQUIRED, 'User ID to populate')
->addArgument('filepath', InputArgument::REQUIRED, 'Path to the JSON file')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln('Start : '.(new \DateTime())->format('d-m-Y G:i:s').' ---');
$em = $this->getContainer()->get('doctrine')->getManager();
// Turning off doctrine default logs queries for saving memory
$em->getConnection()->getConfiguration()->setSQLLogger(null);
$user = $em->getRepository('WallabagUserBundle:User')->findOneById($input->getArgument('userId'));
if (!is_object($user)) {
throw new Exception(sprintf('User with id "%s" not found', $input->getArgument('userId')));
}
$wallabag = $this->getContainer()->get('wallabag_import.wallabag_v1.import');
$res = $wallabag
->setUser($user)
->setFilepath($input->getArgument('filepath'))
->import();
if (true === $res) {
$summary = $wallabag->getSummary();
$output->writeln('<info>'.$summary['imported'].' imported</info>');
$output->writeln('<comment>'.$summary['skipped'].' already saved</comment>');
}
$em->clear();
$output->writeln('End : '.(new \DateTime())->format('d-m-Y G:i:s').' ---');
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Wallabag\ImportBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class ImportController extends Controller
{
/**
* @Route("/", name="import")
*/
public function importAction()
{
return $this->render('WallabagImportBundle:Import:index.html.twig', [
'imports' => $this->get('wallabag_import.chain')->getAll(),
]);
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace Wallabag\ImportBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class PocketController extends Controller
{
/**
* @Route("/pocket", name="import_pocket")
*/
public function indexAction()
{
return $this->render('WallabagImportBundle:Pocket:index.html.twig', [
'import' => $this->get('wallabag_import.pocket.import'),
]);
}
/**
* @Route("/pocket/auth", name="import_pocket_auth")
*/
public function authAction()
{
$requestToken = $this->get('wallabag_import.pocket.import')
->getRequestToken($this->generateUrl('import', [], true));
$this->get('session')->set('import.pocket.code', $requestToken);
return $this->redirect(
'https://getpocket.com/auth/authorize?request_token='.$requestToken.'&redirect_uri='.$this->generateUrl('import_pocket_callback', [], true),
301
);
}
/**
* @Route("/pocket/callback", name="import_pocket_callback")
*/
public function callbackAction()
{
$message = 'Import failed, please try again.';
$pocket = $this->get('wallabag_import.pocket.import');
// something bad happend on pocket side
if (false === $pocket->authorize($this->get('session')->get('import.pocket.code'))) {
$this->get('session')->getFlashBag()->add(
'notice',
$message
);
return $this->redirect($this->generateUrl('import_pocket'));
}
if (true === $pocket->import()) {
$summary = $pocket->getSummary();
$message = 'Import summary: '.$summary['imported'].' imported, '.$summary['skipped'].' already saved.';
}
$this->get('session')->getFlashBag()->add(
'notice',
$message
);
return $this->redirect($this->generateUrl('homepage'));
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Wallabag\ImportBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;
use Wallabag\ImportBundle\Form\Type\UploadImportType;
class WallabagV1Controller extends Controller
{
/**
* @Route("/wallabag-v1", name="import_wallabag_v1")
*/
public function indexAction(Request $request)
{
$form = $this->createForm(new UploadImportType());
$form->handleRequest($request);
$wallabag = $this->get('wallabag_import.wallabag_v1.import');
if ($form->isValid()) {
$file = $form->get('file')->getData();
$name = $this->getUser()->getId().'.json';
if (in_array($file->getClientMimeType(), $this->getParameter('wallabag_import.allow_mimetypes')) && $file->move($this->getParameter('wallabag_import.resource_dir'), $name)) {
$res = $wallabag
->setUser($this->getUser())
->setFilepath($this->getParameter('wallabag_import.resource_dir').'/'.$name)
->import();
$message = 'Import failed, please try again.';
if (true === $res) {
$summary = $wallabag->getSummary();
$message = 'Import summary: '.$summary['imported'].' imported, '.$summary['skipped'].' already saved.';
unlink($this->getParameter('wallabag_import.resource_dir').'/'.$name);
}
$this->get('session')->getFlashBag()->add(
'notice',
$message
);
return $this->redirect($this->generateUrl('homepage'));
} else {
$this->get('session')->getFlashBag()->add(
'notice',
'Error while processing import. Please verify your import file.'
);
}
}
return $this->render('WallabagImportBundle:WallabagV1:index.html.twig', [
'form' => $form->createView(),
'import' => $wallabag,
]);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Wallabag\ImportBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('wallabag_import');
$rootNode
->children()
->arrayNode('allow_mimetypes')
->prototype('scalar')->end()
->end()
->scalarNode('resource_dir')
->end()
->end()
;
return $treeBuilder;
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Wallabag\ImportBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
class WallabagImportExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$container->setParameter('wallabag_import.allow_mimetypes', $config['allow_mimetypes']);
$container->setParameter('wallabag_import.resource_dir', $config['resource_dir']);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
}
public function getAlias()
{
return 'wallabag_import';
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Wallabag\ImportBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class UploadImportType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('file', 'file')
->add('save', 'submit')
;
}
public function getName()
{
return 'upload_import_file';
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Wallabag\ImportBundle\Import;
class ImportChain
{
private $imports;
public function __construct()
{
$this->imports = [];
}
/**
* Add an import to the chain.
*
* @param ImportInterface $import
* @param string $alias
*/
public function addImport(ImportInterface $import, $alias)
{
$this->imports[$alias] = $import;
}
/**
* Get all imports.
*
* @return array<ImportInterface>
*/
public function getAll()
{
return $this->imports;
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Wallabag\ImportBundle\Import;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
class ImportCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('wallabag_import.chain')) {
return;
}
$definition = $container->getDefinition(
'wallabag_import.chain'
);
$taggedServices = $container->findTaggedServiceIds(
'wallabag_import.import'
);
foreach ($taggedServices as $id => $tagAttributes) {
foreach ($tagAttributes as $attributes) {
$definition->addMethodCall(
'addImport',
[new Reference($id), $attributes['alias']]
);
}
}
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace Wallabag\ImportBundle\Import;
use Psr\Log\LoggerAwareInterface;
interface ImportInterface extends LoggerAwareInterface
{
/**
* Name of the import.
*
* @return string
*/
public function getName();
/**
* Url to start the import.
*
* @return string
*/
public function getUrl();
/**
* Description of the import.
*
* @return string
*/
public function getDescription();
/**
* Import content using the user token.
*
* @return bool
*/
public function import();
/**
* Return an array with summary info about the import, with keys:
* - skipped
* - imported.
*
* @return array
*/
public function getSummary();
}

View file

@ -0,0 +1,267 @@
<?php
namespace Wallabag\ImportBundle\Import;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Doctrine\ORM\EntityManager;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Wallabag\CoreBundle\Entity\Entry;
use Wallabag\CoreBundle\Entity\Tag;
use Wallabag\CoreBundle\Helper\ContentProxy;
class PocketImport implements ImportInterface
{
private $user;
private $em;
private $contentProxy;
private $logger;
private $client;
private $consumerKey;
private $skippedEntries = 0;
private $importedEntries = 0;
protected $accessToken;
private $translator;
public function __construct(TokenStorageInterface $tokenStorage, EntityManager $em, ContentProxy $contentProxy, $consumerKey)
{
$this->user = $tokenStorage->getToken()->getUser();
$this->em = $em;
$this->contentProxy = $contentProxy;
$this->consumerKey = $consumerKey;
$this->logger = new NullLogger();
}
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'Pocket';
}
/**
* {@inheritdoc}
*/
public function getUrl()
{
return 'import_pocket';
}
/**
* {@inheritdoc}
*/
public function getDescription()
{
return 'This importer will import all your Pocket data. Pocket doesn\'t allow us to retrieve content from their service, so the readable content of each article will be re-fetched by wallabag.';
}
/**
* Return the oauth url to authenticate the client.
*
* @param string $redirectUri Redirect url in case of error
*
* @return string request_token for callback method
*/
public function getRequestToken($redirectUri)
{
$request = $this->client->createRequest('POST', 'https://getpocket.com/v3/oauth/request',
[
'body' => json_encode([
'consumer_key' => $this->consumerKey,
'redirect_uri' => $redirectUri,
]),
]
);
try {
$response = $this->client->send($request);
} catch (RequestException $e) {
$this->logger->error(sprintf('PocketImport: Failed to request token: %s', $e->getMessage()), ['exception' => $e]);
return false;
}
return $response->json()['code'];
}
/**
* Usually called by the previous callback to authorize the client.
* Then it return a token that can be used for next requests.
*
* @param string $code request_token from getRequestToken
*
* @return bool
*/
public function authorize($code)
{
$request = $this->client->createRequest('POST', 'https://getpocket.com/v3/oauth/authorize',
[
'body' => json_encode([
'consumer_key' => $this->consumerKey,
'code' => $code,
]),
]
);
try {
$response = $this->client->send($request);
} catch (RequestException $e) {
$this->logger->error(sprintf('PocketImport: Failed to authorize client: %s', $e->getMessage()), ['exception' => $e]);
return false;
}
$this->accessToken = $response->json()['access_token'];
return true;
}
/**
* {@inheritdoc}
*/
public function import()
{
$request = $this->client->createRequest('POST', 'https://getpocket.com/v3/get',
[
'body' => json_encode([
'consumer_key' => $this->consumerKey,
'access_token' => $this->accessToken,
'detailType' => 'complete',
'state' => 'all',
'sort' => 'oldest',
]),
]
);
try {
$response = $this->client->send($request);
} catch (RequestException $e) {
$this->logger->error(sprintf('PocketImport: Failed to import: %s', $e->getMessage()), ['exception' => $e]);
return false;
}
$entries = $response->json();
$this->parseEntries($entries['list']);
return true;
}
/**
* {@inheritdoc}
*/
public function getSummary()
{
return [
'skipped' => $this->skippedEntries,
'imported' => $this->importedEntries,
];
}
/**
* Set the Guzzle client.
*
* @param Client $client
*/
public function setClient(Client $client)
{
$this->client = $client;
}
/**
* @todo move that in a more global place
*/
private function assignTagsToEntry(Entry $entry, $tags)
{
foreach ($tags as $tag) {
$label = trim($tag['tag']);
$tagEntity = $this->em
->getRepository('WallabagCoreBundle:Tag')
->findOneByLabel($label);
if (is_object($tagEntity)) {
$entry->addTag($tagEntity);
} else {
$newTag = new Tag();
$newTag->setLabel($label);
$entry->addTag($newTag);
}
$this->em->flush();
}
}
/**
* @see https://getpocket.com/developer/docs/v3/retrieve
*
* @param $entries
*/
private function parseEntries($entries)
{
$i = 1;
foreach ($entries as $pocketEntry) {
$url = isset($pocketEntry['resolved_url']) && $pocketEntry['resolved_url'] != '' ? $pocketEntry['resolved_url'] : $pocketEntry['given_url'];
$existingEntry = $this->em
->getRepository('WallabagCoreBundle:Entry')
->existByUrlAndUserId($url, $this->user->getId());
if (false !== $existingEntry) {
++$this->skippedEntries;
continue;
}
$entry = new Entry($this->user);
$entry = $this->contentProxy->updateEntry($entry, $url);
// 0, 1, 2 - 1 if the item is archived - 2 if the item should be deleted
if ($pocketEntry['status'] == 1) {
$entry->setArchived(true);
}
// 0 or 1 - 1 If the item is favorited
if ($pocketEntry['favorite'] == 1) {
$entry->setStarred(true);
}
$title = 'Untitled';
if (isset($pocketEntry['resolved_title']) && $pocketEntry['resolved_title'] != '') {
$title = $pocketEntry['resolved_title'];
} elseif (isset($pocketEntry['given_title']) && $pocketEntry['given_title'] != '') {
$title = $pocketEntry['given_title'];
}
$entry->setTitle($title);
// 0, 1, or 2 - 1 if the item has images in it - 2 if the item is an image
if (isset($pocketEntry['has_image']) && $pocketEntry['has_image'] > 0 && isset($pocketEntry['images'][1])) {
$entry->setPreviewPicture($pocketEntry['images'][1]['src']);
}
if (isset($pocketEntry['tags']) && !empty($pocketEntry['tags'])) {
$this->assignTagsToEntry($entry, $pocketEntry['tags']);
}
$this->em->persist($entry);
++$this->importedEntries;
// flush every 20 entries
if (($i % 20) === 0) {
$this->em->flush();
}
++$i;
}
$this->em->flush();
}
}

View file

@ -0,0 +1,159 @@
<?php
namespace Wallabag\ImportBundle\Import;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Doctrine\ORM\EntityManager;
use Wallabag\CoreBundle\Entity\Entry;
use Wallabag\UserBundle\Entity\User;
use Wallabag\CoreBundle\Tools\Utils;
class WallabagV1Import implements ImportInterface
{
private $user;
private $em;
private $logger;
private $skippedEntries = 0;
private $importedEntries = 0;
private $filepath;
public function __construct(EntityManager $em)
{
$this->em = $em;
$this->logger = new NullLogger();
}
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* We define the user in a custom call because on the import command there is no logged in user.
* So we can't retrieve user from the `security.token_storage` service.
*
* @param User $user
*/
public function setUser(User $user)
{
$this->user = $user;
return $this;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'wallabag v1';
}
/**
* {@inheritdoc}
*/
public function getUrl()
{
return 'import_wallabag_v1';
}
/**
* {@inheritdoc}
*/
public function getDescription()
{
return 'This importer will import all your wallabag v1 articles. On your config page, click on "JSON export" in the "Export your wallabag data" section. You will have a "wallabag-export-1-xxxx-xx-xx.json" file.';
}
/**
* {@inheritdoc}
*/
public function import()
{
if (!$this->user) {
$this->logger->error('WallabagV1Import: user is not defined');
return false;
}
if (!file_exists($this->filepath) || !is_readable($this->filepath)) {
$this->logger->error('WallabagV1Import: unable to read file', array('filepath' => $this->filepath));
return false;
}
$data = json_decode(file_get_contents($this->filepath), true);
if (empty($data)) {
return false;
}
$this->parseEntries($data);
return true;
}
/**
* {@inheritdoc}
*/
public function getSummary()
{
return [
'skipped' => $this->skippedEntries,
'imported' => $this->importedEntries,
];
}
/**
* Set file path to the json file.
*
* @param string $filepath
*/
public function setFilepath($filepath)
{
$this->filepath = $filepath;
return $this;
}
/**
* @param $entries
*/
private function parseEntries($entries)
{
$i = 1;
foreach ($entries as $importedEntry) {
$existingEntry = $this->em
->getRepository('WallabagCoreBundle:Entry')
->existByUrlAndUserId($importedEntry['url'], $this->user->getId());
if (false !== $existingEntry) {
++$this->skippedEntries;
continue;
}
// @see ContentProxy->updateEntry
$entry = new Entry($this->user);
$entry->setUrl($importedEntry['url']);
$entry->setTitle($importedEntry['title']);
$entry->setArchived($importedEntry['is_read']);
$entry->setStarred($importedEntry['is_fav']);
$entry->setContent($importedEntry['content']);
$entry->setReadingTime(Utils::getReadingTime($importedEntry['content']));
$entry->setDomainName(parse_url($importedEntry['url'], PHP_URL_HOST));
$this->em->persist($entry);
++$this->importedEntries;
// flush every 20 entries
if (($i % 20) === 0) {
$this->em->flush();
}
++$i;
}
$this->em->flush();
}
}

View file

@ -0,0 +1,34 @@
services:
wallabag_import.chain:
class: Wallabag\ImportBundle\Import\ImportChain
wallabag_import.pocket.client:
class: GuzzleHttp\Client
arguments:
-
defaults:
headers:
content-type: "application/json"
X-Accept: "application/json"
wallabag_import.pocket.import:
class: Wallabag\ImportBundle\Import\PocketImport
arguments:
- "@security.token_storage"
- "@doctrine.orm.entity_manager"
- "@wallabag_core.content_proxy"
- %pocket_consumer_key%
calls:
- [ setClient, [ "@wallabag_import.pocket.client" ] ]
- [ setLogger, [ "@logger" ]]
tags:
- { name: wallabag_import.import, alias: pocket }
wallabag_import.wallabag_v1.import:
class: Wallabag\ImportBundle\Import\WallabagV1Import
arguments:
- "@doctrine.orm.entity_manager"
calls:
- [ setLogger, [ "@logger" ]]
tags:
- { name: wallabag_import.import, alias: wallabag_v1 }

View file

@ -0,0 +1,21 @@
{% extends "WallabagCoreBundle::layout.html.twig" %}
{% block title %}{% trans %}Import{% endtrans %}{% endblock %}
{% block content %}
<div class="row">
<div class="col s12">
<div class="card-panel settings">
{% trans %}Welcome on wallabag importer. Please select your previous service that you want to migrate.{% endtrans %}
<ul>
{% for import in imports %}
<li>
<h5>{{ import.name }}</h5>
<blockquote>{{ import.description|trans }}</blockquote>
<p><a class="waves-effect waves-light btn" href="{{ path(import.url) }}">{% trans %}Import contents{% endtrans %}</a></p>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,18 @@
{% extends "WallabagCoreBundle::layout.html.twig" %}
{% block title %}{% trans %}Import > Pocket{% endtrans %}{% endblock %}
{% block content %}
<div class="row">
<div class="col s12">
<div class="card-panel settings">
<blockquote>{{ import.description|trans }}</blockquote>
<p>{% trans %}You can import your data from your Pocket account. You just have to click on the below button and authorize the application to connect to getpocket.com.{% endtrans %}</p>
<form method="post" action="{{ path('import_pocket_auth') }}">
<button class="btn waves-effect waves-light" type="submit" name="action">
{% trans %}Connect to Pocket and import data{% endtrans %}
</button>
</form>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,36 @@
{% extends "WallabagCoreBundle::layout.html.twig" %}
{% block title %}{% trans %}Import > Wallabag v1{% endtrans %}{% endblock %}
{% block content %}
<div class="row">
<div class="col s12">
<div class="card-panel settings">
<div class="row">
<blockquote>{{ import.description|trans }}</blockquote>
<p>{% trans %}Please select your wallabag export and click on the below button to upload and import it.{% endtrans %}</p>
<div class="col s12">
{{ form_start(form, {'method': 'POST'}) }}
{{ form_errors(form) }}
<div class="row">
<div class="file-field input-field col s12">
{{ form_errors(form.file) }}
<div class="btn">
<span>{% trans %}File{% endtrans %}</span>
{{ form_widget(form.file) }}
</div>
<div class="file-path-wrapper">
<input class="file-path validate" type="text">
</div>
</div>
</div>
<div class="hidden">{{ form_rest(form) }}</div>
<button class="btn waves-effect waves-light" type="submit" name="action">
{% trans %}Upload file{% endtrans %}
</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,29 @@
<?php
namespace Wallabag\ImportBundle\Tests\Controller;
use Wallabag\CoreBundle\Tests\WallabagCoreTestCase;
class ImportControllerTest extends WallabagCoreTestCase
{
public function testLogin()
{
$client = $this->getClient();
$client->request('GET', '/import/');
$this->assertEquals(302, $client->getResponse()->getStatusCode());
$this->assertContains('login', $client->getResponse()->headers->get('location'));
}
public function testImportList()
{
$this->logInAs('admin');
$client = $this->getClient();
$crawler = $client->request('GET', '/import/');
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$this->assertEquals(2, $crawler->filter('blockquote')->count());
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Wallabag\ImportBundle\Tests\Controller;
use Wallabag\CoreBundle\Tests\WallabagCoreTestCase;
class PocketControllerTest extends WallabagCoreTestCase
{
public function testImportPocket()
{
$this->logInAs('admin');
$client = $this->getClient();
$crawler = $client->request('GET', '/import/pocket');
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$this->assertEquals(1, $crawler->filter('button[type=submit]')->count());
}
public function testImportPocketAuth()
{
$this->logInAs('admin');
$client = $this->getClient();
$crawler = $client->request('GET', '/import/pocket/auth');
$this->assertEquals(301, $client->getResponse()->getStatusCode());
$this->assertContains('getpocket.com/auth/authorize', $client->getResponse()->headers->get('location'));
}
public function testImportPocketCallbackWithBadToken()
{
$this->logInAs('admin');
$client = $this->getClient();
$crawler = $client->request('GET', '/import/pocket/callback');
$this->assertEquals(302, $client->getResponse()->getStatusCode());
$this->assertContains('import/pocket', $client->getResponse()->headers->get('location'));
$this->assertEquals('Import failed, please try again.', $client->getContainer()->get('session')->getFlashBag()->peek('notice')[0]);
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace Wallabag\ImportBundle\Tests\Controller;
use Wallabag\CoreBundle\Tests\WallabagCoreTestCase;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class WallabagV1ControllerTest extends WallabagCoreTestCase
{
public function testImportWallabag()
{
$this->logInAs('admin');
$client = $this->getClient();
$crawler = $client->request('GET', '/import/wallabag-v1');
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$this->assertEquals(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count());
$this->assertEquals(1, $crawler->filter('input[type=file]')->count());
}
public function testImportWallabagWithFile()
{
$this->logInAs('admin');
$client = $this->getClient();
$crawler = $client->request('GET', '/import/wallabag-v1');
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$file = new UploadedFile(__DIR__.'/../fixtures/wallabag-v1.json', 'wallabag-v1.json');
$data = array(
'upload_import_file[file]' => $file,
);
$client->submit($form, $data);
$this->assertEquals(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
$this->assertGreaterThan(1, $alert = $crawler->filter('div.messages.success')->extract(array('_text')));
$this->assertContains('Import summary', $alert[0]);
}
public function testImportWallabagWithEmptyFile()
{
$this->logInAs('admin');
$client = $this->getClient();
$crawler = $client->request('GET', '/import/wallabag-v1');
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$file = new UploadedFile(__DIR__.'/../fixtures/test.txt', 'test.txt');
$data = array(
'upload_import_file[file]' => $file,
);
$client->submit($form, $data);
$this->assertEquals(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
$this->assertGreaterThan(1, $alert = $crawler->filter('div.messages.success')->extract(array('_text')));
$this->assertContains('Import failed, please try again', $alert[0]);
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Wallabag\ImportBundle\Tests\Import;
use Wallabag\ImportBundle\Import\ImportChain;
class ImportChainTest extends \PHPUnit_Framework_TestCase
{
public function testGetAll()
{
$import = $this->getMockBuilder('Wallabag\ImportBundle\Import\ImportInterface')
->disableOriginalConstructor()
->getMock();
$importChain = new ImportChain();
$importChain->addImport($import, 'alias');
$this->assertCount(1, $importChain->getAll());
$this->assertEquals($import, $importChain->getAll()['alias']);
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Wallabag\ImportBundle\Tests\Import;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Wallabag\ImportBundle\Import\ImportCompilerPass;
class ImportCompilerPassTest extends \PHPUnit_Framework_TestCase
{
public function testProcessNoDefinition()
{
$container = new ContainerBuilder();
$res = $this->process($container);
$this->assertNull($res);
}
public function testProcess()
{
$container = new ContainerBuilder();
$container
->register('wallabag_import.chain')
->setPublic(false)
;
$container
->register('foo')
->addTag('wallabag_import.import', array('alias' => 'pocket'))
;
$this->process($container);
$this->assertTrue($container->hasDefinition('wallabag_import.chain'));
$definition = $container->getDefinition('wallabag_import.chain');
$this->assertTrue($definition->hasMethodCall('addImport'));
$calls = $definition->getMethodCalls();
$this->assertEquals('pocket', $calls[0][1][1]);
}
protected function process(ContainerBuilder $container)
{
$repeatedPass = new ImportCompilerPass();
$repeatedPass->process($container);
}
}

View file

@ -0,0 +1,314 @@
<?php
namespace Wallabag\ImportBundle\Tests\Import;
use Wallabag\UserBundle\Entity\User;
use Wallabag\ImportBundle\Import\PocketImport;
use GuzzleHttp\Client;
use GuzzleHttp\Subscriber\Mock;
use GuzzleHttp\Message\Response;
use GuzzleHttp\Stream\Stream;
use Monolog\Logger;
use Monolog\Handler\TestHandler;
class PocketImportMock extends PocketImport
{
public function getAccessToken()
{
return $this->accessToken;
}
}
class PocketImportTest extends \PHPUnit_Framework_TestCase
{
protected $token;
protected $user;
protected $em;
protected $contentProxy;
protected $logHandler;
private function getPocketImport($consumerKey = 'ConsumerKey')
{
$this->user = new User();
$this->tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')
->disableOriginalConstructor()
->getMock();
$token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')
->disableOriginalConstructor()
->getMock();
$this->contentProxy = $this->getMockBuilder('Wallabag\CoreBundle\Helper\ContentProxy')
->disableOriginalConstructor()
->getMock();
$token->expects($this->once())
->method('getUser')
->willReturn($this->user);
$this->tokenStorage->expects($this->once())
->method('getToken')
->willReturn($token);
$this->em = $this->getMockBuilder('Doctrine\ORM\EntityManager')
->disableOriginalConstructor()
->getMock();
$pocket = new PocketImportMock(
$this->tokenStorage,
$this->em,
$this->contentProxy,
$consumerKey
);
$this->logHandler = new TestHandler();
$logger = new Logger('test', array($this->logHandler));
$pocket->setLogger($logger);
return $pocket;
}
public function testInit()
{
$pocketImport = $this->getPocketImport();
$this->assertEquals('Pocket', $pocketImport->getName());
$this->assertNotEmpty($pocketImport->getUrl());
$this->assertContains('This importer will import all your Pocket data.', $pocketImport->getDescription());
}
public function testOAuthRequest()
{
$client = new Client();
$mock = new Mock([
new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['code' => 'wunderbar_code']))),
]);
$client->getEmitter()->attach($mock);
$pocketImport = $this->getPocketImport();
$pocketImport->setClient($client);
$code = $pocketImport->getRequestToken('http://0.0.0.0/redirect');
$this->assertEquals('wunderbar_code', $code);
}
public function testOAuthRequestBadResponse()
{
$client = new Client();
$mock = new Mock([
new Response(403),
]);
$client->getEmitter()->attach($mock);
$pocketImport = $this->getPocketImport();
$pocketImport->setClient($client);
$code = $pocketImport->getRequestToken('http://0.0.0.0/redirect');
$this->assertFalse($code);
$records = $this->logHandler->getRecords();
$this->assertContains('PocketImport: Failed to request token', $records[0]['message']);
$this->assertEquals('ERROR', $records[0]['level_name']);
}
public function testOAuthAuthorize()
{
$client = new Client();
$mock = new Mock([
new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))),
]);
$client->getEmitter()->attach($mock);
$pocketImport = $this->getPocketImport();
$pocketImport->setClient($client);
$res = $pocketImport->authorize('wunderbar_code');
$this->assertTrue($res);
$this->assertEquals('wunderbar_token', $pocketImport->getAccessToken());
}
public function testOAuthAuthorizeBadResponse()
{
$client = new Client();
$mock = new Mock([
new Response(403),
]);
$client->getEmitter()->attach($mock);
$pocketImport = $this->getPocketImport();
$pocketImport->setClient($client);
$res = $pocketImport->authorize('wunderbar_code');
$this->assertFalse($res);
$records = $this->logHandler->getRecords();
$this->assertContains('PocketImport: Failed to authorize client', $records[0]['message']);
$this->assertEquals('ERROR', $records[0]['level_name']);
}
/**
* Will sample results from https://getpocket.com/developer/docs/v3/retrieve.
*/
public function testImport()
{
$client = new Client();
$mock = new Mock([
new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))),
new Response(200, ['Content-Type' => 'application/json'], Stream::factory('
{
"status": 1,
"list": {
"229279689": {
"item_id": "229279689",
"resolved_id": "229279689",
"given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
"given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland",
"favorite": "1",
"status": "1",
"resolved_title": "The Massive Ryder Cup Preview",
"resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
"excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.",
"is_article": "1",
"has_video": "1",
"has_image": "1",
"word_count": "3197",
"images": {
"1": {
"item_id": "229279689",
"image_id": "1",
"src": "http://a.espncdn.com/combiner/i?img=/photo/2012/0927/grant_g_ryder_cr_640.jpg&w=640&h=360",
"width": "0",
"height": "0",
"credit": "Jamie Squire/Getty Images",
"caption": ""
}
},
"videos": {
"1": {
"item_id": "229279689",
"video_id": "1",
"src": "http://www.youtube.com/v/Er34PbFkVGk?version=3&hl=en_US&rel=0",
"width": "420",
"height": "315",
"type": "1",
"vid": "Er34PbFkVGk"
}
},
"tags": {
"grantland": {
"item_id": "1147652870",
"tag": "grantland"
},
"Ryder Cup": {
"item_id": "1147652870",
"tag": "Ryder Cup"
}
}
},
"229279690": {
"item_id": "229279689",
"resolved_id": "229279689",
"given_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
"given_title": "The Massive Ryder Cup Preview - The Triangle Blog - Grantland",
"favorite": "1",
"status": "1",
"resolved_title": "The Massive Ryder Cup Preview",
"resolved_url": "http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview",
"excerpt": "The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them.",
"is_article": "1",
"has_video": "0",
"has_image": "0",
"word_count": "3197"
}
}
}
')),
]);
$client->getEmitter()->attach($mock);
$pocketImport = $this->getPocketImport();
$entryRepo = $this->getMockBuilder('Wallabag\CoreBundle\Repository\EntryRepository')
->disableOriginalConstructor()
->getMock();
$entryRepo->expects($this->exactly(2))
->method('existByUrlAndUserId')
->will($this->onConsecutiveCalls(false, true));
$tag = $this->getMockBuilder('Wallabag\CoreBundle\Entity\Tag')
->disableOriginalConstructor()
->getMock();
$tagRepo = $this->getMockBuilder('Wallabag\CoreBundle\Repository\TagRepository')
->disableOriginalConstructor()
->getMock();
$tagRepo->expects($this->exactly(2))
// the method `findOneByLabel` doesn't exist, EntityRepository will then call `_call` method
// to magically call the `findOneBy` with ['label' => 'foo']
->method('__call')
->will($this->onConsecutiveCalls(false, $tag));
$this->em
->expects($this->any())
->method('getRepository')
->will($this->onConsecutiveCalls($entryRepo, $tagRepo, $tagRepo, $entryRepo));
$entry = $this->getMockBuilder('Wallabag\CoreBundle\Entity\Entry')
->disableOriginalConstructor()
->getMock();
$this->contentProxy
->expects($this->once())
->method('updateEntry')
->willReturn($entry);
$pocketImport->setClient($client);
$pocketImport->authorize('wunderbar_code');
$res = $pocketImport->import();
$this->assertTrue($res);
$this->assertEquals(['skipped' => 1, 'imported' => 1], $pocketImport->getSummary());
}
public function testImportBadResponse()
{
$client = new Client();
$mock = new Mock([
new Response(200, ['Content-Type' => 'application/json'], Stream::factory(json_encode(['access_token' => 'wunderbar_token']))),
new Response(403),
]);
$client->getEmitter()->attach($mock);
$pocketImport = $this->getPocketImport();
$pocketImport->setClient($client);
$pocketImport->authorize('wunderbar_code');
$res = $pocketImport->import();
$this->assertFalse($res);
$records = $this->logHandler->getRecords();
$this->assertContains('PocketImport: Failed to import', $records[0]['message']);
$this->assertEquals('ERROR', $records[0]['level_name']);
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace Wallabag\ImportBundle\Tests\Import;
use Wallabag\UserBundle\Entity\User;
use Wallabag\ImportBundle\Import\WallabagV1Import;
use Monolog\Logger;
use Monolog\Handler\TestHandler;
class WallabagV1ImportTest extends \PHPUnit_Framework_TestCase
{
protected $user;
protected $em;
protected $logHandler;
private function getWallabagV1Import($unsetUser = false)
{
$this->user = new User();
$this->em = $this->getMockBuilder('Doctrine\ORM\EntityManager')
->disableOriginalConstructor()
->getMock();
$pocket = new WallabagV1Import($this->em);
$this->logHandler = new TestHandler();
$logger = new Logger('test', array($this->logHandler));
$pocket->setLogger($logger);
if (false === $unsetUser) {
$pocket->setUser($this->user);
}
return $pocket;
}
public function testInit()
{
$wallabagV1Import = $this->getWallabagV1Import();
$this->assertEquals('wallabag v1', $wallabagV1Import->getName());
$this->assertNotEmpty($wallabagV1Import->getUrl());
$this->assertContains('This importer will import all your wallabag v1 articles.', $wallabagV1Import->getDescription());
}
public function testImport()
{
$wallabagV1Import = $this->getWallabagV1Import();
$wallabagV1Import->setFilepath(__DIR__.'/../fixtures/wallabag-v1.json');
$entryRepo = $this->getMockBuilder('Wallabag\CoreBundle\Repository\EntryRepository')
->disableOriginalConstructor()
->getMock();
$entryRepo->expects($this->exactly(3))
->method('existByUrlAndUserId')
->will($this->onConsecutiveCalls(false, true, false));
$this->em
->expects($this->any())
->method('getRepository')
->willReturn($entryRepo);
$res = $wallabagV1Import->import();
$this->assertTrue($res);
$this->assertEquals(['skipped' => 1, 'imported' => 2], $wallabagV1Import->getSummary());
}
public function testImportBadFile()
{
$wallabagV1Import = $this->getWallabagV1Import();
$wallabagV1Import->setFilepath(__DIR__.'/../fixtures/wallabag-v1.jsonx');
$res = $wallabagV1Import->import();
$this->assertFalse($res);
$records = $this->logHandler->getRecords();
$this->assertContains('WallabagV1Import: unable to read file', $records[0]['message']);
$this->assertEquals('ERROR', $records[0]['level_name']);
}
public function testImportUserNotDefined()
{
$wallabagV1Import = $this->getWallabagV1Import(true);
$wallabagV1Import->setFilepath(__DIR__.'/../fixtures/wallabag-v1.json');
$res = $wallabagV1Import->import();
$this->assertFalse($res);
$records = $this->logHandler->getRecords();
$this->assertContains('WallabagV1Import: user is not defined', $records[0]['message']);
$this->assertEquals('ERROR', $records[0]['level_name']);
}
}

View file

View file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,17 @@
<?php
namespace Wallabag\ImportBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Wallabag\ImportBundle\Import\ImportCompilerPass;
class WallabagImportBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new ImportCompilerPass());
}
}

View file