The editor was only open to somebody with an account. A guest now picks
a demo project on the sign-in screen and gets the whole editor on a copy
held in the tab. The server writes nothing for a guest, so there is no
guest session, no guest folder and no quota to keep.
- Demo copies are frozen copies like a release, in data/demos/. An
administrator makes, updates and removes one with "Demo copy..." in
the project's main menu. GET demos and GET demos/{token} need no
session. An empty project is just a demo that is empty.
- Demo material: an administrator offers an uploaded file with "Offer
as demo" in the pickers (PATCH {demo}, data/demo-files.json). Every
list carries it with demo: true, for accounts and guests alike.
Signed out, a list holds the demo material alone, without the owner.
- An offered file is only an administrator's to delete. A file a demo
copy names counts as used, like one a release names.
- The guest's project lives in MemoryProjectStore. Every store that
would write refuses in the tab before a request is made, and the
autosave is not attached at all.
- The main menu drops projects, snapshots and release for a guest. The
pickers drop Upload and Delete, a dropped or pasted file is refused,
and a recording is saved as a download.
- The toolbar says the work is not saved. The Account menu holds
"Sign in..." instead of Logout and the password.
- A guest's demo never goes into the address: a reload brings the
sign-in screen back, not the copy. Found by e2e 70.
- GET plugins needs no session any more, since a demo may switch one
on. It only reads the server's own modules.
Tests:
- e2e 70 (atx): ALL PASSED, 13 checks. Counter-test with the guest
autosave sent to the server: 70f and 70g red on the PUT.
- tests/core/guest.test.ts: 9 tests. PHP: demos 20, files 46,
rights 80 checks (the matrix asks every new route).
- Gate (atx): CHECK EXIT 0, 314 files, 4852 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
63 lines
2.2 KiB
PHP
63 lines
2.2 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Uploaded files an administrator offers to everybody as demo material (#428): every account's
|
|
* picker lists them beside its own, and a guest's picker lists nothing else. The files stay where
|
|
* they were uploaded, in their owner's folder; this only says which of them are offered.
|
|
*
|
|
* data/demo-files.json { "textures": ["{folder}/{sha256}.png", …], "sounds": […], … }
|
|
*
|
|
* Written 0640 like everything that is not a file to hand out.
|
|
*/
|
|
final class DemoFiles
|
|
{
|
|
public const KINDS = ['textures', 'fonts', 'sounds', 'videos'];
|
|
|
|
public function __construct(private readonly string $path) {}
|
|
|
|
/** @return list<string> the offered files of one kind, in the order they were offered */
|
|
public function of(string $kind): array
|
|
{
|
|
$files = $this->read()[$kind] ?? [];
|
|
return array_values(array_filter($files, 'is_string'));
|
|
}
|
|
|
|
public function has(string $kind, string $file): bool
|
|
{
|
|
return in_array($file, $this->of($kind), true);
|
|
}
|
|
|
|
/** Offers a file or takes the offer back; answers whether that changed anything. */
|
|
public function set(string $kind, string $file, bool $offered): bool
|
|
{
|
|
if (!in_array($kind, self::KINDS, true)) {
|
|
throw new InvalidArgumentException("Unknown kind: {$kind}");
|
|
}
|
|
$all = $this->read();
|
|
$files = $this->of($kind);
|
|
$has = in_array($file, $files, true);
|
|
if ($has === $offered) {
|
|
return false;
|
|
}
|
|
$all[$kind] = $offered ? [...$files, $file] : array_values(array_diff($files, [$file]));
|
|
$json = json_encode($all, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
|
if ($json === false) {
|
|
throw new RuntimeException('Cannot encode the demo files');
|
|
}
|
|
AtomicFile::write($this->path, $json . "\n", AtomicFile::HIDDEN, 'demo file list');
|
|
return true;
|
|
}
|
|
|
|
/** @return array<string,list<string>> */
|
|
private function read(): array
|
|
{
|
|
if (!is_file($this->path)) {
|
|
return [];
|
|
}
|
|
$raw = file_get_contents($this->path);
|
|
$data = $raw === false ? null : json_decode($raw, true);
|
|
return is_array($data) ? $data : [];
|
|
}
|
|
}
|