mirror of
https://github.com/wallabag/wallabag.git
synced 2025-12-24 12:47:52 +01:00
Compare commits
300 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff5623baac | |||
| 39e84e74a4 | |||
| 092a11b21e | |||
| 980646c52c | |||
| bf85187304 | |||
| f044dddcc0 | |||
| 25282de2be | |||
| dfda2bfbb5 | |||
| fd11d7476d | |||
| 18f5d402b4 | |||
| 2de4d2464f | |||
| 9fce559cb7 | |||
| db2f5d4c5f | |||
| a74ebc65cf | |||
| c589ee9b3c | |||
| db465ace02 | |||
| d954899f5d | |||
| 188da933e0 | |||
| 0cae692681 | |||
| fad18335b1 | |||
| 80b98fbd68 | |||
| 04c2f0de18 | |||
| ed5912e272 | |||
| e229389fd1 | |||
| 7780af1d4b | |||
| e832f1e0be | |||
| 3855de88ff | |||
| da3eaed297 | |||
| 00b33e3855 | |||
| a5972d7983 | |||
| 820d1c69c0 | |||
| fc6a7da71c | |||
| 68aa2cdeaf | |||
| 507ef47bd8 | |||
| ec964ab724 | |||
| dbac4e8591 | |||
| 9ca9b8eb85 | |||
| 3015551b23 | |||
| 5b5584d9f9 | |||
| ebc46ef1fc | |||
| 19178c5a20 | |||
| fc6aafe302 | |||
| ffece8d19a | |||
| 764a79b0ed | |||
| 431d49eb8c | |||
| 4a5f769428 | |||
| 5a9abf8821 | |||
| 68fabb71f8 | |||
| afc3a9b088 | |||
| 70991255b4 | |||
| ca79f8c018 | |||
| d0d7f5ab5e | |||
| 97e922dddc | |||
| a55c4ebf32 | |||
| 5a2821b3ce | |||
| 0364a171bb | |||
| 913fb51707 | |||
| e97902412f | |||
| dfe370eaa3 | |||
| 9fa207bdc6 | |||
| c4a5806180 | |||
| 826ff3f71e | |||
| 486f943e9a | |||
| 2417471bcc | |||
| 41991b326d | |||
| 734bf415c9 | |||
| 470b4aa15b | |||
| f3458bdbd6 | |||
| 15ebd406d4 | |||
| e2d2ce66e7 | |||
| 0cf4a61344 | |||
| ffa9e370f2 | |||
| 7aef830815 | |||
| 9ea4c136d9 | |||
| 74fa98fee3 | |||
| c195dbbd6c | |||
| 2c904346af | |||
| 8f6a3d412c | |||
| 57c896ce81 | |||
| be6695e041 | |||
| d93756d298 | |||
| 5a9612a15e | |||
| 36100365c6 | |||
| 77dbf189ff | |||
| cd17f86c6d | |||
| 843fd97805 | |||
| 5afff609b3 | |||
| 60623246ae | |||
| fa107116cc | |||
| 0cfdddc2eb | |||
| 8b0108d686 | |||
| adc79d0578 | |||
| 5d498135ab | |||
| d287ed6b54 | |||
| e69b07314b | |||
| f3291f5797 | |||
| 7cbe1dafb3 | |||
| 0ed133f3d5 | |||
| bfa5a4a556 | |||
| a0cd522db2 | |||
| 926ff63a73 | |||
| 1145810345 | |||
| 5ac6deda69 | |||
| d8e936add0 | |||
| af374b8cb0 | |||
| 6ebffd0f63 | |||
| e8663003c5 | |||
| 255d71727c | |||
| 0fc117ec41 | |||
| aa06e8328e | |||
| 5240684be9 | |||
| 9ec351e8b6 | |||
| 6fab27f3ce | |||
| e4d69cafe4 | |||
| 34e51243d9 | |||
| d21ccc1e28 | |||
| 9bc026f343 | |||
| 752606941f | |||
| 728aa902bb | |||
| fde129e5c6 | |||
| 3b78bbae64 | |||
| 675d78f9c6 | |||
| a46fd5fc9f | |||
| a26a6bd3a5 | |||
| d021823bab | |||
| 26fbf050f1 | |||
| 8a670fe4fe | |||
| 775a23a6d0 | |||
| e096656e09 | |||
| b2a5e680d3 | |||
| 0a8c9ae64a | |||
| ee266ab12b | |||
| f006bf4324 | |||
| 735bd89cfd | |||
| 46f51c68a9 | |||
| 35c1060f9a | |||
| 5fc91cca06 | |||
| 6a1a7e5f26 | |||
| da0d504c10 | |||
| 25caadf301 | |||
| a083aaf8d3 | |||
| a00763209f | |||
| 2910fb6da4 | |||
| 1761a646ac | |||
| 51c68655fb | |||
| 094604a87c | |||
| 111e17e74d | |||
| 95736f9ada | |||
| 9a29118df9 | |||
| 2bbd1b2e6f | |||
| d640600a40 | |||
| 2feefbf221 | |||
| ed858ef2ac | |||
| 9901556382 | |||
| f4e3cde352 | |||
| f9f320c6dc | |||
| 49e8619c01 | |||
| 94846eb82b | |||
| ace1fa3251 | |||
| a71aeef135 | |||
| 5165ffa6a4 | |||
| a0037b1103 | |||
| 944461d67c | |||
| bec697e23b | |||
| 234ba7c05f | |||
| 049502c444 | |||
| 4c235ddb39 | |||
| af01b63bc4 | |||
| 827bc27e66 | |||
| d231645889 | |||
| efe9e4d291 | |||
| b3308ed8d0 | |||
| f1c9e4ca3a | |||
| 13ee2cf20c | |||
| 9c8d3f1ba0 | |||
| e59a807aa1 | |||
| a4221caec7 | |||
| 91b67ced99 | |||
| b21413cf9a | |||
| 203be88474 | |||
| a8bbc1cc88 | |||
| 750fff8370 | |||
| f46900ca98 | |||
| f06a826c6d | |||
| c7e5ba6dd0 | |||
| c0ecd38e76 | |||
| da588f6202 | |||
| 62ab325ad4 | |||
| c5d21025c4 | |||
| ff7c692da0 | |||
| bce6bb7f7d | |||
| d84c44aaf7 | |||
| d08c4fdaaa | |||
| 8ac80e934e | |||
| 4b04cd5746 | |||
| 820ff41363 | |||
| b49f49f8e6 | |||
| 5db058ff72 | |||
| ab2203ccb7 | |||
| 098c69e845 | |||
| b4ba6584aa | |||
| 120d9b42f9 | |||
| cddc90b7dd | |||
| fdc4d13db2 | |||
| d96d1bf0dc | |||
| 4a5302dd20 | |||
| bf62bf52f7 | |||
| eeeceb193f | |||
| 2dda155f95 | |||
| f042f37bfc | |||
| 9e399a61f4 | |||
| dbed27f8d8 | |||
| 137c8ab756 | |||
| 3ed7c7c37c | |||
| 476f3afec3 | |||
| 0fdffb0b96 | |||
| 2d7d16ee6c | |||
| 18615738c0 | |||
| 452362c17a | |||
| ec4d23b584 | |||
| f78c088304 | |||
| 444d20ba4b | |||
| f6cfae5ed7 | |||
| 340dfe7125 | |||
| faaa98c6d6 | |||
| e5c937dffc | |||
| c0414355aa | |||
| aaf4ab2d2a | |||
| 56983b01a8 | |||
| 6b7e9ec1df | |||
| f052f6f580 | |||
| 16a92c54a7 | |||
| 13b2752e8d | |||
| 634997c9b5 | |||
| ca608ece65 | |||
| 027042cc1a | |||
| 10727cd261 | |||
| 3a859d9fa0 | |||
| ca03eb7332 | |||
| de64ca1251 | |||
| 20f8c369c3 | |||
| c309d86c07 | |||
| e051771782 | |||
| c52bb0cc18 | |||
| b10100ebfd | |||
| 9663747719 | |||
| a606e463ca | |||
| 90c21fa1fb | |||
| f27b0fe405 | |||
| 1958b86777 | |||
| 47660ff6c5 | |||
| e42200ec17 | |||
| 49eea9f921 | |||
| d2e821d843 | |||
| bf66901e40 | |||
| 54ca4b67db | |||
| d012103142 | |||
| 2e134d81c4 | |||
| 22a2fc13a1 | |||
| 796b31c3b1 | |||
| 27e54e0634 | |||
| c93a677926 | |||
| 9313190303 | |||
| 2cf99296a1 | |||
| 9b663800fd | |||
| e30b5a856a | |||
| c6ff0bc691 | |||
| cf9243151a | |||
| e9a837a0ac | |||
| abef36fd2b | |||
| bdb297f2b4 | |||
| bbc20c2071 | |||
| 0fc6ae8cdb | |||
| 2f86dd9c73 | |||
| b1752b619d | |||
| 0e44035b67 | |||
| 741db06447 | |||
| 4482e52f3a | |||
| 2af48b8174 | |||
| a1aac10bd5 | |||
| 40aeeafea2 | |||
| e6abd04026 | |||
| 8ef6a14652 | |||
| 1ce5164e70 | |||
| a3b64611f8 | |||
| 981d6a47da | |||
| 4b338afa40 | |||
| 1c2190fd68 | |||
| 407dd48ed0 | |||
| 397ad455e6 | |||
| 3d22442f8c | |||
| c12239c4f8 | |||
| c99a733aff | |||
| 88c9df9b80 | |||
| cbcfa69c05 | |||
| 20578f0b8e | |||
| 18e1106f76 | |||
| 6ff00315d0 | |||
| 0f17a8cf8a | |||
| 5e4eae42a0 |
15
.github/dependabot.yml
vendored
15
.github/dependabot.yml
vendored
@ -7,6 +7,13 @@ updates:
|
||||
time: "04:00"
|
||||
timezone: Europe/Paris
|
||||
open-pull-requests-limit: 10
|
||||
groups:
|
||||
babel-dependencies:
|
||||
patterns:
|
||||
- "*babel*"
|
||||
fontsource-dependencies:
|
||||
patterns:
|
||||
- "*fontsource*"
|
||||
ignore:
|
||||
- dependency-name: materialize-css
|
||||
versions:
|
||||
@ -18,6 +25,10 @@ updates:
|
||||
time: "04:00"
|
||||
timezone: Europe/Paris
|
||||
open-pull-requests-limit: 10
|
||||
groups:
|
||||
symfony-dependencies:
|
||||
patterns:
|
||||
- "symfony/*"
|
||||
reviewers:
|
||||
- j0k3r
|
||||
- tcitworld
|
||||
@ -26,6 +37,10 @@ updates:
|
||||
- dependency-name: lcobucci/jwt
|
||||
versions:
|
||||
- ">= 4.2.0"
|
||||
# until we add support for Symfony 5+
|
||||
- dependency-name: symfony/*
|
||||
versions:
|
||||
- ">= 5.0.0"
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
|
||||
44
.github/workflows/assets.yml
vendored
44
.github/workflows/assets.yml
vendored
@ -1,44 +0,0 @@
|
||||
name: "Assets"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 2.*
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
js:
|
||||
name: "Building assets"
|
||||
runs-on: "ubuntu-20.04"
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
|
||||
- name: "Install Node"
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "16"
|
||||
|
||||
- name: "Install dependencies with Yarn"
|
||||
run: "yarn install"
|
||||
|
||||
- name: "Build dev assets"
|
||||
run: "yarn run build:dev"
|
||||
|
||||
- name: "Build prod assets"
|
||||
run: "yarn run build:prod"
|
||||
|
||||
- name: "Validate no change were created"
|
||||
run: |
|
||||
GITDIFF=`git diff`
|
||||
if [ "$GITDIFF" == "" ]; then
|
||||
exit 0
|
||||
else
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
||||
4
.github/workflows/coding-standards.yml
vendored
4
.github/workflows/coding-standards.yml
vendored
@ -5,7 +5,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 2.*
|
||||
- "2.**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
|
||||
- name: "Install PHP"
|
||||
uses: "shivammathur/setup-php@v2"
|
||||
|
||||
7
.github/workflows/continuous-integration.yml
vendored
7
.github/workflows/continuous-integration.yml
vendored
@ -5,7 +5,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 2.*
|
||||
- "2.**"
|
||||
|
||||
env:
|
||||
PGPASSWORD: wallabagrocks
|
||||
@ -33,6 +33,7 @@ jobs:
|
||||
- "8.0"
|
||||
- "8.1"
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
database:
|
||||
- "sqlite"
|
||||
- "mysql"
|
||||
@ -40,7 +41,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
@ -106,7 +107,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
|
||||
4
.github/workflows/translations.yml
vendored
4
.github/workflows/translations.yml
vendored
@ -5,7 +5,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 2.*
|
||||
- "2.**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
|
||||
- name: "Install PHP"
|
||||
uses: "shivammathur/setup-php@v2"
|
||||
|
||||
8
.github/workflows/upload-release-package.yml
vendored
8
.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"
|
||||
@ -29,6 +29,12 @@ jobs:
|
||||
env:
|
||||
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: "Install Node"
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "yarn"
|
||||
|
||||
- name: Create the package
|
||||
run: make release VERSION=${{ github.event.release.tag_name }}
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -51,6 +51,7 @@ bin
|
||||
app/Resources/build/
|
||||
!/src/Wallabag/CoreBundle/Resources/public
|
||||
/src/Wallabag/CoreBundle/Resources/public/*
|
||||
package-lock.json
|
||||
|
||||
# Test-generated files
|
||||
admin-export.json
|
||||
|
||||
38
CHANGELOG.md
38
CHANGELOG.md
@ -1,5 +1,43 @@
|
||||
# Changelog
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
26
app/DoctrineMigrations/Version20230728085538.php
Normal file
26
app/DoctrineMigrations/Version20230728085538.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\Migrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Wallabag\CoreBundle\Doctrine\WallabagMigration;
|
||||
|
||||
/**
|
||||
* Remove demonstration mode settings.
|
||||
*/
|
||||
final class Version20230728085538 extends WallabagMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DELETE FROM' . $this->getTable('internal_setting') . " WHERE name = 'demo_mode_enabled';");
|
||||
$this->addSql('DELETE FROM' . $this->getTable('internal_setting') . " WHERE name = 'demo_mode_username';");
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('INSERT INTO ' . $this->getTable('internal_setting') . " (name, value, section) VALUES ('demo_mode_enabled', '0', 'misc');");
|
||||
$this->addSql('INSERT INTO ' . $this->getTable('internal_setting') . " (name, value, section) VALUES ('demo_mode_username', 'wallabag', 'misc');");
|
||||
}
|
||||
}
|
||||
24
app/DoctrineMigrations/Version20230728091417.php
Normal file
24
app/DoctrineMigrations/Version20230728091417.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\Migrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Wallabag\CoreBundle\Doctrine\WallabagMigration;
|
||||
|
||||
/**
|
||||
* Remove mobi export.
|
||||
*/
|
||||
final class Version20230728091417 extends WallabagMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DELETE FROM' . $this->getTable('internal_setting') . " WHERE name = 'export_mobi';");
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('INSERT INTO ' . $this->getTable('internal_setting') . " (name, value, section) VALUES ('export_mobi', '1', 'export');");
|
||||
}
|
||||
}
|
||||
50
app/DoctrineMigrations/Version20230728093912.php
Normal file
50
app/DoctrineMigrations/Version20230728093912.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\Migrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Wallabag\CoreBundle\Doctrine\WallabagMigration;
|
||||
|
||||
/**
|
||||
* Add is_not_parsed field to entry table.
|
||||
*/
|
||||
final class Version20230728093912 extends WallabagMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$entryTable = $schema->getTable($this->getTable('entry'));
|
||||
|
||||
$this->skipIf($entryTable->hasColumn('is_not_parsed'), 'It seems that you already played this migration.');
|
||||
|
||||
$entryTable->addColumn('is_not_parsed', 'boolean', [
|
||||
'default' => 0,
|
||||
'notnull' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query to update entries where content is equal to `fetching_error_message`.
|
||||
*/
|
||||
public function postUp(Schema $schema): void
|
||||
{
|
||||
$entryTable = $schema->getTable($this->getTable('entry'));
|
||||
$this->skipIf(!$entryTable->hasColumn('is_not_parsed'), 'Unable to update is_not_parsed colum');
|
||||
|
||||
// Need to do a `LIKE` with a final percent to handle the new line character
|
||||
$this->connection->executeQuery(
|
||||
'UPDATE ' . $this->getTable('entry') . ' SET is_not_parsed = :isNotParsed WHERE content LIKE :content',
|
||||
[
|
||||
'isNotParsed' => true,
|
||||
'content' => str_replace("\n", '', addslashes($this->container->getParameter('wallabag_core.fetching_error_message'))) . '%',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$entryTable = $schema->getTable($this->getTable('entry'));
|
||||
$entryTable->dropColumn('is_not_parsed');
|
||||
}
|
||||
}
|
||||
47
app/DoctrineMigrations/Version20230729093853.php
Normal file
47
app/DoctrineMigrations/Version20230729093853.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\Migrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Wallabag\CoreBundle\Doctrine\WallabagMigration;
|
||||
|
||||
/**
|
||||
* Add custom_css column to config table.
|
||||
*/
|
||||
final class Version20230729093853 extends WallabagMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$configTable = $schema->getTable($this->getTable('config'));
|
||||
|
||||
$this->skipIf($configTable->hasColumn('custom_css'), 'It seems that you already played this migration.');
|
||||
|
||||
$configTable->addColumn('custom_css', 'text', [
|
||||
'notnull' => false,
|
||||
]);
|
||||
|
||||
$configTable->addColumn('font', 'text', [
|
||||
'notnull' => false,
|
||||
]);
|
||||
|
||||
$configTable->addColumn('fontsize', 'float', [
|
||||
'notnull' => false,
|
||||
]);
|
||||
|
||||
$configTable->addColumn('line_height', 'float', [
|
||||
'notnull' => false,
|
||||
]);
|
||||
|
||||
$configTable->addColumn('max_width', 'float', [
|
||||
'notnull' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$configTable = $schema->getTable($this->getTable('config'));
|
||||
$configTable->dropColumn('custom_css');
|
||||
}
|
||||
}
|
||||
@ -10,13 +10,18 @@ import 'mathjax/es5/tex-svg';
|
||||
/* Fonts */
|
||||
import 'material-design-icons-iconfont/dist/material-design-icons.css';
|
||||
import 'lato-font/css/lato-font.css';
|
||||
import 'open-dyslexic/open-dyslexic-regular.css';
|
||||
import '@fontsource/atkinson-hyperlegible';
|
||||
import '@fontsource/eb-garamond';
|
||||
import '@fontsource/montserrat';
|
||||
import '@fontsource/oswald';
|
||||
import './global.scss';
|
||||
|
||||
/* Shortcuts */
|
||||
import './js/shortcuts/entry';
|
||||
import './js/shortcuts/main';
|
||||
|
||||
/* Hightlight */
|
||||
/* Highlight */
|
||||
import './js/highlight';
|
||||
|
||||
import { savePercent, retrievePercent } from './js/tools';
|
||||
@ -26,7 +31,7 @@ import { savePercent, retrievePercent } from './js/tools';
|
||||
========================================================================== */
|
||||
|
||||
$(document).ready(() => {
|
||||
if ($('article').length) {
|
||||
if ($('#article').length) {
|
||||
const app = new annotator.App();
|
||||
|
||||
app.include(annotator.ui.main, {
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
Article
|
||||
========================================================================== */
|
||||
|
||||
#article {
|
||||
#article, #preview-article {
|
||||
font-size: 20px;
|
||||
margin: 0 auto;
|
||||
max-width: 45em;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -324,6 +324,12 @@ a.original:not(.waves-effect) {
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.settings .settings-range-label {
|
||||
position: absolute;
|
||||
top: -14px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.entries-row {
|
||||
display: grid;
|
||||
margin: 0.4rem 0 0;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,6 +70,7 @@ nav {
|
||||
|
||||
.input-field input {
|
||||
display: block;
|
||||
font-size: 1.2rem;
|
||||
line-height: inherit;
|
||||
height: 3rem;
|
||||
}
|
||||
@ -79,6 +80,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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -8,7 +8,9 @@ import 'materialize-css/dist/js/materialize';
|
||||
import '../_global/index';
|
||||
|
||||
/* Tools */
|
||||
import { initExport, initFilters, initRandom } from './js/tools';
|
||||
import {
|
||||
initExport, initFilters, initRandom, initPreviewText,
|
||||
} from './js/tools';
|
||||
|
||||
/* Import shortcuts */
|
||||
import './js/shortcuts/main';
|
||||
@ -177,6 +179,7 @@ $(document).ready(() => {
|
||||
initRandom();
|
||||
stickyNav();
|
||||
articleScroll();
|
||||
initPreviewText();
|
||||
|
||||
const toggleNav = (toShow, toFocus) => {
|
||||
$('.nav-panel-actions').hide(100);
|
||||
@ -199,6 +202,29 @@ $(document).ready(() => {
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#config_fontsize').on('input', () => {
|
||||
const value = $('#config_fontsize').val();
|
||||
const css = `${value}em`;
|
||||
$('#preview-content').css('font-size', css);
|
||||
});
|
||||
|
||||
$('#config_font').on('change', () => {
|
||||
const value = $('#config_font').val();
|
||||
$('#preview-content').css('font-family', value);
|
||||
});
|
||||
|
||||
$('#config_lineHeight').on('input', () => {
|
||||
const value = $('#config_lineHeight').val();
|
||||
const css = `${value}em`;
|
||||
$('#preview-content').css('line-height', css);
|
||||
});
|
||||
|
||||
$('#config_maxWidth').on('input', () => {
|
||||
const value = $('#config_maxWidth').val();
|
||||
const css = `${value}em`;
|
||||
$('#preview-article').css('max-width', css);
|
||||
});
|
||||
|
||||
const materialAddForm = $('.nav-panel-add');
|
||||
materialAddForm.on('submit', () => {
|
||||
materialAddForm.addClass('disabled');
|
||||
|
||||
@ -29,8 +29,25 @@ function initRandom() {
|
||||
}
|
||||
}
|
||||
|
||||
function initPreviewText() {
|
||||
// no display if preview_text not available
|
||||
if ($('div').is('#preview-article')) {
|
||||
const defaultFontFamily = $('#config_font').val();
|
||||
const defaultFontSize = $('#config_fontsize').val();
|
||||
const defaultLineHeight = $('#config_lineHeight').val();
|
||||
const defaultMaxWidth = $('#config_maxWidth').val();
|
||||
const previewContent = $('#preview-content');
|
||||
|
||||
previewContent.css('font-family', defaultFontFamily);
|
||||
previewContent.css('font-size', `${defaultFontSize}em`);
|
||||
previewContent.css('line-height', `${defaultLineHeight}em`);
|
||||
$('#preview-article').css('max-width', `${defaultMaxWidth}em`);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
initExport,
|
||||
initFilters,
|
||||
initRandom,
|
||||
initPreviewText,
|
||||
};
|
||||
|
||||
@ -29,6 +29,7 @@ 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
|
||||
fragments: ~
|
||||
http_method_override: true
|
||||
assets: ~
|
||||
@ -84,13 +85,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:
|
||||
@ -100,8 +96,6 @@ fos_rest:
|
||||
- 'application/pdf'
|
||||
epub:
|
||||
- 'application/epub+zip'
|
||||
mobi:
|
||||
- 'application/x-mobipocket-ebook'
|
||||
view_response_listener: 'force'
|
||||
formats:
|
||||
xml: true
|
||||
@ -110,15 +104,17 @@ fos_rest:
|
||||
csv: true
|
||||
pdf: true
|
||||
epub: true
|
||||
mobi: true
|
||||
failed_validation: HTTP_BAD_REQUEST
|
||||
routing_loader: false
|
||||
format_listener:
|
||||
enabled: true
|
||||
rules:
|
||||
- { path: "^/api/entries/([0-9]+)/export.(.*)", priorities: ['epub', 'mobi', 'pdf', 'txt', 'csv'], fallback_format: json, prefer_extension: false }
|
||||
- { path: "^/api/entries/([0-9]+)/export.(.*)", priorities: ['epub', '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:
|
||||
@ -278,6 +274,16 @@ 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
|
||||
consumers:
|
||||
import_pocket:
|
||||
connection: default
|
||||
@ -369,6 +375,24 @@ 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%"}
|
||||
|
||||
fos_js_routing:
|
||||
routes_to_expose:
|
||||
|
||||
@ -37,7 +37,7 @@ parameters:
|
||||
twofactor_sender: no-reply@wallabag.org
|
||||
|
||||
# fosuser stuff
|
||||
fosuser_registration: true
|
||||
fosuser_registration: false
|
||||
fosuser_confirmation: true
|
||||
|
||||
# how long the access token should live in seconds for the API
|
||||
|
||||
@ -5,5 +5,5 @@ parameters:
|
||||
opera: https://addons.opera.com/en/extensions/details/wallabagger/?display=en
|
||||
f_droid: https://f-droid.org/app/fr.gaulupeau.apps.InThePoche
|
||||
google_play: https://play.google.com/store/apps/details?id=fr.gaulupeau.apps.InThePoche
|
||||
ios: https://itunes.apple.com/app/wallabag-2/id1170800946?mt=8
|
||||
ios: https://apps.apple.com/app/wallabag-2/id1170800946?mt=8
|
||||
windows: https://www.microsoft.com/store/apps/wallabag/9nblggh11646
|
||||
|
||||
@ -8,3 +8,4 @@ parameters:
|
||||
test_database_path: "%env(TEST_DATABASE_PATH)%"
|
||||
env(TEST_DATABASE_PATH): "%kernel.project_dir%/data/db/wallabag_test.sqlite"
|
||||
test_database_charset: utf8
|
||||
fosuser_registration: true
|
||||
|
||||
@ -30,6 +30,7 @@ services:
|
||||
$senderName: "%scheb_two_factor.email.sender_name%"
|
||||
$storeArticleHeaders: '@=service(''craue_config'').get(''store_article_headers'')'
|
||||
$supportUrl: '@=service(''craue_config'').get(''wallabag_support_url'')'
|
||||
$fonts: '%wallabag_core.fonts%'
|
||||
|
||||
Wallabag\AnnotationBundle\:
|
||||
resource: '../../src/Wallabag/AnnotationBundle/*'
|
||||
@ -116,6 +117,16 @@ 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\:
|
||||
resource: '../../src/Wallabag/ImportBundle/*'
|
||||
exclude: '../../src/Wallabag/ImportBundle/{Consumer,Controller,Redis}'
|
||||
@ -172,6 +183,9 @@ services:
|
||||
MatomoTwigExtension\MatomoTwigExtension:
|
||||
public: false
|
||||
|
||||
ScssPhp\ScssPhp\Compiler:
|
||||
public: false
|
||||
|
||||
Wallabag\CoreBundle\Event\Listener\UserLocaleListener:
|
||||
tags:
|
||||
- { name: kernel.event_listener, event: security.interactive_login, method: onInteractiveLogin }
|
||||
@ -351,6 +365,14 @@ 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 }
|
||||
|
||||
# to factorize the proximity and bypass translation for prev & next
|
||||
pagerfanta.view.default_wallabag:
|
||||
class: Pagerfanta\View\OptionableView
|
||||
|
||||
@ -18,6 +18,8 @@ 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'
|
||||
$shaarliConsumer: '@old_sound_rabbit_mq.import_shaarli_consumer'
|
||||
$pocketHtmlConsumer: '@old_sound_rabbit_mq.import_pocket_html_consumer'
|
||||
|
||||
wallabag_import.consumer.amqp.pocket:
|
||||
class: Wallabag\ImportBundle\Consumer\AMQPEntryConsumer
|
||||
@ -68,3 +70,13 @@ 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'
|
||||
|
||||
@ -164,3 +164,35 @@ 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'
|
||||
|
||||
@ -8,3 +8,4 @@ parameters:
|
||||
test_database_path: ~
|
||||
env(TEST_DATABASE_PATH): ~
|
||||
test_database_charset: utf8mb4
|
||||
fosuser_registration: true
|
||||
|
||||
@ -8,3 +8,4 @@ parameters:
|
||||
test_database_path: ~
|
||||
env(TEST_DATABASE_PATH): ~
|
||||
test_database_charset: utf8
|
||||
fosuser_registration: true
|
||||
|
||||
@ -5,8 +5,9 @@ parameters:
|
||||
test_database_name: ~
|
||||
test_database_user: ~
|
||||
test_database_password: ~
|
||||
# Using an environnement variable in order to avoid the error "attempt to write a readonly database"
|
||||
# Using an environment variable in order to avoid the error "attempt to write a readonly database"
|
||||
# when the schema is dropped then recreate
|
||||
test_database_path: "%env(TEST_DATABASE_PATH)%"
|
||||
env(TEST_DATABASE_PATH): "%kernel.project_dir%/data/db/wallabag_test.sqlite"
|
||||
test_database_charset: utf8
|
||||
fosuser_registration: true
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
wallabag_core:
|
||||
version: 2.6.3
|
||||
version: 2.6.7
|
||||
paypal_url: "https://liberapay.com/wallabag/donate"
|
||||
languages:
|
||||
en: 'English'
|
||||
@ -85,10 +85,6 @@ wallabag_core:
|
||||
name: export_epub
|
||||
value: 1
|
||||
section: export
|
||||
-
|
||||
name: export_mobi
|
||||
value: 0
|
||||
section: export
|
||||
-
|
||||
name: export_pdf
|
||||
value: 1
|
||||
@ -129,14 +125,6 @@ wallabag_core:
|
||||
name: matomo_site_id
|
||||
value: 1
|
||||
section: analytics
|
||||
-
|
||||
name: demo_mode_enabled
|
||||
value: 0
|
||||
section: misc
|
||||
-
|
||||
name: demo_mode_username
|
||||
value: wallabag
|
||||
section: misc
|
||||
-
|
||||
name: download_images_enabled
|
||||
value: 0
|
||||
@ -165,7 +153,16 @@ wallabag_core:
|
||||
rule: host = "feeds.reuters.com"
|
||||
-
|
||||
rule: _all ~ "https?://www\.lemonde\.fr/tiny.*"
|
||||
fonts:
|
||||
- 'Sans-serif'
|
||||
- 'Serif'
|
||||
- 'Atkinson Hyperlegible'
|
||||
- 'EB Garamond'
|
||||
- 'Lato'
|
||||
- 'Montserrat'
|
||||
- 'OpenDyslexicRegular'
|
||||
- 'Oswald'
|
||||
|
||||
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']
|
||||
resource_dir: "%kernel.project_dir%/web/uploads/import"
|
||||
|
||||
@ -114,8 +114,9 @@
|
||||
"scheb/2fa-google-authenticator": "^5.13",
|
||||
"scheb/2fa-qr-code": "^5.13",
|
||||
"scheb/2fa-trusted-device": "^5.13",
|
||||
"scssphp/scssphp": "^1.11",
|
||||
"sensio/framework-extra-bundle": "^6.2",
|
||||
"sentry/sentry-symfony": "4.10.0",
|
||||
"sentry/sentry-symfony": "4.12.0",
|
||||
"stof/doctrine-extensions-bundle": "^1.2",
|
||||
"symfony/asset": "^4.4",
|
||||
"symfony/config": "^4.4",
|
||||
@ -129,6 +130,7 @@
|
||||
"symfony/finder": "^4.4",
|
||||
"symfony/form": "^4.4",
|
||||
"symfony/framework-bundle": "^4.4",
|
||||
"symfony/google-mailer": "^4.4",
|
||||
"symfony/http-foundation": "^4.4",
|
||||
"symfony/http-kernel": "^4.4",
|
||||
"symfony/mailer": "^4.4",
|
||||
@ -147,7 +149,6 @@
|
||||
"twig/extra-bundle": "^3.4",
|
||||
"twig/string-extra": "^3.4",
|
||||
"twig/twig": "^3.4.3",
|
||||
"wallabag/php-mobi": "~1.0",
|
||||
"wallabag/phpepub": "^4.0.10",
|
||||
"willdurand/hateoas": "^3.8",
|
||||
"willdurand/hateoas-bundle": "~2.1"
|
||||
@ -209,7 +210,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": [
|
||||
|
||||
865
composer.lock
generated
865
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -25,7 +25,7 @@ parameters:
|
||||
twofactor_sender: ${TWOFACTOR_SENDER:-no-reply@wallabag.org}
|
||||
|
||||
# fosuser stuff
|
||||
fosuser_registration: ${FOSUSER_REGISTRATION:-true}
|
||||
fosuser_registration: ${FOSUSER_REGISTRATION:-false}
|
||||
fosuser_confirmation: ${FOSUSER_CONFIRMATION:-true}
|
||||
|
||||
fos_oauth_server_access_token_lifetime: 3600
|
||||
|
||||
41
package.json
41
package.json
@ -7,7 +7,7 @@
|
||||
"doc": "docs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
"node": ">=18"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -41,53 +41,56 @@
|
||||
"url": "https://github.com/wallabag/wallabag/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
"@babel/eslint-parser": "^7.22.10",
|
||||
"@babel/preset-env": "^7.22.10",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"@babel/core": "^7.23.3",
|
||||
"@babel/eslint-parser": "^7.23.3",
|
||||
"@babel/preset-env": "^7.23.3",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"babel-loader": "^9.1.3",
|
||||
"css-loader": "^6.8.1",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-webpack-plugin": "^4.0.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"lato-font": "^3.0.0",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"node-sass": "^9.0.0",
|
||||
"postcss": "^8.4.28",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss-loader": "^7.3.3",
|
||||
"postcss-scss": "^4.0.7",
|
||||
"sass": "^1.66.1",
|
||||
"postcss-scss": "^4.0.9",
|
||||
"sass": "^1.69.5",
|
||||
"sass-loader": "^13.3.2",
|
||||
"style-loader": "^3.3.3",
|
||||
"stylelint": "^15.10.3",
|
||||
"stylelint": "^15.11.0",
|
||||
"stylelint-config-standard": "^34.0.0",
|
||||
"stylelint-config-standard-scss": "^10.0.0",
|
||||
"stylelint-scss": "^5.1.0",
|
||||
"stylelint-config-standard-scss": "^11.1.0",
|
||||
"stylelint-scss": "^5.3.1",
|
||||
"stylelint-webpack-plugin": "^4.1.1",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.1",
|
||||
"webpack-manifest-plugin": "^5.0.0",
|
||||
"webpack-merge": "^5.9.0"
|
||||
"webpack-merge": "^5.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/atkinson-hyperlegible": "^5.0.17",
|
||||
"@fontsource/eb-garamond": "^5.0.15",
|
||||
"@fontsource/montserrat": "^5.0.15",
|
||||
"@fontsource/oswald": "^5.0.17",
|
||||
"annotator": "wallabag/annotator#master",
|
||||
"clipboard": "^2.0.11",
|
||||
"hammerjs": "^2.0.8",
|
||||
"highlight.js": "^11.8.0",
|
||||
"highlight.js": "^11.9.0",
|
||||
"icomoon-free-npm": "^0.0.0",
|
||||
"jquery": "^3.7.0",
|
||||
"jquery": "^3.7.1",
|
||||
"jquery.cookie": "^1.4.1",
|
||||
"jr-qrcode": "^1.0.7",
|
||||
"material-design-icons-iconfont": "^6.7.0",
|
||||
"materialize-css": "^0.100.2",
|
||||
"mathjax": "^3.2.2",
|
||||
"mousetrap": "^1.6.0",
|
||||
"ptsans-npm-webfont": "^0.0.4",
|
||||
"open-dyslexic": "^1.0.3",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"waypoints": "^4.0.1"
|
||||
},
|
||||
|
||||
@ -1,10 +1,25 @@
|
||||
parameters:
|
||||
ignoreErrors:
|
||||
-
|
||||
message: "#^Method Wallabag\\\\AnnotationBundle\\\\Controller\\\\WallabagAnnotationController\\:\\:postAnnotationAction\\(\\) should return Symfony\\\\Component\\\\HttpFoundation\\\\JsonResponse but returns Symfony\\\\Component\\\\Form\\\\FormInterface\\<mixed\\>\\.$#"
|
||||
count: 1
|
||||
path: src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php
|
||||
|
||||
-
|
||||
message: "#^Method Wallabag\\\\AnnotationBundle\\\\Controller\\\\WallabagAnnotationController\\:\\:putAnnotationAction\\(\\) should return Symfony\\\\Component\\\\HttpFoundation\\\\JsonResponse but returns Symfony\\\\Component\\\\Form\\\\FormInterface\\<null\\>\\.$#"
|
||||
count: 1
|
||||
path: src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php
|
||||
|
||||
-
|
||||
message: "#^Call to an undefined method Wallabag\\\\CoreBundle\\\\Entity\\\\RuleInterface\\:\\:getConfig\\(\\)\\.$#"
|
||||
count: 1
|
||||
path: src/Wallabag/CoreBundle/Controller/ConfigController.php
|
||||
|
||||
-
|
||||
message: "#^Method FOS\\\\UserBundle\\\\Model\\\\UserManagerInterface\\:\\:updateUser\\(\\) invoked with 2 parameters, 1 required\\.$#"
|
||||
count: 6
|
||||
path: src/Wallabag/CoreBundle/Controller/ConfigController.php
|
||||
|
||||
-
|
||||
message: "#^Call to an undefined method Lexik\\\\Bundle\\\\FormFilterBundle\\\\Filter\\\\Query\\\\QueryInterface\\:\\:getExpressionBuilder\\(\\)\\.$#"
|
||||
count: 1
|
||||
@ -40,27 +55,27 @@ parameters:
|
||||
count: 2
|
||||
path: src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php
|
||||
|
||||
-
|
||||
message: "#^PHPDoc type Symfony\\\\Component\\\\Mailer\\\\MailerInterface of property Wallabag\\\\UserBundle\\\\Mailer\\\\UserMailer\\:\\:\\$mailer is not covariant with PHPDoc type Swift_Mailer of overridden property FOS\\\\UserBundle\\\\Mailer\\\\TwigSwiftMailer\\:\\:\\$mailer\\.$#"
|
||||
count: 1
|
||||
path: src/Wallabag/UserBundle/Mailer/UserMailer.php
|
||||
|
||||
-
|
||||
message: "#^Call to an undefined method DOMNode\\:\\:getAttribute\\(\\)\\.$#"
|
||||
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: "#^Call to an undefined method Wallabag\\\\ImportBundle\\\\Import\\\\ImportInterface\\:\\:setUser\\(\\)\\.$#"
|
||||
count: 1
|
||||
path: src/Wallabag/ImportBundle/Controller/HtmlController.php
|
||||
|
||||
-
|
||||
message: "#^Property Tests\\\\Wallabag\\\\CoreBundle\\\\Helper\\\\RedirectTest\\:\\:\\$routerMock has unknown class PHPUnit_Framework_MockObject_MockObject as its type\\.$#"
|
||||
message: "#^Call to an undefined method Wallabag\\\\ImportBundle\\\\Import\\\\ImportInterface\\:\\:setFilepath\\(\\)\\.$#"
|
||||
count: 1
|
||||
path: tests/Wallabag/CoreBundle/Helper/RedirectTest.php
|
||||
path: src/Wallabag/ImportBundle/Controller/HtmlController.php
|
||||
|
||||
-
|
||||
message: "#^Method Symfony\\\\Contracts\\\\EventDispatcher\\\\EventDispatcherInterface\\:\\:dispatch()#"
|
||||
count: 15
|
||||
count: 16
|
||||
path: src/*
|
||||
|
||||
-
|
||||
message: "#^Method FOS\\\\UserBundle\\\\Model\\\\UserManagerInterface\\:\\:updateUser()#"
|
||||
count: 7
|
||||
path: src/Wallabag/CoreBundle/Controller/ConfigController.php
|
||||
|
||||
@ -2,7 +2,7 @@ includes:
|
||||
- phpstan-baseline.neon
|
||||
|
||||
parameters:
|
||||
level: 2
|
||||
level: 3
|
||||
paths:
|
||||
- src
|
||||
- tests
|
||||
|
||||
@ -10,6 +10,8 @@ ENV=$4
|
||||
rm -rf "${TMP_FOLDER:?}"/"$RELEASE_FOLDER"
|
||||
mkdir "$TMP_FOLDER"/"$RELEASE_FOLDER"
|
||||
git clone https://github.com/wallabag/wallabag.git --single-branch --depth 1 --branch $1 "$TMP_FOLDER"/"$RELEASE_FOLDER"/"$VERSION"
|
||||
cd "$TMP_FOLDER"/"$RELEASE_FOLDER"/"$VERSION" && yarn install --non-interactive
|
||||
cd "$TMP_FOLDER"/"$RELEASE_FOLDER"/"$VERSION" && yarn run --non-interactive build:prod
|
||||
cd "$TMP_FOLDER"/"$RELEASE_FOLDER"/"$VERSION" && SYMFONY_ENV="$ENV" COMPOSER_MEMORY_LIMIT=-1 composer install -n --no-dev
|
||||
cd "$TMP_FOLDER"/"$RELEASE_FOLDER"/"$VERSION" && php bin/console wallabag:install --env="$ENV" -n
|
||||
cd "$TMP_FOLDER"/"$RELEASE_FOLDER"/"$VERSION" && php bin/console assets:install --env="$ENV" --symlink --relative
|
||||
|
||||
@ -123,7 +123,7 @@ class AnnotationRepository extends ServiceEntityRepository
|
||||
|
||||
/**
|
||||
* Remove all annotations for a user id.
|
||||
* Used when a user want to reset all informations.
|
||||
* Used when a user wants to reset all information.
|
||||
*
|
||||
* @param int $userId
|
||||
*/
|
||||
|
||||
@ -4,8 +4,8 @@ namespace Wallabag\ApiBundle\Controller;
|
||||
|
||||
use Nelmio\ApiDocBundle\Annotation\Operation;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Wallabag\AnnotationBundle\Entity\Annotation;
|
||||
use Wallabag\CoreBundle\Entity\Entry;
|
||||
@ -36,7 +36,7 @@ class AnnotationRestController extends WallabagRestController
|
||||
*
|
||||
* @Route("/api/annotations/{entry}.{_format}", methods={"GET"}, name="api_get_annotations", defaults={"_format": "json"})
|
||||
*
|
||||
* @return JsonResponse
|
||||
* @return Response
|
||||
*/
|
||||
public function getAnnotationsAction(Entry $entry)
|
||||
{
|
||||
@ -102,7 +102,7 @@ class AnnotationRestController extends WallabagRestController
|
||||
*
|
||||
* @Route("/api/annotations/{entry}.{_format}", methods={"POST"}, name="api_post_annotation", defaults={"_format": "json"})
|
||||
*
|
||||
* @return JsonResponse
|
||||
* @return Response
|
||||
*/
|
||||
public function postAnnotationAction(Request $request, Entry $entry)
|
||||
{
|
||||
@ -138,7 +138,7 @@ class AnnotationRestController extends WallabagRestController
|
||||
*
|
||||
* @Route("/api/annotations/{annotation}.{_format}", methods={"PUT"}, name="api_put_annotation", defaults={"_format": "json"})
|
||||
*
|
||||
* @return JsonResponse
|
||||
* @return Response
|
||||
*/
|
||||
public function putAnnotationAction(int $annotation, Request $request)
|
||||
{
|
||||
@ -174,7 +174,7 @@ class AnnotationRestController extends WallabagRestController
|
||||
*
|
||||
* @Route("/api/annotations/{annotation}.{_format}", methods={"DELETE"}, name="api_delete_annotation", defaults={"_format": "json"})
|
||||
*
|
||||
* @return JsonResponse
|
||||
* @return Response
|
||||
*/
|
||||
public function deleteAnnotationAction(int $annotation)
|
||||
{
|
||||
|
||||
@ -176,6 +176,17 @@ class EntryRestController extends WallabagRestController
|
||||
* )
|
||||
* ),
|
||||
* @OA\Parameter(
|
||||
* name="notParsed",
|
||||
* in="query",
|
||||
* description="filter by notParsed status. all entries by default",
|
||||
* required=false,
|
||||
* @OA\Schema(
|
||||
* type="integer",
|
||||
* enum={"1", "0"},
|
||||
* default="0"
|
||||
* )
|
||||
* ),
|
||||
* @OA\Parameter(
|
||||
* name="sort",
|
||||
* in="query",
|
||||
* description="sort entries by date.",
|
||||
@ -286,6 +297,7 @@ class EntryRestController extends WallabagRestController
|
||||
$isArchived = (null === $request->query->get('archive')) ? null : (bool) $request->query->get('archive');
|
||||
$isStarred = (null === $request->query->get('starred')) ? null : (bool) $request->query->get('starred');
|
||||
$isPublic = (null === $request->query->get('public')) ? null : (bool) $request->query->get('public');
|
||||
$isNotParsed = (null === $request->query->get('notParsed')) ? null : (bool) $request->query->get('notParsed');
|
||||
$sort = strtolower($request->query->get('sort', 'created'));
|
||||
$order = strtolower($request->query->get('order', 'desc'));
|
||||
$page = (int) $request->query->get('page', 1);
|
||||
@ -307,7 +319,8 @@ class EntryRestController extends WallabagRestController
|
||||
$since,
|
||||
$tags,
|
||||
$detail,
|
||||
$domainName
|
||||
$domainName,
|
||||
$isNotParsed
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
throw new BadRequestHttpException($e->getMessage());
|
||||
@ -325,6 +338,7 @@ class EntryRestController extends WallabagRestController
|
||||
'archive' => $isArchived,
|
||||
'starred' => $isStarred,
|
||||
'public' => $isPublic,
|
||||
'notParsed' => $isNotParsed,
|
||||
'sort' => $sort,
|
||||
'order' => $order,
|
||||
'page' => $page,
|
||||
@ -397,7 +411,7 @@ class EntryRestController extends WallabagRestController
|
||||
* required=true,
|
||||
* @OA\Schema(
|
||||
* type="string",
|
||||
* enum={"xml", "json", "txt", "csv", "pdf", "epub", "mobi"},
|
||||
* enum={"xml", "json", "txt", "csv", "pdf", "epub"},
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
|
||||
@ -22,11 +22,11 @@ use Wallabag\UserBundle\Form\NewUserType;
|
||||
class UserRestController extends WallabagRestController
|
||||
{
|
||||
/**
|
||||
* Retrieve current logged in user informations.
|
||||
* Retrieve current logged in user information.
|
||||
*
|
||||
* @Operation(
|
||||
* tags={"User"},
|
||||
* summary="Retrieve current logged in user informations.",
|
||||
* summary="Retrieve current logged in user information.",
|
||||
* @OA\Response(
|
||||
* response="200",
|
||||
* description="Returned when successful",
|
||||
|
||||
@ -115,7 +115,7 @@ class InstallCommand extends Command
|
||||
|
||||
$rows[] = [sprintf($label, $this->databaseDriver), $status, $help];
|
||||
|
||||
// testing if connection to the database can be etablished
|
||||
// testing if connection to the database can be established
|
||||
$label = '<comment>Database connection</comment>';
|
||||
$status = '<info>OK!</info>';
|
||||
$help = '';
|
||||
@ -415,7 +415,7 @@ class InstallCommand extends Command
|
||||
|
||||
/**
|
||||
* Check if the schema is already created.
|
||||
* If we found at least oen table, it means the schema exists.
|
||||
* If we found at least one table, it means the schema exists.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
@ -7,6 +7,7 @@ use Doctrine\ORM\NoResultException;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
@ -41,13 +42,19 @@ class ReloadEntryCommand extends Command
|
||||
->setDescription('Reload entries')
|
||||
->setHelp('This command reload entries')
|
||||
->addArgument('username', InputArgument::OPTIONAL, 'Reload entries only for the given user')
|
||||
;
|
||||
->addOption(
|
||||
'only-not-parsed',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Only reload entries which have `is_not_parsed` set to `true`'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$onlyNotParsed = (bool) $input->getOption('only-not-parsed');
|
||||
$userId = null;
|
||||
if ($username = $input->getArgument('username')) {
|
||||
try {
|
||||
@ -61,7 +68,8 @@ class ReloadEntryCommand extends Command
|
||||
}
|
||||
}
|
||||
|
||||
$entryIds = $this->entryRepository->findAllEntriesIdByUserId($userId);
|
||||
$methodName = $onlyNotParsed ? 'findAllEntriesIdByUserIdAndNotParsed' : 'findAllEntriesIdByUserId';
|
||||
$entryIds = $this->entryRepository->$methodName($userId);
|
||||
|
||||
$nbEntries = \count($entryIds);
|
||||
if (!$nbEntries) {
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Wallabag\CoreBundle\Command;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Wallabag\CoreBundle\Repository\EntryRepository;
|
||||
|
||||
class UpdatePicturesPathCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
private EntryRepository $entryRepository;
|
||||
private string $wallabagUrl;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, EntryRepository $entryRepository, $wallabagUrl)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->entryRepository = $entryRepository;
|
||||
$this->wallabagUrl = $wallabagUrl;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('wallabag:update-pictures-path')
|
||||
->setDescription('Update the path of the pictures for each entry when you changed your wallabag instance URL.')
|
||||
->addArgument(
|
||||
'old-url',
|
||||
InputArgument::REQUIRED,
|
||||
'URL to replace'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$oldUrl = $input->getArgument('old-url');
|
||||
|
||||
$query = $this->entryRepository->createQueryBuilder('e')->getQuery();
|
||||
$io->text('Retrieve existing entries');
|
||||
$i = 1;
|
||||
foreach ($query->toIterable() as $entry) {
|
||||
$content = $entry->getContent();
|
||||
if (null !== $content) {
|
||||
$entry->setContent(str_replace($oldUrl, $this->wallabagUrl, $content));
|
||||
}
|
||||
|
||||
$previewPicture = $entry->getPreviewPicture();
|
||||
if (null !== $previewPicture) {
|
||||
$entry->setPreviewPicture(str_replace($oldUrl, $this->wallabagUrl, $previewPicture));
|
||||
}
|
||||
|
||||
if (0 === ($i % 20)) {
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
++$i;
|
||||
}
|
||||
$this->entityManager->flush();
|
||||
|
||||
$io->success('Finished updating.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@ use JMS\Serializer\SerializerBuilder;
|
||||
use PragmaRX\Recovery\Recovery as BackupCodes;
|
||||
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@ -25,6 +26,7 @@ use Wallabag\CoreBundle\Entity\Config as ConfigEntity;
|
||||
use Wallabag\CoreBundle\Entity\IgnoreOriginUserRule;
|
||||
use Wallabag\CoreBundle\Entity\RuleInterface;
|
||||
use Wallabag\CoreBundle\Entity\TaggingRule;
|
||||
use Wallabag\CoreBundle\Event\ConfigUpdatedEvent;
|
||||
use Wallabag\CoreBundle\Form\Type\ChangePasswordType;
|
||||
use Wallabag\CoreBundle\Form\Type\ConfigType;
|
||||
use Wallabag\CoreBundle\Form\Type\FeedType;
|
||||
@ -48,15 +50,24 @@ class ConfigController extends AbstractController
|
||||
private TagRepository $tagRepository;
|
||||
private AnnotationRepository $annotationRepository;
|
||||
private ConfigRepository $configRepository;
|
||||
private EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
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,
|
||||
EventDispatcherInterface $eventDispatcher
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->userManager = $userManager;
|
||||
$this->entryRepository = $entryRepository;
|
||||
$this->tagRepository = $tagRepository;
|
||||
$this->annotationRepository = $annotationRepository;
|
||||
$this->configRepository = $configRepository;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -72,6 +83,7 @@ class ConfigController extends AbstractController
|
||||
$configForm->handleRequest($request);
|
||||
|
||||
if ($configForm->isSubmitted() && $configForm->isValid()) {
|
||||
$this->eventDispatcher->dispatch(new ConfigUpdatedEvent($config), ConfigUpdatedEvent::NAME);
|
||||
$this->entityManager->persist($config);
|
||||
$this->entityManager->flush();
|
||||
|
||||
@ -90,14 +102,10 @@ class ConfigController extends AbstractController
|
||||
$pwdForm->handleRequest($request);
|
||||
|
||||
if ($pwdForm->isSubmitted() && $pwdForm->isValid()) {
|
||||
if ($craueConfig->get('demo_mode_enabled') && $craueConfig->get('demo_mode_username') === $user->getUsername()) {
|
||||
$message = 'flashes.config.notice.password_not_updated_demo';
|
||||
} else {
|
||||
$message = 'flashes.config.notice.password_updated';
|
||||
$message = 'flashes.config.notice.password_updated';
|
||||
|
||||
$user->setPlainPassword($pwdForm->get('new_password')->getData());
|
||||
$this->userManager->updateUser($user, true);
|
||||
}
|
||||
$user->setPlainPassword($pwdForm->get('new_password')->getData());
|
||||
$this->userManager->updateUser($user, true);
|
||||
|
||||
$this->addFlash('notice', $message);
|
||||
|
||||
@ -254,10 +262,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 $this->createAccessDeniedException('Bad CSRF token.');
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
$user->setEmailTwoFactor(false);
|
||||
|
||||
@ -274,10 +286,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 $this->createAccessDeniedException('Bad CSRF token.');
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
|
||||
$user->setGoogleAuthenticatorSecret(null);
|
||||
@ -297,10 +313,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 $this->createAccessDeniedException('Bad CSRF token.');
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
|
||||
$user->setGoogleAuthenticatorSecret('');
|
||||
@ -319,10 +339,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 $this->createAccessDeniedException('Bad CSRF token.');
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
$secret = $googleAuthenticator->generateSecret();
|
||||
|
||||
@ -357,8 +381,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,15 +393,19 @@ 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 $this->createAccessDeniedException('Bad CSRF token.');
|
||||
}
|
||||
|
||||
$isValid = $googleAuthenticator->checkCode(
|
||||
$this->getUser(),
|
||||
$request->get('_auth_code')
|
||||
@ -395,7 +425,12 @@ class ConfigController extends AbstractController
|
||||
'scheb_two_factor.code_invalid'
|
||||
);
|
||||
|
||||
return $this->redirect($this->generateUrl('config_otp_app'));
|
||||
$this->addFlash(
|
||||
'notice',
|
||||
'scheb_two_factor.code_invalid'
|
||||
);
|
||||
|
||||
return $this->redirect($this->generateUrl('config') . '#set3');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -518,7 +518,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->getSession()->get('prevUrl', '');
|
||||
$to = (1 !== preg_match('#' . $url . '$#i', $prev) ? $prev : null);
|
||||
|
||||
$redirectUrl = $this->redirectHelper->to($to);
|
||||
@ -615,7 +615,7 @@ class EntryController extends AbstractController
|
||||
*/
|
||||
private function showEntries($type, Request $request, $page)
|
||||
{
|
||||
$searchTerm = (isset($request->get('search_entry')['term']) ? $request->get('search_entry')['term'] : '');
|
||||
$searchTerm = (isset($request->get('search_entry')['term']) ? trim($request->get('search_entry')['term']) : '');
|
||||
$currentRoute = (null !== $request->query->get('currentRoute') ? $request->query->get('currentRoute') : '');
|
||||
$request->getSession()->set('prevUrl', $request->getRequestUri());
|
||||
|
||||
@ -675,9 +675,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 +682,6 @@ class EntryController extends AbstractController
|
||||
'currentPage' => $page,
|
||||
'searchTerm' => $searchTerm,
|
||||
'isFiltered' => $form->isSubmitted(),
|
||||
'nbEntriesUntagged' => $nbEntriesUntagged,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ class ExportController extends AbstractController
|
||||
* Gets one entry content.
|
||||
*
|
||||
* @Route("/export/{id}.{format}", name="export_entry", requirements={
|
||||
* "format": "epub|mobi|pdf|json|xml|txt|csv",
|
||||
* "format": "epub|pdf|json|xml|txt|csv",
|
||||
* "id": "\d+"
|
||||
* })
|
||||
*
|
||||
@ -55,7 +55,7 @@ class ExportController extends AbstractController
|
||||
* Export all entries for current user.
|
||||
*
|
||||
* @Route("/export/{category}.{format}", name="export_entries", requirements={
|
||||
* "format": "epub|mobi|pdf|json|xml|txt|csv",
|
||||
* "format": "epub|pdf|json|xml|txt|csv",
|
||||
* "category": "all|unread|starred|archive|tag_entries|untagged|search|annotated|same_domain"
|
||||
* })
|
||||
*
|
||||
|
||||
@ -5,6 +5,7 @@ namespace Wallabag\CoreBundle\Controller;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@ -138,7 +139,7 @@ class IgnoreOriginInstanceRuleController extends AbstractController
|
||||
*
|
||||
* @param IgnoreOriginInstanceRule $ignoreOriginInstanceRule The ignore origin instance rule entity
|
||||
*
|
||||
* @return Form The form
|
||||
* @return FormInterface The form
|
||||
*/
|
||||
private function createDeleteForm(IgnoreOriginInstanceRule $ignoreOriginInstanceRule)
|
||||
{
|
||||
|
||||
@ -6,6 +6,7 @@ use Craue\ConfigBundle\Util\Config;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@ -173,7 +174,7 @@ class SiteCredentialController extends AbstractController
|
||||
*
|
||||
* @param SiteCredential $siteCredential The site credential entity
|
||||
*
|
||||
* @return Form The form
|
||||
* @return FormInterface The form
|
||||
*/
|
||||
private function createDeleteForm(SiteCredential $siteCredential)
|
||||
{
|
||||
|
||||
@ -45,7 +45,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
|
||||
|
||||
@ -17,6 +17,8 @@ class EntryFixtures extends Fixture implements DependentFixtureInterface
|
||||
*/
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$now = new \DateTime();
|
||||
|
||||
$entries = [
|
||||
'entry1' => [
|
||||
'user' => 'admin-user',
|
||||
@ -72,6 +74,7 @@ class EntryFixtures extends Fixture implements DependentFixtureInterface
|
||||
'content' => 'This is my content /o/',
|
||||
'language' => 'fr',
|
||||
'starred' => true,
|
||||
'starred_at' => $now,
|
||||
'preview' => 'http://0.0.0.0/image.jpg',
|
||||
],
|
||||
'entry6' => [
|
||||
@ -84,7 +87,9 @@ class EntryFixtures extends Fixture implements DependentFixtureInterface
|
||||
'content' => 'This is my content /o/',
|
||||
'language' => 'de',
|
||||
'archived' => true,
|
||||
'archived_at' => $now,
|
||||
'tags' => ['bar-tag'],
|
||||
'is_not_parsed' => true,
|
||||
],
|
||||
];
|
||||
|
||||
@ -112,14 +117,26 @@ class EntryFixtures extends Fixture implements DependentFixtureInterface
|
||||
$entry->setStarred($item['starred']);
|
||||
}
|
||||
|
||||
if (isset($item['starred_at'])) {
|
||||
$entry->setStarredAt($item['starred_at']);
|
||||
}
|
||||
|
||||
if (isset($item['archived'])) {
|
||||
$entry->setArchived($item['archived']);
|
||||
}
|
||||
|
||||
if (isset($item['archived_at'])) {
|
||||
$entry->setArchivedAt($item['archived_at']);
|
||||
}
|
||||
|
||||
if (isset($item['preview'])) {
|
||||
$entry->setPreviewPicture($item['preview']);
|
||||
}
|
||||
|
||||
if (isset($item['is_not_parsed'])) {
|
||||
$entry->setNotParsed($item['is_not_parsed']);
|
||||
}
|
||||
|
||||
$manager->persist($entry);
|
||||
$this->addReference($reference, $entry);
|
||||
}
|
||||
|
||||
@ -72,6 +72,9 @@ class Configuration implements ConfigurationInterface
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('fonts')
|
||||
->prototype('scalar')->end()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ class WallabagCoreExtension extends Extension
|
||||
$container->setParameter('wallabag_core.default_internal_settings', $config['default_internal_settings']);
|
||||
$container->setParameter('wallabag_core.site_credentials.encryption_key_path', $config['encryption_key_path']);
|
||||
$container->setParameter('wallabag_core.default_ignore_origin_instance_rules', $config['default_ignore_origin_instance_rules']);
|
||||
$container->setParameter('wallabag_core.fonts', $config['fonts']);
|
||||
}
|
||||
|
||||
public function getAlias()
|
||||
|
||||
@ -126,18 +126,65 @@ class Config
|
||||
*/
|
||||
private $displayThumbnails;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="font", type="text", nullable=true)
|
||||
*
|
||||
* @Groups({"config_api"})
|
||||
*/
|
||||
private $font;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="fontsize", type="float", nullable=true)
|
||||
*
|
||||
* @Groups({"config_api"})
|
||||
*/
|
||||
private $fontsize;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="line_height", type="float", nullable=true)
|
||||
*
|
||||
* @Groups({"config_api"})
|
||||
*/
|
||||
private $lineHeight;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="max_width", type="float", nullable=true)
|
||||
*
|
||||
* @Groups({"config_api"})
|
||||
*/
|
||||
private $maxWidth;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="custom_css", type="text", nullable=true)
|
||||
*/
|
||||
private $customCSS;
|
||||
|
||||
/**
|
||||
* @ORM\OneToOne(targetEntity="Wallabag\UserBundle\Entity\User", inversedBy="config")
|
||||
*/
|
||||
private $user;
|
||||
|
||||
/**
|
||||
* @var ArrayCollection<TaggingRule>
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="Wallabag\CoreBundle\Entity\TaggingRule", mappedBy="config", cascade={"remove"})
|
||||
* @ORM\OrderBy({"id" = "ASC"})
|
||||
*/
|
||||
private $taggingRules;
|
||||
|
||||
/**
|
||||
@var ArrayCollection<IgnoreOriginUserRule>
|
||||
|
||||
* @ORM\OneToMany(targetEntity="Wallabag\CoreBundle\Entity\IgnoreOriginUserRule", mappedBy="config", cascade={"remove"})
|
||||
* @ORM\OrderBy({"id" = "ASC"})
|
||||
*/
|
||||
@ -371,12 +418,9 @@ class Config
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getDisplayThumbnails(): ?bool
|
||||
public function getDisplayThumbnails(): bool
|
||||
{
|
||||
return $this->displayThumbnails;
|
||||
return (bool) $this->displayThumbnails;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -384,7 +428,94 @@ class Config
|
||||
*/
|
||||
public function setDisplayThumbnails(bool $displayThumbnails)
|
||||
{
|
||||
$this->displayThumbnails = $displayThumbnails;
|
||||
$this->displayThumbnails = $displayThumbnails ? 1 : 0;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFont(): ?string
|
||||
{
|
||||
return $this->font;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setFont(string $font): self
|
||||
{
|
||||
$this->font = $font;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getFontsize(): ?float
|
||||
{
|
||||
return $this->fontsize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setFontsize(float $fontsize): self
|
||||
{
|
||||
$this->fontsize = $fontsize;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getLineHeight(): ?float
|
||||
{
|
||||
return $this->lineHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setLineHeight(float $lineHeight): self
|
||||
{
|
||||
$this->lineHeight = $lineHeight;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getMaxWidth(): ?float
|
||||
{
|
||||
return $this->maxWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setMaxWidth(float $maxWidth): self
|
||||
{
|
||||
$this->maxWidth = $maxWidth;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCustomCSS(): ?string
|
||||
{
|
||||
return $this->customCSS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setCustomCSS(?string $customCSS): self
|
||||
{
|
||||
$this->customCSS = $customCSS;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ class Entry
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="uid", type="string", length=23, nullable=true)
|
||||
*
|
||||
@ -132,7 +132,7 @@ class Entry
|
||||
private $isArchived = false;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @var \DateTimeInterface
|
||||
*
|
||||
* @ORM\Column(name="archived_at", type="datetime", nullable=true)
|
||||
*
|
||||
@ -161,7 +161,7 @@ class Entry
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @var \DateTimeInterface
|
||||
*
|
||||
* @ORM\Column(name="created_at", type="datetime")
|
||||
*
|
||||
@ -170,7 +170,7 @@ class Entry
|
||||
private $createdAt;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @var \DateTimeInterface
|
||||
*
|
||||
* @ORM\Column(name="updated_at", type="datetime")
|
||||
*
|
||||
@ -179,7 +179,7 @@ class Entry
|
||||
private $updatedAt;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @var \DateTimeInterface
|
||||
*
|
||||
* @ORM\Column(name="published_at", type="datetime", nullable=true)
|
||||
*
|
||||
@ -197,7 +197,7 @@ class Entry
|
||||
private $publishedBy;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @var \DateTimeInterface
|
||||
*
|
||||
* @ORM\Column(name="starred_at", type="datetime", nullable=true)
|
||||
*
|
||||
@ -276,6 +276,17 @@ class Entry
|
||||
*/
|
||||
private $headers;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @Exclude
|
||||
*
|
||||
* @ORM\Column(name="is_not_parsed", type="boolean")
|
||||
*
|
||||
* @Groups({"entries_for_user", "export_all"})
|
||||
*/
|
||||
private $isNotParsed = false;
|
||||
|
||||
/**
|
||||
* @Exclude
|
||||
*
|
||||
@ -400,7 +411,7 @@ class Entry
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime|null
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function getArchivedAt()
|
||||
{
|
||||
@ -408,7 +419,7 @@ class Entry
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime|null $archivedAt
|
||||
* @param \DateTimeInterface|null $archivedAt
|
||||
*
|
||||
* @return Entry
|
||||
*/
|
||||
@ -482,7 +493,7 @@ class Entry
|
||||
|
||||
public function toggleStar()
|
||||
{
|
||||
$this->isStarred = $this->isStarred() ^ 1;
|
||||
$this->isStarred = !$this->isStarred();
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -560,7 +571,7 @@ class Entry
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getCreatedAt()
|
||||
{
|
||||
@ -568,7 +579,7 @@ class Entry
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getUpdatedAt()
|
||||
{
|
||||
@ -576,7 +587,7 @@ class Entry
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime|null
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function getStarredAt()
|
||||
{
|
||||
@ -584,7 +595,7 @@ class Entry
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime|null $starredAt
|
||||
* @param \DateTimeInterface|null $starredAt
|
||||
*
|
||||
* @return Entry
|
||||
*/
|
||||
@ -881,7 +892,7 @@ class Entry
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getPublishedAt()
|
||||
{
|
||||
@ -891,7 +902,7 @@ class Entry
|
||||
/**
|
||||
* @return Entry
|
||||
*/
|
||||
public function setPublishedAt(\DateTime $publishedAt)
|
||||
public function setPublishedAt(\DateTimeInterface $publishedAt)
|
||||
{
|
||||
$this->publishedAt = $publishedAt;
|
||||
|
||||
@ -1006,4 +1017,28 @@ class Entry
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set isNotParsed.
|
||||
*
|
||||
* @param bool $isNotParsed
|
||||
*
|
||||
* @return Entry
|
||||
*/
|
||||
public function setNotParsed($isNotParsed)
|
||||
{
|
||||
$this->isNotParsed = $isNotParsed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get isNotParsed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isNotParsed()
|
||||
{
|
||||
return $this->isNotParsed;
|
||||
}
|
||||
}
|
||||
|
||||
26
src/Wallabag/CoreBundle/Event/ConfigUpdatedEvent.php
Normal file
26
src/Wallabag/CoreBundle/Event/ConfigUpdatedEvent.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Wallabag\CoreBundle\Event;
|
||||
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
use Wallabag\CoreBundle\Entity\Config;
|
||||
|
||||
/**
|
||||
* This event is fired as soon as user configuration is updated.
|
||||
*/
|
||||
class ConfigUpdatedEvent extends Event
|
||||
{
|
||||
public const NAME = 'config.updated';
|
||||
|
||||
protected $config;
|
||||
|
||||
public function __construct(Config $entry)
|
||||
{
|
||||
$this->config = $entry;
|
||||
}
|
||||
|
||||
public function getConfig(): Config
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Wallabag\CoreBundle\Event\Subscriber;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use ScssPhp\ScssPhp\Compiler;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Wallabag\CoreBundle\Event\ConfigUpdatedEvent;
|
||||
|
||||
class GenerateCustomCSSSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private $em;
|
||||
private $compiler;
|
||||
|
||||
public function __construct(EntityManagerInterface $em, Compiler $compiler)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->compiler = $compiler;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
ConfigUpdatedEvent::NAME => 'onConfigUpdated',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate custom CSS.
|
||||
*/
|
||||
public function onConfigUpdated(ConfigUpdatedEvent $event)
|
||||
{
|
||||
$config = $event->getConfig();
|
||||
|
||||
$css = $this->compiler->compileString(
|
||||
'h1 { font-family: "' . $config->getFont() . '";}
|
||||
#article {
|
||||
max-width: ' . $config->getMaxWidth() . 'em;
|
||||
font-family: "' . $config->getFont() . '";
|
||||
}
|
||||
#article article {
|
||||
font-size: ' . $config->getFontsize() . 'em;
|
||||
line-height: ' . $config->getLineHeight() . 'em;
|
||||
}
|
||||
;
|
||||
')->getCss();
|
||||
|
||||
$config->setCustomCSS($css);
|
||||
|
||||
$this->em->persist($config);
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
@ -9,7 +9,7 @@ use Doctrine\ORM\Mapping\ClassMetadataInfo;
|
||||
/**
|
||||
* Puts a prefix to each table.
|
||||
* This way were used instead of using the built-in strategy from Doctrine, using `naming_strategy`
|
||||
* Because it conflicts with the DefaultQuoteStrategy (that espace table name, like user for Postgres)
|
||||
* Because it conflicts with the DefaultQuoteStrategy (that escape table name, like user for Postgres)
|
||||
* see #1498 for more detail.
|
||||
*
|
||||
* Solution from :
|
||||
|
||||
@ -49,7 +49,7 @@ class StringToListTransformer implements DataTransformerInterface
|
||||
public function reverseTransform($string)
|
||||
{
|
||||
if (null === $string) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
return array_values(array_filter(array_map('trim', explode($this->separator, $string))));
|
||||
|
||||
@ -6,6 +6,7 @@ use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\RangeType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
@ -14,18 +15,53 @@ use Wallabag\CoreBundle\Entity\Config;
|
||||
class ConfigType extends AbstractType
|
||||
{
|
||||
private $languages = [];
|
||||
private $fonts = [];
|
||||
|
||||
/**
|
||||
* @param array $languages Languages come from configuration, array just code language as key and label as value
|
||||
* @param array $fonts Fonts come from configuration, array just font name as key / value
|
||||
*/
|
||||
public function __construct($languages)
|
||||
public function __construct($languages, $fonts)
|
||||
{
|
||||
$this->languages = $languages;
|
||||
$this->fonts = $fonts;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('font', ChoiceType::class, [
|
||||
'choices' => $this->initFonts(),
|
||||
'label' => 'config.form_settings.font_label',
|
||||
'property_path' => 'font',
|
||||
])
|
||||
->add('fontsize', RangeType::class, [
|
||||
'attr' => [
|
||||
'min' => 0.6,
|
||||
'max' => 2,
|
||||
'step' => 0.1,
|
||||
],
|
||||
'label' => 'config.form_settings.fontsize_label',
|
||||
'property_path' => 'fontsize',
|
||||
])
|
||||
->add('lineHeight', RangeType::class, [
|
||||
'attr' => [
|
||||
'min' => 0.6,
|
||||
'max' => 2,
|
||||
'step' => 0.1,
|
||||
],
|
||||
'label' => 'config.form_settings.lineheight_label',
|
||||
'property_path' => 'lineHeight',
|
||||
])
|
||||
->add('maxWidth', RangeType::class, [
|
||||
'attr' => [
|
||||
'min' => 20,
|
||||
'max' => 60,
|
||||
'step' => 5,
|
||||
],
|
||||
'label' => 'config.form_settings.maxwidth_label',
|
||||
'property_path' => 'maxWidth',
|
||||
])
|
||||
->add('items_per_page', IntegerType::class, [
|
||||
'label' => 'config.form_settings.items_per_page_label',
|
||||
'property_path' => 'itemsPerPage',
|
||||
@ -72,4 +108,20 @@ class ConfigType extends AbstractType
|
||||
{
|
||||
return 'config';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an array with font name as key / value.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function initFonts()
|
||||
{
|
||||
$fonts = [];
|
||||
|
||||
foreach ($this->fonts as $font) {
|
||||
$fonts[$font] = $font;
|
||||
}
|
||||
|
||||
return $fonts;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -94,7 +95,7 @@ class EntryFilterType extends AbstractType
|
||||
->add('domainName', TextFilterType::class, [
|
||||
'apply_filter' => function (QueryInterface $filterQuery, $field, $values) {
|
||||
$value = $values['value'];
|
||||
if (\strlen($value) <= 2 || empty($value)) {
|
||||
if (empty($value) || \strlen($value) <= 2) {
|
||||
return false;
|
||||
}
|
||||
$expression = $filterQuery->getExpr()->like($field, $filterQuery->getExpr()->lower($filterQuery->getExpr()->literal('%' . $value . '%')));
|
||||
@ -102,10 +103,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 +121,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',
|
||||
@ -151,6 +160,10 @@ class EntryFilterType extends AbstractType
|
||||
$qb->innerJoin('e.annotations', 'a');
|
||||
},
|
||||
])
|
||||
->add('isNotParsed', CheckboxFilterType::class, [
|
||||
'label' => 'entry.filters.parsed_label',
|
||||
'data' => $options['filter_parsed'],
|
||||
])
|
||||
->add('previewPicture', CheckboxFilterType::class, [
|
||||
'apply_filter' => function (QueryInterface $filterQuery, $field, $values) {
|
||||
if (false === $values['value']) {
|
||||
@ -198,6 +211,7 @@ class EntryFilterType extends AbstractType
|
||||
'filter_starred' => false,
|
||||
'filter_unread' => false,
|
||||
'filter_annotated' => false,
|
||||
'filter_parsed' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
])
|
||||
|
||||
@ -260,6 +260,7 @@ class ContentProxy
|
||||
|
||||
if (empty($content['html'])) {
|
||||
$content['html'] = $this->fetchingErrorMessage;
|
||||
$entry->setNotParsed(true);
|
||||
|
||||
if (!empty($content['description'])) {
|
||||
$content['html'] .= '<p><i>But we found a short description: </i></p>';
|
||||
|
||||
@ -247,55 +247,6 @@ class EntriesExport
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use PHPMobi to dump a .mobi file.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
private function produceMobi()
|
||||
{
|
||||
$mobi = new \MOBI();
|
||||
$content = new \MOBIFile();
|
||||
|
||||
/*
|
||||
* Book metadata
|
||||
*/
|
||||
$content->set('title', $this->title);
|
||||
$content->set('author', $this->author);
|
||||
$content->set('subject', $this->title);
|
||||
|
||||
/*
|
||||
* Front page
|
||||
*/
|
||||
$content->appendParagraph($this->getExportInformation('PHPMobi'));
|
||||
if (file_exists($this->logoPath)) {
|
||||
$content->appendImage(imagecreatefrompng($this->logoPath));
|
||||
}
|
||||
$content->appendPageBreak();
|
||||
|
||||
/*
|
||||
* Adding actual entries
|
||||
*/
|
||||
foreach ($this->entries as $entry) {
|
||||
$content->appendChapterTitle($entry->getTitle());
|
||||
$content->appendParagraph($entry->getContent());
|
||||
$content->appendPageBreak();
|
||||
}
|
||||
$mobi->setContentProvider($content);
|
||||
|
||||
return Response::create(
|
||||
$mobi->toString(),
|
||||
200,
|
||||
[
|
||||
'Accept-Ranges' => 'bytes',
|
||||
'Content-Description' => 'File Transfer',
|
||||
'Content-type' => 'application/x-mobipocket-ebook',
|
||||
'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.mobi"',
|
||||
'Content-Transfer-Encoding' => 'binary',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use TCPDF to dump a .pdf file.
|
||||
*
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace Wallabag\CoreBundle\Helper;
|
||||
|
||||
use Pagerfanta\Adapter\AdapterInterface;
|
||||
use Pagerfanta\Adapter\NullAdapter;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Wallabag\UserBundle\Entity\User;
|
||||
@ -19,7 +20,7 @@ class PreparePagerForEntries
|
||||
/**
|
||||
* @param User $user If user isn't logged in, we can force it (like for feed)
|
||||
*
|
||||
* @return Pagerfanta|null
|
||||
* @return Pagerfanta
|
||||
*/
|
||||
public function prepare(AdapterInterface $adapter, User $user = null)
|
||||
{
|
||||
@ -28,7 +29,7 @@ class PreparePagerForEntries
|
||||
}
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return;
|
||||
return new Pagerfanta(new NullAdapter());
|
||||
}
|
||||
|
||||
$entries = new Pagerfanta($adapter);
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace Wallabag\CoreBundle\Helper;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RulerZ\RulerZ;
|
||||
use Wallabag\CoreBundle\Entity\Entry;
|
||||
@ -120,7 +121,7 @@ class RuleBasedTagger
|
||||
/**
|
||||
* Retrieves the tagging rules for a given user.
|
||||
*
|
||||
* @return array<TaggingRule>
|
||||
* @return ArrayCollection<TaggingRule>
|
||||
*/
|
||||
private function getRulesForUser(User $user)
|
||||
{
|
||||
|
||||
@ -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.
|
||||
*
|
||||
@ -133,8 +192,10 @@ class EntryRepository extends ServiceEntityRepository
|
||||
|
||||
// We lower() all parts here because PostgreSQL 'LIKE' verb is case-sensitive
|
||||
$qb
|
||||
->andWhere('lower(e.content) LIKE lower(:term) OR lower(e.title) LIKE lower(:term) OR lower(e.url) LIKE lower(:term)')->setParameter('term', '%' . $term . '%')
|
||||
->andWhere('lower(e.content) LIKE lower(:term) OR lower(e.title) LIKE lower(:term) OR lower(e.url) LIKE lower(:term) OR lower(a.text) LIKE lower(:term)')
|
||||
->setParameter('term', '%' . $term . '%')
|
||||
->leftJoin('e.tags', 't')
|
||||
->leftJoin('e.annotations', 'a')
|
||||
->groupBy('e.id');
|
||||
|
||||
return $qb;
|
||||
@ -167,6 +228,21 @@ class EntryRepository extends ServiceEntityRepository
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve entries with annotations count for a user.
|
||||
*
|
||||
* @param int $userId
|
||||
*
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function getCountBuilderForAnnotationsByUser($userId)
|
||||
{
|
||||
return $this
|
||||
->getQueryBuilderByUser($userId)
|
||||
->innerJoin('e.annotations', 'a')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve untagged entries for a user.
|
||||
*
|
||||
@ -207,14 +283,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 $isNotParsed
|
||||
*
|
||||
* @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 = '', $isNotParsed = null)
|
||||
{
|
||||
if (!\in_array(strtolower($detail), ['full', 'metadata'], true)) {
|
||||
throw new \Exception('Detail "' . $detail . '" parameter is wrong, allowed: full or metadata');
|
||||
@ -244,6 +321,10 @@ class EntryRepository extends ServiceEntityRepository
|
||||
$qb->andWhere('e.uid IS ' . (true === $isPublic ? 'NOT' : '') . ' NULL');
|
||||
}
|
||||
|
||||
if (null !== $isNotParsed) {
|
||||
$qb->andWhere('e.isNotParsed = :isNotParsed')->setParameter('isNotParsed', (bool) $isNotParsed);
|
||||
}
|
||||
|
||||
if ($since > 0) {
|
||||
$qb->andWhere('e.updatedAt > :since')->setParameter('since', new \DateTime(date('Y-m-d H:i:s', $since)));
|
||||
}
|
||||
@ -429,7 +510,7 @@ class EntryRepository extends ServiceEntityRepository
|
||||
/**
|
||||
* Find all entries which have an empty value for hash.
|
||||
*
|
||||
* @return Entry|false
|
||||
* @return Entry[]
|
||||
*/
|
||||
public function findByEmptyHashedUrlAndUserId(int $userId)
|
||||
{
|
||||
@ -513,7 +594,7 @@ class EntryRepository extends ServiceEntityRepository
|
||||
|
||||
/**
|
||||
* Remove all entries for a user id.
|
||||
* Used when a user want to reset all informations.
|
||||
* Used when a user wants to reset all information.
|
||||
*
|
||||
* @param int $userId
|
||||
*/
|
||||
@ -563,6 +644,41 @@ class EntryRepository extends ServiceEntityRepository
|
||||
return $qb->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $userId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function findAllEntriesIdByUserIdAndNotParsed($userId = null)
|
||||
{
|
||||
$qb = $this->createQueryBuilder('e')
|
||||
->select('e.id')
|
||||
->where('e.isNotParsed = true');
|
||||
|
||||
if (null !== $userId) {
|
||||
$qb->where('e.user = :userid')->setParameter(':userid', $userId);
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
|
||||
@ -42,7 +42,7 @@ class SiteCredentialRepository extends ServiceEntityRepository
|
||||
->getOneOrNullResult();
|
||||
|
||||
if (null === $res) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// decrypt user & password before returning them
|
||||
|
||||
@ -25,18 +25,18 @@
|
||||
{{ form_start(form.config) }}
|
||||
{{ form_errors(form.config) }}
|
||||
|
||||
<div class="row">
|
||||
<div class="input-field col s11">
|
||||
{{ form_errors(form.config.items_per_page) }}
|
||||
{{ form_widget(form.config.items_per_page) }}
|
||||
{{ form_label(form.config.items_per_page) }}
|
||||
</div>
|
||||
<div class="input-field col s1">
|
||||
<a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_items_per_page'|trans }}">
|
||||
<i class="material-icons">live_help</i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="input-field col s11">
|
||||
{{ form_errors(form.config.items_per_page) }}
|
||||
{{ form_widget(form.config.items_per_page) }}
|
||||
{{ form_label(form.config.items_per_page) }}
|
||||
</div>
|
||||
<div class="input-field col s1">
|
||||
<a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_items_per_page'|trans }}">
|
||||
<i class="material-icons">live_help</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="input-field col s11 settings-checkbox-col">
|
||||
@ -117,6 +117,66 @@
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<h5>{{ 'config.tab_menu.article_display'|trans }}</h5>
|
||||
|
||||
<div class="row">
|
||||
<div class="input-field col s5">
|
||||
{{ form_errors(form.config.font) }}
|
||||
{{ form_widget(form.config.font) }}
|
||||
{{ form_label(form.config.font) }}
|
||||
</div>
|
||||
<div class="input-field col s1">
|
||||
<a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_font'|trans }}">
|
||||
<i class="material-icons">live_help</i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="input-field col s5">
|
||||
{{ form_errors(form.config.lineHeight) }}
|
||||
{{ form_widget(form.config.lineHeight) }}
|
||||
{{ form_label(form.config.lineHeight, null, {'label_attr': {'class': 'settings-range-label'}}) }}
|
||||
</div>
|
||||
<div class="input-field col s1">
|
||||
<a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_lineheight'|trans }}">
|
||||
<i class="material-icons">live_help</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="input-field col s5">
|
||||
{{ form_errors(form.config.fontsize) }}
|
||||
{{ form_widget(form.config.fontsize) }}
|
||||
{{ form_label(form.config.fontsize, null, {'label_attr': {'class': 'settings-range-label'}}) }}
|
||||
</div>
|
||||
<div class="input-field col s1">
|
||||
<a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_fontsize'|trans }}">
|
||||
<i class="material-icons">live_help</i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="input-field col s5">
|
||||
{{ form_errors(form.config.maxWidth) }}
|
||||
{{ form_widget(form.config.maxWidth) }}
|
||||
{{ form_label(form.config.maxWidth, null, {'label_attr': {'class': 'settings-range-label'}}) }}
|
||||
</div>
|
||||
<div class="input-field col s1">
|
||||
<a href="#" class="tooltipped" data-position="left" data-delay="50" data-tooltip="{{ 'config.form_settings.help_maxwidth'|trans }}">
|
||||
<i class="material-icons">live_help</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="input-field col s12">
|
||||
<div id="preview-article">
|
||||
<article id="preview-content">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_widget(form.config.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }}
|
||||
{{ form_rest(form.config) }}
|
||||
</form>
|
||||
@ -209,38 +269,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">
|
||||
|
||||
@ -40,6 +40,7 @@
|
||||
{% 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">
|
||||
@ -49,9 +50,6 @@
|
||||
</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">
|
||||
<label class="entry-checkbox">
|
||||
<input type="checkbox" class="entry-checkbox-input" data-js="entry-checkbox" name="entry-checkbox[]" value="{{ entry.id }}" />
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -53,13 +53,11 @@
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<input id="mass-action-tags-displayed" class="toggle-checkbox" type="checkbox" />
|
||||
<div class="mass-action-tags">
|
||||
<button class="btn cyan darken-1 mass-action-button mass-action-button--tags" type="submit" name="tag" title="{{ 'entry.list.add_tags'|trans }}"><i class="material-icons">label</i></button>
|
||||
<input type="text" class="mass-action-tags-input" name="tags" placeholder="{{ 'entry.list.mass_action_tags_input_placeholder'|trans }}" />
|
||||
<button class="btn cyan darken-1" type="submit" name="tag">{{ 'entry.list.add_tags'|trans }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -101,7 +99,6 @@
|
||||
<h4 class="center">{{ 'entry.list.export_title'|trans }}</h4>
|
||||
<ul>
|
||||
{% if craue_setting('export_epub') %}<li class="bold"><a class="waves-effect" href="{{ path('export_entries', {'category': current_route, 'format': 'epub', 'tag': current_tag, 'search_entry[term]': export_search_term, 'currentRoute': previous_route, 'entry': entry}) }}">EPUB</a></li>{% endif %}
|
||||
{% if craue_setting('export_mobi') %}<li class="bold"><a class="waves-effect" href="{{ path('export_entries', {'category': current_route, 'format': 'mobi', 'tag': current_tag, 'search_entry[term]': export_search_term, 'currentRoute': previous_route, 'entry': entry}) }}">MOBI (deprecated)</a></li>{% endif %}
|
||||
{% if craue_setting('export_pdf') %}<li class="bold"><a class="waves-effect" href="{{ path('export_entries', {'category': current_route, 'format': 'pdf', 'tag': current_tag, 'search_entry[term]': export_search_term, 'currentRoute': previous_route, 'entry': entry}) }}">PDF</a></li>{% endif %}
|
||||
{% if craue_setting('export_json') %}<li class="bold"><a class="waves-effect" href="{{ path('export_entries', {'category': current_route, 'format': 'json', 'tag': current_tag, 'search_entry[term]': export_search_term, 'currentRoute': previous_route, 'entry': entry}) }}">JSON</a></li>{% endif %}
|
||||
{% if craue_setting('export_csv') %}<li class="bold"><a class="waves-effect" href="{{ path('export_entries', {'category': current_route, 'format': 'csv', 'tag': current_tag, 'search_entry[term]': export_search_term, 'currentRoute': previous_route, 'entry': entry}) }}">CSV</a></li>{% endif %}
|
||||
@ -118,9 +115,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 %}
|
||||
|
||||
@ -138,7 +135,7 @@
|
||||
{{ form_label(form.isStarred) }}
|
||||
</div>
|
||||
|
||||
<div class="input-field col s6 with-checkbox">
|
||||
<div class="input-field col s12 with-checkbox">
|
||||
{{ form_widget(form.isUnread) }}
|
||||
{{ form_label(form.isUnread) }}
|
||||
</div>
|
||||
@ -148,6 +145,11 @@
|
||||
{{ form_label(form.isAnnotated) }}
|
||||
</div>
|
||||
|
||||
<div class="input-field col s12 with-checkbox">
|
||||
{{ form_widget(form.isNotParsed) }}
|
||||
{{ form_label(form.isNotParsed) }}
|
||||
</div>
|
||||
|
||||
<div class="col s12">
|
||||
<label>{{ 'entry.filters.preview_picture_help'|trans }}</label>
|
||||
</div>
|
||||
|
||||
@ -209,7 +209,6 @@
|
||||
<div class="collapsible-body">
|
||||
<ul>
|
||||
{% if craue_setting('export_epub') %}<li><a href="{{ path('export_entry', {'id': entry.id, 'format': 'epub'}) }}" title="Generate ePub file">EPUB</a></li>{% endif %}
|
||||
{% if craue_setting('export_mobi') %}<li><a href="{{ path('export_entry', {'id': entry.id, 'format': 'mobi'}) }}" title="Generate Mobi file">MOBI (deprecated)</a></li>{% endif %}
|
||||
{% if craue_setting('export_pdf') %}<li><a href="{{ path('export_entry', {'id': entry.id, 'format': 'pdf'}) }}" title="Generate PDF file">PDF</a></li>{% endif %}
|
||||
{% if craue_setting('export_csv') %}<li><a href="{{ path('export_entry', {'id': entry.id, 'format': 'csv'}) }}" title="Generate CSV file">CSV</a></li>{% endif %}
|
||||
{% if craue_setting('export_json') %}<li><a href="{{ path('export_entry', {'id': entry.id, 'format': 'json'}) }}" title="Generate JSON file">JSON</a></li>{% endif %}
|
||||
@ -320,4 +319,9 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
|
||||
<style>
|
||||
{{ app.user.config.customCSS|raw }}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@ -140,7 +140,6 @@
|
||||
<tr><td>tecnickcom/tcpdf</td><td>LGPLv3</td></tr>
|
||||
<tr><td>twig/extensions</td><td>MIT</td></tr>
|
||||
<tr><td>twig/twig</td><td>BSD-3-Clause</td></tr>
|
||||
<tr><td>wallabag/php-mobi</td><td>Apache-2.0</td></tr>
|
||||
<tr><td>willdurand/hateoas</td><td>MIT</td></tr>
|
||||
<tr><td>willdurand/hateoas-bundle</td><td>MIT</td></tr>
|
||||
<tr><td>willdurand/jsonp-callback-validator</td><td>MIT</td></tr>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -58,11 +58,19 @@ class WallabagExtension extends AbstractExtension implements GlobalsInterface
|
||||
|
||||
public function removeWww($url)
|
||||
{
|
||||
if (!\is_string($url)) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
return preg_replace('/^www\./i', '', $url);
|
||||
}
|
||||
|
||||
public function removeScheme($url)
|
||||
{
|
||||
if (!\is_string($url)) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
return preg_replace('#^https?://#i', '', $url);
|
||||
}
|
||||
|
||||
@ -88,35 +96,32 @@ class WallabagExtension extends AbstractExtension implements GlobalsInterface
|
||||
|
||||
switch ($type) {
|
||||
case 'starred':
|
||||
$qb = $this->entryRepository->getBuilderForStarredByUser($user->getId());
|
||||
$qb = $this->entryRepository->getCountBuilderForStarredByUser($user->getId());
|
||||
break;
|
||||
case 'archive':
|
||||
$qb = $this->entryRepository->getBuilderForArchiveByUser($user->getId());
|
||||
$qb = $this->entryRepository->getCountBuilderForArchiveByUser($user->getId());
|
||||
break;
|
||||
case 'unread':
|
||||
$qb = $this->entryRepository->getBuilderForUnreadByUser($user->getId());
|
||||
$qb = $this->entryRepository->getCountBuilderForUnreadByUser($user->getId());
|
||||
break;
|
||||
case 'annotated':
|
||||
$qb = $this->entryRepository->getBuilderForAnnotationsByUser($user->getId());
|
||||
$qb = $this->entryRepository->getCountBuilderForAnnotationsByUser($user->getId());
|
||||
break;
|
||||
case 'all':
|
||||
$qb = $this->entryRepository->getBuilderForAllByUser($user->getId());
|
||||
$qb = $this->entryRepository->getCountBuilderForAllByUser($user->getId());
|
||||
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')
|
||||
->select('COUNT(e.id)')
|
||||
->getQuery();
|
||||
|
||||
$query->useQueryCache(true);
|
||||
$query->enableResultCache($this->lifeTime);
|
||||
|
||||
return \count($query->getArrayResult());
|
||||
return $query->getSingleScalarResult();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -145,18 +150,17 @@ class WallabagExtension extends AbstractExtension implements GlobalsInterface
|
||||
$user = $this->tokenStorage->getToken() ? $this->tokenStorage->getToken()->getUser() : null;
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return 0;
|
||||
return '';
|
||||
}
|
||||
|
||||
$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,13 @@ 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\PinboardImport;
|
||||
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;
|
||||
@ -35,9 +38,26 @@ class ImportCommand extends Command
|
||||
private PinboardImport $pinboardImport;
|
||||
private DeliciousImport $deliciousImport;
|
||||
private WallabagV1Import $wallabagV1Import;
|
||||
private ElcuratorImport $elcuratorImport;
|
||||
private ShaarliImport $shaarliImport;
|
||||
private PocketHtmlImport $pocketHtmlImport;
|
||||
|
||||
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,
|
||||
ShaarliImport $shaarliImport,
|
||||
PocketHtmlImport $pocketHtmlImport
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->userRepository = $userRepository;
|
||||
@ -49,6 +69,9 @@ class ImportCommand extends Command
|
||||
$this->pinboardImport = $pinboardImport;
|
||||
$this->deliciousImport = $deliciousImport;
|
||||
$this->wallabagV1Import = $wallabagV1Import;
|
||||
$this->elcuratorImport = $elcuratorImport;
|
||||
$this->shaarliImport = $shaarliImport;
|
||||
$this->pocketHtmlImport = $pocketHtmlImport;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
@ -60,7 +83,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 or pocket', '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 +143,15 @@ class ImportCommand extends Command
|
||||
case 'delicious':
|
||||
$import = $this->deliciousImport;
|
||||
break;
|
||||
case 'elcurator':
|
||||
$import = $this->elcuratorImport;
|
||||
break;
|
||||
case 'shaarli':
|
||||
$import = $this->shaarliImport;
|
||||
break;
|
||||
case 'pocket':
|
||||
$import = $this->pocketHtmlImport;
|
||||
break;
|
||||
default:
|
||||
$import = $this->wallabagV1Import;
|
||||
}
|
||||
|
||||
@ -20,9 +20,23 @@ class RabbitMQConsumerTotalProxy
|
||||
private Consumer $pinboardConsumer;
|
||||
private Consumer $deliciousConsumer;
|
||||
private Consumer $elcuratorConsumer;
|
||||
private Consumer $shaarliConsumer;
|
||||
private Consumer $pocketHtmlConsumer;
|
||||
|
||||
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 $shaarliConsumer,
|
||||
Consumer $pocketHtmlConsumer
|
||||
) {
|
||||
$this->pocketConsumer = $pocketConsumer;
|
||||
$this->readabilityConsumer = $readabilityConsumer;
|
||||
$this->wallabagV1Consumer = $wallabagV1Consumer;
|
||||
@ -33,6 +47,8 @@ class RabbitMQConsumerTotalProxy
|
||||
$this->pinboardConsumer = $pinboardConsumer;
|
||||
$this->deliciousConsumer = $deliciousConsumer;
|
||||
$this->elcuratorConsumer = $elcuratorConsumer;
|
||||
$this->shaarliConsumer = $shaarliConsumer;
|
||||
$this->pocketHtmlConsumer = $pocketHtmlConsumer;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,6 +93,12 @@ class RabbitMQConsumerTotalProxy
|
||||
case 'elcurator':
|
||||
$consumer = $this->elcuratorConsumer;
|
||||
break;
|
||||
case 'shaarli':
|
||||
$consumer = $this->shaarliConsumer;
|
||||
break;
|
||||
case 'pocket_html':
|
||||
$consumer = $this->pocketHtmlConsumer;
|
||||
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,8 @@ class ImportController extends AbstractController
|
||||
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('pinboard')
|
||||
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('delicious')
|
||||
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('elcurator')
|
||||
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('shaarli')
|
||||
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('pocket_html')
|
||||
;
|
||||
} catch (\Exception $e) {
|
||||
$rabbitNotInstalled = true;
|
||||
@ -75,6 +77,8 @@ class ImportController extends AbstractController
|
||||
+ $redis->llen('wallabag.import.pinboard')
|
||||
+ $redis->llen('wallabag.import.delicious')
|
||||
+ $redis->llen('wallabag.import.elcurator')
|
||||
+ $redis->llen('wallabag.import.shaarli')
|
||||
+ $redis->llen('wallabag.import.pocket_html')
|
||||
;
|
||||
} catch (\Exception $e) {
|
||||
$redisNotInstalled = true;
|
||||
|
||||
@ -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';
|
||||
}
|
||||
}
|
||||
@ -107,7 +107,7 @@ abstract class AbstractImport implements ImportInterface
|
||||
/**
|
||||
* Parse one entry.
|
||||
*
|
||||
* @return Entry
|
||||
* @return Entry|null
|
||||
*/
|
||||
abstract public function parseEntry(array $importedEntry);
|
||||
|
||||
@ -218,7 +218,7 @@ abstract class AbstractImport implements ImportInterface
|
||||
|
||||
/**
|
||||
* Set current imported entry to archived / read.
|
||||
* Implementation is different accross all imports.
|
||||
* Implementation is different across all imports.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
|
||||
@ -81,28 +81,28 @@ abstract class BrowserImport extends AbstractImport
|
||||
if ($this->producer) {
|
||||
$this->parseEntriesForProducer($importedEntry);
|
||||
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->parseEntries($importedEntry);
|
||||
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (\array_key_exists('children', $importedEntry)) {
|
||||
if ($this->producer) {
|
||||
$this->parseEntriesForProducer($importedEntry['children']);
|
||||
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->parseEntries($importedEntry['children']);
|
||||
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!\array_key_exists('uri', $importedEntry) && !\array_key_exists('url', $importedEntry)) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
$url = \array_key_exists('uri', $importedEntry) ? $importedEntry['uri'] : $importedEntry['url'];
|
||||
@ -114,7 +114,7 @@ abstract class BrowserImport extends AbstractImport
|
||||
if (false !== $existingEntry) {
|
||||
++$this->skippedEntries;
|
||||
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = $this->prepareEntry($importedEntry);
|
||||
|
||||
@ -104,7 +104,7 @@ class DeliciousImport extends AbstractImport
|
||||
if (false !== $existingEntry) {
|
||||
++$this->skippedEntries;
|
||||
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = [
|
||||
|
||||
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 = []);
|
||||
}
|
||||
@ -132,7 +132,7 @@ class InstapaperImport extends AbstractImport
|
||||
if (false !== $existingEntry) {
|
||||
++$this->skippedEntries;
|
||||
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
$entry = new Entry($this->user);
|
||||
|
||||
@ -104,7 +104,7 @@ class PinboardImport extends AbstractImport
|
||||
if (false !== $existingEntry) {
|
||||
++$this->skippedEntries;
|
||||
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = [
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -186,7 +186,7 @@ class PocketImport extends AbstractImport
|
||||
if (false !== $existingEntry) {
|
||||
++$this->skippedEntries;
|
||||
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
$entry = new Entry($this->user);
|
||||
|
||||
@ -104,7 +104,7 @@ class ReadabilityImport extends AbstractImport
|
||||
if (false !== $existingEntry) {
|
||||
++$this->skippedEntries;
|
||||
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = [
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -110,7 +110,7 @@ abstract class WallabagImport extends AbstractImport
|
||||
if (false !== $existingEntry) {
|
||||
++$this->skippedEntries;
|
||||
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = $this->prepareEntry($importedEntry);
|
||||
@ -119,6 +119,10 @@ abstract class WallabagImport extends AbstractImport
|
||||
$entry->setUrl($data['url']);
|
||||
$entry->setTitle($data['title']);
|
||||
|
||||
if (\array_key_exists('is_parsed', $data)) {
|
||||
$entry->setNotParsed(true);
|
||||
}
|
||||
|
||||
// update entry with content (in case fetching failed, the given entry will be return)
|
||||
$this->fetchContent($entry, $data['url'], $data);
|
||||
|
||||
|
||||
@ -65,6 +65,7 @@ class WallabagV1Import extends WallabagImport
|
||||
if (\in_array($entry['title'], $this->untitled, true)) {
|
||||
$data['title'] = $this->fetchingErrorMessageTitle;
|
||||
$data['html'] = $this->fetchingErrorMessage;
|
||||
$entry['is_not_parsed'] = 1;
|
||||
}
|
||||
|
||||
if (\array_key_exists('tags', $entry) && '' !== $entry['tags']) {
|
||||
|
||||
@ -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 %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user