diff --git a/CHANGELOG.md b/CHANGELOG.md index fc8639ea8..295b1b04c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ * **[BC BREAK]** Convert 403 errors to 404 errors by @yguedidi in https://github.com/wallabag/wallabag/pull/8075 * `wallassets/` folder renamed to `build/` +## [2.6.13](https://github.com/wallabag/wallabag/tree/2.6.13) +[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.12...2.6.13) + +### Improvements + +* Add support of Pocket CSV import by @kdecherf and @nicosomb in [https://github.com/wallabag/wallabag/pull/8240](https://github.com/wallabag/wallabag/pull/8240) +* Backport Pocket and Shaarli HTML imports from master by @nicosomb in [https://github.com/wallabag/wallabag/pull/8193](https://github.com/wallabag/wallabag/pull/8193) + +### Fixes + +* Avoid non-validated OTP to be enabled #8139 by @j0k3r in [https://github.com/wallabag/wallabag/pull/8139](https://github.com/wallabag/wallabag/pull/8139) + +### Technical stuff + +* Update j0k3r/php-readability:1.2.13 to fix regression (about latin1 instead of UTF-8 used for entries) by @nicosomb [https://github.com/wallabag/wallabag/pull/8194](https://github.com/wallabag/wallabag/pull/8194) + ## [2.6.12](https://github.com/wallabag/wallabag/tree/2.6.12) [Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.11...2.6.12) @@ -13,7 +29,6 @@ * Fix changelog by @yguedidi in [https://github.com/wallabag/wallabag/pull/8135](https://github.com/wallabag/wallabag/pull/8135) * Update dependencies by @yguedidi in [https://github.com/wallabag/wallabag/pull/8136](https://github.com/wallabag/wallabag/pull/8136) - ## [2.6.11](https://github.com/wallabag/wallabag/tree/2.6.11) [Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.10...2.6.11) diff --git a/app/config/config.yml b/app/config/config.yml index 19e139db1..e170430b7 100644 --- a/app/config/config.yml +++ b/app/config/config.yml @@ -305,6 +305,11 @@ old_sound_rabbit_mq: exchange_options: name: 'wallabag.import.pocket_html' type: topic + import_pocket_csv: + connection: default + exchange_options: + name: 'wallabag.import.pocket_csv' + type: topic consumers: import_pocket: connection: default @@ -423,6 +428,15 @@ old_sound_rabbit_mq: name: 'wallabag.import.pocket_html' callback: wallabag.consumer.amqp.pocket_html qos_options: {prefetch_count: "%rabbitmq_prefetch_count%"} + import_pocket_csv: + connection: default + exchange_options: + name: 'wallabag.import.pocket_csv' + type: topic + queue_options: + name: 'wallabag.import.pocket_csv' + callback: wallabag.consumer.amqp.pocket_csv + qos_options: {prefetch_count: "%rabbitmq_prefetch_count%"} fos_js_routing: routes_to_expose: diff --git a/app/config/services.yml b/app/config/services.yml index 3cca8b30f..620c71087 100644 --- a/app/config/services.yml +++ b/app/config/services.yml @@ -108,6 +108,11 @@ services: $rabbitMqProducer: '@old_sound_rabbit_mq.import_pocket_html_producer' $redisProducer: '@wallabag.producer.redis.pocket_html' + Wallabag\Controller\Import\PocketCsvController: + arguments: + $rabbitMqProducer: '@old_sound_rabbit_mq.import_pocket_csv_producer' + $redisProducer: '@wallabag.producer.redis.pocket_csv' + Wallabag\Doctrine\MigrationFactoryDecorator: decorates: doctrine.migrations.migrations_factory @@ -328,6 +333,10 @@ services: tags: - { name: wallabag.import, alias: pocket_html } + Wallabag\Import\PocketCsvImport: + tags: + - { name: wallabag.import, alias: pocket_csv } + # to factorize the proximity and bypass translation for prev & next pagerfanta.view.default_wallabag: class: Pagerfanta\View\OptionableView diff --git a/app/config/services_rabbit.yml b/app/config/services_rabbit.yml index ff00fc8df..8c83b2123 100644 --- a/app/config/services_rabbit.yml +++ b/app/config/services_rabbit.yml @@ -20,6 +20,7 @@ services: $elcuratorConsumer: '@old_sound_rabbit_mq.import_elcurator_consumer' $shaarliConsumer: '@old_sound_rabbit_mq.import_shaarli_consumer' $pocketHtmlConsumer: '@old_sound_rabbit_mq.import_pocket_html_consumer' + $pocketCsvConsumer: '@old_sound_rabbit_mq.import_pocket_csv_consumer' $omnivoreConsumer: '@old_sound_rabbit_mq.import_omnivore_consumer' wallabag.consumer.amqp.pocket: @@ -86,3 +87,8 @@ services: class: Wallabag\Consumer\AMQPEntryConsumer arguments: $import: '@Wallabag\Import\PocketHtmlImport' + + wallabag.consumer.amqp.pocket_csv: + class: Wallabag\Consumer\AMQPEntryConsumer + arguments: + $import: '@Wallabag\Import\PocketCsvImport' diff --git a/app/config/services_redis.yml b/app/config/services_redis.yml index 2065fe4f5..af9b5ad85 100644 --- a/app/config/services_redis.yml +++ b/app/config/services_redis.yml @@ -212,3 +212,19 @@ services: class: Wallabag\Consumer\RedisEntryConsumer arguments: $import: '@Wallabag\Import\PocketHtmlImport' + + # pocket csv + wallabag.queue.redis.pocket_csv: + class: Simpleue\Queue\RedisQueue + arguments: + $queueName: "wallabag.import.pocket_csv" + + wallabag.producer.redis.pocket_csv: + class: Wallabag\Redis\Producer + arguments: + - "@wallabag.queue.redis.pocket_csv" + + wallabag.consumer.redis.pocket_csv: + class: Wallabag\Consumer\RedisEntryConsumer + arguments: + $import: '@Wallabag\Import\PocketCsvImport' diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php index 6cc280497..d29916219 100644 --- a/composer-dependency-analyser.php +++ b/composer-dependency-analyser.php @@ -23,6 +23,7 @@ $config 'friendsoftwig/twigcs', 'incenteev/composer-parameter-handler', 'j0k3r/graby-site-config', + 'j0k3r/php-readability', 'laminas/laminas-code', 'lcobucci/jwt', 'mgargano/simplehtmldom', diff --git a/composer.json b/composer.json index fd95d1d4a..ffb939cb9 100644 --- a/composer.json +++ b/composer.json @@ -80,6 +80,7 @@ "incenteev/composer-parameter-handler": "^2.2", "j0k3r/graby": "^2.4.6", "j0k3r/graby-site-config": "^1.0.197", + "j0k3r/php-readability": "^1.2.13", "javibravo/simpleue": "^2.1", "jms/serializer": "^3.32.3", "jms/serializer-bundle": "^5.5.1", diff --git a/composer.lock b/composer.lock index 122a3321b..3710f5bae 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "00b8f95df6ec0572c06ad6f34847405c", + "content-hash": "8a12584ee6ea6887963779b321b4860e", "packages": [ { "name": "babdev/pagerfanta-bundle", @@ -4261,16 +4261,16 @@ }, { "name": "j0k3r/php-readability", - "version": "1.2.12", + "version": "1.2.13", "source": { "type": "git", "url": "https://github.com/j0k3r/php-readability.git", - "reference": "109a22662de0d703f01387e5714ad4f9a03b95c0" + "reference": "b9dde0f4cd46e9fc082bb37f75dc94ecd2f8faad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/j0k3r/php-readability/zipball/109a22662de0d703f01387e5714ad4f9a03b95c0", - "reference": "109a22662de0d703f01387e5714ad4f9a03b95c0", + "url": "https://api.github.com/repos/j0k3r/php-readability/zipball/b9dde0f4cd46e9fc082bb37f75dc94ecd2f8faad", + "reference": "b9dde0f4cd46e9fc082bb37f75dc94ecd2f8faad", "shasum": "" }, "require": { @@ -4332,7 +4332,7 @@ ], "support": { "issues": "https://github.com/j0k3r/php-readability/issues", - "source": "https://github.com/j0k3r/php-readability/tree/1.2.12" + "source": "https://github.com/j0k3r/php-readability/tree/1.2.13" }, "funding": [ { @@ -4340,7 +4340,7 @@ "type": "github" } ], - "time": "2025-03-04T09:20:40+00:00" + "time": "2025-06-03T08:02:58+00:00" }, { "name": "javibravo/simpleue", @@ -19449,6 +19449,6 @@ "ext-tokenizer": "*", "ext-xml": "*" }, - "platform-dev": [], - "plugin-api-version": "2.3.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/src/Command/Import/ImportCommand.php b/src/Command/Import/ImportCommand.php index 33436e059..aa225b03c 100644 --- a/src/Command/Import/ImportCommand.php +++ b/src/Command/Import/ImportCommand.php @@ -21,6 +21,7 @@ use Wallabag\Import\FirefoxImport; use Wallabag\Import\InstapaperImport; use Wallabag\Import\OmnivoreImport; use Wallabag\Import\PinboardImport; +use Wallabag\Import\PocketCsvImport; use Wallabag\Import\PocketHtmlImport; use Wallabag\Import\ReadabilityImport; use Wallabag\Import\ShaarliImport; @@ -48,6 +49,7 @@ class ImportCommand extends Command private readonly ElcuratorImport $elcuratorImport, private readonly ShaarliImport $shaarliImport, private readonly PocketHtmlImport $pocketHtmlImport, + private readonly PocketCsvImport $pocketCsvImport, private readonly OmnivoreImport $omnivoreImport, ) { parent::__construct(); @@ -58,7 +60,7 @@ class ImportCommand extends Command $this ->addArgument('username', InputArgument::REQUIRED, 'User to populate') ->addArgument('filepath', InputArgument::REQUIRED, 'Path to the JSON file') - ->addOption('importer', null, InputOption::VALUE_OPTIONAL, 'The importer to use: v1, v2, instapaper, pinboard, delicious, readability, firefox, chrome, elcurator, shaarli or pocket', 'v1') + ->addOption('importer', null, InputOption::VALUE_OPTIONAL, 'The importer to use: v1, v2, instapaper, pinboard, delicious, readability, firefox, chrome, elcurator, shaarli, pocket or pocket_csv', 'v1') ->addOption('markAsRead', null, InputOption::VALUE_OPTIONAL, 'Mark all entries as read', false) ->addOption('useUserId', null, InputOption::VALUE_NONE, 'Use user id instead of username to find account') ->addOption('disableContentUpdate', null, InputOption::VALUE_NONE, 'Disable fetching updated content from URL') @@ -109,6 +111,7 @@ class ImportCommand extends Command 'elcurator' => $this->elcuratorImport, 'shaarli' => $this->shaarliImport, 'pocket' => $this->pocketHtmlImport, + 'pocket_csv' => $this->pocketCsvImport, 'omnivore' => $this->omnivoreImport, default => $this->wallabagV1Import, }; diff --git a/src/Consumer/RabbitMQConsumerTotalProxy.php b/src/Consumer/RabbitMQConsumerTotalProxy.php index b32a0653a..45125a3ac 100644 --- a/src/Consumer/RabbitMQConsumerTotalProxy.php +++ b/src/Consumer/RabbitMQConsumerTotalProxy.php @@ -23,6 +23,7 @@ class RabbitMQConsumerTotalProxy private readonly Consumer $elcuratorConsumer, private readonly Consumer $shaarliConsumer, private readonly Consumer $pocketHtmlConsumer, + private readonly Consumer $pocketCsvConsumer, private readonly Consumer $omnivoreConsumer, ) { } @@ -75,6 +76,9 @@ class RabbitMQConsumerTotalProxy case 'pocket_html': $consumer = $this->pocketHtmlConsumer; break; + case 'pocket_csv': + $consumer = $this->pocketCsvConsumer; + break; case 'omnivore': $consumer = $this->omnivoreConsumer; break; diff --git a/src/Controller/Import/ImportController.php b/src/Controller/Import/ImportController.php index 24ae654c0..8ce60437e 100644 --- a/src/Controller/Import/ImportController.php +++ b/src/Controller/Import/ImportController.php @@ -58,6 +58,7 @@ class ImportController extends AbstractController + $this->rabbitMQConsumerTotalProxy->getTotalMessage('elcurator') + $this->rabbitMQConsumerTotalProxy->getTotalMessage('shaarli') + $this->rabbitMQConsumerTotalProxy->getTotalMessage('pocket_html') + + $this->rabbitMQConsumerTotalProxy->getTotalMessage('pocket_csv') + $this->rabbitMQConsumerTotalProxy->getTotalMessage('omnivore') ; } catch (\Exception) { @@ -77,6 +78,7 @@ class ImportController extends AbstractController + $this->redisClient->llen('wallabag.import.elcurator') + $this->redisClient->llen('wallabag.import.shaarli') + $this->redisClient->llen('wallabag.import.pocket_html') + + $this->redisClient->llen('wallabag.import.pocket_csv') + $this->redisClient->llen('wallabag.import.omnivore') ; } catch (\Exception) { diff --git a/src/Controller/Import/PocketCsvController.php b/src/Controller/Import/PocketCsvController.php new file mode 100644 index 000000000..c76e2cd6a --- /dev/null +++ b/src/Controller/Import/PocketCsvController.php @@ -0,0 +1,46 @@ +craueConfig->get('import_with_rabbitmq')) { + $this->pocketCsvImport->setProducer($this->rabbitMqProducer); + } elseif ($this->craueConfig->get('import_with_redis')) { + $this->pocketCsvImport->setProducer($this->redisProducer); + } + + return $this->pocketCsvImport; + } + + protected function getImportTemplate() + { + return 'Import/PocketCsv/index.html.twig'; + } +} diff --git a/src/Import/PocketCsvImport.php b/src/Import/PocketCsvImport.php new file mode 100644 index 000000000..eaa293fcb --- /dev/null +++ b/src/Import/PocketCsvImport.php @@ -0,0 +1,133 @@ +filepath = $filepath; + + return $this; + } + + public function validateEntry(array $importedEntry) + { + if (empty($importedEntry['url'])) { + return false; + } + + return true; + } + + public function import() + { + if (!$this->user) { + $this->logger->error('Pocket CSV Import: user is not defined'); + + return false; + } + + if (!file_exists($this->filepath) || !is_readable($this->filepath)) { + $this->logger->error('Pocket CSV Import: unable to read file', ['filepath' => $this->filepath]); + + return false; + } + + $entries = []; + $handle = fopen($this->filepath, 'r'); + while (false !== ($data = fgetcsv($handle, 10240))) { + if ('title' === $data[0]) { + continue; + } + + $entries[] = [ + 'url' => $data[1], + 'title' => $data[0], + 'is_archived' => 'archive' === $data[4], + 'created_at' => $data[2], + 'tags' => $data[3], + ]; + } + fclose($handle); + + if (empty($entries)) { + $this->logger->error('PocketCsvImport: no entries in imported file'); + + return false; + } + + if ($this->producer) { + $this->parseEntriesForProducer($entries); + + return true; + } + + $this->parseEntries($entries); + + return true; + } + + public function parseEntry(array $importedEntry) + { + $existingEntry = $this->em + ->getRepository(Entry::class) + ->findByUrlAndUserId($importedEntry['url'], $this->user->getId()); + + if (false !== $existingEntry) { + ++$this->skippedEntries; + + return null; + } + + $entry = new Entry($this->user); + $entry->setUrl($importedEntry['url']); + $entry->setTitle($importedEntry['title']); + + // update entry with content (in case fetching failed, the given entry will be return) + $this->fetchContent($entry, $importedEntry['url'], $importedEntry); + + if (!empty($importedEntry['tags'])) { + $tags = str_replace('|', ',', $importedEntry['tags']); + $this->tagsAssigner->assignTagsToEntry( + $entry, + $tags, + $this->em->getUnitOfWork()->getScheduledEntityInsertions() + ); + } + + $entry->updateArchived($importedEntry['is_archived']); + $entry->setCreatedAt(\DateTime::createFromFormat('U', $importedEntry['created_at'])); + + $this->em->persist($entry); + ++$this->importedEntries; + + return $entry; + } + + protected function setEntryAsRead(array $importedEntry) + { + $importedEntry['is_archived'] = 'archive'; + + return $importedEntry; + } +} diff --git a/templates/Import/PocketCsv/index.html.twig b/templates/Import/PocketCsv/index.html.twig new file mode 100644 index 000000000..77c63ca59 --- /dev/null +++ b/templates/Import/PocketCsv/index.html.twig @@ -0,0 +1,47 @@ +{% extends "layout.html.twig" %} + +{% block title %}{{ 'import.pocket_csv.page_title'|trans }}{% endblock %} + +{% block content %} +
{{ import.description|trans|raw }}+
{{ 'import.pocket_csv.how_to'|trans }}
+ +
diff --git a/tests/fixtures/Import/test.csv b/tests/fixtures/Import/test.csv new file mode 100644 index 000000000..e69de29bb diff --git a/translations/messages.en.yml b/translations/messages.en.yml index 2b330f368..fcf566acf 100644 --- a/translations/messages.en.yml +++ b/translations/messages.en.yml @@ -553,6 +553,10 @@ import: page_title: Import > Pocket HTML description: This importer will import all your Pocket bookmarks (via HTML export). Just go to https://getpocket.com/export, then export the HTML file. An HTML file will be downloaded (like "ril_export.html"). how_to: Please choose the bookmark backup file and click on the button below to import it. Note that the process may take a long time since all articles have to be fetched. + pocket_csv: + page_title: Import > Pocket CSV + description: This importer will import all your Pocket bookmarks (via CSV export). Just go to https://getpocket.com/export, then export the file. A ZIP file will be downloaded (like "pocket.zip"). Extract it, you will obtain a CSV file, called "part_000000.csv". + how_to: Please choose the bookmark backup file and click on the button below to import it. Note that the process may take a long time since all articles have to be fetched. developer: page_title: API clients management welcome_message: Welcome to the wallabag API diff --git a/translations/messages.fr.yml b/translations/messages.fr.yml index a0b3b1935..0f9017d23 100644 --- a/translations/messages.fr.yml +++ b/translations/messages.fr.yml @@ -550,6 +550,10 @@ import: page_title: Importer > Pocket HTML description: Cet importateur importera toutes vos signets Pocket (via exportation HTML). Il suffit d'aller à https://getpocket.com/export, puis d'exporter le fichier HTML. Un fichier HTML sera téléchargé (comme « ril_export.html »). how_to: Veuillez choisir le fichier de sauvegarde de signets et cliquez sur le bouton ci-dessous pour l'importer. Pensez au fait que le processus peut prendre longtemps puisque tous les articles doivent être récupérés. + pocket_csv: + page_title: Importer > Pocket CSV + description: Cet importateur importera toutes vos signets Pocket (via exportation CSV). Il suffit d'aller à https://getpocket.com/export, puis d'exporter le fichier. Un fichier ZIP sera téléchargé (comme « pocket.zip »). Décompressez le et vous obtiendrez un fichier CSV appelé "part_000000.csv". + how_to: Veuillez choisir le fichier de sauvegarde de signets et cliquez sur le bouton ci-dessous pour l'importer. Pensez au fait que le processus peut prendre longtemps puisque tous les articles doivent être récupérés. omnivore: description: Cet outil va importer tous vos articles depuis Omnivore. page_title: Importer > Omnivore