diff --git a/.gitattributes b/.gitattributes
index 0af7b94..cb37b47 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,6 +2,12 @@
.gitattributes text eol=lf export-ignore
.gitignore text eol=lf export-ignore
+# PHP
+*.php text eol=lf
+
# JSON
*.json text eol=lf
*.lock text eol=lf
+
+# YAML
+*.yml text eol=lf
diff --git a/.gitignore b/.gitignore
index 1c71a1b..f39dca2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,8 @@
+# Chandler
+/extensions/available/
+/extensions/enabled/
+/chandler.yml
+
# Composer
/vendor/
/composer.phar
@@ -8,13 +13,6 @@
# PHPUnit
/tests/cache/
-/chandler.yml
-
-/extensions/available/*
-/extensions/enabled/*
-!/extensions/available/.gitkeep
-!/extensions/enabled/.gitkeep
-
/tmp/cache/di_*
/tmp/plugin_artifacts/*
/tmp/cache/database/*
@@ -25,9 +23,5 @@
!/tmp/cache/templates/.gitkeep
!/tmp/cache/yaml/.gitkeep
-/htdocs/*
-!/htdocs/.htaccess
-!/htdocs/index.php
-
/logs/*
!/logs/.gitkeep
diff --git a/chandler/Bootstrap.php b/chandler/Bootstrap.php
index e5361c9..e405792 100644
--- a/chandler/Bootstrap.php
+++ b/chandler/Bootstrap.php
@@ -42,7 +42,7 @@ class Bootstrap
*/
private function igniteExtensions(): void
{
- Chandler\Extensions\ExtensionManager::i();
+ Chandler\Extensions\ExtensionManager::getInstance();
}
private function loadConfig(): void
@@ -103,7 +103,7 @@ class Bootstrap
private function route(string $url): void
{
ob_start();
- $router = Chandler\MVC\Routing\Router::i();
+ $router = Chandler\MVC\Routing\Router::getInstance();
if (($output = $router->execute($url, null)) !== null)
echo $output;
else
diff --git a/chandler/ControlPanel/includes/verify_user.php b/chandler/ControlPanel/includes/verify_user.php
index 445b8ee..b244373 100644
--- a/chandler/ControlPanel/includes/verify_user.php
+++ b/chandler/ControlPanel/includes/verify_user.php
@@ -3,9 +3,9 @@ use Chandler\Security\Authenticator;
return (function(): ?bool
{
- $auth = Authenticator::i();
+ $auth = Authenticator::getInstance();
$user = $auth->getUser();
if(!$user) return NULL;
-
+
return $user->can("access")->model("admin")->whichBelongsTo(NULL);
-});
\ No newline at end of file
+});
diff --git a/chandler/Eventing/EventDispatcher.php b/chandler/Eventing/EventDispatcher.php
deleted file mode 100644
index b00211a..0000000
--- a/chandler/Eventing/EventDispatcher.php
+++ /dev/null
@@ -1,49 +0,0 @@
-hooks[] = $hook;
- return true;
- }
-
- /**
- * @param \Chandler\Eventing\Events\Event $event
- *
- * @return \Chandler\Eventing\Events\Event
- */
- function pushEvent(Events\Event $event): Events\Event
- {
- foreach ($this->hooks as $hook) {
- if ($event instanceof Events\Cancelable)
- if ($event->isCancelled())
- break;
- $method = "on" . str_replace("Event", "", get_class($event));
- if (!method_exists($hook, $method)) continue;
- $hook->$method($event);
- }
- return $event;
- }
-
- use TSimpleSingleton;
-}
diff --git a/chandler/Eventing/Events/Cancelable.php b/chandler/Eventing/Events/Cancelable.php
deleted file mode 100644
index ebc16a2..0000000
--- a/chandler/Eventing/Events/Cancelable.php
+++ /dev/null
@@ -1,15 +0,0 @@
-code;
- }
-
- /**
- * @return string
- */
- public function getData(): string
- {
- return $this->data;
- }
-
- /**
- * @return int
- */
- public function getTime(): int
- {
- return $this->time;
- }
-
- /**
- * @return bool
- */
- public function isTainted(): bool
- {
- return !$this->pristine;
- }
-
- /**
- * @param string $data
- * @param float $code
- */
- public function __construct(string $data = "", float $code = 0.0)
- {
- $this->data = $data;
- $this->code = $code;
- $this->time = time();
- }
-}
diff --git a/chandler/Extensions/ExtensionManager.php b/chandler/Extensions/ExtensionManager.php
index f7dbfd0..e9e396e 100644
--- a/chandler/Extensions/ExtensionManager.php
+++ b/chandler/Extensions/ExtensionManager.php
@@ -1,6 +1,5 @@
in(CHANDLER_EXTENSIONS_AVAILABLE) as $directory) {
$extensionName = $directory->getFilename();
$directory = $directory->getRealPath();
$config = "$directory/manifest.yml";
-
+
if(!file_exists($config)) {
trigger_error("Skipping $extensionName for not having a valid configuration file ($config is not found)", E_USER_WARNING);
continue;
}
-
+
$this->extensions[$extensionName] = (object) chandler_parse_yaml($config);
$this->extensions[$extensionName]->id = $extensionName;
$this->extensions[$extensionName]->rawName = $directory;
$this->extensions[$extensionName]->enabled = CHANDLER_ROOT_CONF["extensions"]["allEnabled"];
}
-
+
if(!CHANDLER_ROOT_CONF["extensions"]["allEnabled"]) {
foreach(Finder::find("*")->in(CHANDLER_EXTENSIONS_ENABLED) as $directory) { #findDirectories doesn't work with symlinks
if(!is_dir($directory->getRealPath())) continue;
-
+
$extension = $directory->getFilename();
-
+
if(!array_key_exists($extension, $this->extensions)) {
trigger_error("Extension $extension is enabled, but not available, skipping", E_USER_WARNING);
continue;
}
-
+
$this->extensions[$extension]->enabled = true;
}
}
-
+
if(!array_key_exists(CHANDLER_ROOT_CONF["rootApp"], $this->extensions) || !$this->extensions[CHANDLER_ROOT_CONF["rootApp"]]->enabled) {
trigger_error("Selected root app is not available", E_USER_ERROR);
}
-
+
$this->rootApp = CHANDLER_ROOT_CONF["rootApp"];
- $this->eventLoop = EventDispatcher::i();
- $this->router = Router::i();
-
+ $this->router = Router::getInstance();
+
$this->init();
}
-
+
private function init(): void
{
foreach($this->getExtensions(true) as $name => $configuration) {
@@ -73,60 +71,60 @@ class ExtensionManager
include_once CHANDLER_EXTENSIONS_ENABLED . \"/\" . str_replace(\"\\\\\", \"/\", \$class) . \".php\";
"));
-
+
define(str_replace("-", "_", mb_strtoupper($name)) . "_ROOT", CHANDLER_EXTENSIONS_ENABLED . "/$name", false);
define(str_replace("-", "_", mb_strtoupper($name)) . "_ROOT_CONF", chandler_parse_yaml(CHANDLER_EXTENSIONS_ENABLED . "/$name/$name.yml"), false);
-
+
if(isset($configuration->init)) {
$init = require(CHANDLER_EXTENSIONS_ENABLED . "/$name/" . $configuration->init);
if(is_callable($init))
$init();
}
-
+
if(is_dir($hooks = CHANDLER_EXTENSIONS_ENABLED . "/$name/Hooks")) {
foreach(Finder::findFiles("*Hook.php")->in($hooks) as $hookFile) {
$hookClassName = "$name\\Hooks\\" . str_replace(".php", "", end(explode("/", $hookFile)));
$hook = new $hookClassName;
-
+
$this->eventLoop->addListener($hook);
}
}
-
+
if(is_dir($app = CHANDLER_EXTENSIONS_ENABLED . "/$name/Web")) #"app" means "web app", thus variable is called $app
$this->router->readRoutes("$app/routes.yml", $name, $this->rootApp !== $name);
}
}
-
+
function getExtensions(bool $onlyEnabled = false): array
{
return $onlyEnabled
? array_filter($this->extensions, function($e) { return $e->enabled; })
: $this->extensions;
}
-
+
function getExtension(string $name): ?object
{
return @$this->extensions[$name];
}
-
+
function disableExtension(string $name): void
{
if(!array_key_exists($name, $this->getExtensions(true))) return;
-
+
if(!unlink(CHANDLER_EXTENSIONS_ENABLED . "/$name")) throw new \Exception("Could not disable extension");
}
-
+
function enableExtension(string $name): void
{
if(CHANDLER_ROOT_CONF["extensions"]["allEnabled"]) return;
-
+
if(array_key_exists($name, $this->getExtensions(true))) return;
-
+
$path = CHANDLER_EXTENSIONS_AVAILABLE . "/$name";
if(!is_dir($path)) throw new \Exception("Extension doesn't exist");
-
+
if(!symlink($path, str_replace("available", "enabled", $path))) throw new \Exception("Could not enable extension");
}
-
+
use TSimpleSingleton;
}
diff --git a/chandler/MVC/Routing/Router.php b/chandler/MVC/Routing/Router.php
index b98f89c..23b7ecc 100644
--- a/chandler/MVC/Routing/Router.php
+++ b/chandler/MVC/Routing/Router.php
@@ -4,7 +4,6 @@ declare(strict_types = 1);
namespace Chandler\MVC\Routing;
-use Chandler\Eventing\EventDispatcher;
use Chandler\MVC\Exceptions\InterruptedException;
use Chandler\MVC\IPresenter;
use Chandler\Patterns\TSimpleSingleton;
@@ -199,7 +198,7 @@ class Router
{
$key = hash("snefru", CHANDLER_ROOT_CONF["security"]["secret"] . bin2hex($nonce));
$data = $route->namespace;
- $data .= Session::i()->get("tok", -1);
+ $data .= Session::getInstance()->get("tok", -1);
return hash_hmac("snefru", $data, $key) . "#" . bin2hex($nonce);
}
@@ -257,10 +256,5 @@ class Router
return null;
}
- private function __construct()
- {
- $this->events = EventDispatcher::i();
- }
-
use TSimpleSingleton;
}
diff --git a/chandler/MVC/SimplePresenter.php b/chandler/MVC/SimplePresenter.php
index 065b9ce..8299571 100644
--- a/chandler/MVC/SimplePresenter.php
+++ b/chandler/MVC/SimplePresenter.php
@@ -10,22 +10,22 @@ abstract class SimplePresenter implements IPresenter
const REDIRECT_TEMPORARY = 2;
const REDIRECT_PERMAMENT_PRESISTENT = 8;
const REDIRECT_TEMPORARY_PRESISTENT = 7;
-
+
protected $mmReader;
protected $template;
protected $errorTemplate = NULL;
-
+
function __construct()
{
$this->template = (object) [];
}
-
+
function getTemplatingEngine(): TemplatingEngine
{
$latte = new TemplatingEngine;
$macros = new \Latte\Macros\MacroSet($latte->getCompiler());
$latte->setTempDirectory(CHANDLER_ROOT . "/tmp/cache/templates");
-
+
$macros->addMacro("css", '
$domain = "' . explode("\\", static::class)[0] . '";
$file = (%node.array)[0];
@@ -62,18 +62,18 @@ abstract class SimplePresenter implements IPresenter
echo "";
'
);
-
+
return $latte;
}
-
+
protected function throwError(int $code = 400, string $desc = "Bad Request", string $message = ""): void
{
if(!is_null($this->errorTemplate)) {
header("HTTP/1.0 $code $desc");
-
+
$ext = explode("\\", get_class($this))[0];
$path = CHANDLER_EXTENSIONS_ENABLED . "/$ext/Web/Presenters/templates/" . $this->errorTemplate . ".xml";
-
+
$latte = $this->getTemplatingEngine();
$latte->render($path, array_merge_recursive([
"code" => $code,
@@ -85,18 +85,18 @@ abstract class SimplePresenter implements IPresenter
chandler_http_panic($code, $desc, $message);
}
}
-
+
protected function assertNoCSRF(): void
{
if(!$GLOBALS["csrfCheck"])
$this->throwError(400, "Bad Request", "CSRF token is missing or invalid.");
}
-
+
protected function terminate(): void
{
throw new Exceptions\InterruptedException;
}
-
+
protected function notFound(): void
{
$this->throwError(
@@ -105,51 +105,51 @@ abstract class SimplePresenter implements IPresenter
"The resource you are looking for has been deleted, had its name changed or doesn't exist."
);
}
-
+
protected function getCaller(): string
{
return $GLOBALS["parentModule"] ?? "libchandler:absolute.0";
}
-
+
protected function redirect(string $location, int $code = 2): void
{
$code = 300 + $code;
if(($code <=> 300) !== 0 && $code > 399) return;
-
+
header("HTTP/1.1 $code");
header("Location: $location");
exit;
}
-
+
protected function pass(string $to, ...$args): void
{
$args = array_merge([$to], $args);
- $router = \Chandler\MVC\Routing\Router::i();
+ $router = \Chandler\MVC\Routing\Router::getInstance();
$__out = $router->execute($router->reverse(...$args), "libchandler:absolute.0");
exit($__out);
}
-
+
protected function sendmail(string $to, string $template, array $params): void
{
$emailDir = pathinfo($template, PATHINFO_DIRNAME);
$template .= ".eml.latte";
-
+
$renderedHTML = (new TemplatingEngine)->renderToString($template, $params);
$document = new \DOMDocument();
$document->loadHTML($renderedHTML, LIBXML_NOEMPTYTAG);
$querySel = new \DOMXPath($document);
-
+
$subject = $querySel->query("//title/text()")->item(0)->data;
-
+
foreach($querySel->query("//link[@rel='stylesheet']") as $link) {
$style = $document->createElement("style");
$style->setAttribute("id", uniqid("mail", true));
$style->appendChild(new \DOMText(file_get_contents("$emailDir/assets/css/" . $link->getAttribute("href"))));
-
+
$link->parentNode->appendChild($style);
$link->parentNode->removeChild($link);
}
-
+
foreach($querySel->query("//img") as $image) {
$imagePath = "$emailDir/assets/res/" . $image->getAttribute("src");
$type = pathinfo($imagePath, PATHINFO_EXTENSION);
@@ -157,36 +157,36 @@ abstract class SimplePresenter implements IPresenter
$image->setAttribute("src", "data:image/$type;base64,$contents");
}
-
+
\Chandler\Email\Email::send($to, $subject, $document->saveHTML());
}
-
+
protected function queryParam(string $index): ?string
{
return $_GET[$index] ?? NULL;
}
-
+
protected function postParam(string $index, bool $csrfCheck = true): ?string
{
if($csrfCheck)
$this->assertNoCSRF();
-
+
return $_POST[$index] ?? NULL;
}
-
+
protected function requestParam(string $index): ?string
{
return $_REQUEST[$index] ?? NULL;
}
-
+
protected function jsonParam(string $index): ?string
{
if(!isset($GLOBALS["jsonInputCache"]))
$GLOBALS["jsonInputCache"] = json_decode($this->queryParam("js") ?? file_get_contents("php://input"), true);
-
+
return $GLOBALS["jsonInputCache"][$index] ?? NULL;
}
-
+
protected function findParam(string $index, array $searchIn = ["json", "post", "query"]): ?string
{
$res = NULL;
@@ -198,38 +198,38 @@ abstract class SimplePresenter implements IPresenter
else if($search === "query")
$res = $this->queryParam($index) ?? $res;
}
-
+
return $res;
}
-
+
protected function checkbox(string $name): bool
{
return ($this->postParam($name) ?? "off") === "on";
}
-
+
function getTemplateScope(): array
{
return (array) $this->template;
}
-
+
function onStartup(): void
{
date_default_timezone_set("UTC");
}
-
+
function onBeforeRender(): void
{
$this->template->csrfToken = $GLOBALS["csrfToken"];
}
-
+
function onAfterRender(): void
{}
-
+
function onStop(): void
{}
-
+
function onDestruction(): void
{}
-
+
use SmartObject;
}
diff --git a/chandler/Patterns/TSimpleSingleton.php b/chandler/Patterns/TSimpleSingleton.php
index c006746..c14672d 100644
--- a/chandler/Patterns/TSimpleSingleton.php
+++ b/chandler/Patterns/TSimpleSingleton.php
@@ -17,7 +17,7 @@ trait TSimpleSingleton
/**
* @return static
*/
- public static function i(): self
+ public static function getInstance(): self
{
if (is_null(static::$instance)) {
return static::$instance = new static();
diff --git a/chandler/Security/Authenticator.php b/chandler/Security/Authenticator.php
index aefd758..aec876c 100644
--- a/chandler/Security/Authenticator.php
+++ b/chandler/Security/Authenticator.php
@@ -8,18 +8,18 @@ class Authenticator
{
private $db;
private $session;
-
+
private function __construct()
{
$this->db = DatabaseConnection::i()->getContext();
- $this->session = Session::i();
+ $this->session = Session::getInstance();
}
-
+
private function verifySuRights(string $uId): bool
{
-
+
}
-
+
private function makeToken(string $user, string $ip, string $ua): string
{
$data = ["user" => $user, "ip" => $ip, "ua" => $ua];
@@ -27,15 +27,15 @@ class Authenticator
->table("ChandlerTokens")
->where($data)
->fetch();
-
+
if(!$token) {
$this->db->table("ChandlerTokens")->insert($data);
$token = $this->db->table("ChandlerTokens")->where($data)->fetch();
}
-
+
return $token->token;
}
-
+
static function verifyHash(string $input, string $hash): bool
{
try {
@@ -54,46 +54,46 @@ class Authenticator
} catch(\SodiumException $ex) {
return false;
}
-
+
return true;
}
-
+
function getUser(): ?User
{
$token = $this->session->get("tok");
if(!$token) return null;
-
+
$token = $this->db
->table("ChandlerTokens")
->where([
"token" => $token,
])
->fetch();
-
+
if(!$token) return null;
-
+
$checksPassed = false;
if(CHANDLER_ROOT_CONF["security"]["extendedValidation"])
$checksPassed = $token->ip === CONNECTING_IP && $token->ua === $_SERVER["HTTP_USER_AGENT"];
else
$checksPassed = true;
-
+
if($checksPassed) {
$su = $this->session->get("_su");
$user = $this->db->table("ChandlerUsers")->get($su ?? $token->user);
if(!$user) return null;
-
+
return new User($user, !is_null($su));
}
-
+
return null;
}
-
+
function authenticate(string $user): void
{
$this->session->set("tok", $this->makeToken($user, CONNECTING_IP, $_SERVER["HTTP_USER_AGENT"]));
}
-
+
function verifyCredentials(string $id, string $password): bool
{
$user = $this->db->table("ChandlerUsers")->get($id);
@@ -101,30 +101,30 @@ class Authenticator
return false;
else if(!$this->verifyHash($password, $user->passwordHash))
return false;
-
+
return true;
}
-
+
function login(string $id, string $password): bool
{
if(!$this->verifyCredentials($id, $password))
return false;
-
+
$this->authenticate($id);
return true;
}
-
+
function logout(bool $revoke = false): bool
{
$token = $this->session->get("tok");
if(!$token) return false;
-
+
if($revoke) $this->db->table("ChandlerTokens")->where("id", $token)->delete();
-
+
$this->session->set("tok", NULL);
-
+
return true;
}
-
+
use TSimpleSingleton;
}
diff --git a/composer.json b/composer.json
index ebadf82..5dfcab8 100644
--- a/composer.json
+++ b/composer.json
@@ -1,8 +1,11 @@
{
"autoload": {
"classmap": [
- "chandler/Eventing/Events/Event.php"
- ]
+ "chandler/Patterns/TSimpleSingleton.php"
+ ],
+ "psr-4": {
+ "Chandler\\": "src"
+ }
},
"autoload-dev": {
"psr-4": {
@@ -14,6 +17,7 @@
"minimum-stability": "stable",
"name": "openvk/chanlder",
"require": {
+ "ext-yaml": "*",
"php": "^7.3",
"nette/utils": "^3.0",
"nette/di": "^3.0",
diff --git a/install/nginx.conf b/install/nginx.conf
index 99ba4cd..c4d582e 100644
--- a/install/nginx.conf
+++ b/install/nginx.conf
@@ -14,10 +14,10 @@ server {
listen [::]:443 ssl http2;
server_name domain.tld;
- root /opt/chandler/htdocs;
-
+ root /opt/chandler/public;
+
client_max_body_size 100m;
-
+
index index.php;
ssl_certificate /path/to/fullchain.pem;
diff --git a/phpunit.xml b/phpunit.xml
index 4410f54..5dcef63 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -3,6 +3,8 @@
chandler/Eventing/Events/Event.php
+ chandler/Eventing/EventDispatcher.php
+ chandler/Patterns/TSimpleSingleton.php
diff --git a/public/index.php b/public/index.php
index 88f0323..7d44b56 100644
--- a/public/index.php
+++ b/public/index.php
@@ -1,5 +1,9 @@
ignite();
diff --git a/tests/Chandler/Eventing/EventDispatcherTest.php b/tests/Chandler/Eventing/EventDispatcherTest.php
deleted file mode 100644
index c37e1ac..0000000
--- a/tests/Chandler/Eventing/EventDispatcherTest.php
+++ /dev/null
@@ -1,71 +0,0 @@
-assertTrue(EventDispatcher::i()->addListener($hook));
- }
-
- /**
- * @return void
- */
- public function testMethodI(): void
- {
- $this->assertSame(EventDispatcher::i(), EventDispatcher::i());
- }
-
- /**
- * @dataProvider provideMethodPushEvent
- *
- * @param \Chandler\Eventing\Events\Event $event
- *
- * @return void
- */
- public function testMethodPushEvent(Event $event): void
- {
- $this->assertSame($event, EventDispatcher::i()->pushEvent($event));
- }
-}
diff --git a/tests/Chandler/Eventing/Events/EventTest.php b/tests/Chandler/Eventing/Events/EventTest.php
deleted file mode 100644
index 56a6e74..0000000
--- a/tests/Chandler/Eventing/Events/EventTest.php
+++ /dev/null
@@ -1,46 +0,0 @@
-assertClassHasAttribute("code", Event::class);
- }
-
- /**
- * @return void
- */
- public function testPropertyData(): void
- {
- $this->assertClassHasAttribute("data", Event::class);
- }
-
- /**
- * @return void
- */
- public function testPropertyPristine(): void
- {
- $this->assertClassHasAttribute("pristine", Event::class);
- }
-
- /**
- * @return void
- */
- public function testPropertyTime(): void
- {
- $this->assertClassHasAttribute("time", Event::class);
- }
-}