<?php
abstract class BootEntry
{
/** Supports both via same entry (stored in PCBIOS entry) */
const AGNOSTIC = 'agnostic';
/** Only valid for legacy BIOS boot */
const BIOS = 'PCBIOS';
/** Only valid for EFI boot */
const EFI = 'EFI';
/** Supports both via distinct entry */
const BOTH = 'PCBIOS-EFI';
/**
* @var string Internal ID - set to your liking, e.g. the MiniLinux version identifier
*/
protected $internalId;
public function __construct(string $internalId)
{
$this->internalId = $internalId;
}
public abstract function supportsMode(string $mode): bool;
public abstract function toScript(ScriptBuilderBase $builder): string;
public abstract function toArray(): array;
public abstract function addFormFields(array &$array): void;
public function internalId(): string
{
return $this->internalId;
}
/*
*
*/
/**
* Return a BootEntry instance from the serialized data.
*
* @param string $module module this entry belongs to, or special values .script/.exec
* @param string $data serialized entry data
* @return ?BootEntry instance representing boot entry, null on error
*/
public static function fromJson(string $module, string $data): ?BootEntry
{
if ($module[0] !== '.') {
// Hook from other module
$hook = Hook::loadSingle($module, 'ipxe-bootentry');
if ($hook === null) {
error_log('Module ' . $module . ' doesnt have an ipxe-bootentry hook');
return null;
}
$ret = $hook->run();
if (!($ret instanceof BootEntryHook))
return null;
return $ret->getBootEntry($data);
}
$data = json_decode($data, true);
if (!is_array($data))
return null;
if ($module === '.script') {
return new CustomBootEntry($data);
}
if ($module === '.exec') {
return new StandardBootEntry($data);
}
if ($module === '.special') {
return new SpecialBootEntry($data);
}
return null;
}
public static function forMenu(int $menuId): MenuBootEntry
{
return new MenuBootEntry($menuId);
}
public static function newStandardBootEntry($initData, $efi = false, $arch = false, string $internalId = ''): ?StandardBootEntry
{
$ret = new StandardBootEntry($initData, $efi, $arch, $internalId);
$list = [];
if ($ret->arch() !== self::EFI) {
$list[] = self::BIOS;
}
if ($ret->arch() === self::EFI || $ret->arch() === self::BOTH) {
$list[] = self::EFI;
}
$data = $ret->toArray();
foreach ($list as $mode) {
if (empty($data[$mode]['executable'])) {
error_log('Incomplete stdbot: ' . print_r($initData, true));
return null;
}
}
return $ret;
}
public static function newCustomBootEntry($initData): ?CustomBootEntry
{
if (!is_array($initData) || empty($initData))
return null;
return new CustomBootEntry($initData);
}
/**
* Return a BootEntry instance from database with the given id.
*
* @return ?BootEntry null = unknown entry type, BootEntry instance on success
*/
public static function fromDatabaseId(string $id): ?BootEntry
{
$row = Database::queryFirst("SELECT module, data FROM serversetup_bootentry
WHERE entryid = :id LIMIT 1", ['id' => $id]);
if ($row === false)
return null;
return self::fromJson($row['module'], $row['data']);
}
/**
* Get all existing BootEntries from database, skipping those of
* unknown type. Returned array is assoc, key is entryid
*
* @return BootEntry[] all existing BootEntries
*/
public static function getAll(): array
{
$res = Database::simpleQuery("SELECT entryid, module, data FROM serversetup_bootentry");
$ret = [];
foreach ($res as $row) {
$tmp = self::fromJson($row['module'], $row['data']);
if ($tmp === null)
continue;
$ret[$row['entryid']] = $tmp;
}
return $ret;
}
}
class StandardBootEntry extends BootEntry
{
/**
* @var ExecData PCBIOS boot data
*/
protected $pcbios;
/**
* @var ExecData same for EFI
*/
protected $efi;
/**
* @var ?string BootEntry Constants above
*/
protected $arch;
const KEYS = ['executable', 'initRd', 'commandLine', 'replace', 'imageFree', 'autoUnload', 'resetConsole', 'dhcpOptions'];
public function __construct($data, $efi = false, ?string $arch = null, string $internalId = '')
{
parent::__construct($internalId);
$this->pcbios = new ExecData();
$this->efi = new ExecData();
if ($data instanceof PxeSection) {
// Import from PXELINUX menu
$this->fromPxeMenu($data);
} elseif ($data instanceof ExecData && is_string($arch)) {
if (!($efi instanceof ExecData)) {
$efi = new ExecData();
}
$this->pcbios = $data;
$this->efi = $efi;
$this->arch = $arch;
} elseif (is_array($data)) {
// Serialized data
if (!isset($data['arch'])) {
error_log('Serialized data to StandardBootEntry doesnt contain arch: ' . json_encode($data));
} else {
$this->arch = $data['arch'];
}
if (isset($data[BootEntry::BIOS]) || isset($data[BootEntry::EFI])) {
// Current format
$this->fromCurrentFormat($data);
} else {
// Convert legacy DB format
$this->fromLegacyFormat($data);
}
} elseif ($arch == BootEntry::EFI && $efi instanceof ExecData) {
$this->efi = $efi;
$this->arch = $arch;
} else {
error_log('Invalid StandardBootEntry constructor call');
}
if (!in_array($this->arch, [BootEntry::BIOS, BootEntry::EFI, BootEntry::BOTH, BootEntry::AGNOSTIC])) {
$this->arch = BootEntry::AGNOSTIC;
}
}
private function fromLegacyFormat($data)
{
$ok = false;
foreach (self::KEYS as $key) {
if (isset($data[$key][BootEntry::BIOS])) {
$this->pcbios->{$key} = $data[$key][BootEntry::BIOS];
$ok = true;
}
if (isset($data[$key][BootEntry::EFI])) {
$this->efi->{$key} = $data[$key][BootEntry::EFI];
$ok = true;
}
}
if (!$ok) {
// Very old entry
foreach (self::KEYS as $key) {
if (isset($data[$key])) {
$this->pcbios->{$key} = $data[$key];
}
}
}
}
private function fromCurrentFormat($data)
{
foreach (self::KEYS as $key) {
if (isset($data[BootEntry::BIOS][$key])) {
$this->pcbios->{$key} = $data[BootEntry::BIOS][$key];
}
if (isset($data[BootEntry::EFI][$key])) {
$this->efi->{$key} = $data[BootEntry::EFI][$key];
}
}
}
private function fromPxeMenu(PxeSection $data): void
{
$bios = $this->pcbios;
$bios->executable = $data->kernel;
$bios->initRd = $data->initrd;
$bios->commandLine = ' ' . str_replace('vga=current', '', $data->append) . ' ';
$bios->resetConsole = true;
$bios->replace = true;
$bios->autoUnload = true;
if (strpos($bios->commandLine, ' quiet ') !== false) {
$bios->commandLine .= ' loglevel=5 rd.systemd.show_status=auto';
}
if ($data->ipAppend & 1) {
$bios->commandLine .= ' ${ipappend1}';
}
if ($data->ipAppend & 2) {
$bios->commandLine .= ' ${ipappend2}';
}
if ($data->ipAppend & 4) {
$bios->commandLine .= ' SYSUUID=${uuid}';
}
$bios->commandLine = trim(preg_replace('/\s+/', ' ', $bios->commandLine));
}
public function arch(): ?string
{
return $this->arch;
}
public function supportsMode(string $mode): bool
{
if ($mode === $this->arch || $this->arch === BootEntry::AGNOSTIC)
return true;
if ($mode === BootEntry::BIOS || $mode === BootEntry::EFI) {
return $this->arch === BootEntry::BOTH;
}
error_log('Unknown iPXE platform: ' . $mode);
return false;
}
public function toScript(ScriptBuilderBase $builder): string
{
if ($this->arch === BootEntry::AGNOSTIC) // Same as below, could construct fall-through but this is more clear
return $builder->execDataToScript($this->pcbios, null, null);
return $builder->execDataToScript(null,
$this->supportsMode(BootEntry::BIOS) ? $this->pcbios : null,
$this->supportsMode(BootEntry::EFI) ? $this->efi : null);
}
public function addFormFields(array &$array): void
{
$array[$this->arch . '_selected'] = 'selected';
$array['entries'][] = $this->pcbios->toFormFields(BootEntry::BIOS);
$array['entries'][] = $this->efi->toFormFields(BootEntry::EFI);
$array['exec_checked'] = 'checked';
}
/**
* @return array{PCBIOS: array, EFI: array, arch: string}
*/
public function toArray(): array
{
return [
BootEntry::BIOS => $this->pcbios->toArray(),
BootEntry::EFI => $this->efi->toArray(),
'arch' => $this->arch,
];
}
}
class CustomBootEntry extends BootEntry
{
/**
* @var string iPXE
*/
protected $ipxe = '';
protected $bash;
protected $grub;
public function __construct($data)
{
parent::__construct('custom');
if (is_array($data)) {
$this->ipxe = $data['script'] ?? ''; // LEGACY
foreach (['bash', 'grub'] as $key) {
$this->{$key} = $data[$key] ?? '';
}
}
}
public function supportsMode(string $mode): bool
{
return true;
}
public function toScript(ScriptBuilderBase $builder): string
{
// TODO: A (very) simple translator for oneliners like "poweroff || goto fail" maybe?
if ($builder instanceof ScriptBuilderIpxe)
return $this->ipxe;
if ($builder instanceof ScriptBuilderBash)
return $this->bash;
if ($builder instanceof ScriptBuilderGrub)
return $this->grub;
return '';
}
public function addFormFields(array &$array): void
{
$array['entry'] = [
'script' => $this->ipxe,
];
$array['script_checked'] = 'checked';
}
/**
* @return array{script: string}
*/
public function toArray(): array
{
return ['script' => $this->ipxe];
}
}
class MenuBootEntry extends BootEntry
{
/** @var int */
protected $menuId;
public function __construct(int $menuId)
{
parent::__construct('menu-' . $menuId);
$this->menuId = $menuId;
}
public function supportsMode(string $mode): bool
{
return true;
}
public function toScript(ScriptBuilderBase $builder): string
{
$menu = IPxeMenu::get($this->menuId, true);
return $builder->menuToScript($menu);
}
public function toArray(): array
{
return [];
}
public function addFormFields(array &$array): void
{
}
}
class SpecialBootEntry extends BootEntry
{
private $type;
public function __construct($type)
{
$this->type = $type['type'] ?? $type;
parent::__construct('special-' . $this->type);
}
public function supportsMode(string $mode): bool
{
return true;
}
public function toScript(ScriptBuilderBase $builder): string
{
return $builder->getSpecial($this->type);
}
public function toArray(): array
{
return [];
}
public function addFormFields(array &$array): void { }
}