diff --git a/.htaccess b/.htaccess index 659993e..484d1ad 100644 --- a/.htaccess +++ b/.htaccess @@ -1,3 +1,77 @@ -RewriteEngine on -RewriteRule ^$ public/ [L] -RewriteRule (.*) public/$1 [L] + + +RewriteEngine On + +# If your homepage is http://yourdomain.com/yoursite +# Set the RewriteBase to: +# RewriteBase /yoursite + +# In some environements, an empty RewriteBase is required: +# RewriteBase / + +# Use this to redirect HTTP to HTTPS on apache servers +# RewriteCond %{HTTPS} off +# RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] + +# Use this to redirect www to non-wwww on apache servers +# RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC] +# RewriteRule ^(.*)$ http://%1/$1 [R=301,L] + +# Use this to redirect slash/ to url without slash on apache servers +# RewriteCond %{REQUEST_FILENAME} !-d +# RewriteRule ^(.*)/$ /$1 [R=301,L] + +# Removes index.php +RewriteCond %{THE_REQUEST} ^GET.*index\.php [NC] +RewriteRule (.*?)index\.php/*(.*) /$1$2 [R=301,NE,L] + +# REWRITE TO INDEX + +# If the requested path and file not /index.php +RewriteCond %{REQUEST_URI} !^/index\.php + +# if requested doesn't match a physical file +RewriteCond %{REQUEST_FILENAME} !-f + +# if requested doesn't match a physical folder +RewriteCond %{REQUEST_FILENAME} !-d + +# then rewrite the request to the index.php script +RewriteRule ^ index.php [QSA,L] + + +# FILE/FOLDER PROTECTION + +# Deny access to these file types generally +RewriteRule ^(.*)?\.yml$ - [F,L] +Rewriterule ^(.*)?\.yaml$ - [F,L] +RewriteRule ^(.*)?\.txt$ - [F,L] +RewriteRule ^(.*)?\.example$ - [F,L] +RewriteRule ^(.*)?\.git+ - [F,L] +RewriteRule ^(.*)?\.md - [F,L] +RewriteCond %{REQUEST_URI} !/index\.php +RewriteRule ^(.*)?\.ph - [F,L] +RewriteRule ^(.*)?\.twig - [F,L] +RewriteRule ^(media\/tmp\/) - [F,L] + +# Block access to specific files in the root folder +RewriteRule ^(composer\.lock|composer\.json|\.htaccess)$ error [F,L] + +# block files and folders starting with a dot except for the .well-known folder (Let's Encrypt) +RewriteRule (^|/)\.(?!well-known\/) index.php [L] + +# Allow access to frontend files in author folder +RewriteRule ^(system\/typemill\/author\/css\/) - [L] +RewriteRule ^(system\/typemill\/author\/img\/) - [L] +RewriteRule ^(system\/typemill\/author\/js\/) - [L] +RewriteRule ^(system\/author\/css\/) - [L] +RewriteRule ^(system\/author\/img\/) - [L] +RewriteRule ^(system\/author\/js\/) - [L] + +# redirect all other direct requests to the following physical folders to the index.php so pages with same name work +RewriteRule ^(system|content|data|settings|(media\/files\/)) index.php [QSA,L] + +# disallow browsing other folders generally +Options -Indexes + + \ No newline at end of file diff --git a/.htaccessold b/.htaccessold deleted file mode 100644 index 70c9ced..0000000 --- a/.htaccessold +++ /dev/null @@ -1,74 +0,0 @@ - - -RewriteEngine On - -# If your homepage is http://yourdomain.com/yoursite -# Set the RewriteBase to: -# RewriteBase /yoursite - -# In some environements, an empty RewriteBase is required: -# RewriteBase / - -# Use this to redirect HTTP to HTTPS on apache servers -# RewriteCond %{HTTPS} off -# RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] - -# Use this to redirect www to non-wwww on apache servers -# RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC] -# RewriteRule ^(.*)$ http://%1/$1 [R=301,L] - -# Use this to redirect slash/ to url without slash on apache servers -# RewriteCond %{REQUEST_FILENAME} !-d -# RewriteRule ^(.*)/$ /$1 [R=301,L] - -# Removes index.php -RewriteCond %{THE_REQUEST} ^GET.*index\.php [NC] -RewriteRule (.*?)index\.php/*(.*) /$1$2 [R=301,NE,L] - -# REWRITE TO INDEX - -# If the requested path and file not /index.php -RewriteCond %{REQUEST_URI} !^/index\.php - -# if requested doesn't match a physical file -RewriteCond %{REQUEST_FILENAME} !-f - -# if requested doesn't match a physical folder -RewriteCond %{REQUEST_FILENAME} !-d - -# then rewrite the request to the index.php script -RewriteRule ^ index.php [QSA,L] - - -# FILE/FOLDER PROTECTION - -# Deny access to these file types generally -RewriteRule ^(.*)?\.yml$ - [F,L] -Rewriterule ^(.*)?\.yaml$ - [F,L] -RewriteRule ^(.*)?\.txt$ - [F,L] -RewriteRule ^(.*)?\.example$ - [F,L] -RewriteRule ^(.*)?\.git+ - [F,L] -RewriteRule ^(.*)?\.md - [F,L] -RewriteCond %{REQUEST_URI} !/index\.php -RewriteRule ^(.*)?\.ph - [F,L] -RewriteRule ^(.*)?\.twig - [F,L] -RewriteRule ^(media\/tmp\/) - [F,L] - -# Block access to specific files in the root folder -RewriteRule ^(composer\.lock|composer\.json|\.htaccess)$ error [F,L] - -# block files and folders starting with a dot except for the .well-known folder (Let's Encrypt) -RewriteRule (^|/)\.(?!well-known\/) index.php [L] - -# Allow access to frontend files in author folder -RewriteRule ^(system\/author\/css\/) - [L] -RewriteRule ^(system\/author\/img\/) - [L] -RewriteRule ^(system\/author\/js\/) - [L] - -# redirect all other direct requests to the following physical folders to the index.php so pages with same name work -RewriteRule ^(system|content|data|settings|(media\/files\/)) index.php [QSA,L] - -# disallow browsing other folders generally -Options -Indexes - - \ No newline at end of file diff --git a/.htaccesspublic b/.htaccesspublic new file mode 100644 index 0000000..659993e --- /dev/null +++ b/.htaccesspublic @@ -0,0 +1,3 @@ +RewriteEngine on +RewriteRule ^$ public/ [L] +RewriteRule (.*) public/$1 [L] diff --git a/cache/securitylog.txt b/cache/securitylog.txt deleted file mode 100644 index 99050d7..0000000 --- a/cache/securitylog.txt +++ /dev/null @@ -1,3 +0,0 @@ -127.0.0.1;2022-05-26 22:14:00;wrong login -127.0.0.1;2022-05-26 22:14:07;wrong captcha http://localhost/typemill/tm/login -127.0.0.1;2022-05-27 21:33:28;wrong login diff --git a/index.php b/index.php index 2fe58d1..3dac4a4 100644 --- a/index.php +++ b/index.php @@ -2,6 +2,6 @@ require __DIR__ . '/system/vendor/autoload.php'; -require __DIR__ . '/system/system.php'; +require __DIR__ . '/system/typemill/system.php'; $app->run(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8be4d23..b3d5bac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,1039 @@ { "name": "typemill", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "typemill", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "fs-extra": "^10.0.1", + "tailwindcss": "^3.1.6" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "dependencies": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==", + "dev": true + }, + "node_modules/detective": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", + "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "dev": true, + "dependencies": { + "acorn-node": "^1.8.2", + "defined": "^1.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "detective": "bin/detective.js" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-extra": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", + "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lilconfig": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", + "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss": { + "version": "8.4.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", + "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", + "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", + "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.6" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.6.tgz", + "integrity": "sha512-7skAOY56erZAFQssT1xkpk+kWt2NrO45kORlxFPXUt3CiGsVPhH1smuH5XoDH6sGPXLyBv+zgCKA2HWBsgCytg==", + "dev": true, + "dependencies": { + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "color-name": "^1.1.4", + "detective": "^5.2.1", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "lilconfig": "^2.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.14", + "postcss-import": "^14.1.0", + "postcss-js": "^4.0.0", + "postcss-load-config": "^3.1.4", + "postcss-nested": "5.0.6", + "postcss-selector-parser": "^6.0.10", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.22.1" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + } + }, "dependencies": { + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "requires": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==", + "dev": true + }, + "detective": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", + "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "dev": true, + "requires": { + "acorn-node": "^1.8.2", + "defined": "^1.0.0", + "minimist": "^1.2.6" + } + }, + "didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, "fs-extra": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", @@ -15,12 +1045,82 @@ "universalify": "^2.0.0" } }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, "graceful-fs": { "version": "4.2.9", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", "dev": true }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -31,11 +1131,272 @@ "universalify": "^2.0.0" } }, + "lilconfig": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", + "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + }, + "postcss": { + "version": "8.4.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", + "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "dev": true, + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + } + }, + "postcss-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", + "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "dev": true, + "requires": { + "camelcase-css": "^2.0.1" + } + }, + "postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "requires": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + } + }, + "postcss-nested": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", + "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.6" + } + }, + "postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "requires": { + "pify": "^2.3.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "tailwindcss": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.6.tgz", + "integrity": "sha512-7skAOY56erZAFQssT1xkpk+kWt2NrO45kORlxFPXUt3CiGsVPhH1smuH5XoDH6sGPXLyBv+zgCKA2HWBsgCytg==", + "dev": true, + "requires": { + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "color-name": "^1.1.4", + "detective": "^5.2.1", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "lilconfig": "^2.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.14", + "postcss-import": "^14.1.0", + "postcss-js": "^4.0.0", + "postcss-load-config": "^3.1.4", + "postcss-nested": "5.0.6", + "postcss-selector-parser": "^6.0.10", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.22.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true } } } diff --git a/package.json b/package.json index 3bf9aef..3bc2e24 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "homepage": "https://github.com/typemill/typemill#readme", "devDependencies": { - "fs-extra": "^10.0.1" + "fs-extra": "^10.0.1", + "tailwindcss": "^3.1.6" } -} \ No newline at end of file +} diff --git a/system/Models/User.php b/system/Models/User.php index ca66671..f0f8dbe 100644 --- a/system/Models/User.php +++ b/system/Models/User.php @@ -141,6 +141,7 @@ class User extends WriteYaml if($user) { $user['lastlogin'] = time(); + $user['tmpApiKey'] = ; $_SESSION['user'] = $user['username']; $_SESSION['role'] = $user['userrole']; diff --git a/system/typemill/Controllers/Controller.php b/system/typemill/Controllers/Controller.php index 567a477..ea51f0a 100644 --- a/system/typemill/Controllers/Controller.php +++ b/system/typemill/Controllers/Controller.php @@ -31,6 +31,33 @@ abstract class Controller } + + + + + + +/* + protected function setUrlCollection($uri) + { + $scheme = $uri->getScheme(); + $authority = $uri->getAuthority(); + $protocol = ($scheme ? $scheme . ':' : '') . ($authority ? '//' . $authority : ''); + + $this->basePath = $this->c->get('basePath'); + $this->currentPath = $uri->getPath(); + $this->fullBaseUrl = $protocol . $this->basePath; + $this->fullCurrentUrl = $protocol . $this->currentPath; + + $this->urlCollection = [ + 'basePath' => $this->basePath, + 'currentPath' => $this->currentPath, + 'fullBaseUrl' => $this->fullBaseUrl, + 'fullCurrentUrl' => $this->fullCurrentUrl + ]; + } + + /* # holds the pimple container protected $c; diff --git a/system/typemill/Controllers/ControllerData.php b/system/typemill/Controllers/ControllerData.php new file mode 100644 index 0000000..2a883ce --- /dev/null +++ b/system/typemill/Controllers/ControllerData.php @@ -0,0 +1,90 @@ +getYaml('system/typemill/settings', 'mainnavi.yaml'); + + $allowedmainnavi = []; + + $acl = $this->c->get('acl'); + + foreach($mainnavi as $name => $naviitem) + { + if($acl->isAllowed($userrole, $naviitem['aclresource'], $naviitem['aclprivilege'])) + { + # not nice: check if the navi-item is active (e.g if segments like "content" or "system" is in current url) + if($name == 'content' && strpos($this->settings['routepath'], 'tm/content')) + { + $naviitem['active'] = true; + } + elseif($name == 'account' && strpos($this->settings['routepath'], 'tm/account')) + { + $naviitem['active'] = true; + } + elseif($name == 'system') + { + $naviitem['active'] = true; + } + + $allowedmainnavi[$name] = $naviitem; + } + } + + # if system is there, then we do not need the account item + if(isset($allowedmainnavi['system'])) + { + unset($allowedmainnavi['account']); + } + + # set correct editor mode according to user settings + if(isset($allowedmainnavi['content']) && $this->settings['editor'] == 'raw') + { + $allowedmainnavi['content']['routename'] = "content.raw"; + } + + return $allowedmainnavi; + } + + protected function getSystemNavigation($userrole) + { + $yaml = new Yaml('\Typemill\Models\Storage'); + + $systemnavi = $yaml->getYaml('system/typemill/settings', 'systemnavi.yaml'); + $systemnavi = $this->c->get('dispatcher')->dispatch(new OnSystemnaviLoaded($systemnavi), 'onSystemnaviLoaded')->getData(); + + $allowedsystemnavi = []; + + $acl = $this->c->get('acl'); + + foreach($systemnavi as $name => $naviitem) + { + # check if the navi-item is active (e.g if segments like "content" or "system" is in current url) + # a bit fragile because url-segment and name/key in systemnavi.yaml and plugins have to be the same + if(strpos($this->settings['routepath'], 'tm/' . $name)) + { + $naviitem['active'] = true; + } + + if($acl->isAllowed($userrole, $naviitem['aclresource'], $naviitem['aclprivilege'])) + { + $allowedsystemnavi[$name] = $naviitem; + } + } + + return $allowedsystemnavi; + } +} \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerSystem.php b/system/typemill/Controllers/ControllerSystem.php new file mode 100644 index 0000000..7965eb4 --- /dev/null +++ b/system/typemill/Controllers/ControllerSystem.php @@ -0,0 +1,1248 @@ +c->get('settings'); + $defaultSettings = \Typemill\Settings::getDefaultSettings(); + $copyright = $this->getCopyright(); + $languages = $this->getLanguages(); + $locale = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? substr($_SERVER["HTTP_ACCEPT_LANGUAGE"],0,2) : 'en'; + $route = $request->getAttribute('route'); + $navigation = $this->getMainNavigation(); + + # set navigation active + $navigation['System']['active'] = true; + + return $this->render($response, 'settings/system.twig', array( + 'settings' => $settings, + 'acl' => $this->c->acl, + 'navigation' => $navigation, + 'copyright' => $copyright, + 'languages' => $languages, + 'locale' => $locale, + 'formats' => $defaultSettings['formats'], + 'route' => $route->getName() + )); + + */ + +/* ENDPOINTS + + -> settings -> get + -> systemnavi -> get + -> mainnavi -> get + -> + +*/ + $user = new User(); + $user->setUser($_SESSION['username']); + $userdata = $user->getUserData(); + + return $this->c->get('view')->render($response, 'system/system.twig', [ + 'basicauth' => $user->getBasicAuth(), + 'settings' => $this->settings, + 'mainnavi' => $this->getMainNavigation($userdata['userrole']), + 'systemnavi' => $this->getSystemNavigation($userdata['userrole']), + 'jsdata' => [ + 'settings' => $this->settings, + ] + + # main navigation + # sidebar navigation + # settings area + + #'captcha' => $this->checkIfAddCaptcha(), + ]); + } + + +/* + public function showBlank($request, $response, $args) + { + $user = new User(); + $settings = $this->c->get('settings'); + $route = $request->getAttribute('route'); + $navigation = $this->getMainNavigation(); + + $content = '

Hello

I am the showBlank method from the settings controller

In most cases I have been called from a plugin. But if you see this content, then the plugin does not work or has forgotten to inject its own content.

'; + + return $this->render($response, 'settings/blank.twig', array( + 'settings' => $settings, + 'acl' => $this->c->acl, + 'navigation' => $navigation, + 'content' => $content, + 'route' => $route->getName() + )); + } + + + public function saveSettings($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('settings.show')); + } + + $settings = \Typemill\Settings::getUserSettings(); + $defaultSettings = \Typemill\Settings::getDefaultSettings(); + $params = $request->getParams(); + $files = $request->getUploadedFiles(); + $newSettings = isset($params['settings']) ? $params['settings'] : false; + $validate = new Validation(); + $processImage = new ProcessImage(); + + if($newSettings) + { + # check for image settings + $imgwidth = isset($newSettings['images']['live']['width']) ? $newSettings['images']['live']['width'] : false; + $imgheight = isset($newSettings['images']['live']['height']) ? $newSettings['images']['live']['height'] : false; + + # make sure only allowed fields are stored + $newSettings = array( + 'title' => $newSettings['title'], + 'author' => $newSettings['author'], + 'copyright' => $newSettings['copyright'], + 'year' => $newSettings['year'], + 'language' => $newSettings['language'], + 'langattr' => $newSettings['langattr'], + 'editor' => $newSettings['editor'], + 'formats' => $newSettings['formats'], + 'access' => isset($newSettings['access']) ? true : null, + 'pageaccess' => isset($newSettings['pageaccess']) ? true : null, + 'hrdelimiter' => isset($newSettings['hrdelimiter']) ? true : null, + 'restrictionnotice' => $newSettings['restrictionnotice'], + 'wraprestrictionnotice' => isset($newSettings['wraprestrictionnotice']) ? true : null, + 'headlineanchors' => isset($newSettings['headlineanchors']) ? $newSettings['headlineanchors'] : null, + 'displayErrorDetails' => isset($newSettings['displayErrorDetails']) ? true : null, + 'twigcache' => isset($newSettings['twigcache']) ? true : null, + 'proxy' => isset($newSettings['proxy']) ? true : null, + 'trustedproxies' => $newSettings['trustedproxies'], + 'headersoff' => isset($newSettings['headersoff']) ? true : null, + 'urlschemes' => $newSettings['urlschemes'], + 'svg' => isset($newSettings['svg']) ? true : null, + 'recoverpw' => isset($newSettings['recoverpw']) ? true : null, + 'recoverfrom' => $newSettings['recoverfrom'], + 'recoversubject' => $newSettings['recoversubject'], + 'recovermessage' => $newSettings['recovermessage'], + 'securitylog' => isset($newSettings['securitylog']) ? true : null, + 'oldslug' => isset($newSettings['oldslug']) ? true : null, + 'refreshcache' => isset($newSettings['refreshcache']) ? true : null, + 'pingsitemap' => isset($newSettings['pingsitemap']) ? true : null, + ); + + # https://www.slimframework.com/docs/v3/cookbook/uploading-files.html; + + $copyright = $this->getCopyright(); + + $validate->settings($newSettings, $copyright, $defaultSettings['formats'], 'settings'); + + # use custom image settings + if( $imgwidth && ctype_digit($imgwidth) && (strlen($imgwidth) < 5) ) + { + $newSettings['images']['live']['width'] = $imgwidth; + } + if( $imgheight && ctype_digit($imgheight) && (strlen($imgheight) < 5) ) + { + $newSettings['images']['live']['height'] = $imgheight; + } + } + else + { + $this->c->flash->addMessage('error', 'Wrong Input'); + return $response->withRedirect($this->c->router->pathFor('settings.show')); + } + + if(isset($_SESSION['errors'])) + { + $this->c->flash->addMessage('error', 'Please correct the errors'); + return $response->withRedirect($this->c->router->pathFor('settings.show')); + } + + if(!$processImage->checkFolders()) + { + $this->c->flash->addMessage('error', 'Please make sure that your media folder exists and is writable.'); + return $response->withRedirect($this->c->router->pathFor('settings.show')); + } + + # handle single input with single file upload + $logo = $files['settings']['logo']; + $allowed = ['jpg', 'jpeg', 'png', 'svg']; + if($logo->getError() === UPLOAD_ERR_OK) + { + $extension = pathinfo($logo->getClientFilename(), PATHINFO_EXTENSION); + if(!in_array(strtolower($extension), $allowed)) + { + $_SESSION['errors']['settings']['logo'] = array('Only jpg, jpeg, png and svg allowed'); + $this->c->flash->addMessage('error', 'Please correct the errors'); + return $response->withRedirect($this->c->router->pathFor('settings.show')); + } + + foreach($allowed as $logoextension) + { + $processImage->deleteImage('logo.' . $logoextension); + } + + $newSettings['logo'] = $processImage->moveUploadedImage($logo, $overwrite = true, $name = 'logo'); + $processImage->copyImage('logo.' . $logoextension, $processImage->liveFolder, $processImage->thumbFolder); + } + elseif(isset($params['settings']['deletelogo']) && $params['settings']['deletelogo'] == 'delete') + { + foreach($allowed as $logoextension) + { + $processImage->deleteImage('logo.' . $logoextension); + } + $newSettings['logo'] = ''; + } + else + { + $newSettings['logo'] = isset($settings['logo']) ? $settings['logo'] : ''; + } + + # handle single input with single file upload + $favicon = $files['settings']['favicon']; + if ($favicon->getError() === UPLOAD_ERR_OK) + { + $extension = pathinfo($favicon->getClientFilename(), PATHINFO_EXTENSION); + if(strtolower($extension) != 'png') + { + $_SESSION['errors']['settings']['favicon'] = array('Only .png-files allowed'); + $this->c->flash->addMessage('error', 'Please correct the errors'); + return $response->withRedirect($this->c->router->pathFor('settings.show')); + } + + $processFavImage = new ProcessImage([ + '16' => ['width' => 16, 'height' => 16], + '32' => ['width' => 32, 'height' => 32], + '72' => ['width' => 72, 'height' => 72], + '114' => ['width' => 114, 'height' => 114], + '144' => ['width' => 144, 'height' => 144], + '180' => ['width' => 180, 'height' => 180], + ]); + $favicons = $processFavImage->generateSizesFromImageFile('favicon.png', $favicon->file); + + foreach($favicons as $key => $favicon) + { + imagepng( $favicon, $processFavImage->fileFolder . 'favicon-' . $key . '.png' ); + } + + $newSettings['favicon'] = 'favicon'; + } + elseif(isset($params['settings']['deletefav']) && $params['settings']['deletefav'] == 'delete') + { + $processFiles = new ProcessFile(); + $processFiles->deleteFileWithName('favicon-*.png'); + $newSettings['favicon'] = ''; + } + else + { + $newSettings['favicon'] = isset($settings['favicon']) ? $settings['favicon'] : ''; + } + + # store updated settings + \Typemill\Settings::updateSettings(array_merge($settings, $newSettings)); + + $this->c->flash->addMessage('info', 'Settings are stored'); + return $response->withRedirect($this->c->router->pathFor('settings.show')); + } + } + + /********************* + ** THEME SETTINGS ** + *********************/ + +/* + public function showThemes($request, $response, $args) + { + $userSettings = $this->c->get('settings'); + $themes = $this->getThemes(); + $themedata = array(); + $fieldsModel = new Fields($this->c); + + foreach($themes as $themeName) + { + # if theme is active, list it first + if($userSettings['theme'] == $themeName) + { + $themedata = array_merge(array($themeName => null), $themedata); + } + else + { + $themedata[$themeName] = null; + } + + $themeSettings = \Typemill\Settings::getObjectSettings('themes', $themeName); + + # add standard-textarea for custom css + $themeSettings['forms']['fields']['customcss'] = ['type' => 'textarea', 'label' => 'Custom CSS', 'rows' => 10, 'class' => 'codearea', 'description' => 'You can overwrite the theme-css with your own css here.']; + + # load custom css-file + $write = new write(); + $customcss = $write->getFile('cache', $themeName . '-custom.css'); + $themeSettings['settings']['customcss'] = $customcss; + + + if($themeSettings) + { + # store them as default theme data with author, year, default settings and field-definitions + $themedata[$themeName] = $themeSettings; + } + + if(isset($themeSettings['forms']['fields'])) + { + $fields = $fieldsModel->getFields($userSettings, 'themes', $themeName, $themeSettings); + + # overwrite original theme form definitions with enhanced form objects + $themedata[$themeName]['forms']['fields'] = $fields; + } + + # add the preview image + $img = getcwd() . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . $themeName . DIRECTORY_SEPARATOR . $themeName; + + $image = false; + if(file_exists($img . '.jpg')) + { + $image = $themeName . '.jpg'; + } + if(file_exists($img . '.png')) + { + $image = $themeName . '.png'; + } + + $themedata[$themeName]['img'] = $image; + } + + # add the users for navigation + $route = $request->getAttribute('route'); + $navigation = $this->getMainNavigation(); + + # set navigation active + $navigation['Themes']['active'] = true; + + return $this->render($response, 'settings/themes.twig', array( + 'settings' => $userSettings, + 'acl' => $this->c->acl, + 'navigation' => $navigation, + 'themes' => $themedata, + 'route' => $route->getName() + )); + } + + public function showPlugins($request, $response, $args) + { + $userSettings = $this->c->get('settings'); + $plugins = array(); + $fieldsModel = new Fields($this->c); + $fields = array(); + + # iterate through the plugins in the stored user settings + foreach($userSettings['plugins'] as $pluginName => $pluginUserSettings) + { + # add plugin to plugin Data, if active, set it first + # if plugin is active, list it first + if($userSettings['plugins'][$pluginName]['active'] == true) + { + $plugins = array_merge(array($pluginName => null), $plugins); + } + else + { + $plugins[$pluginName] = Null; + } + + # Check if the user has deleted a plugin. Then delete it in the settings and store the updated settings. + if(!is_dir($userSettings['rootPath'] . 'plugins' . DIRECTORY_SEPARATOR . $pluginName)) + { + # remove the plugin settings and store updated settings + \Typemill\Settings::removePluginSettings($pluginName); + continue; + } + + # load the original plugin definitions from the plugin folder (author, version and stuff) + $pluginOriginalSettings = \Typemill\Settings::getObjectSettings('plugins', $pluginName); + if($pluginOriginalSettings) + { + # store them as default plugin data with plugin author, plugin year, default settings and field-definitions + $plugins[$pluginName] = $pluginOriginalSettings; + } + + # check, if the plugin has been disabled in the form-session-data + if(isset($_SESSION['old']) && !isset($_SESSION['old'][$pluginName]['active'])) + { + $plugins[$pluginName]['settings']['active'] = false; + } + + # if the plugin defines forms and fields, so that the user can edit the plugin settings in the frontend + if(isset($pluginOriginalSettings['forms']['fields'])) + { + # get all the fields and prefill them with the dafault-data, the user-data or old input data + $fields = $fieldsModel->getFields($userSettings, 'plugins', $pluginName, $pluginOriginalSettings); + + # overwrite original plugin form definitions with enhanced form objects + $plugins[$pluginName]['forms']['fields'] = $fields; + } + } + + $route = $request->getAttribute('route'); + $navigation = $this->getMainNavigation(); + + # set navigation active + $navigation['Plugins']['active'] = true; + + return $this->render($response, 'settings/plugins.twig', array( + 'settings' => $userSettings, + 'acl' => $this->c->acl, + 'navigation' => $navigation, + 'plugins' => $plugins, + 'route' => $route->getName() + )); + } + + /************************************* + ** SAVE THEME- AND PLUGIN-SETTINGS ** + *************************************/ + + /* + public function saveThemes($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('themes.show')); + } + + $userSettings = \Typemill\Settings::getUserSettings(); + $params = $request->getParams(); + $themeName = isset($params['theme']) ? $params['theme'] : false; + $userInput = isset($params[$themeName]) ? $params[$themeName] : false; + $validate = new Validation(); + $themeSettings = \Typemill\Settings::getObjectSettings('themes', $themeName); + + if(isset($themeSettings['settings']['images'])) + { + # get the default settings + $defaultSettings = \Typemill\Settings::getDefaultSettings(); + + # merge the default image settings with the theme image settings, delete all others (image settings from old theme) + $userSettings['images'] = array_merge($defaultSettings['images'], $themeSettings['settings']['images']); + } + + # set theme name and delete theme settings from user settings for the case, that the new theme has no settings + $userSettings['theme'] = $themeName; + + # extract the custom css from user input + $customcss = isset($userInput['customcss']) ? $userInput['customcss'] : false; + + # delete custom css from userinput + unset($userInput['customcss']); + + $write = new write(); + + # make sure no file is set if there is no custom css + if(!$customcss OR $customcss == '') + { + # delete the css file if exists + $write->deleteFileWithPath('cache' . DIRECTORY_SEPARATOR . $themeName . '-custom.css'); + } + else + { + if ( $customcss != strip_tags($customcss) ) + { + $_SESSION['errors'][$themeName]['customcss'][] = 'custom css contains html'; + } + else + { + # store css + $write = new write(); + $write->writeFile('cache', $themeName . '-custom.css', $customcss); + } + } + + if($userInput) + { + # validate the user-input and return image-fields if they are defined + $imageFields = $this->validateInput('themes', $themeName, $userInput, $validate); + + # set user input as theme settings + $userSettings['themes'][$themeName] = $userInput; + } + + # handle images + $images = $request->getUploadedFiles(); + + if(!isset($_SESSION['errors']) && isset($images[$themeName])) + { + $userInput = $this->saveImages($imageFields, $userInput, $userSettings, $images[$themeName]); + + # set user input as theme settings + $userSettings['themes'][$themeName] = $userInput; + } + + # 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($this->c->router->pathFor('themes.show')); + } + + # store updated settings + \Typemill\Settings::updateSettings($userSettings); + + $this->c->flash->addMessage('info', 'Settings are stored'); + return $response->withRedirect($this->c->router->pathFor('themes.show')); + } + } + + public function savePlugins($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('plugins.show')); + } + + $userSettings = \Typemill\Settings::getUserSettings(); + $pluginSettings = array(); + $userInput = $request->getParams(); + $validate = new Validation(); + + # use the stored user settings and iterate over all original plugin settings, so we do not forget any... + foreach($userSettings['plugins'] as $pluginName => $pluginUserSettings) + { + # if there are no input-data for this plugin, then use the stored plugin settings + if(!isset($userInput[$pluginName])) + { + $pluginSettings[$pluginName] = $pluginUserSettings; + } + else + { + # fetch the original settings from the folder to get the field definitions + $originalSettings = \Typemill\Settings::getObjectSettings('plugins', $pluginName); + + # check if the plugin has dependencies + if(isset($userInput[$pluginName]['active']) && isset($originalSettings['dependencies'])) + { + foreach($originalSettings['dependencies'] as $dependency) + { + if(!isset($userInput[$dependency]['active']) OR !$userInput[$dependency]['active']) + { + $this->c->flash->addMessage('error', 'Activate the plugin ' . $dependency . ' before you activate the plugin ' . $pluginName); + return $response->withRedirect($this->c->router->pathFor('plugins.show')); + } + } + } + + # validate the user-input + $imageFields = $this->validateInput('plugins', $pluginName, $userInput[$pluginName], $validate, $originalSettings); + + # use the input data + $pluginSettings[$pluginName] = $userInput[$pluginName]; + } + + # handle images + $images = $request->getUploadedFiles(); + + if(!isset($_SESSION['errors']) && isset($images[$pluginName])) + { + $userInput[$pluginName] = $this->saveImages($imageFields, $userInput[$pluginName], $userSettings, $images[$pluginName]); + + # set user input as theme settings + $pluginSettings[$pluginName] = $userInput[$pluginName]; + } + + # deactivate the plugin, if there is no active flag + if(!isset($userInput[$pluginName]['active'])) + { + $pluginSettings[$pluginName]['active'] = false; + } + } + + if(isset($_SESSION['errors'])) + { + $this->c->flash->addMessage('error', 'Please correct the errors below'); + } + else + { + # if everything is valid, add plugin settings to base settings again + $userSettings['plugins'] = $pluginSettings; + + # store updated settings + \Typemill\Settings::updateSettings($userSettings); + + $this->c->flash->addMessage('info', 'Settings are stored'); + } + + return $response->withRedirect($this->c->router->pathFor('plugins.show')); + } + } + + /*********************** + ** USER MANAGEMENT ** + ***********************/ + + public function showAccount($request, $response, $args) + { + $username = $_SESSION['user']; + + $validate = new Validation(); + + if($validate->username($username)) + { + # get settings + $settings = $this->c->get('settings'); + + # get user with userdata + $user = new User(); + $userdata = $user->getSecureUser($username); + + # 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['Account']['active'] = true; + + 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 +# 'userrole' => false, // not needed ? +# 'username' => $args['username'], // not needed ? + 'route' => $route->getName() // needed to set link active + )); + } + + $this->c->flash->addMessage('error', 'User does not exists'); + return $response->withRedirect($this->c->router->pathFor('home')); + } + + 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 listUser($request, $response) + { + $user = new User(); + $users = $user->getUsers(); + $userdata = array(); + $route = $request->getAttribute('route'); + $settings = $this->c->get('settings'); + $navigation = $this->getMainNavigation(); + + # set navigation active + $navigation['Users']['active'] = true; + + # set standard template + $template = 'settings/userlist.twig'; + + # use vue template for many users + $totalusers = count($users); + + if($totalusers > 10) + { + $template = 'settings/userlistvue.twig'; + } + else + { + foreach($users as $username) + { + $newuser = $user->getSecureUser($username); + if($newuser) + { + $userdata[] = $newuser; + } + } + } + + return $this->render($response, $template, array( + 'settings' => $settings, + 'acl' => $this->c->acl, + 'navigation' => $navigation, + 'users' => $users, + 'userdata' => $userdata, + 'userroles' => $this->c->acl->getRoles(), + 'route' => $route->getName() + )); + } + + #returns userdata + public function getUsersByNames($request, $response, $args) + { + $params = $request->getParams(); + $user = new User(); + $userdata = []; + + if(isset($params['usernames'])) + { + foreach($params['usernames'] as $username) + { + $existinguser = $user->getSecureUser($username); + if($existinguser) + { + $userdata[] = $existinguser; + } + } + } + + return $response->withJson(['userdata' => $userdata]); + } + + # returns userdata + public function getUsersByEmail($request, $response, $args) + { + $params = $request->getParams(); + $user = new User(); + + $userdata = $user->findUsersByEmail($params['email']); + + return $response->withJson(['userdata' => $userdata ]); + } + + #returns userdata + public function getUsersByRole($request, $response, $args) + { + $params = $request->getParams(); + $user = new User(); + + $userdata = $user->findUsersByRole($params['role']); + + return $response->withJson(['userdata' => $userdata ]); + } + + 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) + { + $this->uri = $request->getUri()->withUserInfo(''); + $dir = $this->settings['basePath'] . 'cache'; + + $error = $this->writeCache->deleteCacheFiles($dir); + if($error) + { + return $response->withJson(['errors' => $error], 500); + } + + # create a new draft structure + $this->setFreshStructureDraft(); + + # create a new draft structure + $this->setFreshStructureLive(); + + # create a new draft structure + $this->setFreshNavigation(); + + # update the sitemap + $this->updateSitemap(); + + return $response->withJson(array('errors' => false)); + } + + private function getUserFields($role) + { + # 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->acl->hasRole($role)) + { + $role = 'member'; + } + + $fields = []; + $fields['username'] = ['label' => 'Username (read only)', 'type' => 'text', 'readonly' => true]; + $fields['firstname'] = ['label' => 'First Name', 'type' => 'text']; + $fields['lastname'] = ['label' => 'Last Name', 'type' => 'text']; + $fields['email'] = ['label' => 'E-Mail', 'type' => 'text', 'required' => true]; + $fields['userrole'] = ['label' => 'Role', 'type' => 'text', 'readonly' => true]; + $fields['password'] = ['label' => 'Actual Password', 'type' => 'password']; + $fields['newpassword'] = ['label' => 'New Password', 'type' => 'password']; + + # dispatch fields; + $fields = $this->c->dispatcher->dispatch('onUserfieldsLoaded', new OnUserfieldsLoaded($fields))->getData(); + + # only roles who can edit content need profile image and description + if($this->c->acl->isAllowed($role, 'mycontent', 'create')) + { + $newfield['image'] = ['label' => 'Profile-Image', 'type' => 'image']; + $newfield['description'] = ['label' => 'Author-Description (Markdown)', 'type' => 'textarea']; + + $fields = array_slice($fields, 0, 1, true) + $newfield + array_slice($fields, 1, NULL, true); + # array_splice($fields,1,0,$newfield); + } + + # Only admin can change userroles + if($this->c->acl->isAllowed($_SESSION['role'], 'userlist', 'write')) + { + $userroles = $this->c->acl->getRoles(); + $options = []; + + # we need associative array to make select-field with key/value work + foreach($userroles as $userrole) + { + $options[$userrole] = $userrole; + } + + $fields['userrole'] = ['label' => 'Role', 'type' => 'select', 'options' => $options]; + } + + $userform = []; + $userform['forms']['fields'] = $fields; + return $userform; + } + + private function getThemes() + { + $themeFolder = $this->c->get('settings')['rootPath'] . $this->c->get('settings')['themeFolder']; + $themeFolderC = scandir($themeFolder); + $themes = array(); + foreach ($themeFolderC as $key => $theme) + { + if (!in_array($theme, array(".",".."))) + { + if (is_dir($themeFolder . DIRECTORY_SEPARATOR . $theme)) + { + $themes[] = $theme; + } + } + } + return $themes; + } + + private function getCopyright() + { + return array( + "©", + "CC-BY", + "CC-BY-NC", + "CC-BY-NC-ND", + "CC-BY-NC-SA", + "CC-BY-ND", + "CC-BY-SA", + "None" + ); + } + + private function getLanguages() + { + return array( + 'en' => 'English', + 'ru' => 'Russian', + 'nl' => 'Dutch, Flemish', + 'de' => 'German', + 'it' => 'Italian', + 'fr' => 'French', + ); + } + + private function validateInput($objectType, $objectName, $userInput, $validate, $originalSettings = NULL) + { + if(!$originalSettings) + { + # fetch the original settings from the folder (plugin or theme) to get the field definitions + $originalSettings = \Typemill\Settings::getObjectSettings($objectType, $objectName); + } + + # images get special treatment + $imageFieldDefinitions = array(); + + if(isset($originalSettings['forms']['fields'])) + { + /* flaten the multi-dimensional array with fieldsets to a one-dimensional array */ + $originalFields = array(); + foreach($originalSettings['forms']['fields'] as $fieldName => $fieldValue) + { + if(isset($fieldValue['fields'])) + { + foreach($fieldValue['fields'] as $subFieldName => $subFieldValue) + { + $originalFields[$subFieldName] = $subFieldValue; + } + } + else + { + $originalFields[$fieldName] = $fieldValue; + } + } + + # if plugin is not active, then skip required + $skiprequired = false; + if($objectType == 'plugins' && !isset($userInput['active'])) + { + $skiprequired = true; + } + + /* take the user input data and iterate over all fields and values */ + foreach($userInput as $fieldName => $fieldValue) + { + /* get the corresponding field definition from original plugin settings */ + $fieldDefinition = isset($originalFields[$fieldName]) ? $originalFields[$fieldName] : false; + + if($fieldDefinition) + { + + # check if the field is a select field with dataset = userroles + if(isset($fieldDefinition['type']) && ($fieldDefinition['type'] == 'select' ) && isset($fieldDefinition['dataset']) && ($fieldDefinition['dataset'] == 'userroles' ) ) + { + $userroles = [null => null]; + foreach($this->c->acl->getRoles() as $userrole) + { + $userroles[$userrole] = $userrole; + } + $fieldDefinition['options'] = $userroles; + } + + /* validate user input for this field */ + $validate->objectField($fieldName, $fieldValue, $objectName, $fieldDefinition, $skiprequired); + + if($fieldDefinition['type'] == 'image') + { + # we want to return all images-fields for further processing + $imageFieldDefinitions[$fieldName] = $fieldDefinition; + } + } + if(!$fieldDefinition && $objectType != 'users' && $fieldName != 'active') + { + $_SESSION['errors'][$objectName][$fieldName] = array('This field is not defined!'); + } + } + } + + return $imageFieldDefinitions; + } + + protected function saveImages($imageFields, $userInput, $userSettings, $files) + { + # initiate image processor with standard image sizes + $processImages = new ProcessImage($userSettings['images']); + + if(!$processImages->checkFolders('images')) + { + $this->c->flash->addMessage('error', 'Please make sure that your media folder exists and is writable.'); + return false; + } + + foreach($imageFields as $fieldName => $imageField) + { + if(isset($userInput[$fieldName])) + { + # handle single input with single file upload + $image = $files[$fieldName]; + + if($image->getError() === UPLOAD_ERR_OK) + { + # not the most elegant, but createImage expects a base64-encoded string. + $imageContent = $image->getStream()->getContents(); + $imageData = base64_encode($imageContent); + $imageSrc = 'data: ' . $image->getClientMediaType() . ';base64,' . $imageData; + + if($processImages->createImage($imageSrc, $image->getClientFilename(), $userSettings['images'], $overwrite = NULL)) + { + # returns image path to media library + $userInput[$fieldName] = $processImages->publishImage(); + } + } + } + } + return $userInput; + } + +} \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerSystemApi.php b/system/typemill/Controllers/ControllerSystemApi.php new file mode 100644 index 0000000..45397fe --- /dev/null +++ b/system/typemill/Controllers/ControllerSystemApi.php @@ -0,0 +1,37 @@ +getBody()->write(json_encode([ + 'settings' => $this->settings + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(200); + } + + public function getSystemNavi(Request $request, Response $response) + { + # won't work because api has no session, instead you have to pass user + $response->getBody()->write(json_encode([ + 'systemnavi' => $this->getSystemNavigation('member'), + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(200); + } + + public function getMainNavi(Request $request, Response $response) + { + $response->getBody()->write(json_encode([ + 'mainnavi' => $this->getMainNavigation('member'), + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(200); + } +} \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerWeb.php b/system/typemill/Controllers/ControllerWeb.php index 0bd9827..5275030 100644 --- a/system/typemill/Controllers/ControllerWeb.php +++ b/system/typemill/Controllers/ControllerWeb.php @@ -10,6 +10,7 @@ class ControllerWeb extends Controller { public function __construct(Container $container) { + /* parent::__construct($container); echo '
add twig'; @@ -47,8 +48,26 @@ class ControllerWeb extends Controller return $twig; }); + + protected function setUrlCollection($uri) + { + $scheme = $uri->getScheme(); + $authority = $uri->getAuthority(); + $protocol = ($scheme ? $scheme . ':' : '') . ($authority ? '//' . $authority : ''); + + $this->currentPath = $uri->getPath(); + $this->fullBaseUrl = $protocol . $this->basePath; + $this->fullCurrentUrl = $protocol . $this->currentPath; + + $this->urlCollection = [ + 'basePath' => $this->basePath, + 'currentPath' => $this->currentPath, + 'fullBaseUrl' => $this->fullBaseUrl, + 'fullCurrentUrl' => $this->fullCurrentUrl + ]; + } $this->c->get('dispatcher')->dispatch(new OnTwigLoaded(false), 'onTwigLoaded'); - +*/ } } \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerWebFrontend.php b/system/typemill/Controllers/ControllerWebFrontend.php index b22a46f..fd0370a 100644 --- a/system/typemill/Controllers/ControllerWebFrontend.php +++ b/system/typemill/Controllers/ControllerWebFrontend.php @@ -21,11 +21,10 @@ use Typemill\Events\OnHtmlLoaded; use Typemill\Events\OnRestrictionsLoaded; */ -class ControllerWebFrontend extends ControllerWeb +class ControllerWebFrontend extends Controller { public function index(Request $request, Response $response) { - die('hallo'); return $this->c->get('view')->render($response, 'home.twig', [ 'title' => 'Typemill Version 2', 'description' => 'Typemill Version 2 wird noch besser als Version 1.' diff --git a/system/typemill/Controllers/ControllerWebLogin.php b/system/typemill/Controllers/ControllerWebLogin.php index 2bc802d..a20d373 100644 --- a/system/typemill/Controllers/ControllerWebLogin.php +++ b/system/typemill/Controllers/ControllerWebLogin.php @@ -5,104 +5,89 @@ namespace Typemill\Controllers; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; use Slim\Routing\RouteContext; - -use Slim\Views\Twig; use Typemill\Models\Validation; use Typemill\Models\User; -use Typemill\Models\WriteYaml; -use Typemill\Extensions\ParsedownExtension; -class ControllerWebLogin extends ControllerWeb +class ControllerWebLogin extends Controller { # redirect if visit /setup route public function redirect(Request $request, Response $response) { if(isset($_SESSION['login'])) { - return $response->withRedirect($this->c->router->pathFor('content.raw')); + return $response->withHeader('Location', $this->routeParser->urlFor('content.raw'))->withStatus(302); } else { - return $response->withRedirect($this->c->router->pathFor('auth.show')); + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); } } - /** - * show login form - * - * @param obj $request the slim request object. - * @param obj $response the slim response object. - * @param array $args with arguments past to the slim router - * @return obj $response and string route. - */ - - public function show(Request $request, Response $response, $args) + public function show(Request $request, Response $response) { - return $this->c->get('view')->render($response, 'login.twig', [ + return $this->c->get('view')->render($response, 'auth/login.twig', [ #'captcha' => $this->checkIfAddCaptcha(), - #'url' => $this->urlCollection, ]); - -# $settings = $this->c->get('settings'); - -# return $this->render($response, '/auth/login.twig', ['settings' => $settings]); } - /** - * signin an existing user - * - * @param obj $request the slim request object with form data in the post params. - * @param obj $response the slim response object. - * @return obj $response with redirect to route. - */ - public function login(Request $request, Response $response) { if( ( null !== $request->getattribute('csrf_result') ) OR ( $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('auth.show')); + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show')); } - /* authentication */ - $params = $request->getParams(); + $input = $request->getParsedBody(); $validation = new Validation(); $settings = $this->c->get('settings'); - if($validation->signin($params)) + if($validation->signin($input)) { $user = new User(); - $userdata = $user->getUser($params['username']); - if($userdata && password_verify($params['password'], $userdata['password'])) + if(!$user->setUserWithPassword($input['username'])) { + # return error + } + + $userdata = $user->getUserData(); + + if($userdata && password_verify($input['password'], $userdata['password'])) + { # check if user has confirmed the account if(isset($userdata['optintoken']) && $userdata['optintoken']) { - $this->c->flash->addMessage('error', 'Your registration is not confirmed yet. Please check your e-mails and use the confirmation link.'); - return $response->withRedirect($this->c->router->pathFor('auth.show')); + $this->c->get('flash')->addMessage('error', 'Your registration is not confirmed yet. Please check your e-mails and use the confirmation link.'); + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); } - $user->login($userdata['username']); + $user->login(); +return $response->withHeader('Location', $this->routeParser->urlFor('settings.show'))->withStatus(302); + +/* # if user is allowed to view content-area - if($this->c->acl->hasRole($userdata['userrole']) && $this->c->acl->isAllowed($userdata['userrole'], 'content', 'view')) + $acl = $this->c->get('acl'); + if($acl->hasRole($userdata['userrole']) && $acl->isAllowed($userdata['userrole'], 'content', 'view')) { - $settings = $this->c->get('settings'); - $editor = (isset($settings['editor']) && $settings['editor'] == 'visual') ? 'visual' : 'raw'; + $editor = (isset($this->settings['editor']) && $this->settings['editor'] == 'visual') ? 'visual' : 'raw'; - return $response->withRedirect($this->c->router->pathFor('content.' . $editor)); + return $response->withHeader('Location', $this->routeParser->urlFor('content.' . $editor))->withStatus(302); } - return $response->withRedirect($this->c->router->pathFor('user.account')); + + return $response->withHeader('Location', $this->routeParser->urlFor('user.account'))->withStatus(302); +*/ } } if(isset($this->settings['securitylog']) && $this->settings['securitylog']) { - \Typemill\Models\Helpers::addLogEntry('wrong login'); + \Typemill\Static\Helpers::addLogEntry('wrong login'); } - $this->c->flash->addMessage('error', 'Ups, wrong password or username, please try again.'); - return $response->withRedirect($this->c->router->pathFor('auth.show')); + $this->c->get('flash')->addMessage('error', 'Ups, wrong password or username, please try again.'); + + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); } } \ No newline at end of file diff --git a/system/typemill/Extensions/TwigCsrfExtension.php b/system/typemill/Extensions/TwigCsrfExtension.php index 8160101..ef80782 100644 --- a/system/typemill/Extensions/TwigCsrfExtension.php +++ b/system/typemill/Extensions/TwigCsrfExtension.php @@ -2,9 +2,10 @@ namespace Typemill\Extensions; +use Twig\Extension\AbstractExtension; use Slim\Csrf\Guard; -class TwigCsrfExtension extends \Twig\Extension\AbstractExtension +class TwigCsrfExtension extends AbstractExtension { protected $csrf; @@ -22,8 +23,8 @@ class TwigCsrfExtension extends \Twig\Extension\AbstractExtension public function csrf() { - $csrf = '

TokenNameValue: '. $this->csrf->getTokenName() .'

- '; + $csrf = '

TokenNameValue: '. $this->csrf->getTokenName() .'

+ '; return $csrf; } diff --git a/system/typemill/Extensions/TwigLanguageExtension.php b/system/typemill/Extensions/TwigLanguageExtension.php new file mode 100644 index 0000000..d4338b9 --- /dev/null +++ b/system/typemill/Extensions/TwigLanguageExtension.php @@ -0,0 +1,63 @@ +labels = $labels; + } + + public function getFilters() + { + return [ + new TwigFilter('translate', [$this, 'translate'] ), + ]; + } + + public function getFunctions() + { + return [ + new TwigFunction('translate', array($this, 'translate' )) + ]; + } + + public function translate( $label, $labels_from_plugin = NULL ) + { + # replaces spaces, dots, comma and dash with underscores + $string = str_replace(" ", "_", $label); + $string = str_replace(".", "_", $string); + $string = str_replace(",", "_", $string); + $string = str_replace("-", "_", $string); + + # transforms to uppercase + $string = strtoupper( $string ); + + # translates the string + if(isset($labels_from_plugin)) + { + $translated_label = isset($labels_from_plugin[$string]) ? $labels_from_plugin[$string] : null; + } + else + { + $translated_label = isset($this->labels[$string]) ? $this->labels[$string] : null; + } + + # if the string is not present, set the original string + if( empty($translated_label) ) + { + $translated_label = $label; + } + + # returns the string in the set language + return $translated_label; + } +} diff --git a/system/typemill/Extensions/TwigUrlExtension.php b/system/typemill/Extensions/TwigUrlExtension.php new file mode 100644 index 0000000..0242db0 --- /dev/null +++ b/system/typemill/Extensions/TwigUrlExtension.php @@ -0,0 +1,47 @@ +uri = $uri; + $this->basepath = $basepath; + $this->scheme = $uri->getScheme(); + $this->authority = $uri->getAuthority(); + $this->protocol = ($this->scheme ? $this->scheme . ':' : '') . ($this->authority ? '//' . $this->authority : ''); + } + + public function getFunctions() + { + return [ + new \Twig\TwigFunction('base_url', array($this, 'baseUrl' )), + new \Twig\TwigFunction('current_url', array($this, 'currentUrl' )), + new \Twig\TwigFunction('current_path', array($this, 'currentPath' )) + ]; + } + + public function baseUrl() + { + return $this->protocol . $this->basepath; + } + + public function currentUrl() + { + return $this->protocol . $this->uri->getPath(); + } + + public function currentPath() + { + return $this->uri->getPath(); + } +} \ No newline at end of file diff --git a/system/typemill/Extensions/TwigUserExtension.php b/system/typemill/Extensions/TwigUserExtension.php new file mode 100644 index 0000000..2038bba --- /dev/null +++ b/system/typemill/Extensions/TwigUserExtension.php @@ -0,0 +1,57 @@ +sessionSegments = $session_segments; - - $this->routepath = $routepath; - } - - public function process(Request $request, RequestHandler $handler) :response - { - foreach($this->sessionSegments as $segment) - { - if(substr( $this->routepath, 0, strlen($segment) ) === ltrim($segment, '/')) - { - echo '
Create Session'; - - // configure session - ini_set('session.cookie_httponly', 1 ); - ini_set('session.use_strict_mode', 1); - ini_set('session.cookie_samesite', 'lax'); - /* - if($uri->getScheme() == 'https') - { - ini_set('session.cookie_secure', 1); - session_name('__Secure-typemill-session'); - } - else - { - session_name('typemill-session'); - } - */ - // start session - session_start(); - - $request = $request->withAttribute('session', $_SESSION); - } - } - - return $handler->handle($request); - } -} \ No newline at end of file diff --git a/system/typemill/Middleware/CsrfProtection.php b/system/typemill/Middleware/CsrfProtection.php deleted file mode 100644 index 4a45875..0000000 --- a/system/typemill/Middleware/CsrfProtection.php +++ /dev/null @@ -1,41 +0,0 @@ -container = $container; - $this->responseFactory = $responseFactory; - } - - public function process(Request $request, RequestHandler $handler) :response - { - if(is_array($request->getAttribute('session'))) - { - echo '
csrf protection'; - - $responseFactory = $this->responseFactory; - - # Register Middleware On Container - $this->container->set('csrf', function () use ($responseFactory) - { - return new Guard($responseFactory); - }); - } - - return $handler->handle($request); - } -} \ No newline at end of file diff --git a/system/typemill/Middleware/CsrfProtectionToMiddleware.php b/system/typemill/Middleware/CsrfProtectionToMiddleware.php deleted file mode 100644 index bfe7259..0000000 --- a/system/typemill/Middleware/CsrfProtectionToMiddleware.php +++ /dev/null @@ -1,30 +0,0 @@ -container = $container; - } - - public function __invoke(Request $request, RequestHandler $handler) - { - if(is_array($request->getAttribute('session'))) - { - echo '
csrf protection to middleware'; - - return $this->container->get('csrf'); - } - } -} \ No newline at end of file diff --git a/system/typemill/Middleware/FlashMessages.php b/system/typemill/Middleware/FlashMessages.php index 8775f69..d3f0439 100644 --- a/system/typemill/Middleware/FlashMessages.php +++ b/system/typemill/Middleware/FlashMessages.php @@ -2,31 +2,24 @@ namespace Typemill\Middleware; -use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\RequestHandlerInterface as RequestHandler; -use Psr\Http\Server\MiddlewareInterface; -use Slim\Flash\Messages; +use Slim\Views\Twig; -class FlashMessages implements MiddlewareInterface -{ - - protected $container; - - public function __construct($container) +class FlashMessages +{ + public function __construct(Twig $view) { - $this->container = $container; + $this->view = $view; } - - public function process(Request $request, RequestHandler $handler) :response + + public function __invoke(Request $request, RequestHandler $handler) { - if(is_array($request->getAttribute('session'))) + if(isset($_SESSION['slimFlash']) && is_array($_SESSION['slimFlash'])) { - echo '
flash messages'; - - $this->container->set('flash', function(){ - return new Messages(); - }); + $this->view->getEnvironment()->addGlobal('flash', $_SESSION['slimFlash']); + + unset($_SESSION['slimFlash']); } return $handler->handle($request); diff --git a/system/typemill/Middleware/JsonBodyParser.php b/system/typemill/Middleware/JsonBodyParser.php index ff7f18e..89a720f 100644 --- a/system/typemill/Middleware/JsonBodyParser.php +++ b/system/typemill/Middleware/JsonBodyParser.php @@ -11,7 +11,7 @@ class JsonBodyParser implements MiddlewareInterface { public function process(Request $request, RequestHandler $handler) :response { - echo '
JSON Body parser'; + #echo '
JSON Body parser'; $contentType = $request->getHeaderLine('Content-Type'); diff --git a/system/typemill/Middleware/RedirectIfAuthenticated.php b/system/typemill/Middleware/RedirectIfAuthenticated.php index dcf7f49..aeafa78 100644 --- a/system/typemill/Middleware/RedirectIfAuthenticated.php +++ b/system/typemill/Middleware/RedirectIfAuthenticated.php @@ -2,12 +2,11 @@ namespace Typemill\Middleware; -use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Server\MiddlewareInterface; +use Slim\Routing\RouteParser; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\RequestHandlerInterface as RequestHandler; -use Psr\Http\Server\MiddlewareInterface; -# use Slim\Routing\RouteContext; -use Slim\Routing\RouteParser; +use Slim\Psr7\Response; class RedirectIfAuthenticated implements MiddlewareInterface { @@ -17,18 +16,25 @@ class RedirectIfAuthenticated implements MiddlewareInterface $this->settings = $settings; } - public function process(Request $request, RequestHandler $handler) :response - { - $response = $handler->handle($request); - + public function process(Request $request, RequestHandler $handler) :Response + { $editor = (isset($this->settings['editor']) && $this->settings['editor'] == 'visual') ? 'visual' : 'raw'; - if(isset($_SESSION['login'])) + $authenticated = ( + (isset($_SESSION['username'])) && + (isset($_SESSION['login'])) + ) + ? true : false; + + if($authenticated) { - return $response->withHeader('Location', $this->router->pathFor('content.' . $editor))->withStatus(302); -# $response = $response->withRedirect($this->router->pathFor('content.' . $editor)); + $response = new Response(); + + return $response->withHeader('Location', $this->router->urlFor('content.' . $editor))->withStatus(302); } - + + $response = $handler->handle($request); + return $response; } } \ No newline at end of file diff --git a/system/typemill/Middleware/RedirectIfUnauthenticated.php b/system/typemill/Middleware/RedirectIfUnauthenticated.php new file mode 100644 index 0000000..4c08441 --- /dev/null +++ b/system/typemill/Middleware/RedirectIfUnauthenticated.php @@ -0,0 +1,39 @@ +router = $router; + } + + public function process(Request $request, RequestHandler $handler) :response + { + $authenticated = ( + (isset($_SESSION['username'])) && + (isset($_SESSION['login'])) + ) + ? true : false; + + if(!$authenticated) + { + # this executes only middleware code and not code from route + $response = new Response(); + + return $response->withHeader('Location', $this->router->urlFor('auth.show'))->withStatus(302); + } + + # this executes code from routes first and then executes middleware + $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 new file mode 100644 index 0000000..82628d6 --- /dev/null +++ b/system/typemill/Middleware/RestrictApiAccess.php @@ -0,0 +1,66 @@ +getBasePath(); + + if ($request->hasHeader('X-Session-Auth')) { + + session_start(); + + $authenticated = ( + (isset($_SESSION['username'])) && + (isset($_SESSION['userrole'])) && + (isset($_SESSION['login'])) + ) + ? true : false; + + if($authenticated) + { + $response = $handler->handle($request); + + return $response; + } + } + +# elseif ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') { + # advantage: all xhr-calls to the api will be session based + # no direct calls from javascript possible + # only from server +# } + + + $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 + # 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/Models/Storage.php b/system/typemill/Models/Storage.php new file mode 100644 index 0000000..66090bf --- /dev/null +++ b/system/typemill/Models/Storage.php @@ -0,0 +1,336 @@ +basepath = getcwd() . DIRECTORY_SEPARATOR; + } + + public function checkFolder($folder) + { + $folderpath = $this->basepath . $folder; + + if(!is_dir($folderpath) OR !is_writable($folderpath)) + { + $this->error = "The folder $folder does not exist or is not writable."; + + return false; + } + + return true; + } + + public function createFolder($folder) + { + $folderpath = $this->basepath . $folder; + + if(is_dir($folderpath)) + { + return true; + } + + if(!mkdir($folderpath, 0755, true)) + { + $this->error = "Could not create folder $folder."; + + return false; + } + + return true; + } + + public function checkFile($folder, $filename) + { + if(!file_exists($this->basepath . $folder . DIRECTORY_SEPARATOR . $filename)) + { + $this->error = "The file $filename in folder $folder does not exist."; + + return false; + } + + return true; + } + + public function writeFile($folder, $filename, $data) + { + if(!$this->checkFolder($folder)) + { + if(!$this->createFolder($folder)) + { + return false; + } + } + + $filepath = $this->basepath . $folder . DIRECTORY_SEPARATOR . $filename; + + $openfile = @fopen($filepath, "w"); + if(!$openfile) + { + $this->error = "Could not open and read the file $filename in folder $folder."; + + return true; + } + + fwrite($openfile, $data); + fclose($openfile); + + return true; + } + + public function getFile($folder, $filename) + { + if($this->checkFile($folder, $filename)) + { + # ??? should be with basepath??? + $fileContent = file_get_contents($folder . DIRECTORY_SEPARATOR . $filename); + return $fileContent; + } + + return false; + } + + public function renameFile($folder, $oldname, $newname) + { + + $oldFilePath = $this->basepath . $folder . DIRECTORY_SEPARATOR . $oldname; + $newFilePath = $this->basepath . $folder . DIRECTORY_SEPARATOR . $newname; + + if(!file_exists($oldFilePath)) + { + return false; + } + + if(!rename($oldFilePath, $newFilePath)) + { + return false; + } + + return true; + } + + public function deleteFile($filepath) + { + if($this->checkFileWithPath($filepath)) + { + unlink($this->basePath . $filepath); + return true; + } + return false; + } + + public function getError() + { + return $this->error; + } + + + +/* + public function checkPath($folder) + { + $folderPath = $this->basepath . $folder; + + if(!is_dir($folderPath)) + { + if(@mkdir($folderPath, 0774, true)) + { + return true; + } + else + { + throw new \Exception("The folder '{$folderPath}' is missing and we could not create it. Please create the folder manually on your server."); +# return false; + } + } + + if(@is_writable($folderPath)) + { + return true; + } + else + { + throw new \Exception("Please make the folder '{$folderPath}' writable."); +# return false; + } + return true; + } + +/* + + public function checkFile($folder, $file) + { + if(!file_exists($this->basePath . $folder . DIRECTORY_SEPARATOR . $file)) + { + return false; + } + return true; + } + + public function checkFileWithPath($filepath) + { + if(!file_exists($this->basePath . $filepath)) + { + return false; + } + return true; + } + + public function writeFile($folder, $file, $data) + { + if($this->checkPath($folder)) + { + $filePath = $this->basePath . $folder . DIRECTORY_SEPARATOR . $file; + + $openFile = @fopen($filePath, "w"); + + if(!$openFile) + { + return false; + } + + fwrite($openFile, $data); + fclose($openFile); + + return true; + } + return false; + } + + public function getFile($folderName, $fileName) + { + if($this->checkFile($folderName, $fileName)) + { + $fileContent = file_get_contents($folderName . DIRECTORY_SEPARATOR . $fileName); + return $fileContent; + } + return false; + } + + public function getFileWithPath($filepath) + { + if($this->checkFileWithPath($filepath)) + { + $fileContent = file_get_contents($filepath); + return $fileContent; + } + return false; + } + + public function deleteFileWithPath($filepath) + { + if($this->checkFileWithPath($filepath)) + { + unlink($this->basePath . $filepath); + return true; + } + return false; + } + + public function renameFile($folder, $oldname, $newname) + { + + $oldFilePath = $this->basePath . $folder . DIRECTORY_SEPARATOR . $oldname; + $newFilePath = $this->basePath . $folder . DIRECTORY_SEPARATOR . $newname; + + if(!file_exists($oldFilePath)) + { + return false; + } + + if(@rename($oldFilePath, $newFilePath)) + { + return true; + } + + return false; + } + + public function renamePost($oldPathWithoutType, $newPathWithoutType) + { + $filetypes = array('md', 'txt', 'yaml'); + + $oldPath = $this->basePath . 'content' . $oldPathWithoutType; + $newPath = $this->basePath . 'content' . $newPathWithoutType; + + $result = true; + + foreach($filetypes as $filetype) + { + $oldFilePath = $oldPath . '.' . $filetype; + $newFilePath = $newPath . '.' . $filetype; + + #check if file with filetype exists and rename + if($oldFilePath != $newFilePath && file_exists($oldFilePath)) + { + if(@rename($oldFilePath, $newFilePath)) + { + $result = $result; + } + else + { + $result = false; + } + } + } + + return $result; + } + + public function moveElement($item, $folderPath, $index, $date = null) + { + $filetypes = array('md', 'txt', 'yaml'); + + # set new order as string + $newOrder = ($index < 10) ? '0' . $index : $index; + + # create new path with foldername or filename but without file-type + # $newPath = $this->basePath . 'content' . $folderPath . DIRECTORY_SEPARATOR . $newOrder . '-' . str_replace(" ", "-", $item->name); + + $newPath = $this->basePath . 'content' . $folderPath . DIRECTORY_SEPARATOR . $newOrder . '-' . $item->slug; + + if($item->elementType == 'folder') + { + $oldPath = $this->basePath . 'content' . $item->path; + if(@rename($oldPath, $newPath)) + { + return true; + } + return false; + } + + # create old path but without filetype + $oldPath = substr($item->path, 0, strpos($item->path, ".")); + $oldPath = $this->basePath . 'content' . $oldPath; + + $result = true; + + foreach($filetypes as $filetype) + { + $oldFilePath = $oldPath . '.' . $filetype; + $newFilePath = $newPath . '.' . $filetype; + + #check if file with filetype exists and rename + if($oldFilePath != $newFilePath && file_exists($oldFilePath)) + { + if(@rename($oldFilePath, $newFilePath)) + { + $result = $result; + } + else + { + $result = false; + } + } + } + + return $result; + } + */ +} \ No newline at end of file diff --git a/system/typemill/Models/StorageWrapper.php b/system/typemill/Models/StorageWrapper.php new file mode 100644 index 0000000..306280d --- /dev/null +++ b/system/typemill/Models/StorageWrapper.php @@ -0,0 +1,28 @@ +object = new $classname(); + } + + function __call($method, $args) + { + if(!method_exists($this->object, $method)) + { + throw new \RuntimeException(sprintf('Callable method %s does not exist', $method)); + } + + # Invoke original method on our proxied object + return call_user_func_array([$this->object, $method], $args); + } +} \ No newline at end of file diff --git a/system/typemill/Models/User.php b/system/typemill/Models/User.php new file mode 100644 index 0000000..5df1b3b --- /dev/null +++ b/system/typemill/Models/User.php @@ -0,0 +1,485 @@ +userDir = getcwd() . '/system/settings/users'; + $this->yaml = new Yaml('\Typemill\Models\Storage'); + } + + public function setUser(string $username) + { + if(!$this->user) + { + $this->user = $this->yaml->getYaml('settings/users', $username . '.yaml'); + + 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']); + } + + return $this; + } + + public function setUserWithPassword(string $username) + { + if(!$this->user) + { + $this->user = $this->yaml->getYaml('settings/users', $username . '.yaml'); + + if(!$this->user) + { + $this->error = 'User not found.'; + + return false; + } + } + + return $this; + } + + public function getError() + { + return $this->error; + } + + public function getUserData() + { + return $this->user; + } + + public function getAllUsers() + { + # check if users directory exists + if(!is_dir($this->userDir)) + { + $this->error = 'Directory $this->userDir does not exist.'; + + return false; + } + + # get all user files + $userfiles = array_diff(scandir($this->userDir), array('..', '.', '.logins', 'tmuserindex-mail.txt', 'tmuserindex-role.txt')); + + $usernames = []; + + if(!empty($userfiles)) + { + foreach($userfiles as $key => $userfile) + { + $usernames[] = str_replace('.yaml', '', $userfile); + } + + usort($usernames, 'strnatcasecmp'); + } + + return $usernames; + } + + public function createUser(array $params) + { + $params['password'] = $this->generatePassword($params['password']); + + if($this->yaml->updateYaml('settings/users', $params['username'] . '.yaml', $params)) + { + $this->deleteUserIndex(); + + return $params['username']; + } + + $this->error = $this->yaml->getError(); + + return false; + } + + public function unsetFromUser(array $keys) + { + if(empty($keys) OR !$this->user) + { + $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 updateUser() + { + # 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; + } + 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(); + } + } + + public function generatePassword($password) + { + 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) + { + $usernames = []; + + # Make sure that we scan only the first 11 files even if there are some thousand users. + if ($dh = opendir($this->userDir)) + { + $count = 1; + $exclude = array('..', '.', '.logins', 'tmuserindex-mail.txt', 'tmuserindex-role.txt'); + + while ( ($userfile = readdir($dh)) !== false && $count <= 11 ) + { + if(in_array($userfile, $exclude)){ continue; } + + $usernames[] = str_replace('.yaml', '', $userfile); + $count++; + } + + closedir($dh); + } + + $countusers = count($usernames); + + if($countusers == 0) + { + return false; + } + + # use a simple dirty search if there are less than 10 users (only in use for new user registrations) + if($countusers <= 10) + { + foreach($usernames as $username) + { + $userdata = $this->getSecureUser($username); + + if($userdata['email'] == $email) + { + return $userdata; + } + } + return false; + } + + # if there are more than 10 users, search with an index + $usermails = $this->getUserMailIndex(); + + # search with starting asterix, ending asterix or without asterix + if($email[0] == '*') + { + $userdata = []; + $search = substr($email, 1); + $length = strlen($search); + + foreach($usermails as $usermail => $username) + { + if(substr($usermail, -$length) == $search) + { + $userdata[] = $username; + } + } + + $userdata = empty($userdata) ? false : $userdata; + + return $userdata; + } + elseif(substr($email, -1) == '*') + { + $userdata = []; + $search = substr($email, 0, -1); + $length = strlen($search); + + foreach($usermails as $usermail => $username) + { + if(substr($usermail, 0, $length) == $search) + { + $userdata[] = $username; + } + } + + $userdata = empty($userdata) ? false : $userdata; + + return $userdata; + } + elseif(isset($usermails[$email])) + { + $userdata[] = $usermails[$email]; + return $userdata; + } + + return false; + } + + public function getUserMailIndex() + { + if(file_exists($this->userDir . DIRECTORY_SEPARATOR . 'tmuserindex-mail.txt')) + { + # unserialize and return the file + $usermailindex = unserialize(file_get_contents($this->userDir . DIRECTORY_SEPARATOR . 'tmuserindex-mail.txt')); + + if($usermailindex) + { + return $usermailindex; + } + } + + $usernames = $this->getUsers(); + $usermailindex = []; + + foreach($usernames as $username) + { + $userdata = $this->getSecureUser($username); + + $usermailindex[$userdata['email']] = $username; + } + + file_put_contents($this->userDir . DIRECTORY_SEPARATOR . 'tmuserindex-mail.txt', serialize($usermailindex)); + + return $usermailindex; + } + + # accepts email with or without asterix and returns usernames + public function findUsersByRole($role) + { + +/* + # get all user files + $usernames = $this->getUsers(); + + $countusers = count($usernames); + + if($countusers == 0) + { + return false; + } + + # use a simple dirty search if there are less than 10 users (not in use right now) + if($countusers <= 4) + { + $userdata = []; + foreach($usernames as $key => $username) + { + $userdetails = $this->getSecureUser($username); + + if($userdetails['userrole'] == $role) + { + $userdata[] = $userdetails; + } + } + if(empty($userdata)) + { + return false; + } + + return $userdata; + } +*/ + $userroles = $this->getUserRoleIndex(); + + if(isset($userroles[$role])) + { + return $userroles[$role]; + } + + return false; + } + + public function getUserRoleIndex() + { + if(file_exists($this->userDir . DIRECTORY_SEPARATOR . 'tmuserindex-role.txt')) + { + # unserialize and return the file + $userroleindex = unserialize(file_get_contents($this->userDir . DIRECTORY_SEPARATOR . 'tmuserindex-role.txt')); + if($userroleindex) + { + return $userroleindex; + } + } + + $usernames = $this->getUsers(); + $userroleindex = []; + + foreach($usernames as $username) + { + $userdata = $this->getSecureUser($username); + + $userroleindex[$userdata['userrole']][] = $username; + } + + file_put_contents($this->userDir . DIRECTORY_SEPARATOR . 'tmuserindex-role.txt', serialize($userroleindex)); + + return $userroleindex; + } + + protected function deleteUserIndex() + { + $userDir = __DIR__ . '/../../settings/users'; + + if(file_exists($userDir . DIRECTORY_SEPARATOR . 'tmuserindex-mail.txt')) + { + # read and return the file + 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 new file mode 100644 index 0000000..09f5ccc --- /dev/null +++ b/system/typemill/Models/Validation.php @@ -0,0 +1,609 @@ +findUsersByEmail($email)){ return false; } + return true; + }, 'taken'); + + # checks if email is available if userdata is updated + Validator::addRule('emailChanged', function($field, $value, array $params, array $fields) use ($user) + { + $userdata = $user->getSecureUser($fields['username']); + if($userdata['email'] == $value){ return true; } # user has not updated his email + + $email = trim($value); + if($user->findUsersByEmail($email)){ return false; } + return true; + }, 'taken'); + + # checks if username is free when create new user + Validator::addRule('userAvailable', function($field, $value, array $params, array $fields) use ($user) + { + $activeUser = $user->getUser($value); + $inactiveUser = $user->getUser("_" . $value); + if($activeUser OR $inactiveUser){ return false; } + return true; + }, 'taken'); + + # checks if user exists when userdata is updated + Validator::addRule('userExists', function($field, $value, array $params, array $fields) use ($user) + { + $userdata = $user->getUser($value); + if($userdata){ return true; } + return false; + }, 'does not exist'); + + Validator::addRule('iplist', function($field, $value, array $params, array $fields) use ($user) + { + $iplist = explode(",", $value); + foreach($iplist as $ip) + { + if( filter_var( trim($ip), \FILTER_VALIDATE_IP) === false) + { + return false; + } + } + return true; + }, 'contains one or more invalid ip-adress.'); + + Validator::addRule('customfields', function($field, $customfields, array $params, array $fields) use ($user) + { + if(empty($customfields)) + { + return true; + } + foreach($customfields as $key => $value) + { + if(!isset($key) OR empty($key) OR (preg_match('/^([a-z0-9])+$/i', $key) == false) ) + { + return false; + } + + if (!isset($value) OR empty($value) OR ( $value != strip_tags($value) ) ) + { + return false; + } + } + return true; + }, 'some fields are empty or contain invalid values.'); + + Validator::addRule('checkPassword', function($field, $value, array $params, array $fields) use ($user) + { + $userdata = $user->getUser($fields['username']); + if($userdata && password_verify($value, $userdata['password'])){ return true; } + return false; + }, 'wrong password'); + + Validator::addRule('navigation', function($field, $value, array $params, array $fields) + { + $format = '/[@#^*()=\[\]{};:"\\|,.<>\/]/'; + if ( preg_match($format, $value)) + { + return false; + } + return true; + }, 'contains special characters'); + + Validator::addRule('noSpecialChars', function($field, $value, array $params, array $fields) + { + $format = '/[!@#$%^&*()_+=\[\]{};\':"\\|,.<>\/?]/'; + if ( preg_match($format, $value)) + { + return false; + } + return true; + }, 'contains special characters'); + + Validator::addRule('noHTML', function($field, $value, array $params, array $fields) + { + if ( $value == strip_tags($value) ) + { + return true; + } + return false; + }, 'contains html'); + + Validator::addRule('markdownSecure', function($field, $value, array $params, array $fields) + { + /* strip out code blocks and blockquotes */ + $value = preg_replace('/`{4,}[\s\S]+?`{4,}/', '', $value); + $value = preg_replace('/`{3,}[\s\S]+?`{3,}/', '', $value); + $value = preg_replace('/`{2,}[\s\S]+?`{2,}/', '', $value); + $value = preg_replace('/`{1,}[\s\S]+?`{1,}/', '', $value); + $value = preg_replace('/>[\s\S]+?[\n\r]/', '', $value); + + if ( $value == strip_tags($value) ) + { + return true; + } + return false; + }, 'not secure. For code please use markdown `inline-code` or ````fenced code blocks````.'); + } + + # return valitron standard object + public function returnValidator(array $params) + { + return new Validator($params); + } + + + /** + * validation for signup form + * + * @param array $params with form data. + * @return obj $v the validation object passed to a result method. + */ + + public function signin(array $params) + { + $v = new Validator($params); + $v->rule('required', ['username', 'password'])->message("Required"); + $v->rule('alphaNum', 'username')->message("Invalid characters"); + $v->rule('lengthBetween', 'password', 5, 20)->message("Length between 5 - 20"); + $v->rule('lengthBetween', 'username', 3, 20)->message("Length between 3 - 20"); + + if($v->validate()) + { + return true; + } + else + { + return false; + } + } + + /** + * validation for signup form + * + * @param array $params with form data. + * @return obj $v the validation object passed to a result method. + */ + + public function newUser(array $params, $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', 'username', 3, 20)->message("Length between 3 - 20"); + $v->rule('userAvailable', 'username')->message("User already exists"); + $v->rule('noHTML', 'firstname')->message(" contains HTML"); + $v->rule('lengthBetween', 'firstname', 2, 40); + $v->rule('noHTML', 'lastname')->message(" contains HTML"); + $v->rule('lengthBetween', 'lastname', 2, 40); + $v->rule('email', 'email')->message("e-mail is invalid"); + $v->rule('emailAvailable', 'email')->message("Email already taken"); + $v->rule('in', 'userrole', $userroles); + + return $this->validationResult($v); + } + + public function existingUser(array $params, $userroles) + { + $v = new Validator($params); + $v->rule('required', ['username', 'email', 'userrole'])->message("required"); + $v->rule('alphaNum', 'username')->message("invalid"); + $v->rule('lengthBetween', 'username', 3, 20)->message("Length between 3 - 20"); + $v->rule('userExists', 'username')->message("user does not exist"); + $v->rule('noHTML', 'firstname')->message(" contains HTML"); + $v->rule('lengthBetween', 'firstname', 2, 40); + $v->rule('noHTML', 'lastname')->message(" contains HTML"); + $v->rule('lengthBetween', 'lastname', 2, 40); + $v->rule('email', 'email')->message("e-mail is invalid"); + $v->rule('emailChanged', 'email')->message("Email already taken"); + $v->rule('in', 'userrole', $userroles); + + return $this->validationResult($v); + } + + public function username($username) + { + $v = new Validator($username); + $v->rule('alphaNum', 'username')->message("Only alpha-numeric characters allowed"); + $v->rule('lengthBetween', 'username', 3, 20)->message("Length between 3 - 20"); + + return $this->validationResult($v); + } + + /** + * validation for changing the password + * + * @param array $params with form data. + * @return obj $v the validation object passed to a result method. + */ + + public function newPassword(array $params) + { + $v = new Validator($params); + $v->rule('required', ['password', 'newpassword']); + $v->rule('lengthBetween', 'newpassword', 5, 20); + $v->rule('checkPassword', 'password')->message("Password is wrong"); + + return $this->validationResult($v); + } + + /** + * validation for password recovery + * + * @param array $params with form data. + * @return obj $v the validation object passed to a result method. + */ + + public function recoverPassword(array $params) + { + $v = new Validator($params); + $v->rule('required', ['password', 'passwordrepeat']); + $v->rule('lengthBetween', 'password', 5, 20); + $v->rule('equals', 'passwordrepeat', 'password'); + + return $this->validationResult($v); + } + + /** + * validation for system settings + * + * @param array $params with form data. + * @return obj $v the validation object passed to a result method. + */ + + public function settings(array $params, array $copyright, array $formats, $name = false) + { + $v = new Validator($params); + + $v->rule('required', ['title', 'author', 'copyright', 'year', 'editor']); + $v->rule('lengthBetween', 'title', 2, 50); + $v->rule('lengthBetween', 'author', 2, 50); + $v->rule('noHTML', 'title'); + # $v->rule('regex', 'title', '/^[\pL0-9_ \-]*$/u'); + $v->rule('regex', 'author', '/^[\pL_ \-]*$/u'); + $v->rule('integer', 'year'); + $v->rule('length', 'year', 4); + $v->rule('length', 'langattr', 2); + $v->rule('in', 'editor', ['raw', 'visual']); + $v->rule('values_allowed', 'formats', $formats); + $v->rule('in', 'copyright', $copyright); + $v->rule('noHTML', 'restrictionnotice'); + $v->rule('lengthBetween', 'restrictionnotice', 2, 1000 ); + $v->rule('email', 'recoverfrom'); + $v->rule('noHTML', 'recoversubject'); + $v->rule('lengthBetween', 'recoversubject', 2, 80 ); + $v->rule('noHTML', 'recovermessage'); + $v->rule('lengthBetween', 'recovermessage', 2, 1000 ); + $v->rule('iplist', 'trustedproxies'); + + return $this->validationResult($v, $name); + } + + /** + * validation for content editor + * + * @param array $params with form data. + * @return true or $v->errors with array of errors to use in json-response + */ + + public function editorInput(array $params) + { + $v = new Validator($params); + + $v->rule('required', ['title', 'content', 'url']); + $v->rule('lengthBetween', 'title', 2, 100); + $v->rule('noHTML', 'title'); + $v->rule('markdownSecure', 'content'); + + if($v->validate()) + { + return true; + } + else + { + return $v->errors(); + } + } + + public function blockInput(array $params) + { + $v = new Validator($params); + + $v->rule('required', ['markdown', 'block_id', 'url']); + $v->rule('markdownSecure', 'markdown'); + $v->rule('regex', 'block_id', '/^[0-9.]+$/i'); + + if($v->validate()) + { + return true; + } + else + { + return $v->errors(); + } + } + + /** + * validation for resort navigation + * + * @param array $params with form data. + * @return true or $v->errors with array of errors to use in json-response + */ + + public function navigationSort(array $params) + { + $v = new Validator($params); + + $v->rule('required', ['item_id', 'parent_id_from', 'parent_id_to']); + $v->rule('regex', 'item_id', '/^[0-9.]+$/i'); + $v->rule('regex', 'parent_id_from', '/^[a-zA-Z0-9.]+$/i'); + $v->rule('regex', 'parent_id_to', '/^[a-zA-Z0-9.]+$/i'); + $v->rule('integer', 'index_new'); + + if($v->validate()) + { + return true; + } + else + { + return $v->errors(); + } + } + + /** + * validation for new navigation items + * + * @param array $params with form data. + * @return true or $v->errors with array of errors to use in json-response + */ + + public function navigationItem(array $params) + { + $v = new Validator($params); + + $v->rule('required', ['folder_id', 'item_name', 'type', 'url']); + $v->rule('regex', 'folder_id', '/^[0-9.]+$/i'); +# $v->rule('noSpecialChars', 'item_name'); + $v->rule('navigation', 'item_name'); + $v->rule('lengthBetween', 'item_name', 1, 60); + $v->rule('in', 'type', ['file', 'folder']); + + if($v->validate()) + { + return true; + } + else + { + return $v->errors(); + } + } + + public function navigationBaseItem(array $params) + { + $v = new Validator($params); + + $v->rule('required', ['item_name', 'type', 'url']); +# $v->rule('noSpecialChars', 'item_name'); + $v->rule('navigation', 'item_name'); + $v->rule('lengthBetween', 'item_name', 1, 40); + $v->rule('in', 'type', ['file', 'folder']); + + if($v->validate()) + { + return true; + } + else + { + return $v->errors(); + } + } + + /** + * validation for dynamic fields ( settings for themes and plugins) + * + * @param string $fieldName with the name of the field. + * @param array or string $fieldValue with the values of the field. + * @param string $objectName with the name of the plugin or theme. + * @param array $fieldDefinitions with the field definitions as multi-dimensional array. + * @return obj $v the validation object passed to a result method. + */ + + public function objectField($fieldName, $fieldValue, $objectName, $fieldDefinitions, $skiprequired = NULL) + { + $v = new Validator(array($fieldName => $fieldValue)); + + if(isset($fieldDefinitions['required']) && !$skiprequired) + { + $v->rule('required', $fieldName); + } + if(isset($fieldDefinitions['maxlength'])) + { + $v->rule('lengthMax', $fieldName, $fieldDefinitions['maxlength']); + } + if(isset($fieldDefinitions['max'])) + { + $v->rule('max', $fieldName, $fieldDefinitions['max']); + } + if(isset($fieldDefinitions['min'])) + { + $v->rule('min', $fieldName, $fieldDefinitions['min']); + } + if(isset($fieldDefinitions['pattern'])) + { + $v->rule('regex', $fieldName, '/^' . $fieldDefinitions['pattern'] . '$/'); + } + + 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); + break; + case "text": + $v->rule('noHTML', $fieldName); + $v->rule('lengthMax', $fieldName, 500); +# $v->rule('regex', $fieldName, '/^[\pL0-9_ \-\.\?\!\/\:]*$/u'); + break; + case "textarea": + # it understands array, json, yaml + if(is_array($fieldValue)) + { + $v = $this->checkArray($fieldValue, $v); + } + else + { + $v->rule('noHTML', $fieldName); + $v->rule('lengthMax', $fieldName, 10000); + } + break; + case "paragraph": + $v->rule('noHTML', $fieldName); + $v->rule('lengthMax', $fieldName, 10000); + 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'); + } + return $this->validationResult($v, $objectName); + } + + /** + * result for validation + * + * @param obj $v the validation object. + * @return bool + */ + + public function checkArray($arrayvalues, $v) + { + foreach($arrayvalues as $key => $value) + { + if(is_array($value)) + { + $this->checkArray($value, $v); + } + $v->rule('noHTML', $value); + $v->rule('lengthMax', $value, 1000); + } + return $v; + } + + public function validationResult($v, $name = false) + { + if($v->validate()) + { + return true; + } + else + { + if($name == 'meta') + { + return $v->errors(); + } + elseif($name) + { + if(isset($_SESSION['errors'][$name])) + { + foreach ($v->errors() as $key => $val) + { + $_SESSION['errors'][$name][$key] = $val; + break; + } + } + else + { + $_SESSION['errors'][$name] = $v->errors(); + } + } + else + { + $_SESSION['errors'] = $v->errors(); + } + return false; + } + } +} \ No newline at end of file diff --git a/system/typemill/Models/Yaml.php b/system/typemill/Models/Yaml.php new file mode 100644 index 0000000..b4d67d6 --- /dev/null +++ b/system/typemill/Models/Yaml.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/Static/Helpers.php b/system/typemill/Static/Helpers.php index 957fc2d..7dc2fda 100644 --- a/system/typemill/Static/Helpers.php +++ b/system/typemill/Static/Helpers.php @@ -2,8 +2,89 @@ namespace Typemill\Static; +use Typemill\Models\StorageWrapper; + class Helpers{ + public static function getUserIP() + { + $client = @$_SERVER['HTTP_CLIENT_IP']; + $forward = @$_SERVER['HTTP_X_FORWARDED_FOR']; + $remote = $_SERVER['REMOTE_ADDR']; + + if(filter_var($client, FILTER_VALIDATE_IP)) + { + $ip = $client; + } + elseif(filter_var($forward, FILTER_VALIDATE_IP)) + { + $ip = $forward; + } + else + { + $ip = $remote; + } + + return $ip; + } + + + public static function addLogEntry($action) + { + $line = self::getUserIP(); + $line .= ';' . date("Y-m-d H:i:s"); + $line .= ';' . $action; + + $storage = new StorageWrapper('\Typemill\Models\Storage'); + $logfile = $storage->getFile('cache', 'securitylog.txt'); + + if($logfile) + { + $logfile .= $line . PHP_EOL; + } + else + { + $logfile = $line . PHP_EOL; + } + + $storage->writeFile('cache', 'securitylog.txt', $logfile); + } + + public static function array_sort($array, $on, $order=SORT_ASC) + { + $new_array = array(); + $sortable_array = array(); + + if (count($array) > 0) { + foreach ($array as $k => $v) { + if (is_array($v)) { + foreach ($v as $k2 => $v2) { + if ($k2 == $on) { + $sortable_array[$k] = $v2; + } + } + } else { + $sortable_array[$k] = $v; + } + } + + switch ($order) { + case SORT_ASC: + asort($sortable_array); + break; + case SORT_DESC: + arsort($sortable_array); + break; + } + + foreach ($sortable_array as $k => $v) { + $new_array[] = $array[$k]; + } + } + + return $new_array; + } + public static function printTimer($timer) { $lastTime = NULL; diff --git a/system/typemill/Static/Languages.php b/system/typemill/Static/Languages.php deleted file mode 100644 index fd9d39f..0000000 --- a/system/typemill/Static/Languages.php +++ /dev/null @@ -1,24 +0,0 @@ -' . $segment; - echo '
' . $routepath; + #echo '
' . $segment; + #echo '
' . $routepath; if(substr( $routepath, 0, strlen($segment) ) === ltrim($segment, '/')) { - echo '
Create Session'; + #echo '
Create Session'; # configure session ini_set('session.cookie_httponly', 1 ); diff --git a/system/typemill/Static/Settings.php b/system/typemill/Static/Settings.php index 320fb8b..be4e78e 100644 --- a/system/typemill/Static/Settings.php +++ b/system/typemill/Static/Settings.php @@ -4,8 +4,9 @@ namespace Typemill\Static; class Settings { - public static function loadSettings($rootpath) + public static function loadSettings() { + $rootpath = getcwd(); $defaultsettings = self::getDefaultSettings($rootpath); $usersettings = self::getUserSettings($rootpath); diff --git a/system/typemill/Static/Translations.php b/system/typemill/Static/Translations.php new file mode 100644 index 0000000..10ebfbb --- /dev/null +++ b/system/typemill/Static/Translations.php @@ -0,0 +1,110 @@ +getYaml($theme_language_folder, $theme_language_file); + } + + if($environment == 'admin') + { + $system_language_folder = 'system' . DIRECTORY_SEPARATOR . 'typemill' . DIRECTORY_SEPARATOR . 'author' . DIRECTORY_SEPARATOR . 'translations' . DIRECTORY_SEPARATOR; + $system_language_file = $language . '.yaml'; + if(file_exists($system_language_folder . $system_language_file)) + { + $system_translations = $yaml->getYaml($system_language_folder, $system_language_file); + } + + # Next change, to provide labels for the admin and user environments. + # There may be plugins that only work in the user environment, only in the admin environment, or in both environments. + $plugin_labels = []; + if(isset($settings['plugins']) && !empty($settings['plugins'])) + { + foreach($settings['plugins'] as $plugin => $config) + { + if($config['active'] == 'on') + { + $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); + } + } + } + + foreach($plugins_translations as $key => $value) + { + if(is_array($value)) + { + $plugins_translations = array_merge($plugins_translations, $value); + } + } + } + } + + $translations = []; + if(!empty($plugins_translations)) + { + $translations = array_merge($translations, $plugins_translations); + } + if(!empty($system_translations)) + { + $translations = array_merge($translations, $system_translations); + } + if(!empty($theme_translations)) + { + $translations = array_merge($translations, $theme_translations); + } + + return $translations; + } + + public static function whichLanguage() + { + # Check which languages are available + $langs = []; + $path = __DIR__ . '/author/languages/*.yaml'; + + foreach (glob($path) as $filename) + { + $langs[] = basename($filename,'.yaml'); + } + + # Detect browser language + $accept_lang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2) : false; + $lang = in_array($accept_lang, $langs) ? $accept_lang : 'en'; + + return $lang; + } +} \ No newline at end of file diff --git a/system/typemill/author/auth/login.twig b/system/typemill/author/auth/login.twig new file mode 100644 index 0000000..fa2f5e7 --- /dev/null +++ b/system/typemill/author/auth/login.twig @@ -0,0 +1,83 @@ +{% extends 'layouts/layoutAuth.twig' %} + +{% block title %}Login{% endblock %} + +{% block content %} + +
+ +
+
+ +

Login

+ +
+ +
+ +
+ + + {% if errors.signup_username %} + {{ errors.username|first }} + {% endif %} +
+ +
+ + + {% if errors.password %} + {{ errors.password|first }} + {% endif %} +
+ + + + + +
+ + + + {% if settings.recoverpw %} + + + {% else %} + +
+ + {% endif %} + +
+
+
+ +
+
+

Latest from Typemill

+

Hey, get the latest news from Typemill please!

+

base_url(): {{ base_url() }}

+

current_url(): {{ current_url() }}

+

current_path(): {{ current_path() }}

+
+
+ +
+{% endblock %} \ No newline at end of file diff --git a/system/typemill/author/css/custom.css b/system/typemill/author/css/custom.css new file mode 100644 index 0000000..fceb575 --- /dev/null +++ b/system/typemill/author/css/custom.css @@ -0,0 +1,17 @@ +/******************** +* SVG ICONS * +********************/ + +.icon { + display: inline-block; + width: 1em; + height: 1em; + stroke-width: 0; + stroke: currentColor; + fill: currentColor; +} +.icon.baseline{ + top: 0.125em; + position: relative; +} + diff --git a/system/typemill/author/css/input.css b/system/typemill/author/css/input.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/system/typemill/author/css/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/system/typemill/author/css/output.css b/system/typemill/author/css/output.css new file mode 100644 index 0000000..47ef2f6 --- /dev/null +++ b/system/typemill/author/css/output.css @@ -0,0 +1,1140 @@ +/* +! tailwindcss v3.1.6 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input:-ms-input-placeholder, textarea:-ms-input-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::-webkit-backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.container { + width: 100%; +} + +.\!container { + width: 100% !important; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } + + .\!container { + max-width: 640px !important; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } + + .\!container { + max-width: 768px !important; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } + + .\!container { + max-width: 1024px !important; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } + + .\!container { + max-width: 1280px !important; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } + + .\!container { + max-width: 1536px !important; + } +} + +.static { + position: static; +} + +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.m-0 { + margin: 0px; +} + +.m-auto { + margin: auto; +} + +.my-2 { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.mt-6 { + margin-top: 1.5rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.mt-5 { + margin-top: 1.25rem; +} + +.mt-7 { + margin-top: 1.75rem; +} + +.mr-4 { + margin-right: 1rem; +} + +.mr-2 { + margin-right: 0.5rem; +} + +.mb-1 { + margin-bottom: 0.25rem; +} + +.block { + display: block; +} + +.inline-block { + display: inline-block; +} + +.inline { + display: inline; +} + +.flex { + display: flex; +} + +.table { + display: table; +} + +.hidden { + display: none; +} + +.min-h-screen { + min-height: 100vh; +} + +.w-1\/2 { + width: 50%; +} + +.w-full { + width: 100%; +} + +.w-1\/5 { + width: 20%; +} + +.w-4\/5 { + width: 80%; +} + +.w-1\/4 { + width: 25%; +} + +.w-3\/4 { + width: 75%; +} + +.max-w-md { + max-width: 28rem; +} + +.max-w-6xl { + max-width: 72rem; +} + +.max-w-xl { + max-width: 36rem; +} + +.max-w-sm { + max-width: 24rem; +} + +.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)); +} + +.cursor-pointer { + cursor: pointer; +} + +.content-center { + align-content: center; +} + +.items-center { + align-items: center; +} + +.justify-end { + justify-content: flex-end; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.rounded { + border-radius: 0.25rem; +} + +.border { + border-width: 1px; +} + +.border-0 { + border-width: 0px; +} + +.border-b-2 { + border-bottom-width: 2px; +} + +.border-l-2 { + border-left-width: 2px; +} + +.border-r-2 { + border-right-width: 2px; +} + +.border-b-4 { + border-bottom-width: 4px; +} + +.border-l-4 { + border-left-width: 4px; +} + +.border-solid { + border-style: solid; +} + +.border-gray-300 { + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity)); +} + +.border-stone-200 { + --tw-border-opacity: 1; + border-color: rgb(231 229 228 / var(--tw-border-opacity)); +} + +.border-stone-700 { + --tw-border-opacity: 1; + border-color: rgb(68 64 60 / var(--tw-border-opacity)); +} + +.border-stone-100 { + --tw-border-opacity: 1; + border-color: rgb(245 245 244 / var(--tw-border-opacity)); +} + +.border-slate-100 { + --tw-border-opacity: 1; + border-color: rgb(241 245 249 / var(--tw-border-opacity)); +} + +.border-slate-200 { + --tw-border-opacity: 1; + border-color: rgb(226 232 240 / var(--tw-border-opacity)); +} + +.bg-rose-600 { + --tw-bg-opacity: 1; + background-color: rgb(225 29 72 / var(--tw-bg-opacity)); +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / 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-stone-50 { + --tw-bg-opacity: 1; + background-color: rgb(250 250 249 / var(--tw-bg-opacity)); +} + +.bg-stone-200 { + --tw-bg-opacity: 1; + background-color: rgb(231 229 228 / var(--tw-bg-opacity)); +} + +.bg-stone-400 { + --tw-bg-opacity: 1; + background-color: rgb(168 162 158 / var(--tw-bg-opacity)); +} + +.bg-stone-300 { + --tw-bg-opacity: 1; + background-color: rgb(214 211 209 / var(--tw-bg-opacity)); +} + +.bg-clip-padding { + background-clip: padding-box; +} + +.p-3 { + padding: 0.75rem; +} + +.p-4 { + padding: 1rem; +} + +.p-2 { + padding: 0.5rem; +} + +.py-5 { + padding-top: 1.25rem; + padding-bottom: 1.25rem; +} + +.px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +.py-1\.5 { + padding-top: 0.375rem; + padding-bottom: 0.375rem; +} + +.py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.py-3 { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.pt-4 { + padding-top: 1rem; +} + +.pb-3 { + padding-bottom: 0.75rem; +} + +.pb-2 { + padding-bottom: 0.5rem; +} + +.pb-1 { + padding-bottom: 0.25rem; +} + +.text-6xl { + font-size: 3.75rem; + line-height: 1; +} + +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + +.text-xs { + font-size: 0.75rem; + line-height: 1rem; +} + +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + +.font-normal { + font-weight: 400; +} + +.font-medium { + font-weight: 500; +} + +.font-bold { + font-weight: 700; +} + +.uppercase { + text-transform: uppercase; +} + +.lowercase { + text-transform: lowercase; +} + +.capitalize { + text-transform: capitalize; +} + +.leading-tight { + line-height: 1.25; +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.text-gray-700 { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity)); +} + +.text-rose-600 { + --tw-text-opacity: 1; + color: rgb(225 29 72 / var(--tw-text-opacity)); +} + +.underline { + -webkit-text-decoration-line: underline; + text-decoration-line: underline; +} + +.shadow { + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-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); +} + +.\!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) !important; +} + +.transition { + transition-property: color, background-color, border-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-text-decoration-color, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-text-decoration-color, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.\!transition { + transition-property: color, background-color, border-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-text-decoration-color, -webkit-backdrop-filter !important; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter !important; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-text-decoration-color, -webkit-backdrop-filter !important; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1) !important; + transition-duration: 150ms !important; +} + +.duration-150 { + transition-duration: 150ms; +} + +.ease-in-out { + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + +.hover\:border-stone-700:hover { + --tw-border-opacity: 1; + border-color: rgb(68 64 60 / var(--tw-border-opacity)); +} + +.hover\:border-cyan-300:hover { + --tw-border-opacity: 1; + border-color: rgb(103 232 249 / var(--tw-border-opacity)); +} + +.hover\:border-cyan-500:hover { + --tw-border-opacity: 1; + border-color: rgb(6 182 212 / var(--tw-border-opacity)); +} + +.hover\:bg-gray-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); +} + +.hover\:bg-stone-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(68 64 60 / 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)); +} + +.focus\:border-blue-600:focus { + --tw-border-opacity: 1; + border-color: rgb(37 99 235 / var(--tw-border-opacity)); +} + +.focus\:bg-white:focus { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.focus\:bg-stone-700:focus { + --tw-bg-opacity: 1; + background-color: rgb(68 64 60 / var(--tw-bg-opacity)); +} + +.focus\:bg-stone-50:focus { + --tw-bg-opacity: 1; + background-color: rgb(250 250 249 / var(--tw-bg-opacity)); +} + +.focus\:text-gray-700:focus { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity)); +} + +.focus\:text-white:focus { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.focus\:outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.focus\:ring-0:focus { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.active\:bg-stone-700:active { + --tw-bg-opacity: 1; + background-color: rgb(68 64 60 / var(--tw-bg-opacity)); +} + +.active\:bg-stone-50:active { + --tw-bg-opacity: 1; + background-color: rgb(250 250 249 / var(--tw-bg-opacity)); +} + +.active\:text-white:active { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} \ No newline at end of file diff --git a/system/typemill/author/js/axios.min.js b/system/typemill/author/js/axios.min.js new file mode 100644 index 0000000..e8e4fc1 --- /dev/null +++ b/system/typemill/author/js/axios.min.js @@ -0,0 +1,3 @@ +/* axios v0.27.2 | (c) 2022 by Matt Zabriskie */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.axios=t():e.axios=t()}(this,(function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=13)}([function(e,t,n){"use strict";var r,o=n(4),i=Object.prototype.toString,s=(r=Object.create(null),function(e){var t=i.call(e);return r[t]||(r[t]=t.slice(8,-1).toLowerCase())});function a(e){return e=e.toLowerCase(),function(t){return s(t)===e}}function u(e){return Array.isArray(e)}function c(e){return void 0===e}var f=a("ArrayBuffer");function l(e){return null!==e&&"object"==typeof e}function p(e){if("object"!==s(e))return!1;var t=Object.getPrototypeOf(e);return null===t||t===Object.prototype}var d=a("Date"),h=a("File"),m=a("Blob"),v=a("FileList");function y(e){return"[object Function]"===i.call(e)}var g=a("URLSearchParams");function E(e,t){if(null!=e)if("object"!=typeof e&&(e=[e]),u(e))for(var n=0,r=e.length;n0;)s[i=r[o]]||(t[i]=e[i],s[i]=!0);e=Object.getPrototypeOf(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},kindOf:s,kindOfTest:a,endsWith:function(e,t,n){e=String(e),(void 0===n||n>e.length)&&(n=e.length),n-=t.length;var r=e.indexOf(t,n);return-1!==r&&r===n},toArray:function(e){if(!e)return null;var t=e.length;if(c(t))return null;for(var n=new Array(t);t-- >0;)n[t]=e[t];return n},isTypedArray:O,isFileList:v}},function(e,t,n){"use strict";var r=n(0);function o(e,t,n,r,o){Error.call(this),this.message=e,this.name="AxiosError",t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),o&&(this.response=o)}r.inherits(o,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:this.config,code:this.code,status:this.response&&this.response.status?this.response.status:null}}});var i=o.prototype,s={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED"].forEach((function(e){s[e]={value:e}})),Object.defineProperties(o,s),Object.defineProperty(i,"isAxiosError",{value:!0}),o.from=function(e,t,n,s,a,u){var c=Object.create(i);return r.toFlatObject(e,c,(function(e){return e!==Error.prototype})),o.call(c,e.message,t,n,s,a),c.name=e.name,u&&Object.assign(c,u),c},e.exports=o},function(e,t,n){"use strict";var r=n(1);function o(e){r.call(this,null==e?"canceled":e,r.ERR_CANCELED),this.name="CanceledError"}n(0).inherits(o,r,{__CANCEL__:!0}),e.exports=o},function(e,t,n){"use strict";var r=n(0),o=n(19),i=n(1),s=n(6),a=n(7),u={"Content-Type":"application/x-www-form-urlencoded"};function c(e,t){!r.isUndefined(e)&&r.isUndefined(e["Content-Type"])&&(e["Content-Type"]=t)}var f,l={transitional:s,adapter:(("undefined"!=typeof XMLHttpRequest||"undefined"!=typeof process&&"[object process]"===Object.prototype.toString.call(process))&&(f=n(8)),f),transformRequest:[function(e,t){if(o(t,"Accept"),o(t,"Content-Type"),r.isFormData(e)||r.isArrayBuffer(e)||r.isBuffer(e)||r.isStream(e)||r.isFile(e)||r.isBlob(e))return e;if(r.isArrayBufferView(e))return e.buffer;if(r.isURLSearchParams(e))return c(t,"application/x-www-form-urlencoded;charset=utf-8"),e.toString();var n,i=r.isObject(e),s=t&&t["Content-Type"];if((n=r.isFileList(e))||i&&"multipart/form-data"===s){var u=this.env&&this.env.FormData;return a(n?{"files[]":e}:e,u&&new u)}return i||"application/json"===s?(c(t,"application/json"),function(e,t,n){if(r.isString(e))try{return(t||JSON.parse)(e),r.trim(e)}catch(e){if("SyntaxError"!==e.name)throw e}return(n||JSON.stringify)(e)}(e)):e}],transformResponse:[function(e){var t=this.transitional||l.transitional,n=t&&t.silentJSONParsing,o=t&&t.forcedJSONParsing,s=!n&&"json"===this.responseType;if(s||o&&r.isString(e)&&e.length)try{return JSON.parse(e)}catch(e){if(s){if("SyntaxError"===e.name)throw i.from(e,i.ERR_BAD_RESPONSE,this,null,this.response);throw e}}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:n(27)},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*"}}};r.forEach(["delete","get","head"],(function(e){l.headers[e]={}})),r.forEach(["post","put","patch"],(function(e){l.headers[e]=r.merge(u)})),e.exports=l},function(e,t,n){"use strict";e.exports=function(e,t){return function(){for(var n=new Array(arguments.length),r=0;r=0)return;s[t]="set-cookie"===t?(s[t]?s[t]:[]).concat([n]):s[t]?s[t]+", "+n:n}})),s):s}},function(e,t,n){"use strict";var r=n(0);e.exports=r.isStandardBrowserEnv()?function(){var e,t=/(msie|trident)/i.test(navigator.userAgent),n=document.createElement("a");function o(e){var r=e;return t&&(n.setAttribute("href",r),r=n.href),n.setAttribute("href",r),{href:n.href,protocol:n.protocol?n.protocol.replace(/:$/,""):"",host:n.host,search:n.search?n.search.replace(/^\?/,""):"",hash:n.hash?n.hash.replace(/^#/,""):"",hostname:n.hostname,port:n.port,pathname:"/"===n.pathname.charAt(0)?n.pathname:"/"+n.pathname}}return e=o(window.location.href),function(t){var n=r.isString(t)?o(t):t;return n.protocol===e.protocol&&n.host===e.host}}():function(){return!0}},function(e,t,n){"use strict";e.exports=function(e){var t=/^([-+\w]{1,25})(:?\/\/|:)/.exec(e);return t&&t[1]||""}},function(e,t){e.exports=null},function(e,t,n){"use strict";var r=n(12).version,o=n(1),i={};["object","boolean","number","function","string","symbol"].forEach((function(e,t){i[e]=function(n){return typeof n===e||"a"+(t<1?"n ":" ")+e}}));var s={};i.transitional=function(e,t,n){function i(e,t){return"[Axios v"+r+"] Transitional option '"+e+"'"+t+(n?". "+n:"")}return function(n,r,a){if(!1===e)throw new o(i(r," has been removed"+(t?" in "+t:"")),o.ERR_DEPRECATED);return t&&!s[r]&&(s[r]=!0,console.warn(i(r," has been deprecated since v"+t+" and will be removed in the near future"))),!e||e(n,r,a)}},e.exports={assertOptions:function(e,t,n){if("object"!=typeof e)throw new o("options must be an object",o.ERR_BAD_OPTION_VALUE);for(var r=Object.keys(e),i=r.length;i-- >0;){var s=r[i],a=t[s];if(a){var u=e[s],c=void 0===u||a(u,s,e);if(!0!==c)throw new o("option "+s+" must be "+c,o.ERR_BAD_OPTION_VALUE)}else if(!0!==n)throw new o("Unknown option "+s,o.ERR_BAD_OPTION)}},validators:i}},function(e,t,n){"use strict";var r=n(2);function o(e){if("function"!=typeof e)throw new TypeError("executor must be a function.");var t;this.promise=new Promise((function(e){t=e}));var n=this;this.promise.then((function(e){if(n._listeners){var t,r=n._listeners.length;for(t=0;t { + 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 diff --git a/system/typemill/author/js/vue.js b/system/typemill/author/js/vue.js new file mode 100644 index 0000000..4687e6c --- /dev/null +++ b/system/typemill/author/js/vue.js @@ -0,0 +1,15988 @@ +var Vue = (function (exports) { + 'use strict'; + + /** + * Make a map and return a function for checking if a key + * is in that map. + * IMPORTANT: all calls of this function must be prefixed with + * \/\*#\_\_PURE\_\_\*\/ + * So that rollup can tree-shake them if necessary. + */ + function makeMap(str, expectsLowerCase) { + const map = Object.create(null); + const list = str.split(','); + for (let i = 0; i < list.length; i++) { + map[list[i]] = true; + } + return expectsLowerCase ? val => !!map[val.toLowerCase()] : val => !!map[val]; + } + + /** + * dev only flag -> name mapping + */ + const PatchFlagNames = { + [1 /* PatchFlags.TEXT */]: `TEXT`, + [2 /* PatchFlags.CLASS */]: `CLASS`, + [4 /* PatchFlags.STYLE */]: `STYLE`, + [8 /* PatchFlags.PROPS */]: `PROPS`, + [16 /* PatchFlags.FULL_PROPS */]: `FULL_PROPS`, + [32 /* PatchFlags.HYDRATE_EVENTS */]: `HYDRATE_EVENTS`, + [64 /* PatchFlags.STABLE_FRAGMENT */]: `STABLE_FRAGMENT`, + [128 /* PatchFlags.KEYED_FRAGMENT */]: `KEYED_FRAGMENT`, + [256 /* PatchFlags.UNKEYED_FRAGMENT */]: `UNKEYED_FRAGMENT`, + [512 /* PatchFlags.NEED_PATCH */]: `NEED_PATCH`, + [1024 /* PatchFlags.DYNAMIC_SLOTS */]: `DYNAMIC_SLOTS`, + [2048 /* PatchFlags.DEV_ROOT_FRAGMENT */]: `DEV_ROOT_FRAGMENT`, + [-1 /* PatchFlags.HOISTED */]: `HOISTED`, + [-2 /* PatchFlags.BAIL */]: `BAIL` + }; + + /** + * Dev only + */ + const slotFlagsText = { + [1 /* SlotFlags.STABLE */]: 'STABLE', + [2 /* SlotFlags.DYNAMIC */]: 'DYNAMIC', + [3 /* SlotFlags.FORWARDED */]: 'FORWARDED' + }; + + const GLOBALS_WHITE_LISTED = 'Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,' + + 'decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,' + + 'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt'; + const isGloballyWhitelisted = /*#__PURE__*/ makeMap(GLOBALS_WHITE_LISTED); + + const range = 2; + function generateCodeFrame(source, start = 0, end = source.length) { + // Split the content into individual lines but capture the newline sequence + // that separated each line. This is important because the actual sequence is + // needed to properly take into account the full line length for offset + // comparison + let lines = source.split(/(\r?\n)/); + // Separate the lines and newline sequences into separate arrays for easier referencing + const newlineSequences = lines.filter((_, idx) => idx % 2 === 1); + lines = lines.filter((_, idx) => idx % 2 === 0); + let count = 0; + const res = []; + for (let i = 0; i < lines.length; i++) { + count += + lines[i].length + + ((newlineSequences[i] && newlineSequences[i].length) || 0); + if (count >= start) { + for (let j = i - range; j <= i + range || end > count; j++) { + if (j < 0 || j >= lines.length) + continue; + const line = j + 1; + res.push(`${line}${' '.repeat(Math.max(3 - String(line).length, 0))}| ${lines[j]}`); + const lineLength = lines[j].length; + const newLineSeqLength = (newlineSequences[j] && newlineSequences[j].length) || 0; + if (j === i) { + // push underline + const pad = start - (count - (lineLength + newLineSeqLength)); + const length = Math.max(1, end > count ? lineLength - pad : end - start); + res.push(` | ` + ' '.repeat(pad) + '^'.repeat(length)); + } + else if (j > i) { + if (end > count) { + const length = Math.max(Math.min(end - count, lineLength), 1); + res.push(` | ` + '^'.repeat(length)); + } + count += lineLength + newLineSeqLength; + } + } + break; + } + } + return res.join('\n'); + } + + /** + * On the client we only need to offer special cases for boolean attributes that + * have different names from their corresponding dom properties: + * - itemscope -> N/A + * - allowfullscreen -> allowFullscreen + * - formnovalidate -> formNoValidate + * - ismap -> isMap + * - nomodule -> noModule + * - novalidate -> noValidate + * - readonly -> readOnly + */ + const specialBooleanAttrs = `itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`; + const isSpecialBooleanAttr = /*#__PURE__*/ makeMap(specialBooleanAttrs); + /** + * Boolean attributes should be included if the value is truthy or ''. + * e.g. `