prepare install

This commit is contained in:
Dennis Eichhorn 2022-09-26 23:17:08 +02:00
parent 9b12b8d643
commit 496b7160b6
54 changed files with 3183 additions and 95 deletions

3
.gitmodules vendored
View File

@ -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

View File

@ -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));

View File

@ -12,8 +12,13 @@
#include <stdio.h>
#include <stdlib.h>
#include <string>
#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()

View File

@ -0,0 +1,3 @@
{
}

View File

@ -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);
}
}

View File

@ -12,8 +12,8 @@
namespace Models {
typedef enum {
WEB = 0,
LOCAL = 1
WEB = 1,
LOCAL = 2
} InstallType;
}

View File

@ -21,9 +21,8 @@ namespace Models {
} Organization;
inline
void freeOrganization(Organization *obj)
void free_Organization(Organization *obj)
{
free(obj);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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
#

@ -1 +1 @@
Subproject commit 641ff4e4e2a7de0db388cf7df73ea3e47e5583dd
Subproject commit 7b31df32bccde804f13ed288f4881a27bf6f5ac9

View File

@ -11,6 +11,8 @@
#include <stdio.h>
#include <stdlib.h>
#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);
}

View File

@ -1,10 +0,0 @@
<?php
declare(strict_types=1);
class Application
{
public function run() : string
{
return '';
}
}

View File

@ -0,0 +1,280 @@
<?php
declare(strict_types=1);
namespace Applications\Api;
use Models\CoreSettings;
use Models\SettingsEnum;
use Models\AccountMapper;
use Models\LocalizationMapper;
use Models\NullAccount;
use Models\PermissionCategory;
use phpOMS\Account\Account;
use phpOMS\Account\AccountManager;
use phpOMS\Account\PermissionType;
use phpOMS\Application\ApplicationAbstract;
use phpOMS\Application\ApplicationStatus;
use phpOMS\Auth\Auth;
use phpOMS\DataStorage\Cache\CachePool;
use phpOMS\DataStorage\Cookie\CookieJar;
use phpOMS\DataStorage\Database\DatabasePool;
use phpOMS\DataStorage\Database\Mapper\DataMapperFactory;
use phpOMS\DataStorage\Session\HttpSession;
use phpOMS\Dispatcher\Dispatcher;
use phpOMS\Event\EventManager;
use phpOMS\Localization\L11nManager;
use phpOMS\Message\Http\HttpRequest;
use phpOMS\Message\Http\HttpResponse;
use phpOMS\Message\Http\RequestStatusCode;
use phpOMS\Model\Html\Head;
use phpOMS\Module\ModuleManager;
use phpOMS\Router\RouteVerb;
use phpOMS\Router\WebRouter;
use phpOMS\System\File\PathException;
use phpOMS\System\MimeType;
use phpOMS\Uri\UriFactory;
use phpOMS\Views\View;
use WebApplication;
final class Application
{
private WebApplication $app;
private array $config;
public function __construct(WebApplication $app, array $config)
{
$this->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);
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Applications\E500;
use phpOMS\Asset\AssetType;
use phpOMS\Localization\L11nManager;
use phpOMS\Message\Http\HttpRequest;
use phpOMS\Message\Http\HttpResponse;
use phpOMS\Message\Http\RequestStatusCode;
use phpOMS\Model\Html\Head;
use phpOMS\System\File\PathException;
use phpOMS\Views\View;
use WebApplication;
final class Application
{
private WebApplication $app;
private array $config = [];
public function __construct(WebApplication $app, array $config)
{
$this->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);
}
}

View File

@ -0,0 +1,275 @@
<?php
/**
* Karaka
*
* PHP Version 8.1
*
* @package Install
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://karaka.app
*/
declare(strict_types=1);
namespace Install;
use phpOMS\DataStorage\Database\DatabaseStatus;
use phpOMS\DataStorage\Database\Mapper\DataMapperFactory;
use phpOMS\Dispatcher\Dispatcher;
use phpOMS\Localization\ISO639x1Enum;
use phpOMS\Localization\Localization;
use phpOMS\Log\FileLogger;
use phpOMS\Message\Http\HttpRequest;
use phpOMS\Message\Http\HttpResponse;
use phpOMS\Message\Http\RequestStatusCode;
use phpOMS\Router\RouteVerb;
use phpOMS\Router\WebRouter;
use phpOMS\System\MimeType;
use phpOMS\Uri\UriFactory;
use phpOMS\Views\View;
/**
* Application class.
*
* @package Install
* @license OMS License 1.0
* @link https://karaka.app
* @since 1.0.0
*
* @property \phpOMS\Router\WebRouter $router
*/
final class WebApplication extends InstallAbstract
{
/**
* Constructor.
*
* @param array $config Core config
*
* @since 1.0.0
* @codeCoverageIgnore
*/
public function __construct(array $config)
{
$this->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<string, bool>
*
* @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 [];
}
}

View File

@ -0,0 +1,433 @@
<?php
/**
* Karaka
*
* PHP Version 8.1
*
* @package Install
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://karaka.app
*/
declare(strict_types=1);
namespace Install;
use Models\CoreSettings;
use Models\Account;
use Models\AccountCredentialMapper;
use Models\Group;
use Models\GroupMapper;
use Models\GroupPermission;
use Models\GroupPermissionMapper;
use Models\PermissionCategory;
use Models\ModuleStatusUpdateType;
use Models\NullAccount;
use phpOMS\Account\AccountManager;
use phpOMS\Account\AccountStatus;
use phpOMS\Account\AccountType;
use phpOMS\Account\GroupStatus;
use phpOMS\Account\PermissionType;
use phpOMS\Application\ApplicationAbstract;
use phpOMS\DataStorage\Database\Connection\ConnectionAbstract;
use phpOMS\DataStorage\Database\Connection\ConnectionFactory;
use phpOMS\DataStorage\Database\DatabasePool;
use phpOMS\DataStorage\Database\Schema\Builder as SchemaBuilder;
use phpOMS\DataStorage\Session\HttpSession;
use phpOMS\Dispatcher\Dispatcher;
use phpOMS\Event\EventManager;
use phpOMS\Localization\Localization;
use phpOMS\Message\Http\HttpRequest;
use phpOMS\Message\Http\HttpResponse;
use phpOMS\Message\RequestAbstract;
use phpOMS\Module\ModuleManager;
use phpOMS\System\File\Local\Directory;
use phpOMS\System\MimeType;
use phpOMS\Uri\HttpUri;
use phpOMS\Utils\IO\Zip\Zip;
use phpOMS\Utils\TestUtils;
/**
* Application class.
*
* @package Install
* @license OMS License 1.0
* @link https://karaka.app
* @since 1.0.0
*/
abstract class InstallAbstract extends ApplicationAbstract
{
/**
* Module manager
*
* @var null|ModuleManager
* @since 1.0.0
*/
protected static ?ModuleManager $mManager = null;
/**
* Setup general handlers for the application.
*
* @return void
*
* @since 1.0.0
* @codeCoverageIgnore
*/
protected 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');
}
/**
* Clear old install
*
* @return void
*
* @since 1.0.0
*/
protected static function clearOld() : void
{
\file_put_contents(__DIR__ . '/../Cli/Routes.php', '<?php return [];');
\file_put_contents(__DIR__ . '/../Cli/Hooks.php', '<?php return [];');
$dirs = \scandir(__DIR__ . '/../Web');
if ($dirs === false) {
return; // @codeCoverageIgnore
}
foreach ($dirs as $dir) {
if ($dir === '.' || $dir === '..'
|| $dir === 'Exception'
|| $dir === 'WebApplication.php'
) {
continue;
}
Directory::delete(__DIR__ . '/../Web/' . $dir);
}
}
/**
* Check if has certain php extensions enabled
*
* @return bool
*
* @since 1.0.0
*/
protected static function hasPhpExtensions() : bool
{
return \extension_loaded('pdo')
&& \extension_loaded('mbstring');
}
/**
* Check if database connection is correct and working
*
* @param RequestAbstract $request Request
*
* @return bool
*
* @since 1.0.0
*/
protected static function testDbConnection(RequestAbstract $request) : bool
{
return true;
}
/**
* Create database connection
*
* @param RequestAbstract $request Request
*
* @return ConnectionAbstract
*
* @since 1.0.0
*/
protected static function setupDatabaseConnection(RequestAbstract $request) : ConnectionAbstract
{
return ConnectionFactory::create([
'db' => (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();
}
}

View File

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
namespace Install;
class Install
{
private function installDB()
{
}
}

View File

@ -0,0 +1,172 @@
<?php
/**
* Karaka
*
* PHP Version 8.1
*
* @package Template
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://karaka.app
*/
declare(strict_types=1);
/**
* @var string $db Database
* @var string $host Host
* @var int $port Port
* @var array{login:string, password:string} $admin Admin login data
* @var array{login:string, password:string} $insert Insert login data
* @var array{login:string, password:string} $select Select login data
* @var array{login:string, password:string} $update Update login data
* @var array{login:string, password:string} $delete Delete login data
* @var array{login:string, password:string} $schema Schema login data
* @var string $dbname Database name
* @var string $subdir Subdirectory path
* @var string $tld Top level domain
*/
return <<<EOT
<?php
/**
* Karaka
*
* PHP Version 8.1
*
* @package Install
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://karaka.app
*/
declare(strict_types=1);
return [
'db' => [
'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;

View File

@ -0,0 +1,153 @@
<?php
/**
* Karaka
*
* PHP Version 8.1
*
* @package Template
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://karaka.app
*/
declare(strict_types=1);
$htaccess = <<<EOT
# BEGIN Gzip Compression
<ifmodule mod_rewrite.c>
AddEncoding gzip .gz
<filesmatch "\.js\.gz$">
AddType "text/javascript" .gz
</filesmatch>
<filesmatch "\.css\.gz$">
AddType "text/css" .gz
</filesmatch>
</ifmodule>
AddType font/ttf .ttf
AddType font/otf .otf
AddType application/font-woff .woff
AddType application/vnd.ms-fontobject .eot
<ifmodule mod_deflate.c>
AddOutputFilterByType DEFLATE text/text text/html text/plain text/xml text/css application/x-javascript application/javascript text/javascript
</ifmodule>
# END Gzip Compression
# Force mime for javascript files
<Files "*.js">
ForceType text/javascript
</Files>
# BEGIN Caching
<ifModule mod_expires.c>
ExpiresActive On
ExpiresDefault A300
ExpiresByType image/x-icon A2592000
<FilesMatch ".(php)$">
ExpiresDefault A0
Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
Header set Pragma "no-cache"
</FilesMatch>
</ifModule>
# END Caching
# BEGIN Spelling
<IfModule mod_speling.c>
CheckSpelling On
CheckCaseOnly On
</IfModule>
# END Spelling
# BEGIN URL rewrite
<ifmodule mod_rewrite.c>
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 .= <<<EOT
RewriteCond %{HTTP_HOST} !^www.*$ [L]
EOT;
}
$htaccess .= <<<EOT
RewriteCond %{HTTP_HOST} ^(.*)\.${tld}
RewriteRule ^([a-zA-Z]{2})\/(.*)$ ${fullTLD}/$1/%1/$2 [QSA]
EOT;
}
$htaccess .= <<<EOT
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$
EOT;
$htaccess .= ' ' . $subPath;
$htaccess .= <<<EOT
?{QUERY_STRING} [QSA]
EOT;
$htaccess .= <<< EOT
RewriteCond %{HTTPS} !on
RewriteCond %{HTTP_HOST} !^(127\.0\.0)|(192\.)|(172\.)
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}
</ifmodule>
# END URL rewrite
# BEGIN Access control
<Files *.php>
Order Deny,Allow
Deny from all
Allow from 127.0.0.1
</Files>
<Files index.php>
Allow from all
</Files>
# END Access control
# Disable directory view
Options All -Indexes
# Disable unsupported scripts
Options -ExecCGI
AddHandler cgi-script .pl .py .jsp .asp .shtml .sh .cgi
#<ifmodule mod_headers.c>
# # 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"
#</ifmodule>
<ifmodule mod_headers.c>
<FilesMatch "ServiceWorker.js$">
Header set Service-Worker-Allowed "/"
</FilesMatch>
</ifmodule>
# 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;

View File

@ -0,0 +1,3 @@
{
}

0
app/web/Install/db.json Normal file → Executable file
View File

BIN
app/web/Install/db.sqlite Normal file

Binary file not shown.

BIN
app/web/Install/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

BIN
app/web/Install/img/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

19
app/web/Install/index.php Normal file → Executable file
View File

@ -1,13 +1,28 @@
<?php
/**
* Karaka
*
* PHP Version 8.1
*
* @package Install
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://karaka.app
*/
declare(strict_types=1);
// @codeCoverageIgnoreStart
\ob_start();
//<editor-fold desc="Require/Include">
require_once __DIR__ . '/../phpOMS/Autoloader.php';
$config = require_once __DIR__ . '/../config.php';
//</editor-fold>
$App = new \Application();
echo $App->run();
$App = new \Install\WebApplication($config);
if (\ob_get_level() > 0) {
\ob_end_flush();
}
// @codeCoverageIgnoreEnd

0
app/web/Install/index.tpl.php Normal file → Executable file
View File

119
app/web/Install/styles.css Executable file
View File

@ -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;
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Models;
class Account extends \phpOMS\Account\Account
{
public int $tries = 0;
public string $tempPassword = '';
public array $parents = [];
public ?\DateTimeImmutable $tempPasswordLimit = null;
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Models;
final class AccountCredentialMapper extends AccountMapper
{
/**
* Columns.
*
* @var array<string, array{name:string, type:string, internal:string, autocomplete?:bool, readonly?:bool, writeonly?:bool, annotations?:array}>
* @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;
}

View File

@ -0,0 +1,225 @@
<?php
declare(strict_types=1);
namespace Models;
use phpOMS\Account\AccountStatus;
use phpOMS\Auth\LoginReturnType;
use phpOMS\DataStorage\Database\Mapper\DataMapperFactory;
use phpOMS\DataStorage\Database\Query\Builder;
class AccountMapper extends DataMapperFactory
{
/**
* Columns.
*
* @var array<string, array{name:string, type:string, internal:string, autocomplete?:bool, readonly?:bool, writeonly?:bool, annotations?:array}>
* @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<string, array{mapper:string, external:string, by?:string, column?:string, conditional?:bool}>
* @since 1.0.0
*/
public const OWNS_ONE = [
'l11n' => [
'mapper' => LocalizationMapper::class,
'external' => 'account_localization',
],
];
/**
* Has many relation.
*
* @var array<string, array{mapper:string, table:string, self?:?string, external?:?string, column?:string}>
* @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
}
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Modules\Admin\Models;
use phpOMS\Account\PermissionAbstract;
use phpOMS\Account\PermissionType;
class AccountPermission extends PermissionAbstract
{
private int $account = 0;
public function __construct(
int $account = 0,
int $unit = null,
string $app = null,
string $module = null,
string $from = null,
int $category = null,
int $element = null,
int $component = null,
int $permission = PermissionType::NONE
) {
$this->account = $account;
parent::__construct($unit, $app, $module, $from, $category, $element, $component, $permission);
}
public function getAccount() : int
{
return $this->account;
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Models;
use phpOMS\DataStorage\Database\Mapper\DataMapperFactory;
final class AccountPermissionMapper extends DataMapperFactory
{
/**
* Columns.
*
* @var array<string, array{name:string, type:string, internal:string, autocomplete?:bool, readonly?:bool, writeonly?:bool, annotations?:array}>
* @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';
}

166
app/web/Models/CoreSettings.php Executable file
View File

@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
namespace Models;
use phpOMS\Config\OptionsTrait;
use phpOMS\Config\SettingsInterface;
use phpOMS\DataStorage\Cache\CachePool;
final class CoreSettings implements SettingsInterface
{
use OptionsTrait;
protected ?CachePool $cache = null;
public function get(
mixed $ids = null,
string|array $names = null,
int $app = null,
string $module = null,
int $group = null,
int $account = null
) : mixed
{
$options = [];
// get by ids
if ($ids !== null) {
if (!\is_array($ids)) {
$ids = [$ids];
}
foreach ($ids as $i => $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);
}
}

27
app/web/Models/Group.php Executable file
View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Models;
class Group extends \phpOMS\Account\Group
{
public \DateTimeImmutable $createdAt;
public Account $createdBy;
public string $descriptionRaw = '';
protected array $accounts = [];
public function __construct(string $name = '')
{
$this->createdBy = new NullAccount();
$this->createdAt = new \DateTimeImmutable('now');
$this->name = $name;
}
public function getAccounts() : array
{
return $this->accounts;
}
}

123
app/web/Models/GroupMapper.php Executable file
View File

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Models;
use phpOMS\DataStorage\Database\Mapper\DataMapperFactory;
use phpOMS\DataStorage\Database\Query\Builder;
final class GroupMapper extends DataMapperFactory
{
/**
* Columns.
*
* @var array<string, array{name:string, type:string, internal:string, autocomplete?:bool, readonly?:bool, writeonly?:bool, annotations?:array}>
* @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<string, array{mapper:string, table:string, self?:?string, external?:?string, column?:string}>
* @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<string, int>
*
* @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;
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Models;
use phpOMS\Account\PermissionAbstract;
use phpOMS\Account\PermissionType;
class GroupPermission extends PermissionAbstract
{
private int $group = 0;
public function __construct(
int $group = 0,
int $unit = null,
string $app = null,
string $module = null,
string $from = null,
int $category = null,
int $element = null,
int $component = null,
int $permission = PermissionType::NONE
) {
$this->group = $group;
parent::__construct($unit, $app, $module, $from, $category, $element, $component, $permission);
}
public function getGroup() : int
{
return $this->group;
}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Models;
use phpOMS\DataStorage\Database\Mapper\DataMapperFactory;
final class GroupPermissionMapper extends DataMapperFactory
{
/**
* Columns.
*
* @var array<string, array{name:string, type:string, internal:string, autocomplete?:bool, readonly?:bool, writeonly?:bool, annotations?:array}>
* @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';
}

View File

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Models;
use phpOMS\DataStorage\Database\Mapper\DataMapperFactory;
use phpOMS\Localization\Defaults\CountryMapper;
use phpOMS\Localization\Defaults\CurrencyMapper;
use phpOMS\Localization\Defaults\LanguageMapper;
use phpOMS\Localization\Localization;
final class LocalizationMapper extends DataMapperFactory
{
/**
* Columns.
*
* @var array<string, array{name:string, type:string, internal:string, autocomplete?:bool, readonly?:bool, writeonly?:bool, annotations?:array}>
* @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<string, array{mapper:string, external:string, by?:string, column?:string, conditional?:bool}>
* @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;
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Models;
final class NullAccount extends Account
{
public function __construct(int $id = 0)
{
parent::__construct();
$this->id = $id;
}
}

13
app/web/Models/NullSetting.php Executable file
View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Models;
final class NullSetting extends Setting
{
public function __construct(int $id = 0)
{
$this->id = $id;
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Models;
use phpOMS\Stdlib\Base\Enum;
abstract class PermissionCategory extends Enum
{
public const SETTINGS = 1;
public const ACCOUNT = 2;
public const GROUP = 3;
public const MODULE = 4;
public const LOG = 5;
public const ROUTE = 6;
public const APP = 7;
public const ACCOUNT_SETTINGS = 8;
public const SEARCH = 9;
public const API = 9;
}

73
app/web/Models/Setting.php Executable file
View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Models;
class Setting
{
protected int $id = 0;
public string $name = '';
public string $content = '';
public string $pattern = '';
public ?int $app = null;
public ?string $module = null;
public ?int $group = null;
public ?int $account = null;
public function getId() : int
{
return $this->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;
}
}

139
app/web/Models/SettingMapper.php Executable file
View File

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace Models;
use phpOMS\DataStorage\Database\Mapper\DataMapperFactory;
use phpOMS\DataStorage\Database\Query\Builder;
final class SettingMapper extends DataMapperFactory
{
/**
* Columns.
*
* @var array<string, array{name:string, type:string, internal:string, autocomplete?:bool, readonly?:bool, writeonly?:bool, annotations?:array}>
* @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);
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Models;
use phpOMS\Stdlib\Base\Enum;
abstract class SettingsEnum extends Enum
{
/* Logging settings */
public const PASSWORD_PATTERN = '1000000001';
public const LOGIN_TIMEOUT = '1000000002';
public const PASSWORD_INTERVAL = '1000000003';
public const PASSWORD_HISTORY = '1000000004';
public const LOGIN_TRIES = '1000000005';
public const LOGGING_STATUS = '1000000006';
public const LOGGING_PATH = '1000000007';
/* Organization settings */
public const DEFAULT_ORGANIZATION = '1000000009';
/* Login settings */
public const LOGIN_FORGOTTEN_COUNT = '1000000010';
public const LOGIN_FORGOTTEN_DATE = '1000000011';
public const LOGIN_FORGOTTEN_TOKEN = '1000000012';
public const LOGIN_STATUS = '1000000013';
/* Localization settings */
public const DEFAULT_LOCALIZATION = '1000000014';
/* Mail settings */
public const MAIL_SERVER_ADDR = '1000000015';
public const MAIL_SERVER_TYPE = '1000000016';
public const MAIL_SERVER_USER = '1000000017';
public const MAIL_SERVER_PASS = '1000000018';
public const MAIL_SERVER_CERT = '1000000019';
public const MAIL_SERVER_KEY = '1000000020';
public const MAIL_SERVER_KEYPASS = '1000000021';
public const MAIL_SERVER_TLS = '1000000022';
/* Cli settings */
public const CLI_ACTIVE = '1000000023';
/* Global default templates */
public const DEFAULT_PDF_EXPORT_TEMPLATE = '1000000024';
public const DEFAULT_CSV_EXPORT_TEMPLATE = '1000000025';
public const DEFAULT_EXCEL_EXPORT_TEMPLATE = '1000000026';
public const DEFAULT_WORD_EXPORT_TEMPLATE = '1000000027';
public const DEFAULT_EMAIL_EXPORT_TEMPLATE = '1000000028';
}

212
app/web/WebApplication.php Normal file
View File

@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
use phpOMS\Autoloader;
use phpOMS\Application\ApplicationAbstract;
use phpOMS\Localization\ISO639x1Enum;
use phpOMS\Localization\Localization;
use phpOMS\Log\FileLogger;
use phpOMS\Message\Http\HttpRequest;
use phpOMS\Message\Http\HttpResponse;
use phpOMS\System\File\PathException;
use phpOMS\Uri\HttpUri;
use phpOMS\Uri\UriFactory;
class WebApplication extends ApplicationAbstract
{
public function __construct(array $config)
{
$response = null;
$sub = null;
try {
$this->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);
}
}

View File

@ -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();

1
app/web/jsOMS Submodule

@ -0,0 +1 @@
Subproject commit e3aef7338ab615b8520f4fcbe82572fbf71b0934

View File

@ -0,0 +1,73 @@
<?php
/**
* Karaka
*
* PHP Version 8.1
*
* @package Web\Backend
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://karaka.app
*/
declare(strict_types=1);
use phpOMS\Uri\UriFactory;
/** @var Web\Backend\BackendView $this */
$nav = $this->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') ?? [];
?>
<!DOCTYPE HTML>
<html lang="<?= $this->printHtml($this->response->getLanguage()); ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#343a40">
<meta name="msapplication-navbutton-color" content="#343a40">
<meta name="apple-mobile-web-app-status-bar-style" content="#343a40">
<meta name="description" content="<?= $this->getHtml(':meta', '0', '0'); ?>">
<base href="<?= UriFactory::build('{/base}'); ?>/">
<?= $head->meta->render(); ?>
<link rel="manifest" href="<?= UriFactory::build('Web/Backend/manifest.json'); ?>">
<link rel="manifest" href="<?= UriFactory::build('Web/Backend/manifest.webmanifest'); ?>">
<link rel="shortcut icon" href="<?= UriFactory::build('Web/Backend/img/favicon.ico?v=1.0.0'); ?>" type="image/x-icon">
<title><?= $this->printHtml($head->title); ?></title>
<?= $head->renderAssets(); ?>
<style><?= $head->renderStyle(); ?></style>
<script><?= $head->renderScript(); ?></script>
</head>
<body>
<?php
$c = 0;
foreach ($dispatch as $view) {
if (!($view instanceof \phpOMS\Views\NullView)
&& $view instanceof \phpOMS\Contract\RenderableInterface
) {
++$c;
echo $view->render();
}
}
if ($c === 0) {
echo '<div class="emptyPage"></div>';
}
?>
<?= $head->renderAssetsLate(); ?>