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
19 changes: 8 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</picture>
</h1>

<p align="center"><b>Your self-hosted tool for effortlessly archiving, organizing, and sharing your favorite web links.</b></p>
<p align="center"><b>Your self-hosted app for effortlessly organizing, archiving, and sharing your favorite bookmarks.</b></p>

<p align="center">
<a href="https://mastodon.social/@linkace"><img src="https://img.shields.io/badge/%40linkace%40mastodon.social-6364ff" alt="Follow LinkAce on Mastodon"></a>
Expand Down Expand Up @@ -68,11 +68,8 @@ LinkAce provides multiple ways of installing it on your server. The complete doc
* [Setup without Docker](https://www.linkace.org/docs/v2/setup/setup-with-php/)
* [One-Click Deployment to the Cloud](https://www.linkace.org/docs/v2/setup/one-click-deploy/)
* [Setup with Kubernetes](https://www.linkace.org/docs/v2/setup/setup-to-k8s/) (Beta)
* [Official managed Hosting](https://hosting.linkace.org) (_Beta Waitlist_)
* [Official managed Hosting](https://hosting.linkace.org) (_Beta_)

&nbsp;

> **LinkAce 2.0 was just released!** This is a big upgrade to the application. Please read the [upgrade guide](https://www.linkace.org/docs/v2/upgrade/from-v1/) if you are still using LinkAce 1 and want to use version 2.

&nbsp;

Expand All @@ -83,9 +80,8 @@ I built LinkAce to solve my own problem, and I now offer my solution and code wi

:star: You can get personal and dedicated support by **becoming a supporter on [Open Collective](https://opencollective.com/linkace), [Patreon](https://www.patreon.com/Kovah) or [Github](https://github.com/sponsors/Kovah)**.

#### Our Supporters on Open Collective

<a href="https://opencollective.com/linkace"><img src="https://opencollective.com/linkace/individuals.svg?width=890"></a>
&nbsp;


### Documentation and Community
Expand All @@ -94,6 +90,11 @@ Details about all features and advanced configuration can be found in the [**pro
Additionally, you may visit the [community discussions](https://github.com/Kovah/LinkAce/discussions) to share your ideas, talk with other users or find help for specific problems.


#### Our Supporters on Open Collective

<a href="https://opencollective.com/linkace"><img src="https://opencollective.com/linkace/individuals.svg?width=890"></a>


&nbsp;


Expand All @@ -103,16 +104,12 @@ Additionally, you may visit the [community discussions](https://github.com/Kovah

Please consult the [**contribution guidelines**](CONTRIBUTING.md) to start working on LinkAce.


&nbsp;


Thanks go to these wonderful people for their contributions:

[![List of contributors](https://contrib.rocks/image?repo=kovah/linkace)](https://github.com/Kovah/LinkAce/graphs/contributors)


&nbsp;


LinkAce is a project by [Kevin Woblick](https://woblick.dev) and [Contributors](https://github.com/Kovah/LinkAce/graphs/contributors)
187 changes: 187 additions & 0 deletions app/Console/Commands/DebugConfigCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<?php

namespace App\Console\Commands;

use App\Http\Middleware\TrustHosts;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;

class DebugConfigCommand extends Command
{
protected $signature = 'debug';

protected $description = 'Output debug information to help diagnose configuration issues. Only available when APP_DEBUG=true.';

public function handle(): int
{
if (!config('app.debug')) {
$this->error('This command is only available when APP_DEBUG=true.');
return self::FAILURE;
}

$this->line('');
$this->line('<fg=cyan;options=bold>╔══════════════════════════════════════╗</>');
$this->line('<fg=cyan;options=bold>║ LinkAce Debug Configuration ║</>');
$this->line('<fg=cyan;options=bold>╚══════════════════════════════════════╝</>');
$this->line('');

$this->printApplicationInfo();
$this->printTrustedHostsInfo();
$this->printTrustedProxiesInfo();
$this->printDatabaseInfo();
$this->printSystemRequirements();

return self::SUCCESS;
}

private function resolveVersion(): string
{
try {
$package = json_decode(Storage::disk('root')->get('package.json'), false);
return isset($package->version) ? 'v' . $package->version : '<fg=yellow>unknown</>';
} catch (Exception) {
return '<fg=yellow>unknown</>';
}
}

private function printApplicationInfo(): void
{
$this->line('<options=bold>Application</>');
$this->table([], [
['LinkAce Version', $this->resolveVersion()],
['Laravel Version', app()->version()],
['PHP Version', PHP_VERSION],
['Environment', config('app.env')],
['Debug Mode', config('app.debug') ? '<fg=yellow>true</>' : 'false'],
]);
}

private function printTrustedHostsInfo(): void
{
$this->line('<options=bold>Trusted Hosts</>');

$appUrl = config('app.url');
$trustHosts = new TrustHosts(app());
$patterns = $trustHosts->hosts();

$rows = [
['APP_URL', $appUrl],
['Allowed host pattern', implode(', ', array_filter($patterns))],
];

if ($appUrl === 'http://localhost') {
$rows[] = ['', '<fg=red>⚠ APP_URL is set to the default "http://localhost".</>'];
$rows[] = ['', '<fg=red> Any request from a different host will be rejected with 400.</>'];
} elseif (!str_starts_with($appUrl, 'https://')) {
$rows[] = ['', '<fg=yellow>⚠ APP_URL uses http:// but users may be accessing via https://.</>'];
$rows[] = ['', '<fg=yellow> Requests with a different scheme in the Host header may be blocked.</>'];
}

$this->table([], $rows);
}

private function printTrustedProxiesInfo(): void
{
$this->line('<options=bold>Trusted Proxies</>');

$proxies = config('app.trusted_proxies');

$headerMap = [
Request::HEADER_X_FORWARDED_FOR => 'X-Forwarded-For',
Request::HEADER_X_FORWARDED_HOST => 'X-Forwarded-Host',
Request::HEADER_X_FORWARDED_PORT => 'X-Forwarded-Port',
Request::HEADER_X_FORWARDED_PROTO => 'X-Forwarded-Proto',
Request::HEADER_X_FORWARDED_AWS_ELB => 'X-Forwarded-* (AWS ELB)',
];

$trustedHeaders = array_values(array_filter(
$headerMap,
fn($bit) => (
(Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB) & $bit
) === $bit,
ARRAY_FILTER_USE_KEY
));

$rows = [
['TRUSTED_PROXIES', $proxies ?? '<fg=yellow>null (none trusted)</>'],
['Trusted headers', implode(', ', $trustedHeaders)],
];

if ($proxies === '*') {
$rows[] = ['', '<fg=yellow>⚠ All proxies are trusted (TRUSTED_PROXIES=*).</>'];
$rows[] = ['', '<fg=yellow> X-Forwarded-Host sent by any upstream proxy is accepted and</>'];
$rows[] = ['', '<fg=yellow> validated against the Trusted Hosts pattern above.</>'];
$rows[] = ['', '<fg=yellow> If your proxy forwards an unexpected host, requests will fail.</>'];
} elseif ($proxies === null) {
$rows[] = ['', '<fg=yellow>⚠ No proxies are trusted. If you are behind a reverse proxy,</>'];
$rows[] = ['', '<fg=yellow> set TRUSTED_PROXIES to your proxy IP or subnet.</>'];
}

$this->table([], $rows);
}

private function printDatabaseInfo(): void
{
$this->line('<options=bold>Database</>');

$connection = config('database.default');
$dbConfig = config('database.connections.' . $connection);

$rows = [
['DB_CONNECTION', $connection],
];

if ($connection === 'sqlite') {
$rows[] = ['DB_DATABASE', $dbConfig['database'] ?? 'n/a'];
} else {
$rows[] = ['DB_HOST', $dbConfig['host'] ?? 'n/a'];
$rows[] = ['DB_PORT', $dbConfig['port'] ?? 'n/a'];
$rows[] = ['DB_DATABASE', $dbConfig['database'] ?? 'n/a'];
}

$this->table([], $rows);
}

private function printSystemRequirements(): void
{
$this->line('<options=bold>System Requirements</>');

$ok = '<fg=green>✔ OK</>';
$fail = '<fg=red>✘ FAIL</>';

$phpOk = PHP_VERSION_ID >= 80110;

$extensions = [
'bcmath', 'ctype', 'curl', 'dom', 'fileinfo',
'filter', 'hash', 'json', 'mbstring', 'openssl',
'pcre', 'session', 'tokenizer', 'xml',
];

$dbExtensions = ['pdo_mysql', 'pdo_pgsql', 'pdo_sqlite'];

$rows = [['PHP >= 8.1.10', $phpOk ? $ok : $fail . ' (found ' . PHP_VERSION . ')']];

foreach ($extensions as $ext) {
$rows[] = ['ext-' . $ext, extension_loaded($ext) ? $ok : $fail];
}

$rows[] = ['— Database drivers —', ''];
foreach ($dbExtensions as $ext) {
$rows[] = ['ext-' . $ext, extension_loaded($ext) ? $ok : '<fg=yellow>not loaded</>'];
}

$rows[] = ['— Filesystem —', ''];
$rows[] = ['.env writable', File::isWritable(base_path('.env')) ? $ok : $fail];
$rows[] = ['storage/ writable', File::isWritable(storage_path()) ? $ok : $fail];
$rows[] = ['storage/logs writable', File::isWritable(storage_path('logs')) ? $ok : $fail];

$this->table([], $rows);
}
}
2 changes: 2 additions & 0 deletions app/Http/Controllers/API/LinkController.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ public function show(Request $request, ApiLink $link): JsonResponse

public function update(LinkUpdateRequest $request, ApiLink $link): JsonResponse
{
$this->authorize('update', $link);

$updatedLink = LinkRepository::update($link, $request->all());

return response()->json($updatedLink);
Expand Down
2 changes: 2 additions & 0 deletions app/Http/Controllers/API/ListController.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public function show(ApiLinkList $list): JsonResponse

public function update(ListUpdateRequest $request, ApiLinkList $list): JsonResponse
{
$this->authorize('update', $list);

$updatedList = ListRepository::update($list, $request->all());

return response()->json($updatedList);
Expand Down
2 changes: 2 additions & 0 deletions app/Http/Controllers/API/NoteController.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public function store(NoteStoreRequest $request): JsonResponse

public function update(NoteUpdateRequest $request, ApiNote $note): JsonResponse
{
$this->authorize('update', $note);

$updatedNote = NoteRepository::update($note, $request->validated());

return response()->json($updatedNote);
Expand Down
2 changes: 2 additions & 0 deletions app/Http/Controllers/API/TagController.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public function show(ApiTag $tag): JsonResponse

public function update(TagUpdateRequest $request, ApiTag $tag): JsonResponse
{
$this->authorize('update', $tag);

$updatedTag = TagRepository::update($tag, $request->all());

return response()->json($updatedTag);
Expand Down
2 changes: 1 addition & 1 deletion app/Policies/Api/AuthorizesUserApiActions.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ protected function userCanUpdateModel(User $user, Model $model): bool
}
return $user->tokenCan($this->updateAbility);
}
return $model->visibility !== ModelAttribute::VISIBILITY_PRIVATE;
return false;
}

protected function userCanDeleteModel(User $user, Model $model): bool
Expand Down
2 changes: 1 addition & 1 deletion app/Policies/LinkListPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function create(User $user): bool

public function update(User $user, LinkList $list): bool
{
return $this->userCanAccessList($user, $list);
return $list->user->is($user);
}

public function delete(User $user, LinkList $list): bool
Expand Down
2 changes: 1 addition & 1 deletion app/Policies/LinkPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function create(User $user): bool

public function update(User $user, Link $link): bool
{
return $this->userCanAccessLink($user, $link);
return $link->user->is($user);
}

public function delete(User $user, Link $link): bool
Expand Down
2 changes: 1 addition & 1 deletion app/Policies/NotePolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function create(User $user): bool

public function update(User $user, Note $note): bool
{
return $this->userCanAccessNote($user, $note);
return $note->user->is($user);
}

public function delete(User $user, Note $note): bool
Expand Down
2 changes: 1 addition & 1 deletion app/Policies/TagPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function create(User $user): bool

public function update(User $user, Tag $tag): bool
{
return $this->userCanAccessTag($user, $tag);
return $tag->user->is($user);
}

public function delete(User $user, Tag $tag): bool
Expand Down
2 changes: 1 addition & 1 deletion app/View/Components/History/ActivityEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ protected function processActivity(): void
if ($this->activity->causer() !== null) {
$this->changes[] = trans('audit.activity_entry_with_causer', [
'change' => $change,
'causer' => $this->activity->causer?->name ?: trans('user.unknown_user'),
'causer' => htmlspecialchars($this->activity->causer?->name ?: trans('user.unknown_user')),
]);
return;
}
Expand Down
20 changes: 15 additions & 5 deletions app/View/Components/History/UserEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,25 @@ public function render()
$timestamp = formatDateTime($this->entry->created_at);

if ($this->entry->event === 'deleted') {
$this->changes[] = trans('user.history_deleted', ['name' => $this->entry->getModified()['name']['old']]);
$this->changes[] = trans('user.history_deleted', [
'name' => htmlspecialchars($this->entry->getModified()['name']['old']),
]);
} elseif ($this->entry->event === 'restored') {
$this->changes[] = trans('user.history_restored', ['name' => $this->entry->getModified()['name']['new']]);
$this->changes[] = trans('user.history_restored', [
'name' => htmlspecialchars($this->entry->getModified()['name']['new']),
]);
} elseif ($this->entry->event === 'created') {
$this->changes[] = trans('user.history_created', ['name' => $this->entry->getModified()['name']['new']]);
$this->changes[] = trans('user.history_created', [
'name' => htmlspecialchars($this->entry->getModified()['name']['new']),
]);
} elseif ($this->entry->event === 'blocked') {
$this->changes[] = trans('user.history_blocked', ['name' => $this->entry->auditable->name]);
$this->changes[] = trans('user.history_blocked', [
'name' => htmlspecialchars($this->entry->auditable->name),
]);
} elseif ($this->entry->event === 'unblocked') {
$this->changes[] = trans('user.history_unblocked', ['name' => $this->entry->auditable->name]);
$this->changes[] = trans('user.history_unblocked', [
'name' => htmlspecialchars($this->entry->auditable->name),
]);
} else {
foreach ($this->entry->getModified() as $field => $change) {
$this->processChange($field, $change);
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.5.5",
"version": "2.5.6",
"description": "A small, selfhosted bookmark manager with advanced features, built with Laravel and Docker",
"homepage": "https://github.com/Kovah/LinkAce",
"repository": {
Expand Down
Loading
Loading