Added CSRFP to Whois.
This commit is contained in:
parent
f161d2e922
commit
beed8321ef
8 changed files with 351 additions and 50 deletions
104
assets/common.js/elem.js
Normal file
104
assets/common.js/elem.js
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
const $e = function(info, attrs, child, created) {
|
||||||
|
info = info || {};
|
||||||
|
|
||||||
|
if(typeof info === 'string') {
|
||||||
|
info = {tag: info};
|
||||||
|
if(attrs)
|
||||||
|
info.attrs = attrs;
|
||||||
|
if(child)
|
||||||
|
info.child = child;
|
||||||
|
if(created)
|
||||||
|
info.created = created;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elem = document.createElement(info.tag || 'div');
|
||||||
|
|
||||||
|
if(info.attrs) {
|
||||||
|
const attrs = info.attrs;
|
||||||
|
|
||||||
|
for(let key in attrs) {
|
||||||
|
const attr = attrs[key];
|
||||||
|
if(attr === undefined || attr === null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
switch(typeof attr) {
|
||||||
|
case 'function':
|
||||||
|
if(key.substring(0, 2) === 'on')
|
||||||
|
key = key.substring(2).toLowerCase();
|
||||||
|
elem.addEventListener(key, attr);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'object':
|
||||||
|
if(attr instanceof Array) {
|
||||||
|
if(key === 'class')
|
||||||
|
key = 'classList';
|
||||||
|
|
||||||
|
const prop = elem[key];
|
||||||
|
let addFunc = null;
|
||||||
|
|
||||||
|
if(prop instanceof Array)
|
||||||
|
addFunc = prop.push.bind(prop);
|
||||||
|
else if(prop instanceof DOMTokenList)
|
||||||
|
addFunc = prop.add.bind(prop);
|
||||||
|
|
||||||
|
if(addFunc !== null) {
|
||||||
|
for(let j = 0; j < attr.length; ++j)
|
||||||
|
addFunc(attr[j]);
|
||||||
|
} else {
|
||||||
|
if(key === 'classList')
|
||||||
|
key = 'class';
|
||||||
|
elem.setAttribute(key, attr.toString());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for(const attrKey in attr)
|
||||||
|
elem[key][attrKey] = attr[attrKey];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if(key === 'className')
|
||||||
|
key = 'class';
|
||||||
|
elem.setAttribute(key, attr.toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(info.child) {
|
||||||
|
let children = info.child;
|
||||||
|
|
||||||
|
if(!Array.isArray(children))
|
||||||
|
children = [children];
|
||||||
|
|
||||||
|
for(const child of children) {
|
||||||
|
switch(typeof child) {
|
||||||
|
case 'string':
|
||||||
|
elem.appendChild($t(child));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'object':
|
||||||
|
if(child instanceof Element)
|
||||||
|
elem.appendChild(child);
|
||||||
|
else if(child.getElement) {
|
||||||
|
const childElem = child.getElement();
|
||||||
|
if(childElem instanceof Element)
|
||||||
|
elem.appendChild(childElem);
|
||||||
|
else
|
||||||
|
elem.appendChild($e(child));
|
||||||
|
} else
|
||||||
|
elem.appendChild($e(child));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
elem.appendChild($t(child.toString()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(info.created)
|
||||||
|
info.created(elem);
|
||||||
|
|
||||||
|
return elem;
|
||||||
|
};
|
||||||
|
const $er = (type, props, ...children) => $e({ tag: type, attrs: props, child: children });
|
|
@ -1 +1,64 @@
|
||||||
/* common.js */
|
const $i = document.getElementById.bind(document);
|
||||||
|
const $c = document.getElementsByClassName.bind(document);
|
||||||
|
const $q = document.querySelector.bind(document);
|
||||||
|
const $qa = document.querySelectorAll.bind(document);
|
||||||
|
const $t = document.createTextNode.bind(document);
|
||||||
|
|
||||||
|
const $r = function(element) {
|
||||||
|
if(element && element.parentNode)
|
||||||
|
element.parentNode.removeChild(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
const $ri = function(name) {
|
||||||
|
$r($i(name));
|
||||||
|
};
|
||||||
|
|
||||||
|
const $ib = function(ref, elem) {
|
||||||
|
ref.parentNode.insertBefore(elem, ref);
|
||||||
|
};
|
||||||
|
|
||||||
|
const $rc = function(element) {
|
||||||
|
while(element.lastChild)
|
||||||
|
element.removeChild(element.lastChild);
|
||||||
|
};
|
||||||
|
|
||||||
|
const $ar = function(array, index) {
|
||||||
|
array.splice(index, 1);
|
||||||
|
};
|
||||||
|
const $ari = function(array, item) {
|
||||||
|
let index;
|
||||||
|
while(array.length > 0 && (index = array.indexOf(item)) >= 0)
|
||||||
|
$ar(array, index);
|
||||||
|
};
|
||||||
|
const $arf = function(array, predicate) {
|
||||||
|
let index;
|
||||||
|
while(array.length > 0 && (index = array.findIndex(predicate)) >= 0)
|
||||||
|
$ar(array, index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const $as = function(array) {
|
||||||
|
if(array.length < 2)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for(let i = array.length - 1; i > 0; --i) {
|
||||||
|
let j = Math.floor(Math.random() * (i + 1)),
|
||||||
|
tmp = array[i];
|
||||||
|
array[i] = array[j];
|
||||||
|
array[j] = tmp;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const $csrfp = (function() {
|
||||||
|
const elem = $q('meta[name="csrfp-token"]');
|
||||||
|
|
||||||
|
return {
|
||||||
|
get: () => elem?.content ?? '',
|
||||||
|
set: token => {
|
||||||
|
if(elem != null)
|
||||||
|
elem.content = token?.toString() ?? '';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
#include elem.js
|
||||||
|
#include xhr.js
|
||||||
|
|
102
assets/common.js/xhr.js
Normal file
102
assets/common.js/xhr.js
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
const $x = (function() {
|
||||||
|
const send = function(method, url, options, body) {
|
||||||
|
options ??= {};
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest;
|
||||||
|
const requestHeadersRaw = options?.headers ?? {};
|
||||||
|
const requestHeaders = new Map;
|
||||||
|
|
||||||
|
if(typeof requestHeadersRaw === 'object')
|
||||||
|
for(const name in requestHeadersRaw)
|
||||||
|
if(requestHeadersRaw.hasOwnProperty(name))
|
||||||
|
requestHeaders.set(name.toLowerCase(), requestHeadersRaw[name]);
|
||||||
|
|
||||||
|
if(typeof options.download === 'function') {
|
||||||
|
xhr.onloadstart = ev => options.download(ev);
|
||||||
|
xhr.onprogress = ev => options.download(ev);
|
||||||
|
xhr.onloadend = ev => options.download(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof options.upload === 'function') {
|
||||||
|
xhr.upload.onloadstart = ev => options.upload(ev);
|
||||||
|
xhr.upload.onprogress = ev => options.upload(ev);
|
||||||
|
xhr.upload.onloadend = ev => options.upload(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(options.authed)
|
||||||
|
xhr.withCredentials = true;
|
||||||
|
|
||||||
|
if(typeof options.timeout === 'number')
|
||||||
|
xhr.timeout = options.timeout;
|
||||||
|
|
||||||
|
if(typeof options.abort === 'function')
|
||||||
|
options.abort(() => xhr.abort());
|
||||||
|
|
||||||
|
if(typeof options.xhr === 'function')
|
||||||
|
options.xhr(() => xhr);
|
||||||
|
|
||||||
|
if(typeof body === 'object') {
|
||||||
|
if(body instanceof URLSearchParams) {
|
||||||
|
requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
|
||||||
|
} else if(body instanceof FormData) {
|
||||||
|
requestHeaders.set('content-type', 'multipart/form-data');
|
||||||
|
} else if(body instanceof Blob || body instanceof ArrayBuffer || body instanceof DataView) {
|
||||||
|
if(!requestHeaders.has('content-type'))
|
||||||
|
requestHeaders.set('content-type', 'application/octet-stream');
|
||||||
|
} else if(!requestHeaders.has('content-type')) {
|
||||||
|
const bodyParts = [];
|
||||||
|
for(const name in body)
|
||||||
|
if(body.hasOwnProperty(name))
|
||||||
|
bodyParts.push(encodeURIComponent(name) + '=' + encodeURIComponent(body[name]));
|
||||||
|
body = bodyParts.join('&');
|
||||||
|
requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let responseHeaders = undefined;
|
||||||
|
|
||||||
|
xhr.onload = ev => resolve({
|
||||||
|
status: xhr.status,
|
||||||
|
body: () => xhr.responseText,
|
||||||
|
json: () => JSON.parse(xhr.responseText),
|
||||||
|
headers: () => {
|
||||||
|
if(responseHeaders !== undefined)
|
||||||
|
return responseHeaders;
|
||||||
|
|
||||||
|
responseHeaders = new Map;
|
||||||
|
|
||||||
|
const raw = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/);
|
||||||
|
for(const name in raw)
|
||||||
|
if(raw.hasOwnProperty(name)) {
|
||||||
|
const parts = raw[name].split(': ');
|
||||||
|
responseHeaders.set(parts.shift(), parts.join(': '));
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseHeaders;
|
||||||
|
},
|
||||||
|
xhr: xhr,
|
||||||
|
ev: ev,
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.onerror = ev => reject({
|
||||||
|
xhr: xhr,
|
||||||
|
ev: ev,
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.open(method, url);
|
||||||
|
for(const [name, value] of requestHeaders)
|
||||||
|
xhr.setRequestHeader(name, value);
|
||||||
|
xhr.send(body);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
send: send,
|
||||||
|
get: (url, options, body) => send('GET', url, options, body),
|
||||||
|
post: (url, options, body) => send('POST', url, options, body),
|
||||||
|
delete: (url, options, body) => send('DELETE', url, options, body),
|
||||||
|
patch: (url, options, body) => send('PATCH', url, options, body),
|
||||||
|
put: (url, options, body) => send('PUT', url, options, body),
|
||||||
|
};
|
||||||
|
})();
|
|
@ -1,11 +1,11 @@
|
||||||
(function() {
|
(function() {
|
||||||
var locked = false,
|
let locked = false;
|
||||||
input = document.getElementById('lookup-input'),
|
const input = $i('lookup-input'),
|
||||||
submit = document.getElementById('lookup-submit'),
|
submit = $i('lookup-submit'),
|
||||||
result = document.getElementById('lookup-result'),
|
result = $i('lookup-result'),
|
||||||
tabs = document.getElementById('lookup-tabs');
|
tabs = $i('lookup-tabs');
|
||||||
|
|
||||||
var lock = function() {
|
const lock = function() {
|
||||||
if(locked)
|
if(locked)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var unlock = function() {
|
const unlock = function() {
|
||||||
if(!locked)
|
if(!locked)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
@ -25,66 +25,66 @@
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var lookup = function(target) {
|
const lookup = function(target) {
|
||||||
if(!lock())
|
if(!lock())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var xhr = new XMLHttpRequest;
|
$x.post('/whois/lookup', {}, { _csrfp: $csrfp.get(), target: target }).then(output => {
|
||||||
xhr.addEventListener('load', function() {
|
let headers = output.headers();
|
||||||
var resp = JSON.parse(xhr.responseText);
|
if(headers.has('x-csrfp'))
|
||||||
|
$csrfp.set(headers.get('x-csrfp'));
|
||||||
|
|
||||||
|
let resp = output.json();
|
||||||
if(resp.error)
|
if(resp.error)
|
||||||
alert(resp.text);
|
alert(resp.text);
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
tabs.innerHTML = '';
|
tabs.innerHTML = '';
|
||||||
if(resp.result && resp.result.responses) {
|
if(resp.result && resp.result.responses)
|
||||||
for(var i = 0; i < resp.result.responses.length; ++i) {
|
for(const response of resp.result.responses) {
|
||||||
(function(response) {
|
const tab = document.createElement('a'),
|
||||||
var tab = document.createElement('a'),
|
tabHeader = document.createElement('div'),
|
||||||
tabHeader = document.createElement('div'),
|
tabServer = document.createElement('div');
|
||||||
tabServer = document.createElement('div');
|
|
||||||
|
|
||||||
tab.href = 'javascript:;';
|
tab.href = 'javascript:;';
|
||||||
tab.className = 'whois-result-tab';
|
tab.className = 'whois-result-tab';
|
||||||
if(i === 0) tab.className += ' whois-result-tab-active';
|
if(count === 0) tab.className += ' whois-result-tab-active';
|
||||||
|
|
||||||
tab.onclick = function() {
|
tab.onclick = function() {
|
||||||
var active = document.querySelector('.whois-result-tab-active');
|
const active = $q('.whois-result-tab-active');
|
||||||
if(active) active.classList.remove('whois-result-tab-active');
|
if(active) active.classList.remove('whois-result-tab-active');
|
||||||
tab.classList.add('whois-result-tab-active');
|
tab.classList.add('whois-result-tab-active');
|
||||||
result.textContent = response.lines.join("\r\n").trim();
|
result.textContent = response.lines.join("\r\n").trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
tabHeader.className = 'whois-result-tab-header';
|
tabHeader.className = 'whois-result-tab-header';
|
||||||
tabHeader.textContent = i === 0 ? 'Result' : ('Hop #' + (resp.result.responses.length - i).toString());
|
tabHeader.textContent = count === 0 ? 'Result' : ('Hop #' + (resp.result.responses.length - count).toString());
|
||||||
|
|
||||||
tabServer.className = 'whois-result-tab-server';
|
tabServer.className = 'whois-result-tab-server';
|
||||||
tabServer.textContent = response.server;
|
tabServer.textContent = response.server;
|
||||||
console.log(response);
|
|
||||||
|
|
||||||
tab.appendChild(tabHeader);
|
tab.appendChild(tabHeader);
|
||||||
tab.appendChild(tabServer);
|
tab.appendChild(tabServer);
|
||||||
tabs.appendChild(tab);
|
tabs.appendChild(tab);
|
||||||
})(resp.result.responses[i]);
|
|
||||||
|
++count;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if(tabs.firstChild)
|
if(tabs.firstChild)
|
||||||
tabs.firstChild.click();
|
tabs.firstChild.click();
|
||||||
|
|
||||||
unlock();
|
unlock();
|
||||||
});
|
});
|
||||||
xhr.open('GET', '/whois/lookup?target=' + encodeURIComponent(target));
|
|
||||||
xhr.send();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var readHash = function() {
|
const readHash = function() {
|
||||||
if(location.hash.length < 2) {
|
if(location.hash.length < 2) {
|
||||||
result.textContent = 'Enter a domain or IP address!';
|
result.textContent = 'Enter a domain or IP address!';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var target = decodeURIComponent(location.hash.substring(1));
|
const target = decodeURIComponent(location.hash.substring(1));
|
||||||
if(input.value !== target)
|
if(input.value !== target)
|
||||||
input.value = target;
|
input.value = target;
|
||||||
lookup(target);
|
lookup(target);
|
||||||
|
|
|
@ -18,4 +18,9 @@ set_exception_handler(function(\Throwable $ex) {
|
||||||
exit;
|
exit;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$makai->startCSRFP(
|
||||||
|
$cfg['csrfp'] ?? 'meow',
|
||||||
|
(string)filter_input(INPUT_SERVER, 'REMOTE_ADDR')
|
||||||
|
);
|
||||||
|
|
||||||
$makai->createRouting()->dispatch();
|
$makai->createRouting()->dispatch();
|
||||||
|
|
|
@ -3,11 +3,13 @@ namespace Makai;
|
||||||
|
|
||||||
use Index\Environment;
|
use Index\Environment;
|
||||||
use Index\Data\IDbConnection;
|
use Index\Data\IDbConnection;
|
||||||
|
use Index\Security\CSRFP;
|
||||||
use Sasae\SasaeEnvironment;
|
use Sasae\SasaeEnvironment;
|
||||||
|
|
||||||
final class MakaiContext {
|
final class MakaiContext {
|
||||||
private IDbConnection $dbConn;
|
private IDbConnection $dbConn;
|
||||||
private SasaeEnvironment $templating;
|
private SasaeEnvironment $templating;
|
||||||
|
private CSRFP $csrfp;
|
||||||
|
|
||||||
private SiteInfo $siteInfo;
|
private SiteInfo $siteInfo;
|
||||||
|
|
||||||
|
@ -26,10 +28,6 @@ final class MakaiContext {
|
||||||
$this->sshKeys = new SSHKeys\SSHKeys($dbConn);
|
$this->sshKeys = new SSHKeys\SSHKeys($dbConn);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDatabase(): IDbConnection {
|
|
||||||
return $this->dbConn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSiteInfo(): SiteInfo {
|
public function getSiteInfo(): SiteInfo {
|
||||||
return $this->siteInfo;
|
return $this->siteInfo;
|
||||||
}
|
}
|
||||||
|
@ -46,6 +44,10 @@ final class MakaiContext {
|
||||||
return $this->sshKeys;
|
return $this->sshKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDatabase(): IDbConnection {
|
||||||
|
return $this->dbConn;
|
||||||
|
}
|
||||||
|
|
||||||
public function getTemplating(): SasaeEnvironment {
|
public function getTemplating(): SasaeEnvironment {
|
||||||
return $this->templating;
|
return $this->templating;
|
||||||
}
|
}
|
||||||
|
@ -58,12 +60,21 @@ final class MakaiContext {
|
||||||
cache: $isDebug ? null : ['Makai', GitInfo::hash(true)],
|
cache: $isDebug ? null : ['Makai', GitInfo::hash(true)],
|
||||||
debug: $isDebug,
|
debug: $isDebug,
|
||||||
);
|
);
|
||||||
|
$this->templating->addFunction('csrfp_token', fn() => $this->getCSRFP()->createToken());
|
||||||
$this->templating->addGlobal('globals', [
|
$this->templating->addGlobal('globals', [
|
||||||
'siteInfo' => $this->siteInfo,
|
'siteInfo' => $this->siteInfo,
|
||||||
'assetsInfo' => AssetsInfo::fromCurrent(),
|
'assetsInfo' => AssetsInfo::fromCurrent(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCSRFP(): CSRFP {
|
||||||
|
return $this->csrfp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function startCSRFP(string $secretKey, string $identity): void {
|
||||||
|
$this->csrfp = new CSRFP($secretKey, $identity);
|
||||||
|
}
|
||||||
|
|
||||||
public function createRouting(): RoutingContext {
|
public function createRouting(): RoutingContext {
|
||||||
$routingCtx = new RoutingContext($this->templating);
|
$routingCtx = new RoutingContext($this->templating);
|
||||||
$routingCtx->registerDefaultErrorPages();
|
$routingCtx->registerDefaultErrorPages();
|
||||||
|
@ -74,7 +85,7 @@ final class MakaiContext {
|
||||||
|
|
||||||
$routingCtx->register(new AssetsRoutes($this->siteInfo));
|
$routingCtx->register(new AssetsRoutes($this->siteInfo));
|
||||||
$routingCtx->register(new NowListeningRoutes($this->templating));
|
$routingCtx->register(new NowListeningRoutes($this->templating));
|
||||||
$routingCtx->register(new Whois\WhoisRoutes($this->templating));
|
$routingCtx->register(new Whois\WhoisRoutes($this->templating, fn() => $this->csrfp));
|
||||||
|
|
||||||
$routingCtx->register(new SSHKeys\SSHKeysRoutes($this->sshKeys));
|
$routingCtx->register(new SSHKeys\SSHKeysRoutes($this->sshKeys));
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,8 @@ use Sasae\SasaeEnvironment;
|
||||||
|
|
||||||
class WhoisRoutes extends RouteHandler {
|
class WhoisRoutes extends RouteHandler {
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private SasaeEnvironment $templating
|
private SasaeEnvironment $templating,
|
||||||
|
private \Closure $csrfp,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route('GET', '/whois')]
|
#[Route('GET', '/whois')]
|
||||||
|
@ -15,9 +16,23 @@ class WhoisRoutes extends RouteHandler {
|
||||||
return $this->templating->render('whois/index');
|
return $this->templating->render('whois/index');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('GET', '/whois/lookup')]
|
#[Route('POST', '/whois/lookup')]
|
||||||
public function getLookup($response, $request): array {
|
public function postLookup($response, $request): array {
|
||||||
$target = trim((string)$request->getParam('target'));
|
if(!$request->isFormContent())
|
||||||
|
return 400;
|
||||||
|
|
||||||
|
$content = $request->getContent();
|
||||||
|
$csrfp = ($this->csrfp)();
|
||||||
|
|
||||||
|
if(!$csrfp->verifyToken((string)$content->getParam('_csrfp')))
|
||||||
|
return [
|
||||||
|
'error' => true,
|
||||||
|
'text' => 'Could not validate request, please refresh the page.',
|
||||||
|
];
|
||||||
|
|
||||||
|
$response->setHeader('X-CSRFP', $csrfp->createToken());
|
||||||
|
|
||||||
|
$target = trim((string)$content->getParam('target'));
|
||||||
if(empty($target))
|
if(empty($target))
|
||||||
return [
|
return [
|
||||||
'error' => true,
|
'error' => true,
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<meta charset="{{ master_charset|default('utf-8') }}">
|
<meta charset="{{ master_charset|default('utf-8') }}">
|
||||||
{% if master_title is defined and master_title is not empty %}<title>{{ master_title }}</title>{% endif %}
|
{% if master_title is defined and master_title is not empty %}<title>{{ master_title }}</title>{% endif %}
|
||||||
{% block master_head %}{% endblock %}
|
{% block master_head %}{% endblock %}
|
||||||
|
<meta name="csrfp-token" content="{{ csrfp_token() }}">
|
||||||
{% if styles is defined and styles is iterable and styles is not empty %}
|
{% if styles is defined and styles is iterable and styles is not empty %}
|
||||||
{% for style in styles|reverse %}
|
{% for style in styles|reverse %}
|
||||||
<link href="{{ style }}" type="text/css" rel="stylesheet">
|
<link href="{{ style }}" type="text/css" rel="stylesheet">
|
||||||
|
|
Loading…
Reference in a new issue