diff --git a/app/config/services.yml b/app/config/services.yml index 3cca8b30f..0d9d33741 100644 --- a/app/config/services.yml +++ b/app/config/services.yml @@ -276,55 +276,80 @@ services: Wallabag\Import\PocketImport: calls: + - [ setEnabled, [ '@=service(''craue_config'').get(''pocket_enabled'')' ] ] - [ setClient, [ '@Symfony\Contracts\HttpClient\HttpClientInterface $pocketClient' ] ] tags: - { name: wallabag.import, alias: pocket } Wallabag\Import\WallabagV1Import: + calls: + - [ setEnabled, [ '@=service(''craue_config'').get(''wallabag_v1_enabled'')' ] ] tags: - { name: wallabag.import, alias: wallabag_v1 } Wallabag\Import\WallabagV2Import: + calls: + - [ setEnabled, [ '@=service(''craue_config'').get(''wallabag_v2_enabled'')' ] ] tags: - { name: wallabag.import, alias: wallabag_v2 } Wallabag\Import\ElcuratorImport: + calls: + - [ setEnabled, [ '@=service(''craue_config'').get(''elcurator_enabled'')' ] ] tags: - { name: wallabag.import, alias: elcurator } Wallabag\Import\ReadabilityImport: + calls: + - [ setEnabled, [ '@=service(''craue_config'').get(''readibility_enabled'')' ] ] tags: - { name: wallabag.import, alias: readability } Wallabag\Import\InstapaperImport: + calls: + - [ setEnabled, [ '@=service(''craue_config'').get(''instapaper_enabled'')' ] ] tags: - { name: wallabag.import, alias: instapaper } Wallabag\Import\PinboardImport: + calls: + - [ setEnabled, [ '@=service(''craue_config'').get(''pinboard_enabled'')' ] ] tags: - { name: wallabag.import, alias: pinboard } Wallabag\Import\DeliciousImport: + calls: + - [ setEnabled, [ '@=service(''craue_config'').get(''delicious_enabled'')' ] ] tags: - { name: wallabag.import, alias: delicious } Wallabag\Import\OmnivoreImport: + calls: + - [ setEnabled, [ '@=service(''craue_config'').get(''omnivore_enabled'')' ] ] tags: - { name: wallabag.import, alias: omnivore } Wallabag\Import\FirefoxImport: + calls: + - [ setEnabled, [ '@=service(''craue_config'').get(''firefox_enabled'')' ] ] tags: - { name: wallabag.import, alias: firefox } Wallabag\Import\ChromeImport: + calls: + - [ setEnabled, [ '@=service(''craue_config'').get(''chrome_enabled'')' ] ] tags: - { name: wallabag.import, alias: chrome } Wallabag\Import\ShaarliImport: + calls: + - [ setEnabled, [ '@=service(''craue_config'').get(''shaarli_enabled'')' ] ] tags: - { name: wallabag.import, alias: shaarli } Wallabag\Import\PocketHtmlImport: + calls: + - [ setEnabled, [ '@=service(''craue_config'').get(''pocket_html_enabled'')' ] ] tags: - { name: wallabag.import, alias: pocket_html } diff --git a/app/config/wallabag.yml b/app/config/wallabag.yml index 02106a33b..28ec013c1 100644 --- a/app/config/wallabag.yml +++ b/app/config/wallabag.yml @@ -126,6 +126,62 @@ parameters: name: import_with_rabbitmq value: 0 section: import + - + name: pocket_enabled + value: 1 + section: import + - + name: wallabag_v1_enabled + value: 1 + section: import + - + name: wallabag_v2_enabled + value: 1 + section: import + - + name: elcurator_enabled + value: 1 + section: import + - + name: readibility_enabled + value: 1 + section: import + - + name: instapaper_enabled + value: 1 + section: import + - + name: pinboard_enabled + value: 1 + section: import + - + name: delicious_enabled + value: 1 + section: import + - + name: omnivore_enabled + value: 1 + section: import + - + name: firefox_enabled + value: 1 + section: import + - + name: chrome_enabled + value: 1 + section: import + - + name: shaarli_enabled + value: 1 + section: import + - + name: pocket_html_enabled + value: 1 + section: import + - + name: pocket_csv_enabled + value: 1 + section: import - name: matomo_enabled value: 0 diff --git a/migrations/Version20250526113708.php b/migrations/Version20250526113708.php new file mode 100644 index 000000000..51e264b74 --- /dev/null +++ b/migrations/Version20250526113708.php @@ -0,0 +1,106 @@ + 'pocket_enabled', + 'value' => '1', + 'section' => 'import', + ], + [ + 'name' => 'wallabag_v1_enabled', + 'value' => '1', + 'section' => 'import', + ], + [ + 'name' => 'wallabag_v2_enabled', + 'value' => '1', + 'section' => 'import', + ], + [ + 'name' => 'elcura_enabled', + 'value' => '1', + 'section' => 'import', + ], + [ + 'name' => 'readability_enabled', + 'value' => '1', + 'section' => 'import', + ], + [ + 'name' => 'instapaper_enabled', + 'value' => '1', + 'section' => 'import', + ], + [ + 'name' => 'pinboard_enabled', + 'value' => '1', + 'section' => 'import', + ], + [ + 'name' => 'delicious_enabled', + 'value' => '1', + 'section' => 'import', + ], + [ + 'name' => 'omnivore_enabled', + 'value' => '1', + 'section' => 'import', + ], + [ + 'name' => 'firefox_enabled', + 'value' => '1', + 'section' => 'import', + ], + [ + 'name' => 'chrome_enabled', + 'value' => '1', + 'section' => 'import', + ], + [ + 'name' => 'shaarli_enabled', + 'value' => '1', + 'section' => 'import', + ], + [ + 'name' => 'pocket_html_enabled', + 'value' => '1', + 'section' => 'import', + ], + [ + 'name' => 'pocket_csv_enabled', + 'value' => '1', + 'section' => 'import', + ], + ]; + + public function up(Schema $schema): void + { + foreach ($this->settings as $setting) { + $settingEnabled = $this->connection + ->fetchOne('SELECT * FROM ' . $this->getTable('internal_setting') . " WHERE name = '" . $setting['name'] . "'"); + + if (false !== $settingEnabled) { + continue; + } + + $this->addSql('INSERT INTO ' . $this->getTable('internal_setting') . " (name, value, section) VALUES ('" . $setting['name'] . "', '" . $setting['value'] . "', '" . $setting['section'] . "');"); + } + } + + public function down(Schema $schema): void + { + $this->skipIf(true, 'These settings are required and should not be removed.'); + } +} diff --git a/src/Controller/Import/BrowserController.php b/src/Controller/Import/BrowserController.php index 3b897248b..a7bc6f142 100644 --- a/src/Controller/Import/BrowserController.php +++ b/src/Controller/Import/BrowserController.php @@ -20,10 +20,14 @@ abstract class BrowserController extends AbstractController #[IsGranted('IMPORT_ENTRIES')] public function indexAction(Request $request, TranslatorInterface $translator) { + $wallabag = $this->getImportService(); + if (!$this->isGranted('USE_IMPORTER', $wallabag)) { + throw $this->createAccessDeniedException('You can not access this importer.'); + } + $form = $this->createForm(UploadImportType::class); $form->handleRequest($request); - $wallabag = $this->getImportService(); $wallabag->setUser($this->getUser()); if ($form->isSubmitted() && $form->isValid()) { diff --git a/src/Controller/Import/DeliciousController.php b/src/Controller/Import/DeliciousController.php index 2caa6bdc1..2ead52358 100644 --- a/src/Controller/Import/DeliciousController.php +++ b/src/Controller/Import/DeliciousController.php @@ -23,6 +23,7 @@ class DeliciousController extends AbstractController #[Route(path: '/import/delicious', name: 'import_delicious', methods: ['GET', 'POST'])] #[IsGranted('IMPORT_ENTRIES')] + #[IsGranted('USE_IMPORTER', subject: 'delicious')] public function indexAction(Request $request, DeliciousImport $delicious, Config $craueConfig, TranslatorInterface $translator) { $form = $this->createForm(UploadImportType::class); diff --git a/src/Controller/Import/HtmlController.php b/src/Controller/Import/HtmlController.php index c55e114e1..5ff32d39c 100644 --- a/src/Controller/Import/HtmlController.php +++ b/src/Controller/Import/HtmlController.php @@ -20,10 +20,14 @@ abstract class HtmlController extends AbstractController #[IsGranted('IMPORT_ENTRIES')] public function indexAction(Request $request, TranslatorInterface $translator) { + $wallabag = $this->getImportService(); + if (!$this->isGranted('USE_IMPORTER', $wallabag)) { + throw $this->createAccessDeniedException('You can not access this importer.'); + } + $form = $this->createForm(UploadImportType::class); $form->handleRequest($request); - $wallabag = $this->getImportService(); $wallabag->setUser($this->getUser()); if ($form->isSubmitted() && $form->isValid()) { diff --git a/src/Controller/Import/InstapaperController.php b/src/Controller/Import/InstapaperController.php index 7edb4a9e7..d04e62280 100644 --- a/src/Controller/Import/InstapaperController.php +++ b/src/Controller/Import/InstapaperController.php @@ -23,6 +23,7 @@ class InstapaperController extends AbstractController #[Route(path: '/import/instapaper', name: 'import_instapaper', methods: ['GET', 'POST'])] #[IsGranted('IMPORT_ENTRIES')] + #[IsGranted('USE_IMPORTER', subject: 'instapaper')] public function indexAction(Request $request, InstapaperImport $instapaper, Config $craueConfig, TranslatorInterface $translator) { $form = $this->createForm(UploadImportType::class); diff --git a/src/Controller/Import/OmnivoreController.php b/src/Controller/Import/OmnivoreController.php index 1796cb4ab..388a40b1e 100644 --- a/src/Controller/Import/OmnivoreController.php +++ b/src/Controller/Import/OmnivoreController.php @@ -23,6 +23,7 @@ class OmnivoreController extends AbstractController #[Route(path: '/import/omnivore', name: 'import_omnivore', methods: ['GET', 'POST'])] #[IsGranted('IMPORT_ENTRIES')] + #[IsGranted('USE_IMPORTER', subject: 'omnivore')] public function indexAction(Request $request, OmnivoreImport $omnivore, Config $craueConfig, TranslatorInterface $translator) { $form = $this->createForm(UploadImportType::class); diff --git a/src/Controller/Import/PinboardController.php b/src/Controller/Import/PinboardController.php index 437faac83..2554fcefd 100644 --- a/src/Controller/Import/PinboardController.php +++ b/src/Controller/Import/PinboardController.php @@ -23,6 +23,7 @@ class PinboardController extends AbstractController #[Route(path: '/import/pinboard', name: 'import_pinboard', methods: ['GET', 'POST'])] #[IsGranted('IMPORT_ENTRIES')] + #[IsGranted('USE_IMPORTER', subject: 'pinboard')] public function indexAction(Request $request, PinboardImport $pinboard, Config $craueConfig, TranslatorInterface $translator) { $form = $this->createForm(UploadImportType::class); diff --git a/src/Controller/Import/PocketController.php b/src/Controller/Import/PocketController.php index 543f867aa..c5c392d3a 100644 --- a/src/Controller/Import/PocketController.php +++ b/src/Controller/Import/PocketController.php @@ -27,6 +27,7 @@ class PocketController extends AbstractController #[Route(path: '/import/pocket', name: 'import_pocket', methods: ['GET'])] #[IsGranted('IMPORT_ENTRIES')] + #[IsGranted('USE_IMPORTER', subject: 'pocketImport')] public function indexAction(PocketImport $pocketImport) { $pocket = $this->getPocketImportService($pocketImport); @@ -47,6 +48,7 @@ class PocketController extends AbstractController #[Route(path: '/import/pocket/auth', name: 'import_pocket_auth', methods: ['POST'])] #[IsGranted('IMPORT_ENTRIES')] + #[IsGranted('USE_IMPORTER', subject: 'pocketImport')] public function authAction(Request $request, PocketImport $pocketImport) { $requestToken = $this->getPocketImportService($pocketImport) @@ -76,6 +78,7 @@ class PocketController extends AbstractController #[Route(path: '/import/pocket/callback', name: 'import_pocket_callback', methods: ['GET'])] #[IsGranted('IMPORT_ENTRIES')] + #[IsGranted('USE_IMPORTER', subject: 'pocketImport')] public function callbackAction(PocketImport $pocketImport, TranslatorInterface $translator) { $message = 'flashes.import.notice.failed'; diff --git a/src/Controller/Import/ReadabilityController.php b/src/Controller/Import/ReadabilityController.php index 6409f5d05..2eb10c73c 100644 --- a/src/Controller/Import/ReadabilityController.php +++ b/src/Controller/Import/ReadabilityController.php @@ -23,6 +23,7 @@ class ReadabilityController extends AbstractController #[Route(path: '/import/readability', name: 'import_readability', methods: ['GET', 'POST'])] #[IsGranted('IMPORT_ENTRIES')] + #[IsGranted('USE_IMPORTER', subject: 'readability')] public function indexAction(Request $request, ReadabilityImport $readability, Config $craueConfig, TranslatorInterface $translator) { $form = $this->createForm(UploadImportType::class); diff --git a/src/Controller/Import/WallabagController.php b/src/Controller/Import/WallabagController.php index 12700f902..e8ae8e713 100644 --- a/src/Controller/Import/WallabagController.php +++ b/src/Controller/Import/WallabagController.php @@ -22,10 +22,14 @@ abstract class WallabagController extends AbstractController */ public function indexAction(Request $request, TranslatorInterface $translator) { + $wallabag = $this->getImportService(); + if (!$this->isGranted('USE_IMPORTER', $wallabag)) { + throw $this->createAccessDeniedException('You can not access this importer.'); + } + $form = $this->createForm(UploadImportType::class); $form->handleRequest($request); - $wallabag = $this->getImportService(); $wallabag->setUser($this->getUser()); if ($form->isSubmitted() && $form->isValid()) { diff --git a/src/Import/AbstractImport.php b/src/Import/AbstractImport.php index 09d551028..0f387ee0c 100644 --- a/src/Import/AbstractImport.php +++ b/src/Import/AbstractImport.php @@ -21,6 +21,7 @@ abstract class AbstractImport implements ImportInterface protected $skippedEntries = 0; protected $importedEntries = 0; protected $queuedEntries = 0; + protected $enabled = true; public function __construct( protected EntityManagerInterface $em, @@ -74,6 +75,25 @@ abstract class AbstractImport implements ImportInterface return $this->markAsRead; } + /** + * Get whether the import is enabled. + * If not, the importer won't be available in the UI. + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + /** + * Set whether the import is enabled. + */ + public function setEnabled(bool $enabled): static + { + $this->enabled = $enabled; + + return $this; + } + /** * Set whether articles should be fetched for updated content. * diff --git a/src/Import/ImportChain.php b/src/Import/ImportChain.php index 61f4b354f..a96638c01 100644 --- a/src/Import/ImportChain.php +++ b/src/Import/ImportChain.php @@ -18,7 +18,11 @@ class ImportChain */ public function addImport(ImportInterface $import, $alias) { + // if (true === $import->isEnabled()) { $this->imports[$alias] = $import; + + // } + return $this; } /** diff --git a/src/Import/ImportInterface.php b/src/Import/ImportInterface.php index e4e16dbb7..9510fc1a7 100644 --- a/src/Import/ImportInterface.php +++ b/src/Import/ImportInterface.php @@ -63,4 +63,15 @@ interface ImportInterface extends LoggerAwareInterface * @param bool $markAsRead */ public function setMarkAsRead($markAsRead): static; + + /** + * Get whether the import is enabled. + * If not, the importer won't be available in the UI. + */ + public function isEnabled(): bool; + + /** + * Set whether the import is enabled. + */ + public function setEnabled(bool $enabled): static; } diff --git a/src/Security/Voter/ImportVoter.php b/src/Security/Voter/ImportVoter.php new file mode 100644 index 000000000..a70e565a1 --- /dev/null +++ b/src/Security/Voter/ImportVoter.php @@ -0,0 +1,42 @@ +getUser(); + + if (!$user instanceof User) { + return false; + } + + return match ($attribute) { + self::USE_IMPORTER => true === $subject->isEnabled(), + default => false, + }; + } +} diff --git a/tests/Security/Voter/ImportVoterTest.php b/tests/Security/Voter/ImportVoterTest.php new file mode 100644 index 000000000..1885cca17 --- /dev/null +++ b/tests/Security/Voter/ImportVoterTest.php @@ -0,0 +1,45 @@ +token = $this->createMock(TokenInterface::class); + $this->user = new User(); + + $this->importVoter = new ImportVoter(); + } + + public function testVoteReturnsAbstainForInvalidSubject(): void + { + $this->assertSame(VoterInterface::ACCESS_ABSTAIN, $this->importVoter->vote($this->token, new \stdClass(), [ImportVoter::USE_IMPORTER])); + } + + public function testVoteReturnsAbstainForInvalidAttribute(): void + { + $this->assertSame(VoterInterface::ACCESS_ABSTAIN, $this->importVoter->vote($this->token, new SiteCredential(new User()), ['INVALID'])); + } + + public function testVoteReturnsDeniedForNonSiteCredentialUserEdit(): void + { + $this->assertSame(VoterInterface::ACCESS_DENIED, $this->importVoter->vote($this->token, new SiteCredential(new User()), [ImportVoter::USE_IMPORTER])); + } + + public function testVoteReturnsGrantedForSiteCredentialUserEdit(): void + { + $this->assertSame(VoterInterface::ACCESS_GRANTED, $this->importVoter->vote($this->token, new SiteCredential($this->user), [ImportVoter::USE_IMPORTER])); + } +}