60 lines
1.9 KiB
PHP
60 lines
1.9 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
/**
|
||
* The editor plugins in `data/plugins/`: one ES module per `.js` file. Whether one runs is written
|
||
* in the project that is open, not here (#119) – the server hands out every plugin it has and knows
|
||
* nothing about switches. The folder is still not served statically: a module is only ever handed
|
||
* out by its bare file name, so no request can reach past it.
|
||
*/
|
||
final class PluginStore
|
||
{
|
||
public function __construct(private readonly string $dir)
|
||
{
|
||
}
|
||
|
||
/** A file name as it may be asked for: no path, no leading dot, `.js` at the end. */
|
||
public static function isValidName(string $name): bool
|
||
{
|
||
return (bool) preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]*\.js$/', $name);
|
||
}
|
||
|
||
/**
|
||
* Sorted by file name, which is also the order the editor activates them in.
|
||
*
|
||
* @return list<array{file: string, bytes: int, updatedAt: int}>
|
||
*/
|
||
public function list(): array
|
||
{
|
||
$files = is_dir($this->dir) ? scandir($this->dir) : [];
|
||
$out = [];
|
||
foreach ($files === false ? [] : $files as $file) {
|
||
if ($this->exists($file)) {
|
||
$path = "{$this->dir}/{$file}";
|
||
$out[] = [
|
||
'file' => $file,
|
||
'bytes' => (int) filesize($path),
|
||
'updatedAt' => (int) filemtime($path),
|
||
];
|
||
}
|
||
}
|
||
usort($out, static fn (array $a, array $b): int => strcmp($a['file'], $b['file']));
|
||
return $out;
|
||
}
|
||
|
||
/** The module's source, or null when there is no such plugin. */
|
||
public function read(string $file): ?string
|
||
{
|
||
if (!$this->exists($file)) {
|
||
return null;
|
||
}
|
||
$source = file_get_contents("{$this->dir}/{$file}");
|
||
return $source === false ? null : $source;
|
||
}
|
||
|
||
private function exists(string $file): bool
|
||
{
|
||
return self::isValidName($file) && is_file("{$this->dir}/{$file}");
|
||
}
|
||
}
|