Files
2024-10-19 22:45:13 +02:00

562 lines
16 KiB
PHP

<?php
/**
* fruithost | OpenSource Hosting
*
* @author Adrian Preuß
* @version 1.0.0
* @license MIT
*/
namespace fruithost\System;
use fruithost\Accounting\Auth;
use fruithost\Accounting\Session;
use fruithost\Installer\Installer;
use fruithost\Installer\Repository;
use fruithost\Localization\I18N;
use fruithost\Modules\Modules;
use fruithost\Network\Request;
use fruithost\Network\Response;
use fruithost\Network\Router;
use fruithost\Security\Encryption;
use fruithost\Storage\Database;
use fruithost\Storage\PropertiesCache;
use fruithost\Templating\Template;
use fruithost\UI\Icon;
use fruithost\System\Utils;
use Parsedown\Parsedown;
class Core extends Loader {
private ?Modules $modules = null;
private ?Hooks $hooks = null;
private ?Router $router = null;
private ?Template $template = null;
private ?CoreAdmin $admin = null;
private ?PropertiesCache $cache = null;
private bool $modules_disabled = false;
public function __construct() {
parent::__construct();
$this->init();
}
public function initBindings() : void {
$this->cache = new PropertiesCache();
$this->hooks = new Hooks();
$this->modules = new Modules($this, $this->modules_disabled);
$this->template = new Template($this);
$this->router = new Router($this);
$this->admin = new CoreAdmin($this, null);
}
public function init() : void {
Request::init();
$this->initBindings();
Icon::init($this);
Update::bind($this)::check();
$this->getHooks()->runAction('core_pre_init');
$this->initRoutes();
$this->getHooks()->runAction('core_init');
$this->router->run();
}
public function getHooks() : ?Hooks {
return $this->hooks;
}
protected function initRoutes() : void {
$this->router->addRoute('^/theme/([^/]+)/(.*)$', function(?string $theme = null, ?string $file = null) {
$path = sprintf('%1$s%2$sthemes%2$s%3$s%2$s%4$s', dirname(PATH), DS, $theme, $file);
// @ToDo Is Secure?
if(file_exists($path)) {
Response::setContentType(Utils::getMimeType($path));
Response::header();
readfile($path);
exit();
}
$this->template->display('error/404');
});
$this->router->addRoute('/', function() {
if(Auth::isLoggedIn()) {
Response::redirect('/overview');
}
Response::redirect('/login');
});
$this->router->addRoute('/logout', function() {
if(Auth::isLoggedIn()) {
Auth::logout();
}
Response::redirect('/');
});
$this->router->addRoute('/overview', function() {
if(!Auth::isLoggedIn()) {
Response::redirect('/');
}
$this->template->display('overview');
});
$this->router->addRoute('^/settings(?:/([a-zA-Z0-9\-_]+))?$', function(?string $tab = null) {
if(!Auth::isLoggedIn()) {
Response::redirect('/');
}
$this->template->display('settings', [
'tab' => $tab,
'languages' => I18N::getLanguages(),
'timezones' => json_decode(file_get_contents(dirname(PATH).'/config/timezones.json'))
]);
});
$this->router->addRoute('^/account(?:/([a-zA-Z0-9\-_]+))?$', function(?string $tab = null) {
if(!Auth::isLoggedIn()) {
Response::redirect('/');
}
$data = Database::single('SELECT * FROM `'.DATABASE_PREFIX.'users_data` WHERE `user_id`=:user_id LIMIT 1', [
'user_id' => Auth::getID()
]);
if($data !== false) {
foreach($data as $index => $entry) {
if(in_array($index, [
'id',
'user_id'
])) {
continue;
}
$data->{$index} = Encryption::decrypt($entry, ENCRYPTION_SALT);
}
}
$this->template->display('account', [
'tab' => $tab,
'data' => $data
]);
});
$this->router->addRoute('^/app/([a-zA-Z0-9\-_]+)/(.*)$', function(?string $module = null, ?string $file = null) {
if(empty($module)) {
$this->template->display('error/module', array_merge($this->template->getAssigns(), [
'module' => $module
]));
return;
}
$module = $this->getModules()->getModule($module);
if(empty($module)) {
header('HTTP/1.1 403 Forbidden');
require_once(dirname(PATH).'/placeholder/errors/403.html');
exit();
}
if(!method_exists($module->getInstance(), 'frame')) {
header('HTTP/1.1 403 Forbidden');
require_once(dirname(PATH).'/placeholder/errors/403.html');
exit();
}
if(!file_exists(sprintf('%s/www/', $module->getPath()))) {
header('HTTP/1.1 403 Forbidden');
require_once(dirname(PATH).'/placeholder/errors/403.html');
exit();
}
$file = sprintf('%s/www/%s', $module->getPath(), $file);
if(!file_exists($file)) {
header('HTTP/1.1 403 Forbidden');
require_once(dirname(PATH).'/placeholder/errors/403.html');
exit();
}
Response::setContentType(mime_content_type($file));
Response::header();
readfile($file);
exit();
});
$this->router->addRoute('^/module(?:(?:/([a-zA-Z0-9\-_]+)?)(?:/([a-zA-Z0-9\-_]+)(?:/([a-zA-Z0-9\-_]+))?)?)?$', function(?string $module = null, ?string $submodule = null, ?string $action = null) {
if(!Auth::isLoggedIn()) {
Response::redirect('/');
}
if(Session::has('success')) {
$this->template->assign('success', Session::get('success'));
Session::remove('success');
}
if(Session::has('error')) {
$this->template->assign('error', Session::get('error'));
Session::remove('error');
}
$errors = [];
foreach(($this->getModules()->getList()) as $m) {
try {
if($m != null && $m->getInstance() != null && method_exists($m->getInstance(), 'preLoad')) {
$m->getInstance()->preLoad();
}
} catch(\Exception $e) {
$errors[] = (object) [
'name' => $m->getInfo()->getName(),
'message' => $e->getMessage(),
'stack' => $e->getTrace()
];
}
}
if(count($errors) > 0) {
$message = I18N::get('Following Modules have some Errors:');
$message .= '<br />';
foreach($errors as $entry) {
$message .= $entry->name;
}
$this->template->assign('error', $message);
}
if(empty($module)) {
$this->template->display('error/module', array_merge($this->template->getAssigns(), [
'module' => $module,
'submodule' => $submodule,
'action' => $action
]));
return;
}
$module = $this->getModules()->getModule($module);
if(empty($module)) {
$this->template->display('error/module', array_merge($this->template->getAssigns(), [
'module' => $module,
'submodule' => $submodule,
'action' => $action
]));
return;
}
$permissions = $module->getInfo()->getPermissions();
$visible = true;
if(!empty($permissions)) {
$visible = false;
foreach($permissions as $permission) {
if(Auth::hasPermission($permission)) {
$visible = true;
}
}
}
if(!$visible) {
$this->template->display('error/permissions', array_merge($this->template->getAssigns(), [
'module' => $module,
'submodule' => $submodule,
'action' => $action
]));
return;
}
if(method_exists($module->getInstance(), 'load')) {
$module->getInstance()->load($submodule, $action);
}
if(isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST' && method_exists($module->getInstance(), 'onPOST')) {
$module->getInstance()->onPOST(array_merge([
'module' => $module,
'submodule' => $submodule,
'action' => $action
], $_POST));
}
if(!method_exists($module->getInstance(), 'content') && !method_exists($module->getInstance(), 'frame')) {
$this->template->display('error/module_empty', array_merge($this->template->getAssigns(), [
'module' => $module,
'submodule' => $submodule,
'action' => $action
]));
return;
}
$this->template->display('module', array_merge($this->template->getAssigns(), [
'module' => $module,
'submodule' => $submodule,
'action' => $action
]));
});
$this->router->addRoute('^/lost-password(?:/([a-zA-Z0-9\-_]+))?$', function(?string $token = null) {
if(Auth::isLoggedIn()) {
Response::redirect('/');
}
$this->template->display('lost-password', [
'token' => $token
]);
});
$this->router->addRoute('/login', function() {
if(Auth::isLoggedIn()) {
Response::redirect('/');
}
$this->template->display('login');
});
$this->router->addRoute('/ajax', function() {
if(!Auth::isLoggedIn()) {
header('HTTP/1.1 403 Forbidden');
require_once(dirname(PATH).'/placeholder/errors/403.html');
exit();
}
if(empty($_SERVER['HTTP_X_REQUESTED_WITH']) || strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) !== 'xmlhttprequest') {
header('HTTP/1.1 405 Method Not Allowed');
require_once(dirname(PATH).'/placeholder/errors/405.html');
exit();
}
$this->getHooks()->runAction('ajax');
$this->router->run(true);
// Fire modals
if(!empty($_POST['modal'])) {
$modals = $this->getHooks()->applyFilter('modals', []);
if(count($modals) == 0) {
print 'No registred modals.';
} else {
foreach($modals as $modal) {
if($modal->getName() === $_POST['modal']) {
$callback = $modal->getCallback('save');
$data = [];
foreach($_POST as $name => $value) {
if(is_array($value)) {
$data[trim($name)] = [];
foreach($value as $key => $entry) {
$data[trim($name)][trim($key)] = trim($entry);
}
} else {
$data[trim($name)] = trim($value);
}
}
$result = call_user_func_array($callback, [ $data ]);
if(is_bool($result)) {
print json_encode($result);
return;
}
print $result;
}
}
}
/* Modules */
} else if(!empty($_POST['search'])) {
$query = $_POST['search'];
$results = [];
$repositorys = Installer::getRepositorys();
foreach($repositorys as $entry) {
$repository = Installer::getRepository($entry->id);
$content = Installer::getFile($repository, 'modules.list');
if(!empty($content)) {
$modules = explode(PHP_EOL, $content.PHP_EOL);
foreach($modules as $name) {
if(empty($name)) {
continue;
}
// @ToDo Cache
$name = trim($name);
$info = Installer::getFile($repository, sprintf('%s/module.package', $name));
$info = json_decode($info);
if(!empty($info)) {
$info->installed = $this->getModules()->hasModule($name, true);
}
$info->icon_raw = $info->icon;
if(preg_match('/^(http|https|data):/', $info->icon)) {
$info->icon = sprintf('<img alt="" class="module-icon" src="%s" />', $info->icon);
} else if(str_starts_with($info->icon, '/')) {
$info->icon = sprintf('<img alt="" class="module-icon" src="/app/%s" />', $info->icon);
} else {
$info->icon = sprintf('<i class="bi bi-%s"></i>', $info->icon);
}
if($query === 'null' || (!empty($query) && preg_match('/'.$query.'/Uis', $info->name) || preg_match('/'.$query.'/Uis', $info->description))) {
$results[$name] = $info;
}
}
}
}
print json_encode($results);
/* Module Info */
} else if(!empty($_POST['module'])) {
if(!Auth::hasPermission('MODULES::INSTALL')) {
print 'NO_PERMISSIONS';
return;
}
$repositorys = Installer::getRepositorys();
if(count($repositorys) === 0) {
print 'NO_REPOSITORYS';
return;
}
$found = null;
$repo = null;
foreach($repositorys as $repository) {
$content = Installer::getFile($repository, 'modules.list');
if(!($content == Repository::FORBIDDEN || empty($content))) {
$modules = explode(PHP_EOL, $content);
foreach($modules as $name) {
if(empty($name)) {
continue;
}
$name = trim($name);
if($name == $_POST['module']) {
$found = $name;
$repo = $repository;
}
}
}
}
if(empty($found)) {
print 'MODULE_NOT_FOUND';
return;
}
$info = Installer::getFile($repo, sprintf('%s/module.package?time=%d', $found, time()));
if(empty($info)) {
print 'MODULE_EMPTY';
return;
}
$info = json_decode($info);
$screenshots = [];
$info->icon_raw = $info->icon;
/* Icon */
if(preg_match('/^(http|https|data):/', $info->icon)) {
$info->icon = sprintf('<img alt="" class="module-icon" src="%s" />', $info->icon);
} else if(str_starts_with($info->icon, '/')) {
$info->icon = sprintf('<img alt="" class="module-icon" src="/app/%s" />', $info->icon);
} else {
$info->icon = sprintf('<i class="bi bi-%s"></i>', $info->icon);
}
/* Readme */
$readme = Installer::getFile($repo, sprintf('%s/README.md?time=%d', $found, time()));
if(empty($readme)) {
$readme = '';
}
/* Screenshots */
if(isset($info->screenshots)) {
// Fix Github Repos
if(preg_match('/github\.com\/([^\/]+)\/(.*)(?:\/)?$/Uis', $repo->url, $matches)) {
$repo->url = sprintf('https://raw.githubusercontent.com/%s/%s/master', $matches[1], rtrim($matches[2], '/'));
}
foreach($info->screenshots as $screenshot) {
$screenshots[] = sprintf('%s/%s/%s?raw=true', $repo->url, $found, $screenshot->file);
}
}
/* Changelog */
$changelog = Installer::getFile($repo, sprintf('%s/CHANGELOG.md?time=%d', $found, time()));
if(empty($changelog)) {
$changelog = '';
}
/* Features */
// @ToDo Analyze module files for these informations...
$features = [
'hooks' => [],
'filters' => [],
'actions' => [],
'extended' => false,
'settings' => false,
'daemon' => false,
'install' => false,
'deinstall' => false,
'check' => false,
'permissions' => []
];
print json_encode([
'repository' => $repo,
'info' => $info,
'screenshots' => $screenshots,
'installed' => $this->getModules()->hasModule($found, true),
'readme' => (new Parsedown())->text($readme),
'changelog' => (new Parsedown())->text($changelog),
'features' => $features
]);
}
});
}
public function getModules() : ?Modules {
return $this->modules;
}
public function getAdminCore() : ?CoreAdmin {
return $this->admin;
}
public function disableModules() {
$this->modules_disabled = true;
}
public function enableModules() {
$this->modules_disabled = false;
}
public function getTemplate() : ?Template {
return $this->template;
}
public function getRouter() : ?Router {
return $this->router;
}
public function hasSettings(string $name) : bool {
return $this->cache->exists('settings', $name);
}
public function removeSettings(string $name) : void {
$this->cache->remove('settings', $name);
}
public function getSettings(?string $name = null, mixed $default = null) : mixed {
return $this->cache->get('settings', $name, $default);
}
public function setSettings(string $name, mixed $value = null) : void {
$this->cache->set('settings', $name, $value);
}
}
?>