53 lines
1.8 KiB
PHP
53 lines
1.8 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
/**
|
||
* The editor plugins in `data/plugins/`: one ES module per `.js` file, and every file there is
|
||
* active. Read-only for now – putting a file there is done by hand, by the build and by the e2e
|
||
* seeder. The folder is 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) {
|
||
$path = "{$this->dir}/{$file}";
|
||
if (!self::isValidName($file) || !is_file($path)) {
|
||
continue;
|
||
}
|
||
$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
|
||
{
|
||
$path = "{$this->dir}/{$file}";
|
||
if (!self::isValidName($file) || !is_file($path)) {
|
||
return null;
|
||
}
|
||
$source = file_get_contents($path);
|
||
return $source === false ? null : $source;
|
||
}
|
||
}
|