mirror of
https://github.com/wallabag/wallabag.git
synced 2025-12-25 22:57:33 +01:00
Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74cbfd945b | |||
| de107deed2 | |||
| 7108eea2da | |||
| 7aadb8eaa8 | |||
| f2a3968a2f | |||
| 19c0b9c800 | |||
| 1a69855090 | |||
| 797d48905c | |||
| d24b703315 | |||
| 1eca3cf327 | |||
| 91384c531d | |||
| a86b16d679 | |||
| 2bfc3fd852 | |||
| 653b198f8e | |||
| 5c5b20c83b | |||
| d00fe83366 | |||
| f0d3db70c0 | |||
| 4f34cfa6fc | |||
| 8e90c0f320 | |||
| 6ffd7382c7 | |||
| 46d6e4d923 | |||
| 09af127446 | |||
| 4c23196304 | |||
| 42746f418e | |||
| 35c4feedd8 | |||
| c451cc96e5 | |||
| c4240c866b | |||
| 27d66d9e1d | |||
| c1397f43ac | |||
| 52a16bb75f | |||
| f82c87b520 | |||
| 772a802596 | |||
| 29162bde9d | |||
| b1614e9267 | |||
| 70999075a6 | |||
| 262f674245 | |||
| 01ffc6c3d5 | |||
| 5586930376 | |||
| b45116b73e | |||
| c2e38cedac | |||
| 35dcc43366 | |||
| de8f859536 | |||
| 5cdac6c0bb | |||
| 466cd17d5b | |||
| 14cdd123ce | |||
| bdb420b13f | |||
| 99c8a06594 | |||
| 4c52f71895 | |||
| 677b2986bc | |||
| 5ea5115a72 | |||
| 27f0d94db7 | |||
| cf49be6940 | |||
| ddf2e80842 | |||
| d1e128900a | |||
| 0d8429dfc7 | |||
| eb8408b22f | |||
| 00d0e6f951 | |||
| edffef8375 | |||
| 3817010e29 | |||
| ed1acf59e1 | |||
| e162408139 | |||
| 6fa61c0f9c | |||
| 264f91126e | |||
| ac5b5fb379 | |||
| d703fa6a3a | |||
| f71d8332e0 | |||
| 3dffcadc03 | |||
| fab0c02ba0 | |||
| c4857564f3 | |||
| c7c74de4b8 | |||
| 08b68d4d87 | |||
| 93e877f086 | |||
| 82430b50c6 | |||
| 89db5690a0 | |||
| d4171e9d63 | |||
| 74175f2e54 | |||
| bd8ccf924f | |||
| 1cc321bec4 | |||
| 048221dbcf | |||
| 7ddf5066ef | |||
| 5d13648420 | |||
| f2c72e1569 | |||
| 5ac6788715 | |||
| d757612c77 | |||
| b6faa844cb | |||
| 898890c371 | |||
| 580c5fe810 | |||
| bee59d1c4a | |||
| f8f7f962ce | |||
| 54ba9a6da8 | |||
| 8cceb89261 | |||
| 09c2ddb79e | |||
| 7246b4c1db | |||
| 87ce9fd48d | |||
| db55cfbc0a | |||
| 3e18b66e62 | |||
| bc16155ec2 | |||
| a4820b21ca | |||
| 3d26ddde88 | |||
| c8f036ad0f | |||
| 5e89fc2606 | |||
| 9bef459882 | |||
| 7ebc96f3b9 | |||
| f4493f7472 | |||
| ffec47bd88 | |||
| 4a8fb78c7c | |||
| 967f8fa495 | |||
| 3a7e597ffc | |||
| 9927a06cab | |||
| d6c401423f | |||
| 6725228ed5 | |||
| 165973d834 | |||
| e018daf2fa | |||
| 902edbfeb6 | |||
| 75f6f53d25 | |||
| cc17b3acaf | |||
| 60623246ae | |||
| fa107116cc | |||
| 0cfdddc2eb | |||
| aa06e8328e | |||
| 5240684be9 | |||
| 9ec351e8b6 | |||
| 6fab27f3ce | |||
| e4d69cafe4 | |||
| 34e51243d9 | |||
| 9bc026f343 | |||
| a46fd5fc9f | |||
| f06a826c6d | |||
| c7e5ba6dd0 | |||
| 62ab325ad4 | |||
| c5d21025c4 | |||
| 8ac80e934e | |||
| 4b04cd5746 | |||
| dbed27f8d8 | |||
| 137c8ab756 | |||
| 0fdffb0b96 | |||
| 2d7d16ee6c | |||
| 18615738c0 | |||
| 452362c17a | |||
| ec4d23b584 | |||
| c0414355aa | |||
| aaf4ab2d2a | |||
| 56983b01a8 | |||
| 6b7e9ec1df | |||
| f052f6f580 | |||
| 16a92c54a7 | |||
| 13b2752e8d | |||
| 634997c9b5 | |||
| ca608ece65 | |||
| 027042cc1a | |||
| cf9243151a | |||
| e9a837a0ac | |||
| abef36fd2b | |||
| 2f86dd9c73 | |||
| 3d22442f8c | |||
| c99a733aff |
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -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.
|
||||
|
||||
10
.github/workflows/assets.yml
vendored
10
.github/workflows/assets.yml
vendored
@ -5,7 +5,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 2.*
|
||||
- "2.**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@ -13,16 +13,16 @@ permissions:
|
||||
jobs:
|
||||
js:
|
||||
name: "Building assets"
|
||||
runs-on: "ubuntu-20.04"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
|
||||
- name: "Install Node"
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "16"
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: "Install dependencies with Yarn"
|
||||
run: "yarn install"
|
||||
|
||||
6
.github/workflows/coding-standards.yml
vendored
6
.github/workflows/coding-standards.yml
vendored
@ -5,7 +5,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 2.*
|
||||
- "2.**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@ -13,11 +13,11 @@ permissions:
|
||||
jobs:
|
||||
coding-standards:
|
||||
name: "CS Fixer, PHPStan & TwigCS"
|
||||
runs-on: "ubuntu-20.04"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
|
||||
- name: "Install PHP"
|
||||
uses: "shivammathur/setup-php@v2"
|
||||
|
||||
10
.github/workflows/continuous-integration.yml
vendored
10
.github/workflows/continuous-integration.yml
vendored
@ -5,7 +5,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 2.*
|
||||
- "2.**"
|
||||
|
||||
env:
|
||||
PGPASSWORD: wallabagrocks
|
||||
@ -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
|
||||
@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
@ -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
|
||||
@ -106,7 +106,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
|
||||
6
.github/workflows/translations.yml
vendored
6
.github/workflows/translations.yml
vendored
@ -5,7 +5,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 2.*
|
||||
- "2.**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@ -13,7 +13,7 @@ permissions:
|
||||
jobs:
|
||||
translations:
|
||||
name: "Translations"
|
||||
runs-on: "ubuntu-20.04"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
|
||||
- name: "Install PHP"
|
||||
uses: "shivammathur/setup-php@v2"
|
||||
|
||||
2
.github/workflows/upload-release-package.yml
vendored
2
.github/workflows/upload-release-package.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
|
||||
- name: "Install PHP"
|
||||
uses: "shivammathur/setup-php@v2"
|
||||
|
||||
131
CHANGELOG.md
131
CHANGELOG.md
@ -1,5 +1,136 @@
|
||||
# Changelog
|
||||
|
||||
## [2.6.14](https://github.com/wallabag/wallabag/tree/2.6.14)
|
||||
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.13...2.6.14)
|
||||
|
||||
### Improvements
|
||||
|
||||
* Add annotations filter to entries API endpoint by @skn in https://github.com/wallabag/wallabag/pull/8346
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix reading time computation for short entries by @andreadecorte in https://github.com/wallabag/wallabag/pull/8332
|
||||
* Fix `urls` parameter when sending many urls to be stored using the API by @j0k3r in https://github.com/wallabag/wallabag/pull/8488
|
||||
* Fix docker base image by @yguedidi in https://github.com/wallabag/wallabag/pull/8440
|
||||
|
||||
### Technical stuff
|
||||
|
||||
* Change version in wallabag.yml by @nicosomb in https://github.com/wallabag/wallabag/pull/8251
|
||||
* Fix deprecation by @j0k3r in https://github.com/wallabag/wallabag/pull/8267
|
||||
* Update dependencies by @yguedidi in https://github.com/wallabag/wallabag/pull/8435
|
||||
* Bump deps (mostly for siteconfig) by @j0k3r in https://github.com/wallabag/wallabag/pull/8489
|
||||
|
||||
## [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
|
||||
* Backport Pocket and Shaarli HTML imports from master by @nicosomb in 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
|
||||
|
||||
### 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
|
||||
|
||||
## [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
|
||||
* Update dependencies by @yguedidi in 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
|
||||
* Fix title tag filter by @nicosomb in 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
|
||||
* Fix entries counter for annotated entries in the menu by @j0k3r in https://github.com/wallabag/wallabag/pull/7999
|
||||
|
||||
### Technical stuff
|
||||
|
||||
* Prepare 2.6.11 release by @yguedidi in 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)
|
||||
|
||||
### Fixes
|
||||
* Update deps & Node 20 by @j0k3r in https://github.com/wallabag/wallabag/pull/7134
|
||||
* Fix dark mode disabled url 2.6 by @Simounet in https://github.com/wallabag/wallabag/pull/7133
|
||||
* Make database dependent commands lazy by @yguedidi in https://github.com/wallabag/wallabag/pull/7142
|
||||
* Fix docker setup by @yguedidi in https://github.com/wallabag/wallabag/pull/7141
|
||||
* Remove session-based redirection by @yguedidi in https://github.com/wallabag/wallabag/pull/7140
|
||||
|
||||
## [2.6.7](https://github.com/wallabag/wallabag/tree/2.6.7)
|
||||
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.6...2.6.7)
|
||||
|
||||
### Security fix
|
||||
* A user can disable her 2FA unintentionally by @kdecherf in https://github.com/wallabag/wallabag/commit/0cfdddc2eb0aee5ffb69bf499d377d75655ba157
|
||||
|
||||
### Fixes
|
||||
* Fix deprecated null tag parameter by @Simounet in https://github.com/wallabag/wallabag/pull/6985
|
||||
* Full clickable card on mass action by @Simounet in https://github.com/wallabag/wallabag/pull/6991
|
||||
* Add tag form submit button always displayed by @Simounet in https://github.com/wallabag/wallabag/pull/6986
|
||||
|
||||
## [2.6.6](https://github.com/wallabag/wallabag/tree/2.6.6)
|
||||
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.5...2.6.6)
|
||||
|
||||
### Security fix
|
||||
* Force secure cookie on HTTPS connection by @j0k3r in https://github.com/wallabag/wallabag/pull/6924
|
||||
|
||||
### Fixes
|
||||
* Fix checkboxes pointer events issue by @Simounet in https://github.com/wallabag/wallabag/pull/6897
|
||||
* Add Google mailer by @j0k3r in https://github.com/wallabag/wallabag/pull/6899
|
||||
* Improve performance on homepage by @Simounet in https://github.com/wallabag/wallabag/pull/6909
|
||||
* Mass action layout improved by @Simounet in https://github.com/wallabag/wallabag/pull/6912
|
||||
|
||||
## [2.6.5](https://github.com/wallabag/wallabag/tree/2.6.5)
|
||||
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.4...2.6.5)
|
||||
|
||||
### Fixes
|
||||
* "Fix checkboxes pointer-events disabled" by @Simounet https://github.com/wallabag/wallabag/pull/6874
|
||||
* "Fix nav input styles" by @Simounet https://github.com/wallabag/wallabag/pull/6877
|
||||
* "Change domain status filters html types" by @Simounet https://github.com/wallabag/wallabag/pull/6888
|
||||
|
||||
## [2.6.4](https://github.com/wallabag/wallabag/tree/2.6.4)
|
||||
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.3...2.6.4)
|
||||
|
||||
### Fixes
|
||||
* Fix API token generation by @nicosomb https://github.com/wallabag/wallabag/pull/6869
|
||||
* Fix checkboxes which were broken by @nicosomb https://github.com/wallabag/wallabag/pull/6864
|
||||
|
||||
## [2.6.3](https://github.com/wallabag/wallabag/tree/2.6.3)
|
||||
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.2...2.6.3)
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# wallabag
|
||||
|
||||

|
||||
[](https://gitter.im/wallabag/wallabag)
|
||||
[](https://matrix.to/#/#wallabag:matrix.org)
|
||||
[](https://liberapay.com/wallabag/donate)
|
||||
[](https://hosted.weblate.org/engage/wallabag/?utm_source=widget)
|
||||

|
||||
|
||||
@ -27,12 +27,12 @@ class Version20170511211659 extends WallabagMigration
|
||||
$this->addSql(<<<EOD
|
||||
CREATE TEMPORARY TABLE __temp__wallabag_annotation AS
|
||||
SELECT id, user_id, entry_id, text, created_at, updated_at, quote, ranges
|
||||
FROM ${annotationTableName}
|
||||
FROM {$annotationTableName}
|
||||
EOD
|
||||
);
|
||||
$this->addSql('DROP TABLE ' . $annotationTableName);
|
||||
$this->addSql(<<<EOD
|
||||
CREATE TABLE ${annotationTableName}
|
||||
CREATE TABLE {$annotationTableName}
|
||||
(
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
user_id INTEGER DEFAULT NULL,
|
||||
@ -42,16 +42,16 @@ CREATE TABLE ${annotationTableName}
|
||||
updated_at DATETIME NOT NULL,
|
||||
quote CLOB NOT NULL,
|
||||
ranges CLOB NOT NULL,
|
||||
CONSTRAINT FK_A7AED006A76ED395 FOREIGN KEY (user_id) REFERENCES ${userTableName} (id),
|
||||
CONSTRAINT FK_A7AED006BA364942 FOREIGN KEY (entry_id) REFERENCES ${entryTableName} (id) ON DELETE CASCADE
|
||||
CONSTRAINT FK_A7AED006A76ED395 FOREIGN KEY (user_id) REFERENCES {$userTableName} (id),
|
||||
CONSTRAINT FK_A7AED006BA364942 FOREIGN KEY (entry_id) REFERENCES {$entryTableName} (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IDX_A7AED006A76ED395 ON ${annotationTableName} (user_id);
|
||||
CREATE INDEX IDX_A7AED006BA364942 ON ${annotationTableName} (entry_id);
|
||||
CREATE INDEX IDX_A7AED006A76ED395 ON {$annotationTableName} (user_id);
|
||||
CREATE INDEX IDX_A7AED006BA364942 ON {$annotationTableName} (entry_id);
|
||||
EOD
|
||||
);
|
||||
|
||||
$this->addSql(<<<EOD
|
||||
INSERT INTO ${annotationTableName} (id, user_id, entry_id, text, created_at, updated_at, quote, ranges)
|
||||
INSERT INTO {$annotationTableName} (id, user_id, entry_id, text, created_at, updated_at, quote, ranges)
|
||||
SELECT id, user_id, entry_id, text, created_at, updated_at, quote, ranges
|
||||
FROM __temp__wallabag_annotation;
|
||||
EOD
|
||||
|
||||
@ -235,6 +235,12 @@
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.tags-add-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 640px) {
|
||||
.entry-info {
|
||||
margin-bottom: 20px;
|
||||
@ -258,4 +264,12 @@
|
||||
#article .entry-info .chip-action {
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.tags-add-form {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tags-add-form-submit {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -85,7 +87,9 @@
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
.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;
|
||||
@ -131,6 +135,10 @@
|
||||
color: #abb2bf;
|
||||
}
|
||||
|
||||
input[type="url"]:not(.browser-default):disabled {
|
||||
color: #9e9e9e;
|
||||
}
|
||||
|
||||
.input-field.nav-panel-add.disabled,
|
||||
.input-field.nav-panel-add.disabled input {
|
||||
background-color: transparent;
|
||||
|
||||
@ -14,44 +14,53 @@
|
||||
}
|
||||
|
||||
.mass-action {
|
||||
margin: 10px 5px 10px 20px;
|
||||
margin: 20px 5px 10px 20px;
|
||||
}
|
||||
|
||||
.mass-action-group {
|
||||
display: flex;
|
||||
padding: 3px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.mass-action-button {
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
padding: 0 0.5rem;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
padding: 0 0.7rem;
|
||||
|
||||
i {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.entry-checkbox {
|
||||
margin: 10px 15px 10px 5px;
|
||||
.mass-action-button--tags {
|
||||
border-radius: 2px 0 0 2px;
|
||||
}
|
||||
|
||||
.card & {
|
||||
float: right;
|
||||
margin-right: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
.card-stacked .entry-checkbox {
|
||||
margin: 10px 15px 10px 5px;
|
||||
}
|
||||
|
||||
.card .entry-checkbox {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
inset: 0;
|
||||
justify-content: flex-end;
|
||||
align-items: start;
|
||||
background-color: rgb(0 172 193 / 20%);
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.entries .entry-checkbox-input,
|
||||
.mass-action .entry-checkbox-input {
|
||||
position: relative;
|
||||
left: 0;
|
||||
width: 20px;
|
||||
min-height: 25px;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
opacity: initial;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@ -64,11 +73,19 @@
|
||||
|
||||
.mass-action-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
|
||||
.mass-action-tags-input {
|
||||
.mass-action-tags-input.mass-action-tags-input {
|
||||
margin: 0;
|
||||
padding: 0 5px;
|
||||
height: 34px;
|
||||
background: white;
|
||||
border-bottom: 3px solid #c5ebef;
|
||||
}
|
||||
|
||||
.mass-action-tags-input.mass-action-tags-input.mass-action-tags-input:focus {
|
||||
border-bottom: 3px solid $blue-accent-color;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,13 +105,16 @@
|
||||
|
||||
.results {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
padding: 1rem 1rem 0;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.nb-results {
|
||||
display: inline-flex;
|
||||
}
|
||||
.nb-results {
|
||||
display: inline-flex;
|
||||
margin-bottom: 20px;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.results-item {
|
||||
@ -173,9 +193,38 @@ footer {
|
||||
}
|
||||
|
||||
@media screen and (min-width: 993px) {
|
||||
.results {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.nb-results {
|
||||
margin-bottom: 0;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.mass-action-button {
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.mass-action-group {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mass-action-tags {
|
||||
margin-top: 0;
|
||||
margin-left: 7px;
|
||||
flex-wrap: initial;
|
||||
}
|
||||
|
||||
.mass-action {
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
|
||||
.mass-action-tags-input.mass-action-tags-input {
|
||||
height: 21px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -70,6 +92,7 @@ nav {
|
||||
|
||||
.input-field input {
|
||||
display: block;
|
||||
font-size: 1.2rem;
|
||||
line-height: inherit;
|
||||
height: 3rem;
|
||||
}
|
||||
@ -79,6 +102,17 @@ nav {
|
||||
box-shadow: none;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
/* materializecss override */
|
||||
.input-field.input-field input {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.input-field.input-field input:focus {
|
||||
border-bottom: none;
|
||||
box-shadow: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-panel-top {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -15,6 +15,13 @@ div.settings div.file-field {
|
||||
}
|
||||
}
|
||||
|
||||
/* override materializecss pointer-event disabled on checkboxes */
|
||||
[type="checkbox"]:not(:checked),
|
||||
[type="checkbox"]:checked,
|
||||
.input-field label {
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
.input-field label.active {
|
||||
font-size: 1rem;
|
||||
}
|
||||
@ -31,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;
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -29,6 +29,8 @@ framework:
|
||||
# handler_id set to null will use default session handler from php.ini
|
||||
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: ~
|
||||
@ -84,13 +86,8 @@ doctrine_migrations:
|
||||
executed_at_column_name: 'executed_at'
|
||||
|
||||
fos_rest:
|
||||
zone:
|
||||
- { path: ^/api }
|
||||
- { path: ^/annotations }
|
||||
param_fetcher_listener: true
|
||||
body_listener: true
|
||||
exception:
|
||||
serializer_error_renderer: true
|
||||
view:
|
||||
mime_types:
|
||||
csv:
|
||||
@ -119,6 +116,9 @@ fos_rest:
|
||||
- { path: "^/api/entries/([0-9]+)/export.(.*)", priorities: ['epub', 'mobi', 'pdf', 'txt', 'csv'], fallback_format: json, prefer_extension: false }
|
||||
- { path: "^/api", priorities: ['json', 'xml'], fallback_format: json, prefer_extension: false }
|
||||
- { path: "^/annotations", priorities: ['json', 'xml'], fallback_format: json, prefer_extension: false }
|
||||
# for an unknown reason, EACH REQUEST goes to FOS\RestBundle\EventListener\FormatListener
|
||||
# so we need to add custom rule for custom api export but also for all other routes of the application...
|
||||
- { path: '^/', priorities: ['text/html', '*/*'], fallback_format: html, prefer_extension: false }
|
||||
|
||||
nelmio_api_doc:
|
||||
areas:
|
||||
@ -268,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:
|
||||
@ -278,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
|
||||
@ -351,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:
|
||||
@ -369,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:
|
||||
|
||||
@ -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}'
|
||||
@ -268,9 +288,23 @@ services:
|
||||
arguments:
|
||||
$baseFolder: "%kernel.project_dir%/web/assets/images"
|
||||
|
||||
Wallabag\CoreBundle\Command\CleanDownloadedImagesCommand:
|
||||
tags:
|
||||
- { name: console.command, command: 'wallabag:clean-downloaded-images' }
|
||||
|
||||
Wallabag\CoreBundle\Command\CleanDuplicatesCommand:
|
||||
tags:
|
||||
- { name: console.command, command: 'wallabag:clean-duplicates' }
|
||||
|
||||
Wallabag\CoreBundle\Command\ExportCommand:
|
||||
arguments:
|
||||
$projectDir: '%kernel.project_dir%'
|
||||
tags:
|
||||
- { name: console.command, command: 'wallabag:export' }
|
||||
|
||||
Wallabag\CoreBundle\Command\GenerateUrlHashesCommand:
|
||||
tags:
|
||||
- { name: console.command, command: 'wallabag:generate-hashed-urls' }
|
||||
|
||||
Wallabag\CoreBundle\Command\InstallCommand:
|
||||
arguments:
|
||||
@ -279,6 +313,26 @@ services:
|
||||
$defaultSettings: '%wallabag_core.default_internal_settings%'
|
||||
$defaultIgnoreOriginInstanceRules: '%wallabag_core.default_ignore_origin_instance_rules%'
|
||||
|
||||
Wallabag\CoreBundle\Command\ListUserCommand:
|
||||
tags:
|
||||
- { name: console.command, command: 'wallabag:user:list' }
|
||||
|
||||
Wallabag\CoreBundle\Command\ReloadEntryCommand:
|
||||
tags:
|
||||
- { name: console.command, command: 'wallabag:entry:reload' }
|
||||
|
||||
Wallabag\CoreBundle\Command\ShowUserCommand:
|
||||
tags:
|
||||
- { name: console.command, command: 'wallabag:user:show' }
|
||||
|
||||
Wallabag\CoreBundle\Command\TagAllCommand:
|
||||
tags:
|
||||
- { name: console.command, command: 'wallabag:tag:all' }
|
||||
|
||||
Wallabag\ImportBundle\Command\ImportCommand:
|
||||
tags:
|
||||
- { name: console.command, command: 'wallabag:import' }
|
||||
|
||||
wallabag_core.entry.download_images.client:
|
||||
alias: 'httplug.client.wallabag_core.entry.download_images'
|
||||
|
||||
@ -343,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 }
|
||||
@ -351,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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
wallabag_core:
|
||||
version: 2.6.3
|
||||
version: 2.6.14
|
||||
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"
|
||||
|
||||
230
composer.json
230
composer.json
@ -56,123 +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/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"
|
||||
@ -195,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": {
|
||||
@ -209,7 +211,11 @@
|
||||
"incenteev-parameters": {
|
||||
"file": "app/config/parameters.yml"
|
||||
},
|
||||
"public-dir": "web"
|
||||
"public-dir": "web",
|
||||
"symfony": {
|
||||
"allow-contrib": true,
|
||||
"require": "4.4.*"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-install-cmd": [
|
||||
|
||||
2854
composer.lock
generated
2854
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
FROM php:8.1-fpm AS rootless
|
||||
FROM php:8.1-fpm-bookworm AS rootless
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG NODE_VERSION=16
|
||||
ARG NODE_VERSION=20
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y \
|
||||
@ -33,8 +33,7 @@ RUN apt-get update && apt-get install -y \
|
||||
zlib1g-dev \
|
||||
git \
|
||||
build-essential \
|
||||
nodejs \
|
||||
npm
|
||||
nodejs
|
||||
RUN docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp
|
||||
RUN docker-php-ext-install -j "$(nproc)" \
|
||||
bcmath \
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
"doc": "docs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
"node": ">=20"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@ -45,11 +45,6 @@ parameters:
|
||||
count: 1
|
||||
path: tests/Wallabag/CoreBundle/Controller/FeedControllerTest.php
|
||||
|
||||
-
|
||||
message: "#^Call to method generate\\(\\) on an unknown class PHPUnit_Framework_MockObject_MockObject\\.$#"
|
||||
count: 2
|
||||
path: tests/Wallabag/CoreBundle/Helper/RedirectTest.php
|
||||
|
||||
-
|
||||
message: "#^Property Tests\\\\Wallabag\\\\CoreBundle\\\\Helper\\\\RedirectTest\\:\\:\\$routerMock has unknown class PHPUnit_Framework_MockObject_MockObject as its type\\.$#"
|
||||
count: 1
|
||||
@ -64,3 +59,13 @@ parameters:
|
||||
message: "#^Method FOS\\\\UserBundle\\\\Model\\\\UserManagerInterface\\:\\:updateUser()#"
|
||||
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
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -269,6 +269,16 @@ class EntryRestController extends WallabagRestController
|
||||
* example="example.com",
|
||||
* )
|
||||
* ),
|
||||
* @OA\Parameter(
|
||||
* name="annotations",
|
||||
* in="query",
|
||||
* description="filter by entries with annotations. Use 1 for entries with annotations, 0 for entries without annotations. All entries by default",
|
||||
* required=false,
|
||||
* @OA\Schema(
|
||||
* type="integer",
|
||||
* enum={"1", "0"}
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="200",
|
||||
* description="Returned when successful"
|
||||
@ -294,6 +304,7 @@ class EntryRestController extends WallabagRestController
|
||||
$since = $request->query->get('since', 0);
|
||||
$detail = strtolower($request->query->get('detail', 'full'));
|
||||
$domainName = (null === $request->query->get('domain_name')) ? '' : (string) $request->query->get('domain_name');
|
||||
$hasAnnotations = (null === $request->query->get('annotations')) ? null : (bool) $request->query->get('annotations');
|
||||
|
||||
try {
|
||||
/** @var Pagerfanta $pager */
|
||||
@ -307,7 +318,8 @@ class EntryRestController extends WallabagRestController
|
||||
$since,
|
||||
$tags,
|
||||
$detail,
|
||||
$domainName
|
||||
$domainName,
|
||||
$hasAnnotations
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
throw new BadRequestHttpException($e->getMessage());
|
||||
@ -332,6 +344,7 @@ class EntryRestController extends WallabagRestController
|
||||
'tags' => $tags,
|
||||
'since' => $since,
|
||||
'detail' => $detail,
|
||||
'annotations' => $hasAnnotations,
|
||||
],
|
||||
true
|
||||
)
|
||||
@ -489,7 +502,7 @@ class EntryRestController extends WallabagRestController
|
||||
* @OA\Parameter(
|
||||
* name="urls",
|
||||
* in="query",
|
||||
* description="Urls (as an array) to create. A JSON array of urls [{'url': 'http://...'}, {'url': 'http://...'}]",
|
||||
* description="Urls (as an array) to create. A JSON array of urls ['http://...', 'http://...']",
|
||||
* required=true,
|
||||
* @OA\Schema(type="string")
|
||||
* ),
|
||||
|
||||
@ -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;
|
||||
@ -32,6 +33,7 @@ use Wallabag\CoreBundle\Form\Type\IgnoreOriginUserRuleType;
|
||||
use Wallabag\CoreBundle\Form\Type\TaggingRuleImportType;
|
||||
use Wallabag\CoreBundle\Form\Type\TaggingRuleType;
|
||||
use Wallabag\CoreBundle\Form\Type\UserInformationType;
|
||||
use Wallabag\CoreBundle\Helper\Redirect;
|
||||
use Wallabag\CoreBundle\Repository\ConfigRepository;
|
||||
use Wallabag\CoreBundle\Repository\EntryRepository;
|
||||
use Wallabag\CoreBundle\Repository\IgnoreOriginUserRuleRepository;
|
||||
@ -48,8 +50,9 @@ class ConfigController extends AbstractController
|
||||
private TagRepository $tagRepository;
|
||||
private AnnotationRepository $annotationRepository;
|
||||
private ConfigRepository $configRepository;
|
||||
private Redirect $redirectHelper;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, UserManagerInterface $userManager, EntryRepository $entryRepository, TagRepository $tagRepository, AnnotationRepository $annotationRepository, ConfigRepository $configRepository)
|
||||
public function __construct(EntityManagerInterface $entityManager, UserManagerInterface $userManager, EntryRepository $entryRepository, TagRepository $tagRepository, AnnotationRepository $annotationRepository, ConfigRepository $configRepository, Redirect $redirectHelper)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->userManager = $userManager;
|
||||
@ -57,6 +60,7 @@ class ConfigController extends AbstractController
|
||||
$this->tagRepository = $tagRepository;
|
||||
$this->annotationRepository = $annotationRepository;
|
||||
$this->configRepository = $configRepository;
|
||||
$this->redirectHelper = $redirectHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -254,10 +258,14 @@ class ConfigController extends AbstractController
|
||||
/**
|
||||
* Disable 2FA using email.
|
||||
*
|
||||
* @Route("/config/otp/email/disable", name="disable_otp_email")
|
||||
* @Route("/config/otp/email/disable", name="disable_otp_email", methods={"POST"})
|
||||
*/
|
||||
public function disableOtpEmailAction()
|
||||
public function disableOtpEmailAction(Request $request)
|
||||
{
|
||||
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
|
||||
throw new BadRequestHttpException('Bad CSRF token.');
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
$user->setEmailTwoFactor(false);
|
||||
|
||||
@ -274,10 +282,14 @@ class ConfigController extends AbstractController
|
||||
/**
|
||||
* Enable 2FA using email.
|
||||
*
|
||||
* @Route("/config/otp/email", name="config_otp_email")
|
||||
* @Route("/config/otp/email", name="config_otp_email", methods={"POST"})
|
||||
*/
|
||||
public function otpEmailAction()
|
||||
public function otpEmailAction(Request $request)
|
||||
{
|
||||
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
|
||||
throw new BadRequestHttpException('Bad CSRF token.');
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
|
||||
$user->setGoogleAuthenticatorSecret(null);
|
||||
@ -297,10 +309,14 @@ class ConfigController extends AbstractController
|
||||
/**
|
||||
* Disable 2FA using OTP app.
|
||||
*
|
||||
* @Route("/config/otp/app/disable", name="disable_otp_app")
|
||||
* @Route("/config/otp/app/disable", name="disable_otp_app", methods={"POST"})
|
||||
*/
|
||||
public function disableOtpAppAction()
|
||||
public function disableOtpAppAction(Request $request)
|
||||
{
|
||||
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
|
||||
throw new BadRequestHttpException('Bad CSRF token.');
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
|
||||
$user->setGoogleAuthenticatorSecret('');
|
||||
@ -319,10 +335,14 @@ class ConfigController extends AbstractController
|
||||
/**
|
||||
* Enable 2FA using OTP app, user will need to confirm the generated code from the app.
|
||||
*
|
||||
* @Route("/config/otp/app", name="config_otp_app")
|
||||
* @Route("/config/otp/app", name="config_otp_app", methods={"POST"})
|
||||
*/
|
||||
public function otpAppAction(GoogleAuthenticatorInterface $googleAuthenticator)
|
||||
public function otpAppAction(Request $request, GoogleAuthenticatorInterface $googleAuthenticator)
|
||||
{
|
||||
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
|
||||
throw new BadRequestHttpException('Bad CSRF token.');
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
$secret = $googleAuthenticator->generateSecret();
|
||||
|
||||
@ -357,8 +377,10 @@ class ConfigController extends AbstractController
|
||||
* Cancelling 2FA using OTP app.
|
||||
*
|
||||
* @Route("/config/otp/app/cancel", name="config_otp_app_cancel")
|
||||
*
|
||||
* XXX: commented until we rewrite 2fa with a real two-steps activation
|
||||
*/
|
||||
public function otpAppCancelAction()
|
||||
/*public function otpAppCancelAction()
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$user->setGoogleAuthenticatorSecret(null);
|
||||
@ -367,21 +389,27 @@ class ConfigController extends AbstractController
|
||||
$this->userManager->updateUser($user, true);
|
||||
|
||||
return $this->redirect($this->generateUrl('config') . '#set3');
|
||||
}
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Validate OTP code.
|
||||
*
|
||||
* @Route("/config/otp/app/check", name="config_otp_app_check")
|
||||
* @Route("/config/otp/app/check", name="config_otp_app_check", methods={"POST"})
|
||||
*/
|
||||
public function otpAppCheckAction(Request $request, GoogleAuthenticatorInterface $googleAuthenticator)
|
||||
{
|
||||
if (!$this->isCsrfTokenValid('otp', $request->request->get('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'
|
||||
@ -391,30 +419,35 @@ class ConfigController extends AbstractController
|
||||
}
|
||||
|
||||
$this->addFlash(
|
||||
'two_factor',
|
||||
'scheb_two_factor.code_invalid'
|
||||
'notice',
|
||||
'flashes.config.notice.otp_code_invalid'
|
||||
);
|
||||
|
||||
return $this->redirect($this->generateUrl('config_otp_app'));
|
||||
$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'
|
||||
@ -424,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'
|
||||
@ -451,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);
|
||||
@ -487,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);
|
||||
@ -530,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) {
|
||||
@ -584,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();
|
||||
@ -607,19 +648,25 @@ 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());
|
||||
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->redirect($request->getSession()->get('prevUrl'));
|
||||
$redirectUrl = $this->redirectHelper->to($request->query->get('redirect'));
|
||||
|
||||
return $this->redirect($redirectUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -627,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)) {
|
||||
|
||||
@ -14,8 +14,8 @@ 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\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Wallabag\CoreBundle\Entity\Entry;
|
||||
use Wallabag\CoreBundle\Entity\Tag;
|
||||
@ -53,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 = [];
|
||||
@ -128,7 +132,7 @@ class EntryController extends AbstractController
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
$redirectUrl = $this->redirectHelper->to($request->getSession()->get('prevUrl'));
|
||||
$redirectUrl = $this->redirectHelper->to($request->query->get('redirect'));
|
||||
|
||||
return $this->redirect($redirectUrl);
|
||||
}
|
||||
@ -390,7 +394,6 @@ class EntryController extends AbstractController
|
||||
public function viewAction(Entry $entry)
|
||||
{
|
||||
$this->checkUserAction($entry);
|
||||
$this->get('session')->set('prevUrl', $this->generateUrl('view', ['id' => $entry->getId()]));
|
||||
|
||||
return $this->render(
|
||||
'@WallabagCore/Entry/entry.html.twig',
|
||||
@ -402,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');
|
||||
@ -431,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();
|
||||
@ -452,7 +463,7 @@ class EntryController extends AbstractController
|
||||
$message
|
||||
);
|
||||
|
||||
$redirectUrl = $this->redirectHelper->to($request->getSession()->get('prevUrl'));
|
||||
$redirectUrl = $this->redirectHelper->to($request->query->get('redirect'));
|
||||
|
||||
return $this->redirect($redirectUrl);
|
||||
}
|
||||
@ -460,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();
|
||||
@ -482,7 +497,7 @@ class EntryController extends AbstractController
|
||||
$message
|
||||
);
|
||||
|
||||
$redirectUrl = $this->redirectHelper->to($request->getSession()->get('prevUrl'));
|
||||
$redirectUrl = $this->redirectHelper->to($request->query->get('redirect'));
|
||||
|
||||
return $this->redirect($redirectUrl);
|
||||
}
|
||||
@ -490,20 +505,23 @@ 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
|
||||
// to avoid redirecting to the deleted entry. Ugh.
|
||||
$url = $this->generateUrl(
|
||||
'view',
|
||||
['id' => $entry->getId()],
|
||||
UrlGeneratorInterface::ABSOLUTE_PATH
|
||||
['id' => $entry->getId()]
|
||||
);
|
||||
|
||||
// entry deleted, dispatch event about it!
|
||||
@ -518,7 +536,7 @@ class EntryController extends AbstractController
|
||||
);
|
||||
|
||||
// don't redirect user to the deleted entry (check that the referer doesn't end with the same url)
|
||||
$prev = $request->getSession()->get('prevUrl');
|
||||
$prev = $request->query->get('redirect', '');
|
||||
$to = (1 !== preg_match('#' . $url . '$#i', $prev) ? $prev : null);
|
||||
|
||||
$redirectUrl = $this->redirectHelper->to($to);
|
||||
@ -529,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()) {
|
||||
@ -552,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();
|
||||
@ -573,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
|
||||
@ -595,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
|
||||
*/
|
||||
@ -617,7 +643,6 @@ class EntryController extends AbstractController
|
||||
{
|
||||
$searchTerm = (isset($request->get('search_entry')['term']) ? $request->get('search_entry')['term'] : '');
|
||||
$currentRoute = (null !== $request->query->get('currentRoute') ? $request->query->get('currentRoute') : '');
|
||||
$request->getSession()->set('prevUrl', $request->getRequestUri());
|
||||
|
||||
$formOptions = [];
|
||||
|
||||
@ -675,9 +700,6 @@ class EntryController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
$nbEntriesUntagged = $this->entryRepository
|
||||
->countUntaggedEntriesByUser($this->getUser()->getId());
|
||||
|
||||
return $this->render(
|
||||
'@WallabagCore/Entry/entries.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
@ -685,7 +707,6 @@ class EntryController extends AbstractController
|
||||
'currentPage' => $page,
|
||||
'searchTerm' => $searchTerm,
|
||||
'isFiltered' => $form->isSubmitted(),
|
||||
'nbEntriesUntagged' => $nbEntriesUntagged,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -45,7 +46,7 @@ class TagController extends AbstractController
|
||||
$form = $this->createForm(NewTagType::class, new Tag());
|
||||
$form->handleRequest($request);
|
||||
|
||||
$tags = $form->get('label')->getData();
|
||||
$tags = $form->get('label')->getData() ?? '';
|
||||
$tagsExploded = explode(',', $tags);
|
||||
|
||||
// avoid too much tag to be added
|
||||
@ -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);
|
||||
@ -104,7 +109,7 @@ class TagController extends AbstractController
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
$redirectUrl = $this->redirectHelper->to($request->getSession()->get('prevUrl'), '', true);
|
||||
$redirectUrl = $this->redirectHelper->to($request->query->get('redirect'), true);
|
||||
|
||||
return $this->redirect($redirectUrl);
|
||||
}
|
||||
@ -185,7 +190,7 @@ class TagController extends AbstractController
|
||||
$form = $this->createForm(RenameTagType::class, new Tag());
|
||||
$form->handleRequest($request);
|
||||
|
||||
$redirectUrl = $this->redirectHelper->to($request->getSession()->get('prevUrl'), '', true);
|
||||
$redirectUrl = $this->redirectHelper->to($request->query->get('redirect'), true);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$newTag = new 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 */
|
||||
@ -257,19 +266,23 @@ class TagController extends AbstractController
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
return $this->redirect($this->redirectHelper->to($request->getSession()->get('prevUrl'), '', true));
|
||||
return $this->redirect($this->redirectHelper->to($request->query->get('redirect'), true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@ -279,7 +292,7 @@ class TagController extends AbstractController
|
||||
$this->entityManager->remove($tag);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
$redirectUrl = $this->redirectHelper->to($request->getSession()->get('prevUrl'), '', true);
|
||||
$redirectUrl = $this->redirectHelper->to($request->query->get('redirect'), true);
|
||||
|
||||
return $this->redirect($redirectUrl);
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Wallabag\AnnotationBundle\Entity\Annotation;
|
||||
use Wallabag\CoreBundle\Helper\EntityTimestampsTrait;
|
||||
use Wallabag\CoreBundle\Helper\UrlHasher;
|
||||
use Wallabag\CoreBundle\Tools\Utils;
|
||||
use Wallabag\UserBundle\Entity\User;
|
||||
|
||||
/**
|
||||
@ -658,6 +659,14 @@ class Entry
|
||||
$this->readingTime = $readingTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getUserReadingTime()
|
||||
{
|
||||
return round($this->readingTime / $this->getUser()->getConfig()->getReadingSpeed() * Utils::DEFAULT_WORDS_PER_MINUTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@ -6,6 +6,7 @@ use Lexik\Bundle\FormFilterBundle\Filter\FilterOperands;
|
||||
use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\CheckboxFilterType;
|
||||
use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\ChoiceFilterType;
|
||||
use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\DateRangeFilterType;
|
||||
use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\NumberFilterType;
|
||||
use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\NumberRangeFilterType;
|
||||
use Lexik\Bundle\FormFilterBundle\Filter\Form\Type\TextFilterType;
|
||||
use Lexik\Bundle\FormFilterBundle\Filter\Query\QueryInterface;
|
||||
@ -15,6 +16,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Wallabag\CoreBundle\Repository\EntryRepository;
|
||||
use Wallabag\CoreBundle\Tools\Utils;
|
||||
use Wallabag\UserBundle\Entity\User;
|
||||
|
||||
class EntryFilterType extends AbstractType
|
||||
@ -56,8 +58,8 @@ class EntryFilterType extends AbstractType
|
||||
return;
|
||||
}
|
||||
|
||||
$min = (int) ($lower * $user->getConfig()->getReadingSpeed() / 200);
|
||||
$max = (int) ($upper * $user->getConfig()->getReadingSpeed() / 200);
|
||||
$min = (int) ($lower * $user->getConfig()->getReadingSpeed() / Utils::DEFAULT_WORDS_PER_MINUTE);
|
||||
$max = (int) ($upper * $user->getConfig()->getReadingSpeed() / Utils::DEFAULT_WORDS_PER_MINUTE);
|
||||
|
||||
if (null === $lower && null !== $upper) {
|
||||
// only lower value is defined: query all entries with reading LOWER THAN this value
|
||||
@ -102,10 +104,13 @@ class EntryFilterType extends AbstractType
|
||||
return $filterQuery->createCondition($expression);
|
||||
},
|
||||
'label' => 'entry.filters.domain_label',
|
||||
'attr' => [
|
||||
'autocapitalize' => 'off',
|
||||
],
|
||||
])
|
||||
->add('httpStatus', TextFilterType::class, [
|
||||
->add('httpStatus', NumberFilterType::class, [
|
||||
'apply_filter' => function (QueryInterface $filterQuery, $field, $values) {
|
||||
$value = $values['value'];
|
||||
$value = (int) $values['value'];
|
||||
if (false === \array_key_exists($value, Response::$statusTexts)) {
|
||||
return false;
|
||||
}
|
||||
@ -117,6 +122,11 @@ class EntryFilterType extends AbstractType
|
||||
return $filterQuery->createCondition($expression, $parameters);
|
||||
},
|
||||
'label' => 'entry.filters.http_status_label',
|
||||
'html5' => true,
|
||||
'attr' => [
|
||||
'min' => 100,
|
||||
'max' => 527,
|
||||
],
|
||||
])
|
||||
->add('isArchived', CheckboxFilterType::class, [
|
||||
'label' => 'entry.filters.archived_label',
|
||||
|
||||
@ -4,7 +4,6 @@ namespace Wallabag\CoreBundle\Form\Type;
|
||||
|
||||
use FOS\UserBundle\Form\Type\RegistrationFormType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
@ -23,15 +22,6 @@ class UserInformationType extends AbstractType
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'config.form_user.email_label',
|
||||
])
|
||||
->add('emailTwoFactor', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'config.form_user.emailTwoFactor_label',
|
||||
])
|
||||
->add('googleTwoFactor', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'config.form_user.googleTwoFactor_label',
|
||||
'mapped' => false,
|
||||
])
|
||||
->add('save', SubmitType::class, [
|
||||
'label' => 'config.form.save',
|
||||
])
|
||||
|
||||
@ -210,7 +210,7 @@ class EntriesExport
|
||||
$publishedDate = $entry->getPublishedAt()->format('Y-m-d');
|
||||
}
|
||||
|
||||
$readingTime = round($entry->getReadingTime() / $user->getConfig()->getReadingSpeed() * 200);
|
||||
$readingTime = $entry->getUserReadingTime();
|
||||
|
||||
$titlepage = $content_start .
|
||||
'<h1>' . $entry->getTitle() . '</h1>' .
|
||||
@ -331,7 +331,7 @@ class EntriesExport
|
||||
$authors = implode(',', $publishedBy);
|
||||
}
|
||||
|
||||
$readingTime = $entry->getReadingTime() / $user->getConfig()->getReadingSpeed() * 200;
|
||||
$readingTime = $entry->getUserReadingTime();
|
||||
|
||||
$pdf->addPage();
|
||||
$html = '<h1>' . $entry->getTitle() . '</h1>' .
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace Wallabag\CoreBundle\Helper;
|
||||
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Wallabag\CoreBundle\Entity\Config;
|
||||
@ -23,16 +24,23 @@ class Redirect
|
||||
|
||||
/**
|
||||
* @param string $url URL to redirect
|
||||
* @param string $fallback Fallback URL if $url is null
|
||||
* @param bool $ignoreActionMarkAsRead Ignore configured action when mark as read
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function to($url, $fallback = '', $ignoreActionMarkAsRead = false)
|
||||
public function to($url, $ignoreActionMarkAsRead = false)
|
||||
{
|
||||
$user = $this->tokenStorage->getToken() ? $this->tokenStorage->getToken()->getUser() : null;
|
||||
|
||||
if (!$user instanceof User) {
|
||||
if (null === $url) {
|
||||
return $this->router->generate('homepage');
|
||||
}
|
||||
|
||||
if (!Uri::isAbsolutePathReference(new Uri($url))) {
|
||||
return $this->router->generate('homepage');
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
@ -41,14 +49,14 @@ class Redirect
|
||||
return $this->router->generate('homepage');
|
||||
}
|
||||
|
||||
if (null !== $url) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
if ('' === $fallback) {
|
||||
if (null === $url) {
|
||||
return $this->router->generate('homepage');
|
||||
}
|
||||
|
||||
return $fallback;
|
||||
if (!Uri::isAbsolutePathReference(new Uri($url))) {
|
||||
return $this->router->generate('homepage');
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,7 +133,7 @@ class RuleBasedTagger
|
||||
private function fixEntry(Entry $entry)
|
||||
{
|
||||
$clonedEntry = clone $entry;
|
||||
$clonedEntry->setReadingTime($entry->getReadingTime() / $entry->getUser()->getConfig()->getReadingSpeed() * 200);
|
||||
$clonedEntry->setReadingTime($entry->getUserReadingTime());
|
||||
|
||||
return $clonedEntry;
|
||||
}
|
||||
|
||||
@ -37,6 +37,20 @@ class EntryRepository extends ServiceEntityRepository
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all entries count for a user.
|
||||
*
|
||||
* @param int $userId
|
||||
*
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function getCountBuilderForAllByUser($userId)
|
||||
{
|
||||
return $this
|
||||
->getQueryBuilderByUser($userId)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves unread entries for a user.
|
||||
*
|
||||
@ -52,6 +66,21 @@ class EntryRepository extends ServiceEntityRepository
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves unread entries count for a user.
|
||||
*
|
||||
* @param int $userId
|
||||
*
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function getCountBuilderForUnreadByUser($userId)
|
||||
{
|
||||
return $this
|
||||
->getQueryBuilderByUser($userId)
|
||||
->andWhere('e.isArchived = false')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves entries with the same domain.
|
||||
*
|
||||
@ -94,6 +123,21 @@ class EntryRepository extends ServiceEntityRepository
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves read entries count for a user.
|
||||
*
|
||||
* @param int $userId
|
||||
*
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function getCountBuilderForArchiveByUser($userId)
|
||||
{
|
||||
return $this
|
||||
->getQueryBuilderByUser($userId)
|
||||
->andWhere('e.isArchived = true')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves starred entries for a user.
|
||||
*
|
||||
@ -109,6 +153,21 @@ class EntryRepository extends ServiceEntityRepository
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves starred entries count for a user.
|
||||
*
|
||||
* @param int $userId
|
||||
*
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function getCountBuilderForStarredByUser($userId)
|
||||
{
|
||||
return $this
|
||||
->getQueryBuilderByUser($userId)
|
||||
->andWhere('e.isStarred = true')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves entries filtered with a search term for a user.
|
||||
*
|
||||
@ -207,14 +266,15 @@ class EntryRepository extends ServiceEntityRepository
|
||||
* @param string $order
|
||||
* @param int $since
|
||||
* @param string $tags
|
||||
* @param string $detail 'metadata' or 'full'. Include content field if 'full'
|
||||
* @param string $detail 'metadata' or 'full'. Include content field if 'full'
|
||||
* @param string $domainName
|
||||
* @param bool $hasAnnotations
|
||||
*
|
||||
* @todo Breaking change: replace default detail=full by detail=metadata in a future version
|
||||
*
|
||||
* @return Pagerfanta
|
||||
*/
|
||||
public function findEntries($userId, $isArchived = null, $isStarred = null, $isPublic = null, $sort = 'created', $order = 'asc', $since = 0, $tags = '', $detail = 'full', $domainName = '')
|
||||
public function findEntries($userId, $isArchived = null, $isStarred = null, $isPublic = null, $sort = 'created', $order = 'asc', $since = 0, $tags = '', $detail = 'full', $domainName = '', $hasAnnotations = null)
|
||||
{
|
||||
if (!\in_array(strtolower($detail), ['full', 'metadata'], true)) {
|
||||
throw new \Exception('Detail "' . $detail . '" parameter is wrong, allowed: full or metadata');
|
||||
@ -273,6 +333,16 @@ class EntryRepository extends ServiceEntityRepository
|
||||
$qb->andWhere('e.domainName = :domainName')->setParameter('domainName', $domainName);
|
||||
}
|
||||
|
||||
if (null !== $hasAnnotations) {
|
||||
if ($hasAnnotations) {
|
||||
$qb->leftJoin('e.annotations', 'a')
|
||||
->andWhere('a.id IS NOT NULL');
|
||||
} else {
|
||||
$qb->leftJoin('e.annotations', 'a')
|
||||
->andWhere('a.id IS NULL');
|
||||
}
|
||||
}
|
||||
|
||||
if (!\in_array(strtolower($order), ['asc', 'desc'], true)) {
|
||||
throw new \Exception('Order "' . $order . '" parameter is wrong, allowed: asc or desc');
|
||||
}
|
||||
@ -563,6 +633,23 @@ class EntryRepository extends ServiceEntityRepository
|
||||
return $qb->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $userId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function findEmptyEntriesIdByUserId($userId = null)
|
||||
{
|
||||
$qb = $this->createQueryBuilder('e')
|
||||
->select('e.id');
|
||||
|
||||
if (null !== $userId) {
|
||||
$qb->where('e.user = :userid AND e.content IS NULL')->setParameter(':userid', $userId);
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all entries by url and owner.
|
||||
*
|
||||
|
||||
@ -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) }}
|
||||
@ -209,38 +224,66 @@
|
||||
|
||||
{{ form_widget(form.user.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }}
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<h5>{{ 'config.otp.page_title'|trans }}</h5>
|
||||
|
||||
<p>{{ 'config.form_user.two_factor_description'|trans }}</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'config.form_user.two_factor.table_method'|trans }}</th>
|
||||
<th>{{ 'config.form_user.two_factor.table_state'|trans }}</th>
|
||||
<th>{{ 'config.form_user.two_factor.table_action'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ 'config.form_user.two_factor.emailTwoFactor_label'|trans }}</td>
|
||||
<td>{% if app.user.isEmailTwoFactor %}<b>{{ 'config.form_user.two_factor.state_enabled'|trans }}</b>{% else %}{{ 'config.form_user.two_factor.state_disabled'|trans }}{% endif %}</td>
|
||||
<td><a href="{{ path('config_otp_email') }}" class="waves-effect waves-light btn{% if app.user.isEmailTwoFactor %} disabled{% endif %}">{{ 'config.form_user.two_factor.action_email'|trans }}</a> {% if app.user.isEmailTwoFactor %}<a href="{{ path('disable_otp_email') }}" class="waves-effect waves-light btn red">Disable</a>{% endif %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ 'config.form_user.two_factor.googleTwoFactor_label'|trans }}</td>
|
||||
<td>{% if app.user.isGoogleTwoFactor %}<b>{{ 'config.form_user.two_factor.state_enabled'|trans }}</b>{% else %}{{ 'config.form_user.two_factor.state_disabled'|trans }}{% endif %}</td>
|
||||
<td><a href="{{ path('config_otp_app') }}" class="waves-effect waves-light btn{% if app.user.isGoogleTwoFactor %} disabled{% endif %}">{{ 'config.form_user.two_factor.action_app'|trans }}</a> {% if app.user.isGoogleTwoFactor %}<a href="{{ path('disable_otp_app') }}" class="waves-effect waves-light btn red">Disable</a>{% endif %}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ form_widget(form.user._token) }}
|
||||
</form>
|
||||
|
||||
{{ form_end(form.user) }}
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<h5>{{ 'config.otp.page_title'|trans }}</h5>
|
||||
|
||||
<p>{{ 'config.form_user.two_factor_description'|trans }}</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'config.form_user.two_factor.table_method'|trans }}</th>
|
||||
<th>{{ 'config.form_user.two_factor.table_state'|trans }}</th>
|
||||
<th>{{ 'config.form_user.two_factor.table_action'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ 'config.form_user.two_factor.emailTwoFactor_label'|trans }}</td>
|
||||
<td>{% if app.user.isEmailTwoFactor %}<b>{{ 'config.form_user.two_factor.state_enabled'|trans }}</b>{% else %}{{ 'config.form_user.two_factor.state_disabled'|trans }}{% endif %}</td>
|
||||
<td>
|
||||
<form action="{{ path('config_otp_email') }}" method="post" name="config_otp_email">
|
||||
<input type="hidden" name="token" value="{{ csrf_token('otp') }}" />
|
||||
|
||||
<button class="waves-effect waves-light btn{% if app.user.isEmailTwoFactor %} disabled{% endif %}" type="submit">{{ 'config.form_user.two_factor.action_email'|trans }}</button>
|
||||
</form>
|
||||
{% if app.user.isEmailTwoFactor %}
|
||||
<form action="{{ path('disable_otp_email') }}" method="post" name="disable_otp_email">
|
||||
<input type="hidden" name="token" value="{{ csrf_token('otp') }}" />
|
||||
|
||||
<button class="waves-effect waves-light btn red" type="submit">Disable</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ 'config.form_user.two_factor.googleTwoFactor_label'|trans }}</td>
|
||||
<td>{% if app.user.isGoogleTwoFactor %}<b>{{ 'config.form_user.two_factor.state_enabled'|trans }}</b>{% else %}{{ 'config.form_user.two_factor.state_disabled'|trans }}{% endif %}</td>
|
||||
<td>
|
||||
<form action="{{ path('config_otp_app') }}" method="post" name="config_otp_app">
|
||||
<input type="hidden" name="token" value="{{ csrf_token('otp') }}" />
|
||||
|
||||
<button class="waves-effect waves-light btn{% if app.user.isGoogleTwoFactor %} disabled{% endif %}" type="submit">{{ 'config.form_user.two_factor.action_app'|trans }}</button>
|
||||
</form>
|
||||
{% if app.user.isGoogleTwoFactor %}
|
||||
<form action="{{ path('disable_otp_app') }}" method="post" name="disable_otp_app">
|
||||
<input type="hidden" name="token" value="{{ csrf_token('otp') }}" />
|
||||
|
||||
<button class="waves-effect waves-light btn red" type="submit">Disable</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="set4" class="col s12">
|
||||
@ -297,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>
|
||||
@ -477,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>
|
||||
|
||||
@ -40,18 +40,16 @@
|
||||
{% endfor %}
|
||||
|
||||
<form class="form" action="{{ path("config_otp_app_check") }}" method="post">
|
||||
<input type="hidden" name="token" value="{{ csrf_token('otp') }}" />
|
||||
<div class="card-content">
|
||||
<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>
|
||||
<div class="card-action">
|
||||
<a href="{{ path('config_otp_app_cancel') }}" class="waves-effect waves-light grey btn">
|
||||
{{ 'config.otp.app.cancel'|trans }}
|
||||
</a>
|
||||
<button class="btn waves-effect waves-light" type="submit" name="send">
|
||||
{{ 'config.otp.app.enable'|trans }}
|
||||
<i class="material-icons right">send</i>
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
<div class="entry-checkbox">
|
||||
<input type="checkbox" class="entry-checkbox-input" data-js="entry-checkbox" name="entry-checkbox[]" value="{{ entry.id }}" />
|
||||
</div>
|
||||
<label class="entry-checkbox">
|
||||
<input type="checkbox" form="form_mass_action" class="entry-checkbox-input" data-js="entry-checkbox" name="entry-checkbox[]" value="{{ entry.id }}" />
|
||||
</label>
|
||||
|
||||
@ -7,18 +7,38 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% 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}) }}" data-action="same_domain" data-entry-id="{{ entry.id }}"><i class="material-icons">language</i></a>
|
||||
<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}) }}" 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}) }}" 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}) }}" 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>
|
||||
|
||||
@ -9,12 +9,33 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include "@WallabagCore/Entry/Card/_content.html.twig" with {'entry': entry, 'withMetadata': true, 'subClass': 'metadata'} only %}
|
||||
|
||||
{% 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}) }}"><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}) }}"><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}) }}"><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}) }}"><i class="material-icons">delete</i></a>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<div class="card entry-card{% if currentRoute in routes and entry.isArchived %} archived{% endif %}">
|
||||
{% include "@WallabagCore/Entry/Card/_mass_checkbox.html.twig" with {'entry': entry} only %}
|
||||
<div class="card-body">
|
||||
<div class="{% if app.user.config.displayThumbnails %}card-image{% endif %} waves-effect waves-block waves-light">
|
||||
{% include "@WallabagCore/Entry/Card/_mass_checkbox.html.twig" with {'entry': entry} only %}
|
||||
<ul class="card-entry-labels">
|
||||
{% for tag in entry.tags|slice(0, 3) %}
|
||||
<li title="{{ tag.label }}"><a href="{{ path('tag_entries', {'slug': tag.slug}) }}">{{ tag.label }}</a></li>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{% set reading_time = entry.readingTime / app.user.config.readingSpeed * 200 %}
|
||||
{% set reading_time = entry.userReadingTime %}
|
||||
<i class="material-icons grey-text">timer</i>
|
||||
{% if reading_time > 0 %}
|
||||
<span>{{ 'entry.list.reading_time_minutes_short'|trans({'%readingTime%': reading_time|round}) }}</span>
|
||||
<span>{{ 'entry.list.reading_time_minutes_short'|trans({'%readingTime%': reading_time}) }}</span>
|
||||
{% else %}
|
||||
<span>{{ 'entry.list.reading_time_less_one_minute_short'|trans|raw }}</span>
|
||||
{% endif %}
|
||||
|
||||
@ -4,9 +4,14 @@
|
||||
<li class="chip">
|
||||
<a class="chip-label" href="{{ path('tag_entries', {'slug': tag.slug}) }}">{{ tag.label }}</a>
|
||||
{% if withRemove is defined and withRemove == true %}
|
||||
<a class="chip-action" href="{{ path('remove_tag', {'entry': entryId, 'tag': tag.id}) }}" onclick="return confirm('{{ 'entry.confirm.delete_tag'|trans|escape('js') }}')">
|
||||
<i class="material-icons vertical-align-middle">delete</i>
|
||||
</a>
|
||||
{% set current_path = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %}
|
||||
<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 %}
|
||||
|
||||
@ -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' %}
|
||||
|
||||
@ -19,18 +19,27 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% 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') }}" 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') }}"><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>
|
||||
@ -39,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')}) }}" 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 %}
|
||||
@ -49,17 +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>
|
||||
<label for="mass-action-tags-displayed" class="mass-action-button btn cyan darken-1" type="button" title="{{ 'entry.list.add_tags'|trans }}"><i class="material-icons">label</i></label>
|
||||
<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>
|
||||
|
||||
<input id="mass-action-tags-displayed" class="toggle-checkbox" type="checkbox" />
|
||||
<div class="mass-action-tags">
|
||||
<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" type="submit" name="tag">{{ 'entry.list.add_tags'|trans }}</button>
|
||||
<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>
|
||||
|
||||
@ -78,7 +91,6 @@
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
{% if entries.getNbPages > 1 %}
|
||||
<div class="results">
|
||||
@ -118,9 +130,9 @@
|
||||
<h4 class="center">{{ 'entry.filters.title'|trans }}</h4>
|
||||
|
||||
<div class="row">
|
||||
{% if current_route != 'untagged' and nbEntriesUntagged != 0 %}
|
||||
{% if current_route != 'untagged' %}
|
||||
<div class="col s12 center-align">
|
||||
<a href="{{ path('untagged') }}">{{ 'tag.list.see_untagged_entries'|trans }} ({{ nbEntriesUntagged }})</a>
|
||||
<a href="{{ path('untagged') }}">{{ 'tag.list.see_untagged_entries'|trans }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
|
||||
{% block body_class %}entry{% endblock %}
|
||||
|
||||
{% set current_path = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %}
|
||||
|
||||
{% block menu %}
|
||||
<div class="progress">
|
||||
<div class="determinate"></div>
|
||||
@ -24,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}) }}" 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}) }}" 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>
|
||||
@ -54,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>
|
||||
|
||||
@ -67,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}) }}" 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}) }}" 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}) }}">
|
||||
<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>
|
||||
|
||||
@ -133,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') %}
|
||||
@ -298,9 +332,33 @@
|
||||
<i class="material-icons">menu</i>
|
||||
</a>
|
||||
<ul>
|
||||
<li><a class="btn-floating" href="{{ path('archive_entry', {'id': entry.id}) }}"><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}) }}"><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}) }}" 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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<form name="tag" method="post" action="{{ path('new_tag', {'entry': entry.id}) }}">
|
||||
<form class="tags-add-form" name="tag" method="post" action="{{ path('new_tag', {'entry': entry.id}) }}">
|
||||
{% if form_errors(form) %}
|
||||
<span class="black-text">{{ form_errors(form) }}</span>
|
||||
{% endif %}
|
||||
@ -9,6 +9,6 @@
|
||||
|
||||
{{ form_widget(form.label, {'attr': {'autocomplete': 'off'}}) }}
|
||||
|
||||
{{ form_widget(form.add, {'attr': {'class': 'btn waves-effect waves-light hide-on-large-only'}}) }}
|
||||
{{ form_widget(form.add, {'attr': {'class': 'btn waves-effect waves-light tags-add-form-submit'}}) }}
|
||||
{{ form_widget(form._token) }}
|
||||
</form>
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
{% block title %}{{ 'tag.page_title'|trans }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% set current_path = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %}
|
||||
|
||||
<div class="results clearfix">
|
||||
{{ 'tag.list.number_on_the_page'|trans({'%count%': tags|length}) }}
|
||||
</div>
|
||||
@ -18,7 +20,7 @@
|
||||
{{ tag.label }} ({{ tag.nbEntries }})
|
||||
</a>
|
||||
{% if renameForms is defined and renameForms[tag.id] is defined %}
|
||||
<form class="card-tag-form hidden" data-handle="tag-rename-form" action="{{ path('tag_rename', {'slug': tag.slug}) }}" method="POST">
|
||||
<form class="card-tag-form hidden" data-handle="tag-rename-form" action="{{ path('tag_rename', {'slug': tag.slug, redirect: current_path}) }}" method="POST">
|
||||
{{ form_widget(renameForms[tag.id].label, {'attr': {'value': tag.label}}) }}
|
||||
{{ form_rest(renameForms[tag.id]) }}
|
||||
</form>
|
||||
@ -26,9 +28,13 @@
|
||||
<i class="material-icons">mode_edit</i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a id="delete-{{ tag.slug }}" href="{{ path('tag_delete', {'slug': tag.slug}) }}" 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 %}
|
||||
|
||||
@ -168,7 +168,7 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col m12 l8">
|
||||
<p class="footer-text" title="{{ display_stats()|raw|striptags }}">
|
||||
<p class="footer-text">
|
||||
{{ display_stats() }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -4,6 +4,8 @@ namespace Wallabag\CoreBundle\Tools;
|
||||
|
||||
class Utils
|
||||
{
|
||||
public const DEFAULT_WORDS_PER_MINUTE = 200;
|
||||
|
||||
/**
|
||||
* Generate a token used for Feeds.
|
||||
*
|
||||
@ -28,6 +30,6 @@ class Utils
|
||||
*/
|
||||
public static function getReadingTime($text)
|
||||
{
|
||||
return floor(\count(preg_split('~([^\p{L}\p{N}\']+|(\p{Han}|\p{Hiragana}|\p{Katakana}|\p{Hangul}){1,2})~u', strip_tags($text))) / 200);
|
||||
return floor(\count(preg_split('~([^\p{L}\p{N}\']+|(\p{Han}|\p{Hiragana}|\p{Katakana}|\p{Hangul}){1,2})~u', strip_tags($text))) / self::DEFAULT_WORDS_PER_MINUTE);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,35 +91,29 @@ class WallabagExtension extends AbstractExtension implements GlobalsInterface
|
||||
|
||||
switch ($type) {
|
||||
case 'starred':
|
||||
$qb = $this->entryRepository->getBuilderForStarredByUser($user->getId());
|
||||
$qb = $this->entryRepository->getCountBuilderForStarredByUser($user->getId())->select('COUNT(e.id)');
|
||||
break;
|
||||
case 'archive':
|
||||
$qb = $this->entryRepository->getBuilderForArchiveByUser($user->getId());
|
||||
$qb = $this->entryRepository->getCountBuilderForArchiveByUser($user->getId())->select('COUNT(e.id)');
|
||||
break;
|
||||
case 'unread':
|
||||
$qb = $this->entryRepository->getBuilderForUnreadByUser($user->getId());
|
||||
$qb = $this->entryRepository->getCountBuilderForUnreadByUser($user->getId())->select('COUNT(e.id)');
|
||||
break;
|
||||
case 'annotated':
|
||||
$qb = $this->entryRepository->getBuilderForAnnotationsByUser($user->getId());
|
||||
$qb = $this->annotationRepository->getCountBuilderByUser($user->getId())->select('COUNT(DISTINCT e.entry)');
|
||||
break;
|
||||
case 'all':
|
||||
$qb = $this->entryRepository->getBuilderForAllByUser($user->getId());
|
||||
$qb = $this->entryRepository->getCountBuilderForAllByUser($user->getId())->select('COUNT(e.id)');
|
||||
break;
|
||||
default:
|
||||
throw new \InvalidArgumentException(sprintf('Type "%s" is not implemented.', $type));
|
||||
}
|
||||
|
||||
// THANKS to PostgreSQL we CAN'T make a DEAD SIMPLE count(e.id)
|
||||
// ERROR: column "e0_.id" must appear in the GROUP BY clause or be used in an aggregate function
|
||||
$query = $qb
|
||||
->select('e.id')
|
||||
->groupBy('e.id')
|
||||
->getQuery();
|
||||
|
||||
$query = $qb->getQuery();
|
||||
$query->useQueryCache(true);
|
||||
$query->enableResultCache($this->lifeTime);
|
||||
|
||||
return \count($query->getArrayResult());
|
||||
return $query->getSingleScalarResult();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -148,15 +145,14 @@ class WallabagExtension extends AbstractExtension implements GlobalsInterface
|
||||
return 0;
|
||||
}
|
||||
|
||||
$query = $this->entryRepository->getBuilderForArchiveByUser($user->getId())
|
||||
->select('e.id')
|
||||
->groupBy('e.id')
|
||||
$query = $this->entryRepository->getCountBuilderForArchiveByUser($user->getId())
|
||||
->select('COUNT(e.id)')
|
||||
->getQuery();
|
||||
|
||||
$query->useQueryCache(true);
|
||||
$query->enableResultCache($this->lifeTime);
|
||||
|
||||
$nbArchives = \count($query->getArrayResult());
|
||||
$nbArchives = $query->getSingleScalarResult();
|
||||
|
||||
$interval = $user->getCreatedAt()->diff(new \DateTime('now'));
|
||||
$nbDays = (int) $interval->format('%a') ?: 1;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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)
|
||||
;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
83
src/Wallabag/ImportBundle/Controller/HtmlController.php
Normal file
83
src/Wallabag/ImportBundle/Controller/HtmlController.php
Normal 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();
|
||||
}
|
||||
@ -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;
|
||||
|
||||
84
src/Wallabag/ImportBundle/Controller/OmnivoreController.php
Normal file
84
src/Wallabag/ImportBundle/Controller/OmnivoreController.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
57
src/Wallabag/ImportBundle/Controller/PocketCsvController.php
Normal file
57
src/Wallabag/ImportBundle/Controller/PocketCsvController.php
Normal 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';
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
}
|
||||
}
|
||||
57
src/Wallabag/ImportBundle/Controller/ShaarliController.php
Normal file
57
src/Wallabag/ImportBundle/Controller/ShaarliController.php
Normal 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';
|
||||
}
|
||||
}
|
||||
210
src/Wallabag/ImportBundle/Import/HtmlImport.php
Normal file
210
src/Wallabag/ImportBundle/Import/HtmlImport.php
Normal 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 = []);
|
||||
}
|
||||
158
src/Wallabag/ImportBundle/Import/OmnivoreImport.php
Normal file
158
src/Wallabag/ImportBundle/Import/OmnivoreImport.php
Normal 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;
|
||||
}
|
||||
}
|
||||
156
src/Wallabag/ImportBundle/Import/PocketCsvImport.php
Normal file
156
src/Wallabag/ImportBundle/Import/PocketCsvImport.php
Normal 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;
|
||||
}
|
||||
}
|
||||
113
src/Wallabag/ImportBundle/Import/PocketHtmlImport.php
Normal file
113
src/Wallabag/ImportBundle/Import/PocketHtmlImport.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
*/
|
||||
|
||||
66
src/Wallabag/ImportBundle/Import/ShaarliImport.php
Normal file
66
src/Wallabag/ImportBundle/Import/ShaarliImport.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
@ -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 %}
|
||||
@ -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 %}
|
||||
@ -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 %}
|
||||
@ -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>
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -190,6 +190,7 @@ class EntryRestControllerTest extends WallabagApiTestCase
|
||||
'tags' => 'foo',
|
||||
'since' => 1443274283,
|
||||
'public' => 0,
|
||||
'annotations' => 1,
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $this->client->getResponse()->getStatusCode());
|
||||
@ -217,6 +218,7 @@ class EntryRestControllerTest extends WallabagApiTestCase
|
||||
$this->assertStringContainsString('tags=foo', $content['_links'][$link]['href']);
|
||||
$this->assertStringContainsString('since=1443274283', $content['_links'][$link]['href']);
|
||||
$this->assertStringContainsString('public=0', $content['_links'][$link]['href']);
|
||||
$this->assertStringContainsString('annotations=1', $content['_links'][$link]['href']);
|
||||
}
|
||||
|
||||
$this->assertSame('application/json', $this->client->getResponse()->headers->get('Content-Type'));
|
||||
@ -1383,4 +1385,85 @@ class EntryRestControllerTest extends WallabagApiTestCase
|
||||
$this->assertGreaterThan(0, $content['id']);
|
||||
$this->assertSame('https://www.lemonde.fr/m-perso/article/2017/06/25/antoine-de-caunes-je-veux-avoir-le-droit-de-tatonner_5150728_4497916.html', $content['url']);
|
||||
}
|
||||
|
||||
public function testGetEntriesWithAnnotationsFilter()
|
||||
{
|
||||
// Test filter for entries WITH annotations
|
||||
// From fixtures: entry1, entry2 have annotations (for admin-user), entry3 has annotations (for bob-user)
|
||||
// entry4, entry5, entry6 don't have annotations
|
||||
$this->client->request('GET', '/api/entries', [
|
||||
'annotations' => 1,
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$content = json_decode($this->client->getResponse()->getContent(), true);
|
||||
|
||||
$this->assertArrayHasKey('items', $content['_embedded']);
|
||||
|
||||
// Check that only entries with annotations are returned
|
||||
$entriesWithAnnotations = ['http://0.0.0.0/entry1', 'http://0.0.0.0/entry2'];
|
||||
$entriesWithoutAnnotations = ['http://0.0.0.0/entry4', 'http://0.0.0.0/entry5', 'http://0.0.0.0/entry6'];
|
||||
|
||||
foreach ($content['_embedded']['items'] as $item) {
|
||||
if (\in_array($item['url'], $entriesWithAnnotations, true)) {
|
||||
$this->assertNotEmpty($item['annotations'], 'Entry with URL ' . $item['url'] . ' should have annotations');
|
||||
}
|
||||
$this->assertNotContains($item['url'], $entriesWithoutAnnotations, 'Entry without annotations should NOT be in the results');
|
||||
}
|
||||
|
||||
// Ensure we have at least the entries with annotations for admin-user
|
||||
$foundUrls = array_column($content['_embedded']['items'], 'url');
|
||||
$this->assertContains('http://0.0.0.0/entry1', $foundUrls, 'entry1 with annotations should be in the results');
|
||||
$this->assertContains('http://0.0.0.0/entry2', $foundUrls, 'entry2 with annotations should be in the results');
|
||||
|
||||
// Check pagination links contain the filter
|
||||
$this->assertArrayHasKey('_links', $content);
|
||||
foreach (['self', 'first', 'last'] as $link) {
|
||||
$this->assertArrayHasKey('href', $content['_links'][$link]);
|
||||
$this->assertStringContainsString('annotations=1', $content['_links'][$link]['href']);
|
||||
}
|
||||
|
||||
$this->assertSame('application/json', $this->client->getResponse()->headers->get('Content-Type'));
|
||||
}
|
||||
|
||||
public function testGetEntriesWithoutAnnotationsFilter()
|
||||
{
|
||||
// Test filter for entries WITHOUT annotations
|
||||
$this->client->request('GET', '/api/entries', [
|
||||
'annotations' => 0,
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$content = json_decode($this->client->getResponse()->getContent(), true);
|
||||
|
||||
$this->assertArrayHasKey('items', $content['_embedded']);
|
||||
|
||||
// Check that only entries without annotations are returned
|
||||
$entriesWithoutAnnotations = ['http://0.0.0.0/entry4', 'http://0.0.0.0/entry5', 'http://0.0.0.0/entry6'];
|
||||
$entriesWithAnnotations = ['http://0.0.0.0/entry1', 'http://0.0.0.0/entry2'];
|
||||
|
||||
foreach ($content['_embedded']['items'] as $item) {
|
||||
if (\in_array($item['url'], $entriesWithoutAnnotations, true)) {
|
||||
$this->assertEmpty($item['annotations'], 'Entry with URL ' . $item['url'] . ' should NOT have annotations');
|
||||
}
|
||||
$this->assertNotContains($item['url'], $entriesWithAnnotations, 'Entry with annotations should NOT be in the results');
|
||||
}
|
||||
|
||||
// Ensure we have the entries without annotations for admin-user
|
||||
$foundUrls = array_column($content['_embedded']['items'], 'url');
|
||||
$this->assertContains('http://0.0.0.0/entry4', $foundUrls, 'entry4 without annotations should be in the results');
|
||||
$this->assertContains('http://0.0.0.0/entry5', $foundUrls, 'entry5 without annotations should be in the results');
|
||||
$this->assertContains('http://0.0.0.0/entry6', $foundUrls, 'entry6 without annotations should be in the results');
|
||||
|
||||
// Check pagination links contain the filter
|
||||
$this->assertArrayHasKey('_links', $content);
|
||||
foreach (['self', 'first', 'last'] as $link) {
|
||||
$this->assertArrayHasKey('href', $content['_links'][$link]);
|
||||
$this->assertStringContainsString('annotations=0', $content['_links'][$link]['href']);
|
||||
}
|
||||
|
||||
$this->assertSame('application/json', $this->client->getResponse()->headers->get('Content-Type'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
@ -1174,14 +1175,13 @@ class ConfigControllerTest extends WallabagCoreTestCase
|
||||
$this->logInAs('admin');
|
||||
$client = $this->getTestClient();
|
||||
|
||||
$crawler = $client->request('GET', '/config/otp/email');
|
||||
$crawler = $client->request('GET', '/config');
|
||||
|
||||
$form = $crawler->filter('form[name=config_otp_email]')->form();
|
||||
$client->submit($form);
|
||||
|
||||
$this->assertSame(302, $client->getResponse()->getStatusCode());
|
||||
|
||||
$crawler = $client->followRedirect();
|
||||
|
||||
$this->assertGreaterThan(1, $alert = $crawler->filter('body')->extract(['_text']));
|
||||
$this->assertStringContainsString('flashes.config.notice.otp_enabled', $alert[0]);
|
||||
$this->assertStringContainsString('flashes.config.notice.otp_enabled', $client->getContainer()->get(SessionInterface::class)->getFlashBag()->get('notice')[0]);
|
||||
|
||||
// restore user
|
||||
$em = $this->getEntityManager();
|
||||
@ -1201,14 +1201,23 @@ class ConfigControllerTest extends WallabagCoreTestCase
|
||||
$this->logInAs('admin');
|
||||
$client = $this->getTestClient();
|
||||
|
||||
$crawler = $client->request('GET', '/config/otp/email/disable');
|
||||
$em = $this->getEntityManager();
|
||||
$user = $em
|
||||
->getRepository(User::class)
|
||||
->findOneByUsername('admin');
|
||||
|
||||
$user->setEmailTwoFactor(true);
|
||||
$em->persist($user);
|
||||
$em->flush();
|
||||
|
||||
$crawler = $client->request('GET', '/config');
|
||||
|
||||
$form = $crawler->filter('form[name=disable_otp_email]')->form();
|
||||
$client->submit($form);
|
||||
|
||||
$this->assertSame(302, $client->getResponse()->getStatusCode());
|
||||
|
||||
$crawler = $client->followRedirect();
|
||||
|
||||
$this->assertGreaterThan(1, $alert = $crawler->filter('body')->extract(['_text']));
|
||||
$this->assertStringContainsString('flashes.config.notice.otp_disabled', $alert[0]);
|
||||
$this->assertStringContainsString('flashes.config.notice.otp_disabled', $client->getContainer()->get(SessionInterface::class)->getFlashBag()->get('notice')[0]);
|
||||
|
||||
// restore user
|
||||
$em = $this->getEntityManager();
|
||||
@ -1224,7 +1233,10 @@ class ConfigControllerTest extends WallabagCoreTestCase
|
||||
$this->logInAs('admin');
|
||||
$client = $this->getTestClient();
|
||||
|
||||
$crawler = $client->request('GET', '/config/otp/app');
|
||||
$crawler = $client->request('GET', '/config');
|
||||
|
||||
$form = $crawler->filter('form[name=config_otp_app]')->form();
|
||||
$client->submit($form);
|
||||
|
||||
$this->assertSame(200, $client->getResponse()->getStatusCode());
|
||||
|
||||
@ -1243,49 +1255,28 @@ class ConfigControllerTest extends WallabagCoreTestCase
|
||||
$em->flush();
|
||||
}
|
||||
|
||||
public function testUserEnable2faGoogleCancel()
|
||||
{
|
||||
$this->logInAs('admin');
|
||||
$client = $this->getTestClient();
|
||||
|
||||
$crawler = $client->request('GET', '/config/otp/app');
|
||||
|
||||
$this->assertSame(200, $client->getResponse()->getStatusCode());
|
||||
|
||||
// restore user
|
||||
$em = $this->getEntityManager();
|
||||
$user = $em
|
||||
->getRepository(User::class)
|
||||
->findOneByUsername('admin');
|
||||
|
||||
$this->assertTrue($user->isGoogleTwoFactor());
|
||||
$this->assertGreaterThan(0, $user->getBackupCodes());
|
||||
|
||||
$crawler = $client->request('GET', '/config/otp/app/cancel');
|
||||
|
||||
$this->assertSame(302, $client->getResponse()->getStatusCode());
|
||||
|
||||
$user = $em
|
||||
->getRepository(User::class)
|
||||
->findOneByUsername('admin');
|
||||
|
||||
$this->assertFalse($user->isGoogleTwoFactor());
|
||||
$this->assertEmpty($user->getBackupCodes());
|
||||
}
|
||||
|
||||
public function testUserDisable2faGoogle()
|
||||
{
|
||||
$this->logInAs('admin');
|
||||
$client = $this->getTestClient();
|
||||
|
||||
$crawler = $client->request('GET', '/config/otp/app/disable');
|
||||
$em = $this->getEntityManager();
|
||||
$user = $em
|
||||
->getRepository(User::class)
|
||||
->findOneByUsername('admin');
|
||||
|
||||
$user->setGoogleAuthenticatorSecret('Google2FA');
|
||||
$em->persist($user);
|
||||
$em->flush();
|
||||
|
||||
$crawler = $client->request('GET', '/config');
|
||||
|
||||
$form = $crawler->filter('form[name=disable_otp_app]')->form();
|
||||
$client->submit($form);
|
||||
|
||||
$this->assertSame(302, $client->getResponse()->getStatusCode());
|
||||
|
||||
$crawler = $client->followRedirect();
|
||||
|
||||
$this->assertGreaterThan(1, $alert = $crawler->filter('body')->extract(['_text']));
|
||||
$this->assertStringContainsString('flashes.config.notice.otp_disabled', $alert[0]);
|
||||
$this->assertStringContainsString('flashes.config.notice.otp_disabled', $client->getContainer()->get(SessionInterface::class)->getFlashBag()->get('notice')[0]);
|
||||
|
||||
// restore user
|
||||
$em = $this->getEntityManager();
|
||||
@ -1392,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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'));
|
||||
@ -1302,8 +1322,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->assertStringContainsString('/view/' . $entry->getId(), $client->getResponse()->headers->get('location'));
|
||||
@ -1427,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');
|
||||
@ -1473,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 [
|
||||
@ -1497,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' => [
|
||||
@ -1640,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();
|
||||
@ -1651,12 +1705,9 @@ 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->assertSame(sprintf('/remove-tag/%s/%s', $entry->getId(), $tag->getId()), $link);
|
||||
$this->assertStringStartsWith(sprintf('/remove-tag/%s/%s', $entry->getId(), $tag->getId()), $link);
|
||||
}
|
||||
|
||||
public function testRandom()
|
||||
@ -1710,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,
|
||||
]);
|
||||
@ -1735,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,
|
||||
]);
|
||||
@ -1757,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,
|
||||
@ -1787,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()
|
||||
|
||||
@ -123,9 +123,11 @@ class TagControllerTest extends WallabagCoreTestCase
|
||||
$this->getEntityManager()->clear();
|
||||
|
||||
// We make a first request to set an history and test redirection after tag deletion
|
||||
$client->request('GET', '/view/' . $entry->getId());
|
||||
$crawler = $client->request('GET', '/view/' . $entry->getId());
|
||||
$entryUri = $client->getRequest()->getRequestUri();
|
||||
$client->request('GET', '/remove-tag/' . $entry->getId() . '/' . $tag->getId());
|
||||
|
||||
$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());
|
||||
@ -134,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)
|
||||
@ -167,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)
|
||||
@ -546,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()
|
||||
|
||||
@ -33,7 +33,7 @@ class RedirectTest extends TestCase
|
||||
$this->routerMock->expects($this->any())
|
||||
->method('generate')
|
||||
->with('homepage')
|
||||
->willReturn('homepage');
|
||||
->willReturn('/');
|
||||
|
||||
$this->user = new User();
|
||||
$this->user->setName('youpi');
|
||||
@ -59,18 +59,11 @@ class RedirectTest extends TestCase
|
||||
$this->redirect = new Redirect($this->routerMock, $tokenStorage);
|
||||
}
|
||||
|
||||
public function testRedirectToNullWithFallback()
|
||||
{
|
||||
$redirectUrl = $this->redirect->to(null, 'fallback');
|
||||
|
||||
$this->assertSame('fallback', $redirectUrl);
|
||||
}
|
||||
|
||||
public function testRedirectToNullWithoutFallback()
|
||||
public function testRedirectToNull()
|
||||
{
|
||||
$redirectUrl = $this->redirect->to(null);
|
||||
|
||||
$this->assertSame($this->routerMock->generate('homepage'), $redirectUrl);
|
||||
$this->assertSame('/', $redirectUrl);
|
||||
}
|
||||
|
||||
public function testRedirectToValidUrl()
|
||||
@ -80,6 +73,13 @@ class RedirectTest extends TestCase
|
||||
$this->assertSame('/unread/list', $redirectUrl);
|
||||
}
|
||||
|
||||
public function testRedirectToAbsoluteUrl()
|
||||
{
|
||||
$redirectUrl = $this->redirect->to('https://www.google.com/');
|
||||
|
||||
$this->assertSame('/', $redirectUrl);
|
||||
}
|
||||
|
||||
public function testWithNotLoggedUser()
|
||||
{
|
||||
$redirect = new Redirect($this->routerMock, new TokenStorage());
|
||||
@ -94,24 +94,24 @@ class RedirectTest extends TestCase
|
||||
|
||||
$redirectUrl = $this->redirect->to('/unread/list');
|
||||
|
||||
$this->assertSame($this->routerMock->generate('homepage'), $redirectUrl);
|
||||
$this->assertSame('/', $redirectUrl);
|
||||
}
|
||||
|
||||
public function testUserForRedirectWithIgnoreActionMarkAsRead()
|
||||
{
|
||||
$this->user->getConfig()->setActionMarkAsRead(Config::REDIRECT_TO_HOMEPAGE);
|
||||
|
||||
$redirectUrl = $this->redirect->to('/unread/list', '', true);
|
||||
$redirectUrl = $this->redirect->to('/unread/list', true);
|
||||
|
||||
$this->assertSame('/unread/list', $redirectUrl);
|
||||
}
|
||||
|
||||
public function testUserForRedirectNullWithFallbackWithIgnoreActionMarkAsRead()
|
||||
public function testUserForRedirectNullWithIgnoreActionMarkAsRead()
|
||||
{
|
||||
$this->user->getConfig()->setActionMarkAsRead(Config::REDIRECT_TO_HOMEPAGE);
|
||||
|
||||
$redirectUrl = $this->redirect->to(null, 'fallback', true);
|
||||
$redirectUrl = $this->redirect->to(null, true);
|
||||
|
||||
$this->assertSame('fallback', $redirectUrl);
|
||||
$this->assertSame('/', $redirectUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'));
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
168
tests/Wallabag/ImportBundle/Controller/ShaarliControllerTest.php
Normal file
168
tests/Wallabag/ImportBundle/Controller/ShaarliControllerTest.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
252
tests/Wallabag/ImportBundle/Import/PocketCsvImportTest.php
Normal file
252
tests/Wallabag/ImportBundle/Import/PocketCsvImportTest.php
Normal 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;
|
||||
}
|
||||
}
|
||||
254
tests/Wallabag/ImportBundle/Import/PocketHtmlImportTest.php
Normal file
254
tests/Wallabag/ImportBundle/Import/PocketHtmlImportTest.php
Normal 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;
|
||||
}
|
||||
}
|
||||
254
tests/Wallabag/ImportBundle/Import/ShaarliImportTest.php
Normal file
254
tests/Wallabag/ImportBundle/Import/ShaarliImportTest.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
343
tests/Wallabag/ImportBundle/fixtures/omnivore.json
Normal file
343
tests/Wallabag/ImportBundle/fixtures/omnivore.json
Normal 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 n’exclut 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 : l’enquête visant l’épouse de Pedro Sanchez élargie, ce dernier fait part de sa « tranquillité absolue »",
|
||||
"description": "Begoña Gomez fait l’objet d’une enquête pour corruption et trafic d’influence, ouverte après des plaintes déposées par deux associations réputées proches de l’extrê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 d’une jeune femme des suites d’une méningite, malgré des appels au SAMU et aux pompiers",
|
||||
"description": "Malgré deux appels aux secours, l’un au 15 et l’autre au 18, ce sont deux amis qui ont conduit la femme de 25 ans à une clinique montpelliéraine, avant qu’elle 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": "L’incidence de la maladie est en baisse de 8,3 % par rapport aux chiffres de 2015, mais reste loin de l’objectif initialement fixé de diviser par deux le nombre de malades d’ici à 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": "L’Union européenne adopte jusqu’à 35 % de surtaxes sur les voitures électriques importées de Chine",
|
||||
"description": "L’objectif 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": "L’ex-patron de la DGSE Bernard Bajolet sera jugé pour complicité de tentative d’extorsion",
|
||||
"description": "L’homme d’affaires Alain Duménil accuse le service de renseignement d’avoir fait usage de la contrainte pour lui réclamer de l’argent 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 d’appel de Paris confirme le procès pour viol du rappeur Naps",
|
||||
"description": "L’artiste est soupçonné d’avoir violé une jeune femme pendant son sommeil à l’automne 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 l’exécutif souhaite ne plus payer les trois premiers jours d’absence des agents publics, le fait qu’il prenne peu en compte l’amélioration de la qualité de vie au travail lui vaut de vives critiques, de la part des syndicats, mais aussi de personnalités ayant l’expé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é : l’Eglise 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 l’Eglise. 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 l’immigration illégale et réaffirme son soutien à la « souveraineté marocaine » au Sahara occidental",
|
||||
"description": "Le chef de l’Etat 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 l’indé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 : l’Etat condamné car trop lent à améliorer les conditions de détention",
|
||||
"description": "En 2020, le Conseil d’Etat avait exigé des mesures urgentes pour les droits des détenus au Camp-Est, mais l’administration a pris du retard et les travaux n’auront pas lieu avant 2028, selon l’Observatoire 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 l’Hexagone d’Ouest 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 s’illustrer.",
|
||||
"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. L’attaque 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 l’extrême droite progresse, sans enregistrer la percée qu’elle 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 d’Hugo 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 s’entremêlent",
|
||||
"description": "Le verdict du procès visant l’ancien président ne devrait être connu qu’aprè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"
|
||||
}
|
||||
]
|
||||
10
tests/Wallabag/ImportBundle/fixtures/pocket.csv
Normal file
10
tests/Wallabag/ImportBundle/fixtures/pocket.csv
Normal file
@ -0,0 +1,10 @@
|
||||
title,url,time_added,tags,status
|
||||
You Might Not Need jQuery,http://youmightnotneedjquery.com/,1600322788,,unread
|
||||
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
|
||||
"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
|
||||
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 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
|
||||
"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
|
||||
|
21
tests/Wallabag/ImportBundle/fixtures/ril_export.html
Normal file
21
tests/Wallabag/ImportBundle/fixtures/ril_export.html
Normal 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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user