Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions app/Actions/ImportCsvBookmarks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

namespace App\Actions;

use App\Enums\ModelAttribute;
use App\Jobs\ImportLinkJob;
use App\Models\Link;
use App\Models\Tag;
use Illuminate\Support\Facades\Log;
use League\Csv\Reader;

/**
* CSV import based on Pocket exports (https://getpocket.com).
*/
class ImportCsvBookmarks
{
protected int $queued = 0;
protected int $skipped = 0;
protected ?Tag $importTag = null;

public function run(string $data, string $userId, bool $generateMeta = true): bool
{
try {
$reader = Reader::createFromString($data);
$records = $reader->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;
}
}
9 changes: 7 additions & 2 deletions app/Http/Controllers/App/ImportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Requests/DoImportRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public function rules(): array
'import-file' => [
'required',
'file',
'mimes:html,htm',
'mimes:html,htm,csv',
],
];
}
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 6 additions & 6 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lang/de_DE/import.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
2 changes: 1 addition & 1 deletion lang/en_US/import.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
32 changes: 32 additions & 0 deletions tests/Controller/App/ImportControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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([
Expand Down
7 changes: 7 additions & 0 deletions tests/Controller/App/data/import_example.csv
Original file line number Diff line number Diff line change
@@ -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
Loading