diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2e52475cd..178d84a17 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -68,7 +68,7 @@ jobs: - name: Get Composer Cache Directory id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies uses: actions/cache@v4 @@ -98,3 +98,11 @@ jobs: with: project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} coverage-reports: ./test-coverage.xml + + - name: Upload Laravel logs on failure + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: logs-test-php-${{ matrix.php-versions }} + path: storage/logs/** + if-no-files-found: ignore diff --git a/app/Helper/Sharing.php b/app/Helper/Sharing.php index 42c55c928..d0ceaf558 100644 --- a/app/Helper/Sharing.php +++ b/app/Helper/Sharing.php @@ -44,9 +44,9 @@ protected static function generateLinkData(Link $link): array return [ $link->url, // URL self::encode($link->url), // encoded URL - $subject, // subject + htmlentities($subject), // subject self::encode($subject), // encoded subject - $shareText, // share text + htmlentities($shareText), // share text self::encode($shareText), // encoded share text ]; } diff --git a/app/Http/Controllers/App/ExportController.php b/app/Http/Controllers/App/ExportController.php index 83b2bcd54..bf45dab5b 100644 --- a/app/Http/Controllers/App/ExportController.php +++ b/app/Http/Controllers/App/ExportController.php @@ -32,7 +32,7 @@ public function getExport(): View */ public function doHtmlExport(): StreamedResponse { - $links = Link::oldest('title')->with('tags')->get(); + $links = Link::whereUserId(auth()->id())->oldest('title')->with('tags')->get(); $fileContent = view()->make('app.export.html-export', ['links' => $links])->render(); $fileName = config('app.name') . '_export.html'; @@ -51,11 +51,11 @@ public function doHtmlExport(): StreamedResponse */ public function doCsvExport() { - $links = Link::oldest('title')->get(); + $links = Link::whereUserId(auth()->id())->oldest('title')->get(); $rows = $links->map(function (Link $link) { - $link->tags = $link->tags()->get()->pluck('name')->join(','); - $link->lists = $link->lists()->get()->pluck('name')->join(','); + $link->tags = $link->tags()->visibleForUser()->get()->pluck('name')->join(','); + $link->lists = $link->lists()->visibleForUser()->get()->pluck('name')->join(','); return $link; })->toArray(); diff --git a/app/Http/Controllers/App/FeedController.php b/app/Http/Controllers/App/FeedController.php index bb3e232c1..9fba3f33c 100644 --- a/app/Http/Controllers/App/FeedController.php +++ b/app/Http/Controllers/App/FeedController.php @@ -13,7 +13,7 @@ class FeedController extends Controller { public function links(Request $request): Response { - $links = Link::latest()->with('user')->get(); + $links = Link::visibleForUser()->latest()->with('user')->get(); $meta = [ 'title' => 'LinkAce Links', 'link' => $request->fullUrl(), @@ -29,7 +29,7 @@ public function links(Request $request): Response public function lists(Request $request): Response { - $lists = LinkList::latest()->with('user')->get(); + $lists = LinkList::visibleForUser()->latest()->with('user')->get(); $meta = [ 'title' => 'LinkAce Lists', 'link' => $request->fullUrl(), @@ -45,7 +45,7 @@ public function lists(Request $request): Response public function listLinks(Request $request, LinkList $list): Response { - $links = $list->links()->with('user')->latest()->get(); + $links = $list->links()->visibleForUser()->with('user')->latest()->get(); $meta = [ 'title' => $list->name, 'link' => $request->fullUrl(), @@ -61,7 +61,7 @@ public function listLinks(Request $request, LinkList $list): Response public function tags(Request $request): Response { - $tags = Tag::latest()->with('user')->get(); + $tags = Tag::visibleForUser()->latest()->with('user')->get(); $meta = [ 'title' => 'LinkAce Links', 'link' => $request->fullUrl(), @@ -77,7 +77,7 @@ public function tags(Request $request): Response public function tagLinks(Request $request, Tag $tag): Response { - $links = $tag->links()->with('user')->latest()->get(); + $links = $tag->links()->visibleForUser()->with('user')->latest()->get(); $meta = [ 'title' => $tag->name, 'link' => $request->fullUrl(), diff --git a/app/Http/Controllers/FetchController.php b/app/Http/Controllers/FetchController.php index 1f0317e97..457d17911 100644 --- a/app/Http/Controllers/FetchController.php +++ b/app/Http/Controllers/FetchController.php @@ -6,6 +6,7 @@ use App\Models\Link; use App\Models\LinkList; use App\Models\Tag; +use App\Rules\NoPrivateIpRule; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Masterminds\HTML5; @@ -92,10 +93,10 @@ public static function checkForUpdates(): JsonResponse * @param Request $request * @return JsonResponse */ - public function htmlKeywordsFromUrl(Request $request) + public function htmlKeywordsFromUrl(Request $request): JsonResponse { $request->validate([ - 'url' => ['url'], + 'url' => ['url', new NoPrivateIpRule], ]); $url = $request->input('url'); diff --git a/app/Rules/NoPrivateIpRule.php b/app/Rules/NoPrivateIpRule.php new file mode 100644 index 000000000..19d236e43 --- /dev/null +++ b/app/Rules/NoPrivateIpRule.php @@ -0,0 +1,24 @@ + 'The :attribute must be at least :min characters.', 'array' => 'The :attribute must have at least :min items.', ], + 'no_private_ip' => 'The given URL must not contain a private IP address.', 'not_in' => 'The selected :attribute is invalid.', 'not_regex' => 'The :attribute format is invalid.', 'numeric' => 'The :attribute must be a number.', diff --git a/package-lock.json b/package-lock.json index d20936e73..cd7062954 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "linkace", - "version": "2.3.1", + "version": "2.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "linkace", - "version": "2.3.1", + "version": "2.4.0", "license": "GPL-3.0-or-later", "dependencies": { "bootstrap": "^5.3.3", diff --git a/package.json b/package.json index 75f6fd99a..e7d431206 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "linkace", - "version": "2.3.1", + "version": "2.4.0", "description": "A small, selfhosted bookmark manager with advanced features, built with Laravel and Docker", "homepage": "https://github.com/Kovah/LinkAce", "repository": { diff --git a/tests/Controller/App/ExportControllerTest.php b/tests/Controller/App/ExportControllerTest.php index 64c842d0f..dfda6c872 100644 --- a/tests/Controller/App/ExportControllerTest.php +++ b/tests/Controller/App/ExportControllerTest.php @@ -2,6 +2,7 @@ namespace Tests\Controller\App; +use App\Enums\ModelAttribute; use App\Models\Link; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -33,6 +34,9 @@ public function test_valid_export_response(): void public function test_valid_html_export_generation(): void { + $otherUser = User::factory()->create(); + $otherLink = Link::factory()->for($otherUser)->create(['visibility' => ModelAttribute::VISIBILITY_PRIVATE]); + $response = $this->post('export/html'); $response->assertOk(); @@ -42,6 +46,7 @@ public function test_valid_html_export_generation(): void '', $content ); + $this->assertStringNotContainsString($otherLink->url, $content); } public function test_valid_csv_export_generation(): void @@ -49,6 +54,9 @@ public function test_valid_csv_export_generation(): void /** @var Link $link */ $link = Link::inRandomOrder()->first(); + $otherUser = User::factory()->create(); + $otherLink = Link::factory()->for($otherUser)->create(['visibility' => ModelAttribute::VISIBILITY_PRIVATE]); + $response = $this->post('export/csv'); $response->assertOk(); @@ -58,5 +66,6 @@ public function test_valid_csv_export_generation(): void sprintf('%s,%s,%s', $link->id, $link->user_id, $link->url), $content ); + $this->assertStringNotContainsString($otherLink->url, $content); } } diff --git a/tests/Controller/App/FeedControllerTest.php b/tests/Controller/App/FeedControllerTest.php index 9e3f8f4a6..2cb57e337 100644 --- a/tests/Controller/App/FeedControllerTest.php +++ b/tests/Controller/App/FeedControllerTest.php @@ -3,6 +3,7 @@ namespace Tests\Controller\App; use App\Enums\ApiToken; +use App\Enums\ModelAttribute; use App\Models\Link; use App\Models\LinkList; use App\Models\Tag; @@ -35,18 +36,24 @@ public function test_links_feed(): void { $link = Link::factory()->create(); + $otherUser = User::factory()->create(); + $otherLink = Link::factory()->for($otherUser)->create(['visibility' => ModelAttribute::VISIBILITY_PRIVATE]); + $response = $this->getAuthorized('links/feed'); - $response->assertOk()->assertSee($link->url); + $response->assertOk()->assertSee($link->url)->assertDontSee($otherLink->url); } public function test_lists_feed(): void { $list = LinkList::factory()->create(); + $otherUser = User::factory()->create(); + $otherTList = LinkList::factory()->for($otherUser)->create(['visibility' => ModelAttribute::VISIBILITY_PRIVATE]); + $response = $this->getAuthorized('lists/feed'); - $response->assertOk()->assertSee($list->name); + $response->assertOk()->assertSee($list->name)->assertDontSee($otherTList->name); } public function test_list_link_feed(): void @@ -55,13 +62,17 @@ public function test_list_link_feed(): void $listLink = Link::factory()->create(); $unrelatedLink = Link::factory()->create(); - $listLink->lists()->sync([$link->id]); + $otherUser = User::factory()->create(); + $otherLink = Link::factory()->for($otherUser)->create(['visibility' => ModelAttribute::VISIBILITY_PRIVATE]); + + $listLink->lists()->sync([$link->id, $otherLink->id]); $response = $this->getAuthorized('lists/1/feed'); $response->assertOk() ->assertSee($link->name) ->assertSee($listLink->url) + ->assertDontSee($otherLink->url) ->assertDontSee($unrelatedLink->url); } @@ -69,9 +80,12 @@ public function test_tags_feed(): void { $tag = Tag::factory()->create(); + $otherUser = User::factory()->create(); + $otherTag = Tag::factory()->for($otherUser)->create(['visibility' => ModelAttribute::VISIBILITY_PRIVATE]); + $response = $this->getAuthorized('tags/feed'); - $response->assertOk()->assertSee($tag->name); + $response->assertOk()->assertSee($tag->name)->assertDontSee($otherTag->name); } public function test_tag_link_feed(): void @@ -80,13 +94,17 @@ public function test_tag_link_feed(): void $tagLink = Link::factory()->create(); $unrelatedLink = Link::factory()->create(); - $tagLink->tags()->sync([$tag->id]); + $otherUser = User::factory()->create(); + $otherLink = Link::factory()->for($otherUser)->create(['visibility' => ModelAttribute::VISIBILITY_PRIVATE]); + + $tagLink->tags()->sync([$tag->id, $otherLink->id]); $response = $this->getAuthorized('tags/1/feed'); $response->assertOk() ->assertSee($tag->name) ->assertSee($tagLink->url) + ->assertDontSee($otherLink->url) ->assertDontSee($unrelatedLink->url); } diff --git a/tests/Controller/FetchControllerTest.php b/tests/Controller/FetchControllerTest.php index 7966f65b6..ba4ec27da 100644 --- a/tests/Controller/FetchControllerTest.php +++ b/tests/Controller/FetchControllerTest.php @@ -115,6 +115,34 @@ public function test_get_keywords_for_invalid_url(): void $response->assertSessionHasErrors('url'); } + public function test_get_keywords_for_private_ip_url(): void + { + $response = $this->post('fetch/keywords-for-url', [ + 'url' => 'http://192.168.0.126/admin', + ]); + + $response->assertSessionHasErrors(['url' => 'The given URL must not contain a private IP address.']); + } + + public function test_get_keywords_for_public_ip_url(): void + { + $testHtml = '
' . + '