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 0000000..46da2ca Binary files /dev/null and b/app/web/Install/db.sqlite differ diff --git a/app/web/Install/favicon.ico b/app/web/Install/favicon.ico new file mode 100755 index 0000000..ec514e2 Binary files /dev/null and b/app/web/Install/favicon.ico differ diff --git a/app/web/Install/img/logo.png b/app/web/Install/img/logo.png new file mode 100755 index 0000000..27d7f3c Binary files /dev/null and b/app/web/Install/img/logo.png differ diff --git a/app/web/Install/index.php b/app/web/Install/index.php old mode 100644 new mode 100755 index 71c188e..bc9768d --- a/app/web/Install/index.php +++ b/app/web/Install/index.php @@ -1,13 +1,28 @@ require_once __DIR__ . '/../phpOMS/Autoloader.php'; +$config = require_once __DIR__ . '/../config.php'; +// -$App = new \Application(); -echo $App->run(); +$App = new \Install\WebApplication($config); if (\ob_get_level() > 0) { \ob_end_flush(); } +// @codeCoverageIgnoreEnd diff --git a/app/web/Install/index.tpl.php b/app/web/Install/index.tpl.php old mode 100644 new mode 100755 diff --git a/app/web/Install/styles.css b/app/web/Install/styles.css new file mode 100755 index 0000000..15db151 --- /dev/null +++ b/app/web/Install/styles.css @@ -0,0 +1,119 @@ +html, body { + height: 100%; + min-height: 100%; + margin: 0; + overflow-x: hidden; + overflow-y: hidden; + font-family: Open Sans, sans-serif; + font-size: 1.0rem; +} + +main { + height: 100%; + width: 600%; + + background: #263729; + transition: margin 700ms; +} + +.logo { + float: right; +} + +.page { + float: left; + background: #2f2f2f; + min-height: 100%; + width: 16.666%; + text-align: center; + height: 100%; + overflow-y: auto; +} + +section { + background: #f0f0f0; + display: inline-block; + margin: 10px 0 10px 0; + width: 800px; + max-width: 90%; + text-align: left; + line-height: 1.4rem; + font-size: 1.0rem; + padding: 20px; + box-sizing: border-box; +} + +button { + border: 1px solid #b7b7b7; + padding: 7px 15px 7px 15px; + cursor: pointer; + background: #dcdcdc; +} + +button.next, button.install { + float: right; +} + +button.prev { + float: left; +} + +button:hover { + background: #f0f0f0; +} + +section p:last-child { + margin-bottom: 0px; +} + +blockquote { + background: #e0e0e0; + border: 1px solid #ccc; + padding: 20px; + margin: 0px; + font-size: 0.9rem; +} + +blockquote>p { + margin-top: 0; +} + +th { + padding: 10px; + white-space: nowrap; +} + +td { + padding: 5px 10px 5px 10px; +} + +.OK { + color: green; +} + +.FAILED { + color: red; +} + +input, select { + padding: 5px 10px 5px 10px; + width: 100%; + margin: 0; + box-sizing: border-box; +} + +input:invalid { + transition: all 0.5s; + border: 1px solid #a5302a; + background: #ff7d79; +} + +ul, li { + padding: 0; + margin: 0; +} + +li { + list-style: none; + padding: 5px 0 5px 0; +} \ No newline at end of file diff --git a/app/web/Models/Account.php b/app/web/Models/Account.php new file mode 100644 index 0000000..5d30636 --- /dev/null +++ b/app/web/Models/Account.php @@ -0,0 +1,15 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'account_id' => ['name' => 'account_id', 'type' => 'int', 'internal' => 'id'], + 'account_status' => ['name' => 'account_status', 'type' => 'int', 'internal' => 'status'], + 'account_type' => ['name' => 'account_type', 'type' => 'int', 'internal' => 'type'], + 'account_login' => ['name' => 'account_login', 'type' => 'string', 'internal' => 'login', 'autocomplete' => true], + 'account_name1' => ['name' => 'account_name1', 'type' => 'string', 'internal' => 'name1', 'autocomplete' => true, 'annotations' => ['gdpr' => true]], + 'account_name2' => ['name' => 'account_name2', 'type' => 'string', 'internal' => 'name2', 'autocomplete' => true, 'annotations' => ['gdpr' => true]], + 'account_name3' => ['name' => 'account_name3', 'type' => 'string', 'internal' => 'name3', 'autocomplete' => true, 'annotations' => ['gdpr' => true]], + 'account_password' => ['name' => 'account_password', 'type' => 'string', 'internal' => 'password', 'writeonly' => true], + 'account_password_temp' => ['name' => 'account_password_temp', 'type' => 'string', 'internal' => 'tempPassword', 'writeonly' => true], + 'account_password_temp_limit' => ['name' => 'account_password_temp_limit', 'type' => 'DateTimeImmutable', 'internal' => 'tempPasswordLimit'], + 'account_email' => ['name' => 'account_email', 'type' => 'string', 'internal' => 'email', 'autocomplete' => true, 'annotations' => ['gdpr' => true]], + 'account_tries' => ['name' => 'account_tries', 'type' => 'int', 'internal' => 'tries'], + 'account_lactive' => ['name' => 'account_lactive', 'type' => 'DateTime', 'internal' => 'lastActive'], + 'account_localization' => ['name' => 'account_localization', 'type' => 'int', 'internal' => 'l11n'], + 'account_created_at' => ['name' => 'account_created_at', 'type' => 'DateTimeImmutable', 'internal' => 'createdAt', 'readonly' => true], + ]; + + /** + * Model to use by the mapper. + * + * @var string + * @since 1.0.0 + */ + public const MODEL = Account::class; +} diff --git a/app/web/Models/AccountMapper.php b/app/web/Models/AccountMapper.php new file mode 100644 index 0000000..006e176 --- /dev/null +++ b/app/web/Models/AccountMapper.php @@ -0,0 +1,225 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'account_id' => ['name' => 'account_id', 'type' => 'int', 'internal' => 'id'], + 'account_status' => ['name' => 'account_status', 'type' => 'int', 'internal' => 'status'], + 'account_type' => ['name' => 'account_type', 'type' => 'int', 'internal' => 'type'], + 'account_login' => ['name' => 'account_login', 'type' => 'string', 'internal' => 'login', 'autocomplete' => true], + 'account_name1' => ['name' => 'account_name1', 'type' => 'string', 'internal' => 'name1', 'autocomplete' => true, 'annotations' => ['gdpr' => true]], + 'account_name2' => ['name' => 'account_name2', 'type' => 'string', 'internal' => 'name2', 'autocomplete' => true, 'annotations' => ['gdpr' => true]], + 'account_name3' => ['name' => 'account_name3', 'type' => 'string', 'internal' => 'name3', 'autocomplete' => true, 'annotations' => ['gdpr' => true]], + 'account_email' => ['name' => 'account_email', 'type' => 'string', 'internal' => 'email', 'autocomplete' => true, 'annotations' => ['gdpr' => true]], + 'account_tries' => ['name' => 'account_tries', 'type' => 'int', 'internal' => 'tries'], + 'account_lactive' => ['name' => 'account_lactive', 'type' => 'DateTime', 'internal' => 'lastActive'], + 'account_localization' => ['name' => 'account_localization', 'type' => 'int', 'internal' => 'l11n'], + 'account_created_at' => ['name' => 'account_created_at', 'type' => 'DateTimeImmutable', 'internal' => 'createdAt', 'readonly' => true], + ]; + + /** + * Has one relation. + * + * @var array + * @since 1.0.0 + */ + public const OWNS_ONE = [ + 'l11n' => [ + 'mapper' => LocalizationMapper::class, + 'external' => 'account_localization', + ], + ]; + + /** + * Has many relation. + * + * @var array + * @since 1.0.0 + */ + public const HAS_MANY = [ + 'groups' => [ + 'mapper' => GroupMapper::class, + 'table' => 'account_group', + 'external' => 'account_group_group', + 'self' => 'account_group_account', + ], + 'parents' => [ + 'mapper' => self::class, + 'table' => 'account_account_rel', + 'external' => 'account_account_rel_root', + 'self' => 'account_account_rel_child', + ], + ]; + + /** + * Model to use by the mapper. + * + * @var string + * @since 1.0.0 + */ + public const MODEL = Account::class; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'account'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD ='account_id'; + + /** + * Created at column + * + * @var string + * @since 1.0.0 + */ + public const CREATED_AT = 'account_created_at'; + + /** + * Get account with permissions + * + * @param int $id Account id + * + * @return Account + * + * @since 1.0.0 + */ + public static function getWithPermissions(int $id) : Account + { + $account = self::get()->with('groups')->with('groups/permissions')->with('l11n')->where('id', $id)->execute(); + $groups = \array_keys($account->getGroups()); + + /** @var \Modules\Admin\Models\GroupPermission[] $groupPermissions */ + $groupPermissions = empty($groups) + ? [] + : GroupPermissionMapper::getAll() + ->where('group', \array_keys($account->getGroups()), 'in') + ->where('element', null) + ->execute(); + + foreach ($groupPermissions as $permission) { + $account->addPermissions(\is_array($permission) ? $permission : [$permission]); + } + + /** @var \Modules\Admin\Models\AccountPermission[] $accountPermission */ + $accountPermissions = AccountPermissionMapper::getAll() + ->where('account', $id) + ->where('element', null) + ->execute(); + + foreach ($accountPermissions as $permission) { + $account->addPermissions(\is_array($permission) ? $permission : [$permission]); + } + + return $account; + } + + /** + * Login user. + * + * @param string $login Username + * @param string $password Password + * @param int $tries Allowed login tries + * + * @return int Login code + * + * @since 1.0.0 + */ + public static function login(string $login, string $password, int $tries = 3) : int + { + if (empty($password)) { + return LoginReturnType::WRONG_PASSWORD; + } + + try { + $result = null; + + $query = new Builder(self::$db); + $result = $query->select('account_id', 'account_login', 'account_password', 'account_password_temp', 'account_tries', 'account_status') + ->from('account') + ->where('account_login', '=', $login) + ->execute() + ?->fetchAll(); + + if ($result === null || !isset($result[0])) { + return LoginReturnType::WRONG_USERNAME; + } + + $result = $result[0]; + + if ($result['account_tries'] >= $tries) { + return LoginReturnType::WRONG_INPUT_EXCEEDED; + } + + if ($result['account_status'] !== AccountStatus::ACTIVE) { + return LoginReturnType::INACTIVE; + } + + if (empty($result['account_password'])) { + return LoginReturnType::EMPTY_PASSWORD; + } + + if (\password_verify($password, $result['account_password'] ?? '')) { + $query->update('account') + ->set([ + 'account_lactive' => new \DateTime('now'), + 'account_tries' => 0, + ]) + ->where('account_login', '=', $login) + ->execute(); + + return $result['account_id']; + } + + if (!empty($result['account_password_temp']) + && $result['account_password_temp_limit'] !== null + && (new \DateTime('now'))->getTimestamp() < (new \DateTime($result['account_password_temp_limit']))->getTimestamp() + && \password_verify($password, $result['account_password_temp'] ?? '') + ) { + $query->update('account') + ->set([ + 'account_password_temp' => '', + 'account_lactive' => new \DateTime('now'), + 'account_tries' => 0, + ]) + ->where('account_login', '=', $login) + ->execute(); + + return $result['account_id']; + } + + $query->update('account') + ->set([ + 'account_tries' => $result['account_tries'] + 1, + ]) + ->where('account_login', '=', $login) + ->execute(); + + return LoginReturnType::WRONG_PASSWORD; + } catch (\Exception $e) { + return LoginReturnType::FAILURE; // @codeCoverageIgnore + } + } +} diff --git a/app/web/Models/AccountPermission.php b/app/web/Models/AccountPermission.php new file mode 100644 index 0000000..f13c576 --- /dev/null +++ b/app/web/Models/AccountPermission.php @@ -0,0 +1,32 @@ +account = $account; + parent::__construct($unit, $app, $module, $from, $category, $element, $component, $permission); + } + + public function getAccount() : int + { + return $this->account; + } +} diff --git a/app/web/Models/AccountPermissionMapper.php b/app/web/Models/AccountPermissionMapper.php new file mode 100644 index 0000000..2a85177 --- /dev/null +++ b/app/web/Models/AccountPermissionMapper.php @@ -0,0 +1,56 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'account_permission_id' => ['name' => 'account_permission_id', 'type' => 'int', 'internal' => 'id'], + 'account_permission_account' => ['name' => 'account_permission_account', 'type' => 'int', 'internal' => 'account'], + 'account_permission_unit' => ['name' => 'account_permission_unit', 'type' => 'int', 'internal' => 'unit'], + 'account_permission_app' => ['name' => 'account_permission_app', 'type' => 'string', 'internal' => 'app'], + 'account_permission_module' => ['name' => 'account_permission_module', 'type' => 'string', 'internal' => 'module'], + 'account_permission_from' => ['name' => 'account_permission_from', 'type' => 'string', 'internal' => 'from'], + 'account_permission_category' => ['name' => 'account_permission_category', 'type' => 'int', 'internal' => 'category'], + 'account_permission_element' => ['name' => 'account_permission_element', 'type' => 'int', 'internal' => 'element'], + 'account_permission_component' => ['name' => 'account_permission_component', 'type' => 'int', 'internal' => 'component'], + 'account_permission_hasread' => ['name' => 'account_permission_hasread', 'type' => 'bool', 'internal' => 'hasRead'], + 'account_permission_hascreate' => ['name' => 'account_permission_hascreate', 'type' => 'bool', 'internal' => 'hasCreate'], + 'account_permission_hasmodify' => ['name' => 'account_permission_hasmodify', 'type' => 'bool', 'internal' => 'hasModify'], + 'account_permission_hasdelete' => ['name' => 'account_permission_hasdelete', 'type' => 'bool', 'internal' => 'hasDelete'], + 'account_permission_haspermission' => ['name' => 'account_permission_haspermission', 'type' => 'bool', 'internal' => 'hasPermission'], + ]; + + /** + * Model to use by the mapper. + * + * @var string + * @since 1.0.0 + */ + public const MODEL = AccountPermission::class; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'account_permission'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD ='account_permission_id'; +} diff --git a/app/web/Models/CoreSettings.php b/app/web/Models/CoreSettings.php new file mode 100755 index 0000000..fe816d1 --- /dev/null +++ b/app/web/Models/CoreSettings.php @@ -0,0 +1,166 @@ + $id) { + if ($this->exists($id)) { + $options[$id] = $this->getOption($id); + unset($ids[$i]); + } + } + } + + // get by names + if ($names !== null) { + if (!\is_array($names)) { + $names = [$names]; + } + + foreach ($names as $i => $name) { + $key = ($name ?? '') + . ':' . ($app ?? '') + . ':' . ($module ?? '') + . ':' . ($group ?? '') + . ':' . ($account ?? ''); + + $key = \trim($key, ':'); + + if ($this->exists($key)) { + $options[$key] = $this->getOption($key); + unset($names[$i]); + } + } + } + + // all from cache + if (empty($ids) && empty($names)) { + return \count($options) > 1 ? $options : \reset($options); + } + + /** @var \Model\Setting[] $dbOptions */ + $dbOptions = SettingMapper::getSettings([ + 'ids' => $ids, + 'names' => $names, + 'app' => $app, + 'module' => $module, + 'group' => $group, + 'account' => $account, + ]); + + // remaining from storage + try { + foreach ($dbOptions as $option) { + $key = ($option->name) + . ':' . ($option->app ?? '') + . ':' . ($option->module ?? '') + . ':' . ($option->group ?? '') + . ':' . ($option->account ?? ''); + + $key = \trim($key, ':'); + + $this->setOption($key, $option, true); + + $options[$key] = $option; + } + } catch (\Throwable $e) { + throw $e; // @codeCoverageIgnore + } + + return \count($options) > 1 ? $options : \reset($options); + } + + public function set(array $options, bool $store = false) : void + { + /** @var array $option */ + foreach ($options as $option) { + $key = ($option['name'] ?? '') + . ':' . ($option['app'] ?? '') + . ':' . ($option['module'] ?? '') + . ':' . ($option['group'] ?? '') + . ':' . ($option['account'] ?? ''); + + $key = \trim($key, ':'); + + $setting = new Setting(); + $setting->with( + $option['id'] ?? 0, + $option['name'] ?? '', + $option['content'] ?? '', + $option['pattern'] ?? '', + $option['app'] ?? null, + $option['module'] ?? null, + $option['group'] ?? null, + $option['account'] ?? null, + ); + + $this->setOption($key, $setting, true); + + if ($store) { + SettingMapper::saveSetting($setting); + } + } + } + + public function save(array $options = []) : void + { + $options = empty($options) ? $this->options : $options; + + foreach ($options as $option) { + if (\is_array($option)) { + $setting = new Setting(); + $setting->with( + $option['id'] ?? 0, + $option['name'] ?? '', + $option['content'] ?? '', + $option['pattern'] ?? '', + $option['app'] ?? null, + $option['module'] ?? null, + $option['group'] ?? null, + $option['account'] ?? null, + ); + + $option = $setting; + } + + SettingMapper::saveSetting($option); + } + } + + public function create(array $options = []) : void + { + $setting = new Setting(); + foreach ($options as $column => $option) { + $setting->{$column} = $option; + } + + SettingMapper::create()->execute($setting); + } +} diff --git a/app/web/Models/Group.php b/app/web/Models/Group.php new file mode 100755 index 0000000..45b41cd --- /dev/null +++ b/app/web/Models/Group.php @@ -0,0 +1,27 @@ +createdBy = new NullAccount(); + $this->createdAt = new \DateTimeImmutable('now'); + $this->name = $name; + } + + public function getAccounts() : array + { + return $this->accounts; + } +} diff --git a/app/web/Models/GroupMapper.php b/app/web/Models/GroupMapper.php new file mode 100755 index 0000000..6236b81 --- /dev/null +++ b/app/web/Models/GroupMapper.php @@ -0,0 +1,123 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'group_id' => ['name' => 'group_id', 'type' => 'int', 'internal' => 'id'], + 'group_name' => ['name' => 'group_name', 'type' => 'string', 'internal' => 'name', 'autocomplete' => true], + 'group_status' => ['name' => 'group_status', 'type' => 'int', 'internal' => 'status'], + 'group_desc' => ['name' => 'group_desc', 'type' => 'string', 'internal' => 'description'], + 'group_desc_raw' => ['name' => 'group_desc_raw', 'type' => 'string', 'internal' => 'descriptionRaw'], + 'group_created' => ['name' => 'group_created', 'type' => 'DateTimeImmutable', 'internal' => 'createdAt', 'readonly' => true], + ]; + + /** + * Model to use by the mapper. + * + * @var string + * @since 1.0.0 + */ + public const MODEL = Group::class; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'group'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD ='group_id'; + + /** + * Created at column + * + * @var string + * @since 1.0.0 + */ + public const CREATED_AT = 'group_created'; + + /** + * Has many relation. + * + * @var array + * @since 1.0.0 + */ + public const HAS_MANY = [ + 'accounts' => [ + 'mapper' => AccountMapper::class, + 'table' => 'account_group', + 'external' => 'account_group_account', + 'self' => 'account_group_group', + ], + 'permissions' => [ + 'mapper' => GroupPermissionMapper::class, + 'table' => 'group_permission', + 'external' => null, + 'self' => 'group_permission_group', + ], + ]; + + /** + * Get groups which reference a certain module + * + * @param string $module Module + * + * @return array + * + * @since 1.0.0 + */ + public static function getPermissionForModule(string $module) : array + { + $query = self::getQuery(); + $query->innerJoin(GroupPermissionMapper::TABLE) + ->on(self::TABLE . '_d1.group_id', '=', GroupPermissionMapper::TABLE . '.group_permission_group') + ->where(GroupPermissionMapper::TABLE . '.group_permission_module', '=', $module); + + return self::getAll()->execute($query); + } + + /** + * Count the number of group members + * + * @param int $group Group to inspect (0 = all groups) + * + * @return array + * + * @since 1.0.0 + */ + public static function countMembers(int $group = 0) : array + { + $query = new Builder(self::$db); + $query->select(self::HAS_MANY['accounts']['self']) + ->select('COUNT(' . self::HAS_MANY['accounts']['external'] . ')') + ->from(self::HAS_MANY['accounts']['table']) + ->groupBy(self::HAS_MANY['accounts']['self']); + + if ($group !== 0) { + $query->where(self::HAS_MANY['accounts']['self'], '=', $group); + } + + $result = $query->execute()?->fetchAll(\PDO::FETCH_KEY_PAIR); + + return $result === null ? [] : $result; + } +} diff --git a/app/web/Models/GroupPermission.php b/app/web/Models/GroupPermission.php new file mode 100644 index 0000000..7f0ffac --- /dev/null +++ b/app/web/Models/GroupPermission.php @@ -0,0 +1,34 @@ +group = $group; + parent::__construct($unit, $app, $module, $from, $category, $element, $component, $permission); + } + + public function getGroup() : int + { + return $this->group; + } +} diff --git a/app/web/Models/GroupPermissionMapper.php b/app/web/Models/GroupPermissionMapper.php new file mode 100644 index 0000000..d782eaa --- /dev/null +++ b/app/web/Models/GroupPermissionMapper.php @@ -0,0 +1,57 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'group_permission_id' => ['name' => 'group_permission_id', 'type' => 'int', 'internal' => 'id'], + 'group_permission_group' => ['name' => 'group_permission_group', 'type' => 'int', 'internal' => 'group'], + 'group_permission_unit' => ['name' => 'group_permission_unit', 'type' => 'int', 'internal' => 'unit'], + 'group_permission_app' => ['name' => 'group_permission_app', 'type' => 'string', 'internal' => 'app'], + 'group_permission_module' => ['name' => 'group_permission_module', 'type' => 'string', 'internal' => 'module'], + 'group_permission_from' => ['name' => 'group_permission_from', 'type' => 'string', 'internal' => 'from'], + 'group_permission_category' => ['name' => 'group_permission_category', 'type' => 'int', 'internal' => 'category'], + 'group_permission_element' => ['name' => 'group_permission_element', 'type' => 'int', 'internal' => 'element'], + 'group_permission_component' => ['name' => 'group_permission_component', 'type' => 'int', 'internal' => 'component'], + 'group_permission_hasread' => ['name' => 'group_permission_hasread', 'type' => 'bool', 'internal' => 'hasRead'], + 'group_permission_hascreate' => ['name' => 'group_permission_hascreate', 'type' => 'bool', 'internal' => 'hasCreate'], + 'group_permission_hasmodify' => ['name' => 'group_permission_hasmodify', 'type' => 'bool', 'internal' => 'hasModify'], + 'group_permission_hasdelete' => ['name' => 'group_permission_hasdelete', 'type' => 'bool', 'internal' => 'hasDelete'], + 'group_permission_haspermission' => ['name' => 'group_permission_haspermission', 'type' => 'bool', 'internal' => 'hasPermission'], + ]; + + /** + * Model to use by the mapper. + * + * @var string + * @since 1.0.0 + */ + public const MODEL = GroupPermission::class; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'group_permission'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD ='group_permission_id'; +} diff --git a/app/web/Models/LocalizationMapper.php b/app/web/Models/LocalizationMapper.php new file mode 100644 index 0000000..0bfdad1 --- /dev/null +++ b/app/web/Models/LocalizationMapper.php @@ -0,0 +1,123 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'l11n_id' => ['name' => 'l11n_id', 'type' => 'int', 'internal' => 'id'], + 'l11n_country' => ['name' => 'l11n_country', 'type' => 'string', 'internal' => 'country'], + 'l11n_language' => ['name' => 'l11n_language', 'type' => 'string', 'internal' => 'language'], + 'l11n_currency' => ['name' => 'l11n_currency', 'type' => 'string', 'internal' => 'currency'], + 'l11n_currency_format' => ['name' => 'l11n_currency_format', 'type' => 'string', 'internal' => 'currencyFormat'], + 'l11n_number_thousand' => ['name' => 'l11n_number_thousand', 'type' => 'string', 'internal' => 'thousands'], + 'l11n_number_decimal' => ['name' => 'l11n_number_decimal', 'type' => 'string', 'internal' => 'decimal'], + 'l11n_angle' => ['name' => 'l11n_angle', 'type' => 'string', 'internal' => 'angle'], + 'l11n_temperature' => ['name' => 'l11n_temperature', 'type' => 'string', 'internal' => 'temperature'], + 'l11n_weight_very_light' => ['name' => 'l11n_weight_very_light', 'type' => 'string', 'internal' => 'weight/very_light'], + 'l11n_weight_light' => ['name' => 'l11n_weight_light', 'type' => 'string', 'internal' => 'weight/light'], + 'l11n_weight_medium' => ['name' => 'l11n_weight_medium', 'type' => 'string', 'internal' => 'weight/medium'], + 'l11n_weight_heavy' => ['name' => 'l11n_weight_heavy', 'type' => 'string', 'internal' => 'weight/heavy'], + 'l11n_weight_very_heavy' => ['name' => 'l11n_weight_very_heavy', 'type' => 'string', 'internal' => 'weight/very_heavy'], + 'l11n_speed_very_slow' => ['name' => 'l11n_speed_very_slow', 'type' => 'string', 'internal' => 'speed/very_slow'], + 'l11n_speed_slow' => ['name' => 'l11n_speed_slow', 'type' => 'string', 'internal' => 'speed/slow'], + 'l11n_speed_medium' => ['name' => 'l11n_speed_medium', 'type' => 'string', 'internal' => 'speed/medium'], + 'l11n_speed_fast' => ['name' => 'l11n_speed_fast', 'type' => 'string', 'internal' => 'speed/fast'], + 'l11n_speed_very_fast' => ['name' => 'l11n_speed_very_fast', 'type' => 'string', 'internal' => 'speed/very_fast'], + 'l11n_speed_sea' => ['name' => 'l11n_speed_sea', 'type' => 'string', 'internal' => 'speed/sea'], + 'l11n_length_very_short' => ['name' => 'l11n_length_very_short', 'type' => 'string', 'internal' => 'length/very_short'], + 'l11n_length_short' => ['name' => 'l11n_length_short', 'type' => 'string', 'internal' => 'length/short'], + 'l11n_length_medium' => ['name' => 'l11n_length_medium', 'type' => 'string', 'internal' => 'length/medium'], + 'l11n_length_long' => ['name' => 'l11n_length_long', 'type' => 'string', 'internal' => 'length/long'], + 'l11n_length_very_long' => ['name' => 'l11n_length_very_long', 'type' => 'string', 'internal' => 'length/very_long'], + 'l11n_length_sea' => ['name' => 'l11n_length_sea', 'type' => 'string', 'internal' => 'length/sea'], + 'l11n_area_very_small' => ['name' => 'l11n_area_very_small', 'type' => 'string', 'internal' => 'area/very_small'], + 'l11n_area_small' => ['name' => 'l11n_area_small', 'type' => 'string', 'internal' => 'area/small'], + 'l11n_area_medium' => ['name' => 'l11n_area_medium', 'type' => 'string', 'internal' => 'area/medium'], + 'l11n_area_large' => ['name' => 'l11n_area_large', 'type' => 'string', 'internal' => 'area/large'], + 'l11n_area_very_large' => ['name' => 'l11n_area_very_large', 'type' => 'string', 'internal' => 'area/very_large'], + 'l11n_volume_very_small' => ['name' => 'l11n_volume_very_small', 'type' => 'string', 'internal' => 'volume/very_small'], + 'l11n_volume_small' => ['name' => 'l11n_volume_small', 'type' => 'string', 'internal' => 'volume/small'], + 'l11n_volume_medium' => ['name' => 'l11n_volume_medium', 'type' => 'string', 'internal' => 'volume/medium'], + 'l11n_volume_large' => ['name' => 'l11n_volume_large', 'type' => 'string', 'internal' => 'volume/large'], + 'l11n_volume_very_large' => ['name' => 'l11n_volume_very_large', 'type' => 'string', 'internal' => 'volume/very_large'], + 'l11n_volume_teaspoon' => ['name' => 'l11n_volume_teaspoon', 'type' => 'string', 'internal' => 'volume/teaspoon'], + 'l11n_volume_tablespoon' => ['name' => 'l11n_volume_tablespoon', 'type' => 'string', 'internal' => 'volume/tablespoon'], + 'l11n_volume_glass' => ['name' => 'l11n_volume_glass', 'type' => 'string', 'internal' => 'volume/glass'], + 'l11n_timezone' => ['name' => 'l11n_timezone', 'type' => 'string', 'internal' => 'timezone'], + 'l11n_datetime_very_short' => ['name' => 'l11n_datetime_very_short', 'type' => 'string', 'internal' => 'datetime/very_short'], + 'l11n_datetime_short' => ['name' => 'l11n_datetime_short', 'type' => 'string', 'internal' => 'datetime/short'], + 'l11n_datetime_medium' => ['name' => 'l11n_datetime_medium', 'type' => 'string', 'internal' => 'datetime/medium'], + 'l11n_datetime_long' => ['name' => 'l11n_datetime_long', 'type' => 'string', 'internal' => 'datetime/long'], + 'l11n_datetime_very_long' => ['name' => 'l11n_datetime_very_long', 'type' => 'string', 'internal' => 'datetime/very_long'], + 'l11n_precision_very_short' => ['name' => 'l11n_precision_very_short', 'type' => 'int', 'internal' => 'precision/very_short'], + 'l11n_precision_short' => ['name' => 'l11n_precision_short', 'type' => 'int', 'internal' => 'precision/short'], + 'l11n_precision_medium' => ['name' => 'l11n_precision_medium', 'type' => 'int', 'internal' => 'precision/medium'], + 'l11n_precision_long' => ['name' => 'l11n_precision_long', 'type' => 'int', 'internal' => 'precision/long'], + 'l11n_precision_very_long' => ['name' => 'l11n_precision_very_long', 'type' => 'int', 'internal' => 'precision/very_long'], + ]; + + /** + * Has one relation. + * + * @var array + * @since 1.0.0 + */ + public const OWNS_ONE = [ + 'country' => [ + 'mapper' => CountryMapper::class, + 'external' => 'l11n_country', + 'by' => 'code2', + 'column' => 'code2', + ], + 'language' => [ + 'mapper' => LanguageMapper::class, + 'external' => 'l11n_language', + 'by' => 'code2', + 'column' => 'code2', + ], + 'currency' => [ + 'mapper' => CurrencyMapper::class, + 'external' => 'l11n_currency', + 'by' => 'code', + 'column' => 'code', + ], + ]; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'l11n'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD ='l11n_id'; + + /** + * Model to use by the mapper. + * + * @var string + * @since 1.0.0 + */ + public const MODEL = Localization::class; +} diff --git a/app/web/Models/NullAccount.php b/app/web/Models/NullAccount.php new file mode 100644 index 0000000..77cac2a --- /dev/null +++ b/app/web/Models/NullAccount.php @@ -0,0 +1,13 @@ +id = $id; + } +} diff --git a/app/web/Models/NullSetting.php b/app/web/Models/NullSetting.php new file mode 100755 index 0000000..3e77792 --- /dev/null +++ b/app/web/Models/NullSetting.php @@ -0,0 +1,13 @@ +id = $id; + } +} diff --git a/app/web/Models/PermissionCategory.php b/app/web/Models/PermissionCategory.php new file mode 100644 index 0000000..9437250 --- /dev/null +++ b/app/web/Models/PermissionCategory.php @@ -0,0 +1,29 @@ +id; + } + + public function with( + int $id = 0, + string $name = '', + string $content = '', + string $pattern = '', + int $app = null, + string $module = null, + int $group = null, + int $account = null + ) : self + { + $this->id = $id; + $this->name = $name; + $this->content = $content; + $this->pattern = $pattern; + $this->app = $app; + $this->module = $module; + $this->group = $group; + $this->account = $account; + + return $this; + } + + public function __construct( + int $id = 0, + string $name = '', + string $content = '', + string $pattern = '', + int $app = null, + string $module = null, + int $group = null, + int $account = null + ) { + $this->id = $id; + $this->name = $name; + $this->content = $content; + $this->pattern = $pattern; + $this->app = $app; + $this->module = $module; + $this->group = $group; + $this->account = $account; + } +} diff --git a/app/web/Models/SettingMapper.php b/app/web/Models/SettingMapper.php new file mode 100755 index 0000000..f8f81f1 --- /dev/null +++ b/app/web/Models/SettingMapper.php @@ -0,0 +1,139 @@ + + * @since 1.0.0 + */ + public const COLUMNS = [ + 'settings_id' => ['name' => 'settings_id', 'type' => 'int', 'internal' => 'id'], + 'settings_name' => ['name' => 'settings_name', 'type' => 'string', 'internal' => 'name'], + 'settings_content' => ['name' => 'settings_content', 'type' => 'string', 'internal' => 'content'], + 'settings_pattern' => ['name' => 'settings_pattern', 'type' => 'string', 'internal' => 'pattern'], + 'settings_app' => ['name' => 'settings_app', 'type' => 'int', 'internal' => 'app'], + 'settings_module' => ['name' => 'settings_module', 'type' => 'string', 'internal' => 'module'], + 'settings_group' => ['name' => 'settings_group', 'type' => 'int', 'internal' => 'group'], + 'settings_account' => ['name' => 'settings_account', 'type' => 'int', 'internal' => 'account'], + ]; + + /** + * Model to use by the mapper. + * + * @var string + * @since 1.0.0 + */ + public const MODEL = Setting::class; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'settings'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD ='settings_id'; + + /** + * Save setting / option to database + * + * @param Setting $option Option / setting + * + * @return void + * + * @since 1.0.0 + */ + public static function saveSetting(Setting $option) : void + { + $query = new Builder(self::$db); + $query->update(self::TABLE) + ->set(['settings_content' => $option->content]); + + if (!empty($option->getId())) { + $query->where('settings_id', '=', $option->getId()); + } + + if (!empty($option->name)) { + $query->andWhere('settings_name', '=', $option->name); + } + + if (!empty($option->app)) { + $query->andWhere('settings_app', '=', $option->app); + } + + if (!empty($option->module)) { + $query->andWhere('settings_module', '=', $option->module); + } + + if (!empty($option->group)) { + $query->andWhere('settings_group', '=', $option->group); + } + + if (!empty($option->account)) { + $query->andWhere('settings_account', '=', $option->account); + } + + $sth = self::$db->con->prepare($query->toSql()); + if ($sth === false) { + return; // @codeCoverageIgnore + } + + $sth->execute(); + } + + /** + * Get setting / option from database + * + * @param array $where Where conditions + * + * @return array + * + * @since 1.0.0 + */ + public static function getSettings(array $where) : array + { + $query = self::getQuery(); + + if (!empty($where['ids'])) { + $query->where('settings_id', 'in', $where['ids']); + } + + if (!empty($where['names'])) { + $query->andWhere('settings_name', 'in', $where['names']); + } + + if (!empty($where['app'])) { + $query->andWhere('settings_app', '=', $where['app']); + } + + if (!empty($where['module'])) { + $query->andWhere('settings_module', '=', $where['module']); + } + + if (!empty($where['group'])) { + $query->andWhere('settings_group', '=', $where['group']); + } + + if (!empty($where['account'])) { + $query->andWhere('settings_account', '=', $where['account']); + } + + return self::getAll()->execute($query); + } +} diff --git a/app/web/Models/SettingsEnum.php b/app/web/Models/SettingsEnum.php new file mode 100644 index 0000000..1255ad6 --- /dev/null +++ b/app/web/Models/SettingsEnum.php @@ -0,0 +1,70 @@ +setupHandlers(); + + $this->logger = FileLogger::getInstance($config['log']['file']['path'], false); + + UriFactory::setQuery('/prefix', ''); + UriFactory::setQuery('/api', 'api/'); + $applicationName = $this->getApplicationName(HttpUri::fromCurrent(), $config['app'], $config['page']['root']); + $request = $this->initRequest($config['page']['root'], $config['app']); + $response = $this->initResponse($request, $config); + + $this->theme = $this->getApplicationTheme($request, $config['app']['domains']); + + $app = '\Applications\\' . $applicationName . '\Application'; + $sub = new $app($this, $config); + } catch (\Throwable $e) { + $this->logger->critical(FileLogger::MSG_FULL, [ + 'message' => $e->getMessage(), + 'line' => __LINE__, ]); + $sub = new \Applications\E500\Application($this, $config); + } finally { + if ($sub === null) { + $sub = new \Applications\E500\Application($this, $config); + } + + if ($response === null) { + $response = new HttpResponse(); + } + + $request ??= HttpRequest::createFromSuperglobals(); + $sub->run($request, $response); + + $body = $response->getBody(true); + + if (isset($this->sessionManager)) { + $this->sessionManager->save(); + } + + if (!$response->header->isLocked()) { + $response->header->push(); + } + + if (isset($this->sessionManager)) { + $this->sessionManager->lock(); + } + + echo $body; + } + } + + private function setupHandlers() : void + { + \set_exception_handler(['\phpOMS\UnhandledHandler', 'exceptionHandler']); + \set_error_handler(['\phpOMS\UnhandledHandler', 'errorHandler']); + \register_shutdown_function(['\phpOMS\UnhandledHandler', 'shutdownHandler']); + \mb_internal_encoding('UTF-8'); + } + + private function initRequest(string $rootPath, array $config) : HttpRequest + { + $request = HttpRequest::createFromSuperglobals(); + $subDirDepth = \substr_count($rootPath, '/') - 1; + + $defaultLang = $config['domains'][$request->uri->host]['lang'] ?? $config['default']['lang']; + $uriLang = \strtolower($request->uri->getPathElement($subDirDepth + 0)); + $requestLang = $request->getRequestLanguage(); + $langCode = ISO639x1Enum::isValidValue($uriLang) + ? $uriLang + : (ISO639x1Enum::isValidValue($requestLang) + ? $requestLang + : $defaultLang + ); + + $pathOffset = $subDirDepth + + (ISO639x1Enum::isValidValue($uriLang) + ? 1 + ($this->getApplicationNameFromString($request->uri->getPathElement($subDirDepth + 1)) !== 'E500' ? 1 : 0) + : 0 + ($this->getApplicationNameFromString($request->uri->getPathElement($subDirDepth + 0)) !== 'E500' ? 1 : 0) + ); + + $request->createRequestHashs($pathOffset); + $request->uri->setRootPath($rootPath); + $request->uri->setPathOffset($pathOffset); + UriFactory::setupUriBuilder($request->uri); + + $request->header->l11n->loadFromLanguage($langCode, \explode('_', $request->getLocale())[1] ?? '*'); + + return $request; + } + + private function initResponse(HttpRequest $request, array $config) : 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'); + } + + $defaultLang = $config['app']['domains'][$request->uri->host]['lang'] ?? $config['app']['default']['lang']; + $uriLang = \strtolower($request->uri->getPathElement(0)); + $requestLang = $request->getLanguage(); + $langCode = ISO639x1Enum::isValidValue($requestLang) && \in_array($requestLang, $config['language']) + ? $requestLang + : (ISO639x1Enum::isValidValue($uriLang) && \in_array($uriLang, $config['language']) + ? $uriLang + : $defaultLang + ); + + $response->header->l11n->loadFromLanguage($langCode, \explode('_', $request->getLocale())[1] ?? '*'); + UriFactory::setQuery('/lang', $request->getLanguage()); + + if (ISO639x1Enum::isValidValue($uriLang)) { + UriFactory::setQuery('/prefix', $uriLang . '/' . (empty(UriFactory::getQuery('/prefix')) ? '' : UriFactory::getQuery('/prefix'))); + UriFactory::setQuery('/api', $uriLang . '/' . (empty(UriFactory::getQuery('/api')) ? '' : UriFactory::getQuery('/api'))); + } + + return $response; + } + + private function getApplicationName(HttpUri $uri, array $config, string $rootPath) : string + { + $subDirDepth = \substr_count($rootPath, '/') - 1; + + // check subdomain + $appName = $uri->getSubdomain(); + $appName = $this->getApplicationNameFromString($appName); + + if ($appName !== 'E500') { + return $appName; + } + + // check uri path 0 (no language is defined) + $appName = $uri->getPathElement($subDirDepth + 0); + $appName = $this->getApplicationNameFromString($appName); + + if ($appName !== 'E500') { + UriFactory::setQuery('/prefix', (empty(UriFactory::getQuery('/prefix')) ? '' : UriFactory::getQuery('/prefix') . '/') . $uri->getPathElement($subDirDepth + 1) . '/'); + + return $appName; + } + + // check uri path 1 (language is defined) + if (ISO639x1Enum::isValidValue($uri->getPathElement($subDirDepth + 0))) { + $appName = $uri->getPathElement($subDirDepth + 1); + $appName = $this->getApplicationNameFromString($appName); + + if ($appName !== 'E500') { + UriFactory::setQuery('/prefix', (empty(UriFactory::getQuery('/prefix')) ? '' : UriFactory::getQuery('/prefix') . '/') . $uri->getPathElement($subDirDepth + 1) . '/'); + + return $appName; + } + } + + // check config + $appName = $config['domains'][$uri->host]['app'] ?? $config['default']['app']; + + return $this->getApplicationNameFromString($appName); + } + + private function getApplicationNameFromString(string $app) : string + { + $applicationName = \ucfirst(\strtolower($app)); + + if (empty($applicationName) || !Autoloader::exists('\\Applications\\' . $applicationName . '\\Application')) { + $applicationName = 'E500'; + } + + return $applicationName; + } + + private function getApplicationTheme(HttpRequest $request, array $config) : string + { + return $config[$request->uri->host]['theme'] ?? 'Backend'; + } + + public 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->l11nManager->loadLanguage($language, '0', $themeLanguage); + } +} diff --git a/app/web/index.php b/app/web/index.php index c179a85..77ba164 100644 --- a/app/web/index.php +++ b/app/web/index.php @@ -5,8 +5,9 @@ declare(strict_types=1); require_once __DIR__ . '/phpOMS/Autoloader.php'; -$App = new \Application(); -echo $App->run(); +$config = require_once __DIR__ . '/config.php'; + +$App = new \WebApplication($config); if (\ob_get_level() > 0) { \ob_end_flush(); diff --git a/app/web/jsOMS b/app/web/jsOMS new file mode 160000 index 0000000..e3aef73 --- /dev/null +++ b/app/web/jsOMS @@ -0,0 +1 @@ +Subproject commit e3aef7338ab615b8520f4fcbe82572fbf71b0934 diff --git a/app/web/tpl/index.tpl.php b/app/web/tpl/index.tpl.php index e69de29..b3c6d55 100644 --- a/app/web/tpl/index.tpl.php +++ b/app/web/tpl/index.tpl.php @@ -0,0 +1,73 @@ +getData('nav'); + +$nav->setTemplate('/Modules/Navigation/Theme/Backend/top'); +$top = $nav->render(); + +$nav->setTemplate('/Modules/Navigation/Theme/Backend/side'); +$side = $nav->render(); + +/** @var phpOMS\Model\Html\Head $head */ +$head = $this->getData('head'); + +/** @var array $dispatch */ +$dispatch = $this->getData('dispatch') ?? []; +?> + + + + + + + + + + + + meta->render(); ?> + + + + + + <?= $this->printHtml($head->title); ?> + + renderAssets(); ?> + + + + + +render(); + } +} + +if ($c === 0) { + echo '
'; +} +?> +renderAssetsLate(); ?> \ No newline at end of file diff --git a/app/web/tpl/login.tpl.php b/app/web/tpl/login.tpl.php deleted file mode 100644 index e69de29..0000000