A share can now hold a private text (a password, a key, a short note) with its files or on its own. The upload page's new "Private text" card takes up to 100 KB; the browser encrypts the text and sends it through the same chunk pipeline as a file, flagged is_text on share_files, so it gets the share's password, expiry, download limit and cleanup. The recipient sees the text only after pressing "Show text", which counts as their download, so a messenger link preview cannot use up a share limited to one download. The text is left out of the file list, the ZIP and the file counts; the admin dashboard marks shares that hold one with "Text". The website gains a Private text feature card and a fifth phone screenshot; every screenshot is retaken. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
131 lines
5.0 KiB
PHP
131 lines
5.0 KiB
PHP
<?php
|
|
|
|
/*
|
|
* website/ is uploaded by hand as it is, so these guard what a broken upload would show: files that
|
|
* are not there, requests to other hosts, and claims the application does not keep.
|
|
*/
|
|
|
|
/**
|
|
* The site's HTML pages, by name.
|
|
*
|
|
* @return array<string, string>
|
|
*/
|
|
function websitePages(): array
|
|
{
|
|
return [
|
|
'index.html' => file_get_contents(base_path('website/index.html')),
|
|
'privacy.html' => file_get_contents(base_path('website/privacy.html')),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Every URL an HTML document refers to in `src`, `href`, `srcset` and the gallery's `data-*-src` and
|
|
* `data-*-srcset`, with each srcset entry on its own.
|
|
*
|
|
* @return list<string>
|
|
*/
|
|
function websiteReferences(string $html): array
|
|
{
|
|
preg_match_all('/\s(src|href|srcset|data-[a-z]+-src|data-[a-z]+-srcset)="([^"]*)"/', $html, $matches, PREG_SET_ORDER);
|
|
|
|
return collect($matches)
|
|
->flatMap(fn (array $match): array => str_ends_with($match[1], 'srcset')
|
|
? array_map(fn (string $candidate): string => strtok(trim($candidate), ' '), explode(',', $match[2]))
|
|
: [$match[2]])
|
|
->map(fn (string $url): string => html_entity_decode($url))
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
test('every file the pages, their stylesheets and the README refer to exists', function () {
|
|
$missing = [];
|
|
|
|
foreach (websitePages() as $page => $html) {
|
|
foreach (websiteReferences($html) as $url) {
|
|
if (preg_match('/^(#|https?:|mailto:)/', $url) === 1) {
|
|
continue;
|
|
}
|
|
|
|
if (! is_file(base_path('website/'.strtok($url, '#?')))) {
|
|
$missing[] = "{$page}: {$url}";
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (glob(base_path('website/css/*.css')) as $stylesheet) {
|
|
preg_match_all('/url\([\'"]?([^\'")]+)[\'"]?\)/', file_get_contents($stylesheet), $matches);
|
|
|
|
foreach ($matches[1] as $url) {
|
|
if (! str_starts_with($url, 'data:') && ! is_file(dirname($stylesheet).'/'.strtok($url, '#?'))) {
|
|
$missing[] = basename($stylesheet).": {$url}";
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (websiteReferences(file_get_contents(base_path('README.md'))) as $url) {
|
|
if (str_starts_with($url, 'website/') && ! is_file(base_path($url))) {
|
|
$missing[] = "README.md: {$url}";
|
|
}
|
|
}
|
|
|
|
expect($missing)->toBe([]);
|
|
});
|
|
|
|
test('the screenshots are all there, for both themes and both widths', function () {
|
|
$expected = collect([
|
|
'desktop' => ['01-upload', '02-share-created', '03-qr-code', '04-download', '05-admin-dashboard', '06-admin-settings'],
|
|
'phone' => ['01-upload', '02-password', '03-download', '04-qr-code', '05-private-text'],
|
|
])->flatMap(fn (array $names, string $device): array => collect(['light', 'dark'])
|
|
->crossJoin($names, $device === 'desktop' ? [1600, 800] : [1080, 540])
|
|
->map(fn (array $shot): string => "{$device}/{$shot[0]}/{$shot[1]}-{$shot[2]}.webp")
|
|
->all())
|
|
->sort()->values()->all();
|
|
|
|
$published = collect(glob(base_path('website/img/screenshots/*/*/*.webp')))
|
|
->map(fn (string $path): string => str($path)->after('website/img/screenshots/')->toString())
|
|
->sort()->values()->all();
|
|
|
|
expect($published)->toBe($expected);
|
|
});
|
|
|
|
test('the pages load nothing from another host but Plausible', function () {
|
|
foreach (websitePages() as $page => $html) {
|
|
preg_match_all('/<(?:script|img|source|link)\b[^>]*\s(?:src|srcset|href)="(https?:[^"]*)"/', $html, $matches);
|
|
|
|
expect(collect($matches[1])->reject(fn (string $url): bool => str_starts_with($url, 'https://plausible.io/'))->all())
|
|
->toBe([], "{$page} loads from another host");
|
|
}
|
|
|
|
foreach (glob(base_path('website/css/*.css')) as $stylesheet) {
|
|
expect(file_get_contents($stylesheet))->not->toMatch('/(url\(\s*[\'"]?https?:|@import)/');
|
|
}
|
|
});
|
|
|
|
test('the pages are titled, described and in English', function () {
|
|
foreach (websitePages() as $page => $html) {
|
|
expect($html)
|
|
->toContain('<html lang="en"')
|
|
->toMatch('/<title>[^<]+<\/title>/')
|
|
->toMatch('/<meta name="description" content="[^"]+"/')
|
|
->toContain('<script async src="https://plausible.io/js/pa-zWB4R2-rrBeLfwms3uIBT.js"></script>')
|
|
->toContain('plausible.init()');
|
|
}
|
|
});
|
|
|
|
test('neither the site nor the README calls the encryption end-to-end', function () {
|
|
$index = websitePages()['index.html'];
|
|
|
|
preg_match('/<section[^>]*id="features".*?<\/section>/s', $index, $features);
|
|
preg_match_all('/data-compare="sealshare-e2e">([^<]*)</', $index, $cells);
|
|
|
|
expect($features[0])->not->toMatch('/end-to-end/i')
|
|
->and($cells[1])->not->toBeEmpty()->each->toBe('No')
|
|
->and(file_get_contents(base_path('README.md')))->not->toContain('End-to-End Encryption');
|
|
});
|
|
|
|
test('nothing is left to fill in', function () {
|
|
foreach ([...websitePages(), 'theme.css' => file_get_contents(base_path('website/css/theme.css'))] as $file => $contents) {
|
|
expect(str_contains($contents, 'TODO'))->toBeFalse("{$file} still has a TODO");
|
|
}
|
|
});
|