2020-02-09 23:02:19 +01:00
< ? php
2022-11-18 18:06:46 +01:00
namespace Core\Documents {
2020-04-03 15:56:04 +02:00
use Documents\Install\InstallBody ;
use Documents\Install\InstallHead ;
2022-11-18 18:06:46 +01:00
use Core\Elements\HtmlDocument ;
use Core\Objects\Router\Router ;
2020-04-03 15:56:04 +02:00
2021-12-08 16:53:43 +01:00
class Install extends HtmlDocument {
2022-06-01 12:28:50 +02:00
public function __construct ( Router $router ) {
parent :: __construct ( $router , InstallHead :: class , InstallBody :: class );
2020-04-02 21:39:02 +02:00
$this -> databaseRequired = false ;
2020-02-09 23:02:19 +01:00
}
}
}
namespace Documents\Install {
2022-11-18 18:06:46 +01:00
use Core\Configuration\Configuration ;
use Core\Configuration\CreateDatabase ;
2023-01-05 22:47:17 +01:00
use Core\Driver\SQL\Expression\Count ;
2022-11-18 18:06:46 +01:00
use Core\Driver\SQL\SQL ;
use Core\Elements\Body ;
use Core\Elements\Head ;
use Core\Elements\Link ;
use Core\Elements\Script ;
use Core\External\PHPMailer\Exception ;
use Core\External\PHPMailer\PHPMailer ;
use Core\Objects\ConnectionData ;
2022-11-20 17:13:53 +01:00
use Core\Objects\DatabaseEntity\Group ;
2024-05-04 12:23:14 +02:00
use Core\Objects\DatabaseEntity\User ;
2020-04-03 15:56:04 +02:00
class InstallHead extends Head {
2020-02-09 23:02:19 +01:00
public function __construct ( $document ) {
parent :: __construct ( $document );
}
protected function initSources () {
$this -> loadJQuery ();
$this -> loadBootstrap ();
$this -> loadFontawesome ();
2020-04-03 15:56:04 +02:00
$this -> addJS ( Script :: CORE );
$this -> addCSS ( Link :: CORE );
$this -> addJS ( Script :: INSTALL );
2020-02-09 23:02:19 +01:00
}
2021-04-02 21:58:06 +02:00
protected function initMetas () : array {
2024-05-04 12:23:14 +02:00
return [
[ 'name' => 'viewport' , 'content' => 'width=device-width, initial-scale=1.0' ],
[ 'name' => 'format-detection' , 'content' => 'telephone=yes' ],
[ 'charset' => 'utf-8' ],
[ " http-equiv " => 'expires' , 'content' => '0' ],
[ " name " => 'robots' , 'content' => 'noarchive' ],
];
2020-02-09 23:02:19 +01:00
}
2021-04-02 21:58:06 +02:00
protected function initRawFields () : array {
2024-05-04 12:23:14 +02:00
return [];
2020-02-09 23:02:19 +01:00
}
2021-04-02 21:58:06 +02:00
protected function initTitle () : string {
2020-02-09 23:02:19 +01:00
return " WebBase - Installation " ;
}
}
2020-04-03 15:56:04 +02:00
class InstallBody extends Body {
2020-02-09 23:02:19 +01:00
// Status enum
const NOT_STARTED = 0 ;
const PENDING = 1 ;
2020-04-03 15:56:04 +02:00
const SUCCESSFUL = 2 ;
2020-02-09 23:02:19 +01:00
const ERROR = 3 ;
// Step enum
2020-04-03 15:56:04 +02:00
const CHECKING_REQUIREMENTS = 1 ;
2022-02-20 23:17:17 +01:00
const INSTALL_DEPENDENCIES = 2 ;
const DATABASE_CONFIGURATION = 3 ;
const CREATE_USER = 4 ;
const ADD_MAIL_SERVICE = 5 ;
const FINISH_INSTALLATION = 6 ;
2020-02-09 23:02:19 +01:00
//
2020-04-03 15:56:04 +02:00
private string $errorString ;
private int $currentStep ;
private array $steps ;
2020-02-09 23:02:19 +01:00
function __construct ( $document ) {
parent :: __construct ( $document );
2020-02-09 23:30:26 +01:00
$this -> errorString = " " ;
2020-04-03 15:56:04 +02:00
$this -> currentStep = InstallBody :: CHECKING_REQUIREMENTS ;
2024-05-04 12:23:14 +02:00
$this -> steps = [];
2020-02-09 23:02:19 +01:00
}
2021-04-02 21:58:06 +02:00
private function getParameter ( $name ) : ? string {
if ( isset ( $_REQUEST [ $name ]) && is_string ( $_REQUEST [ $name ])) {
2020-02-09 23:02:19 +01:00
return trim ( $_REQUEST [ $name ]);
}
return NULL ;
}
2022-12-02 13:52:24 +01:00
private function yarnInstall ( string $reactDir ) : array {
$fds = [
" 1 " => [ " pipe " , " w " ],
" 2 " => [ " pipe " , " w " ],
];
$proc = proc_open ( " yarn install --frozen-lockfile --non-interactive " , $fds , $pipes , $reactDir );
$output = stream_get_contents ( $pipes [ 1 ]) . stream_get_contents ( $pipes [ 2 ]);
$status = proc_close ( $proc );
return [ $status , $output ];
}
private function yarnBuild ( string $reactDir ) : array {
$fds = [
" 1 " => [ " pipe " , " w " ],
" 2 " => [ " pipe " , " w " ],
];
$proc = proc_open ( " yarn run build " , $fds , $pipes , $reactDir );
$output = stream_get_contents ( $pipes [ 1 ]) . stream_get_contents ( $pipes [ 2 ]);
$status = proc_close ( $proc );
return [ $status , $output ];
}
2022-02-21 00:32:40 +01:00
private function composerInstall ( bool $dryRun = false ) : array {
$command = " composer install " ;
2022-02-20 23:17:17 +01:00
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 ];
}
2022-02-21 00:32:40 +01:00
private function getExternalDirectory ( bool $absolute = true ) : string {
if ( $absolute ) {
2022-11-18 18:06:46 +01:00
return implode ( DIRECTORY_SEPARATOR , [ WEBROOT , " Core " , " External " ]);
2022-02-21 00:32:40 +01:00
} else {
2022-11-18 18:06:46 +01:00
return implode ( DIRECTORY_SEPARATOR , [ " Core " , " External " ]);
2022-02-21 00:32:40 +01:00
}
2022-02-20 23:17:17 +01:00
}
2021-04-02 21:58:06 +02:00
private function getCurrentStep () : int {
2020-02-09 23:02:19 +01:00
2021-04-02 21:58:06 +02:00
if ( ! $this -> checkRequirements ()[ " success " ]) {
2020-04-03 15:56:04 +02:00
return self :: CHECKING_REQUIREMENTS ;
2020-02-09 23:02:19 +01:00
}
2022-02-20 23:17:17 +01:00
$externalDir = $this -> getExternalDirectory ();
2022-02-21 00:32:40 +01:00
$autoload = implode ( DIRECTORY_SEPARATOR , [ $externalDir , " vendor " , " autoload.php " ]);
if ( ! is_file ( $autoload )) {
2022-02-20 23:17:17 +01:00
return self :: INSTALL_DEPENDENCIES ;
} else {
2022-02-21 00:32:40 +01:00
list ( $status , $output ) = $this -> composerInstall ( true );
2022-02-20 23:17:17 +01:00
if ( $status !== 0 ) {
2022-02-21 00:32:40 +01:00
$this -> errorString = " Error executing 'composer install --dry-run'. Please verify that the command succeeds locally and then try again. Status Code: $status , Output: $output " ;
2022-02-20 23:17:17 +01:00
return self :: CHECKING_REQUIREMENTS ;
} else {
2022-02-21 00:32:40 +01:00
if ( ! contains ( $output , " Nothing to install, update or remove " )) {
2022-02-20 23:17:17 +01:00
return self :: INSTALL_DEPENDENCIES ;
}
}
}
2022-06-20 19:52:31 +02:00
$context = $this -> getDocument () -> getContext ();
$config = $context -> getConfig ();
2020-02-09 23:02:19 +01:00
// Check if database configuration exists
2021-04-02 21:58:06 +02:00
if ( ! $config -> getDatabase ()) {
2020-02-09 23:02:19 +01:00
return self :: DATABASE_CONFIGURATION ;
}
2022-06-20 19:52:31 +02:00
$sql = $context -> getSQL ();
2021-04-02 21:58:06 +02:00
if ( ! $sql || ! $sql -> isConnected ()) {
2020-06-25 16:54:58 +02:00
return self :: DATABASE_CONFIGURATION ;
}
2024-05-04 12:23:14 +02:00
$userCount = User :: count ( $sql );
if ( $userCount === FALSE ) {
2020-04-02 00:02:51 +02:00
return self :: DATABASE_CONFIGURATION ;
} else {
2024-05-04 12:23:14 +02:00
if ( $userCount > 0 ) {
2020-02-10 00:52:25 +01:00
$step = self :: ADD_MAIL_SERVICE ;
} else {
return self :: CREATE_USER ;
2020-02-09 23:02:19 +01:00
}
}
2020-06-25 16:54:58 +02:00
if ( $step === self :: ADD_MAIL_SERVICE ) {
2022-11-18 18:06:46 +01:00
$req = new \Core\API\Settings\Get ( $context );
2024-05-04 12:23:14 +02:00
$success = $req -> execute ([ " key " => " ^mail_enabled $ " ]);
2020-06-25 16:54:58 +02:00
if ( ! $success ) {
$this -> errorString = $req -> getLastError ();
return self :: DATABASE_CONFIGURATION ;
} else if ( isset ( $req -> getResult ()[ " settings " ][ " mail_enabled " ])) {
$step = self :: FINISH_INSTALLATION ;
2022-11-18 18:06:46 +01:00
$req = new \Core\API\Settings\Set ( $context );
2024-05-04 12:23:14 +02:00
$success = $req -> execute ([ " settings " => [ " installation_completed " => " 1 " ]]);
2020-04-02 21:19:06 +02:00
if ( ! $success ) {
$this -> errorString = $req -> getLastError ();
}
2020-02-09 23:30:26 +01:00
}
2020-02-09 23:02:19 +01:00
}
return $step ;
}
2022-02-20 23:17:17 +01:00
private function command_exist ( string $cmd ) : bool {
$return = shell_exec ( sprintf ( " which %s 2>/dev/null " , escapeshellarg ( $cmd )));
return ! empty ( $return );
}
2021-04-02 21:58:06 +02:00
private function checkRequirements () : array {
2020-02-09 23:02:19 +01:00
2020-02-09 23:30:26 +01:00
$msg = $this -> errorString ;
2020-02-09 23:02:19 +01:00
$success = true ;
2024-05-04 12:23:14 +02:00
$failedRequirements = [];
2020-02-09 23:02:19 +01:00
2024-04-22 12:03:24 +02:00
$requiredDirectories = [
" /Site/Cache " ,
" /Site/Logs " ,
" /Site/Configuration " ,
" /Core/External/vendor " ,
" /files/uploaded " ,
];
$nonWritableDirectories = [];
foreach ( $requiredDirectories as $directory ) {
if ( ! is_writeable ( WEBROOT . $directory )) {
$nonWritableDirectories [] = $directory ;
}
}
if ( ! empty ( $nonWritableDirectories )) {
$currentUser = getCurrentUsername ();
if ( function_exists ( " posix_getuid " )) {
$currentUser .= " (uid: " . posix_getuid () . " ) " ;
}
$failedRequirements [] = " One or more directories are not writable. " .
" Make sure the current user $currentUser has write-access to following locations: " .
$this -> createUnorderedList ( $nonWritableDirectories );
2020-04-02 21:57:06 +02:00
$success = false ;
2020-02-09 23:02:19 +01:00
}
2024-04-23 20:14:32 +02:00
if ( ! class_exists ( " Redis " )) {
$failedRequirements [] = " <b>redis</b> extension is not installed. " ;
$success = false ;
}
2022-02-20 23:17:17 +01:00
if ( ! function_exists ( " yaml_emit " )) {
$failedRequirements [] = " <b>YAML</b> extension is not installed. " ;
$success = false ;
}
2024-04-22 12:41:15 +02:00
$requiredVersion = " 8.2 " ;
2022-08-20 22:04:09 +02:00
if ( version_compare ( PHP_VERSION , $requiredVersion , '<' )) {
$failedRequirements [] = " PHP Version <b>>= $requiredVersion </b> is required. Got: <b> " . PHP_VERSION . " </b> " ;
2021-04-02 21:58:06 +02:00
$success = false ;
2020-02-09 23:02:19 +01:00
}
2022-02-20 23:17:17 +01:00
if ( ! $this -> command_exist ( " composer " )) {
$failedRequirements [] = " <b>Composer</b> is not installed or cannot be found. " ;
$success = false ;
}
2022-12-02 13:52:24 +01:00
if ( ! $this -> command_exist ( " yarn " )) {
$failedRequirements [] = " <b>Yarn</b> is not installed or cannot be found. " ;
$success = false ;
}
2021-04-02 21:58:06 +02:00
if ( ! $success ) {
2020-02-09 23:02:19 +01:00
$msg = " The following requirements failed the check:<br> " .
$this -> createUnorderedList ( $failedRequirements );
2020-04-02 22:25:13 +02:00
$this -> errorString = $msg ;
2020-02-09 23:02:19 +01:00
}
2024-04-22 12:41:15 +02:00
return [ " success " => $success , " msg " => $msg ];
2020-02-09 23:02:19 +01:00
}
2022-02-20 23:17:17 +01:00
private function installDependencies () : array {
2022-02-21 00:32:40 +01:00
list ( $status , $output ) = $this -> composerInstall ();
2022-12-02 13:52:24 +01:00
if ( $status === 0 ) {
$reactDir = implode ( DIRECTORY_SEPARATOR , [ WEBROOT , " react " ]);
list ( $status , $output ) = $this -> yarnInstall ( $reactDir );
if ( $status === 0 ) {
list ( $status , $output ) = $this -> yarnBuild ( $reactDir );
}
}
2022-02-20 23:17:17 +01:00
return [ " success " => $status === 0 , " msg " => $output ];
}
2021-04-02 21:58:06 +02:00
private function databaseConfiguration () : array {
2020-02-09 23:02:19 +01:00
$host = $this -> getParameter ( " host " );
$port = $this -> getParameter ( " port " );
$username = $this -> getParameter ( " username " );
$password = $this -> getParameter ( " password " );
$database = $this -> getParameter ( " database " );
2020-04-02 00:02:51 +02:00
$type = $this -> getParameter ( " type " );
2022-02-20 23:17:17 +01:00
$encoding = $this -> getParameter ( " encoding " ) ? ? " UTF8 " ;
2020-02-09 23:02:19 +01:00
$success = true ;
2024-05-04 12:23:14 +02:00
$missingInputs = [];
2022-02-20 23:17:17 +01:00
if ( empty ( $host )) {
2020-02-09 23:02:19 +01:00
$success = false ;
$missingInputs [] = " Host " ;
}
2022-02-20 23:17:17 +01:00
if ( empty ( $port )) {
2020-02-09 23:02:19 +01:00
$success = false ;
$missingInputs [] = " Port " ;
}
2022-02-20 23:17:17 +01:00
if ( empty ( $username )) {
2020-02-09 23:02:19 +01:00
$success = false ;
$missingInputs [] = " Username " ;
}
2021-04-02 21:58:06 +02:00
if ( is_null ( $password )) {
2020-02-09 23:02:19 +01:00
$success = false ;
$missingInputs [] = " Password " ;
}
2022-02-20 23:17:17 +01:00
if ( empty ( $database )) {
2020-02-09 23:02:19 +01:00
$success = false ;
$missingInputs [] = " Database " ;
}
2022-02-20 23:17:17 +01:00
if ( empty ( $type )) {
2020-04-02 00:02:51 +02:00
$success = false ;
$missingInputs [] = " Type " ;
}
2024-04-22 12:41:15 +02:00
$supportedTypes = [ " mysql " , " postgres " ];
2021-04-02 21:58:06 +02:00
if ( ! $success ) {
2020-02-09 23:02:19 +01:00
$msg = " Please fill out the following inputs:<br> " .
$this -> createUnorderedList ( $missingInputs );
2021-04-02 21:58:06 +02:00
} else if ( ! is_numeric ( $port ) || ( $port = intval ( $port )) < 1 || $port > 65535 ) {
2020-02-09 23:02:19 +01:00
$msg = " Port must be in range of 1-65535. " ;
$success = false ;
2021-04-02 21:58:06 +02:00
} else if ( ! in_array ( $type , $supportedTypes )) {
2020-04-02 00:02:51 +02:00
$msg = " Unsupported database type. Must be one of: " . implode ( " , " , $supportedTypes );
$success = false ;
2020-02-09 23:02:19 +01:00
} else {
2020-04-03 15:56:04 +02:00
$connectionData = new ConnectionData ( $host , $port , $username , $password );
2020-02-09 23:02:19 +01:00
$connectionData -> setProperty ( 'database' , $database );
2020-04-02 00:02:51 +02:00
$connectionData -> setProperty ( 'encoding' , $encoding );
$connectionData -> setProperty ( 'type' , $type );
2024-04-23 20:14:32 +02:00
$connectionData -> setProperty ( 'isDocker' , isDocker ());
2020-04-03 15:56:04 +02:00
$sql = SQL :: createConnection ( $connectionData );
2020-04-02 00:02:51 +02:00
$success = false ;
2021-04-02 21:58:06 +02:00
if ( is_string ( $sql )) {
2020-04-03 15:56:04 +02:00
$msg = " Error connecting to database: $sql " ;
2021-04-02 21:58:06 +02:00
} else if ( ! $sql -> isConnected ()) {
2020-04-04 15:11:38 +02:00
if ( ! $sql -> checkRequirements ()) {
2020-04-02 01:48:46 +02:00
$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:<br> " . $sql -> getLastError ();
}
2020-02-09 23:02:19 +01:00
} else {
2020-04-02 00:02:51 +02:00
$msg = " " ;
$success = true ;
2020-04-03 15:56:04 +02:00
$queries = CreateDatabase :: createQueries ( $sql );
2024-03-27 14:12:01 +01:00
try {
$sql -> startTransaction ();
foreach ( $queries as $query ) {
2022-02-20 18:31:54 +01:00
if ( ! $query -> execute ()) {
$msg = " Error creating tables: " . $sql -> getLastError ();
$success = false ;
}
2024-03-27 14:12:01 +01:00
2022-02-20 18:31:54 +01:00
if ( ! $success ) {
2024-03-27 14:12:01 +01:00
break ;
2022-02-20 18:31:54 +01:00
}
}
2024-03-27 14:12:01 +01:00
} finally {
2022-02-20 18:31:54 +01:00
if ( ! $success ) {
2024-03-27 14:12:01 +01:00
$sql -> rollback ();
} else {
$sql -> commit ();
2020-02-09 23:02:19 +01:00
}
}
2022-06-20 19:52:31 +02:00
if ( $success ) {
$context = $this -> getDocument () -> getContext ();
$config = $context -> getConfig ();
2022-11-18 18:06:46 +01:00
if ( Configuration :: create ( \Site\Configuration\Database :: class , $connectionData ) === false ) {
2022-05-31 16:14:49 +02:00
$success = false ;
2022-06-20 19:52:31 +02:00
$msg = " Unable to write database file " ;
2022-05-31 16:14:49 +02:00
} else {
2022-06-20 19:52:31 +02:00
$config -> setDatabase ( $connectionData );
if ( ! $context -> initSQL ()) {
2022-05-31 16:14:49 +02:00
$success = false ;
2022-06-20 19:52:31 +02:00
$msg = " Unable to verify database connection after installation " ;
} else {
2022-11-18 18:06:46 +01:00
$req = new \Core\API\Routes\GenerateCache ( $context );
2022-06-20 19:52:31 +02:00
if ( ! $req -> execute ()) {
$success = false ;
$msg = " Unable to write route file: " . $req -> getLastError ();
}
2022-05-31 16:14:49 +02:00
}
}
2020-02-09 23:02:19 +01:00
}
2020-04-02 00:02:51 +02:00
$sql -> close ();
}
2020-02-09 23:02:19 +01:00
}
2024-05-04 12:23:14 +02:00
return [ " success " => $success , " msg " => $msg ];
2020-02-09 23:02:19 +01:00
}
2021-04-02 21:58:06 +02:00
private function createUser () : array {
2020-02-09 23:02:19 +01:00
2022-06-20 19:52:31 +02:00
$context = $this -> getDocument () -> getContext ();
2021-04-02 21:58:06 +02:00
if ( $this -> getParameter ( " prev " ) === " true " ) {
2024-05-04 12:23:14 +02:00
// TODO: drop the previous database here?
$success = $context -> getConfig () -> delete ( " \\ Site \\ Configuration \\ Database " );
2020-02-09 23:02:19 +01:00
$msg = $success ? " " : error_get_last ();
2024-05-04 12:23:14 +02:00
return [ " success " => $success , " msg " => $msg ];
2020-02-09 23:02:19 +01:00
}
$username = $this -> getParameter ( " username " );
$password = $this -> getParameter ( " password " );
$confirmPassword = $this -> getParameter ( " confirmPassword " );
2020-04-04 01:15:59 +02:00
$email = $this -> getParameter ( " email " ) ? ? " " ;
2020-02-09 23:02:19 +01:00
$success = true ;
2024-05-04 12:23:14 +02:00
$missingInputs = [];
2020-02-09 23:02:19 +01:00
2022-02-20 23:17:17 +01:00
if ( empty ( $username )) {
2020-02-09 23:02:19 +01:00
$success = false ;
$missingInputs [] = " Username " ;
}
2022-02-20 23:17:17 +01:00
if ( empty ( $password )) {
2020-02-09 23:02:19 +01:00
$success = false ;
$missingInputs [] = " Password " ;
}
2022-02-20 23:17:17 +01:00
if ( empty ( $confirmPassword )) {
2020-02-09 23:02:19 +01:00
$success = false ;
$missingInputs [] = " Confirm Password " ;
}
2021-04-02 21:58:06 +02:00
if ( ! $success ) {
2020-02-09 23:02:19 +01:00
$msg = " Please fill out the following inputs:<br> " .
$this -> createUnorderedList ( $missingInputs );
} else {
2022-11-18 18:06:46 +01:00
$req = new \Core\API\User\Create ( $context );
2024-05-04 12:23:14 +02:00
$success = $req -> execute ([
2020-06-23 18:40:43 +02:00
'username' => $username ,
'email' => $email ,
'password' => $password ,
'confirmPassword' => $confirmPassword ,
2022-11-20 17:13:53 +01:00
'groups' => [ Group :: ADMIN ]
2024-05-04 12:23:14 +02:00
]);
2020-06-23 18:40:43 +02:00
$msg = $req -> getLastError ();
2020-02-09 23:02:19 +01:00
}
2024-05-04 12:23:14 +02:00
return [ " msg " => $msg , " success " => $success ];
2020-02-09 23:02:19 +01:00
}
2021-04-02 21:58:06 +02:00
private function addMailService () : array {
2020-02-09 23:02:19 +01:00
2022-06-20 19:52:31 +02:00
$context = $this -> getDocument () -> getContext ();
2021-04-02 21:58:06 +02:00
if ( $this -> getParameter ( " prev " ) === " true " ) {
2022-06-20 19:52:31 +02:00
$sql = $context -> getSQL ();
2020-04-02 00:02:51 +02:00
$success = $sql -> delete ( " User " ) -> execute ();
$msg = $sql -> getLastError ();
2024-05-04 12:23:14 +02:00
return [ " success " => $success , " msg " => $msg ];
2020-02-09 23:02:19 +01:00
}
2021-04-02 21:58:06 +02:00
if ( $this -> getParameter ( " skip " ) === " true " ) {
2022-11-18 18:06:46 +01:00
$req = new \Core\API\Settings\Set ( $context );
2024-04-11 20:41:03 +02:00
$success = $req -> execute ([ " settings " => [ " mail_enabled " => false ]]);
2020-06-25 16:54:58 +02:00
$msg = $req -> getLastError ();
2020-02-09 23:02:19 +01:00
} else {
$address = $this -> getParameter ( " address " );
$port = $this -> getParameter ( " port " );
$username = $this -> getParameter ( " username " );
$password = $this -> getParameter ( " password " );
$success = true ;
2024-05-04 12:23:14 +02:00
$missingInputs = [];
2022-06-20 19:52:31 +02:00
if ( empty ( $address )) {
2020-02-09 23:02:19 +01:00
$success = false ;
$missingInputs [] = " SMTP Address " ;
}
2022-06-20 19:52:31 +02:00
if ( empty ( $port )) {
2020-02-09 23:02:19 +01:00
$success = false ;
$missingInputs [] = " Port " ;
}
2022-06-20 19:52:31 +02:00
if ( empty ( $username )) {
2020-02-09 23:02:19 +01:00
$success = false ;
$missingInputs [] = " Username " ;
}
2021-04-02 21:58:06 +02:00
if ( is_null ( $password )) {
2020-02-09 23:02:19 +01:00
$success = false ;
$missingInputs [] = " Password " ;
}
2021-04-02 21:58:06 +02:00
if ( ! $success ) {
2020-02-09 23:02:19 +01:00
$msg = " Please fill out the following inputs:<br> " .
$this -> createUnorderedList ( $missingInputs );
2021-04-02 21:58:06 +02:00
} else if ( ! is_numeric ( $port ) || ( $port = intval ( $port )) < 1 || $port > 65535 ) {
2020-02-09 23:02:19 +01:00
$msg = " Port must be in range of 1-65535. " ;
$success = false ;
} else {
$success = false ;
2020-04-03 15:56:04 +02:00
$mail = new PHPMailer ( true );
2020-02-09 23:02:19 +01:00
$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 ();
2021-04-02 21:58:06 +02:00
if ( ! $success ) {
2020-02-09 23:02:19 +01:00
$error = empty ( $mail -> ErrorInfo ) ? " Unknown Error " : $mail -> ErrorInfo ;
$msg = " Could not connect to SMTP Server: $error " ;
} else {
$success = true ;
$msg = " " ;
$mail -> smtpClose ();
}
2021-04-02 21:58:06 +02:00
} catch ( Exception $error ) {
2020-02-09 23:02:19 +01:00
$msg = " Could not connect to SMTP Server: " . $error -> errorMessage ();
}
2021-04-02 21:58:06 +02:00
if ( $success ) {
2022-11-18 18:06:46 +01:00
$req = new \Core\API\Settings\Set ( $context );
2024-04-11 20:41:03 +02:00
$success = $req -> execute ([ " settings " => [
" mail_enabled " => true ,
" mail_host " => $address ,
" mail_port " => $port ,
" mail_username " => $username ,
" mail_password " => $password ,
]]);
2020-06-25 16:54:58 +02:00
$msg = $req -> getLastError ();
2020-02-09 23:02:19 +01:00
}
}
}
2024-05-04 12:23:14 +02:00
return [ " success " => $success , " msg " => $msg ];
2020-02-09 23:02:19 +01:00
}
2021-04-02 21:58:06 +02:00
private function performStep () : array {
2024-05-04 12:23:14 +02:00
return match ( $this -> currentStep ) {
self :: CHECKING_REQUIREMENTS => $this -> checkRequirements (),
self :: INSTALL_DEPENDENCIES => $this -> installDependencies (),
self :: DATABASE_CONFIGURATION => $this -> databaseConfiguration (),
self :: CREATE_USER => $this -> createUser (),
self :: ADD_MAIL_SERVICE => $this -> addMailService (),
default => [
" success " => false ,
" msg " => " Invalid step number "
],
};
2020-02-09 23:02:19 +01:00
}
2024-05-04 12:23:14 +02:00
private function createProgressSidebar () : array {
$items = [];
2021-04-02 21:58:06 +02:00
foreach ( $this -> steps as $num => $step ) {
2020-02-09 23:02:19 +01:00
$title = $step [ " title " ];
$status = $step [ " status " ];
2021-04-02 21:58:06 +02:00
switch ( $status ) {
2020-02-09 23:02:19 +01:00
case self :: PENDING :
2021-04-02 21:58:06 +02:00
$statusIcon = $this -> createIcon ( " spinner " );
$statusText = " Loading… " ;
2020-02-09 23:02:19 +01:00
$statusColor = " muted " ;
break ;
2020-04-03 15:56:04 +02:00
case self :: SUCCESSFUL :
2021-04-02 21:58:06 +02:00
$statusIcon = $this -> createIcon ( " check-circle " );
$statusText = " Successful " ;
2020-02-09 23:02:19 +01:00
$statusColor = " success " ;
break ;
case self :: ERROR :
2021-04-02 21:58:06 +02:00
$statusIcon = $this -> createIcon ( " times-circle " );
$statusText = " Failed " ;
2020-02-09 23:02:19 +01:00
$statusColor = " danger " ;
break ;
case self :: NOT_STARTED :
default :
2020-06-25 16:54:58 +02:00
$statusIcon = $this -> createIcon ( " circle " , " far " );
2020-02-09 23:02:19 +01:00
$statusText = " Pending " ;
$statusColor = " muted " ;
break ;
}
2024-05-04 12:23:14 +02:00
$attr = [ " class " => " list-group-item d-flex justify-content-between lh-condensed " ];
if ( $num == $this -> currentStep ) {
$attr [ " id " ] = " currentStep " ;
}
$items [] = html_tag ( " li " , $attr , [
html_tag ( " div " , [], [
html_tag ( " h6 " , [ " class " => " my-0 " ], $title ),
html_tag ( " small " , [ " class " => " text- $statusColor " ], $statusText ),
], false ),
html_tag ( " span " , [ " class " => " text- $statusColor " ], $statusIcon , false )
], false );
2020-02-09 23:02:19 +01:00
}
2024-05-04 12:23:14 +02:00
return $items ;
2020-02-09 23:02:19 +01:00
}
2021-04-02 21:58:06 +02:00
private function createFormItem ( $formItem , $inline = false ) : string {
2020-02-09 23:02:19 +01:00
$title = $formItem [ " title " ];
2021-04-02 21:58:06 +02:00
$name = $formItem [ " name " ];
$type = $formItem [ " type " ];
2020-02-09 23:02:19 +01:00
2024-05-04 12:23:14 +02:00
$attributes = [
2020-02-09 23:02:19 +01:00
" name " => $name ,
" id " => $name ,
2020-04-02 00:02:51 +02:00
" class " => " form-control "
2024-05-04 12:23:14 +02:00
];
2020-02-09 23:02:19 +01:00
2021-04-02 21:58:06 +02:00
if ( isset ( $formItem [ " required " ]) && $formItem [ " required " ]) {
2020-02-09 23:02:19 +01:00
$attributes [ " required " ] = " " ;
}
2020-04-02 00:02:51 +02:00
if ( $type !== " select " ) {
$attributes [ " type " ] = $type ;
2021-04-02 21:58:06 +02:00
if ( isset ( $formItem [ " value " ]) && $formItem [ " value " ]) {
2020-04-02 00:02:51 +02:00
$attributes [ " value " ] = $formItem [ " value " ];
}
2021-04-02 21:58:06 +02:00
if ( $type === " number " ) {
if ( isset ( $formItem [ " min " ]) && is_numeric ( $formItem [ " min " ]))
2020-04-02 00:02:51 +02:00
$attributes [ " min " ] = $formItem [ " min " ];
2021-04-02 21:58:06 +02:00
if ( isset ( $formItem [ " max " ]) && is_numeric ( $formItem [ " max " ]))
2020-04-02 00:02:51 +02:00
$attributes [ " max " ] = $formItem [ " max " ];
2021-04-02 21:58:06 +02:00
if ( isset ( $formItem [ " step " ]) && is_numeric ( $formItem [ " step " ]))
2020-04-02 00:02:51 +02:00
$attributes [ " step " ] = $formItem [ " step " ];
2021-04-07 19:43:22 +02:00
} else {
if ( isset ( $formItem [ " default " ])) {
$attributes [ " value " ] = $formItem [ " default " ];
}
2020-04-02 00:02:51 +02:00
}
2020-02-09 23:02:19 +01:00
}
2020-04-02 00:02:51 +02:00
if ( $type === " select " ) {
2024-05-04 12:23:14 +02:00
$items = $formItem [ " items " ] ? ? [];
2022-06-20 19:52:31 +02:00
$options = [];
2021-04-02 21:58:06 +02:00
foreach ( $items as $key => $val ) {
2022-06-20 19:52:31 +02:00
$options [] = html_tag_ex ( " option " , [ " value " => $key ], $val , true , false );
2020-04-02 00:02:51 +02:00
}
2020-02-09 23:02:19 +01:00
2022-06-20 19:52:31 +02:00
$element = html_tag_ex ( " select " , $attributes , $options , false );
2020-02-09 23:02:19 +01:00
} else {
2022-06-20 19:52:31 +02:00
$element = html_tag_short ( " input " , $attributes );
2020-02-09 23:02:19 +01:00
}
2022-06-20 19:52:31 +02:00
$label = html_tag_ex ( " label " , [ " for " => $name ], $title , true , false );
$className = ( $inline ? " col-md-6 mb-3 " : " d-block my-3 " );
return html_tag_ex ( " div " , [ " class " => $className ], $label . $element , false );
2020-02-09 23:02:19 +01:00
}
2024-05-04 12:23:14 +02:00
private function createProgressMainView () : string {
2020-02-09 23:02:19 +01:00
2024-04-23 20:14:32 +02:00
if ( isDocker ()) {
2024-05-04 12:23:14 +02:00
$env = loadEnv ();
2024-04-23 20:14:32 +02:00
$defaultHost = " db " ;
$defaultUsername = " root " ;
$defaultDatabase = " webbase " ;
2024-05-04 12:23:14 +02:00
$defaultPassword = $env && array_key_exists ( " MYSQL_ROOT_PASSWORD " , $env ) ? $env [ " MYSQL_ROOT_PASSWORD " ] : " " ;
2024-04-23 20:14:32 +02:00
} else {
$defaultHost = " localhost " ;
$defaultUsername = " " ;
$defaultDatabase = " " ;
2024-05-04 12:23:14 +02:00
$defaultPassword = " " ;
2024-04-23 20:14:32 +02:00
}
2021-04-07 19:43:22 +02:00
2024-05-04 12:23:14 +02:00
$views = [
self :: CHECKING_REQUIREMENTS => [
2020-02-09 23:02:19 +01:00
" title " => " Application Requirements " ,
" progressText " => " Checking requirements, please wait a moment… "
2024-05-04 12:23:14 +02:00
],
self :: INSTALL_DEPENDENCIES => [
2022-02-20 23:17:17 +01:00
" title " => " Installing Dependencies " ,
" progressText " => " Please wait while required dependencies are being installed… " ,
2024-05-04 12:23:14 +02:00
],
self :: DATABASE_CONFIGURATION => [
2020-02-09 23:02:19 +01:00
" title " => " Database configuration " ,
2024-05-04 12:23:14 +02:00
" form " => [
[ " title " => " Database Type " , " name " => " type " , " type " => " select " , " required " => true , " items " => [
2021-03-31 13:59:02 +02:00
" mysql " => " MySQL " , " postgres " => " PostgreSQL "
2024-05-04 12:23:14 +02:00
]],
[ " title " => " Username " , " name " => " username " , " type " => " text " , " required " => true , " default " => $defaultUsername ],
[ " title " => " Password " , " name " => " password " , " type " => " password " , " default " => $defaultPassword ],
[ " title " => " Database " , " name " => " database " , " type " => " text " , " required " => true , " default " => $defaultDatabase ],
[ " type " => " row " , " items " => [
[
2021-04-02 21:58:06 +02:00
" title " => " Address " , " name " => " host " , " type " => " text " , " required " => true ,
2021-04-07 19:43:22 +02:00
" value " => " localhost " , " row " => true , " default " => $defaultHost
2024-05-04 12:23:14 +02:00
],
[
2021-04-02 21:58:06 +02:00
" title " => " Port " , " name " => " port " , " type " => " number " , " required " => true ,
2020-02-09 23:02:19 +01:00
" value " => " 3306 " , " min " => " 1 " , " max " => " 65535 " , " row " => true
2024-05-04 12:23:14 +02:00
]
]],
[
2021-04-02 21:58:06 +02:00
" title " => " Encoding " , " name " => " encoding " , " type " => " text " , " required " => false ,
2022-02-20 18:31:54 +01:00
" value " => " UTF8 "
2024-05-04 12:23:14 +02:00
],
]
],
self :: CREATE_USER => [
2020-02-09 23:02:19 +01:00
" title " => " Create a User " ,
2024-05-04 12:23:14 +02:00
" form " => [
[ " title " => " Username " , " name " => " username " , " type " => " text " , " required " => true ],
[ " title " => " Email " , " name " => " email " , " type " => " text " ],
[ " title " => " Password " , " name " => " password " , " type " => " password " , " required " => true ],
[ " title " => " Confirm Password " , " name " => " confirmPassword " , " type " => " password " , " required " => true ],
],
2020-02-10 00:52:25 +01:00
" previousButton " => true
2024-05-04 12:23:14 +02:00
],
self :: ADD_MAIL_SERVICE => [
2020-02-09 23:02:19 +01:00
" title " => " Optional: Add Mail Service " ,
2024-05-04 12:23:14 +02:00
" form " => [
[ " title " => " Username " , " name " => " username " , " type " => " text " , " required " => true ],
[ " title " => " Password " , " name " => " password " , " type " => " password " ],
[ " type " => " row " , " items " => [
[
2021-04-02 21:58:06 +02:00
" title " => " SMTP Address " , " name " => " address " , " type " => " text " , " required " => true ,
2020-02-09 23:02:19 +01:00
" value " => " localhost " , " row " => true
2024-05-04 12:23:14 +02:00
],
[
2021-04-02 21:58:06 +02:00
" title " => " Port " , " name " => " port " , " type " => " number " , " required " => true ,
2020-02-09 23:02:19 +01:00
" value " => " 587 " , " min " => " 1 " , " max " => " 65535 " , " row " => true
2024-05-04 12:23:14 +02:00
]
]],
],
2020-02-10 00:52:25 +01:00
" skip " => true ,
" previousButton " => true
2024-05-04 12:23:14 +02:00
],
self :: FINISH_INSTALLATION => [
2020-02-09 23:30:26 +01:00
" title " => " Finish Installation " ,
" text " => " Installation finished, you can now customize your own website, check the source code and stuff. "
2024-05-04 12:23:14 +02:00
]
];
2020-02-09 23:02:19 +01:00
2021-04-02 21:58:06 +02:00
if ( ! isset ( $views [ $this -> currentStep ])) {
2020-02-09 23:02:19 +01:00
return " " ;
}
$currentView = $views [ $this -> currentStep ];
2020-02-10 00:52:25 +01:00
$prevDisabled = ! isset ( $currentView [ " previousButton " ]) || ! $currentView [ " previousButton " ];
2020-02-09 23:02:19 +01:00
$spinnerIcon = $this -> createIcon ( " spinner " );
$title = $currentView [ " title " ];
2024-05-04 12:23:14 +02:00
$html = html_tag ( " h4 " , [ " class " => " mb-3 " ], $title );
$html .= html_tag_short ( " h4 " , [ " class " => " mb-4 " ]);
2020-02-09 23:02:19 +01:00
2021-04-02 21:58:06 +02:00
if ( isset ( $currentView [ " text " ])) {
2020-02-09 23:30:26 +01:00
$text = $currentView [ " text " ];
2024-05-04 12:23:14 +02:00
$html .= html_tag ( " div " , [ " class " => " my-3 " ], $text );
2020-02-09 23:30:26 +01:00
}
2021-04-02 21:58:06 +02:00
if ( isset ( $currentView [ " progressText " ])) {
2024-05-04 12:23:14 +02:00
$progressText = htmlspecialchars ( $currentView [ " progressText " ]);
$class = [ " my-3 " ];
if ( ! in_array ( $this -> currentStep , [ self :: CHECKING_REQUIREMENTS , self :: INSTALL_DEPENDENCIES ])) {
$class [] = " hidden " ;
}
$html .= html_tag ( " div " , [ " class " => $class , " id " => " progressText " ], [ $progressText , $spinnerIcon ], false );
2020-02-09 23:02:19 +01:00
}
2021-04-02 21:58:06 +02:00
if ( isset ( $currentView [ " form " ])) {
2024-05-04 12:23:14 +02:00
$rows = [];
2020-02-09 23:02:19 +01:00
2021-04-02 21:58:06 +02:00
foreach ( $currentView [ " form " ] as $formItem ) {
if ( $formItem [ " type " ] === " row " ) {
2024-05-04 12:23:14 +02:00
$rows [] = html_tag ( " div " , [ " class " => " row " ], array_map ( function ( $item ) {
return $this -> createFormItem ( $item , true );
}, $formItem [ " items " ]), false );
2020-02-09 23:02:19 +01:00
} else {
2024-05-04 12:23:14 +02:00
$rows [] = $this -> createFormItem ( $formItem );
2020-02-09 23:02:19 +01:00
}
}
2024-05-04 12:23:14 +02:00
$html .= html_tag ( " form " , [ " id " => " installForm " ], $rows , false );
2020-02-09 23:02:19 +01:00
}
2024-05-04 12:23:14 +02:00
$buttons = [
[ " title " => " Go Back " , " type " => " info " , " id " => " btnPrev " , " float " => " left " , " disabled " => $prevDisabled ]
];
2020-02-09 23:02:19 +01:00
2021-04-02 21:58:06 +02:00
if ( $this -> currentStep != self :: FINISH_INSTALLATION ) {
2022-02-20 23:17:17 +01:00
if ( in_array ( $this -> currentStep , [ self :: CHECKING_REQUIREMENTS , self :: INSTALL_DEPENDENCIES ])) {
2024-05-04 12:23:14 +02:00
$buttons [] = [ " title " => " Retry " , " type " => " success " , " id " => " btnRetry " , " float " => " right " , " hidden " => true ];
2020-04-02 22:25:13 +02:00
} else {
2024-05-04 12:23:14 +02:00
$buttons [] = [ " title " => " Submit " , " type " => " success " , " id " => " btnSubmit " , " float " => " right " ];
2020-04-02 22:25:13 +02:00
}
2020-02-09 23:30:26 +01:00
} else {
2024-05-04 12:23:14 +02:00
$buttons [] = [ " title " => " Finish " , " type " => " success " , " id " => " btnFinish " , " float " => " right " ];
2020-02-09 23:30:26 +01:00
}
2021-04-02 21:58:06 +02:00
if ( isset ( $currentView [ " skip " ])) {
2024-05-04 12:23:14 +02:00
$buttons [] = [ " title " => " Skip " , " type " => " secondary " , " id " => " btnSkip " , " float " => " right " ];
2020-02-09 23:02:19 +01:00
}
2024-05-04 12:23:14 +02:00
$buttonsLeft = [];
$buttonsRight = [];
2021-04-02 21:58:06 +02:00
foreach ( $buttons as $button ) {
2020-02-09 23:02:19 +01:00
$title = $button [ " title " ];
$type = $button [ " type " ];
$id = $button [ " id " ];
$float = $button [ " float " ];
2024-05-04 12:23:14 +02:00
$attrs = [ " id " => $id , " class " => [ " m-1 " , " btn " , " btn- $type " ]];
if ( isset ( $button [ " hidden " ]) && $button [ " hidden " ]) {
$attrs [ " class " ][] = " hidden " ;
}
if ( isset ( $button [ " disabled " ]) && $button [ " disabled " ]) {
$attrs [ " class " ][] = " disabled " ;
}
$button = html_tag ( " button " , $attrs , $title , false );
2021-04-02 21:58:06 +02:00
if ( $float === " left " ) {
2024-05-04 12:23:14 +02:00
$buttonsLeft [] = $button ;
2020-02-09 23:02:19 +01:00
} else {
2024-05-04 12:23:14 +02:00
$buttonsRight [] = $button ;
2020-02-09 23:02:19 +01:00
}
}
2024-05-04 12:23:14 +02:00
$html .= html_tag ( " div " , [ " class " => " row " ], [
html_tag ( " div " , [ " class " => " col-6 float-left text-left " ], $buttonsLeft , false ),
html_tag ( " div " , [ " class " => " col-6 float-right text-right " ], $buttonsRight , false ),
], false );
2020-02-09 23:02:19 +01:00
return $html ;
}
2021-04-02 21:58:06 +02:00
function getCode () : string {
2020-02-09 23:02:19 +01:00
$html = parent :: getCode ();
2024-05-04 12:23:14 +02:00
$this -> steps = [
self :: CHECKING_REQUIREMENTS => [
2020-02-09 23:02:19 +01:00
" title " => " Checking requirements " ,
2020-04-02 22:25:13 +02:00
" status " => self :: ERROR
2024-05-04 12:23:14 +02:00
],
self :: INSTALL_DEPENDENCIES => [
2022-02-20 23:17:17 +01:00
" title " => " Install dependencies " ,
" status " => self :: NOT_STARTED
2024-05-04 12:23:14 +02:00
],
self :: DATABASE_CONFIGURATION => [
2020-02-09 23:02:19 +01:00
" title " => " Database configuration " ,
" status " => self :: NOT_STARTED
2024-05-04 12:23:14 +02:00
],
self :: CREATE_USER => [
2020-02-09 23:02:19 +01:00
" title " => " Create User " ,
" status " => self :: NOT_STARTED
2024-05-04 12:23:14 +02:00
],
self :: ADD_MAIL_SERVICE => [
2020-02-09 23:02:19 +01:00
" title " => " Add Mail Service " ,
" status " => self :: NOT_STARTED
2024-05-04 12:23:14 +02:00
],
self :: FINISH_INSTALLATION => [
2020-02-09 23:30:26 +01:00
" title " => " Finish Installation " ,
2020-02-09 23:02:19 +01:00
" status " => self :: NOT_STARTED
2024-05-04 12:23:14 +02:00
],
];
2020-02-09 23:02:19 +01:00
$this -> currentStep = $this -> getCurrentStep ();
// set status
2021-04-02 21:58:06 +02:00
for ( $step = self :: CHECKING_REQUIREMENTS ; $step < $this -> currentStep ; $step ++ ) {
2020-04-03 15:56:04 +02:00
$this -> steps [ $step ][ " status " ] = self :: SUCCESSFUL ;
2020-02-09 23:02:19 +01:00
}
2021-04-02 21:58:06 +02:00
if ( $this -> currentStep == self :: FINISH_INSTALLATION ) {
2020-04-03 15:56:04 +02:00
$this -> steps [ $this -> currentStep ][ " status " ] = self :: SUCCESSFUL ;
2020-02-09 23:30:26 +01:00
}
2020-02-09 23:02:19 +01:00
// POST
2021-04-02 21:58:06 +02:00
if ( $_SERVER [ 'REQUEST_METHOD' ] == 'POST' ) {
2022-02-20 23:17:17 +01:00
if ( ! isset ( $_REQUEST [ 'status' ])) {
$response = $this -> performStep ();
} else {
$response = [ " error " => $this -> errorString ];
}
2020-04-02 22:25:13 +02:00
$response [ " step " ] = $this -> currentStep ;
2020-02-09 23:02:19 +01:00
die ( json_encode ( $response ));
}
$progressSidebar = $this -> createProgressSidebar ();
2024-05-04 12:23:14 +02:00
$progressMainView = $this -> createProgressMainView ();
$errorAttrs = [ " class " => [ " alert " , " alert-danger " , " mt-4 " ], " id " => " status " ];
if ( $this -> errorString ) {
$errorAttrs [ " class " ][] = " alert-danger " ;
} else {
$errorAttrs [ " class " ][] = " d-none " ;
}
$html .= html_tag ( " body " , [ " class " => " bg-light " ],
html_tag ( " div " , [ " class " => " container " ], [
// title
html_tag ( " div " , [ " class " => " py-5 text-center " ], [
html_tag ( " h2 " , [], " WebBase - Installation " ),
html_tag ( " p " , [ " class " => " lead " ],
" Process the following steps and fill out the required forms to install your WebBase-Installation. "
)
], false ),
// content
html_tag ( " div " , [ " class " => " row " ], [
// right column
html_tag ( " div " , [ " class " => " col-md-4 order-md-2 mb-4 " ], [
html_tag ( " h4 " , [ " class " => " d-flex justify-content-between align-items-center mb-3 " ],
html_tag ( " span " , [ " class " => " text-muted " ], " Progress " ),
false
),
html_tag ( " ul " , [ " class " => " list-group mb-3 " ], $progressSidebar , false )
], false ),
// left column
html_tag ( " div " , [ " class " => " col-md-8 order-md-1 " ], [
$progressMainView ,
html_tag ( " div " , $errorAttrs , $this -> errorString , false )
], false )
], false ),
], false ),
false
);
2020-02-09 23:02:19 +01:00
return $html ;
}
}
2020-04-04 15:11:38 +02:00
}