diff options
Diffstat (limited to 'modules-available/serversetup-bwlp-ipxe/inc')
15 files changed, 985 insertions, 534 deletions
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php index 614f5ee4..5812c0cd 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php @@ -12,17 +12,28 @@ abstract class BootEntry /** Supports both via distinct entry */ const BOTH = 'PCBIOS-EFI'; - public abstract function supportsMode($mode); - /** - * @param ScriptBuilderBase $builder - * @return string + * @var string Internal ID - set to your liking, e.g. the MiniLinux version identifier */ - public abstract function toScript($builder); + protected $internalId; + + public function __construct(string $internalId) + { + $this->internalId = $internalId; + } + + public abstract function supportsMode(string $mode): bool; - public abstract function toArray(); + public abstract function toScript(ScriptBuilderBase $builder): string; - public abstract function addFormFields(&$array); + public abstract function toArray(): array; + + public abstract function addFormFields(array &$array): void; + + public function internalId(): string + { + return $this->internalId; + } /* * @@ -32,15 +43,15 @@ abstract class BootEntry * Return a BootEntry instance from the serialized data. * * @param string $module module this entry belongs to, or special values .script/.exec - * @param string $jsonString serialized entry data - * @return BootEntry|null instance representing boot entry, null on error + * @param string $data serialized entry data + * @return ?BootEntry instance representing boot entry, null on error */ - public static function fromJson($module, $data) + public static function fromJson(string $module, string $data): ?BootEntry { - if ($module{0} !== '.') { + if ($module[0] !== '.') { // Hook from other module $hook = Hook::loadSingle($module, 'ipxe-bootentry'); - if ($hook === false) { + if ($hook === null) { error_log('Module ' . $module . ' doesnt have an ipxe-bootentry hook'); return null; } @@ -49,9 +60,9 @@ abstract class BootEntry return null; return $ret->getBootEntry($data); } - if (is_string($data)) { - $data = json_decode($data, true); - } + $data = json_decode($data, true); + if (!is_array($data)) + return null; if ($module === '.script') { return new CustomBootEntry($data); } @@ -64,14 +75,14 @@ abstract class BootEntry return null; } - public static function forMenu($menuId) + public static function forMenu(int $menuId): MenuBootEntry { return new MenuBootEntry($menuId); } - public static function newStandardBootEntry($initData, $efi = false, $arch = false) + public static function newStandardBootEntry($initData, $efi = false, $arch = false, string $internalId = ''): ?StandardBootEntry { - $ret = new StandardBootEntry($initData, $efi, $arch); + $ret = new StandardBootEntry($initData, $efi, $arch, $internalId); $list = []; if ($ret->arch() !== self::EFI) { $list[] = self::BIOS; @@ -89,7 +100,7 @@ abstract class BootEntry return $ret; } - public static function newCustomBootEntry($initData) + public static function newCustomBootEntry($initData): ?CustomBootEntry { if (!is_array($initData) || empty($initData)) return null; @@ -99,15 +110,14 @@ abstract class BootEntry /** * Return a BootEntry instance from database with the given id. * - * @param string $id - * @return BootEntry|null|false false == unknown id, null = unknown entry type, BootEntry instance on success + * @return ?BootEntry null = unknown entry type, BootEntry instance on success */ - public static function fromDatabaseId($id) + 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 false; + return null; return self::fromJson($row['module'], $row['data']); } @@ -117,11 +127,11 @@ abstract class BootEntry * * @return BootEntry[] all existing BootEntries */ - public static function getAll() + public static function getAll(): array { - $res = Database::simpleQuery("SELECT entryid, data FROM serversetup_bootentry"); + $res = Database::simpleQuery("SELECT entryid, module, data FROM serversetup_bootentry"); $ret = []; - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { $tmp = self::fromJson($row['module'], $row['data']); if ($tmp === null) continue; @@ -143,14 +153,15 @@ class StandardBootEntry extends BootEntry */ protected $efi; /** - * @var string BootEntry Constants above + * @var ?string BootEntry Constants above */ protected $arch; const KEYS = ['executable', 'initRd', 'commandLine', 'replace', 'imageFree', 'autoUnload', 'resetConsole', 'dhcpOptions']; - public function __construct($data, $efi = false, $arch = false) + 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) { @@ -223,10 +234,7 @@ class StandardBootEntry extends BootEntry } } - /** - * @param PxeSection $data - */ - private function fromPxeMenu($data) + private function fromPxeMenu(PxeSection $data): void { $bios = $this->pcbios; $bios->executable = $data->kernel; @@ -250,12 +258,12 @@ class StandardBootEntry extends BootEntry $bios->commandLine = trim(preg_replace('/\s+/', ' ', $bios->commandLine)); } - public function arch() + public function arch(): ?string { return $this->arch; } - public function supportsMode($mode) + public function supportsMode(string $mode): bool { if ($mode === $this->arch || $this->arch === BootEntry::AGNOSTIC) return true; @@ -266,7 +274,7 @@ class StandardBootEntry extends BootEntry return false; } - public function toScript($builder) + 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); @@ -275,7 +283,7 @@ class StandardBootEntry extends BootEntry $this->supportsMode(BootEntry::EFI) ? $this->efi : null); } - public function addFormFields(&$array) + public function addFormFields(array &$array): void { $array[$this->arch . '_selected'] = 'selected'; $array['entries'][] = $this->pcbios->toFormFields(BootEntry::BIOS); @@ -283,7 +291,10 @@ class StandardBootEntry extends BootEntry $array['exec_checked'] = 'checked'; } - public function toArray() + /** + * @return array{PCBIOS: array, EFI: array, arch: string} + */ + public function toArray(): array { return [ BootEntry::BIOS => $this->pcbios->toArray(), @@ -298,7 +309,7 @@ class CustomBootEntry extends BootEntry /** * @var string iPXE */ - protected $ipxe; + protected $ipxe = ''; protected $bash; @@ -306,6 +317,7 @@ class CustomBootEntry extends BootEntry public function __construct($data) { + parent::__construct('custom'); if (is_array($data)) { $this->ipxe = $data['script'] ?? ''; // LEGACY foreach (['bash', 'grub'] as $key) { @@ -314,21 +326,24 @@ class CustomBootEntry extends BootEntry } } - public function supportsMode($mode) + public function supportsMode(string $mode): bool { return true; } - public function toScript($builder) + 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) + public function addFormFields(array &$array): void { $array['entry'] = [ 'script' => $this->ipxe, @@ -336,7 +351,10 @@ class CustomBootEntry extends BootEntry $array['script_checked'] = 'checked'; } - public function toArray() + /** + * @return array{script: string} + */ + public function toArray(): array { return ['script' => $this->ipxe]; } @@ -344,30 +362,32 @@ class CustomBootEntry extends BootEntry class MenuBootEntry extends BootEntry { + /** @var int */ protected $menuId; - public function __construct($menuId) + public function __construct(int $menuId) { + parent::__construct('menu-' . $menuId); $this->menuId = $menuId; } - public function supportsMode($mode) + public function supportsMode(string $mode): bool { return true; } - public function toScript($builder) + public function toScript(ScriptBuilderBase $builder): string { - $menu = IPxeMenu::get($this->menuId); + $menu = IPxeMenu::get($this->menuId, true); return $builder->menuToScript($menu); } - public function toArray() + public function toArray(): array { return []; } - public function addFormFields(&$array) + public function addFormFields(array &$array): void { } } @@ -380,23 +400,24 @@ class SpecialBootEntry extends BootEntry public function __construct($type) { $this->type = $type['type'] ?? $type; + parent::__construct('special-' . $this->type); } - public function supportsMode($mode) + public function supportsMode(string $mode): bool { return true; } - public function toScript($builder) + public function toScript(ScriptBuilderBase $builder): string { return $builder->getSpecial($this->type); } - public function toArray() + public function toArray(): array { return []; } - public function addFormFields(&$array) { } + public function addFormFields(array &$array): void { } -}
\ No newline at end of file +} diff --git a/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php index 73611b0a..ab55c888 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php @@ -6,41 +6,37 @@ abstract class BootEntryHook /** * @var string -- set by ipxe, not module implementing hook */ - public $moduleId; + public $moduleId = ''; /** * @var string -- set by ipxe, not module implementing hook */ - public $checked; + public $checked = ''; - private $selectedId; + private $selectedId = ''; private $data = []; /** * @return string */ - public abstract function name(); + public abstract function name(): string; /** * @return HookExtraField[] */ - public abstract function extraFields(); + public abstract function extraFields(): array; - /** - * @param string $id - * @return bool - */ - public abstract function isValidId($id); + public abstract function isValidId(string $id): bool; /** * @return HookEntryGroup[] */ - protected abstract function groupsInternal(); + protected abstract function groupsInternal(): array; /** * @return HookEntryGroup[] */ - public final function groups() + public final function groups(): array { $groups = $this->groupsInternal(); foreach ($groups as $group) { @@ -54,16 +50,13 @@ abstract class BootEntryHook } /** - * @param $id * @return BootEntry|null the actual boot entry instance for given entry, null if invalid id */ - public abstract function getBootEntryInternal($localData); + public abstract function getBootEntryInternal(array $localData): ?BootEntry; - public final function getBootEntry($data) + public final function getBootEntry(string $jsonString): ?BootEntry { - if (!is_array($data)) { - $data = json_decode($data, true); - } + $data = json_decode($jsonString, true); return $this->getBootEntryInternal($data); } @@ -71,7 +64,7 @@ abstract class BootEntryHook * @param string $mixed either the plain ID if the entry to be marked as selected, or the JSON string representing * the entire entry, which must have a key called 'id' that will be used as the ID then. */ - public function setSelected($mixed) + public function setSelected(string $mixed): void { $json = @json_decode($mixed, true); if (is_array($json)) { @@ -86,16 +79,19 @@ abstract class BootEntryHook /** * @return string ID of entry that was marked as selected by setSelected() */ - public function getSelected() + public function getSelected(): string { return $this->selectedId; } - public function renderExtraFields() + /** + * @return HookExtraField[] + */ + public function renderExtraFields(): array { $list = $this->extraFields(); - foreach ($list as &$entry) { - $entry->currentValue = isset($this->data[$entry->name]) ? $this->data[$entry->name] : $entry->default; + foreach ($list as $entry) { + $entry->currentValue = $this->data[$entry->name] ?? $entry->default; $entry->hook = $this; } return $list; @@ -144,14 +140,7 @@ class HookEntry */ public $selected; - /** - * HookEntry constructor. - * - * @param string $id - * @param string $name - * @param bool $valid - */ - public function __construct($id, $name, $valid) + public function __construct(string $id, string $name, bool $valid) { $this->id = $id; $this->name = $name; @@ -182,7 +171,7 @@ class HookExtraField */ public $hook; - public function __construct($name, $type, $default) + public function __construct(string $name, string $type, $default) { $this->name = $name; $this->type = $type; @@ -203,10 +192,10 @@ class HookExtraField return $val; } - public function html() + public function html(): string { $fieldId = 'extra-' . $this->hook->moduleId . '-' . $this->name; - $fieldText = htmlspecialchars(Dictionary::translateFileModule($this->hook->moduleId, 'module', 'ipxe-' . $this->name, true)); + $fieldText = htmlspecialchars(Dictionary::translateFileModule($this->hook->moduleId, 'module', 'ipxe-' . $this->name)); if (is_array($this->type)) { $out = '<label for="' . $fieldId . '">' . $fieldText . '</label><select class="form-control" name="' . $fieldId . '" id="' . $fieldId . '">'; foreach ($this->type as $entry) { diff --git a/modules-available/serversetup-bwlp-ipxe/inc/execdata.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/execdata.inc.php index 1f6fa265..e4f7a1d7 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/execdata.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/execdata.inc.php @@ -108,7 +108,10 @@ class ExecData $this->dhcpOptions = array_values($this->dhcpOptions); } - public function toArray() + /** + * @return array{executable: string, initRd: string[], commandLine: string, imageFree: bool, replace: bool, autoUnload: bool, resetConsole: bool, dhcpOptions: array} + */ + public function toArray(): array { $this->sanitize(); return [ @@ -123,7 +126,7 @@ class ExecData ]; } - public function toFormFields($arch) + public function toFormFields(string $arch): array { $this->sanitize(); $opts = []; diff --git a/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php index 29885588..5e0531ab 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php @@ -12,13 +12,13 @@ class IPxe * Import all IP-Range based pxe menus from the given directory. * * @param string $configPath The pxelinux.cfg path where to look for menu files in hexadecimal IP format. - * @return Number of menus imported + * @return int Number of menus imported */ - public static function importSubnetPxeMenus($configPath) + public static function importSubnetPxeMenus(string $configPath): int { $res = Database::simpleQuery('SELECT menuid, entryid FROM serversetup_menuentry ORDER BY sortval ASC'); $menus = []; - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { if (!isset($menus[$row['menuid']])) { $menus[(int)$row['menuid']] = []; } @@ -40,7 +40,7 @@ class IPxe WHERE startaddr >= :start AND endaddr <= :end", compact('start', 'end')); $locations = []; // Iterate over result, eliminate those that are dominated by others - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { foreach ($locations as &$loc) { if ($row['startaddr'] <= $loc['startaddr'] && $row['endaddr'] >= $loc['endaddr']) { $loc = false; @@ -52,6 +52,10 @@ class IPxe $locations[] = $row; } $menu = PxeLinux::parsePxeLinux($content, true); + if ($menu === null) { + error_log("Skipping empty pxelinux menu file $file"); + continue; + } // Insert all entries first, so we can get the list of entry IDs $entries = []; self::importPxeMenuEntries($menu, $entries); @@ -83,10 +87,10 @@ class IPxe } else { error_log('Imported menu ' . $menu->title . ' is NEW, using for ' . count($locations) . ' locations.'); // Insert new menu - $menuId = self::insertMenu($menu, 'Auto Imported', false, 0, [], []); - if ($menuId === false) + $menuId = self::insertMenu($menu, 'Auto Imported', null, 0, [], []); + if ($menuId === null) continue; - $menus[(int)$menuId] = $entries; + $menus[$menuId] = $entries; $importCount++; } foreach ($locations as $loc) { @@ -103,20 +107,20 @@ class IPxe return $importCount; } - public static function importLegacyMenu($force = false) + public static function importLegacyMenu(bool $force = false): bool { // See if anything is there if (!$force && false !== Database::queryFirst("SELECT menuentryid FROM serversetup_menuentry LIMIT 1")) return false; // Already exists // Now create the default entry self::createDefaultEntries(); - $prepend = ['bwlp-default' => false, 'localboot' => false]; + $prepend = ['bwlp-default' => null, 'localboot' => null]; $defaultLabel = 'bwlp-default'; $menuTitle = 'bwLehrpool Bootauswahl'; $pxeConfig = ''; $timeoutSecs = 60; - // Try to import any customization - $oldMenu = Property::getBootMenu(); + // Try to import any customization of the legacy PXELinux menu (despite the property name hinting at iPXE) + $oldMenu = json_decode(Property::get('ipxe-menu'), true); if (is_array($oldMenu)) { // if (isset($oldMenu['timeout'])) { @@ -136,47 +140,45 @@ class IPxe } } $append = [ - '', - 'bwlp-default-dbg' => false, - '', - 'poweroff' => false, + new PxeSection(null), + 'bwlp-default-dbg' => null, + new PxeSection(null), + 'poweroff' => null, ]; - return self::insertMenu(PxeLinux::parsePxeLinux($pxeConfig, false), $menuTitle, $defaultLabel, $timeoutSecs, $prepend, $append); + self::insertMenu(PxeLinux::parsePxeLinux($pxeConfig, false), $menuTitle, $defaultLabel, $timeoutSecs, $prepend, $append); + return !empty($pxeConfig); } /** - * @param PxeMenu $pxeMenu - * @param string $menuTitle - * @param string|false $defaultLabel Fallback for the default label, if PxeMenu doesn't set one + * @param ?string $defaultLabel Fallback for the default label, if PxeMenu doesn't set one * @param int $defaultTimeoutSeconds Default timeout, if PxeMenu doesn't set one - * @param array $prepend - * @param array $append - * @return int|false + * @param (?PxeSection)[] $prepend + * @param (?PxeSection)[] $append + * @return ?int ID of newly created menu, or null on error, e.g. if the menu is empty */ - public static function insertMenu($pxeMenu, $menuTitle, $defaultLabel, $defaultTimeoutSeconds, $prepend, $append) + public static function insertMenu(?PxeMenu $pxeMenu, string $menuTitle, ?string $defaultLabel, int $defaultTimeoutSeconds, + array $prepend, array $append): ?int { $timeoutMs = []; $menuEntries = $prepend; - settype($menuEntries, 'array'); - if (!empty($pxeMenu)) { - $pxe =& $pxeMenu; - if (!empty($pxe->title)) { - $menuTitle = $pxe->title; + if ($pxeMenu !== null) { + if (!empty($pxeMenu->title)) { + $menuTitle = $pxeMenu->title; } - if ($pxe->timeoutLabel !== null && $pxe->hasLabel($pxe->timeoutLabel)) { - $defaultLabel = $pxe->timeoutLabel; - } elseif ($pxe->hasLabel($pxe->default)) { - $defaultLabel = $pxe->default; + if ($pxeMenu->timeoutLabel !== null && $pxeMenu->hasLabel($pxeMenu->timeoutLabel)) { + $defaultLabel = $pxeMenu->timeoutLabel; + } elseif ($pxeMenu->hasLabel($pxeMenu->default)) { + $defaultLabel = $pxeMenu->default; } - $timeoutMs[] = $pxe->timeoutMs; - $timeoutMs[] = $pxe->totalTimeoutMs; - self::importPxeMenuEntries($pxe, $menuEntries); + $timeoutMs[] = $pxeMenu->timeoutMs; + $timeoutMs[] = $pxeMenu->totalTimeoutMs; + self::importPxeMenuEntries($pxeMenu, $menuEntries); } - if (is_array($append)) { + if (!empty($append)) { $menuEntries += $append; } if (empty($menuEntries)) - return false; + return null; // Make menu $timeoutMs = array_filter($timeoutMs, function($x) { return is_int($x) && $x > 0; }); if (empty($timeoutMs)) { @@ -195,25 +197,29 @@ class IPxe // Figure out entryid for default label // Fiddly diddly way of getting the mangled entryid for the wanted pxe menu label $defaultEntryId = false; + $fallbackDefault = false; foreach ($menuEntries as $entryId => $section) { - if ($section instanceof PxeSection) { - if ($section->isDefault) { - $defaultEntryId = $entryId; - break; - } - if ($section->label === $defaultLabel) { - $defaultEntryId = $entryId; - } + if ($section === null) + continue; + if ($section->isDefault) { + $defaultEntryId = $entryId; + break; + } + if ($section->label === $defaultLabel) { + $defaultEntryId = $entryId; + } + if ($fallbackDefault === false && !empty($entryId)) { + $fallbackDefault = $entryId; } } if ($defaultEntryId === false) { - $defaultEntryId = array_keys($menuEntries)[0]; + $defaultEntryId = $fallbackDefault; } // Link boot entries to menu $defaultMenuEntryId = null; $order = 1000; foreach ($menuEntries as $entryId => $entry) { - if (is_string($entry)) { + if ($entry !== null && $entry->isTextOnly()) { // Gap entry Database::exec("INSERT INTO serversetup_menuentry (menuid, entryid, hotkey, title, hidden, sortval, plainpass, md5pass) @@ -221,7 +227,7 @@ class IPxe 'menuid' => $menuId, 'entryid' => null, 'hotkey' => '', - 'title' => self::sanitizeIpxeString($entry), + 'title' => self::sanitizeIpxeString($entry->title), 'hidden' => 0, 'sortval' => $order += 100, ]); @@ -232,7 +238,7 @@ class IPxe continue; $data['pass'] = ''; $data['hidden'] = 0; - if ($entry instanceof PxeSection) { + if ($entry !== null) { $data['hidden'] = (int)$entry->isHidden; // Prefer explicit data from this imported menu over the defaults $title = self::sanitizeIpxeString($entry->title); @@ -243,7 +249,7 @@ class IPxe $data['hotkey'] = $entry->hotkey; } if (!empty($entry->passwd)) { - // Most likely it's a hash so we cannot recover; ask people to reset + // Most likely it's a hash, so we cannot recover; ask people to reset $data['pass'] ='please_reset'; } } @@ -268,36 +274,28 @@ class IPxe /** * Import only the bootentries from the given PXELinux menu - * @param PxeMenu $pxe - * @param array $menuEntries Where to append the generated menu items to + * + * @param PxeSection[] $menuEntries Where to append the generated menu items to */ - public static function importPxeMenuEntries($pxe, &$menuEntries) + public static function importPxeMenuEntries(PxeMenu $pxe, array &$menuEntries): void { if (self::$allEntries === false) { self::$allEntries = BootEntry::getAll(); } foreach ($pxe->sections as $section) { - if ($section->localBoot !== false || preg_match('/chain\.c32$/i', $section->kernel)) { + if ($section->isLocalboot()) { $menuEntries['localboot'] = $section; continue; } - if ($section->label === null) { - if (!$section->isHidden && !empty($section->title)) { - $menuEntries[] = $section->title; - } - continue; - } - if (empty($section->kernel)) { - if (!$section->isHidden && !empty($section->title)) { - $menuEntries[] = $section->title; - } + if ($section->isTextOnly()) { + $menuEntries[] = $section; continue; } $label = self::cleanLabelFixLocal($section); $entry = self::pxe2BootEntry($section); if ($entry === null) continue; // Error? Ignore - if ($label !== false || ($label = array_search($entry, self::$allEntries))) { + if ($label !== false || ($label = array_search($entry, self::$allEntries)) !== false) { // Exact Duplicate, Do Nothing error_log('Ignoring duplicate boot entry ' . $section->label . ' (' . $section->kernel . ')'); } else { @@ -321,7 +319,7 @@ class IPxe Database::exec('INSERT IGNORE INTO serversetup_bootentry (entryid, module, hotkey, title, builtin, data) VALUES (:label, :module, :hotkey, :title, 0, :data)', [ 'label' => $label, - 'module' => ($entry instanceof \StandardBootEntry) ? '.exec' : '.script', + 'module' => ($entry instanceof StandardBootEntry) ? '.exec' : '.script', 'hotkey' => $hotkey, 'title' => $title, 'data' => json_encode($data), @@ -408,10 +406,9 @@ class IPxe * Also it patches the entry if it's referencing the local bwlp install * but with different options. * - * @param PxeSection $section * @return string|false existing label if match, false otherwise */ - private static function cleanLabelFixLocal($section) + private static function cleanLabelFixLocal(PxeSection $section) { $myip = Property::getServerIp(); // Detect our "old" entry types @@ -434,10 +431,9 @@ class IPxe } /** - * @param PxeSection $section * @return BootEntry|null The according boot entry, null if it's unparsable */ - private static function pxe2BootEntry($section) + private static function pxe2BootEntry(PxeSection $section): ?BootEntry { if (preg_match('/(pxechain\.com|pxechn\.c32)$/i', $section->kernel)) { // Chaining -- create script @@ -466,7 +462,7 @@ class IPxe $script .= "set netX/{$opt}:{$type} {$args[$i]} || goto %fail%\n"; } } - } elseif ($arg{0} === '-') { + } elseif ($arg[0] === '-') { continue; } elseif ($file === false) { $file = self::parseFile($arg); @@ -496,11 +492,8 @@ class IPxe /** * Parse PXELINUX file notion. Basically, turn * server::file into tftp://server/file. - * - * @param string $file - * @return string */ - private static function parseFile($file) + private static function parseFile(string $file): string { if (preg_match(',^([^:/]+)::(.*)$,', $file, $out)) { return 'tftp://' . $out[1] . '/' . $out[2]; @@ -508,12 +501,12 @@ class IPxe return $file; } - public static function sanitizeIpxeString($string) + public static function sanitizeIpxeString(string $string): string { return str_replace(['&', '|', ';', '$', "\r", "\n"], ['+', '/', ':', 'S', ' ', ' '], $string); } - public static function makeMd5Pass($plainpass, $salt) + public static function makeMd5Pass(string $plainpass, string $salt): string { if (empty($plainpass)) return ''; @@ -528,15 +521,16 @@ class IPxe * remove any occurrence of either "option" or "option=something". If the argument starts with a * '+', it will be added to the command line after removing the '+'. If the argument starts with any * other character, it will also be added to the command line. + * * @param string $cmdLine command line to modify - * @param string $modifier modification string of space separated arguments + * @param string $modifier modification string of space separated arguments * @return string the modified command line */ - public static function modifyCommandLine($cmdLine, $modifier) + public static function modifyCommandLine(string $cmdLine, string $modifier): string { $items = preg_split('/\s+/', $modifier, -1, PREG_SPLIT_NO_EMPTY); foreach ($items as $item) { - if ($item{0} === '-') { + if ($item[0] === '-') { $item = preg_quote(substr($item, 1), '/'); $cmdLine = preg_replace('/(^|\s)' . $item . '(=\S*)?($|\s)/', ' ', $cmdLine); } else { diff --git a/modules-available/serversetup-bwlp-ipxe/inc/ipxebuilder.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/ipxebuilder.inc.php new file mode 100644 index 00000000..a2b25f55 --- /dev/null +++ b/modules-available/serversetup-bwlp-ipxe/inc/ipxebuilder.inc.php @@ -0,0 +1,80 @@ +<?php + +class IPxeBuilder +{ + + const PROP_IPXE_BUILDSTRING = 'ipxe.compile-time'; + const PROP_IPXE_HASH = 'ipxe.commit-hash'; + const PROP_IPXE_COMPILE_TASKID = 'ipxe-task-id'; + const VERSION_LIST_TASK = 'ipxe-version-list-id'; + const PROP_VERSION_SELECT_TASKID = 'ipxe-version-select-id'; + + /** + * Checkout given commit/ref of ipxe repo. Returns the according task-id, or null on error + * + * @param string $version version ref (commit, tag, ...) + * @param ?string $parent parent task id, if any + */ + public static function setIpxeVersion(string $version, ?string $parent = null): ?string + { + $task = Taskmanager::submit('IpxeVersion', [ + 'action' => 'CHECKOUT', + 'ref' => $version, + 'parentTask' => $parent, + ]); + if (!Taskmanager::isTask($task)) + return null; + TaskmanagerCallback::addCallback($task, 'ipxeVersionSet'); + Property::set(IPxeBuilder::PROP_VERSION_SELECT_TASKID, $task['id'], 2); + return $task['id']; + } + + public static function getVersionTaskResult(): ?array + { + $task = Taskmanager::status(IPxeBuilder::VERSION_LIST_TASK); + if (!Taskmanager::isTask($task) || Taskmanager::isFailed($task)) { + $task = Taskmanager::submit('IpxeVersion', + ['id' => IPxeBuilder::VERSION_LIST_TASK, 'action' => 'LIST']); + } + $task = Taskmanager::waitComplete($task); + if (Taskmanager::isFinished($task) && !Taskmanager::isFailed($task)) { + return $task['data']; + } + return null; + } + + /** + * Callback when compile Taskmanager job finished + */ + public static function compileCompleteCallback(array $task): void + { + if (!Taskmanager::isFinished($task) || Taskmanager::isFailed($task)) + return; + $version = 'Unknown'; + if (isset($task['data']['hash'])) { + $hash = $task['data']['hash']; + Property::set(IPxeBuilder::PROP_IPXE_HASH, $hash); + $version = $hash; + $list = IPxeBuilder::getVersionTaskResult(); + if (isset($list['versions'])) { + foreach ($list['versions'] as $v) { + if ($v['hash'] === $version) { + // Do NOT change (see below) + $version = date('Y-m-d H:i', $v['date']) . ' (' . substr($version, 0, 7) . ')'; + break; + } + } + } + } + // Do NOT change the format of this string -- we depend on it in ScriptBuilderIpxe::output() + $buildString = date('d.m.Y H:i') . ', Version: ' . $version; + Property::set(IPxeBuilder::PROP_IPXE_BUILDSTRING, $buildString); + } + + public static function setIPxeVersionCallback(array $task): void + { + if (!Taskmanager::isFinished($task) || Taskmanager::isFailed($task) || empty($task['data']['ref'])) + return; + Property::set(IPxeBuilder::PROP_IPXE_HASH, $task['data']['ref']); + } +}
\ No newline at end of file diff --git a/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php index b1e13e87..3ffecba1 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php @@ -3,19 +3,28 @@ class IPxeMenu { + /** + * @var int ID of this menu, from DB + */ protected $menuid; + /** + * @var int 0 = disabled, otherwise, launch default option after timeout + */ public $timeoutMs; + /** + * @var string title to display above menu + */ public $title; + /** + * @var int menu entry id from DB + */ public $defaultEntryId; /** * @var MenuEntry[] */ public $items = []; - /** - * @param int $menuId - */ - public static function get($menuId, $emptyFallback = false) + public static function get(int $menuId, bool $emptyFallback = false): ?IPxeMenu { $menu = Database::queryFirst("SELECT menuid, timeoutms, title, defaultentryid FROM serversetup_menu WHERE menuid = :menuid LIMIT 1", ['menuid' => $menuId]); @@ -31,33 +40,34 @@ class IPxeMenu * * @param array $menu array for according menu row */ - public function __construct($menu) + public function __construct(array $menu) { $this->menuid = (int)$menu['menuid']; $this->timeoutMs = (int)$menu['timeoutms']; - $this->title = $menu['title']; - $this->defaultEntryId = $menu['defaultentryid']; + $this->title = (string)$menu['title']; + $defaultEntryId = $menu['defaultentryid']; $res = Database::simpleQuery("SELECT e.menuentryid, e.entryid, e.refmenuid, e.hotkey, e.title, e.hidden, e.sortval, e.md5pass, b.module, b.data AS bootentry, b.title AS betitle FROM serversetup_menuentry e LEFT JOIN serversetup_bootentry b USING (entryid) WHERE e.menuid = :menuid ORDER BY e.sortval ASC, e.title ASC", ['menuid' => $menu['menuid']]); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { $this->items[] = new MenuEntry($row); } // Make sure we have a default entry if the menu isn't empty - if ($this->defaultEntryId === null && !empty($this->items)) { - $this->defaultEntryId = $this->items[0]->menuEntryId(); + if ($defaultEntryId === null && !empty($this->items)) { + $defaultEntryId = $this->items[0]->menuEntryId(); } + $this->defaultEntryId = (int)$defaultEntryId; } - public function title() + public function title(): string { return $this->title; } - public function timeoutMs() + public function timeoutMs(): int { return $this->timeoutMs; } @@ -65,36 +75,74 @@ class IPxeMenu /** * @return int Number of items in this menu */ - public function itemCount() + public function itemCount(): int { return count($this->items); } /** - * @return string|null Return script label of default entry, null if not set + * @return MenuEntry|null Return preselected menu entry */ - public function getDefaultEntryId() + public function defaultEntry(): ?MenuEntry { - return $this->defaultEntryId; + foreach ($this->items as $item) { + if ($item->menuEntryId() === $this->defaultEntryId) + return $item; + } + return null; + } + + private function maybeOverrideDefault(string $uuid) + { + $e = $this->defaultEntry(); + // Shortcut - is already bwlp and timeout is reasonable (1-15s), do nothing + $defIsMl = $e !== null && substr($e->internalId(), 0, 3) === 'ml-'; + $timeoutOk = $this->timeoutMs > 0 && $this->timeoutMs <= 15000; + if ($timeoutOk && $defIsMl) + return; + // No runmode module anyways + if (!Module::isAvailable('runmode')) + return; + $rm = RunMode::getRunMode($uuid); + // No runmode for this client, cannot be PVSmgr + if ($rm === false) + return; + // Is not pvsmgr + if ($rm['module'] !== 'roomplanner') + return; + // See if it's a dedicated station, if so make sure it boots into bwLehrpool + $data = json_decode($rm['modedata'], true); + if ($data['dedicatedmgr'] ?? false) { + if (!$defIsMl) { + $this->overrideDefaultToMinilinux(); + } + if (!$timeoutOk) { + $this->timeoutMs = 5000; + } + } } /** - * @return MenuEntry|null Return preselected menu entry + * Patch the menu to make sure bwLehrpool/"MiniLinux" is the default + * boot option, and set timeout to something reasonable. This is used + * for dedicated PVS managers, as they might not have a keyboard + * connected. */ - public function defaultEntry() + private function overrideDefaultToMinilinux() { foreach ($this->items as $item) { - if ($item->menuEntryId() == $this->defaultEntryId) - return $item; + if (substr($item->internalId(), 0, 3) === 'ml-') { + $this->defaultEntryId = $item->menuEntryId(); + return; + } } - return null; } /* * */ - public static function forLocation($locationId) : IPxeMenu + public static function forLocation(int $locationId): IPxeMenu { $chain = null; if (Module::isAvailable('locations')) { @@ -109,7 +157,7 @@ class IPxeMenu if ($res->rowCount() > 0) { // Make the location id key, preserving order (closest location is first) $chain = array_flip($chain); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { // Overwrite the value (numeric ascending values, useless) with menu array of according location $chain[(int)$row['locationid']] = $row; } @@ -132,13 +180,19 @@ class IPxeMenu return new IPxeMenu($menu); } - public static function forClient($ip, $uuid) : IPxeMenu + public static function forClient(string $ip, ?string $uuid): IPxeMenu { $locationId = 0; if (Module::isAvailable('locations')) { $locationId = Location::getFromIpAndUuid($ip, $uuid); } - return self::forLocation($locationId); + $menu = self::forLocation($locationId); + if ($uuid !== null) { + // Super specialcase hackery: If this is a dedicated PVS, force the default to + // be bwlp/"minilinux" + $menu->maybeOverrideDefault($uuid); + } + return $menu; } } @@ -146,11 +200,14 @@ class IPxeMenu class EmptyIPxeMenu extends IPxeMenu { - /** @noinspection PhpMissingParentConstructorInspection */ public function __construct() { - $this->title = 'No menu defined'; - $this->menuid = -1; + parent::__construct([ + 'menuid' => -1, + 'timeoutms' => 120, + 'defaultentryid' => null, + 'title' => 'No menu defined', + ]); $this->items[] = new MenuEntry([ 'title' => 'Please create a menu in Server-Setup first' ]); diff --git a/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php index 4203f931..4d1a56c7 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php @@ -7,38 +7,49 @@ class Localboot const BOOT_METHODS = [ 'PCBIOS' => [ - 'EXIT' => 'exit 1', + 'EXIT' => 'set slx_exit 1 || +exit 1', 'COMBOOT' => 'set netX/209:string localboot.cfg || set netX/210:string http://${serverip}/tftp/sl-bios/ || chain -ar /tftp/sl-bios/lpxelinux.0', 'SANBOOT' => 'sanboot --no-describe', ], 'EFI' => [ - 'EXIT' => 'exit 1', - 'COMBOOT' => 'set netX/209:string localboot.cfg || -set netX/210:string http://${serverip}/tftp/sl-efi64/ || -chain -ar /tftp/sl-efi64/syslinux.efi', + 'EXIT' => 'set slx_exit 1 || +exit 1', + 'SANBOOT' => 'imgfree || +console || +set filename \EFI\Boot\bootx64.efi || +set i:int32 0 || +:blubber +sanboot --no-describe --drive ${i} --filename ${filename} || +inc i +iseq ${i} 10 || goto blubber', + 'GRUB' => 'chain /tftp/grub-boot.img', ], ]; - public static function getDefault() + /** + * @return array{PCBIOS: string, EFI: string} + */ + public static function getDefault(): array { - $ret = explode(',', Property::get(self::PROPERTY_KEY, 'SANBOOT,EXIT')); + $ret = explode(',', Property::get(self::PROPERTY_KEY, 'SANBOOT,GRUB')); if (empty($ret)) { - $ret = ['SANBOOT', 'EXIT']; + $ret = ['SANBOOT', 'GRUB']; } elseif (count($ret) < 2) { - $ret[] = 'EXIT'; + $ret[] = 'SANBOOT'; } - if (null === self::BOOT_METHODS['PCBIOS'][$ret[0]]) { + if (!isset(self::BOOT_METHODS['PCBIOS'][$ret[0]])) { $ret[0] = 'SANBOOT'; } - if (null === self::BOOT_METHODS['EFI'][$ret[1]]) { - $ret[1] = 'EXIT'; + if (!isset(self::BOOT_METHODS['EFI'][$ret[1]])) { + $ret[1] = 'GRUB'; } return ['PCBIOS' => $ret[0], 'EFI' => $ret[1]]; } - public static function setDefault($pcbios, $efi) + public static function setDefault(string $pcbios, string $efi) { Property::set(self::PROPERTY_KEY, "$pcbios,$efi"); } diff --git a/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php index eb4a98de..da94a16b 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php @@ -27,7 +27,7 @@ class MenuEntry */ public $sortval; /** - * @var BootEntry + * @var ?BootEntry */ public $bootEntry = null; @@ -35,11 +35,7 @@ class MenuEntry public $md5pass = null; - /** - * @param int $menuEntryId - * @return MenuEntry|null - */ - public static function get($menuEntryId) + public static function get(int $menuEntryId): ?MenuEntry { $row = Database::queryFirst("SELECT e.menuentryid, e.entryid, e.refmenuid, e.hotkey, e.title, e.hidden, e.sortval, e.plainpass, e.md5pass, b.module, b.data AS bootentry, b.title AS betitle @@ -56,48 +52,53 @@ class MenuEntry * * @param array $row row from database */ - public function __construct($row) + public function __construct(array $row) { - if (is_array($row)) { - if (empty($row['title']) && !empty($row['betitle'])) { - $row['title'] = $row['betitle']; - } - foreach ($row as $key => $value) { - if (property_exists($this, $key)) { - $this->{$key} = $value; - } - } - $this->hotkey = self::getKeyCode($row['hotkey'] ?? ''); - if (!empty($row['bootentry'])) { - $this->bootEntry = BootEntry::fromJson($row['module'], $row['bootentry']); - } elseif (isset($row['refmenuid'])) { - $this->bootEntry = BootEntry::forMenu($row['refmenuid']); + if (empty($row['title']) && !empty($row['betitle'])) { + $row['title'] = $row['betitle']; + } + foreach ($row as $key => $value) { + if (property_exists($this, $key)) { + $this->{$key} = $value; } - $this->gap = (array_key_exists('entryid', $row) && $row['entryid'] === null && $row['refmenuid'] === null); } + $this->hotkey = self::getKeyCode($row['hotkey'] ?? ''); + if (!empty($row['bootentry'])) { + $this->bootEntry = BootEntry::fromJson($row['module'], $row['bootentry']); + } elseif (isset($row['refmenuid'])) { + $this->bootEntry = BootEntry::forMenu($row['refmenuid']); + } + $this->gap = (array_key_exists('entryid', $row) && $row['entryid'] === null && $row['refmenuid'] === null); settype($this->hidden, 'bool'); settype($this->gap, 'bool'); settype($this->sortval, 'int'); settype($this->menuentryid, 'int'); } - public function getBootEntryScript($builder) + public function getBootEntryScript(ScriptBuilderBase $builder): string { if ($this->bootEntry === null) return ''; return $this->bootEntry->toScript($builder); } - public function menuEntryId() + public function menuEntryId(): int { return $this->menuentryid; } - public function title() + public function title(): string { return $this->title; } + public function internalId(): string + { + if ($this->bootEntry === null) + return ''; + return $this->bootEntry->internalId(); + } + /* * */ @@ -137,7 +138,7 @@ class MenuEntry * * @return string[] list of known key names */ - public static function getKeyList() + public static function getKeyList(): array { return array_keys(self::getKeyArray()); } @@ -146,10 +147,9 @@ class MenuEntry * Get the key code ipxe expects for the given named * key. Returns false if the key name is unknown. * - * @param string $keyName * @return false|string Key code as hex string, or false if not found */ - public static function getKeyCode($keyName) + public static function getKeyCode(string $keyName) { $data = self::getKeyArray(); if (isset($data[$keyName])) @@ -161,7 +161,7 @@ class MenuEntry * @param string $keyName desired key name * @return string $keyName if it's known, empty string otherwise */ - public static function filterKeyName($keyName) + public static function filterKeyName(string $keyName): string { $data = self::getKeyArray(); if (isset($data[$keyName])) diff --git a/modules-available/serversetup-bwlp-ipxe/inc/pxelinux.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/pxelinux.inc.php index 3f406767..24b099dc 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/pxelinux.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/pxelinux.inc.php @@ -7,11 +7,14 @@ class PxeLinux /** * Takes a (partial) pxelinux menu and parses it into * a PxeMenu object. + * * @param string $input The pxelinux menu to parse - * @return PxeMenu the parsed menu + * @return ?PxeMenu the parsed menu, or null if input is not a PXELinux menu */ - public static function parsePxeLinux($input, $isCp437) + public static function parsePxeLinux(string $input, bool $isCp437): ?PxeMenu { + if (empty($input)) + return null; if ($isCp437) { $input = iconv('IBM437', 'UTF8//TRANSLIT//IGNORE', $input); } @@ -76,12 +79,14 @@ class PxeLinux } $section->helpText = $text; } elseif (self::handleKeyword($key, $val, $sectionPropMap, $section)) { - continue; + //continue; } } if ($section !== null) { $menu->sections[] = $section; } + if (empty($menu->sections)) + return null; // Probably not a PXE menu but random text? foreach ($menu->sections as $section) { $section->mangle(); } @@ -93,13 +98,14 @@ class PxeLinux * to the given object. The map to look up the keyword has to be passed * as well as the object to set the value in. Map and object should * obviously match. + * * @param string $key keyword of parsed line * @param string $val raw value of currently parsed line (empty if not present) * @param array $map Map in which $key is looked up as key - * @param PxeMenu|PxeSection The object to set the parsed and sanitized value in + * @param PxeMenu|PxeSection $object The object to set the parsed and sanitized value in * @return bool true if the value was found in the map (and set in the object), false otherwise */ - private static function handleKeyword($key, $val, $map, $object) + private static function handleKeyword(string $key, string $val, array $map, $object): bool { if (!isset($map[$key])) return false; @@ -122,161 +128,3 @@ class PxeLinux } -/** - * Class representing a parsed pxelinux menu. Members - * will be set to their annotated type if present or - * be null otherwise, except for present-only boolean - * options, which will default to false. - */ -class PxeMenu -{ - - /** - * @var string menu title, shown at the top of the menu - */ - public $title; - /** - * @var int initial timeout after which $timeoutLabel would be executed - */ - public $timeoutMs; - /** - * @var int if the user canceled the timeout by pressing a key, this timeout would still eventually - * trigger and launch the $timeoutLabel section - */ - public $totalTimeoutMs; - /** - * @var string label of section which will execute if the timeout expires - */ - public $timeoutLabel; - /** - * @var bool hide menu and just show background after triggering an entry - */ - public $menuClear = false; - /** - * @var bool boot the associated entry directly if its corresponding hotkey is pressed instead of just highlighting - */ - public $immediateHotkeys = false; - /** - * @var PxeSection[] list of sections the menu contains - */ - public $sections = []; - /** - * @var string The DEFAULT entry of the menu. Usually refers either to a - * LABEL, or a loadable module (like vesamenu.c32) - */ - public $default; - - /** - * Check if any of the sections has the given label. - */ - public function hasLabel($label) - { - foreach ($this->sections as $section) { - if ($section->label === $label) - return true; - } - return false; - } - -} - -/** - * Class representing a parsed pxelinux menu entry. Members - * will be set to their annotated type if present or - * be null otherwise, except for present-only boolean - * options, which will default to false. - */ -class PxeSection -{ - - /** - * @var string label used internally in PXEMENU definition to address this entry - */ - public $label; - /** - * @var string MENU LABEL of PXEMENU - title of entry displayed to the user - */ - public $title; - /** - * @var int Number of spaces to prefix the title with - */ - public $indent; - /** - * @var string help text to display when the entry is highlighted - */ - public $helpText; - /** - * @var string Kernel to load - */ - public $kernel; - /** - * @var string|string[] initrd to load for the kernel. - * If mangle() has been called this will be an array, - * otherwise it's a comma separated list. - */ - public $initrd; - /** - * @var string command line options to pass to the kernel - */ - public $append; - /** - * @var int IPAPPEND from PXEMENU. Bitmask of valid options 1 and 2. - */ - public $ipAppend; - /** - * @var string Password protecting the entry. This is most likely in encrypted form. - */ - public $passwd; - /** - * @var bool whether this section is marked as default (booted after timeout) - */ - public $isDefault = false; - /** - * @var bool Menu entry is not visible (can only be triggered by timeout) - */ - public $isHidden = false; - /** - * @var bool Disable this entry, making it unselectable - */ - public $isDisabled = false; - /** - * @var int|false Value of the LOCALBOOT field, false if not set - */ - public $localBoot = false; - /** - * @var string hotkey to trigger item. Only valid after calling mangle() - */ - public $hotkey; - - public function __construct($label) { $this->label = $label; } - - public function mangle() - { - if (($i = strpos($this->title, '^')) !== false) { - $this->hotkey = strtoupper($this->title{$i+1}); - $this->title = substr($this->title, 0, $i) . substr($this->title, $i + 1); - } - if (strpos($this->append, 'initrd=') !== false) { - $parts = preg_split('/\s+/', $this->append); - $this->append = ''; - for ($i = 0; $i < count($parts); ++$i) { - if (preg_match('/^initrd=(.*)$/', $parts[$i], $out)) { - if (!empty($this->initrd)) { - $this->initrd .= ','; - } - $this->initrd .= $out[1]; - } else { - $this->append .= ' ' . $parts[$i]; - } - } - $this->append = trim($this->append); - } - if (is_string($this->initrd)) { - $this->initrd = explode(',', $this->initrd); - } elseif (!is_array($this->initrd)) { - $this->initrd = []; - } - } - -} - diff --git a/modules-available/serversetup-bwlp-ipxe/inc/pxemenu.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/pxemenu.inc.php new file mode 100644 index 00000000..7be57ef1 --- /dev/null +++ b/modules-available/serversetup-bwlp-ipxe/inc/pxemenu.inc.php @@ -0,0 +1,59 @@ +<?php + +/** + * Class representing a parsed pxelinux menu. Members + * will be set to their annotated type if present or + * be null otherwise, except for present-only boolean + * options, which will default to false. + */ +class PxeMenu +{ + + /** + * @var string menu title, shown at the top of the menu + */ + public $title; + /** + * @var int initial timeout after which $timeoutLabel would be executed + */ + public $timeoutMs; + /** + * @var int if the user canceled the timeout by pressing a key, this timeout would still eventually + * trigger and launch the $timeoutLabel section + */ + public $totalTimeoutMs; + /** + * @var string label of section which will execute if the timeout expires + */ + public $timeoutLabel; + /** + * @var bool hide menu and just show background after triggering an entry + */ + public $menuClear = false; + /** + * @var bool boot the associated entry directly if its corresponding hotkey is pressed instead of just highlighting + */ + public $immediateHotkeys = false; + /** + * @var PxeSection[] list of sections the menu contains + */ + public $sections = []; + /** + * @var string The DEFAULT entry of the menu. Usually refers either to a + * LABEL, or a loadable module (like vesamenu.c32) + */ + public $default; + + /** + * Check if any of the sections has the given label. + */ + public function hasLabel(string $label): bool + { + foreach ($this->sections as $section) { + if ($section->label === $label) + return true; + } + return false; + } + +}
\ No newline at end of file diff --git a/modules-available/serversetup-bwlp-ipxe/inc/pxesection.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/pxesection.inc.php new file mode 100644 index 00000000..2d9cd6ab --- /dev/null +++ b/modules-available/serversetup-bwlp-ipxe/inc/pxesection.inc.php @@ -0,0 +1,117 @@ +<?php + +/** + * Class representing a parsed pxelinux menu entry. Members + * will be set to their annotated type if present or + * be null otherwise, except for present-only boolean + * options, which will default to false. + */ +class PxeSection +{ + + /** + * @var ?string label used internally in PXEMENU definition to address this entry + */ + public $label; + /** + * @var string MENU LABEL of PXEMENU - title of entry displayed to the user + */ + public $title; + /** + * @var int Number of spaces to prefix the title with + */ + public $indent; + /** + * @var string help text to display when the entry is highlighted + */ + public $helpText; + /** + * @var string Kernel to load + */ + public $kernel; + /** + * @var string|string[] initrd to load for the kernel. + * If mangle() has been called this will be an array, + * otherwise it's a comma separated list. + */ + public $initrd; + /** + * @var string command line options to pass to the kernel + */ + public $append; + /** + * @var int IPAPPEND from PXEMENU. Bitmask of valid options 1 and 2. + */ + public $ipAppend; + /** + * @var string Password protecting the entry. This is most likely in encrypted form. + */ + public $passwd; + /** + * @var bool whether this section is marked as default (booted after timeout) + */ + public $isDefault = false; + /** + * @var bool Menu entry is not visible (can only be triggered by timeout) + */ + public $isHidden = false; + /** + * @var bool Disable this entry, making it unselectable + */ + public $isDisabled = false; + /** + * @var int|false Value of the LOCALBOOT field, false if not set + */ + public $localBoot = false; + /** + * @var string hotkey to trigger item. Only valid after calling mangle() + */ + public $hotkey; + + public function __construct(?string $label) { $this->label = $label; } + + public function mangle() + { + if (($i = strpos($this->title, '^')) !== false) { + $this->hotkey = strtoupper($this->title[$i + 1]); + $this->title = substr($this->title, 0, $i) . substr($this->title, $i + 1); + } + if (strpos($this->append, 'initrd=') !== false) { + $parts = preg_split('/\s+/', $this->append); + $this->append = ''; + for ($i = 0; $i < count($parts); ++$i) { + if (preg_match('/^initrd=(.*)$/', $parts[$i], $out)) { + if (!empty($this->initrd)) { + $this->initrd .= ','; + } + $this->initrd .= $out[1]; + } else { + $this->append .= ' ' . $parts[$i]; + } + } + $this->append = trim($this->append); + } + if (is_string($this->initrd)) { + $this->initrd = explode(',', $this->initrd); + } elseif (!is_array($this->initrd)) { + $this->initrd = []; + } + } + + /** + * Does this appear to be an entry that triggers localboot? + */ + public function isLocalboot(): bool + { + return $this->localBoot !== false || preg_match('/chain\.c32$/i', $this->kernel); + } + + /** + * Is this (most likely) a separating entry only that cannot be selected? + */ + public function isTextOnly(): bool + { + return ($this->label === null || empty($this->kernel)) && !$this->isHidden && !empty($this->title); + } + +}
\ No newline at end of file diff --git a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php index 84cfd7db..9cd07388 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php @@ -9,8 +9,10 @@ abstract class ScriptBuilderBase protected $platform = ''; + /** @var string */ protected $clientIp; + /** @var ?string */ protected $uuid; /** @@ -18,57 +20,57 @@ abstract class ScriptBuilderBase */ protected $hasExtension = false; - public function hasExtensions() + public function hasExtensions(): bool { return $this->hasExtension; } - public function platform() + public function platform(): string { return $this->platform; } - public function uuid() + public function uuid(): ?string { return $this->uuid; } - public function clientIp() + public function clientIp(): string { return $this->clientIp; } - public function getLabel() + public function getLabel(): string { return 'b' . mt_rand(100, 999) . 'x' . (++$this->lblId); } - public function __construct($platform = null, $serverIp = null, $slxExtensions = null) + public function __construct(?string $platform = null, ?string $serverIp = null, ?bool $slxExtensions = null) { - $this->clientIp = $_SERVER['REMOTE_ADDR']; + $this->clientIp = (string)$_SERVER['REMOTE_ADDR']; if (substr($this->clientIp, 0, 7) === '::ffff:') { $this->clientIp = substr($this->clientIp, 7); } $this->serverIp = $serverIp ?? $_SERVER['SERVER_ADDR'] ?? Property::getServerIp(); - $this->platform = $platform ?? Request::any('platform', false, 'string'); - if ($this->platform !== false) { + $this->platform = $platform ?? Request::any('platform', null, 'string'); + if ($this->platform !== null) { $this->platform = strtoupper($this->platform); } if ($this->platform !== 'EFI' && $this->platform !== 'PCBIOS') { $this->platform = ''; } $this->hasExtension = $slxExtensions ?? (bool)Request::any('slx-extensions', false, 'int'); - $this->uuid = Request::any('uuid', false, 'string'); - if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $this->uuid)) { - $this->uuid = false; + $uuid = Request::any('uuid', null, 'string'); + if ($uuid !== null + && preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $uuid)) { + $this->uuid = (string)$uuid; } } /** * Output given string (script) to client, in a suitable encoding, headers, etc. - * @param string $string */ - public abstract function output($string); + public abstract function output(string $string): void; public abstract function bootstrapLive(); @@ -79,31 +81,22 @@ abstract class ScriptBuilderBase * @param bool $honorPassword Whether we should generate a password dialog if protected, or skip * @return string generated script/code/... */ - public abstract function getMenuEntry($menuEntry, $honorPassword = true); + public abstract function getMenuEntry(?MenuEntry $entry, bool $honorPassword = true): string; /** * @param BootEntry|null|false $bootEntry - * @return string */ - public abstract function getBootEntry($bootEntry); + public abstract function getBootEntry(?BootEntry $entry): string; - public abstract function getSpecial($special); + public abstract function getSpecial(string $special); - /** - * @param IPxeMenu|null $menu - * @return string - */ - public abstract function menuToScript($menu); + public abstract function menuToScript(IPxeMenu $menu): string; /** * Pass EITHER only $agnostic, OR $bios and/or $efi * If $agnostic is given, it should be used unconditionally, * and $bios/$efi should be ignored. - * @param ExecData $agnostic - * @param ExecData $bios - * @param ExecData $efi - * @return string */ - public abstract function execDataToScript($agnostic, $bios, $efi); + public abstract function execDataToScript(?ExecData $agnostic, ?ExecData $bios, ?ExecData $efi): string; }
\ No newline at end of file diff --git a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbash.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbash.inc.php index 86b2931f..d6b542ec 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbash.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbash.inc.php @@ -3,39 +3,39 @@ class ScriptBuilderBash extends ScriptBuilderBase { - public function output($string) + public function output(string $string): void { echo $string; } - public function bootstrapLive() { return false; } + public function bootstrapLive(): bool { return false; } - public function getMenu(IPxeMenu $menu, bool $bootstrap) + public function getMenu(IPxeMenu $menu, bool $bootstrap): string { return $this->menuToScript($menu); } - public function getBootEntry($entry) + public function getBootEntry(?BootEntry $entry): string { - if (!$entry) { + if ($entry === null) { return "echo 'Invalid boot entry id'\nread -n1 -r _\n"; } return $entry->toScript($this); } - public function getMenuEntry($entry, $honorPassword = true) + public function getMenuEntry(?MenuEntry $entry, bool $honorPassword = true): string { if ($entry === null) return "echo 'Invalid menu entry id - press any key to continue'\nread -n1 -r _\n"; return $entry->getBootEntryScript($this); } - public function getSpecial($special) + public function getSpecial(string $special): string { return ''; // We can't really do localboot here I guess } - public function menuToScript($menu) + public function menuToScript(IPxeMenu $menu): string { $output = "declare -A items_name items_gap hotkey_item\ndeclare menu_default menu_timeout menu_title\n"; foreach ($menu->items as $entry) { @@ -59,7 +59,7 @@ class ScriptBuilderBash extends ScriptBuilderBase . "\nmenu_title=" . $this->bashString($menu->title) . "\n"; } - public function execDataToScript($agnostic, $bios, $efi) : string + public function execDataToScript(?ExecData $agnostic, ?ExecData $bios, ?ExecData $efi): string { if ($agnostic !== null) return $this->execDataToScriptInternal($agnostic); @@ -86,7 +86,7 @@ class ScriptBuilderBash extends ScriptBuilderBase return $script; } - private function bashString($string) + private function bashString(string $string): string { if (strpos($string, "'") === false) { return "'$string'"; diff --git a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuildergrub.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuildergrub.inc.php new file mode 100644 index 00000000..9dce5214 --- /dev/null +++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuildergrub.inc.php @@ -0,0 +1,330 @@ +<?php + +class ScriptBuilderGrub extends ScriptBuilderBase +{ + + /** @var bool */ + private $confCodeEmited = false; + + public function __construct(?string $platform = null, ?string $serverIp = null) + { + if (empty($platform)) { + $platform = Request::any('platform', null, 'string'); + if ($platform === 'pc' || stripos($platform, 'bios') !== false) { + $platform = 'PCBIOS'; + } + } + parent::__construct($platform, $serverIp, false); + } + + private function getConfCode(): string + { + if ($this->confCodeEmited) + return ''; + $this->confCodeEmited = true; + $str = ' +if ! [ "$uuid" ] ; then + smbios --type 1 --get-uuid 8 --set uuid +fi +set serverip="' . $this->getLocalIp() . '" +'; + foreach (['mac', 'ip', 'domain', 'hostname'] as $var) { + $str .= <<<EOF +if ! [ "\$$var" ] ; then + set $var="\$net_default_$var" +fi +if ! [ "\$$var" ] ; then + set $var="\$net_efinet0_dhcp_$var" +fi + +EOF; + + } + return $str; + } + + private function getLocalIp(): string + { + if (isset($_SERVER['SCRIPT_URI']) && preg_match('#^\w+://([^/]+)#', $_SERVER['SCRIPT_URI'], $out)) { + $host = $out[1]; + } elseif (isset($_SERVER['SERVER_NAME'])) { + $host = $_SERVER['SERVER_NAME']; + } elseif (isset($_SERVER['SERVER_ADDR'])) { + $host = $_SERVER['SERVER_ADDR']; + } else { + $host = $this->serverIp; + } + return $host; + } + + private function getGrubBase(): string + { + return '(http,' . $this->getLocalIp() . ')'; + } + + private function getUrlBase(): string + { + $host = $this->getGrubBase(); + if (isset($_SERVER['REQUEST_URI'])) { + $url = parse_url($_SERVER['REQUEST_URI']); + $path = $url['path']; + } else { + // Static fallback + $path = '/boot/ipxe'; + } + return $host . $path; + + } + + private function getUrlFull(?string $key = null, ?string $value = null): string + { + $url = parse_url($_SERVER['REQUEST_URI']); + $urlbase = $this->getUrlBase(); + if (empty($url['query'])) { + $fromQuery = []; + } else { + parse_str($url['query'], $fromQuery); + foreach ($fromQuery as &$v) { + $v = urlencode($v); + } + unset($v); + } + unset($fromQuery['entryid'], $fromQuery['special'], $fromQuery['redir']); + if ($key !== null) { + $fromQuery[$key] = $value; + } + $required = [ + 'type' => 'grub', + 'uuid' => '$uuid', + 'mac' => '$mac', + 'platform' => '$grub_platform', + ]; + $fullQuery = '?'; + foreach ($required + $fromQuery as $k => $v) { // Loop instead of http_build_query since we don't want escaping for the varnames! + $fullQuery .= $k . '=' . $v . '&'; + } + return $urlbase . $fullQuery; + } + + /** + * Redirect to same URL, but add our extended params + */ + private function redirect(string $key = null, string $value = null): string + { + // Redirect to self with added parameters + $urlfull = $this->getUrlFull($key, $value); + return $this->getConfCode() . <<<HERE + +set self="${urlfull}" +echo "Chaining to \$self..." +configfile \${self}redir=1 + +HERE; + } + + /** + * Called when we handle a real client request, and don't just generate static data + * for whatever use-case that might have. In the latter case, it wouldn't make much sense + * to generate a redirect code snippet. + * + * @return string + */ + public function bootstrapLive() + { + // Check if required arguments are given; if not, spit out according script and chain to self + if ($this->uuid === null || $this->platform === '') { + // REQUIRED so we can hide incompatible entries + // but avoid redirect cycle + if (Request::any('redir', '', 'string') === '') { + return $this->redirect(); + } + } + return false; + } + + public function getBootEntry(?BootEntry $entry): string + { + if ($entry === null) { + return "echo Invalid boot entry id\nsleep --interruptible --verbose 10\n"; + } + return $entry->toScript($this); + } + + public function getMenu(IPxeMenu $menu, bool $bootstrap): string + { + $base = $this->getUrlFull(); + return $this->getConfCode() + . "set self=\"{$base}\"\n" + . $this->menuToScript($menu); + } + + public function menuToScript(IPxeMenu $menu): string + { + if ($menu->defaultEntryId === null) { + $output = <<<EOF +set timeout=0 + +EOF; + + } else { + $secs = (int)($menu->timeoutMs / 1000); + $output = <<<EOF +set timeout={$secs} +set default="id-{$menu->defaultEntryId}" + +EOF; + } + $output .= $this->getConfCode(); + foreach ($menu->items as $item) { + $output .= $this->getMenuItemScript($item); + } + return $output; + } + + private function getMenuItemScript(MenuEntry $entry): string + { + $str = "menuentry '" . str_replace("'", '', $entry->title) . "' --id 'id-" . $entry->menuentryid . "' {\n"; + if ($entry->gap) { + $str .= "true\n"; // AFAICT, not possible in GRUB + } elseif ($entry->bootEntry === null || (!empty($this->platform) && !$entry->bootEntry->supportsMode($this->platform))) { + $str .= "echo Type mismatch\n"; + } elseif ($entry->hidden && $entry->hotkey === false) { + $str .= "echo Hidden entries without hotkey are illegal\n"; // Hidden entries without hotkey are illegal + } else { + if ($entry->hotkey !== false) { + // Not supported by grub... + } + if ($entry->bootEntry instanceof MenuBootEntry) { + // Link + $str .= "configfile \${self}entryid={$entry->menuentryid}\n"; + } else { + // Embed directly + // TODO: Password. Use read etc.; might need hashsum.mod, in that case, don't embed entry directly but use configfile... + $str .= $this->getMenuEntry($entry, true); + } + } + return $str . "}\n"; + } + + public function getSpecial(string $special): string + { + if ($special === 'localboot') { + // Sync this with setup-scripts/grub_localboot occasionally... + $output = <<<'EOF' +insmod chain +if [ "$grub_platform" = "pc" ] ; then + chainloader (hd0)+1 + chainloader (hd1)+1 + chainloader (hd2)+1 +fi +insmod fat +insmod part_gpt +echo "Scanning, first pass..." +for efi in (*,gpt*)/efi/grub/grubx64.efi (*,gpt*)/efi/boot/bootx64.efi (*,gpt*)/efi/*/*/bootmgfw.efi (*,gpt*)/efi/*/*.efi \ + (*,msdos*)/efi/grub/grubx64.efi (*,msdos*)/efi/boot/bootx64.efi (*,msdos*)/efi/*/*/bootmgfw.efi (*,msdos*)/efi/*/*.efi; do + regexp --set=1:efi_device '^\((.*)\)/' "${efi}" +done + +echo "Scanning, second pass..." +for efi in (*,gpt*)/efi/grub/grubx64.efi (*,gpt*)/efi/boot/bootx64.efi (*,gpt*)/efi/*/*/bootmgfw.efi (*,gpt*)/efi/*/*.efi \ + (*,msdos*)/efi/grub/grubx64.efi (*,msdos*)/efi/boot/bootx64.efi (*,msdos*)/efi/*/*/bootmgfw.efi (*,msdos*)/efi/*/*.efi; do + if [ -e "${efi}" ]; then + #regexp --set=1:efi_device '^\((.*)\)/' "${efi}" + regexp --set=1:root '^(\(.*\))/' "${efi}" + regexp --set=1:efi_path '^\(.*\)(/.*)$' "${efi}" + echo " >> Found operating system! <<" + echo " Path: '${efi}' on '${root}'" + echo " Fallback '${efi_path}'" + chainloader "${efi}" + boot + echo " That failed..." + fi +done + +echo "No EFI known OS found. Exiting." +exit +EOF; + + } else { + $output = <<<EOF +echo "Unknown special command: $special" +sleep --interruptible --verbose 10 +EOF; + } + return $output; + } + + public function output(string $string): void + { + Header('Content-Type: text/plain; charset=UTF-8'); + echo $string; + } + + public function getMenuEntry(?MenuEntry $entry, bool $honorPassword = true): string + { + if ($entry === null) + return "echo Invalid menu entry id\nsleep --interruptible --verbose 10\n"; + // TODO: Check for password + if ($honorPassword && !empty($entry->md5pass)) { + return "echo TODO: Implement password check...\nsleep --interruptible --verbose 10\n"; + } + $meid = $entry->menuEntryId(); + $output = $this->getConfCode() . "set menuentryid=$meid\n"; + // Output actual entry + $output .= str_replace('%fail%', 'fail', $entry->getBootEntryScript($this)); + return $output; + } + + public function execDataToScript(?ExecData $agnostic, ?ExecData $bios, ?ExecData $efi): string + { + if ($agnostic !== null) + return $this->execDataToScriptInternal($agnostic); + + if ($efi !== null && $this->platform === BootEntry::EFI) + return $this->execDataToScriptInternal($efi); + // Unknown or not EFI, should be BIOS at this point + return $this->execDataToScriptInternal($bios ?? $efi ?? new ExecData()); + } + + private function execDataToScriptInternal(ExecData $entry): string + { + $entry->sanitize(); + $base = $this->getGrubBase(); + $script = ''; + // Overriding dhcpOpts probably not possible/necessary + $initrds = []; + if (!empty($entry->initRd)) { + foreach ($entry->initRd as $initrd) { + if (empty($initrd)) + continue; + $initrds[] = $this->combineUrl($base, $initrd); + } + } + $file = $this->combineUrl($base, $entry->executable); + $script .= "linux $file {$entry->commandLine} slx.ipxe.id=\${menuentryid}\n"; + if (!empty($initrds)) { + $script .= "initrd " . implode(' ', $initrds) . "\n"; + } + return $script; + } + + private function combineUrl(string $base, string $path): string + { + $url = parse_url($path); + if (isset($url['host'])) { + $scheme = $url['scheme'] ?? 'http'; + $host = $url['host']; + $base = "($scheme,$host)"; + $path = $url['path'] ?? '/'; + if (isset($url['query'])) { + $path .= '?' . $url['query']; + } + } else { + if ($path[0] !== '/') { + $path = '/' . $path; + } + } + return $base . $path; + } + +} diff --git a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php index 385cd15f..9421684f 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php @@ -3,7 +3,7 @@ class ScriptBuilderIpxe extends ScriptBuilderBase { - private function getUrlBase() + private function getUrlBase(): string { if (isset($_SERVER['REQUEST_URI'])) { $url = parse_url($_SERVER['REQUEST_URI']); @@ -23,7 +23,7 @@ class ScriptBuilderIpxe extends ScriptBuilderBase } - private function getUrlFull(&$hasExt, $key = null, $value = null) + private function getUrlFull(?bool &$hasExt = null, ?string $key = null, ?string $value = null): string { $url = parse_url($_SERVER['REQUEST_URI']); $urlbase = $this->getUrlBase(); @@ -58,7 +58,7 @@ class ScriptBuilderIpxe extends ScriptBuilderBase /** * Redirect to same URL, but add our extended params */ - private function redirect($key = null, $value = null) + private function redirect(string $key = null, string $value = null): string { // Redirect to self with added parameters $urlfull = $this->getUrlFull($hasExt, $key, $value); @@ -102,27 +102,24 @@ HERE; return false; } - public function getBootEntry($entry) + public function getBootEntry(?BootEntry $entry): string { - if (!$entry) { + if ($entry === null) { return "#!ipxe\nprompt --timeout 5000 Invalid boot entry id\n"; } return $entry->toScript($this); } - public function getMenu(IPxeMenu $menu, bool $bootstrap) + public function getMenu(IPxeMenu $menu, bool $bootstrap): string { if ($bootstrap) { return "#!ipxe\nimgfree ||\n" . $this->menuToScript($menu); } - $base = $this->getUrlFull($he); + $base = $this->getUrlFull(); return "#!ipxe\nset self {$base} ||\n" . $this->menuToScript($menu); } - /** - * @param IPxeMenu $menu - */ - public function menuToScript($menu) + public function menuToScript(IPxeMenu $menu): string { if ($this->hasExtension) { $slxConsoleUpdate = '--update'; @@ -136,13 +133,13 @@ HERE; imgstat bg-menu || imgfetch --name bg-menu /tftp/pxe-menu.png || console --left 55 --top 88 --right 63 --bottom 64 --keep --picture bg-menu || -colour --rgb 0xffffff 7 -colour --rgb 0xcccccc 5 -colour --rgb 0x000000 0 -colour --rgb 0xdddddd 6 -cpair --foreground 0 --background 4 1 -cpair --foreground 0 --background 5 2 -cpair --foreground 7 --background 9 0 +colour --rgb 0xffffff 7 || +colour --rgb 0xcccccc 5 || +colour --rgb 0x000000 0 || +colour --rgb 0xdddddd 6 || +cpair --foreground 0 --background 4 1 || +cpair --foreground 0 --background 5 2 || +cpair --foreground 7 --background 9 0 || :slx_menu @@ -166,7 +163,11 @@ HERE; $output .= " selection || goto default || goto fail\n"; $output .= <<<HERE console --left 60 --top 130 --right 67 --bottom 86 $slxConsoleUpdate || +set slx_exit \${} || chain -a \${self}&entryid=\${selection} || +iseq \${slx_exit} \${} || console || +iseq \${slx_exit} \${} || echo Exiting with code \${slx_exit} || +iseq \${slx_exit} \${} || exit \${slx_exit} goto fail || goto start goto \${target} || echo Could not find menu entry in script. @@ -179,49 +180,10 @@ prompt Boot failed. Press any key to start. goto start HERE; - - /* - - :i5 - chain -a /tftp/memtest.0 passes=1 onepass || goto membad - prompt Memory OK. Press a key. - goto init - - :i8 - set x:int32 0 - :again - console --left 60 --top 130 --right 67 --bottom 96 --picture bg-load --keep || - console --left 55 --top 88 --right 63 --bottom 64 --picture bg-menu --keep || - inc x - iseq \${x} 20 || goto again - prompt DONE. Press dein Knie. - goto slx_menu - - :membad - iseq \${errno} 0x1 || goto memaborted - params - param scrot \${vram} - imgfetch -a http://132.230.8.113/screen.php##params || - prompt Memory is bad. Press a key. - goto init - - :memaborted - params - param scrot \${vram} - imgfetch -a http://132.230.8.113/screen.php##params || - prompt Memory test aborted. Press a key. - goto init - - */ return $output; } - /** - * @param $requestedDefaultId - * @param MenuEntry $entry - * @return string - */ - private function getMenuItemScript($requestedDefaultId, $entry) + private function getMenuItemScript(int $requestedDefaultId, MenuEntry $entry): string { $str = 'item '; if ($entry->gap) { @@ -250,7 +212,7 @@ HERE; return $str . " || prompt Could not create menu item for {$entry->menuentryid}\n"; } - public function getSpecial($special) + public function getSpecial(string $special): string { if ($special === 'localboot') { // Get preferred localboot method, depending on system model @@ -299,11 +261,7 @@ HERE; } } // Convert to actual ipxe code - if (isset($BOOT_METHODS[$localboot])) { - $localboot = $BOOT_METHODS[$localboot]; - } else { - $localboot = 'prompt Localboot not possible'; - } + $localboot = $BOOT_METHODS[$localboot] ?? 'prompt Localboot not possible'; $output = <<<BLA imgfree || console || @@ -317,17 +275,27 @@ BLA; return $output; } - public function output($string) + public function output(string $string): void { - setlocale(LC_ALL, 'de_DE.UTF-8', 'de_DE.utf-8', 'de_DE.utf8', 'de_DE', 'de', 'German', 'ge', 'en_US.UTF-8', 'en_US.utf-8'); - if ($this->platform === 'EFI') { - $cs = 'ASCII'; + // iPXE introduced UTF-8 support at some point in 2022, and now expects all text/script files to be + // encoded as such. Since we still offer to use older versions, we need to detect that here and handle + // all non-ASCII chars differently. + // Use 'ipxe.compile-time' instead of const from IpxeBuilder to avoid pulling in another include + if (!preg_match('/Version: (\d{4})-\d{2}-\d{2}\b/', Property::get('ipxe.compile-time'), $out) + || (int)$out[1] >= 2022) { + Header('Content-Type: text/plain; charset=UTF-8'); + echo $string; } else { - $cs = 'IBM437'; - } - Header('Content-Type: text/plain; charset=' . $cs); + if ($this->platform === 'EFI') { + $cs = 'ASCII'; + } else { + $cs = 'IBM437'; + } + Header('Content-Type: text/plain; charset=' . $cs); - echo iconv('UTF-8', $cs . '//TRANSLIT//IGNORE', $string); + setlocale(LC_ALL, 'de_DE.UTF-8', 'de_DE.utf-8', 'de_DE.utf8', 'de_DE', 'de', 'German', 'ge', 'en_US.UTF-8', 'en_US.utf-8'); + echo iconv('UTF-8', $cs . '//TRANSLIT//IGNORE', $string); + } } public function modfilt($str) @@ -337,28 +305,9 @@ BLA; return trim(preg_replace('/\s+/', ' ', $str)); } - /** - * @param IPxeMenu $menu - */ - private function menuCheckAutostart($menu) - { - // If this is a menu with a single item, treat a timeout of 0 as "boot immediately" instead of "infinite" - if ($menu->itemCount() === 1 && $menu->timeoutMs() === 0 && ($tmp = $menu->getDefaultEntryId()) !== null) { - if (empty($menu->items[0]->md5pass)) { - return $menu->items[0]->getBootEntryScript($this); - } else { - return $this->passwordDialog($menu->items[0]); - } - } - return ''; - } - const PROP_PW_SALT = 'ipxe.salt.'; - /** - * @param MenuEntry $menuEntryId - */ - private function passwordDialog($entry) + private function passwordDialog(MenuEntry $entry): string { if ($this->hasExtension) { $salt = dechex(mt_rand(0x100000, 0xFFFFFF)); @@ -384,7 +333,7 @@ chain -a \${self}&entryid={$entry->menuentryid}##params || goto fail || HERE; } - public function getMenuEntry($entry, $honorPassword = true) + public function getMenuEntry(?MenuEntry $entry, bool $honorPassword = true): string { if ($entry === null) return "#!ipxe\nprompt --timeout 10000 Invalid menu entry id\n"; @@ -438,7 +387,7 @@ HERE; return $output; } - public function execDataToScript($agnostic, $bios, $efi) : string + public function execDataToScript(?ExecData $agnostic, ?ExecData $bios, ?ExecData $efi) : string { if ($agnostic !== null) return $this->execDataToScriptInternal($agnostic) . "\ngoto fail\n"; |