fix(versions): dev box lists BOTH its source and the public repo
Follow-up to the Projekt-card change: the dev/release-control box must list its actual update source (e.g. private Gitea) AND the public repo — the previous commit showed only the source on dev. Replaced the single project link with projectLinks(): the public repo is always listed (validated slug, canonical fallback); the dev box prepends its source when it is a real URL (a garbage/hostless source is dropped, never an empty link). Staging/stable still list the public repo only — no private leak. Codex review: no leak path on non-dev; hardened the dev source against a hostless URL. Verified: 433 tests green (dev-both / non-dev-only / malformed-slug / garbage-source), pint clean, /versions shows both links on the dev box with zero console errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/v1-foundation
parent
8e77f31f00
commit
ab728efc6f
|
|
@ -564,29 +564,43 @@ class Index extends Component
|
|||
}
|
||||
|
||||
/**
|
||||
* The repository shown on the "Projekt" card. Only the dev/release-control box surfaces its
|
||||
* actual update source (which may be the private Gitea); EVERY other install — staging or
|
||||
* stable — links the public open-core repo only, so a non-dev server can never expose a private
|
||||
* source regardless of how its CLUSEV_REPOSITORY happens to be set. (The update SOURCE is still
|
||||
* config('clusev.repository'); this only governs the displayed project link.)
|
||||
* Links shown on the "Projekt" card. EVERY install lists the public open-core repo. The
|
||||
* dev/release-control box ALSO lists its actual update source first (which may be the private
|
||||
* Gitea) — staging and stable never do, so a non-dev server can never expose a private source
|
||||
* regardless of how its CLUSEV_REPOSITORY is set. (The update SOURCE is still
|
||||
* config('clusev.repository'); this only governs the displayed links.)
|
||||
*
|
||||
* release_controls=true is the dev-box invariant: it also gates the dev-only /release page + the
|
||||
* host release bridge, so a staging/stable box never has it on — and even if it did, that box's
|
||||
* CLUSEV_REPOSITORY is the public repo anyway (it pulls from public). On a non-dev box the link
|
||||
* is built from the public slug ONLY, and a malformed slug (empty / slashed / a full URL) falls
|
||||
* back to the canonical public repo — never to a private host.
|
||||
* CLUSEV_REPOSITORY is the public repo anyway. The public link is built from the public slug
|
||||
* ONLY, and a malformed slug (empty / slashed / a full URL) falls back to the canonical public
|
||||
* repo — never to a private host.
|
||||
*
|
||||
* @return list<array{url: string, host: string, path: string}>
|
||||
*/
|
||||
private function projectUrl(): string
|
||||
private function projectLinks(): array
|
||||
{
|
||||
if (config('clusev.release_controls')) {
|
||||
return (string) config('clusev.repository');
|
||||
}
|
||||
$slug = trim((string) config('clusev.public_slug'), '/');
|
||||
if (! preg_match('#^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$#', $slug)) {
|
||||
$slug = 'clusev/clusev';
|
||||
}
|
||||
$urls = ['https://github.com/'.$slug];
|
||||
|
||||
return 'https://github.com/'.$slug;
|
||||
// Dev box only: ALSO surface the actual update source (e.g. the private Gitea), shown first.
|
||||
// Require a real URL (a parseable host) so a misconfigured/garbage source is dropped rather
|
||||
// than rendered as an empty link — the public repo still shows.
|
||||
if (config('clusev.release_controls')) {
|
||||
$source = (string) config('clusev.repository');
|
||||
if (parse_url($source, PHP_URL_HOST) !== null && ! in_array($source, $urls, true)) {
|
||||
array_unshift($urls, $source);
|
||||
}
|
||||
}
|
||||
|
||||
return array_map(static fn (string $u): array => [
|
||||
'url' => $u,
|
||||
'host' => (string) parse_url($u, PHP_URL_HOST),
|
||||
'path' => trim((string) parse_url($u, PHP_URL_PATH), '/'),
|
||||
], $urls);
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
|
@ -628,7 +642,7 @@ class Index extends Component
|
|||
return view('livewire.versions.index', [
|
||||
'version' => config('clusev.version'),
|
||||
'channel' => $this->channel(),
|
||||
'repository' => $this->projectUrl(),
|
||||
'repositories' => $this->projectLinks(),
|
||||
'license' => config('clusev.license'),
|
||||
'build' => $this->build(),
|
||||
'seriesList' => $seriesList,
|
||||
|
|
|
|||
|
|
@ -18,9 +18,6 @@
|
|||
// Toned glance-dots on each collapsed release row (static classes so Tailwind's scanner sees them).
|
||||
$dotClass = ['accent' => 'bg-accent', 'cyan' => 'bg-cyan', 'neutral' => 'bg-ink-4'];
|
||||
|
||||
$repoHost = parse_url($repository, PHP_URL_HOST);
|
||||
$repoPath = trim((string) parse_url($repository, PHP_URL_PATH), '/');
|
||||
|
||||
$isUpdate = $updateState === 'update';
|
||||
@endphp
|
||||
|
||||
|
|
@ -288,14 +285,16 @@
|
|||
|
||||
<x-panel :title="__('versions.project_title')" :padded="false">
|
||||
<div class="space-y-3 px-4 py-4 sm:px-5">
|
||||
<a href="{{ $repository }}" target="_blank" rel="noopener noreferrer" class="flex items-center gap-3 rounded-md border border-line bg-inset px-3 py-2.5 transition-colors hover:border-accent/30">
|
||||
@foreach ($repositories as $repo)
|
||||
<a href="{{ $repo['url'] }}" target="_blank" rel="noopener noreferrer" wire:key="repo-{{ $loop->index }}" class="flex items-center gap-3 rounded-md border border-line bg-inset px-3 py-2.5 transition-colors hover:border-accent/30">
|
||||
<span class="grid h-8 w-8 shrink-0 place-items-center rounded-md bg-raised text-ink-3"><x-icon name="git-branch" class="h-4 w-4" /></span>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate font-mono text-xs text-ink">{{ $repoPath }}</span>
|
||||
<span class="block truncate font-mono text-[11px] text-ink-3">{{ $repoHost }}</span>
|
||||
<span class="block truncate font-mono text-xs text-ink">{{ $repo['path'] }}</span>
|
||||
<span class="block truncate font-mono text-[11px] text-ink-3">{{ $repo['host'] }}</span>
|
||||
</span>
|
||||
<x-icon name="chevron-left" class="h-4 w-4 shrink-0 rotate-180 text-ink-4" />
|
||||
</a>
|
||||
@endforeach
|
||||
<p class="flex items-center gap-2 font-mono text-[11px] text-ink-3"><x-icon name="shield" class="h-3.5 w-3.5 shrink-0 text-cyan" /> {{ __('versions.open_core', ['license' => $license]) }}</p>
|
||||
</div>
|
||||
</x-panel>
|
||||
|
|
|
|||
|
|
@ -306,35 +306,62 @@ class VersionUpdateCheckTest extends TestCase
|
|||
);
|
||||
}
|
||||
|
||||
// ── Project link: only the dev box may expose its (possibly private) source ───
|
||||
// ── Project links: dev lists source + public; everyone else only public ──────
|
||||
|
||||
public function test_dev_box_shows_its_actual_repository_on_the_project_card(): void
|
||||
/** @param array<int,array{url:string}> $repos */
|
||||
private static function urls(array $repos): array
|
||||
{
|
||||
config()->set('clusev.release_controls', true); // the dev / release-control box
|
||||
Http::fake();
|
||||
|
||||
// setUp() set clusev.repository to the (Gitea-style) source — the dev box may surface it.
|
||||
Livewire::test(Index::class)->assertViewHas('repository', self::REPO);
|
||||
return array_column($repos, 'url');
|
||||
}
|
||||
|
||||
public function test_non_dev_install_only_links_the_public_repo(): void
|
||||
public function test_dev_box_lists_its_source_and_the_public_repo(): void
|
||||
{
|
||||
config()->set('clusev.release_controls', true); // the dev / release-control box
|
||||
config()->set('clusev.public_slug', 'clusev/clusev');
|
||||
Http::fake();
|
||||
|
||||
// setUp() set clusev.repository to the (Gitea-style) source — the dev box lists it FIRST,
|
||||
// then the public repo. Both visible.
|
||||
Livewire::test(Index::class)->assertViewHas('repositories', fn ($repos) => self::urls($repos) === [
|
||||
self::REPO, 'https://github.com/clusev/clusev',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_dev_box_drops_a_garbage_source_url(): void
|
||||
{
|
||||
config()->set('clusev.release_controls', true);
|
||||
config()->set('clusev.repository', 'not-a-url'); // misconfigured source (no host)
|
||||
config()->set('clusev.public_slug', 'clusev/clusev');
|
||||
Http::fake();
|
||||
|
||||
// A garbage source is dropped (would render an empty link) — the public repo still shows.
|
||||
Livewire::test(Index::class)->assertViewHas('repositories', fn ($repos) => self::urls($repos) === [
|
||||
'https://github.com/clusev/clusev',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_non_dev_install_lists_only_the_public_repo(): void
|
||||
{
|
||||
config()->set('clusev.release_controls', false); // staging / stable
|
||||
config()->set('clusev.public_slug', 'clusev/clusev');
|
||||
Http::fake();
|
||||
|
||||
// Even though clusev.repository points at the private Gitea source (setUp), the project card
|
||||
// must link ONLY the public repo — a non-dev server never exposes the private source.
|
||||
Livewire::test(Index::class)->assertViewHas('repository', 'https://github.com/clusev/clusev');
|
||||
// Even though clusev.repository points at the private Gitea source (setUp), the card lists
|
||||
// ONLY the public repo — a non-dev server never exposes the private source.
|
||||
Livewire::test(Index::class)->assertViewHas('repositories', fn ($repos) => self::urls($repos) === [
|
||||
'https://github.com/clusev/clusev',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_non_dev_project_link_follows_the_public_slug(): void
|
||||
public function test_non_dev_public_link_follows_the_public_slug(): void
|
||||
{
|
||||
config()->set('clusev.release_controls', false);
|
||||
config()->set('clusev.public_slug', 'acme/pub');
|
||||
Http::fake();
|
||||
|
||||
Livewire::test(Index::class)->assertViewHas('repository', 'https://github.com/acme/pub');
|
||||
Livewire::test(Index::class)->assertViewHas('repositories', fn ($repos) => self::urls($repos) === [
|
||||
'https://github.com/acme/pub',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -343,13 +370,13 @@ class VersionUpdateCheckTest extends TestCase
|
|||
* (possibly private) URL.
|
||||
*/
|
||||
#[DataProvider('malformedSlugs')]
|
||||
public function test_non_dev_project_link_falls_back_for_a_malformed_slug(string $slug, string $expected): void
|
||||
public function test_non_dev_public_link_falls_back_for_a_malformed_slug(string $slug, string $expected): void
|
||||
{
|
||||
config()->set('clusev.release_controls', false);
|
||||
config()->set('clusev.public_slug', $slug);
|
||||
Http::fake();
|
||||
|
||||
Livewire::test(Index::class)->assertViewHas('repository', $expected);
|
||||
Livewire::test(Index::class)->assertViewHas('repositories', fn ($repos) => self::urls($repos) === [$expected]);
|
||||
}
|
||||
|
||||
/** @return array<string,array{0:string,1:string}> */
|
||||
|
|
|
|||
Loading…
Reference in New Issue