Routing, static views
This commit is contained in:
parent
0f1c86f210
commit
eb04206989
18
.htaccess
18
.htaccess
@ -1,14 +1,16 @@
|
|||||||
php_flag display_errors on
|
php_flag display_errors on
|
||||||
Options -Indexes
|
Options -Indexes
|
||||||
|
|
||||||
|
DirectorySlash Off
|
||||||
|
|
||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
RewriteRule ^api(/.*)?$ /index.php?api=$1 [L,QSA]
|
RewriteRule ^api(/.*)?$ /index.php?api=$1 [L,QSA]
|
||||||
|
|
||||||
<IfModule mod_rewrite.c>
|
RewriteEngine On
|
||||||
RewriteEngine On
|
RewriteOptions AllowNoSlash
|
||||||
DirectorySlash Off
|
RewriteRule ^((\.idea|\.git|src|test|core|static)(/.*)?)$ /index.php?site=$1 [L,QSA]
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d [OR]
|
RewriteEngine On
|
||||||
RewriteCond %{REQUEST_URI} "(\.idea|\.git|src|test|core)(/.*)?"
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
RewriteRule ^(.*)$ /index.php?site=$1 [L,QSA]
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
</IfModule>
|
RewriteRule ^(.*)$ /index.php?site=$1 [L,QSA]
|
@ -133,7 +133,7 @@ class CreateDatabase {
|
|||||||
->addRow("^/register(/)?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\Register")
|
->addRow("^/register(/)?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\Register")
|
||||||
->addRow("^/confirmEmail(/)?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\ConfirmEmail")
|
->addRow("^/confirmEmail(/)?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\ConfirmEmail")
|
||||||
->addRow("^/acceptInvite(/)?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\AcceptInvite")
|
->addRow("^/acceptInvite(/)?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\AcceptInvite")
|
||||||
->addRow("^/$", "dynamic", "\\Documents\\Welcome", NULL);
|
->addRow("^/$", "static", "/static/welcome.html", NULL);
|
||||||
|
|
||||||
return $queries;
|
return $queries;
|
||||||
}
|
}
|
||||||
|
@ -29,12 +29,7 @@ namespace Documents\Admin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected function initSources() {
|
protected function initSources() {
|
||||||
// $this->loadJQuery();
|
|
||||||
$this->loadFontawesome();
|
$this->loadFontawesome();
|
||||||
// $this->addJS(Script::CORE);
|
|
||||||
// $this->addCSS(Link::CORE);
|
|
||||||
// $this->addJS(Script::ADMIN);
|
|
||||||
// $this->addCSS(Link::ADMIN);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function initMetas() {
|
protected function initMetas() {
|
||||||
|
@ -17,6 +17,7 @@ namespace Documents\Document404 {
|
|||||||
|
|
||||||
use Elements\Body;
|
use Elements\Body;
|
||||||
use Elements\Head;
|
use Elements\Head;
|
||||||
|
use Elements\SimpleBody;
|
||||||
use Views\View404;
|
use Views\View404;
|
||||||
|
|
||||||
class Head404 extends Head {
|
class Head404 extends Head {
|
||||||
@ -47,7 +48,7 @@ namespace Documents\Document404 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Body404 extends Body {
|
class Body404 extends SimpleBody {
|
||||||
|
|
||||||
public function __construct($document) {
|
public function __construct($document) {
|
||||||
parent::__construct($document);
|
parent::__construct($document);
|
||||||
@ -57,10 +58,8 @@ namespace Documents\Document404 {
|
|||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCode() {
|
protected function getContent() {
|
||||||
$html = parent::getCode();
|
return $this->load(View404::class);
|
||||||
$html .= "<body>" . (new View404($this->getDocument())) . "</body>";
|
|
||||||
return $html;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Documents {
|
|
||||||
|
|
||||||
use Documents\Welcome\WelcomeBody;
|
|
||||||
use Documents\Welcome\WelcomeHead;
|
|
||||||
use Elements\Document;
|
|
||||||
use Objects\User;
|
|
||||||
|
|
||||||
class Welcome extends Document {
|
|
||||||
public function __construct(User $user, ?string $view) {
|
|
||||||
parent::__construct($user, WelcomeHead::class, WelcomeBody::class, $view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Documents\Welcome {
|
|
||||||
|
|
||||||
use Elements\Head;
|
|
||||||
use Elements\SimpleBody;
|
|
||||||
|
|
||||||
class WelcomeHead extends Head {
|
|
||||||
|
|
||||||
public function __construct($document) {
|
|
||||||
parent::__construct($document);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function initSources() {
|
|
||||||
$this->loadBootstrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function initMetas() {
|
|
||||||
return array(
|
|
||||||
array('name' => 'viewport', 'content' => 'width=device-width, initial-scale=1.0'),
|
|
||||||
array('name' => 'format-detection', 'content' => 'telephone=yes'),
|
|
||||||
array('charset' => 'utf-8'),
|
|
||||||
array("http-equiv" => 'expires', 'content' => '0'),
|
|
||||||
array("name" => 'robots', 'content' => 'noarchive'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function initRawFields() {
|
|
||||||
return array();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function initTitle() {
|
|
||||||
return "Welcome";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WelcomeBody extends SimpleBody {
|
|
||||||
|
|
||||||
public function __construct($document) {
|
|
||||||
parent::__construct($document);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getContent() {
|
|
||||||
return
|
|
||||||
"<div class='container mt-5'>
|
|
||||||
<div class='row'>
|
|
||||||
<div class='col-lg-9 col-12 mx-auto'>
|
|
||||||
<div class='jumbotron'>
|
|
||||||
<h1>Congratulations!</h1>
|
|
||||||
<p class='lead'>Your Web-Base Installation is now ready to use!</p>
|
|
||||||
<hr class='my-4' />
|
|
||||||
<p>
|
|
||||||
You can now login into your <a href='/admin'>Administrator Dashboard</a> to adjust your settings
|
|
||||||
and add routes & pages.
|
|
||||||
You can add new documents and views by adding classes in the corresponding
|
|
||||||
directories and link to them, by creating rules in the Administrator Dashboard.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -15,6 +15,7 @@ abstract class Head extends View {
|
|||||||
function __construct($document) {
|
function __construct($document) {
|
||||||
parent::__construct($document);
|
parent::__construct($document);
|
||||||
$this->sources = array();
|
$this->sources = array();
|
||||||
|
$this->searchable = false;
|
||||||
$this->metas = $this->initMetas();
|
$this->metas = $this->initMetas();
|
||||||
$this->rawFields = $this->initRawFields();
|
$this->rawFields = $this->initRawFields();
|
||||||
$this->title = $this->initTitle();
|
$this->title = $this->initTitle();
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace Elements;
|
namespace Elements;
|
||||||
|
|
||||||
|
use External\PHPMailer\Exception;
|
||||||
|
|
||||||
abstract class View extends StaticView {
|
abstract class View extends StaticView {
|
||||||
|
|
||||||
private Document $document;
|
private Document $document;
|
||||||
@ -25,6 +27,21 @@ abstract class View extends StaticView {
|
|||||||
public function isSearchable() { return $this->searchable; }
|
public function isSearchable() { return $this->searchable; }
|
||||||
public function getReference() { return $this->reference; }
|
public function getReference() { return $this->reference; }
|
||||||
|
|
||||||
|
protected function load(string $viewClass) : string {
|
||||||
|
try {
|
||||||
|
$reflectionClass = new \ReflectionClass($viewClass);
|
||||||
|
if ($reflectionClass->isSubclassOf(View::class) && $reflectionClass->isInstantiable()) {
|
||||||
|
$view = $reflectionClass->newInstanceArgs(array($this->getDocument()));
|
||||||
|
$view->loadView();
|
||||||
|
return $view;
|
||||||
|
}
|
||||||
|
} catch(\ReflectionException $e) {
|
||||||
|
error_log($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
private function loadLanguageModules() {
|
private function loadLanguageModules() {
|
||||||
$lang = $this->document->getUser()->getLanguage();
|
$lang = $this->document->getUser()->getLanguage();
|
||||||
foreach($this->langModules as $langModule) {
|
foreach($this->langModules as $langModule) {
|
||||||
|
@ -11,9 +11,11 @@ class LanguageFlags extends View {
|
|||||||
public function __construct($document) {
|
public function __construct($document) {
|
||||||
parent::__construct($document);
|
parent::__construct($document);
|
||||||
$this->languageFlags = array();
|
$this->languageFlags = array();
|
||||||
|
$this->searchable = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCode() {
|
public function loadView() {
|
||||||
|
parent::loadView();
|
||||||
|
|
||||||
$request = new \Api\Language\Get($this->getDocument()->getUser());
|
$request = new \Api\Language\Get($this->getDocument()->getUser());
|
||||||
if ($request->execute()) {
|
if ($request->execute()) {
|
||||||
@ -51,8 +53,10 @@ class LanguageFlags extends View {
|
|||||||
"<img class=\"p-1 clickable\" src=\"/img/icons/lang/$langCode.gif\" alt=\"$langName\" title=\"$langName\">"
|
"<img class=\"p-1 clickable\" src=\"/img/icons/lang/$langCode.gif\" alt=\"$langName\" title=\"$langName\">"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return implode('', $this->languageFlags);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCode() {
|
||||||
|
return implode('', $this->languageFlags);
|
||||||
|
}
|
||||||
}
|
}
|
@ -32,7 +32,7 @@ class LoginBody extends Body {
|
|||||||
$backToStartPage = L("Back to Start Page");
|
$backToStartPage = L("Back to Start Page");
|
||||||
$stayLoggedIn = L("Stay logged in");
|
$stayLoggedIn = L("Stay logged in");
|
||||||
|
|
||||||
$flags = new LanguageFlags($this->getDocument());
|
$flags = $this->load(LanguageFlags::class);
|
||||||
$iconBack = $this->createIcon("arrow-circle-left");
|
$iconBack = $this->createIcon("arrow-circle-left");
|
||||||
$domain = $_SERVER['HTTP_HOST'];
|
$domain = $_SERVER['HTTP_HOST'];
|
||||||
$protocol = getProtocol();
|
$protocol = getProtocol();
|
||||||
|
198
core/core.php
198
core/core.php
@ -1,104 +1,122 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
define("WEBBASE_VERSION", "0.1.0-alpha");
|
define("WEBBASE_VERSION", "0.1.0-alpha");
|
||||||
|
|
||||||
function getSubclassesOf($parent) {
|
function getProtocol() {
|
||||||
$result = array();
|
return stripos($_SERVER['SERVER_PROTOCOL'], 'https') === 0 ? 'https' : 'http';
|
||||||
foreach (get_declared_classes() as $class) {
|
}
|
||||||
if (is_subclass_of($class, $parent))
|
|
||||||
$result[] = $class;
|
|
||||||
}
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProtocol() {
|
function generateRandomString($length): string {
|
||||||
return stripos($_SERVER['SERVER_PROTOCOL'],'https') === 0 ? 'https' : 'http';
|
$randomString = '';
|
||||||
}
|
if ($length > 0) {
|
||||||
|
$numCharacters = 26 + 26 + 10; // a-z + A-Z + 0-9
|
||||||
function generateRandomString($length) : string {
|
for ($i = 0; $i < $length; $i++) {
|
||||||
$randomString = '';
|
try {
|
||||||
if($length > 0) {
|
$num = random_int(0, $numCharacters - 1);
|
||||||
$numCharacters = 26 + 26 + 10; // a-z + A-Z + 0-9
|
} catch (Exception $e) {
|
||||||
for ($i = 0; $i < $length; $i++)
|
$num = rand(0, $numCharacters - 1);
|
||||||
{
|
|
||||||
try {
|
|
||||||
$num = random_int(0, $numCharacters - 1);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$num = rand(0, $numCharacters - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if($num < 26) $randomString .= chr(ord('a') + $num);
|
|
||||||
else if($num - 26 < 26) $randomString .= chr(ord('A') + $num - 26);
|
|
||||||
else $randomString .= chr(ord('0') + $num - 26 - 26);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $randomString;
|
|
||||||
}
|
|
||||||
|
|
||||||
function startsWith($haystack, $needle) {
|
|
||||||
$length = strlen($needle);
|
|
||||||
return (substr($haystack, 0, $length) === $needle);
|
|
||||||
}
|
|
||||||
|
|
||||||
function endsWith($haystack, $needle) {
|
|
||||||
$length = strlen($needle);
|
|
||||||
if ($length == 0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return (substr($haystack, -$length) === $needle);
|
|
||||||
}
|
|
||||||
|
|
||||||
function intendCode($code, $escape=true) {
|
|
||||||
$newCode = "";
|
|
||||||
$first = true;
|
|
||||||
$brackets = array();
|
|
||||||
$intend = 0;
|
|
||||||
|
|
||||||
foreach(explode("\n", $code) as $line) {
|
|
||||||
if(!$first) $newCode .= "\n";
|
|
||||||
if($escape) $line = htmlspecialchars($line);
|
|
||||||
$line = trim($line);
|
|
||||||
|
|
||||||
if(count($brackets) > 0 && startsWith($line, current($brackets))) {
|
|
||||||
$intend = max(0, $intend - 2);
|
|
||||||
array_pop($brackets);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$newCode .= str_repeat(" ", $intend);
|
if ($num < 26) $randomString .= chr(ord('a') + $num);
|
||||||
$newCode .= $line;
|
else if ($num - 26 < 26) $randomString .= chr(ord('A') + $num - 26);
|
||||||
$first = false;
|
else $randomString .= chr(ord('0') + $num - 26 - 26);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(endsWith($line, "{")) {
|
return $randomString;
|
||||||
$intend += 2;
|
}
|
||||||
array_push($brackets, "}");
|
|
||||||
} else if(endsWith($line, "(")) {
|
function startsWith($haystack, $needle) {
|
||||||
$intend += 2;
|
$length = strlen($needle);
|
||||||
array_push($brackets, ")");
|
return (substr($haystack, 0, $length) === $needle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function endsWith($haystack, $needle) {
|
||||||
|
$length = strlen($needle);
|
||||||
|
if ($length == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return (substr($haystack, -$length) === $needle);
|
||||||
|
}
|
||||||
|
|
||||||
|
function intendCode($code, $escape = true) {
|
||||||
|
$newCode = "";
|
||||||
|
$first = true;
|
||||||
|
$brackets = array();
|
||||||
|
$intend = 0;
|
||||||
|
|
||||||
|
foreach (explode("\n", $code) as $line) {
|
||||||
|
if (!$first) $newCode .= "\n";
|
||||||
|
if ($escape) $line = htmlspecialchars($line);
|
||||||
|
$line = trim($line);
|
||||||
|
|
||||||
|
if (count($brackets) > 0 && startsWith($line, current($brackets))) {
|
||||||
|
$intend = max(0, $intend - 2);
|
||||||
|
array_pop($brackets);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $newCode;
|
$newCode .= str_repeat(" ", $intend);
|
||||||
}
|
$newCode .= $line;
|
||||||
|
$first = false;
|
||||||
|
|
||||||
function replaceCssSelector($sel) {
|
if (endsWith($line, "{")) {
|
||||||
return preg_replace("~[.#<>]~", "_", preg_replace("~[:\-]~", "", $sel));
|
$intend += 2;
|
||||||
}
|
array_push($brackets, "}");
|
||||||
|
} else if (endsWith($line, "(")) {
|
||||||
function getClassPath($class, $suffix=true) {
|
$intend += 2;
|
||||||
$path = str_replace('\\', '/', $class);
|
array_push($brackets, ")");
|
||||||
$path = array_values(array_filter(explode("/", $path)));
|
|
||||||
|
|
||||||
if (strcasecmp($path[0], "api") === 0 && count($path) > 2 && strcasecmp($path[1], "Parameter") !== 0) {
|
|
||||||
$path = "Api/" . $path[1] . "API";
|
|
||||||
} else {
|
|
||||||
$path = implode("/", $path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$suffix = ($suffix ? ".class" : "");
|
|
||||||
return "core/$path$suffix.php";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createError($msg) {
|
return $newCode;
|
||||||
return json_encode(array("success" => false, "msg" => $msg));
|
}
|
||||||
|
|
||||||
|
function replaceCssSelector($sel) {
|
||||||
|
return preg_replace("~[.#<>]~", "_", preg_replace("~[:\-]~", "", $sel));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClassPath($class, $suffix = true) {
|
||||||
|
$path = str_replace('\\', '/', $class);
|
||||||
|
$path = array_values(array_filter(explode("/", $path)));
|
||||||
|
|
||||||
|
if (strcasecmp($path[0], "api") === 0 && count($path) > 2 && strcasecmp($path[1], "Parameter") !== 0) {
|
||||||
|
$path = "Api/" . $path[1] . "API";
|
||||||
|
} else {
|
||||||
|
$path = implode("/", $path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$suffix = ($suffix ? ".class" : "");
|
||||||
|
return "core/$path$suffix.php";
|
||||||
|
}
|
||||||
|
|
||||||
|
function createError($msg) {
|
||||||
|
return json_encode(array("success" => false, "msg" => $msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
function serveStatic(string $webRoot, string $file): string {
|
||||||
|
|
||||||
|
$path = realpath($webRoot . "/" . $file);
|
||||||
|
if (!startsWith($path, $webRoot . "/")) {
|
||||||
|
http_response_code(406);
|
||||||
|
return "<b>Access restricted, requested file outside web root:</b> " . htmlspecialchars($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists($path) || !is_file($path) || !is_readable($path)) {
|
||||||
|
http_response_code(500);
|
||||||
|
return "<b>Unable to read file:</b> " . htmlspecialchars($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pathInfo = pathinfo($file);
|
||||||
|
|
||||||
|
// maybe I will allow more later…
|
||||||
|
$allowedExtension = array("html", "htm");
|
||||||
|
$ext = $pathInfo["extension"] ?? "";
|
||||||
|
if (!in_array($ext, $allowedExtension)) {
|
||||||
|
http_response_code(406);
|
||||||
|
return "<b>Access restricted:</b> Extension '" . htmlspecialchars($ext) . "' not allowed.";
|
||||||
|
}
|
||||||
|
|
||||||
|
$mimeType = mime_content_type($file);
|
||||||
|
header("Content-Type: $mimeType");
|
||||||
|
return readfile($file);
|
||||||
|
}
|
||||||
|
@ -89,6 +89,7 @@ if(isset($_GET["api"]) && is_string($_GET["api"])) {
|
|||||||
$success = $req->execute(array("request" => $documentName));
|
$success = $req->execute(array("request" => $documentName));
|
||||||
$response = "";
|
$response = "";
|
||||||
if (!$success) {
|
if (!$success) {
|
||||||
|
http_response_code(500);
|
||||||
$response = "Unable to find route: " . $req->getLastError();
|
$response = "Unable to find route: " . $req->getLastError();
|
||||||
} else {
|
} else {
|
||||||
$route = $req->getResult()["route"];
|
$route = $req->getResult()["route"];
|
||||||
@ -106,8 +107,8 @@ if(isset($_GET["api"]) && is_string($_GET["api"])) {
|
|||||||
header("Location: $target");
|
header("Location: $target");
|
||||||
break;
|
break;
|
||||||
case "static":
|
case "static":
|
||||||
http_response_code(501);
|
$currentDir = dirname(__FILE__);
|
||||||
$response = "Not implemented yet.";
|
$response = serveStatic($currentDir, $target);
|
||||||
break;
|
break;
|
||||||
case "dynamic":
|
case "dynamic":
|
||||||
$view = $route["extra"] ?? "";
|
$view = $route["extra"] ?? "";
|
||||||
|
30
static/welcome.html
Normal file
30
static/welcome.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="format-detection" content="telephone=yes">
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="expires" content="0">
|
||||||
|
<meta name="robots" content="noarchive">
|
||||||
|
<title>Welcome</title>
|
||||||
|
<link rel="stylesheet" href="/css/bootstrap.min.css" type="text/css">
|
||||||
|
<script type="text/javascript" src="/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-9 col-12 mx-auto">
|
||||||
|
<div class="jumbotron">
|
||||||
|
<h1>Congratulations!</h1>
|
||||||
|
<p class="lead">Your Web-Base Installation is now ready to use!</p>
|
||||||
|
<hr class="my-4" />
|
||||||
|
<p>
|
||||||
|
You can now login into your <a href="/admin">Administrator Dashboard</a> to adjust your settings
|
||||||
|
and add routes & pages.
|
||||||
|
You can add new documents and views by adding classes in the corresponding
|
||||||
|
directories and link to them, by creating rules in the Administrator Dashboard.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
Loading…
Reference in New Issue
Block a user