diff --git a/app/Actions/ImportCsvBookmarks.php b/app/Actions/ImportCsvBookmarks.php new file mode 100644 index 000000000..a7dc8f7fa --- /dev/null +++ b/app/Actions/ImportCsvBookmarks.php @@ -0,0 +1,95 @@ +getRecords(); + } catch (\Exception $e) { + Log::error($e->getMessage()); + return false; + } + + $this->importTag = Tag::firstOrCreate([ + 'user_id' => $userId, + 'name' => 'import-' . now()->format('YmdHis'), + 'visibility' => ModelAttribute::VISIBILITY_PRIVATE, + ]); + + foreach ($records as $i => $record) { + // map Pocket's export columns title, url, time_added, tags, status + // to Linkace's default bookmark structure + [$name, $url, $dateCreated, $tags, $status] = $record; + + if (filter_var($url, FILTER_VALIDATE_URL) === false) { + // skip any links that are not a valid URL + $this->skipped++; + continue; + } + + if (Link::whereUrl($url)->first()) { + $this->skipped++; + continue; + } + + if ($name === $url) { + // ignore names that equal the URL + $name = ''; + } else { + // normalize white-space + $name = preg_replace('/\s+/', ' ', $name); + $name = trim($name); + } + + if (!empty($tags)) { + // Pocket exports join tags with pipes + $tags = explode('|', $tags); + } + + // there is no description included in Pocket export but expected by Linkace + $description = ''; + + // build link array + $link = compact('url', 'name', 'description', 'dateCreated', 'tags'); + + dispatch(new ImportLinkJob($userId, $link, $this->importTag, $generateMeta))->delay($i); + + $this->queued++; + } + + return true; + } + + public function getQueuedCount(): int + { + return $this->queued; + } + + public function getSkippedCount(): int + { + return $this->skipped; + } + + public function getImportTag(): ?Tag + { + return $this->importTag; + } +} diff --git a/app/Http/Controllers/App/ImportController.php b/app/Http/Controllers/App/ImportController.php index 3980e90ae..3cf0ac871 100644 --- a/app/Http/Controllers/App/ImportController.php +++ b/app/Http/Controllers/App/ImportController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\App; use App\Actions\ImportHtmlBookmarks; +use App\Actions\ImportCsvBookmarks; use App\Http\Controllers\Controller; use App\Http\Requests\DoImportRequest; use App\Jobs\ImportLinkJob; @@ -43,9 +44,13 @@ public function queue(): View */ public function doImport(DoImportRequest $request): JsonResponse { - $data = $request->file('import-file')->get(); + if ($request->file('import-file')->getMimeType() === 'text/csv') { + $importer = new ImportCsvBookmarks; + } else { + $importer = new ImportHtmlBookmarks; + } - $importer = new ImportHtmlBookmarks; + $data = $request->file('import-file')->get(); $result = $importer->run($data, auth()->id()); if ($result === false) { diff --git a/app/Http/Requests/DoImportRequest.php b/app/Http/Requests/DoImportRequest.php index d6cbf2bce..b6553bbb4 100644 --- a/app/Http/Requests/DoImportRequest.php +++ b/app/Http/Requests/DoImportRequest.php @@ -12,7 +12,7 @@ public function rules(): array 'import-file' => [ 'required', 'file', - 'mimes:html,htm', + 'mimes:html,htm,csv', ], ]; } diff --git a/composer.json b/composer.json index 9b58d3000..c2b0f304a 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "laravel/framework": "^v10.43", "laravel/sanctum": "^v3.3", "laravel/socialite": "^5.16", - "league/csv": "^9.6", + "league/csv": "^9.23.0", "league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-ftp": "^3.0", "league/flysystem-sftp-v3": "^3.0", diff --git a/composer.lock b/composer.lock index c71a17345..548cc9ba2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b99110961df74f6b16b2b5a8b25a582f", + "content-hash": "ca9894ba2c3506a798e315901fb8efae", "packages": [ { "name": "aws/aws-crt-php", @@ -2770,16 +2770,16 @@ }, { "name": "league/csv", - "version": "9.22.0", + "version": "9.23.0", "source": { "type": "git", "url": "https://github.com/thephpleague/csv.git", - "reference": "afc109aa11f3086b8be8dfffa04ac31480b36b76" + "reference": "774008ad8a634448e4f8e288905e070e8b317ff3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/csv/zipball/afc109aa11f3086b8be8dfffa04ac31480b36b76", - "reference": "afc109aa11f3086b8be8dfffa04ac31480b36b76", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/774008ad8a634448e4f8e288905e070e8b317ff3", + "reference": "774008ad8a634448e4f8e288905e070e8b317ff3", "shasum": "" }, "require": { @@ -2857,7 +2857,7 @@ "type": "github" } ], - "time": "2025-02-28T10:00:39+00:00" + "time": "2025-03-28T06:52:04+00:00" }, { "name": "league/flysystem", diff --git a/lang/de_DE/import.php b/lang/de_DE/import.php index 1c3f90bf4..f4bf14074 100644 --- a/lang/de_DE/import.php +++ b/lang/de_DE/import.php @@ -8,7 +8,7 @@ 'import_running' => 'Import läuft...', 'import_file' => 'Datei für den Import', - 'import_help' => 'Sie können hier Ihre vorhandenen Lesezeichen importieren. Normalerweise werden Lesezeichen von Ihrem Browser in eine .html Datei exportiert. Wählen Sie die Datei hier aus und starten Sie den Import. Bitte beachten Sie, dass ein Cron konfiguriert sein muss, damit der Import funktioniert.', + 'import_help' => 'Sie können hier Ihre vorhandenen Lesezeichen importieren. Dies kann entweder eine HTML-Datei mit allen Lesezeichen sein, wie Sie sie aus Ihrem Browser exportieren können. Oder es kann eine CSV-Datei sein, wie sie von Diensten wie Pocket als Export bereitgestellt wird. Wählen Sie die Datei hier aus und starten Sie den Import. Bitte beachten Sie, dass ein Cron konfiguriert sein muss, damit der Import funktioniert.', 'import_networkerror' => 'Beim Importieren der Lesezeichen ist ein Fehler aufgetreten. Bitte überprüfen Sie die Konsole des Browsers oder die Logs der Anwendung für Details.', 'import_error' => 'Beim Importieren der Lesezeichen ist ein Fehler aufgetreten. Bitte prüfen Sie die Logs der Anwendung.', diff --git a/lang/en_US/import.php b/lang/en_US/import.php index e00767ebf..649db08d5 100644 --- a/lang/en_US/import.php +++ b/lang/en_US/import.php @@ -8,7 +8,7 @@ 'import_running' => 'Import running...', 'import_file' => 'File for Import', - 'import_help' => 'You can import your existing browser bookmarks here. Usually, bookmarks are exported into an .html file by your browser. Select the file here and start the import. Please note that a cron must be configured for the import to work.', + 'import_help' => 'You can import your existing bookmarks here. This can either be an .html file including your bookmarks that you exported from your browser. Or it can be an .csv file containing your bookmarks as services like Pocket provide as export. Select the file here and start the import. Please note that a cron must be configured for the import to work.', 'import_networkerror' => 'Something went wrong while trying to import the bookmarks. Please check your browser console for details or consult the application logs.', 'import_error' => 'Something went wrong while trying to import the bookmarks. Please consult the application logs.', diff --git a/tests/Controller/App/ImportControllerTest.php b/tests/Controller/App/ImportControllerTest.php index a34b8775a..fa18d98ef 100644 --- a/tests/Controller/App/ImportControllerTest.php +++ b/tests/Controller/App/ImportControllerTest.php @@ -49,6 +49,20 @@ public function test_valid_import_action_response(): void Queue::assertPushed(ImportLinkJob::class, 5); } + public function test_valid_import_action_response_csv(): void + { + Queue::fake(); + + $exampleData = file_get_contents(__DIR__ . '/data/import_example.csv'); + $file = UploadedFile::fake()->createWithContent('import_example.csv', $exampleData); + + $response = $this->post('import', ['import-file' => $file], ['Accept' => 'text/csv']); + + $response->assertOk()->assertJson(['success' => true]); + + Queue::assertPushed(ImportLinkJob::class, 5); + } + public function test_queue_page(): void { $exampleData = file_get_contents(__DIR__ . '/data/import_example.html'); @@ -66,6 +80,24 @@ public function test_queue_page(): void ]); } + + public function test_queue_page_csv(): void + { + $exampleData = file_get_contents(__DIR__ . '/data/import_example.csv'); + $file = UploadedFile::fake()->createWithContent('import_example.csv', $exampleData); + + $response = $this->post('import', ['import-file' => $file], ['Accept' => 'text/csv']); + $response->assertOk()->assertJson(['success' => true]); + + $this->get('import/queue')->assertSeeInOrder([ + 'https://medium.com/accelerated-intelligence', + 'https://adele.uxpin.com', + 'https://color.adobe.com/create/color-wheel', + 'https://loader.io', + 'https://astralapp.com', + ]); + } + public function test_link_import_job(): void { UserSettings::fake([ diff --git a/tests/Controller/App/data/import_example.csv b/tests/Controller/App/data/import_example.csv new file mode 100644 index 000000000..257b5e664 --- /dev/null +++ b/tests/Controller/App/data/import_example.csv @@ -0,0 +1,7 @@ +Title,url,time_added,tags,status +"5-Hour Rule: If you’re not spending 5 hours per week learning, you’re being irresponsible",https://medium.com/accelerated-intelligence/the-5-hour-rule-if-youre-not-spending-5-hours-per-week-learning-you-re-being-irresponsible-791c3f18f5e6,1556456091,article|intelligence,archive +Application Load Testing Tools for API Endpoints with loader.io",https://adele.uxpin.com/,1548884240,collection|design|design systems|library|patterns,archive +Adobe Color CC,https://color.adobe.com/create/color-wheel/,1548884360,color-palettes|color-picker|color-wheel|colors,Archive +Application Load Testing Tools for API Endpoints with loader.io,https://loader.io/,1549406711,load-testing|testing|website|website-testing, +Astral — Organize Your GitHub Stars With Ease,https://astralapp.com/,1548884360,color-palettes|color-picker|color-wheel|colors,archive +This is Scary,"javascript:alert('XSS 1 - Document Cookie: ' + document.cookie);",1549404064,vulnerability,archive