<?php
class Page_MiniLinux extends Page
{
protected function doPreprocess()
{
User::load();
if (!User::isLoggedIn()) {
Message::addError('main.no-permission');
Util::redirect('?do=Main');
}
if (Request::isPost()) {
$show = Request::post('show', false, 'string');
if ($show === 'delete') {
$this->deleteVersion();
} elseif ($show === 'updatesources') {
$this->updateSources();
} elseif ($show === 'setdefault') {
$this->setDefault();
}
Util::redirect('?do=minilinux');
}
User::assertPermission('view');
Dashboard::addSubmenu('?do=minilinux', Dictionary::translate('menu-versions'));
Dashboard::addSubmenu('?do=minilinux&show=sources', Dictionary::translate('menu-sources'));
}
protected function doRender()
{
$show = Request::get('show', 'list', 'string');
if ($show === 'list') {
// List branches and versions
$branches = Database::queryAll('SELECT sourceid, branchid, title, color, description FROM minilinux_branch ORDER BY title ASC');
$versions = MiniLinux::queryAllVersionsByBranch();
$usage = MiniLinux::getBootMenuUsage();
$sourceList = [];
// Group by branch for detailed listing, add usage info
foreach ($branches as &$branch) {
// Little hack: We abuse the title for ordering, so if the second char is a space, assume the first one
// is just for sort order and remove it.
if ($branch['title'][1] === ' ') {
$branch['title'] = substr($branch['title'], 2);
}
$bid = 'div-' . str_replace('/', '-', $branch['branchid']);
if (!isset($sourceList[$branch['sourceid']])) {
$sourceList[$branch['sourceid']] = ['sourceid' => $branch['sourceid'], 'list' => []];
}
$sourceList[$branch['sourceid']]['list'][] = [
'title' => $branch['title'],
'color' => $branch['color'],
'bid' => $bid
];
$branch['bid'] = $bid;
if (isset($versions[$branch['branchid']])) {
$branch['versionlist'] = $this->renderVersionList($versions[$branch['branchid']], $usage);
}
}
unset($branch);
$sourceList = array_values($sourceList);
} elseif ($show === 'sources') {
// List sources
$res = Database::simpleQuery('SELECT sourceid, title, url, lastupdate, pubkey FROM minilinux_source ORDER BY title, sourceid');
$sourceViewData = ['list' => [], 'show_refresh' => true];
$tooOld = strtotime('-7 days');
$showRefresh = strtotime('-5 minutes');
foreach ($res as $row) {
$row['lastupdate_s'] = Util::prettyTime($row['lastupdate']);
if ($row['lastupdate'] != 0 && $row['lastupdate'] < $tooOld) {
$row['update_class'] = 'text-danger';
}
if ($row['lastupdate'] > $showRefresh) {
$sourceViewData['show_refresh'] = false;
}
$sourceViewData['list'][] = $row;
}
}
// Output
Render::addTemplate('page-minilinux', [
'default' => Property::get(MiniLinux::PROPERTY_DEFAULT_BOOT),
'sources' => $sourceList ?? null,
]);
// Warning
if (!MiniLinux::updateCurrentBootSetting()) {
Message::addError('default-not-installed', Property::get(MiniLinux::PROPERTY_DEFAULT_BOOT));
}
if (isset($branches)) {
Render::addTemplate('branches', ['branches' => $branches]);
} elseif (isset($sourceViewData)) {
Render::addTemplate('sources', $sourceViewData);
} else {
Message::addError('main.invalid-action', $show);
}
}
protected function doAjax()
{
User::load();
$show = Request::post('show', false, 'string');
if ($show === 'version') {
$this->ajaxVersionDetails();
} elseif ($show === 'download') {
$this->ajaxDownload();
}
}
private function renderVersionList(array $versions, array $usage): string
{
$def = Property::get(MiniLinux::PROPERTY_DEFAULT_BOOT);
//$eff = Property::get(MiniLinux::PROPERTY_DEFAULT_BOOT_EFFECTIVE);
foreach ($versions as &$version) {
$version['dateline_s'] = Util::prettyTime($version['dateline']);
$version['orphan'] = ($version['orphan'] > 0 && $version['installed'] == MiniLinux::INSTALL_MISSING) || ($version['orphan'] > 1);
$version['downloading'] = $version['taskid'] && Taskmanager::isRunning(Taskmanager::status($version['taskid']));
if ($version['installed'] != MiniLinux::INSTALL_MISSING && $version['versionid'] !== $def) {
$version['showsetdefault'] = true;
}
if ($version['versionid'] === $def) {
$version['isdefault'] = true;
if (!$version['installed'] != MiniLinux::INSTALL_OK) {
$version['default_class'] = 'bg-danger';
}
}
if (isset($usage[$version['versionid']])) {
$version['usage'] = $usage[$version['versionid']];
}
$version['versionid_dash'] = str_replace('/', '-', $version['versionid']);
}
return Render::parse('versionlist', ['versions' => array_values($versions)]);
}
private function ajaxVersionDetails()
{
User::assertPermission('view');
$verify = Request::post('verify', false, 'bool');
$versionid = Request::post('version', false, 'string');
if ($versionid === false) {
die('What!');
}
$ver = Database::queryFirst('SELECT versionid, description, taskid, data, installed
FROM minilinux_version WHERE versionid = :versionid',
['versionid' => $versionid]);
if ($ver === false) {
die('No such version');
}
$versionid = $ver['versionid']; // Just to be sure -- should be safe for building a path either way
$data = json_decode($ver['data'], true);
if (!is_array($data)) {
die('Corrupted data');
}
$data['versionid'] = $versionid;
$data['dltask'] = MiniLinux::validateDownloadTask($versionid, $ver['taskid']);
$data['verify_button'] = !$verify && $data['dltask'] === null;
if (is_array($data['files'])) {
$valid = true;
$sort = [];
foreach ($data['files'] as &$file) {
if (empty($file['name'])) {
$sort[] = 'zzz' . implode(',', $file);
continue;
}
$sort[] = $file['name'];
$s = $this->getFileState($versionid, $file, $verify);
if ($s !== self::FILE_OK) {
$valid = false;
}
if ($s !== self::FILE_MISSING) {
$data['delete_button'] = true;
}
$file['state'] = $this->fileStateToString($s);
if (isset($file['size'])) {
$file['size_s'] = Util::readableFileSize($file['size']);
}
if (isset($file['mtime'])) {
$file['mtime_s'] = Util::prettyTime($file['mtime']);
}
if ($data['dltask'] !== null) {
$file['fileid'] = MiniLinux::fileToId($versionid, $file['name']);
}
}
unset($file);
array_multisort($sort, SORT_ASC, $data['files']);
if (!$valid) {
$data['verify_button'] = false;
if ($ver['installed'] != MiniLinux::INSTALL_MISSING) {
MiniLinux::setInstalledState($versionid, MiniLinux::INSTALL_BROKEN);
}
} elseif ($ver['installed'] != MiniLinux::INSTALL_OK && $verify) {
MiniLinux::setInstalledState($versionid, MiniLinux::INSTALL_OK);
}
if ((!$valid || !$verify) && $ver['installed'] != MiniLinux::INSTALL_MISSING) {
$data['delete_button'] = true;
}
}
if ($data['dltask'] !== null || $ver['installed'] != MiniLinux::INSTALL_MISSING) {
MiniLinux::checkStage4($data, $data['s4_errors']);
}
$data['changelog'] = Util::markup($ver['description'] ?? '');
echo Render::parse('filelist', $data);
}
const FILE_OK = 0;
const FILE_MISSING = 1;
const FILE_SIZE_MISMATCH = 2;
const FILE_CHECKSUM_BAD = 3;
const FILE_NOT_READABLE = 4;
private function getFileState(string $versionid, array $file, bool $verify): int
{
$path = CONFIG_HTTP_DIR . '/' . $versionid . '/' . $file['name'];
if (!is_file($path))
return self::FILE_MISSING;
if (isset($file['size']) && filesize($path) != $file['size'])
return self::FILE_SIZE_MISMATCH;
if (!is_readable($path))
return self::FILE_NOT_READABLE;
if ($verify) {
foreach (['sha512', 'sha384', 'sha256', 'sha224', 'sha1', 'md5'] as $algo) {
if (isset($file[$algo])) {
$calced = hash_file($algo, $path);
if ($calced === false)
continue; // Algo not supported?
if ($calced !== $file[$algo])
return self::FILE_CHECKSUM_BAD;
}
}
}
return self::FILE_OK;
}
private function fileStateToString($state)
{
switch ($state) {
case self::FILE_CHECKSUM_BAD:
return Dictionary::translate('file-checksum-bad');
case self::FILE_SIZE_MISMATCH:
return Dictionary::translate('file-size-mismatch');
case self::FILE_MISSING:
return Dictionary::translate('file-missing');
case self::FILE_NOT_READABLE:
return Dictionary::translate('file-not-readable');
case self::FILE_OK:
return Dictionary::translate('file-ok');
}
return '???';
}
private function ajaxDownload()
{
User::assertPermission('update');
$version = Request::post('version', false, 'string');
if ($version === false) {
die('No version');
}
$task = MiniLinux::downloadVersion($version);
if ($task === null) {
Message::addError('no-such-version', $version);
Message::renderList();
} else {
$this->ajaxVersionDetails();
}
}
private function deleteVersion()
{
User::assertPermission('delete');
$versionid = Request::post('version', Request::REQUIRED, 'string');
$version = Database::queryFirst('SELECT versionid FROM minilinux_version WHERE versionid = :versionid',
['versionid' => $versionid]);
if ($version === false) {
Message::addError('no-such-version');
return;
}
$path = CONFIG_HTTP_DIR . '/' . $version['versionid'];
$task = Taskmanager::submit('DeleteDirectory', [
'path' => $path,
'recursive' => true,
]);
if ($task !== false) {
$task = Taskmanager::waitComplete($task, 2500);
if (Taskmanager::isFailed($task)) {
MiniLinux::setInstalledState($version['versionid'], MiniLinux::INSTALL_BROKEN);
Message::addError('delete-error', $versionid, $task['data']['error']);
} else {
MiniLinux::setInstalledState($version['versionid'], MiniLinux::INSTALL_MISSING);
Message::addSuccess('version-deleted', $versionid);
}
}
}
private function updateSources()
{
User::assertPermission('view'); // As it doesn't really change anything, accept view permission
$ret = MiniLinux::updateList();
if ($ret > 0) {
for ($i = 0; $i < 6; ++$i) {
sleep(1);
if (!Trigger::checkCallbacks())
break;
}
}
}
private function setDefault()
{
User::assertPermission('update');
$versionid = Request::post('version', Request::REQUIRED, 'string');
$version = Database::queryFirst('SELECT versionid FROM minilinux_version WHERE versionid = :versionid',
['versionid' => $versionid]);
if ($version === false) {
Message::addError('no-such-version');
return;
}
MiniLinux::setDefaultVersion($version['versionid']);
}
}