From f2f023cf696fb7dce51cc8fe690eeee231b0ed26 Mon Sep 17 00:00:00 2001 From: trendschau Date: Mon, 27 Feb 2023 14:47:49 +0100 Subject: [PATCH] restored status tm2 27.02.2023 --- system/Controllers/ControllerSettings.php | 2 +- system/author/css/a11y-dark.min.css | 7 + system/author/js/highlight.min.js | 709 ++++++++++++++++++ system/typemill/Controllers/Controller.php | 72 ++ .../Controllers/ControllerApiGlobals.php | 2 +- .../Controllers/ControllerApiImage.php | 335 +++++++++ .../Controllers/ControllerApiMediaOld.php | 663 ++++++++++++++++ .../ControllerApiSystemLicense.php | 59 ++ .../ControllerApiSystemPlugins.php | 26 +- .../ControllerApiSystemSettings.php | 33 +- .../Controllers/ControllerApiSystemThemes.php | 25 +- .../Controllers/ControllerApiSystemUsers.php | 362 ++++----- .../typemill/Controllers/ControllerData.php | 71 +- ...llerWebLogin.php => ControllerWebAuth.php} | 52 +- .../Controllers/ControllerWebSystem.php | 398 +++------- .../typemill/Middleware/ApiAuthentication.php | 144 ++++ .../typemill/Middleware/ApiAuthorization.php | 39 + .../typemill/Middleware/RestrictApiAccess.php | 81 -- .../typemill/Middleware/WebAuthorization.php | 34 + ...ted.php => WebRedirectIfAuthenticated.php} | 7 +- ...d.php => WebRedirectIfUnauthenticated.php} | 26 +- system/typemill/Models/License.php | 258 +++++++ system/typemill/Models/ProcessAssets.php | 243 ++++++ system/typemill/Models/ProcessFile.php | 40 + system/typemill/Models/ProcessImage.php | 585 +++++++++++++++ system/typemill/Models/Storage.php | 198 ++++- system/typemill/Models/User.php | 220 +----- system/typemill/Models/Validation.php | 177 +++-- system/typemill/Models/Yaml.php | 2 + system/typemill/Static/License.php | 229 ++++++ system/typemill/Static/Plugins.php | 21 + system/typemill/Static/Settings.php | 64 +- system/typemill/Static/Slug.php | 45 ++ system/typemill/Static/Translations.php | 12 +- system/typemill/author/css/a11y-dark.min.css | 7 + system/typemill/author/css/custom.css | 105 +++ system/typemill/author/css/output.css | 294 ++++++-- system/typemill/author/js/codejar.js | 449 ----------- system/typemill/author/js/highlight.min.js | 709 ++++++++++++++++++ system/typemill/author/js/vue-license.js | 131 ++++ system/typemill/author/js/vue-plugins.js | 88 ++- system/typemill/author/js/vue-shared.js | 359 ++++++--- system/typemill/author/js/vue-system.js | 382 +--------- system/typemill/author/js/vue-user.js | 136 ++++ .../js/{vue-account.js => vue-usernew.js} | 61 +- system/typemill/author/js/vue-users.js | 154 +--- .../typemill/author/layouts/layoutSystem.twig | 6 +- .../typemill/author/partials/systemNavi.twig | 4 +- system/typemill/author/system/account.twig | 2 +- system/typemill/author/system/license.twig | 27 + system/typemill/author/system/plugins.twig | 6 +- system/typemill/author/system/system.twig | 1 + system/typemill/author/system/themes.twig | 1 + system/typemill/author/system/user.twig | 21 + system/typemill/author/system/usernew.twig | 21 + system/typemill/routes/api.php | 43 +- system/typemill/routes/web.php | 50 +- system/typemill/settings/license.yaml | 15 + system/typemill/settings/mainnavi.yaml | 8 +- system/typemill/settings/permissions.yaml | 2 +- system/typemill/settings/resources.yaml | 2 +- system/typemill/settings/system.yaml | 83 +- system/typemill/settings/systemnavi.yaml | 10 +- system/typemill/system.php | 31 +- tailwind.config.js | 5 +- themes/cyanine/cyanine.yaml | 16 +- 66 files changed, 6237 insertions(+), 2233 deletions(-) create mode 100644 system/author/css/a11y-dark.min.css create mode 100644 system/author/js/highlight.min.js create mode 100644 system/typemill/Controllers/ControllerApiImage.php create mode 100644 system/typemill/Controllers/ControllerApiMediaOld.php create mode 100644 system/typemill/Controllers/ControllerApiSystemLicense.php rename system/typemill/Controllers/{ControllerWebLogin.php => ControllerWebAuth.php} (73%) create mode 100644 system/typemill/Middleware/ApiAuthentication.php create mode 100644 system/typemill/Middleware/ApiAuthorization.php delete mode 100644 system/typemill/Middleware/RestrictApiAccess.php create mode 100644 system/typemill/Middleware/WebAuthorization.php rename system/typemill/Middleware/{RedirectIfAuthenticated.php => WebRedirectIfAuthenticated.php} (82%) rename system/typemill/Middleware/{RedirectIfUnauthenticated.php => WebRedirectIfUnauthenticated.php} (67%) create mode 100644 system/typemill/Models/License.php create mode 100644 system/typemill/Models/ProcessAssets.php create mode 100644 system/typemill/Models/ProcessFile.php create mode 100644 system/typemill/Models/ProcessImage.php create mode 100644 system/typemill/Static/License.php create mode 100644 system/typemill/Static/Slug.php create mode 100644 system/typemill/author/css/a11y-dark.min.css delete mode 100644 system/typemill/author/js/codejar.js create mode 100644 system/typemill/author/js/highlight.min.js create mode 100644 system/typemill/author/js/vue-license.js create mode 100644 system/typemill/author/js/vue-user.js rename system/typemill/author/js/{vue-account.js => vue-usernew.js} (64%) create mode 100644 system/typemill/author/system/license.twig create mode 100644 system/typemill/author/system/user.twig create mode 100644 system/typemill/author/system/usernew.twig create mode 100644 system/typemill/settings/license.yaml diff --git a/system/Controllers/ControllerSettings.php b/system/Controllers/ControllerSettings.php index 73873c9..9e43199 100644 --- a/system/Controllers/ControllerSettings.php +++ b/system/Controllers/ControllerSettings.php @@ -856,7 +856,7 @@ class ControllerSettings extends ControllerShared $userdata['userrole'] = $_SESSION['role']; } - # validate standard fields for users + # validate standard fields for users if($validate->existingUser($userdata, $userroles)) { # validate custom input fields and return images diff --git a/system/author/css/a11y-dark.min.css b/system/author/css/a11y-dark.min.css new file mode 100644 index 0000000..7820d7d --- /dev/null +++ b/system/author/css/a11y-dark.min.css @@ -0,0 +1,7 @@ +pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*! + Theme: a11y-dark + Author: @ericwbailey + Maintainer: @ericwbailey + + Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css +*/.hljs{background:#2b2b2b;color:#f8f8f2}.hljs-comment,.hljs-quote{color:#d4d0ab}.hljs-deletion,.hljs-name,.hljs-regexp,.hljs-selector-class,.hljs-selector-id,.hljs-tag,.hljs-template-variable,.hljs-variable{color:#ffa07a}.hljs-built_in,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-type{color:#f5ab35}.hljs-attribute{color:gold}.hljs-addition,.hljs-bullet,.hljs-string,.hljs-symbol{color:#abe338}.hljs-section,.hljs-title{color:#00e0e0}.hljs-keyword,.hljs-selector-tag{color:#dcc6e0}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}@media screen and (-ms-high-contrast:active){.hljs-addition,.hljs-attribute,.hljs-built_in,.hljs-bullet,.hljs-comment,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-quote,.hljs-string,.hljs-symbol,.hljs-type{color:highlight}.hljs-keyword,.hljs-selector-tag{font-weight:700}} \ No newline at end of file diff --git a/system/author/js/highlight.min.js b/system/author/js/highlight.min.js new file mode 100644 index 0000000..476704d --- /dev/null +++ b/system/author/js/highlight.min.js @@ -0,0 +1,709 @@ +/*! + Highlight.js v11.7.0 (git: 82688fad18) + (c) 2006-2022 undefined and other contributors + License: BSD-3-Clause + */ +var hljs=function(){"use strict";var e={exports:{}};function t(e){ +return e instanceof Map?e.clear=e.delete=e.set=()=>{ +throw Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=()=>{ +throw Error("set is read-only") +}),Object.freeze(e),Object.getOwnPropertyNames(e).forEach((n=>{var i=e[n] +;"object"!=typeof i||Object.isFrozen(i)||t(i)})),e} +e.exports=t,e.exports.default=t;class n{constructor(e){ +void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} +ignoreMatch(){this.isMatchIgnored=!0}}function i(e){ +return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") +}function r(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t] +;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n} +const s=e=>!!e.scope||e.sublanguage&&e.language;class o{constructor(e,t){ +this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){ +this.buffer+=i(e)}openNode(e){if(!s(e))return;let t="" +;t=e.sublanguage?"language-"+e.language:((e,{prefix:t})=>{if(e.includes(".")){ +const n=e.split(".") +;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ") +}return`${t}${e}`})(e.scope,{prefix:this.classPrefix}),this.span(t)} +closeNode(e){s(e)&&(this.buffer+="")}value(){return this.buffer}span(e){ +this.buffer+=``}}const a=(e={})=>{const t={children:[]} +;return Object.assign(t,e),t};class c{constructor(){ +this.rootNode=a(),this.stack=[this.rootNode]}get top(){ +return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ +this.top.children.push(e)}openNode(e){const t=a({scope:e}) +;this.add(t),this.stack.push(t)}closeNode(){ +if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ +for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} +walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){ +return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t), +t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){ +"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ +c._collapse(e)})))}}class l extends c{constructor(e){super(),this.options=e} +addKeyword(e,t){""!==e&&(this.openNode(t),this.addText(e),this.closeNode())} +addText(e){""!==e&&this.add(e)}addSublanguage(e,t){const n=e.root +;n.sublanguage=!0,n.language=t,this.add(n)}toHTML(){ +return new o(this,this.options).value()}finalize(){return!0}}function g(e){ +return e?"string"==typeof e?e:e.source:null}function d(e){return p("(?=",e,")")} +function u(e){return p("(?:",e,")*")}function h(e){return p("(?:",e,")?")} +function p(...e){return e.map((e=>g(e))).join("")}function f(...e){const t=(e=>{ +const t=e[e.length-1] +;return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{} +})(e);return"("+(t.capture?"":"?:")+e.map((e=>g(e))).join("|")+")"} +function b(e){return RegExp(e.toString()+"|").exec("").length-1} +const m=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ +;function E(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n +;let i=g(e),r="";for(;i.length>0;){const e=m.exec(i);if(!e){r+=i;break} +r+=i.substring(0,e.index), +i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?r+="\\"+(Number(e[1])+t):(r+=e[0], +"("===e[0]&&n++)}return r})).map((e=>`(${e})`)).join(t)} +const x="[a-zA-Z]\\w*",w="[a-zA-Z_]\\w*",y="\\b\\d+(\\.\\d+)?",_="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",O="\\b(0b[01]+)",v={ +begin:"\\\\[\\s\\S]",relevance:0},N={scope:"string",begin:"'",end:"'", +illegal:"\\n",contains:[v]},k={scope:"string",begin:'"',end:'"',illegal:"\\n", +contains:[v]},M=(e,t,n={})=>{const i=r({scope:"comment",begin:e,end:t, +contains:[]},n);i.contains.push({scope:"doctag", +begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", +end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) +;const s=f("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) +;return i.contains.push({begin:p(/[ ]+/,"(",s,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i +},S=M("//","$"),R=M("/\\*","\\*/"),j=M("#","$");var A=Object.freeze({ +__proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:x,UNDERSCORE_IDENT_RE:w, +NUMBER_RE:y,C_NUMBER_RE:_,BINARY_NUMBER_RE:O, +RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", +SHEBANG:(e={})=>{const t=/^#![ ]*\// +;return e.binary&&(e.begin=p(t,/.*\b/,e.binary,/\b.*/)),r({scope:"meta",begin:t, +end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)}, +BACKSLASH_ESCAPE:v,APOS_STRING_MODE:N,QUOTE_STRING_MODE:k,PHRASAL_WORDS_MODE:{ +begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ +},COMMENT:M,C_LINE_COMMENT_MODE:S,C_BLOCK_COMMENT_MODE:R,HASH_COMMENT_MODE:j, +NUMBER_MODE:{scope:"number",begin:y,relevance:0},C_NUMBER_MODE:{scope:"number", +begin:_,relevance:0},BINARY_NUMBER_MODE:{scope:"number",begin:O,relevance:0}, +REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{scope:"regexp",begin:/\//, +end:/\/[gimuy]*/,illegal:/\n/,contains:[v,{begin:/\[/,end:/\]/,relevance:0, +contains:[v]}]}]},TITLE_MODE:{scope:"title",begin:x,relevance:0}, +UNDERSCORE_TITLE_MODE:{scope:"title",begin:w,relevance:0},METHOD_GUARD:{ +begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0},END_SAME_AS_BEGIN:e=>Object.assign(e,{ +"on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{ +t.data._beginMatch!==e[1]&&t.ignoreMatch()}})});function I(e,t){ +"."===e.input[e.index-1]&&t.ignoreMatch()}function T(e,t){ +void 0!==e.className&&(e.scope=e.className,delete e.className)}function L(e,t){ +t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", +e.__beforeBegin=I,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, +void 0===e.relevance&&(e.relevance=0))}function B(e,t){ +Array.isArray(e.illegal)&&(e.illegal=f(...e.illegal))}function D(e,t){ +if(e.match){ +if(e.begin||e.end)throw Error("begin & end are not supported with match") +;e.begin=e.match,delete e.match}}function H(e,t){ +void 0===e.relevance&&(e.relevance=1)}const P=(e,t)=>{if(!e.beforeMatch)return +;if(e.starts)throw Error("beforeMatch cannot be used with starts") +;const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t] +})),e.keywords=n.keywords,e.begin=p(n.beforeMatch,d(n.begin)),e.starts={ +relevance:0,contains:[Object.assign(n,{endsParent:!0})] +},e.relevance=0,delete n.beforeMatch +},C=["of","and","for","in","not","or","if","then","parent","list","value"] +;function $(e,t,n="keyword"){const i=Object.create(null) +;return"string"==typeof e?r(n,e.split(" ")):Array.isArray(e)?r(n,e):Object.keys(e).forEach((n=>{ +Object.assign(i,$(e[n],t,n))})),i;function r(e,n){ +t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|") +;i[n[0]]=[e,U(n[0],n[1])]}))}}function U(e,t){ +return t?Number(t):(e=>C.includes(e.toLowerCase()))(e)?0:1}const z={},K=e=>{ +console.error(e)},W=(e,...t)=>{console.log("WARN: "+e,...t)},X=(e,t)=>{ +z[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),z[`${e}/${t}`]=!0) +},G=Error();function Z(e,t,{key:n}){let i=0;const r=e[n],s={},o={} +;for(let e=1;e<=t.length;e++)o[e+i]=r[e],s[e+i]=!0,i+=b(t[e-1]) +;e[n]=o,e[n]._emit=s,e[n]._multi=!0}function F(e){(e=>{ +e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, +delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ +_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope +}),(e=>{if(Array.isArray(e.begin)){ +if(e.skip||e.excludeBegin||e.returnBegin)throw K("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), +G +;if("object"!=typeof e.beginScope||null===e.beginScope)throw K("beginScope must be object"), +G;Z(e,e.begin,{key:"beginScope"}),e.begin=E(e.begin,{joinWith:""})}})(e),(e=>{ +if(Array.isArray(e.end)){ +if(e.skip||e.excludeEnd||e.returnEnd)throw K("skip, excludeEnd, returnEnd not compatible with endScope: {}"), +G +;if("object"!=typeof e.endScope||null===e.endScope)throw K("endScope must be object"), +G;Z(e,e.end,{key:"endScope"}),e.end=E(e.end,{joinWith:""})}})(e)}function V(e){ +function t(t,n){ +return RegExp(g(t),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(n?"g":"")) +}class n{constructor(){ +this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} +addRule(e,t){ +t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]), +this.matchAt+=b(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) +;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(E(e,{joinWith:"|" +}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex +;const t=this.matcherRe.exec(e);if(!t)return null +;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n] +;return t.splice(0,n),Object.assign(t,i)}}class i{constructor(){ +this.rules=[],this.multiRegexes=[], +this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ +if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n +;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))), +t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){ +return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){ +this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){ +const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex +;let n=t.exec(e) +;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{ +const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)} +return n&&(this.regexIndex+=n.position+1, +this.regexIndex===this.count&&this.considerAll()),n}} +if(e.compilerExtensions||(e.compilerExtensions=[]), +e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") +;return e.classNameAliases=r(e.classNameAliases||{}),function n(s,o){const a=s +;if(s.isCompiled)return a +;[T,D,F,P].forEach((e=>e(s,o))),e.compilerExtensions.forEach((e=>e(s,o))), +s.__beforeBegin=null,[L,B,H].forEach((e=>e(s,o))),s.isCompiled=!0;let c=null +;return"object"==typeof s.keywords&&s.keywords.$pattern&&(s.keywords=Object.assign({},s.keywords), +c=s.keywords.$pattern, +delete s.keywords.$pattern),c=c||/\w+/,s.keywords&&(s.keywords=$(s.keywords,e.case_insensitive)), +a.keywordPatternRe=t(c,!0), +o&&(s.begin||(s.begin=/\B|\b/),a.beginRe=t(a.begin),s.end||s.endsWithParent||(s.end=/\B|\b/), +s.end&&(a.endRe=t(a.end)), +a.terminatorEnd=g(a.end)||"",s.endsWithParent&&o.terminatorEnd&&(a.terminatorEnd+=(s.end?"|":"")+o.terminatorEnd)), +s.illegal&&(a.illegalRe=t(s.illegal)), +s.contains||(s.contains=[]),s.contains=[].concat(...s.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>r(e,{ +variants:null},t)))),e.cachedVariants?e.cachedVariants:q(e)?r(e,{ +starts:e.starts?r(e.starts):null +}):Object.isFrozen(e)?r(e):e))("self"===e?s:e)))),s.contains.forEach((e=>{n(e,a) +})),s.starts&&n(s.starts,o),a.matcher=(e=>{const t=new i +;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin" +}))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end" +}),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(a),a}(e)}function q(e){ +return!!e&&(e.endsWithParent||q(e.starts))}class J extends Error{ +constructor(e,t){super(e),this.name="HTMLInjectionError",this.html=t}} +const Y=i,Q=r,ee=Symbol("nomatch");var te=(t=>{ +const i=Object.create(null),r=Object.create(null),s=[];let o=!0 +;const a="Could not find the language '{}', did you forget to load/include a language module?",c={ +disableAutodetect:!0,name:"Plain text",contains:[]};let g={ +ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, +languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", +cssSelector:"pre code",languages:null,__emitter:l};function b(e){ +return g.noHighlightRe.test(e)}function m(e,t,n){let i="",r="" +;"object"==typeof t?(i=e, +n=t.ignoreIllegals,r=t.language):(X("10.7.0","highlight(lang, code, ...args) has been deprecated."), +X("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), +r=e,i=t),void 0===n&&(n=!0);const s={code:i,language:r};k("before:highlight",s) +;const o=s.result?s.result:E(s.language,s.code,n) +;return o.code=s.code,k("after:highlight",o),o}function E(e,t,r,s){ +const c=Object.create(null);function l(){if(!N.keywords)return void M.addText(S) +;let e=0;N.keywordPatternRe.lastIndex=0;let t=N.keywordPatternRe.exec(S),n="" +;for(;t;){n+=S.substring(e,t.index) +;const r=y.case_insensitive?t[0].toLowerCase():t[0],s=(i=r,N.keywords[i]);if(s){ +const[e,i]=s +;if(M.addText(n),n="",c[r]=(c[r]||0)+1,c[r]<=7&&(R+=i),e.startsWith("_"))n+=t[0];else{ +const n=y.classNameAliases[e]||e;M.addKeyword(t[0],n)}}else n+=t[0] +;e=N.keywordPatternRe.lastIndex,t=N.keywordPatternRe.exec(S)}var i +;n+=S.substring(e),M.addText(n)}function d(){null!=N.subLanguage?(()=>{ +if(""===S)return;let e=null;if("string"==typeof N.subLanguage){ +if(!i[N.subLanguage])return void M.addText(S) +;e=E(N.subLanguage,S,!0,k[N.subLanguage]),k[N.subLanguage]=e._top +}else e=x(S,N.subLanguage.length?N.subLanguage:null) +;N.relevance>0&&(R+=e.relevance),M.addSublanguage(e._emitter,e.language) +})():l(),S=""}function u(e,t){let n=1;const i=t.length-1;for(;n<=i;){ +if(!e._emit[n]){n++;continue}const i=y.classNameAliases[e[n]]||e[n],r=t[n] +;i?M.addKeyword(r,i):(S=r,l(),S=""),n++}}function h(e,t){ +return e.scope&&"string"==typeof e.scope&&M.openNode(y.classNameAliases[e.scope]||e.scope), +e.beginScope&&(e.beginScope._wrap?(M.addKeyword(S,y.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), +S=""):e.beginScope._multi&&(u(e.beginScope,t),S="")),N=Object.create(e,{parent:{ +value:N}}),N}function p(e,t,i){let r=((e,t)=>{const n=e&&e.exec(t) +;return n&&0===n.index})(e.endRe,i);if(r){if(e["on:end"]){const i=new n(e) +;e["on:end"](t,i),i.isMatchIgnored&&(r=!1)}if(r){ +for(;e.endsParent&&e.parent;)e=e.parent;return e}} +if(e.endsWithParent)return p(e.parent,t,i)}function f(e){ +return 0===N.matcher.regexIndex?(S+=e[0],1):(I=!0,0)}function b(e){ +const n=e[0],i=t.substring(e.index),r=p(N,e,i);if(!r)return ee;const s=N +;N.endScope&&N.endScope._wrap?(d(), +M.addKeyword(n,N.endScope._wrap)):N.endScope&&N.endScope._multi?(d(), +u(N.endScope,e)):s.skip?S+=n:(s.returnEnd||s.excludeEnd||(S+=n), +d(),s.excludeEnd&&(S=n));do{ +N.scope&&M.closeNode(),N.skip||N.subLanguage||(R+=N.relevance),N=N.parent +}while(N!==r.parent);return r.starts&&h(r.starts,e),s.returnEnd?0:n.length} +let m={};function w(i,s){const a=s&&s[0];if(S+=i,null==a)return d(),0 +;if("begin"===m.type&&"end"===s.type&&m.index===s.index&&""===a){ +if(S+=t.slice(s.index,s.index+1),!o){const t=Error(`0 width match regex (${e})`) +;throw t.languageName=e,t.badRule=m.rule,t}return 1} +if(m=s,"begin"===s.type)return(e=>{ +const t=e[0],i=e.rule,r=new n(i),s=[i.__beforeBegin,i["on:begin"]] +;for(const n of s)if(n&&(n(e,r),r.isMatchIgnored))return f(t) +;return i.skip?S+=t:(i.excludeBegin&&(S+=t), +d(),i.returnBegin||i.excludeBegin||(S=t)),h(i,e),i.returnBegin?0:t.length})(s) +;if("illegal"===s.type&&!r){ +const e=Error('Illegal lexeme "'+a+'" for mode "'+(N.scope||"")+'"') +;throw e.mode=N,e}if("end"===s.type){const e=b(s);if(e!==ee)return e} +if("illegal"===s.type&&""===a)return 1 +;if(A>1e5&&A>3*s.index)throw Error("potential infinite loop, way more iterations than matches") +;return S+=a,a.length}const y=O(e) +;if(!y)throw K(a.replace("{}",e)),Error('Unknown language: "'+e+'"') +;const _=V(y);let v="",N=s||_;const k={},M=new g.__emitter(g);(()=>{const e=[] +;for(let t=N;t!==y;t=t.parent)t.scope&&e.unshift(t.scope) +;e.forEach((e=>M.openNode(e)))})();let S="",R=0,j=0,A=0,I=!1;try{ +for(N.matcher.considerAll();;){ +A++,I?I=!1:N.matcher.considerAll(),N.matcher.lastIndex=j +;const e=N.matcher.exec(t);if(!e)break;const n=w(t.substring(j,e.index),e) +;j=e.index+n} +return w(t.substring(j)),M.closeAllNodes(),M.finalize(),v=M.toHTML(),{ +language:e,value:v,relevance:R,illegal:!1,_emitter:M,_top:N}}catch(n){ +if(n.message&&n.message.includes("Illegal"))return{language:e,value:Y(t), +illegal:!0,relevance:0,_illegalBy:{message:n.message,index:j, +context:t.slice(j-100,j+100),mode:n.mode,resultSoFar:v},_emitter:M};if(o)return{ +language:e,value:Y(t),illegal:!1,relevance:0,errorRaised:n,_emitter:M,_top:N} +;throw n}}function x(e,t){t=t||g.languages||Object.keys(i);const n=(e=>{ +const t={value:Y(e),illegal:!1,relevance:0,_top:c,_emitter:new g.__emitter(g)} +;return t._emitter.addText(e),t})(e),r=t.filter(O).filter(N).map((t=>E(t,e,!1))) +;r.unshift(n);const s=r.sort(((e,t)=>{ +if(e.relevance!==t.relevance)return t.relevance-e.relevance +;if(e.language&&t.language){if(O(e.language).supersetOf===t.language)return 1 +;if(O(t.language).supersetOf===e.language)return-1}return 0})),[o,a]=s,l=o +;return l.secondBest=a,l}function w(e){let t=null;const n=(e=>{ +let t=e.className+" ";t+=e.parentNode?e.parentNode.className:"" +;const n=g.languageDetectRe.exec(t);if(n){const t=O(n[1]) +;return t||(W(a.replace("{}",n[1])), +W("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"} +return t.split(/\s+/).find((e=>b(e)||O(e)))})(e);if(b(n))return +;if(k("before:highlightElement",{el:e,language:n +}),e.children.length>0&&(g.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), +console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), +console.warn("The element with unescaped HTML:"), +console.warn(e)),g.throwUnescapedHTML))throw new J("One of your code blocks includes unescaped HTML.",e.innerHTML) +;t=e;const i=t.textContent,s=n?m(i,{language:n,ignoreIllegals:!0}):x(i) +;e.innerHTML=s.value,((e,t,n)=>{const i=t&&r[t]||n +;e.classList.add("hljs"),e.classList.add("language-"+i) +})(e,n,s.language),e.result={language:s.language,re:s.relevance, +relevance:s.relevance},s.secondBest&&(e.secondBest={ +language:s.secondBest.language,relevance:s.secondBest.relevance +}),k("after:highlightElement",{el:e,result:s,text:i})}let y=!1;function _(){ +"loading"!==document.readyState?document.querySelectorAll(g.cssSelector).forEach(w):y=!0 +}function O(e){return e=(e||"").toLowerCase(),i[e]||i[r[e]]} +function v(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ +r[e.toLowerCase()]=t}))}function N(e){const t=O(e) +;return t&&!t.disableAutodetect}function k(e,t){const n=e;s.forEach((e=>{ +e[n]&&e[n](t)}))} +"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ +y&&_()}),!1),Object.assign(t,{highlight:m,highlightAuto:x,highlightAll:_, +highlightElement:w, +highlightBlock:e=>(X("10.7.0","highlightBlock will be removed entirely in v12.0"), +X("10.7.0","Please use highlightElement now."),w(e)),configure:e=>{g=Q(g,e)}, +initHighlighting:()=>{ +_(),X("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, +initHighlightingOnLoad:()=>{ +_(),X("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") +},registerLanguage:(e,n)=>{let r=null;try{r=n(t)}catch(t){ +if(K("Language definition for '{}' could not be registered.".replace("{}",e)), +!o)throw t;K(t),r=c} +r.name||(r.name=e),i[e]=r,r.rawDefinition=n.bind(null,t),r.aliases&&v(r.aliases,{ +languageName:e})},unregisterLanguage:e=>{delete i[e] +;for(const t of Object.keys(r))r[t]===e&&delete r[t]}, +listLanguages:()=>Object.keys(i),getLanguage:O,registerAliases:v, +autoDetection:N,inherit:Q,addPlugin:e=>{(e=>{ +e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{ +e["before:highlightBlock"](Object.assign({block:t.el},t)) +}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{ +e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),s.push(e)} +}),t.debugMode=()=>{o=!1},t.safeMode=()=>{o=!0 +},t.versionString="11.7.0",t.regex={concat:p,lookahead:d,either:f,optional:h, +anyNumberOfTimes:u};for(const t in A)"object"==typeof A[t]&&e.exports(A[t]) +;return Object.assign(t,A),t})({});return te}() +;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);/*! `javascript` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict" +;const e="[A-Za-z$_][0-9A-Za-z$_]*",n=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],a=["true","false","null","undefined","NaN","Infinity"],t=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],s=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],r=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],c=["arguments","this","super","console","window","document","localStorage","module","global"],i=[].concat(r,t,s) +;return o=>{const l=o.regex,b=e,d={begin:/<[A-Za-z0-9\\._:-]+/, +end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ +const a=e[0].length+e.index,t=e.input[a] +;if("<"===t||","===t)return void n.ignoreMatch();let s +;">"===t&&(((e,{after:n})=>{const a="",M={ +match:[/const|var|let/,/\s+/,b,/\s*/,/=\s*/,/(async\s*)?/,l.lookahead(C)], +keywords:"async",className:{1:"keyword",3:"title.function"},contains:[S]} +;return{name:"Javascript",aliases:["js","jsx","mjs","cjs"],keywords:g,exports:{ +PARAMS_CONTAINS:p,CLASS_REFERENCE:R},illegal:/#(?![$_A-z])/, +contains:[o.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ +label:"use_strict",className:"meta",relevance:10, +begin:/^\s*['"]use (strict|asm)['"]/ +},o.APOS_STRING_MODE,o.QUOTE_STRING_MODE,y,N,_,h,{match:/\$\d+/},E,R,{ +className:"attr",begin:b+l.lookahead(":"),relevance:0},M,{ +begin:"("+o.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", +keywords:"return throw case",relevance:0,contains:[h,o.REGEXP_MODE,{ +className:"function",begin:C,returnBegin:!0,end:"\\s*=>",contains:[{ +className:"params",variants:[{begin:o.UNDERSCORE_IDENT_RE,relevance:0},{ +className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0, +excludeEnd:!0,keywords:g,contains:p}]}]},{begin:/,/,relevance:0},{match:/\s+/, +relevance:0},{variants:[{begin:"<>",end:""},{ +match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:d.begin, +"on:begin":d.isTrulyOpeningTag,end:d.end}],subLanguage:"xml",contains:[{ +begin:d.begin,end:d.end,skip:!0,contains:["self"]}]}]},O,{ +beginKeywords:"while if switch catch for"},{ +begin:"\\b(?!function)"+o.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", +returnBegin:!0,label:"func.def",contains:[S,o.inherit(o.TITLE_MODE,{begin:b, +className:"title.function"})]},{match:/\.\.\./,relevance:0},x,{match:"\\$"+b, +relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, +contains:[S]},k,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},w,T,{match:/\$[(.]/}]}}})() +;hljs.registerLanguage("javascript",e)})();/*! `xml` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const a=e.regex,n=a.concat(/[\p{L}_]/u,a.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),s={ +className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},t={begin:/\s/, +contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}] +},i=e.inherit(t,{begin:/\(/,end:/\)/}),c=e.inherit(e.APOS_STRING_MODE,{ +className:"string"}),l=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),r={ +endsWithParent:!0,illegal:/`]+/}]}]}]};return{ +name:"HTML, XML", +aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"], +case_insensitive:!0,unicodeRegex:!0,contains:[{className:"meta",begin://,relevance:10,contains:[t,l,c,i,{begin:/\[/,end:/\]/,contains:[{ +className:"meta",begin://,contains:[t,i,l,c]}]}] +},e.COMMENT(//,{relevance:10}),{begin://, +relevance:10},s,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/, +relevance:10,contains:[l]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag", +begin:/)/,end:/>/,keywords:{name:"style"},contains:[r],starts:{ +end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag", +begin:/)/,end:/>/,keywords:{name:"script"},contains:[r],starts:{ +end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{ +className:"tag",begin:/<>|<\/>/},{className:"tag", +begin:a.concat(//,/>/,/\s/)))), +end:/\/?>/,contains:[{className:"name",begin:n,relevance:0,starts:r}]},{ +className:"tag",begin:a.concat(/<\//,a.lookahead(a.concat(n,/>/))),contains:[{ +className:"name",begin:n,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}} +})();hljs.registerLanguage("xml",e)})();/*! `twig` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const a=e.regex,t=["absolute_url","asset|0","asset_version","attribute","block","constant","controller|0","country_timezones","csrf_token","cycle","date","dump","expression","form|0","form_end","form_errors","form_help","form_label","form_rest","form_row","form_start","form_widget","html_classes","include","is_granted","logout_path","logout_url","max","min","parent","path|0","random","range","relative_path","render","render_esi","source","template_from_string","url|0"] +;let r=["apply","autoescape","block","cache","deprecated","do","embed","extends","filter","flush","for","form_theme","from","if","import","include","macro","sandbox","set","stopwatch","trans","trans_default_domain","transchoice","use","verbatim","with"] +;r=r.concat(r.map((e=>"end"+e)));const n={scope:"string",variants:[{begin:/'/, +end:/'/},{begin:/"/,end:/"/}]},o={scope:"number",match:/\d+/},s={begin:/\(/, +end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:[n,o]},c={ +beginKeywords:t.join(" "),keywords:{name:t},relevance:0,contains:[s]},m={ +match:/\|(?=[A-Za-z_]+:?)/,beginScope:"punctuation",relevance:0,contains:[{ +match:/[A-Za-z_]+:?/, +keywords:["abs","abbr_class","abbr_method","batch","capitalize","column","convert_encoding","country_name","currency_name","currency_symbol","data_uri","date","date_modify","default","escape","file_excerpt","file_link","file_relative","filter","first","format","format_args","format_args_as_text","format_currency","format_date","format_datetime","format_file","format_file_from_text","format_number","format_time","html_to_markdown","humanize","inky_to_html","inline_css","join","json_encode","keys","language_name","last","length","locale_name","lower","map","markdown","markdown_to_html","merge","nl2br","number_format","raw","reduce","replace","reverse","round","slice","slug","sort","spaceless","split","striptags","timezone_name","title","trans","transchoice","trim","u|0","upper","url_encode","yaml_dump","yaml_encode"] +}]},i=(e,{relevance:t})=>({beginScope:{1:"template-tag",3:"name"}, +relevance:t||2,endScope:"template-tag",begin:[/\{%/,/\s*/,a.either(...e)], +end:/%\}/,keywords:"in",contains:[m,c,n,o]}),l=i(r,{relevance:2 +}),_=i([/[a-z_]+/],{relevance:1});return{name:"Twig",aliases:["craftcms"], +case_insensitive:!0,subLanguage:"xml",contains:[e.COMMENT(/\{#/,/#\}/),l,_,{ +className:"template-variable",begin:/\{\{/,end:/\}\}/,contains:["self",m,c,n,o] +}]}}})();hljs.registerLanguage("twig",e)})();/*! `json` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const a=["true","false","null"],n={ +scope:"literal",beginKeywords:a.join(" ")};return{name:"JSON",keywords:{ +literal:a},contains:[{className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/, +relevance:1.01},{match:/[{}[\],:]/,className:"punctuation",relevance:0 +},e.QUOTE_STRING_MODE,n,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE], +illegal:"\\S"}}})();hljs.registerLanguage("json",e)})();/*! `graphql` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const a=e.regex;return{name:"GraphQL", +aliases:["gql"],case_insensitive:!0,disableAutodetect:!1,keywords:{ +keyword:["query","mutation","subscription","type","input","schema","directive","interface","union","scalar","fragment","enum","on"], +literal:["true","false","null"]}, +contains:[e.HASH_COMMENT_MODE,e.QUOTE_STRING_MODE,e.NUMBER_MODE,{ +scope:"punctuation",match:/[.]{3}/,relevance:0},{scope:"punctuation", +begin:/[\!\(\)\:\=\[\]\{\|\}]{1}/,relevance:0},{scope:"variable",begin:/\$/, +end:/\W/,excludeEnd:!0,relevance:0},{scope:"meta",match:/@\w+/,excludeEnd:!0},{ +scope:"symbol",begin:a.concat(/[_A-Za-z][_0-9A-Za-z]*/,a.lookahead(/\s*:/)), +relevance:0}],illegal:[/[;<']/,/BEGIN/]}}})();hljs.registerLanguage("graphql",e) +})();/*! `css` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict" +;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],i=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],r=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],t=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],o=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse() +;return n=>{const a=n.regex,l=(e=>({IMPORTANT:{scope:"meta",begin:"!important"}, +BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number", +begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},FUNCTION_DISPATCH:{ +className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{ +scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", +contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ +scope:"number", +begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", +relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z][A-Za-z0-9_-]*/} +}))(n),s=[n.APOS_STRING_MODE,n.QUOTE_STRING_MODE];return{name:"CSS", +case_insensitive:!0,illegal:/[=|'\$]/,keywords:{keyframePosition:"from to"}, +classNameAliases:{keyframePosition:"selector-tag"},contains:[l.BLOCK_COMMENT,{ +begin:/-(webkit|moz|ms|o)-(?=[a-z])/},l.CSS_NUMBER_MODE,{ +className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0},{ +className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0 +},l.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{ +begin:":("+r.join("|")+")"},{begin:":(:)?("+t.join("|")+")"}]},l.CSS_VARIABLE,{ +className:"attribute",begin:"\\b("+o.join("|")+")\\b"},{begin:/:/,end:/[;}{]/, +contains:[l.BLOCK_COMMENT,l.HEXCOLOR,l.IMPORTANT,l.CSS_NUMBER_MODE,...s,{ +begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri" +},contains:[...s,{className:"string",begin:/[^)]/,endsWithParent:!0, +excludeEnd:!0}]},l.FUNCTION_DISPATCH]},{begin:a.lookahead(/@/),end:"[{;]", +relevance:0,illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/ +},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{ +$pattern:/[a-z-]+/,keyword:"and or not only",attribute:i.join(" ")},contains:[{ +begin:/[a-z-]+(?=:)/,className:"attribute"},...s,l.CSS_NUMBER_MODE]}]},{ +className:"selector-tag",begin:"\\b("+e.join("|")+")\\b"}]}}})() +;hljs.registerLanguage("css",e)})();/*! `plaintext` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var t=(()=>{"use strict";return t=>({name:"Plain text", +aliases:["text","txt"],disableAutodetect:!0})})() +;hljs.registerLanguage("plaintext",t)})();/*! `scss` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict" +;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],r=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],i=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],t=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],o=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse() +;return n=>{const a=(e=>({IMPORTANT:{scope:"meta",begin:"!important"}, +BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number", +begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},FUNCTION_DISPATCH:{ +className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{ +scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", +contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ +scope:"number", +begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", +relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z][A-Za-z0-9_-]*/} +}))(n),l=t,s=i,d="@[a-z-]+",c={className:"variable", +begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b",relevance:0};return{name:"SCSS", +case_insensitive:!0,illegal:"[=/|']", +contains:[n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE,a.CSS_NUMBER_MODE,{ +className:"selector-id",begin:"#[A-Za-z0-9_-]+",relevance:0},{ +className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0 +},a.ATTRIBUTE_SELECTOR_MODE,{className:"selector-tag", +begin:"\\b("+e.join("|")+")\\b",relevance:0},{className:"selector-pseudo", +begin:":("+s.join("|")+")"},{className:"selector-pseudo", +begin:":(:)?("+l.join("|")+")"},c,{begin:/\(/,end:/\)/, +contains:[a.CSS_NUMBER_MODE]},a.CSS_VARIABLE,{className:"attribute", +begin:"\\b("+o.join("|")+")\\b"},{ +begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b" +},{begin:/:/,end:/[;}{]/,relevance:0, +contains:[a.BLOCK_COMMENT,c,a.HEXCOLOR,a.CSS_NUMBER_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,a.IMPORTANT,a.FUNCTION_DISPATCH] +},{begin:"@(page|font-face)",keywords:{$pattern:d,keyword:"@page @font-face"}},{ +begin:"@",end:"[{;]",returnBegin:!0,keywords:{$pattern:/[a-z-]+/, +keyword:"and or not only",attribute:r.join(" ")},contains:[{begin:d, +className:"keyword"},{begin:/[a-z-]+(?=:)/,className:"attribute" +},c,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,a.HEXCOLOR,a.CSS_NUMBER_MODE] +},a.FUNCTION_DISPATCH]}}})();hljs.registerLanguage("scss",e)})();/*! `typescript` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict" +;const e="[A-Za-z$_][0-9A-Za-z$_]*",n=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],a=["true","false","null","undefined","NaN","Infinity"],t=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],s=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],c=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],r=["arguments","this","super","console","window","document","localStorage","module","global"],i=[].concat(c,t,s) +;function o(o){const l=o.regex,d=e,b={begin:/<[A-Za-z0-9\\._:-]+/, +end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ +const a=e[0].length+e.index,t=e.input[a] +;if("<"===t||","===t)return void n.ignoreMatch();let s +;">"===t&&(((e,{after:n})=>{const a="",M={ +match:[/const|var|let/,/\s+/,d,/\s*/,/=\s*/,/(async\s*)?/,l.lookahead(T)], +keywords:"async",className:{1:"keyword",3:"title.function"},contains:[S]} +;return{name:"Javascript",aliases:["js","jsx","mjs","cjs"],keywords:g,exports:{ +PARAMS_CONTAINS:v,CLASS_REFERENCE:R},illegal:/#(?![$_A-z])/, +contains:[o.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ +label:"use_strict",className:"meta",relevance:10, +begin:/^\s*['"]use (strict|asm)['"]/ +},o.APOS_STRING_MODE,o.QUOTE_STRING_MODE,A,p,_,N,{match:/\$\d+/},E,R,{ +className:"attr",begin:d+l.lookahead(":"),relevance:0},M,{ +begin:"("+o.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", +keywords:"return throw case",relevance:0,contains:[N,o.REGEXP_MODE,{ +className:"function",begin:T,returnBegin:!0,end:"\\s*=>",contains:[{ +className:"params",variants:[{begin:o.UNDERSCORE_IDENT_RE,relevance:0},{ +className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0, +excludeEnd:!0,keywords:g,contains:v}]}]},{begin:/,/,relevance:0},{match:/\s+/, +relevance:0},{variants:[{begin:"<>",end:""},{ +match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:b.begin, +"on:begin":b.isTrulyOpeningTag,end:b.end}],subLanguage:"xml",contains:[{ +begin:b.begin,end:b.end,skip:!0,contains:["self"]}]}]},x,{ +beginKeywords:"while if switch catch for"},{ +begin:"\\b(?!function)"+o.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", +returnBegin:!0,label:"func.def",contains:[S,o.inherit(o.TITLE_MODE,{begin:d, +className:"title.function"})]},{match:/\.\.\./,relevance:0},I,{match:"\\$"+d, +relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, +contains:[S]},k,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},w,C,{match:/\$[(.]/}]}}return t=>{ +const s=o(t),c=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],l={ +beginKeywords:"namespace",end:/\{/,excludeEnd:!0, +contains:[s.exports.CLASS_REFERENCE]},d={beginKeywords:"interface",end:/\{/, +excludeEnd:!0,keywords:{keyword:"interface extends",built_in:c}, +contains:[s.exports.CLASS_REFERENCE]},b={$pattern:e, +keyword:n.concat(["type","namespace","interface","public","private","protected","implements","declare","abstract","readonly","enum","override"]), +literal:a,built_in:i.concat(c),"variable.language":r},g={className:"meta", +begin:"@[A-Za-z$_][0-9A-Za-z$_]*"},u=(e,n,a)=>{ +const t=e.contains.findIndex((e=>e.label===n)) +;if(-1===t)throw Error("can not find mode to replace");e.contains.splice(t,1,a)} +;return Object.assign(s.keywords,b), +s.exports.PARAMS_CONTAINS.push(g),s.contains=s.contains.concat([g,l,d]), +u(s,"shebang",t.SHEBANG()),u(s,"use_strict",{className:"meta",relevance:10, +begin:/^\s*['"]use strict['"]/ +}),s.contains.find((e=>"func.def"===e.label)).relevance=0,Object.assign(s,{ +name:"TypeScript",aliases:["ts","tsx"]}),s}})() +;hljs.registerLanguage("typescript",e)})();/*! `ruby` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const n=e.regex,a="([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)",s=n.either(/\b([A-Z]+[a-z0-9]+)+/,/\b([A-Z]+[a-z0-9]+)+[A-Z]+/),i=n.concat(s,/(::\w+)*/),t={ +"variable.constant":["__FILE__","__LINE__","__ENCODING__"], +"variable.language":["self","super"], +keyword:["alias","and","begin","BEGIN","break","case","class","defined","do","else","elsif","end","END","ensure","for","if","in","module","next","not","or","redo","require","rescue","retry","return","then","undef","unless","until","when","while","yield","include","extend","prepend","public","private","protected","raise","throw"], +built_in:["proc","lambda","attr_accessor","attr_reader","attr_writer","define_method","private_constant","module_function"], +literal:["true","false","nil"]},c={className:"doctag",begin:"@[A-Za-z]+"},r={ +begin:"#<",end:">"},b=[e.COMMENT("#","$",{contains:[c] +}),e.COMMENT("^=begin","^=end",{contains:[c],relevance:10 +}),e.COMMENT("^__END__",e.MATCH_NOTHING_RE)],l={className:"subst",begin:/#\{/, +end:/\}/,keywords:t},d={className:"string",contains:[e.BACKSLASH_ESCAPE,l], +variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{ +begin:/%[qQwWx]?\(/,end:/\)/},{begin:/%[qQwWx]?\[/,end:/\]/},{ +begin:/%[qQwWx]?\{/,end:/\}/},{begin:/%[qQwWx]?/},{begin:/%[qQwWx]?\//, +end:/\//},{begin:/%[qQwWx]?%/,end:/%/},{begin:/%[qQwWx]?-/,end:/-/},{ +begin:/%[qQwWx]?\|/,end:/\|/},{begin:/\B\?(\\\d{1,3})/},{ +begin:/\B\?(\\x[A-Fa-f0-9]{1,2})/},{begin:/\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/},{ +begin:/\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/},{ +begin:/\B\?\\(c|C-)[\x20-\x7e]/},{begin:/\B\?\\?\S/},{ +begin:n.concat(/<<[-~]?'?/,n.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)), +contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/, +contains:[e.BACKSLASH_ESCAPE,l]})]}]},o="[0-9](_?[0-9])*",g={className:"number", +relevance:0,variants:[{ +begin:`\\b([1-9](_?[0-9])*|0)(\\.(${o}))?([eE][+-]?(${o})|r)?i?\\b`},{ +begin:"\\b0[dD][0-9](_?[0-9])*r?i?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*r?i?\\b" +},{begin:"\\b0[oO][0-7](_?[0-7])*r?i?\\b"},{ +begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b"},{ +begin:"\\b0(_?[0-7])+r?i?\\b"}]},_={variants:[{match:/\(\)/},{ +className:"params",begin:/\(/,end:/(?=\))/,excludeBegin:!0,endsParent:!0, +keywords:t}]},u=[d,{variants:[{match:[/class\s+/,i,/\s+<\s+/,i]},{ +match:[/\b(class|module)\s+/,i]}],scope:{2:"title.class", +4:"title.class.inherited"},keywords:t},{match:[/(include|extend)\s+/,i],scope:{ +2:"title.class"},keywords:t},{relevance:0,match:[i,/\.new[. (]/],scope:{ +1:"title.class"}},{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},{relevance:0,match:s,scope:"title.class"},{ +match:[/def/,/\s+/,a],scope:{1:"keyword",3:"title.function"},contains:[_]},{ +begin:e.IDENT_RE+"::"},{className:"symbol", +begin:e.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{className:"symbol", +begin:":(?!\\s)",contains:[d,{begin:a}],relevance:0},g,{className:"variable", +begin:"(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])"},{ +className:"params",begin:/\|/,end:/\|/,excludeBegin:!0,excludeEnd:!0, +relevance:0,keywords:t},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*", +keywords:"unless",contains:[{className:"regexp",contains:[e.BACKSLASH_ESCAPE,l], +illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:/%r\{/,end:/\}[a-z]*/},{ +begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[", +end:"\\][a-z]*"}]}].concat(r,b),relevance:0}].concat(r,b) +;l.contains=u,_.contains=u;const m=[{begin:/^\s*=>/,starts:{end:"$",contains:u} +},{className:"meta.prompt", +begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])", +starts:{end:"$",keywords:t,contains:u}}];return b.unshift(r),{name:"Ruby", +aliases:["rb","gemspec","podspec","thor","irb"],keywords:t,illegal:/\/\*/, +contains:[e.SHEBANG({binary:"ruby"})].concat(m).concat(b).concat(u)}}})() +;hljs.registerLanguage("ruby",e)})();/*! `yaml` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const n="true false yes no null",a="[\\w#;/?:@&=+$,.~*'()[\\]]+",s={ +className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/ +},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable", +variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},i=e.inherit(s,{ +variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),l={ +end:",",endsWithParent:!0,excludeEnd:!0,keywords:n,relevance:0},t={begin:/\{/, +end:/\}/,contains:[l],illegal:"\\n",relevance:0},g={begin:"\\[",end:"\\]", +contains:[l],illegal:"\\n",relevance:0},b=[{className:"attr",variants:[{ +begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{ +begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$", +relevance:10},{className:"string", +begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{ +begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0, +relevance:0},{className:"type",begin:"!\\w+!"+a},{className:"type", +begin:"!<"+a+">"},{className:"type",begin:"!"+a},{className:"type",begin:"!!"+a +},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta", +begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)", +relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{ +className:"number", +begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b" +},{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},t,g,s],r=[...b] +;return r.pop(),r.push(i),l.contains=r,{name:"YAML",case_insensitive:!0, +aliases:["yml"],contains:b}}})();hljs.registerLanguage("yaml",e)})();/*! `markdown` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const n={begin:/<\/?[A-Za-z_]/, +end:">",subLanguage:"xml",relevance:0},a={variants:[{begin:/\[.+?\]\[.*?\]/, +relevance:0},{ +begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/, +relevance:2},{ +begin:e.regex.concat(/\[.+?\]\(/,/[A-Za-z][A-Za-z0-9+.-]*/,/:\/\/.*?\)/), +relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{ +begin:/\[.*?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{match:/\[(?=\])/ +},{className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0, +returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)", +excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[", +end:"\\]",excludeBegin:!0,excludeEnd:!0}]},i={className:"strong",contains:[], +variants:[{begin:/_{2}(?!\s)/,end:/_{2}/},{begin:/\*{2}(?!\s)/,end:/\*{2}/}] +},s={className:"emphasis",contains:[],variants:[{begin:/\*(?![*\s])/,end:/\*/},{ +begin:/_(?![_\s])/,end:/_/,relevance:0}]},c=e.inherit(i,{contains:[] +}),t=e.inherit(s,{contains:[]});i.contains.push(t),s.contains.push(c) +;let g=[n,a];return[i,s,c,t].forEach((e=>{e.contains=e.contains.concat(g) +})),g=g.concat(i,s),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{ +className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:g},{ +begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n", +contains:g}]}]},n,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)", +end:"\\s+",excludeEnd:!0},i,s,{className:"quote",begin:"^>\\s+",contains:g, +end:"$"},{className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{ +begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{ +begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))", +contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{ +begin:"^[-\\*]{3,}",end:"$"},a,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{ +className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{ +className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}}})() +;hljs.registerLanguage("markdown",e)})(); \ No newline at end of file diff --git a/system/typemill/Controllers/Controller.php b/system/typemill/Controllers/Controller.php index ea51f0a..91abf15 100644 --- a/system/typemill/Controllers/Controller.php +++ b/system/typemill/Controllers/Controller.php @@ -4,6 +4,7 @@ namespace Typemill\Controllers; use DI\Container; use Slim\Routing\RouteContext; +use Typemill\Models\StorageWrapper; # use Psr\Container\ContainerInterface; # use Typemill\Models\Folder; @@ -30,9 +31,79 @@ abstract class Controller $this->settings = $container->get('settings'); } + protected function settingActive($setting) + { + if(isset($this->settings[$setting]) && $this->settings[$setting]) + { + return true; + } + return false; + } + # move to another place?? + protected function recursiveValidation($validator, array $formdefinitions, $input, $output = []) + { + # loop through form-definitions, ignores everything that is not defined in yaml + foreach($formdefinitions as $fieldname => $fielddefinitions) + { + if(is_array($fielddefinitions) && $fielddefinitions['type'] == 'fieldset') + { + $output = $this->recursiveValidation($validator, $fielddefinitions['fields'], $input, $output); + } + # do not store values for disabled fields + if(isset($fielddefinitions['disabled']) && $fielddefinitions['type']) + { + continue; + } + + if(isset($input[$fieldname])) + { + $fieldvalue = $input[$fieldname]; + + $validationresult = $validator->field($fieldname, $fieldvalue, $fielddefinitions); + + if($validationresult === true) + { + # MOVE THIS TO A SEPARATE FUNCTION SO YOU CAN STORE IMAGES ONLY IF ALL FIELDS SUCCESSFULLY VALIDATED + # images have special treatment, check ProcessImage-Model and ImageApiController + if($fielddefinitions['type'] == 'image') + { + # then check if file is there already: check for name and maybe correct image extension (if quality has been changed) + $storage = new StorageWrapper('\Typemill\Models\Storage'); + $existingImagePath = $storage->checkImage($fieldvalue); + + if($existingImagePath) + { + $fieldvalue = $existingImagePath; + } + else + { + # there is no published image with that name, so check if there is an unpublished image in tmp folder and publish it + $newImagePath = $storage->publishImage($fieldvalue); + if($newImagePath) + { + $fieldvalue = $newImagePath; + } + else + { + $fieldvalue = ''; + } + } + } + + $output[$fieldname] = $fieldvalue; + } + else + { + $this->errors[$fieldname] = $validationresult[$fieldname][0]; + } + } + } + + return $output; + } @@ -324,6 +395,7 @@ abstract class Controller return true; } + public function updateSitemap($ping = false) { $sitemap = '' . "\n"; diff --git a/system/typemill/Controllers/ControllerApiGlobals.php b/system/typemill/Controllers/ControllerApiGlobals.php index 68ae64d..f26ea3d 100644 --- a/system/typemill/Controllers/ControllerApiGlobals.php +++ b/system/typemill/Controllers/ControllerApiGlobals.php @@ -5,7 +5,7 @@ namespace Typemill\Controllers; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; -class ControllerApiGlobal extends ControllerData +class ControllerApiGlobals extends ControllerData { public function getSystemNavi(Request $request, Response $response) { diff --git a/system/typemill/Controllers/ControllerApiImage.php b/system/typemill/Controllers/ControllerApiImage.php new file mode 100644 index 0000000..dc16154 --- /dev/null +++ b/system/typemill/Controllers/ControllerApiImage.php @@ -0,0 +1,335 @@ +getParsedBody(); + + if(!isset($params['image']) OR !isset($params['name'])) + { + $response->getBody()->write(json_encode([ + 'message' => 'Image or name is missing.', + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $img = new ProcessImage(); + + if($this->settingActive('allowsvg')) + { + $img->addAllowedExtension('svg'); + } + + # prepare the image + if(!$img->prepareImage($params['image'], $params['name'])) + { + $response->getBody()->write(json_encode([ + 'message' => $img->errors[0], + 'fullerrors' => $img->errors, + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + # check if image name already exisits in live folder and create an unique name (do not overwrite existing files) + $storage = new StorageWrapper('\Typemill\Models\Storage'); + $uniqueImageName = $storage->createUniqueImageName($img->getFilename(), $img->getExtension()); + $img->setFilename($uniqueImageName); + + # store the original image + if(!$img->storeOriginalToTmp()) + { + $response->getBody()->write(json_encode([ + 'message' => $img->errors[0], + 'fullerrors' => $img->errors, + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + # if image is not resizable (animated gif or svg) + if(!$img->isResizable()) + { + if($img->saveOriginalForAll()) + { + $response->getBody()->write(json_encode([ + 'message' => 'Image saved successfully', + 'name' => 'media/live/' . $img->getFullName(), + ])); + + return $response->withHeader('Content-Type', 'application/json'); + } + + $response->getBody()->write(json_encode([ + 'message' => $img->errors[0], + 'fullerrors' => $img->errors, + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + # for all other image types, check if they should be transformed to webp + if($this->settingActive('convertwebp')) + { + $img->setExtension('webp'); + } + + if(!$img->storeRenditionsToTmp($this->settings['images'])) + { + $response->getBody()->write(json_encode([ + 'message' => $img->errors[0], + 'fullerrors' => $img->errors, + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(500); + } + +/* + if(isset($params['publish']) && $params['publish']) + { + if(!$img->publishImage($img->getFullName())) + { + $response->getBody()->write(json_encode([ + 'message' => $img->errors[0], + 'fullerrors' => $img->errors, + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(500); + } + } +*/ + $response->getBody()->write(json_encode([ + 'message' => 'Image saved successfully', + 'name' => 'media/tmp/' . $img->getFullName(), + ])); + + return $response->withHeader('Content-Type', 'application/json'); + + } + + + + + + + + + + + + + public function publishImage(Request $request, Response $response, $args) + { + $params = $request->getParsedBody(); + + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders()) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + # check the resize modifier in the image markdown, set it to true and delete it from markdown + $noresize = false; + $markdown = isset($params['markdown']) ? $params['markdown'] : false; + + if($markdown && (strlen($markdown) > 9) && (substr($markdown, -9) == '|noresize') ) + { + $noresize = true; + $params['markdown'] = substr($markdown,0,-9); + } + + if($imageProcessor->publishImage($noresize)) + { + $request = $request->withParsedBody($params); + + $block = new ControllerAuthorBlockApi($this->c); + if($params['new']) + { + return $block->addBlock($request, $response, $args); + } + return $block->updateBlock($request, $response, $args); + } + + return $response->withJson(['errors' => 'could not store image to media folder'],500); + } + + + + + public function getMediaLibImages(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParsedBody(); + $this->uri = $request->getUri()->withUserInfo(''); + + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders('images')) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + $imagelist = $imageProcessor->scanMediaFlat(); + + $response->getBody()->write(json_encode([ + 'images' => $imagelist + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(200); + } + + public function getImage(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParsedBody(); + $this->uri = $request->getUri()->withUserInfo(''); + + $this->setStructureDraft(); + + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders('images')) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + $imageDetails = $imageProcessor->getImageDetails($this->params['name'], $this->structureDraft); + + if($imageDetails) + { + return $response->withJson(['image' => $imageDetails]); + } + + return $response->withJson(['errors' => 'Image not found or image name not valid.'], 404); + } + + public function deleteImage(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri()->withUserInfo(''); + + # minimum permission is that user is allowed to delete content + if(!$this->c->acl->isAllowed($_SESSION['role'], 'content', 'delete')) + { + return $response->withJson(array('data' => false, 'errors' => 'You are not allowed to delete images.'), 403); + } + + if(!isset($this->params['name'])) + { + return $response->withJson(['errors' => 'image name is missing'],500); + } + + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders('images')) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + if($imageProcessor->deleteImage($this->params['name'])) + { + return $response->withJson(['errors' => false]); + } + + return $response->withJson(['errors' => 'Oops, looks like we could not delete all sizes of that image.'], 500); + } + + + public function saveVideoImage(Request $request, Response $response, $args) + { + /* get params from call */ + $this->params = $request->getParams(); + $this->uri = $request->getUri()->withUserInfo(''); + $class = false; + + $imageUrl = $this->params['markdown']; + + if(strpos($imageUrl, 'https://www.youtube.com/watch?v=') !== false) + { + $videoID = str_replace('https://www.youtube.com/watch?v=', '', $imageUrl); + $videoID = strpos($videoID, '&') ? substr($videoID, 0, strpos($videoID, '&')) : $videoID; + $class = 'youtube'; + } + if(strpos($imageUrl, 'https://youtu.be/') !== false) + { + $videoID = str_replace('https://youtu.be/', '', $imageUrl); + $videoID = strpos($videoID, '?') ? substr($videoID, 0, strpos($videoID, '?')) : $videoID; + $class = 'youtube'; + } + + if($class == 'youtube') + { + $videoURLmaxres = 'https://i1.ytimg.com/vi/' . $videoID . '/maxresdefault.jpg'; + $videoURL0 = 'https://i1.ytimg.com/vi/' . $videoID . '/0.jpg'; + } + + $ctx = stream_context_create(array( + 'https' => array( + 'timeout' => 1 + ) + ) + ); + + $imageData = @file_get_contents($videoURLmaxres, 0, $ctx); + if($imageData === false) + { + $imageData = @file_get_contents($videoURL0, 0, $ctx); + if($imageData === false) + { + return $response->withJson(array('errors' => 'could not get the video image')); + } + } + + $imageData64 = 'data:image/jpeg;base64,' . base64_encode($imageData); + $desiredSizes = ['live' => ['width' => 560, 'height' => 315]]; + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders()) + { + return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); + } + + $tmpImage = $imageProcessor->createImage($imageData64, $videoID, $desiredSizes); + + if(!$tmpImage) + { + return $response->withJson(array('errors' => 'could not create temporary image')); + } + + $imageUrl = $imageProcessor->publishImage(); + if($imageUrl) + { + $this->params['markdown'] = '![' . $class . '-video](' . $imageUrl . ' "click to load video"){#' . $videoID. ' .' . $class . '}'; + + $request = $request->withParsedBody($this->params); + $block = new ControllerAuthorBlockApi($this->c); + if($this->params['new']) + { + return $block->addBlock($request, $response, $args); + } + return $block->updateBlock($request, $response, $args); + } + + return $response->withJson(array('errors' => 'could not store the preview image')); + } +} diff --git a/system/typemill/Controllers/ControllerApiMediaOld.php b/system/typemill/Controllers/ControllerApiMediaOld.php new file mode 100644 index 0000000..3189b9e --- /dev/null +++ b/system/typemill/Controllers/ControllerApiMediaOld.php @@ -0,0 +1,663 @@ +getParsedBody(); + + $imageProcessor = new ProcessImage($this->settings['images']); + + if(!$imageProcessor->checkFolders('images')) + { + $response->getBody()->write(json_encode([ + 'message' => 'Please check if your media-folder exists and all folders inside are writable.' + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(403); + } + + $imageParts = explode(";base64,", $params['image']); + $imageType = explode("/", $imageParts[0]); + + if(!isset($imageType[1])) + { + $response->getBody()->write(json_encode([ + 'message' => 'We did not find an image type, the file might be corrupted.' + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(403); + } + + $acceptedTypes = [ + 'png' => true, + 'jpg' => true, + 'jpeg' => true, + 'gif' => true, + 'webp' => true, + ]; + + if(isset($this->settings['svg']) && $this->settings['svg']) + { + $acceptedTypes['svg+xml'] = true; + } + + if(!isset($acceptedTypes[$imageType[1]])) + { + $response->getBody()->write(json_encode([ + 'message' => 'The image type is not supported.' + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(403); + } + + $imageResult = $imageProcessor->createImage($params['image'], $params['name'], $this->settings['images']); + + if($imageResult) + { + if(is_array($imageResult) && isset($imageResult['errors'])) + { + return $response->withJson($imageResult,422); + } + + # publish image directly, used for example by image field for meta-tabs + if($params['publish']) + { + $imageProcessor->publishImage(); + } + return $response->withJson(['name' => 'media/live/' . $imageProcessor->getFullName(),'errors' => false]); + } + + $response->getBody()->write(json_encode([ + 'message' => 'could not store image to temporary folder.' + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(403); + } + + public function getMediaLibImages(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParsedBody(); + $this->uri = $request->getUri()->withUserInfo(''); + + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders('images')) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + $imagelist = $imageProcessor->scanMediaFlat(); + + $response->getBody()->write(json_encode([ + 'images' => $imagelist + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(200); + } + + public function getMediaLibFiles(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParsedBody(); + $this->uri = $request->getUri()->withUserInfo(''); + + $fileProcessor = new ProcessFile(); + if(!$fileProcessor->checkFolders()) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + $filelist = $fileProcessor->scanFilesFlat(); + + $response->getBody()->write(json_encode([ + 'files' => $filelist + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(200); + } + + public function getImage(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParsedBody(); + $this->uri = $request->getUri()->withUserInfo(''); + + $this->setStructureDraft(); + + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders('images')) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + $imageDetails = $imageProcessor->getImageDetails($this->params['name'], $this->structureDraft); + + if($imageDetails) + { + return $response->withJson(['image' => $imageDetails]); + } + + return $response->withJson(['errors' => 'Image not found or image name not valid.'], 404); + } + + public function getFile(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri()->withUserInfo(''); + + $this->setStructureDraft(); + + $fileProcessor = new ProcessFile(); + if(!$fileProcessor->checkFolders()) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + $fileDetails = $fileProcessor->getFileDetails($this->params['name'], $this->structureDraft); + + if($fileDetails) + { + return $response->withJson(['file' => $fileDetails]); + } + + return $response->withJson(['errors' => 'file not found or file name invalid'],404); + } + + public function getFileRestrictions(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri()->withUserInfo(''); + $restriction = 'all'; + + $userroles = $this->c->acl->getRoles(); + + if(isset($this->params['filename']) && $this->params['filename'] != '') + { + $writeYaml = new WriteYaml(); + $restrictions = $writeYaml->getYaml('media' . DIRECTORY_SEPARATOR . 'files', 'filerestrictions.yaml'); + if(isset($restrictions[$this->params['filename']])) + { + $restriction = $restrictions[$this->params['filename']]; + } + } + + return $response->withJson(['userroles' => $userroles, 'restriction' => $restriction]); + } + + public function updateFileRestrictions(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri()->withUserInfo(''); + $filename = isset($this->params['filename']) ? $this->params['filename'] : false; + $role = isset($this->params['role']) ? $this->params['role'] : false; + + if(!$filename OR !$role) + { + return $response->withJson(['errors' => ['message' => 'Filename or userrole is missing.']], 422); + } + + $userroles = $this->c->acl->getRoles(); + + if($role != 'all' AND !in_array($role, $userroles)) + { + return $response->withJson(['errors' => ['message' => 'Userrole is unknown.']], 422); + } + + $writeYaml = new WriteYaml(); + $restrictions = $writeYaml->getYaml('media' . DIRECTORY_SEPARATOR . 'files', 'filerestrictions.yaml'); + if(!$restrictions) + { + $restrictions = []; + } + + if($role == 'all') + { + unset($restrictions[$filename]); + } + else + { + $restrictions[$filename] = $role; + } + + $writeYaml->updateYaml('media' . DIRECTORY_SEPARATOR . 'files', 'filerestrictions.yaml', $restrictions); + + return $response->withJson(['restrictions' => $restrictions]); + } + + + public function uploadFile(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri()->withUserInfo(''); + + if (!isset($this->params['file'])) + { + return $response->withJson(['errors' => 'No file found.'],404); + } + + $size = (int) (strlen(rtrim($this->params['file'], '=')) * 3 / 4); + $extension = pathinfo($this->params['name'], PATHINFO_EXTENSION); + $finfo = finfo_open( FILEINFO_MIME_TYPE ); + $mtype = @finfo_file( $finfo, $this->params['file'] ); + finfo_close($finfo); + + if ($size === 0) + { + return $response->withJson(['errors' => 'File is empty.'],422); + } + + # 20 MB (1 byte * 1024 * 1024 * 20 (for 20 MB)) + if ($size > 20971520) + { + return $response->withJson(['errors' => 'File is bigger than 20MB.'],422); + } + + # check extension first + if (!$this->checkAllowedExtensions($extension)) + { + return $response->withJson(['errors' => 'File is not allowed.'],422); + } + + # check mimetype and extension if there is a mimetype. + # in some environments the finfo_file does not work with a base64 string. + if($mtype) + { + if(!$this->checkAllowedMimeTypes($mtype, $extension)) + { + return $response->withJson(['errors' => 'The mime-type or file extension is not allowed.'],422); + } + } + + $fileProcessor = new ProcessFile(); + + if(!$fileProcessor->checkFolders()) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + $fileinfo = $fileProcessor->storeFile($this->params['file'], $this->params['name']); + + if($fileinfo) + { + # if the previous check of the mtype with the base64 string failed, then do it now again with the temporary file + if(!$mtype) + { + $filePath = str_replace('media/files', 'media/tmp', $fileinfo['url']); + $fullPath = $this->settings['rootPath'] . $filePath; + $finfo = finfo_open( FILEINFO_MIME_TYPE ); + $mtype = @finfo_file( $finfo, $fullPath ); + finfo_close($finfo); + + if(!$mtype OR !$this->checkAllowedMimeTypes($mtype, $extension)) + { + $fileProcessor->clearTempFolder(); + + return $response->withJson(['errors' => 'The mime-type is missing, not allowed or does not fit to the file extension.'],422); + } + } + + # publish file directly, used for example by file field for meta-tabs + if(isset($this->params['publish']) && $this->params['publish']) + { + $fileProcessor->publishFile(); + } + + return $response->withJson(['errors' => false, 'info' => $fileinfo]); + } + + return $response->withJson(['errors' => 'could not store file to temporary folder'],500); + } + + public function publishImage(Request $request, Response $response, $args) + { + $params = $request->getParsedBody(); + + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders()) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + # check the resize modifier in the image markdown, set it to true and delete it from markdown + $noresize = false; + $markdown = isset($params['markdown']) ? $params['markdown'] : false; + + if($markdown && (strlen($markdown) > 9) && (substr($markdown, -9) == '|noresize') ) + { + $noresize = true; + $params['markdown'] = substr($markdown,0,-9); + } + + if($imageProcessor->publishImage($noresize)) + { + $request = $request->withParsedBody($params); + + $block = new ControllerAuthorBlockApi($this->c); + if($params['new']) + { + return $block->addBlock($request, $response, $args); + } + return $block->updateBlock($request, $response, $args); + } + + return $response->withJson(['errors' => 'could not store image to media folder'],500); + } + + public function publishFile(Request $request, Response $response, $args) + { + $params = $request->getParsedBody(); + + $fileProcessor = new ProcessFile(); + if(!$fileProcessor->checkFolders()) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + if($fileProcessor->publishFile()) + { + $request = $request->withParsedBody($params); + + $block = new ControllerAuthorBlockApi($this->c); + if($params['new']) + { + return $block->addBlock($request, $response, $args); + } + return $block->updateBlock($request, $response, $args); + } + + return $response->withJson(['errors' => 'could not store file to media folder'],500); + } + + public function deleteImage(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri()->withUserInfo(''); + + # minimum permission is that user is allowed to delete content + if(!$this->c->acl->isAllowed($_SESSION['role'], 'content', 'delete')) + { + return $response->withJson(array('data' => false, 'errors' => 'You are not allowed to delete images.'), 403); + } + + if(!isset($this->params['name'])) + { + return $response->withJson(['errors' => 'image name is missing'],500); + } + + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders('images')) + { + return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500); + } + + if($imageProcessor->deleteImage($this->params['name'])) + { + return $response->withJson(['errors' => false]); + } + + return $response->withJson(['errors' => 'Oops, looks like we could not delete all sizes of that image.'], 500); + } + + public function deleteFile(Request $request, Response $response, $args) + { + # get params from call + $this->params = $request->getParams(); + $this->uri = $request->getUri()->withUserInfo(''); + + # minimum permission is that user is allowed to delete content + if(!$this->c->acl->isAllowed($_SESSION['role'], 'content', 'delete')) + { + return $response->withJson(array('data' => false, 'errors' => 'You are not allowed to delete files.'), 403); + } + + if(!isset($this->params['name'])) + { + return $response->withJson(['errors' => 'file name is missing'],500); + } + + $fileProcessor = new ProcessFile(); + + if($fileProcessor->deleteFile($this->params['name'])) + { + return $response->withJson(['errors' => false]); + } + + return $response->withJson(['errors' => 'could not delete the file'],500); + } + + public function saveVideoImage(Request $request, Response $response, $args) + { + /* get params from call */ + $this->params = $request->getParams(); + $this->uri = $request->getUri()->withUserInfo(''); + $class = false; + + $imageUrl = $this->params['markdown']; + + if(strpos($imageUrl, 'https://www.youtube.com/watch?v=') !== false) + { + $videoID = str_replace('https://www.youtube.com/watch?v=', '', $imageUrl); + $videoID = strpos($videoID, '&') ? substr($videoID, 0, strpos($videoID, '&')) : $videoID; + $class = 'youtube'; + } + if(strpos($imageUrl, 'https://youtu.be/') !== false) + { + $videoID = str_replace('https://youtu.be/', '', $imageUrl); + $videoID = strpos($videoID, '?') ? substr($videoID, 0, strpos($videoID, '?')) : $videoID; + $class = 'youtube'; + } + + if($class == 'youtube') + { + $videoURLmaxres = 'https://i1.ytimg.com/vi/' . $videoID . '/maxresdefault.jpg'; + $videoURL0 = 'https://i1.ytimg.com/vi/' . $videoID . '/0.jpg'; + } + + $ctx = stream_context_create(array( + 'https' => array( + 'timeout' => 1 + ) + ) + ); + + $imageData = @file_get_contents($videoURLmaxres, 0, $ctx); + if($imageData === false) + { + $imageData = @file_get_contents($videoURL0, 0, $ctx); + if($imageData === false) + { + return $response->withJson(array('errors' => 'could not get the video image')); + } + } + + $imageData64 = 'data:image/jpeg;base64,' . base64_encode($imageData); + $desiredSizes = ['live' => ['width' => 560, 'height' => 315]]; + $imageProcessor = new ProcessImage($this->settings['images']); + if(!$imageProcessor->checkFolders()) + { + return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500); + } + + $tmpImage = $imageProcessor->createImage($imageData64, $videoID, $desiredSizes); + + if(!$tmpImage) + { + return $response->withJson(array('errors' => 'could not create temporary image')); + } + + $imageUrl = $imageProcessor->publishImage(); + if($imageUrl) + { + $this->params['markdown'] = '![' . $class . '-video](' . $imageUrl . ' "click to load video"){#' . $videoID. ' .' . $class . '}'; + + $request = $request->withParsedBody($this->params); + $block = new ControllerAuthorBlockApi($this->c); + if($this->params['new']) + { + return $block->addBlock($request, $response, $args); + } + return $block->updateBlock($request, $response, $args); + } + + return $response->withJson(array('errors' => 'could not store the preview image')); + } + + # https://www.sitepoint.com/mime-types-complete-list/ + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types + # https://wiki.selfhtml.org/wiki/MIME-Type/%C3%9Cbersicht + # http://www.mime-type.net/application/x-latex/ + private function getAllowedMtypes() + { + return array( + 'application/vnd.oasis.opendocument.chart' => 'odc', + 'application/vnd.oasis.opendocument.formula' => 'odf', + 'application/vnd.oasis.opendocument.graphics' => 'odg', + 'application/vnd.oasis.opendocument.image' => 'odi', + 'application/vnd.oasis.opendocument.presentation' => 'odp', + 'application/vnd.oasis.opendocument.spreadsheet' => 'ods', + 'application/vnd.oasis.opendocument.text' => 'odt', + 'application/vnd.oasis.opendocument.text-master' => 'odm', + + 'application/powerpoint' => 'ppt', + 'application/mspowerpoint' => ['ppt','ppz','pps','pot'], + 'application/x-mspowerpoint' => 'ppt', + 'application/vnd.ms-powerpoint' => 'ppt', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', + + 'application/x-visio' => ['vsd','vst','msw'], + 'application/vnd.visio' => ['vsd','vst','msw'], + 'application/x-project' => ['mpc','mpt','mpv','mpx'], + 'application/vnd.ms-project' => 'mpp', + + 'application/excel' => ['xla','xlb','xlc','xld','xlk','xll','xlm','xls','xlt','xlv','xlw'], + 'application/msexcel' => ['xls','xla'], + 'application/x-excel' => ['xla','xlb','xlc','xld','xlk','xll','xlm','xls','xlt','xlv','xlw'], + 'application/x-msexcel' => ['xls', 'xla','xlw'], + 'application/vnd.ms-excel' => ['xlb','xlc','xll','xlm','xls','xlw'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', + + 'application/mshelp' => ['hlp','chm'], + 'application/msword' => ['doc','dot'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', + + 'application/vnd.apple.keynote' => 'key', + 'application/vnd.apple.numbers' => 'numbers', + 'application/vnd.apple.pages' => 'pages', + + 'application/x-latex' => ['ltx','latex'], + 'application/pdf' => 'pdf', + + 'application/vnd.amazon.mobi8-ebook' => 'azw3', + 'application/x-mobipocket-ebook' => 'mobi', + 'application/epub+zip' => 'epub', + + 'application/x-gtar' => 'gtar', + 'application/x-tar' => 'tar', + 'application/zip' => 'zip', + 'application/gzip' => 'gz', + 'application/x-gzip' => ['gz', 'gzip'], + 'application/x-compressed' => ['gz','tgz','z','zip'], + 'application/x-zip-compressed' => 'zip', + 'application/vnd.rar' => 'rar', + 'application/x-7z-compressed' => '7z', + + 'application/rtf' => 'rtf', + 'application/x-rtf' => 'rtf', + + 'text/calendar' => 'ics', + 'text/comma-separated-values' => 'csv', + 'text/css' => 'css', + 'text/plain' => 'txt', + 'text/richtext' => 'rtx', + 'text/rtf' => 'rtf', + + 'audio/basic' => ['au','snd'], + 'audio/mpeg' => 'mp3', + 'audio/mp4' => 'mp4', + 'audio/ogg' => 'ogg', + 'audio/wav' => 'wav', + 'audio/x-aiff' => ['aif','aiff','aifc'], + 'audio/x-midi' => ['mid','midi'], + 'audio/x-mpeg' => 'mp2', + 'audio/x-pn-realaudio' => ['ram','ra'], + + 'image/png' => 'png', + 'image/jpeg' => ['jpeg','jpe','jpg'], + 'image/gif' => 'gif', + 'image/tiff' => ['tiff','tif'], + 'image/svg+xml' => 'svg', + 'image/x-icon' => 'ico', + 'image/webp' => 'webp', + + 'video/mpeg' => ['mpeg','mpg','mpe'], + 'video/mp4' => 'mp4', + 'video/ogg' => ['ogg','ogv'], + 'video/quicktime' => ['qt','mov'], + 'video/vnd.vivo' => ['viv','vivo'], + 'video/webm' => 'webm', + 'video/x-msvideo' => 'avi', + 'video/x-sgi-movie' => 'movie', + 'video/3gpp' => '3gp', + ); + } + + protected function checkAllowedMimeTypes($mtype, $extension) + { + $allowedMimes = $this->getAllowedMtypes(); + + if(!isset($allowedMimes[$mtype])) + { + return false; + } + + if( + (is_array($allowedMimes[$mtype]) && !in_array($extension, $allowedMimes[$mtype])) OR + (!is_array($allowedMimes[$mtype]) && $allowedMimes[$mtype] != $extension ) + ) + { + return false; + } + + return true; + } + + protected function checkAllowedExtensions($extension) + { + $mtypes = $this->getAllowedMtypes(); + foreach($mtypes as $mtExtension) + { + if(is_array($mtExtension)) + { + if(in_array($extension, $mtExtension)) + { + return true; + } + } + else + { + if($extension == $mtExtension) + { + return true; + } + } + } + + return false; + } +} diff --git a/system/typemill/Controllers/ControllerApiSystemLicense.php b/system/typemill/Controllers/ControllerApiSystemLicense.php new file mode 100644 index 0000000..72055cc --- /dev/null +++ b/system/typemill/Controllers/ControllerApiSystemLicense.php @@ -0,0 +1,59 @@ +getParsedBody(); + + if(!isset($params['license']) OR !is_array($params['license'])) + { + $response->getBody()->write(json_encode([ + 'message' => 'License data missing.', + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + # validate input + $validate = new Validation(); + $validationresult = $validate->newLicense($params['license']); + if($validationresult !== true) + { + $response->getBody()->write(json_encode([ + 'message' => 'Please correct errors in form.', + 'errors' => $validate->returnFirstValidationErrors($validationresult) + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $license = new License(); + + $licensedata = $license->activateLicense($params['license']); + + if(!$licensedata) + { + $response->getBody()->write(json_encode([ + 'message' => $license->getMessage() + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $response->getBody()->write(json_encode([ + 'message' => 'Licence has been stored', + 'licensedata' => $license->getLicenseData($this->c->get('urlinfo')) + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(200); + } +} \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerApiSystemPlugins.php b/system/typemill/Controllers/ControllerApiSystemPlugins.php index 92e8d9b..14f90dd 100644 --- a/system/typemill/Controllers/ControllerApiSystemPlugins.php +++ b/system/typemill/Controllers/ControllerApiSystemPlugins.php @@ -5,32 +5,24 @@ namespace Typemill\Controllers; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; use Typemill\Models\Validation; -use Typemill\Models\Yaml; +use Typemill\Models\StorageWrapper; use Typemill\Models\User; +use Typemill\Static\Settings; class ControllerApiSystemPlugins extends ControllerData { public function updatePlugin(Request $request, Response $response) { - # minimum permission are admin rights - if(!$this->c->get('acl')->isAllowed($request->getAttribute('userrole'), 'system', 'update')) - { - $response->getBody()->write(json_encode([ - 'message' => 'You are not allowed to update settings.' - ])); - - return $response->withHeader('Content-Type', 'application/json')->withStatus(403); - } - $params = $request->getParsedBody(); $pluginname = $params['plugin']; $plugininput = $params['settings']; - $yaml = new Yaml('\Typemill\Models\Storage'); - $formdefinitions = $yaml->getYaml('plugins' . DIRECTORY_SEPARATOR . $pluginname, $pluginname . '.yaml'); + $storage = new StorageWrapper('\Typemill\Models\Storage'); + $formdefinitions = $storage->getYaml('plugins' . DIRECTORY_SEPARATOR . $pluginname, $pluginname . '.yaml'); + $plugindata = []; # validate input $validator = new Validation(); - $this->recursiveValidation($formdefinitions['forms']['fields'], $plugininput, $validator, $themeOrPlugin = 'plugins', $name = $pluginname); + $validatedOutput = $this->recursiveValidation($validator, $formdefinitions['forms']['fields'], $plugininput); if(!empty($this->errors)) { @@ -42,11 +34,13 @@ class ControllerApiSystemPlugins extends ControllerData return $response->withHeader('Content-Type', 'application/json')->withStatus(400); } + $plugindata['plugins'][$pluginname] = $validatedOutput; + # store updated settings here - $yaml->updateYaml('settings', 'settings.yaml', $this->settings); + $updatedSettings = Settings::updateSettings($plugindata); $response->getBody()->write(json_encode([ - 'message' => 'settings have been saved', + 'message' => 'settings have been saved' ])); return $response->withHeader('Content-Type', 'application/json')->withStatus(200); diff --git a/system/typemill/Controllers/ControllerApiSystemSettings.php b/system/typemill/Controllers/ControllerApiSystemSettings.php index 6d6b11d..a9c4736 100644 --- a/system/typemill/Controllers/ControllerApiSystemSettings.php +++ b/system/typemill/Controllers/ControllerApiSystemSettings.php @@ -5,8 +5,9 @@ namespace Typemill\Controllers; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; use Typemill\Models\Validation; -use Typemill\Models\Yaml; +use Typemill\Models\StorageWrapper; use Typemill\Models\User; +use Typemill\Static\Settings; # how to translate results in API call ??? @@ -16,16 +17,6 @@ class ControllerApiSystemSettings extends ControllerData { public function getSettings(Request $request, Response $response) { - # minimum permission are admin rights - if(!$this->c->get('acl')->isAllowed($request->getAttribute('userrole'), 'system', 'update')) - { - $response->getBody()->write(json_encode([ - 'message' => 'You are not allowed to update settings.' - ])); - - return $response->withHeader('Content-Type', 'application/json')->withStatus(403); - } - $response->getBody()->write(json_encode([ 'settings' => $this->settings ])); @@ -34,25 +25,15 @@ class ControllerApiSystemSettings extends ControllerData } public function updateSettings(Request $request, Response $response) - { - # minimum permission are admin rights - if(!$this->c->get('acl')->isAllowed($request->getAttribute('userrole'), 'system', 'update')) - { - $response->getBody()->write(json_encode([ - 'message' => 'You are not allowed to update settings.' - ])); - - return $response->withHeader('Content-Type', 'application/json')->withStatus(403); - } - + { $params = $request->getParsedBody(); $settingsinput = $params['settings']; - $yaml = new Yaml('\Typemill\Models\Storage'); - $formdefinitions = $yaml->getYaml('system/typemill/settings', 'system.yaml'); + $storage = new StorageWrapper('\Typemill\Models\Storage'); + $formdefinitions = $storage->getYaml('system/typemill/settings', 'system.yaml'); # validate input $validator = new Validation(); - $this->recursiveValidation($formdefinitions, $settingsinput, $validator); + $validatedOutput = $this->recursiveValidation($validator, $formdefinitions, $settingsinput); if(!empty($this->errors)) { @@ -65,7 +46,7 @@ class ControllerApiSystemSettings extends ControllerData } # store updated settings here - $yaml->updateYaml('settings', 'settings.yaml', $this->settings); + $updatedSettings = Settings::updateSettings($validatedOutput); $response->getBody()->write(json_encode([ 'message' => 'settings have been saved', diff --git a/system/typemill/Controllers/ControllerApiSystemThemes.php b/system/typemill/Controllers/ControllerApiSystemThemes.php index b1d5e3f..dcd943e 100644 --- a/system/typemill/Controllers/ControllerApiSystemThemes.php +++ b/system/typemill/Controllers/ControllerApiSystemThemes.php @@ -5,32 +5,25 @@ namespace Typemill\Controllers; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; use Typemill\Models\Validation; -use Typemill\Models\Yaml; +use Typemill\Models\StorageWrapper; use Typemill\Models\User; +use Typemill\Static\Settings; + class ControllerApiSystemThemes extends ControllerData { public function updateTheme(Request $request, Response $response) { - # minimum permission are admin rights - if(!$this->c->get('acl')->isAllowed($request->getAttribute('userrole'), 'system', 'update')) - { - $response->getBody()->write(json_encode([ - 'message' => 'You are not allowed to update settings.' - ])); - - return $response->withHeader('Content-Type', 'application/json')->withStatus(403); - } - $params = $request->getParsedBody(); $themename = $params['theme']; $themeinput = $params['settings']; - $yaml = new Yaml('\Typemill\Models\Storage'); - $formdefinitions = $yaml->getYaml('themes' . DIRECTORY_SEPARATOR . $themename, $themename . '.yaml'); + $storage = new StorageWrapper('\Typemill\Models\Storage'); + $formdefinitions = $storage->getYaml('themes' . DIRECTORY_SEPARATOR . $themename, $themename . '.yaml'); + $themedata = []; # validate input $validator = new Validation(); - $this->recursiveValidation($formdefinitions['forms']['fields'], $themeinput, $validator, $themeOrPlugin = 'themes', $name = $themename); + $validatedOutput = $this->recursiveValidation($validator, $formdefinitions['forms']['fields'], $themeinput); if(!empty($this->errors)) { @@ -42,8 +35,10 @@ class ControllerApiSystemThemes extends ControllerData return $response->withHeader('Content-Type', 'application/json')->withStatus(400); } + $themedata['themes'][$themename] = $validatedOutput; + # store updated settings here - $yaml->updateYaml('settings', 'settings.yaml', $this->settings); + $updatedSettings = Settings::updateSettings($themedata); $response->getBody()->write(json_encode([ 'message' => 'settings have been saved', diff --git a/system/typemill/Controllers/ControllerApiSystemUsers.php b/system/typemill/Controllers/ControllerApiSystemUsers.php index bfdf7f0..7498c56 100644 --- a/system/typemill/Controllers/ControllerApiSystemUsers.php +++ b/system/typemill/Controllers/ControllerApiSystemUsers.php @@ -5,7 +5,6 @@ namespace Typemill\Controllers; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; use Typemill\Models\Validation; -use Typemill\Models\Yaml; use Typemill\Models\User; class ControllerApiSystemUsers extends ControllerData @@ -15,18 +14,8 @@ class ControllerApiSystemUsers extends ControllerData # getUserByName #returns userdata - public function getUsersByNames($request, $response, $args) + public function getUsersByNames(Request $request, Response $response, $args) { - # minimum permission are admin rights - if(!$this->c->get('acl')->isAllowed($request->getAttribute('userrole'), 'system', 'update')) - { - $response->getBody()->write(json_encode([ - 'message' => 'You are not allowed to update settings.' - ])); - - return $response->withHeader('Content-Type', 'application/json')->withStatus(403); - } - $usernames = $request->getQueryParams()['usernames'] ?? false; $user = new User(); $userdata = []; @@ -51,18 +40,8 @@ class ControllerApiSystemUsers extends ControllerData } # returns userdata - public function getUsersByEmail($request, $response, $args) + public function getUsersByEmail(Request $request, Response $response, $args) { - # minimum permission are admin rights - if(!$this->c->get('acl')->isAllowed($request->getAttribute('userrole'), 'system', 'update')) - { - $response->getBody()->write(json_encode([ - 'message' => 'You are not allowed to update settings.' - ])); - - return $response->withHeader('Content-Type', 'application/json')->withStatus(403); - } - $email = $request->getQueryParams()['email'] ?? false; $user = new User(); $userdata = []; @@ -86,18 +65,8 @@ class ControllerApiSystemUsers extends ControllerData } #returns userdata - public function getUsersByRole($request, $response, $args) + public function getUsersByRole(Request $request, Response $response, $args) { - # minimum permission are admin rights - if(!$this->c->get('acl')->isAllowed($request->getAttribute('userrole'), 'system', 'update')) - { - $response->getBody()->write(json_encode([ - 'message' => 'You are not allowed to update settings.' - ])); - - return $response->withHeader('Content-Type', 'application/json')->withStatus(403); - } - $role = $request->getQueryParams()['role'] ?? false; $user = new User(); $userdata = []; @@ -120,12 +89,12 @@ class ControllerApiSystemUsers extends ControllerData return $response->withHeader('Content-Type', 'application/json')->withStatus(200); } - public function updateUser($request, $response, $args) + public function updateUser(Request $request, Response $response, $args) { $params = $request->getParsedBody(); $userdata = $params['userdata'] ?? false; $username = $params['userdata']['username'] ?? false; - $isAdmin = $this->c->get('acl')->isAllowed($request->getAttribute('userrole'), 'userlist', 'write'); + $isAdmin = $this->c->get('acl')->isAllowed($request->getAttribute('c_userrole'), 'user', 'write'); if(!$userdata OR !$username) { @@ -146,72 +115,49 @@ class ControllerApiSystemUsers extends ControllerData return $response->withHeader('Content-Type', 'application/json')->withStatus(403); } - # make sure that invalid password input is stripped out - if(isset($userdata['password']) && $userdata['password'] == '' ) - { - unset($userdata['password']); - unset($userdata['newpassword']); - } - $user = new User(); # make sure you set a user with password when you update, otherwise it will delete the password completely $user->setUserWithPassword($username); - $userfields = $this->getUserFields($request->getAttribute('userrole')); + $formdefinitions = $this->getUserFields($request->getAttribute('c_userrole')); # validate input $validator = new Validation(); - # loop through form-definitions, ignores everything that is not defined in yaml - foreach($userfields as $fieldname => $fielddefinitions) + # cleanup password entry + if(isset($userdata['password']) AND $userdata['password'] == '') { - # if there is no value for a field - if(!isset($userdata[$fieldname])) - { - continue; - } - - # ignore readonly-fields - if(isset($fielddefinitions['readonly']) && ($fielddefinitions['readonly'] !== false) ) - { - continue; - } - - # new password needs special validation - if($fieldname == 'password') - { - $validationresult = $validator->newPassword($userdata); - - if($validationresult === true) - { - # encrypt new password - $newpassword = $user->generatePassword($userdata['newpassword']); - - # if input is valid, overwrite value in original user - $user->setValue('password', $newpassword); - } - else - { - $this->errors[$fieldname] = $validationresult[$fieldname][0]; - } - } - else - { - # standard validation - $validationresult = $validator->field($fieldname, $userdata[$fieldname], $fielddefinitions); - - if($validationresult === true) - { - # if input is valid, overwrite value in original user - $user->setValue($fieldname, $userdata[$fieldname]); - } - else - { - $this->errors[$fieldname] = $validationresult[$fieldname][0]; - } - } + unset($userdata['password']); } + if(isset($userdata['newpassword']) AND $userdata['newpassword'] == '') + { + unset($userdata['newpassword']); + } + + # validate passwort changes if valid input + if(isset($userdata['password']) OR isset($userdata['newpassword'])) + { + $validpass = $validator->newPassword($userdata); + + if($validpass === true) + { + # encrypt new password + $userdata['password'] = $user->generatePassword($userdata['newpassword']); + } + elseif(is_array($validpass)) + { + foreach($validpass as $fieldname => $errors) + { + $this->errors[$fieldname] = $errors[0]; + } + } + + # in all cases unset newpassword + unset($userdata['newpassword']); + } + + $validatedOutput = $this->recursiveValidation($validator, $formdefinitions, $userdata); if(!empty($this->errors)) { @@ -223,6 +169,12 @@ class ControllerApiSystemUsers extends ControllerData return $response->withHeader('Content-Type', 'application/json')->withStatus(400); } + # if input is valid, overwrite value in original user + foreach($validatedOutput as $fieldname => $value) + { + $user->setValue($fieldname, $value); + } + if(!$user->updateUser()) { $response->getBody()->write(json_encode([ @@ -239,99 +191,153 @@ class ControllerApiSystemUsers extends ControllerData return $response->withHeader('Content-Type', 'application/json'); } -/* - public function updateUser($request, $response, $args) + public function getNewUserForm(Request $request, Response $response, $args) { - # check if user is allowed to view (edit) userlist and other users - if(!$this->c->acl->isAllowed($_SESSION['role'], 'userlist', 'write')) + $userrole = $request->getQueryParams()['userrole'] ?? false; + if(!$userrole) { - # if an editor tries to update other userdata than its own - if($_SESSION['user'] !== $userdata['username']) - { - return $response->withRedirect($this->c->router->pathFor('user.account')); - } - - # non admins cannot change their userrole, so set it to session-value - $userdata['userrole'] = $_SESSION['role']; + $response->getBody()->write(json_encode([ + 'message' => 'Userrole is required.' + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(422); } + $userform = $this->getUserFields($userrole,$inspectorrole = $request->getAttribute('c_userrole')); + # fix the standard form + $userform['password']['label'] = 'Password'; + $userform['password']['generator'] = true; + $userform['username']['label'] = 'Username'; + unset($userform['username']['readonly']); + unset($userform['userrole']); + unset($userform['newpassword']); - $params = $request->getParams(); - $userdata = $params['user']; - $user = new User(); - $validate = new Validation(); - $userroles = $this->c->acl->getRoles(); + $response->getBody()->write(json_encode([ + 'userform' => $userform, + ])); - $redirectRoute = ($userdata['username'] == $_SESSION['user']) ? $this->c->router->pathFor('user.account') : $this->c->router->pathFor('user.show', ['username' => $userdata['username']]); - - # validate standard fields for users - if($validate->existingUser($userdata, $userroles)) - { - # validate custom input fields and return images - $userfields = $this->getUserFields($userdata['userrole']); - $imageFields = $this->validateInput('users', 'user', $userdata, $validate, $userfields); - - if(!empty($imageFields)) - { - $images = $request->getUploadedFiles(); - - if(isset($images['user'])) - { - # set image size - $settings = $this->c->get('settings'); - $imageSizes = $settings['images']; - $imageSizes['live'] = ['width' => 500, 'height' => 500]; - $settings->replace(['images' => $imageSizes]); - $imageresult = $this->saveImages($imageFields, $userdata, $settings, $images['user']); - - if(isset($_SESSION['slimFlash']['error'])) - { - return $response->withRedirect($redirectRoute); - } - elseif(isset($imageresult['username'])) - { - $userdata = $imageresult; - } - } - } - - # check for errors and redirect to path, if errors found - if(isset($_SESSION['errors'])) - { - $this->c->flash->addMessage('error', 'Please correct the errors'); - return $response->withRedirect($redirectRoute); - } - - if(empty($userdata['password']) AND empty($userdata['newpassword'])) - { - # make sure no invalid passwords go into model - unset($userdata['password']); - unset($userdata['newpassword']); - - $user->updateUser($userdata); - $this->c->flash->addMessage('info', 'Saved all changes'); - return $response->withRedirect($redirectRoute); - } - elseif($validate->newPassword($userdata)) - { - $userdata['password'] = $userdata['newpassword']; - unset($userdata['newpassword']); - - $user->updateUser($userdata); - $this->c->flash->addMessage('info', 'Saved all changes'); - return $response->withRedirect($redirectRoute); - } - } - - # change error-array for formbuilder - $errors = $_SESSION['errors']; - unset($_SESSION['errors']); - $_SESSION['errors']['user'] = $errors;# - - $this->c->flash->addMessage('error', 'Please correct your input'); - return $response->withRedirect($redirectRoute); - } + return $response->withHeader('Content-Type', 'application/json'); + } + + public function createUser(Request $request, Response $response, $args) + { + $params = $request->getParsedBody(); + $userdata = $params['userdata'] ?? false; + if(!$userdata) + { + $response->getBody()->write(json_encode([ + 'message' => 'Userdata are required.' + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(422); + } + + $validate = new Validation(); + + # standard validation for new users + $userroles = $this->c->get('acl')->getRoles(); + $valresult = $validate->newUser($userdata, $userroles); + if($valresult !== true) + { + $response->getBody()->write(json_encode([ + 'message' => 'Please correct the errors above.', + 'errors' => $validate->returnFirstValidationErrors($valresult) + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + + # additional validation for extra fields and image handling + $formdefinitions = $this->getUserFields($userdata['userrole'],$inspectorrole = $request->getAttribute('c_userrole')); + unset($formdefinitions['username']['readonly']); + $validatedOutput = $this->recursiveValidation($validate, $formdefinitions, $userdata); + if(!empty($this->errors)) + { + $response->getBody()->write(json_encode([ + 'message' => 'Please correct tbe errors in form.', + 'errors' => $this->errors + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $user = new User(); + if(!$user->createUser($validatedOutput)) + { + $response->getBody()->write(json_encode([ + 'message' => 'We could not store the new user', + 'error' => $user->error, + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $response->getBody()->write(json_encode([ + 'message' => 'New user created.', + ])); + + return $response->withHeader('Content-Type', 'application/json'); + } + + + public function deleteUser(Request $request, Response $response, $args) + { + $params = $request->getParsedBody(); + $username = $params['username'] ?? false; + $isAdmin = $this->c->get('acl')->isAllowed($request->getAttribute('c_userrole'), 'user', 'delete'); + + if(!$username) + { + $response->getBody()->write(json_encode([ + 'message' => 'Username is required.' + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(422); + } + + # if a non-admin-user tries to delete another account + if(!$isAdmin AND ($username !== $request->getAttribute('c_username')) ) + { + $response->getBody()->write(json_encode([ + 'message' => 'You are not allowed to delete another user.' + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(403); + } + + $user = new User(); + if(!$user->setUser($username)) + { + $response->getBody()->write(json_encode([ + 'message' => 'We could not find the user', + 'error' => $user->error + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + if(!$user->deleteUser()) + { + $response->getBody()->write(json_encode([ + 'message' => 'We could not delete the user', + 'error' => $user->error + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } +/* + # if user deleted his own account + if(isset($_SESSION['user']) && $_SESSION['user'] == $username) + { + session_destroy(); + } +*/ + $response->getBody()->write(json_encode([ + 'message' => 'User deleted.', + ])); + + return $response->withHeader('Content-Type', 'application/json'); } - */ } \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerData.php b/system/typemill/Controllers/ControllerData.php index 62ccb81..2826522 100644 --- a/system/typemill/Controllers/ControllerData.php +++ b/system/typemill/Controllers/ControllerData.php @@ -2,7 +2,7 @@ namespace Typemill\Controllers; -use Typemill\Models\Yaml; +use Typemill\Models\StorageWrapper; use Typemill\Events\OnSystemnaviLoaded; # this controller handels data for web and api @@ -16,9 +16,9 @@ class ControllerData extends Controller protected function getMainNavigation($userrole) { - $yaml = new Yaml('\Typemill\Models\Storage'); + $storage = new StorageWrapper('\Typemill\Models\Storage'); - $mainnavi = $yaml->getYaml('system/typemill/settings', 'mainnavi.yaml'); + $mainnavi = $storage->getYaml('system/typemill/settings', 'mainnavi.yaml'); $allowedmainnavi = []; @@ -63,9 +63,9 @@ class ControllerData extends Controller protected function getSystemNavigation($userrole) { - $yaml = new Yaml('\Typemill\Models\Storage'); + $storage = new StorageWrapper('\Typemill\Models\Storage'); - $systemnavi = $yaml->getYaml('system/typemill/settings', 'systemnavi.yaml'); + $systemnavi = $storage->getYaml('system/typemill/settings', 'systemnavi.yaml'); $systemnavi = $this->c->get('dispatcher')->dispatch(new OnSystemnaviLoaded($systemnavi), 'onSystemnaviLoaded')->getData(); $allowedsystemnavi = []; @@ -90,6 +90,7 @@ class ControllerData extends Controller return $allowedsystemnavi; } + protected function getThemeDetails() { $themes = $this->getThemes(); @@ -124,15 +125,14 @@ class ControllerData extends Controller protected function getThemeDefinition($themeName) { - $yaml = new Yaml('\Typemill\Models\Storage'); + $storage = new StorageWrapper('\Typemill\Models\Storage'); - $themeSettings = $yaml->getYaml('themes' . DIRECTORY_SEPARATOR . $themeName, $themeName . '.yaml'); + $themeSettings = $storage->getYaml('themes' . DIRECTORY_SEPARATOR . $themeName, $themeName . '.yaml'); # add standard-textarea for custom css $themeSettings['forms']['fields']['customcss'] = [ - 'type' => 'textarea', + 'type' => 'codearea', 'label' => 'Custom CSS', - 'rows' => 10, 'class' => 'codearea', 'description' => 'You can overwrite the theme-css with your own css here.' ]; @@ -177,9 +177,9 @@ class ControllerData extends Controller protected function getPluginDefinition($pluginName) { - $yaml = new Yaml('\Typemill\Models\Storage'); + $storage = new StorageWrapper('\Typemill\Models\Storage'); - $pluginSettings = $yaml->getYaml('plugins' . DIRECTORY_SEPARATOR . $pluginName, $pluginName . '.yaml'); + $pluginSettings = $storage->getYaml('plugins' . DIRECTORY_SEPARATOR . $pluginName, $pluginName . '.yaml'); return $pluginSettings; } @@ -194,9 +194,9 @@ class ControllerData extends Controller $inspectorrole = $userrole; } - $yaml = new Yaml('\Typemill\Models\Storage'); + $storage = new StorageWrapper('\Typemill\Models\Storage'); - $userfields = $yaml->getYaml('system/typemill/settings', 'user.yaml'); + $userfields = $storage->getYaml('system/typemill/settings', 'user.yaml'); # if a plugin with a role has been deactivated, then users with the role throw an error, so set them back to member... if(!$this->c->get('acl')->hasRole($userrole)) @@ -217,9 +217,11 @@ class ControllerData extends Controller # array_splice($fields,1,0,$newfield); } - # Only admin can change userroles - if($this->c->get('acl')->isAllowed($inspectorrole, 'userlist', 'write')) + # Only admin ... + if($this->c->get('acl')->isAllowed($inspectorrole, 'user', 'write')) { + + # can change userroles $definedroles = $this->c->get('acl')->getRoles(); $options = []; @@ -230,44 +232,11 @@ class ControllerData extends Controller } $userfields['userrole'] = ['label' => 'Role', 'type' => 'select', 'options' => $options]; + + # can activate api access + $userfields['apiaccess'] = ['label' => 'API access', 'checkboxlabel' => 'Activate API access for this user. Use username and password for api calls.', 'type' => 'checkbox']; } return $userfields; } - - protected function recursiveValidation($formdefinitions, $input, $validator, $themeOrPlugin = false, $name = false) - { - # loop through form-definitions, ignores everything that is not defined in yaml - foreach($formdefinitions as $fieldname => $fielddefinitions) - { - if(is_array($fielddefinitions) && $fielddefinitions['type'] == 'fieldset') - { - $this->recursiveValidation($fielddefinitions['fields'], $input, $validator, $themeOrPlugin, $name); - } - - $fieldvalue = isset($input[$fieldname]) ? $input[$fieldname] : false; - - if($fieldvalue) - { - $validationresult = $validator->field($fieldname, $fieldvalue, $fielddefinitions); - - if($validationresult === true) - { - # if input is valid, overwrite value in original settings - if($themeOrPlugin) - { - $this->settings[$themeOrPlugin][$name][$fieldname] = $fieldvalue; - } - else - { - $this->settings[$fieldname] = $fieldvalue; - } - } - else - { - $this->errors[$fieldname] = $validationresult[$fieldname][0]; - } - } - } - } } \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerWebLogin.php b/system/typemill/Controllers/ControllerWebAuth.php similarity index 73% rename from system/typemill/Controllers/ControllerWebLogin.php rename to system/typemill/Controllers/ControllerWebAuth.php index a20d373..79b7028 100644 --- a/system/typemill/Controllers/ControllerWebLogin.php +++ b/system/typemill/Controllers/ControllerWebAuth.php @@ -8,21 +8,8 @@ use Slim\Routing\RouteContext; use Typemill\Models\Validation; use Typemill\Models\User; -class ControllerWebLogin extends Controller +class ControllerWebAuth extends Controller { - # redirect if visit /setup route - public function redirect(Request $request, Response $response) - { - if(isset($_SESSION['login'])) - { - return $response->withHeader('Location', $this->routeParser->urlFor('content.raw'))->withStatus(302); - } - else - { - return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); - } - } - public function show(Request $request, Response $response) { return $this->c->get('view')->render($response, 'auth/login.twig', [ @@ -90,4 +77,41 @@ return $response->withHeader('Location', $this->routeParser->urlFor('settings.sh return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); } + + /** + * log out a user + * + * @param obj $request the slim request object + * @param obj $response the slim response object + * @return obje $response with redirect to route + */ + + public function logout(Request $request, Response $response) + { + # check https://www.php.net/session_destroy + if(isset($_SESSION)) + { + # Unset all of the session variables. + $_SESSION = array(); + + # If it's desired to kill the session, also delete the session cookie. This will destroy the session, and not just the session data! + if (ini_get("session.use_cookies")) + { + $params = session_get_cookie_params(); + + setcookie( + session_name(), + '', + time() - 42000, + $params["path"], $params["domain"], + $params["secure"], $params["httponly"] + ); + } + + # Finally, destroy the session. + session_destroy(); + } + + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); + } } \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerWebSystem.php b/system/typemill/Controllers/ControllerWebSystem.php index eab55ed..06d2f03 100644 --- a/system/typemill/Controllers/ControllerWebSystem.php +++ b/system/typemill/Controllers/ControllerWebSystem.php @@ -2,15 +2,16 @@ namespace Typemill\Controllers; -use Typemill\Models\Yaml; +use Typemill\Models\StorageWrapper; use Typemill\Models\User; +use Typemill\Models\License; class ControllerWebSystem extends ControllerData { public function showSettings($request, $response, $args) { - $yaml = new Yaml('\Typemill\Models\Storage'); - $systemfields = $yaml->getYaml('system/typemill/settings', 'system.yaml'); + $storage = new StorageWrapper('\Typemill\Models\Storage'); + $systemfields = $storage->getYaml('system/typemill/settings', 'system.yaml'); $translations = $this->c->get('translations'); # add full url for sitemap to settings @@ -19,21 +20,21 @@ class ControllerWebSystem extends ControllerData return $this->c->get('view')->render($response, 'system/system.twig', [ # 'basicauth' => $user->getBasicAuth(), 'settings' => $this->settings, - 'mainnavi' => $this->getMainNavigation($request->getAttribute('userrole')), - 'systemnavi' => $this->getSystemNavigation($request->getAttribute('userrole')), + 'mainnavi' => $this->getMainNavigation($request->getAttribute('c_userrole')), + 'systemnavi' => $this->getSystemNavigation($request->getAttribute('c_userrole')), 'jsdata' => [ 'settings' => $this->settings, 'system' => $systemfields, 'labels' => $translations, 'urlinfo' => $this->c->get('urlinfo') ] - #'captcha' => $this->checkIfAddCaptcha(), +# 'captcha' => $this->checkIfAddCaptcha(), ]); } public function showThemes($request, $response, $args) { - $yaml = new Yaml('\Typemill\Models\Storage'); + $storage = new StorageWrapper('\Typemill\Models\Storage'); $translations = $this->c->get('translations'); $themeSettings = $this->getThemeDetails(); @@ -42,13 +43,13 @@ class ControllerWebSystem extends ControllerData foreach($this->settings['themes'] as $themename => $themeinputs) { $themedata[$themename] = $themeinputs; - $themedata[$themename]['customcss'] = $yaml->getFile('cache', $themename . '-custom.css'); + $themedata[$themename]['customcss'] = $storage->getFile('cache', $themename . '-custom.css'); } return $this->c->get('view')->render($response, 'system/themes.twig', [ 'settings' => $this->settings, - 'mainnavi' => $this->getMainNavigation($request->getAttribute('userrole')), - 'systemnavi' => $this->getSystemNavigation($request->getAttribute('userrole')), + 'mainnavi' => $this->getMainNavigation($request->getAttribute('c_userrole')), + 'systemnavi' => $this->getSystemNavigation($request->getAttribute('c_userrole')), 'jsdata' => [ 'settings' => $themedata, 'themes' => $themeSettings, @@ -60,10 +61,9 @@ class ControllerWebSystem extends ControllerData public function showPlugins($request, $response, $args) { -# $yaml = new Yaml('\Typemill\Models\Storage'); $translations = $this->c->get('translations'); $pluginSettings = $this->getPluginDetails(); - + $plugindata = []; foreach($this->settings['plugins'] as $pluginname => $plugininputs) @@ -73,8 +73,8 @@ class ControllerWebSystem extends ControllerData return $this->c->get('view')->render($response, 'system/plugins.twig', [ 'settings' => $this->settings, - 'mainnavi' => $this->getMainNavigation($request->getAttribute('userrole')), - 'systemnavi' => $this->getSystemNavigation($request->getAttribute('userrole')), + 'mainnavi' => $this->getMainNavigation($request->getAttribute('c_userrole')), + 'systemnavi' => $this->getSystemNavigation($request->getAttribute('c_userrole')), 'jsdata' => [ 'settings' => $plugindata, 'plugins' => $pluginSettings, @@ -84,6 +84,58 @@ class ControllerWebSystem extends ControllerData ]); } + public function showLicense($request, $response, $args) + { + $storage = new StorageWrapper('\Typemill\Models\Storage'); + $license = new License(); + $licensefields = $storage->getYaml('system/typemill/settings', 'license.yaml'); + $translations = $this->c->get('translations'); + + $licensedata = $license->getLicenseData($this->c->get('urlinfo')); + if($licensedata) + { + foreach($licensefields as $key => $licensefield) + { + $licensefields[$key]['disabled'] = true; + } + } + + return $this->c->get('view')->render($response, 'system/license.twig', [ + 'settings' => $this->settings, + 'mainnavi' => $this->getMainNavigation($request->getAttribute('c_userrole')), + 'systemnavi' => $this->getSystemNavigation($request->getAttribute('c_userrole')), + 'jsdata' => [ + 'licensedata' => $licensedata, + 'licensefields' => $licensefields, + 'labels' => $translations, + 'urlinfo' => $this->c->get('urlinfo') ] + ]); + } + + public function showAccount($request, $response, $args) + { + $translations = $this->c->get('translations'); + $username = $request->getAttribute('c_username'); + $user = new User(); + + $user->setUser($username); + $userdata = $user->getUserData(); + $userfields = $this->getUserFields($userdata['userrole']); + + return $this->c->get('view')->render($response, 'system/account.twig', [ + 'settings' => $this->settings, + 'mainnavi' => $this->getMainNavigation($request->getAttribute('c_userrole')), + 'systemnavi' => $this->getSystemNavigation($request->getAttribute('c_userrole')), + 'jsdata' => [ + 'userdata' => $userdata, + 'userfields' => $userfields, + 'userroles' => $this->c->get('acl')->getRoles(), + 'labels' => $translations, + 'urlinfo' => $this->c->get('urlinfo') + ] + ]); + } + public function showUsers($request, $response, $args) { $translations = $this->c->get('translations'); @@ -102,8 +154,8 @@ class ControllerWebSystem extends ControllerData return $this->c->get('view')->render($response, 'system/users.twig', [ 'settings' => $this->settings, - 'mainnavi' => $this->getMainNavigation($request->getAttribute('userrole')), - 'systemnavi' => $this->getSystemNavigation($request->getAttribute('userrole')), + 'mainnavi' => $this->getMainNavigation($request->getAttribute('c_userrole')), + 'systemnavi' => $this->getSystemNavigation($request->getAttribute('c_userrole')), 'jsdata' => [ 'totalusers' => count($usernames), 'usernames' => $usernames, @@ -115,21 +167,25 @@ class ControllerWebSystem extends ControllerData ]); } - public function showAccount($request, $response, $args) + public function showUser($request, $response, $args) { - - $translations = $this->c->get('translations'); - $username = $request->getAttribute('username'); + $translations = $this->c->get('translations'); + $username = $args['username'] ?? false; + $inspector = $request->getAttribute('c_userrole'); $user = new User(); - $user->setUser($username); - $userdata = $user->getUserData(); - $userfields = $this->getUserFields($userdata['userrole']); + if(!$user->setUser($username)) + { + die("return a not found page"); + } - return $this->c->get('view')->render($response, 'system/account.twig', [ + $userdata = $user->getUserData(); + $userfields = $this->getUserFields($userdata['userrole'], $inspector); + + return $this->c->get('view')->render($response, 'system/user.twig', [ 'settings' => $this->settings, - 'mainnavi' => $this->getMainNavigation($request->getAttribute('userrole')), - 'systemnavi' => $this->getSystemNavigation($request->getAttribute('userrole')), + 'mainnavi' => $this->getMainNavigation($request->getAttribute('c_userrole')), + 'systemnavi' => $this->getSystemNavigation($request->getAttribute('c_userrole')), 'jsdata' => [ 'userdata' => $userdata, 'userfields' => $userfields, @@ -140,6 +196,27 @@ class ControllerWebSystem extends ControllerData ]); } + public function newUser($request, $response, $args) + { + $translations = $this->c->get('translations'); + + return $this->c->get('view')->render($response, 'system/usernew.twig', [ + 'settings' => $this->settings, + 'mainnavi' => $this->getMainNavigation($request->getAttribute('c_userrole')), + 'systemnavi' => $this->getSystemNavigation($request->getAttribute('c_userrole')), + 'jsdata' => [ + 'userroles' => $this->c->get('acl')->getRoles(), + 'labels' => $translations, + 'urlinfo' => $this->c->get('urlinfo') + ] + ]); + } + + + + + + /* public function showBlank($request, $response, $args) @@ -159,276 +236,7 @@ class ControllerWebSystem extends ControllerData 'route' => $route->getName() )); } - - - - - - public function showUser($request, $response, $args) - { - # if user has no rights to watch userlist, then redirect to - if(!$this->c->acl->isAllowed($_SESSION['role'], 'userlist', 'view') && $_SESSION['user'] !== $args['username'] ) - { - return $response->withRedirect($this->c->router->pathFor('user.show', ['username' => $_SESSION['user']] )); - } - - # get settings - $settings = $this->c->get('settings'); - - # get user with userdata - $user = new User(); - $userdata = $user->getSecureUser($args['username']); - - if(!$userdata) - { - $this->c->flash->addMessage('error', 'User does not exists'); - return $response->withRedirect($this->c->router->pathFor('user.account')); - } - # instantiate field-builder - $fieldsModel = new Fields($this->c); - - # get the field-definitions - $fieldDefinitions = $this->getUserFields($userdata['userrole']); - - # prepare userdata for field-builder - $userSettings['users']['user'] = $userdata; - - # generate the input form - $userform = $fieldsModel->getFields($userSettings, 'users', 'user', $fieldDefinitions); - - $route = $request->getAttribute('route'); - $navigation = $this->getMainNavigation(); - - # set navigation active - $navigation['Users']['active'] = true; - - if(isset($userdata['lastlogin'])) - { - $userdata['lastlogin'] = date("d.m.Y H:i:s", $userdata['lastlogin']); - } - - return $this->render($response, 'settings/user.twig', array( - 'settings' => $settings, - 'acl' => $this->c->acl, - 'navigation' => $navigation, - 'usersettings' => $userSettings, // needed for image url in form, will overwrite settings for field-template - 'userform' => $userform, // field model, needed to generate frontend-field - 'userdata' => $userdata, // needed to fill form with data - 'route' => $route->getName() // needed to set link active - )); - } - - - public function newUser($request, $response, $args) - { - $user = new User(); - $users = $user->getUsers(); - $userroles = $this->c->acl->getRoles(); - $route = $request->getAttribute('route'); - $settings = $this->c->get('settings'); - $navigation = $this->getMainNavigation(); - - # set navigation active - $navigation['Users']['active'] = true; - - return $this->render($response, 'settings/usernew.twig', array( - 'settings' => $settings, - 'acl' => $this->c->acl, - 'navigation' => $navigation, - 'users' => $users, - 'userrole' => $userroles, - 'route' => $route->getName() - )); - } - - public function createUser($request, $response, $args) - { - if($request->isPost()) - { - if( $request->getattribute('csrf_result') === false ) - { - $this->c->flash->addMessage('error', 'The form has a timeout, please try again.'); - return $response->withRedirect($this->c->router->pathFor('user.new')); - } - - $params = $request->getParams(); - $user = new User(); - $validate = new Validation(); - $userroles = $this->c->acl->getRoles(); - - if($validate->newUser($params, $userroles)) - { - $userdata = array( - 'username' => $params['username'], - 'email' => $params['email'], - 'userrole' => $params['userrole'], - 'password' => $params['password']); - - $user->createUser($userdata); - - $this->c->flash->addMessage('info', 'Welcome, there is a new user!'); - return $response->withRedirect($this->c->router->pathFor('user.list')); - } - - $this->c->flash->addMessage('error', 'Please correct your input'); - return $response->withRedirect($this->c->router->pathFor('user.new')); - } - } - - public function updateUser($request, $response, $args) - { - - if($request->isPost()) - { - if( $request->getattribute('csrf_result') === false ) - { - $this->c->flash->addMessage('error', 'The form has a timeout, please try again.'); - return $response->withRedirect($this->c->router->pathFor('user.account')); - } - - $params = $request->getParams(); - $userdata = $params['user']; - $user = new User(); - $validate = new Validation(); - $userroles = $this->c->acl->getRoles(); - - $redirectRoute = ($userdata['username'] == $_SESSION['user']) ? $this->c->router->pathFor('user.account') : $this->c->router->pathFor('user.show', ['username' => $userdata['username']]); - - # check if user is allowed to view (edit) userlist and other users - if(!$this->c->acl->isAllowed($_SESSION['role'], 'userlist', 'write')) - { - # if an editor tries to update other userdata than its own - if($_SESSION['user'] !== $userdata['username']) - { - return $response->withRedirect($this->c->router->pathFor('user.account')); - } - - # non admins cannot change their userrole, so set it to session-value - $userdata['userrole'] = $_SESSION['role']; - } - - # validate standard fields for users - if($validate->existingUser($userdata, $userroles)) - { - # validate custom input fields and return images - $userfields = $this->getUserFields($userdata['userrole']); - $imageFields = $this->validateInput('users', 'user', $userdata, $validate, $userfields); - - if(!empty($imageFields)) - { - $images = $request->getUploadedFiles(); - - if(isset($images['user'])) - { - # set image size - $settings = $this->c->get('settings'); - $imageSizes = $settings['images']; - $imageSizes['live'] = ['width' => 500, 'height' => 500]; - $settings->replace(['images' => $imageSizes]); - $imageresult = $this->saveImages($imageFields, $userdata, $settings, $images['user']); - - if(isset($_SESSION['slimFlash']['error'])) - { - return $response->withRedirect($redirectRoute); - } - elseif(isset($imageresult['username'])) - { - $userdata = $imageresult; - } - } - } - - # check for errors and redirect to path, if errors found - if(isset($_SESSION['errors'])) - { - $this->c->flash->addMessage('error', 'Please correct the errors'); - return $response->withRedirect($redirectRoute); - } - - if(empty($userdata['password']) AND empty($userdata['newpassword'])) - { - # make sure no invalid passwords go into model - unset($userdata['password']); - unset($userdata['newpassword']); - - $user->updateUser($userdata); - $this->c->flash->addMessage('info', 'Saved all changes'); - return $response->withRedirect($redirectRoute); - } - elseif($validate->newPassword($userdata)) - { - $userdata['password'] = $userdata['newpassword']; - unset($userdata['newpassword']); - - $user->updateUser($userdata); - $this->c->flash->addMessage('info', 'Saved all changes'); - return $response->withRedirect($redirectRoute); - } - } - - # change error-array for formbuilder - $errors = $_SESSION['errors']; - unset($_SESSION['errors']); - $_SESSION['errors']['user'] = $errors;# - - $this->c->flash->addMessage('error', 'Please correct your input'); - return $response->withRedirect($redirectRoute); - } - } - - public function deleteUser($request, $response, $args) - { - if($request->isPost()) - { - if( $request->getattribute('csrf_result') === false ) - { - $this->c->flash->addMessage('error', 'The form has a timeout, please try again.'); - return $response->withRedirect($this->c->router->pathFor('user.account')); - } - - $params = $request->getParams(); - $validate = new Validation(); - $user = new User(); - - # check if user is allowed to view (edit) userlist and other users - if(!$this->c->acl->isAllowed($_SESSION['role'], 'userlist', 'write')) - { - # if an editor tries to delete other user than its own - if($_SESSION['user'] !== $params['username']) - { - return $response->withRedirect($this->c->router->pathFor('user.account')); - } - } - - if($validate->username($params['username'])) - { - $userdata = $user->getSecureUser($params['username']); - if(!$userdata) - { - $this->c->flash->addMessage('error', 'Ups, we did not find that user'); - return $response->withRedirect($this->c->router->pathFor('user.show', ['username' => $params['username']])); - } - - $user->deleteUser($params['username']); - - $this->c->dispatcher->dispatch('onUserDeleted', new OnUserDeleted($userdata)); - - # if user deleted his own account - if($_SESSION['user'] == $params['username']) - { - session_destroy(); - return $response->withRedirect($this->c->router->pathFor('auth.show')); - } - - $this->c->flash->addMessage('info', 'Say goodbye, the user is gone!'); - return $response->withRedirect($this->c->router->pathFor('user.list')); - } - - $this->c->flash->addMessage('error', 'Ups, it is not a valid username'); - return $response->withRedirect($this->c->router->pathFor('user.show', ['username' => $params['username']])); - } - } public function clearCache($request, $response, $args) { diff --git a/system/typemill/Middleware/ApiAuthentication.php b/system/typemill/Middleware/ApiAuthentication.php new file mode 100644 index 0000000..30247f6 --- /dev/null +++ b/system/typemill/Middleware/ApiAuthentication.php @@ -0,0 +1,144 @@ +getBasePath(); + + # check if it is a session based authentication + if ($request->hasHeader('X-Session-Auth')) + { + session_start(); + + $authenticated = ( + (isset($_SESSION['username'])) && + (isset($_SESSION['login'])) + ) + ? true : false; + + if($authenticated) + { + # here we have to load userdata and pass them through request or response + $user = new User(); + + if($user->setUser($_SESSION['username'])) + { + $userdata = $user->getUserData(); + + $request = $request->withAttribute('c_username', $userdata['username']); + $request = $request->withAttribute('c_userrole', $userdata['userrole']); + + $response = $handler->handle($request); + + return $response; + } + } + else + { + # return error message + } + } + + + # api authentication with basic auth + # inspired by tuupola + $host = $request->getUri()->getHost(); + $scheme = $request->getUri()->getScheme(); + $server_params = $request->getServerParams(); + + /* + # HTTP allowed only if secure is false or server is in relaxed array. + # use own logic for https proto forwarding + if($scheme !== "https" && $this->options["secure"] !== true) + { + $allowedHost = in_array($host, $this->options["relaxed"]); + + # if 'headers' is in the 'relaxed' key, then we check for forwarding + $allowedForward = false; + if (in_array("headers", $this->options["relaxed"])) + { + if ( $request->getHeaderLine("X-Forwarded-Proto") === "https" && $request->getHeaderLine('X-Forwarded-Port') === "443") + { + $allowedForward = true; + } + } + + if (!($allowedHost || $allowedForward)) + { + $message = sprintf("Insecure use of middleware over %s denied by configuration.", strtoupper($scheme)); + throw new \RuntimeException($message); + } + } + */ + + $params = []; + + if (preg_match("/Basic\s+(.*)$/i", $request->getHeaderLine("Authorization"), $matches)) + { + $explodedCredential = explode(":", base64_decode($matches[1]), 2); + if (count($explodedCredential) == 2) + { + [$params["user"], $params["password"]] = $explodedCredential; + } + } + + if(!empty($params)) + { + # load userdata + $user = new User(); + + if($user->setUserWithPassword($params['user'])) + { + $userdata = $user->getUserData(); + + # this might be unsecure, check for === comparator + $apiaccess = ( isset($userdata['apiaccess']) && $userdata['apiaccess'] == true ) ? true : false; + + if($userdata && $apiaccess && password_verify($params['password'], $userdata['password'])) + { + $request = $request->withAttribute('c_username', $userdata['username']); + $request = $request->withAttribute('c_userrole', $userdata['userrole']); + + # this executes code from routes first and then executes middleware + $response = $handler->handle($request); + + return $response; + } + else + { + # if basic auth is set but with wrong credentials + $response = new Response(); + + $response->getBody()->write(json_encode([ + 'message' => 'Authentication failed.' + ])); + + return $response->withHeader('WWW-Authenticate', 'Basic realm=')->withStatus(401); + } + } + } + +# elseif ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') { + # if you use this, then all xhr-calls need a session. + # no direct xhr calls without session are possible + # might increase security, but can have unwanted cases e.g. when you + # want to provide public api accessible for all by javascript (do you ever want??) +# } + + $response = new Response(); + + $response->getBody()->write('Zugriff nicht erlaubt.'); + + return $response->withStatus(401); + } +} \ No newline at end of file diff --git a/system/typemill/Middleware/ApiAuthorization.php b/system/typemill/Middleware/ApiAuthorization.php new file mode 100644 index 0000000..a95d20e --- /dev/null +++ b/system/typemill/Middleware/ApiAuthorization.php @@ -0,0 +1,39 @@ +acl = $acl; + $this->resource = $resource; + $this->action = $action; + } + + public function process(Request $request, RequestHandler $handler) :Response + { + if(!$this->acl->isAllowed($request->getAttribute('c_userrole'), $this->resource, $this->action)) + { + + $message = 'userrole: ' . $request->getAttribute('c_userrole') . ' resource: ' . $this->resource . ' action: ' . $this->action; + $response = new Response(); + + $response->getBody()->write(json_encode([ + 'message' => $message + ])); + + return $response->withStatus(401); + } + + $response = $handler->handle($request); + + return $response; + } +} \ No newline at end of file diff --git a/system/typemill/Middleware/RestrictApiAccess.php b/system/typemill/Middleware/RestrictApiAccess.php deleted file mode 100644 index f7913cc..0000000 --- a/system/typemill/Middleware/RestrictApiAccess.php +++ /dev/null @@ -1,81 +0,0 @@ -getBasePath(); - - # check if it a session based authentication - if ($request->hasHeader('X-Session-Auth')) - { - session_start(); - - $authenticated = ( - (isset($_SESSION['username'])) && - (isset($_SESSION['login'])) - ) - ? true : false; - - if($authenticated) - { - # here we have to load userdata and pass them through request or response - $user = new User(); - - if($user->setUser($_SESSION['username'])) - { - $userdata = $user->getUserData(); - - $request = $request->withAttribute('username', $userdata['username']); - $request = $request->withAttribute('userrole', $userdata['userrole']); - - $response = $handler->handle($request); - - return $response; - } - } - } - -# elseif ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') { - # if you use this, then all xhr-calls need a session. - # no direct xhr calls without session are possible - # might increase security, but can have unwanted cases e.g. when you - # want to provide public api accessible for all by javascript (do you ever want??) -# } - - # this is for api-key authentication - $user = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : false; - $apikey = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : false; - - if($user && $apikey) - { - # get user with username - # or get user with apikey - - # check if user has tmpApiKey - # check if user has permanentApiKey - # check if user has tmpApiKey - # check if tmpApiKey has expired - # check if user keys are correct - - $response = $handler->handle($request); - - return $response; - } - - $response = new Response(); - - $response->getBody()->write('Zugriff nicht erlaubt.'); - - return $response->withStatus(401); - } -} \ No newline at end of file diff --git a/system/typemill/Middleware/WebAuthorization.php b/system/typemill/Middleware/WebAuthorization.php new file mode 100644 index 0000000..b30b7be --- /dev/null +++ b/system/typemill/Middleware/WebAuthorization.php @@ -0,0 +1,34 @@ +router = $router; + $this->acl = $acl; + $this->resource = $resource; + $this->action = $action; + } + + public function process(Request $request, RequestHandler $handler) :Response + { + if(!$this->acl->isAllowed($request->getAttribute('c_userrole'), $this->resource, $this->action)) + { + $response = new Response(); + + return $response->withHeader('Location', $this->router->urlFor('user.account'))->withStatus(302); + } + + $response = $handler->handle($request); + + return $response; + } +} \ No newline at end of file diff --git a/system/typemill/Middleware/RedirectIfAuthenticated.php b/system/typemill/Middleware/WebRedirectIfAuthenticated.php similarity index 82% rename from system/typemill/Middleware/RedirectIfAuthenticated.php rename to system/typemill/Middleware/WebRedirectIfAuthenticated.php index aeafa78..90abafb 100644 --- a/system/typemill/Middleware/RedirectIfAuthenticated.php +++ b/system/typemill/Middleware/WebRedirectIfAuthenticated.php @@ -8,7 +8,7 @@ use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\RequestHandlerInterface as RequestHandler; use Slim\Psr7\Response; -class RedirectIfAuthenticated implements MiddlewareInterface +class WebRedirectIfAuthenticated implements MiddlewareInterface { public function __construct(RouteParser $router, $settings) { @@ -18,8 +18,6 @@ class RedirectIfAuthenticated implements MiddlewareInterface public function process(Request $request, RequestHandler $handler) :Response { - $editor = (isset($this->settings['editor']) && $this->settings['editor'] == 'visual') ? 'visual' : 'raw'; - $authenticated = ( (isset($_SESSION['username'])) && (isset($_SESSION['login'])) @@ -28,6 +26,9 @@ class RedirectIfAuthenticated implements MiddlewareInterface if($authenticated) { + + $editor = (isset($this->settings['editor']) && $this->settings['editor'] == 'visual') ? 'visual' : 'raw'; + $response = new Response(); return $response->withHeader('Location', $this->router->urlFor('content.' . $editor))->withStatus(302); diff --git a/system/typemill/Middleware/RedirectIfUnauthenticated.php b/system/typemill/Middleware/WebRedirectIfUnauthenticated.php similarity index 67% rename from system/typemill/Middleware/RedirectIfUnauthenticated.php rename to system/typemill/Middleware/WebRedirectIfUnauthenticated.php index 7ba8c97..2328992 100644 --- a/system/typemill/Middleware/RedirectIfUnauthenticated.php +++ b/system/typemill/Middleware/WebRedirectIfUnauthenticated.php @@ -9,8 +9,8 @@ use Psr\Http\Server\RequestHandlerInterface as RequestHandler; use Slim\Psr7\Response; use Typemill\Models\User; -class RedirectIfUnauthenticated implements MiddlewareInterface -{ +class WebRedirectIfUnauthenticated implements MiddlewareInterface +{ public function __construct(RouteParser $router) { $this->router = $router; @@ -18,29 +18,29 @@ class RedirectIfUnauthenticated implements MiddlewareInterface public function process(Request $request, RequestHandler $handler) :response { - $authenticated = ( - (isset($_SESSION['username'])) && - (isset($_SESSION['login'])) - ) - ? true : false; - - if($authenticated) + # session authentication + if( + (isset($_SESSION['username'])) && + (isset($_SESSION['login'])) + ) { - # here we have to load userdata and pass them through request or response + # load userdata $user = new User(); if($user->setUser($_SESSION['username'])) { + + # pass username and userrole $userdata = $user->getUserData(); - $request = $request->withAttribute('username', $userdata['username']); - $request = $request->withAttribute('userrole', $userdata['userrole']); + $request = $request->withAttribute('c_username', $userdata['username']); + $request = $request->withAttribute('c_userrole', $userdata['userrole']); # this executes code from routes first and then executes middleware $response = $handler->handle($request); return $response; - } + } } # this executes only middleware code and not code from route diff --git a/system/typemill/Models/License.php b/system/typemill/Models/License.php new file mode 100644 index 0000000..f2a3708 --- /dev/null +++ b/system/typemill/Models/License.php @@ -0,0 +1,258 @@ + [ + 'name' => 'MAKER', + 'scope' => ['MAKER' => true] + ], + '33334' => [ + 'name' => 'BUSINESS', + 'scope' => ['MAKER' => true, 'BUSINESS' => true] + ] + ]; + + public function getMessage() + { + return $this->message; + } + + # used for license management in admin settings + public function getLicenseData(array $urlinfo) + { + # returns data for settings page + $licensedata = $this->checkLicense(); + if($licensedata) + { + $licensedata['plan'] = $this->plans[$licensedata['plan']]['name']; + $licensedata['domaincheck'] = $this->checkLicenseDomain($licensedata['domain'], $urlinfo); + $licensedata['datecheck'] = $this->checkLicenseDate($licensedata['payed_until']); + + return $licensedata; + } + + return false; + } + + # used to activate or deactivate features that require a license + public function getLicenseScope(array $urlinfo) + { + $licensedata = $this->checkLicense(); + + if(!$licensedata) + { + return false; + } + + $domain = $this->checkLicenseDomain($licensedata['domain'], $urlinfo); + $date = $this->checkLicenseDate($licensedata['payed_until']); + + $domain = true; + + if($domain && $date) + { + return $this->plans[$licensedata['plan']]['scope']; + } + + return false; + } + + public function refreshLicense() + { + + } + + # check the local licence file (like pem or pub) + private function checkLicense() + { + $storage = new StorageWrapper('\Typemill\Models\Storage'); + + $licensedata = $storage->getYaml('settings', 'license.yaml'); + + if(!$licensedata) + { + $this->message = 'no license found'; + + return false; + } + + if(!isset($licensedata['license'],$licensedata['email'],$licensedata['domain'],$licensedata['plan'],$licensedata['payed_until'],$licensedata['signature'])) + { + $this->message = 'License data incomplete'; + + return false; + } + $licenseStatus = $this->validateLicense($licensedata); + + if($licenseStatus === true) + { + unset($licensedata['signature']); + + # check here if payed until is in past + return $licensedata; + } + + return false; + } + + private function validateLicense($data) + { + $public_key_pem = $this->getPublicKeyPem(); + + $binary_signature = base64_decode($data['signature']); + + $data['email'] = $this->hashMail($data['email']); + unset($data['signature']); + + # test manipulate data + #$data['plan'] = 'wrong'; + + $data = json_encode($data); + + # Check signature + $verified = openssl_verify($data, $binary_signature, $public_key_pem, OPENSSL_ALGO_SHA256); + + if ($verified == 1) + { + return true; + } + elseif ($verified == 0) + { + $this->message = 'License data are invalid'; + + return false; + } + else + { + $this->message = 'There was an error checking the license signature'; + + return false; + } + } + + public function activateLicense($params) + { + # prepare data for call to licence server + $licensedata = [ + 'license' => $params['license'], + 'email' => $this->hashMail($params['email']), + 'domain' => $params['domain'] + ]; + + $postdata = http_build_query($licensedata); + + $authstring = $this->getPublicKeyPem(); + $authstring = hash('sha256', substr($authstring, 0, 50)); + + $options = array ( + 'http' => array ( + 'method' => 'POST', + 'ignore_errors' => true, + 'header' => "Content-Type: application/x-www-form-urlencoded\r\n" . + "Accept: application/json\r\n" . + "Authorization: $authstring\r\n" . + "Connection: close\r\n", + 'content' => $postdata + ) + ); + + $context = stream_context_create($options); + + $response = file_get_contents('https://service.typemill.net/api/v2/activate', false, $context); + + if(substr($http_response_header[0], -6) != "200 OK") + { + $this->message = 'the license server responded with: ' . $http_response_header[0]; + + return false; + } + + $signedLicense = json_decode($response,true); + + if(isset($signedLicense['code'])) + { +# $this->message = 'Something went wrong. Please check your input data or contact the support.'; + $this->message = $signedLicense['code']; + return false; + } + +/* + # check for positive and validate response data + if($signedLicense['license']) + { + $this->message = ; + } +*/ + $signedLicense['license']['email'] = trim($params['email']); + $storage = new StorageWrapper('\Typemill\Models\Storage'); + + $storage->updateYaml('settings', 'license.yaml', $signedLicense['license']); + + return true; + } + + private function updateLicence() + { + # todo + } + + private function checkLicenseDomain(string $licensedomain, array $urlinfo) + { + $licensehost = parse_url($licensedomain, PHP_URL_HOST); + $licensehost = str_replace("www.", "", $licensehost); + + $thishost = parse_url($urlinfo['baseurl'], PHP_URL_HOST); + $thishost = str_replace("www.", "", $thishost); + + $whitelist = ['localhost', '127.0.0.1', 'typemilltest.', $licensehost]; + + foreach($whitelist as $domain) + { + if(substr($thishost, 0, strlen($domain)) == $domain) + { + return true; + } + } + + return false; + } + + private function checkLicenseDate(string $payed_until) + { + if(strtotime($payed_until) > strtotime(date('Y-m-d'))) + { + return true; + } + return false; + } + + private function hashMail(string $mail) + { + return hash('sha256', trim($mail) . 'TYla5xa8JUur'); + } + + private function getPublicKeyPem() + { + $pkeyfile = getcwd() . DIRECTORY_SEPARATOR . 'settings' . DIRECTORY_SEPARATOR . "public_key.pem"; + + if(file_exists($pkeyfile) && is_readable($pkeyfile)) + { + # fetch public key from file and ready it + $fp = fopen($pkeyfile, "r"); + $public_key_pem = fread($fp, 8192); + fclose($fp); + + return $public_key_pem; + } + + return false; + } + +} \ No newline at end of file diff --git a/system/typemill/Models/ProcessAssets.php b/system/typemill/Models/ProcessAssets.php new file mode 100644 index 0000000..448e7b3 --- /dev/null +++ b/system/typemill/Models/ProcessAssets.php @@ -0,0 +1,243 @@ +basepath = getcwd() . DIRECTORY_SEPARATOR; + + $this->tmpFolder = $this->basepath . 'media' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR; + } + + public function clearTempFolder() + { + $files = scandir($this->tmpFolder); + $now = time(); + $result = true; + + foreach($files as $file) + { + if (!in_array($file, array(".",".."))) + { + $filelink = $this->tmpFolder . $file; + if(file_exists($filelink)) + { + $filetime = filemtime($filelink); + if($now - $filetime > 1800) + { + if(!unlink($filelink)) + { + $result = false; + } + } + } + } + } + + return $result; + } + + + +/* + public function checkFolders($forassets = null) + { + + $folders = [$this->mediaFolder, $this->tmpFolder, $this->fileFolder]; + + if($forassets == 'images') + { + $folders = [$this->mediaFolder, $this->tmpFolder, $this->originalFolder, $this->liveFolder, $this->thumbFolder, $this->customFolder]; + } + + foreach($folders as $folder) + { + if(!file_exists($folder) && !is_dir( $folder )) + { + if(!mkdir($folder, 0755, true)) + { + return false; + } + if($folder == $this->thumbFolder) + { + # cleanup old systems + $this->cleanupLiveFolder(); + + # generate thumbnails from live folder + $this->generateThumbs(); + } + } + elseif(!is_writeable($folder) OR !is_readable($folder)) + { + return false; + } + + # check if thumb-folder is empty, then generate thumbs from live folder + if($folder == $this->thumbFolder && $this->is_dir_empty($folder)) + { + # cleanup old systems + $this->cleanupLiveFolder(); + + # generate thumbnails from live folder + $this->generateThumbs(); + } + } + return true; + } +*/ + + public function is_dir_empty($dir) + { + return (count(scandir($dir)) == 2); + } + +/* + public function setFileName($originalname, $type, $overwrite = NULL) + { + $pathinfo = pathinfo($originalname); + $this->extension = isset($pathinfo['extension']) ? strtolower($pathinfo['extension']) : null; + $this->filename = Folder::createSlug($pathinfo['filename']); + + $filename = $this->filename; + + # check if file name is + if(!$overwrite) + { + $suffix = 1; + + $destination = $this->liveFolder; + if($type == 'file') + { + $destination = $this->fileFolder; + } + + while(file_exists($destination . $filename . '.' . $this->extension)) + { + $filename = $this->filename . '-' . $suffix; + $suffix++; + } + } + + $this->filename = $filename; + + return true; + } +*/ + +/* + public function getName() + { + return $this->filename; + } + + public function setExtension($extension) + { + $this->extension = $extension; + } + + public function getExtension() + { + return $this->extension; + } + + public function getFullName() + { + return $this->filename . '.' . $this->extension; + } + +*/ + +/* + public function cleanupLiveFolder() + { + # delete all old thumbs mlibrary in live folder + foreach(glob($this->liveFolder . '*mlibrary*') as $filename) + { + unlink($filename); + } + + return true; + } +*/ + + public function findPagesWithUrl($structure, $url, $result) + { + foreach ($structure as $key => $item) + { + if($item->elementType == 'folder') + { + $result = $this->findPagesWithUrl($item->folderContent, $url, $result); + } + else + { + $live = getcwd() . DIRECTORY_SEPARATOR . 'content' . $item->pathWithoutType . '.md'; + $draft = getcwd() . DIRECTORY_SEPARATOR . 'content' . $item->pathWithoutType . '.txt'; + + # check live first + if(file_exists($live)) + { + $content = file_get_contents($live); + + if (stripos($content, $url) !== false) + { + $result[] = $item->urlRelWoF; + } + # if not in live, check in draft + elseif(file_exists($draft)) + { + $content = file_get_contents($draft); + + if (stripos($content, $url) !== false) + { + $result[] = $item->urlRelWoF; + } + } + } + } + } + return $result; + } + + public function formatSizeUnits($bytes) + { + if ($bytes >= 1073741824) + { + $bytes = number_format($bytes / 1073741824, 2) . ' GB'; + } + elseif ($bytes >= 1048576) + { + $bytes = number_format($bytes / 1048576, 2) . ' MB'; + } + elseif ($bytes >= 1024) + { + $bytes = number_format($bytes / 1024, 2) . ' KB'; + } + elseif ($bytes > 1) + { + $bytes = $bytes . ' bytes'; + } + elseif ($bytes == 1) + { + $bytes = $bytes . ' byte'; + } + else + { + $bytes = '0 bytes'; + } + + return $bytes; + } +} \ No newline at end of file diff --git a/system/typemill/Models/ProcessFile.php b/system/typemill/Models/ProcessFile.php new file mode 100644 index 0000000..b4d67d6 --- /dev/null +++ b/system/typemill/Models/ProcessFile.php @@ -0,0 +1,40 @@ +getFile($folderName, $yamlFileName); + + if($yaml) + { + return \Symfony\Component\Yaml\Yaml::parse($yaml); + } + + return false; + } + + /** + * Writes a yaml file. + * @param string $fileName is the name of the Yaml Folder. + * @param string $yamlFileName is the name of the Yaml File. + * @param array $contentArray is the content as an array. + */ + public function updateYaml($folderName, $yamlFileName, $contentArray) + { + $yaml = \Symfony\Component\Yaml\Yaml::dump($contentArray,6); + if($this->writeFile($folderName, $yamlFileName, $yaml)) + { + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/system/typemill/Models/ProcessImage.php b/system/typemill/Models/ProcessImage.php new file mode 100644 index 0000000..769a58e --- /dev/null +++ b/system/typemill/Models/ProcessImage.php @@ -0,0 +1,585 @@ + true, 'jpg' => true, 'jpeg' => true, 'webp' => true]; + protected $filename = false; + protected $animated = false; + protected $resizable = true; + protected $sizes = []; + + public function prepareImage($image, $name) + { + # change clear tmp folder and delete only old ones + $this->clearTempFolder(); + #$this->checkFolders('image'); + $this->decode($image); + $this->setPathInfo($name); + $this->checkAllowedExtension(); + + if(empty($this->errors)) + { + return true; + } + + return false; + } + + public function storeOriginalToTmp() + { + # $this->saveName(); + $this->saveOriginal(); + + if(empty($this->errors)) + { + return true; + } + + return false; + } + + public function storeRenditionsToTmp($sizes) + { + # transform image-stream into image + $image = $this->createImage(); + + $originalsize = $this->getImageSize($image); + + foreach($sizes as $destinationfolder => $desiredsize) + { + $desiredsize = $this->calculateSize($originalsize, $desiredsize); + + $resizedImage = $this->resizeImage($image, $desiredsize, $originalsize); + + $this->saveResizedImage($resizedImage, $destinationfolder, $this->extension); + + imagedestroy($resizedImage); + } + + imagedestroy($image); + + if(empty($this->errors)) + { + return true; + } + + return false; + } + + + # decode a base64 image string from js image components + public function decode(string $image) + { + $imageParts = explode(";base64,", $image); + + if(!isset($imageParts[0]) OR !isset($imageParts[1])) + { + $this->errors[] = 'Could not decode image, probably not a base64 encoding.'; + + return false; + } + + $type = explode("/", $imageParts[0]); + $this->type = strtolower($type[0]); + $this->imgstring = base64_decode($imageParts[1]); + + return true; + } + + # set the pathinfo (name and extension) and slugify a unique name if option to overwrite existing files is false + public function setPathInfo(string $name) + { + $pathinfo = pathinfo($name); + if(!$pathinfo) + { + $this->errors[] = 'Could not read pathinfo.'; + + return false; + } + + $this->extension = isset($pathinfo['extension']) ? strtolower($pathinfo['extension']) : false; + $this->filename = Slug::createSlug($pathinfo['filename']); + + if(!$this->extension OR !$this->filename) + { + $this->errors[] = 'Extension or filename are missing.'; + + return false; + } + + return true; + } + + public function getExtension() + { + return $this->extension; + } + + public function getFilename() + { + return $this->filename; + } + + public function setFilename($filename) + { + $this->filename = $filename; + } + + public function getFullName() + { + return $this->filename . '.' . $this->extension; + } + + # add an allowed image extension like svg + public function addAllowedExtension(string $extension) + { + $this->allowedExtensions[$extension] = true; + } + + + # force an image type like webp + public function setExtension(string $extension) + { + $this->extension = $extension; + } + + public function checkAllowedExtension() + { + if(!isset($this->allowedExtensions[$this->extension])) + { + $this->errors[] = 'Images with this extension are not allowed.'; + + return false; + } + + return true; + } + + # check if image should not be resized (animated gif and svg) + public function isResizable() + { + if($this->type == 'gif' && $this->detectAnimatedGif()) + { + $this->resizable = false; + } + + if($this->type == 'svg+xml') + { + $this->resizable = false; + } + + return $this->resizable; + } + + public function detectAnimatedGif() + { + $is_animated = preg_match('#(\x00\x21\xF9\x04.{4}\x00\x2C.*){2,}#s', $this->imgstring); + if ($is_animated == 1) + { + $this->animated = true; + } + + return $this->animated; + } + + # save the original image to temp folder + public function saveOriginal($destinationfolder = 'ORIGINAL') + { + $path = $this->tmpFolder . $destinationfolder . '+' . $this->filename . '.' . $this->extension; + + if(!file_put_contents($path, $this->imgstring)) + { + $this->errors[] = 'could not store the image in the temporary folder'; + } + } + + # save the original image for all sizes/folders + public function saveOriginalForAll() + { + $this->saveOriginal('LIVE'); + $this->saveOriginal('THUMBS'); + } + + public function createImage() + { + return imagecreatefromstring($this->imgstring); + } + + public function getImageSize($image) + { + return ['width' => imagesx($image), 'height' => imagesy($image)]; + } + + public function calculateSize(array $originalsize, array $desiredsize) + { + # if desired size is bigger than the actual image, then drop the desired sizes and use the actual image size instead + if($desiredsize['width'] > $originalsize['width']) + { + return $originalsize; + } + + if(!isset($desiredsize['height'])) + { + $resizeFactor = $originalsize['width'] / $desiredsize['width']; + $desiredsize['height'] = round( ($originalsize['height'] / $resizeFactor), 0); + } + + return $desiredsize; + } + + public function resizeImage($image, array $desired, array $original) + { + # resize + $ratio = max($desired['width']/$original['width'], $desired['height']/$original['height']); + $h = $desired['height'] / $ratio; + $x = ($original['width'] - $desired['width'] / $ratio) / 2; + $y = ($original['height'] - $desired['height'] / $ratio) / 2; + $w = $desired['width'] / $ratio; + + $resizedImage = imagecreatetruecolor($desired['width'], $desired['height']); + + # preserve transparency + if($this->extension == "gif" or $this->extension == "png" or $this->extension == "webp") + { + imagecolortransparent($resizedImage, imagecolorallocatealpha($resizedImage, 0, 0, 0, 127)); + imagealphablending($resizedImage, false); + imagesavealpha($resizedImage, true); + } + + imagecopyresampled($resizedImage, $image, 0, 0, $x, $y, $desired['width'], $desired['height'], $w, $h); + + return $resizedImage; + } + + public function saveResizedImage($resizedImage, string $destinationfolder, string $extension) + { + $destinationfolder = strtoupper($destinationfolder); + + switch($extension) + { + case "png": + $storedImage = imagepng( $resizedImage, $this->tmpFolder . $destinationfolder . '+' . $this->filename . '.png', 9 ); + break; + case "gif": + $storedImage = imagegif( $resizedImage, $this->tmpFolder . $destinationfolder . '+' . $this->filename . '.gif' ); + break; + case "webp": + $storedImage = imagewebp( $resizedImage, $this->tmpFolder . $destinationfolder . '+' . $this->filename . '.webp', 80); + break; + case "jpg": + case "jpeg": + $storedImage = imagejpeg( $resizedImage, $this->tmpFolder . $destinationfolder . '+' . $this->filename . '.' . $extension, 80); + break; + default: + $storedImage = false; + } + + if(!$storedImage) + { + $failedImage = $this->tmpFolder . $destinationfolder . '+' . $this->filename . '.' . $extension; + + $this->errors[] = "Could not store the resized version $failedImage"; + + return false; + } + + return true; + } + + + # publish image function is moved to storage model + + + # MOVE TO STORAGE ?? + public function deleteImage($name) + { + # validate name + $name = basename($name); + + if(!file_exists($this->originalFolder . $name) OR !unlink($this->originalFolder . $name)) + { + $this->errors[] = "We could not delete the original image"; + } + + if(!file_exists($this->liveFolder . $name) OR !unlink($this->liveFolder . $name)) + { + $this->errors[] = "We could not delete the live image"; + } + + if(!file_exists($this->thumbFolder . $name) OR !unlink($this->thumbFolder . $name)) + { + $this->errors[] = "we could not delete the thumb image"; + } + + # delete custom images (resized and grayscaled) array_map('unlink', glob("some/dir/*.txt")); + $pathinfo = pathinfo($name); + foreach(glob($this->customFolder . $pathinfo['filename'] . '\-*.' . $pathinfo['extension']) as $image) + { + # you could check if extension is the same here + if(!unlink($image)) + { + $this->errors[] = "we could not delete a custom image (grayscale or resized)"; + } + } + + if(empty($this->errors)) + { + return true; + } + + return false; + } + + + + # in use ?? + public function deleteImageWithName($name) + { + # e.g. delete $name = 'logo...'; + + $name = basename($name); + + if($name != '' && !in_array($name, array(".",".."))) + { + foreach(glob($this->liveFolder . $name) as $file) + { + unlink($file); + } + foreach(glob($this->originalFolder . $name) as $file) + { + unlink($file); + } + foreach(glob($this->thumbFolder . $name) as $file) + { + unlink($file); + } + } + } + + # in use ?? + public function copyImage($name,$sourcefolder,$targetfolder) + { + copy($sourcefolder . $name, $targetfolder . $name); + } + + + + + + + + + + + + + + + + /** + * Moves the uploaded file to the upload directory. Only used for settings / NON VUE.JS uploads + * + * @param string $directory directory to which the file is moved + * @param UploadedFile $uploadedFile file uploaded file to move + * @return string filename of moved file + */ + public function moveUploadedImage(UploadedFile $uploadedFile, $overwrite = false, $name = false, $folder = NULL) + { + $this->setFileName($uploadedFile->getClientFilename(), 'file'); + + if($name) + { + $this->setFileName($name . '.' . $this->extension, 'file', $overwrite); + } + + if(!$folder) + { + $folder = $this->liveFolder; + } + + $uploadedFile->moveTo($folder . $this->getFullName()); + + return $this->getFullName(); + } + + + + + + + + + + + +/* + # save the image name as txt to temp folder + public function saveName() + { + $path = $this->tmpFolder . $this->filename . '.txt'; + + if(!fopen($path, "w")) + { + $this->errors[] = 'could not store the filename in the temporary folder'; + } + } +*/ + + + + /* + * scans content of a folder (without recursion) + * vars: folder path as string + * returns: one-dimensional array with names of folders and files + */ + public function scanMediaFlat() + { + $thumbs = array_diff(scandir($this->thumbFolder), array('..', '.')); + $imagelist = array(); + + foreach ($thumbs as $key => $name) + { + if (file_exists($this->liveFolder . $name)) + { + $imagelist[] = [ + 'name' => $name, + 'timestamp' => filemtime($this->liveFolder . $name), + 'src_thumb' => 'media/thumbs/' . $name, + 'src_live' => 'media/live/' . $name, + ]; + } + } + + $imagelist = Helpers::array_sort($imagelist, 'timestamp', SORT_DESC); + + return $imagelist; + } + + + # get details from existing image for media library + public function getImageDetails($name, $structure) + { + $name = basename($name); + + if (!in_array($name, array(".","..")) && file_exists($this->liveFolder . $name)) + { + $imageinfo = getimagesize($this->liveFolder . $name); + + if(!$imageinfo && pathinfo($this->liveFolder . $name, PATHINFO_EXTENSION) == 'svg') + { + $imagedetails = [ + 'name' => $name, + 'timestamp' => filemtime($this->liveFolder . $name), + 'bytes' => filesize($this->liveFolder . $name), + 'width' => '---', + 'height' => '---', + 'type' => 'svg', + 'src_thumb' => 'media/thumbs/' . $name, + 'src_live' => 'media/live/' . $name, + 'pages' => $this->findPagesWithUrl($structure, $name, $result = []) + ]; + } + else + { + $imagedetails = [ + 'name' => $name, + 'timestamp' => filemtime($this->liveFolder . $name), + 'bytes' => filesize($this->liveFolder . $name), + 'width' => $imageinfo[0], + 'height' => $imageinfo[1], + 'type' => $imageinfo['mime'], + 'src_thumb' => 'media/thumbs/' . $name, + 'src_live' => 'media/live/' . $name, + 'pages' => $this->findPagesWithUrl($structure, $name, $result = []) + ]; + } + + return $imagedetails; + } + + return false; + } + + public function generateThumbs() + { + # generate images from live folder to 'tmthumbs' + $liveImages = scandir($this->liveFolder); + + $result = false; + + foreach ($liveImages as $key => $name) + { + if (!in_array($name, array(".",".."))) + { + $result = $this->generateThumbFromImageFile($name); + } + } + return $result; + } + + public function generateThumbFromImageFile($filename) + { + $this->setFileName($filename, 'image', $overwrite = true); + + $image = $this->createImageFromPath($this->liveFolder . $filename, $this->extension); + + $originalSize = $this->getImageSize($image); + + $thumbSize = $this->desiredSizes['thumbs']; + + $thumb = $this->imageResize($image, $originalSize, ['thumbs' => $thumbSize ], $this->extension); + + $saveImage = $this->saveImage($this->thumbFolder, $thumb['thumbs'], $this->filename, $this->extension); + if($saveImage) + { + return true; + } + return false; + } + + # filename and imagepath can be a tmp-version after upload. + public function generateSizesFromImageFile($filename, $imagePath) + { + $this->setFileName($filename, 'image'); + + $image = $this->createImageFromPath($imagePath, $this->extension); + + $originalSize = $this->getImageSize($image); + + $resizedImages = $this->imageResize($image, $originalSize, $this->desiredSizes, $this->extension); + + return $resizedImages; + } + + public function grayscale($imagePath, $extension) + { + $image = $this->createImageFromPath($imagePath, $extension); + + imagefilter($image, IMG_FILTER_GRAYSCALE); + + return $image; + } + + public function createImageFromPath($imagePath, $extension) + { + switch($extension) + { + case 'gif': $image = imagecreatefromgif($imagePath); break; + case 'jpg' : + case 'jpeg': $image = imagecreatefromjpeg($imagePath); break; + case 'png': $image = imagecreatefrompng($imagePath); break; + case 'webp': $image = imagecreatefromwebp($imagePath); break; + default: return 'image type not supported'; + } + + return $image; + } +} \ No newline at end of file diff --git a/system/typemill/Models/Storage.php b/system/typemill/Models/Storage.php index 0999f18..5f1c661 100644 --- a/system/typemill/Models/Storage.php +++ b/system/typemill/Models/Storage.php @@ -4,13 +4,42 @@ namespace Typemill\Models; class Storage { - protected $basepath; - public $error = false; + protected $basepath = false; + + protected $tmpFolder = false; + + protected $originalFolder = false; + + protected $liveFolder = false; + + protected $thumbsFolder = false; + + protected $customFolder = false; + + protected $fileFolder = false; + public function __construct() { - $this->basepath = getcwd() . DIRECTORY_SEPARATOR; + $this->basepath = getcwd() . DIRECTORY_SEPARATOR; + + $this->tmpFolder = $this->basepath . 'media' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR; + + $this->originalFolder = $this->basepath . 'media' . DIRECTORY_SEPARATOR . 'original' . DIRECTORY_SEPARATOR; + + $this->liveFolder = $this->basepath . 'media' . DIRECTORY_SEPARATOR . 'live' . DIRECTORY_SEPARATOR; + + $this->thumbsFolder = $this->basepath . 'media' . DIRECTORY_SEPARATOR . 'thumbs' . DIRECTORY_SEPARATOR; + + $this->customFolder = $this->basepath . 'media' . DIRECTORY_SEPARATOR . 'custom' . DIRECTORY_SEPARATOR; + + $this->fileFolder = $this->basepath . 'media' . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR; + } + + public function getError() + { + return $this->error; } public function checkFolder($folder) @@ -123,22 +152,173 @@ class Storage return true; } - public function deleteFile($filepath) + public function deleteFile($folder, $filename) { - if($this->checkFileWithPath($filepath)) + if($this->checkFile($folder, $filename)) { - unlink($this->basePath . $filepath); - return true; + if(unlink($this->basepath . $folder . DIRECTORY_SEPARATOR . $filename)) + { + return true; + } + + $this->error = "We found the file but could not delete $filename"; } + return false; } - public function getError() + public function moveFile() { - return $this->error; + + } + + /** + * Get the a yaml file. + * @param string $fileName is the name of the Yaml Folder. + * @param string $yamlFileName is the name of the Yaml File. + */ + public function getYaml($folderName, $yamlFileName) + { + $yaml = $this->getFile($folderName, $yamlFileName); + + if($yaml) + { + return \Symfony\Component\Yaml\Yaml::parse($yaml); + } + + return false; + } + + /** + * Writes a yaml file. + * @param string $fileName is the name of the Yaml Folder. + * @param string $yamlFileName is the name of the Yaml File. + * @param array $contentArray is the content as an array. + */ + public function updateYaml($folderName, $yamlFileName, $contentArray) + { + $yaml = \Symfony\Component\Yaml\Yaml::dump($contentArray,6); + if($this->writeFile($folderName, $yamlFileName, $yaml)) + { + return true; + } + + return false; } + public function createUniqueImageName($filename, $extension) + { + $defaultfilename = $filename; + + $suffix = 1; + + while(file_exists($this->originalFolder . $filename . '.' . $extension)) + { + $filename = $defaultfilename . '-' . $suffix; + $suffix++; + } + + return $filename; + } + + public function publishImage($name) + { + $pathinfo = pathinfo($name); + if(!$pathinfo) + { + $this->errors[] = 'Could not read pathinfo.'; + + return false; + } + + $extension = isset($pathinfo['extension']) ? strtolower($pathinfo['extension']) : false; + $imagename = isset($pathinfo['filename']) ? $pathinfo['filename'] : false; + + $imagesInTmp = glob($this->tmpFolder . "*$imagename.*"); + if(empty($imagesInTmp) OR !$imagesInTmp) + { + $this->errors[] = "We did not find the image in the tmp-folder or could not read it."; + return false; + } + + # case: image is not published yet and in tmp + foreach( $imagesInTmp as $imagepath) + { + $tmpimagename = explode("+", basename($imagepath)); + $destinationfolder = strtolower($tmpimagename[0]); + $filename = $tmpimagename[1]; + + switch($destinationfolder) + { + case 'original': + if(!rename($imagepath, $this->originalFolder . $filename)) + { + $this->errors[] = "We could not store the original image to the original folder"; + } + break; + case 'live': + if(!rename($imagepath, $this->liveFolder . $filename)) + { + $this->errors[] = "We could not store the live image to the live folder"; + } + break; + case 'thumbs': + if(!rename($imagepath, $this->thumbsFolder . $filename)) + { + $this->errors[] = "We could not store the thumb to the thumb folder"; + } + break; + } + } + + if(empty($this->errors)) + { + # return true; + return 'media/live/' . $imagename . '.' . $extension; + } + + return false; + } + + # check if an image exists in the live folder or in the original folder independent from extension + public function checkImage($imagepath) + { + $original = stripos($imagepath, '/original/'); + $live = stripos($imagepath, '/live/'); + + $pathinfo = pathinfo($imagepath); + if(!$pathinfo) + { + $this->errors[] = 'Could not read pathinfo.'; + + return false; + } + + $extension = isset($pathinfo['extension']) ? strtolower($pathinfo['extension']) : false; + $imagename = isset($pathinfo['filename']) ? $pathinfo['filename'] : false; + $newpath = false; + + if($original) + { + $image = glob($this->originalFolder . "$imagename.*"); + if(isset($image[0])) + { + $newpath = 'media/original/' . basename($image[0]); + } + } + elseif($live) + { + $image = glob($this->liveFolder . "$imagename.*"); + if(isset($image[0])) + { + $newpath = 'media/live/' . basename($image[0]); + } + } + + return $newpath; + + } /* public function checkPath($folder) diff --git a/system/typemill/Models/User.php b/system/typemill/Models/User.php index f82672b..260b160 100644 --- a/system/typemill/Models/User.php +++ b/system/typemill/Models/User.php @@ -2,7 +2,7 @@ namespace Typemill\Models; -use Typemill\Models\Yaml; +use Typemill\Models\StorageWrapper; class User { @@ -12,57 +12,55 @@ class User private $user = false; - private $password = false; - public $error = false; public function __construct() { $this->userDir = getcwd() . '/settings/users'; - $this->yaml = new Yaml('\Typemill\Models\Storage'); + $this->storage = new StorageWrapper('\Typemill\Models\Storage'); } public function setUser(string $username) { - # if no user is set or requested user has a different username -# if(!$this->user OR ($this->user['username'] != $username)) -# { - $this->user = $this->yaml->getYaml('settings/users', $username . '.yaml'); + $this->user = $this->storage->getYaml('settings/users', $username . '.yaml'); + + if(!$this->user) + { + $this->error = 'User not found'; + + return false; + } - if(!$this->user) - { - $this->error = 'User not found'; - - return false; - } - - # store password in private property so it is not accessible outside - $this->password = $this->user['password']; - - # delete password from public userdata - unset($this->user['password']); -# } + # delete password from public userdata + unset($this->user['password']); return $this; } public function setUserWithPassword(string $username) { + $this->user = $this->storage->getYaml('settings/users', $username . '.yaml'); + if(!$this->user) { - $this->user = $this->yaml->getYaml('settings/users', $username . '.yaml'); + $this->error = 'User not found.'; - if(!$this->user) - { - $this->error = 'User not found.'; - - return false; - } + return false; } return $this; } + public function setValue($key, $value) + { + $this->user[$key] = $value; + } + + public function unsetValue($key) + { + unset($this->user[$key]); + } + public function getUserData() { return $this->user; @@ -105,192 +103,62 @@ class User { $params['password'] = $this->generatePassword($params['password']); - if($this->yaml->updateYaml('settings/users', $params['username'] . '.yaml', $params)) + if($this->storage->updateYaml('settings/users', $params['username'] . '.yaml', $params)) { $this->deleteUserIndex(); - return $params['username']; + return true; } - $this->error = $this->yaml->getError(); + $this->error = $this->storage->getError(); return false; } - public function setValue($key, $value) - { - $this->user[$key] = $value; - } - - public function unsetValue($key) - { - unset($this->user[$key]); - } - public function updateUser() { - if($this->yaml->updateYaml('settings/users', $this->user['username'] . '.yaml', $this->user)) + if($this->storage->updateYaml('settings/users', $this->user['username'] . '.yaml', $this->user)) { $this->deleteUserIndex(); return true; } - $this->error = $this->yaml->getError(); + $this->error = $this->storage->getError(); return false; } - - - - - - - - - - - public function unsetFromUser(array $keys) + public function deleteUser() { - if(empty($keys) OR !$this->user) + if($this->storage->deleteFile('settings/users/', $this->user['username'] . '.yaml')) { - $this->error = 'Keys are empty or user does not exist.'; - - return false; - } - - foreach($keys as $key) - { - if(isset($this->user[$key])) - { - unset($this->user[$key]); - } - } - - $this->yaml->updateYaml('settings/users', $this->user['username'] . '.yaml', $this->user); - - return true; - } - - public function updateUserOld() - { - # add password back to userdata before you store/update user - if($this->password) - { - $this->user['password'] = $this->password; - } - - if($this->yaml->updateYaml('settings/users', $this->user['username'] . '.yaml', $this->user)) - { - return true; - } - - $this->error = $this->yaml->getError(); - - return false; - } - - - public function updateUserWithInput(array $input) - { - if(!isset($input['username']) OR !$this->user) - { - return false; - } - - # make sure new password is not stored - if(isset($input['newpassword'])) - { - unset($input['newpassword']); - } - - # make sure password is set correctly - if(isset($input['password'])) - { - if(empty($input['password'])) - { - unset($input['password']); - } - else - { - $input['password'] = $this->generatePassword($input['password']); - } - } - - # set old password back to original userdate - if($this->password) - { - $this->user['password'] = $this->password; - } - - # overwrite old userdata with new userdata - $updatedUser = array_merge($this->user, $input); - - # cleanup data here - - $this->updateYaml('settings/users', $this->user['username'] . '.yaml', $updatedUser); - - $this->deleteUserIndex(); - - # if user updated his own profile, update session data - if(isset($_SESSION['username']) && $_SESSION['username'] == $input['username']) - { - $_SESSION['userrole'] = $updatedUser['userrole']; - - if(isset($updatedUser['firstname'])) - { - $_SESSION['firstname'] = $updatedUser['firstname']; - } - if(isset($updatedUser['lastname'])) - { - $_SESSION['lastname'] = $updatedUser['lastname']; - } - } - - return $this->user['username']; - } - - public function deleteUser(string $username) - { - if($this->getUser($username)) - { - unlink('settings/users/' . $username . '.yaml'); - $this->deleteUserIndex(); return true; } + + $this->error = $this->storage->getError(); + return false; } - + public function login() { if($this->user) { $this->user['lastlogin'] = time(); -# $this->user['internalApiKey'] = bin2hex(random_bytes(32)); $_SESSION['username'] = $this->user['username']; -# $_SESSION['userrole'] = $this->user['userrole']; $_SESSION['login'] = $this->user['lastlogin']; -/* - if(isset($this->user['firstname'])) - { - $_SESSION['firstname'] = $this->user['firstname']; - } - if(isset($this->user['lastname'])) - { - $_SESSION['lastname'] = $this->user['lastname']; - } -*/ + if(isset($this->user['recovertoken']) OR isset($this->user['recoverdate'])) { $this->unsetFromUser($this->user['username'], ['recovertoken', 'recoverdate']); } # update user last login - $this->updateUser(); + $this->updateUser(); } } @@ -299,12 +167,14 @@ class User return \password_hash($password, PASSWORD_DEFAULT, ['cost' => 10]); } +/* public function getBasicAuth() { $basicauth = $this->user['username'] . ":" . $this->user['internalApiKey']; return base64_encode($basicauth); } +*/ # accepts email with or without asterix and returns userdata public function findUsersByEmail($email) @@ -483,12 +353,4 @@ class User unlink($userDir . DIRECTORY_SEPARATOR . 'tmuserindex-mail.txt'); } } - - # deprecated - public function getSecureUser(string $username) - { - $user = $this->getYaml('settings/users', $username . '.yaml'); - unset($user['password']); - return $user; - } } \ No newline at end of file diff --git a/system/typemill/Models/Validation.php b/system/typemill/Models/Validation.php index 794fee3..77ec3b6 100644 --- a/system/typemill/Models/Validation.php +++ b/system/typemill/Models/Validation.php @@ -40,7 +40,7 @@ class Validation Validator::addRule('emailAvailable', function($field, $value, array $params, array $fields) use ($user) { $email = trim($value); - if($user->findUsersByEmail($email)){ return false; } + if($email == '' OR $user->findUsersByEmail($email)){ return false; } return true; }, 'taken'); @@ -163,6 +163,29 @@ class Validation } return false; }, 'not secure. For code please use markdown `inline-code` or ````fenced code blocks````.'); + + Validator::addRule('checkLicense', function($field, $value, array $params, array $fields) + { + $parts = explode("-",$value); + if(count($parts) != 5) + { + return false; + } + if($parts[0] != "TM2") + { + return false; + } + unset($parts[0]); + foreach($parts as $key => $part) + { + if(strlen($part) != 5 OR !preg_match("/^[A-Z0-9]*$/",$part) ) + { + return false; + } + } + + return true; + }, 'format is not valid.'); } # return valitron standard object @@ -171,6 +194,15 @@ class Validation return new Validator($params); } + public function returnFirstValidationErrors($errors) + { + foreach($errors as $key => $error) + { + $errors[$key] = $error[0]; + } + + return $errors; + } /** * validation for signup form @@ -204,12 +236,12 @@ class Validation * @return obj $v the validation object passed to a result method. */ - public function newUser(array $params, $userroles) + public function newUser(array $params, array $userroles) { $v = new Validator($params); $v->rule('required', ['username', 'email', 'password'])->message("required"); $v->rule('alphaNum', 'username')->message("invalid characters"); - $v->rule('lengthBetween', 'password', 5, 20)->message("Length between 5 - 20"); + $v->rule('lengthBetween', 'password', 5, 40)->message("Length between 5 - 40"); $v->rule('lengthBetween', 'username', 3, 20)->message("Length between 3 - 20"); $v->rule('userAvailable', 'username')->message("User already exists"); $v->rule('noHTML', 'firstname')->message(" contains HTML"); @@ -220,7 +252,12 @@ class Validation $v->rule('emailAvailable', 'email')->message("Email already taken"); $v->rule('in', 'userrole', $userroles); - return $this->validationResult($v); + if($v->validate()) + { + return true; + } + + return $v->errors(); } public function existingUser(array $params, $userroles) @@ -307,6 +344,30 @@ class Validation return $this->validationResult($v); } + /** + * validation for changing the password api case + * + * @param array $params with form data. + * @return obj $v the validation object passed to a result method. + */ + + public function newLicense(array $params) + { + $v = new Validator($params); + $v->rule('required', ['license', 'email', 'domain']); + $v->rule('checkLicense', 'license'); + $v->rule('email', 'email'); + $v->rule('url', 'domain'); + + if($v->validate()) + { + return true; + } + + return $v->errors(); + } + + /** * validation for system settings * @@ -499,47 +560,65 @@ class Validation switch($fieldDefinitions['type']) { - case "select": - /* create array with option keys as value */ - $options = array(); - foreach($fieldDefinitions['options'] as $key => $value){ $options[] = $key; } - $v->rule('in', $fieldName, $options); - break; - case "radio": - $v->rule('in', $fieldName, $fieldDefinitions['options']); - break; - case "checkboxlist": - if(isset($fieldValue) && is_array($fieldValue)) - { - /* create array with option keys as value */ - $options = array(); - foreach($fieldDefinitions['options'] as $key => $value){ $options[] = $key; } - - /* loop over input values and check, if the options of the field definitions (options for checkboxlist) contains the key (input from user, key is used as value, value is used as label) */ - foreach($fieldValue as $key => $value) - { - $v->rule('in', $key, $options); - } - } - break; - case "color": - $v->rule('regex', $fieldName, '/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/'); - break; - case "email": - $v->rule('email', $fieldName); - break; - case "date": - $v->rule('date', $fieldName); - break; case "checkbox": if(isset($fieldDefinitions['required'])) { $v->rule('accepted', $fieldName); } break; - case "url": - $v->rule('url', $fieldName); - $v->rule('lengthMax', $fieldName, 200); + case "checkboxlist": + if(isset($fieldValue) && is_array($fieldValue)) + { + # create array with option keys as value + $options = array(); + foreach($fieldDefinitions['options'] as $key => $value){ $options[] = $key; } + + # loop over input values and check, if the options of the field definitions (options for checkboxlist) contains the key (input from user, key is used as value, value is used as label) + foreach($fieldValue as $key => $value) + { + $v->rule('in', $key, $options); + } + } + break; + case "code": + $v->rule('lengthMax', $fieldName, 10000); + break; + case "color": + $v->rule('regex', $fieldName, '/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/'); + break; + case "customfields": + $v->rule('array', $fieldName); + $v->rule('customfields', $fieldName); + break; + case "date": + $v->rule('date', $fieldName); + break; + case "email": + $v->rule('email', $fieldName); + break; + case "image": + $v->rule('noHTML', $fieldName); + $v->rule('lengthMax', $fieldName, 1000); + $v->rule('image_types', $fieldName); + break; + case "number": + $v->rule('integer', $fieldName); + break; + case "paragraph": + $v->rule('noHTML', $fieldName); + $v->rule('lengthMax', $fieldName, 10000); + break; + case "password": + $v->rule('lengthMax', $fieldName, 100); + break; + case "radio": + $v->rule('in', $fieldName, $fieldDefinitions['options']); + break; + case "select": + # create array with option keys as value + $options = array(); + foreach($fieldDefinitions['options'] as $key => $value){ $options[] = $key; } + $v->rule('in', $fieldName, $options); break; case "text": $v->rule('noHTML', $fieldName); @@ -558,22 +637,10 @@ class Validation $v->rule('lengthMax', $fieldName, 10000); } break; - case "paragraph": - $v->rule('noHTML', $fieldName); - $v->rule('lengthMax', $fieldName, 10000); + case "url": + $v->rule('url', $fieldName); + $v->rule('lengthMax', $fieldName, 200); break; - case "password": - $v->rule('lengthMax', $fieldName, 100); - break; - case "image": - $v->rule('noHTML', $fieldName); - $v->rule('lengthMax', $fieldName, 1000); - $v->rule('image_types', $fieldName); - break; - case "customfields": - $v->rule('array', $fieldName); - $v->rule('customfields', $fieldName); - break; default: $v->rule('lengthMax', $fieldName, 1000); $v->rule('regex', $fieldName, '/^[\pL0-9_ \-]*$/u'); @@ -585,8 +652,6 @@ class Validation } return true; - - return $this->validationResult($v); } /** diff --git a/system/typemill/Models/Yaml.php b/system/typemill/Models/Yaml.php index b4d67d6..a21db7a 100644 --- a/system/typemill/Models/Yaml.php +++ b/system/typemill/Models/Yaml.php @@ -11,6 +11,7 @@ class Yaml extends StorageWrapper */ public function getYaml($folderName, $yamlFileName) { + die('Yaml class outdated. Use storage instead.'); $yaml = $this->getFile($folderName, $yamlFileName); if($yaml) @@ -29,6 +30,7 @@ class Yaml extends StorageWrapper */ public function updateYaml($folderName, $yamlFileName, $contentArray) { + die('Yaml class outdated. Use storage instead.'); $yaml = \Symfony\Component\Yaml\Yaml::dump($contentArray,6); if($this->writeFile($folderName, $yamlFileName, $yaml)) { diff --git a/system/typemill/Static/License.php b/system/typemill/Static/License.php new file mode 100644 index 0000000..cd91f9f --- /dev/null +++ b/system/typemill/Static/License.php @@ -0,0 +1,229 @@ +getYaml('settings', 'license.yaml'); + + if(!$licensedata) + { + return ['result' => false, 'message' => 'no license found']; + } + + if(!isset($licensedata['license'],$licensedata['email'],$licensedata['domain'],$licensedata['plan'],$licensedata['payed_until'],$licensedata['signature'])) + { + return ['result' => false, 'message' => 'License data not complete']; + } + + $licenseStatus = self::validateLicense($licensedata); + + unset($licensedata['signature']); + + if($licenseStatus === false) + { + return ['result' => false, 'message' => 'License data are invalid']; + } + elseif($licenseStatus === true) + { + echo '
';
+			print_r($licensedata);
+			die();
+		}
+		else
+		{
+			die('error checking signature');
+		}
+	}
+
+	public static function validateLicense($data)
+	{
+		$public_key_pem 	= self::getPublicKeyPem();
+
+		$binary_signature 	= base64_decode($data['signature']);
+
+		$data['email'] 		= self::hashMail($data['email']);
+		unset($data['signature']);
+
+		# manipulate data
+		# $data['product'] 	= 'business';
+
+		$data = json_encode($data);
+
+		# Check signature
+		$verified = openssl_verify($data, $binary_signature, $public_key_pem, OPENSSL_ALGO_SHA256);
+
+		if ($verified == 1)
+		{
+		    return true;
+		} 
+		elseif ($verified == 0)
+		{
+		    return false;
+		} 
+		else
+		{
+		    die("ugly, error checking signature");
+		}
+	}
+
+	public static function hashMail($mail)
+	{
+		return hash('sha256', trim($mail) . 'TYla5xa8JUur');
+	}
+
+	public static function getPublicKeyPem()
+	{
+		$pkeyfile = getcwd() . DIRECTORY_SEPARATOR . 'settings' . DIRECTORY_SEPARATOR . "public_key.pem";
+
+		if(file_exists($pkeyfile) && is_readable($pkeyfile))
+		{
+			# fetch public key from file and ready it
+			$fp 				= fopen($pkeyfile, "r");
+			$public_key_pem 	= fread($fp, 8192);
+			fclose($fp);
+
+			return $public_key_pem;
+		}
+
+		return false;
+	}
+}
+
+
+
+/* KIRBY -> source -> cms -> system.php
+
+	/**
+	 * Loads the license file and returns
+	 * the license information if available
+	 *
+	 * @return string|bool License key or `false` if the current user has
+	 *                     permissions for access.settings, otherwise just a
+	 *                     boolean that tells whether a valid license is active
+	 
+	public function license()
+	{
+		try {
+			$license = Json::read($this->app->root('license'));
+		} catch (Throwable) {
+			return false;
+		}
+
+		// check for all required fields for the validation
+		if (isset(
+			$license['license'],
+			$license['order'],
+			$license['date'],
+			$license['email'],
+			$license['domain'],
+			$license['signature']
+		) !== true) {
+			return false;
+		}
+
+		// build the license verification data
+		$data = [
+			'license' => $license['license'],
+			'order'   => $license['order'],
+			'email'   => hash('sha256', $license['email'] . 'kwAHMLyLPBnHEskzH9pPbJsBxQhKXZnX'),
+			'domain'  => $license['domain'],
+			'date'    => $license['date']
+		];
+
+
+		// get the public key
+		$pubKey = F::read($this->app->root('kirby') . '/kirby.pub');
+
+		// verify the license signature
+		$data      = json_encode($data);
+		$signature = hex2bin($license['signature']);
+		if (openssl_verify($data, $signature, $pubKey, 'RSA-SHA256') !== 1) {
+			return false;
+		}
+
+		// verify the URL
+		if ($this->licenseUrl() !== $this->licenseUrl($license['domain'])) {
+			return false;
+		}
+
+		// only return the actual license key if the
+		// current user has appropriate permissions
+		if ($this->app->user()?->isAdmin() === true) {
+			return $license['license'];
+		}
+
+		return true;
+	}
+
+
+	/**
+	 * Validates the license key
+	 * and adds it to the .license file in the config
+	 * folder if possible.
+	 *
+	 * @throws \Kirby\Exception\Exception
+	 * @throws \Kirby\Exception\InvalidArgumentException
+	 *
+	public function register(string $license = null, string $email = null): bool
+	{
+		if (Str::startsWith($license, 'K3-PRO-') === false) {
+			throw new InvalidArgumentException(['key' => 'license.format']);
+		}
+
+		if (V::email($email) === false) {
+			throw new InvalidArgumentException(['key' => 'license.email']);
+		}
+
+		// @codeCoverageIgnoreStart
+		$response = Remote::get('https://hub.getkirby.com/register', [
+			'data' => [
+				'license' => $license,
+				'email'   => Str::lower(trim($email)),
+				'domain'  => $this->indexUrl()
+			]
+		]);
+
+		if ($response->code() !== 200) {
+			throw new Exception($response->content());
+		}
+
+		// decode the response
+		$json = Json::decode($response->content());
+
+		// replace the email with the plaintext version
+		$json['email'] = $email;
+
+		// where to store the license file
+		$file = $this->app->root('license');
+
+		// save the license information
+		Json::write($file, $json);
+
+		if ($this->license() === false) {
+			throw new InvalidArgumentException([
+				'key' => 'license.verification'
+			]);
+		}
+		// @codeCoverageIgnoreEnd
+
+		return true;
+	}
+
+*/
\ No newline at end of file
diff --git a/system/typemill/Static/Plugins.php b/system/typemill/Static/Plugins.php
index 59409e1..f50ddaa 100644
--- a/system/typemill/Static/Plugins.php
+++ b/system/typemill/Static/Plugins.php
@@ -86,6 +86,27 @@ class Plugins
 		
 		return $middleware;
 	}
+
+	public static function getPremiumLicence($className)
+	{
+		$premiumlist = [
+			'\Plugins\demo\demo' => 'BUSINESS'
+		];
+
+		if(isset($premiumList['className']))
+		{
+			return $premiumList['className'];
+		}
+
+		$licenceType = false;
+
+		if(method_exists($className, 'setPremiumLicence'))
+		{
+			$licenceType = $className::setPremiumLicence();			
+		}
+		
+		return $licenceType;
+	}
 	
 	private static function checkRouteArray($routes,$route)
 	{
diff --git a/system/typemill/Static/Settings.php b/system/typemill/Static/Settings.php
index be4e78e..24fa686 100644
--- a/system/typemill/Static/Settings.php
+++ b/system/typemill/Static/Settings.php
@@ -2,13 +2,14 @@
 
 namespace Typemill\Static;
 
+use Typemill\Models\StorageWrapper;
+
 class Settings
 {
 	public static function loadSettings()
 	{
-		$rootpath			= getcwd();
-		$defaultsettings 	= self::getDefaultSettings($rootpath);
-		$usersettings 		= self::getUserSettings($rootpath);
+		$defaultsettings 	= self::getDefaultSettings();
+		$usersettings 		= self::getUserSettings();
 		
 		$settings 			= $defaultsettings;
 
@@ -24,6 +25,7 @@ class Settings
 			}
 		}
 
+		$settings['rootPath'] = getcwd();
 		$settings = self::addThemeSettings($settings);
 
 		return $settings;
@@ -32,7 +34,8 @@ class Settings
 	public static function addThemeSettings($settings)
 	{
 		# we have to check if the theme has been deleted
-		$themefolder = $settings['rootPath'] . DIRECTORY_SEPARATOR . $settings['themeFolder'] . DIRECTORY_SEPARATOR;
+		$rootpath		= getcwd();
+		$themefolder 	= $rootpath . DIRECTORY_SEPARATOR . $settings['themeFolder'] . DIRECTORY_SEPARATOR;
 
 		# if there is no theme in settings or theme has been deleted
 		if(!isset($settings['theme']) OR !file_exists($themefolder . $settings['theme']))
@@ -69,8 +72,9 @@ class Settings
 		return $settings;
 	}
 	
-	public static function getDefaultSettings($rootpath)
+	public static function getDefaultSettings()
 	{
+		$rootpath				= getcwd();
 		$defaultsettingspath 	= $rootpath . DIRECTORY_SEPARATOR . 'system' . DIRECTORY_SEPARATOR . 'typemill' . DIRECTORY_SEPARATOR . 'settings' . DIRECTORY_SEPARATOR;
 		$defaultsettingsfile 	= $defaultsettingspath . 'defaults.yaml';
 
@@ -78,7 +82,6 @@ class Settings
 		{
 			$defaultsettingsyaml 					= file_get_contents($defaultsettingsfile);
 			$defaultsettings 						= \Symfony\Component\Yaml\Yaml::parse($defaultsettingsyaml);
-			$defaultsettings['rootPath'] 			= $rootpath;
 			$defaultsettings['defaultSettingsPath'] = $defaultsettingspath;
 			
 			return $defaultsettings;
@@ -87,9 +90,10 @@ class Settings
 		return false;
 	}
 	
-	public static function getUserSettings($rootpath)
+	public static function getUserSettings()
 	{	
-		$usersettingsfile 	= $rootpath . DIRECTORY_SEPARATOR . 'settings' . DIRECTORY_SEPARATOR . 'settings.yaml';
+		$rootpath				= getcwd();
+		$usersettingsfile 		= $rootpath . DIRECTORY_SEPARATOR . 'settings' . DIRECTORY_SEPARATOR . 'settings.yaml';
 
 		if(file_exists($usersettingsfile))
 		{
@@ -102,22 +106,40 @@ class Settings
 		return false;
 	}
 
+	public static function getObjectSettings($objectType, $objectName)
+	{
+#		$yaml = new Models\WriteYaml();
+		
+		$rootpath 		= getcwd();
+		$objectfile 	= $rootpath . DIRECTORY_SEPARATOR . $objectType . DIRECTORY_SEPARATOR . $objectName . DIRECTORY_SEPARATOR . $objectName . '.yaml';
+
+		if(file_exists($objectfile))
+		{
+			$objectsettingsyaml 	= file_get_contents($objectfile);
+			$objectsettings 		= \Symfony\Component\Yaml\Yaml::parse($objectsettingsyaml);
+			
+			return $objectsettings;
+		}
+
+		return false;
+	}
+
+	public static function updateSettings(array $newSettings)
+	{
+		# only allow if usersettings already exists (setup has been done)
+		$userSettings 	= self::getUserSettings();
+
+		# merge usersettings with new settings
+		$settings 	= array_merge($userSettings, $newSettings);
+		
+		$storage 	= new StorageWrapper('\Typemill\Models\Storage');
+		
+		$storage->updateYaml('settings', 'settings.yaml', $settings);
+	}
 
 
 
 ### refactor
-
-
-	public static function getObjectSettings($objectType, $objectName)
-	{
-		$yaml = new Models\WriteYaml();
-		
-		$objectFolder 	= $objectType . DIRECTORY_SEPARATOR . $objectName;
-		$objectFile		= $objectName . '.yaml';
-		$objectSettings = $yaml->getYaml($objectFolder, $objectFile);
-
-		return $objectSettings;
-	}
   
 	public static function createSettings()
 	{
@@ -133,7 +155,7 @@ class Settings
 		return false;
 	}
 
-	public static function updateSettings($settings)
+	public static function oldupdateSettings($settings)
 	{
 		# only allow if usersettings already exists (setup has been done)
 		$userSettings 	= self::getUserSettings();
diff --git a/system/typemill/Static/Slug.php b/system/typemill/Static/Slug.php
new file mode 100644
index 0000000..dd7e9d9
--- /dev/null
+++ b/system/typemill/Static/Slug.php
@@ -0,0 +1,45 @@
+getYaml($theme_language_folder, $theme_language_file);
+			$theme_translations = $storage->getYaml($theme_language_folder, $theme_language_file);
 		}
 
 		if($environment == 'admin')
@@ -42,7 +42,7 @@ class Translations
 			$system_language_file 	= $language . '.yaml';
 			if(file_exists($system_language_folder . $system_language_file))
 			{
-				$system_translations = $yaml->getYaml($system_language_folder, $system_language_file);
+				$system_translations = $storage->getYaml($system_language_folder, $system_language_file);
 			}
 
 			# Next change, to provide labels for the admin and user environments.
@@ -52,13 +52,13 @@ class Translations
 	  		{
 			  	foreach($settings['plugins'] as $plugin => $config)
 			  	{
-					if($config['active'] == 'on')
+					if(isset($config['active']) && $config['active'])
 					{
 				  		$plugin_language_folder = 'plugins' . DIRECTORY_SEPARATOR . $plugin . DIRECTORY_SEPARATOR . 'languages' . DIRECTORY_SEPARATOR;
 				  		$plugin_language_file = $language . '.yaml';
 				  		if (file_exists($plugin_language_folder . $plugin_language_file))
 				  		{
-							$plugins_translations[$plugin] = $yaml->getYaml($plugin_language_folder, $plugin_language_file);
+							$plugins_translations[$plugin] = $storage->getYaml($plugin_language_folder, $plugin_language_file);
 				  		}
 					}
 			  	}
diff --git a/system/typemill/author/css/a11y-dark.min.css b/system/typemill/author/css/a11y-dark.min.css
new file mode 100644
index 0000000..7820d7d
--- /dev/null
+++ b/system/typemill/author/css/a11y-dark.min.css
@@ -0,0 +1,7 @@
+pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
+  Theme: a11y-dark
+  Author: @ericwbailey
+  Maintainer: @ericwbailey
+
+  Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css
+*/.hljs{background:#2b2b2b;color:#f8f8f2}.hljs-comment,.hljs-quote{color:#d4d0ab}.hljs-deletion,.hljs-name,.hljs-regexp,.hljs-selector-class,.hljs-selector-id,.hljs-tag,.hljs-template-variable,.hljs-variable{color:#ffa07a}.hljs-built_in,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-type{color:#f5ab35}.hljs-attribute{color:gold}.hljs-addition,.hljs-bullet,.hljs-string,.hljs-symbol{color:#abe338}.hljs-section,.hljs-title{color:#00e0e0}.hljs-keyword,.hljs-selector-tag{color:#dcc6e0}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}@media screen and (-ms-high-contrast:active){.hljs-addition,.hljs-attribute,.hljs-built_in,.hljs-bullet,.hljs-comment,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-quote,.hljs-string,.hljs-symbol,.hljs-type{color:highlight}.hljs-keyword,.hljs-selector-tag{font-weight:700}}
\ No newline at end of file
diff --git a/system/typemill/author/css/custom.css b/system/typemill/author/css/custom.css
index 4182565..47a861d 100644
--- a/system/typemill/author/css/custom.css
+++ b/system/typemill/author/css/custom.css
@@ -26,4 +26,109 @@
 .initial-enter-from,
 .initial-leave-to {
   opacity: 0;
+}
+
+/* CODEAREA */
+
+.codearea{
+  flex: 1 0 20rem;
+  position: relative;
+}
+.editor, .highlight {
+  width: 100%;
+  font-size: 1rem;
+/*  font-size: 1.1rem; */
+  font-family: monospace;
+  margin: 0;
+  padding: 0.7rem 1.4ch;
+  line-height: 1.313;
+}
+.highlight {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  border: 1px solid transparent;
+  pointer-events: none;
+  color: white;
+}
+[data-el="editor"] {
+  border-width: 1px;
+  background:rgb(68 64 60);
+  color: white;
+  caret-color: white;
+  white-space: break-spaces;
+  word-break: break-word;
+  resize: vertical;
+}
+[data-el="editor"][data-initialized="true"] {
+  color: transparent;
+  resize: none;
+  overflow: hidden;
+}
+[data-el="highlight"] {
+  font-family: inherit;
+  line-height: inherit;
+  font-size: inherit;
+  margin: 0;
+  padding: 0;
+  white-space: break-spaces;
+  word-break: break-word;
+}
+
+/* HIGHLIGHT */
+
+.hljs-comment,.hljs-quote{
+  color:#d4d0ab
+}
+.hljs-deletion,.hljs-name,.hljs-regexp,.hljs-selector-class,.hljs-selector-id,.hljs-tag,.hljs-template-variable,.hljs-variable{
+  color:#ffa07a
+}
+.hljs-built_in,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-type{
+  color:#f5ab35
+}
+.hljs-attribute{
+  color:gold
+}
+.hljs-addition,.hljs-bullet,.hljs-string,.hljs-symbol{
+  color:#abe338
+}
+.hljs-section,.hljs-title{
+  color:#00e0e0
+}
+.hljs-keyword,.hljs-selector-tag{
+  color:#dcc6e0
+}
+.hljs-emphasis{
+  font-style:italic
+}
+.hljs-strong{
+  font-weight:700
+}
+@media screen and (-ms-high-contrast:active){
+  .hljs-addition,.hljs-attribute,.hljs-built_in,.hljs-bullet,.hljs-comment,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-quote,.hljs-string,.hljs-symbol,.hljs-type{
+    color:highlight
+  }
+  .hljs-keyword,.hljs-selector-tag{
+    font-weight:700
+  }
+}
+
+.bg-chess {
+  background-image:  repeating-linear-gradient(
+    45deg, 
+    #D6D3D1 25%, 
+    transparent 25%, 
+    transparent 75%, 
+    #D6D3D1 75%, 
+    #D6D3D1), 
+  repeating-linear-gradient(
+    45deg, 
+    #D6D3D1 25%, 
+    #F5F5F4 25%, 
+    #F5F5F4 75%, 
+    #D6D3D1 75%, 
+    #D6D3D1);
+  background-position: 0 0, 10px 10px;
+  background-size: 20px 20px;
 }
\ No newline at end of file
diff --git a/system/typemill/author/css/output.css b/system/typemill/author/css/output.css
index 978f593..932faa5 100644
--- a/system/typemill/author/css/output.css
+++ b/system/typemill/author/css/output.css
@@ -624,6 +624,10 @@ video {
   position: static;
 }
 
+.fixed {
+  position: fixed;
+}
+
 .absolute {
   position: absolute;
 }
@@ -632,6 +636,13 @@ video {
   position: relative;
 }
 
+.inset-0 {
+  top: 0px;
+  right: 0px;
+  bottom: 0px;
+  left: 0px;
+}
+
 .inset-y-0 {
   top: 0px;
   bottom: 0px;
@@ -665,6 +676,10 @@ video {
   top: 0.75rem;
 }
 
+.z-50 {
+  z-index: 50;
+}
+
 .m-0 {
   margin: 0px;
 }
@@ -683,6 +698,11 @@ video {
   margin-bottom: 2rem;
 }
 
+.mx-auto {
+  margin-left: auto;
+  margin-right: auto;
+}
+
 .my-5 {
   margin-top: 1.25rem;
   margin-bottom: 1.25rem;
@@ -707,6 +727,18 @@ video {
   margin-top: 1.5rem;
 }
 
+.mb-1 {
+  margin-bottom: 0.25rem;
+}
+
+.mt-3 {
+  margin-top: 0.75rem;
+}
+
+.mb-4 {
+  margin-bottom: 1rem;
+}
+
 .mb-3 {
   margin-bottom: 0.75rem;
 }
@@ -719,10 +751,6 @@ video {
   margin-bottom: 1.25rem;
 }
 
-.mb-1 {
-  margin-bottom: 0.25rem;
-}
-
 .ml-2 {
   margin-left: 0.5rem;
 }
@@ -735,12 +763,12 @@ video {
   margin-top: 0.5rem;
 }
 
-.mt-4 {
-  margin-top: 1rem;
+.mr-3 {
+  margin-right: 0.75rem;
 }
 
-.mb-4 {
-  margin-bottom: 1rem;
+.mt-4 {
+  margin-top: 1rem;
 }
 
 .mt-auto {
@@ -787,6 +815,14 @@ video {
   display: table;
 }
 
+.table-cell {
+  display: table-cell;
+}
+
+.grid {
+  display: grid;
+}
+
 .hidden {
   display: none;
 }
@@ -807,10 +843,18 @@ video {
   height: 1.25rem;
 }
 
+.h-80 {
+  height: 20rem;
+}
+
 .h-64 {
   height: 16rem;
 }
 
+.max-h-80 {
+  max-height: 20rem;
+}
+
 .min-h-screen {
   min-height: 100vh;
 }
@@ -823,6 +867,14 @@ video {
   width: 100%;
 }
 
+.w-2\/5 {
+  width: 40%;
+}
+
+.w-3\/5 {
+  width: 60%;
+}
+
 .w-half {
   width: 48%;
 }
@@ -843,10 +895,26 @@ video {
   width: 33.333333%;
 }
 
+.w-80 {
+  width: 20rem;
+}
+
+.w-1\/6 {
+  width: 16.666667%;
+}
+
+.w-5\/6 {
+  width: 83.333333%;
+}
+
 .w-10 {
   width: 2.5rem;
 }
 
+.w-11\/12 {
+  width: 91.666667%;
+}
+
 .w-3\/4 {
   width: 75%;
 }
@@ -859,10 +927,30 @@ video {
   max-width: 28rem;
 }
 
+.max-w-xs {
+  max-width: 20rem;
+}
+
 .max-w-6xl {
   max-width: 72rem;
 }
 
+.flex-shrink {
+  flex-shrink: 1;
+}
+
+.flex-grow {
+  flex-grow: 1;
+}
+
+.grow {
+  flex-grow: 1;
+}
+
+.border-collapse {
+  border-collapse: collapse;
+}
+
 .transform {
   transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
 }
@@ -871,6 +959,10 @@ video {
   cursor: pointer;
 }
 
+.resize {
+  resize: both;
+}
+
 .flex-col {
   flex-direction: column;
 }
@@ -915,6 +1007,10 @@ video {
   overflow: hidden;
 }
 
+.overflow-y-auto {
+  overflow-y: auto;
+}
+
 .truncate {
   overflow: hidden;
   text-overflow: ellipsis;
@@ -937,6 +1033,10 @@ video {
   border-width: 2px;
 }
 
+.border-b {
+  border-bottom-width: 1px;
+}
+
 .border-b-2 {
   border-bottom-width: 2px;
 }
@@ -971,6 +1071,11 @@ video {
   border-color: rgb(231 229 228 / var(--tw-border-opacity));
 }
 
+.border-white {
+  --tw-border-opacity: 1;
+  border-color: rgb(255 255 255 / var(--tw-border-opacity));
+}
+
 .border-red-500 {
   --tw-border-opacity: 1;
   border-color: rgb(239 68 68 / var(--tw-border-opacity));
@@ -981,11 +1086,21 @@ video {
   border-color: rgb(214 211 209 / var(--tw-border-opacity));
 }
 
+.border-teal-500 {
+  --tw-border-opacity: 1;
+  border-color: rgb(20 184 166 / var(--tw-border-opacity));
+}
+
 .border-stone-700 {
   --tw-border-opacity: 1;
   border-color: rgb(68 64 60 / var(--tw-border-opacity));
 }
 
+.border-rose-100 {
+  --tw-border-opacity: 1;
+  border-color: rgb(255 228 230 / var(--tw-border-opacity));
+}
+
 .border-stone-100 {
   --tw-border-opacity: 1;
   border-color: rgb(245 245 244 / var(--tw-border-opacity));
@@ -996,11 +1111,6 @@ video {
   border-color: rgb(250 250 249 / var(--tw-border-opacity));
 }
 
-.border-white {
-  --tw-border-opacity: 1;
-  border-color: rgb(255 255 255 / var(--tw-border-opacity));
-}
-
 .border-cyan-500 {
   --tw-border-opacity: 1;
   border-color: rgb(6 182 212 / var(--tw-border-opacity));
@@ -1021,9 +1131,9 @@ video {
   background-color: rgb(255 255 255 / var(--tw-bg-opacity));
 }
 
-.bg-stone-700 {
+.bg-rose-500 {
   --tw-bg-opacity: 1;
-  background-color: rgb(68 64 60 / var(--tw-bg-opacity));
+  background-color: rgb(244 63 94 / var(--tw-bg-opacity));
 }
 
 .bg-teal-500 {
@@ -1031,16 +1141,16 @@ video {
   background-color: rgb(20 184 166 / var(--tw-bg-opacity));
 }
 
-.bg-rose-500 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(244 63 94 / var(--tw-bg-opacity));
-}
-
 .bg-stone-100 {
   --tw-bg-opacity: 1;
   background-color: rgb(245 245 244 / var(--tw-bg-opacity));
 }
 
+.bg-stone-700 {
+  --tw-bg-opacity: 1;
+  background-color: rgb(68 64 60 / var(--tw-bg-opacity));
+}
+
 .bg-red-100 {
   --tw-bg-opacity: 1;
   background-color: rgb(254 226 226 / var(--tw-bg-opacity));
@@ -1051,11 +1161,20 @@ video {
   background-color: rgb(231 229 228 / var(--tw-bg-opacity));
 }
 
+.bg-stone-900 {
+  --tw-bg-opacity: 1;
+  background-color: rgb(28 25 23 / var(--tw-bg-opacity));
+}
+
 .bg-stone-50 {
   --tw-bg-opacity: 1;
   background-color: rgb(250 250 249 / var(--tw-bg-opacity));
 }
 
+.bg-opacity-90 {
+  --tw-bg-opacity: 0.9;
+}
+
 .bg-clip-padding {
   background-clip: padding-box;
 }
@@ -1064,6 +1183,14 @@ video {
   background-position: center;
 }
 
+.p-2 {
+  padding: 0.5rem;
+}
+
+.p-8 {
+  padding: 2rem;
+}
+
 .p-4 {
   padding: 1rem;
 }
@@ -1072,18 +1199,14 @@ video {
   padding: 0.75rem;
 }
 
-.p-8 {
-  padding: 2rem;
+.p-6 {
+  padding: 1.5rem;
 }
 
 .p-1 {
   padding: 0.25rem;
 }
 
-.p-2 {
-  padding: 0.5rem;
-}
-
 .py-5 {
   padding-top: 1.25rem;
   padding-bottom: 1.25rem;
@@ -1114,9 +1237,9 @@ video {
   padding-right: 0.5rem;
 }
 
-.py-2 {
-  padding-top: 0.5rem;
-  padding-bottom: 0.5rem;
+.px-1 {
+  padding-left: 0.25rem;
+  padding-right: 0.25rem;
 }
 
 .px-4 {
@@ -1124,6 +1247,21 @@ video {
   padding-right: 1rem;
 }
 
+.py-2 {
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+}
+
+.px-8 {
+  padding-left: 2rem;
+  padding-right: 2rem;
+}
+
+.py-4 {
+  padding-top: 1rem;
+  padding-bottom: 1rem;
+}
+
 .pl-3 {
   padding-left: 0.75rem;
 }
@@ -1148,6 +1286,10 @@ video {
   padding-right: 0.75rem;
 }
 
+.pt-2 {
+  padding-top: 0.5rem;
+}
+
 .pl-8 {
   padding-left: 2rem;
 }
@@ -1160,10 +1302,26 @@ video {
   padding-bottom: 0.75rem;
 }
 
+.text-left {
+  text-align: left;
+}
+
+.text-center {
+  text-align: center;
+}
+
 .text-right {
   text-align: right;
 }
 
+.text-justify {
+  text-align: justify;
+}
+
+.align-middle {
+  vertical-align: middle;
+}
+
 .text-6xl {
   font-size: 3.75rem;
   line-height: 1;
@@ -1267,14 +1425,34 @@ video {
   color: rgb(13 148 136 / var(--tw-text-opacity));
 }
 
+.text-black {
+  --tw-text-opacity: 1;
+  color: rgb(0 0 0 / var(--tw-text-opacity));
+}
+
+.text-rose-500 {
+  --tw-text-opacity: 1;
+  color: rgb(244 63 94 / var(--tw-text-opacity));
+}
+
+.text-cyan-500 {
+  --tw-text-opacity: 1;
+  color: rgb(6 182 212 / var(--tw-text-opacity));
+}
+
 .underline {
   -webkit-text-decoration-line: underline;
           text-decoration-line: underline;
 }
 
-.no-underline {
-  -webkit-text-decoration-line: none;
-          text-decoration-line: none;
+.opacity-0 {
+  opacity: 0;
+}
+
+.shadow-lg {
+  --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
+  --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
 }
 
 .shadow {
@@ -1283,20 +1461,16 @@ video {
   box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
 }
 
+.shadow-md {
+  --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
+  --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
 .outline {
   outline-style: solid;
 }
 
-.blur {
-  --tw-blur: blur(8px);
-  filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
-}
-
-.drop-shadow-md {
-  --tw-drop-shadow: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06));
-  filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
-}
-
 .filter {
   filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
 }
@@ -1342,6 +1516,11 @@ video {
   border-color: rgb(68 64 60 / var(--tw-border-opacity));
 }
 
+.hover\:border-rose-500:hover {
+  --tw-border-opacity: 1;
+  border-color: rgb(244 63 94 / var(--tw-border-opacity));
+}
+
 .hover\:border-teal-500:hover {
   --tw-border-opacity: 1;
   border-color: rgb(20 184 166 / var(--tw-border-opacity));
@@ -1362,21 +1541,46 @@ video {
   background-color: rgb(13 148 136 / var(--tw-bg-opacity));
 }
 
+.hover\:bg-rose-500:hover {
+  --tw-bg-opacity: 1;
+  background-color: rgb(244 63 94 / var(--tw-bg-opacity));
+}
+
+.hover\:bg-stone-300:hover {
+  --tw-bg-opacity: 1;
+  background-color: rgb(214 211 209 / var(--tw-bg-opacity));
+}
+
 .hover\:bg-stone-200:hover {
   --tw-bg-opacity: 1;
   background-color: rgb(231 229 228 / var(--tw-bg-opacity));
 }
 
+.hover\:bg-rose-700:hover {
+  --tw-bg-opacity: 1;
+  background-color: rgb(190 18 60 / var(--tw-bg-opacity));
+}
+
 .hover\:bg-stone-100:hover {
   --tw-bg-opacity: 1;
   background-color: rgb(245 245 244 / var(--tw-bg-opacity));
 }
 
+.hover\:bg-cyan-500:hover {
+  --tw-bg-opacity: 1;
+  background-color: rgb(6 182 212 / var(--tw-bg-opacity));
+}
+
 .hover\:bg-stone-50:hover {
   --tw-bg-opacity: 1;
   background-color: rgb(250 250 249 / var(--tw-bg-opacity));
 }
 
+.hover\:text-white:hover {
+  --tw-text-opacity: 1;
+  color: rgb(255 255 255 / var(--tw-text-opacity));
+}
+
 .hover\:underline:hover {
   -webkit-text-decoration-line: underline;
           text-decoration-line: underline;
@@ -1423,4 +1627,10 @@ video {
     --tw-text-opacity: 1;
     color: rgb(156 163 175 / var(--tw-text-opacity));
   }
+}
+
+@media (min-width: 768px) {
+  .md\:max-w-md {
+    max-width: 28rem;
+  }
 }
\ No newline at end of file
diff --git a/system/typemill/author/js/codejar.js b/system/typemill/author/js/codejar.js
deleted file mode 100644
index 8260b3f..0000000
--- a/system/typemill/author/js/codejar.js
+++ /dev/null
@@ -1,449 +0,0 @@
-const globalWindow = window;
-export function CodeJar(editor, highlight, opt = {}) {
-    const options = Object.assign({ tab: '\t', indentOn: /[({\[]$/, moveToNewLine: /^[)}\]]/, spellcheck: false, catchTab: true, preserveIdent: true, addClosing: true, history: true, window: globalWindow }, opt);
-    const window = options.window;
-    const document = window.document;
-    let listeners = [];
-    let history = [];
-    let at = -1;
-    let focus = false;
-    let callback;
-    let prev; // code content prior keydown event
-    editor.setAttribute('contenteditable', 'plaintext-only');
-    editor.setAttribute('spellcheck', options.spellcheck ? 'true' : 'false');
-    editor.style.outline = 'none';
-    editor.style.overflowWrap = 'break-word';
-    editor.style.overflowY = 'auto';
-    editor.style.whiteSpace = 'pre-wrap';
-    let isLegacy = false; // true if plaintext-only is not supported
-    highlight(editor);
-    if (editor.contentEditable !== 'plaintext-only')
-        isLegacy = true;
-    if (isLegacy)
-        editor.setAttribute('contenteditable', 'true');
-    const debounceHighlight = debounce(() => {
-        const pos = save();
-        highlight(editor, pos);
-        restore(pos);
-    }, 30);
-    let recording = false;
-    const shouldRecord = (event) => {
-        return !isUndo(event) && !isRedo(event)
-            && event.key !== 'Meta'
-            && event.key !== 'Control'
-            && event.key !== 'Alt'
-            && !event.key.startsWith('Arrow');
-    };
-    const debounceRecordHistory = debounce((event) => {
-        if (shouldRecord(event)) {
-            recordHistory();
-            recording = false;
-        }
-    }, 300);
-    const on = (type, fn) => {
-        listeners.push([type, fn]);
-        editor.addEventListener(type, fn);
-    };
-    on('keydown', event => {
-        if (event.defaultPrevented)
-            return;
-        prev = toString();
-        if (options.preserveIdent)
-            handleNewLine(event);
-        else
-            legacyNewLineFix(event);
-        if (options.catchTab)
-            handleTabCharacters(event);
-        if (options.addClosing)
-            handleSelfClosingCharacters(event);
-        if (options.history) {
-            handleUndoRedo(event);
-            if (shouldRecord(event) && !recording) {
-                recordHistory();
-                recording = true;
-            }
-        }
-        if (isLegacy)
-            restore(save());
-    });
-    on('keyup', event => {
-        if (event.defaultPrevented)
-            return;
-        if (event.isComposing)
-            return;
-        if (prev !== toString())
-            debounceHighlight();
-        debounceRecordHistory(event);
-        if (callback)
-            callback(toString());
-    });
-    on('focus', _event => {
-        focus = true;
-    });
-    on('blur', _event => {
-        focus = false;
-    });
-    on('paste', event => {
-        recordHistory();
-        handlePaste(event);
-        recordHistory();
-        if (callback)
-            callback(toString());
-    });
-    function save() {
-        const s = getSelection();
-        const pos = { start: 0, end: 0, dir: undefined };
-        let { anchorNode, anchorOffset, focusNode, focusOffset } = s;
-        if (!anchorNode || !focusNode)
-            throw 'error1';
-        // Selection anchor and focus are expected to be text nodes,
-        // so normalize them.
-        if (anchorNode.nodeType === Node.ELEMENT_NODE) {
-            const node = document.createTextNode('');
-            anchorNode.insertBefore(node, anchorNode.childNodes[anchorOffset]);
-            anchorNode = node;
-            anchorOffset = 0;
-        }
-        if (focusNode.nodeType === Node.ELEMENT_NODE) {
-            const node = document.createTextNode('');
-            focusNode.insertBefore(node, focusNode.childNodes[focusOffset]);
-            focusNode = node;
-            focusOffset = 0;
-        }
-        visit(editor, el => {
-            if (el === anchorNode && el === focusNode) {
-                pos.start += anchorOffset;
-                pos.end += focusOffset;
-                pos.dir = anchorOffset <= focusOffset ? '->' : '<-';
-                return 'stop';
-            }
-            if (el === anchorNode) {
-                pos.start += anchorOffset;
-                if (!pos.dir) {
-                    pos.dir = '->';
-                }
-                else {
-                    return 'stop';
-                }
-            }
-            else if (el === focusNode) {
-                pos.end += focusOffset;
-                if (!pos.dir) {
-                    pos.dir = '<-';
-                }
-                else {
-                    return 'stop';
-                }
-            }
-            if (el.nodeType === Node.TEXT_NODE) {
-                if (pos.dir != '->')
-                    pos.start += el.nodeValue.length;
-                if (pos.dir != '<-')
-                    pos.end += el.nodeValue.length;
-            }
-        });
-        // collapse empty text nodes
-        editor.normalize();
-        return pos;
-    }
-    function restore(pos) {
-        const s = getSelection();
-        let startNode, startOffset = 0;
-        let endNode, endOffset = 0;
-        if (!pos.dir)
-            pos.dir = '->';
-        if (pos.start < 0)
-            pos.start = 0;
-        if (pos.end < 0)
-            pos.end = 0;
-        // Flip start and end if the direction reversed
-        if (pos.dir == '<-') {
-            const { start, end } = pos;
-            pos.start = end;
-            pos.end = start;
-        }
-        let current = 0;
-        visit(editor, el => {
-            if (el.nodeType !== Node.TEXT_NODE)
-                return;
-            const len = (el.nodeValue || '').length;
-            if (current + len > pos.start) {
-                if (!startNode) {
-                    startNode = el;
-                    startOffset = pos.start - current;
-                }
-                if (current + len > pos.end) {
-                    endNode = el;
-                    endOffset = pos.end - current;
-                    return 'stop';
-                }
-            }
-            current += len;
-        });
-        if (!startNode)
-            startNode = editor, startOffset = editor.childNodes.length;
-        if (!endNode)
-            endNode = editor, endOffset = editor.childNodes.length;
-        // Flip back the selection
-        if (pos.dir == '<-') {
-            [startNode, startOffset, endNode, endOffset] = [endNode, endOffset, startNode, startOffset];
-        }
-        s.setBaseAndExtent(startNode, startOffset, endNode, endOffset);
-    }
-    function beforeCursor() {
-        const s = getSelection();
-        const r0 = s.getRangeAt(0);
-        const r = document.createRange();
-        r.selectNodeContents(editor);
-        r.setEnd(r0.startContainer, r0.startOffset);
-        return r.toString();
-    }
-    function afterCursor() {
-        const s = getSelection();
-        const r0 = s.getRangeAt(0);
-        const r = document.createRange();
-        r.selectNodeContents(editor);
-        r.setStart(r0.endContainer, r0.endOffset);
-        return r.toString();
-    }
-    function handleNewLine(event) {
-        if (event.key === 'Enter') {
-            const before = beforeCursor();
-            const after = afterCursor();
-            let [padding] = findPadding(before);
-            let newLinePadding = padding;
-            // If last symbol is "{" ident new line
-            if (options.indentOn.test(before)) {
-                newLinePadding += options.tab;
-            }
-            // Preserve padding
-            if (newLinePadding.length > 0) {
-                preventDefault(event);
-                event.stopPropagation();
-                insert('\n' + newLinePadding);
-            }
-            else {
-                legacyNewLineFix(event);
-            }
-            // Place adjacent "}" on next line
-            if (newLinePadding !== padding && options.moveToNewLine.test(after)) {
-                const pos = save();
-                insert('\n' + padding);
-                restore(pos);
-            }
-        }
-    }
-    function legacyNewLineFix(event) {
-        // Firefox does not support plaintext-only mode
-        // and puts 

on Enter. Let's help. - if (isLegacy && event.key === 'Enter') { - preventDefault(event); - event.stopPropagation(); - if (afterCursor() == '') { - insert('\n '); - const pos = save(); - pos.start = --pos.end; - restore(pos); - } - else { - insert('\n'); - } - } - } - function handleSelfClosingCharacters(event) { - const open = `([{'"`; - const close = `)]}'"`; - const codeAfter = afterCursor(); - const codeBefore = beforeCursor(); - const escapeCharacter = codeBefore.substr(codeBefore.length - 1) === '\\'; - const charAfter = codeAfter.substr(0, 1); - if (close.includes(event.key) && !escapeCharacter && charAfter === event.key) { - // We already have closing char next to cursor. - // Move one char to right. - const pos = save(); - preventDefault(event); - pos.start = ++pos.end; - restore(pos); - } - else if (open.includes(event.key) - && !escapeCharacter - && (`"'`.includes(event.key) || ['', ' ', '\n'].includes(charAfter))) { - preventDefault(event); - const pos = save(); - const wrapText = pos.start == pos.end ? '' : getSelection().toString(); - const text = event.key + wrapText + close[open.indexOf(event.key)]; - insert(text); - pos.start++; - pos.end++; - restore(pos); - } - } - function handleTabCharacters(event) { - if (event.key === 'Tab') { - preventDefault(event); - if (event.shiftKey) { - const before = beforeCursor(); - let [padding, start,] = findPadding(before); - if (padding.length > 0) { - const pos = save(); - // Remove full length tab or just remaining padding - const len = Math.min(options.tab.length, padding.length); - restore({ start, end: start + len }); - document.execCommand('delete'); - pos.start -= len; - pos.end -= len; - restore(pos); - } - } - else { - insert(options.tab); - } - } - } - function handleUndoRedo(event) { - if (isUndo(event)) { - preventDefault(event); - at--; - const record = history[at]; - if (record) { - editor.innerHTML = record.html; - restore(record.pos); - } - if (at < 0) - at = 0; - } - if (isRedo(event)) { - preventDefault(event); - at++; - const record = history[at]; - if (record) { - editor.innerHTML = record.html; - restore(record.pos); - } - if (at >= history.length) - at--; - } - } - function recordHistory() { - if (!focus) - return; - const html = editor.innerHTML; - const pos = save(); - const lastRecord = history[at]; - if (lastRecord) { - if (lastRecord.html === html - && lastRecord.pos.start === pos.start - && lastRecord.pos.end === pos.end) - return; - } - at++; - history[at] = { html, pos }; - history.splice(at + 1); - const maxHistory = 300; - if (at > maxHistory) { - at = maxHistory; - history.splice(0, 1); - } - } - function handlePaste(event) { - preventDefault(event); - const text = (event.originalEvent || event) - .clipboardData - .getData('text/plain') - .replace(/\r/g, ''); - const pos = save(); - insert(text); - highlight(editor); - restore({ - start: Math.min(pos.start, pos.end) + text.length, - end: Math.min(pos.start, pos.end) + text.length, - dir: '<-', - }); - } - function visit(editor, visitor) { - const queue = []; - if (editor.firstChild) - queue.push(editor.firstChild); - let el = queue.pop(); - while (el) { - if (visitor(el) === 'stop') - break; - if (el.nextSibling) - queue.push(el.nextSibling); - if (el.firstChild) - queue.push(el.firstChild); - el = queue.pop(); - } - } - function isCtrl(event) { - return event.metaKey || event.ctrlKey; - } - function isUndo(event) { - return isCtrl(event) && !event.shiftKey && event.code === 'KeyZ'; - } - function isRedo(event) { - return isCtrl(event) && event.shiftKey && event.code === 'KeyZ'; - } - function insert(text) { - text = text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - document.execCommand('insertHTML', false, text); - } - function debounce(cb, wait) { - let timeout = 0; - return (...args) => { - clearTimeout(timeout); - timeout = window.setTimeout(() => cb(...args), wait); - }; - } - function findPadding(text) { - // Find beginning of previous line. - let i = text.length - 1; - while (i >= 0 && text[i] !== '\n') - i--; - i++; - // Find padding of the line. - let j = i; - while (j < text.length && /[ \t]/.test(text[j])) - j++; - return [text.substring(i, j) || '', i, j]; - } - function toString() { - return editor.textContent || ''; - } - function preventDefault(event) { - event.preventDefault(); - } - function getSelection() { - var _a; - if (((_a = editor.parentNode) === null || _a === void 0 ? void 0 : _a.nodeType) == Node.DOCUMENT_FRAGMENT_NODE) { - return editor.parentNode.getSelection(); - } - return window.getSelection(); - } - return { - updateOptions(newOptions) { - Object.assign(options, newOptions); - }, - updateCode(code) { - editor.textContent = code; - highlight(editor); - }, - onUpdate(cb) { - callback = cb; - }, - toString, - save, - restore, - recordHistory, - destroy() { - for (let [type, fn] of listeners) { - editor.removeEventListener(type, fn); - } - }, - }; -} diff --git a/system/typemill/author/js/highlight.min.js b/system/typemill/author/js/highlight.min.js new file mode 100644 index 0000000..476704d --- /dev/null +++ b/system/typemill/author/js/highlight.min.js @@ -0,0 +1,709 @@ +/*! + Highlight.js v11.7.0 (git: 82688fad18) + (c) 2006-2022 undefined and other contributors + License: BSD-3-Clause + */ +var hljs=function(){"use strict";var e={exports:{}};function t(e){ +return e instanceof Map?e.clear=e.delete=e.set=()=>{ +throw Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=()=>{ +throw Error("set is read-only") +}),Object.freeze(e),Object.getOwnPropertyNames(e).forEach((n=>{var i=e[n] +;"object"!=typeof i||Object.isFrozen(i)||t(i)})),e} +e.exports=t,e.exports.default=t;class n{constructor(e){ +void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} +ignoreMatch(){this.isMatchIgnored=!0}}function i(e){ +return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") +}function r(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t] +;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n} +const s=e=>!!e.scope||e.sublanguage&&e.language;class o{constructor(e,t){ +this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){ +this.buffer+=i(e)}openNode(e){if(!s(e))return;let t="" +;t=e.sublanguage?"language-"+e.language:((e,{prefix:t})=>{if(e.includes(".")){ +const n=e.split(".") +;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ") +}return`${t}${e}`})(e.scope,{prefix:this.classPrefix}),this.span(t)} +closeNode(e){s(e)&&(this.buffer+="")}value(){return this.buffer}span(e){ +this.buffer+=``}}const a=(e={})=>{const t={children:[]} +;return Object.assign(t,e),t};class c{constructor(){ +this.rootNode=a(),this.stack=[this.rootNode]}get top(){ +return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ +this.top.children.push(e)}openNode(e){const t=a({scope:e}) +;this.add(t),this.stack.push(t)}closeNode(){ +if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ +for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} +walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){ +return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t), +t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){ +"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ +c._collapse(e)})))}}class l extends c{constructor(e){super(),this.options=e} +addKeyword(e,t){""!==e&&(this.openNode(t),this.addText(e),this.closeNode())} +addText(e){""!==e&&this.add(e)}addSublanguage(e,t){const n=e.root +;n.sublanguage=!0,n.language=t,this.add(n)}toHTML(){ +return new o(this,this.options).value()}finalize(){return!0}}function g(e){ +return e?"string"==typeof e?e:e.source:null}function d(e){return p("(?=",e,")")} +function u(e){return p("(?:",e,")*")}function h(e){return p("(?:",e,")?")} +function p(...e){return e.map((e=>g(e))).join("")}function f(...e){const t=(e=>{ +const t=e[e.length-1] +;return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{} +})(e);return"("+(t.capture?"":"?:")+e.map((e=>g(e))).join("|")+")"} +function b(e){return RegExp(e.toString()+"|").exec("").length-1} +const m=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ +;function E(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n +;let i=g(e),r="";for(;i.length>0;){const e=m.exec(i);if(!e){r+=i;break} +r+=i.substring(0,e.index), +i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?r+="\\"+(Number(e[1])+t):(r+=e[0], +"("===e[0]&&n++)}return r})).map((e=>`(${e})`)).join(t)} +const x="[a-zA-Z]\\w*",w="[a-zA-Z_]\\w*",y="\\b\\d+(\\.\\d+)?",_="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",O="\\b(0b[01]+)",v={ +begin:"\\\\[\\s\\S]",relevance:0},N={scope:"string",begin:"'",end:"'", +illegal:"\\n",contains:[v]},k={scope:"string",begin:'"',end:'"',illegal:"\\n", +contains:[v]},M=(e,t,n={})=>{const i=r({scope:"comment",begin:e,end:t, +contains:[]},n);i.contains.push({scope:"doctag", +begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", +end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) +;const s=f("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) +;return i.contains.push({begin:p(/[ ]+/,"(",s,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i +},S=M("//","$"),R=M("/\\*","\\*/"),j=M("#","$");var A=Object.freeze({ +__proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:x,UNDERSCORE_IDENT_RE:w, +NUMBER_RE:y,C_NUMBER_RE:_,BINARY_NUMBER_RE:O, +RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", +SHEBANG:(e={})=>{const t=/^#![ ]*\// +;return e.binary&&(e.begin=p(t,/.*\b/,e.binary,/\b.*/)),r({scope:"meta",begin:t, +end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)}, +BACKSLASH_ESCAPE:v,APOS_STRING_MODE:N,QUOTE_STRING_MODE:k,PHRASAL_WORDS_MODE:{ +begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ +},COMMENT:M,C_LINE_COMMENT_MODE:S,C_BLOCK_COMMENT_MODE:R,HASH_COMMENT_MODE:j, +NUMBER_MODE:{scope:"number",begin:y,relevance:0},C_NUMBER_MODE:{scope:"number", +begin:_,relevance:0},BINARY_NUMBER_MODE:{scope:"number",begin:O,relevance:0}, +REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{scope:"regexp",begin:/\//, +end:/\/[gimuy]*/,illegal:/\n/,contains:[v,{begin:/\[/,end:/\]/,relevance:0, +contains:[v]}]}]},TITLE_MODE:{scope:"title",begin:x,relevance:0}, +UNDERSCORE_TITLE_MODE:{scope:"title",begin:w,relevance:0},METHOD_GUARD:{ +begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0},END_SAME_AS_BEGIN:e=>Object.assign(e,{ +"on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{ +t.data._beginMatch!==e[1]&&t.ignoreMatch()}})});function I(e,t){ +"."===e.input[e.index-1]&&t.ignoreMatch()}function T(e,t){ +void 0!==e.className&&(e.scope=e.className,delete e.className)}function L(e,t){ +t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", +e.__beforeBegin=I,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, +void 0===e.relevance&&(e.relevance=0))}function B(e,t){ +Array.isArray(e.illegal)&&(e.illegal=f(...e.illegal))}function D(e,t){ +if(e.match){ +if(e.begin||e.end)throw Error("begin & end are not supported with match") +;e.begin=e.match,delete e.match}}function H(e,t){ +void 0===e.relevance&&(e.relevance=1)}const P=(e,t)=>{if(!e.beforeMatch)return +;if(e.starts)throw Error("beforeMatch cannot be used with starts") +;const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t] +})),e.keywords=n.keywords,e.begin=p(n.beforeMatch,d(n.begin)),e.starts={ +relevance:0,contains:[Object.assign(n,{endsParent:!0})] +},e.relevance=0,delete n.beforeMatch +},C=["of","and","for","in","not","or","if","then","parent","list","value"] +;function $(e,t,n="keyword"){const i=Object.create(null) +;return"string"==typeof e?r(n,e.split(" ")):Array.isArray(e)?r(n,e):Object.keys(e).forEach((n=>{ +Object.assign(i,$(e[n],t,n))})),i;function r(e,n){ +t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|") +;i[n[0]]=[e,U(n[0],n[1])]}))}}function U(e,t){ +return t?Number(t):(e=>C.includes(e.toLowerCase()))(e)?0:1}const z={},K=e=>{ +console.error(e)},W=(e,...t)=>{console.log("WARN: "+e,...t)},X=(e,t)=>{ +z[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),z[`${e}/${t}`]=!0) +},G=Error();function Z(e,t,{key:n}){let i=0;const r=e[n],s={},o={} +;for(let e=1;e<=t.length;e++)o[e+i]=r[e],s[e+i]=!0,i+=b(t[e-1]) +;e[n]=o,e[n]._emit=s,e[n]._multi=!0}function F(e){(e=>{ +e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, +delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ +_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope +}),(e=>{if(Array.isArray(e.begin)){ +if(e.skip||e.excludeBegin||e.returnBegin)throw K("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), +G +;if("object"!=typeof e.beginScope||null===e.beginScope)throw K("beginScope must be object"), +G;Z(e,e.begin,{key:"beginScope"}),e.begin=E(e.begin,{joinWith:""})}})(e),(e=>{ +if(Array.isArray(e.end)){ +if(e.skip||e.excludeEnd||e.returnEnd)throw K("skip, excludeEnd, returnEnd not compatible with endScope: {}"), +G +;if("object"!=typeof e.endScope||null===e.endScope)throw K("endScope must be object"), +G;Z(e,e.end,{key:"endScope"}),e.end=E(e.end,{joinWith:""})}})(e)}function V(e){ +function t(t,n){ +return RegExp(g(t),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(n?"g":"")) +}class n{constructor(){ +this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} +addRule(e,t){ +t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]), +this.matchAt+=b(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) +;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(E(e,{joinWith:"|" +}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex +;const t=this.matcherRe.exec(e);if(!t)return null +;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n] +;return t.splice(0,n),Object.assign(t,i)}}class i{constructor(){ +this.rules=[],this.multiRegexes=[], +this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ +if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n +;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))), +t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){ +return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){ +this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){ +const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex +;let n=t.exec(e) +;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{ +const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)} +return n&&(this.regexIndex+=n.position+1, +this.regexIndex===this.count&&this.considerAll()),n}} +if(e.compilerExtensions||(e.compilerExtensions=[]), +e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") +;return e.classNameAliases=r(e.classNameAliases||{}),function n(s,o){const a=s +;if(s.isCompiled)return a +;[T,D,F,P].forEach((e=>e(s,o))),e.compilerExtensions.forEach((e=>e(s,o))), +s.__beforeBegin=null,[L,B,H].forEach((e=>e(s,o))),s.isCompiled=!0;let c=null +;return"object"==typeof s.keywords&&s.keywords.$pattern&&(s.keywords=Object.assign({},s.keywords), +c=s.keywords.$pattern, +delete s.keywords.$pattern),c=c||/\w+/,s.keywords&&(s.keywords=$(s.keywords,e.case_insensitive)), +a.keywordPatternRe=t(c,!0), +o&&(s.begin||(s.begin=/\B|\b/),a.beginRe=t(a.begin),s.end||s.endsWithParent||(s.end=/\B|\b/), +s.end&&(a.endRe=t(a.end)), +a.terminatorEnd=g(a.end)||"",s.endsWithParent&&o.terminatorEnd&&(a.terminatorEnd+=(s.end?"|":"")+o.terminatorEnd)), +s.illegal&&(a.illegalRe=t(s.illegal)), +s.contains||(s.contains=[]),s.contains=[].concat(...s.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>r(e,{ +variants:null},t)))),e.cachedVariants?e.cachedVariants:q(e)?r(e,{ +starts:e.starts?r(e.starts):null +}):Object.isFrozen(e)?r(e):e))("self"===e?s:e)))),s.contains.forEach((e=>{n(e,a) +})),s.starts&&n(s.starts,o),a.matcher=(e=>{const t=new i +;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin" +}))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end" +}),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(a),a}(e)}function q(e){ +return!!e&&(e.endsWithParent||q(e.starts))}class J extends Error{ +constructor(e,t){super(e),this.name="HTMLInjectionError",this.html=t}} +const Y=i,Q=r,ee=Symbol("nomatch");var te=(t=>{ +const i=Object.create(null),r=Object.create(null),s=[];let o=!0 +;const a="Could not find the language '{}', did you forget to load/include a language module?",c={ +disableAutodetect:!0,name:"Plain text",contains:[]};let g={ +ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, +languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", +cssSelector:"pre code",languages:null,__emitter:l};function b(e){ +return g.noHighlightRe.test(e)}function m(e,t,n){let i="",r="" +;"object"==typeof t?(i=e, +n=t.ignoreIllegals,r=t.language):(X("10.7.0","highlight(lang, code, ...args) has been deprecated."), +X("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), +r=e,i=t),void 0===n&&(n=!0);const s={code:i,language:r};k("before:highlight",s) +;const o=s.result?s.result:E(s.language,s.code,n) +;return o.code=s.code,k("after:highlight",o),o}function E(e,t,r,s){ +const c=Object.create(null);function l(){if(!N.keywords)return void M.addText(S) +;let e=0;N.keywordPatternRe.lastIndex=0;let t=N.keywordPatternRe.exec(S),n="" +;for(;t;){n+=S.substring(e,t.index) +;const r=y.case_insensitive?t[0].toLowerCase():t[0],s=(i=r,N.keywords[i]);if(s){ +const[e,i]=s +;if(M.addText(n),n="",c[r]=(c[r]||0)+1,c[r]<=7&&(R+=i),e.startsWith("_"))n+=t[0];else{ +const n=y.classNameAliases[e]||e;M.addKeyword(t[0],n)}}else n+=t[0] +;e=N.keywordPatternRe.lastIndex,t=N.keywordPatternRe.exec(S)}var i +;n+=S.substring(e),M.addText(n)}function d(){null!=N.subLanguage?(()=>{ +if(""===S)return;let e=null;if("string"==typeof N.subLanguage){ +if(!i[N.subLanguage])return void M.addText(S) +;e=E(N.subLanguage,S,!0,k[N.subLanguage]),k[N.subLanguage]=e._top +}else e=x(S,N.subLanguage.length?N.subLanguage:null) +;N.relevance>0&&(R+=e.relevance),M.addSublanguage(e._emitter,e.language) +})():l(),S=""}function u(e,t){let n=1;const i=t.length-1;for(;n<=i;){ +if(!e._emit[n]){n++;continue}const i=y.classNameAliases[e[n]]||e[n],r=t[n] +;i?M.addKeyword(r,i):(S=r,l(),S=""),n++}}function h(e,t){ +return e.scope&&"string"==typeof e.scope&&M.openNode(y.classNameAliases[e.scope]||e.scope), +e.beginScope&&(e.beginScope._wrap?(M.addKeyword(S,y.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), +S=""):e.beginScope._multi&&(u(e.beginScope,t),S="")),N=Object.create(e,{parent:{ +value:N}}),N}function p(e,t,i){let r=((e,t)=>{const n=e&&e.exec(t) +;return n&&0===n.index})(e.endRe,i);if(r){if(e["on:end"]){const i=new n(e) +;e["on:end"](t,i),i.isMatchIgnored&&(r=!1)}if(r){ +for(;e.endsParent&&e.parent;)e=e.parent;return e}} +if(e.endsWithParent)return p(e.parent,t,i)}function f(e){ +return 0===N.matcher.regexIndex?(S+=e[0],1):(I=!0,0)}function b(e){ +const n=e[0],i=t.substring(e.index),r=p(N,e,i);if(!r)return ee;const s=N +;N.endScope&&N.endScope._wrap?(d(), +M.addKeyword(n,N.endScope._wrap)):N.endScope&&N.endScope._multi?(d(), +u(N.endScope,e)):s.skip?S+=n:(s.returnEnd||s.excludeEnd||(S+=n), +d(),s.excludeEnd&&(S=n));do{ +N.scope&&M.closeNode(),N.skip||N.subLanguage||(R+=N.relevance),N=N.parent +}while(N!==r.parent);return r.starts&&h(r.starts,e),s.returnEnd?0:n.length} +let m={};function w(i,s){const a=s&&s[0];if(S+=i,null==a)return d(),0 +;if("begin"===m.type&&"end"===s.type&&m.index===s.index&&""===a){ +if(S+=t.slice(s.index,s.index+1),!o){const t=Error(`0 width match regex (${e})`) +;throw t.languageName=e,t.badRule=m.rule,t}return 1} +if(m=s,"begin"===s.type)return(e=>{ +const t=e[0],i=e.rule,r=new n(i),s=[i.__beforeBegin,i["on:begin"]] +;for(const n of s)if(n&&(n(e,r),r.isMatchIgnored))return f(t) +;return i.skip?S+=t:(i.excludeBegin&&(S+=t), +d(),i.returnBegin||i.excludeBegin||(S=t)),h(i,e),i.returnBegin?0:t.length})(s) +;if("illegal"===s.type&&!r){ +const e=Error('Illegal lexeme "'+a+'" for mode "'+(N.scope||"")+'"') +;throw e.mode=N,e}if("end"===s.type){const e=b(s);if(e!==ee)return e} +if("illegal"===s.type&&""===a)return 1 +;if(A>1e5&&A>3*s.index)throw Error("potential infinite loop, way more iterations than matches") +;return S+=a,a.length}const y=O(e) +;if(!y)throw K(a.replace("{}",e)),Error('Unknown language: "'+e+'"') +;const _=V(y);let v="",N=s||_;const k={},M=new g.__emitter(g);(()=>{const e=[] +;for(let t=N;t!==y;t=t.parent)t.scope&&e.unshift(t.scope) +;e.forEach((e=>M.openNode(e)))})();let S="",R=0,j=0,A=0,I=!1;try{ +for(N.matcher.considerAll();;){ +A++,I?I=!1:N.matcher.considerAll(),N.matcher.lastIndex=j +;const e=N.matcher.exec(t);if(!e)break;const n=w(t.substring(j,e.index),e) +;j=e.index+n} +return w(t.substring(j)),M.closeAllNodes(),M.finalize(),v=M.toHTML(),{ +language:e,value:v,relevance:R,illegal:!1,_emitter:M,_top:N}}catch(n){ +if(n.message&&n.message.includes("Illegal"))return{language:e,value:Y(t), +illegal:!0,relevance:0,_illegalBy:{message:n.message,index:j, +context:t.slice(j-100,j+100),mode:n.mode,resultSoFar:v},_emitter:M};if(o)return{ +language:e,value:Y(t),illegal:!1,relevance:0,errorRaised:n,_emitter:M,_top:N} +;throw n}}function x(e,t){t=t||g.languages||Object.keys(i);const n=(e=>{ +const t={value:Y(e),illegal:!1,relevance:0,_top:c,_emitter:new g.__emitter(g)} +;return t._emitter.addText(e),t})(e),r=t.filter(O).filter(N).map((t=>E(t,e,!1))) +;r.unshift(n);const s=r.sort(((e,t)=>{ +if(e.relevance!==t.relevance)return t.relevance-e.relevance +;if(e.language&&t.language){if(O(e.language).supersetOf===t.language)return 1 +;if(O(t.language).supersetOf===e.language)return-1}return 0})),[o,a]=s,l=o +;return l.secondBest=a,l}function w(e){let t=null;const n=(e=>{ +let t=e.className+" ";t+=e.parentNode?e.parentNode.className:"" +;const n=g.languageDetectRe.exec(t);if(n){const t=O(n[1]) +;return t||(W(a.replace("{}",n[1])), +W("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"} +return t.split(/\s+/).find((e=>b(e)||O(e)))})(e);if(b(n))return +;if(k("before:highlightElement",{el:e,language:n +}),e.children.length>0&&(g.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), +console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), +console.warn("The element with unescaped HTML:"), +console.warn(e)),g.throwUnescapedHTML))throw new J("One of your code blocks includes unescaped HTML.",e.innerHTML) +;t=e;const i=t.textContent,s=n?m(i,{language:n,ignoreIllegals:!0}):x(i) +;e.innerHTML=s.value,((e,t,n)=>{const i=t&&r[t]||n +;e.classList.add("hljs"),e.classList.add("language-"+i) +})(e,n,s.language),e.result={language:s.language,re:s.relevance, +relevance:s.relevance},s.secondBest&&(e.secondBest={ +language:s.secondBest.language,relevance:s.secondBest.relevance +}),k("after:highlightElement",{el:e,result:s,text:i})}let y=!1;function _(){ +"loading"!==document.readyState?document.querySelectorAll(g.cssSelector).forEach(w):y=!0 +}function O(e){return e=(e||"").toLowerCase(),i[e]||i[r[e]]} +function v(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ +r[e.toLowerCase()]=t}))}function N(e){const t=O(e) +;return t&&!t.disableAutodetect}function k(e,t){const n=e;s.forEach((e=>{ +e[n]&&e[n](t)}))} +"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ +y&&_()}),!1),Object.assign(t,{highlight:m,highlightAuto:x,highlightAll:_, +highlightElement:w, +highlightBlock:e=>(X("10.7.0","highlightBlock will be removed entirely in v12.0"), +X("10.7.0","Please use highlightElement now."),w(e)),configure:e=>{g=Q(g,e)}, +initHighlighting:()=>{ +_(),X("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, +initHighlightingOnLoad:()=>{ +_(),X("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") +},registerLanguage:(e,n)=>{let r=null;try{r=n(t)}catch(t){ +if(K("Language definition for '{}' could not be registered.".replace("{}",e)), +!o)throw t;K(t),r=c} +r.name||(r.name=e),i[e]=r,r.rawDefinition=n.bind(null,t),r.aliases&&v(r.aliases,{ +languageName:e})},unregisterLanguage:e=>{delete i[e] +;for(const t of Object.keys(r))r[t]===e&&delete r[t]}, +listLanguages:()=>Object.keys(i),getLanguage:O,registerAliases:v, +autoDetection:N,inherit:Q,addPlugin:e=>{(e=>{ +e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{ +e["before:highlightBlock"](Object.assign({block:t.el},t)) +}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{ +e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),s.push(e)} +}),t.debugMode=()=>{o=!1},t.safeMode=()=>{o=!0 +},t.versionString="11.7.0",t.regex={concat:p,lookahead:d,either:f,optional:h, +anyNumberOfTimes:u};for(const t in A)"object"==typeof A[t]&&e.exports(A[t]) +;return Object.assign(t,A),t})({});return te}() +;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);/*! `javascript` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict" +;const e="[A-Za-z$_][0-9A-Za-z$_]*",n=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],a=["true","false","null","undefined","NaN","Infinity"],t=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],s=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],r=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],c=["arguments","this","super","console","window","document","localStorage","module","global"],i=[].concat(r,t,s) +;return o=>{const l=o.regex,b=e,d={begin:/<[A-Za-z0-9\\._:-]+/, +end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ +const a=e[0].length+e.index,t=e.input[a] +;if("<"===t||","===t)return void n.ignoreMatch();let s +;">"===t&&(((e,{after:n})=>{const a="",M={ +match:[/const|var|let/,/\s+/,b,/\s*/,/=\s*/,/(async\s*)?/,l.lookahead(C)], +keywords:"async",className:{1:"keyword",3:"title.function"},contains:[S]} +;return{name:"Javascript",aliases:["js","jsx","mjs","cjs"],keywords:g,exports:{ +PARAMS_CONTAINS:p,CLASS_REFERENCE:R},illegal:/#(?![$_A-z])/, +contains:[o.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ +label:"use_strict",className:"meta",relevance:10, +begin:/^\s*['"]use (strict|asm)['"]/ +},o.APOS_STRING_MODE,o.QUOTE_STRING_MODE,y,N,_,h,{match:/\$\d+/},E,R,{ +className:"attr",begin:b+l.lookahead(":"),relevance:0},M,{ +begin:"("+o.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", +keywords:"return throw case",relevance:0,contains:[h,o.REGEXP_MODE,{ +className:"function",begin:C,returnBegin:!0,end:"\\s*=>",contains:[{ +className:"params",variants:[{begin:o.UNDERSCORE_IDENT_RE,relevance:0},{ +className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0, +excludeEnd:!0,keywords:g,contains:p}]}]},{begin:/,/,relevance:0},{match:/\s+/, +relevance:0},{variants:[{begin:"<>",end:""},{ +match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:d.begin, +"on:begin":d.isTrulyOpeningTag,end:d.end}],subLanguage:"xml",contains:[{ +begin:d.begin,end:d.end,skip:!0,contains:["self"]}]}]},O,{ +beginKeywords:"while if switch catch for"},{ +begin:"\\b(?!function)"+o.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", +returnBegin:!0,label:"func.def",contains:[S,o.inherit(o.TITLE_MODE,{begin:b, +className:"title.function"})]},{match:/\.\.\./,relevance:0},x,{match:"\\$"+b, +relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, +contains:[S]},k,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},w,T,{match:/\$[(.]/}]}}})() +;hljs.registerLanguage("javascript",e)})();/*! `xml` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const a=e.regex,n=a.concat(/[\p{L}_]/u,a.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),s={ +className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},t={begin:/\s/, +contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}] +},i=e.inherit(t,{begin:/\(/,end:/\)/}),c=e.inherit(e.APOS_STRING_MODE,{ +className:"string"}),l=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),r={ +endsWithParent:!0,illegal:/`]+/}]}]}]};return{ +name:"HTML, XML", +aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"], +case_insensitive:!0,unicodeRegex:!0,contains:[{className:"meta",begin://,relevance:10,contains:[t,l,c,i,{begin:/\[/,end:/\]/,contains:[{ +className:"meta",begin://,contains:[t,i,l,c]}]}] +},e.COMMENT(//,{relevance:10}),{begin://, +relevance:10},s,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/, +relevance:10,contains:[l]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag", +begin:/)/,end:/>/,keywords:{name:"style"},contains:[r],starts:{ +end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag", +begin:/)/,end:/>/,keywords:{name:"script"},contains:[r],starts:{ +end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{ +className:"tag",begin:/<>|<\/>/},{className:"tag", +begin:a.concat(//,/>/,/\s/)))), +end:/\/?>/,contains:[{className:"name",begin:n,relevance:0,starts:r}]},{ +className:"tag",begin:a.concat(/<\//,a.lookahead(a.concat(n,/>/))),contains:[{ +className:"name",begin:n,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}} +})();hljs.registerLanguage("xml",e)})();/*! `twig` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const a=e.regex,t=["absolute_url","asset|0","asset_version","attribute","block","constant","controller|0","country_timezones","csrf_token","cycle","date","dump","expression","form|0","form_end","form_errors","form_help","form_label","form_rest","form_row","form_start","form_widget","html_classes","include","is_granted","logout_path","logout_url","max","min","parent","path|0","random","range","relative_path","render","render_esi","source","template_from_string","url|0"] +;let r=["apply","autoescape","block","cache","deprecated","do","embed","extends","filter","flush","for","form_theme","from","if","import","include","macro","sandbox","set","stopwatch","trans","trans_default_domain","transchoice","use","verbatim","with"] +;r=r.concat(r.map((e=>"end"+e)));const n={scope:"string",variants:[{begin:/'/, +end:/'/},{begin:/"/,end:/"/}]},o={scope:"number",match:/\d+/},s={begin:/\(/, +end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:[n,o]},c={ +beginKeywords:t.join(" "),keywords:{name:t},relevance:0,contains:[s]},m={ +match:/\|(?=[A-Za-z_]+:?)/,beginScope:"punctuation",relevance:0,contains:[{ +match:/[A-Za-z_]+:?/, +keywords:["abs","abbr_class","abbr_method","batch","capitalize","column","convert_encoding","country_name","currency_name","currency_symbol","data_uri","date","date_modify","default","escape","file_excerpt","file_link","file_relative","filter","first","format","format_args","format_args_as_text","format_currency","format_date","format_datetime","format_file","format_file_from_text","format_number","format_time","html_to_markdown","humanize","inky_to_html","inline_css","join","json_encode","keys","language_name","last","length","locale_name","lower","map","markdown","markdown_to_html","merge","nl2br","number_format","raw","reduce","replace","reverse","round","slice","slug","sort","spaceless","split","striptags","timezone_name","title","trans","transchoice","trim","u|0","upper","url_encode","yaml_dump","yaml_encode"] +}]},i=(e,{relevance:t})=>({beginScope:{1:"template-tag",3:"name"}, +relevance:t||2,endScope:"template-tag",begin:[/\{%/,/\s*/,a.either(...e)], +end:/%\}/,keywords:"in",contains:[m,c,n,o]}),l=i(r,{relevance:2 +}),_=i([/[a-z_]+/],{relevance:1});return{name:"Twig",aliases:["craftcms"], +case_insensitive:!0,subLanguage:"xml",contains:[e.COMMENT(/\{#/,/#\}/),l,_,{ +className:"template-variable",begin:/\{\{/,end:/\}\}/,contains:["self",m,c,n,o] +}]}}})();hljs.registerLanguage("twig",e)})();/*! `json` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const a=["true","false","null"],n={ +scope:"literal",beginKeywords:a.join(" ")};return{name:"JSON",keywords:{ +literal:a},contains:[{className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/, +relevance:1.01},{match:/[{}[\],:]/,className:"punctuation",relevance:0 +},e.QUOTE_STRING_MODE,n,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE], +illegal:"\\S"}}})();hljs.registerLanguage("json",e)})();/*! `graphql` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const a=e.regex;return{name:"GraphQL", +aliases:["gql"],case_insensitive:!0,disableAutodetect:!1,keywords:{ +keyword:["query","mutation","subscription","type","input","schema","directive","interface","union","scalar","fragment","enum","on"], +literal:["true","false","null"]}, +contains:[e.HASH_COMMENT_MODE,e.QUOTE_STRING_MODE,e.NUMBER_MODE,{ +scope:"punctuation",match:/[.]{3}/,relevance:0},{scope:"punctuation", +begin:/[\!\(\)\:\=\[\]\{\|\}]{1}/,relevance:0},{scope:"variable",begin:/\$/, +end:/\W/,excludeEnd:!0,relevance:0},{scope:"meta",match:/@\w+/,excludeEnd:!0},{ +scope:"symbol",begin:a.concat(/[_A-Za-z][_0-9A-Za-z]*/,a.lookahead(/\s*:/)), +relevance:0}],illegal:[/[;<']/,/BEGIN/]}}})();hljs.registerLanguage("graphql",e) +})();/*! `css` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict" +;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],i=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],r=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],t=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],o=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse() +;return n=>{const a=n.regex,l=(e=>({IMPORTANT:{scope:"meta",begin:"!important"}, +BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number", +begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},FUNCTION_DISPATCH:{ +className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{ +scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", +contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ +scope:"number", +begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", +relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z][A-Za-z0-9_-]*/} +}))(n),s=[n.APOS_STRING_MODE,n.QUOTE_STRING_MODE];return{name:"CSS", +case_insensitive:!0,illegal:/[=|'\$]/,keywords:{keyframePosition:"from to"}, +classNameAliases:{keyframePosition:"selector-tag"},contains:[l.BLOCK_COMMENT,{ +begin:/-(webkit|moz|ms|o)-(?=[a-z])/},l.CSS_NUMBER_MODE,{ +className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0},{ +className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0 +},l.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{ +begin:":("+r.join("|")+")"},{begin:":(:)?("+t.join("|")+")"}]},l.CSS_VARIABLE,{ +className:"attribute",begin:"\\b("+o.join("|")+")\\b"},{begin:/:/,end:/[;}{]/, +contains:[l.BLOCK_COMMENT,l.HEXCOLOR,l.IMPORTANT,l.CSS_NUMBER_MODE,...s,{ +begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri" +},contains:[...s,{className:"string",begin:/[^)]/,endsWithParent:!0, +excludeEnd:!0}]},l.FUNCTION_DISPATCH]},{begin:a.lookahead(/@/),end:"[{;]", +relevance:0,illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/ +},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{ +$pattern:/[a-z-]+/,keyword:"and or not only",attribute:i.join(" ")},contains:[{ +begin:/[a-z-]+(?=:)/,className:"attribute"},...s,l.CSS_NUMBER_MODE]}]},{ +className:"selector-tag",begin:"\\b("+e.join("|")+")\\b"}]}}})() +;hljs.registerLanguage("css",e)})();/*! `plaintext` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var t=(()=>{"use strict";return t=>({name:"Plain text", +aliases:["text","txt"],disableAutodetect:!0})})() +;hljs.registerLanguage("plaintext",t)})();/*! `scss` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict" +;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],r=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],i=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],t=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],o=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse() +;return n=>{const a=(e=>({IMPORTANT:{scope:"meta",begin:"!important"}, +BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number", +begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},FUNCTION_DISPATCH:{ +className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{ +scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", +contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ +scope:"number", +begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", +relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z][A-Za-z0-9_-]*/} +}))(n),l=t,s=i,d="@[a-z-]+",c={className:"variable", +begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b",relevance:0};return{name:"SCSS", +case_insensitive:!0,illegal:"[=/|']", +contains:[n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE,a.CSS_NUMBER_MODE,{ +className:"selector-id",begin:"#[A-Za-z0-9_-]+",relevance:0},{ +className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0 +},a.ATTRIBUTE_SELECTOR_MODE,{className:"selector-tag", +begin:"\\b("+e.join("|")+")\\b",relevance:0},{className:"selector-pseudo", +begin:":("+s.join("|")+")"},{className:"selector-pseudo", +begin:":(:)?("+l.join("|")+")"},c,{begin:/\(/,end:/\)/, +contains:[a.CSS_NUMBER_MODE]},a.CSS_VARIABLE,{className:"attribute", +begin:"\\b("+o.join("|")+")\\b"},{ +begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b" +},{begin:/:/,end:/[;}{]/,relevance:0, +contains:[a.BLOCK_COMMENT,c,a.HEXCOLOR,a.CSS_NUMBER_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,a.IMPORTANT,a.FUNCTION_DISPATCH] +},{begin:"@(page|font-face)",keywords:{$pattern:d,keyword:"@page @font-face"}},{ +begin:"@",end:"[{;]",returnBegin:!0,keywords:{$pattern:/[a-z-]+/, +keyword:"and or not only",attribute:r.join(" ")},contains:[{begin:d, +className:"keyword"},{begin:/[a-z-]+(?=:)/,className:"attribute" +},c,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,a.HEXCOLOR,a.CSS_NUMBER_MODE] +},a.FUNCTION_DISPATCH]}}})();hljs.registerLanguage("scss",e)})();/*! `typescript` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict" +;const e="[A-Za-z$_][0-9A-Za-z$_]*",n=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],a=["true","false","null","undefined","NaN","Infinity"],t=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],s=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],c=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],r=["arguments","this","super","console","window","document","localStorage","module","global"],i=[].concat(c,t,s) +;function o(o){const l=o.regex,d=e,b={begin:/<[A-Za-z0-9\\._:-]+/, +end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ +const a=e[0].length+e.index,t=e.input[a] +;if("<"===t||","===t)return void n.ignoreMatch();let s +;">"===t&&(((e,{after:n})=>{const a="",M={ +match:[/const|var|let/,/\s+/,d,/\s*/,/=\s*/,/(async\s*)?/,l.lookahead(T)], +keywords:"async",className:{1:"keyword",3:"title.function"},contains:[S]} +;return{name:"Javascript",aliases:["js","jsx","mjs","cjs"],keywords:g,exports:{ +PARAMS_CONTAINS:v,CLASS_REFERENCE:R},illegal:/#(?![$_A-z])/, +contains:[o.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ +label:"use_strict",className:"meta",relevance:10, +begin:/^\s*['"]use (strict|asm)['"]/ +},o.APOS_STRING_MODE,o.QUOTE_STRING_MODE,A,p,_,N,{match:/\$\d+/},E,R,{ +className:"attr",begin:d+l.lookahead(":"),relevance:0},M,{ +begin:"("+o.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", +keywords:"return throw case",relevance:0,contains:[N,o.REGEXP_MODE,{ +className:"function",begin:T,returnBegin:!0,end:"\\s*=>",contains:[{ +className:"params",variants:[{begin:o.UNDERSCORE_IDENT_RE,relevance:0},{ +className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0, +excludeEnd:!0,keywords:g,contains:v}]}]},{begin:/,/,relevance:0},{match:/\s+/, +relevance:0},{variants:[{begin:"<>",end:""},{ +match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:b.begin, +"on:begin":b.isTrulyOpeningTag,end:b.end}],subLanguage:"xml",contains:[{ +begin:b.begin,end:b.end,skip:!0,contains:["self"]}]}]},x,{ +beginKeywords:"while if switch catch for"},{ +begin:"\\b(?!function)"+o.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", +returnBegin:!0,label:"func.def",contains:[S,o.inherit(o.TITLE_MODE,{begin:d, +className:"title.function"})]},{match:/\.\.\./,relevance:0},I,{match:"\\$"+d, +relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, +contains:[S]},k,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},w,C,{match:/\$[(.]/}]}}return t=>{ +const s=o(t),c=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],l={ +beginKeywords:"namespace",end:/\{/,excludeEnd:!0, +contains:[s.exports.CLASS_REFERENCE]},d={beginKeywords:"interface",end:/\{/, +excludeEnd:!0,keywords:{keyword:"interface extends",built_in:c}, +contains:[s.exports.CLASS_REFERENCE]},b={$pattern:e, +keyword:n.concat(["type","namespace","interface","public","private","protected","implements","declare","abstract","readonly","enum","override"]), +literal:a,built_in:i.concat(c),"variable.language":r},g={className:"meta", +begin:"@[A-Za-z$_][0-9A-Za-z$_]*"},u=(e,n,a)=>{ +const t=e.contains.findIndex((e=>e.label===n)) +;if(-1===t)throw Error("can not find mode to replace");e.contains.splice(t,1,a)} +;return Object.assign(s.keywords,b), +s.exports.PARAMS_CONTAINS.push(g),s.contains=s.contains.concat([g,l,d]), +u(s,"shebang",t.SHEBANG()),u(s,"use_strict",{className:"meta",relevance:10, +begin:/^\s*['"]use strict['"]/ +}),s.contains.find((e=>"func.def"===e.label)).relevance=0,Object.assign(s,{ +name:"TypeScript",aliases:["ts","tsx"]}),s}})() +;hljs.registerLanguage("typescript",e)})();/*! `ruby` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const n=e.regex,a="([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)",s=n.either(/\b([A-Z]+[a-z0-9]+)+/,/\b([A-Z]+[a-z0-9]+)+[A-Z]+/),i=n.concat(s,/(::\w+)*/),t={ +"variable.constant":["__FILE__","__LINE__","__ENCODING__"], +"variable.language":["self","super"], +keyword:["alias","and","begin","BEGIN","break","case","class","defined","do","else","elsif","end","END","ensure","for","if","in","module","next","not","or","redo","require","rescue","retry","return","then","undef","unless","until","when","while","yield","include","extend","prepend","public","private","protected","raise","throw"], +built_in:["proc","lambda","attr_accessor","attr_reader","attr_writer","define_method","private_constant","module_function"], +literal:["true","false","nil"]},c={className:"doctag",begin:"@[A-Za-z]+"},r={ +begin:"#<",end:">"},b=[e.COMMENT("#","$",{contains:[c] +}),e.COMMENT("^=begin","^=end",{contains:[c],relevance:10 +}),e.COMMENT("^__END__",e.MATCH_NOTHING_RE)],l={className:"subst",begin:/#\{/, +end:/\}/,keywords:t},d={className:"string",contains:[e.BACKSLASH_ESCAPE,l], +variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{ +begin:/%[qQwWx]?\(/,end:/\)/},{begin:/%[qQwWx]?\[/,end:/\]/},{ +begin:/%[qQwWx]?\{/,end:/\}/},{begin:/%[qQwWx]?/},{begin:/%[qQwWx]?\//, +end:/\//},{begin:/%[qQwWx]?%/,end:/%/},{begin:/%[qQwWx]?-/,end:/-/},{ +begin:/%[qQwWx]?\|/,end:/\|/},{begin:/\B\?(\\\d{1,3})/},{ +begin:/\B\?(\\x[A-Fa-f0-9]{1,2})/},{begin:/\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/},{ +begin:/\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/},{ +begin:/\B\?\\(c|C-)[\x20-\x7e]/},{begin:/\B\?\\?\S/},{ +begin:n.concat(/<<[-~]?'?/,n.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)), +contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/, +contains:[e.BACKSLASH_ESCAPE,l]})]}]},o="[0-9](_?[0-9])*",g={className:"number", +relevance:0,variants:[{ +begin:`\\b([1-9](_?[0-9])*|0)(\\.(${o}))?([eE][+-]?(${o})|r)?i?\\b`},{ +begin:"\\b0[dD][0-9](_?[0-9])*r?i?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*r?i?\\b" +},{begin:"\\b0[oO][0-7](_?[0-7])*r?i?\\b"},{ +begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b"},{ +begin:"\\b0(_?[0-7])+r?i?\\b"}]},_={variants:[{match:/\(\)/},{ +className:"params",begin:/\(/,end:/(?=\))/,excludeBegin:!0,endsParent:!0, +keywords:t}]},u=[d,{variants:[{match:[/class\s+/,i,/\s+<\s+/,i]},{ +match:[/\b(class|module)\s+/,i]}],scope:{2:"title.class", +4:"title.class.inherited"},keywords:t},{match:[/(include|extend)\s+/,i],scope:{ +2:"title.class"},keywords:t},{relevance:0,match:[i,/\.new[. (]/],scope:{ +1:"title.class"}},{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},{relevance:0,match:s,scope:"title.class"},{ +match:[/def/,/\s+/,a],scope:{1:"keyword",3:"title.function"},contains:[_]},{ +begin:e.IDENT_RE+"::"},{className:"symbol", +begin:e.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{className:"symbol", +begin:":(?!\\s)",contains:[d,{begin:a}],relevance:0},g,{className:"variable", +begin:"(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])"},{ +className:"params",begin:/\|/,end:/\|/,excludeBegin:!0,excludeEnd:!0, +relevance:0,keywords:t},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*", +keywords:"unless",contains:[{className:"regexp",contains:[e.BACKSLASH_ESCAPE,l], +illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:/%r\{/,end:/\}[a-z]*/},{ +begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[", +end:"\\][a-z]*"}]}].concat(r,b),relevance:0}].concat(r,b) +;l.contains=u,_.contains=u;const m=[{begin:/^\s*=>/,starts:{end:"$",contains:u} +},{className:"meta.prompt", +begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])", +starts:{end:"$",keywords:t,contains:u}}];return b.unshift(r),{name:"Ruby", +aliases:["rb","gemspec","podspec","thor","irb"],keywords:t,illegal:/\/\*/, +contains:[e.SHEBANG({binary:"ruby"})].concat(m).concat(b).concat(u)}}})() +;hljs.registerLanguage("ruby",e)})();/*! `yaml` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const n="true false yes no null",a="[\\w#;/?:@&=+$,.~*'()[\\]]+",s={ +className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/ +},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable", +variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},i=e.inherit(s,{ +variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),l={ +end:",",endsWithParent:!0,excludeEnd:!0,keywords:n,relevance:0},t={begin:/\{/, +end:/\}/,contains:[l],illegal:"\\n",relevance:0},g={begin:"\\[",end:"\\]", +contains:[l],illegal:"\\n",relevance:0},b=[{className:"attr",variants:[{ +begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{ +begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$", +relevance:10},{className:"string", +begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{ +begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0, +relevance:0},{className:"type",begin:"!\\w+!"+a},{className:"type", +begin:"!<"+a+">"},{className:"type",begin:"!"+a},{className:"type",begin:"!!"+a +},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta", +begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)", +relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{ +className:"number", +begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b" +},{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},t,g,s],r=[...b] +;return r.pop(),r.push(i),l.contains=r,{name:"YAML",case_insensitive:!0, +aliases:["yml"],contains:b}}})();hljs.registerLanguage("yaml",e)})();/*! `markdown` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const n={begin:/<\/?[A-Za-z_]/, +end:">",subLanguage:"xml",relevance:0},a={variants:[{begin:/\[.+?\]\[.*?\]/, +relevance:0},{ +begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/, +relevance:2},{ +begin:e.regex.concat(/\[.+?\]\(/,/[A-Za-z][A-Za-z0-9+.-]*/,/:\/\/.*?\)/), +relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{ +begin:/\[.*?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{match:/\[(?=\])/ +},{className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0, +returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)", +excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[", +end:"\\]",excludeBegin:!0,excludeEnd:!0}]},i={className:"strong",contains:[], +variants:[{begin:/_{2}(?!\s)/,end:/_{2}/},{begin:/\*{2}(?!\s)/,end:/\*{2}/}] +},s={className:"emphasis",contains:[],variants:[{begin:/\*(?![*\s])/,end:/\*/},{ +begin:/_(?![_\s])/,end:/_/,relevance:0}]},c=e.inherit(i,{contains:[] +}),t=e.inherit(s,{contains:[]});i.contains.push(t),s.contains.push(c) +;let g=[n,a];return[i,s,c,t].forEach((e=>{e.contains=e.contains.concat(g) +})),g=g.concat(i,s),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{ +className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:g},{ +begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n", +contains:g}]}]},n,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)", +end:"\\s+",excludeEnd:!0},i,s,{className:"quote",begin:"^>\\s+",contains:g, +end:"$"},{className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{ +begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{ +begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))", +contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{ +begin:"^[-\\*]{3,}",end:"$"},a,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{ +className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{ +className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}}})() +;hljs.registerLanguage("markdown",e)})(); \ No newline at end of file diff --git a/system/typemill/author/js/vue-license.js b/system/typemill/author/js/vue-license.js new file mode 100644 index 0000000..644b917 --- /dev/null +++ b/system/typemill/author/js/vue-license.js @@ -0,0 +1,131 @@ +const app = Vue.createApp({ + template: ` +
+
+

Your license is out of date. Please check if the payments for your subscription were successfull.

+

Your license is only valid for the domain listed in your license data below.

+

Congratulations! Your license is ok and you can enjoy all features.

+
+
+
+
+ +
+
+

{{ licenseData.plan }}-LICENSE

+
+
+
+

License-key:

+

{{ licenseData.license }}

+

Domain:

+

{{ licenseData.domain }}

+

E-Mail:

+

{{ licenseData.email }}

+

Payed until:

+

{{ licenseData.payed_until }}

+
+
+
+
+
+

Buy a typemill-license and enjoy our flatrate-model for premium-plugins and -themes.

We offer two types of subscription-based licenses:

+
+
+

Maker License

+

Use all maker-prodcuts (plugins and themes) for one year. The subscription will automatically renewed after a year.

+ Buy now for 29,00 €/year +
+
+

Business License

+

Use all business- and maker-products (plugins, themes, services) for one year. The subscription will automatically renewed after a year.

+ Buy now for 229,00 €/year +
+
+
+
+
+ {{ fieldDefinition.legend }} + + +
+ + +
+
+
{{ message }}
+ +
+
+
`, + data() { + return { + licenseData: data.licensedata, + formDefinitions: data.licensefields, + formData: {}, + message: '', + messageClass: '', + errors: {}, + src: tmaxios.defaults.baseURL + "/system/author/img/favicon-144.png" + } + }, + mounted() { + eventBus.$on('forminput', formdata => { + this.formData[formdata.name] = formdata.value; + }); + }, + methods: { + selectComponent: function(type) + { + return 'component-'+type; + }, + save: function() + { + this.reset(); + var self = this; + + tmaxios.post('/api/v1/license',{ + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + 'license': this.formData + }) + .then(function (response) + { + self.messageClass = 'bg-teal-500'; + self.message = response.data.message; + self.licenseData = response.data.licensedata; + }) + .catch(function (error) + { + self.messageClass = 'bg-rose-500'; + self.message = error.response.data.message; + + /* form validation errors */ + if(error.response.data.errors !== undefined) + { + self.errors = error.response.data.errors; + } + }); + }, + reset: function() + { + this.errors = {}; + this.message = ''; + this.messageClass = ''; + } + }, +}) \ No newline at end of file diff --git a/system/typemill/author/js/vue-plugins.js b/system/typemill/author/js/vue-plugins.js index 7f11a91..896be14 100644 --- a/system/typemill/author/js/vue-plugins.js +++ b/system/typemill/author/js/vue-plugins.js @@ -1,55 +1,65 @@ const app = Vue.createApp({ template: ` -
-
    -
  • -
    -
    -

    {{theme.name}}

    -
    author: {{theme.author}} | version: {{theme.version}} | {{theme.licence}}
    -

    {{theme.description}}

    -
    -
    - - -
    -
    -
    -
    -
    - {{ fieldDefinition.legend }} - +
      +
    • +
      +

      License: free

      + +
      +
      +
      +

      {{plugin.name}}

      +
      author: {{plugin.author}} | version: {{plugin.version}}
      +

      {{plugin.description}}

      +
      +
      + + +
      +
      + +
      +
      + {{ fieldDefinition.legend }} + - -
      - +
    + - -
    -
    -
    {{ message }}
    -
    - - -
    -
    -
    -
  • -
-
-
`, + + +
+
{{ message }}
+
+ + +
+
+ + + + + `, data() { return { current: '', @@ -58,7 +68,7 @@ const app = Vue.createApp({ message: '', messageClass: '', errors: {}, - userroles: false + userroles: false, } }, mounted() { diff --git a/system/typemill/author/js/vue-shared.js b/system/typemill/author/js/vue-shared.js index cfe2949..a89049c 100644 --- a/system/typemill/author/js/vue-shared.js +++ b/system/typemill/author/js/vue-shared.js @@ -26,22 +26,17 @@ app.component('component-text', { app.component('component-textarea', { props: ['id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'css', 'value', 'errors'], - data: function () { - return { - textareaclass: '' - } - }, template: `

{{ errors[name] }}

{{ $filters.translate(description) }}

@@ -53,12 +48,77 @@ app.component('component-textarea', { }, formatValue: function(value) { + /* if(value !== null && typeof value === 'object') { this.textareaclass = 'codearea'; return JSON.stringify(value, undefined, 4); } return value; + */ + }, + }, +}) + +app.component('component-codearea', { + props: ['id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'css', 'value', 'errors'], + data: function() { + return { + highlighted: '', + } + }, + template: `
+ +
+ + +
+

{{ errors[name] }}

+

{{ $filters.translate(description) }}

+
`, + mounted: function() + { + this.resizeCodearea(); + this.highlight(this.value); + }, + methods: { + update: function($event, name) + { + this.highlight($event.target.value); + this.resizeCodearea(); + eventBus.$emit('forminput', {'name': name, 'value': $event.target.value}); + }, + resizeCodearea: function() + { + let codeeditor = this.$refs["editor"]; + + window.requestAnimationFrame(() => { + codeeditor.style.height = '200px'; + if (codeeditor.scrollHeight > 200) + { + codeeditor.style.height = `${codeeditor.scrollHeight + 2}px`; + } + }); + }, + highlight: function(code) + { + if(code === undefined) + { + return; + } + + window.requestAnimationFrame(() => { + highlighted = hljs.highlightAuto(code, ['xml','css','yaml','markdown']).value; + this.highlighted = highlighted; + }); }, }, }) @@ -90,6 +150,11 @@ app.component('component-select', { app.component('component-checkbox', { props: ['id', 'description', 'readonly', 'required', 'disabled', 'label', 'checkboxlabel', 'name', 'type', 'css', 'value', 'errors'], + data() { + return { + checked: false + } + }, template: `
{{ $filters.translate(label) }}
+

{{ errors[name] }}

{{ $filters.translate(description) }}

`, - methods: { - update: function($event, value, name) + mounted: function() + { + if(this.value === true || this.value == 'on') { - eventBus.$emit('forminput', {'name': name, 'value': value}); + this.checked = true; + } + }, + methods: { + update: function(checked, name) + { + eventBus.$emit('forminput', {'name': name, 'value': checked}); }, }, }) app.component('component-checkboxlist', { props: ['description', 'readonly', 'required', 'disabled', 'label', 'checkboxlabel', 'options', 'name', 'type', 'css', 'value', 'errors'], + data() { + return { + checkedoptions: [] + } + }, template: `
{{ $filters.translate(label) }}

{{ errors[name] }}

{{ $filters.translate(description) }}

`, - methods: { - update: function($event, value, optionvalue, name) + mounted: function() + { + if(typeof this.value === 'object') { - /* if value (array) for checkboxlist is not initialized yet */ - if(value === true || value === false) - { - value = [optionvalue]; - } - eventBus.$emit('forminput', {'name': name, 'value': value}); + this.checkedoptions = this.value; + } + }, + methods: { + update: function(checkedoptions, name) + { + eventBus.$emit('forminput', {'name': name, 'value': checkedoptions}); }, }, }) app.component('component-radio', { props: ['id', 'description', 'readonly', 'required', 'disabled', 'options', 'label', 'name', 'type', 'css', 'value', 'errors'], + data() { + return { + picked: this.value + } + }, template: `
{{ $filters.translate(label) }}

{{ errors[name] }}

{{ $filters.translate(description) }}

`, methods: { - update: function($event, value, name) + update: function(picked, name) { - eventBus.$emit('forminput', {'name': name, 'value': value}); + eventBus.$emit('forminput', {'name': name, 'value': picked}); }, }, }) @@ -604,29 +688,40 @@ app.component('component-customfields', { }, }) + app.component('component-image', { - props: ['id', 'description', 'maxlength', 'hidden', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'errors'], - template: `
- -
-
-
- + props: ['id', 'description', 'maxlength', 'hidden', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'css', 'errors'], + data: function(){ + return { + maxsize: 10, // megabyte + imagepreview: '', + showmedialib: false, +// load: false, + quality: false, + qualitylabel: false, + } + }, + template: `
+ +
+
+
+
-
-
- ' + -

{{ $filters.translate('upload an image') }}

'+ +
+
+

{{ $filters.translate('upload an image') }}

+
-
- +
+
-
- +
+
- - x +
-
- -
+
+ +
-

{{ $filters.translate(description) }}

-
{{ errors[name] }}
+

{{ errors[name] }}

+

{{ $filters.translate(description) }}

@@ -652,38 +747,56 @@ app.component('component-image', {
`, - data: function(){ - return { - maxsize: 10, // megabyte - imgpreview: false, - showmedialib: false, - load: false, - quality: false, - qualitylabel: false, + mounted: function() { + if(this.hasValue(this.value)) + { + this.imagepreview = tmaxios.defaults.baseURL + '/' + this.value; + + /* switcher for quality */ + if(this.value.indexOf("/live/") > -1 ) + { + this.quality = 'optimized'; + this.qualitylabel = 'switch size to: maximum'; + } + else if(this.value.indexOf("/original/") > -1) + { + this.quality = 'maximum'; + this.qualitylabel = 'switch size to: optimized'; + } } }, methods: { - getimagesrc: function(value) + hasValue: function(value) { - if(value !== undefined && value !== null && value !== '') + if(typeof this.value !== "undefined" && this.value !== false && this.value !== null && this.value !== '') { - var imgpreview = myaxios.defaults.baseURL + '/' + value; - if(value.indexOf("media/live") > -1 ) + return true; + } + return false; + }, + switchQuality: function(value) + { + if(this.hasValue(value)) + { + if(this.quality == 'optimized') { - this.quality = 'live'; - this.qualitylabel = 'switch quality to: original'; + this.quality = 'maximum'; + this.qualitylabel = 'switch size to: optimized'; + var newUrl = value.replace("/live/", "/original/"); + this.update(newUrl); } - else if(value.indexOf("media/original") > -1) + else { - this.quality = 'original'; - this.qualitylabel = 'switch quality to: live'; + this.quality = 'optimized'; + this.qualitylabel = 'switch quality to: maximum'; + var newUrl = value.replace("/original/", "/live/"); + this.update(newUrl); } - return imgpreview; } }, - update: function(value) + update: function(filepath) { - FormBus.$emit('forminput', {'name': this.name, 'value': value}); + eventBus.$emit('forminput', {'name': this.name, 'value': filepath}); }, updatemarkdown: function(markdown, url) { @@ -697,29 +810,9 @@ app.component('component-image', { }, deleteImage: function() { - this.imgpreview = false; + this.imagepreview = ''; this.update(''); }, - switchQuality: function(value) - { - if(value !== null && value !== '') - { - if(this.quality == 'live') - { - var newUrl = value.replace("media/live", "media/original"); - this.update(newUrl); - this.quality = 'original'; - this.qualitylabel = 'switch quality to: live'; - } - else - { - var newUrl = value.replace("media/original", "media/live"); - this.update(newUrl); - this.quality = 'live'; - this.qualitylabel = 'switch quality to: original'; - } - } - }, openmedialib: function() { this.showmedialib = true; @@ -733,11 +826,13 @@ app.component('component-image', { if (!imageFile.type.match('image.*')) { - publishController.errors.message = "Only images are allowed."; + alert('only images allowed'); +/* publishController.errors.message = "Only images are allowed."; */ } else if (size > this.maxsize) { - publishController.errors.message = "The maximal size of images is " + this.maxsize + " MB"; + alert('too big'); +/* publishController.errors.message = "The maximal size of images is " + this.maxsize + " MB"; */ } else { @@ -747,10 +842,9 @@ app.component('component-image', { reader.readAsDataURL(imageFile); reader.onload = function(e) { - sharedself.imgpreview = e.target.result; + sharedself.imagepreview = e.target.result; - myaxios.post('/api/v1/image',{ - 'url': document.getElementById("path").value, + tmaxios.post('/api/v1/image',{ 'image': e.target.result, 'name': imageFile.name, 'publish': true, @@ -765,7 +859,8 @@ app.component('component-image', { sharedself.load = false; if(error.response) { - publishController.errors.message = error.response.data.errors; + console.info(error.response); +/* publishController.errors.message = error.response.data.errors; */ } }); } @@ -957,6 +1052,78 @@ app.component('component-file', { } }) + +app.component('modal', { + props: ['labelconfirm', 'labelcancel'], + template: ` +
+
+
+
+ + default header + +
+
+ + default body + +
+
+ + + default button + +
+
+
+
+
`, + methods: { + update: function($event, name) + { + eventBus.$emit('forminput', {'name': name, 'value': $event.target.value}); + }, + }, +}) + +app.component('activebox', { + props: ['id', 'description', 'readonly', 'required', 'disabled', 'label', 'checkboxlabel', 'name', 'type', 'css', 'value', 'errors'], + data() { + return { + checked: false + } + }, + template: `
+ + +

{{ errors[name] }}

+

{{ $filters.translate(description) }}

+
`, + mounted: function() + { + if(this.value === true || this.value == 'on') + { + this.checked = true; + } + }, + methods: { + activate: function(checked, name) + { + alert("yes"); + }, + }, +}) + + + + + const medialib = app.component('medialib', { props: ['parentcomponent'], template: `
diff --git a/system/typemill/author/js/vue-system.js b/system/typemill/author/js/vue-system.js index 55bfebb..d29ed25 100644 --- a/system/typemill/author/js/vue-system.js +++ b/system/typemill/author/js/vue-system.js @@ -59,6 +59,7 @@ const app = Vue.createApp({ }, activateTab: function(tab){ this.currentTab = tab; + this.reset(); }, save: function() { @@ -92,383 +93,4 @@ const app = Vue.createApp({ this.messageClass = ''; } }, -}) - - -/* -Vue.component('tab-meta', { - props: ['saved', 'errors', 'formdata', 'schema', 'userroles'], - data: function () { - return { - slug: false, - originalSlug: false, - slugerror: false, - disabled: "disabled", - } - }, - template: '
' + - '
' + - '' + - '
{{ slugerror }}
' + - '
' + - '
' + - '
{{field.legend}}' + - '' + - '' + - '
' + - '' + - '' + - '
' + - '
{{ \'Saved successfully\'|translate }}
' + - '
{{ \'Please correct the errors above\'|translate }}
' + - '
' + - '
', - mounted: function() - { - if(this.$parent.item.slug != '') - { - this.slug = this.$parent.item.slug; - this.originalSlug = this.slug; - } - }, - methods: { - selectComponent: function(field) - { - return 'component-'+field.type; - }, - saveInput: function() - { - this.$emit('saveform'); - }, - changeSlug: function() - { - if(this.slug == this.originalSlug) - { - this.slugerror = false; - this.disabled = "disabled"; - return; - } - if(this.slug == '') - { - this.slugerror = 'empty slugs are not allowed'; - this.disabled = "disabled"; - return; - } - - this.slug = this.slug.replace(/ /g, '-'); - - if(this.slug.match(/^[a-z0-9\-]*$/)) - { - this.slugerror = false; - this.disabled = false; - } - else - { - this.slugerror = 'Only lowercase a-z and 0-9 and "-" is allowed for slugs.'; - this.disabled = "disabled"; - } - }, - storeSlug: function() - { - - if(this.slug.match(/^[a-z0-9\-]*$/) && this.slug != this.originalSlug) - { - var self = this; - - myaxios.post('/api/v1/article/rename',{ - 'url': document.getElementById("path").value, - 'csrf_name': document.getElementById("csrf_name").value, - 'csrf_value': document.getElementById("csrf_value").value, - 'slug': this.slug, - }) - .then(function (response) - { - window.location.replace(response.data.url); - }) - .catch(function (error) - { - if(error.response.data.errors.message) - { - publishController.errors.message = error.response.data.errors.message; - } - }); - } - } - } -}) - - - - -/* - -let system = new Vue({ - delimiters: ['${', '}'], - el: '#wrong', - data: function () { - return { - root: document.getElementById("main").dataset.url, - currentTab: 'Content', - tabs: ['Content'], - formDefinitions: [], - formData: [], - formErrors: {}, - formErrorsReset: {}, - item: false, - userroles: false, - saved: false, - } - }, - computed: { - currentTabComponent: function () { - if(this.currentTab == 'Content') - { - editor.showEditor = 'show'; - posts.showPosts = 'show'; - } - else - { - editor.showEditor = 'hidden'; - posts.showPosts = 'hidden'; - return 'tab-' + this.currentTab.toLowerCase() - } - } - }, - mounted: function(){ - - var self = this; - - myaxios.get('/api/v1/article/metaobject',{ - params: { - 'url': document.getElementById("path").value, - 'csrf_name': document.getElementById("csrf_name").value, - 'csrf_value': document.getElementById("csrf_value").value, - } - }) - .then(function (response) { - - var formdefinitions = response.data.metadefinitions; - - for (var key in formdefinitions) { - if (formdefinitions.hasOwnProperty(key)) { - self.tabs.push(key); - self.formErrors[key] = false; - } - } - - self.formErrorsReset = self.formErrors; - self.formDefinitions = formdefinitions; - - self.formData = response.data.metadata; - - self.userroles = response.data.userroles; - - self.item = response.data.item; - - if(self.item.elementType == "folder" && self.item.contains == "posts") - { - posts.posts = self.item.folderContent; - posts.folderid = self.item.keyPath; - } - else - { - posts.posts = false; - } - }) - .catch(function (error) - { - if(error.response) - { - } - }); - - /* update single value or array - this.$set(this.someObject, 'b', 2) * - FormBus.$on('forminput', formdata => { - this.$set(this.formData[this.currentTab], formdata.name, formdata.value); - }); - - /* update values that are objects - this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 }) * - - FormBus.$on('forminputobject', formdata => { - this.formData[this.currentTab][formdata.name] = Object.assign({}, this.formData[this.currentTab][formdata.name], formdata.value); - }); - }, - methods: { - saveForm: function() - { - this.saved = false; - - self = this; - - myaxios.post('/api/v1/article/metadata',{ - 'url': document.getElementById("path").value, - 'csrf_name': document.getElementById("csrf_name").value, - 'csrf_value': document.getElementById("csrf_value").value, - 'tab': self.currentTab, - 'data': self.formData[self.currentTab] - }) - .then(function (response) { - self.saved = true; - self.formErrors = self.formErrorsReset; - if(response.data.structure) - { - navi.items = response.data.structure; - } - - var item = response.data.item; - if(item.elementType == "folder" && item.contains == "posts") - { - posts.posts = item.folderContent; - posts.folderid = item.keyPath; - } - else - { - posts.posts = false; - } - }) - .catch(function (error) - { - if(error.response) - { - self.formErrors = error.response.data.errors; - } - if(error.response.data.errors.message) - { - publishController.errors.message = error.response.data.errors.message; - } - }); - }, - } -}); - -Vue.component('tab-meta', { - props: ['saved', 'errors', 'formdata', 'schema', 'userroles'], - data: function () { - return { - slug: false, - originalSlug: false, - slugerror: false, - disabled: "disabled", - } - }, - template: '
' + - '
' + - '' + - '
{{ slugerror }}
' + - '
' + - '
' + - '
{{field.legend}}' + - '' + - '' + - '
' + - '' + - '' + - '
' + - '
{{ \'Saved successfully\'|translate }}
' + - '
{{ \'Please correct the errors above\'|translate }}
' + - '
' + - '
', - mounted: function() - { - if(this.$parent.item.slug != '') - { - this.slug = this.$parent.item.slug; - this.originalSlug = this.slug; - } - }, - methods: { - selectComponent: function(field) - { - return 'component-'+field.type; - }, - saveInput: function() - { - this.$emit('saveform'); - }, - changeSlug: function() - { - if(this.slug == this.originalSlug) - { - this.slugerror = false; - this.disabled = "disabled"; - return; - } - if(this.slug == '') - { - this.slugerror = 'empty slugs are not allowed'; - this.disabled = "disabled"; - return; - } - - this.slug = this.slug.replace(/ /g, '-'); - - if(this.slug.match(/^[a-z0-9\-]*$/)) - { - this.slugerror = false; - this.disabled = false; - } - else - { - this.slugerror = 'Only lowercase a-z and 0-9 and "-" is allowed for slugs.'; - this.disabled = "disabled"; - } - }, - storeSlug: function() - { - - if(this.slug.match(/^[a-z0-9\-]*$/) && this.slug != this.originalSlug) - { - var self = this; - - myaxios.post('/api/v1/article/rename',{ - 'url': document.getElementById("path").value, - 'csrf_name': document.getElementById("csrf_name").value, - 'csrf_value': document.getElementById("csrf_value").value, - 'slug': this.slug, - }) - .then(function (response) - { - window.location.replace(response.data.url); - }) - .catch(function (error) - { - if(error.response.data.errors.message) - { - publishController.errors.message = error.response.data.errors.message; - } - }); - } - } - } -}) -*/ \ No newline at end of file +}) \ No newline at end of file diff --git a/system/typemill/author/js/vue-user.js b/system/typemill/author/js/vue-user.js new file mode 100644 index 0000000..a6637cd --- /dev/null +++ b/system/typemill/author/js/vue-user.js @@ -0,0 +1,136 @@ +const app = Vue.createApp({ + template: ` +
+
+
+
+ {{ fieldDefinition.legend }} + + +
+ + +
+
+
{{ message }}
+ +
+
+
+ + + + + + +
+
+
`, + data() { + return { + formDefinitions: data.userfields, + formData: data.userdata, + userroles: data.userroles, + message: '', + messageClass: '', + errors: {}, + showModal: false, + } + }, + mounted() { + eventBus.$on('forminput', formdata => { + this.formData[formdata.name] = formdata.value; + }); + }, + methods: { + selectComponent: function(type) + { + return 'component-'+type; + }, + changeForm: function() + { + /* change input form if user role changed */ + }, + save: function() + { + this.reset(); + var self = this; + + tmaxios.put('/api/v1/user',{ + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + 'userdata': this.formData + }) + .then(function (response) + { + self.messageClass = 'bg-teal-500'; + self.message = response.data.message; + }) + .catch(function (error) + { + self.messageClass = 'bg-rose-500'; + self.message = error.response.data.message; + if(error.response.data.errors !== undefined) + { + self.errors = error.response.data.errors; + } + }); + }, + deleteuser: function() + { + this.reset(); + var self = this; + + tmaxios.delete('/api/v1/user',{ + data: { + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + 'username': this.formData.username + } + }) + .then(function (response) + { + self.showModal = false; + self.messageClass = 'bg-teal-500'; + self.message = response.data.message; + /* redirect to userlist */ + }) + .catch(function (error) + { + self.showModal = false; + self.messageClass = 'bg-rose-500'; + self.message = error.response.data.message; + if(error.response.data.errors !== undefined) + { + self.errors = error.response.data.errors; + } + }); + }, + reset: function() + { + this.errors = {}; + this.message = ''; + this.messageClass = ''; + } + }, +}) \ No newline at end of file diff --git a/system/typemill/author/js/vue-account.js b/system/typemill/author/js/vue-usernew.js similarity index 64% rename from system/typemill/author/js/vue-account.js rename to system/typemill/author/js/vue-usernew.js index 0dc05ad..aa1a955 100644 --- a/system/typemill/author/js/vue-account.js +++ b/system/typemill/author/js/vue-usernew.js @@ -1,7 +1,16 @@ const app = Vue.createApp({ template: `
-
+
+ + +
+
{{ fieldDefinition.legend }} @@ -34,8 +43,9 @@ const app = Vue.createApp({ `, data() { return { - formDefinitions: data.userfields, - formData: data.userdata, + selectedrole: false, + formDefinitions: false, + formData: {}, userroles: data.userroles, message: '', messageClass: '', @@ -52,20 +62,22 @@ const app = Vue.createApp({ { return 'component-'+type; }, - save: function() + generateForm: function() { this.reset(); var self = this; - tmaxios.put('/api/v1/account',{ - 'csrf_name': document.getElementById("csrf_name").value, - 'csrf_value': document.getElementById("csrf_value").value, - 'userdata': this.formData + tmaxios.get('/api/v1/userform',{ + params: { + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + 'userrole': this.selectedrole + } }) .then(function (response) { - self.messageClass = 'bg-teal-500'; - self.message = response.data.message; + self.formDefinitions = response.data.userform; + self.formData.userrole = self.selectedrole; }) .catch(function (error) { @@ -75,7 +87,34 @@ const app = Vue.createApp({ { self.errors = error.response.data.errors; } - }); + }); + }, + save: function() + { + this.reset(); + var self = this; + + tmaxios.post('/api/v1/user',{ + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + 'userdata': this.formData + }) + .then(function (response) + { + self.messageClass = 'bg-teal-500'; + self.message = response.data.message; + + window.location = tmaxios.defaults.baseURL + '/tm/user/' + self.formData.username; + }) + .catch(function (error) + { + self.messageClass = 'bg-rose-500'; + self.message = error.response.data.message; + if(error.response.data.errors !== undefined) + { + self.errors = error.response.data.errors; + } + }); }, reset: function() { diff --git a/system/typemill/author/js/vue-users.js b/system/typemill/author/js/vue-users.js index 4e96261..3fc8672 100644 --- a/system/typemill/author/js/vue-users.js +++ b/system/typemill/author/js/vue-users.js @@ -181,7 +181,7 @@ app.component('searchbox', { - +
@@ -244,13 +244,22 @@ app.component('usertable', { {{ user.username }} {{ user.userrole }} {{ user.email }} - edit + edit - `, + + New user + + + add + + `, methods: { - getEditLink: function(username){ - return this.$root.$data.root + '/tm/user/' + username; - }, + getEditLink: function(username){ + return tmaxios.defaults.baseURL + '/tm/user/' + username; + }, + getNewUserLink: function(){ + return tmaxios.defaults.baseURL + '/tm/user/new'; + }, } }) @@ -273,135 +282,4 @@ app.component('pagination', { return 'bg-stone-100'; } } -}) - - - - -/* -const app = Vue.createApp({ - template: ` -
-
    -
  • -
    -
    - -
    -
    -
    -

    {{theme.name}}

    -
    author: {{theme.author}} | version: {{theme.version}} | {{theme.licence}}
    -

    {{theme.description}}

    -
    -
    - - -
    -
    -
    - -
    -
    - {{ fieldDefinition.legend }} - - -
    - - -
    -
    -
    {{ message }}
    -
    - - -
    -
    -
  • - -
-
-
`, - data() { - return { - current: '', - formDefinitions: data.themes, - formData: data.settings, - message: '', - messageClass: '', - errors: {}, - userroles: false - } - }, - mounted() { - eventBus.$on('forminput', formdata => { - this.formData[this.current][formdata.name] = formdata.value; - }); - }, - methods: { - setCurrent: function(name) - { - if(this.current == name) - { - this.current = ''; - } - else - { - this.current = name; - } - }, - selectComponent: function(type) - { - return 'component-'+type; - }, - save: function() - { - this.reset(); - var self = this; - - tmaxios.post('/api/v1/theme',{ - 'csrf_name': document.getElementById("csrf_name").value, - 'csrf_value': document.getElementById("csrf_value").value, - 'theme': this.current, - 'settings': this.formData[this.current] - }) - .then(function (response) - { - self.messageClass = 'bg-teal-500'; - self.message = response.data.message; - - self.updateCSS(); - }) - .catch(function (error) - { - self.messageClass = 'bg-rose-500'; - self.message = error.response.data.message; - if(error.response.data.errors !== undefined) - { - self.errors = error.response.data.errors; - } - }); - }, - reset: function() - { - this.errors = {}; - this.message = ''; - this.messageClass = ''; - } - }, -}) -*/ \ No newline at end of file +}) \ No newline at end of file diff --git a/system/typemill/author/layouts/layoutSystem.twig b/system/typemill/author/layouts/layoutSystem.twig index c19bd0c..8c1b957 100644 --- a/system/typemill/author/layouts/layoutSystem.twig +++ b/system/typemill/author/layouts/layoutSystem.twig @@ -19,6 +19,8 @@ + {% block stylesheet %}{% endblock %} + {{ assets.renderCSS() }} @@ -36,7 +38,7 @@ -
+
{% block content %}{% endblock %}
@@ -49,7 +51,7 @@ diff --git a/system/typemill/author/partials/systemNavi.twig b/system/typemill/author/partials/systemNavi.twig index 12b23cc..8b57522 100644 --- a/system/typemill/author/partials/systemNavi.twig +++ b/system/typemill/author/partials/systemNavi.twig @@ -2,10 +2,10 @@
    - {% for name,navitem in systemnavi %} + {% for name, navitem in systemnavi %}
  • - {{ translate(name) }} + {{ translate(navitem.title) }}
  • {% endfor %} diff --git a/system/typemill/author/system/account.twig b/system/typemill/author/system/account.twig index 58f1fb9..1c248b2 100644 --- a/system/typemill/author/system/account.twig +++ b/system/typemill/author/system/account.twig @@ -11,7 +11,7 @@ {% block javascript %} - + + + + + + +{% endblock %} \ No newline at end of file diff --git a/system/typemill/author/system/plugins.twig b/system/typemill/author/system/plugins.twig index 2cf88df..8dd8555 100644 --- a/system/typemill/author/system/plugins.twig +++ b/system/typemill/author/system/plugins.twig @@ -11,9 +11,11 @@ {% block javascript %} - - + + + + diff --git a/system/typemill/author/system/system.twig b/system/typemill/author/system/system.twig index b41ee5d..74bbd11 100644 --- a/system/typemill/author/system/system.twig +++ b/system/typemill/author/system/system.twig @@ -11,6 +11,7 @@ {% block javascript %} + diff --git a/system/typemill/author/system/themes.twig b/system/typemill/author/system/themes.twig index 58edaea..e8c3f9a 100644 --- a/system/typemill/author/system/themes.twig +++ b/system/typemill/author/system/themes.twig @@ -11,6 +11,7 @@ {% block javascript %} + diff --git a/system/typemill/author/system/user.twig b/system/typemill/author/system/user.twig new file mode 100644 index 0000000..7f20a3b --- /dev/null +++ b/system/typemill/author/system/user.twig @@ -0,0 +1,21 @@ +{% extends 'layouts/layoutSystem.twig' %} +{% block title %}{{ translate('Account') }}{% endblock %} + +{% block content %} + +

    {{ translate('User') }}

    + +
    + +{% endblock %} + +{% block javascript %} + + + + + + +{% endblock %} diff --git a/system/typemill/author/system/usernew.twig b/system/typemill/author/system/usernew.twig new file mode 100644 index 0000000..d3197a0 --- /dev/null +++ b/system/typemill/author/system/usernew.twig @@ -0,0 +1,21 @@ +{% extends 'layouts/layoutSystem.twig' %} +{% block title %}{{ translate('Create user') }}{% endblock %} + +{% block content %} + +

    {{ translate('Create user') }}

    + +
    + +{% endblock %} + +{% block javascript %} + + + + + + +{% endblock %} diff --git a/system/typemill/routes/api.php b/system/typemill/routes/api.php index 24658a7..54a78bd 100644 --- a/system/typemill/routes/api.php +++ b/system/typemill/routes/api.php @@ -1,23 +1,44 @@ get('/api/v1/systemnavi', ControllerApiGlobals::class . ':getSystemnavi')->setName('api.systemnavi.get')->add(new RestrictApiAccess()); -$app->get('/api/v1/mainnavi', ControllerApiGlobals::class . ':getMainnavi')->setName('api.mainnavi.get')->add(new RestrictApiAccess()); -$app->get('/api/v1/settings', ControllerApiSystemSettings::class . ':getSettings')->setName('api.settings.get')->add(new RestrictApiAccess()); -$app->post('/api/v1/settings', ControllerApiSystemSettings::class . ':updateSettings')->setName('api.settings.set')->add(new RestrictApiAccess()); -$app->post('/api/v1/theme', ControllerApiSystemThemes::class . ':updateTheme')->setName('api.theme.set')->add(new RestrictApiAccess()); -$app->post('/api/v1/plugin', ControllerApiSystemPlugins::class . ':updatePlugin')->setName('api.plugin.set')->add(new RestrictApiAccess()); -$app->get('/api/v1/users/getbynames', ControllerApiSystemUsers::class . ':getUsersByNames')->setName('api.usersbynames')->add(new RestrictApiAccess()); -$app->get('/api/v1/users/getbyemail', ControllerApiSystemUsers::class . ':getUsersByEmail')->setName('api.usersbyemail')->add(new RestrictApiAccess()); -$app->get('/api/v1/users/getbyrole', ControllerApiSystemUsers::class . ':getUsersByRole')->setName('api.usersbyrole')->add(new RestrictApiAccess()); -$app->put('/api/v1/account', ControllerApiSystemUsers::class . ':updateUser')->setName('api.user.update')->add(new RestrictApiAccess()); +$app->group('/api/v1', function (RouteCollectorProxy $group) use ($acl) { + # GLOBALS + $group->get('/systemnavi', ControllerApiGlobals::class . ':getSystemnavi')->setName('api.systemnavi.get')->add(new ApiAuthorization($acl, 'account', 'view')); # member + $group->get('/mainnavi', ControllerApiGlobals::class . ':getMainnavi')->setName('api.mainnavi.get')->add(new ApiAuthorization($acl, 'account', 'view')); # member + + # SYSTEM + $group->get('/settings', ControllerApiSystemSettings::class . ':getSettings')->setName('api.settings.get')->add(new ApiAuthorization($acl, 'system', 'view')); # admin + $group->post('/settings', ControllerApiSystemSettings::class . ':updateSettings')->setName('api.settings.set')->add(new ApiAuthorization($acl, 'system', 'update')); # admin + $group->post('/license', ControllerApiSystemLicense::class . ':createLicense')->setName('api.license.create')->add(new ApiAuthorization($acl, 'system', 'update')); # admin + $group->post('/theme', ControllerApiSystemThemes::class . ':updateTheme')->setName('api.theme.set')->add(new ApiAuthorization($acl, 'system', 'update')); # admin + $group->post('/plugin', ControllerApiSystemPlugins::class . ':updatePlugin')->setName('api.plugin.set')->add(new ApiAuthorization($acl, 'system', 'update')); # admin + $group->get('/users/getbynames', ControllerApiSystemUsers::class . ':getUsersByNames')->setName('api.usersbynames')->add(new ApiAuthorization($acl, 'user', 'update')); # admin + $group->get('/users/getbyemail', ControllerApiSystemUsers::class . ':getUsersByEmail')->setName('api.usersbyemail')->add(new ApiAuthorization($acl, 'user', 'update')); # admin + $group->get('/users/getbyrole', ControllerApiSystemUsers::class . ':getUsersByRole')->setName('api.usersbyrole')->add(new ApiAuthorization($acl, 'user', 'update')); # admin + $group->get('/userform', ControllerApiSystemUsers::class . ':getNewUserForm')->setName('api.user.form')->add(new ApiAuthorization($acl, 'user', 'update')); # admin + $group->post('/user', ControllerApiSystemUsers::class . ':createUser')->setName('api.user.create')->add(new ApiAuthorization($acl, 'user', 'update')); # admin + $group->put('/user', ControllerApiSystemUsers::class . ':updateUser')->setName('api.user.update')->add(new ApiAuthorization($acl, 'account', 'update')); # member + $group->delete('/user', ControllerApiSystemUsers::class . ':deleteUser')->setName('api.user.delete')->add(new ApiAuthorization($acl, 'account', 'delete')); # member + + # MEDIA + $group->post('/image', ControllerApiImage::class . ':saveImage')->setName('api.image.create'); + $group->get('/image', ControllerApiMedia::class . ':getImage')->setName('api.image.get'); + $group->put('/image', ControllerApiMedia::class . ':publishImage')->setName('api.image.publish'); + $group->delete('/image', ControllerApiMedia::class . ':deleteImage')->setName('api.image.delete'); + +})->add(new ApiAuthentication()); # https://stackoverflow.blog/2021/10/06/best-practices-for-authentication-and-authorization-for-rest-apis/ diff --git a/system/typemill/routes/web.php b/system/typemill/routes/web.php index ec9b852..2859f39 100644 --- a/system/typemill/routes/web.php +++ b/system/typemill/routes/web.php @@ -1,38 +1,50 @@ get('/tm/login', ControllerWebLogin::class . ':show')->setName('auth.show')->add(new RedirectIfAuthenticated($routeParser, $settings)); -$app->post('/tm/login', ControllerWebLogin::class . ':login')->setName('auth.login')->add(new RedirectIfAuthenticated($routeParser, $settings)); +# login/register +$app->group('/tm', function (RouteCollectorProxy $group) { -$app->get('/tm/system', ControllerWebSystem::class . ':showSettings')->setName('settings.show')->add(new RedirectIfUnauthenticated($routeParser)); -$app->get('/tm/themes', ControllerWebSystem::class . ':showThemes')->setName('themes.show')->add(new RedirectIfUnauthenticated($routeParser)); -$app->get('/tm/plugins', ControllerWebSystem::class . ':showPlugins')->setName('plugins.show')->add(new RedirectIfUnauthenticated($routeParser)); -$app->get('/tm/account', ControllerWebSystem::class . ':showAccount')->setName('user.account')->add(new RedirectIfUnauthenticated($routeParser)); -$app->get('/tm/users', ControllerWebSystem::class . ':showUsers')->setName('users.show')->add(new RedirectIfUnauthenticated($routeParser)); + $group->get('/login', ControllerWebAuth::class . ':show')->setName('auth.show'); + $group->post('/login', ControllerWebAuth::class . ':login')->setName('auth.login'); +})->add(new WebRedirectIfAuthenticated($routeParser, $settings)); +# author and editor area, requires authentication +$app->group('/tm', function (RouteCollectorProxy $group) use ($routeParser,$acl) { + $group->get('/logout', ControllerWebAuth::class . ':logout')->setName('auth.logout'); + $group->get('/system', ControllerWebSystem::class . ':showSettings')->setName('settings.show')->add(new WebAuthorization($routeParser, $acl, 'system', 'show')); # admin; + $group->get('/license', ControllerWebSystem::class . ':showLicense')->setName('license.show')->add(new WebAuthorization($routeParser, $acl, 'system', 'show')); # admin; + $group->get('/themes', ControllerWebSystem::class . ':showThemes')->setName('themes.show')->add(new WebAuthorization($routeParser, $acl, 'system', 'show')); # admin; + $group->get('/plugins', ControllerWebSystem::class . ':showPlugins')->setName('plugins.show')->add(new WebAuthorization($routeParser, $acl, 'system', 'show')); # admin; + $group->get('/account', ControllerWebSystem::class . ':showAccount')->setName('user.account')->add(new WebAuthorization($routeParser, $acl, 'account', 'view')); # member; + $group->get('/users', ControllerWebSystem::class . ':showUsers')->setName('users.show')->add(new WebAuthorization($routeParser, $acl, 'user', 'show')); # admin; + $group->get('/user/new', ControllerWebSystem::class . ':newUser')->setName('user.new')->add(new WebAuthorization($routeParser, $acl, 'user', 'create')); # admin; + $group->get('/user/{username}', ControllerWebSystem::class . ':showUser')->setName('user.show')->add(new WebAuthorization($routeParser, $acl, 'user', 'show')); # admin;; -# $app->get('/tm/user/new', ControllerSystem::class . ':newUser')->setName('user.new'); -# $app->get('/tm/users', ControllerSystem::class . ':listUser')->setName('user.list'); -# $app->post('/tm/user/create', ControllerSettings::class . ':createUser')->setName('user.create')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'create')); -# $app->post('/tm/user/update', ControllerSettings::class . ':updateUser')->setName('user.update')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'update')); -# $app->post('/tm/user/delete', ControllerSettings::class . ':deleteUser')->setName('user.delete')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'delete')); -# $app->get('/tm/user/{username}', ControllerSettings::class . ':showUser')->setName('user.show')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'view')); + # REFACTOR + $group->get('/content/visual[/{params:.*}]', ControllerAuthorEditor::class . ':showBlox')->setName('content.visual'); +})->add(new WebRedirectIfUnauthenticated($routeParser)); +$app->redirect('/tm', $routeParser->urlFor('auth.show'), 302); +$app->redirect('/tm/', $routeParser->urlFor('auth.show'), 302); +# same with setup redirect -$app->get('/tm/content/visual[/{params:.*}]', ControllerAuthorEditor::class . ':showBlox')->setName('content.visual'); - +# website $app->get('/[{params:.*}]', ControllerWebFrontend::class . ':index')->setName('home'); + + /* use Typemill\Controllers\ControllerAuthorEditor; use Typemill\Controllers\ControllerSettings; diff --git a/system/typemill/settings/license.yaml b/system/typemill/settings/license.yaml new file mode 100644 index 0000000..7a3b123 --- /dev/null +++ b/system/typemill/settings/license.yaml @@ -0,0 +1,15 @@ +license: + name: license + label: 'Your license key' + type: 'text' + required: true +email: + name: email + label: 'Your email' + type: 'text' + required: true +domain: + name: domain + label: 'Domain for license' + type: 'text' + required: true \ No newline at end of file diff --git a/system/typemill/settings/mainnavi.yaml b/system/typemill/settings/mainnavi.yaml index 28d03c2..e533b17 100644 --- a/system/typemill/settings/mainnavi.yaml +++ b/system/typemill/settings/mainnavi.yaml @@ -11,15 +11,15 @@ 'account': 'title': 'Account' 'routename': 'user.account' - 'aclresource': 'user' + 'aclresource': 'account' 'aclprivilege': 'view' 'frontend': 'title': 'Frontend' 'routename': 'home' - 'aclresource': 'user' + 'aclresource': 'account' 'aclprivilege': 'view' 'logout': 'title': 'Logout' - 'routename': 'auth.login' - 'aclresource': 'user' + 'routename': 'auth.logout' + 'aclresource': 'account' 'aclprivilege': 'view' \ No newline at end of file diff --git a/system/typemill/settings/permissions.yaml b/system/typemill/settings/permissions.yaml index cd1decf..a897056 100644 --- a/system/typemill/settings/permissions.yaml +++ b/system/typemill/settings/permissions.yaml @@ -2,7 +2,7 @@ member: name: member inherits: NULL permissions: - user: + account: - 'view' - 'update' - 'delete' diff --git a/system/typemill/settings/resources.yaml b/system/typemill/settings/resources.yaml index d9bf424..f50c45c 100644 --- a/system/typemill/settings/resources.yaml +++ b/system/typemill/settings/resources.yaml @@ -1,5 +1,5 @@ - 'content' - 'mycontent' +- 'account' - 'user' -- 'userlist' - 'system' \ No newline at end of file diff --git a/system/typemill/settings/system.yaml b/system/typemill/settings/system.yaml index fb2cc10..913a676 100644 --- a/system/typemill/settings/system.yaml +++ b/system/typemill/settings/system.yaml @@ -52,7 +52,7 @@ fieldsetsystem: type: text label: Google sitemap (readonly) css: w-half - readonly: true + disabled: true description: You can ping the sitemap with the following links to google and bing pingsitemap: type: checkbox @@ -64,37 +64,42 @@ fieldsetmedia: legend: Media fields: logo: - type: text + type: image label: Logo - css: w-half - maxlength: 60 favicon: - type: text + type: image label: Favicon + liveimagewidth: + type: number + label: Standard width for live pictures + placeholder: 820 + description: Default width of live images is 820px. Changes will apply to future uploads. css: w-half - maxlength: 60 - width: - type: text - label: Standard width for images - description: This applies only for future images in the content area. - css: w-half - maxlength: 60 - height: - type: text - label: Standard height for images + liveimageheight: + type: number + label: Standard height for live pictures description: If you add a value for the height, then the image will be cropped. css: w-half - maxlength: 60 - svg: - type: checkbox - label: Upload svg images - checkboxlabel: Allow upload of svg images (svg can be malicious, use trusted sources) - css: w-full - maxuploadsize: + maximageuploads: type: number - label: Maximum size for file-uploads in MB + label: Maximum size for image uploads in MB + description: The maximum image size might be limited by your server settings. + css: w-full + allowsvg: + type: checkbox + label: Allow svg + checkboxlabel: Allow the upload of svg images + css: w-full + convertwebp: + type: checkbox + label: Convert to webp + checkboxlabel: Try to convert uploaded images into the webp-format + css: w-full + maxfileuploads: + type: number + label: Maximum size for file uploads in MB description: The maximum file size might be limited by your server settings. - css: w-half + css: w-full fieldsetwriting: type: fieldset legend: Writing @@ -229,34 +234,4 @@ fieldsetdeveloper: type: checkbox label: Disable headers checkboxlabel: Disable the typemill headers and send your owwn - css: w-full -fieldsetapi: - type: fieldset - legend: API - fields: - api: - type: checkbox - label: Activate api - checkboxlabel: Activate the api - css: w-full - apikey: - type: text - label: Api key - css: w-full - apirole: - type: select - label: Api role - css: w-full - options: - 'admin': 'admin' - 'editor': 'editor' - 'author': 'author' - 'member': 'member' -fieldsetlicence: - type: fieldset - legend: Licence - fields: - licencekey: - type: text - label: Your licence for maker or business features css: w-full \ No newline at end of file diff --git a/system/typemill/settings/systemnavi.yaml b/system/typemill/settings/systemnavi.yaml index 47fbd99..b4fb962 100644 --- a/system/typemill/settings/systemnavi.yaml +++ b/system/typemill/settings/systemnavi.yaml @@ -16,15 +16,21 @@ 'icon': 'icon-plug' 'aclresource': 'system' 'aclprivilege': 'view' +'license': + 'title': 'License' + 'routename': 'license.show' + 'icon': 'icon-wrench' + 'aclresource': 'system' + 'aclprivilege': 'view' 'account': 'title': 'Account' 'routename': 'user.account' 'icon': 'icon-user' - 'aclresource': 'user' + 'aclresource': 'account' 'aclprivilege': 'view' 'users': 'title': 'Users' 'routename': 'users.show' 'icon': 'icon-group' - 'aclresource': 'userlist' + 'aclresource': 'user' 'aclprivilege': 'view' \ No newline at end of file diff --git a/system/typemill/system.php b/system/typemill/system.php index c046c16..81c3030 100644 --- a/system/typemill/system.php +++ b/system/typemill/system.php @@ -29,6 +29,7 @@ use Typemill\Extensions\TwigCsrfExtension; use Typemill\Extensions\TwigUrlExtension; use Typemill\Extensions\TwigUserExtension; use Typemill\Models\StorageWrapper; +use Typemill\Models\License; $timer = []; $timer['start'] = microtime(true); @@ -54,7 +55,7 @@ $settings = Settings::loadSettings(); if(isset($settings['displayErrorDetails']) && $settings['displayErrorDetails']) { ini_set('display_errors', 1); - ini_set('display_startup_errors', 1); +# ini_set('display_startup_errors', 1); } /**************************** @@ -93,13 +94,18 @@ $app->setBasePath($urlinfo['basepath']); $timer['container'] = microtime(true); - /**************************** * CREATE EVENT DISPATCHER * ****************************/ $dispatcher = new EventDispatcher(); +/**************************** +* Check Licence * +****************************/ + +$license = new License(); +$settings['license'] = $license->getLicenseScope($urlinfo); /**************************** * LOAD & UPDATE PLUGINS * @@ -110,7 +116,7 @@ $routes = []; $middleware = []; # if there are less plugins in the scan than in the settings, then a plugin has been removed -if(count($plugins) < count($settings['plugins'])) +if(isset($settings['plugins']) && (count($plugins) < count($settings['plugins'])) ) { $updateSettings = true; } @@ -130,8 +136,18 @@ foreach($plugins as $plugin) $updateSettings = true; } + # licence check + $PluginLicence = Plugins::getPremiumLicence($className); + if($PluginLicence) + { + if(!$settings['license'] OR !isset($settings['license'][$PluginLicence])) + { + $settings['plugins'][$pluginName]['active'] = false; + } + } + # if the plugin is activated, add routes/middleware and add plugin as event subscriber - if($settings['plugins'][$pluginName]['active']) + if(isset($settings['plugins'][$pluginName]['active']) && $settings['plugins'][$pluginName]['active']) { $routes = Plugins::getNewRoutes($className, $routes); $middleware = Plugins::getNewMiddleware($className, $middleware); @@ -140,9 +156,13 @@ foreach($plugins as $plugin) } } +# echo '
    ';
    +# print_r($settings);
    +# die();
    +
     # if plugins have been added or removed
     if(isset($updateSettings))
    -{	
    +{
     	# update stored settings file
     	Settings::updateSettings($settings);
     }
    @@ -262,6 +282,7 @@ $container->set('view', function() use ($settings, $csrf, $urlinfo, $translation
     
     	# $twig->addExtension(new \Nquire\Extensions\TwigUserExtension());
     
    +	# start csrf only if session is active
     	if($csrf)
     	{
     		$twig->addExtension(new TwigCsrfExtension($csrf));
    diff --git a/tailwind.config.js b/tailwind.config.js
    index 03933d8..3ea2f3a 100644
    --- a/tailwind.config.js
    +++ b/tailwind.config.js
    @@ -5,7 +5,10 @@ module.exports = {
         extend: {
           width: {
             'half': '48%',
    -      }       
    +      },
    +      opacity: {
    +        '0': '0',
    +      }             
         },
       },
       plugins: [],
    diff --git a/themes/cyanine/cyanine.yaml b/themes/cyanine/cyanine.yaml
    index 487e424..4f4f8ff 100644
    --- a/themes/cyanine/cyanine.yaml
    +++ b/themes/cyanine/cyanine.yaml
    @@ -9,8 +9,8 @@ amount: 25
     
     settings:
       landingpage: on
    -  landingpageIntro: 1
    -  landingpageNavi: 2
    +  introPosition: '1'
    +  naviPosition: '2'
       naviTitle: Get Started
       fontheadline: sans-serif
       fontnavi: sans-serif
    @@ -62,7 +62,7 @@ forms:
           type: fieldset
           legend: Landingpage Intro Segment
           fields:
    -        landingpageIntro:
    +        introPosition:
               type: number
               label: Position of Intro Segment
               description: Use 0 to disable the section
    @@ -104,7 +104,7 @@ forms:
           type: fieldset
           legend: Landingpage Info Segment
           fields:
    -        landingpageInfo:
    +        infoPosition:
               type: number
               label: Position of Info Segment
               description: Use 0 to disable the section          
    @@ -116,7 +116,7 @@ forms:
           type: fieldset
           legend: Landingpage Teaser Segment
           fields:
    -        landingpageTeaser:
    +        teaserPosition:
               type: number
               label: Position of Teaser Segment
               description: Use 0 to disable the section
    @@ -173,7 +173,7 @@ forms:
           type: fieldset
           legend: Landingpage Contrast Segment
           fields:
    -        landingpageContrast:
    +        contrastPosition:
               type: number
               label: Position of Contrast Segment
               description: Use 0 to disable the section          
    @@ -196,7 +196,7 @@ forms:
           type: fieldset
           legend: Landingpage Navigation Segment
           fields:
    -        landingpageNavi:
    +        naviPosition:
               type: number
               label: Position of Navi Segment
               description: Use 0 to disable the section          
    @@ -211,7 +211,7 @@ forms:
           type: fieldset
           legend: Landingpage News Segment      
           fields:
    -        landingpageNews:
    +        newsPosition:
               type: number
               label: Position of News Segment
               description: Use 0 to disable the section