databaseRequired = false;
}
}
}
namespace Documents\Install {
use Configuration\Configuration;
use Configuration\CreateDatabase;
use Driver\SQL\Query\Commit;
use Driver\SQL\Query\RollBack;
use Driver\SQL\Query\StartTransaction;
use Driver\SQL\SQL;
use Elements\Body;
use Elements\Head;
use Elements\Link;
use Elements\Script;
use External\PHPMailer\Exception;
use External\PHPMailer\PHPMailer;
use Objects\ConnectionData;
class InstallHead extends Head {
public function __construct($document) {
parent::__construct($document);
}
protected function initSources() {
$this->loadJQuery();
$this->loadBootstrap();
$this->loadFontawesome();
$this->addJS(Script::CORE);
$this->addCSS(Link::CORE);
$this->addJS(Script::INSTALL);
}
protected function initMetas(): array {
return array(
array('name' => 'viewport', 'content' => 'width=device-width, initial-scale=1.0'),
array('name' => 'format-detection', 'content' => 'telephone=yes'),
array('charset' => 'utf-8'),
array("http-equiv" => 'expires', 'content' => '0'),
array("name" => 'robots', 'content' => 'noarchive'),
);
}
protected function initRawFields(): array {
return array();
}
protected function initTitle(): string {
return "WebBase - Installation";
}
}
class InstallBody extends Body {
// Status enum
const NOT_STARTED = 0;
const PENDING = 1;
const SUCCESSFUL = 2;
const ERROR = 3;
// Step enum
const CHECKING_REQUIREMENTS = 1;
const INSTALL_DEPENDENCIES = 2;
const DATABASE_CONFIGURATION = 3;
const CREATE_USER = 4;
const ADD_MAIL_SERVICE = 5;
const FINISH_INSTALLATION = 6;
//
private string $errorString;
private int $currentStep;
private array $steps;
function __construct($document) {
parent::__construct($document);
$this->errorString = "";
$this->currentStep = InstallBody::CHECKING_REQUIREMENTS;
$this->steps = array();
}
function isDocker(): bool {
return file_exists("/.dockerenv");
}
private function getParameter($name): ?string {
if (isset($_REQUEST[$name]) && is_string($_REQUEST[$name])) {
return trim($_REQUEST[$name]);
}
return NULL;
}
private function composerInstall(bool $dryRun = false): array {
$command = "composer install";
if ($dryRun) {
$command .= " --dry-run";
}
$fds = [
"1" => ["pipe", "w"],
"2" => ["pipe", "w"],
];
$dir = $this->getExternalDirectory();
$env = null;
if (!getenv("HOME")) {
$env = ["COMPOSER_HOME" => $dir];
}
$proc = proc_open($command, $fds, $pipes, $dir, $env);
$output = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]);
$status = proc_close($proc);
return [$status, $output];
}
private function getExternalDirectory(bool $absolute = true): string {
if ($absolute) {
return implode(DIRECTORY_SEPARATOR, [WEBROOT, "core", "External"]);;
} else {
return implode(DIRECTORY_SEPARATOR, ["core", "External"]);
}
}
private function getCurrentStep(): int {
if (!$this->checkRequirements()["success"]) {
return self::CHECKING_REQUIREMENTS;
}
$externalDir = $this->getExternalDirectory();
$autoload = implode(DIRECTORY_SEPARATOR, [$externalDir, "vendor", "autoload.php"]);
if (!is_file($autoload)) {
return self::INSTALL_DEPENDENCIES;
} else {
list ($status, $output) = $this->composerInstall(true);
if ($status !== 0) {
$this->errorString = "Error executing 'composer install --dry-run'. Please verify that the command succeeds locally and then try again. Status Code: $status, Output: $output";
return self::CHECKING_REQUIREMENTS;
} else {
if (!contains($output, "Nothing to install, update or remove")) {
return self::INSTALL_DEPENDENCIES;
}
}
}
$context = $this->getDocument()->getContext();
$config = $context->getConfig();
// Check if database configuration exists
if (!$config->getDatabase()) {
return self::DATABASE_CONFIGURATION;
}
$sql = $context->getSQL();
if (!$sql || !$sql->isConnected()) {
return self::DATABASE_CONFIGURATION;
}
$countKeyword = $sql->count();
$res = $sql->select($countKeyword)->from("User")->execute();
if ($res === FALSE) {
return self::DATABASE_CONFIGURATION;
} else {
if ($res[0]["count"] > 0) {
$step = self::ADD_MAIL_SERVICE;
} else {
return self::CREATE_USER;
}
}
if ($step === self::ADD_MAIL_SERVICE) {
$req = new \Api\Settings\Get($context);
$success = $req->execute(array("key" => "^mail_enabled$"));
if (!$success) {
$this->errorString = $req->getLastError();
return self::DATABASE_CONFIGURATION;
} else if (isset($req->getResult()["settings"]["mail_enabled"])) {
$step = self::FINISH_INSTALLATION;
$req = new \Api\Settings\Set($context);
$success = $req->execute(array("settings" => array("installation_completed" => "1")));
if (!$success) {
$this->errorString = $req->getLastError();
} else {
$req = new \Api\Notifications\Create($context);
$req->execute(array(
"title" => "Welcome",
"message" => "Your Web-base was successfully installed. Check out the admin dashboard. Have fun!",
"groupId" => USER_GROUP_ADMIN
)
);
$this->errorString = $req->getLastError();
}
}
}
return $step;
}
private function command_exist(string $cmd): bool {
$return = shell_exec(sprintf("which %s 2>/dev/null", escapeshellarg($cmd)));
return !empty($return);
}
private function checkRequirements(): array {
$msg = $this->errorString;
$success = true;
$failedRequirements = array();
if (!is_writeable(WEBROOT)) {
$failedRequirements[] = sprintf("%s is not writeable. Try running chmod 700 %s", WEBROOT, WEBROOT);
$success = false;
}
if (function_exists("posix_getuid")) {
$userId = posix_getuid();
if (fileowner(WEBROOT) !== $userId) {
$username = posix_getpwuid($userId)['name'];
$failedRequirements[] = sprintf("%s is not owned by current user: $username ($userId). " .
"Try running chown -R $userId %s or give the required directories write permissions: " .
"core/Configuration, core/Cache, core/External",
WEBROOT, WEBROOT);
$success = false;
}
}
if (!function_exists("yaml_emit")) {
$failedRequirements[] = "YAML extension is not installed.";
$success = false;
}
if (version_compare(PHP_VERSION, '7.4', '<')) {
$failedRequirements[] = "PHP Version >= 7.4 is required. Got: " . PHP_VERSION . "";
$success = false;
}
if (!$this->command_exist("composer")) {
$failedRequirements[] = "Composer is not installed or cannot be found.";
$success = false;
}
if (!$success) {
$msg = "The following requirements failed the check:
" .
$this->createUnorderedList($failedRequirements);
$this->errorString = $msg;
}
return array("success" => $success, "msg" => $msg);
}
private function installDependencies(): array {
list ($status, $output) = $this->composerInstall();
return ["success" => $status === 0, "msg" => $output];
}
private function databaseConfiguration(): array {
$host = $this->getParameter("host");
$port = $this->getParameter("port");
$username = $this->getParameter("username");
$password = $this->getParameter("password");
$database = $this->getParameter("database");
$type = $this->getParameter("type");
$encoding = $this->getParameter("encoding") ?? "UTF8";
$success = true;
$missingInputs = array();
if (empty($host)) {
$success = false;
$missingInputs[] = "Host";
}
if (empty($port)) {
$success = false;
$missingInputs[] = "Port";
}
if (empty($username)) {
$success = false;
$missingInputs[] = "Username";
}
if (is_null($password)) {
$success = false;
$missingInputs[] = "Password";
}
if (empty($database)) {
$success = false;
$missingInputs[] = "Database";
}
if (empty($type)) {
$success = false;
$missingInputs[] = "Type";
}
$supportedTypes = array("mysql", "postgres");
if (!$success) {
$msg = "Please fill out the following inputs:
" .
$this->createUnorderedList($missingInputs);
} else if (!is_numeric($port) || ($port = intval($port)) < 1 || $port > 65535) {
$msg = "Port must be in range of 1-65535.";
$success = false;
} else if (!in_array($type, $supportedTypes)) {
$msg = "Unsupported database type. Must be one of: " . implode(", ", $supportedTypes);
$success = false;
} else {
$connectionData = new ConnectionData($host, $port, $username, $password);
$connectionData->setProperty('database', $database);
$connectionData->setProperty('encoding', $encoding);
$connectionData->setProperty('type', $type);
$connectionData->setProperty('isDocker', $this->isDocker());
$sql = SQL::createConnection($connectionData);
$success = false;
if (is_string($sql)) {
$msg = "Error connecting to database: $sql";
} else if (!$sql->isConnected()) {
if (!$sql->checkRequirements()) {
$driverName = $sql->getDriverName();
$installLink = "https://www.php.net/manual/en/$driverName.setup.php";
$link = $this->createExternalLink($installLink);
$msg = "$driverName is not enabled yet. See: $link";
} else {
$msg = "Error connecting to database:
" . $sql->getLastError();
}
} else {
$msg = "";
$success = true;
$queries = CreateDatabase::createQueries($sql);
array_unshift($queries, new StartTransaction($sql));
$queries[] = new Commit($sql);
foreach ($queries as $query) {
try {
if (!$query->execute()) {
$msg = "Error creating tables: " . $sql->getLastError();
$success = false;
}
} finally {
if (!$success) {
(new RollBack($sql))->execute();
}
}
if (!$success) {
break;
}
}
if ($success) {
$context = $this->getDocument()->getContext();
$config = $context->getConfig();
if (Configuration::create("Database", $connectionData) === false) {
$success = false;
$msg = "Unable to write database file";
} else {
$config->setDatabase($connectionData);
if (!$context->initSQL()) {
$success = false;
$msg = "Unable to verify database connection after installation";
} else {
$req = new \Api\Routes\GenerateCache($context);
if (!$req->execute()) {
$success = false;
$msg = "Unable to write route file: " . $req->getLastError();
}
}
}
}
$sql->close();
}
}
return array("success" => $success, "msg" => $msg);
}
private function createUser(): array {
$context = $this->getDocument()->getContext();
if ($this->getParameter("prev") === "true") {
$success = $context->getConfig()->delete("Database");
$msg = $success ? "" : error_get_last();
return array("success" => $success, "msg" => $msg);
}
$username = $this->getParameter("username");
$password = $this->getParameter("password");
$confirmPassword = $this->getParameter("confirmPassword");
$email = $this->getParameter("email") ?? "";
$success = true;
$missingInputs = array();
if (empty($username)) {
$success = false;
$missingInputs[] = "Username";
}
if (empty($password)) {
$success = false;
$missingInputs[] = "Password";
}
if (empty($confirmPassword)) {
$success = false;
$missingInputs[] = "Confirm Password";
}
if (!$success) {
$msg = "Please fill out the following inputs:
" .
$this->createUnorderedList($missingInputs);
} else {
$req = new \Api\User\Create($context);
$success = $req->execute(array(
'username' => $username,
'email' => $email,
'password' => $password,
'confirmPassword' => $confirmPassword,
));
$msg = $req->getLastError();
if ($success) {
$sql = $context->getSQL();
$success = $sql->insert("UserGroup", array("group_id", "user_id"))
->addRow(USER_GROUP_ADMIN, $req->getResult()["userId"])
->execute();
$msg = $sql->getLastError();
}
}
return array("msg" => $msg, "success" => $success);
}
private function addMailService(): array {
$context = $this->getDocument()->getContext();
if ($this->getParameter("prev") === "true") {
$sql = $context->getSQL();
$success = $sql->delete("User")->execute();
$msg = $sql->getLastError();
return array("success" => $success, "msg" => $msg);
}
if ($this->getParameter("skip") === "true") {
$req = new \Api\Settings\Set($context);
$success = $req->execute(array("settings" => array("mail_enabled" => "0")));
$msg = $req->getLastError();
} else {
$address = $this->getParameter("address");
$port = $this->getParameter("port");
$username = $this->getParameter("username");
$password = $this->getParameter("password");
$success = true;
$missingInputs = array();
if (empty($address)) {
$success = false;
$missingInputs[] = "SMTP Address";
}
if (empty($port)) {
$success = false;
$missingInputs[] = "Port";
}
if (empty($username)) {
$success = false;
$missingInputs[] = "Username";
}
if (is_null($password)) {
$success = false;
$missingInputs[] = "Password";
}
if (!$success) {
$msg = "Please fill out the following inputs:
" .
$this->createUnorderedList($missingInputs);
} else if (!is_numeric($port) || ($port = intval($port)) < 1 || $port > 65535) {
$msg = "Port must be in range of 1-65535.";
$success = false;
} else {
$success = false;
$mail = new PHPMailer(true);
$mail->IsSMTP();
$mail->SMTPAuth = true;
$mail->Username = $username;
$mail->Password = $password;
$mail->Host = $address;
$mail->Port = $port;
$mail->SMTPSecure = 'tls';
$mail->Timeout = 10;
try {
$success = $mail->SmtpConnect();
if (!$success) {
$error = empty($mail->ErrorInfo) ? "Unknown Error" : $mail->ErrorInfo;
$msg = "Could not connect to SMTP Server: $error";
} else {
$success = true;
$msg = "";
$mail->smtpClose();
}
} catch (Exception $error) {
$msg = "Could not connect to SMTP Server: " . $error->errorMessage();
}
if ($success) {
$req = new \Api\Settings\Set($context);
$success = $req->execute(array("settings" => array(
"mail_enabled" => "1",
"mail_host" => "$address",
"mail_port" => "$port",
"mail_username" => "$username",
"mail_password" => "$password",
)));
$msg = $req->getLastError();
}
}
}
return array("success" => $success, "msg" => $msg);
}
private function performStep(): array {
switch ($this->currentStep) {
case self::CHECKING_REQUIREMENTS:
return $this->checkRequirements();
case self::INSTALL_DEPENDENCIES:
return $this->installDependencies();
case self::DATABASE_CONFIGURATION:
return $this->databaseConfiguration();
case self::CREATE_USER:
return $this->createUser();
case self::ADD_MAIL_SERVICE:
return $this->addMailService();
default:
return array(
"success" => false,
"msg" => "Invalid step number"
);
}
}
private function createProgressSidebar(): string {
$items = array();
foreach ($this->steps as $num => $step) {
$title = $step["title"];
$status = $step["status"];
$currentStep = ($num == $this->currentStep) ? " id=\"currentStep\"" : "";
switch ($status) {
case self::PENDING:
$statusIcon = $this->createIcon("spinner");
$statusText = "Loading…";
$statusColor = "muted";
break;
case self::SUCCESSFUL:
$statusIcon = $this->createIcon("check-circle");
$statusText = "Successful";
$statusColor = "success";
break;
case self::ERROR:
$statusIcon = $this->createIcon("times-circle");
$statusText = "Failed";
$statusColor = "danger";
break;
case self::NOT_STARTED:
default:
$statusIcon = $this->createIcon("circle", "far");
$statusText = "Pending";
$statusColor = "muted";
break;
}
$items[] = "
Process the following steps and fill out the required forms to install your WebBase-Installation.