Compare commits

..

79 Commits

Author SHA1 Message Date
6ffd7382c7 Merge pull request #8229 from wallabag/prepare-2613
Prepare 2.6.13 release
2025-06-04 17:29:19 +02:00
46d6e4d923 Update CHANGELOG 2025-06-04 13:59:00 +02:00
09af127446 Unlock php-readability 2025-06-04 13:59:00 +02:00
4c23196304 Prepare 2.6.13 release 2025-06-04 13:59:00 +02:00
42746f418e Merge pull request #8250 from wallabag/update-site-config
Update joker/graby-site-config
2025-06-04 13:54:23 +02:00
35c4feedd8 Update joker/graby-site-config 2025-06-04 13:42:08 +02:00
c451cc96e5 Merge pull request #8240 from wallabag/import/pocket-csv
Add support of Pocket CSV import
2025-06-03 13:32:51 +02:00
c4240c866b PocketCsvImport: rework test to ensure we import all entries
This also adds a test for #8217

Signed-off-by: Kevin Decherf <kevin@kdecherf.com>
2025-06-03 13:24:27 +02:00
27d66d9e1d Add 'application/vnd.ms-excel' to allowed MIME types config
Backport #8036
2025-06-03 13:24:27 +02:00
c1397f43ac Add Pocket CSV import 2025-06-03 13:24:27 +02:00
52a16bb75f Merge pull request #8193 from wallabag/add-pocket-shaarli-import
Backport Pocket and Shaarli imports from master
2025-06-02 14:23:33 +02:00
f82c87b520 import: fix some tests
Signed-off-by: Kevin Decherf <kevin@kdecherf.com>
2025-06-02 13:42:44 +02:00
772a802596 Merge pull request #8194 from wallabag/fix-8158
Add j0k3r/php-readability:1.2.10 as a dependency
2025-06-01 16:51:35 +02:00
29162bde9d Pin j0k3r/php-readability to 1.2.10
We pin php-readability to 1.2.10 because of a regression in 1.2.12

Workaround for #8151

Signed-off-by: Kevin Decherf <kevin@kdecherf.com>
2025-06-01 16:37:22 +02:00
b1614e9267 Add Pocket and Shaarli imports 2025-05-24 16:25:48 +02:00
70999075a6 Merge pull request #8139 from wallabag/fix/otp-wrongly-enabled
Avoid non-validated OTP to be enabled
2025-04-14 09:46:17 +02:00
262f674245 Avoid non-validated OTP to be enabled
The OTP code must be required when enabling OTP. If the provided code is wrong, disable OTP, redirect and notice the user.
2025-04-14 09:18:37 +02:00
01ffc6c3d5 Merge pull request #8137 from wallabag/prepare-2.6.12-release
Prepare 2.6.12 release
2025-04-10 11:59:57 +02:00
5586930376 Prepare 2.6.12 release 2025-04-10 11:39:50 +02:00
b45116b73e Merge pull request #8136 from wallabag/update-dependencies
Update dependencies
2025-04-10 11:32:25 +02:00
c2e38cedac Update dependencies 2025-04-10 11:22:33 +02:00
35dcc43366 Merge pull request #8135 from wallabag/fix-changelog
Fix changelog
2025-04-10 11:10:58 +02:00
de8f859536 Fix changelog 2025-04-10 10:59:31 +02:00
5cdac6c0bb Merge pull request #8133 from wallabag/prepare-2.6.11-release
Prepare 2.6.11 release
2025-04-08 22:42:32 +02:00
466cd17d5b Prepare 2.6.11 release 2025-04-08 21:23:58 +02:00
14cdd123ce Update generated assets 2025-04-08 21:23:58 +02:00
bdb420b13f Use ubuntu latest in all jobs 2025-04-08 21:16:07 +02:00
99c8a06594 Merge commit from fork
Protect actions with a CSRF token
2025-04-08 21:00:14 +02:00
4c52f71895 Add SameSite=lax to session cookie 2025-03-30 06:18:32 +02:00
677b2986bc Use 400 Bad Request errors for invalid CSRF everywhere 2025-03-30 06:18:32 +02:00
5ea5115a72 Protect mass_action with a CSRF token 2025-03-30 06:18:32 +02:00
27f0d94db7 Protect tag_delete with a CSRF token 2025-03-30 06:18:32 +02:00
cf49be6940 Protect tag_this_search with a CSRF token 2025-03-30 06:18:32 +02:00
ddf2e80842 Protect remove_tag with a CSRF token 2025-03-30 06:18:32 +02:00
d1e128900a Protect delete_share with a CSRF token 2025-03-30 06:18:32 +02:00
0d8429dfc7 Protect share with a CSRF token 2025-03-30 06:18:32 +02:00
eb8408b22f Protect delete_entry with a CSRF token 2025-03-30 06:18:32 +02:00
00d0e6f951 Protect star_entry with a CSRF token 2025-03-30 06:18:32 +02:00
edffef8375 Protect archive_entry with a CSRF token 2025-03-30 06:18:32 +02:00
3817010e29 Protect reload_entry with a CSRF token 2025-03-30 06:18:32 +02:00
ed1acf59e1 Protect changeLocale with a CSRF token 2025-03-30 06:18:29 +02:00
e162408139 Protect switch_view_mode with a CSRF token 2025-03-23 19:13:21 +01:00
6fa61c0f9c Protect delete_ignore_origin_rule with a CSRF token 2025-03-23 19:13:17 +01:00
264f91126e Protect delete_tagging_rule with a CSRF token 2025-03-23 19:13:14 +01:00
ac5b5fb379 Protect revoke_token with a CSRF token 2025-03-23 19:13:09 +01:00
d703fa6a3a Protect generate_token with a CSRF token 2025-03-23 19:13:06 +01:00
f71d8332e0 Merge pull request #7999 from wallabag/fix/menu-entry-with-annotations
Fix entries counter for annotated entries in the menu
2025-02-10 10:12:45 +01:00
3dffcadc03 Fix entries counter for annotated entries in the menu
The query were badly made and return all annotations for the current user instead of the total of entries with annotation(s).
2025-02-10 08:42:06 +01:00
fab0c02ba0 Merge pull request #7993 from wallabag/fix/pocket-api-import 2025-02-07 21:26:07 +01:00
c4857564f3 Change NB_ELEMENTS in pocket importer to 30 to comply with Pocket API restriction. 2025-02-07 18:51:37 +01:00
c7c74de4b8 Merge pull request #7846 from wallabag/fix-title-tag-filter
Fix title tag filter
2024-11-22 14:05:14 +01:00
08b68d4d87 Display tag label instead of tag slug in page title 2024-11-22 13:49:08 +01:00
93e877f086 Merge pull request #7827 from wallabag/fix-redirection-action-search
Fix redirection after action in search results
2024-11-21 13:57:12 +01:00
82430b50c6 Fix redirection after action in search results 2024-11-21 13:36:20 +01:00
89db5690a0 Merge pull request #7767 from wallabag/prepare-2610
Prepare 2.6.10 release
2024-11-03 16:00:31 +01:00
d4171e9d63 Prepare 2.6.10 release 2024-11-03 08:05:50 +01:00
74175f2e54 Merge pull request #7754 from wallabag/add-omnivore-import
Added Omnivore Import
2024-11-01 11:11:12 +01:00
bd8ccf924f Added Omnivore Import 2024-11-01 11:05:16 +01:00
1cc321bec4 Merge pull request #7753 from wallabag/replace-gitter
Replaced gitter with matrix
2024-10-31 16:19:16 +01:00
048221dbcf Removed uncertain translations 2024-10-31 12:47:19 +01:00
7ddf5066ef Replaced gitter with matrix 2024-10-31 08:17:40 +01:00
5d13648420 Merge pull request #7624 from wallabag/prepare-2.6.10
Prepare 2.6.10
2024-08-19 08:05:06 +02:00
f2c72e1569 Prepare 2.6.10 2024-08-19 07:28:28 +02:00
5ac6788715 Merge pull request #7623 from wallabag/update-site-config
Update site config
2024-08-19 07:16:46 +02:00
d757612c77 Update site config 2024-08-17 22:16:29 +02:00
b6faa844cb Merge pull request #7582 from wallabag/fix/2.6-site-config
Update site config & tests
2024-07-15 17:14:15 +02:00
898890c371 Fix tests 2024-07-15 13:11:18 +02:00
580c5fe810 Downgrade lcobucci/jwt because of auth problem 2024-07-15 10:54:21 +02:00
bee59d1c4a Update site config 2024-07-15 10:44:49 +02:00
f8f7f962ce Merge pull request #7395 from wallabag/release/2.6.9
Prepare 2.6.9
2024-04-03 10:56:40 +02:00
54ba9a6da8 Prepare 2.6.9
Also update deps.
2024-04-03 09:26:58 +02:00
8cceb89261 Merge pull request #7323 from wallabag/fix/2.6-elcurator-how-to
Use a proper "how to" for elCurator
2024-03-08 09:35:04 +01:00
09c2ddb79e Use a proper "how to" for elCurator 2024-03-05 15:46:40 +01:00
7246b4c1db Merge pull request #7272 from yguedidi/upgrade-php-dependencies
Upgrade PHP dependencies
2024-02-19 11:27:08 +01:00
87ce9fd48d Normalize composer.json 2024-02-19 11:08:46 +01:00
db55cfbc0a Bump dependencies 2024-02-19 11:05:25 +01:00
3e18b66e62 Upgrade PHP dependencies 2024-02-19 11:04:17 +01:00
bc16155ec2 Merge pull request #7266 from yguedidi/fix-same-domain-pagination
Fix same domain pagination
2024-02-19 07:03:08 +01:00
a4820b21ca Fix same domain pagination 2024-02-18 23:29:59 +01:00
109 changed files with 5531 additions and 1450 deletions

View File

@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Want to ask something?
url: https://gitter.im/wallabag/wallabag
about: Use Gitter to ask questions.
url: https://matrix.to/#/#wallabag:matrix.org
about: Use Matrix to ask questions.

View File

@ -13,7 +13,7 @@ permissions:
jobs:
js:
name: "Building assets"
runs-on: "ubuntu-20.04"
runs-on: ubuntu-latest
steps:
- name: "Checkout"

View File

@ -13,7 +13,7 @@ permissions:
jobs:
coding-standards:
name: "CS Fixer, PHPStan & TwigCS"
runs-on: "ubuntu-20.04"
runs-on: ubuntu-latest
steps:
- name: "Checkout"

View File

@ -14,7 +14,7 @@ env:
jobs:
phpunit:
name: "PHP ${{ matrix.php }} using ${{ matrix.database }}"
runs-on: "ubuntu-20.04"
runs-on: ubuntu-latest
services:
rabbitmq:
image: rabbitmq:3-alpine
@ -83,7 +83,7 @@ jobs:
phpunit_no_prefix:
name: "PHP ${{ matrix.php }} using ${{ matrix.database }} without prefix"
runs-on: "ubuntu-20.04"
runs-on: ubuntu-latest
services:
rabbitmq:
image: rabbitmq:3-alpine

View File

@ -13,7 +13,7 @@ permissions:
jobs:
translations:
name: "Translations"
runs-on: "ubuntu-20.04"
runs-on: ubuntu-latest
strategy:
matrix:

View File

@ -1,5 +1,68 @@
# Changelog
## [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)
### Technical stuff
* 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)
### Security fix
* Protect actions with a CSRF token by @yguedidi in https://github.com/wallabag/wallabag/commit/99c8a06594d6ee7480ce4d041ccff3025b353656
### Fixes
* Fix redirection after action in search results by @nicosomb in [https://github.com/wallabag/wallabag/pull/7827](https://github.com/wallabag/wallabag/pull/7827)
* Fix title tag filter by @nicosomb in [https://github.com/wallabag/wallabag/pull/7846](https://github.com/wallabag/wallabag/pull/7846)
* Change NB_ELEMENTS in pocket importer to 30 by @j0k3r in [https://github.com/wallabag/wallabag/pull/7993](https://github.com/wallabag/wallabag/pull/7993)
* Fix entries counter for annotated entries in the menu by @j0k3r in [https://github.com/wallabag/wallabag/pull/7999](https://github.com/wallabag/wallabag/pull/7999)
### Technical stuff
* Prepare 2.6.11 release by @yguedidi in [https://github.com/wallabag/wallabag/pull/8133](https://github.com/wallabag/wallabag/pull/8133)
## [2.6.10](https://github.com/wallabag/wallabag/tree/2.6.10)
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.9...2.6.10)
### Improvement
* Add Omnivore import by @nicosomb in https://github.com/wallabag/wallabag/pull/7754
### Fixes
* Update site config & tests by @j0k3r in https://github.com/wallabag/wallabag/pull/7582 (fixes "Key provided is shorter
than 256 bits, only 240 bits provided" https://github.com/wallabag/wallabag/issues/7531)
* Update site config by @yguedidi in https://github.com/wallabag/wallabag/pull/7623
* Replace gitter with matrix by @nicosomb in https://github.com/wallabag/wallabag/pull/7753
## [2.6.9](https://github.com/wallabag/wallabag/tree/2.6.9)
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.8...2.6.9)
### Fixes
* Fix same domain pagination by @yguedidi in https://github.com/wallabag/wallabag/pull/7266
* Upgrade PHP dependencies by @yguedidi in https://github.com/wallabag/wallabag/pull/7272
* Use a proper "how to" for elCurator by @j0k3r in https://github.com/wallabag/wallabag/pull/7323
## [2.6.8](https://github.com/wallabag/wallabag/tree/2.6.8)
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.7...2.6.8)

View File

@ -1,7 +1,7 @@
# wallabag
![CI](https://github.com/wallabag/wallabag/workflows/CI/badge.svg)
[![Gitter](https://badges.gitter.im/gitterHQ/gitter.svg)](https://gitter.im/wallabag/wallabag)
[![Matrix](https://matrix.to/img/matrix-badge.svg)](https://matrix.to/#/#wallabag:matrix.org)
[![Donation Status](https://img.shields.io/liberapay/goal/wallabag.svg?logo=liberapay)](https://liberapay.com/wallabag/donate)
[![Translation status](https://hosted.weblate.org/widgets/wallabag/-/svg-badge.svg)](https://hosted.weblate.org/engage/wallabag/?utm_source=widget)
![License](https://img.shields.io/github/license/wallabag/wallabag)

View File

@ -177,6 +177,7 @@ a.original:not(.waves-effect) {
.card-entry-tags a,
.card-entry-labels a,
.card-tag-labels a,
.card-tag-labels button,
.card-entry-labels-hidden a,
#list .chip a {
text-decoration: none;

View File

@ -62,7 +62,9 @@
.nav-panels .input-field input:focus,
.results-item,
.side-nav li > a,
.side-nav li > a > i.material-icons {
.side-nav li > a > i.material-icons,
.side-nav li button,
.side-nav li button > i.material-icons {
color: #dfdfdf;
}
@ -87,6 +89,7 @@
.mass-action-tags .mass-action-tags-input.mass-action-tags-input,
.side-nav li:not(.logo) > a:hover,
.side-nav li:not(.logo) button:hover,
.side-nav .collapsible-header:hover,
.side-nav.fixed .collapsible-header:hover {
background-color: #1d1d1d;

View File

@ -6,11 +6,32 @@ nav {
line-height: initial;
}
// adapted from anchor styles from node_modules/materialize-css/sass/components/_navbar.scss
nav ul button {
transition: background-color .3s;
font-size: 1rem;
color: #fff;
display: block;
padding: 0 15px;
cursor: pointer;
background: none;
border: 0;
&:focus {
background: none;
}
&:hover {
background-color: rgba(0 0 0 / 10%);
}
}
nav {
input {
color: #aaa;
}
ul button:hover,
ul a:hover {
background-color: initial;
}
@ -34,6 +55,7 @@ nav {
justify-content: space-between;
align-items: center;
button,
a {
padding: 10px 15px;
}

View File

@ -12,6 +12,7 @@
background: initial;
}
& button > i.material-icons.theme-toggle-icon,
& > a > i.material-icons.theme-toggle-icon {
float: none;
margin-left: 0;
@ -22,6 +23,7 @@
margin: 0;
}
&.fixed button,
&.fixed a {
font-size: 13px;
line-height: 44px;
@ -41,7 +43,35 @@
}
}
.bold > a {
// adapted from anchor styles from node_modules/materialize-css/sass/components/_sideNav.scss
.side-nav li button {
color: rgba(0 0 0 / 87%);
display: block;
font-size: 14px;
font-weight: 500;
height: 48px;
line-height: 48px;
padding: 0 (16px * 2);
width: 100%;
text-align: left;
&:hover {
background-color: rgba(0 0 0 / 5%);
}
& > i,
& > i.material-icons {
float: left;
height: 48px;
line-height: 48px;
margin: 0 (16px * 2) 0 0;
width: 24px;
color: rgba(0 0 0 / 54%);
}
}
.bold > a,
.bold > button {
font-weight: bold;
}

View File

@ -38,3 +38,18 @@ nav .input-field input {
.tab {
flex: 1;
}
.btn-link {
background: none;
border: 0;
padding: 0;
color: $blue-accent-color;
&:focus {
background: none;
}
}
.inline-block {
display: inline-block;
}

View File

@ -228,10 +228,10 @@ $(document).ready(() => {
});
});
}
$('form[name="form_mass_action"] input[name="tags"]').on('keydown', (e) => {
$('input[name="tags"][form="form_mass_action"]').on('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
$('form[name="form_mass_action"] button[name="tag"]').trigger('click');
$('button[name="tag"][form="form_mass_action"]').trigger('click');
}
});
});

View File

@ -10,17 +10,17 @@ $(document).ready(() => {
/* mark as favorite */
Mousetrap.bind('f', () => {
$('ul.side-nav a.favorite i')[0].click();
$('ul.side-nav button.favorite i')[0].click();
});
/* mark as read */
Mousetrap.bind('a', () => {
$('ul.side-nav a.markasread i')[0].click();
$('ul.side-nav button.markasread i')[0].click();
});
/* delete */
Mousetrap.bind('del', () => {
$('ul.side-nav a.delete i')[0].click();
$('ul.side-nav button.delete i')[0].click();
});
}
});

View File

@ -30,6 +30,7 @@ framework:
handler_id: session.handler.native_file
save_path: "%kernel.project_dir%/var/sessions/%kernel.environment%"
cookie_secure: auto
cookie_samesite: lax
fragments: ~
http_method_override: true
assets: ~
@ -267,6 +268,11 @@ old_sound_rabbit_mq:
exchange_options:
name: 'wallabag.import.elcurator'
type: topic
import_omnivore:
connection: default
exchange_options:
name: 'wallabag.import.omnivore'
type: topic
import_firefox:
connection: default
exchange_options:
@ -277,6 +283,21 @@ old_sound_rabbit_mq:
exchange_options:
name: 'wallabag.import.chrome'
type: topic
import_shaarli:
connection: default
exchange_options:
name: 'wallabag.import.shaarli'
type: topic
import_pocket_html:
connection: default
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
@ -350,6 +371,15 @@ old_sound_rabbit_mq:
name: 'wallabag.import.elcurator'
callback: wallabag_import.consumer.amqp.elcurator
qos_options: {prefetch_count: "%rabbitmq_prefetch_count%"}
import_omnivore:
connection: default
exchange_options:
name: 'wallabag.import.omnivore'
type: topic
queue_options:
name: 'wallabag.import.omnivore'
callback: wallabag_import.consumer.amqp.omnivore
qos_options: {prefetch_count: "%rabbitmq_prefetch_count%"}
import_firefox:
connection: default
exchange_options:
@ -368,6 +398,33 @@ old_sound_rabbit_mq:
name: 'wallabag.import.chrome'
callback: wallabag_import.consumer.amqp.chrome
qos_options: {prefetch_count: "%rabbitmq_prefetch_count%"}
import_shaarli:
connection: default
exchange_options:
name: 'wallabag.import.shaarli'
type: topic
queue_options:
name: 'wallabag.import.shaarli'
callback: wallabag_import.consumer.amqp.shaarli
qos_options: {prefetch_count: "%rabbitmq_prefetch_count%"}
import_pocket_html:
connection: default
exchange_options:
name: 'wallabag.import.pocket_html'
type: topic
queue_options:
name: 'wallabag.import.pocket_html'
callback: wallabag_import.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_import.consumer.amqp.pocket_csv
qos_options: {prefetch_count: "%rabbitmq_prefetch_count%"}
fos_js_routing:
routes_to_expose:

View File

@ -81,6 +81,11 @@ services:
$rabbitMqProducer: '@old_sound_rabbit_mq.import_elcurator_producer'
$redisProducer: '@wallabag_import.producer.redis.elcurator'
Wallabag\ImportBundle\Controller\OmnivoreController:
arguments:
$rabbitMqProducer: '@old_sound_rabbit_mq.import_omnivore_producer'
$redisProducer: '@wallabag_import.producer.redis.omnivore'
Wallabag\ImportBundle\Controller\FirefoxController:
arguments:
$rabbitMqProducer: '@old_sound_rabbit_mq.import_firefox_producer'
@ -116,6 +121,21 @@ services:
$rabbitMqProducer: '@old_sound_rabbit_mq.import_wallabag_v2_producer'
$redisProducer: '@wallabag_import.producer.redis.wallabag_v2'
Wallabag\ImportBundle\Controller\ShaarliController:
arguments:
$rabbitMqProducer: '@old_sound_rabbit_mq.import_shaarli_producer'
$redisProducer: '@wallabag_import.producer.redis.shaarli'
Wallabag\ImportBundle\Controller\PocketHtmlController:
arguments:
$rabbitMqProducer: '@old_sound_rabbit_mq.import_pocket_html_producer'
$redisProducer: '@wallabag_import.producer.redis.pocket_html'
Wallabag\ImportBundle\Controller\PocketCsvController:
arguments:
$rabbitMqProducer: '@old_sound_rabbit_mq.import_pocket_csv_producer'
$redisProducer: '@wallabag_import.producer.redis.pocket_csv'
Wallabag\ImportBundle\:
resource: '../../src/Wallabag/ImportBundle/*'
exclude: '../../src/Wallabag/ImportBundle/{Consumer,Controller,Redis}'
@ -377,6 +397,10 @@ services:
tags:
- { name: wallabag_import.import, alias: delicious }
Wallabag\ImportBundle\Import\OmnivoreImport:
tags:
- { name: wallabag_import.import, alias: omnivore }
Wallabag\ImportBundle\Import\FirefoxImport:
tags:
- { name: wallabag_import.import, alias: firefox }
@ -385,6 +409,18 @@ services:
tags:
- { name: wallabag_import.import, alias: chrome }
Wallabag\ImportBundle\Import\ShaarliImport:
tags:
- { name: wallabag_import.import, alias: shaarli }
Wallabag\ImportBundle\Import\PocketHtmlImport:
tags:
- { name: wallabag_import.import, alias: pocket_html }
Wallabag\ImportBundle\Import\PocketCsvImport:
tags:
- { name: wallabag_import.import, alias: pocket_csv }
# to factorize the proximity and bypass translation for prev & next
pagerfanta.view.default_wallabag:
class: Pagerfanta\View\OptionableView

View File

@ -18,6 +18,10 @@ services:
$pinboardConsumer: '@old_sound_rabbit_mq.import_pinboard_consumer'
$deliciousConsumer: '@old_sound_rabbit_mq.import_delicious_consumer'
$elcuratorConsumer: '@old_sound_rabbit_mq.import_elcurator_consumer'
$omnivoreConsumer: '@old_sound_rabbit_mq.import_omnivore_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'
wallabag_import.consumer.amqp.pocket:
class: Wallabag\ImportBundle\Consumer\AMQPEntryConsumer
@ -44,6 +48,11 @@ services:
arguments:
$import: '@Wallabag\ImportBundle\Import\DeliciousImport'
wallabag_import.consumer.amqp.omnivore:
class: Wallabag\ImportBundle\Consumer\AMQPEntryConsumer
arguments:
$import: '@Wallabag\ImportBundle\Import\OmnivoreImport'
wallabag_import.consumer.amqp.wallabag_v1:
class: Wallabag\ImportBundle\Consumer\AMQPEntryConsumer
arguments:
@ -68,3 +77,18 @@ services:
class: Wallabag\ImportBundle\Consumer\AMQPEntryConsumer
arguments:
$import: '@Wallabag\ImportBundle\Import\ChromeImport'
wallabag_import.consumer.amqp.shaarli:
class: Wallabag\ImportBundle\Consumer\AMQPEntryConsumer
arguments:
$import: '@Wallabag\ImportBundle\Import\ShaarliImport'
wallabag_import.consumer.amqp.pocket_html:
class: Wallabag\ImportBundle\Consumer\AMQPEntryConsumer
arguments:
$import: '@Wallabag\ImportBundle\Import\PocketHtmlImport'
wallabag_import.consumer.amqp.pocket_csv:
class: Wallabag\ImportBundle\Consumer\AMQPEntryConsumer
arguments:
$import: '@Wallabag\ImportBundle\Import\PocketCsvImport'

View File

@ -69,6 +69,22 @@ services:
arguments:
$import: '@Wallabag\ImportBundle\Import\DeliciousImport'
# Omnivore
wallabag_import.queue.redis.omnivore:
class: Simpleue\Queue\RedisQueue
arguments:
$queueName: "wallabag.import.omnivore"
wallabag_import.producer.redis.omnivore:
class: Wallabag\ImportBundle\Redis\Producer
arguments:
- "@wallabag_import.queue.redis.omnivore"
wallabag_import.consumer.redis.omnivore:
class: Wallabag\ImportBundle\Consumer\RedisEntryConsumer
arguments:
$import: '@Wallabag\ImportBundle\Import\OmnivoreImport'
# pocket
wallabag_import.queue.redis.pocket:
class: Simpleue\Queue\RedisQueue
@ -164,3 +180,51 @@ services:
class: Wallabag\ImportBundle\Consumer\RedisEntryConsumer
arguments:
$import: '@Wallabag\ImportBundle\Import\ChromeImport'
# shaarli
wallabag_import.queue.redis.shaarli:
class: Simpleue\Queue\RedisQueue
arguments:
$queueName: "wallabag.import.shaarli"
wallabag_import.producer.redis.shaarli:
class: Wallabag\ImportBundle\Redis\Producer
arguments:
- "@wallabag_import.queue.redis.shaarli"
wallabag_import.consumer.redis.shaarli:
class: Wallabag\ImportBundle\Consumer\RedisEntryConsumer
arguments:
$import: '@Wallabag\ImportBundle\Import\ShaarliImport'
# pocket html
wallabag_import.queue.redis.pocket_html:
class: Simpleue\Queue\RedisQueue
arguments:
$queueName: "wallabag.import.pocket_html"
wallabag_import.producer.redis.pocket_html:
class: Wallabag\ImportBundle\Redis\Producer
arguments:
- "@wallabag_import.queue.redis.pocket_html"
wallabag_import.consumer.redis.pocket_html:
class: Wallabag\ImportBundle\Consumer\RedisEntryConsumer
arguments:
$import: '@Wallabag\ImportBundle\Import\PocketHtmlImport'
# pocket csv
wallabag_import.queue.redis.pocket_csv:
class: Simpleue\Queue\RedisQueue
arguments:
$queueName: "wallabag.import.pocket_csv"
wallabag_import.producer.redis.pocket_csv:
class: Wallabag\ImportBundle\Redis\Producer
arguments:
- "@wallabag_import.queue.redis.pocket_csv"
wallabag_import.consumer.redis.pocket_csv:
class: Wallabag\ImportBundle\Consumer\RedisEntryConsumer
arguments:
$import: '@Wallabag\ImportBundle\Import\PocketCsvImport'

View File

@ -1,5 +1,5 @@
wallabag_core:
version: 2.6.8
version: 2.6.13
paypal_url: "https://liberapay.com/wallabag/donate"
languages:
en: 'English'
@ -167,5 +167,11 @@ wallabag_core:
rule: _all ~ "https?://www\.lemonde\.fr/tiny.*"
wallabag_import:
allow_mimetypes: ['application/octet-stream', 'application/json', 'text/plain', 'text/csv']
allow_mimetypes:
- 'application/octet-stream'
- 'application/json'
- 'text/plain'
- 'text/csv'
- 'text/html'
- 'application/vnd.ms-excel'
resource_dir: "%kernel.project_dir%/web/uploads/import"

View File

@ -56,124 +56,125 @@
"ext-tidy": "*",
"ext-tokenizer": "*",
"ext-xml": "*",
"babdev/pagerfanta-bundle": "^3.7",
"bdunogier/guzzle-site-authenticator": "^1.0.0",
"craue/config-bundle": "^2.3.0",
"defuse/php-encryption": "^2.1",
"doctrine/collections": "^1.6",
"doctrine/common": "^3.0",
"doctrine/dbal": "^3.3",
"doctrine/doctrine-bundle": "^2.0",
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/event-manager": "^1.1",
"doctrine/migrations": "^3.2",
"doctrine/orm": "^2.6",
"doctrine/persistence": "^3.0",
"egulias/email-validator": "^3.2",
"babdev/pagerfanta-bundle": "^3.8",
"bdunogier/guzzle-site-authenticator": "^1.1.0",
"craue/config-bundle": "^2.7.0",
"defuse/php-encryption": "^2.4",
"doctrine/collections": "^1.8",
"doctrine/common": "^3.4.3",
"doctrine/dbal": "^3.8.2",
"doctrine/doctrine-bundle": "^2.7.2",
"doctrine/doctrine-migrations-bundle": "^3.2.5",
"doctrine/event-manager": "^1.2",
"doctrine/migrations": "^3.5.5",
"doctrine/orm": "^2.18",
"doctrine/persistence": "^3.2",
"egulias/email-validator": "^3.2.6",
"enshrined/svg-sanitize": "^0.15.4",
"friendsofsymfony/jsrouting-bundle": "^2.2",
"friendsofsymfony/jsrouting-bundle": "^2.8",
"friendsofsymfony/oauth-server-bundle": "dev-master#dc8ff343363cf794d30eb1a123610d186a43f162",
"friendsofsymfony/rest-bundle": "~3.4",
"friendsofsymfony/user-bundle": "^3.1",
"guzzlehttp/guzzle": "^5.3.1",
"guzzlehttp/psr7": "^2.5",
"html2text/html2text": "^4.1",
"incenteev/composer-parameter-handler": "^2.1",
"j0k3r/graby": "^2.0",
"javibravo/simpleue": "^2.0",
"jms/serializer": "^3.17",
"jms/serializer-bundle": "~5.0",
"kphoen/rulerz": "^0.21",
"kphoen/rulerz-bundle": "~0.13",
"laminas/laminas-code": "^4.7",
"lcobucci/jwt": "~4.1.5",
"lexik/form-filter-bundle": "^7.0",
"mgargano/simplehtmldom": "~1.5",
"friendsofsymfony/rest-bundle": "^3.5",
"friendsofsymfony/user-bundle": "^3.2.1",
"guzzlehttp/guzzle": "^5.3.4",
"guzzlehttp/psr7": "^2.6.2",
"html2text/html2text": "^4.3.1",
"incenteev/composer-parameter-handler": "^2.1.5",
"j0k3r/graby": "^2.4.5",
"j0k3r/php-readability": "^1.2.13",
"javibravo/simpleue": "^2.1",
"jms/serializer": "^3.29.1",
"jms/serializer-bundle": "^5.3.1",
"kphoen/rulerz": "^0.21.1",
"kphoen/rulerz-bundle": "^0.15",
"laminas/laminas-code": "^4.7.1",
"lcobucci/jwt": "4.1.5",
"lexik/form-filter-bundle": "^7.0.3",
"mgargano/simplehtmldom": "^1.5",
"mnapoli/piwik-twig-extension": "^3.0",
"nelmio/api-doc-bundle": "^4.10",
"nelmio/cors-bundle": "~2.2",
"nelmio/api-doc-bundle": "^4.11.1",
"nelmio/cors-bundle": "^2.3.1",
"ocramius/proxy-manager": "^2.1.1",
"pagerfanta/doctrine-orm-adapter": "^3.7",
"pagerfanta/twig": "^3.7",
"php-amqplib/php-amqplib": "^3.4",
"php-amqplib/rabbitmq-bundle": "^2.11",
"php-http/client-common": "^2.4",
"php-http/discovery": "^1.14",
"pagerfanta/doctrine-orm-adapter": "^3.8",
"pagerfanta/twig": "^3.8",
"php-amqplib/php-amqplib": "^3.6.1",
"php-amqplib/rabbitmq-bundle": "^2.14",
"php-http/client-common": "^2.7.1",
"php-http/discovery": "^1.19.2",
"php-http/guzzle5-adapter": "^2.0",
"php-http/httplug": "^2.3",
"php-http/httplug-bundle": "^1.14",
"php-http/message": "^1.13",
"php-http/message-factory": "^1.0",
"pragmarx/recovery": "^0.2.0",
"predis/predis": "^2.0.3",
"psr/http-message": "^1.0",
"psr/log": "^1.1",
"scheb/2fa-backup-code": "^5.13",
"scheb/2fa-bundle": "^5.13",
"scheb/2fa-email": "^5.13",
"scheb/2fa-google-authenticator": "^5.13",
"scheb/2fa-qr-code": "^5.13",
"scheb/2fa-trusted-device": "^5.13",
"sensio/framework-extra-bundle": "^6.2",
"sentry/sentry-symfony": "4.10.0",
"stof/doctrine-extensions-bundle": "^1.2",
"symfony/asset": "^4.4",
"symfony/config": "^4.4",
"symfony/console": "^4.4",
"symfony/debug": "^4.4",
"symfony/dependency-injection": "^4.4",
"symfony/doctrine-bridge": "^4.4",
"symfony/dom-crawler": "^4.4",
"symfony/error-handler": "^4.4",
"symfony/event-dispatcher": "^4.4",
"symfony/finder": "^4.4",
"symfony/form": "^4.4",
"symfony/framework-bundle": "^4.4",
"symfony/google-mailer": "^4.4",
"symfony/http-foundation": "^4.4",
"symfony/http-kernel": "^4.4",
"symfony/mailer": "^4.4",
"symfony/mime": "^4.4",
"symfony/monolog-bundle": "^3.1",
"symfony/options-resolver": "^4.4",
"symfony/proxy-manager-bridge": "^4.4",
"symfony/routing": "^4.4",
"symfony/security-bundle": "^4.4",
"symfony/security-core": "^4.4",
"symfony/security-http": "^4.4",
"symfony/templating": "^4.4",
"symfony/twig-bundle": "^4.4",
"symfony/validator": "^4.4",
"tecnickcom/tcpdf": "^6.3.0",
"twig/extra-bundle": "^3.4",
"twig/string-extra": "^3.4",
"twig/twig": "^3.4.3",
"wallabag/php-mobi": "~1.0",
"php-http/httplug": "^2.4",
"php-http/httplug-bundle": "^1.32",
"php-http/message": "^1.16",
"php-http/message-factory": "^1.1",
"pragmarx/recovery": "^0.2.1",
"predis/predis": "^2.2.2",
"psr/http-message": "^1.1",
"psr/log": "^1.1.4",
"scheb/2fa-backup-code": "^5.13.2",
"scheb/2fa-bundle": "^5.13.2",
"scheb/2fa-email": "^5.13.2",
"scheb/2fa-google-authenticator": "^5.13.2",
"scheb/2fa-qr-code": "^5.13.2",
"scheb/2fa-trusted-device": "^5.13.2",
"sensio/framework-extra-bundle": "^6.2.10",
"sentry/sentry-symfony": "^4.13.2",
"stof/doctrine-extensions-bundle": "^1.7.2",
"symfony/asset": "^4.4.46",
"symfony/config": "^4.4.44",
"symfony/console": "^4.4.49",
"symfony/debug": "^4.4.44",
"symfony/dependency-injection": "^4.4.49",
"symfony/doctrine-bridge": "^4.4.48",
"symfony/dom-crawler": "^4.4.45",
"symfony/error-handler": "^4.4.44",
"symfony/event-dispatcher": "^4.4.44",
"symfony/finder": "^4.4.44",
"symfony/form": "^4.4.48",
"symfony/framework-bundle": "^4.4.51",
"symfony/google-mailer": "^4.4.41",
"symfony/http-foundation": "^4.4.49",
"symfony/http-kernel": "^4.4.51",
"symfony/mailer": "^4.4.49",
"symfony/mime": "^4.4.47",
"symfony/monolog-bundle": "^3.8",
"symfony/options-resolver": "^4.4.44",
"symfony/proxy-manager-bridge": "^4.4.39",
"symfony/routing": "^4.4.44",
"symfony/security-bundle": "^4.4.50",
"symfony/security-core": "^4.4.48",
"symfony/security-http": "^4.4.50",
"symfony/templating": "^4.4.44",
"symfony/twig-bundle": "^4.4.41",
"symfony/validator": "^4.4.48",
"tecnickcom/tcpdf": "^6.6.5",
"twig/extra-bundle": "^3.7",
"twig/string-extra": "^3.8",
"twig/twig": "^3.8.0",
"wallabag/php-mobi": "^1.1.1",
"wallabag/phpepub": "^4.0.10",
"willdurand/hateoas": "^3.8",
"willdurand/hateoas-bundle": "~2.1"
"willdurand/hateoas": "^3.10",
"willdurand/hateoas-bundle": "^2.6"
},
"require-dev": {
"dama/doctrine-test-bundle": "^7.1",
"doctrine/doctrine-fixtures-bundle": "~3.0",
"ergebnis/composer-normalize": "^2.28",
"friendsofphp/php-cs-fixer": "~3.4",
"friendsoftwig/twigcs": "^6.0",
"m6web/redis-mock": "^5.0",
"php-http/mock-client": "^1.0",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-doctrine": "^1.3",
"phpstan/phpstan-phpunit": "^1.1",
"phpstan/phpstan-symfony": "^1.2",
"symfony/browser-kit": "^4.4",
"symfony/css-selector": "^4.4",
"symfony/debug-bundle": "^4.4",
"symfony/maker-bundle": "^1.18",
"symfony/phpunit-bridge": "^6.0",
"symfony/var-dumper": "^4.4",
"symfony/web-profiler-bundle": "^4.4",
"symfony/web-server-bundle": "^4.4"
"dama/doctrine-test-bundle": "^7.1.1",
"doctrine/doctrine-fixtures-bundle": "^3.4.5",
"ergebnis/composer-normalize": "^2.42",
"friendsofphp/php-cs-fixer": "^3.4",
"friendsoftwig/twigcs": "^6.1",
"m6web/redis-mock": "^5.6",
"php-http/mock-client": "^1.6",
"phpstan/extension-installer": "^1.3.1",
"phpstan/phpstan": "^1.10.58",
"phpstan/phpstan-doctrine": "^1.3.62",
"phpstan/phpstan-phpunit": "^1.3.15",
"phpstan/phpstan-symfony": "^1.3.7",
"symfony/browser-kit": "^4.4.44",
"symfony/css-selector": "^4.4.44",
"symfony/debug-bundle": "^4.4.37",
"symfony/maker-bundle": "^1.39.1",
"symfony/phpunit-bridge": "^6.4.3",
"symfony/var-dumper": "^4.4.47",
"symfony/web-profiler-bundle": "^4.4.47",
"symfony/web-server-bundle": "^4.4.44"
},
"suggest": {
"ext-imagick": "To keep GIF animation when downloading image is enabled"
@ -196,9 +197,9 @@
},
"config": {
"allow-plugins": {
"phpstan/extension-installer": true,
"ergebnis/composer-normalize": true,
"php-http/discovery": true,
"ergebnis/composer-normalize": true
"phpstan/extension-installer": true
},
"bin-dir": "bin",
"platform": {

2200
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -57,5 +57,15 @@ parameters:
-
message: "#^Method FOS\\\\UserBundle\\\\Model\\\\UserManagerInterface\\:\\:updateUser()#"
count: 6
count: 7
path: src/Wallabag/CoreBundle/Controller/ConfigController.php
-
message: "#^Call to an undefined method Wallabag\\\\ImportBundle\\\\Import\\\\ImportInterface\\:\\:setUser\\(\\)\\.$#"
count: 1
path: src/Wallabag/ImportBundle/Controller/HtmlController.php
-
message: "#^Call to an undefined method Wallabag\\\\ImportBundle\\\\Import\\\\ImportInterface\\:\\:setFilepath\\(\\)\\.$#"
count: 1
path: src/Wallabag/ImportBundle/Controller/HtmlController.php

View File

@ -152,6 +152,12 @@ class AnnotationRepository extends ServiceEntityRepository
->getResult();
}
public function getCountBuilderByUser($userId = null)
{
return $this->createQueryBuilder('e')
->andWhere('e.user = :userId')->setParameter('userId', $userId);
}
/**
* Return a query builder to used by other getBuilderFor* method.
*

View File

@ -7,6 +7,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\ApiBundle\Entity\Client;
@ -76,7 +77,7 @@ class DeveloperController extends AbstractController
public function deleteClientAction(Request $request, Client $client, EntityManagerInterface $entityManager, TranslatorInterface $translator)
{
if (!$this->isCsrfTokenValid('delete-client', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
throw new BadRequestHttpException('Bad CSRF token.');
}
if (null === $this->getUser() || $client->getUser()->getId() !== $this->getUser()->getId()) {

View File

@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Validator\Constraints\Locale as LocaleConstraint;
@ -262,7 +263,7 @@ class ConfigController extends AbstractController
public function disableOtpEmailAction(Request $request)
{
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
throw new BadRequestHttpException('Bad CSRF token.');
}
$user = $this->getUser();
@ -286,7 +287,7 @@ class ConfigController extends AbstractController
public function otpEmailAction(Request $request)
{
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
throw new BadRequestHttpException('Bad CSRF token.');
}
$user = $this->getUser();
@ -313,7 +314,7 @@ class ConfigController extends AbstractController
public function disableOtpAppAction(Request $request)
{
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
throw new BadRequestHttpException('Bad CSRF token.');
}
$user = $this->getUser();
@ -339,7 +340,7 @@ class ConfigController extends AbstractController
public function otpAppAction(Request $request, GoogleAuthenticatorInterface $googleAuthenticator)
{
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
throw new BadRequestHttpException('Bad CSRF token.');
}
$user = $this->getUser();
@ -398,15 +399,17 @@ class ConfigController extends AbstractController
public function otpAppCheckAction(Request $request, GoogleAuthenticatorInterface $googleAuthenticator)
{
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
throw new BadRequestHttpException('Bad CSRF token.');
}
$user = $this->getUser();
$isValid = $googleAuthenticator->checkCode(
$this->getUser(),
$user,
$request->get('_auth_code')
);
if (true === $isValid) {
if ($isValid) {
$this->addFlash(
'notice',
'flashes.config.notice.otp_enabled'
@ -416,35 +419,35 @@ class ConfigController extends AbstractController
}
$this->addFlash(
'two_factor',
'scheb_two_factor.code_invalid'
'notice',
'flashes.config.notice.otp_code_invalid'
);
$this->addFlash(
'notice',
'scheb_two_factor.code_invalid'
);
$user->setGoogleAuthenticatorSecret(null);
$user->setBackupCodes(null);
$this->userManager->updateUser($user, true);
return $this->redirect($this->generateUrl('config') . '#set3');
}
/**
* @Route("/generate-token", name="generate_token")
* @Route("/generate-token", name="generate_token", methods={"POST"})
*
* @return RedirectResponse|JsonResponse
*/
public function generateTokenAction(Request $request)
{
if (!$this->isCsrfTokenValid('generate-token', $request->request->get('token'))) {
throw new BadRequestHttpException('Bad CSRF token.');
}
$config = $this->getConfig();
$config->setFeedToken(Utils::generateToken());
$this->entityManager->persist($config);
$this->entityManager->flush();
if ($request->isXmlHttpRequest()) {
return new JsonResponse(['token' => $config->getFeedToken()]);
}
$this->addFlash(
'notice',
'flashes.config.notice.feed_token_updated'
@ -454,22 +457,22 @@ class ConfigController extends AbstractController
}
/**
* @Route("/revoke-token", name="revoke_token")
* @Route("/revoke-token", name="revoke_token", methods={"POST"})
*
* @return RedirectResponse|JsonResponse
*/
public function revokeTokenAction(Request $request)
{
if (!$this->isCsrfTokenValid('revoke-token', $request->request->get('token'))) {
throw new BadRequestHttpException('Bad CSRF token.');
}
$config = $this->getConfig();
$config->setFeedToken(null);
$this->entityManager->persist($config);
$this->entityManager->flush();
if ($request->isXmlHttpRequest()) {
return new JsonResponse();
}
$this->addFlash(
'notice',
'flashes.config.notice.feed_token_revoked'
@ -481,12 +484,16 @@ class ConfigController extends AbstractController
/**
* Deletes a tagging rule and redirect to the config homepage.
*
* @Route("/tagging-rule/delete/{id}", requirements={"id" = "\d+"}, name="delete_tagging_rule")
* @Route("/tagging-rule/delete/{id}", name="delete_tagging_rule", methods={"POST"}, requirements={"id" = "\d+"})
*
* @return RedirectResponse
*/
public function deleteTaggingRuleAction(TaggingRule $rule)
public function deleteTaggingRuleAction(Request $request, TaggingRule $rule)
{
if (!$this->isCsrfTokenValid('delete-tagging-rule', $request->request->get('token'))) {
throw new BadRequestHttpException('Bad CSRF token.');
}
$this->validateRuleAction($rule);
$this->entityManager->remove($rule);
@ -517,12 +524,16 @@ class ConfigController extends AbstractController
/**
* Deletes an ignore origin rule and redirect to the config homepage.
*
* @Route("/ignore-origin-user-rule/delete/{id}", requirements={"id" = "\d+"}, name="delete_ignore_origin_rule")
* @Route("/ignore-origin-user-rule/delete/{id}", name="delete_ignore_origin_rule", methods={"POST"}, requirements={"id" = "\d+"})
*
* @return RedirectResponse
*/
public function deleteIgnoreOriginRuleAction(IgnoreOriginUserRule $rule)
public function deleteIgnoreOriginRuleAction(Request $request, IgnoreOriginUserRule $rule)
{
if (!$this->isCsrfTokenValid('delete-ignore-origin-rule', $request->request->get('token'))) {
throw new BadRequestHttpException('Bad CSRF token.');
}
$this->validateRuleAction($rule);
$this->entityManager->remove($rule);
@ -560,7 +571,7 @@ class ConfigController extends AbstractController
public function resetAction(Request $request, string $type, AnnotationRepository $annotationRepository, EntryRepository $entryRepository)
{
if (!$this->isCsrfTokenValid('reset-area', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
throw new BadRequestHttpException('Bad CSRF token.');
}
switch ($type) {
@ -614,7 +625,7 @@ class ConfigController extends AbstractController
public function deleteAccountAction(Request $request, UserRepository $userRepository, TokenStorageInterface $tokenStorage)
{
if (!$this->isCsrfTokenValid('delete-account', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
throw new BadRequestHttpException('Bad CSRF token.');
}
$enabledUsers = $userRepository->getSumEnabledUsers();
@ -637,12 +648,16 @@ class ConfigController extends AbstractController
/**
* Switch view mode for current user.
*
* @Route("/config/view-mode", name="switch_view_mode")
* @Route("/config/view-mode", name="switch_view_mode", methods={"POST"})
*
* @return RedirectResponse
*/
public function changeViewModeAction(Request $request)
{
if (!$this->isCsrfTokenValid('switch-view-mode', $request->request->get('token'))) {
throw new BadRequestHttpException('Bad CSRF token.');
}
$user = $this->getUser();
$user->getConfig()->setListMode(!$user->getConfig()->getListMode());
@ -659,12 +674,16 @@ class ConfigController extends AbstractController
*
* @param string $language
*
* @Route("/locale/{language}", name="changeLocale")
* @Route("/locale/{language}", name="changeLocale", methods={"POST"})
*
* @return RedirectResponse
*/
public function setLocaleAction(Request $request, ValidatorInterface $validator, $language = null)
{
if (!$this->isCsrfTokenValid('change-locale', $request->request->get('token'))) {
throw new BadRequestHttpException('Bad CSRF token.');
}
$errors = $validator->validate($language, (new LocaleConstraint()));
if (0 === \count($errors)) {

View File

@ -14,6 +14,7 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Entity\Entry;
@ -52,12 +53,16 @@ class EntryController extends AbstractController
}
/**
* @Route("/mass", name="mass_action")
* @Route("/mass", name="mass_action", methods={"POST"})
*
* @return Response
*/
public function massAction(Request $request, TagRepository $tagRepository)
{
if (!$this->isCsrfTokenValid('mass-action', $request->request->get('token'))) {
throw new BadRequestHttpException('Bad CSRF token.');
}
$values = $request->request->all();
$tagsToAdd = [];
@ -400,12 +405,16 @@ class EntryController extends AbstractController
* Reload an entry.
* Refetch content from the website and make it readable again.
*
* @Route("/reload/{id}", requirements={"id" = "\d+"}, name="reload_entry")
* @Route("/reload/{id}", name="reload_entry", methods={"POST"}, requirements={"id" = "\d+"})
*
* @return RedirectResponse
*/
public function reloadAction(Entry $entry)
public function reloadAction(Request $request, Entry $entry)
{
if (!$this->isCsrfTokenValid('reload-entry', $request->request->get('token'))) {
throw new BadRequestHttpException('Bad CSRF token.');
}
$this->checkUserAction($entry);
$this->updateEntry($entry, 'entry_reloaded');
@ -429,12 +438,16 @@ class EntryController extends AbstractController
/**
* Changes read status for an entry.
*
* @Route("/archive/{id}", requirements={"id" = "\d+"}, name="archive_entry")
* @Route("/archive/{id}", name="archive_entry", methods={"POST"}, requirements={"id" = "\d+"})
*
* @return RedirectResponse
*/
public function toggleArchiveAction(Request $request, Entry $entry)
{
if (!$this->isCsrfTokenValid('archive-entry', $request->request->get('token'))) {
throw new BadRequestHttpException('Bad CSRF token.');
}
$this->checkUserAction($entry);
$entry->toggleArchive();
@ -458,12 +471,16 @@ class EntryController extends AbstractController
/**
* Changes starred status for an entry.
*
* @Route("/star/{id}", requirements={"id" = "\d+"}, name="star_entry")
* @Route("/star/{id}", name="star_entry", methods={"POST"}, requirements={"id" = "\d+"})
*
* @return RedirectResponse
*/
public function toggleStarAction(Request $request, Entry $entry)
{
if (!$this->isCsrfTokenValid('star-entry', $request->request->get('token'))) {
throw new BadRequestHttpException('Bad CSRF token.');
}
$this->checkUserAction($entry);
$entry->toggleStar();
@ -488,12 +505,16 @@ class EntryController extends AbstractController
/**
* Deletes entry and redirect to the homepage or the last viewed page.
*
* @Route("/delete/{id}", requirements={"id" = "\d+"}, name="delete_entry")
* @Route("/delete/{id}", name="delete_entry", methods={"POST"}, requirements={"id" = "\d+"})
*
* @return RedirectResponse
*/
public function deleteEntryAction(Request $request, Entry $entry)
{
if (!$this->isCsrfTokenValid('delete-entry', $request->request->get('token'))) {
throw new BadRequestHttpException('Bad CSRF token.');
}
$this->checkUserAction($entry);
// generates the view url for this entry to check for redirection later
@ -526,12 +547,16 @@ class EntryController extends AbstractController
/**
* Get public URL for entry (and generate it if necessary).
*
* @Route("/share/{id}", requirements={"id" = "\d+"}, name="share")
* @Route("/share/{id}", name="share", methods={"POST"}, requirements={"id" = "\d+"})
*
* @return Response
*/
public function shareAction(Entry $entry)
public function shareAction(Request $request, Entry $entry)
{
if (!$this->isCsrfTokenValid('share-entry', $request->request->get('token'))) {
throw new BadRequestHttpException('Bad CSRF token.');
}
$this->checkUserAction($entry);
if (null === $entry->getUid()) {
@ -549,12 +574,16 @@ class EntryController extends AbstractController
/**
* Disable public sharing for an entry.
*
* @Route("/share/delete/{id}", requirements={"id" = "\d+"}, name="delete_share")
* @Route("/share/delete/{id}", name="delete_share", methods={"POST"}, requirements={"id" = "\d+"})
*
* @return Response
*/
public function deleteShareAction(Entry $entry)
public function deleteShareAction(Request $request, Entry $entry)
{
if (!$this->isCsrfTokenValid('delete-share', $request->request->get('token'))) {
throw new BadRequestHttpException('Bad CSRF token.');
}
$this->checkUserAction($entry);
$entry->cleanUid();
@ -570,7 +599,7 @@ class EntryController extends AbstractController
/**
* Ability to view a content publicly.
*
* @Route("/share/{uid}", requirements={"uid" = ".+"}, name="share_entry")
* @Route("/share/{uid}", name="share_entry", methods={"GET"}, requirements={"uid" = ".+"})
* @Cache(maxage="25200", smaxage="25200", public=true)
*
* @return Response
@ -592,7 +621,7 @@ class EntryController extends AbstractController
*
* @param int $page
*
* @Route("/domain/{id}/{page}", requirements={"id" = ".+"}, defaults={"page" = 1}, name="same_domain")
* @Route("/domain/{id}/{page}", requirements={"id" = "\d+"}, defaults={"page" = 1}, name="same_domain")
*
* @return Response
*/

View File

@ -10,6 +10,7 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Entity\Entry;
@ -87,12 +88,16 @@ class TagController extends AbstractController
/**
* Removes tag from entry.
*
* @Route("/remove-tag/{entry}/{tag}", requirements={"entry" = "\d+", "tag" = "\d+"}, name="remove_tag")
* @Route("/remove-tag/{entry}/{tag}", name="remove_tag", methods={"POST"}, requirements={"entry" = "\d+", "tag" = "\d+"})
*
* @return Response
*/
public function removeTagFromEntry(Request $request, Entry $entry, Tag $tag)
{
if (!$this->isCsrfTokenValid('remove-tag', $request->request->get('token'))) {
throw new BadRequestHttpException('Bad CSRF token.');
}
$this->checkUserAction($entry);
$entry->removeTag($tag);
@ -228,12 +233,16 @@ class TagController extends AbstractController
/**
* Tag search results with the current search term.
*
* @Route("/tag/search/{filter}", name="tag_this_search")
* @Route("/tag/search/{filter}", name="tag_this_search", methods={"POST"})
*
* @return Response
*/
public function tagThisSearchAction($filter, Request $request, EntryRepository $entryRepository)
{
if (!$this->isCsrfTokenValid('tag-this-search', $request->request->get('token'))) {
throw new BadRequestHttpException('Bad CSRF token.');
}
$currentRoute = $request->query->has('currentRoute') ? $request->query->get('currentRoute') : '';
/** @var QueryBuilder $qb */
@ -263,13 +272,17 @@ class TagController extends AbstractController
/**
* Delete a given tag for the current user.
*
* @Route("/tag/delete/{slug}", name="tag_delete")
* @Route("/tag/delete/{slug}", name="tag_delete", methods={"POST"})
* @ParamConverter("tag", options={"mapping": {"slug": "slug"}})
*
* @return Response
*/
public function removeTagAction(Tag $tag, Request $request, EntryRepository $entryRepository)
{
if (!$this->isCsrfTokenValid('tag-delete', $request->request->get('token'))) {
throw new BadRequestHttpException('Bad CSRF token.');
}
foreach ($tag->getEntriesByUserId($this->getUser()->getId()) as $entry) {
$entryRepository->removeTag($this->getUser()->getId(), $tag);
}

View File

@ -226,21 +226,6 @@ class EntryRepository extends ServiceEntityRepository
;
}
/**
* Retrieve entries with annotations count for a user.
*
* @param int $userId
*
* @return QueryBuilder
*/
public function getCountBuilderForAnnotationsByUser($userId)
{
return $this
->getQueryBuilderByUser($userId)
->innerJoin('e.annotations', 'a')
;
}
/**
* Retrieve untagged entries for a user.
*

View File

@ -123,48 +123,63 @@
</div>
<div id="set2" class="col s12">
<div class="row">
<div class="input-field col s12">
{{ 'config.form_feed.description'|trans }}
</div>
</div>
<div class="row">
<div class="col s12">
<h6 class="grey-text">{{ 'config.form_feed.token_label'|trans }}</h6>
<div>
{% if feed.token %}
{{ feed.token }}
{% else %}
<em>{{ 'config.form_feed.no_token'|trans }}</em>
{% endif %}
{% if feed.token %}
<form action="{{ path('generate_token') }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('generate-token') }}"/>
<button type="submit" class="btn-link">{{ 'config.form_feed.token_reset'|trans }}</button>
</form>
<form action="{{ path('revoke_token') }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('revoke-token') }}"/>
<button type="submit" class="btn-link">{{ 'config.form_feed.token_revoke'|trans }}</button>
</form>
{% else %}
<form action="{{ path('generate_token') }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('generate-token') }}"/>
<button type="submit" class="btn-link">{{ 'config.form_feed.token_create'|trans }}</button>
</form>
{% endif %}
</div>
</div>
</div>
{% if feed.token %}
<div class="row">
<div class="col s12">
<h6 class="grey-text">{{ 'config.form_feed.feed_links'|trans }}</h6>
<ul>
<li><a href="{{ path('unread_feed', {'username': feed.username, 'token': feed.token}) }}">{{ 'config.form_feed.feed_link.unread'|trans }}</a></li>
<li><a href="{{ path('starred_feed', {'username': feed.username, 'token': feed.token}) }}">{{ 'config.form_feed.feed_link.starred'|trans }}</a></li>
<li><a href="{{ path('archive_feed', {'username': feed.username, 'token': feed.token}) }}">{{ 'config.form_feed.feed_link.archive'|trans }}</a></li>
<li><a href="{{ path('all_feed', {'username': feed.username, 'token': feed.token}) }}">{{ 'config.form_feed.feed_link.all'|trans }}</a></li>
</ul>
</div>
</div>
{% endif %}
{{ form_start(form.feed) }}
{{ form_errors(form.feed) }}
<div class="row">
<div class="input-field col s12">
{{ 'config.form_feed.description'|trans }}
</div>
</div>
<div class="row">
<div class="col s12">
<h6 class="grey-text">{{ 'config.form_feed.token_label'|trans }}</h6>
<div>
{% if feed.token %}
{{ feed.token }}
{% else %}
<em>{{ 'config.form_feed.no_token'|trans }}</em>
{% endif %}
{% if feed.token %}
<a href="{{ path('generate_token') }}">{{ 'config.form_feed.token_reset'|trans }}</a>
<a href="{{ path('revoke_token') }}">{{ 'config.form_feed.token_revoke'|trans }}</a>
{% else %}
<a href="{{ path('generate_token') }}">{{ 'config.form_feed.token_create'|trans }}</a>
{% endif %}
</div>
</div>
</div>
{% if feed.token %}
<div class="row">
<div class="col s12">
<h6 class="grey-text">{{ 'config.form_feed.feed_links'|trans }}</h6>
<ul>
<li><a href="{{ path('unread_feed', {'username': feed.username, 'token': feed.token}) }}">{{ 'config.form_feed.feed_link.unread'|trans }}</a></li>
<li><a href="{{ path('starred_feed', {'username': feed.username, 'token': feed.token}) }}">{{ 'config.form_feed.feed_link.starred'|trans }}</a></li>
<li><a href="{{ path('archive_feed', {'username': feed.username, 'token': feed.token}) }}">{{ 'config.form_feed.feed_link.archive'|trans }}</a></li>
<li><a href="{{ path('all_feed', {'username': feed.username, 'token': feed.token}) }}">{{ 'config.form_feed.feed_link.all'|trans }}</a></li>
</ul>
</div>
</div>
{% endif %}
<div class="row">
<div class="input-field col s12">
{{ form_label(form.feed.feed_limit) }}
@ -325,9 +340,13 @@
<a href="{{ path('edit_tagging_rule', {id: tagging_rule.id}) }}" title="{{ 'config.form_rules.edit_rule_label'|trans }}" class="mode_edit_tagging_rule">
<i class="tool grey-text material-icons">mode_edit</i>
</a>
<a href="{{ path('delete_tagging_rule', {id: tagging_rule.id}) }}" title="{{ 'config.form_rules.delete_rule_label'|trans }}" class="delete_tagging_rule">
<i class="tool grey-text material-icons">delete</i>
</a>
<form action="{{ path('delete_tagging_rule', {id: tagging_rule.id}) }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('delete-tagging-rule') }}"/>
<button type="submit" title="{{ 'config.form_rules.delete_rule_label'|trans }}" class="btn-link">
<i class="tool grey-text material-icons">delete</i>
</button>
</form>
</li>
{% endfor %}
</ul>
@ -505,9 +524,13 @@
<a href="{{ path('edit_ignore_origin_rule', {id: ignore_origin_rule.id}) }}" title="{{ 'config.form_rules.edit_rule_label'|trans }}" class="mode_edit">
<i class="tool grey-text material-icons">mode_edit</i>
</a>
<a href="{{ path('delete_ignore_origin_rule', {id: ignore_origin_rule.id}) }}" title="{{ 'config.form_rules.delete_rule_label'|trans }}" class="delete">
<i class="tool grey-text material-icons">delete</i>
</a>
<form action="{{ path('delete_ignore_origin_rule', {id: ignore_origin_rule.id}) }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('delete-ignore-origin-rule') }}"/>
<button type="submit" title="{{ 'config.form_rules.delete_rule_label'|trans }}" class="btn-link">
<i class="tool grey-text material-icons">delete</i>
</button>
</form>
</li>
{% endfor %}
</ul>

View File

@ -45,7 +45,7 @@
<div class="row">
<div class="input-field col s12">
<label for="_auth_code">{{ "auth_code"|trans({}, 'SchebTwoFactorBundle') }}</label>
<input id="_auth_code" type="text" autocomplete="off" name="_auth_code" />
<input id="_auth_code" type="text" autocomplete="off" name="_auth_code" required="required" />
</div>
</div>
</div>

View File

@ -1,3 +1,3 @@
<label class="entry-checkbox">
<input type="checkbox" class="entry-checkbox-input" data-js="entry-checkbox" name="entry-checkbox[]" value="{{ entry.id }}" />
<input type="checkbox" form="form_mass_action" class="entry-checkbox-input" data-js="entry-checkbox" name="entry-checkbox[]" value="{{ entry.id }}" />
</label>

View File

@ -7,20 +7,38 @@
</div>
</div>
{% set current_path = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %}
{% set current_path = app.request.requesturi %}
<ul class="tools right">
<li>
<a title="{{ 'entry.list.show_same_domain'|trans }}" class="tool grey-text" href="{{ path('same_domain', {'id': entry.id, redirect: current_path}) }}" data-action="same_domain" data-entry-id="{{ entry.id }}"><i class="material-icons">language</i></a>
</li>
<li>
<a title="{{ 'entry.list.toogle_as_read'|trans }}" class="tool grey-text" href="{{ path('archive_entry', {'id': entry.id, redirect: current_path}) }}" data-action="archived" data-entry-id="{{ entry.id }}"><i class="material-icons">{% if entry.isArchived == 0 %}done{% else %}unarchive{% endif %}</i></a>
<form action="{{ path('archive_entry', {'id': entry.id, redirect: current_path}) }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('archive-entry') }}"/>
<button type="submit" class="btn-link tool grey-text" title="{{ 'entry.list.toogle_as_read'|trans }}">
<i class="material-icons">{% if entry.isArchived == 0 %}done{% else %}unarchive{% endif %}</i>
</button>
</form>
</li>
<li>
<a title="{{ 'entry.list.toogle_as_star'|trans }}" class="tool grey-text" href="{{ path('star_entry', {'id': entry.id, redirect: current_path}) }}" data-action="star" data-entry-id="{{ entry.id }}"><i class="material-icons">{% if entry.isStarred == 0 %}star_border{% else %}star{% endif %}</i></a>
<form action="{{ path('star_entry', {'id': entry.id, redirect: current_path}) }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('star-entry') }}"/>
<button type="submit" class="btn-link tool grey-text" title="{{ 'entry.list.toogle_as_star'|trans }}">
<i class="material-icons">{% if entry.isStarred == 0 %}star_border{% else %}star{% endif %}</i>
</button>
</form>
</li>
<li>
<a title="{{ 'entry.list.delete'|trans }}" onclick="return confirm('{{ 'entry.confirm.delete'|trans|escape('js') }}')" data-action-confirm="{{ 'entry.confirm.delete'|trans }}" class="tool grey-text delete" href="{{ path('delete_entry', {'id': entry.id, redirect: current_path}) }}" data-action="delete" data-entry-id="{{ entry.id }}"><i class="material-icons">delete</i></a>
<form action="{{ path('delete_entry', {'id': entry.id, redirect: current_path}) }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('delete-entry') }}"/>
<button type="submit" class="btn-link tool grey-text" title="{{ 'entry.list.delete'|trans }}" onclick="return confirm('{{ 'entry.confirm.delete'|trans|escape('js') }}')">
<i class="material-icons">delete</i>
</button>
</form>
</li>
</ul>
</div>

View File

@ -10,14 +10,32 @@
{% endif %}
{% include "@WallabagCore/Entry/Card/_content.html.twig" with {'entry': entry, 'withMetadata': true, 'subClass': 'metadata'} only %}
{% set current_path = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %}
{% set current_path = app.request.requesturi %}
<ul class="tools-list hide-on-small-only">
<li>
<a title="{{ 'entry.list.show_same_domain'|trans }}" class="tool grey-text" href="{{ path('same_domain', {'id': entry.id, redirect: current_path}) }}"><i class="material-icons">language</i></a>
<a title="{{ 'entry.list.toogle_as_read'|trans }}" class="tool grey-text" href="{{ path('archive_entry', {'id': entry.id, redirect: current_path}) }}"><i class="material-icons">{% if entry.isArchived == 0 %}done{% else %}unarchive{% endif %}</i></a>
<a title="{{ 'entry.list.toogle_as_star'|trans }}" class="tool grey-text" href="{{ path('star_entry', {'id': entry.id, redirect: current_path}) }}"><i class="material-icons">{% if entry.isStarred == 0 %}star_border{% else %}star{% endif %}</i></a>
<a title="{{ 'entry.list.delete'|trans }}" onclick="return confirm('{{ 'entry.confirm.delete'|trans|escape('js') }}')" class="tool grey-text delete" href="{{ path('delete_entry', {'id': entry.id, redirect: current_path}) }}"><i class="material-icons">delete</i></a>
<form action="{{ path('archive_entry', {'id': entry.id, redirect: current_path}) }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('archive-entry') }}"/>
<button type="submit" class="btn-link tool grey-text" title="{{ 'entry.list.toogle_as_read'|trans }}">
<i class="material-icons">{% if entry.isArchived == 0 %}done{% else %}unarchive{% endif %}</i>
</button>
</form>
<form action="{{ path('star_entry', {'id': entry.id, redirect: current_path}) }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('star-entry') }}"/>
<button type="submit" class="btn-link tool grey-text" title="{{ 'entry.list.toogle_as_star'|trans }}">
<i class="material-icons">{% if entry.isStarred == 0 %}star_border{% else %}star{% endif %}</i>
</button>
</form>
<form action="{{ path('delete_entry', {'id': entry.id, redirect: current_path}) }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('delete-entry') }}"/>
<button type="submit" class="btn-link tool grey-text" title="{{ 'entry.list.delete'|trans }}" onclick="return confirm('{{ 'entry.confirm.delete'|trans|escape('js') }}')">
<i class="material-icons">delete</i>
</button>
</form>
</li>
</ul>
</div>

View File

@ -5,9 +5,13 @@
<a class="chip-label" href="{{ path('tag_entries', {'slug': tag.slug}) }}">{{ tag.label }}</a>
{% if withRemove is defined and withRemove == true %}
{% set current_path = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %}
<a class="chip-action" href="{{ path('remove_tag', {'entry': entryId, 'tag': tag.id, redirect: current_path}) }}" onclick="return confirm('{{ 'entry.confirm.delete_tag'|trans|escape('js') }}')">
<i class="material-icons vertical-align-middle">delete</i>
</a>
<form action="{{ path('remove_tag', {'entry': entryId, 'tag': tag.id, redirect: current_path}) }}" method="post">
<input type="hidden" name="token" value="{{ csrf_token('remove-tag') }}"/>
<button type="submit" class="btn-link chip-action" onclick="return confirm('{{ 'entry.confirm.delete_tag'|trans|escape('js') }}')">
<i class="material-icons vertical-align-middle">delete</i>
</button>
</form>
{% endif %}
</li>
{% endfor %}

View File

@ -9,7 +9,7 @@
{% elseif current_route == 'search' %}
{{ 'entry.page_titles.filtered_search'|trans }} {{ filter }}
{% elseif current_route == 'tag_entries' %}
{{ 'entry.page_titles.filtered_tags'|trans }} {{ filter }}
{{ 'entry.page_titles.filtered_tags'|trans }} {{ tag.label }}
{% elseif current_route == 'untagged' %}
{{ 'entry.page_titles.untagged'|trans }}
{% elseif current_route == 'same_domain' %}

View File

@ -19,19 +19,27 @@
{% endblock %}
{% block content %}
{% set current_path = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %}
{% set current_path = app.request.requesturi %}
{% set list_mode = app.user.config.listMode %}
{% set entries_with_archived_class_routes = ['tag_entries', 'search', 'all'] %}
{% set current_route = app.request.attributes.get('_route') %}
{% if current_route == 'homepage' %}
{% set current_route = 'unread' %}
{% endif %}
<form name="form_mass_action" action="{{ path('mass_action', {redirect: current_path}) }}" method="post">
<form id="form_mass_action" name="form_mass_action" action="{{ path('mass_action', {redirect: current_path}) }}" method="post">
<input type="hidden" name="token" value="{{ csrf_token('mass-action') }}"/>
</form>
<div class="results">
<div class="nb-results">
{{ 'entry.list.number_on_the_page'|trans({'%count%': entries.count}) }}
{% if entries.count > 0 %}
<a class="results-item" href="{{ path('switch_view_mode', {redirect: current_path}) }}"><i class="material-icons">{% if list_mode == 0 %}view_list{% else %}view_module{% endif %}</i></a>
<form action="{{ path('switch_view_mode', {redirect: current_path}) }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('switch-view-mode') }}"/>
<button type="submit" class="btn-link results-item">
<i class="material-icons">{% if list_mode == 0 %}view_list{% else %}view_module{% endif %}</i>
</button>
</form>
{% endif %}
{% if entries.count > 0 %}
<label for="mass-action-inputs-displayed" class="mass-action-toggle results-item tooltipped" data-position="right" data-delay="50" data-tooltip="{{ 'entry.list.toggle_mass_action'|trans }}"><i class="material-icons">library_add_check</i></label>
@ -40,7 +48,13 @@
{% include "@WallabagCore/Entry/_feed_link.html.twig" %}
{% endif %}
</div>
{% if current_route == 'search' %}<div><a href="{{ path('tag_this_search', {'filter': searchTerm, 'currentRoute': app.request.get('currentRoute'), redirect: current_path}) }}" title="{{ 'entry.list.assign_search_tag'|trans }}">{{ 'entry.list.assign_search_tag'|trans }}</a></div>{% endif %}
{% if current_route == 'search' %}
<form action="{{ path('tag_this_search', {'filter': searchTerm, 'currentRoute': app.request.get('currentRoute'), redirect: current_path}) }}" method="post">
<input type="hidden" name="token" value="{{ csrf_token('tag-this-search') }}"/>
<button type="submit" class="btn-link" title="{{ 'entry.list.assign_search_tag'|trans }}">{{ 'entry.list.assign_search_tag'|trans }}</button>
</form>
{% endif %}
{% if entries.getNbPages > 1 %}
{{ pagerfanta(entries, 'default_wallabag') }}
{% endif %}
@ -50,15 +64,15 @@
<input id="mass-action-inputs-displayed" class="toggle-checkbox" type="checkbox" />
<div class="mass-action">
<div class="mass-action-group">
<input type="checkbox" class="entry-checkbox-input" data-toggle="[data-js='entry-checkbox']" data-js="checkboxes-toggle" />
<button class="mass-action-button btn cyan darken-1" type="submit" name="toggle-read" title="{{ 'entry.list.toogle_as_read'|trans }}"><i class="material-icons">done</i></button>
<button class="mass-action-button btn cyan darken-1" type="submit" name="toggle-star" title="{{ 'entry.list.toogle_as_star'|trans }}" ><i class="material-icons">star</i></button>
<button class="mass-action-button btn cyan darken-1" type="submit" name="delete" onclick="return confirm('{{ 'entry.confirm.delete_entries'|trans|escape('js') }}')" title="{{ 'entry.list.delete'|trans }}"><i class="material-icons">delete</i></button>
<input type="checkbox" form="form_mass_action" class="entry-checkbox-input" data-toggle="[data-js='entry-checkbox']" data-js="checkboxes-toggle" />
<button class="mass-action-button btn cyan darken-1" type="submit" form="form_mass_action" name="toggle-read" title="{{ 'entry.list.toogle_as_read'|trans }}"><i class="material-icons">done</i></button>
<button class="mass-action-button btn cyan darken-1" type="submit" form="form_mass_action" name="toggle-star" title="{{ 'entry.list.toogle_as_star'|trans }}" ><i class="material-icons">star</i></button>
<button class="mass-action-button btn cyan darken-1" type="submit" form="form_mass_action" name="delete" onclick="return confirm('{{ 'entry.confirm.delete_entries'|trans|escape('js') }}')" title="{{ 'entry.list.delete'|trans }}"><i class="material-icons">delete</i></button>
</div>
<div class="mass-action-tags">
<button class="btn cyan darken-1 mass-action-button mass-action-button--tags" type="submit" name="tag" title="{{ 'entry.list.add_tags'|trans }}"><i class="material-icons">label</i></button>
<input type="text" class="mass-action-tags-input" name="tags" placeholder="{{ 'entry.list.mass_action_tags_input_placeholder'|trans }}" />
<button class="btn cyan darken-1 mass-action-button mass-action-button--tags" type="submit" form="form_mass_action" name="tag" title="{{ 'entry.list.add_tags'|trans }}"><i class="material-icons">label</i></button>
<input type="text" form="form_mass_action" class="mass-action-tags-input" name="tags" placeholder="{{ 'entry.list.mass_action_tags_input_placeholder'|trans }}" />
</div>
</div>
@ -77,7 +91,6 @@
{% endfor %}
</ol>
{% endif %}
</form>
{% if entries.getNbPages > 1 %}
<div class="results">

View File

@ -26,14 +26,22 @@
</ul>
<ul class="right">
<li>
<a class="waves-effect" title="{{ 'entry.view.left_menu.set_as_read'|trans }}" href="{{ path('archive_entry', {'id': entry.id, redirect: current_path}) }}" id="markAsRead">
<i class="material-icons small">{% if entry.isArchived == 0 %}done{% else %}unarchive{% endif %}</i>
</a>
<form action="{{ path('archive_entry', {'id': entry.id, redirect: current_path}) }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('archive-entry') }}"/>
<button type="submit" class="waves-effect" title="{{ 'entry.view.left_menu.set_as_read'|trans }}">
<i class="material-icons small">{% if entry.isArchived == 0 %}done{% else %}unarchive{% endif %}</i>
</button>
</form>
</li>
<li>
<a class="waves-effect" title="{{ 'entry.view.left_menu.set_as_starred'|trans }}" href="{{ path('star_entry', {'id': entry.id, redirect: current_path}) }}" id="setFav">
<i class="material-icons small">{% if entry.isStarred == 0 %}star_outline{% else %}star{% endif %}</i>
</a>
<form action="{{ path('star_entry', {'id': entry.id, redirect: current_path}) }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('star-entry') }}"/>
<button type="submit" class="waves-effect" title="{{ 'entry.view.left_menu.set_as_starred'|trans }}">
<i class="material-icons small">{% if entry.isStarred == 0 %}star_outline{% else %}star{% endif %}</i>
</button>
</form>
</li>
</ul>
</div>
@ -56,10 +64,14 @@
</li>
<li class="bold">
<a class="waves-effect collapsible-header" onclick="return confirm('{{ 'entry.confirm.reload'|trans|escape('js') }}')" title="{{ 'entry.view.left_menu.re_fetch_content'|trans }}" href="{{ path('reload_entry', {'id': entry.id}) }}" id="reload">
<i class="material-icons small">refresh</i>
<span>{{ 'entry.view.left_menu.re_fetch_content'|trans }}</span>
</a>
<form action="{{ path('reload_entry', {'id': entry.id}) }}" method="post">
<input type="hidden" name="token" value="{{ csrf_token('reload-entry') }}"/>
<button type="submit" class="waves-effect collapsible-header" onclick="return confirm('{{ 'entry.confirm.reload'|trans|escape('js') }}')" title="{{ 'entry.view.left_menu.re_fetch_content'|trans }}">
<i class="material-icons small">refresh</i>
<span>{{ 'entry.view.left_menu.re_fetch_content'|trans }}</span>
</button>
</form>
<div class="collapsible-body"></div>
</li>
@ -69,25 +81,37 @@
{% endif %}
<li class="bold hide-on-med-and-down">
<a class="waves-effect collapsible-header markasread" title="{{ mark_as_read_label|trans }}" href="{{ path('archive_entry', {'id': entry.id, redirect: current_path}) }}" id="markAsRead">
<i class="material-icons small">{% if entry.isArchived == 0 %}done{% else %}unarchive{% endif %}</i>
<span>{{ mark_as_read_label|trans }}</span>
</a>
<form action="{{ path('archive_entry', {'id': entry.id, redirect: current_path}) }}" method="post">
<input type="hidden" name="token" value="{{ csrf_token('archive-entry') }}"/>
<button type="submit" class="waves-effect collapsible-header markasread" title="{{ mark_as_read_label|trans }}">
<i class="material-icons small">{% if entry.isArchived == 0 %}done{% else %}unarchive{% endif %}</i>
<span>{{ mark_as_read_label|trans }}</span>
</button>
</form>
<div class="collapsible-body"></div>
</li>
<li class="bold hide-on-med-and-down">
<a class="waves-effect collapsible-header favorite" title="{{ 'entry.view.left_menu.set_as_starred'|trans }}" href="{{ path('star_entry', {'id': entry.id, redirect: current_path}) }}" id="setFav">
<i class="material-icons spall">{% if entry.isStarred == 0 %}star_outline{% else %}star{% endif %}</i>
<span>{{ 'entry.view.left_menu.set_as_starred'|trans }}</span>
</a>
<form action="{{ path('star_entry', {'id': entry.id, redirect: current_path}) }}" method="post">
<input type="hidden" name="token" value="{{ csrf_token('star-entry') }}"/>
<button type="submit" class="waves-effect collapsible-header favorite" title="{{ 'entry.view.left_menu.set_as_starred'|trans }}">
<i class="material-icons spall">{% if entry.isStarred == 0 %}star_outline{% else %}star{% endif %}</i>
<span>{{ 'entry.view.left_menu.set_as_starred'|trans }}</span>
</button>
</form>
<div class="collapsible-body"></div>
</li>
<li class="bold border-bottom">
<a class="waves-effect collapsible-header delete" onclick="return confirm('{{ 'entry.confirm.delete'|trans|escape('js') }}')" title="{{ 'entry.view.left_menu.delete'|trans }}" href="{{ path('delete_entry', {'id': entry.id, redirect: current_path}) }}">
<i class="material-icons small">delete</i>
<span>{{ 'entry.view.left_menu.delete'|trans }}</span>
</a>
<form action="{{ path('delete_entry', {'id': entry.id, redirect: current_path}) }}" method="post">
<input type="hidden" name="token" value="{{ csrf_token('delete-entry') }}"/>
<button type="submit" class="waves-effect collapsible-header delete" onclick="return confirm('{{ 'entry.confirm.delete'|trans|escape('js') }}')" title="{{ 'entry.view.left_menu.delete'|trans }}">
<i class="material-icons small">delete</i>
<span>{{ 'entry.view.left_menu.delete'|trans }}</span>
</button>
</form>
<div class="collapsible-body"></div>
</li>
@ -135,14 +159,22 @@
<ul>
{% if craue_setting('share_public') %}
<li>
<a href="{{ path('share', {'id': entry.id}) }}" target="_blank" title="{{ 'entry.view.left_menu.public_link'|trans }}" class="tool icon-eye">
<span>{{ 'entry.view.left_menu.public_link'|trans }}</span>
</a>
<form action="{{ path('share', {'id': entry.id}) }}" method="post">
<input type="hidden" name="token" value="{{ csrf_token('share-entry') }}"/>
<button type="submit" formtarget="_blank" class="btn-link tool icon-eye" title="{{ 'entry.view.left_menu.public_link'|trans }}">
<span>{{ 'entry.view.left_menu.public_link'|trans }}</span>
</button>
</form>
</li>
<li>
<a href="{{ path('delete_share', {'id': entry.id}) }}" title="{{ 'entry.view.left_menu.delete_public_link'|trans }}" class="tool icon-no-eye">
<span>{{ 'entry.view.left_menu.delete_public_link'|trans }}</span>
</a>
<form action="{{ path('delete_share', {'id': entry.id}) }}" method="post">
<input type="hidden" name="token" value="{{ csrf_token('delete-share') }}"/>
<button type="submit" class="btn-link tool icon-no-eye" title="{{ 'entry.view.left_menu.delete_public_link'|trans }}">
<span>{{ 'entry.view.left_menu.delete_public_link'|trans }}</span>
</button>
</form>
</li>
{% endif %}
{% if craue_setting('share_twitter') %}
@ -300,9 +332,33 @@
<i class="material-icons">menu</i>
</a>
<ul>
<li><a class="btn-floating" href="{{ path('archive_entry', {'id': entry.id, redirect: current_path}) }}"><i class="material-icons">{% if entry.isArchived == 0 %}done{% else %}unarchive{% endif %}</i></a></li>
<li><a class="btn-floating" href="{{ path('star_entry', {'id': entry.id, redirect: current_path}) }}"><i class="material-icons">{% if entry.isStarred == 0 %}star_outline{% else %}star{% endif %}</i></a></li>
<li><a class="btn-floating" href="{{ path('delete_entry', {'id': entry.id, redirect: current_path}) }}" onclick="return confirm('{{ 'entry.confirm.delete'|trans|escape('js') }}')"><i class="material-icons">delete</i></a></li>
<li>
<form action="{{ path('archive_entry', {'id': entry.id, redirect: current_path}) }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('archive-entry') }}"/>
<button type="submit" class="btn-floating">
<i class="material-icons">{% if entry.isArchived == 0 %}done{% else %}unarchive{% endif %}</i>
</button>
</form>
</li>
<li>
<form action="{{ path('star_entry', {'id': entry.id, redirect: current_path}) }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('star-entry') }}"/>
<button type="submit" class="btn-floating">
<i class="material-icons">{% if entry.isStarred == 0 %}star_outline{% else %}star{% endif %}</i>
</button>
</form>
</li>
<li>
<form action="{{ path('delete_entry', {'id': entry.id, redirect: current_path}) }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('delete-entry') }}"/>
<button type="submit" class="btn-floating" onclick="return confirm('{{ 'entry.confirm.delete'|trans|escape('js') }}')">
<i class="material-icons">delete</i>
</button>
</form>
</li>
</ul>
</div>
</div>

View File

@ -102,7 +102,7 @@
<ul>
<li><a href="https://github.com/wallabag/wallabag/issues/">{{ 'quickstart.support.github'|trans }}</a></li>
<li><a href="mailto:hello@wallabag.org">{{ 'quickstart.support.email'|trans }}</a></li>
<li><a href="https://gitter.im/wallabag/wallabag">{{ 'quickstart.support.gitter'|trans }}</a></li>
<li><a href="https://matrix.to/#/#wallabag:matrix.org">{{ 'quickstart.support.matrix'|trans }}</a></li>
</ul>
</div>
</div>

View File

@ -28,9 +28,13 @@
<i class="material-icons">mode_edit</i>
</a>
{% endif %}
<a id="delete-{{ tag.slug }}" href="{{ path('tag_delete', {'slug': tag.slug, redirect: current_path}) }}" class="card-tag-icon card-tag-delete" onclick="return confirm('{{ 'tag.confirm.delete'|trans({'%name%': tag.label})|escape('js') }}')">
<i class="material-icons">delete</i>
</a>
<form action="{{ path('tag_delete', {'slug': tag.slug, redirect: current_path}) }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('tag-delete') }}"/>
<button type="submit" class="btn-link card-tag-icon card-tag-delete" onclick="return confirm('{{ 'tag.confirm.delete'|trans({'%name%': tag.label})|escape('js') }}')">
<i class="material-icons">delete</i>
</button>
</form>
{% if app.user.config.feedToken %}
<a rel="alternate" type="application/atom+xml" href="{{ path('tag_feed', {'username': app.user.username, 'token': app.user.config.feedToken, 'slug': tag.slug}) }}" class="card-tag-icon"><i class="material-icons">rss_feed</i></a>
{% endif %}

View File

@ -8,6 +8,7 @@ use Twig\Extension\AbstractExtension;
use Twig\Extension\GlobalsInterface;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Wallabag\AnnotationBundle\Repository\AnnotationRepository;
use Wallabag\CoreBundle\Repository\EntryRepository;
use Wallabag\CoreBundle\Repository\TagRepository;
use Wallabag\UserBundle\Entity\User;
@ -16,14 +17,16 @@ class WallabagExtension extends AbstractExtension implements GlobalsInterface
{
private $tokenStorage;
private $entryRepository;
private $annotationRepository;
private $tagRepository;
private $lifeTime;
private $translator;
private $projectDir;
public function __construct(EntryRepository $entryRepository, TagRepository $tagRepository, TokenStorageInterface $tokenStorage, $lifeTime, TranslatorInterface $translator, string $projectDir)
public function __construct(EntryRepository $entryRepository, AnnotationRepository $annotationRepository, TagRepository $tagRepository, TokenStorageInterface $tokenStorage, $lifeTime, TranslatorInterface $translator, string $projectDir)
{
$this->entryRepository = $entryRepository;
$this->annotationRepository = $annotationRepository;
$this->tagRepository = $tagRepository;
$this->tokenStorage = $tokenStorage;
$this->lifeTime = $lifeTime;
@ -88,28 +91,25 @@ class WallabagExtension extends AbstractExtension implements GlobalsInterface
switch ($type) {
case 'starred':
$qb = $this->entryRepository->getCountBuilderForStarredByUser($user->getId());
$qb = $this->entryRepository->getCountBuilderForStarredByUser($user->getId())->select('COUNT(e.id)');
break;
case 'archive':
$qb = $this->entryRepository->getCountBuilderForArchiveByUser($user->getId());
$qb = $this->entryRepository->getCountBuilderForArchiveByUser($user->getId())->select('COUNT(e.id)');
break;
case 'unread':
$qb = $this->entryRepository->getCountBuilderForUnreadByUser($user->getId());
$qb = $this->entryRepository->getCountBuilderForUnreadByUser($user->getId())->select('COUNT(e.id)');
break;
case 'annotated':
$qb = $this->entryRepository->getCountBuilderForAnnotationsByUser($user->getId());
$qb = $this->annotationRepository->getCountBuilderByUser($user->getId())->select('COUNT(DISTINCT e.entry)');
break;
case 'all':
$qb = $this->entryRepository->getCountBuilderForAllByUser($user->getId());
$qb = $this->entryRepository->getCountBuilderForAllByUser($user->getId())->select('COUNT(e.id)');
break;
default:
throw new \InvalidArgumentException(sprintf('Type "%s" is not implemented.', $type));
}
$query = $qb
->select('COUNT(e.id)')
->getQuery();
$query = $qb->getQuery();
$query->useQueryCache(true);
$query->enableResultCache($this->lifeTime);

View File

@ -13,10 +13,15 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInt
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Wallabag\ImportBundle\Import\ChromeImport;
use Wallabag\ImportBundle\Import\DeliciousImport;
use Wallabag\ImportBundle\Import\ElcuratorImport;
use Wallabag\ImportBundle\Import\FirefoxImport;
use Wallabag\ImportBundle\Import\InstapaperImport;
use Wallabag\ImportBundle\Import\OmnivoreImport;
use Wallabag\ImportBundle\Import\PinboardImport;
use Wallabag\ImportBundle\Import\PocketCsvImport;
use Wallabag\ImportBundle\Import\PocketHtmlImport;
use Wallabag\ImportBundle\Import\ReadabilityImport;
use Wallabag\ImportBundle\Import\ShaarliImport;
use Wallabag\ImportBundle\Import\WallabagV1Import;
use Wallabag\ImportBundle\Import\WallabagV2Import;
use Wallabag\UserBundle\Entity\User;
@ -34,10 +39,31 @@ class ImportCommand extends Command
private InstapaperImport $instapaperImport;
private PinboardImport $pinboardImport;
private DeliciousImport $deliciousImport;
private OmnivoreImport $omnivoreImport;
private WallabagV1Import $wallabagV1Import;
private ElcuratorImport $elcuratorImport;
private ShaarliImport $shaarliImport;
private PocketHtmlImport $pocketHtmlImport;
private PocketCsvImport $pocketCsvImport;
public function __construct(EntityManagerInterface $entityManager, TokenStorageInterface $tokenStorage, UserRepository $userRepository, WallabagV2Import $wallabagV2Import, FirefoxImport $firefoxImport, ChromeImport $chromeImport, ReadabilityImport $readabilityImport, InstapaperImport $instapaperImport, PinboardImport $pinboardImport, DeliciousImport $deliciousImport, WallabagV1Import $wallabagV1Import)
{
public function __construct(
EntityManagerInterface $entityManager,
TokenStorageInterface $tokenStorage,
UserRepository $userRepository,
WallabagV2Import $wallabagV2Import,
FirefoxImport $firefoxImport,
ChromeImport $chromeImport,
ReadabilityImport $readabilityImport,
InstapaperImport $instapaperImport,
PinboardImport $pinboardImport,
DeliciousImport $deliciousImport,
WallabagV1Import $wallabagV1Import,
ElcuratorImport $elcuratorImport,
OmnivoreImport $omnivoreImport,
ShaarliImport $shaarliImport,
PocketHtmlImport $pocketHtmlImport,
PocketCsvImport $pocketCsvImport
) {
$this->entityManager = $entityManager;
$this->tokenStorage = $tokenStorage;
$this->userRepository = $userRepository;
@ -48,7 +74,12 @@ class ImportCommand extends Command
$this->instapaperImport = $instapaperImport;
$this->pinboardImport = $pinboardImport;
$this->deliciousImport = $deliciousImport;
$this->omnivoreImport = $omnivoreImport;
$this->wallabagV1Import = $wallabagV1Import;
$this->elcuratorImport = $elcuratorImport;
$this->shaarliImport = $shaarliImport;
$this->pocketHtmlImport = $pocketHtmlImport;
$this->pocketCsvImport = $pocketCsvImport;
parent::__construct();
}
@ -60,7 +91,7 @@ class ImportCommand extends Command
->setDescription('Import entries from a JSON export')
->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 or chrome', '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')
@ -120,6 +151,21 @@ class ImportCommand extends Command
case 'delicious':
$import = $this->deliciousImport;
break;
case 'omnivore':
$import = $this->omnivoreImport;
break;
case 'elcurator':
$import = $this->elcuratorImport;
break;
case 'shaarli':
$import = $this->shaarliImport;
break;
case 'pocket':
$import = $this->pocketHtmlImport;
break;
case 'pocket_csv':
$import = $this->pocketCsvImport;
break;
default:
$import = $this->wallabagV1Import;
}

View File

@ -27,7 +27,7 @@ class RedisWorkerCommand extends Command
$this
->setName('wallabag:import:redis-worker')
->setDescription('Launch Redis worker')
->addArgument('serviceName', InputArgument::REQUIRED, 'Service to use: wallabag_v1, wallabag_v2, pocket, readability, pinboard, delicious, firefox, chrome or instapaper')
->addArgument('serviceName', InputArgument::REQUIRED, 'Service to use: wallabag_v1, wallabag_v2, pocket, readability, pinboard, delicious, omnivore, firefox, chrome or instapaper')
->addOption('maxIterations', '', InputOption::VALUE_OPTIONAL, 'Number of iterations before stopping', false)
;
}

View File

@ -20,9 +20,27 @@ class RabbitMQConsumerTotalProxy
private Consumer $pinboardConsumer;
private Consumer $deliciousConsumer;
private Consumer $elcuratorConsumer;
private Consumer $omnivoreConsumer;
private Consumer $shaarliConsumer;
private Consumer $pocketHtmlConsumer;
private Consumer $pocketCsvConsumer;
public function __construct(Consumer $pocketConsumer, Consumer $readabilityConsumer, Consumer $wallabagV1Consumer, Consumer $wallabagV2Consumer, Consumer $firefoxConsumer, Consumer $chromeConsumer, Consumer $instapaperConsumer, Consumer $pinboardConsumer, Consumer $deliciousConsumer, Consumer $elcuratorConsumer)
{
public function __construct(
Consumer $pocketConsumer,
Consumer $readabilityConsumer,
Consumer $wallabagV1Consumer,
Consumer $wallabagV2Consumer,
Consumer $firefoxConsumer,
Consumer $chromeConsumer,
Consumer $instapaperConsumer,
Consumer $pinboardConsumer,
Consumer $deliciousConsumer,
Consumer $elcuratorConsumer,
Consumer $omnivoreConsumer,
Consumer $shaarliConsumer,
Consumer $pocketHtmlConsumer,
Consumer $pocketCsvConsumer
) {
$this->pocketConsumer = $pocketConsumer;
$this->readabilityConsumer = $readabilityConsumer;
$this->wallabagV1Consumer = $wallabagV1Consumer;
@ -33,6 +51,10 @@ class RabbitMQConsumerTotalProxy
$this->pinboardConsumer = $pinboardConsumer;
$this->deliciousConsumer = $deliciousConsumer;
$this->elcuratorConsumer = $elcuratorConsumer;
$this->omnivoreConsumer = $omnivoreConsumer;
$this->shaarliConsumer = $shaarliConsumer;
$this->pocketHtmlConsumer = $pocketHtmlConsumer;
$this->pocketCsvConsumer = $pocketCsvConsumer;
}
/**
@ -77,6 +99,18 @@ class RabbitMQConsumerTotalProxy
case 'elcurator':
$consumer = $this->elcuratorConsumer;
break;
case 'omnivore':
$consumer = $this->omnivoreConsumer;
break;
case 'shaarli':
$consumer = $this->shaarliConsumer;
break;
case 'pocket_html':
$consumer = $this->pocketHtmlConsumer;
break;
case 'pocket_csv':
$consumer = $this->pocketCsvConsumer;
break;
default:
return 0;
}

View File

@ -0,0 +1,83 @@
<?php
namespace Wallabag\ImportBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\ImportBundle\Form\Type\UploadImportType;
use Wallabag\ImportBundle\Import\ImportInterface;
abstract class HtmlController extends AbstractController
{
/**
* @Route("/html", name="import_html")
*
* @return Response
*/
public function indexAction(Request $request, TranslatorInterface $translator)
{
$form = $this->createForm(UploadImportType::class);
$form->handleRequest($request);
$wallabag = $this->getImportService();
$wallabag->setUser($this->getUser());
if ($form->isSubmitted() && $form->isValid()) {
$file = $form->get('file')->getData();
$markAsRead = $form->get('mark_as_read')->getData();
$name = $this->getUser()->getId() . '.html';
if (null !== $file && \in_array($file->getClientMimeType(), $this->getParameter('wallabag_import.allow_mimetypes'), true) && $file->move($this->getParameter('wallabag_import.resource_dir'), $name)) {
$res = $wallabag
->setFilepath($this->getParameter('wallabag_import.resource_dir') . '/' . $name)
->setMarkAsRead($markAsRead)
->import();
$message = 'flashes.import.notice.failed';
if (true === $res) {
$summary = $wallabag->getSummary();
$message = $translator->trans('flashes.import.notice.summary', [
'%imported%' => $summary['imported'],
'%skipped%' => $summary['skipped'],
]);
if (0 < $summary['queued']) {
$message = $translator->trans('flashes.import.notice.summary_with_queue', [
'%queued%' => $summary['queued'],
]);
}
unlink($this->getParameter('wallabag_import.resource_dir') . '/' . $name);
}
$this->addFlash('notice', $message);
return $this->redirect($this->generateUrl('homepage'));
}
$this->addFlash('notice', 'flashes.import.notice.failed_on_file');
}
return $this->render($this->getImportTemplate(), [
'form' => $form->createView(),
'import' => $wallabag,
]);
}
/**
* Return the service to handle the import.
*
* @return ImportInterface
*/
abstract protected function getImportService();
/**
* Return the template used for the form.
*
* @return string
*/
abstract protected function getImportTemplate();
}

View File

@ -57,6 +57,10 @@ class ImportController extends AbstractController
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('pinboard')
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('delicious')
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('elcurator')
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('omnivore')
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('shaarli')
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('pocket_html')
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('pocket_csv')
;
} catch (\Exception $e) {
$rabbitNotInstalled = true;
@ -75,6 +79,10 @@ class ImportController extends AbstractController
+ $redis->llen('wallabag.import.pinboard')
+ $redis->llen('wallabag.import.delicious')
+ $redis->llen('wallabag.import.elcurator')
+ $redis->llen('wallabag.import.omnivore')
+ $redis->llen('wallabag.import.shaarli')
+ $redis->llen('wallabag.import.pocket_html')
+ $redis->llen('wallabag.import.pocket_csv')
;
} catch (\Exception $e) {
$redisNotInstalled = true;

View File

@ -0,0 +1,84 @@
<?php
namespace Wallabag\ImportBundle\Controller;
use Craue\ConfigBundle\Util\Config;
use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\ImportBundle\Form\Type\UploadImportType;
use Wallabag\ImportBundle\Import\OmnivoreImport;
use Wallabag\ImportBundle\Redis\Producer as RedisProducer;
class OmnivoreController extends AbstractController
{
private RabbitMqProducer $rabbitMqProducer;
private RedisProducer $redisProducer;
public function __construct(RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer)
{
$this->rabbitMqProducer = $rabbitMqProducer;
$this->redisProducer = $redisProducer;
}
/**
* @Route("/omnivore", name="import_omnivore")
*/
public function indexAction(Request $request, OmnivoreImport $omnivore, Config $craueConfig, TranslatorInterface $translator)
{
$form = $this->createForm(UploadImportType::class);
$form->handleRequest($request);
$omnivore->setUser($this->getUser());
if ($craueConfig->get('import_with_rabbitmq')) {
$omnivore->setProducer($this->rabbitMqProducer);
} elseif ($craueConfig->get('import_with_redis')) {
$omnivore->setProducer($this->redisProducer);
}
if ($form->isSubmitted() && $form->isValid()) {
$file = $form->get('file')->getData();
$markAsRead = $form->get('mark_as_read')->getData();
$name = 'omnivore_' . $this->getUser()->getId() . '.json';
if (null !== $file && \in_array($file->getClientMimeType(), $this->getParameter('wallabag_import.allow_mimetypes'), true) && $file->move($this->getParameter('wallabag_import.resource_dir'), $name)) {
$res = $omnivore
->setFilepath($this->getParameter('wallabag_import.resource_dir') . '/' . $name)
->setMarkAsRead($markAsRead)
->import();
$message = 'flashes.import.notice.failed';
if (true === $res) {
$summary = $omnivore->getSummary();
$message = $translator->trans('flashes.import.notice.summary', [
'%imported%' => $summary['imported'],
'%skipped%' => $summary['skipped'],
]);
if (0 < $summary['queued']) {
$message = $translator->trans('flashes.import.notice.summary_with_queue', [
'%queued%' => $summary['queued'],
]);
}
unlink($this->getParameter('wallabag_import.resource_dir') . '/' . $name);
}
$this->addFlash('notice', $message);
return $this->redirect($this->generateUrl('homepage'));
}
$this->addFlash('notice', 'flashes.import.notice.failed_on_file');
}
return $this->render('@WallabagImport/Omnivore/index.html.twig', [
'form' => $form->createView(),
'import' => $omnivore,
]);
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace Wallabag\ImportBundle\Controller;
use Craue\ConfigBundle\Util\Config;
use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\ImportBundle\Import\PocketCsvImport;
use Wallabag\ImportBundle\Redis\Producer as RedisProducer;
class PocketCsvController extends HtmlController
{
private PocketCsvImport $pocketCsvImport;
private Config $craueConfig;
private RabbitMqProducer $rabbitMqProducer;
private RedisProducer $redisProducer;
public function __construct(PocketCsvImport $pocketCsvImport, Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer)
{
$this->pocketCsvImport = $pocketCsvImport;
$this->craueConfig = $craueConfig;
$this->rabbitMqProducer = $rabbitMqProducer;
$this->redisProducer = $redisProducer;
}
/**
* @Route("/pocket_csv", name="import_pocket_csv")
*/
public function indexAction(Request $request, TranslatorInterface $translator)
{
return parent::indexAction($request, $translator);
}
/**
* {@inheritdoc}
*/
protected function getImportService()
{
if ($this->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;
}
/**
* {@inheritdoc}
*/
protected function getImportTemplate()
{
return '@WallabagImport/PocketCsv/index.html.twig';
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace Wallabag\ImportBundle\Controller;
use Craue\ConfigBundle\Util\Config;
use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\ImportBundle\Import\PocketHtmlImport;
use Wallabag\ImportBundle\Redis\Producer as RedisProducer;
class PocketHtmlController extends HtmlController
{
private PocketHtmlImport $pocketHtmlImport;
private Config $craueConfig;
private RabbitMqProducer $rabbitMqProducer;
private RedisProducer $redisProducer;
public function __construct(PocketHtmlImport $pocketHtmlImport, Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer)
{
$this->pocketHtmlImport = $pocketHtmlImport;
$this->craueConfig = $craueConfig;
$this->rabbitMqProducer = $rabbitMqProducer;
$this->redisProducer = $redisProducer;
}
/**
* @Route("/pocket_html", name="import_pocket_html")
*/
public function indexAction(Request $request, TranslatorInterface $translator)
{
return parent::indexAction($request, $translator);
}
/**
* {@inheritdoc}
*/
protected function getImportService()
{
if ($this->craueConfig->get('import_with_rabbitmq')) {
$this->pocketHtmlImport->setProducer($this->rabbitMqProducer);
} elseif ($this->craueConfig->get('import_with_redis')) {
$this->pocketHtmlImport->setProducer($this->redisProducer);
}
return $this->pocketHtmlImport;
}
/**
* {@inheritdoc}
*/
protected function getImportTemplate()
{
return '@WallabagImport/PocketHtml/index.html.twig';
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace Wallabag\ImportBundle\Controller;
use Craue\ConfigBundle\Util\Config;
use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\ImportBundle\Import\ShaarliImport;
use Wallabag\ImportBundle\Redis\Producer as RedisProducer;
class ShaarliController extends HtmlController
{
private ShaarliImport $shaarliImport;
private Config $craueConfig;
private RabbitMqProducer $rabbitMqProducer;
private RedisProducer $redisProducer;
public function __construct(ShaarliImport $shaarliImport, Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer)
{
$this->shaarliImport = $shaarliImport;
$this->craueConfig = $craueConfig;
$this->rabbitMqProducer = $rabbitMqProducer;
$this->redisProducer = $redisProducer;
}
/**
* @Route("/shaarli", name="import_shaarli")
*/
public function indexAction(Request $request, TranslatorInterface $translator)
{
return parent::indexAction($request, $translator);
}
/**
* {@inheritdoc}
*/
protected function getImportService()
{
if ($this->craueConfig->get('import_with_rabbitmq')) {
$this->shaarliImport->setProducer($this->rabbitMqProducer);
} elseif ($this->craueConfig->get('import_with_redis')) {
$this->shaarliImport->setProducer($this->redisProducer);
}
return $this->shaarliImport;
}
/**
* {@inheritdoc}
*/
protected function getImportTemplate()
{
return '@WallabagImport/Shaarli/index.html.twig';
}
}

View File

@ -0,0 +1,210 @@
<?php
namespace Wallabag\ImportBundle\Import;
use Wallabag\CoreBundle\Entity\Entry;
use Wallabag\CoreBundle\Event\EntrySavedEvent;
abstract class HtmlImport extends AbstractImport
{
protected $filepath;
/**
* {@inheritdoc}
*/
abstract public function getName();
/**
* {@inheritdoc}
*/
abstract public function getUrl();
/**
* {@inheritdoc}
*/
abstract public function getDescription();
/**
* {@inheritdoc}
*/
public function import()
{
if (!$this->user) {
$this->logger->error('Wallabag HTML Import: user is not defined');
return false;
}
if (!file_exists($this->filepath) || !is_readable($this->filepath)) {
$this->logger->error('Wallabag HTML Import: unable to read file', ['filepath' => $this->filepath]);
return false;
}
$html = new \DOMDocument();
libxml_use_internal_errors(true);
$html->loadHTMLFile($this->filepath);
$hrefs = $html->getElementsByTagName('a');
libxml_use_internal_errors(false);
if (0 === $hrefs->length) {
$this->logger->error('Wallabag HTML: no entries in imported file');
return false;
}
$entries = [];
foreach ($hrefs as $href) {
$entry = [];
$entry['url'] = $href->getAttribute('href');
$entry['tags'] = $href->getAttribute('tags');
$entry['created_at'] = $href->getAttribute('add_date');
$entries[] = $entry;
}
if ($this->producer) {
$this->parseEntriesForProducer($entries);
return true;
}
$this->parseEntries($entries);
return true;
}
/**
* Set file path to the html file.
*
* @param string $filepath
*/
public function setFilepath($filepath)
{
$this->filepath = $filepath;
return $this;
}
/**
* {@inheritdoc}
*/
public function parseEntry(array $importedEntry)
{
$url = $importedEntry['url'];
$existingEntry = $this->em
->getRepository(Entry::class)
->findByUrlAndUserId($url, $this->user->getId());
if (false !== $existingEntry) {
++$this->skippedEntries;
return null;
}
$data = $this->prepareEntry($importedEntry);
$entry = new Entry($this->user);
$entry->setUrl($data['url']);
$entry->updateArchived($data['is_archived']);
$createdAt = new \DateTime();
$createdAt->setTimestamp($data['created_at']);
$entry->setCreatedAt($createdAt);
// update entry with content (in case fetching failed, the given entry will be return)
$this->fetchContent($entry, $data['url'], $data);
if (\array_key_exists('tags', $data)) {
$this->tagsAssigner->assignTagsToEntry(
$entry,
$data['tags']
);
}
$this->em->persist($entry);
++$this->importedEntries;
return $entry;
}
/**
* Parse and insert all given entries.
*/
protected function parseEntries(array $entries)
{
$i = 1;
$entryToBeFlushed = [];
foreach ($entries as $importedEntry) {
$entry = $this->parseEntry($importedEntry);
if (null === $entry) {
continue;
}
// @see AbstractImport
$entryToBeFlushed[] = $entry;
// flush every 20 entries
if (0 === ($i % 20)) {
$this->em->flush();
foreach ($entryToBeFlushed as $entry) {
$this->eventDispatcher->dispatch(new EntrySavedEvent($entry), EntrySavedEvent::NAME);
}
$entryToBeFlushed = [];
}
++$i;
}
$this->em->flush();
if (!empty($entryToBeFlushed)) {
foreach ($entryToBeFlushed as $entry) {
$this->eventDispatcher->dispatch(new EntrySavedEvent($entry), EntrySavedEvent::NAME);
}
}
}
/**
* Parse entries and send them to the queue.
* It should just be a simple loop on all item, no call to the database should be done
* to speedup queuing.
*
* Faster parse entries for Producer.
* We don't care to make check at this time. They'll be done by the consumer.
*/
protected function parseEntriesForProducer(array $entries)
{
foreach ($entries as $importedEntry) {
if ((array) $importedEntry !== $importedEntry) {
continue;
}
// set userId for the producer (it won't know which user is connected)
$importedEntry['userId'] = $this->user->getId();
if ($this->markAsRead) {
$importedEntry = $this->setEntryAsRead($importedEntry);
}
++$this->queuedEntries;
$this->producer->publish(json_encode($importedEntry));
}
}
/**
* {@inheritdoc}
*/
protected function setEntryAsRead(array $importedEntry)
{
$importedEntry['is_archived'] = 1;
return $importedEntry;
}
abstract protected function prepareEntry(array $entry = []);
}

View File

@ -0,0 +1,158 @@
<?php
namespace Wallabag\ImportBundle\Import;
use Wallabag\CoreBundle\Entity\Entry;
class OmnivoreImport extends AbstractImport
{
private $filepath;
/**
* {@inheritdoc}
*/
public function getName()
{
return 'Omnivore';
}
/**
* {@inheritdoc}
*/
public function getUrl()
{
return 'import_omnivore';
}
/**
* {@inheritdoc}
*/
public function getDescription()
{
return 'import.omnivore.description';
}
/**
* Set file path to the json file.
*
* @param string $filepath
*/
public function setFilepath($filepath)
{
$this->filepath = $filepath;
return $this;
}
/**
* {@inheritdoc}
*/
public function import()
{
if (!$this->user) {
$this->logger->error('OmnivoreImport: user is not defined');
return false;
}
if (!file_exists($this->filepath) || !is_readable($this->filepath)) {
$this->logger->error('OmnivoreImport: unable to read file', ['filepath' => $this->filepath]);
return false;
}
$data = json_decode(file_get_contents($this->filepath), true);
if (empty($data)) {
$this->logger->error('OmnivoreImport: no entries in imported file');
return false;
}
if ($this->producer) {
$this->parseEntriesForProducer($data);
return true;
}
$this->parseEntries($data);
return true;
}
/**
* {@inheritdoc}
*/
public function validateEntry(array $importedEntry)
{
if (empty($importedEntry['url'])) {
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
public function parseEntry(array $importedEntry)
{
$existingEntry = $this->em
->getRepository(Entry::class)
->findByUrlAndUserId($importedEntry['url'], $this->user->getId());
if (false !== $existingEntry) {
++$this->skippedEntries;
return;
}
$data = [
'title' => $importedEntry['title'],
'url' => $importedEntry['url'],
'is_archived' => ('Archived' === $importedEntry['state']) || $this->markAsRead,
'is_starred' => false,
'created_at' => $importedEntry['savedAt'],
'tags' => $importedEntry['labels'],
'published_by' => [$importedEntry['author']],
'published_at' => $importedEntry['publishedAt'],
'preview_picture' => $importedEntry['thumbnail'],
];
$entry = new Entry($this->user);
$entry->setUrl($data['url']);
$entry->setTitle($data['title']);
// update entry with content (in case fetching failed, the given entry will be return)
$this->fetchContent($entry, $data['url'], $data);
if (!empty($data['tags'])) {
$this->tagsAssigner->assignTagsToEntry(
$entry,
$data['tags'],
$this->em->getUnitOfWork()->getScheduledEntityInsertions()
);
}
$entry->updateArchived($data['is_archived']);
$entry->setCreatedAt(\DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $data['created_at']));
if (null !== $data['published_at']) {
$entry->setPublishedAt(\DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $data['published_at']));
}
$entry->setPublishedBy($data['published_by']);
$entry->setPreviewPicture($data['preview_picture']);
$this->em->persist($entry);
++$this->importedEntries;
return $entry;
}
/**
* {@inheritdoc}
*/
protected function setEntryAsRead(array $importedEntry)
{
return $importedEntry;
}
}

View File

@ -0,0 +1,156 @@
<?php
namespace Wallabag\ImportBundle\Import;
use Wallabag\CoreBundle\Entity\Entry;
class PocketCsvImport extends AbstractImport
{
protected $filepath;
/**
* {@inheritdoc}
*/
public function getName()
{
return 'Pocket CSV';
}
/**
* {@inheritdoc}
*/
public function getUrl()
{
return 'import_pocket_csv';
}
/**
* {@inheritdoc}
*/
public function getDescription()
{
return 'import.pocket_csv.description';
}
/**
* Set file path to the csv file.
*
* @param string $filepath
*/
public function setFilepath($filepath)
{
$this->filepath = $filepath;
return $this;
}
/**
* {@inheritdoc}
*/
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;
}
/**
* {@inheritdoc}
*/
public function parseEntry(array $importedEntry)
{
$existingEntry = $this->em
->getRepository(Entry::class)
->findByUrlAndUserId($importedEntry['url'], $this->user->getId());
if (false !== $existingEntry) {
++$this->skippedEntries;
return;
}
$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;
}
/**
* {@inheritdoc}
*/
protected function setEntryAsRead(array $importedEntry)
{
$importedEntry['is_archived'] = 'archive';
return $importedEntry;
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace Wallabag\ImportBundle\Import;
class PocketHtmlImport extends HtmlImport
{
protected $filepath;
/**
* {@inheritdoc}
*/
public function getName()
{
return 'Pocket HTML';
}
/**
* {@inheritdoc}
*/
public function getUrl()
{
return 'import_pocket_html';
}
/**
* {@inheritdoc}
*/
public function getDescription()
{
return 'import.pocket_html.description';
}
/**
* {@inheritdoc}
*/
public function validateEntry(array $importedEntry)
{
if (empty($importedEntry['url'])) {
return false;
}
return true;
}
public function import()
{
if (!$this->user) {
$this->logger->error('Pocket HTML Import: user is not defined');
return false;
}
if (!file_exists($this->filepath) || !is_readable($this->filepath)) {
$this->logger->error('Pocket HTML Import: unable to read file', ['filepath' => $this->filepath]);
return false;
}
$html = new \DOMDocument();
libxml_use_internal_errors(true);
$html->loadHTMLFile($this->filepath);
$hrefs = $html->getElementsByTagName('a');
libxml_use_internal_errors(false);
if (0 === $hrefs->length) {
$this->logger->error('Pocket HTML: no entries in imported file');
return false;
}
$entries = [];
foreach ($hrefs as $href) {
$entry = [];
$entry['url'] = $href->getAttribute('href');
$entry['tags'] = $href->getAttribute('tags');
$entry['created_at'] = $href->getAttribute('time_added');
$entries[] = $entry;
}
if ($this->producer) {
$this->parseEntriesForProducer($entries);
return true;
}
$this->parseEntries($entries);
return true;
}
/**
* {@inheritdoc}
*/
protected function prepareEntry(array $entry = [])
{
$data = [
'title' => '',
'html' => false,
'url' => $entry['url'],
'is_archived' => (int) $this->markAsRead,
'is_starred' => false,
'tags' => '',
'created_at' => $entry['created_at'],
];
if (\array_key_exists('tags', $entry) && '' !== $entry['tags']) {
$data['tags'] = $entry['tags'];
}
return $data;
}
}

View File

@ -15,7 +15,7 @@ use Wallabag\CoreBundle\Entity\Entry;
class PocketImport extends AbstractImport
{
public const NB_ELEMENTS = 5000;
public const NB_ELEMENTS = 30;
/**
* @var HttpMethodsClient
*/

View File

@ -0,0 +1,66 @@
<?php
namespace Wallabag\ImportBundle\Import;
class ShaarliImport extends HtmlImport
{
protected $filepath;
/**
* {@inheritdoc}
*/
public function getName()
{
return 'Shaarli';
}
/**
* {@inheritdoc}
*/
public function getUrl()
{
return 'import_shaarli';
}
/**
* {@inheritdoc}
*/
public function getDescription()
{
return 'import.shaarli.description';
}
/**
* {@inheritdoc}
*/
public function validateEntry(array $importedEntry)
{
if (empty($importedEntry['url'])) {
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
protected function prepareEntry(array $entry = [])
{
$data = [
'title' => '',
'html' => false,
'url' => $entry['url'],
'is_archived' => (int) $this->markAsRead,
'is_starred' => false,
'tags' => '',
'created_at' => $entry['created_at'],
];
if (\array_key_exists('tags', $entry) && '' !== $entry['tags']) {
$data['tags'] = $entry['tags'];
}
return $data;
}
}

View File

@ -1,3 +1,45 @@
{% extends "@WallabagImport/WallabagV1/index.html.twig" %}
{% extends "@WallabagCore/layout.html.twig" %}
{% block title %}{{ 'import.elcurator.page_title'|trans }}{% endblock %}
{% block content %}
<div class="row">
<div class="col s12">
<div class="card-panel settings">
{% include '@WallabagImport/Import/_information.html.twig' %}
<div class="row">
<blockquote>{{ import.description|trans }}</blockquote>
<p>{{ 'import.elcurator.how_to'|trans }}</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>{{ form.file.vars.label|trans }}</span>
{{ form_widget(form.file) }}
</div>
<div class="file-path-wrapper">
<input class="file-path validate" type="text">
</div>
</div>
<div class="input-field col s6 with-checkbox">
<h6>{{ 'import.form.mark_as_read_title'|trans }}</h6>
{{ form_widget(form.mark_as_read) }}
{{ form_label(form.mark_as_read) }}
</div>
</div>
{{ form_widget(form.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }}
{{ form_rest(form) }}
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,45 @@
{% extends "@WallabagCore/layout.html.twig" %}
{% block title %}{{ 'import.omnivore.page_title'|trans }}{% endblock %}
{% block content %}
<div class="row">
<div class="col s12">
<div class="card-panel settings">
{% include '@WallabagImport/Import/_information.html.twig' %}
<div class="row">
<blockquote>{{ import.description|trans }}</blockquote>
<p>{{ 'import.omnivore.how_to'|trans }}</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>{{ form.file.vars.label|trans }}</span>
{{ form_widget(form.file) }}
</div>
<div class="file-path-wrapper">
<input class="file-path validate" type="text">
</div>
</div>
<div class="input-field col s6 with-checkbox">
<h6>{{ 'import.form.mark_as_read_title'|trans }}</h6>
{{ form_widget(form.mark_as_read) }}
{{ form_label(form.mark_as_read) }}
</div>
</div>
{{ form_widget(form.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }}
{{ form_rest(form) }}
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,45 @@
{% extends "@WallabagCore/layout.html.twig" %}
{% block title %}{{ 'import.pocket_csv.page_title'|trans }}{% endblock %}
{% block content %}
<div class="row">
<div class="col s12">
<div class="card-panel settings">
{% include '@WallabagImport/Import/_information.html.twig' %}
<div class="row">
<blockquote>{{ import.description|trans|raw }}</blockquote>
<p>{{ 'import.pocket_csv.how_to'|trans }}</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>{{ form.file.vars.label|trans }}</span>
{{ form_widget(form.file) }}
</div>
<div class="file-path-wrapper">
<input class="file-path validate" type="text">
</div>
</div>
<div class="input-field col s6 with-checkbox">
<h6>{{ 'import.form.mark_as_read_title'|trans }}</h6>
{{ form_widget(form.mark_as_read) }}
{{ form_label(form.mark_as_read) }}
</div>
</div>
{{ form_widget(form.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }}
{{ form_rest(form) }}
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,45 @@
{% extends "@WallabagCore/layout.html.twig" %}
{% block title %}{{ 'import.pocket_html.page_title'|trans }}{% endblock %}
{% block content %}
<div class="row">
<div class="col s12">
<div class="card-panel settings">
{% include '@WallabagImport/Import/_information.html.twig' %}
<div class="row">
<blockquote>{{ import.description|trans|raw }}</blockquote>
<p>{{ 'import.pocket_html.how_to'|trans }}</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>{{ form.file.vars.label|trans }}</span>
{{ form_widget(form.file) }}
</div>
<div class="file-path-wrapper">
<input class="file-path validate" type="text">
</div>
</div>
<div class="input-field col s6 with-checkbox">
<h6>{{ 'import.form.mark_as_read_title'|trans }}</h6>
{{ form_widget(form.mark_as_read) }}
{{ form_label(form.mark_as_read) }}
</div>
</div>
{{ form_widget(form.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }}
{{ form_rest(form) }}
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,45 @@
{% extends "@WallabagCore/layout.html.twig" %}
{% block title %}{{ 'import.shaarli.page_title'|trans }}{% endblock %}
{% block content %}
<div class="row">
<div class="col s12">
<div class="card-panel settings">
{% include '@WallabagImport/Import/_information.html.twig' %}
<div class="row">
<blockquote>{{ import.description|trans|raw }}</blockquote>
<p>{{ 'import.shaarli.how_to'|trans }}</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>{{ form.file.vars.label|trans }}</span>
{{ form_widget(form.file) }}
</div>
<div class="file-path-wrapper">
<input class="file-path validate" type="text">
</div>
</div>
<div class="input-field col s6 with-checkbox">
<h6>{{ 'import.form.mark_as_read_title'|trans }}</h6>
{{ form_widget(form.mark_as_read) }}
{{ form_label(form.mark_as_read) }}
</div>
</div>
{{ form_widget(form.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }}
{{ form_rest(form) }}
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -16,9 +16,23 @@
{% endblock fos_user_content %}
</div>
<div class="center">
<a href="{{ path('changeLocale', {'language': 'de'}) }}">Deutsch</a>
<a href="{{ path('changeLocale', {'language': 'en'}) }}">English</a>
<a href="{{ path('changeLocale', {'language': 'fr'}) }}">Français</a>
<form action="{{ path('changeLocale', {'language': 'de'}) }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('change-locale') }}"/>
<button type="submit" class="btn-link">Deutsch</button>
</form>
<form action="{{ path('changeLocale', {'language': 'en'}) }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('change-locale') }}"/>
<button type="submit" class="btn-link">English</button>
</form>
<form action="{{ path('changeLocale', {'language': 'fr'}) }}" method="post" class="inline-block">
<input type="hidden" name="token" value="{{ csrf_token('change-locale') }}"/>
<button type="submit" class="btn-link">Français</button>
</form>
</div>
</div>
</main>

View File

@ -105,7 +105,7 @@ class DeveloperControllerTest extends WallabagCoreTestCase
$this->logInAs('bob');
$client->request('POST', '/developer/client/delete/' . $adminApiClient->getId());
$this->assertSame(403, $client->getResponse()->getStatusCode());
$this->assertSame(400, $client->getResponse()->getStatusCode());
// Try to remove the admin's client with the good user
$this->logInAs('admin');

View File

@ -328,7 +328,8 @@ class ConfigControllerTest extends WallabagCoreTestCase
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('config.form_feed.no_token', $body[0]);
$client->request('GET', '/generate-token');
$client->submit($crawler->selectButton('config.form_feed.token_create')->form());
$this->assertSame(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
@ -337,38 +338,34 @@ class ConfigControllerTest extends WallabagCoreTestCase
$this->assertStringContainsString('config.form_feed.token_reset', $body[0]);
}
public function testGenerateTokenAjax()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$client->request(
'GET',
'/generate-token',
[],
[],
['HTTP_X-Requested-With' => 'XMLHttpRequest']
);
$this->assertSame(200, $client->getResponse()->getStatusCode());
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertArrayHasKey('token', $content);
}
public function testRevokeTokenAjax()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$client->request(
'GET',
'/revoke-token',
[],
[],
['HTTP_X-Requested-With' => 'XMLHttpRequest']
);
// set the token
$em = $client->getContainer()->get(EntityManagerInterface::class);
$user = $em
->getRepository(User::class)
->findOneByUsername('admin');
$this->assertSame(200, $client->getResponse()->getStatusCode());
if (!$user) {
$this->markTestSkipped('No user found in db.');
}
$config = $user->getConfig();
$config->setFeedToken('abcd1234');
$em->persist($config);
$em->flush();
$crawler = $client->request('GET', '/config');
$client->submit($crawler->selectButton('config.form_feed.token_revoke')->form());
$crawler = $client->followRedirect();
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('config.form_feed.token_create', $body[0]);
}
public function testFeedUpdate()
@ -484,9 +481,8 @@ class ConfigControllerTest extends WallabagCoreTestCase
$this->assertStringContainsString('readingTime <= 30', $crawler->filter('body')->extract(['_text'])[0]);
$deleteLink = $crawler->filter('.delete_tagging_rule')->last()->link();
$crawler = $client->submit($crawler->filter('#set5')->selectButton('delete')->form());
$crawler = $client->click($deleteLink);
$this->assertSame(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
@ -576,11 +572,11 @@ class ConfigControllerTest extends WallabagCoreTestCase
->getRepository(TaggingRule::class)
->findAll()[0];
$crawler = $client->request('GET', '/tagging-rule/delete/' . $rule->getId());
$crawler = $client->request('POST', '/tagging-rule/delete/' . $rule->getId());
$this->assertSame(403, $client->getResponse()->getStatusCode());
$this->assertSame(400, $client->getResponse()->getStatusCode());
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('You can not access this rule', $body[0]);
$this->assertStringContainsString('Bad CSRF token.', $body[0]);
}
public function testEditingTaggingRuleFromAnOtherUser()
@ -646,9 +642,9 @@ class ConfigControllerTest extends WallabagCoreTestCase
$this->assertStringContainsString('host = "example.org"', $crawler->filter('body')->extract(['_text'])[0]);
$deleteLink = $crawler->filter('div[id=set6] a.delete')->last()->link();
$form = $crawler->filter('#set6')->selectButton('delete')->form();
$crawler = $client->click($deleteLink);
$crawler = $client->submit($form);
$this->assertSame(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
@ -713,11 +709,11 @@ class ConfigControllerTest extends WallabagCoreTestCase
->getRepository(IgnoreOriginUserRule::class)
->findAll()[0];
$crawler = $client->request('GET', '/ignore-origin-user-rule/edit/' . $rule->getId());
$crawler = $client->request('POST', '/ignore-origin-user-rule/delete/' . $rule->getId());
$this->assertSame(403, $client->getResponse()->getStatusCode());
$this->assertSame(400, $client->getResponse()->getStatusCode());
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('You can not access this rule', $body[0]);
$this->assertStringContainsString('Bad CSRF token.', $body[0]);
}
public function testEditingIgnoreOriginRuleFromAnOtherUser()
@ -798,7 +794,7 @@ class ConfigControllerTest extends WallabagCoreTestCase
$this->assertStringNotContainsString('config.form_user.delete.button', $body[0]);
$client->request('POST', '/account/delete');
$this->assertSame(403, $client->getResponse()->getStatusCode());
$this->assertSame(400, $client->getResponse()->getStatusCode());
$user = $em
->getRepository(User::class)
@ -1120,37 +1116,38 @@ class ConfigControllerTest extends WallabagCoreTestCase
$this->logInAs('admin');
$client = $this->getTestClient();
$client->request('GET', '/unread/list');
$crawler = $client->request('GET', '/unread/list');
$this->assertStringContainsString('row data', $client->getResponse()->getContent());
$client->request('GET', '/config/view-mode');
$crawler = $client->followRedirect();
$form = $crawler->filter('.nb-results')->selectButton('view_list')->form();
$client->request('GET', '/unread/list');
$client->submit($form);
$client->followRedirect();
$this->assertStringContainsString('collection', $client->getResponse()->getContent());
$client->request('GET', '/config/view-mode');
}
public function testChangeLocaleWithoutReferer()
{
$client = $this->getTestClient();
$client->request('GET', '/locale/de');
$client->followRedirect();
$crawler = $client->request('POST', '/locale/de');
$this->assertSame('de', $client->getRequest()->getLocale());
$this->assertSame('de', $client->getContainer()->get(SessionInterface::class)->get('_locale'));
$this->assertSame(400, $client->getResponse()->getStatusCode());
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('Bad CSRF token.', $body[0]);
}
public function testChangeLocaleWithReferer()
{
$client = $this->getTestClient();
$client->request('GET', '/login');
$client->request('GET', '/locale/de');
$crawler = $client->request('GET', '/login');
$client->submit($crawler->selectButton('Deutsch')->form());
$client->followRedirect();
$this->assertSame('de', $client->getRequest()->getLocale());
@ -1161,8 +1158,12 @@ class ConfigControllerTest extends WallabagCoreTestCase
{
$client = $this->getTestClient();
$client->request('GET', '/login');
$client->request('GET', '/locale/yuyuyuyu');
$crawler = $client->request('GET', '/login');
$token = $crawler->filter('form[action="/locale/de"] input[name=token]')->attr('value');
$client->request('POST', '/locale/yuyuyuyu', [
'token' => $token,
]);
$client->followRedirect();
$this->assertNotSame('yuyuyuyu', $client->getRequest()->getLocale());
@ -1382,7 +1383,5 @@ class ConfigControllerTest extends WallabagCoreTestCase
$client->request('GET', '/unread/list');
$this->assertStringNotContainsString('class="preview"', $client->getResponse()->getContent());
$client->request('GET', '/config/view-mode');
}
}

View File

@ -17,9 +17,9 @@ use Wallabag\UserBundle\Entity\User;
class EntryControllerTest extends WallabagCoreTestCase
{
public const AN_URL_CONTAINING_AN_ARTICLE_WITH_IMAGE = 'https://www.lemonde.fr/judo/article/2017/11/11/judo-la-decima-de-teddy-riner_5213605_1556020.html';
public const AN_URL_CONTAINING_AN_ARTICLE_WITH_IMAGE = 'https://www.20minutes.fr/sport/jo_2024/4095122-20240712-jo-paris-2024-saut-ange-bombe-comment-anne-hidalgo-va-plonger-seine-si-fait-vraiment';
public $downloadImagesEnabled = false;
public $url = 'https://www.lemonde.fr/pixels/article/2019/06/18/ce-qu-il-faut-savoir-sur-le-libra-la-cryptomonnaie-de-facebook_5477887_4408996.html';
public $url = 'https://www.20minutes.fr/sport/jo_2024/4095122-20240712-jo-paris-2024-saut-ange-bombe-comment-anne-hidalgo-va-plonger-seine-si-fait-vraiment';
private $entryDataTestAttribute = '[data-test="entry"]';
/**
@ -175,9 +175,9 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->assertInstanceOf(Entry::class, $content);
$this->assertSame($this->url, $content->getUrl());
$this->assertStringContainsString('la cryptomonnaie de Facebook', $content->getTitle());
$this->assertStringContainsString('Comment Hidalgo', $content->getTitle());
$this->assertSame('fr', $content->getLanguage());
$this->assertArrayHasKey('x-frame-options', $content->getHeaders());
$this->assertArrayHasKey('cache-control', $content->getHeaders());
$client->getContainer()->get(Config::class)->set('store_article_headers', 0);
}
@ -509,7 +509,9 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->getEntityManager()->flush();
$this->getEntityManager()->clear();
$client->request('GET', '/reload/' . $entry->getId());
$crawler = $client->request('GET', '/view/' . $entry->getId());
$client->submit($crawler->selectButton('entry.view.left_menu.re_fetch_content')->form());
$this->assertSame(302, $client->getResponse()->getStatusCode());
@ -530,7 +532,9 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->getEntityManager()->persist($entry);
$this->getEntityManager()->flush();
$client->request('GET', '/reload/' . $entry->getId());
$crawler = $client->request('GET', '/view/' . $entry->getId());
$client->submit($crawler->selectButton('entry.view.left_menu.re_fetch_content')->form());
$this->assertSame(302, $client->getResponse()->getStatusCode());
@ -641,7 +645,9 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->getEntityManager()->flush();
$this->getEntityManager()->clear();
$client->request('GET', '/archive/' . $entry->getId());
$crawler = $client->request('GET', '/view/' . $entry->getId());
$client->submit($crawler->filter('.left-bar')->selectButton('entry.view.left_menu.set_as_read')->form());
$this->assertSame(302, $client->getResponse()->getStatusCode());
@ -664,7 +670,9 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->getEntityManager()->flush();
$this->getEntityManager()->clear();
$client->request('GET', '/star/' . $entry->getId());
$crawler = $client->request('GET', '/view/' . $entry->getId());
$client->submit($crawler->filter('.left-bar')->selectButton('entry.view.left_menu.set_as_starred')->form());
$this->assertSame(302, $client->getResponse()->getStatusCode());
@ -686,13 +694,11 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->getEntityManager()->persist($entry);
$this->getEntityManager()->flush();
$client->request('GET', '/delete/' . $entry->getId());
$crawler = $client->request('POST', '/delete/' . $entry->getId());
$this->assertSame(302, $client->getResponse()->getStatusCode());
$client->request('GET', '/delete/' . $entry->getId());
$this->assertSame(404, $client->getResponse()->getStatusCode());
$this->assertSame(400, $client->getResponse()->getStatusCode());
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('Bad CSRF token.', $body[0]);
}
/**
@ -728,10 +734,11 @@ class EntryControllerTest extends WallabagCoreTestCase
$em->persist($content);
$em->flush();
$client->request('GET', '/view/' . $content->getId());
$crawler = $client->request('GET', '/view/' . $content->getId());
$this->assertSame(200, $client->getResponse()->getStatusCode());
$client->request('GET', '/delete/' . $content->getId());
$client->submit($crawler->filter('.left-bar')->selectButton('entry.view.left_menu.delete')->form());
$this->assertSame(302, $client->getResponse()->getStatusCode());
$client->followRedirect();
@ -1148,7 +1155,10 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->assertSame(404, $client->getResponse()->getStatusCode());
// generating the uid
$client->request('GET', '/share/' . $content->getId());
$crawler = $client->request('GET', '/view/' . $content->getId());
$client->submit($crawler->filter('.left-bar')->selectButton('entry.view.left_menu.public_link')->form());
$this->assertSame(302, $client->getResponse()->getStatusCode());
$shareUrl = $client->getResponse()->getTargetUrl();
@ -1175,12 +1185,19 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->assertSame(404, $client->getResponse()->getStatusCode());
// removing the share
$client->request('GET', '/share/delete/' . $content->getId());
$client->getContainer()->get(Config::class)->set('share_public', 1);
$this->logInAs('admin');
$crawler = $client->request('GET', '/view/' . $content->getId());
$client->submit($crawler->filter('.left-bar')->selectButton('entry.view.left_menu.delete_public_link')->form());
$this->assertSame(302, $client->getResponse()->getStatusCode());
// share is now disable
// share is now removed
$client->request('GET', '/share/' . $content->getUid());
$this->assertSame(404, $client->getResponse()->getStatusCode());
$client->getContainer()->get(Config::class)->set('share_public', 0);
}
/**
@ -1218,7 +1235,7 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->assertInstanceOf(Entry::class, $entry);
$this->assertSame($url, $entry->getUrl());
$this->assertStringContainsString('Judo', $entry->getTitle());
$this->assertStringContainsString('Comment Hidalgo', $entry->getTitle());
// instead of checking for the filename (which might change) check that the image is now local
$this->assertStringContainsString(rtrim($client->getContainer()->getParameter('domain_name'), '/') . '/assets/images/', $entry->getContent());
@ -1256,7 +1273,9 @@ class EntryControllerTest extends WallabagCoreTestCase
->getRepository(Entry::class)
->findByUrlAndUserId($url, $this->getLoggedInUserId());
$client->request('GET', '/delete/' . $content->getId());
$crawler = $client->request('GET', '/view/' . $content->getId());
$client->submit($crawler->filter('.left-bar')->selectButton('entry.view.left_menu.delete')->form());
$this->assertSame(302, $client->getResponse()->getStatusCode());
@ -1279,8 +1298,9 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->getEntityManager()->flush();
$client->request('GET', '/view/' . $entry->getId());
$client->request('GET', '/archive/' . $entry->getId());
$crawler = $client->request('GET', '/view/' . $entry->getId());
$client->submit($crawler->filter('.left-bar')->selectButton('entry.view.left_menu.set_as_read')->form());
$this->assertSame(302, $client->getResponse()->getStatusCode());
$this->assertSame('/', $client->getResponse()->headers->get('location'));
@ -1304,8 +1324,7 @@ class EntryControllerTest extends WallabagCoreTestCase
$crawler = $client->request('GET', '/view/' . $entry->getId());
$link = $crawler->filter('a[id="markAsRead"]')->link();
$client->click($link);
$client->submit($crawler->filter('.left-bar')->selectButton('entry.view.left_menu.set_as_read')->form());
$this->assertSame(302, $client->getResponse()->getStatusCode());
$this->assertStringContainsString('/view/' . $entry->getId(), $client->getResponse()->headers->get('location'));
@ -1429,7 +1448,8 @@ class EntryControllerTest extends WallabagCoreTestCase
$crawler = $client->submit($form, $data);
$this->assertCount(1, $crawler->filter($this->entryDataTestAttribute));
$client->request('GET', '/delete/' . $entry->getId());
$client->submit($crawler->filter('.tools, .tools-list')->selectButton('delete')->form());
// test on list of all articles
$crawler = $client->request('GET', '/all/list');
@ -1475,6 +1495,38 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->assertCount(1, $crawler->filter($this->entryDataTestAttribute));
}
public function testActionInSearchResults()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$config = $this->getLoggedInUser()->getConfig();
$config->setActionMarkAsRead(ConfigEntity::REDIRECT_TO_CURRENT_PAGE);
$this->getEntityManager()->persist($config);
$entry = new Entry($this->getLoggedInUser());
$entry->setUrl($this->url);
$entry->setTitle('ActionInSearchResults');
$this->getEntityManager()->persist($entry);
$this->getEntityManager()->flush();
// Search on unread list
$crawler = $client->request('GET', '/unread/list');
$form = $crawler->filter('form[name=search]')->form();
$data = [
'search_entry[term]' => 'ActionInSearchResults',
];
$crawler = $client->submit($form, $data);
$currentUrl = $client->getRequest()->getUri();
$form = $crawler->filter('.tools, .tools-list')->selectButton('delete')->form();
$client->submit($form);
$client->followRedirect();
$nextUrl = $client->getRequest()->getUri();
$this->assertSame($currentUrl, $nextUrl);
}
public function dataForLanguage()
{
return [
@ -1499,7 +1551,7 @@ class EntryControllerTest extends WallabagCoreTestCase
'zh_CN',
],
'pt_BR' => [
'https://esportes.r7.com/lance/futebol/victor-hugo-e-matheus-franca-devem-desfalcar-flamengo-no-carioca-22112022',
'https://esportes.r7.com/lance/futebol/victor-hugo-e-matheus-franca-devem-desfalcar-flamengo-no-carioca-22112022/',
'pt_BR',
],
'es-ES' => [
@ -1642,7 +1694,7 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->assertSame('example.com', $content->getDomainName());
}
public function testEntryDeleteTagLink()
public function testEntryDeleteTagForm()
{
$this->logInAs('admin');
$client = $this->getTestClient();
@ -1653,10 +1705,7 @@ class EntryControllerTest extends WallabagCoreTestCase
$crawler = $client->request('GET', '/view/' . $entry->getId());
// As long as the deletion link of a tag is following
// a link to the tag view, we take the second one to retrieve
// the deletion link of the first tag
$link = $crawler->filter('body div#article div.tools ul.tags li.chip a')->extract(['href'])[1];
$link = $crawler->filter('body div#article div.tools ul.tags li.chip form')->extract(['action'])[0];
$this->assertStringStartsWith(sprintf('/remove-tag/%s/%s', $entry->getId(), $tag->getId()), $link);
}
@ -1712,11 +1761,15 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->getEntityManager()->clear();
$entries = [];
$entries[] = $entry1->getId();
$entries[] = $entry2->getId();
$entries[] = $entry1Id = $entry1->getId();
$entries[] = $entry2Id = $entry2->getId();
$crawler = $client->request('GET', '/all/list');
$token = $crawler->filter('#form_mass_action input[name=token]')->attr('value');
// Mass actions : archive
$client->request('POST', '/mass', [
'token' => $token,
'toggle-archive' => '',
'entry-checkbox' => $entries,
]);
@ -1737,8 +1790,12 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->assertSame(1, $res->isArchived());
$crawler = $client->request('GET', '/all/list');
$token = $crawler->filter('#form_mass_action input[name=token]')->attr('value');
// Mass actions : star
$client->request('POST', '/mass', [
'token' => $token,
'toggle-star' => '',
'entry-checkbox' => $entries,
]);
@ -1759,8 +1816,12 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->assertSame(1, $res->isStarred());
$crawler = $client->request('GET', '/all/list');
$token = $crawler->filter('#form_mass_action input[name=token]')->attr('value');
// Mass actions : tag
$client->request('POST', '/mass', [
'token' => $token,
'tag' => '',
'tags' => 'foo',
'entry-checkbox' => $entries,
@ -1789,17 +1850,29 @@ class EntryControllerTest extends WallabagCoreTestCase
$this->assertNotContains('foo', $res->getTagsLabel());
$crawler = $client->request('GET', '/all/list');
$token = $crawler->filter('#form_mass_action input[name=token]')->attr('value');
// Mass actions : delete
$client->request('POST', '/mass', [
'token' => $token,
'delete' => '',
'entry-checkbox' => $entries,
]);
$client->request('GET', '/delete/' . $entry1->getId());
$this->assertSame(404, $client->getResponse()->getStatusCode());
$res = $client->getContainer()
->get(EntityManagerInterface::class)
->getRepository(Entry::class)
->find($entry1Id);
$client->request('GET', '/delete/' . $entry2->getId());
$this->assertSame(404, $client->getResponse()->getStatusCode());
$this->assertNull($res);
$res = $client->getContainer()
->get(EntityManagerInterface::class)
->getRepository(Entry::class)
->find($entry2Id);
$this->assertNull($res);
}
public function testGetSameDomainEntries()

View File

@ -126,8 +126,8 @@ class TagControllerTest extends WallabagCoreTestCase
$crawler = $client->request('GET', '/view/' . $entry->getId());
$entryUri = $client->getRequest()->getRequestUri();
$link = $crawler->filter('a[href^="/remove-tag/' . $entry->getId() . '/' . $tag->getId() . '"]')->link();
$client->click($link);
$form = $crawler->filter('form[action^="/remove-tag/' . $entry->getId() . '/' . $tag->getId() . '"]')->form();
$client->submit($form);
$this->assertSame(302, $client->getResponse()->getStatusCode());
$this->assertSame($entryUri, $client->getResponse()->getTargetUrl());
@ -136,9 +136,8 @@ class TagControllerTest extends WallabagCoreTestCase
$entry = $this->getEntityManager()->getRepository(Entry::class)->find($entry->getId());
$this->assertNotContains($this->tagName, $entry->getTagsLabel());
$client->request('GET', '/remove-tag/' . $entry->getId() . '/' . $tag->getId());
$this->assertSame(404, $client->getResponse()->getStatusCode());
$client->request('GET', '/view/' . $entry->getId());
$this->assertStringNotContainsString('/remove-tag/' . $entry->getId() . '/' . $tag->getId(), $client->getResponse()->getContent());
$tag = $client->getContainer()
->get(EntityManagerInterface::class)
@ -169,8 +168,8 @@ class TagControllerTest extends WallabagCoreTestCase
$this->getEntityManager()->clear();
$crawler = $client->request('GET', '/tag/list');
$link = $crawler->filter('a[id="delete-' . $tag->getSlug() . '"]')->link();
$client->click($link);
$form = $crawler->filter('#tag-' . $tag->getId())->selectButton('delete')->form();
$client->submit($form);
$tag = $client->getContainer()
->get(EntityManagerInterface::class)
@ -548,7 +547,7 @@ class TagControllerTest extends WallabagCoreTestCase
$crawler = $client->submit($form, $data);
$client->click($crawler->selectLink('entry.list.assign_search_tag')->link());
$client->submit($crawler->selectButton('entry.list.assign_search_tag')->form());
$client->followRedirect();
$entries = $client->getContainer()

View File

@ -5,6 +5,7 @@ namespace Tests\Wallabag\CoreBundle\Twig;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\AnnotationBundle\Repository\AnnotationRepository;
use Wallabag\CoreBundle\Repository\EntryRepository;
use Wallabag\CoreBundle\Repository\TagRepository;
use Wallabag\CoreBundle\Twig\WallabagExtension;
@ -17,6 +18,10 @@ class WallabagExtensionTest extends TestCase
->disableOriginalConstructor()
->getMock();
$annotationRepository = $this->getMockBuilder(AnnotationRepository::class)
->disableOriginalConstructor()
->getMock();
$tagRepository = $this->getMockBuilder(TagRepository::class)
->disableOriginalConstructor()
->getMock();
@ -29,7 +34,7 @@ class WallabagExtensionTest extends TestCase
->disableOriginalConstructor()
->getMock();
$extension = new WallabagExtension($entryRepository, $tagRepository, $tokenStorage, 0, $translator, '');
$extension = new WallabagExtension($entryRepository, $annotationRepository, $tagRepository, $tokenStorage, 0, $translator, '');
$this->assertSame('lemonde.fr', $extension->removeWww('www.lemonde.fr'));
$this->assertSame('lemonde.fr', $extension->removeWww('lemonde.fr'));
@ -42,6 +47,10 @@ class WallabagExtensionTest extends TestCase
->disableOriginalConstructor()
->getMock();
$annotationRepository = $this->getMockBuilder(AnnotationRepository::class)
->disableOriginalConstructor()
->getMock();
$tagRepository = $this->getMockBuilder(TagRepository::class)
->disableOriginalConstructor()
->getMock();
@ -54,7 +63,7 @@ class WallabagExtensionTest extends TestCase
->disableOriginalConstructor()
->getMock();
$extension = new WallabagExtension($entryRepository, $tagRepository, $tokenStorage, 0, $translator, '');
$extension = new WallabagExtension($entryRepository, $annotationRepository, $tagRepository, $tokenStorage, 0, $translator, '');
$this->assertSame('lemonde.fr', $extension->removeScheme('lemonde.fr'));
$this->assertSame('gist.github.com', $extension->removeScheme('gist.github.com'));
@ -67,6 +76,10 @@ class WallabagExtensionTest extends TestCase
->disableOriginalConstructor()
->getMock();
$annotationRepository = $this->getMockBuilder(AnnotationRepository::class)
->disableOriginalConstructor()
->getMock();
$tagRepository = $this->getMockBuilder(TagRepository::class)
->disableOriginalConstructor()
->getMock();
@ -79,7 +92,7 @@ class WallabagExtensionTest extends TestCase
->disableOriginalConstructor()
->getMock();
$extension = new WallabagExtension($entryRepository, $tagRepository, $tokenStorage, 0, $translator, '');
$extension = new WallabagExtension($entryRepository, $annotationRepository, $tagRepository, $tokenStorage, 0, $translator, '');
$this->assertSame('lemonde.fr', $extension->removeSchemeAndWww('www.lemonde.fr'));
$this->assertSame('lemonde.fr', $extension->removeSchemeAndWww('http://lemonde.fr'));

View File

@ -123,27 +123,10 @@ class FirefoxControllerTest extends WallabagCoreTestCase
);
$this->assertInstanceOf(Entry::class, $content);
$this->assertNotEmpty($content->getMimetype(), 'Mimetype for http://lexpansion.lexpress.fr is ok');
$this->assertNotEmpty($content->getPreviewPicture(), 'Preview picture for http://lexpansion.lexpress.fr is ok');
$this->assertNotEmpty($content->getLanguage(), 'Language for http://lexpansion.lexpress.fr is ok');
$this->assertNotEmpty($content->getMimetype(), 'Mimetype for 20minutes.fr is ok');
$this->assertNotEmpty($content->getPreviewPicture(), 'Preview picture for 20minutes.fr is ok');
$this->assertNotEmpty($content->getLanguage(), 'Language for 20minutes.fr is ok');
$this->assertCount(3, $content->getTags());
$content = $client->getContainer()
->get(EntityManagerInterface::class)
->getRepository(Entry::class)
->findByUrlAndUserId(
'https://www.lemonde.fr/disparitions/article/2018/07/05/le-journaliste-et-cineaste-claude-lanzmann-est-mort_5326313_3382.html',
$this->getLoggedInUserId()
);
$this->assertInstanceOf(Entry::class, $content);
$this->assertNotEmpty($content->getMimetype(), 'Mimetype for https://www.lemonde.fr is ok');
$this->assertNotEmpty($content->getPreviewPicture(), 'Preview picture for https://www.lemonde.fr is ok');
$this->assertNotEmpty($content->getLanguage(), 'Language for https://www.lemonde.fr is ok');
$createdAt = $content->getCreatedAt();
$this->assertSame('2013', $createdAt->format('Y'));
$this->assertSame('12', $createdAt->format('m'));
}
public function testImportWallabagWithEmptyFile()

View File

@ -24,6 +24,6 @@ class ImportControllerTest extends WallabagCoreTestCase
$crawler = $client->request('GET', '/import/');
$this->assertSame(200, $client->getResponse()->getStatusCode());
$this->assertSame(10, $crawler->filter('blockquote')->count());
$this->assertSame(14, $crawler->filter('blockquote')->count());
}
}

View File

@ -0,0 +1,203 @@
<?php
namespace Tests\Wallabag\ImportBundle\Controller;
use Craue\ConfigBundle\Util\Config;
use Doctrine\ORM\EntityManagerInterface;
use Predis\Client;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Tests\Wallabag\CoreBundle\WallabagCoreTestCase;
use Wallabag\CoreBundle\Entity\Entry;
class OmnivoreControllerTest extends WallabagCoreTestCase
{
public function testImportOmnivore()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/import/omnivore');
$this->assertSame(200, $client->getResponse()->getStatusCode());
$this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count());
$this->assertSame(1, $crawler->filter('input[type=file]')->count());
}
public function testImportOmnivoreWithRabbitEnabled()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$client->getContainer()->get(Config::class)->set('import_with_rabbitmq', 1);
$crawler = $client->request('GET', '/import/omnivore');
$this->assertSame(200, $client->getResponse()->getStatusCode());
$this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count());
$this->assertSame(1, $crawler->filter('input[type=file]')->count());
$client->getContainer()->get(Config::class)->set('import_with_rabbitmq', 0);
}
public function testImportOmnivoreBadFile()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/import/omnivore');
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$data = [
'upload_import_file[file]' => '',
];
$client->submit($form, $data);
$this->assertSame(200, $client->getResponse()->getStatusCode());
}
public function testImportOmnivoreWithRedisEnabled()
{
$this->checkRedis();
$this->logInAs('admin');
$client = $this->getTestClient();
$client->getContainer()->get(Config::class)->set('import_with_redis', 1);
$crawler = $client->request('GET', '/import/omnivore');
$this->assertSame(200, $client->getResponse()->getStatusCode());
$this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count());
$this->assertSame(1, $crawler->filter('input[type=file]')->count());
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$file = new UploadedFile(__DIR__ . '/../fixtures/omnivore.json', 'omnivore.json');
$data = [
'upload_import_file[file]' => $file,
];
$client->submit($form, $data);
$this->assertSame(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('flashes.import.notice.summary', $body[0]);
$this->assertNotEmpty($client->getContainer()->get(Client::class)->lpop('wallabag.import.omnivore'));
$client->getContainer()->get(Config::class)->set('import_with_redis', 0);
}
public function testImportOmnivoreWithFile()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/import/omnivore');
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$file = new UploadedFile(__DIR__ . '/../fixtures/omnivore.json', 'omnivore.json');
$data = [
'upload_import_file[file]' => $file,
];
$client->submit($form, $data);
$this->assertSame(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
$content = $client->getContainer()
->get(EntityManagerInterface::class)
->getRepository(Entry::class)
->findByUrlAndUserId(
'https://www.lemonde.fr/economie/article/2024/10/29/malgre-la-crise-du-marche-des-montres-breitling-etend-son-reseau-commercial-et-devoile-ses-ambitions_6365425_3234.html',
$this->getLoggedInUserId()
);
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('flashes.import.notice.summary', $body[0]);
$this->assertInstanceOf(Entry::class, $content);
$tags = $content->getTagsLabel();
$this->assertContains('rss', $tags, 'It includes the "rss" tag');
$this->assertGreaterThanOrEqual(2, \count($tags));
$this->assertInstanceOf(\DateTime::class, $content->getCreatedAt());
$this->assertSame('2024-10-29', $content->getCreatedAt()->format('Y-m-d'));
}
public function testImportOmnivoreWithFileAndMarkAllAsRead()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/import/omnivore');
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$file = new UploadedFile(__DIR__ . '/../fixtures/omnivore.json', 'omnivore-read.json');
$data = [
'upload_import_file[file]' => $file,
'upload_import_file[mark_as_read]' => 1,
];
$client->submit($form, $data);
$this->assertSame(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
$content1 = $client->getContainer()
->get(EntityManagerInterface::class)
->getRepository(Entry::class)
->findByUrlAndUserId(
'https://www.lemonde.fr/economie/article/2024/10/29/l-union-europeenne-adopte-jusqu-a-35-de-surtaxes-sur-les-voitures-electriques-importees-de-chine_6365258_3234.html',
$this->getLoggedInUserId()
);
$this->assertInstanceOf(Entry::class, $content1);
$content2 = $client->getContainer()
->get(EntityManagerInterface::class)
->getRepository(Entry::class)
->findByUrlAndUserId(
'https://www.lemonde.fr/les-decodeurs/article/2024/10/29/presidentielle-americaine-2024-comment-le-calendrier-de-l-election-et-des-affaires-judiciaires-de-trump-s-entremelent_6210916_3211.html',
$this->getLoggedInUserId()
);
$this->assertInstanceOf(Entry::class, $content2);
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('flashes.import.notice.summary', $body[0]);
}
public function testImportOmnivoreWithEmptyFile()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/import/omnivore');
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$file = new UploadedFile(__DIR__ . '/../fixtures/test.txt', 'test.txt');
$data = [
'upload_import_file[file]' => $file,
];
$client->submit($form, $data);
$this->assertSame(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('flashes.import.notice.failed', $body[0]);
}
}

View File

@ -0,0 +1,168 @@
<?php
namespace Tests\Wallabag\ImportBundle\Controller;
use Craue\ConfigBundle\Util\Config;
use Doctrine\ORM\EntityManagerInterface;
use Predis\Client;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Tests\Wallabag\CoreBundle\WallabagCoreTestCase;
use Wallabag\CoreBundle\Entity\Entry;
class PocketCsvControllerTest extends WallabagCoreTestCase
{
public function testImportPocketCsv()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/import/pocket_csv');
$this->assertSame(200, $client->getResponse()->getStatusCode());
$this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count());
$this->assertSame(1, $crawler->filter('input[type=file]')->count());
}
public function testImportPocketCsvWithRabbitEnabled()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$client->getContainer()->get(Config::class)->set('import_with_rabbitmq', 1);
$crawler = $client->request('GET', '/import/pocket_csv');
$this->assertSame(200, $client->getResponse()->getStatusCode());
$this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count());
$this->assertSame(1, $crawler->filter('input[type=file]')->count());
$client->getContainer()->get(Config::class)->set('import_with_rabbitmq', 0);
}
public function testImportPocketCsvBadFile()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/import/pocket_csv');
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$data = [
'upload_import_file[file]' => '',
];
$client->submit($form, $data);
$this->assertSame(200, $client->getResponse()->getStatusCode());
}
public function testImportPocketCsvWithRedisEnabled()
{
$this->checkRedis();
$this->logInAs('admin');
$client = $this->getTestClient();
$client->getContainer()->get(Config::class)->set('import_with_redis', 1);
$crawler = $client->request('GET', '/import/pocket_csv');
$this->assertSame(200, $client->getResponse()->getStatusCode());
$this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count());
$this->assertSame(1, $crawler->filter('input[type=file]')->count());
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$file = new UploadedFile(__DIR__ . '/../fixtures/pocket.csv', 'Bookmarks');
$data = [
'upload_import_file[file]' => $file,
];
$client->submit($form, $data);
$this->assertSame(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('flashes.import.notice.summary', $body[0]);
$this->assertNotEmpty($client->getContainer()->get(Client::class)->lpop('wallabag.import.pocket_csv'));
$client->getContainer()->get(Config::class)->set('import_with_redis', 0);
}
public function testImportWallabagWithPocketCsvFile()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/import/pocket_csv');
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$file = new UploadedFile(__DIR__ . '/../fixtures/pocket.csv', 'Bookmarks');
$data = [
'upload_import_file[file]' => $file,
];
$client->submit($form, $data);
$this->assertSame(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('flashes.import.notice.summary', $body[0]);
$entries = $client->getContainer()
->get(EntityManagerInterface::class)
->getRepository(Entry::class)
->findBy(['user' => $this->getLoggedInUserId()]);
$expectedEntries = [
'http://youmightnotneedjquery.com/,1600322788',
'https://jp-lambert.me/est-ce-que-jai-besoin-d-un-scrum-master-604f5a471c73',
'https://www.monde-diplomatique.fr/2020/09/HALIMI/62165',
'https://www.reddit.com/r/DataHoarder/comments/ioupbk/archivebox_question_how_do_i_import_links_from_a/',
'https://www.numerama.com/politique/646826-tu-vas-pleurer-les-premieres-fois-que-se-passe-t-il-au-sein-du-studio-dubisoft-derriere-trackmania.html#utm_medium=distibuted&utm_source=rss&utm_campaign=646826',
'https://www.nouvelobs.com/rue89/20200911.OBS33165/comment-konbini-s-est-fait-pieger-par-un-pere-masculiniste.html',
'https://reporterre.net/Des-abeilles-pour-resoudre-les-conflits-entre-les-humains-et-les-elephants',
];
$matchedEntries = array_map(function ($expectedUrl) use ($entries) {
foreach ($entries as $entry) {
if ($entry->getUrl() === $expectedUrl) {
return $entry;
}
}
return null;
}, $expectedEntries);
$this->assertCount(\count($expectedEntries), $matchedEntries, 'Should have 7 entries imported from pocket.csv');
}
public function testImportWallabagWithEmptyFile()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/import/pocket_csv');
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$file = new UploadedFile(__DIR__ . '/../fixtures/test.csv', 'test.csv');
$data = [
'upload_import_file[file]' => $file,
];
$client->submit($form, $data);
$this->assertSame(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('flashes.import.notice.failed', $body[0]);
}
}

View File

@ -0,0 +1,168 @@
<?php
namespace Tests\Wallabag\ImportBundle\Controller;
use Craue\ConfigBundle\Util\Config;
use Doctrine\ORM\EntityManagerInterface;
use Predis\Client;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Tests\Wallabag\CoreBundle\WallabagCoreTestCase;
use Wallabag\CoreBundle\Entity\Entry;
class PocketHtmlControllerTest extends WallabagCoreTestCase
{
public function testImportPocketHtml()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/import/pocket_html');
$this->assertSame(200, $client->getResponse()->getStatusCode());
$this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count());
$this->assertSame(1, $crawler->filter('input[type=file]')->count());
}
public function testImportPocketHtmlWithRabbitEnabled()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$client->getContainer()->get(Config::class)->set('import_with_rabbitmq', 1);
$crawler = $client->request('GET', '/import/pocket_html');
$this->assertSame(200, $client->getResponse()->getStatusCode());
$this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count());
$this->assertSame(1, $crawler->filter('input[type=file]')->count());
$client->getContainer()->get(Config::class)->set('import_with_rabbitmq', 0);
}
public function testImportPocketHtmlBadFile()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/import/pocket_html');
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$data = [
'upload_import_file[file]' => '',
];
$client->submit($form, $data);
$this->assertSame(200, $client->getResponse()->getStatusCode());
}
public function testImportPocketHtmlWithRedisEnabled()
{
$this->checkRedis();
$this->logInAs('admin');
$client = $this->getTestClient();
$client->getContainer()->get(Config::class)->set('import_with_redis', 1);
$crawler = $client->request('GET', '/import/pocket_html');
$this->assertSame(200, $client->getResponse()->getStatusCode());
$this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count());
$this->assertSame(1, $crawler->filter('input[type=file]')->count());
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$file = new UploadedFile(__DIR__ . '/../fixtures/ril_export.html', 'Bookmarks');
$data = [
'upload_import_file[file]' => $file,
];
$client->submit($form, $data);
$this->assertSame(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('flashes.import.notice.summary', $body[0]);
$this->assertNotEmpty($client->getContainer()->get(Client::class)->lpop('wallabag.import.pocket_html'));
$client->getContainer()->get(Config::class)->set('import_with_redis', 0);
}
public function testImportWallabagWithPocketHtmlFile()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/import/pocket_html');
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$file = new UploadedFile(__DIR__ . '/../fixtures/ril_export.html', 'Bookmarks');
$data = [
'upload_import_file[file]' => $file,
];
$client->submit($form, $data);
$this->assertSame(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('flashes.import.notice.summary', $body[0]);
$content = $client->getContainer()
->get(EntityManagerInterface::class)
->getRepository(Entry::class)
->findByUrlAndUserId(
'https://www.20minutes.fr/sport/4002755-20220928-tarn-lapins-ravagent-terrain-match-rugby-doit-etre-annule',
$this->getLoggedInUserId()
);
$this->assertInstanceOf(Entry::class, $content);
$this->assertNotEmpty($content->getMimetype(), 'Mimetype for 20minutes.fr is ok');
$this->assertNotEmpty($content->getPreviewPicture(), 'Preview picture for 20minutes.fr is ok');
$this->assertNotEmpty($content->getLanguage(), 'Language for 20minutes.fr is ok');
$this->assertCount(3, $content->getTags());
$content = $client->getContainer()
->get(EntityManagerInterface::class)
->getRepository(Entry::class)
->findByUrlAndUserId(
'https://www.20minutes.fr/paris/4100740-20240715-jo-paris-2024-courir-capitale-maintenant-quais-fermes',
$this->getLoggedInUserId()
);
$this->assertInstanceOf(Entry::class, $content);
$this->assertNotEmpty($content->getMimetype(), 'Mimetype for 20minutes.fr is ok');
$this->assertNotEmpty($content->getPreviewPicture(), 'Preview picture for 20minutes.fr is ok');
$this->assertNotEmpty($content->getLanguage(), 'Language for 20minutes.fr is ok');
}
public function testImportWallabagWithEmptyFile()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/import/pocket_html');
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$file = new UploadedFile(__DIR__ . '/../fixtures/test.html', 'test.html');
$data = [
'upload_import_file[file]' => $file,
];
$client->submit($form, $data);
$this->assertSame(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('flashes.import.notice.failed', $body[0]);
}
}

View File

@ -0,0 +1,168 @@
<?php
namespace Tests\Wallabag\ImportBundle\Controller;
use Craue\ConfigBundle\Util\Config;
use Doctrine\ORM\EntityManagerInterface;
use Predis\Client;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Tests\Wallabag\CoreBundle\WallabagCoreTestCase;
use Wallabag\CoreBundle\Entity\Entry;
class ShaarliControllerTest extends WallabagCoreTestCase
{
public function testImportShaarli()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/import/shaarli');
$this->assertSame(200, $client->getResponse()->getStatusCode());
$this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count());
$this->assertSame(1, $crawler->filter('input[type=file]')->count());
}
public function testImportShaarliWithRabbitEnabled()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$client->getContainer()->get(Config::class)->set('import_with_rabbitmq', 1);
$crawler = $client->request('GET', '/import/shaarli');
$this->assertSame(200, $client->getResponse()->getStatusCode());
$this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count());
$this->assertSame(1, $crawler->filter('input[type=file]')->count());
$client->getContainer()->get(Config::class)->set('import_with_rabbitmq', 0);
}
public function testImportShaarliBadFile()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/import/shaarli');
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$data = [
'upload_import_file[file]' => '',
];
$client->submit($form, $data);
$this->assertSame(200, $client->getResponse()->getStatusCode());
}
public function testImportShaarliWithRedisEnabled()
{
$this->checkRedis();
$this->logInAs('admin');
$client = $this->getTestClient();
$client->getContainer()->get(Config::class)->set('import_with_redis', 1);
$crawler = $client->request('GET', '/import/shaarli');
$this->assertSame(200, $client->getResponse()->getStatusCode());
$this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count());
$this->assertSame(1, $crawler->filter('input[type=file]')->count());
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$file = new UploadedFile(__DIR__ . '/../fixtures/shaarli-bookmarks.html', 'Bookmarks');
$data = [
'upload_import_file[file]' => $file,
];
$client->submit($form, $data);
$this->assertSame(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('flashes.import.notice.summary', $body[0]);
$this->assertNotEmpty($client->getContainer()->get(Client::class)->lpop('wallabag.import.shaarli'));
$client->getContainer()->get(Config::class)->set('import_with_redis', 0);
}
public function testImportWallabagWithShaarliFile()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/import/shaarli');
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$file = new UploadedFile(__DIR__ . '/../fixtures/shaarli-bookmarks.html', 'Bookmarks');
$data = [
'upload_import_file[file]' => $file,
];
$client->submit($form, $data);
$this->assertSame(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('flashes.import.notice.summary', $body[0]);
$content = $client->getContainer()
->get(EntityManagerInterface::class)
->getRepository(Entry::class)
->findByUrlAndUserId(
'https://www.20minutes.fr/sport/4002755-20220928-tarn-lapins-ravagent-terrain-match-rugby-doit-etre-annule',
$this->getLoggedInUserId()
);
$this->assertInstanceOf(Entry::class, $content);
$this->assertNotEmpty($content->getMimetype(), 'Mimetype for 20minutes.fr is ok');
$this->assertNotEmpty($content->getPreviewPicture(), 'Preview picture for 20minutes.fr is ok');
$this->assertNotEmpty($content->getLanguage(), 'Language for 20minutes.fr is ok');
$this->assertCount(2, $content->getTags());
$content = $client->getContainer()
->get(EntityManagerInterface::class)
->getRepository(Entry::class)
->findByUrlAndUserId(
'https://www.20minutes.fr/paris/4100740-20240715-jo-paris-2024-courir-capitale-maintenant-quais-fermes',
$this->getLoggedInUserId()
);
$this->assertInstanceOf(Entry::class, $content);
$this->assertNotEmpty($content->getMimetype(), 'Mimetype for 20minutes.fr is ok');
$this->assertNotEmpty($content->getPreviewPicture(), 'Preview picture for 20minutes.fr is ok');
$this->assertNotEmpty($content->getLanguage(), 'Language for 20minutes.fr is ok');
}
public function testImportWallabagWithEmptyFile()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/import/shaarli');
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$file = new UploadedFile(__DIR__ . '/../fixtures/test.html', 'test.html');
$data = [
'upload_import_file[file]' => $file,
];
$client->submit($form, $data);
$this->assertSame(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('flashes.import.notice.failed', $body[0]);
}
}

View File

@ -0,0 +1,252 @@
<?php
namespace Tests\Wallabag\ImportBundle\Import;
use Doctrine\ORM\EntityManager;
use M6Web\Component\RedisMock\RedisMockFactory;
use Monolog\Handler\TestHandler;
use Monolog\Logger;
use PHPUnit\Framework\TestCase;
use Predis\Client;
use Simpleue\Queue\RedisQueue;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Wallabag\CoreBundle\Entity\Entry;
use Wallabag\CoreBundle\Helper\ContentProxy;
use Wallabag\CoreBundle\Helper\TagsAssigner;
use Wallabag\CoreBundle\Repository\EntryRepository;
use Wallabag\ImportBundle\Import\PocketCsvImport;
use Wallabag\ImportBundle\Redis\Producer;
use Wallabag\UserBundle\Entity\User;
class PocketCsvImportTest extends TestCase
{
protected $user;
protected $em;
protected $logHandler;
protected $contentProxy;
protected $tagsAssigner;
public function testInit()
{
$pocketCsvImport = $this->getPocketCsvImport();
$this->assertSame('Pocket CSV', $pocketCsvImport->getName());
$this->assertNotEmpty($pocketCsvImport->getUrl());
$this->assertSame('import.pocket_csv.description', $pocketCsvImport->getDescription());
}
public function testImport()
{
$pocketCsvImport = $this->getPocketCsvImport(false, 7);
$pocketCsvImport->setFilepath(__DIR__ . '/../fixtures/pocket.csv');
$entryRepo = $this->getMockBuilder(EntryRepository::class)
->disableOriginalConstructor()
->getMock();
$entryRepo->expects($this->exactly(7))
->method('findByUrlAndUserId')
->willReturn(false);
$this->em
->expects($this->any())
->method('getRepository')
->willReturn($entryRepo);
$entry = $this->getMockBuilder(Entry::class)
->disableOriginalConstructor()
->getMock();
$this->contentProxy
->expects($this->exactly(7))
->method('updateEntry')
->willReturn($entry);
$res = $pocketCsvImport->import();
$this->assertTrue($res);
$this->assertSame(['skipped' => 0, 'imported' => 7, 'queued' => 0], $pocketCsvImport->getSummary());
}
public function testImportAndMarkAllAsRead()
{
$pocketCsvImport = $this->getPocketCsvImport(false, 1);
$pocketCsvImport->setFilepath(__DIR__ . '/../fixtures/pocket.csv');
$entryRepo = $this->getMockBuilder(EntryRepository::class)
->disableOriginalConstructor()
->getMock();
$entryRepo->expects($this->exactly(7))
->method('findByUrlAndUserId')
->will($this->onConsecutiveCalls(false, true));
$this->em
->expects($this->any())
->method('getRepository')
->willReturn($entryRepo);
$this->contentProxy
->expects($this->exactly(1))
->method('updateEntry')
->willReturn(new Entry($this->user));
// check that every entry persisted are archived
$this->em
->expects($this->any())
->method('persist')
->with($this->callback(fn ($persistedEntry) => (bool) $persistedEntry->isArchived()));
$res = $pocketCsvImport
->setMarkAsRead(true)
->import();
$this->assertTrue($res);
$this->assertSame(['skipped' => 6, 'imported' => 1, 'queued' => 0], $pocketCsvImport->getSummary());
}
public function testImportWithRabbit()
{
$pocketCsvImport = $this->getPocketCsvImport();
$pocketCsvImport->setFilepath(__DIR__ . '/../fixtures/pocket.csv');
$entryRepo = $this->getMockBuilder(EntryRepository::class)
->disableOriginalConstructor()
->getMock();
$entryRepo->expects($this->never())
->method('findByUrlAndUserId');
$this->em
->expects($this->never())
->method('getRepository');
$entry = $this->getMockBuilder(Entry::class)
->disableOriginalConstructor()
->getMock();
$this->contentProxy
->expects($this->never())
->method('updateEntry');
$producer = $this->getMockBuilder(\OldSound\RabbitMqBundle\RabbitMq\Producer::class)
->disableOriginalConstructor()
->getMock();
$producer
->expects($this->exactly(7))
->method('publish');
$pocketCsvImport->setProducer($producer);
$res = $pocketCsvImport->setMarkAsRead(true)->import();
$this->assertTrue($res);
$this->assertSame(['skipped' => 0, 'imported' => 0, 'queued' => 7], $pocketCsvImport->getSummary());
}
public function testImportWithRedis()
{
$pocketCsvImport = $this->getPocketCsvImport();
$pocketCsvImport->setFilepath(__DIR__ . '/../fixtures/pocket.csv');
$entryRepo = $this->getMockBuilder(EntryRepository::class)
->disableOriginalConstructor()
->getMock();
$entryRepo->expects($this->never())
->method('findByUrlAndUserId');
$this->em
->expects($this->never())
->method('getRepository');
$entry = $this->getMockBuilder(Entry::class)
->disableOriginalConstructor()
->getMock();
$this->contentProxy
->expects($this->never())
->method('updateEntry');
$factory = new RedisMockFactory();
$redisMock = $factory->getAdapter(Client::class, true);
$queue = new RedisQueue($redisMock, 'pocket_csv');
$producer = new Producer($queue);
$pocketCsvImport->setProducer($producer);
$res = $pocketCsvImport->setMarkAsRead(true)->import();
$this->assertTrue($res);
$this->assertSame(['skipped' => 0, 'imported' => 0, 'queued' => 7], $pocketCsvImport->getSummary());
$this->assertNotEmpty($redisMock->lpop('pocket_csv'));
}
public function testImportBadFile()
{
$pocketCsvImport = $this->getPocketCsvImport();
$pocketCsvImport->setFilepath(__DIR__ . '/../fixtures/Import/wallabag-v1.jsonx');
$res = $pocketCsvImport->import();
$this->assertFalse($res);
$records = $this->logHandler->getRecords();
$this->assertStringContainsString('Pocket CSV Import: unable to read file', $records[0]['message']);
$this->assertSame('ERROR', $records[0]['level_name']);
}
public function testImportUserNotDefined()
{
$pocketCsvImport = $this->getPocketCsvImport(true);
$pocketCsvImport->setFilepath(__DIR__ . '/../fixtures/pocket.csv');
$res = $pocketCsvImport->import();
$this->assertFalse($res);
$records = $this->logHandler->getRecords();
$this->assertStringContainsString('Pocket CSV Import: user is not defined', $records[0]['message']);
$this->assertSame('ERROR', $records[0]['level_name']);
}
private function getPocketCsvImport($unsetUser = false, $dispatched = 0)
{
$this->user = new User();
$this->em = $this->getMockBuilder(EntityManager::class)
->disableOriginalConstructor()
->getMock();
$this->contentProxy = $this->getMockBuilder(ContentProxy::class)
->disableOriginalConstructor()
->getMock();
$this->tagsAssigner = $this->getMockBuilder(TagsAssigner::class)
->disableOriginalConstructor()
->getMock();
$dispatcher = $this->getMockBuilder(EventDispatcher::class)
->disableOriginalConstructor()
->getMock();
$dispatcher
->expects($this->exactly($dispatched))
->method('dispatch');
$this->logHandler = new TestHandler();
$logger = new Logger('test', [$this->logHandler]);
$wallabag = new PocketCsvImport($this->em, $this->contentProxy, $this->tagsAssigner, $dispatcher, $logger);
if (false === $unsetUser) {
$wallabag->setUser($this->user);
}
return $wallabag;
}
}

View File

@ -0,0 +1,254 @@
<?php
namespace Tests\Wallabag\ImportBundle\Import;
use Doctrine\ORM\EntityManager;
use M6Web\Component\RedisMock\RedisMockFactory;
use Monolog\Handler\TestHandler;
use Monolog\Logger;
use PHPUnit\Framework\TestCase;
use Predis\Client;
use Simpleue\Queue\RedisQueue;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Wallabag\CoreBundle\Entity\Entry;
use Wallabag\CoreBundle\Helper\ContentProxy;
use Wallabag\CoreBundle\Helper\TagsAssigner;
use Wallabag\CoreBundle\Repository\EntryRepository;
use Wallabag\ImportBundle\Import\PocketHtmlImport;
use Wallabag\ImportBundle\Redis\Producer;
use Wallabag\UserBundle\Entity\User;
class PocketHtmlImportTest extends TestCase
{
protected $user;
protected $em;
protected $logHandler;
protected $contentProxy;
protected $tagsAssigner;
public function testInit()
{
$pocketHtmlImport = $this->getPocketHtmlImport();
$this->assertSame('Pocket HTML', $pocketHtmlImport->getName());
$this->assertNotEmpty($pocketHtmlImport->getUrl());
$this->assertSame('import.pocket_html.description', $pocketHtmlImport->getDescription());
}
public function testImport()
{
$pocketHtmlImport = $this->getPocketHtmlImport(false, 2);
$pocketHtmlImport->setFilepath(__DIR__ . '/../fixtures/ril_export.html');
$entryRepo = $this->getMockBuilder(EntryRepository::class)
->disableOriginalConstructor()
->getMock();
$entryRepo->expects($this->exactly(2))
->method('findByUrlAndUserId')
->willReturn(false);
$this->em
->expects($this->any())
->method('getRepository')
->willReturn($entryRepo);
$entry = $this->getMockBuilder(Entry::class)
->disableOriginalConstructor()
->getMock();
$this->contentProxy
->expects($this->exactly(2))
->method('updateEntry')
->willReturn($entry);
$res = $pocketHtmlImport->import();
$this->assertTrue($res);
$this->assertSame(['skipped' => 0, 'imported' => 2, 'queued' => 0], $pocketHtmlImport->getSummary());
}
public function testImportAndMarkAllAsRead()
{
$pocketHtmlImport = $this->getPocketHtmlImport(false, 1);
$pocketHtmlImport->setFilepath(__DIR__ . '/../fixtures/ril_export.html');
$entryRepo = $this->getMockBuilder(EntryRepository::class)
->disableOriginalConstructor()
->getMock();
$entryRepo->expects($this->exactly(2))
->method('findByUrlAndUserId')
->will($this->onConsecutiveCalls(false, true));
$this->em
->expects($this->any())
->method('getRepository')
->willReturn($entryRepo);
$this->contentProxy
->expects($this->exactly(1))
->method('updateEntry')
->willReturn(new Entry($this->user));
// check that every entry persisted are archived
$this->em
->expects($this->any())
->method('persist')
->with($this->callback(function ($persistedEntry) {
return (bool) $persistedEntry->isArchived();
}));
$res = $pocketHtmlImport
->setMarkAsRead(true)
->import();
$this->assertTrue($res);
$this->assertSame(['skipped' => 1, 'imported' => 1, 'queued' => 0], $pocketHtmlImport->getSummary());
}
public function testImportWithRabbit()
{
$pocketHtmlImport = $this->getPocketHtmlImport();
$pocketHtmlImport->setFilepath(__DIR__ . '/../fixtures/ril_export.html');
$entryRepo = $this->getMockBuilder(EntryRepository::class)
->disableOriginalConstructor()
->getMock();
$entryRepo->expects($this->never())
->method('findByUrlAndUserId');
$this->em
->expects($this->never())
->method('getRepository');
$entry = $this->getMockBuilder(Entry::class)
->disableOriginalConstructor()
->getMock();
$this->contentProxy
->expects($this->never())
->method('updateEntry');
$producer = $this->getMockBuilder(\OldSound\RabbitMqBundle\RabbitMq\Producer::class)
->disableOriginalConstructor()
->getMock();
$producer
->expects($this->exactly(2))
->method('publish');
$pocketHtmlImport->setProducer($producer);
$res = $pocketHtmlImport->setMarkAsRead(true)->import();
$this->assertTrue($res);
$this->assertSame(['skipped' => 0, 'imported' => 0, 'queued' => 2], $pocketHtmlImport->getSummary());
}
public function testImportWithRedis()
{
$pocketHtmlImport = $this->getPocketHtmlImport();
$pocketHtmlImport->setFilepath(__DIR__ . '/../fixtures/ril_export.html');
$entryRepo = $this->getMockBuilder(EntryRepository::class)
->disableOriginalConstructor()
->getMock();
$entryRepo->expects($this->never())
->method('findByUrlAndUserId');
$this->em
->expects($this->never())
->method('getRepository');
$entry = $this->getMockBuilder(Entry::class)
->disableOriginalConstructor()
->getMock();
$this->contentProxy
->expects($this->never())
->method('updateEntry');
$factory = new RedisMockFactory();
$redisMock = $factory->getAdapter(Client::class, true);
$queue = new RedisQueue($redisMock, 'pocket_html');
$producer = new Producer($queue);
$pocketHtmlImport->setProducer($producer);
$res = $pocketHtmlImport->setMarkAsRead(true)->import();
$this->assertTrue($res);
$this->assertSame(['skipped' => 0, 'imported' => 0, 'queued' => 2], $pocketHtmlImport->getSummary());
$this->assertNotEmpty($redisMock->lpop('pocket_html'));
}
public function testImportBadFile()
{
$pocketHtmlImport = $this->getPocketHtmlImport();
$pocketHtmlImport->setFilepath(__DIR__ . '/../fixtures/wallabag-v1.jsonx');
$res = $pocketHtmlImport->import();
$this->assertFalse($res);
$records = $this->logHandler->getRecords();
$this->assertStringContainsString('Pocket HTML Import: unable to read file', $records[0]['message']);
$this->assertSame('ERROR', $records[0]['level_name']);
}
public function testImportUserNotDefined()
{
$pocketHtmlImport = $this->getPocketHtmlImport(true);
$pocketHtmlImport->setFilepath(__DIR__ . '/../fixtures/ril_export.html');
$res = $pocketHtmlImport->import();
$this->assertFalse($res);
$records = $this->logHandler->getRecords();
$this->assertStringContainsString('Pocket HTML Import: user is not defined', $records[0]['message']);
$this->assertSame('ERROR', $records[0]['level_name']);
}
private function getPocketHtmlImport($unsetUser = false, $dispatched = 0)
{
$this->user = new User();
$this->em = $this->getMockBuilder(EntityManager::class)
->disableOriginalConstructor()
->getMock();
$this->contentProxy = $this->getMockBuilder(ContentProxy::class)
->disableOriginalConstructor()
->getMock();
$this->tagsAssigner = $this->getMockBuilder(TagsAssigner::class)
->disableOriginalConstructor()
->getMock();
$dispatcher = $this->getMockBuilder(EventDispatcher::class)
->disableOriginalConstructor()
->getMock();
$dispatcher
->expects($this->exactly($dispatched))
->method('dispatch');
$this->logHandler = new TestHandler();
$logger = new Logger('test', [$this->logHandler]);
$wallabag = new PocketHtmlImport($this->em, $this->contentProxy, $this->tagsAssigner, $dispatcher, $logger);
if (false === $unsetUser) {
$wallabag->setUser($this->user);
}
return $wallabag;
}
}

View File

@ -0,0 +1,254 @@
<?php
namespace Tests\Wallabag\ImportBundle\Import;
use Doctrine\ORM\EntityManager;
use M6Web\Component\RedisMock\RedisMockFactory;
use Monolog\Handler\TestHandler;
use Monolog\Logger;
use PHPUnit\Framework\TestCase;
use Predis\Client;
use Simpleue\Queue\RedisQueue;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Wallabag\CoreBundle\Entity\Entry;
use Wallabag\CoreBundle\Helper\ContentProxy;
use Wallabag\CoreBundle\Helper\TagsAssigner;
use Wallabag\CoreBundle\Repository\EntryRepository;
use Wallabag\ImportBundle\Import\ShaarliImport;
use Wallabag\ImportBundle\Redis\Producer;
use Wallabag\UserBundle\Entity\User;
class ShaarliImportTest extends TestCase
{
protected $user;
protected $em;
protected $logHandler;
protected $contentProxy;
protected $tagsAssigner;
public function testInit()
{
$shaarliImport = $this->getShaarliImport();
$this->assertSame('Shaarli', $shaarliImport->getName());
$this->assertNotEmpty($shaarliImport->getUrl());
$this->assertSame('import.shaarli.description', $shaarliImport->getDescription());
}
public function testImport()
{
$shaarliImport = $this->getShaarliImport(false, 2);
$shaarliImport->setFilepath(__DIR__ . '/../fixtures/shaarli-bookmarks.html');
$entryRepo = $this->getMockBuilder(EntryRepository::class)
->disableOriginalConstructor()
->getMock();
$entryRepo->expects($this->exactly(2))
->method('findByUrlAndUserId')
->willReturn(false);
$this->em
->expects($this->any())
->method('getRepository')
->willReturn($entryRepo);
$entry = $this->getMockBuilder(Entry::class)
->disableOriginalConstructor()
->getMock();
$this->contentProxy
->expects($this->exactly(2))
->method('updateEntry')
->willReturn($entry);
$res = $shaarliImport->import();
$this->assertTrue($res);
$this->assertSame(['skipped' => 0, 'imported' => 2, 'queued' => 0], $shaarliImport->getSummary());
}
public function testImportAndMarkAllAsRead()
{
$shaarliImport = $this->getShaarliImport(false, 1);
$shaarliImport->setFilepath(__DIR__ . '/../fixtures/shaarli-bookmarks.html');
$entryRepo = $this->getMockBuilder(EntryRepository::class)
->disableOriginalConstructor()
->getMock();
$entryRepo->expects($this->exactly(2))
->method('findByUrlAndUserId')
->will($this->onConsecutiveCalls(false, true));
$this->em
->expects($this->any())
->method('getRepository')
->willReturn($entryRepo);
$this->contentProxy
->expects($this->exactly(1))
->method('updateEntry')
->willReturn(new Entry($this->user));
// check that every entry persisted are archived
$this->em
->expects($this->any())
->method('persist')
->with($this->callback(function ($persistedEntry) {
return (bool) $persistedEntry->isArchived();
}));
$res = $shaarliImport
->setMarkAsRead(true)
->import();
$this->assertTrue($res);
$this->assertSame(['skipped' => 1, 'imported' => 1, 'queued' => 0], $shaarliImport->getSummary());
}
public function testImportWithRabbit()
{
$shaarliImport = $this->getShaarliImport();
$shaarliImport->setFilepath(__DIR__ . '/../fixtures/shaarli-bookmarks.html');
$entryRepo = $this->getMockBuilder(EntryRepository::class)
->disableOriginalConstructor()
->getMock();
$entryRepo->expects($this->never())
->method('findByUrlAndUserId');
$this->em
->expects($this->never())
->method('getRepository');
$entry = $this->getMockBuilder(Entry::class)
->disableOriginalConstructor()
->getMock();
$this->contentProxy
->expects($this->never())
->method('updateEntry');
$producer = $this->getMockBuilder(\OldSound\RabbitMqBundle\RabbitMq\Producer::class)
->disableOriginalConstructor()
->getMock();
$producer
->expects($this->exactly(2))
->method('publish');
$shaarliImport->setProducer($producer);
$res = $shaarliImport->setMarkAsRead(true)->import();
$this->assertTrue($res);
$this->assertSame(['skipped' => 0, 'imported' => 0, 'queued' => 2], $shaarliImport->getSummary());
}
public function testImportWithRedis()
{
$shaarliImport = $this->getShaarliImport();
$shaarliImport->setFilepath(__DIR__ . '/../fixtures/shaarli-bookmarks.html');
$entryRepo = $this->getMockBuilder(EntryRepository::class)
->disableOriginalConstructor()
->getMock();
$entryRepo->expects($this->never())
->method('findByUrlAndUserId');
$this->em
->expects($this->never())
->method('getRepository');
$entry = $this->getMockBuilder(Entry::class)
->disableOriginalConstructor()
->getMock();
$this->contentProxy
->expects($this->never())
->method('updateEntry');
$factory = new RedisMockFactory();
$redisMock = $factory->getAdapter(Client::class, true);
$queue = new RedisQueue($redisMock, 'shaarli');
$producer = new Producer($queue);
$shaarliImport->setProducer($producer);
$res = $shaarliImport->setMarkAsRead(true)->import();
$this->assertTrue($res);
$this->assertSame(['skipped' => 0, 'imported' => 0, 'queued' => 2], $shaarliImport->getSummary());
$this->assertNotEmpty($redisMock->lpop('shaarli'));
}
public function testImportBadFile()
{
$shaarliImport = $this->getShaarliImport();
$shaarliImport->setFilepath(__DIR__ . '/../fixtures/wallabag-v1.jsonx');
$res = $shaarliImport->import();
$this->assertFalse($res);
$records = $this->logHandler->getRecords();
$this->assertStringContainsString('Wallabag HTML Import: unable to read file', $records[0]['message']);
$this->assertSame('ERROR', $records[0]['level_name']);
}
public function testImportUserNotDefined()
{
$shaarliImport = $this->getShaarliImport(true);
$shaarliImport->setFilepath(__DIR__ . '/../fixtures/shaarli-bookmarks.html');
$res = $shaarliImport->import();
$this->assertFalse($res);
$records = $this->logHandler->getRecords();
$this->assertStringContainsString('Wallabag HTML Import: user is not defined', $records[0]['message']);
$this->assertSame('ERROR', $records[0]['level_name']);
}
private function getShaarliImport($unsetUser = false, $dispatched = 0)
{
$this->user = new User();
$this->em = $this->getMockBuilder(EntityManager::class)
->disableOriginalConstructor()
->getMock();
$this->contentProxy = $this->getMockBuilder(ContentProxy::class)
->disableOriginalConstructor()
->getMock();
$this->tagsAssigner = $this->getMockBuilder(TagsAssigner::class)
->disableOriginalConstructor()
->getMock();
$dispatcher = $this->getMockBuilder(EventDispatcher::class)
->disableOriginalConstructor()
->getMock();
$dispatcher
->expects($this->exactly($dispatched))
->method('dispatch');
$this->logHandler = new TestHandler();
$logger = new Logger('test', [$this->logHandler]);
$wallabag = new ShaarliImport($this->em, $this->contentProxy, $this->tagsAssigner, $dispatcher, $logger);
if (false === $unsetUser) {
$wallabag->setUser($this->user);
}
return $wallabag;
}
}

View File

@ -39,13 +39,13 @@
},
{
"guid": "E385l9vZ_LVn",
"title": "Le journaliste et cinéaste Claude Lanzmann est mort",
"title": "JO Paris 2024 : Où courir dans la capitale maintenant que les quais sont fermés ?",
"index": 1,
"dateAdded": 1388166091544000,
"lastModified": 1388166091545000,
"id": 5,
"type": "text/x-moz-place",
"uri": "https://www.lemonde.fr/disparitions/article/2018/07/05/le-journaliste-et-cineaste-claude-lanzmann-est-mort_5326313_3382.html"
"uri": "https://www.20minutes.fr/paris/4100740-20240715-jo-paris-2024-courir-capitale-maintenant-quais-fermes"
}
]
},

View File

@ -0,0 +1,343 @@
[
{
"id": "20db074a-34e1-4f55-b0e9-161e367946f6",
"slug": "malgre-la-crise-du-marche-des-montres-breitling-etend-son-reseau-192daf3a84e",
"title": "Malgré la crise du marché des montres, Breitling étend son réseau commercial et dévoile ses ambitions",
"description": "La marque suisse  peu présente en Chine  veut sétendre ailleurs en Asie et nexclut pas des acquisitions. En France, après sêtre installée sur les Champs-Elysées, elle ouvre boutique à Lille et à Monaco.",
"author": "Juliette Garnier",
"url": "https://www.lemonde.fr/economie/article/2024/10/29/malgre-la-crise-du-marche-des-montres-breitling-etend-son-reseau-commercial-et-devoile-ses-ambitions_6365425_3234.html",
"state": "Archived",
"readingProgress": 0,
"thumbnail": "https://img.lemde.fr/2024/10/29/234/0/6192/3096/1440/720/60/0/5ee3f32_1730201062245-063-1432929408.jpg",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T16:30:11.000Z",
"updatedAt": "2024-10-31T13:01:28.320Z",
"publishedAt": "2024-10-29T16:30:11.000Z"
},
{
"id": "5ace624c-ba30-48cc-82ba-5a4d585e0cd4",
"slug": "espagne-l-enquete-visant-l-epouse-de-pedro-sanchez-elargie-ce-de-192d9fe5ee4",
"title": "Espagne : lenquête visant lépouse de Pedro Sanchez élargie, ce dernier fait part de sa « tranquillité absolue »",
"description": "Begoña Gomez fait lobjet dune enquête pour corruption et trafic dinfluence, ouverte après des plaintes déposées par deux associations réputées proches de lextrême droite.",
"author": "Le Monde avec AFP",
"url": "https://www.lemonde.fr/international/article/2024/10/29/espagne-l-enquete-visant-l-epouse-de-pedro-sanchez-elargie-ce-dernier-fait-part-de-sa-tranquillite-absolue_6365392_3210.html",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://img.lemde.fr/2024/10/29/385/0/3266/1633/1440/720/60/0/54ca5e3_2024-10-29t141242z-866115530-rc2g8aaxq4l9-rtrmadp-3-spain-argentina.JPG",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T16:21:32.000Z",
"updatedAt": "2024-10-29T20:36:19.400Z",
"publishedAt": "2024-10-29T16:21:32.000Z"
},
{
"id": "1d72bb2e-2b3d-4d2d-8f17-90f441024358",
"slug": "montpellier-enquete-ouverte-apres-la-mort-d-une-jeune-femme-des--192d9fe4e61",
"title": "Montpellier : enquête ouverte après la mort dune jeune femme des suites dune méningite, malgré des appels au SAMU et aux pompiers",
"description": "Malgré deux appels aux secours, lun au 15 et lautre au 18, ce sont deux amis qui ont conduit la femme de 25 ans à une clinique montpelliéraine, avant quelle ne soit transférée au CHU et ne meure.",
"author": "Le Monde",
"url": "https://www.lemonde.fr/societe/article/2024/10/29/montpellier-enquete-ouverte-apres-la-mort-d-une-jeune-femme-des-suites-d-une-meningite-malgre-des-appels-au-samu-et-aux-pompiers_6365359_3224.html",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://img.lemde.fr/2024/10/24/187/0/2250/1125/1440/720/60/0/4e171be_1729786778722-frame-159.jpg",
"labels": [
"RSS",
"TEST"
],
"savedAt": "2024-10-29T16:09:19.000Z",
"updatedAt": "2024-10-31T13:03:21.779Z",
"publishedAt": "2024-10-29T16:09:19.000Z"
},
{
"id": "5ac06f9c-52e8-47df-984c-038c501819cb",
"slug": "tuberculose-le-nombre-de-cas-dans-le-monde-se-stabilise-apres-le-192daa35669",
"title": "Tuberculose : le nombre de cas dans le monde se stabilise après le regain des années Covid",
"description": "Lincidence de la maladie est en baisse de 8,3 % par rapport aux chiffres de 2015, mais reste loin de lobjectif initialement fixé de diviser par deux le nombre de malades dici à 2025.",
"author": "Delphine Roucaute",
"url": "https://www.lemonde.fr/planete/article/2024/10/29/tuberculose-le-nombre-de-cas-dans-le-monde-se-stabilise-apres-le-regain-des-annees-covid_6365326_3244.html",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://img.lemde.fr/2024/10/29/501/0/6009/3004/1440/720/60/0/01bc1cc_1730215288364-000-34ne6t2.jpg",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T16:02:03.000Z",
"updatedAt": "2024-10-29T23:36:30.655Z",
"publishedAt": "2024-10-29T16:02:03.000Z"
},
{
"id": "2f870f6d-79a9-45a4-8a64-e4cd3e7c4488",
"slug": "l-union-europeenne-adopte-jusqu-a-35-de-surtaxes-sur-les-voiture-192d9fe4fa0",
"title": "LUnion européenne adopte jusquà 35 % de surtaxes sur les voitures électriques importées de Chine",
"description": "Lobjectif affiché est de rétablir des conditions de concurrence équitables avec des constructeurs accusés de profiter de subventions publiques massives.",
"author": "Le Monde avec AFP",
"url": "https://www.lemonde.fr/economie/article/2024/10/29/l-union-europeenne-adopte-jusqu-a-35-de-surtaxes-sur-les-voitures-electriques-importees-de-chine_6365258_3234.html",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://img.lemde.fr/2024/10/03/131/0/2405/1202/1440/720/60/0/9f89e19_5860742-01-06.jpg",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T15:36:53.000Z",
"updatedAt": "2024-10-29T20:36:15.460Z",
"publishedAt": "2024-10-29T15:36:53.000Z"
},
{
"id": "fe9efcb1-1782-46c3-805d-c9b8074bee3b",
"slug": "l-ex-patron-de-la-dgse-bernard-bajolet-sera-juge-pour-complicite-192da0f4619",
"title": "Lex-patron de la DGSE Bernard Bajolet sera jugé pour complicité de tentative dextorsion",
"description": "Lhomme daffaires Alain Duménil accuse le service de renseignement davoir fait usage de la contrainte pour lui réclamer de largent en 2016.",
"author": "Le Monde avec AFP",
"url": "https://www.lemonde.fr/societe/article/2024/10/29/l-ex-patron-de-la-dgse-bernard-bajolet-sera-juge-pour-complicite-de-tentative-d-extorsion_6365225_3224.html",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://img.lemde.fr/2024/10/29/99/0/4804/2402/1440/720/60/0/e26408c_1730213831930-000-32hg2qa.jpg",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T15:32:35.000Z",
"updatedAt": "2024-10-29T20:54:47.245Z",
"publishedAt": "2024-10-29T15:32:35.000Z"
},
{
"id": "885df5b1-b564-4535-8ad7-f65bb934375d",
"slug": "la-cour-d-appel-de-paris-confirme-le-proces-pour-viol-du-rappeur-192da4a24d7",
"title": "La cour dappel de Paris confirme le procès pour viol du rappeur Naps",
"description": "Lartiste est soupçonné davoir violé une jeune femme pendant son sommeil à lautomne 2021.",
"author": "Le Monde avec AFP",
"url": "https://www.lemonde.fr/societe/article/2024/10/29/la-cour-d-appel-de-paris-confirme-le-proces-pour-viol-du-rappeur-naps_6365192_3224.html",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://img.lemde.fr/2024/10/29/369/0/4430/2215/1440/720/60/0/78d02d3_1730214584579-000-9t294r.jpg",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T15:26:15.000Z",
"updatedAt": "2024-10-29T21:59:05.276Z",
"publishedAt": "2024-10-29T15:26:15.000Z"
},
{
"id": "647e36c0-2782-42b0-8694-8599b61e912a",
"slug": "les-serbes-apres-leur-medaille-de-bronze-aux-jo-on-a-bu-pendant--192d9ed5eef",
"title": "Les Serbes après leur médaille de bronze aux JO : \"On a bu pendant huit heures !\"",
"description": "Lors de la cérémonie de remise de médailles des Jeux Olympiques de Paris, les Serbes se sont fait remarquer en titubant sur le podium. La raison est simple...",
"author": "La rédaction",
"url": "https://www.basketeurope.com/les-serbes-apres-leur-medaille-de-bronze-aux-j0-on-a-bu-pendant-huit-heures/",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://www.basketeurope.com/content/images/2024/10/Marinkovic.webp",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T13:59:23.000Z",
"updatedAt": "2024-10-29T20:17:45.252Z",
"publishedAt": "2024-10-29T13:59:23.000Z"
},
{
"id": "40c17460-d4bc-4408-aafb-3f6ffbe8d413",
"slug": "a-quoi-ressemble-le-parcours-du-tour-de-france-2025-192d9fe6f1d",
"title": "A quoi ressemble le parcours du Tour de France 2025 ?",
"description": "Le parcours de la prochaine Grande Boucle cycliste a été dévoilé mardi. Le peloton sélancera de Lille pour retrouver, trois semaines plus tard, la traditionnelle arrivée sur les Champs-Elysées, à Paris. Entre-temps, il lui faudra enchaîner les cols mythiques.",
"author": "Valentin Moinard",
"url": "https://www.lemonde.fr/sport/article/2024/10/29/cyclisme-le-tour-de-france-2025-fera-la-part-belle-aux-grimpeurs_6364955_3242.html",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://img.lemde.fr/2024/07/07/410/0/5994/2997/1440/720/60/0/1df95b2_5013615-01-06.jpg",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T13:12:05.000Z",
"updatedAt": "2024-10-29T20:36:23.653Z",
"publishedAt": "2024-10-29T13:12:05.000Z"
},
{
"id": "d4eb58ef-1cab-4aef-aee1-89dca60b8a80",
"slug": "arrets-maladie-des-fonctionnaires-les-arguments-discutables-du-g-192d9fe680a",
"title": "Arrêts maladie des fonctionnaires : les arguments discutables du gouvernement pour justifier sa réforme",
"description": "Alors que lexécutif souhaite ne plus payer les trois premiers jours dabsence des agents publics, le fait quil prenne peu en compte lamélioration de la qualité de vie au travail lui vaut de vives critiques, de la part des syndicats, mais aussi de personnalités ayant lexpérience du terrain.",
"author": "Bertrand Bissuel",
"url": "https://www.lemonde.fr/politique/article/2024/10/29/arrets-maladie-des-fonctionnaires-les-arguments-discutables-du-gouvernement-pour-justifier-sa-reforme_6364919_823448.html",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://img.lemde.fr/2024/10/29/265/0/6720/3360/1440/720/60/0/2f2f937_1730191618529-cbi1223010.jpg",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T13:00:08.000Z",
"updatedAt": "2024-10-29T20:36:21.703Z",
"publishedAt": "2024-10-29T13:00:08.000Z"
},
{
"id": "ff9c411a-e91f-424b-a570-f92b311026a5",
"slug": "premium-jean-denys-choulet-selectionneur-du-kosovo-ma-nationalit-192db836507",
"title": "[Premium] Jean-Denys Choulet, sélectionneur du Kosovo : \"Ma nationalité, c'est le basket\"",
"description": "Huit mois après son licenciement de la Chorale de Roanne, Jean-Denys Choulet a retrouvé un poste en tant que sélectionneur du Kosovo. À 66 ans, il ne se voyait pas quitter le milieu du basket et reste ouvert à l'idée de diriger un club.",
"author": "Morgan Parmentier",
"url": "https://www.basketeurope.com/premium-jean-denys-choulet-ma-nationalite-cest-le-basket/",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://www.basketeurope.com/content/images/size/w1200/2024/10/choulet-jd-chorale-roanne-tuan-nguyen.webp",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T12:31:47.000Z",
"updatedAt": "2024-10-30T03:41:14.412Z",
"publishedAt": "2024-10-29T12:31:47.000Z"
},
{
"id": "54ae4346-03c0-407e-b204-b09351174365",
"slug": "pedocriminalite-l-eglise-doit-mieux-sanctionner-les-auteurs-et-a-192da2d8b8f",
"title": "Pédocriminalité : lEglise doit mieux sanctionner les auteurs et aider les victimes, selon un rapport du Vatican",
"description": "En avril 2022, le pape François avait demandé à une commission pontificale un rapport sur la protection des mineurs dans lEglise. Très attendu, il vient dêtre publié par le Saint-Siège.",
"author": "Le Monde avec AFP",
"url": "https://www.lemonde.fr/international/article/2024/10/29/pedocriminalite-l-eglise-doit-mieux-sanctionner-les-auteurs-et-aider-les-victimes-selon-un-rapport-du-vatican_6364916_3210.html",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://img.lemde.fr/2024/10/27/688/0/8256/4128/1440/720/60/0/a7b4e07_5084272-01-06.jpg",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T12:26:36.000Z",
"updatedAt": "2024-10-29T21:27:50.999Z",
"publishedAt": "2024-10-29T12:26:36.000Z"
},
{
"id": "b2d30e8c-c344-4460-a583-4a2955fdc197",
"slug": "cybercriminalite-les-stealers-redline-et-meta-vises-par-une-oper-192d90d1624",
"title": "Cybercriminalité : les « stealers » Redline et META visés par une opération policière internationale",
"description": "Le marché des identifiants dérobés est devenu un secteur central de la cybercriminalité. Les deux virus visés par cette opération policière, baptisée « Magnus », ont permis le vol de plus de 227 millions de mots de passe en 2024.",
"author": "Florian Reynaud",
"url": "https://www.lemonde.fr/pixels/article/2024/10/29/cybercriminalite-les-stealers-redline-et-meta-vises-par-une-operation-policiere-internationale_6364915_4408996.html",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://img.lemde.fr/2024/10/10/252/0/3008/1504/1440/720/60/0/b3dbd8e_1728546840972-papier-271-16.jpg",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T12:21:06.000Z",
"updatedAt": "2024-10-29T16:12:46.658Z",
"publishedAt": "2024-10-29T12:21:06.000Z"
},
{
"id": "a4a6e509-beab-4577-99db-2f1db819d669",
"slug": "au-maroc-emmanuel-macron-appelle-a-plus-de-resultats-contre-l-im-192d90d159a",
"title": "Au Maroc, Emmanuel Macron appelle à plus de « résultats » contre limmigration illégale et réaffirme son soutien à la « souveraineté marocaine » au Sahara occidental",
"description": "Le chef de lEtat français a également proposé au roi du Maroc, Mohammed VI, de signer un nouveau « cadre stratégique » bilatéral en 2025 à Paris, soixante-dix ans après la déclaration de la Celle-Saint-Cloud qui scella lindépendance du Maroc de la France.",
"author": "Le Monde avec AFP",
"url": "https://www.lemonde.fr/international/article/2024/10/29/au-maroc-emmanuel-macron-appelle-a-plus-de-resultats-contre-l-immigration-illegale-et-reaffirme-son-soutien-a-la-souverainete-marocaine-au-sahara-occidental_6364914_3210.html",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://img.lemde.fr/2024/10/29/350/0/4201/2100/1440/720/60/0/c37301c_5103313-01-06.jpg",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T12:14:53.000Z",
"updatedAt": "2024-10-29T16:12:46.456Z",
"publishedAt": "2024-10-29T12:14:53.000Z"
},
{
"id": "0ab9e8c7-c88b-4a31-af3e-62ec4766b99d",
"slug": "prison-de-noumea-l-etat-condamne-car-trop-lent-a-ameliorer-les-c-192d90d0722",
"title": "Prison de Nouméa : lEtat condamné car trop lent à améliorer les conditions de détention",
"description": "En 2020, le Conseil dEtat avait exigé des mesures urgentes pour les droits des détenus au Camp-Est, mais ladministration a pris du retard et les travaux nauront pas lieu avant 2028, selon lObservatoire international des prisons.",
"author": "Le Monde avec AFP",
"url": "https://www.lemonde.fr/societe/article/2024/10/29/prison-de-noumea-l-etat-condamne-car-trop-lent-a-ameliorer-les-conditions-de-detention_6364912_3224.html",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://img.lemde.fr/2024/10/29/520/0/4160/2080/1440/720/60/0/9ec59fb_1730203377797-cdo-cellule-case-g-cp-nouma-a-2.jpg",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T12:08:18.000Z",
"updatedAt": "2024-10-29T16:12:42.744Z",
"publishedAt": "2024-10-29T12:08:18.000Z"
},
{
"id": "fe79a70e-d1e1-43fe-8555-094209d1b48d",
"slug": "tour-de-france-femmes-2025-la-course-traversera-l-hexagone-d-oue-192da131a9a",
"title": "Tour de France Femmes 2025 : la course traversera lHexagone dOuest en Est, de la Bretagne aux Alpes",
"description": "Le parcours de la quatrième édition de la course cycliste a été présenté mardi. Du 26 juillet au 3 août, il fera la part belle aux grimpeuses, qui auront trois étapes finales dans le massif alpin pour sillustrer.",
"author": "Valentin Moinard",
"url": "https://www.lemonde.fr/sport/article/2024/10/29/tour-de-france-femmes-2025-de-la-bretagne-aux-alpes-la-course-traversera-la-france-d-ouest-en-est_6364908_3242.html",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://img.lemde.fr/2024/08/17/1194/0/7268/3634/1440/720/60/0/8bafa27_5442255-01-06.jpg",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T11:52:45.000Z",
"updatedAt": "2024-10-29T20:58:58.164Z",
"publishedAt": "2024-10-29T11:52:45.000Z"
},
{
"id": "54687e28-21e1-42b4-bb92-cc5f39716464",
"slug": "en-direct-guerre-au-proche-orient-le-bombardement-israelien-sur--192da214287",
"title": "En direct, guerre au Proche-Orient : le bombardement israélien sur le nord de la bande de Gaza a fait 93 morts, selon un nouveau bilan de la défense civile",
"description": "Un précédent bilan, établi par la même source, faisait état de 55 morts. Lattaque aérienne, qui a eu lieu dans la nuit de lundi à mardi, a visé la ville de Beit Lahya, dans le nord de la bande de Gaza.",
"author": "Seb2000",
"url": "https://www.lemonde.fr/international/live/2024/10/29/en-direct-guerre-au-proche-orient-le-bombardement-israelien-sur-le-nord-de-la-bande-de-gaza-a-fait-93-morts-selon-un-nouveau-bilan-de-la-defense-civile_6362390_3210.html",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://img.lemde.fr/2024/10/29/303/0/3644/1822/1440/720/60/0/6b6faba_1730210101426-468617.jpg",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T11:52:34.000Z",
"updatedAt": "2024-10-29T21:14:26.095Z",
"publishedAt": "2024-10-29T11:52:34.000Z"
},
{
"id": "a2b54509-62ad-4ab7-a68a-2a970ac25952",
"slug": "au-chili-le-gouvernement-de-gauche-malmene-aux-elections-locales-192da0e7d74",
"title": "Au Chili, le gouvernement de gauche malmené aux élections locales",
"description": "La droite de la coalition Chile Vamos sort renforcée des élections municipales et régionales, tandis que lextrême droite progresse, sans enregistrer la percée quelle espérait.",
"author": "Flora Genoux",
"url": "https://www.lemonde.fr/international/article/2024/10/29/au-chili-le-gouvernement-de-gauche-malmene-aux-elections-locales_6364875_3210.html",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://img.lemde.fr/2024/10/29/720/0/8640/4320/1440/720/60/0/3632d71_1730193220296-852876.jpg",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T11:31:53.000Z",
"updatedAt": "2024-10-29T20:53:55.779Z",
"publishedAt": "2024-10-29T11:31:53.000Z"
},
{
"id": "9c68ff83-1927-4377-a3b0-a048516a725d",
"slug": "en-isere-lyes-louffok-est-le-candidat-insoumis-investi-a-l-elect-192d90d319e",
"title": "En Isère, Lyes Louffok est le candidat « insoumis » investi à lélection législative partielle",
"description": "Le siège est vacant depuis la démission dHugo Prevost (LFI), accusé de violences sexistes et sexuelles.",
"author": "Le Monde avec AFP",
"url": "https://www.lemonde.fr/politique/article/2024/10/29/en-isere-lyes-louffok-est-le-candidat-insoumis-investi-a-l-election-legislative-partielle_6364842_823448.html",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://img.lemde.fr/2024/10/29/552/0/6628/3314/1440/720/60/0/607f0f6_1730199775062-000-34r33w7.jpg",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T11:24:36.000Z",
"updatedAt": "2024-10-29T16:12:53.630Z",
"publishedAt": "2024-10-29T11:24:36.000Z"
},
{
"id": "1015c224-bd79-4ed9-9268-8fa9803a52cd",
"slug": "presidentielle-americaine-2024-comment-le-calendrier-de-l-electi-192d90d3440",
"title": "Présidentielle américaine 2024 : comment le calendrier de lélection et des affaires judiciaires de Trump sentremêlent",
"description": "Le verdict du procès visant lancien président ne devrait être connu quaprès lélection, et le reste des poursuites pénales reste incertain. « Le Monde » vous propose de suivre le déroulé, mis à jour continuellement, de cette année décisive pour les Etats-Unis.",
"author": "Gary Dagorn, Jean-Philippe Lefief",
"url": "https://www.lemonde.fr/les-decodeurs/article/2024/10/29/presidentielle-americaine-2024-comment-le-calendrier-de-l-election-et-des-affaires-judiciaires-de-trump-s-entremelent_6210916_3211.html",
"state": "Active",
"readingProgress": 0,
"thumbnail": "https://img.lemde.fr/2024/01/12/0/0/1500/750/1440/720/60/0/e9367e3_1705065713486-chrono-media-appel.png",
"labels": [
"RSS"
],
"savedAt": "2024-10-29T11:17:19.000Z",
"updatedAt": "2024-10-29T16:12:54.352Z",
"publishedAt": "2024-10-29T11:17:19.000Z"
}
]

View File

@ -0,0 +1,10 @@
title,url,time_added,tags,status
You Might Not Need jQuery,http://youmightnotneedjquery.com/,1600322788,,unread
Est-ce que jai besoin dun Scrum Master ? | by Jean-Pierre Lambert | Jean-,https://jp-lambert.me/est-ce-que-jai-besoin-d-un-scrum-master-604f5a471c73,1600172739,,unread
"Avec les accusés dEl Halia, par Gisèle Halimi (Le Monde diplomatique, sept",https://www.monde-diplomatique.fr/2020/09/HALIMI/62165,1599806347,,unread
ArchiveBox question: How do I import links from a RSS feed?,https://www.reddit.com/r/DataHoarder/comments/ioupbk/archivebox_question_how_do_i_import_links_from_a/,1600961496,,archive
« Tu vas pleurer les premières fois » : que se passe-t-il au sein du studio,https://www.numerama.com/politique/646826-tu-vas-pleurer-les-premieres-fois-que-se-passe-t-il-au-sein-du-studio-dubisoft-derriere-trackmania.html#utm_medium=distibuted&utm_source=rss&utm_campaign=646826,1599809025,,unread
Comment Konbini sest fait piéger par un « père masculiniste »,https://www.nouvelobs.com/rue89/20200911.OBS33165/comment-konbini-s-est-fait-pieger-par-un-pere-masculiniste.html,1599819251,,archive
"Des abeilles pour résoudre les « conflits » entre les humains
et les élépha",https://reporterre.net/Des-abeilles-pour-resoudre-les-conflits-entre-les-humains-et-les-elephants,1599890673,,unread
1 title url time_added tags status
2 You Might Not Need jQuery http://youmightnotneedjquery.com/ 1600322788 unread
3 Est-ce que j’ai besoin d’un Scrum Master ? | by Jean-Pierre Lambert | Jean- https://jp-lambert.me/est-ce-que-jai-besoin-d-un-scrum-master-604f5a471c73 1600172739 unread
4 Avec les accusés d’El Halia, par Gisèle Halimi (Le Monde diplomatique, sept https://www.monde-diplomatique.fr/2020/09/HALIMI/62165 1599806347 unread
5 ArchiveBox question: How do I import links from a RSS feed? https://www.reddit.com/r/DataHoarder/comments/ioupbk/archivebox_question_how_do_i_import_links_from_a/ 1600961496 archive
6 « Tu vas pleurer les premières fois » : que se passe-t-il au sein du studio https://www.numerama.com/politique/646826-tu-vas-pleurer-les-premieres-fois-que-se-passe-t-il-au-sein-du-studio-dubisoft-derriere-trackmania.html#utm_medium=distibuted&utm_source=rss&utm_campaign=646826 1599809025 unread
7 Comment Konbini s’est fait piéger par un « père masculiniste » https://www.nouvelobs.com/rue89/20200911.OBS33165/comment-konbini-s-est-fait-pieger-par-un-pere-masculiniste.html 1599819251 archive
8 Des abeilles pour résoudre les « conflits » entre les humains et les élépha https://reporterre.net/Des-abeilles-pour-resoudre-les-conflits-entre-les-humains-et-les-elephants 1599890673 unread

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<!--So long and thanks for all the fish-->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Pocket Export</title>
</head>
<body>
<h1>Unread</h1>
<ul>
<li><a href="https://www.20minutes.fr/sport/4002755-20220928-tarn-lapins-ravagent-terrain-match-rugby-doit-etre-annule" time_added="1688628695" tags="ifttt,new_entry_simple">Tarn : Des lapins ravagent le terrain, le match de rugby doit être annulé</a></li>
<li><a href="https://www.20minutes.fr/paris/4100740-20240715-jo-paris-2024-courir-capitale-maintenant-quais-fermes" time_added="1688627412" tags="ifttt,new_entry_simple">JO Paris 2024 : Où courir dans la capitale maintenant que les quais sont fermés ?</a></li>
</ul>
<h1>Read Archive</h1>
<ul>
</ul>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<!-- This is an automatically generated file.
It will be read and overwritten.
Do Not Edit! -->
<TITLE>Bookmarks</TITLE>
<H1>Shaarli export of all bookmarks on Mon, 17 Jul 23 14:31:25 +0200</H1>
<DL><p>
<DT><A HREF="https://www.20minutes.fr/sport/4002755-20220928-tarn-lapins-ravagent-terrain-match-rugby-doit-etre-annule" ADD_DATE="1686813518" LAST_MODIFIED="1686813519" PRIVATE="0" TAGS="firefoxos">The Legacy of Firefox OS. In the two years or so since Mozilla… | by Ben Francis | Medium</A>
<DD>In the two years or so since Mozilla announced the end of Firefox OS as a Mozilla-run project, the B2G source code has found its way into a surprising number of commercial products.
<DT><A HREF="https://www.20minutes.fr/paris/4100740-20240715-jo-paris-2024-courir-capitale-maintenant-quais-fermes" ADD_DATE="1683376565" LAST_MODIFIED="1686813519" PRIVATE="0" TAGS="firefoxos">JO Paris 2024 : Où courir dans la capitale maintenant que les quais sont fermés ?</A>
</DL><p>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script>

View File

@ -343,7 +343,6 @@ tag:
quickstart:
support:
title: Podpora
gitter: Na Gitteru
email: E-mailem
github: Na GitHubu
description: Pokud potřebujete pomoc, jsme tu pro vás.

View File

@ -467,7 +467,7 @@ quickstart:
description: Wenn du Hilfe brauchst, wir sind für dich da.
github: Auf GitHub
email: Über E-Mail
gitter: Auf Gitter
matrix: Auf Matrix
tag:
page_title: Tags
list:

View File

@ -249,7 +249,7 @@ tag:
page_title: Ετικέτες
quickstart:
support:
gitter: Στο Gitter
matrix: Στο Matrix
email: Με email
github: Στο GitHub
description: Αν χρειάζεστε βοήθεια, είμαστε εδώ για εσάς.

View File

@ -457,7 +457,7 @@ quickstart:
description: If you need some help, we are here for you.
github: On GitHub
email: By email
gitter: On Gitter
matrix: On Matrix
tag:
confirm:
delete: Delete the %name% tag
@ -502,6 +502,11 @@ import:
elcurator:
page_title: 'Import > elCurator'
description: 'This importer will import all your elCurator articles.'
how_to: Please select your elCurator export and click on the button below to upload and import it.
omnivore:
page_title: 'Import > Omnivore'
description: 'This importer will import all your Omnivore articles.'
how_to: Please unzip your Omnivore export, then upload each JSON file named "metadata_x_to_y.json" one by one.
readability:
page_title: Import > Readability
description: This importer will import all your Readability articles.
@ -529,6 +534,18 @@ import:
page_title: Import > del.icio.us
description: This importer will import all your Delicious bookmarks. Since 2021, you can export again your data from it using the export page (https://del.icio.us/export). Choose the "JSON" format and download it (like "delicious_export.2021.02.06_21.10.json").
how_to: Please select your Delicious export and click on the button below to upload and import it.
shaarli:
page_title: Import > Shaarli
description: This importer will import all your Shaarli bookmarks. Just go to the Tools section, then into "Export database", choose your bookmarks and export them. You will obtain a HTML file.
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_html:
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
@ -663,6 +680,7 @@ flashes:
archived_reset: Archived entries deleted
otp_enabled: Two-factor authentication enabled
otp_disabled: Two-factor authentication disabled
otp_code_invalid: Invalid two-factor authentication code
tagging_rules_imported: Tagging rules imported
tagging_rules_not_imported: Error while importing tagging rules
ignore_origin_rules_deleted: 'Ignore origin rule deleted'

View File

@ -470,7 +470,7 @@ quickstart:
description: 'Si necesitas ayuda, estamos a tu disposición.'
github: 'En GitHub'
email: 'Por correo electrónico'
gitter: 'En Gitter'
matrix: 'En Matrix'
tag:
page_title: 'Etiquetas'
list:

View File

@ -270,7 +270,6 @@ quickstart:
description: به کمک نیاز دارید؟ ما پشتیبان شما هستیم.
github: روی گیت‌هاب
email: با ایمیل
gitter: روی گیتر
tag:
page_title: برچسب‌ها
list:

View File

@ -457,7 +457,7 @@ quickstart:
description: Parce que vous avez peut-être besoin de nous poser une question, nous sommes disponibles pour vous.
github: Sur GitHub
email: Par courriel
gitter: Sur Gitter
matrix: Sur Matrix
tag:
confirm:
delete: Supprimer le tag %name%
@ -495,7 +495,7 @@ import:
wallabag_v1:
page_title: Importer > wallabag v1
description: 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 ».
how_to: Choisissez le fichier de votre export wallabag v1 et cliquez sur le bouton ci-dessous pour limporter.
how_to: Choisissez le fichier de votre export wallabag et cliquez sur le bouton ci-dessous pour limporter.
wallabag_v2:
page_title: Importer > wallabag v2
description: Cet outil va importer tous vos articles dune autre instance de wallabag v2. Allez dans tous vos articles, puis, sur la barre latérale, cliquez sur « JSON ». Vous allez récupérer un fichier « All articles.json ».
@ -525,10 +525,24 @@ import:
elcurator:
description: Cet outil va importer tous vos articles depuis elCurator.
page_title: Importer > elCurator
how_to: Choisissez le fichier de votre export elCurator et cliquez sur le bouton ci-dessous pour limporter.
delicious:
page_title: Importer > del.icio.us
how_to: Choisissez le fichier de votre export Delicious et cliquez sur le bouton ci-dessous pour l'importer.
description: Depuis 2021, vous pouvez à nouveau exporter vos données depuis Delicious (https://del.icio.us/export). Choisissez le format "JSON" et téléchargez le (un fichier du genre "delicious_export.2021.02.06_21.10.json").
shaarli:
page_title: Importer > Shaarli
description: Cet importateur importera toutes vos signets Shaarli. Il suffit d'aller à la section Outils, puis dans « Base de données d'exportation », choisissez vos signets et exportez-les. Vous obtiendrez un fichier HTML.
how_to: Veuillez sélectionner le fichier de sauvegarde de signet et cliquez sur le bouton ci-dessous pour l'importer. Notez que le processus peut prendre beaucoup de temps puisque tous les articles doivent être récupérés.
pocket_html:
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.
developer:
page_title: Gestion des clients API
welcome_message: Bienvenue sur lAPI de wallabag

View File

@ -477,7 +477,7 @@ tag:
delete: Eliminar a etiqueta %name%
quickstart:
support:
gitter: En Gitter
matrix: En Matrix
email: Por email
github: En GitHub
description: Se precisas axuda, aquí estamos para ti.

View File

@ -628,7 +628,6 @@ quickstart:
use_docker: Koristi Docker za instliranje wallabaga
support:
github: Na GitHubu
gitter: Na Gitteru
title: Podrška
email: Putem e-maila
description: Ako trebaš pomoć, spremni smo ti pomoći.

View File

@ -382,7 +382,7 @@ quickstart:
description: Ha segítségre van szüksége, itt vagyunk az Ön számára.
github: A GitHub-on
email: E-mailben
gitter: A Gitter-en
matrix: A Matrix-en
tag:
page_title: Címkék
list:

View File

@ -399,7 +399,7 @@ quickstart:
description: Se hai bisogno di aiuto, siamo qui per te.
github: Su GitHub
email: Per e-mail
gitter: Su Gitter
matrix: Su Matrix
tag:
page_title: Etichette
list:

View File

@ -462,7 +462,7 @@ quickstart:
description: 何か助けが必要な場合は、私たちがお役に立ちます。
github: GitHub 上で
email: メールで
gitter: Gitter 上で
matrix: Matrix 上で
tag:
list:
number_on_the_page: '{0} タグはありません。|{1} 1 つタグがあります。|]1,Inf[ %count% タグがあります。'

View File

@ -643,7 +643,7 @@ quickstart:
more: 더보기…
page_title: 빠른시작
support:
gitter: Gitter 에서
matrix: Matrix 에서
email: 이메일로
github: GitHub에서
description: 도움이 필요하시면 저희가 도와 드리겠습니다.

View File

@ -410,7 +410,7 @@ quickstart:
language: Endre språk og utseende
title: Sett opp programmet
support:
gitter: Gitter
matrix: Matrix
email: Per e-post
github: på GitHub
title: Støtte

View File

@ -464,7 +464,7 @@ quickstart:
support:
description: Wij zijn er voor u als u hulp nodig heeft.
title: Ondersteuning
gitter: Op Gitter
matrix: Op Matrix
email: Per e-mail
github: Op GitHub
docs:

View File

@ -473,7 +473,7 @@ quickstart:
description: Perque avètz benlèu besonh de nos pausar una question, sèm disponibles per vosautres.
github: Sus GitHub
email: Per e-mail
gitter: Sus Gitter
matrix: Sus Matrix
tag:
page_title: Etiquetas
list:

View File

@ -474,7 +474,6 @@ quickstart:
description: Jeżeli potrzebujesz pomocy, jesteśmy tutaj dla ciebie.
github: na GitHubie
email: przez e-mail
gitter: na Gitterze
tag:
page_title: Tagi
list:

View File

@ -322,7 +322,7 @@ quickstart:
description: 'Se você precisa de ajuda, nós estamos aqui.'
github: 'No GitHub'
email: 'Por e-mail'
gitter: 'No Gitter'
matrix: 'No Matrix'
tag:
page_title: 'Tags'
list:

View File

@ -469,7 +469,7 @@ quickstart:
description: 'Если Вам нужна помощь, мы здесь чтобы помочь Вам.'
github: 'На GitHub'
email: 'По email'
gitter: 'На Gitter'
matrix: 'На Matrix'
tag:
page_title: 'Теги'
list:

Some files were not shown because too many files have changed in this diff Show More