From 496b7160b6363de1b77f907c8fe7f07d1fc1c909 Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Mon, 26 Sep 2022 23:17:08 +0200 Subject: [PATCH] prepare install --- .gitmodules | 3 + app/server/Controller/ApiController.h | 11 + app/server/Controller/InstallController.h | 103 ++++- app/server/Install/config.json | 3 + app/server/{install => Install}/db.sqlite | Bin app/server/Models/Account.h | 9 +- app/server/Models/InstallType.h | 4 +- app/server/Models/Organization.h | 3 +- app/server/Models/Resource.h | 10 +- app/server/Models/ResourceInfo.h | 8 +- app/server/Routes.h | 2 +- app/server/build/install.sh | 4 +- app/server/cOMS | 2 +- app/server/main.cpp | 62 ++- app/web/Application.php | 10 - app/web/Applications/Api/Application.php | 280 +++++++++++ .../Backend/Application.php} | 0 app/web/Applications/E500/Application.php | 53 +++ .../Frontend/Application.php} | 0 app/web/Install/Application.php | 275 +++++++++++ app/web/Install/InstallAbstract.php | 433 ++++++++++++++++++ app/web/Install/Installer.php | 12 - app/web/Install/Templates/config.tpl.php | 172 +++++++ app/web/Install/Templates/htaccess.tpl.php | 153 +++++++ app/web/Install/config.json | 3 + app/web/Install/db.json | 0 app/web/Install/db.sqlite | Bin 0 -> 45056 bytes app/web/Install/favicon.ico | Bin 0 -> 370070 bytes app/web/Install/img/logo.png | Bin 0 -> 90210 bytes app/web/Install/index.php | 19 +- app/web/Install/index.tpl.php | 0 app/web/Install/styles.css | 119 +++++ app/web/Models/Account.php | 15 + app/web/Models/AccountCredentialMapper.php | 39 ++ app/web/Models/AccountMapper.php | 225 +++++++++ app/web/Models/AccountPermission.php | 32 ++ app/web/Models/AccountPermissionMapper.php | 56 +++ app/web/Models/CoreSettings.php | 166 +++++++ app/web/Models/Group.php | 27 ++ app/web/Models/GroupMapper.php | 123 +++++ app/web/Models/GroupPermission.php | 34 ++ app/web/Models/GroupPermissionMapper.php | 57 +++ app/web/Models/LocalizationMapper.php | 123 +++++ app/web/Models/NullAccount.php | 13 + app/web/Models/NullSetting.php | 13 + app/web/Models/PermissionCategory.php | 29 ++ app/web/Models/Setting.php | 73 +++ app/web/Models/SettingMapper.php | 139 ++++++ app/web/Models/SettingsEnum.php | 70 +++ app/web/WebApplication.php | 212 +++++++++ app/web/index.php | 5 +- app/web/jsOMS | 1 + app/web/tpl/index.tpl.php | 73 +++ app/web/tpl/login.tpl.php | 0 54 files changed, 3183 insertions(+), 95 deletions(-) create mode 100644 app/server/Install/config.json rename app/server/{install => Install}/db.sqlite (100%) delete mode 100644 app/web/Application.php create mode 100644 app/web/Applications/Api/Application.php rename app/web/{Install/oem/Routes.php => Applications/Backend/Application.php} (100%) create mode 100644 app/web/Applications/E500/Application.php rename app/web/{Install/self/index.tpl.php => Applications/Frontend/Application.php} (100%) create mode 100644 app/web/Install/Application.php create mode 100644 app/web/Install/InstallAbstract.php delete mode 100644 app/web/Install/Installer.php create mode 100755 app/web/Install/Templates/config.tpl.php create mode 100755 app/web/Install/Templates/htaccess.tpl.php create mode 100644 app/web/Install/config.json mode change 100644 => 100755 app/web/Install/db.json create mode 100644 app/web/Install/db.sqlite create mode 100755 app/web/Install/favicon.ico create mode 100755 app/web/Install/img/logo.png mode change 100644 => 100755 app/web/Install/index.php mode change 100644 => 100755 app/web/Install/index.tpl.php create mode 100755 app/web/Install/styles.css create mode 100644 app/web/Models/Account.php create mode 100644 app/web/Models/AccountCredentialMapper.php create mode 100644 app/web/Models/AccountMapper.php create mode 100644 app/web/Models/AccountPermission.php create mode 100644 app/web/Models/AccountPermissionMapper.php create mode 100755 app/web/Models/CoreSettings.php create mode 100755 app/web/Models/Group.php create mode 100755 app/web/Models/GroupMapper.php create mode 100644 app/web/Models/GroupPermission.php create mode 100644 app/web/Models/GroupPermissionMapper.php create mode 100644 app/web/Models/LocalizationMapper.php create mode 100644 app/web/Models/NullAccount.php create mode 100755 app/web/Models/NullSetting.php create mode 100644 app/web/Models/PermissionCategory.php create mode 100755 app/web/Models/Setting.php create mode 100755 app/web/Models/SettingMapper.php create mode 100644 app/web/Models/SettingsEnum.php create mode 100644 app/web/WebApplication.php create mode 160000 app/web/jsOMS delete mode 100644 app/web/tpl/login.tpl.php diff --git a/.gitmodules b/.gitmodules index 9dbf9fe..d4477ab 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "app/web/cssOMS"] path = app/web/cssOMS url = https://github.com/Karaka-Management/cssOMS.git +[submodule "app/web/jsOMS"] + path = app/web/jsOMS + url = https://github.com/Karaka-Management/jsOMS.git diff --git a/app/server/Controller/ApiController.h b/app/server/Controller/ApiController.h index 6e76e50..4e5c3ba 100644 --- a/app/server/Controller/ApiController.h +++ b/app/server/Controller/ApiController.h @@ -38,6 +38,17 @@ namespace Controller { printf("Version: 1.0.0\n"); } + void notInstalled(int argc, char **argv) + { + printf("No config file available, is the application installed?\n"); + printf("If not, run the application with:\n"); + printf(" --install -t 1 or\n"); + printf(" --install -t 2\n"); + printf("where 1 = web installation and 2 = local installation.\n\n"); + printf("Usually, '-t 2' is necessary if you see this message since the web\n"); + printf("installation is performed in the web installer as described in the README.\n"); + } + void checkResources(int argc, char **argv) { unsigned long long resourceId = atoll(Utils::ArrayUtils::get_arg("-r", argv, argc)); diff --git a/app/server/Controller/InstallController.h b/app/server/Controller/InstallController.h index 82128c4..d14db20 100644 --- a/app/server/Controller/InstallController.h +++ b/app/server/Controller/InstallController.h @@ -12,8 +12,13 @@ #include #include +#include #include "cOMS/Utils/Parser/Json.h" +#include "cOMS/Utils/ArrayUtils.h" +#include "DataStorage/Database/Connection/ConnectionFactory.h" +#include "DataStorage/Database/Connection/ConnectionAbstract.h" +#include "DataStorage/Database/Connection/DbConnectionConfig.h" #include "../Models/InstallType.h" @@ -21,26 +26,92 @@ namespace Controller { namespace InstallController { void installApplication(int argc, char **argv) { - // @todo handle install - // create config - // check install type - // web = copy config from web - // local - // create sqlite db - // create config from template - } - - void install(Models::InstallType type = Models::InstallType::LOCAL) - { - if (type == Models::InstallType::LOCAL) { - // create sqlite database + Models::InstallType type = (Models::InstallType) atoi(Utils::ArrayUtils::get_arg("-t", argv, argc)); + int status = 0; + if (type == Models::InstallType::WEB) { + status = installWeb(); } else { - + status = installLocal(); } - // create config file - nlohmann::json config; + if (status == 0) { + printf("Application successfully installed\n"); + } else { + printf("Application installation failed\n"); + } + } + + int installWeb() + { + // Create config by copying weg config (nothing else necessary) + Utils::FileUtils::file_body config = Utils::FileUtils::read_file("../web/config.json"); + + FILE *fp = fopen("config.json", "w"); + if (fp == NULL || config.content == NULL) { + if (config.content != NULL) { + free(config.content); + } + + return -1; + } + + fwrite(config.content, sizeof(char), config.size, fp); + fclose(fp); + + free(config.content); + + return 0; + } + + int installLocal() + { + // Create config by copying config template + FILE *in = fopen("Install/config.json", "r"); + if (in == NULL) { + return -1; + } + + nlohmann::json config = nlohmann::json::parse(in); + + std::string strJson = config.dump(4); + + FILE *out = fopen("config.json", "w"); + if (out == NULL) { + return -1; + } + + fwrite(strJson.c_str(), sizeof(char), strJson.size(), out); + + fclose(in); + fclose(out); + + // Create sqlite database + FILE *fp = fopen("db.sqlite", "w"); + if (fp == NULL) { + return -2; + } + fclose(fp); + + DataStorage::Database::DbConnectionConfig dbdata; + DataStorage::Database::ConnectionAbstract *db = DataStorage::Database::create_connection(dbdata); + if (db == NULL) { + return -2; + } + + // DbSchema *schema = DbSchema::fromJson(jsonString); + // QueryBuilder::createFromSchema(schema); + // QueryBuilder query = QueryBuilder(db, false); + // query.createTable() + // .field() + // .field() + // query->execute(); + + DataStorage::Database::close(db, dbdata); + free(db); + DataStorage::Database::free_DbConnectionConfig(&dbdata); + + return 0; } void parseConfigFile() diff --git a/app/server/Install/config.json b/app/server/Install/config.json new file mode 100644 index 0000000..0e0dcd2 --- /dev/null +++ b/app/server/Install/config.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/app/server/install/db.sqlite b/app/server/Install/db.sqlite similarity index 100% rename from app/server/install/db.sqlite rename to app/server/Install/db.sqlite diff --git a/app/server/Models/Account.h b/app/server/Models/Account.h index c738bbc..28fb5b0 100644 --- a/app/server/Models/Account.h +++ b/app/server/Models/Account.h @@ -32,7 +32,7 @@ namespace Models { } Account; inline - void freeAccount(Account *obj) + void free_Account(Account *obj) { if (obj->email != NULL) { free(obj->email); @@ -47,12 +47,11 @@ namespace Models { } if (obj->org != NULL) { - freeOrganization(obj->org); + free_Organization(obj->org); + free(obj->org); + obj->org = NULL; - } - - free(obj); } } diff --git a/app/server/Models/InstallType.h b/app/server/Models/InstallType.h index 76bed96..fea5a3d 100644 --- a/app/server/Models/InstallType.h +++ b/app/server/Models/InstallType.h @@ -12,8 +12,8 @@ namespace Models { typedef enum { - WEB = 0, - LOCAL = 1 + WEB = 1, + LOCAL = 2 } InstallType; } diff --git a/app/server/Models/Organization.h b/app/server/Models/Organization.h index 8daef66..1944d28 100644 --- a/app/server/Models/Organization.h +++ b/app/server/Models/Organization.h @@ -21,9 +21,8 @@ namespace Models { } Organization; inline - void freeOrganization(Organization *obj) + void free_Organization(Organization *obj) { - free(obj); } } diff --git a/app/server/Models/Resource.h b/app/server/Models/Resource.h index cb239e8..738016f 100644 --- a/app/server/Models/Resource.h +++ b/app/server/Models/Resource.h @@ -43,7 +43,7 @@ namespace Models { } Resource; inline - void freeResource(Resource *obj) + void free_Resource(Resource *obj) { if (obj->uri != NULL) { free(obj->uri); @@ -70,18 +70,20 @@ namespace Models { } if (obj->info != NULL) { + free_ResourceInfo(obj->info); free(obj->info); + obj->info = NULL; } if (obj->org != NULL) { - freeOrganization(obj->org); + free_Organization(obj->org); + free(obj->org); + obj->org = NULL; } - - free(obj); } } diff --git a/app/server/Models/ResourceInfo.h b/app/server/Models/ResourceInfo.h index 6fe6029..0c1d7ed 100644 --- a/app/server/Models/ResourceInfo.h +++ b/app/server/Models/ResourceInfo.h @@ -27,7 +27,7 @@ namespace Models { } ResourceInfo; inline - void freeResourceInfo(ResourceInfo *obj) + void free_ResourceInfo(ResourceInfo *obj) { if (obj->mail != NULL) { free(obj->mail); @@ -36,11 +36,11 @@ namespace Models { } if (obj->account != NULL) { - freeAccount(obj->account); + free_Account(obj->account); + free(obj->account); + obj->account = NULL; } - - free(obj); } } diff --git a/app/server/Routes.h b/app/server/Routes.h index 815ec59..7e2d34e 100644 --- a/app/server/Routes.h +++ b/app/server/Routes.h @@ -22,7 +22,7 @@ typedef void (*Fptr)(int, char **); Stdlib::HashTable::ht *generate_routes() { - Stdlib::HashTable::ht *table = Stdlib::HashTable::create_table(); + Stdlib::HashTable::ht *table = Stdlib::HashTable::create_table(4, true); if (table == NULL) { return NULL; } diff --git a/app/server/build/install.sh b/app/server/build/install.sh index eaaf0f8..3cb2350 100644 --- a/app/server/build/install.sh +++ b/app/server/build/install.sh @@ -5,4 +5,6 @@ sudo apt-get install sqlite3 libsqlite3-dev sudo apt install default-libmysqlclient-dev sudo apt-get install libxml2-dev -# install maria db https://mariadb.com/docs/connect/programming-languages/cpp/install/ +# Windows +# regex http://gnuwin32.sourceforge.net/packages/pcre.htm +# diff --git a/app/server/cOMS b/app/server/cOMS index 641ff4e..7b31df3 160000 --- a/app/server/cOMS +++ b/app/server/cOMS @@ -1 +1 @@ -Subproject commit 641ff4e4e2a7de0db388cf7df73ea3e47e5583dd +Subproject commit 7b31df32bccde804f13ed288f4881a27bf6f5ac9 diff --git a/app/server/main.cpp b/app/server/main.cpp index 6c959de..69f7846 100644 --- a/app/server/main.cpp +++ b/app/server/main.cpp @@ -11,6 +11,8 @@ #include #include +#include "cOMS/Utils/ApplicationUtils.h" +#include "DataStorage/Database/Connection/ConnectionAbstract.h" #include "cOMS/Utils/Parser/Json.h" #include "Stdlib/HashTable.h" @@ -20,47 +22,30 @@ #define OMS_DEMO false #endif -void parseConfigFile() -{ - FILE *fp = fopen("config.json", "r"); +typedef struct { + DataStorage::Database::ConnectionAbstract *db; + nlohmann::json config; +} App; - nlohmann::json config = nlohmann::json::parse(fp); -} - -char *compile_arg_line(int argc, char **argv) -{ - size_t max = 512; - size_t length = 0; - char *arg = (char *) calloc(max, sizeof(char)); - - for (int i = 1; i < argc; ++i) { - size_t argv_length = strlen(argv[i]); - if (length + strlen(argv[i]) + 1 > max) { - char *tmp = (char *) calloc(max + 128, sizeof(char)); - memcpy(tmp, arg, (length + 1) * sizeof(char)); - - free(arg); - arg = tmp; - max += 128; - } - - strcat(arg, argv[i]); - length += argv_length; - } - - return arg; -} +App app; int main(int argc, char **argv) { - char *arg = compile_arg_line(argc, argv); + char *arg = Utils::ApplicationUtils::compile_arg_line(argc, argv); - // @todo: Check is installed? - // no? install + // Set program path as cwd + char *cwd = Utils::ApplicationUtils::cwd(); + if (cwd == NULL) { + printf("Couldn't get the CWD\n"); + + return -1; + } + + Utils::ApplicationUtils::chdir_application(cwd, argv[0]); // Load config if (!Utils::FileUtils::file_exists("config.json")) { - printf("No config file available."); + Controller::ApiController::notInstalled(argc, argv); return -1; } @@ -77,6 +62,17 @@ int main(int argc, char **argv) (*ptr)(argc, argv); Stdlib::HashTable::free_table(routes); + free(routes); + free(arg); arg = NULL; + + // Reset CWD (don't know if this is necessary) + #ifdef _WIN32 + _chdir(cwd); + #else + chdir(cwd); + #endif + + free(cwd); } diff --git a/app/web/Application.php b/app/web/Application.php deleted file mode 100644 index b32f694..0000000 --- a/app/web/Application.php +++ /dev/null @@ -1,10 +0,0 @@ -app = $app; + $this->app->appName = 'Api'; + $this->config = $config; + UriFactory::setQuery('/app', \strtolower($this->app->appName)); + } + + public function run(HttpRequest $request, HttpResponse $response): void + { + $response->header->set('Content-Type', 'text/plain; charset=utf-8'); + $pageView = new View($this->app->l11nManager, $request, $response); + + $this->app->l11nManager = new L11nManager($this->app->appName); + $this->app->dbPool = new DatabasePool(); + $this->app->router = new WebRouter(); + $this->app->router->importFromFile(__DIR__ . '/Routes.php'); + + $this->app->sessionManager = new HttpSession(0); + $this->app->cookieJar = new CookieJar(); + $this->app->moduleManager = new ModuleManager($this->app, __DIR__ . '/../../Modules/'); + $this->app->dispatcher = new Dispatcher($this->app); + + $this->app->dbPool->create('core', $this->config['db']['core']['masters']['admin']); + $this->app->dbPool->create('insert', $this->config['db']['core']['masters']['insert']); + $this->app->dbPool->create('select', $this->config['db']['core']['masters']['select']); + $this->app->dbPool->create('update', $this->config['db']['core']['masters']['update']); + $this->app->dbPool->create('delete', $this->config['db']['core']['masters']['delete']); + $this->app->dbPool->create('schema', $this->config['db']['core']['masters']['schema']); + + /* Checking csrf token, if a csrf token is required at all has to be decided in the route or controller */ + if ($request->getData('CSRF') !== null + && !\hash_equals($this->app->sessionManager->get('CSRF'), $request->getData('CSRF')) + ) { + $response->header->status = RequestStatusCode::R_403; + + return; + } + + /** @var \phpOMS\DataStorage\Database\Connection\ConnectionAbstract $con */ + $con = $this->app->dbPool->get(); + DataMapperFactory::db($con); + + $this->app->cachePool = new CachePool(); + $this->app->appSettings = new CoreSettings(); + $this->app->eventManager = new EventManager($this->app->dispatcher); + $this->app->eventManager->importFromFile(__DIR__ . '/Hooks.php'); + + $this->app->accountManager = new AccountManager($this->app->sessionManager); + $this->app->l11nServer = LocalizationMapper::get()->where('id', 1)->execute(); + + $this->app->orgId = $this->getApplicationOrganization($request, $this->config['app']); + $pageView->setData('orgId', $this->app->orgId); + + $aid = Auth::authenticate($this->app->sessionManager); + $request->header->account = $aid; + $response->header->account = $aid; + + $account = $this->loadAccount($request); + + if (!($account instanceof NullAccount)) { + $response->header->l11n = $account->l11n; + } elseif ($this->app->sessionManager->get('language') !== null) { + $response->header->l11n + ->loadFromLanguage( + $this->app->sessionManager->get('language'), + $this->app->sessionManager->get('country') ?? '*' + ); + } elseif ($this->app->cookieJar->get('language') !== null) { + $response->header->l11n + ->loadFromLanguage( + $this->app->cookieJar->get('language'), + $this->app->cookieJar->get('country') ?? '*' + ); + } + + UriFactory::setQuery('/lang', $response->getLanguage()); + $response->header->set('content-language', $response->getLanguage(), true); + + // Cache general settings + $this->app->appSettings->get(null, [ + SettingsEnum::LOGGING_STATUS, SettingsEnum::CLI_ACTIVE, + ]); + + $appStatus = (int) ($this->app->appSettings->get(null, SettingsEnum::LOGIN_STATUS)->content ?? 0); + if ($appStatus === ApplicationStatus::READ_ONLY || $appStatus === ApplicationStatus::DISABLED) { + if (!$account->hasPermission(PermissionType::CREATE | PermissionType::MODIFY, module: 'Admin', type: PermissionCategory::APP)) { + if ($request->getRouteVerb() !== RouteVerb::GET) { + // Application is in read only mode or completely disabled + // If read only mode is active only GET requests are allowed + // A user who is part of the admin group is excluded from this rule + $response->header->status = RequestStatusCode::R_405; + + return; + } + + $this->app->dbPool->remove('admin'); + $this->app->dbPool->remove('insert'); + $this->app->dbPool->remove('update'); + $this->app->dbPool->remove('delete'); + $this->app->dbPool->remove('schema'); + } + } + + if (!empty($uris = $request->uri->getQuery('r'))) { + $this->handleBatchRequest($uris, $request, $response); + + return; + } + + $this->app->moduleManager->initRequestModules($request); + + // add tpl loading + $this->app->router->add( + '/api/tpl/.*', + function () use ($account, $request, $response): void { + $appName = \ucfirst($request->getData('app') ?? 'Backend'); + $app = new class() extends ApplicationAbstract + { + }; + + $app->appName = $appName; + $app->dbPool = $this->app->dbPool; + $app->orgId = $this->app->orgId; + $app->accountManager = $this->app->accountManager; + $app->appSettings = $this->app->appSettings; + $app->l11nManager = new L11nManager($app->appName); + $app->moduleManager = new ModuleManager($app, __DIR__ . '/../../Modules/'); + $app->dispatcher = new Dispatcher($app); + $app->eventManager = new EventManager($app->dispatcher); + $app->router = new WebRouter(); + + $app->eventManager->importFromFile(__DIR__ . '/../' . $appName . '/Hooks.php'); + $app->router->importFromFile(__DIR__ . '/../' . $appName . '/Routes.php'); + + $route = \str_replace('/api/tpl', '/' . $appName, $request->uri->getRoute()); + + $view = new View(); + $view->setTemplate('/Web/Api/index'); + + $response->set('Content', $view); + $response->get('Content')->setData('head', new Head()); + + $app->l11nManager->loadLanguage( + $response->getLanguage(), + '0', + include __DIR__ . '/../' . $appName . '/lang/' . $response->getLanguage() . '.lang.php' + ); + + $routed = $app->router->route( + $route, + $request->getData('CSRF'), + $request->getRouteVerb(), + $appName, + $this->app->orgId, + $account, + $request->getData() + ); + + $response->get('Content')->setData('dispatch', $app->dispatcher->dispatch($routed, $request, $response)); + }, + RouteVerb::GET + ); + + $routed = $this->app->router->route( + $request->uri->getRoute(), + $request->getData('CSRF'), + $request->getRouteVerb(), + $this->app->appName, + $this->app->orgId, + $account, + $request->getData() + ); + + $dispatched = $this->app->dispatcher->dispatch($routed, $request, $response); + + if (empty($dispatched)) { + $response->header->set('Content-Type', MimeType::M_JSON . '; charset=utf-8', true); + $response->header->status = RequestStatusCode::R_404; + $response->set($request->uri->__toString(), [ + 'status' => \phpOMS\Message\NotificationLevel::ERROR, + 'title' => '', + 'message' => '', + 'response' => [], + ]); + } + + $pageView->addData('dispatch', $dispatched); + } + + private function loadAccount(HttpRequest $request): Account + { + $account = AccountMapper::getWithPermissions($request->header->account); + $this->app->accountManager->add($account); + + return $account; + } + + private function handleBatchRequest(string $uris, HttpRequest $request, HttpResponse $response): void + { + $request_r = clone $request; + $uris = \json_decode($uris, true); + + foreach ($uris as $uri) { + $modules = $this->app->moduleManager->getRoutedModules($request_r); + $this->app->moduleManager->initModule($modules); + + $this->app->dispatcher->dispatch( + $this->app->router->route( + $request->uri->getRoute(), + $request->getData('CSRF') ?? null + ), + $request, + $response + ); + } + } + + private function getApplicationOrganization(HttpRequest $request, array $config): int + { + return (int) ($request->getData('u') ?? ($config['domains'][$request->uri->host]['org'] ?? $config['default']['org'])); + } + + private function loadLanguageFromPath(string $language, string $path): void + { + /* Load theme language */ + if (($absPath = \realpath($path)) === false) { + throw new PathException($path); + } + + /** @noinspection PhpIncludeInspection */ + $themeLanguage = include $absPath; + $this->app->l11nManager->loadLanguage($language, '0', $themeLanguage); + } +} diff --git a/app/web/Install/oem/Routes.php b/app/web/Applications/Backend/Application.php similarity index 100% rename from app/web/Install/oem/Routes.php rename to app/web/Applications/Backend/Application.php diff --git a/app/web/Applications/E500/Application.php b/app/web/Applications/E500/Application.php new file mode 100644 index 0000000..6ec0fdb --- /dev/null +++ b/app/web/Applications/E500/Application.php @@ -0,0 +1,53 @@ +app = $app; + $this->config = $config; + $this->app->appName = 'E500'; + } + + public function run(HttpRequest $request, HttpResponse $response) : void + { + $pageView = new View($this->app->l11nManager, $request, $response); + $pageView->setTemplate('/Applications/E500/index'); + $response->set('Content', $pageView); + $response->header->status = RequestStatusCode::R_500; + + /* Load theme language */ + if (($path = \realpath($oldPath = __DIR__ . '/lang/' . $response->getLanguage() . '.lang.php')) === false) { + throw new PathException($oldPath); + } + + $this->app->l11nManager = new L11nManager($this->app->appName); + + /** @noinspection PhpIncludeInspection */ + $themeLanguage = include $path; + $this->app->l11nManager->loadLanguage($response->getLanguage(), '0', $themeLanguage); + + $head = new Head(); + $baseUri = $request->uri->getBase(); + $head->addAsset(AssetType::CSS, $baseUri . 'cssOMS/styles.css?v=1.0.0'); + + $pageView->setData('head', $head); + } +} diff --git a/app/web/Install/self/index.tpl.php b/app/web/Applications/Frontend/Application.php similarity index 100% rename from app/web/Install/self/index.tpl.php rename to app/web/Applications/Frontend/Application.php diff --git a/app/web/Install/Application.php b/app/web/Install/Application.php new file mode 100644 index 0000000..6bdaa79 --- /dev/null +++ b/app/web/Install/Application.php @@ -0,0 +1,275 @@ +setupHandlers(); + + $this->logger = FileLogger::getInstance($config['log']['file']['path'], false); + $request = $this->initRequest($config['page']['root'], $config['language'][0]); + $response = $this->initResponse($request, $config['language']); + + UriFactory::setupUriBuilder($request->uri); + + $this->run($request, $response); + + $response->header->push(); + echo $response->getBody(); + } + + /** + * Initialize current application request + * + * @param string $rootPath Web root path + * @param string $language Fallback language + * + * @return HttpRequest Initial client request + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function initRequest(string $rootPath, string $language) : HttpRequest + { + $request = HttpRequest::createFromSuperglobals(); + $subDirDepth = \substr_count($rootPath, '/'); + + $request->createRequestHashs($subDirDepth); + $request->uri->setRootPath($rootPath); + UriFactory::setupUriBuilder($request->uri); + + $langCode = \strtolower($request->uri->getPathElement(0)); + $request->header->l11n->setLanguage( + empty($langCode) || !ISO639x1Enum::isValidValue($langCode) ? $language : $langCode + ); + UriFactory::setQuery('/lang', $request->getLanguage()); + + return $request; + } + + /** + * Initialize basic response + * + * @param HttpRequest $request Client request + * @param array $languages Supported languages + * + * @return HttpResponse Initial client request + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function initResponse(HttpRequest $request, array $languages) : HttpResponse + { + $response = new HttpResponse(new Localization()); + $response->header->set('content-type', 'text/html; charset=utf-8'); + $response->header->set('x-xss-protection', '1; mode=block'); + $response->header->set('x-content-type-options', 'nosniff'); + $response->header->set('x-frame-options', 'SAMEORIGIN'); + $response->header->set('referrer-policy', 'same-origin'); + + if ($request->isHttps()) { + $response->header->set('strict-transport-security', 'max-age=31536000'); + } + + $response->header->l11n->setLanguage( + !\in_array($request->getLanguage(), $languages) ? 'en' : $request->getLanguage() + ); + + return $response; + } + + /** + * Rendering backend. + * + * @param HttpRequest $request Request + * @param HttpResponse $response Response + * + * @return void + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function run(HttpRequest $request, HttpResponse $response) : void + { + $this->dispatcher = new Dispatcher($this); + $this->router = new WebRouter(); + + $this->setupRoutes(); + $response->header->set('content-language', $response->getLanguage(), true); + UriFactory::setQuery('/lang', $response->getLanguage()); + + $this->dispatcher->dispatch( + $this->router->route( + $request->uri->getRoute(), + $request->getData('CSRF'), + $request->getRouteVerb() + ), + $request, + $response + ); + } + + /** + * Setup routes for installer + * + * @return void + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function setupRoutes() : void + { + $this->router->add('^.*', '\Install\WebApplication::installView', RouteVerb::GET); + $this->router->add('^.*', '\Install\WebApplication::installRequest', RouteVerb::PUT); + } + + /** + * Create install view + * + * @param HttpRequest $request Request + * @param HttpResponse $response Response + * + * @return void + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + public static function installView(HttpRequest $request, HttpResponse $response) : void + { + $view = new View(null, $request, $response); + $view->setTemplate('/Install/index'); + $response->set('Content', $view); + } + + /** + * Handle install request. + * + * @param HttpRequest $request Request + * @param HttpResponse $response Response + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public static function installRequest(HttpRequest $request, HttpResponse $response) : void + { + $response->header->set('Content-Type', MimeType::M_JSON . '; charset=utf-8', true); + + if (!empty(self::validateRequest($request))) { + $response->header->status = RequestStatusCode::R_400; + return; + } + + $db = self::setupDatabaseConnection($request); + $db->connect(); + + if ($db->getStatus() !== DatabaseStatus::OK) { + $response->header->status = RequestStatusCode::R_400; + return; + } + + DataMapperFactory::db($db); + + self::clearOld(); + self::installConfigFile($request); + self::installCore($db); + self::installGroups($db); + self::installUsers($request, $db); + self::installApplications($request, $db); + self::configureCoreModules($request, $db); + + $response->header->status = RequestStatusCode::R_200; + } + + /** + * Validate install request. + * + * @param HttpRequest $request Request + * + * @return array + * + * @since 1.0.0 + */ + private static function validateRequest(HttpRequest $request) : array + { + $valid = []; + + if (($valid['php_extensions'] = !self::hasPhpExtensions()) + || ($valid['iDbHost'] = empty($request->getData('dbhost'))) + || ($valid['iDbType'] = empty($request->getData('dbtype'))) + || ($valid['iDbPort'] = empty($request->getData('dbport'))) + || ($valid['iDbName'] = empty($request->getData('dbname'))) + || ($valid['iSchemaUser'] = empty($request->getData('schemauser'))) + //|| ($valid['iSchemaPassword'] = empty($request->getData('schemapassword'))) + || ($valid['iCreateUser'] = empty($request->getData('createuser'))) + //|| ($valid['iCreatePassword'] = empty($request->getData('createpassword'))) + || ($valid['iSelectUser'] = empty($request->getData('selectuser'))) + //|| ($valid['iSelectPassword'] = empty($request->getData('selectpassword'))) + || ($valid['iDeleteUser'] = empty($request->getData('deleteuser'))) + //|| ($valid['iDeletePassword'] = empty($request->getData('deletepassword'))) + || ($valid['iDbName'] = !self::testDbConnection($request)) + || ($valid['iOrgName'] = empty($request->getData('orgname'))) + || ($valid['iAdminName'] = empty($request->getData('adminname'))) + //|| ($valid['iAdminPassword'] = empty($request->getData('adminpassword'))) + || ($valid['iAdminEmail'] = empty($request->getData('adminemail'))) + || ($valid['iDomain'] = empty($request->getData('domain'))) + || ($valid['iWebSubdir'] = empty($request->getData('websubdir'))) + || ($valid['iDefaultLang'] = empty($request->getData('defaultlang'))) + ) { + return $valid; + } + + return []; + } +} diff --git a/app/web/Install/InstallAbstract.php b/app/web/Install/InstallAbstract.php new file mode 100644 index 0000000..c2be1bc --- /dev/null +++ b/app/web/Install/InstallAbstract.php @@ -0,0 +1,433 @@ + (string) $request->getData('dbtype'), + 'host' => (string) $request->getData('dbhost'), + 'port' => (int) $request->getData('dbport'), + 'database' => (string) $request->getData('dbname'), + 'login' => (string) $request->getData('schemauser'), + 'password' => (string) $request->getData('schemapassword'), + ]); + } + + /** + * Install/setup configuration + * + * @param RequestAbstract $request Request + * + * @return void + * + * @since 1.0.0 + */ + protected static function installConfigFile(RequestAbstract $request) : void + { + self::editConfigFile($request); + self::editHtaccessFile($request); + } + + /** + * Modify config file + * + * @param RequestAbstract $request Request + * + * @return void + * + * @since 1.0.0 + */ + protected static function editConfigFile(RequestAbstract $request) : void + { + $db = $request->getData('dbtype'); + $host = $request->getData('dbhost'); + $port = (int) $request->getData('dbport'); + $dbname = $request->getData('dbname'); + + $admin = ['login' => $request->getData('schemauser'), 'password' => $request->getData('schemapassword')]; + $insert = ['login' => $request->getData('createuser'), 'password' => $request->getData('createpassword')]; + $select = ['login' => $request->getData('selectuser'), 'password' => $request->getData('selectpassword')]; + $update = ['login' => $request->getData('updateuser'), 'password' => $request->getData('updatepassword')]; + $delete = ['login' => $request->getData('deleteuser'), 'password' => $request->getData('deletepassword')]; + $schema = ['login' => $request->getData('schemauser'), 'password' => $request->getData('schemapassword')]; + + $subdir = $request->getData('websubdir'); + $tld = $request->getData('domain'); + + $tldOrg = 1; + $defaultOrg = 1; + + $config = include __DIR__ . '/Templates/config.tpl.php'; + + \file_put_contents(__DIR__ . '/../config.php', $config); + } + + /** + * Modify htaccess file + * + * @param RequestAbstract $request Request + * + * @return void + * + * @since 1.0.0 + */ + protected static function editHtaccessFile(RequestAbstract $request) : void + { + $fullTLD = $request->getData('domain'); + $tld = \str_replace(['.', 'http://', 'https://'], ['\.', '', ''], $request->getData('domain') ?? ''); + $subPath = $request->getData('websubdir') ?? '/'; + + $config = include __DIR__ . '/Templates/htaccess.tpl.php'; + + \file_put_contents(__DIR__ . '/../.htaccess', $config); + } + + /** + * Install core functionality + * + * @param ConnectionAbstract $db Database connection + * + * @return void + * + * @since 1.0.0 + */ + protected static function installCore(ConnectionAbstract $db) : void + { + self::createBaseTables($db); + } + + /** + * Create module table + * + * @param ConnectionAbstract $db Database connection + * + * @return void + * + * @since 1.0.0 + */ + protected static function createBaseTables(ConnectionAbstract $db) : void + { + $path = __DIR__ . '/db.json'; + if (!\is_file($path)) { + return; // @codeCoverageIgnore + } + + $content = \file_get_contents($path); + if ($content === false) { + return; // @codeCoverageIgnore + } + + $definitions = \json_decode($content, true); + foreach ($definitions as $definition) { + SchemaBuilder::createFromSchema($definition, $db)->execute(); + } + } + + /** + * Install basic groups + * + * @param ConnectionAbstract $db Database connection + * + * @return void + * + * @since 1.0.0 + */ + protected static function installGroups(ConnectionAbstract $db) : void + { + self::installMainGroups($db); + self::installGroupPermissions($db); + } + + /** + * Create basic groups in db + * + * @param ConnectionAbstract $db Database connection + * + * @return void + * + * @since 1.0.0 + */ + protected static function installMainGroups(ConnectionAbstract $db) : void + { + $guest = new Group('guest'); + $guest->setStatus(GroupStatus::ACTIVE); + GroupMapper::create()->execute($guest); + + $user = new Group('user'); + $user->setStatus(GroupStatus::ACTIVE); + GroupMapper::create()->execute($user); + + $admin = new Group('admin'); + $admin->setStatus(GroupStatus::ACTIVE); + GroupMapper::create()->execute($admin); + } + + /** + * Set permissions of basic groups + * + * @param ConnectionAbstract $db Database connection + * + * @return void + * + * @since 1.0.0 + */ + protected static function installGroupPermissions(ConnectionAbstract $db) : void + { + $searchPermission = new GroupPermission( + group: 2, + category: PermissionCategory::SEARCH, + permission: PermissionType::READ + ); + + $adminPermission = new GroupPermission( + group: 3, + permission: PermissionType::READ | PermissionType::CREATE | PermissionType::MODIFY | PermissionType::DELETE | PermissionType::PERMISSION + ); + + GroupPermissionMapper::create()->execute($searchPermission); + GroupPermissionMapper::create()->execute($adminPermission); + } + + /** + * Install users + * + * @param RequestAbstract $request Request + * @param ConnectionAbstract $db Database connection + * + * @return void + * + * @since 1.0.0 + */ + protected static function installUsers(RequestAbstract $request, ConnectionAbstract $db) : void + { + self::installMainUser($request, $db); + } + + /** + * Install applications + * + * @param RequestAbstract $request Request + * @param ConnectionAbstract $db Database connection + * + * @return void + * + * @since 1.0.0 + */ + protected static function installApplications(RequestAbstract $request, ConnectionAbstract $db) : void + { + if (self::$mManager === null) { + return; + } + + $apps = $request->getDataList('apps'); + $theme = 'Default'; + + /** @var \Modules\CMS\Controller\ApiController $module */ + $module = self::$mManager->get('CMS'); + + foreach ($apps as $app) { + $temp = new HttpRequest(new HttpUri('')); + $temp->header->account = 1; + $temp->setData('name', \basename($app)); + $temp->setData('theme', $theme); + + Zip::pack(__DIR__ . '/../' . $app, __DIR__ . '/' . \basename($app) . '.zip'); + + TestUtils::setMember($temp, 'files', [ + [ + 'name' => \basename($app) . '.zip', + 'type' => MimeType::M_ZIP, + 'tmp_name' => __DIR__ . '/' . \basename($app) . '.zip', + 'error' => \UPLOAD_ERR_OK, + 'size' => \filesize(__DIR__ . '/' . \basename($app) . '.zip'), + ], + ]); + + $module->apiApplicationInstall($temp, new HttpResponse()); + } + } + + /** + * Setup root user in database + * + * @param RequestAbstract $request Request + * @param ConnectionAbstract $db Database connection + * + * @return void + * + * @since 1.0.0 + */ + protected static function installMainUser(RequestAbstract $request, ConnectionAbstract $db) : void + { + $account = new Account(); + $account->setStatus(AccountStatus::ACTIVE); + $account->tries = 0; + $account->setType(AccountType::USER); + $account->login = (string) $request->getData('adminname'); + $account->name1 = (string) $request->getData('adminname'); + $account->generatePassword((string) $request->getData('adminpassword')); + $account->setEmail((string) $request->getData('adminemail')); + + $l11n = $account->l11n; + $l11n->loadFromLanguage($request->getData('defaultlang') ?? 'en', $request->getData('defaultcountry') ?? 'us'); + + AccountCredentialMapper::create()->execute($account); + + $sth = $db->con->prepare( + 'INSERT INTO `account_group` (`account_group_group`, `account_group_account`) VALUES + (3, ' . $account->getId() . ');' + ); + + if ($sth === false) { + return; // @codeCoverageIgnore + } + + $sth->execute(); + } +} diff --git a/app/web/Install/Installer.php b/app/web/Install/Installer.php deleted file mode 100644 index babcc90..0000000 --- a/app/web/Install/Installer.php +++ /dev/null @@ -1,12 +0,0 @@ - [ + 'core' => [ + 'masters' => [ + 'admin' => [ + 'db' => '${db}', /* db type */ + 'host' => '${host}', /* db host address */ + 'port' => '${port}', /* db host port */ + 'login' => '{$admin['login']}', /* db login name */ + 'password' => '{$admin['password']}', /* db login password */ + 'database' => '${dbname}', /* db name */ + 'weight' => 1000, /* db table weight */ + ], + 'insert' => [ + 'db' => '${db}', /* db type */ + 'host' => '${host}', /* db host address */ + 'port' => '${port}', /* db host port */ + 'login' => '{$insert['login']}', /* db login name */ + 'password' => '{$insert['password']}', /* db login password */ + 'database' => '${dbname}', /* db name */ + 'weight' => 1000, /* db table weight */ + ], + 'select' => [ + 'db' => '${db}', /* db type */ + 'host' => '${host}', /* db host address */ + 'port' => '${port}', /* db host port */ + 'login' => '{$select['login']}', /* db login name */ + 'password' => '{$select['password']}', /* db login password */ + 'database' => '${dbname}', /* db name */ + 'weight' => 1000, /* db table weight */ + ], + 'update' => [ + 'db' => '${db}', /* db type */ + 'host' => '${host}', /* db host address */ + 'port' => '${port}', /* db host port */ + 'login' => '{$update['login']}', /* db login name */ + 'password' => '{$update['password']}', /* db login password */ + 'database' => '${dbname}', /* db name */ + 'weight' => 1000, /* db table weight */ + ], + 'delete' => [ + 'db' => '${db}', /* db type */ + 'host' => '${host}', /* db host address */ + 'port' => '${port}', /* db host port */ + 'login' => '{$delete['login']}', /* db login name */ + 'password' => '{$delete['password']}', /* db login password */ + 'database' => '${dbname}', /* db name */ + 'weight' => 1000, /* db table weight */ + ], + 'schema' => [ + 'db' => '${db}', /* db type */ + 'host' => '${host}', /* db host address */ + 'port' => '${port}', /* db host port */ + 'login' => '{$schema['login']}', /* db login name */ + 'password' => '{$schema['password']}', /* db login password */ + 'database' => '${dbname}', /* db name */ + 'weight' => 1000, /* db table weight */ + ], + ], + ], + ], + 'mail' => [ + 'imap' => [ + 'host' => '127.0.0.1', + 'port' => 143, + 'ssl' => false, + 'user' => 'test', + 'password' => '123456', + ], + 'pop3' => [ + 'host' => '127.0.0.1', + 'port' => 25, + 'ssl' => false, + 'user' => 'test', + 'password' => '123456', + ], + ], + 'cache' => [ + 'redis' => [ + 'db' => 1, + 'host' => '127.0.0.1', + 'port' => 6379, + ], + 'memcached' => [ + 'host' => '127.0.0.1', + 'port' => 11211, + ], + ], + 'log' => [ + 'file' => [ + 'path' => __DIR__ . '/Logs', + ], + ], + 'page' => [ + 'root' => '${subdir}', + 'https' => false, + ], + 'app' => [ + 'path' => __DIR__, + 'default' => [ + 'app' => 'Backend', + 'id' => 'backend', + 'lang' => 'en', + 'theme' => 'Backend', + 'org' => ${defaultOrg}, + ], + 'domains' => [ + '${tld}' => [ + 'app' => 'Backend', + 'id' => 'backend', + 'lang' => 'en', + 'theme' => 'Backend', + 'org' => ${tldOrg}, + ], + ], + ], + 'socket' => [ + 'master' => [ + 'host' => '${tld}', + 'limit' => 300, + 'port' => 4310, + ], + ], + 'language' => [ + 'en', 'de', 'it', + ], +]; + +EOT; diff --git a/app/web/Install/Templates/htaccess.tpl.php b/app/web/Install/Templates/htaccess.tpl.php new file mode 100755 index 0000000..739bbca --- /dev/null +++ b/app/web/Install/Templates/htaccess.tpl.php @@ -0,0 +1,153 @@ + + AddEncoding gzip .gz + + AddType "text/javascript" .gz + + + AddType "text/css" .gz + + + +AddType font/ttf .ttf +AddType font/otf .otf +AddType application/font-woff .woff +AddType application/vnd.ms-fontobject .eot + + + AddOutputFilterByType DEFLATE text/text text/html text/plain text/xml text/css application/x-javascript application/javascript text/javascript + +# END Gzip Compression + +# Force mime for javascript files + + ForceType text/javascript + + +# BEGIN Caching + + ExpiresActive On + ExpiresDefault A300 + + ExpiresByType image/x-icon A2592000 + + + ExpiresDefault A0 + Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0" + Header set Pragma "no-cache" + + +# END Caching + +# BEGIN Spelling + + CheckSpelling On + CheckCaseOnly On + +# END Spelling + +# BEGIN URL rewrite + + RewriteEngine On + RewriteBase / + RewriteCond %{HTTP:Accept-encoding} gzip + RewriteCond %{REQUEST_FILENAME} \.(js|css)$ + RewriteCond %{REQUEST_FILENAME}.gz -f + RewriteRule ^(.*)$ $1.gz [QSA,L] + +EOT; + +if (\stripos($fullTLD, '127.0.0.1') === false) { + if (\filter_var($fullTLD, \FILTER_VALIDATE_IP) === false) { +$htaccess .= << +# END URL rewrite + +# BEGIN Access control + + Order Deny,Allow + Deny from all + Allow from 127.0.0.1 + + + Allow from all + +# END Access control + +# Disable directory view +Options All -Indexes + +# Disable unsupported scripts +Options -ExecCGI +AddHandler cgi-script .pl .py .jsp .asp .shtml .sh .cgi + +# +# # XSS protection +# header always set x-xss-protection "1; mode=block" +# +# # Nosnif +# header always set x-content-type-options "nosniff" +# +# # Iframes only from self +# header always set x-frame-options "SAMEORIGIN" +# + + + + Header set Service-Worker-Allowed "/" + + + +# Php config +# This should be removed from here and adjusted in the php.ini file +php_value upload_max_filesize 40M +php_value post_max_size 40M +php_value memory_limit 128M +php_value max_input_time 30 +php_value max_execution_time 30 +EOT; + +return $htaccess; diff --git a/app/web/Install/config.json b/app/web/Install/config.json new file mode 100644 index 0000000..0e0dcd2 --- /dev/null +++ b/app/web/Install/config.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/app/web/Install/db.json b/app/web/Install/db.json old mode 100644 new mode 100755 diff --git a/app/web/Install/db.sqlite b/app/web/Install/db.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..46da2cad3a64ec767b41ed9df5e0292161d0453d GIT binary patch literal 45056 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 sHRDjqY=6utbbjC(&u9iR>ASWI2`M9Sdrq7P+ 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