Skip to content
Merged
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
10 changes: 9 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions app/Helper/Sharing.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
];
}
Expand Down
8 changes: 4 additions & 4 deletions app/Http/Controllers/App/ExportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();

Expand Down
10 changes: 5 additions & 5 deletions app/Http/Controllers/App/FeedController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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(),
Expand All @@ -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(),
Expand All @@ -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(),
Expand All @@ -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(),
Expand Down
5 changes: 3 additions & 2 deletions app/Http/Controllers/FetchController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
Expand Down
24 changes: 24 additions & 0 deletions app/Rules/NoPrivateIpRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class NoPrivateIpRule implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$domain = parse_url($value, PHP_URL_HOST);

if (filter_var($domain, FILTER_VALIDATE_IP) === false) {
// Hostname is not an IP address
return;
}

if (filter_var($domain, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
// Hostname contains an IP address from the private or reserved ranges
$fail(trans('validation.no_private_ip'));
}
}
}
1 change: 1 addition & 0 deletions lang/en_US/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
'string' => '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.',
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
9 changes: 9 additions & 0 deletions tests/Controller/App/ExportControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand All @@ -42,13 +46,17 @@ public function test_valid_html_export_generation(): void
'<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">',
$content
);
$this->assertStringNotContainsString($otherLink->url, $content);
}

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();

Expand All @@ -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);
}
}
28 changes: 23 additions & 5 deletions tests/Controller/App/FeedControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -55,23 +62,30 @@ 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);
}

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
Expand All @@ -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);
}

Expand Down
28 changes: 28 additions & 0 deletions tests/Controller/FetchControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<!DOCTYPE html><head>' .
'<title>Example Title</title>' .
'<meta name="description" content="This an example description">' .
'<meta name="keywords" content="html, css, javascript">' .
'</head></html>';

Http::fake([
'104.102.37.33' => Http::response($testHtml, 200),
]);

$response = $this->post('fetch/keywords-for-url', [
'url' => 'http://104.102.37.33/research/cold_fusion.html',
]);

$response->assertSessionHasNoErrors();
}

public function test_get_keywords_for_url_with_failure(): void
{
Http::fake([
Expand Down
Loading