From d4f2fcdf7908ffa176350468a85e631f0497646c Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 2 Aug 2022 20:11:02 +0100 Subject: [PATCH 001/477] Started codemirror update, In broken state --- dev/build/esbuild.js | 16 +- package-lock.json | 633 +++++++++++++++------- package.json | 13 +- resources/js/{code.mjs => code/index.mjs} | 161 ++---- resources/js/code/modes.js | 134 +++++ resources/js/code/setups.js | 32 ++ 6 files changed, 661 insertions(+), 328 deletions(-) rename resources/js/{code.mjs => code/index.mjs} (55%) create mode 100644 resources/js/code/modes.js create mode 100644 resources/js/code/setups.js diff --git a/dev/build/esbuild.js b/dev/build/esbuild.js index 46357038a..57a224876 100644 --- a/dev/build/esbuild.js +++ b/dev/build/esbuild.js @@ -1,7 +1,6 @@ #!/usr/bin/env node const esbuild = require('esbuild'); -const fs = require('fs'); const path = require('path'); // Check if we're building for production @@ -9,20 +8,19 @@ const path = require('path'); const isProd = process.argv[2] === 'production'; // Gather our input files -const jsInDir = path.join(__dirname, '../../resources/js'); -const jsInDirFiles = fs.readdirSync(jsInDir, 'utf8'); -const entryFiles = jsInDirFiles - .filter(f => f.endsWith('.js') || f.endsWith('.mjs')) - .map(f => path.join(jsInDir, f)); +const entryPoints = { + app: path.join(__dirname, '../../resources/js/app.js'), + code: path.join(__dirname, '../../resources/js/code/index.mjs'), +}; // Locate our output directory -const outDir = path.join(__dirname, '../../public/dist'); +const outdir = path.join(__dirname, '../../public/dist'); // Build via esbuild esbuild.build({ bundle: true, - entryPoints: entryFiles, - outdir: outDir, + entryPoints, + outdir, sourcemap: true, target: 'es2020', mainFields: ['module', 'main'], diff --git a/package-lock.json b/package-lock.json index 1448d592f..c141e654a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,21 +5,131 @@ "packages": { "": { "dependencies": { + "@codemirror/commands": "^6.0.1", + "@codemirror/language": "^6.2.1", + "@codemirror/legacy-modes": "^6.1.0", + "@codemirror/state": "^6.1.0", + "@codemirror/view": "^6.1.2", "clipboard": "^2.0.11", - "codemirror": "^5.65.5", + "codemirror": "^6.0.1", "dropzone": "^5.9.3", "markdown-it": "^13.0.1", "markdown-it-task-lists": "^2.1.1", - "snabbdom": "^3.5.0", + "snabbdom": "^3.5.1", "sortablejs": "^1.15.0" }, "devDependencies": { "chokidar-cli": "^3.0", - "esbuild": "0.14.42", + "esbuild": "0.14.51", "livereload": "^0.9.3", "npm-run-all": "^4.1.5", "punycode": "^2.1.1", - "sass": "^1.52.1" + "sass": "^1.54.0" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.1.0.tgz", + "integrity": "sha512-wtO4O5WDyXhhCd4q4utDIDZxnQfmJ++3dGBCG9LMtI79+92OcA1DVk/n7BEupKmjIr8AzvptDz7YQ9ud6OkU+A==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.0.1.tgz", + "integrity": "sha512-iNHDByicYqQjs0Wo1MKGfqNbMYMyhS9WV6EwMVwsHXImlFemgEUC+c5X22bXKBStN3qnwg4fArNZM+gkv22baQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.2.1.tgz", + "integrity": "sha512-MC3svxuvIj0MRpFlGHxLS6vPyIdbTr2KKPEW46kCoCXw2ktb4NTkpkPBI/lSP/FoNXLCBJ0mrnUi1OoZxtpW1Q==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.1.0.tgz", + "integrity": "sha512-V/PgGpndkZeTn3Hdlg/gd8MLFdyvTCIX+iwJzjUw5iNziWiNsAY8X0jvf7m3gSfxnKkNzmid6l0g4rYSpiDaCw==", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.0.0.tgz", + "integrity": "sha512-nUUXcJW1Xp54kNs+a1ToPLK8MadO0rMTnJB8Zk4Z8gBdrN0kqV7uvUraU/T2yqg+grDNR38Vmy/MrhQN/RgwiA==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.0.1.tgz", + "integrity": "sha512-uOinkOrM+daMduCgMPomDfKLr7drGHB4jHl3Vq6xY2WRlL7MkNsBE0b+XHYa/Mee2npsJOgwvkW4n1lMFeBW2Q==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.1.0.tgz", + "integrity": "sha512-qbUr94DZTe6/V1VS7LDLz11rM/1t/nJxR1El4I6UaxDEdc0aZZvq6JCLJWiRmUf95NRAnDH6fhXn+PWp9wGCIg==" + }, + "node_modules/@codemirror/view": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.1.2.tgz", + "integrity": "sha512-puUydfKwfmOo+ixtuB+uN/ZpcteEYSnpjHmMaow1sOQhNICsKtGBup3i9ybVqyzDagARRYzSHTWjbdeHqmn31w==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@lezer/common": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz", + "integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==" + }, + "node_modules/@lezer/highlight": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.0.0.tgz", + "integrity": "sha512-nsCnNtim90UKsB5YxoX65v3GEIw3iCHw9RM2DtdgkiqAbKh9pCdvi8AWNwkYf10Lu6fxNhXPpkpHbW6mihhvJA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.0.tgz", + "integrity": "sha512-TgEpfm9br2SX8JwtwKT8HsQZKuFkLRg6g+IRxObk9nVKQLKnkP3oMh+QGcTBL9GQsfQ2ADtKPbj2iGSMf3ytiA==", + "dependencies": { + "@lezer/common": "^1.0.0" } }, "node_modules/ansi-regex": { @@ -195,9 +305,18 @@ } }, "node_modules/codemirror": { - "version": "5.65.5", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.5.tgz", - "integrity": "sha512-HNyhvGLnYz5c+kIsB9QKVitiZUevha3ovbIYaQiGzKo7ECSL/elWD9RXt3JgNr0NdnyqE9/Rc/7uLfkJQL638w==" + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } }, "node_modules/color-convert": { "version": "1.9.3", @@ -220,6 +339,11 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "node_modules/crelt": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz", + "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==" + }, "node_modules/cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -345,9 +469,9 @@ } }, "node_modules/esbuild": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.42.tgz", - "integrity": "sha512-V0uPZotCEHokJdNqyozH6qsaQXqmZEOiZWrXnds/zaH/0SyrIayRXWRB98CENO73MIZ9T3HBIOsmds5twWtmgw==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.51.tgz", + "integrity": "sha512-+CvnDitD7Q5sT7F+FM65sWkF8wJRf+j9fPcprxYV4j+ohmzVj2W7caUqH2s5kCaCJAfcAICjSlKhDCcvDpU7nw==", "dev": true, "hasInstallScript": true, "bin": { @@ -357,32 +481,32 @@ "node": ">=12" }, "optionalDependencies": { - "esbuild-android-64": "0.14.42", - "esbuild-android-arm64": "0.14.42", - "esbuild-darwin-64": "0.14.42", - "esbuild-darwin-arm64": "0.14.42", - "esbuild-freebsd-64": "0.14.42", - "esbuild-freebsd-arm64": "0.14.42", - "esbuild-linux-32": "0.14.42", - "esbuild-linux-64": "0.14.42", - "esbuild-linux-arm": "0.14.42", - "esbuild-linux-arm64": "0.14.42", - "esbuild-linux-mips64le": "0.14.42", - "esbuild-linux-ppc64le": "0.14.42", - "esbuild-linux-riscv64": "0.14.42", - "esbuild-linux-s390x": "0.14.42", - "esbuild-netbsd-64": "0.14.42", - "esbuild-openbsd-64": "0.14.42", - "esbuild-sunos-64": "0.14.42", - "esbuild-windows-32": "0.14.42", - "esbuild-windows-64": "0.14.42", - "esbuild-windows-arm64": "0.14.42" + "esbuild-android-64": "0.14.51", + "esbuild-android-arm64": "0.14.51", + "esbuild-darwin-64": "0.14.51", + "esbuild-darwin-arm64": "0.14.51", + "esbuild-freebsd-64": "0.14.51", + "esbuild-freebsd-arm64": "0.14.51", + "esbuild-linux-32": "0.14.51", + "esbuild-linux-64": "0.14.51", + "esbuild-linux-arm": "0.14.51", + "esbuild-linux-arm64": "0.14.51", + "esbuild-linux-mips64le": "0.14.51", + "esbuild-linux-ppc64le": "0.14.51", + "esbuild-linux-riscv64": "0.14.51", + "esbuild-linux-s390x": "0.14.51", + "esbuild-netbsd-64": "0.14.51", + "esbuild-openbsd-64": "0.14.51", + "esbuild-sunos-64": "0.14.51", + "esbuild-windows-32": "0.14.51", + "esbuild-windows-64": "0.14.51", + "esbuild-windows-arm64": "0.14.51" } }, "node_modules/esbuild-android-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.42.tgz", - "integrity": "sha512-P4Y36VUtRhK/zivqGVMqhptSrFILAGlYp0Z8r9UQqHJ3iWztRCNWnlBzD9HRx0DbueXikzOiwyOri+ojAFfW6A==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.51.tgz", + "integrity": "sha512-6FOuKTHnC86dtrKDmdSj2CkcKF8PnqkaIXqvgydqfJmqBazCPdw+relrMlhGjkvVdiiGV70rpdnyFmA65ekBCQ==", "cpu": [ "x64" ], @@ -396,9 +520,9 @@ } }, "node_modules/esbuild-android-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.42.tgz", - "integrity": "sha512-0cOqCubq+RWScPqvtQdjXG3Czb3AWI2CaKw3HeXry2eoA2rrPr85HF7IpdU26UWdBXgPYtlTN1LUiuXbboROhg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.51.tgz", + "integrity": "sha512-vBtp//5VVkZWmYYvHsqBRCMMi1MzKuMIn5XDScmnykMTu9+TD9v0NMEDqQxvtFToeYmojdo5UCV2vzMQWJcJ4A==", "cpu": [ "arm64" ], @@ -412,9 +536,9 @@ } }, "node_modules/esbuild-darwin-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.42.tgz", - "integrity": "sha512-ipiBdCA3ZjYgRfRLdQwP82rTiv/YVMtW36hTvAN5ZKAIfxBOyPXY7Cejp3bMXWgzKD8B6O+zoMzh01GZsCuEIA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.51.tgz", + "integrity": "sha512-YFmXPIOvuagDcwCejMRtCDjgPfnDu+bNeh5FU2Ryi68ADDVlWEpbtpAbrtf/lvFTWPexbgyKgzppNgsmLPr8PA==", "cpu": [ "x64" ], @@ -428,9 +552,9 @@ } }, "node_modules/esbuild-darwin-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.42.tgz", - "integrity": "sha512-bU2tHRqTPOaoH/4m0zYHbFWpiYDmaA0gt90/3BMEFaM0PqVK/a6MA2V/ypV5PO0v8QxN6gH5hBPY4YJ2lopXgA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.51.tgz", + "integrity": "sha512-juYD0QnSKwAMfzwKdIF6YbueXzS6N7y4GXPDeDkApz/1RzlT42mvX9jgNmyOlWKN7YzQAYbcUEJmZJYQGdf2ow==", "cpu": [ "arm64" ], @@ -444,9 +568,9 @@ } }, "node_modules/esbuild-freebsd-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.42.tgz", - "integrity": "sha512-75h1+22Ivy07+QvxHyhVqOdekupiTZVLN1PMwCDonAqyXd8TVNJfIRFrdL8QmSJrOJJ5h8H1I9ETyl2L8LQDaw==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.51.tgz", + "integrity": "sha512-cLEI/aXjb6vo5O2Y8rvVSQ7smgLldwYY5xMxqh/dQGfWO+R1NJOFsiax3IS4Ng300SVp7Gz3czxT6d6qf2cw0g==", "cpu": [ "x64" ], @@ -460,9 +584,9 @@ } }, "node_modules/esbuild-freebsd-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.42.tgz", - "integrity": "sha512-W6Jebeu5TTDQMJUJVarEzRU9LlKpNkPBbjqSu+GUPTHDCly5zZEQq9uHkmHHl7OKm+mQ2zFySN83nmfCeZCyNA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.51.tgz", + "integrity": "sha512-TcWVw/rCL2F+jUgRkgLa3qltd5gzKjIMGhkVybkjk6PJadYInPtgtUBp1/hG+mxyigaT7ib+od1Xb84b+L+1Mg==", "cpu": [ "arm64" ], @@ -476,9 +600,9 @@ } }, "node_modules/esbuild-linux-32": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.42.tgz", - "integrity": "sha512-Ooy/Bj+mJ1z4jlWcK5Dl6SlPlCgQB9zg1UrTCeY8XagvuWZ4qGPyYEWGkT94HUsRi2hKsXvcs6ThTOjBaJSMfg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.51.tgz", + "integrity": "sha512-RFqpyC5ChyWrjx8Xj2K0EC1aN0A37H6OJfmUXIASEqJoHcntuV3j2Efr9RNmUhMfNE6yEj2VpYuDteZLGDMr0w==", "cpu": [ "ia32" ], @@ -492,9 +616,9 @@ } }, "node_modules/esbuild-linux-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.42.tgz", - "integrity": "sha512-2L0HbzQfbTuemUWfVqNIjOfaTRt9zsvjnme6lnr7/MO9toz/MJ5tZhjqrG6uDWDxhsaHI2/nsDgrv8uEEN2eoA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.51.tgz", + "integrity": "sha512-dxjhrqo5i7Rq6DXwz5v+MEHVs9VNFItJmHBe1CxROWNf4miOGoQhqSG8StStbDkQ1Mtobg6ng+4fwByOhoQoeA==", "cpu": [ "x64" ], @@ -508,9 +632,9 @@ } }, "node_modules/esbuild-linux-arm": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.42.tgz", - "integrity": "sha512-STq69yzCMhdRaWnh29UYrLSr/qaWMm/KqwaRF1pMEK7kDiagaXhSL1zQGXbYv94GuGY/zAwzK98+6idCMUOOCg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.51.tgz", + "integrity": "sha512-LsJynDxYF6Neg7ZC7748yweCDD+N8ByCv22/7IAZglIEniEkqdF4HCaa49JNDLw1UQGlYuhOB8ZT/MmcSWzcWg==", "cpu": [ "arm" ], @@ -524,9 +648,9 @@ } }, "node_modules/esbuild-linux-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.42.tgz", - "integrity": "sha512-c3Ug3e9JpVr8jAcfbhirtpBauLxzYPpycjWulD71CF6ZSY26tvzmXMJYooQ2YKqDY4e/fPu5K8bm7MiXMnyxuA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.51.tgz", + "integrity": "sha512-D9rFxGutoqQX3xJPxqd6o+kvYKeIbM0ifW2y0bgKk5HPgQQOo2k9/2Vpto3ybGYaFPCE5qTGtqQta9PoP6ZEzw==", "cpu": [ "arm64" ], @@ -540,9 +664,9 @@ } }, "node_modules/esbuild-linux-mips64le": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.42.tgz", - "integrity": "sha512-QuvpHGbYlkyXWf2cGm51LBCHx6eUakjaSrRpUqhPwjh/uvNUYvLmz2LgPTTPwCqaKt0iwL+OGVL0tXA5aDbAbg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.51.tgz", + "integrity": "sha512-vS54wQjy4IinLSlb5EIlLoln8buh1yDgliP4CuEHumrPk4PvvP4kTRIG4SzMXm6t19N0rIfT4bNdAxzJLg2k6A==", "cpu": [ "mips64el" ], @@ -556,9 +680,9 @@ } }, "node_modules/esbuild-linux-ppc64le": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.42.tgz", - "integrity": "sha512-8ohIVIWDbDT+i7lCx44YCyIRrOW1MYlks9fxTo0ME2LS/fxxdoJBwHWzaDYhjvf8kNpA+MInZvyOEAGoVDrMHg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.51.tgz", + "integrity": "sha512-xcdd62Y3VfGoyphNP/aIV9LP+RzFw5M5Z7ja+zdpQHHvokJM7d0rlDRMN+iSSwvUymQkqZO+G/xjb4/75du8BQ==", "cpu": [ "ppc64" ], @@ -572,9 +696,9 @@ } }, "node_modules/esbuild-linux-riscv64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.42.tgz", - "integrity": "sha512-DzDqK3TuoXktPyG1Lwx7vhaF49Onv3eR61KwQyxYo4y5UKTpL3NmuarHSIaSVlTFDDpcIajCDwz5/uwKLLgKiQ==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.51.tgz", + "integrity": "sha512-syXHGak9wkAnFz0gMmRBoy44JV0rp4kVCEA36P5MCeZcxFq8+fllBC2t6sKI23w3qd8Vwo9pTADCgjTSf3L3rA==", "cpu": [ "riscv64" ], @@ -588,9 +712,9 @@ } }, "node_modules/esbuild-linux-s390x": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.42.tgz", - "integrity": "sha512-YFRhPCxl8nb//Wn6SiS5pmtplBi4z9yC2gLrYoYI/tvwuB1jldir9r7JwAGy1Ck4D7sE7wBN9GFtUUX/DLdcEQ==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.51.tgz", + "integrity": "sha512-kFAJY3dv+Wq8o28K/C7xkZk/X34rgTwhknSsElIqoEo8armCOjMJ6NsMxm48KaWY2h2RUYGtQmr+RGuUPKBhyw==", "cpu": [ "s390x" ], @@ -604,9 +728,9 @@ } }, "node_modules/esbuild-netbsd-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.42.tgz", - "integrity": "sha512-QYSD2k+oT9dqB/4eEM9c+7KyNYsIPgzYOSrmfNGDIyJrbT1d+CFVKvnKahDKNJLfOYj8N4MgyFaU9/Ytc6w5Vw==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.51.tgz", + "integrity": "sha512-ZZBI7qrR1FevdPBVHz/1GSk1x5GDL/iy42Zy8+neEm/HA7ma+hH/bwPEjeHXKWUDvM36CZpSL/fn1/y9/Hb+1A==", "cpu": [ "x64" ], @@ -620,9 +744,9 @@ } }, "node_modules/esbuild-openbsd-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.42.tgz", - "integrity": "sha512-M2meNVIKWsm2HMY7+TU9AxM7ZVwI9havdsw6m/6EzdXysyCFFSoaTQ/Jg03izjCsK17FsVRHqRe26Llj6x0MNA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.51.tgz", + "integrity": "sha512-7R1/p39M+LSVQVgDVlcY1KKm6kFKjERSX1lipMG51NPcspJD1tmiZSmmBXoY5jhHIu6JL1QkFDTx94gMYK6vfA==", "cpu": [ "x64" ], @@ -636,9 +760,9 @@ } }, "node_modules/esbuild-sunos-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.42.tgz", - "integrity": "sha512-uXV8TAZEw36DkgW8Ak3MpSJs1ofBb3Smkc/6pZ29sCAN1KzCAQzsje4sUwugf+FVicrHvlamCOlFZIXgct+iqQ==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.51.tgz", + "integrity": "sha512-HoHaCswHxLEYN8eBTtyO0bFEWvA3Kdb++hSQ/lLG7TyKF69TeSG0RNoBRAs45x/oCeWaTDntEZlYwAfQlhEtJA==", "cpu": [ "x64" ], @@ -652,9 +776,9 @@ } }, "node_modules/esbuild-windows-32": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.42.tgz", - "integrity": "sha512-4iw/8qWmRICWi9ZOnJJf9sYt6wmtp3hsN4TdI5NqgjfOkBVMxNdM9Vt3626G1Rda9ya2Q0hjQRD9W1o+m6Lz6g==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.51.tgz", + "integrity": "sha512-4rtwSAM35A07CBt1/X8RWieDj3ZUHQqUOaEo5ZBs69rt5WAFjP4aqCIobdqOy4FdhYw1yF8Z0xFBTyc9lgPtEg==", "cpu": [ "ia32" ], @@ -668,9 +792,9 @@ } }, "node_modules/esbuild-windows-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.42.tgz", - "integrity": "sha512-j3cdK+Y3+a5H0wHKmLGTJcq0+/2mMBHPWkItR3vytp/aUGD/ua/t2BLdfBIzbNN9nLCRL9sywCRpOpFMx3CxzA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.51.tgz", + "integrity": "sha512-HoN/5HGRXJpWODprGCgKbdMvrC3A2gqvzewu2eECRw2sYxOUoh2TV1tS+G7bHNapPGI79woQJGV6pFH7GH7qnA==", "cpu": [ "x64" ], @@ -684,9 +808,9 @@ } }, "node_modules/esbuild-windows-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.42.tgz", - "integrity": "sha512-+lRAARnF+hf8J0mN27ujO+VbhPbDqJ8rCcJKye4y7YZLV6C4n3pTRThAb388k/zqF5uM0lS5O201u0OqoWSicw==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.51.tgz", + "integrity": "sha512-JQDqPjuOH7o+BsKMSddMfmVJXrnYZxXDHsoLHc0xgmAZkOOCflRmC43q31pk79F9xuyWY45jDBPolb5ZgGOf9g==", "cpu": [ "arm64" ], @@ -1524,9 +1648,9 @@ } }, "node_modules/sass": { - "version": "1.52.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.52.1.tgz", - "integrity": "sha512-fSzYTbr7z8oQnVJ3Acp9hV80dM1fkMN7mSD/25mpcct9F7FPBMOI8krEYALgU1aZoqGhQNhTPsuSmxjnIvAm4Q==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.54.0.tgz", + "integrity": "sha512-C4zp79GCXZfK0yoHZg+GxF818/aclhp9F48XBu/+bm9vXEVAYov9iU3FBVRMq3Hx3OA4jfKL+p2K9180mEh0xQ==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -1602,9 +1726,9 @@ } }, "node_modules/snabbdom": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.0.tgz", - "integrity": "sha512-Ff5BKG18KrrPuskHJlA9aujPHqEabItaDl96l7ZZndF4zt5AYSczz7ZjjgQAX5IBd5cd25lw9NfgX21yVUJ+9g==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.1.tgz", + "integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==", "engines": { "node": ">=8.3.0" } @@ -1733,6 +1857,11 @@ "node": ">=4" } }, + "node_modules/style-mod": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", + "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1792,6 +1921,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/w3c-keyname": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.5.tgz", + "integrity": "sha512-WJrK7i6w+ULuZsGscCezbCH4Aev5U3xY87vnSimzzEgPQhb0Sa0a1rE3c2jtEwrFtSfi61Jefw3jI5/DD/3jbQ==" + }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -1897,6 +2031,105 @@ } }, "dependencies": { + "@codemirror/autocomplete": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.1.0.tgz", + "integrity": "sha512-wtO4O5WDyXhhCd4q4utDIDZxnQfmJ++3dGBCG9LMtI79+92OcA1DVk/n7BEupKmjIr8AzvptDz7YQ9ud6OkU+A==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "@codemirror/commands": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.0.1.tgz", + "integrity": "sha512-iNHDByicYqQjs0Wo1MKGfqNbMYMyhS9WV6EwMVwsHXImlFemgEUC+c5X22bXKBStN3qnwg4fArNZM+gkv22baQ==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "@codemirror/language": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.2.1.tgz", + "integrity": "sha512-MC3svxuvIj0MRpFlGHxLS6vPyIdbTr2KKPEW46kCoCXw2ktb4NTkpkPBI/lSP/FoNXLCBJ0mrnUi1OoZxtpW1Q==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "@codemirror/legacy-modes": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.1.0.tgz", + "integrity": "sha512-V/PgGpndkZeTn3Hdlg/gd8MLFdyvTCIX+iwJzjUw5iNziWiNsAY8X0jvf7m3gSfxnKkNzmid6l0g4rYSpiDaCw==", + "requires": { + "@codemirror/language": "^6.0.0" + } + }, + "@codemirror/lint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.0.0.tgz", + "integrity": "sha512-nUUXcJW1Xp54kNs+a1ToPLK8MadO0rMTnJB8Zk4Z8gBdrN0kqV7uvUraU/T2yqg+grDNR38Vmy/MrhQN/RgwiA==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/search": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.0.1.tgz", + "integrity": "sha512-uOinkOrM+daMduCgMPomDfKLr7drGHB4jHl3Vq6xY2WRlL7MkNsBE0b+XHYa/Mee2npsJOgwvkW4n1lMFeBW2Q==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/state": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.1.0.tgz", + "integrity": "sha512-qbUr94DZTe6/V1VS7LDLz11rM/1t/nJxR1El4I6UaxDEdc0aZZvq6JCLJWiRmUf95NRAnDH6fhXn+PWp9wGCIg==" + }, + "@codemirror/view": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.1.2.tgz", + "integrity": "sha512-puUydfKwfmOo+ixtuB+uN/ZpcteEYSnpjHmMaow1sOQhNICsKtGBup3i9ybVqyzDagARRYzSHTWjbdeHqmn31w==", + "requires": { + "@codemirror/state": "^6.0.0", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, + "@lezer/common": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz", + "integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==" + }, + "@lezer/highlight": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.0.0.tgz", + "integrity": "sha512-nsCnNtim90UKsB5YxoX65v3GEIw3iCHw9RM2DtdgkiqAbKh9pCdvi8AWNwkYf10Lu6fxNhXPpkpHbW6mihhvJA==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, + "@lezer/lr": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.0.tgz", + "integrity": "sha512-TgEpfm9br2SX8JwtwKT8HsQZKuFkLRg6g+IRxObk9nVKQLKnkP3oMh+QGcTBL9GQsfQ2ADtKPbj2iGSMf3ytiA==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, "ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", @@ -2035,9 +2268,18 @@ } }, "codemirror": { - "version": "5.65.5", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.5.tgz", - "integrity": "sha512-HNyhvGLnYz5c+kIsB9QKVitiZUevha3ovbIYaQiGzKo7ECSL/elWD9RXt3JgNr0NdnyqE9/Rc/7uLfkJQL638w==" + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } }, "color-convert": { "version": "1.9.3", @@ -2060,6 +2302,11 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "crelt": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz", + "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==" + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -2158,170 +2405,170 @@ } }, "esbuild": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.42.tgz", - "integrity": "sha512-V0uPZotCEHokJdNqyozH6qsaQXqmZEOiZWrXnds/zaH/0SyrIayRXWRB98CENO73MIZ9T3HBIOsmds5twWtmgw==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.51.tgz", + "integrity": "sha512-+CvnDitD7Q5sT7F+FM65sWkF8wJRf+j9fPcprxYV4j+ohmzVj2W7caUqH2s5kCaCJAfcAICjSlKhDCcvDpU7nw==", "dev": true, "requires": { - "esbuild-android-64": "0.14.42", - "esbuild-android-arm64": "0.14.42", - "esbuild-darwin-64": "0.14.42", - "esbuild-darwin-arm64": "0.14.42", - "esbuild-freebsd-64": "0.14.42", - "esbuild-freebsd-arm64": "0.14.42", - "esbuild-linux-32": "0.14.42", - "esbuild-linux-64": "0.14.42", - "esbuild-linux-arm": "0.14.42", - "esbuild-linux-arm64": "0.14.42", - "esbuild-linux-mips64le": "0.14.42", - "esbuild-linux-ppc64le": "0.14.42", - "esbuild-linux-riscv64": "0.14.42", - "esbuild-linux-s390x": "0.14.42", - "esbuild-netbsd-64": "0.14.42", - "esbuild-openbsd-64": "0.14.42", - "esbuild-sunos-64": "0.14.42", - "esbuild-windows-32": "0.14.42", - "esbuild-windows-64": "0.14.42", - "esbuild-windows-arm64": "0.14.42" + "esbuild-android-64": "0.14.51", + "esbuild-android-arm64": "0.14.51", + "esbuild-darwin-64": "0.14.51", + "esbuild-darwin-arm64": "0.14.51", + "esbuild-freebsd-64": "0.14.51", + "esbuild-freebsd-arm64": "0.14.51", + "esbuild-linux-32": "0.14.51", + "esbuild-linux-64": "0.14.51", + "esbuild-linux-arm": "0.14.51", + "esbuild-linux-arm64": "0.14.51", + "esbuild-linux-mips64le": "0.14.51", + "esbuild-linux-ppc64le": "0.14.51", + "esbuild-linux-riscv64": "0.14.51", + "esbuild-linux-s390x": "0.14.51", + "esbuild-netbsd-64": "0.14.51", + "esbuild-openbsd-64": "0.14.51", + "esbuild-sunos-64": "0.14.51", + "esbuild-windows-32": "0.14.51", + "esbuild-windows-64": "0.14.51", + "esbuild-windows-arm64": "0.14.51" } }, "esbuild-android-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.42.tgz", - "integrity": "sha512-P4Y36VUtRhK/zivqGVMqhptSrFILAGlYp0Z8r9UQqHJ3iWztRCNWnlBzD9HRx0DbueXikzOiwyOri+ojAFfW6A==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.51.tgz", + "integrity": "sha512-6FOuKTHnC86dtrKDmdSj2CkcKF8PnqkaIXqvgydqfJmqBazCPdw+relrMlhGjkvVdiiGV70rpdnyFmA65ekBCQ==", "dev": true, "optional": true }, "esbuild-android-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.42.tgz", - "integrity": "sha512-0cOqCubq+RWScPqvtQdjXG3Czb3AWI2CaKw3HeXry2eoA2rrPr85HF7IpdU26UWdBXgPYtlTN1LUiuXbboROhg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.51.tgz", + "integrity": "sha512-vBtp//5VVkZWmYYvHsqBRCMMi1MzKuMIn5XDScmnykMTu9+TD9v0NMEDqQxvtFToeYmojdo5UCV2vzMQWJcJ4A==", "dev": true, "optional": true }, "esbuild-darwin-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.42.tgz", - "integrity": "sha512-ipiBdCA3ZjYgRfRLdQwP82rTiv/YVMtW36hTvAN5ZKAIfxBOyPXY7Cejp3bMXWgzKD8B6O+zoMzh01GZsCuEIA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.51.tgz", + "integrity": "sha512-YFmXPIOvuagDcwCejMRtCDjgPfnDu+bNeh5FU2Ryi68ADDVlWEpbtpAbrtf/lvFTWPexbgyKgzppNgsmLPr8PA==", "dev": true, "optional": true }, "esbuild-darwin-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.42.tgz", - "integrity": "sha512-bU2tHRqTPOaoH/4m0zYHbFWpiYDmaA0gt90/3BMEFaM0PqVK/a6MA2V/ypV5PO0v8QxN6gH5hBPY4YJ2lopXgA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.51.tgz", + "integrity": "sha512-juYD0QnSKwAMfzwKdIF6YbueXzS6N7y4GXPDeDkApz/1RzlT42mvX9jgNmyOlWKN7YzQAYbcUEJmZJYQGdf2ow==", "dev": true, "optional": true }, "esbuild-freebsd-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.42.tgz", - "integrity": "sha512-75h1+22Ivy07+QvxHyhVqOdekupiTZVLN1PMwCDonAqyXd8TVNJfIRFrdL8QmSJrOJJ5h8H1I9ETyl2L8LQDaw==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.51.tgz", + "integrity": "sha512-cLEI/aXjb6vo5O2Y8rvVSQ7smgLldwYY5xMxqh/dQGfWO+R1NJOFsiax3IS4Ng300SVp7Gz3czxT6d6qf2cw0g==", "dev": true, "optional": true }, "esbuild-freebsd-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.42.tgz", - "integrity": "sha512-W6Jebeu5TTDQMJUJVarEzRU9LlKpNkPBbjqSu+GUPTHDCly5zZEQq9uHkmHHl7OKm+mQ2zFySN83nmfCeZCyNA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.51.tgz", + "integrity": "sha512-TcWVw/rCL2F+jUgRkgLa3qltd5gzKjIMGhkVybkjk6PJadYInPtgtUBp1/hG+mxyigaT7ib+od1Xb84b+L+1Mg==", "dev": true, "optional": true }, "esbuild-linux-32": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.42.tgz", - "integrity": "sha512-Ooy/Bj+mJ1z4jlWcK5Dl6SlPlCgQB9zg1UrTCeY8XagvuWZ4qGPyYEWGkT94HUsRi2hKsXvcs6ThTOjBaJSMfg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.51.tgz", + "integrity": "sha512-RFqpyC5ChyWrjx8Xj2K0EC1aN0A37H6OJfmUXIASEqJoHcntuV3j2Efr9RNmUhMfNE6yEj2VpYuDteZLGDMr0w==", "dev": true, "optional": true }, "esbuild-linux-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.42.tgz", - "integrity": "sha512-2L0HbzQfbTuemUWfVqNIjOfaTRt9zsvjnme6lnr7/MO9toz/MJ5tZhjqrG6uDWDxhsaHI2/nsDgrv8uEEN2eoA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.51.tgz", + "integrity": "sha512-dxjhrqo5i7Rq6DXwz5v+MEHVs9VNFItJmHBe1CxROWNf4miOGoQhqSG8StStbDkQ1Mtobg6ng+4fwByOhoQoeA==", "dev": true, "optional": true }, "esbuild-linux-arm": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.42.tgz", - "integrity": "sha512-STq69yzCMhdRaWnh29UYrLSr/qaWMm/KqwaRF1pMEK7kDiagaXhSL1zQGXbYv94GuGY/zAwzK98+6idCMUOOCg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.51.tgz", + "integrity": "sha512-LsJynDxYF6Neg7ZC7748yweCDD+N8ByCv22/7IAZglIEniEkqdF4HCaa49JNDLw1UQGlYuhOB8ZT/MmcSWzcWg==", "dev": true, "optional": true }, "esbuild-linux-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.42.tgz", - "integrity": "sha512-c3Ug3e9JpVr8jAcfbhirtpBauLxzYPpycjWulD71CF6ZSY26tvzmXMJYooQ2YKqDY4e/fPu5K8bm7MiXMnyxuA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.51.tgz", + "integrity": "sha512-D9rFxGutoqQX3xJPxqd6o+kvYKeIbM0ifW2y0bgKk5HPgQQOo2k9/2Vpto3ybGYaFPCE5qTGtqQta9PoP6ZEzw==", "dev": true, "optional": true }, "esbuild-linux-mips64le": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.42.tgz", - "integrity": "sha512-QuvpHGbYlkyXWf2cGm51LBCHx6eUakjaSrRpUqhPwjh/uvNUYvLmz2LgPTTPwCqaKt0iwL+OGVL0tXA5aDbAbg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.51.tgz", + "integrity": "sha512-vS54wQjy4IinLSlb5EIlLoln8buh1yDgliP4CuEHumrPk4PvvP4kTRIG4SzMXm6t19N0rIfT4bNdAxzJLg2k6A==", "dev": true, "optional": true }, "esbuild-linux-ppc64le": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.42.tgz", - "integrity": "sha512-8ohIVIWDbDT+i7lCx44YCyIRrOW1MYlks9fxTo0ME2LS/fxxdoJBwHWzaDYhjvf8kNpA+MInZvyOEAGoVDrMHg==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.51.tgz", + "integrity": "sha512-xcdd62Y3VfGoyphNP/aIV9LP+RzFw5M5Z7ja+zdpQHHvokJM7d0rlDRMN+iSSwvUymQkqZO+G/xjb4/75du8BQ==", "dev": true, "optional": true }, "esbuild-linux-riscv64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.42.tgz", - "integrity": "sha512-DzDqK3TuoXktPyG1Lwx7vhaF49Onv3eR61KwQyxYo4y5UKTpL3NmuarHSIaSVlTFDDpcIajCDwz5/uwKLLgKiQ==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.51.tgz", + "integrity": "sha512-syXHGak9wkAnFz0gMmRBoy44JV0rp4kVCEA36P5MCeZcxFq8+fllBC2t6sKI23w3qd8Vwo9pTADCgjTSf3L3rA==", "dev": true, "optional": true }, "esbuild-linux-s390x": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.42.tgz", - "integrity": "sha512-YFRhPCxl8nb//Wn6SiS5pmtplBi4z9yC2gLrYoYI/tvwuB1jldir9r7JwAGy1Ck4D7sE7wBN9GFtUUX/DLdcEQ==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.51.tgz", + "integrity": "sha512-kFAJY3dv+Wq8o28K/C7xkZk/X34rgTwhknSsElIqoEo8armCOjMJ6NsMxm48KaWY2h2RUYGtQmr+RGuUPKBhyw==", "dev": true, "optional": true }, "esbuild-netbsd-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.42.tgz", - "integrity": "sha512-QYSD2k+oT9dqB/4eEM9c+7KyNYsIPgzYOSrmfNGDIyJrbT1d+CFVKvnKahDKNJLfOYj8N4MgyFaU9/Ytc6w5Vw==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.51.tgz", + "integrity": "sha512-ZZBI7qrR1FevdPBVHz/1GSk1x5GDL/iy42Zy8+neEm/HA7ma+hH/bwPEjeHXKWUDvM36CZpSL/fn1/y9/Hb+1A==", "dev": true, "optional": true }, "esbuild-openbsd-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.42.tgz", - "integrity": "sha512-M2meNVIKWsm2HMY7+TU9AxM7ZVwI9havdsw6m/6EzdXysyCFFSoaTQ/Jg03izjCsK17FsVRHqRe26Llj6x0MNA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.51.tgz", + "integrity": "sha512-7R1/p39M+LSVQVgDVlcY1KKm6kFKjERSX1lipMG51NPcspJD1tmiZSmmBXoY5jhHIu6JL1QkFDTx94gMYK6vfA==", "dev": true, "optional": true }, "esbuild-sunos-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.42.tgz", - "integrity": "sha512-uXV8TAZEw36DkgW8Ak3MpSJs1ofBb3Smkc/6pZ29sCAN1KzCAQzsje4sUwugf+FVicrHvlamCOlFZIXgct+iqQ==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.51.tgz", + "integrity": "sha512-HoHaCswHxLEYN8eBTtyO0bFEWvA3Kdb++hSQ/lLG7TyKF69TeSG0RNoBRAs45x/oCeWaTDntEZlYwAfQlhEtJA==", "dev": true, "optional": true }, "esbuild-windows-32": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.42.tgz", - "integrity": "sha512-4iw/8qWmRICWi9ZOnJJf9sYt6wmtp3hsN4TdI5NqgjfOkBVMxNdM9Vt3626G1Rda9ya2Q0hjQRD9W1o+m6Lz6g==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.51.tgz", + "integrity": "sha512-4rtwSAM35A07CBt1/X8RWieDj3ZUHQqUOaEo5ZBs69rt5WAFjP4aqCIobdqOy4FdhYw1yF8Z0xFBTyc9lgPtEg==", "dev": true, "optional": true }, "esbuild-windows-64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.42.tgz", - "integrity": "sha512-j3cdK+Y3+a5H0wHKmLGTJcq0+/2mMBHPWkItR3vytp/aUGD/ua/t2BLdfBIzbNN9nLCRL9sywCRpOpFMx3CxzA==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.51.tgz", + "integrity": "sha512-HoN/5HGRXJpWODprGCgKbdMvrC3A2gqvzewu2eECRw2sYxOUoh2TV1tS+G7bHNapPGI79woQJGV6pFH7GH7qnA==", "dev": true, "optional": true }, "esbuild-windows-arm64": { - "version": "0.14.42", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.42.tgz", - "integrity": "sha512-+lRAARnF+hf8J0mN27ujO+VbhPbDqJ8rCcJKye4y7YZLV6C4n3pTRThAb388k/zqF5uM0lS5O201u0OqoWSicw==", + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.51.tgz", + "integrity": "sha512-JQDqPjuOH7o+BsKMSddMfmVJXrnYZxXDHsoLHc0xgmAZkOOCflRmC43q31pk79F9xuyWY45jDBPolb5ZgGOf9g==", "dev": true, "optional": true }, @@ -2922,9 +3169,9 @@ } }, "sass": { - "version": "1.52.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.52.1.tgz", - "integrity": "sha512-fSzYTbr7z8oQnVJ3Acp9hV80dM1fkMN7mSD/25mpcct9F7FPBMOI8krEYALgU1aZoqGhQNhTPsuSmxjnIvAm4Q==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.54.0.tgz", + "integrity": "sha512-C4zp79GCXZfK0yoHZg+GxF818/aclhp9F48XBu/+bm9vXEVAYov9iU3FBVRMq3Hx3OA4jfKL+p2K9180mEh0xQ==", "dev": true, "requires": { "chokidar": ">=3.0.0 <4.0.0", @@ -2982,9 +3229,9 @@ } }, "snabbdom": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.0.tgz", - "integrity": "sha512-Ff5BKG18KrrPuskHJlA9aujPHqEabItaDl96l7ZZndF4zt5AYSczz7ZjjgQAX5IBd5cd25lw9NfgX21yVUJ+9g==" + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.1.tgz", + "integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==" }, "sortablejs": { "version": "1.15.0", @@ -3086,6 +3333,11 @@ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "dev": true }, + "style-mod": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", + "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==" + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -3136,6 +3388,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "w3c-keyname": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.5.tgz", + "integrity": "sha512-WJrK7i6w+ULuZsGscCezbCH4Aev5U3xY87vnSimzzEgPQhb0Sa0a1rE3c2jtEwrFtSfi61Jefw3jI5/DD/3jbQ==" + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index 9a2f66448..8407ffc77 100644 --- a/package.json +++ b/package.json @@ -16,19 +16,24 @@ }, "devDependencies": { "chokidar-cli": "^3.0", - "esbuild": "0.14.42", + "esbuild": "0.14.51", "livereload": "^0.9.3", "npm-run-all": "^4.1.5", "punycode": "^2.1.1", - "sass": "^1.52.1" + "sass": "^1.54.0" }, "dependencies": { + "@codemirror/commands": "^6.0.1", + "@codemirror/language": "^6.2.1", + "@codemirror/legacy-modes": "^6.1.0", + "@codemirror/state": "^6.1.0", + "@codemirror/view": "^6.1.2", "clipboard": "^2.0.11", - "codemirror": "^5.65.5", + "codemirror": "^6.0.1", "dropzone": "^5.9.3", "markdown-it": "^13.0.1", "markdown-it-task-lists": "^2.1.1", - "snabbdom": "^3.5.0", + "snabbdom": "^3.5.1", "sortablejs": "^1.15.0" } } diff --git a/resources/js/code.mjs b/resources/js/code/index.mjs similarity index 55% rename from resources/js/code.mjs rename to resources/js/code/index.mjs index eca941f1c..ff60cbff5 100644 --- a/resources/js/code.mjs +++ b/resources/js/code/index.mjs @@ -1,106 +1,10 @@ -import CodeMirror from "codemirror"; +import {EditorView} from "@codemirror/view" +// import {EditorState} from "@codemirror/state" import Clipboard from "clipboard/dist/clipboard.min"; // Modes -import 'codemirror/mode/css/css'; -import 'codemirror/mode/clike/clike'; -import 'codemirror/mode/diff/diff'; -import 'codemirror/mode/fortran/fortran'; -import 'codemirror/mode/go/go'; -import 'codemirror/mode/haskell/haskell'; -import 'codemirror/mode/htmlmixed/htmlmixed'; -import 'codemirror/mode/javascript/javascript'; -import 'codemirror/mode/julia/julia'; -import 'codemirror/mode/lua/lua'; -import 'codemirror/mode/markdown/markdown'; -import 'codemirror/mode/mllike/mllike'; -import 'codemirror/mode/nginx/nginx'; -import 'codemirror/mode/perl/perl'; -import 'codemirror/mode/pascal/pascal'; -import 'codemirror/mode/php/php'; -import 'codemirror/mode/powershell/powershell'; -import 'codemirror/mode/properties/properties'; -import 'codemirror/mode/python/python'; -import 'codemirror/mode/ruby/ruby'; -import 'codemirror/mode/rust/rust'; -import 'codemirror/mode/shell/shell'; -import 'codemirror/mode/sql/sql'; -import 'codemirror/mode/stex/stex'; -import 'codemirror/mode/toml/toml'; -import 'codemirror/mode/vb/vb'; -import 'codemirror/mode/vbscript/vbscript'; -import 'codemirror/mode/xml/xml'; -import 'codemirror/mode/yaml/yaml'; - -// Addons -import 'codemirror/addon/scroll/scrollpastend'; - -// Mapping of possible languages or formats from user input to their codemirror modes. -// Value can be a mode string or a function that will receive the code content & return the mode string. -// The function option is used in the event the exact mode could be dynamic depending on the code. -const modeMap = { - bash: 'shell', - css: 'css', - c: 'text/x-csrc', - java: 'text/x-java', - scala: 'text/x-scala', - kotlin: 'text/x-kotlin', - 'c++': 'text/x-c++src', - 'c#': 'text/x-csharp', - csharp: 'text/x-csharp', - diff: 'diff', - for: 'fortran', - fortran: 'fortran', - 'f#': 'text/x-fsharp', - fsharp: 'text/x-fsharp', - go: 'go', - haskell: 'haskell', - hs: 'haskell', - html: 'htmlmixed', - ini: 'properties', - javascript: 'text/javascript', - json: 'application/json', - js: 'text/javascript', - jl: 'text/x-julia', - julia: 'text/x-julia', - latex: 'text/x-stex', - lua: 'lua', - md: 'markdown', - mdown: 'markdown', - markdown: 'markdown', - ml: 'mllike', - nginx: 'nginx', - perl: 'perl', - pl: 'perl', - powershell: 'powershell', - properties: 'properties', - ocaml: 'text/x-ocaml', - pascal: 'text/x-pascal', - pas: 'text/x-pascal', - php: (content) => { - return content.includes('`; - const copyButton = document.createElement('div'); - copyButton.classList.add('CodeMirror-copy'); - copyButton.innerHTML = copyIcon; - cmInstance.display.wrapper.appendChild(copyButton); - - const clipboard = new Clipboard(copyButton, { - text: function(trigger) { - return cmInstance.getValue() - } - }); - - clipboard.on('success', event => { - copyButton.classList.add('success'); - setTimeout(() => { - copyButton.classList.remove('success'); - }, 240); - }); + // TODO + // const copyIcon = ``; + // const copyButton = document.createElement('div'); + // copyButton.classList.add('CodeMirror-copy'); + // copyButton.innerHTML = copyIcon; + // cmInstance.display.wrapper.appendChild(copyButton); + // + // const clipboard = new Clipboard(copyButton, { + // text: function(trigger) { + // return cmInstance.getValue() + // } + // }); + // + // clipboard.on('success', event => { + // copyButton.classList.add('success'); + // setTimeout(() => { + // copyButton.classList.remove('success'); + // }, 240); + // }); } /** diff --git a/resources/js/code/modes.js b/resources/js/code/modes.js new file mode 100644 index 000000000..5a89255a5 --- /dev/null +++ b/resources/js/code/modes.js @@ -0,0 +1,134 @@ +import {StreamLanguage} from "@codemirror/language" + +import {css as langCss} from '@codemirror/legacy-modes/mode/css'; +import {clike as langClike} from '@codemirror/legacy-modes/mode/clike'; +import {diff as langDiff} from '@codemirror/legacy-modes/mode/diff'; +import {fortran as langFortran} from '@codemirror/legacy-modes/mode/fortran'; +import {go as langGo} from '@codemirror/legacy-modes/mode/go'; +import {haskell as langHaskell} from '@codemirror/legacy-modes/mode/haskell'; +// import {htmlmixed as langHtmlmixed} from '@codemirror/legacy-modes/mode/htmlmixed'; +import {javascript as langJavascript} from '@codemirror/legacy-modes/mode/javascript'; +import {julia as langJulia} from '@codemirror/legacy-modes/mode/julia'; +import {lua as langLua} from '@codemirror/legacy-modes/mode/lua'; +// import {markdown as langMarkdown} from '@codemirror/legacy-modes/mode/markdown'; +import {oCaml as langMllike} from '@codemirror/legacy-modes/mode/mllike'; +import {nginx as langNginx} from '@codemirror/legacy-modes/mode/nginx'; +import {perl as langPerl} from '@codemirror/legacy-modes/mode/perl'; +import {pascal as langPascal} from '@codemirror/legacy-modes/mode/pascal'; +// import {php as langPhp} from '@codemirror/legacy-modes/mode/php'; +import {powerShell as langPowershell} from '@codemirror/legacy-modes/mode/powershell'; +import {properties as langProperties} from '@codemirror/legacy-modes/mode/properties'; +import {python as langPython} from '@codemirror/legacy-modes/mode/python'; +import {ruby as langRuby} from '@codemirror/legacy-modes/mode/ruby'; +import {rust as langRust} from '@codemirror/legacy-modes/mode/rust'; +import {shell as langShell} from '@codemirror/legacy-modes/mode/shell'; +import {sql as langSql} from '@codemirror/legacy-modes/mode/sql'; +import {stex as langStex} from '@codemirror/legacy-modes/mode/stex'; +import {toml as langToml} from '@codemirror/legacy-modes/mode/toml'; +import {vb as langVb} from '@codemirror/legacy-modes/mode/vb'; +import {vbScript as langVbscript} from '@codemirror/legacy-modes/mode/vbscript'; +import {xml as langXml} from '@codemirror/legacy-modes/mode/xml'; +import {yaml as langYaml} from '@codemirror/legacy-modes/mode/yaml'; + +export const modes = [ + langCss, + langClike, + langDiff, + langFortran, + langGo, + langHaskell, + // langHtmlmixed, + langJavascript, + langJulia, + langLua, + // langMarkdown, + langMllike, + langNginx, + langPerl, + langPascal, + // langPhp, + langPowershell, + langProperties, + langPython, + langRuby, + langRust, + langShell, + langSql, + langStex, + langToml, + langVb, + langVbscript, + langXml, + langYaml, +]; + +// Mapping of possible languages or formats from user input to their codemirror modes. +// Value can be a mode string or a function that will receive the code content & return the mode string. +// The function option is used in the event the exact mode could be dynamic depending on the code. +export const modeMap = { + bash: 'shell', + css: 'css', + c: 'text/x-csrc', + java: 'text/x-java', + scala: 'text/x-scala', + kotlin: 'text/x-kotlin', + 'c++': 'text/x-c++src', + 'c#': 'text/x-csharp', + csharp: 'text/x-csharp', + diff: 'diff', + for: 'fortran', + fortran: 'fortran', + 'f#': 'text/x-fsharp', + fsharp: 'text/x-fsharp', + go: 'go', + haskell: 'haskell', + hs: 'haskell', + html: 'htmlmixed', + ini: 'properties', + javascript: 'text/javascript', + json: 'application/json', + js: 'text/javascript', + jl: 'text/x-julia', + julia: 'text/x-julia', + latex: 'text/x-stex', + lua: 'lua', + md: 'markdown', + mdown: 'markdown', + markdown: 'markdown', + ml: 'mllike', + nginx: 'nginx', + perl: 'perl', + pl: 'perl', + powershell: 'powershell', + properties: 'properties', + ocaml: 'text/x-ocaml', + pascal: 'text/x-pascal', + pas: 'text/x-pascal', + php: (content) => { + return content.includes(' StreamLanguage.define(mode)); +} \ No newline at end of file diff --git a/resources/js/code/setups.js b/resources/js/code/setups.js new file mode 100644 index 000000000..768d3a35d --- /dev/null +++ b/resources/js/code/setups.js @@ -0,0 +1,32 @@ + +import {keymap, highlightSpecialChars, drawSelection, highlightActiveLine, dropCursor, + rectangularSelection, lineNumbers, highlightActiveLineGutter} from "@codemirror/view" +import {defaultHighlightStyle, syntaxHighlighting, bracketMatching, + foldKeymap} from "@codemirror/language" +import {defaultKeymap, history, historyKeymap} from "@codemirror/commands" +import {EditorState} from "@codemirror/state" + +import {modesAsStreamLanguages} from "./modes"; + + +export function viewer() { + return [ + lineNumbers(), + highlightActiveLineGutter(), + highlightSpecialChars(), + history(), + drawSelection(), + dropCursor(), + syntaxHighlighting(defaultHighlightStyle, {fallback: true}), + bracketMatching(), + rectangularSelection(), + highlightActiveLine(), + keymap.of([ + ...defaultKeymap, + ...historyKeymap, + ...foldKeymap, + ]), + EditorState.readOnly.of(true), + ...modesAsStreamLanguages(), + ]; +} \ No newline at end of file From 97146a63590fa193a4958c09202b40340e840a1f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 3 Aug 2022 19:40:16 +0100 Subject: [PATCH 002/477] Added handling of codemirror 6 code languages --- package-lock.json | 261 +++++++++++++++++++++++++++++++++ package.json | 5 + resources/js/code/index.mjs | 49 ++----- resources/js/code/languages.js | 120 +++++++++++++++ resources/js/code/modes.js | 134 ----------------- resources/js/code/setups.js | 4 - resources/js/code/views.js | 38 +++++ 7 files changed, 437 insertions(+), 174 deletions(-) create mode 100644 resources/js/code/languages.js delete mode 100644 resources/js/code/modes.js create mode 100644 resources/js/code/views.js diff --git a/package-lock.json b/package-lock.json index c141e654a..0eca777cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,11 @@ "": { "dependencies": { "@codemirror/commands": "^6.0.1", + "@codemirror/lang-html": "^6.1.0", + "@codemirror/lang-javascript": "^6.0.2", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-markdown": "^6.0.1", + "@codemirror/lang-php": "^6.0.0", "@codemirror/language": "^6.2.1", "@codemirror/legacy-modes": "^6.1.0", "@codemirror/state": "^6.1.0", @@ -55,6 +60,79 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@codemirror/lang-css": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.0.0.tgz", + "integrity": "sha512-jBqc+BTuwhNOTlrimFghLlSrN6iFuE44HULKWoR4qKYObhOIl9Lci1iYj6zMIte1XTQmZguNvjXMyr43LUKwSw==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/css": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.1.0.tgz", + "integrity": "sha512-gA7NmJxqvnhwza05CvR7W/39Ap9r/4Vs9uiC0IeFYo1hSlJzc/8N6Evviz6vTW1x8SpHcRYyqKOf6rpl6LfWtg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/html": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.0.2.tgz", + "integrity": "sha512-BZRJ9u/zl16hLkSpDAWm73mrfIR7HJrr0lvnhoSOCQVea5BglguWI/slxexhvUb0CB5cXgKWuo2bM+N9EhIaZw==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.0.tgz", + "integrity": "sha512-DvTcYTKLmg2viADXlTdufrT334M9jowe1qO02W28nvm+nejcvhM5vot5mE8/kPrxYw/HJHhwu1z2PyBpnMLCNQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.0.1.tgz", + "integrity": "sha512-pHPQuRwf9cUrmkmsTHRjtS9ZnGu3fA9YzAdh2++d+L9wbfnC2XbKh0Xvm/0YiUjdCnoCx9wDFEoCuAnkqKWLIw==", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-php": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.0.tgz", + "integrity": "sha512-96CEjq0xEgbzc6bdFPwILPfZ6m8917JRbh2oPszZJABlYxG4Y+eYjtYkUTDb4yuyjQKyigHoeGC6zoIOYA1NWA==", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/php": "^1.0.0" + } + }, "node_modules/@codemirror/language": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.2.1.tgz", @@ -116,6 +194,15 @@ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz", "integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==" }, + "node_modules/@lezer/css": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.0.0.tgz", + "integrity": "sha512-616VqgDKumHmYIuxs3tnX1irEQmoDHgF/TlP4O5ICWwyHwLMErq+8iKVuzTkOdBqvYAVmObqThcDEAaaMJjAdg==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lezer/highlight": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.0.0.tgz", @@ -124,6 +211,34 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@lezer/html": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.0.1.tgz", + "integrity": "sha512-sC00zEt3GBh3vVO6QaGX4YZCl41S9dHWN/WGBsDixy9G+sqOC7gsa4cxA/fmRVAiBvhqYkJk+5Ul4oul92CPVw==", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.0.2.tgz", + "integrity": "sha512-IjOVeIRhM8IuafWNnk+UzRz7p4/JSOKBNINLYLsdSGuJS9Ju7vFdc82AlTt0jgtV5D8eBZf4g0vK4d3ttBNz7A==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.0.tgz", + "integrity": "sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lezer/lr": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.0.tgz", @@ -132,6 +247,24 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@lezer/markdown": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.0.1.tgz", + "integrity": "sha512-LlpNWLqes3XQvd8TwpJTHf9ENl4fI6H32xQkMgltUITFMMdQpOASXQtDawWR03yS6hskh4bkhATQbgjdGMoUvA==", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@lezer/php": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.0.tgz", + "integrity": "sha512-kFQu/mk/vmjpA+fjQU87d9eimqKJ9PFCa8CZCPFWGEwNnm7Ahpw32N+HYEU/YAQ0XcfmOAnW/YJCEa8WpUOMMw==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", @@ -2053,6 +2186,79 @@ "@lezer/common": "^1.0.0" } }, + "@codemirror/lang-css": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.0.0.tgz", + "integrity": "sha512-jBqc+BTuwhNOTlrimFghLlSrN6iFuE44HULKWoR4qKYObhOIl9Lci1iYj6zMIte1XTQmZguNvjXMyr43LUKwSw==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/css": "^1.0.0" + } + }, + "@codemirror/lang-html": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.1.0.tgz", + "integrity": "sha512-gA7NmJxqvnhwza05CvR7W/39Ap9r/4Vs9uiC0IeFYo1hSlJzc/8N6Evviz6vTW1x8SpHcRYyqKOf6rpl6LfWtg==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/html": "^1.0.0" + } + }, + "@codemirror/lang-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.0.2.tgz", + "integrity": "sha512-BZRJ9u/zl16hLkSpDAWm73mrfIR7HJrr0lvnhoSOCQVea5BglguWI/slxexhvUb0CB5cXgKWuo2bM+N9EhIaZw==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "@codemirror/lang-json": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.0.tgz", + "integrity": "sha512-DvTcYTKLmg2viADXlTdufrT334M9jowe1qO02W28nvm+nejcvhM5vot5mE8/kPrxYw/HJHhwu1z2PyBpnMLCNQ==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "@codemirror/lang-markdown": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.0.1.tgz", + "integrity": "sha512-pHPQuRwf9cUrmkmsTHRjtS9ZnGu3fA9YzAdh2++d+L9wbfnC2XbKh0Xvm/0YiUjdCnoCx9wDFEoCuAnkqKWLIw==", + "requires": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/markdown": "^1.0.0" + } + }, + "@codemirror/lang-php": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.0.tgz", + "integrity": "sha512-96CEjq0xEgbzc6bdFPwILPfZ6m8917JRbh2oPszZJABlYxG4Y+eYjtYkUTDb4yuyjQKyigHoeGC6zoIOYA1NWA==", + "requires": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/php": "^1.0.0" + } + }, "@codemirror/language": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.2.1.tgz", @@ -2114,6 +2320,15 @@ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz", "integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==" }, + "@lezer/css": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.0.0.tgz", + "integrity": "sha512-616VqgDKumHmYIuxs3tnX1irEQmoDHgF/TlP4O5ICWwyHwLMErq+8iKVuzTkOdBqvYAVmObqThcDEAaaMJjAdg==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "@lezer/highlight": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.0.0.tgz", @@ -2122,6 +2337,34 @@ "@lezer/common": "^1.0.0" } }, + "@lezer/html": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.0.1.tgz", + "integrity": "sha512-sC00zEt3GBh3vVO6QaGX4YZCl41S9dHWN/WGBsDixy9G+sqOC7gsa4cxA/fmRVAiBvhqYkJk+5Ul4oul92CPVw==", + "requires": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/javascript": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.0.2.tgz", + "integrity": "sha512-IjOVeIRhM8IuafWNnk+UzRz7p4/JSOKBNINLYLsdSGuJS9Ju7vFdc82AlTt0jgtV5D8eBZf4g0vK4d3ttBNz7A==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.0.tgz", + "integrity": "sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "@lezer/lr": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.0.tgz", @@ -2130,6 +2373,24 @@ "@lezer/common": "^1.0.0" } }, + "@lezer/markdown": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.0.1.tgz", + "integrity": "sha512-LlpNWLqes3XQvd8TwpJTHf9ENl4fI6H32xQkMgltUITFMMdQpOASXQtDawWR03yS6hskh4bkhATQbgjdGMoUvA==", + "requires": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "@lezer/php": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.0.tgz", + "integrity": "sha512-kFQu/mk/vmjpA+fjQU87d9eimqKJ9PFCa8CZCPFWGEwNnm7Ahpw32N+HYEU/YAQ0XcfmOAnW/YJCEa8WpUOMMw==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", diff --git a/package.json b/package.json index 8407ffc77..4d2b70247 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,11 @@ }, "dependencies": { "@codemirror/commands": "^6.0.1", + "@codemirror/lang-html": "^6.1.0", + "@codemirror/lang-javascript": "^6.0.2", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-markdown": "^6.0.1", + "@codemirror/lang-php": "^6.0.0", "@codemirror/language": "^6.2.1", "@codemirror/legacy-modes": "^6.1.0", "@codemirror/state": "^6.1.0", diff --git a/resources/js/code/index.mjs b/resources/js/code/index.mjs index ff60cbff5..6ef659994 100644 --- a/resources/js/code/index.mjs +++ b/resources/js/code/index.mjs @@ -1,10 +1,9 @@ import {EditorView} from "@codemirror/view" -// import {EditorState} from "@codemirror/state" import Clipboard from "clipboard/dist/clipboard.min"; // Modes -import {modes, modeMap, modesAsStreamLanguages} from "./modes"; import {viewer} from "./setups.js"; +import {createView, updateViewLanguage} from "./views.js"; /** * Highlight pre elements on a page @@ -36,26 +35,24 @@ function highlightElem(elem) { elem.innerHTML = elem.innerHTML.replace(//gi ,'\n'); const content = elem.textContent.trimEnd(); - let mode = ''; + let langName = ''; if (innerCodeElem !== null) { - const langName = innerCodeElem.className.replace('language-', ''); - mode = getMode(langName, content); + langName = innerCodeElem.className.replace('language-', ''); } const wrapper = document.createElement('div'); elem.parentNode.insertBefore(wrapper, elem); - const cm = new EditorView({ + const ev = createView({ parent: wrapper, doc: content, extensions: viewer(), }); + setMode(ev, langName, content); elem.remove(); - // TODO - theme: getTheme(), - // TODO - mode, - addCopyIcon(cm); + addCopyIcon(ev); } /** @@ -84,28 +81,6 @@ function addCopyIcon(cmInstance) { // }); } -/** - * Search for a codemirror code based off a user suggestion - * @param {String} suggestion - * @param {String} content - * @returns {string} - */ -function getMode(suggestion, content) { - suggestion = suggestion.trim().replace(/^\./g, '').toLowerCase(); - - const modeMapType = typeof modeMap[suggestion]; - - if (modeMapType === 'undefined') { - return ''; - } - - if (modeMapType === 'function') { - return modeMap[suggestion](content); - } - - return modeMap[suggestion]; -} - /** * Ge the theme to use for CodeMirror instances. * @returns {*|string} @@ -172,12 +147,14 @@ export function inlineEditor(textArea, mode) { } /** - * Set the mode of a codemirror instance. - * @param cmInstance - * @param modeSuggestion + * Set the language mode of a codemirror EditorView. + * + * @param {EditorView} ev + * @param {string} modeSuggestion + * @param {string} content */ -export function setMode(cmInstance, modeSuggestion, content) { - cmInstance.setOption('mode', getMode(modeSuggestion, content)); +export function setMode(ev, modeSuggestion, content) { + updateViewLanguage(ev, modeSuggestion, content); } /** diff --git a/resources/js/code/languages.js b/resources/js/code/languages.js new file mode 100644 index 000000000..4b04bdb14 --- /dev/null +++ b/resources/js/code/languages.js @@ -0,0 +1,120 @@ +import {StreamLanguage} from "@codemirror/language" + +import {css} from '@codemirror/legacy-modes/mode/css'; +import {c, java, cpp, csharp, kotlin, scala} from '@codemirror/legacy-modes/mode/clike'; +import {diff} from '@codemirror/legacy-modes/mode/diff'; +import {fortran} from '@codemirror/legacy-modes/mode/fortran'; +import {go} from '@codemirror/legacy-modes/mode/go'; +import {haskell} from '@codemirror/legacy-modes/mode/haskell'; +import {html} from '@codemirror/lang-html'; +import {javascript} from '@codemirror/lang-javascript'; +import {json} from '@codemirror/lang-json'; +import {julia} from '@codemirror/legacy-modes/mode/julia'; +import {lua} from '@codemirror/legacy-modes/mode/lua'; +import {markdown} from '@codemirror/lang-markdown'; +import {oCaml, fSharp, sml} from '@codemirror/legacy-modes/mode/mllike'; +import {nginx} from '@codemirror/legacy-modes/mode/nginx'; +import {perl} from '@codemirror/legacy-modes/mode/perl'; +import {pascal} from '@codemirror/legacy-modes/mode/pascal'; +import {php} from '@codemirror/lang-php'; +import {powerShell} from '@codemirror/legacy-modes/mode/powershell'; +import {properties} from '@codemirror/legacy-modes/mode/properties'; +import {python} from '@codemirror/legacy-modes/mode/python'; +import {ruby} from '@codemirror/legacy-modes/mode/ruby'; +import {rust} from '@codemirror/legacy-modes/mode/rust'; +import {shell} from '@codemirror/legacy-modes/mode/shell'; +import {sql} from '@codemirror/legacy-modes/mode/sql'; +import {stex} from '@codemirror/legacy-modes/mode/stex'; +import {toml} from '@codemirror/legacy-modes/mode/toml'; +import {vb} from '@codemirror/legacy-modes/mode/vb'; +import {vbScript} from '@codemirror/legacy-modes/mode/vbscript'; +import {xml} from '@codemirror/legacy-modes/mode/xml'; +import {yaml} from '@codemirror/legacy-modes/mode/yaml'; + + +// Mapping of possible languages or formats from user input to their codemirror modes. +// Value can be a mode string or a function that will receive the code content & return the mode string. +// The function option is used in the event the exact mode could be dynamic depending on the code. +const modeMap = { + bash: () => StreamLanguage.define(shell), + css: () => StreamLanguage.define(css), + c: () => StreamLanguage.define(c), + java: () => StreamLanguage.define(java), + scala: () => StreamLanguage.define(scala), + kotlin: () => StreamLanguage.define(kotlin), + 'c++': () => StreamLanguage.define(cpp), + 'c#': () => StreamLanguage.define(csharp), + csharp: () => StreamLanguage.define(csharp), + diff: () => StreamLanguage.define(diff), + for: () => StreamLanguage.define(fortran), + fortran: () => StreamLanguage.define(fortran), + 'f#': () => StreamLanguage.define(fSharp), + fsharp: () => StreamLanguage.define(fSharp), + go: () => StreamLanguage.define(go), + haskell: () => StreamLanguage.define(haskell), + hs: () => StreamLanguage.define(haskell), + html: () => html(), + ini: () => StreamLanguage.define(properties), + javascript: () => javascript(), + json: () => json(), + js: () => javascript(), + jl: () => StreamLanguage.define(julia), + julia: () => StreamLanguage.define(julia), + latex: () => StreamLanguage.define(stex), + lua: () => StreamLanguage.define(lua), + md: () => StreamLanguage.define(markdown), + mdown: () => StreamLanguage.define(markdown), + markdown: () => StreamLanguage.define(markdown), + ml: () => StreamLanguage.define(sml), + nginx: () => StreamLanguage.define(nginx), + perl: () => StreamLanguage.define(perl), + pl: () => StreamLanguage.define(perl), + powershell: () => StreamLanguage.define(powerShell), + properties: () => StreamLanguage.define(properties), + ocaml: () => StreamLanguage.define(oCaml), + pascal: () => StreamLanguage.define(pascal), + pas: () => StreamLanguage.define(pascal), + php: (code) => { + const hasTags = code.includes(' StreamLanguage.define(python), + python: () => StreamLanguage.define(python), + ruby: () => StreamLanguage.define(ruby), + rust: () => StreamLanguage.define(rust), + rb: () => StreamLanguage.define(ruby), + rs: () => StreamLanguage.define(rust), + shell: () => StreamLanguage.define(shell), + sh: () => StreamLanguage.define(shell), + stext: () => StreamLanguage.define(stex), + toml: () => StreamLanguage.define(toml), + ts: () => javascript({typescript: true}), + typescript: () => javascript({typescript: true}), + sql: () => StreamLanguage.define(sql), + vbs: () => StreamLanguage.define(vbScript), + vbscript: () => StreamLanguage.define(vbScript), + 'vb.net': () => StreamLanguage.define(vb), + vbnet: () => StreamLanguage.define(vb), + xml: () => StreamLanguage.define(xml), + yaml: () => StreamLanguage.define(yaml), + yml: () => StreamLanguage.define(yaml), +}; + +/** + * Get the relevant codemirror language extension based upon the given language + * suggestion and content. + * @param {String} langSuggestion + * @param {String} content + * @returns {StreamLanguage} + */ +export function getLanguageExtension(langSuggestion, content) { + const suggestion = langSuggestion.trim().replace(/^\./g, '').toLowerCase(); + + const language = modeMap[suggestion]; + + if (typeof language === 'undefined') { + return undefined; + } + + return language(content); +} \ No newline at end of file diff --git a/resources/js/code/modes.js b/resources/js/code/modes.js deleted file mode 100644 index 5a89255a5..000000000 --- a/resources/js/code/modes.js +++ /dev/null @@ -1,134 +0,0 @@ -import {StreamLanguage} from "@codemirror/language" - -import {css as langCss} from '@codemirror/legacy-modes/mode/css'; -import {clike as langClike} from '@codemirror/legacy-modes/mode/clike'; -import {diff as langDiff} from '@codemirror/legacy-modes/mode/diff'; -import {fortran as langFortran} from '@codemirror/legacy-modes/mode/fortran'; -import {go as langGo} from '@codemirror/legacy-modes/mode/go'; -import {haskell as langHaskell} from '@codemirror/legacy-modes/mode/haskell'; -// import {htmlmixed as langHtmlmixed} from '@codemirror/legacy-modes/mode/htmlmixed'; -import {javascript as langJavascript} from '@codemirror/legacy-modes/mode/javascript'; -import {julia as langJulia} from '@codemirror/legacy-modes/mode/julia'; -import {lua as langLua} from '@codemirror/legacy-modes/mode/lua'; -// import {markdown as langMarkdown} from '@codemirror/legacy-modes/mode/markdown'; -import {oCaml as langMllike} from '@codemirror/legacy-modes/mode/mllike'; -import {nginx as langNginx} from '@codemirror/legacy-modes/mode/nginx'; -import {perl as langPerl} from '@codemirror/legacy-modes/mode/perl'; -import {pascal as langPascal} from '@codemirror/legacy-modes/mode/pascal'; -// import {php as langPhp} from '@codemirror/legacy-modes/mode/php'; -import {powerShell as langPowershell} from '@codemirror/legacy-modes/mode/powershell'; -import {properties as langProperties} from '@codemirror/legacy-modes/mode/properties'; -import {python as langPython} from '@codemirror/legacy-modes/mode/python'; -import {ruby as langRuby} from '@codemirror/legacy-modes/mode/ruby'; -import {rust as langRust} from '@codemirror/legacy-modes/mode/rust'; -import {shell as langShell} from '@codemirror/legacy-modes/mode/shell'; -import {sql as langSql} from '@codemirror/legacy-modes/mode/sql'; -import {stex as langStex} from '@codemirror/legacy-modes/mode/stex'; -import {toml as langToml} from '@codemirror/legacy-modes/mode/toml'; -import {vb as langVb} from '@codemirror/legacy-modes/mode/vb'; -import {vbScript as langVbscript} from '@codemirror/legacy-modes/mode/vbscript'; -import {xml as langXml} from '@codemirror/legacy-modes/mode/xml'; -import {yaml as langYaml} from '@codemirror/legacy-modes/mode/yaml'; - -export const modes = [ - langCss, - langClike, - langDiff, - langFortran, - langGo, - langHaskell, - // langHtmlmixed, - langJavascript, - langJulia, - langLua, - // langMarkdown, - langMllike, - langNginx, - langPerl, - langPascal, - // langPhp, - langPowershell, - langProperties, - langPython, - langRuby, - langRust, - langShell, - langSql, - langStex, - langToml, - langVb, - langVbscript, - langXml, - langYaml, -]; - -// Mapping of possible languages or formats from user input to their codemirror modes. -// Value can be a mode string or a function that will receive the code content & return the mode string. -// The function option is used in the event the exact mode could be dynamic depending on the code. -export const modeMap = { - bash: 'shell', - css: 'css', - c: 'text/x-csrc', - java: 'text/x-java', - scala: 'text/x-scala', - kotlin: 'text/x-kotlin', - 'c++': 'text/x-c++src', - 'c#': 'text/x-csharp', - csharp: 'text/x-csharp', - diff: 'diff', - for: 'fortran', - fortran: 'fortran', - 'f#': 'text/x-fsharp', - fsharp: 'text/x-fsharp', - go: 'go', - haskell: 'haskell', - hs: 'haskell', - html: 'htmlmixed', - ini: 'properties', - javascript: 'text/javascript', - json: 'application/json', - js: 'text/javascript', - jl: 'text/x-julia', - julia: 'text/x-julia', - latex: 'text/x-stex', - lua: 'lua', - md: 'markdown', - mdown: 'markdown', - markdown: 'markdown', - ml: 'mllike', - nginx: 'nginx', - perl: 'perl', - pl: 'perl', - powershell: 'powershell', - properties: 'properties', - ocaml: 'text/x-ocaml', - pascal: 'text/x-pascal', - pas: 'text/x-pascal', - php: (content) => { - return content.includes(' StreamLanguage.define(mode)); -} \ No newline at end of file diff --git a/resources/js/code/setups.js b/resources/js/code/setups.js index 768d3a35d..45cc9c317 100644 --- a/resources/js/code/setups.js +++ b/resources/js/code/setups.js @@ -6,9 +6,6 @@ import {defaultHighlightStyle, syntaxHighlighting, bracketMatching, import {defaultKeymap, history, historyKeymap} from "@codemirror/commands" import {EditorState} from "@codemirror/state" -import {modesAsStreamLanguages} from "./modes"; - - export function viewer() { return [ lineNumbers(), @@ -27,6 +24,5 @@ export function viewer() { ...foldKeymap, ]), EditorState.readOnly.of(true), - ...modesAsStreamLanguages(), ]; } \ No newline at end of file diff --git a/resources/js/code/views.js b/resources/js/code/views.js new file mode 100644 index 000000000..e87718939 --- /dev/null +++ b/resources/js/code/views.js @@ -0,0 +1,38 @@ +import {getLanguageExtension} from "./languages"; +import {Compartment} from "@codemirror/state" +import {EditorView} from "@codemirror/view" + +const viewLangCompartments = new WeakMap(); + +/** + * Create a new editor view. + * + * @param {Object} config + * @returns {EditorView} + */ +export function createView(config) { + const langCompartment = new Compartment(); + config.extensions.push(langCompartment.of([])); + + const ev = new EditorView(config); + + viewLangCompartments.set(ev, langCompartment); + + return ev; +} + +/** + * Set the language mode of an EditorView. + * + * @param {EditorView} ev + * @param {string} modeSuggestion + * @param {string} content + */ +export function updateViewLanguage(ev, modeSuggestion, content) { + const compartment = viewLangCompartments.get(ev); + const language = getLanguageExtension(modeSuggestion, content); + + ev.dispatch({ + effects: compartment.reconfigure(language ? language : []) + }) +} \ No newline at end of file From 4757ed9453c0cfd8dd229e34ee3a64f74dfe0f3a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 4 Aug 2022 13:33:51 +0100 Subject: [PATCH 003/477] Converted codemirror languges to new packages where available Does increase bundle size massively though, Will need to think about solutions for this. --- package-lock.json | 217 +++++++++++++++++++++++++++++++++ package.json | 7 ++ resources/js/code/languages.js | 56 +++++---- 3 files changed, 253 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0eca777cb..a29be3324 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,11 +6,18 @@ "": { "dependencies": { "@codemirror/commands": "^6.0.1", + "@codemirror/lang-cpp": "^6.0.1", + "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-html": "^6.1.0", + "@codemirror/lang-java": "^6.0.0", "@codemirror/lang-javascript": "^6.0.2", "@codemirror/lang-json": "^6.0.0", "@codemirror/lang-markdown": "^6.0.1", "@codemirror/lang-php": "^6.0.0", + "@codemirror/lang-python": "^6.0.1", + "@codemirror/lang-rust": "^6.0.0", + "@codemirror/lang-sql": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", "@codemirror/language": "^6.2.1", "@codemirror/legacy-modes": "^6.1.0", "@codemirror/state": "^6.1.0", @@ -60,6 +67,15 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@codemirror/lang-cpp": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.1.tgz", + "integrity": "sha512-46p3ohfhjzkLWJ3VwvzX0aqlXh8UkEqX1xo2Eds9l6Ql3uDoxI2IZEjR9cgJaGOZTXCkDzQuQH7sfYAxMvzLjA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/cpp": "^1.0.0" + } + }, "node_modules/@codemirror/lang-css": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.0.0.tgz", @@ -85,6 +101,15 @@ "@lezer/html": "^1.0.0" } }, + "node_modules/@codemirror/lang-java": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.0.tgz", + "integrity": "sha512-aeWq+ikUS6Eubk6RBbiMgxuBIT4Ih8Asb1qc2pSiMcstrwr4ODbazPXsBHbLBYg3aObvFyOm2bNQncbQJjZ3sQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/java": "^1.0.0" + } + }, "node_modules/@codemirror/lang-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.0.2.tgz", @@ -133,6 +158,48 @@ "@lezer/php": "^1.0.0" } }, + "node_modules/@codemirror/lang-python": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.0.1.tgz", + "integrity": "sha512-w2jTSY+LgXnK7iIBLgMxk6xtJhZHkcxcGGveuq9zYmncURmOTFXKnDvBaBClNIHKgjkHXZqGK8ZduCMK23hZPA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/python": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-rust": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.0.tgz", + "integrity": "sha512-VQql3Qk1BwoXb3SUkeWll/EEwhsgQWc1bpia7CFqqp2PhQBb5A6r4Vj2JCkU/nE6A7TDPSGHTOoqJSG5s/VXtQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/rust": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-sql": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.0.0.tgz", + "integrity": "sha512-mq4NwTDbbo7QZktfgPsS+ms0FmAceH4WM2jLbgf+N28FoKUy0JzGe3XJymgnTewXnNUwujKBxArQzibxSDdVyQ==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-xml": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.0.0.tgz", + "integrity": "sha512-M/HLWxIiP956xGjtrxkeHkCmDGVQGKu782x8pOH5CLJIMkWtiB1DWfDoDHqpFjdEE9dkfcqPWvYfVi6GbhuXEg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, "node_modules/@codemirror/language": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.2.1.tgz", @@ -194,6 +261,15 @@ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz", "integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==" }, + "node_modules/@lezer/cpp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.0.0.tgz", + "integrity": "sha512-Klk3/AIEKoptmm6cNm7xTulNXjdTKkD+hVOEcz/NeRg8tIestP5hsGHJeFDR/XtyDTxsjoPjKZRIGohht7zbKw==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lezer/css": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.0.0.tgz", @@ -221,6 +297,15 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@lezer/java": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.0.0.tgz", + "integrity": "sha512-z2EA0JHq2WoiKfQy5uOOd4t17PJtq8guh58gPkSzOnNcQ7DNbkrU+Axak+jL8+Noinwyz2tRNOseQFj+Tg+P0A==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lezer/javascript": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.0.2.tgz", @@ -265,6 +350,33 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@lezer/python": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.0.tgz", + "integrity": "sha512-FVPp2usfj3zZuc+2RidXAY94WAcsHQ3dbKDbXuZgoAwUungAcXwd3EWXiWQvwNqbae+ek51bWi8dwbiQqweWCg==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/rust": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.0.tgz", + "integrity": "sha512-IpGAxIjNxYmX9ra6GfQTSPegdCAWNeq23WNmrsMMQI7YNSvKtYxO4TX5rgZUmbhEucWn0KTBMeDEPXg99YKtTA==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/xml": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.0.tgz", + "integrity": "sha512-73iI9UK8iqSvWtLlOEl/g+50ivwQn8Ge6foHVN66AXUS1RccFnAoc7BYU8b3c8/rP6dfCOGqAGaWLxBzhj60MA==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", @@ -2186,6 +2298,15 @@ "@lezer/common": "^1.0.0" } }, + "@codemirror/lang-cpp": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.1.tgz", + "integrity": "sha512-46p3ohfhjzkLWJ3VwvzX0aqlXh8UkEqX1xo2Eds9l6Ql3uDoxI2IZEjR9cgJaGOZTXCkDzQuQH7sfYAxMvzLjA==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/cpp": "^1.0.0" + } + }, "@codemirror/lang-css": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.0.0.tgz", @@ -2211,6 +2332,15 @@ "@lezer/html": "^1.0.0" } }, + "@codemirror/lang-java": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.0.tgz", + "integrity": "sha512-aeWq+ikUS6Eubk6RBbiMgxuBIT4Ih8Asb1qc2pSiMcstrwr4ODbazPXsBHbLBYg3aObvFyOm2bNQncbQJjZ3sQ==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/java": "^1.0.0" + } + }, "@codemirror/lang-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.0.2.tgz", @@ -2259,6 +2389,48 @@ "@lezer/php": "^1.0.0" } }, + "@codemirror/lang-python": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.0.1.tgz", + "integrity": "sha512-w2jTSY+LgXnK7iIBLgMxk6xtJhZHkcxcGGveuq9zYmncURmOTFXKnDvBaBClNIHKgjkHXZqGK8ZduCMK23hZPA==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/python": "^1.0.0" + } + }, + "@codemirror/lang-rust": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.0.tgz", + "integrity": "sha512-VQql3Qk1BwoXb3SUkeWll/EEwhsgQWc1bpia7CFqqp2PhQBb5A6r4Vj2JCkU/nE6A7TDPSGHTOoqJSG5s/VXtQ==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/rust": "^1.0.0" + } + }, + "@codemirror/lang-sql": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.0.0.tgz", + "integrity": "sha512-mq4NwTDbbo7QZktfgPsS+ms0FmAceH4WM2jLbgf+N28FoKUy0JzGe3XJymgnTewXnNUwujKBxArQzibxSDdVyQ==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@codemirror/lang-xml": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.0.0.tgz", + "integrity": "sha512-M/HLWxIiP956xGjtrxkeHkCmDGVQGKu782x8pOH5CLJIMkWtiB1DWfDoDHqpFjdEE9dkfcqPWvYfVi6GbhuXEg==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, "@codemirror/language": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.2.1.tgz", @@ -2320,6 +2492,15 @@ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz", "integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==" }, + "@lezer/cpp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.0.0.tgz", + "integrity": "sha512-Klk3/AIEKoptmm6cNm7xTulNXjdTKkD+hVOEcz/NeRg8tIestP5hsGHJeFDR/XtyDTxsjoPjKZRIGohht7zbKw==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "@lezer/css": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.0.0.tgz", @@ -2347,6 +2528,15 @@ "@lezer/lr": "^1.0.0" } }, + "@lezer/java": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.0.0.tgz", + "integrity": "sha512-z2EA0JHq2WoiKfQy5uOOd4t17PJtq8guh58gPkSzOnNcQ7DNbkrU+Axak+jL8+Noinwyz2tRNOseQFj+Tg+P0A==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "@lezer/javascript": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.0.2.tgz", @@ -2391,6 +2581,33 @@ "@lezer/lr": "^1.0.0" } }, + "@lezer/python": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.0.tgz", + "integrity": "sha512-FVPp2usfj3zZuc+2RidXAY94WAcsHQ3dbKDbXuZgoAwUungAcXwd3EWXiWQvwNqbae+ek51bWi8dwbiQqweWCg==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/rust": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.0.tgz", + "integrity": "sha512-IpGAxIjNxYmX9ra6GfQTSPegdCAWNeq23WNmrsMMQI7YNSvKtYxO4TX5rgZUmbhEucWn0KTBMeDEPXg99YKtTA==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/xml": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.0.tgz", + "integrity": "sha512-73iI9UK8iqSvWtLlOEl/g+50ivwQn8Ge6foHVN66AXUS1RccFnAoc7BYU8b3c8/rP6dfCOGqAGaWLxBzhj60MA==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", diff --git a/package.json b/package.json index 4d2b70247..11e69962e 100644 --- a/package.json +++ b/package.json @@ -24,11 +24,18 @@ }, "dependencies": { "@codemirror/commands": "^6.0.1", + "@codemirror/lang-cpp": "^6.0.1", + "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-html": "^6.1.0", + "@codemirror/lang-java": "^6.0.0", "@codemirror/lang-javascript": "^6.0.2", "@codemirror/lang-json": "^6.0.0", "@codemirror/lang-markdown": "^6.0.1", "@codemirror/lang-php": "^6.0.0", + "@codemirror/lang-python": "^6.0.1", + "@codemirror/lang-rust": "^6.0.0", + "@codemirror/lang-sql": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", "@codemirror/language": "^6.2.1", "@codemirror/legacy-modes": "^6.1.0", "@codemirror/state": "^6.1.0", diff --git a/resources/js/code/languages.js b/resources/js/code/languages.js index 4b04bdb14..b43b1c397 100644 --- a/resources/js/code/languages.js +++ b/resources/js/code/languages.js @@ -1,12 +1,14 @@ import {StreamLanguage} from "@codemirror/language" -import {css} from '@codemirror/legacy-modes/mode/css'; -import {c, java, cpp, csharp, kotlin, scala} from '@codemirror/legacy-modes/mode/clike'; +import {css} from '@codemirror/lang-css'; +import {c, csharp, kotlin, scala} from '@codemirror/legacy-modes/mode/clike'; +import {cpp} from '@codemirror/lang-cpp'; import {diff} from '@codemirror/legacy-modes/mode/diff'; import {fortran} from '@codemirror/legacy-modes/mode/fortran'; import {go} from '@codemirror/legacy-modes/mode/go'; import {haskell} from '@codemirror/legacy-modes/mode/haskell'; import {html} from '@codemirror/lang-html'; +import {java} from '@codemirror/lang-java'; import {javascript} from '@codemirror/lang-javascript'; import {json} from '@codemirror/lang-json'; import {julia} from '@codemirror/legacy-modes/mode/julia'; @@ -19,16 +21,16 @@ import {pascal} from '@codemirror/legacy-modes/mode/pascal'; import {php} from '@codemirror/lang-php'; import {powerShell} from '@codemirror/legacy-modes/mode/powershell'; import {properties} from '@codemirror/legacy-modes/mode/properties'; -import {python} from '@codemirror/legacy-modes/mode/python'; +import {python} from '@codemirror/lang-python'; import {ruby} from '@codemirror/legacy-modes/mode/ruby'; -import {rust} from '@codemirror/legacy-modes/mode/rust'; +import {rust} from '@codemirror/lang-rust'; import {shell} from '@codemirror/legacy-modes/mode/shell'; -import {sql} from '@codemirror/legacy-modes/mode/sql'; +import {sql} from '@codemirror/lang-sql'; import {stex} from '@codemirror/legacy-modes/mode/stex'; import {toml} from '@codemirror/legacy-modes/mode/toml'; import {vb} from '@codemirror/legacy-modes/mode/vb'; import {vbScript} from '@codemirror/legacy-modes/mode/vbscript'; -import {xml} from '@codemirror/legacy-modes/mode/xml'; +import {xml} from '@codemirror/lang-xml'; import {yaml} from '@codemirror/legacy-modes/mode/yaml'; @@ -37,12 +39,9 @@ import {yaml} from '@codemirror/legacy-modes/mode/yaml'; // The function option is used in the event the exact mode could be dynamic depending on the code. const modeMap = { bash: () => StreamLanguage.define(shell), - css: () => StreamLanguage.define(css), c: () => StreamLanguage.define(c), - java: () => StreamLanguage.define(java), - scala: () => StreamLanguage.define(scala), - kotlin: () => StreamLanguage.define(kotlin), - 'c++': () => StreamLanguage.define(cpp), + css: () => css(), + 'c++': () => cpp(), 'c#': () => StreamLanguage.define(csharp), csharp: () => StreamLanguage.define(csharp), diff: () => StreamLanguage.define(diff), @@ -55,47 +54,50 @@ const modeMap = { hs: () => StreamLanguage.define(haskell), html: () => html(), ini: () => StreamLanguage.define(properties), + java: () => java(), javascript: () => javascript(), json: () => json(), js: () => javascript(), jl: () => StreamLanguage.define(julia), julia: () => StreamLanguage.define(julia), + kotlin: () => StreamLanguage.define(kotlin), latex: () => StreamLanguage.define(stex), lua: () => StreamLanguage.define(lua), - md: () => StreamLanguage.define(markdown), - mdown: () => StreamLanguage.define(markdown), - markdown: () => StreamLanguage.define(markdown), + markdown: () => markdown(), + md: () => markdown(), + mdown: () => markdown(), ml: () => StreamLanguage.define(sml), nginx: () => StreamLanguage.define(nginx), - perl: () => StreamLanguage.define(perl), - pl: () => StreamLanguage.define(perl), - powershell: () => StreamLanguage.define(powerShell), - properties: () => StreamLanguage.define(properties), - ocaml: () => StreamLanguage.define(oCaml), - pascal: () => StreamLanguage.define(pascal), pas: () => StreamLanguage.define(pascal), + pascal: () => StreamLanguage.define(pascal), + perl: () => StreamLanguage.define(perl), php: (code) => { const hasTags = code.includes(' StreamLanguage.define(python), - python: () => StreamLanguage.define(python), - ruby: () => StreamLanguage.define(ruby), - rust: () => StreamLanguage.define(rust), + pl: () => StreamLanguage.define(perl), + powershell: () => StreamLanguage.define(powerShell), + properties: () => StreamLanguage.define(properties), + ocaml: () => StreamLanguage.define(oCaml), + py: () => python(), + python: () => python(), rb: () => StreamLanguage.define(ruby), - rs: () => StreamLanguage.define(rust), + rs: () => rust(), + ruby: () => StreamLanguage.define(ruby), + rust: () => rust(), + scala: () => StreamLanguage.define(scala), shell: () => StreamLanguage.define(shell), sh: () => StreamLanguage.define(shell), stext: () => StreamLanguage.define(stex), toml: () => StreamLanguage.define(toml), ts: () => javascript({typescript: true}), typescript: () => javascript({typescript: true}), - sql: () => StreamLanguage.define(sql), + sql: () => sql(), vbs: () => StreamLanguage.define(vbScript), vbscript: () => StreamLanguage.define(vbScript), 'vb.net': () => StreamLanguage.define(vb), vbnet: () => StreamLanguage.define(vb), - xml: () => StreamLanguage.define(xml), + xml: () => xml(), yaml: () => StreamLanguage.define(yaml), yml: () => StreamLanguage.define(yaml), }; From 9fd7a6abedaf69efc449c7ea00724eb890fc71f7 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 4 Aug 2022 14:19:04 +0100 Subject: [PATCH 004/477] Added dark theme handling --- package-lock.json | 23 +++++++++++++++++++++++ package.json | 1 + resources/js/code/views.js | 23 +++++++++++++++++++++-- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index a29be3324..8327421b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@codemirror/language": "^6.2.1", "@codemirror/legacy-modes": "^6.1.0", "@codemirror/state": "^6.1.0", + "@codemirror/theme-one-dark": "^6.0.0", "@codemirror/view": "^6.1.2", "clipboard": "^2.0.11", "codemirror": "^6.0.1", @@ -246,6 +247,17 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.1.0.tgz", "integrity": "sha512-qbUr94DZTe6/V1VS7LDLz11rM/1t/nJxR1El4I6UaxDEdc0aZZvq6JCLJWiRmUf95NRAnDH6fhXn+PWp9wGCIg==" }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.0.0.tgz", + "integrity": "sha512-jTCfi1I8QT++3m21Ui6sU8qwu3F/hLv161KLxfvkV1cYWSBwyUanmQFs89ChobQjBHi2x7s2k71wF9WYvE8fdw==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, "node_modules/@codemirror/view": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.1.2.tgz", @@ -2477,6 +2489,17 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.1.0.tgz", "integrity": "sha512-qbUr94DZTe6/V1VS7LDLz11rM/1t/nJxR1El4I6UaxDEdc0aZZvq6JCLJWiRmUf95NRAnDH6fhXn+PWp9wGCIg==" }, + "@codemirror/theme-one-dark": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.0.0.tgz", + "integrity": "sha512-jTCfi1I8QT++3m21Ui6sU8qwu3F/hLv161KLxfvkV1cYWSBwyUanmQFs89ChobQjBHi2x7s2k71wF9WYvE8fdw==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, "@codemirror/view": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.1.2.tgz", diff --git a/package.json b/package.json index 11e69962e..3a655c87a 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@codemirror/language": "^6.2.1", "@codemirror/legacy-modes": "^6.1.0", "@codemirror/state": "^6.1.0", + "@codemirror/theme-one-dark": "^6.0.0", "@codemirror/view": "^6.1.2", "clipboard": "^2.0.11", "codemirror": "^6.0.1", diff --git a/resources/js/code/views.js b/resources/js/code/views.js index e87718939..cada9a1d6 100644 --- a/resources/js/code/views.js +++ b/resources/js/code/views.js @@ -1,18 +1,20 @@ -import {getLanguageExtension} from "./languages"; +import {getLanguageExtension} from "./languages" import {Compartment} from "@codemirror/state" import {EditorView} from "@codemirror/view" +import {oneDark} from "@codemirror/theme-one-dark" const viewLangCompartments = new WeakMap(); /** * Create a new editor view. * - * @param {Object} config + * @param {{parent: Element, doc: String, extensions: Array}} config * @returns {EditorView} */ export function createView(config) { const langCompartment = new Compartment(); config.extensions.push(langCompartment.of([])); + config.extensions.push(getTheme(config.parent)); const ev = new EditorView(config); @@ -21,6 +23,23 @@ export function createView(config) { return ev; } +/** + * Ge the theme extension to use for editor view instance. + * @returns {Extension} + */ +function getTheme(viewParentEl) { + const darkMode = document.documentElement.classList.contains('dark-mode'); + + const eventData = { + darkMode: darkMode, + theme: null, + }; + + window.$events.emitPublic(viewParentEl, 'library-cm6::configure-theme', eventData); + + return eventData.theme || (darkMode ? oneDark : []); +} + /** * Set the language mode of an EditorView. * From d5056423364188f5b5862334e63f976834065360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20H=C3=B6rmann?= Date: Tue, 29 Nov 2022 14:53:41 +0100 Subject: [PATCH 005/477] Add popular PHP templating languages to code editor Smarty and Twig are two very popular PHP templating engines and might be useful to some Bookstack users too. --- resources/js/code.mjs | 4 ++++ resources/views/pages/parts/code-editor.blade.php | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/resources/js/code.mjs b/resources/js/code.mjs index ad282f2be..8d8062689 100644 --- a/resources/js/code.mjs +++ b/resources/js/code.mjs @@ -26,10 +26,12 @@ import 'codemirror/mode/python/python'; import 'codemirror/mode/ruby/ruby'; import 'codemirror/mode/rust/rust'; import 'codemirror/mode/shell/shell'; +import 'codemirror/mode/smarty/smarty'; import 'codemirror/mode/sql/sql'; import 'codemirror/mode/stex/stex'; import 'codemirror/mode/swift/swift'; import 'codemirror/mode/toml/toml'; +import 'codemirror/mode/twig/twig'; import 'codemirror/mode/vb/vb'; import 'codemirror/mode/vbscript/vbscript'; import 'codemirror/mode/xml/xml'; @@ -94,11 +96,13 @@ const modeMap = { rs: 'rust', shell: 'shell', sh: 'shell', + smarty: 'smarty', sql: 'text/x-sql', stext: 'text/x-stex', swift: 'text/x-swift', toml: 'toml', ts: 'text/typescript', + twig: 'twig', typescript: 'text/typescript', vbs: 'vbscript', vbscript: 'vbscript', diff --git a/resources/views/pages/parts/code-editor.blade.php b/resources/views/pages/parts/code-editor.blade.php index 18c9ad423..212f2e5e7 100644 --- a/resources/views/pages/parts/code-editor.blade.php +++ b/resources/views/pages/parts/code-editor.blade.php @@ -25,8 +25,8 @@ $languages = [ 'Bash', 'CSS', 'C', 'C++', 'C#', 'Dart', 'Diff', 'Fortran', 'F#', 'Go', 'Haskell', 'HTML', 'INI', 'Java', 'JavaScript', 'JSON', 'Julia', 'Kotlin', 'LaTeX', 'Lua', 'MarkDown', 'MATLAB', 'Nginx', 'OCaml', - 'Octave', 'Pascal', 'Perl', 'PHP', 'Powershell', 'Python', 'Ruby', 'Rust', 'Shell', 'SQL', 'Swift', - 'TypeScript', 'VBScript', 'VB.NET', 'XML', 'YAML', + 'Octave', 'Pascal', 'Perl', 'PHP', 'Powershell', 'Python', 'Ruby', 'Rust', 'Shell', 'Smarty', 'SQL', 'Swift', + 'Twig', 'TypeScript', 'VBScript', 'VB.NET', 'XML', 'YAML', ]; @endphp From 2c0a7346b1635b5fd1599a86e33e3c834d9a967c Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 16 Dec 2022 17:03:48 +0000 Subject: [PATCH 006/477] Prevent search focus change on left/right arrow press For #3920 --- resources/js/services/keyboard-navigation.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/resources/js/services/keyboard-navigation.js b/resources/js/services/keyboard-navigation.js index 9e05ef528..0e1dcf1a7 100644 --- a/resources/js/services/keyboard-navigation.js +++ b/resources/js/services/keyboard-navigation.js @@ -57,6 +57,12 @@ export class KeyboardNavigationHandler { * @param {KeyboardEvent} event */ #keydownHandler(event) { + + // Ignore certain key events in inputs to allow text editing. + if (event.target.matches('input') && (event.key === 'ArrowRight' || event.key === 'ArrowLeft')) { + return; + } + if (event.key === 'ArrowDown' || event.key === 'ArrowRight') { this.focusNext(); event.preventDefault(); From 7053a8669f4f25cd05fbfbe361fe48a9d756304a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 16 Dec 2022 17:06:52 +0000 Subject: [PATCH 007/477] New Crowdin updates (#3881) --- resources/lang/cs/settings.php | 2 +- resources/lang/de/activities.php | 28 +++++----- resources/lang/de/auth.php | 4 +- resources/lang/de/common.php | 6 +- resources/lang/de/editor.php | 8 +-- resources/lang/de/entities.php | 12 ++-- resources/lang/de/preferences.php | 10 ++-- resources/lang/de/settings.php | 16 +++--- resources/lang/de_informal/auth.php | 42 +++++++------- resources/lang/de_informal/common.php | 6 +- resources/lang/de_informal/components.php | 2 +- resources/lang/de_informal/editor.php | 10 ++-- resources/lang/de_informal/entities.php | 50 ++++++++--------- resources/lang/de_informal/errors.php | 24 ++++---- resources/lang/de_informal/preferences.php | 14 ++--- resources/lang/de_informal/settings.php | 64 +++++++++++----------- resources/lang/el/auth.php | 4 +- resources/lang/el/common.php | 6 +- resources/lang/el/editor.php | 8 +-- resources/lang/el/entities.php | 14 ++--- resources/lang/el/preferences.php | 20 +++---- resources/lang/el/settings.php | 4 +- resources/lang/es/preferences.php | 10 ++-- resources/lang/et/entities.php | 4 +- resources/lang/eu/activities.php | 20 +++---- resources/lang/eu/preferences.php | 20 +++---- resources/lang/fa/entities.php | 2 +- resources/lang/he/editor.php | 2 +- resources/lang/it/entities.php | 4 +- resources/lang/ja/auth.php | 4 +- resources/lang/ja/common.php | 8 +-- resources/lang/ja/editor.php | 8 +-- resources/lang/ja/entities.php | 24 ++++---- resources/lang/ja/preferences.php | 20 +++---- resources/lang/ja/settings.php | 12 ++-- resources/lang/pl/auth.php | 4 +- resources/lang/pl/entities.php | 4 +- resources/lang/pt_BR/entities.php | 4 +- resources/lang/zh_CN/auth.php | 18 +++--- resources/lang/zh_CN/common.php | 6 +- resources/lang/zh_CN/editor.php | 6 +- resources/lang/zh_CN/entities.php | 12 ++-- resources/lang/zh_CN/errors.php | 6 +- resources/lang/zh_CN/preferences.php | 20 +++---- resources/lang/zh_CN/settings.php | 18 +++--- 45 files changed, 295 insertions(+), 295 deletions(-) diff --git a/resources/lang/cs/settings.php b/resources/lang/cs/settings.php index 5f16b3a64..5302a33fc 100644 --- a/resources/lang/cs/settings.php +++ b/resources/lang/cs/settings.php @@ -26,7 +26,7 @@ return [ 'app_public_viewing' => 'Povolit prohlížení veřejností?', 'app_secure_images' => 'Nahrávat obrázky neveřejně a zabezpečeně', 'app_secure_images_toggle' => 'Zapnout bezpečnější nahrávání obrázků', - 'app_secure_images_desc' => 'Z výkonnostních důvodů jsou všechny obrázky veřejně dostupné. Tato volba přidá do adresy obrázku náhodný řetězec, aby nikdo neodhadnul adresu obrázku. Ujistěte se, že server nezobrazuje v adresáři seznam souborů, což by přístup k přístup opět otevřelo.', + 'app_secure_images_desc' => 'Z výkonnostních důvodů jsou všechny obrázky veřejně dostupné. Tato volba přidá do adresy obrázku náhodný řetězec, aby nikdo neodhadnul adresu obrázku. Ujistěte se, že server nezobrazuje v adresáři seznam souborů, což by přístup k obrázkům opět otevřelo.', 'app_default_editor' => 'Výchozí editor', 'app_default_editor_desc' => 'Vyberte, který editor bude použit ve výchozím nastavení při úpravách nových stránek. To může být přepsáno na úrovni stránky, kde to dovolují oprávnění.', 'app_custom_html' => 'Vlastní obsah hlavičky HTML', diff --git a/resources/lang/de/activities.php b/resources/lang/de/activities.php index 0d3f337b7..c00b35398 100644 --- a/resources/lang/de/activities.php +++ b/resources/lang/de/activities.php @@ -8,7 +8,7 @@ return [ // Pages 'page_create' => 'erstellte Seite', 'page_create_notification' => 'Seite erfolgreich erstellt', - 'page_update' => 'hat die Seite aktualisiert', + 'page_update' => 'aktualisierte Seite', 'page_update_notification' => 'Seite erfolgreich aktualisiert', 'page_delete' => 'gelöschte Seite', 'page_delete_notification' => 'Seite erfolgreich gelöscht', @@ -19,32 +19,32 @@ return [ // Chapters 'chapter_create' => 'erstellte Kapitel', 'chapter_create_notification' => 'Kapitel erfolgreich erstellt', - 'chapter_update' => 'hat das Kapitel geändert', + 'chapter_update' => 'aktualisierte Kapitel', 'chapter_update_notification' => 'Kapitel erfolgreich aktualisiert', - 'chapter_delete' => 'hat das Kapitel gelöscht', + 'chapter_delete' => 'löschte Kapitel', 'chapter_delete_notification' => 'Kapitel erfolgreich gelöscht', - 'chapter_move' => 'hat das Kapitel verschoben', + 'chapter_move' => 'verschob Kapitel', // Books - 'book_create' => 'hat das Buch erstellt', + 'book_create' => 'erstellte Buch', 'book_create_notification' => 'Buch erfolgreich erstellt', - 'book_create_from_chapter' => 'umgewandeltes Kapitel zum Buch', - 'book_create_from_chapter_notification' => 'Kapitel erfolgreich in ein Buch umgewandelt', - 'book_update' => 'hat das Buch aktualisiert', + 'book_create_from_chapter' => 'konvertierte Kapitel zu Buch', + 'book_create_from_chapter_notification' => 'Kapitel erfolgreich in ein Buch konvertiert', + 'book_update' => 'aktualisierte Buch', 'book_update_notification' => 'Buch erfolgreich aktualisiert', - 'book_delete' => 'hat das Buch gelöscht', + 'book_delete' => 'löschte Buch', 'book_delete_notification' => 'Buch erfolgreich gelöscht', - 'book_sort' => 'hat die Buch-Sortierung geändert', + 'book_sort' => 'sortierte Buch', 'book_sort_notification' => 'Das Buch wurde erfolgreich umsortiert', // Bookshelves - 'bookshelf_create' => 'erstelltes Regal', + 'bookshelf_create' => 'erstellte Regal', 'bookshelf_create_notification' => 'Regal erfolgreich erstellt', - 'bookshelf_create_from_book' => 'zu Regal konvertiertes Buch', + 'bookshelf_create_from_book' => 'konvertierte Buch zu Regal', 'bookshelf_create_from_book_notification' => 'Buch erfolgreich in ein Regal konvertiert', - 'bookshelf_update' => 'aktualisiertes Regal', + 'bookshelf_update' => 'aktualisierte Regal', 'bookshelf_update_notification' => 'Regal erfolgreich aktualisiert', - 'bookshelf_delete' => 'gelöschtes Regal', + 'bookshelf_delete' => 'löschte Regal', 'bookshelf_delete_notification' => 'Regal erfolgreich gelöscht', // Favourites diff --git a/resources/lang/de/auth.php b/resources/lang/de/auth.php index 85480f5ea..c05d98732 100644 --- a/resources/lang/de/auth.php +++ b/resources/lang/de/auth.php @@ -61,8 +61,8 @@ return [ 'email_confirm_send_error' => 'Leider konnte die für die Registrierung notwendige E-Mail zur Bestätigung Ihrer E-Mail-Adresse nicht versandt werden. Bitte kontaktieren Sie den Systemadministrator!', 'email_confirm_success' => 'Ihre E-Mail wurde bestätigt! Sie sollten nun in der Lage sein, sich mit dieser E-Mail-Adresse anzumelden.', 'email_confirm_resent' => 'Bestätigungs-E-Mail wurde erneut versendet, bitte überprüfen Sie Ihren Posteingang.', - 'email_confirm_thanks' => 'Thanks for confirming!', - 'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.', + 'email_confirm_thanks' => 'Vielen Dank für das Bestätigen!', + 'email_confirm_thanks_desc' => 'Bitte warten Sie einen Augenblick, während Ihre Bestätigung bearbeitet wird. Wenn Sie nach 3 Sekunden nicht weitergeleitet werden, drücken Sie unten den "Weiter" Link, um fortzufahren.', 'email_not_confirmed' => 'E-Mail-Adresse ist nicht bestätigt', 'email_not_confirmed_text' => 'Ihre E-Mail-Adresse ist bisher nicht bestätigt.', diff --git a/resources/lang/de/common.php b/resources/lang/de/common.php index 3ca22701a..d640fe6a4 100644 --- a/resources/lang/de/common.php +++ b/resources/lang/de/common.php @@ -25,7 +25,7 @@ return [ 'actions' => 'Aktionen', 'view' => 'Anzeigen', 'view_all' => 'Alle anzeigen', - 'new' => 'New', + 'new' => 'Neu', 'create' => 'Erstellen', 'update' => 'Aktualisieren', 'edit' => 'Bearbeiten', @@ -81,14 +81,14 @@ return [ 'none' => 'Nichts', // Header - 'homepage' => 'Homepage', + 'homepage' => 'Startseite', 'header_menu_expand' => 'Header-Menü erweitern', 'profile_menu' => 'Profilmenü', 'view_profile' => 'Profil ansehen', 'edit_profile' => 'Profil bearbeiten', 'dark_mode' => 'Dunkler Modus', 'light_mode' => 'Heller Modus', - 'global_search' => 'Global Search', + 'global_search' => 'Globale Suche', // Layout tabs 'tab_info' => 'Info', diff --git a/resources/lang/de/editor.php b/resources/lang/de/editor.php index 72ede3377..ae7a99775 100644 --- a/resources/lang/de/editor.php +++ b/resources/lang/de/editor.php @@ -66,7 +66,7 @@ return [ 'insert_link_title' => 'Link einfügen/ändern', 'insert_horizontal_line' => 'Horizontale Linie einfügen', 'insert_code_block' => 'Code-Block einfügen', - 'edit_code_block' => 'Edit code block', + 'edit_code_block' => 'Code-Block bearbeiten', 'insert_drawing' => 'Zeichnung einfügen/ändern', 'drawing_manager' => 'Zeichnungsmanager', 'insert_media' => 'Medien einfügen/ändern', @@ -144,11 +144,11 @@ return [ 'url' => 'URL', 'text_to_display' => 'Anzuzeigender Text', 'title' => 'Titel', - 'open_link' => 'Open link', - 'open_link_in' => 'Open link in...', + 'open_link' => 'Link öffnen', + 'open_link_in' => 'Link öffnen in...', 'open_link_current' => 'Aktuelles Fenster', 'open_link_new' => 'Neues Fenster', - 'remove_link' => 'Remove link', + 'remove_link' => 'Link entfernen', 'insert_collapsible' => 'Einklappbarer Block einfügen', 'collapsible_unwrap' => 'Auspacken', 'edit_label' => 'Label bearbeiten', diff --git a/resources/lang/de/entities.php b/resources/lang/de/entities.php index dad280c69..77192cb26 100644 --- a/resources/lang/de/entities.php +++ b/resources/lang/de/entities.php @@ -50,7 +50,7 @@ return [ 'permissions_role_everyone_else' => 'Alle anderen', 'permissions_role_everyone_else_desc' => 'Berechtigungen für alle Rollen setzen, die nicht explizit überschrieben wurden.', 'permissions_role_override' => 'Berechtigungen für Rolle überschreiben', - 'permissions_inherit_defaults' => 'Inherit defaults', + 'permissions_inherit_defaults' => 'Standardeinstellungen vererben', // Search 'search_results' => 'Suchergebnisse', @@ -224,8 +224,8 @@ return [ 'pages_md_insert_image' => 'Bild einfügen', 'pages_md_insert_link' => 'Link zu einem Objekt einfügen', 'pages_md_insert_drawing' => 'Zeichnung einfügen', - 'pages_md_show_preview' => 'Show preview', - 'pages_md_sync_scroll' => 'Sync preview scroll', + 'pages_md_show_preview' => 'Vorschau anzeigen', + 'pages_md_sync_scroll' => 'Vorschau synchronisieren', 'pages_not_in_chapter' => 'Seite ist in keinem Kapitel', 'pages_move' => 'Seite verschieben', 'pages_move_success' => 'Seite nach ":parentName" verschoben', @@ -236,14 +236,14 @@ return [ 'pages_permissions_success' => 'Seiten Berechtigungen aktualisiert', 'pages_revision' => 'Version', 'pages_revisions' => 'Seitenversionen', - 'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.', + 'pages_revisions_desc' => 'Alle vorherhigen Revisionen dieser Seite sind unten aufgelistet. Sie können zurückschauen, vergleichen und alte Seitenversionen wiederherstellen, wenn die Berechtigungen dies erlauben. Der vollständige Verlauf der Seite kann hier möglicherweise nicht vollständig wiedergegeben werden, da je nach Systemkonfiguration alte Revisionen automatisch hätten gelöscht werden können.', 'pages_revisions_named' => 'Seitenversionen von ":pageName"', 'pages_revision_named' => 'Seitenversion von ":pageName"', 'pages_revision_restored_from' => 'Wiederhergestellt von #:id; :summary', 'pages_revisions_created_by' => 'Erstellt von', 'pages_revisions_date' => 'Versionsdatum', 'pages_revisions_number' => '#', - 'pages_revisions_sort_number' => 'Revision Number', + 'pages_revisions_sort_number' => 'Revisionsnummer', 'pages_revisions_numbered' => 'Revision #:id', 'pages_revisions_numbered_changes' => 'Revision #:id Änderungen', 'pages_revisions_editor' => 'Editor-Typ', @@ -280,7 +280,7 @@ return [ 'shelf_tags' => 'Regal-Schlagwörter', 'tag' => 'Schlagwort', 'tags' => 'Schlagwörter', - 'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.', + 'tags_index_desc' => 'Tags können auf Inhalte im System angewendet werden, um eine flexible Form der Kategorisierung anzuwenden. Tags können sowohl einen Schlüssel als auch einen Wert haben, wobei der Wert optional ist. Einmal angewendet, können Inhalte unter Verwendung des Tag-Namens und Wertes abgefragt werden.', 'tag_name' => 'Schlagwort Name', 'tag_value' => 'Inhalt (Optional)', 'tags_explain' => "Fügen Sie Schlagwörter hinzu, um Ihren Inhalt zu kategorisieren.\nSie können einen erklärenden Inhalt hinzufügen, um eine genauere Unterteilung vorzunehmen.", diff --git a/resources/lang/de/preferences.php b/resources/lang/de/preferences.php index cc757aa9a..31ae2272d 100644 --- a/resources/lang/de/preferences.php +++ b/resources/lang/de/preferences.php @@ -6,12 +6,12 @@ return [ 'shortcuts' => 'Tastenkürzel', - 'shortcuts_interface' => 'Interface Keyboard Shortcuts', - 'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.', - 'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.', - 'shortcuts_toggle_label' => 'Keyboard shortcuts enabled', + 'shortcuts_interface' => 'Oberflächen-Tastaturkürzel', + 'shortcuts_toggle_desc' => 'Hier können Sie Tastaturkürzel für die Systemoberfläche für Navigation und Aktionen aktivieren oder deaktivieren.', + 'shortcuts_customize_desc' => 'Unten können Sie alle Tastenkürzel anpassen. Drücken Sie einfach die gewünschte Tastenkombination, nachdem Sie die Eingabe für eine Tastenkombination ausgewählt haben.', + 'shortcuts_toggle_label' => 'Tastaturkürzel aktiviert', 'shortcuts_section_navigation' => 'Navigation', - 'shortcuts_section_actions' => 'Gemeinsame Aktionen', + 'shortcuts_section_actions' => 'Häufige Aktionen', 'shortcuts_save' => 'Tastenkürzel speichern', 'shortcuts_overlay_desc' => 'Hinweis: Wenn Tastenkürzel aktiviert sind, ist ein Hilfsoverlay durch Drücken von "?" verfügbar, welches die verfügbaren Tastenkürzel für Aktionen hervorhebt, die aktuell auf dem Bildschirm sichtbar sind.', 'shortcuts_update_success' => 'Tastenkürzel Einstellungen wurden aktualisiert!', diff --git a/resources/lang/de/settings.php b/resources/lang/de/settings.php index 9d449923e..7842034b5 100644 --- a/resources/lang/de/settings.php +++ b/resources/lang/de/settings.php @@ -136,11 +136,11 @@ Hinweis: Benutzer können ihre E-Mail-Adresse nach erfolgreicher Registrierung // Role Settings 'roles' => 'Rollen', 'role_user_roles' => 'Benutzer-Rollen', - 'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.', - 'roles_x_users_assigned' => '1 user assigned|:count users assigned', - 'roles_x_permissions_provided' => '1 permission|:count permissions', - 'roles_assigned_users' => 'Assigned Users', - 'roles_permissions_provided' => 'Provided Permissions', + 'roles_index_desc' => 'Rollen werden verwendet, um Benutzer zu gruppieren System-Berechtigung für ihre Mitglieder zuzuweisen. Wenn ein Benutzer Mitglied mehrerer Rollen ist, stapeln die gewährten Berechtigungen und der Benutzer wird alle Fähigkeiten erben.', + 'roles_x_users_assigned' => '1 Benutzer zugewiesen|:count Benutzer zugewiesen', + 'roles_x_permissions_provided' => '1 Berechtigung|:count Berechtigungen', + 'roles_assigned_users' => 'Zugewiesene Benutzer', + 'roles_permissions_provided' => 'Genutzte Berechtigungen', 'role_create' => 'Neue Rolle anlegen', 'role_create_success' => 'Rolle erfolgreich angelegt', 'role_delete' => 'Rolle löschen', @@ -180,7 +180,7 @@ Hinweis: Benutzer können ihre E-Mail-Adresse nach erfolgreicher Registrierung // Users 'users' => 'Benutzer', - 'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.', + 'users_index_desc' => 'Erstellen und Verwalten Sie individuelle Benutzerkonten innerhalb des Systems. Benutzerkonten werden zur Anmeldung und Besitz von Inhalten und Aktivitäten verwendet. Zugriffsberechtigungen sind in erster Linie rollenbasiert, aber Besitz von Benutzerinhalten kann unter anderem auch Berechtigungen beeinflussen.', 'user_profile' => 'Benutzerprofil', 'users_add_new' => 'Benutzer hinzufügen', 'users_search' => 'Benutzer suchen', @@ -250,8 +250,8 @@ Hinweis: Benutzer können ihre E-Mail-Adresse nach erfolgreicher Registrierung // Webhooks 'webhooks' => 'Webhooks', - 'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.', - 'webhooks_x_trigger_events' => '1 trigger event|:count trigger events', + 'webhooks_index_desc' => 'Webhooks sind eine Möglichkeit, Daten an externe URLs zu senden, wenn bestimmte Aktionen und Ereignisse im System auftreten, was eine ereignisbasierte Integration mit externen Plattformen wie Messaging- oder Benachrichtigungssystemen ermöglicht.', + 'webhooks_x_trigger_events' => '1 Triggerereignis|:count Triggerereignisse', 'webhooks_create' => 'Neuen Webhook erstellen', 'webhooks_none_created' => 'Es wurden noch keine Webhooks erstellt.', 'webhooks_edit' => 'Webhook bearbeiten', diff --git a/resources/lang/de_informal/auth.php b/resources/lang/de_informal/auth.php index 5e7dca202..b133df49e 100644 --- a/resources/lang/de_informal/auth.php +++ b/resources/lang/de_informal/auth.php @@ -30,10 +30,10 @@ return [ 'dont_have_account' => 'Noch kein Konto erstellt?', 'social_login' => 'Mit Sozialem Netzwerk anmelden', 'social_registration' => 'Mit Sozialem Netzwerk registrieren', - 'social_registration_text' => 'Mit einer dieser Dienste registrieren oder anmelden', + 'social_registration_text' => 'Mit einem dieser Dienste registrieren oder anmelden', 'register_thanks' => 'Vielen Dank für deine Registrierung!', - 'register_confirm' => 'Bitte prüfe Deinen Posteingang und bestätig die Registrierung.', + 'register_confirm' => 'Bitte prüfe deinen Posteingang und bestätige die Registrierung.', 'registrations_disabled' => 'Eine Registrierung ist momentan nicht möglich', 'registration_email_domain_invalid' => 'Du kannst dich mit dieser E-Mail nicht registrieren.', 'register_success' => 'Vielen Dank für deine Registrierung! Du bist jetzt registriert und eingeloggt.', @@ -45,12 +45,12 @@ return [ // Password Reset 'reset_password' => 'Passwort vergessen', - 'reset_password_send_instructions' => 'Bitte gib Deine E-Mail-Adresse ein. Danach erhältst Du eine E-Mail mit einem Link zum Zurücksetzen Deines Passwortes.', + 'reset_password_send_instructions' => 'Bitte gib Deine E-Mail-Adresse ein. Danach erhältst Du eine E-Mail mit einem Link zum Zurücksetzen deines Passwortes.', 'reset_password_send_button' => 'Passwort zurücksetzen', 'reset_password_sent' => 'Ein Link zum Zurücksetzen des Passworts wird an :email gesendet, wenn diese E-Mail-Adresse im System gefunden wird.', 'reset_password_success' => 'Dein Passwort wurde erfolgreich zurückgesetzt.', 'email_reset_subject' => 'Passwort zurücksetzen für :appName', - 'email_reset_text' => 'Du erhältst diese E-Mail, weil jemand versucht hat, Dein Passwort zurückzusetzen.', + 'email_reset_text' => 'Du erhältst diese E-Mail, weil jemand versucht hat, dein Passwort zurückzusetzen.', 'email_reset_not_requested' => 'Wenn du das Zurücksetzen des Passworts nicht angefordert hast, ist keine weitere Aktion erforderlich.', // Email Confirmation @@ -58,15 +58,15 @@ return [ 'email_confirm_greeting' => 'Danke, dass Du dich für :appName registrierst hast!', 'email_confirm_text' => 'Bitte bestätige Deine E-Mail-Adresse, indem Du auf die Schaltfläche klickst:', 'email_confirm_action' => 'E-Mail-Adresse bestätigen', - 'email_confirm_send_error' => 'Leider konnte die für die Registrierung notwendige E-Mail zur Bestätigung Deiner E-Mail-Adresse nicht versandt werden. Bitte kontaktiere den Systemadministrator!', - 'email_confirm_success' => 'Ihre E-Mail wurde bestätigt! Sie sollten nun in der Lage sein, sich mit dieser E-Mail-Adresse anzumelden.', - 'email_confirm_resent' => 'Bestätigungs-E-Mail wurde erneut versendet, bitte überprüfe Deinen Posteingang.', - 'email_confirm_thanks' => 'Thanks for confirming!', - 'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.', + 'email_confirm_send_error' => 'Leider konnte die für die Registrierung notwendige E-Mail zur Bestätigung deiner E-Mail-Adresse nicht versandt werden. Bitte kontaktiere deinen Systemadministrator!', + 'email_confirm_success' => 'Deine E-Mail Adresse wurde bestätigt! Du solltest nun in der Lage sein, dich mit deiner E-Mail-Adresse anzumelden.', + 'email_confirm_resent' => 'Bestätigungs-E-Mail wurde erneut versendet, bitte überprüfe deinen Posteingang.', + 'email_confirm_thanks' => 'Vielen Dank für das Bestätigen!', + 'email_confirm_thanks_desc' => 'Bitte warte einen Augenblick, während deine Bestätigung bearbeitet wird. Wenn Du nach 3 Sekunden nicht weitergeleitet wirst, drücke unten den "Weiter" Link, um fortzufahren.', 'email_not_confirmed' => 'E-Mail-Adresse ist nicht bestätigt', 'email_not_confirmed_text' => 'Deine E-Mail-Adresse ist bisher nicht bestätigt.', - 'email_not_confirmed_click_link' => 'Bitte klicke auf den Link in der E-Mail, die Du nach der Registrierung erhalten hast.', + 'email_not_confirmed_click_link' => 'Bitte klicke auf den Link in der E-Mail, die du nach der Registrierung erhalten hast.', 'email_not_confirmed_resend' => 'Wenn Du die E-Mail nicht erhalten hast, kannst Du die Nachricht erneut anfordern. Fülle hierzu bitte das folgende Formular aus:', 'email_not_confirmed_resend_button' => 'Bestätigungs-E-Mail erneut senden', @@ -76,30 +76,30 @@ return [ 'user_invite_email_text' => 'Klicke auf die Schaltfläche unten, um ein Passwort festzulegen und Zugriff zu erhalten:', 'user_invite_email_action' => 'Konto-Passwort festlegen', 'user_invite_page_welcome' => 'Willkommen bei :appName!', - 'user_invite_page_text' => 'Um die Anmeldung abzuschließen und Zugriff auf :appName zu bekommen muss noch ein Passwort festgelegt werden. Dieses wird in Zukunft zum Einloggen benötigt.', + 'user_invite_page_text' => 'Um die Anmeldung abzuschließen und Zugriff auf :appName zu bekommen, muss noch ein Passwort festgelegt werden. Dieses wird in Zukunft für die Anmeldung benötigt.', 'user_invite_page_confirm_button' => 'Passwort bestätigen', - 'user_invite_success_login' => 'Passwort gesetzt, Sie sollten nun in der Lage sein, sich mit Ihrem Passwort an :appName anzumelden!', + 'user_invite_success_login' => 'Passwort gesetzt, du solltest nun in der Lage sein, dich mit deinem Passwort an :appName anzumelden!', // Multi-factor Authentication 'mfa_setup' => 'Multi-Faktor-Authentifizierung einrichten', - 'mfa_setup_desc' => 'Richten Sie Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für Ihr Benutzerkonto ein.', + 'mfa_setup_desc' => 'Richte eine Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für dein Benutzerkonto ein.', 'mfa_setup_configured' => 'Bereits konfiguriert', 'mfa_setup_reconfigure' => 'Umkonfigurieren', - 'mfa_setup_remove_confirmation' => 'Sind Sie sicher, dass Sie diese Multi-Faktor-Authentifizierungsmethode entfernen möchten?', + 'mfa_setup_remove_confirmation' => 'Bist du sicher, dass du diese Multi-Faktor-Authentifizierungsmethode entfernen möchtest?', 'mfa_setup_action' => 'Einrichtung', - 'mfa_backup_codes_usage_limit_warning' => 'Sie haben weniger als 5 Backup-Codes übrig, Bitte erstellen und speichern Sie ein neues set bevor Sie keine codes mehr haben, um zu verhindern, dass Sie von Ihrem konto gesperrt werden.', + 'mfa_backup_codes_usage_limit_warning' => 'Du hast weniger als 5 Backup-Codes übrig. Bitte erstelle und speichere einen neuen Satz, bevor Du keine Codes mehr hast, um zu verhindern, dass du von deinem Konto ausgesperrt wirst.', 'mfa_option_totp_title' => 'Mobile App', - 'mfa_option_totp_desc' => 'Um Mehrfach-Faktor-Authentifizierung nutzen zu können, benötigen Sie eine mobile Anwendung, die TOTP unterstützt, wie Google Authenticator, Authy oder Microsoft Authenticator.', + 'mfa_option_totp_desc' => 'Um Mehrfach-Faktor-Authentifizierung nutzen zu können, benötigst du eine mobile Anwendung, die TOTP unterstützt, wie Google Authenticator, Authy oder Microsoft Authenticator.', 'mfa_option_backup_codes_title' => 'Backup Code', - 'mfa_option_backup_codes_desc' => 'Speichern Sie sicher eine reihe von einmaligen Backup-Codes, die Sie eingeben können, um ihre identität zu überprüfen.', + 'mfa_option_backup_codes_desc' => 'Speichere eine Reihe von einmaligen Backup-Codes an einem sicheren Ort. Du kannst damit deine identität bestätigen.', 'mfa_gen_confirm_and_enable' => 'Bestätigen und aktivieren', 'mfa_gen_backup_codes_title' => 'Backup-Codes einrichten', - 'mfa_gen_backup_codes_desc' => 'Speichern Sie die folgende Liste der Codes an einem sicheren Ort. Wenn Sie auf das System zugreifen, können Sie einen der Codes als zweiten Authentifizierungsmechanismus verwenden.', - 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_desc' => 'Speichere die folgende Liste der Codes an einem sicheren Ort. Wenn du auf das System zugreifst, kannst du einen der Codes als zweiten Authentifizierungsmechanismus verwenden.', + 'mfa_gen_backup_codes_download' => 'Codes herunterladen', 'mfa_gen_backup_codes_usage_warning' => 'Jeder Code kann nur einmal verwendet werden', 'mfa_gen_totp_title' => 'Mobile App einrichten', - 'mfa_gen_totp_desc' => 'Um Mehrfach-Faktor-Authentifizierung nutzen zu können, benötigen Sie eine mobile Anwendung, die TOTP unterstützt, wie Google Authenticator, Authy oder Microsoft Authenticator.', - 'mfa_gen_totp_scan' => 'Scannen Sie den QR-Code unten mit ihrer bevorzugten Authentifizierungs-App, um loszulegen.', + 'mfa_gen_totp_desc' => 'Um Mehrfach-Faktor-Authentifizierung nutzen zu können, benötigst du eine mobile Anwendung, die TOTP unterstützt, wie Google Authenticator, Authy oder Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scanne den QR-Code unten mit deiner bevorzugten Authentifizierungs-App, um zu beginnen.', 'mfa_gen_totp_verify_setup' => 'Setup überprüfen', 'mfa_gen_totp_verify_setup_desc' => 'Überprüfe, dass alles funktioniert, indem du einen Code aus deiner Authentifizierungs-App in das Eingabefeld unten eingibst:', 'mfa_gen_totp_provide_code_here' => 'Gib hier den von der App generierten Code ein', diff --git a/resources/lang/de_informal/common.php b/resources/lang/de_informal/common.php index 80ba2fb1b..1d53a7da9 100644 --- a/resources/lang/de_informal/common.php +++ b/resources/lang/de_informal/common.php @@ -25,7 +25,7 @@ return [ 'actions' => 'Aktionen', 'view' => 'Anzeigen', 'view_all' => 'Alle anzeigen', - 'new' => 'New', + 'new' => 'Neu', 'create' => 'Anlegen', 'update' => 'Aktualisieren', 'edit' => 'Bearbeiten', @@ -81,14 +81,14 @@ return [ 'none' => 'Keine', // Header - 'homepage' => 'Homepage', + 'homepage' => 'Startseite', 'header_menu_expand' => 'Header-Menü erweitern', 'profile_menu' => 'Profilmenü', 'view_profile' => 'Profil ansehen', 'edit_profile' => 'Profil bearbeiten', 'dark_mode' => 'Dunkler Modus', 'light_mode' => 'Heller Modus', - 'global_search' => 'Global Search', + 'global_search' => 'Globale Suche', // Layout tabs 'tab_info' => 'Info', diff --git a/resources/lang/de_informal/components.php b/resources/lang/de_informal/components.php index 56060ea23..2a9782fca 100644 --- a/resources/lang/de_informal/components.php +++ b/resources/lang/de_informal/components.php @@ -14,7 +14,7 @@ return [ 'image_uploaded' => 'Hochgeladen am :uploadedDate', 'image_load_more' => 'Mehr', 'image_image_name' => 'Bildname', - 'image_delete_used' => 'Dieses Bild wird auf den folgenden Seiten benutzt. ', + 'image_delete_used' => 'Dieses Bild wird auf den folgenden Seiten benutzt.', 'image_delete_confirm_text' => 'Bist Du sicher, dass Du diese Seite löschen möchtest?', 'image_select_image' => 'Bild auswählen', 'image_dropzone' => 'Ziehe Bilder hierher oder klicke hier, um ein Bild auszuwählen', diff --git a/resources/lang/de_informal/editor.php b/resources/lang/de_informal/editor.php index 213380b7f..314961088 100644 --- a/resources/lang/de_informal/editor.php +++ b/resources/lang/de_informal/editor.php @@ -66,7 +66,7 @@ return [ 'insert_link_title' => 'Link einfügen/bearbeiten', 'insert_horizontal_line' => 'Horizontale Linie einfügen', 'insert_code_block' => 'Codeblock einfügen', - 'edit_code_block' => 'Edit code block', + 'edit_code_block' => 'Codeblock bearbeiten', 'insert_drawing' => 'Zeichnung einfügen/bearbeiten', 'drawing_manager' => 'Zeichnungsmanager', 'insert_media' => 'Medien einfügen/bearbeiten', @@ -121,7 +121,7 @@ return [ 'paste_column_before' => 'Spalte davor einfügen', 'paste_column_after' => 'Spalte danach einfügen', 'cell_padding' => 'Zellenabstand', - 'cell_spacing' => 'Zellen-Außenabstand', + 'cell_spacing' => 'Zellenaußenabstand', 'caption' => 'Überschrift', 'show_caption' => 'Überschrift anzeigen', 'constrain' => 'Proportionen festsetzen', @@ -144,11 +144,11 @@ return [ 'url' => 'URL', 'text_to_display' => 'Anzuzeigender Text', 'title' => 'Titel', - 'open_link' => 'Open link', - 'open_link_in' => 'Open link in...', + 'open_link' => 'Link öffnen', + 'open_link_in' => 'Link öffnen in...', 'open_link_current' => 'Aktuellem Fenster', 'open_link_new' => 'Neuem Fenster', - 'remove_link' => 'Remove link', + 'remove_link' => 'Link entfernen', 'insert_collapsible' => 'Einklappbaren Block einfügen', 'collapsible_unwrap' => 'Entfernen', 'edit_label' => 'Beschriftung bearbeiten', diff --git a/resources/lang/de_informal/entities.php b/resources/lang/de_informal/entities.php index f6a87363b..76d7c6bb7 100644 --- a/resources/lang/de_informal/entities.php +++ b/resources/lang/de_informal/entities.php @@ -38,11 +38,11 @@ return [ 'export_html' => 'HTML-Datei', 'export_pdf' => 'PDF-Datei', 'export_text' => 'Textdatei', - 'export_md' => 'Markdown-Dateir', + 'export_md' => 'Markdown-Datei', // Permissions and restrictions 'permissions' => 'Berechtigungen', - 'permissions_desc' => 'Legen Sie hier Berechtigungen fest, um die Standardberechtigungen von Benutzerrollen zu überschreiben.', + 'permissions_desc' => 'Lege hier Berechtigungen fest, um die Standardberechtigungen von Benutzerrollen zu überschreiben.', 'permissions_book_cascade' => 'In Büchern festgelegte Berechtigungen werden automatisch in untergeordnete Kapitel und Seiten kaskadiert, es sei denn, sie haben eigene Berechtigungen definiert.', 'permissions_chapter_cascade' => 'In Kapiteln festgelegte Berechtigungen werden automatisch in untergeordnete Seiten kaskadiert, es sei denn, sie haben eigene Berechtigungen definiert.', 'permissions_save' => 'Berechtigungen speichern', @@ -50,7 +50,7 @@ return [ 'permissions_role_everyone_else' => 'Alle anderen', 'permissions_role_everyone_else_desc' => 'Berechtigungen für alle Rollen setzen, die nicht explizit überschrieben wurden.', 'permissions_role_override' => 'Berechtigungen für Rolle überschreiben', - 'permissions_inherit_defaults' => 'Inherit defaults', + 'permissions_inherit_defaults' => 'Standardeinstellungen vererben', // Search 'search_results' => 'Suchergebnisse', @@ -71,7 +71,7 @@ return [ 'search_created_by_me' => 'Von mir erstellt', 'search_updated_by_me' => 'Von mir aktualisiert', 'search_owned_by_me' => 'Besitzt von mir', - 'search_date_options' => 'Datums Optionen', + 'search_date_options' => 'Datumsoptionen', 'search_updated_before' => 'Aktualisiert vor', 'search_updated_after' => 'Aktualisiert nach', 'search_created_before' => 'Erstellt vor', @@ -93,7 +93,7 @@ return [ 'shelves_save' => 'Regal speichern', 'shelves_books' => 'Bücher in diesem Regal', 'shelves_add_books' => 'Buch zu diesem Regal hinzufügen', - 'shelves_drag_books' => 'Ziehen die Bücher nach unten, um sie zu diesem Regal hinzuzufügen', + 'shelves_drag_books' => 'Ziehe die Bücher nach unten, um sie zu diesem Regal hinzuzufügen', 'shelves_empty_contents' => 'Diesem Regal sind keine Bücher zugewiesen', 'shelves_edit_and_assign' => 'Regal bearbeiten um Bücher hinzuzufügen', 'shelves_edit_named' => 'Regal :name bearbeiten', @@ -126,7 +126,7 @@ return [ 'books_delete' => 'Buch löschen', 'books_delete_named' => 'Buch ":bookName" löschen', 'books_delete_explain' => 'Das Buch ":bookName" wird gelöscht und alle zugehörigen Kapitel und Seiten entfernt.', - 'books_delete_confirmation' => 'Bist Du sicher, dass Du dieses Buch löschen möchtest?', + 'books_delete_confirmation' => 'Bist Du sicher, dass du dieses Buch löschen möchtest?', 'books_edit' => 'Buch bearbeiten', 'books_edit_named' => 'Buch ":bookName" bearbeiten', 'books_form_book_name' => 'Name des Buches', @@ -161,8 +161,8 @@ return [ 'chapters_create' => 'Neues Kapitel anlegen', 'chapters_delete' => 'Kapitel entfernen', 'chapters_delete_named' => 'Kapitel ":chapterName" entfernen', - 'chapters_delete_explain' => 'Hiermit löschen Sie das Kapitel mit dem Namen \':chapterName\'. Alle Seiten, die innerhalb dieses Kapitels existieren, werden ebenfalls gelöscht.', - 'chapters_delete_confirm' => 'Bist Du sicher, dass Du dieses Kapitel löschen möchtest?', + 'chapters_delete_explain' => 'Hiermit löscht du das Kapitel mit dem Namen \':chapterName\'. Alle Seiten, die innerhalb dieses Kapitels existieren, werden ebenfalls gelöscht.', + 'chapters_delete_confirm' => 'Bist du sicher, dass du dieses Kapitel löschen möchtest?', 'chapters_edit' => 'Kapitel bearbeiten', 'chapters_edit_named' => 'Kapitel ":chapterName" bearbeiten', 'chapters_save' => 'Kapitel speichern', @@ -192,8 +192,8 @@ return [ 'pages_delete_draft' => 'Seitenentwurf löschen', 'pages_delete_success' => 'Seite gelöscht', 'pages_delete_draft_success' => 'Seitenentwurf gelöscht', - 'pages_delete_confirm' => 'Bist Du sicher, dass Du diese Seite löschen möchtest?', - 'pages_delete_draft_confirm' => 'Bist Du sicher, dass Du diesen Seitenentwurf löschen möchtest?', + 'pages_delete_confirm' => 'Bist du sicher, dass du diese Seite löschen möchtest?', + 'pages_delete_draft_confirm' => 'Bist du sicher, dass du diesen Seitenentwurf löschen möchtest?', 'pages_editing_named' => 'Seite ":pageName" bearbeiten', 'pages_edit_draft_options' => 'Entwurfsoptionen', 'pages_edit_save_draft' => 'Entwurf speichern', @@ -208,11 +208,11 @@ return [ 'pages_edit_switch_to_markdown_stable' => '(Stabiler Inhalt)', 'pages_edit_switch_to_wysiwyg' => 'Zum WYSIWYG Editor wechseln', 'pages_edit_set_changelog' => 'Änderungsprotokoll hinzufügen', - 'pages_edit_enter_changelog_desc' => 'Bitte gib eine kurze Zusammenfassung Deiner Änderungen ein', + 'pages_edit_enter_changelog_desc' => 'Bitte gib eine kurze Zusammenfassung deiner Änderungen ein', 'pages_edit_enter_changelog' => 'Änderungsprotokoll eingeben', 'pages_editor_switch_title' => 'Editor wechseln', 'pages_editor_switch_are_you_sure' => 'Bist du dir sicher, dass du den Editor für diese Seite ändern willst?', - 'pages_editor_switch_consider_following' => 'Beachte beim Wechsel des Editors Folgendes:', + 'pages_editor_switch_consider_following' => 'Beachte beim Wechsel des Editors folgendes:', 'pages_editor_switch_consideration_a' => 'Nach dem Speichern wird der neue Editor von allen zukünftigen Bearbeitern verwendet, auch von denen, die den Editortyp nicht selbst ändern können.', 'pages_editor_switch_consideration_b' => 'Dies kann unter Umständen zu einem Verlust von Details und Syntax führen.', 'pages_editor_switch_consideration_c' => 'Tag- oder Changelog-Änderungen, die seit dem letzten Speichern vorgenommen wurden, bleiben bei dieser Änderung nicht erhalten.', @@ -224,8 +224,8 @@ return [ 'pages_md_insert_image' => 'Bild einfügen', 'pages_md_insert_link' => 'Link zu einem Objekt einfügen', 'pages_md_insert_drawing' => 'Zeichnung einfügen', - 'pages_md_show_preview' => 'Show preview', - 'pages_md_sync_scroll' => 'Sync preview scroll', + 'pages_md_show_preview' => 'Vorschau anzeigen', + 'pages_md_sync_scroll' => 'Vorschau synchronisieren', 'pages_not_in_chapter' => 'Seite ist in keinem Kapitel', 'pages_move' => 'Seite verschieben', 'pages_move_success' => 'Seite nach ":parentName" verschoben', @@ -236,14 +236,14 @@ return [ 'pages_permissions_success' => 'Seiten Berechtigungen aktualisiert', 'pages_revision' => 'Version', 'pages_revisions' => 'Seitenversionen', - 'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.', + 'pages_revisions_desc' => 'Alle vorherhigen Revisionen dieser Seite sind unten aufgelistet. Du kannst zurückschauen, vergleichen und alte Seitenversionen wiederherstellen, wenn die Berechtigungen dies erlauben. Der vollständige Verlauf der Seite kann hier möglicherweise nicht vollständig wiedergegeben werden, da je nach Systemkonfiguration alte Revisionen automatisch gelöscht werden könnten.', 'pages_revisions_named' => 'Seitenversionen von ":pageName"', 'pages_revision_named' => 'Seitenversion von ":pageName"', 'pages_revision_restored_from' => 'Wiederhergestellt von #:id; :summary', 'pages_revisions_created_by' => 'Erstellt von', 'pages_revisions_date' => 'Versionsdatum', 'pages_revisions_number' => '#', - 'pages_revisions_sort_number' => 'Revision Number', + 'pages_revisions_sort_number' => 'Revisionsnummer', 'pages_revisions_numbered' => 'Revision #:id', 'pages_revisions_numbered_changes' => 'Revision #:id Änderungen', 'pages_revisions_editor' => 'Editortyp', @@ -260,7 +260,7 @@ return [ 'pages_references_update_revision' => 'Automatische Systemaktualisierung interner Links', 'pages_initial_name' => 'Neue Seite', 'pages_editing_draft_notification' => 'Du bearbeitest momenten einen Entwurf, der zuletzt :timeDiff gespeichert wurde.', - 'pages_draft_edited_notification' => 'Diese Seite wurde seit diesem Zeitpunkt verändert. Wir empfehlen Ihnen, diesen Entwurf zu verwerfen.', + 'pages_draft_edited_notification' => 'Diese Seite wurde seit diesem Zeitpunkt verändert. Wir empfehlen diesen Entwurf zu verwerfen.', 'pages_draft_page_changed_since_creation' => 'Diese Seite wurde seit der Erstellung dieses Entwurfs aktualisiert. Es wird empfohlen, diesen Entwurf zu verwerfen oder darauf zu achten, dass keine Seitenänderungen überschrieben werden.', 'pages_draft_edit_active' => [ 'start_a' => ':count Benutzer bearbeiten derzeit diese Seite.', @@ -280,7 +280,7 @@ return [ 'shelf_tags' => 'Regal-Schlagwörter', 'tag' => 'Schlagwort', 'tags' => 'Schlagwörter', - 'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.', + 'tags_index_desc' => 'Tags können auf Inhalte im System angewendet werden, um eine flexible Form der Kategorisierung anzuwenden. Tags können sowohl einen Schlüssel als auch einen Wert haben, wobei der Wert optional ist. Einmal angewendet, können Inhalte unter Verwendung des Tag-Namens und Wertes abgefragt werden.', 'tag_name' => 'Schlagwort Name', 'tag_value' => 'Inhalt (Optional)', 'tags_explain' => "Füge Schlagwörter hinzu, um ihren Inhalt zu kategorisieren.\nDu kannst einen erklärenden Inhalt hinzufügen, um eine genauere Unterteilung vorzunehmen.", @@ -297,16 +297,16 @@ return [ 'tags_view_existing_tags' => 'Vorhandene Tags anzeigen', 'tags_list_empty_hint' => 'Tags können über die Seitenleiste des Seiteneditors oder bei der Bearbeitung der Details eines Buches, Kapitels oder Regals vergeben werden.', 'attachments' => 'Anhänge', - 'attachments_explain' => 'Du kannst auf Deiner Seite Dateien hochladen oder Links hinzufügen. Diese werden in der Seitenleiste angezeigt.', + 'attachments_explain' => 'Du kannst auf deiner Seite Dateien hochladen oder Links hinzufügen. Diese werden in der Seitenleiste angezeigt.', 'attachments_explain_instant_save' => 'Änderungen werden direkt gespeichert.', 'attachments_items' => 'Angefügte Elemente', 'attachments_upload' => 'Datei hochladen', 'attachments_link' => 'Link hinzufügen', 'attachments_set_link' => 'Link setzen', - 'attachments_delete' => 'Bist Du sicher, dass Du diesen Anhang löschen möchtest?', + 'attachments_delete' => 'Bist du sicher, dass du diesen Anhang löschen möchtest?', 'attachments_dropzone' => 'Ziehe Dateien hierher oder klicke hier, um eine Datei auszuwählen', 'attachments_no_files' => 'Es wurden bisher keine Dateien hochgeladen.', - 'attachments_explain_link' => 'Wenn Du keine Datei hochladen möchtest, kannst Du stattdessen einen Link hinzufügen. Dieser Link kann auf eine andere Seite oder eine Datei im Internet verweisen.', + 'attachments_explain_link' => 'Wenn du keine Datei hochladen möchtest, kannst du stattdessen einen Link hinzufügen. Dieser Link kann auf eine andere Seite oder eine Datei im Internet verweisen.', 'attachments_link_name' => 'Link-Name', 'attachment_link' => 'Link zum Anhang', 'attachments_link_url' => 'Link zu einer Datei', @@ -341,7 +341,7 @@ return [ 'comment' => 'Kommentar', 'comments' => 'Kommentare', 'comment_add' => 'Kommentieren', - 'comment_placeholder' => 'Gib hier Deine Kommentare ein (Markdown unterstützt)', + 'comment_placeholder' => 'Gib hier deine Kommentare ein (Markdown unterstützt)', 'comment_count' => '{0} Keine Kommentare|{1} 1 Kommentar|[2,*] :count Kommentare', 'comment_save' => 'Kommentar speichern', 'comment_saving' => 'Kommentar wird gespeichert...', @@ -352,12 +352,12 @@ return [ 'comment_deleted_success' => 'Kommentar gelöscht', 'comment_created_success' => 'Kommentar hinzugefügt', 'comment_updated_success' => 'Kommentar aktualisiert', - 'comment_delete_confirm' => 'Möchtst Du diesen Kommentar wirklich löschen?', + 'comment_delete_confirm' => 'Möchtst du diesen Kommentar wirklich löschen?', 'comment_in_reply_to' => 'Antwort auf :commentId', // Revision - 'revision_delete_confirm' => 'Bist Du sicher, dass Du diese Revision löschen möchtest?', - 'revision_restore_confirm' => 'Sind Sie sicher, dass Sie diese Revision wiederherstellen wollen? Der aktuelle Seiteninhalt wird ersetzt.', + 'revision_delete_confirm' => 'Bist du sicher, dass du diese Revision löschen möchtest?', + 'revision_restore_confirm' => 'Bist du sicher, dass du diese Revision wiederherstellen willst? Der aktuelle Seiteninhalt wird ersetzt.', 'revision_delete_success' => 'Revision gelöscht', 'revision_cannot_delete_latest' => 'Die letzte Version kann nicht gelöscht werden.', diff --git a/resources/lang/de_informal/errors.php b/resources/lang/de_informal/errors.php index db75693c8..84942ade2 100644 --- a/resources/lang/de_informal/errors.php +++ b/resources/lang/de_informal/errors.php @@ -14,14 +14,14 @@ return [ 'email_confirmation_invalid' => 'Der Bestätigungslink ist nicht gültig oder wurde bereits verwendet. Bitte registriere dich erneut.', 'email_confirmation_expired' => 'Der Bestätigungslink ist abgelaufen. Es wurde eine neue Bestätigungs-E-Mail gesendet.', 'email_confirmation_awaiting' => 'Die E-Mail-Adresse für das verwendete Konto muss bestätigt werden', - 'ldap_fail_anonymous' => 'Anonymer LDAP-Zugriff ist fehlgeschlafgen', + 'ldap_fail_anonymous' => 'Anonymer LDAP-Zugriff ist fehlgeschlagen', 'ldap_fail_authed' => 'LDAP-Zugriff mit DN und Passwort ist fehlgeschlagen', 'ldap_extension_not_installed' => 'LDAP-PHP-Erweiterung ist nicht installiert', 'ldap_cannot_connect' => 'Die Verbindung zum LDAP-Server ist fehlgeschlagen. Beim initialen Verbindungsaufbau trat ein Fehler auf', 'saml_already_logged_in' => 'Du bist bereits angemeldet', 'saml_user_not_registered' => 'Kein Benutzer mit ID :name registriert und die automatische Registrierung ist deaktiviert', 'saml_no_email_address' => 'Es konnte keine E-Mail-Adresse für diesen Benutzer in den vom externen Authentifizierungssystem zur Verfügung gestellten Daten gefunden werden', - 'saml_invalid_response_id' => 'Die Anfrage vom externen Authentifizierungssystem wird von einem von dieser Anwendung gestarteten Prozess nicht erkannt. Das Zurückgehen nach einem Login könnte dieses Problem verursachen.', + 'saml_invalid_response_id' => 'Die Anfrage vom externen Authentifizierungssystem wird von einem von dieser Anwendung gestarteten Prozess nicht erkannt. Das Zurückblättern nach einem Login könnte dieses Problem verursachen.', 'saml_fail_authed' => 'Anmeldung mit :system fehlgeschlagen, System konnte keine erfolgreiche Autorisierung bereitstellen', 'oidc_already_logged_in' => 'Bereits angemeldet', 'oidc_user_not_registered' => 'Der Benutzer :name ist nicht registriert und die automatische Registrierung ist deaktiviert', @@ -30,13 +30,13 @@ return [ 'social_no_action_defined' => 'Es ist keine Aktion definiert', 'social_login_bad_response' => "Fehler bei :socialAccount Login: \n:error", 'social_account_in_use' => 'Dieses :socialAccount-Konto wird bereits verwendet. Bitte melde dich mit dem :socialAccount-Konto an.', - 'social_account_email_in_use' => 'Die E-Mail-Adresse ":email" ist bereits registriert. Wenn Du bereits registriert bist, kannst Du Dein :socialAccount-Konto in Deinen Profil-Einstellungen verknüpfen.', - 'social_account_existing' => 'Dieses :socialAccount-Konto ist bereits mit Ihrem Profil verknüpft.', + 'social_account_email_in_use' => 'Die E-Mail-Adresse ":email" ist bereits registriert. Wenn du bereits registriert bist, kannst du Dein :socialAccount-Konto in Deinen Profil-Einstellungen verknüpfen.', + 'social_account_existing' => 'Dieses :socialAccount-Konto ist bereits mit deinem Profil verknüpft.', 'social_account_already_used_existing' => 'Dieses :socialAccount-Konto wird bereits von einem anderen Benutzer verwendet.', - 'social_account_not_used' => 'Dieses :socialAccount-Konto ist bisher keinem Benutzer zugeordnet. Du kannst das in Deinen Profil-Einstellungen tun.', - 'social_account_register_instructions' => 'Wenn Du bisher kein Social-Media Konto besitzt, kannst Du ein solches Konto mit der :socialAccount Option anlegen.', + 'social_account_not_used' => 'Dieses :socialAccount-Konto ist bisher keinem Benutzer zugeordnet. Du kannst das in deinen Profil-Einstellungen tun.', + 'social_account_register_instructions' => 'Wenn du bisher kein Social-Media Konto besitzt, kannst du ein solches Konto mit der :socialAccount Option anlegen.', 'social_driver_not_found' => 'Treiber für Social-Media-Konten nicht gefunden', - 'social_driver_not_configured' => 'Ihr :socialAccount-Konto ist nicht korrekt konfiguriert.', + 'social_driver_not_configured' => 'Dein :socialAccount-Konto ist nicht korrekt konfiguriert.', 'invite_token_expired' => 'Dieser Einladungslink ist abgelaufen. Du kannst stattdessen versuchen, dein Passwort zurückzusetzen.', // System @@ -44,7 +44,7 @@ return [ 'cannot_get_image_from_url' => 'Bild konnte nicht von der URL :url geladen werden.', 'cannot_create_thumbs' => 'Der Server kann keine Vorschau-Bilder erzeugen. Bitte prüfe, ob die GD PHP-Erweiterung installiert ist.', 'server_upload_limit' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuche es mit einer kleineren Datei.', - 'uploaded' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuchen Sie es mit einer kleineren Datei.', + 'uploaded' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuche es mit einer kleineren Datei.', 'image_upload_error' => 'Beim Hochladen des Bildes trat ein Fehler auf.', 'image_upload_type_error' => 'Der Bildtyp der hochgeladenen Datei ist ungültig.', 'file_upload_timeout' => 'Der Upload der Datei ist abgelaufen.', @@ -53,7 +53,7 @@ return [ 'attachment_not_found' => 'Anhang konnte nicht gefunden werden.', // Pages - 'page_draft_autosave_fail' => 'Fehler beim Speichern des Entwurfs. Stelle sicher, dass Du mit dem Internet verbunden bist, bevor Du den Entwurf dieser Seite speicherst.', + 'page_draft_autosave_fail' => 'Fehler beim Speichern des Entwurfs. Stelle sicher, dass du mit dem Internet verbunden bist, bevor du den Entwurf dieser Seite speicherst.', 'page_custom_home_deletion' => 'Eine als Startseite gesetzte Seite kann nicht gelöscht werden.', // Entities @@ -74,7 +74,7 @@ return [ 'role_cannot_be_edited' => 'Diese Rolle kann nicht bearbeitet werden.', 'role_system_cannot_be_deleted' => 'Dies ist eine Systemrolle und kann nicht gelöscht werden', 'role_registration_default_cannot_delete' => 'Diese Rolle kann nicht gelöscht werden, solange sie als Standardrolle für neue Registrierungen gesetzt ist', - 'role_cannot_remove_only_admin' => 'Dieser Benutzer ist der einzige Benutzer, welchem die Administratorrolle zugeordnet ist. Ordnen Sie die Administratorrolle einem anderen Benutzer zu, bevor Sie versuchen, sie hier zu entfernen.', + 'role_cannot_remove_only_admin' => 'Dieser Benutzer ist der einzige Benutzer, welchem die Administratorrolle zugeordnet ist. Ordne die Administratorrolle einem anderen Benutzer zu, bevor du versuchst, sie hier zu entfernen.', // Comments 'comment_list' => 'Beim Abrufen der Kommentare ist ein Fehler aufgetreten.', @@ -85,7 +85,7 @@ return [ // Error pages '404_page_not_found' => 'Seite nicht gefunden', - 'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Du angefordert hast, wurde nicht gefunden.', + 'sorry_page_not_found' => 'Entschuldigung. Die Seite, die du angefordert hast, wurde nicht gefunden.', 'sorry_page_not_found_permission_warning' => 'Wenn du erwartet hast, dass diese Seite existiert, hast du möglicherweise nicht die Berechtigung, sie anzuzeigen.', 'image_not_found' => 'Bild nicht gefunden', 'image_not_found_subtitle' => 'Sorry. Die Bilddatei, nach der du suchst, konnte nicht gefunden werden.', @@ -99,7 +99,7 @@ return [ 'api_no_authorization_found' => 'Kein Autorisierungs-Token für die Anfrage gefunden', 'api_bad_authorization_format' => 'Ein Autorisierungs-Token wurde auf die Anfrage gefunden, aber das Format schien falsch zu sein', 'api_user_token_not_found' => 'Es wurde kein passender API-Token für den angegebenen Autorisierungs-Token gefunden', - 'api_incorrect_token_secret' => 'Das für den API-Token angegebene geheimen Token ist falsch', + 'api_incorrect_token_secret' => 'Das für den API-Token angegebene geheime Token ist falsch', 'api_user_no_api_permission' => 'Der Besitzer des verwendeten API-Token hat keine Berechtigung für API-Aufrufe', 'api_user_token_expired' => 'Das verwendete Autorisierungs-Token ist abgelaufen', diff --git a/resources/lang/de_informal/preferences.php b/resources/lang/de_informal/preferences.php index cc757aa9a..5670fcee5 100644 --- a/resources/lang/de_informal/preferences.php +++ b/resources/lang/de_informal/preferences.php @@ -5,14 +5,14 @@ */ return [ - 'shortcuts' => 'Tastenkürzel', - 'shortcuts_interface' => 'Interface Keyboard Shortcuts', - 'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.', - 'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.', - 'shortcuts_toggle_label' => 'Keyboard shortcuts enabled', + 'shortcuts' => 'Kürzel', + 'shortcuts_interface' => 'Oberflächen-Tastaturkürzel', + 'shortcuts_toggle_desc' => 'Hier kannst du Tastaturkürzel für die Systemoberfläche für Navigation und Aktionen aktivieren oder deaktivieren.', + 'shortcuts_customize_desc' => 'Unten kannst du alle Tastenkürzel anpassen. Drücke einfach die gewünschte Tastenkombination, nachdem du die Eingabe für eine Tastenkombination ausgewählt hast.', + 'shortcuts_toggle_label' => 'Tastaturkürzel aktiviert', 'shortcuts_section_navigation' => 'Navigation', - 'shortcuts_section_actions' => 'Gemeinsame Aktionen', + 'shortcuts_section_actions' => 'Häufige Aktionen', 'shortcuts_save' => 'Tastenkürzel speichern', - 'shortcuts_overlay_desc' => 'Hinweis: Wenn Tastenkürzel aktiviert sind, ist ein Hilfsoverlay durch Drücken von "?" verfügbar, welches die verfügbaren Tastenkürzel für Aktionen hervorhebt, die aktuell auf dem Bildschirm sichtbar sind.', + 'shortcuts_overlay_desc' => 'Hinweis: Wenn Tastenkürzel aktiviert sind, ist ein Hilfefähnchen durch Drücken von "?" verfügbar, welches die verfügbaren Tastenkürzel für Aktionen hervorhebt, die aktuell auf dem Bildschirm sichtbar sind.', 'shortcuts_update_success' => 'Tastenkürzel Einstellungen wurden aktualisiert!', ]; \ No newline at end of file diff --git a/resources/lang/de_informal/settings.php b/resources/lang/de_informal/settings.php index a8f12fa0e..f0da529c3 100644 --- a/resources/lang/de_informal/settings.php +++ b/resources/lang/de_informal/settings.php @@ -20,13 +20,13 @@ return [ 'app_name_desc' => 'Dieser Name wird im Header und in E-Mails angezeigt.', 'app_name_header' => 'Anwendungsname im Header anzeigen?', 'app_public_access' => 'Öffentlicher Zugriff', - 'app_public_access_desc' => 'Wenn Sie diese Option aktivieren, können Besucher, die nicht angemeldet sind, auf Inhalte in Ihrer BookStack-Instanz zugreifen.', + 'app_public_access_desc' => 'Wenn du diese Option aktivierst, können Besucher, die nicht angemeldet sind, auf Inhalte in deiner BookStack-Instanz zugreifen.', 'app_public_access_desc_guest' => 'Der Zugang für öffentliche Besucher kann über den Benutzer "Guest" gesteuert werden.', 'app_public_access_toggle' => 'Öffentlichen Zugriff erlauben', 'app_public_viewing' => 'Öffentliche Ansicht erlauben?', 'app_secure_images' => 'Erhöhte Sicherheit für hochgeladene Bilder aktivieren?', 'app_secure_images_toggle' => 'Aktiviere Bild-Upload mit höherer Sicherheit', - 'app_secure_images_desc' => 'Aus Leistungsgründen sind alle Bilder öffentlich sichtbar. Diese Option fügt zufällige, schwer zu eratene, Zeichenketten zu Bild-URLs hinzu. Stellen sie sicher, dass Verzeichnisindizes deaktiviert sind, um einen einfachen Zugriff zu verhindern.', + 'app_secure_images_desc' => 'Aus Leistungsgründen sind alle Bilder öffentlich sichtbar. Diese Option fügt zufällige, schwer zu erratene, Zeichenketten zu Bild-URLs hinzu. Stelle sicher, dass Verzeichnisindizes deaktiviert sind, um einen einfachen Zugriff zu verhindern.', 'app_default_editor' => 'Standard Seiteneditor', 'app_default_editor_desc' => 'Wähle aus, welcher Editor bei der Bearbeitung neuer Seiten standardmäßig verwendet werden soll. Dies kann auf Seitenebene außer Kraft gesetzt werden, sofern die Berechtigungen dies zulassen.', 'app_custom_html' => 'Benutzerdefinierter HTML Inhalt', @@ -37,12 +37,12 @@ return [ Größere Bilder werden verkleinert.', 'app_primary_color' => 'Primäre Anwendungsfarbe', 'app_primary_color_desc' => 'Dies sollte ein HEX Wert sein. -Wenn Du nichts eingibst, wird die Anwendung auf die Standardfarbe zurückgesetzt.', +Wenn du nichts eingibst, wird die Anwendung auf die Standardfarbe zurückgesetzt.', 'app_homepage' => 'Startseite der Anwendung', 'app_homepage_desc' => 'Wähle eine Seite als Startseite aus, die statt der Standardansicht angezeigt werden soll. Seitenberechtigungen werden für die ausgewählten Seiten ignoriert.', - 'app_homepage_select' => 'Wählen Sie eine Seite aus', + 'app_homepage_select' => 'Wähle eine Seite aus', 'app_footer_links' => 'Fußzeilen-Links', - 'app_footer_links_desc' => 'Fügen Sie Links hinzu, die innerhalb der Seitenfußzeile angezeigt werden. Diese werden am unteren Ende der meisten Seiten angezeigt, einschließlich derjenigen, die keinen Login benötigen. Sie können die Bezeichnung "trans::" verwenden, um systemdefinierte Übersetzungen zu verwenden. Beispiel: Mit "trans::common.privacy_policy" wird der übersetzte Text "Privacy Policy" bereitgestellt, und "trans::common.terms_of_service" liefert den übersetzten Text "Terms of Service".', + 'app_footer_links_desc' => 'Füge Links hinzu, die innerhalb der Seitenfußzeile angezeigt werden. Diese werden am unteren Ende der meisten Seiten angezeigt, einschließlich derjenigen, die keinen Login benötigen. Du kannst die Bezeichnung "trans::" verwenden, um systemdefinierte Übersetzungen zu verwenden. Beispiel: Mit "trans::common.privacy_policy" wird der übersetzte Text "Privacy Policy" bereitgestellt, und "trans::common.terms_of_service" liefert den übersetzten Text "Terms of Service".', 'app_footer_links_label' => 'Link-Label', 'app_footer_links_url' => 'Link-URL', 'app_footer_links_add' => 'Fußzeilenlink hinzufügen', @@ -70,7 +70,7 @@ Wenn Du nichts eingibst, wird die Anwendung auf die Standardfarbe zurückgesetzt 'reg_email_confirmation_toggle' => 'Bestätigung per E-Mail erforderlich', 'reg_confirm_email_desc' => 'Falls die Einschränkung für Domains genutzt wird, ist die Bestätigung per E-Mail zwingend erforderlich und der untenstehende Wert wird ignoriert.', 'reg_confirm_restrict_domain' => 'Registrierung auf bestimmte Domains einschränken', - 'reg_confirm_restrict_domain_desc' => 'Fügen sie eine durch Komma getrennte Liste von Domains hinzu, auf die die Registrierung eingeschränkt werden soll. Benutzern wird eine E-Mail gesendet, um ihre E-Mail Adresse zu bestätigen, bevor sie diese Anwendung nutzen können. + 'reg_confirm_restrict_domain_desc' => 'Füge eine durch Komma getrennte Liste von Domains hinzu, auf die die Registrierung eingeschränkt werden soll. Benutzern wird eine E-Mail gesendet, um ihre E-Mail Adresse zu bestätigen, bevor sie diese Anwendung nutzen können. Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung ändern.', 'reg_confirm_restrict_domain_placeholder' => 'Keine Einschränkung gesetzt', @@ -80,10 +80,10 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'maint_image_cleanup_desc' => 'Überprüft Seiten- und Versionsinhalte auf ungenutzte und mehrfach vorhandene Bilder. Erstelle vor dem Start ein Backup Deiner Datenbank und Bilder.', 'maint_delete_images_only_in_revisions' => 'Lösche auch Bilder, die nur in alten Seitenüberarbeitungen vorhanden sind', 'maint_image_cleanup_run' => 'Reinigung starten', - 'maint_image_cleanup_warning' => ':count eventuell unbenutze Bilder wurden gefunden. Möchtest Du diese Bilder löschen?', + 'maint_image_cleanup_warning' => ':count eventuell unbenutze Bilder wurden gefunden. Möchtest du diese Bilder löschen?', 'maint_image_cleanup_success' => ':count eventuell unbenutze Bilder wurden gefunden und gelöscht.', 'maint_image_cleanup_nothing_found' => 'Keine unbenutzen Bilder gefunden. Nichts zu löschen!', - 'maint_send_test_email' => 'Test Email versenden', + 'maint_send_test_email' => 'Test E-Mail versenden', 'maint_send_test_email_desc' => 'Dies sendet eine Test E-Mail an die in deinem Profil angegebene E-Mail-Adresse.', 'maint_send_test_email_run' => 'Sende eine Test E-Mail', 'maint_send_test_email_success' => 'E-Mail wurde an :address gesendet', @@ -99,7 +99,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung // Recycle Bin 'recycle_bin' => 'Papierkorb', - 'recycle_bin_desc' => 'Hier können Sie gelöschte Einträge wiederherstellen oder sie dauerhaft aus dem System entfernen. Diese Liste ist nicht gefiltert, im Gegensatz zu ähnlichen Aktivitätslisten im System, wo Berechtigungsfilter angewendet werden.', + 'recycle_bin_desc' => 'Hier kannst du gelöschte Einträge wiederherstellen oder sie dauerhaft aus dem System entfernen. Diese Liste ist nicht gefiltert, im Gegensatz zu ähnlichen Aktivitätslisten im System, wo Berechtigungsfilter angewendet werden.', 'recycle_bin_deleted_item' => 'Gelöschter Eintrag', 'recycle_bin_deleted_parent' => 'Übergeordnet', 'recycle_bin_deleted_by' => 'Gelöscht von', @@ -108,11 +108,11 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'recycle_bin_restore' => 'Wiederherstellen', 'recycle_bin_contents_empty' => 'Der Papierkorb ist derzeit leer', 'recycle_bin_empty' => 'Papierkorb leeren', - 'recycle_bin_empty_confirm' => 'Dies wird alle Einträge im Papierkorb dauerhaft entfernen, einschließlich der Inhalte, die darin enthalten sind. Sind Sie sicher, dass Sie den Papierkorb leeren möchten?', - 'recycle_bin_destroy_confirm' => 'Diese Aktion wird diesen Eintrag zusammen mit allen unten aufgeführten Untereinträgen dauerhaft aus dem System löschen und Sie werden nicht in der Lage sein, diesen Inhalt wiederherzustellen. Sind Sie sicher, dass Sie diesen Eintrag endgültig löschen möchten?', + 'recycle_bin_empty_confirm' => 'Dies wird alle Einträge im Papierkorb dauerhaft entfernen, einschließlich der Inhalte, die darin enthalten sind. Bist du sicher, dass du den Papierkorb leeren möchtest?', + 'recycle_bin_destroy_confirm' => 'Diese Aktion wird diesen Eintrag zusammen mit allen unten aufgeführten Untereinträgen dauerhaft aus dem System löschen und du wirst nicht in der Lage sein, diesen Inhalt wiederherzustellen. Bist du sicher, dass du diesen Eintrag endgültig löschen möchtest?', 'recycle_bin_destroy_list' => 'Zu löschende Einträge', 'recycle_bin_restore_list' => 'Wiederherzustellende Einträge', - 'recycle_bin_restore_confirm' => 'Mit dieser Aktion wird der gelöschte Eintrag einschließlich aller untergeordneten Einträge an seinen ursprünglichen Ort wiederherstellen. Wenn der ursprüngliche Ort gelöscht wurde und sich nun im Papierkorb befindet, muss auch der übergeordnete Eintrag wiederhergestellt werden.', + 'recycle_bin_restore_confirm' => 'Mit dieser Aktion wird der gelöschte Eintrag einschließlich aller untergeordneten Einträge an seinem ursprünglichen Ort wiederhergestellt. Wenn der ursprüngliche Ort gelöscht wurde und sich nun im Papierkorb befindet, muss auch der übergeordnete Eintrag wiederhergestellt werden.', 'recycle_bin_restore_deleted_parent' => 'Der übergeordnete Eintrag wurde ebenfalls gelöscht. Dieser Eintrag wird weiterhin als gelöscht zählen, bis auch der übergeordnete Eintrag wiederhergestellt wurde.', 'recycle_bin_restore_parent' => 'Übergeordneter Eintrag wiederherstellen', 'recycle_bin_destroy_notification' => ':count Einträge wurden aus dem Papierkorb gelöscht.', @@ -136,18 +136,18 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung // Role Settings 'roles' => 'Rollen', 'role_user_roles' => 'Benutzer-Rollen', - 'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.', - 'roles_x_users_assigned' => '1 user assigned|:count users assigned', - 'roles_x_permissions_provided' => '1 permission|:count permissions', - 'roles_assigned_users' => 'Assigned Users', - 'roles_permissions_provided' => 'Provided Permissions', + 'roles_index_desc' => 'Rollen werden verwendet, um Benutzer zu gruppieren und System-Berechtigungen für ihre Mitglieder zuzuweisen. Wenn ein Benutzer Mitglied mehrerer Rollen ist, stapeln die gewährten Berechtigungen und der Benutzer wird alle Fähigkeiten erben.', + 'roles_x_users_assigned' => '1 Benutzer zugewiesen|:count Benutzer zugewiesen', + 'roles_x_permissions_provided' => '1 Berechtigung|:count Berechtigungen', + 'roles_assigned_users' => 'Zugewiesene Benutzer', + 'roles_permissions_provided' => 'Genutzte Berechtigungen', 'role_create' => 'Neue Rolle anlegen', 'role_create_success' => 'Rolle erfolgreich angelegt', 'role_delete' => 'Rolle löschen', - 'role_delete_confirm' => 'Du möchtest die Rolle ":roleName" löschen.', - 'role_delete_users_assigned' => 'Diese Rolle ist :userCount Benutzern zugeordnet. Du kannst unten eine neue Rolle auswählen, die Du diesen Benutzern zuordnen möchtest.', + 'role_delete_confirm' => 'Dies wird die Rolle ":roleName" löschen.', + 'role_delete_users_assigned' => 'Diese Rolle ist :userCount Benutzern zugeordnet. Du kannst unten eine neue Rolle auswählen, die du diesen Benutzern zuordnen möchtest.', 'role_delete_no_migration' => "Den Benutzern keine andere Rolle zuordnen", - 'role_delete_sure' => 'Bist Du sicher, dass Du diese Rolle löschen möchtest?', + 'role_delete_sure' => 'Bist du sicher, dass du diese Rolle löschen möchtest?', 'role_delete_success' => 'Rolle erfolgreich gelöscht', 'role_edit' => 'Rolle bearbeiten', 'role_details' => 'Rollendetails', @@ -166,8 +166,8 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'role_export_content' => 'Inhalt exportieren', 'role_editor_change' => 'Seiteneditor ändern', 'role_asset' => 'Berechtigungen', - 'roles_system_warning' => 'Beachten Sie, dass der Zugriff auf eine der oben genannten drei Berechtigungen einem Benutzer erlauben kann, seine eigenen Berechtigungen oder die Rechte anderer im System zu ändern. Weisen Sie nur Rollen, mit diesen Berechtigungen, vertrauenswürdigen Benutzern zu.', - 'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungenen.', + 'roles_system_warning' => 'Beachte, dass der Zugriff auf eine der oben genannten drei Berechtigungen einem Benutzer erlauben kann, seine eigenen Berechtigungen oder die Rechte anderer im System zu ändern. Weise nur Rollen mit diesen Berechtigungen vertrauenswürdigen Benutzern zu.', + 'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungen.', 'role_asset_admins' => 'Administratoren erhalten automatisch Zugriff auf alle Inhalte, aber diese Optionen können Oberflächenoptionen ein- oder ausblenden.', 'role_asset_image_view_note' => 'Das bezieht sich auf die Sichtbarkeit innerhalb des Bildmanagers. Der tatsächliche Zugriff auf hochgeladene Bilddateien hängt von der Speicheroption des Systems für Bilder ab.', 'role_all' => 'Alle', @@ -180,14 +180,14 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung // Users 'users' => 'Benutzer', - 'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.', + 'users_index_desc' => 'Erstelle und Verwalte individuelle Benutzerkonten innerhalb des Systems. Benutzerkonten werden zur Anmeldung und der Zuordnung von Inhalten und Aktivitäten verwendet. Zugriffsberechtigungen sind in erster Linie rollenbasiert, aber der Besitz von Benutzerinhalten kann unter anderem auch Berechtigungen beeinflussen.', 'user_profile' => 'Benutzerprofil', 'users_add_new' => 'Benutzer hinzufügen', 'users_search' => 'Benutzer suchen', 'users_latest_activity' => 'Neueste Aktivitäten', 'users_details' => 'Benutzerdetails', - 'users_details_desc' => 'Legen Sie für diesen Benutzer einen Anzeigenamen und eine E-Mail-Adresse fest. Die E-Mail-Adresse wird bei der Anmeldung verwendet.', - 'users_details_desc_no_email' => 'Legen Sie für diesen Benutzer einen Anzeigenamen fest, damit andere ihn erkennen können.', + 'users_details_desc' => 'Lege für diesen Benutzer einen Anzeigenamen und eine E-Mail-Adresse fest. Die E-Mail-Adresse wird bei der Anmeldung verwendet.', + 'users_details_desc_no_email' => 'Lege für diesen Benutzer einen Anzeigenamen fest, damit andere ihn erkennen können.', 'users_role' => 'Benutzerrollen', 'users_role_desc' => 'Wählen Sie aus, welchen Rollen dieser Benutzer zugeordnet werden soll. Wenn ein Benutzer mehreren Rollen zugeordnet ist, werden die Berechtigungen dieser Rollen gestapelt und er erhält alle Fähigkeiten der zugewiesenen Rollen.', 'users_password' => 'Benutzerpasswort', @@ -196,14 +196,14 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'users_send_invite_option' => 'Benutzer-Einladungs-E-Mail senden', 'users_external_auth_id' => 'Externe Authentifizierungs-ID', 'users_external_auth_id_desc' => 'Dies ist die ID, die verwendet wird, um diesen Benutzer bei der Kommunikation mit deinem externen Authentifizierungssystem abzugleichen.', - 'users_password_warning' => 'Fülle die folgenden Felder nur aus, wenn Du Dein Passwort ändern möchtest:', + 'users_password_warning' => 'Fülle die folgenden Felder nur aus, wenn du dein Passwort ändern möchtest:', 'users_system_public' => 'Dieser Benutzer repräsentiert alle unangemeldeten Benutzer, die diese Seite betrachten. Er kann nicht zum Anmelden benutzt werden, sondern wird automatisch zugeordnet.', 'users_delete' => 'Benutzer löschen', 'users_delete_named' => 'Benutzer ":userName" löschen', 'users_delete_warning' => 'Der Benutzer ":userName" wird aus dem System gelöscht.', - 'users_delete_confirm' => 'Bist Du sicher, dass Du diesen Benutzer löschen möchtest?', + 'users_delete_confirm' => 'Bist du sicher, dass du diesen Benutzer löschen möchtest?', 'users_migrate_ownership' => 'Besitz migrieren', - 'users_migrate_ownership_desc' => 'Wählen Sie hier einen Benutzer, wenn Sie möchten, dass ein anderer Benutzer der Besitzer aller Einträge wird, die diesem Benutzer derzeit gehören.', + 'users_migrate_ownership_desc' => 'Wähle hier einen Benutzer, wenn du möchtest, dass ein anderer Benutzer der Besitzer aller Einträge wird, die diesem Benutzer derzeit gehören.', 'users_none_selected' => 'Kein Benutzer ausgewählt', 'users_edit' => 'Benutzer bearbeiten', 'users_edit_profile' => 'Profil bearbeiten', @@ -218,7 +218,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'users_social_connected' => ':socialAccount-Konto wurde erfolgreich mit dem Profil verknüpft.', 'users_social_disconnected' => ':socialAccount-Konto wurde erfolgreich vom Profil gelöst.', 'users_api_tokens' => 'API-Token', - 'users_api_tokens_none' => 'Für diesen Benutzer wurden keine API-Token erstellt', + 'users_api_tokens_none' => 'Für diesen Benutzer wurden kein API-Token erstellt', 'users_api_tokens_create' => 'Token erstellen', 'users_api_tokens_expires' => 'Endet', 'users_api_tokens_docs' => 'API Dokumentation', @@ -232,7 +232,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'user_api_token_name' => 'Name', 'user_api_token_name_desc' => 'Gebe deinem Token einen aussagekräftigen Namen als spätere Erinnerung an seinen Verwendungszweck.', 'user_api_token_expiry' => 'Ablaufdatum', - 'user_api_token_expiry_desc' => 'Lege ein Datum fest, an dem dieser Token abläuft. Nach diesem Datum funktionieren Anfragen, die mit diesem Token gestellt werden, nicht mehr. Wenn du dieses Feld leer lässt, wird ein Ablaufdatum von 100 Jahren in der Zukunft festgelegt.', + 'user_api_token_expiry_desc' => 'Lege ein Datum fest, zu dem dieser Token abläuft. Nach diesem Datum funktionieren Anfragen, die mit diesem Token gestellt werden, nicht mehr. Wenn du dieses Feld leer lässt, wird ein Ablaufdatum von 100 Jahren in der Zukunft festgelegt.', 'user_api_token_create_secret_message' => 'Unmittelbar nach der Erstellung dieses Tokens wird eine "Token ID" & ein "Token Kennwort" generiert und angezeigt. Das Kennwort wird nur ein einziges Mal angezeigt. Stelle also sicher, dass du den Inhalt an einen sicheren Ort kopierst, bevor du fortfährst.', 'user_api_token_create_success' => 'API-Token erfolgreich erstellt', 'user_api_token_update_success' => 'API-Token erfolgreich aktualisiert', @@ -250,8 +250,8 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung // Webhooks 'webhooks' => 'Webhooks', - 'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.', - 'webhooks_x_trigger_events' => '1 trigger event|:count trigger events', + 'webhooks_index_desc' => 'Webhooks sind eine Möglichkeit, Daten an externe URLs zu senden, wenn bestimmte Aktionen und Ereignisse im System auftreten, was eine ereignisbasierte Integration mit externen Plattformen wie Messaging- oder Benachrichtigungssystemen ermöglicht.', + 'webhooks_x_trigger_events' => '1 Triggerereignis|:count Triggerereignisse', 'webhooks_create' => 'Neuen Webhook erstellen', 'webhooks_none_created' => 'Es wurden noch keine Webhooks erstellt.', 'webhooks_edit' => 'Webhook bearbeiten', diff --git a/resources/lang/el/auth.php b/resources/lang/el/auth.php index 6f1e61db2..b6a237dab 100644 --- a/resources/lang/el/auth.php +++ b/resources/lang/el/auth.php @@ -61,8 +61,8 @@ return [ 'email_confirm_send_error' => 'Απαιτείται επιβεβαίωση μέσω email, αλλά το σύστημα δεν μπόρεσε να στείλει το email. Επικοινωνήστε με τον διαχειριστή για να βεβαιωθείτε ότι το email έχει ρυθμιστεί σωστά.', 'email_confirm_success' => 'Το email σας επιβεβαιώθηκε! Θα πρέπει τώρα να μπορείτε να συνδεθείτε χρησιμοποιώντας αυτήν τη διεύθυνση email.', 'email_confirm_resent' => 'Το email επιβεβαίωσης στάλθηκε εκ νέου. Ελέγξτε τα εισερχόμενά σας.', - 'email_confirm_thanks' => 'Thanks for confirming!', - 'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.', + 'email_confirm_thanks' => 'Ευχαριστούμε για την επιβεβαίωση!', + 'email_confirm_thanks_desc' => 'Παρακαλώ περιμένετε λίγο όσο διεκπεραιώνεται η επιβεβαίωσή σας. Εάν δεν ανακατευθυνθείτε μετά από 3 δευτερόλεπτα, πατήστε τον παρακάτω σύνδεσμο "Συνέχεια" για να συνεχίσετε.', 'email_not_confirmed' => 'Η διεύθυνση email δεν επιβεβαιώθηκε', 'email_not_confirmed_text' => 'Η διεύθυνση email σας δεν έχει ακόμη επιβεβαιωθεί.', diff --git a/resources/lang/el/common.php b/resources/lang/el/common.php index 02be7a661..0b4970a12 100644 --- a/resources/lang/el/common.php +++ b/resources/lang/el/common.php @@ -25,7 +25,7 @@ return [ 'actions' => 'Ενέργειες', 'view' => 'Προβολή', 'view_all' => 'Προβολή όλων', - 'new' => 'New', + 'new' => 'Νέο', 'create' => 'Δημιουργία', 'update' => 'Ενημέρωση', 'edit' => 'Επεξεργασία', @@ -81,14 +81,14 @@ return [ 'none' => 'Κανένας', // Header - 'homepage' => 'Homepage', + 'homepage' => 'Αρχική σελίδα', 'header_menu_expand' => 'Αναπτύξτε το Head Menu', 'profile_menu' => 'Μενού Προφίλ', 'view_profile' => 'Προβολή προφίλ', 'edit_profile' => 'Επεξεργασία προφίλ', 'dark_mode' => 'Σκουρόχρωμη εμφάνιση', 'light_mode' => 'Ανοιχτόχρωμη εμφάνιση', - 'global_search' => 'Global Search', + 'global_search' => 'Καθολική αναζήτηση', // Layout tabs 'tab_info' => 'Πληροφορίες', diff --git a/resources/lang/el/editor.php b/resources/lang/el/editor.php index 596a63b52..c011a5ac8 100644 --- a/resources/lang/el/editor.php +++ b/resources/lang/el/editor.php @@ -66,7 +66,7 @@ return [ 'insert_link_title' => 'Εισαγωγή/Επεξεργασία συνδέσμου', 'insert_horizontal_line' => 'Εισαγωγή οριζόντιας γραμμής', 'insert_code_block' => 'Εισαγωγή μπλοκ κώδικα', - 'edit_code_block' => 'Edit code block', + 'edit_code_block' => 'Επεξεργασία μπλοκ κώδικα', 'insert_drawing' => 'Εισαγωγή/Επεξεργασία σχεδίου', 'drawing_manager' => 'Διαχειριστής σχεδίασης', 'insert_media' => 'Εισαγωγή/Επεξεργασία πολυμέσων', @@ -144,11 +144,11 @@ return [ 'url' => 'URL', 'text_to_display' => 'Κείμενο εμφάνισης', 'title' => 'Τίτλος', - 'open_link' => 'Open link', - 'open_link_in' => 'Open link in...', + 'open_link' => 'Άνοιγμα συνδέσμου', + 'open_link_in' => 'Άνοιγμα συνδέσμου σε...', 'open_link_current' => 'Τρέχον παράθυρο', 'open_link_new' => 'Νέο παράθυρο', - 'remove_link' => 'Remove link', + 'remove_link' => 'Αφαίρεση συνδέσμου', 'insert_collapsible' => 'Εισαγωγή πτυσσόμενου μπλοκ', 'collapsible_unwrap' => 'Μετατροπή πτυσσόμενου μπλοκ σε παράγραφο', 'edit_label' => 'Επεξεργασία ετικέτας', diff --git a/resources/lang/el/entities.php b/resources/lang/el/entities.php index 681fefda0..e2c318d80 100644 --- a/resources/lang/el/entities.php +++ b/resources/lang/el/entities.php @@ -42,15 +42,15 @@ return [ // Permissions and restrictions 'permissions' => 'Δικαιώματα', - 'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.', + 'permissions_desc' => 'Ορίστε εδώ δικαιώματα για να παρακάμψετε τα προκαθορισμένα δικαιώματα που παρέχονται από τους ρόλους των χρηστών.', 'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.', 'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.', 'permissions_save' => 'Αποθήκευση Δικαιωμάτων', 'permissions_owner' => 'Ιδιοκτήτης / Κάτοχος', 'permissions_role_everyone_else' => 'Everyone Else', - 'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.', - 'permissions_role_override' => 'Override permissions for role', - 'permissions_inherit_defaults' => 'Inherit defaults', + 'permissions_role_everyone_else_desc' => 'Ορίστε δικαιώματα για όλους τους ρόλους που δεν παραβλέπονται συγκεκριμένα.', + 'permissions_role_override' => 'Παράκαμψη δικαιωμάτων για ρόλο', + 'permissions_inherit_defaults' => 'Κληρονόμηση προεπιλογών', // Search 'search_results' => 'Αποτελέσματα αναζήτησης', @@ -224,8 +224,8 @@ return [ 'pages_md_insert_image' => 'Εισαγωγή Εικόνας', 'pages_md_insert_link' => 'Εισαγωγή/Επεξεργασία συνδέσμου', 'pages_md_insert_drawing' => 'Εισαγωγή Σχεδίου', - 'pages_md_show_preview' => 'Show preview', - 'pages_md_sync_scroll' => 'Sync preview scroll', + 'pages_md_show_preview' => 'Εμφάνιση προεπισκόπησης', + 'pages_md_sync_scroll' => 'Συγχρονισμός προεπισκόπησης', 'pages_not_in_chapter' => 'Η σελίδα δεν είναι σε κεφάλαιο', 'pages_move' => 'Μετακίνηση Σελίδας', 'pages_move_success' => 'Η σελίδα μετακινήθηκε στο ":parentName"', @@ -243,7 +243,7 @@ return [ 'pages_revisions_created_by' => 'Δημιουργήθηκε από', 'pages_revisions_date' => 'Ημερομηνία Αναθεώρησης', 'pages_revisions_number' => '#', - 'pages_revisions_sort_number' => 'Revision Number', + 'pages_revisions_sort_number' => 'Αριθμός αναθεώρησης', 'pages_revisions_numbered' => 'Αναθεώρηση #', 'pages_revisions_numbered_changes' => 'Αναθεώρηση #:id Αλλαγές', 'pages_revisions_editor' => 'Τύπος Επεξεργαστή', diff --git a/resources/lang/el/preferences.php b/resources/lang/el/preferences.php index e9a47461b..f9db475aa 100644 --- a/resources/lang/el/preferences.php +++ b/resources/lang/el/preferences.php @@ -5,14 +5,14 @@ */ return [ - 'shortcuts' => 'Shortcuts', - 'shortcuts_interface' => 'Interface Keyboard Shortcuts', - 'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.', - 'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.', - 'shortcuts_toggle_label' => 'Keyboard shortcuts enabled', - 'shortcuts_section_navigation' => 'Navigation', - 'shortcuts_section_actions' => 'Common Actions', - 'shortcuts_save' => 'Save Shortcuts', - 'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.', - 'shortcuts_update_success' => 'Shortcut preferences have been updated!', + 'shortcuts' => 'Συντομεύσεις', + 'shortcuts_interface' => 'Συντομεύσεις Πληκτρολογίου Διεπαφής', + 'shortcuts_toggle_desc' => 'Εδώ μπορείτε να ενεργοποιήσετε ή να απενεργοποιήσετε τις συντομεύσεις του συστήματος πληκτρολογίου, που χρησιμοποιούνται για την πλοήγηση και τις ενέργειες.', + 'shortcuts_customize_desc' => 'Μπορείτε να προσαρμόσετε κάθε μία από τις παρακάτω συντομεύσεις. Απλά πατήστε το επιθυμητό συνδυασμό πλήκτρων μετά την επιλογή της εισόδου για μια συντόμευση.', + 'shortcuts_toggle_label' => 'Ενεργοποίηση συντομεύσεων πληκτρολογίου', + 'shortcuts_section_navigation' => 'Πλοήγηση', + 'shortcuts_section_actions' => 'Κοινές ενέργειες', + 'shortcuts_save' => 'Αποθήκευση Συντομεύσεων', + 'shortcuts_overlay_desc' => 'Σημείωση: Όταν οι συντομεύσεις είναι ενεργοποιημένες μια βοηθητική επικάλυψη είναι διαθέσιμη πατώντας "?" που θα τονίσει τις διαθέσιμες συντομεύσεις για ενέργειες που είναι ορατές στην οθόνη.', + 'shortcuts_update_success' => 'Οι προτιμήσεις σας αποθηκεύτηκαν!', ]; \ No newline at end of file diff --git a/resources/lang/el/settings.php b/resources/lang/el/settings.php index e49f57aec..ea508450d 100644 --- a/resources/lang/el/settings.php +++ b/resources/lang/el/settings.php @@ -136,8 +136,8 @@ return [ 'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.', 'roles_x_users_assigned' => '1 user assigned|:count users assigned', 'roles_x_permissions_provided' => '1 permission|:count permissions', - 'roles_assigned_users' => 'Assigned Users', - 'roles_permissions_provided' => 'Provided Permissions', + 'roles_assigned_users' => 'Εκχωρημένοι χρήστες', + 'roles_permissions_provided' => 'Παρεχόμενα Δικαιώματα', 'role_create' => 'Δημιουργία νέου ρόλου', 'role_create_success' => 'Ο Ρόλος δημιουργήθηκε με επιτυχία', 'role_delete' => 'Διαγραφή Ρόλου', diff --git a/resources/lang/es/preferences.php b/resources/lang/es/preferences.php index 3735c5f2a..1ad40d802 100644 --- a/resources/lang/es/preferences.php +++ b/resources/lang/es/preferences.php @@ -5,14 +5,14 @@ */ return [ - 'shortcuts' => 'Accesos rápidos', - 'shortcuts_interface' => 'Atajos del Teclado de la Interfaz', - 'shortcuts_toggle_desc' => 'Aquí puede activar o desactivar los accesos rápidos de la interfaz, utilizados para la navegación y las acciones.', + 'shortcuts' => 'Accesos directos', + 'shortcuts_interface' => 'Accesos directos de la interfaz', + 'shortcuts_toggle_desc' => 'Aquí puede activar o desactivar los accesos directos de la interfaz, utilizados para la navegación y las acciones.', 'shortcuts_customize_desc' => 'Puede personalizar cada uno de los accesos directos a continuación. Simplemente pulse la combinación de teclas deseada después de seleccionar la entrada para un acceso directo.', 'shortcuts_toggle_label' => 'Accesos directos habilitados', 'shortcuts_section_navigation' => 'Navegación', 'shortcuts_section_actions' => 'Acciones comunes', - 'shortcuts_save' => 'Guardar atajos', + 'shortcuts_save' => 'Guardar accesos directos', 'shortcuts_overlay_desc' => 'Nota: Cuando se activan los accesos directos se puede mostrar la ayuda presionando la tecla "?" que resaltará los accesos rápidos disponibles para las acciones actualmente visibles en la pantalla.', - 'shortcuts_update_success' => '¡Las preferencias de accesos rápidos han sido actualizadas!', + 'shortcuts_update_success' => '¡Las preferencias de accesos directos han sido actualizadas!', ]; \ No newline at end of file diff --git a/resources/lang/et/entities.php b/resources/lang/et/entities.php index d9a0ef7dc..5b0d761b4 100644 --- a/resources/lang/et/entities.php +++ b/resources/lang/et/entities.php @@ -224,8 +224,8 @@ return [ 'pages_md_insert_image' => 'Lisa pilt', 'pages_md_insert_link' => 'Lisa viide', 'pages_md_insert_drawing' => 'Lisa joonis', - 'pages_md_show_preview' => 'Show preview', - 'pages_md_sync_scroll' => 'Sync preview scroll', + 'pages_md_show_preview' => 'Näita eelvaadet', + 'pages_md_sync_scroll' => 'Sünkrooni eelvaate kerimine', 'pages_not_in_chapter' => 'Leht ei kuulu peatüki alla', 'pages_move' => 'Liiguta leht', 'pages_move_success' => 'Leht liigutatud ":parentName" alla', diff --git a/resources/lang/eu/activities.php b/resources/lang/eu/activities.php index 716fe63e3..9dadac059 100644 --- a/resources/lang/eu/activities.php +++ b/resources/lang/eu/activities.php @@ -28,8 +28,8 @@ return [ // Books 'book_create' => 'liburua sortuta', 'book_create_notification' => 'Liburua ongi sortu da', - 'book_create_from_chapter' => 'converted chapter to book', - 'book_create_from_chapter_notification' => 'Chapter successfully converted to a book', + 'book_create_from_chapter' => 'bihurtu kapitulua liburu', + 'book_create_from_chapter_notification' => 'Kapitulua ongi bilakatu da liburu', 'book_update' => 'liburua eguneratuta', 'book_update_notification' => 'Liburua egoki eguneratua', 'book_delete' => 'liburua ezabatua', @@ -38,14 +38,14 @@ return [ 'book_sort_notification' => 'Liburua ongi bersailaktu da', // Bookshelves - 'bookshelf_create' => 'created shelf', - 'bookshelf_create_notification' => 'Shelf successfully created', - 'bookshelf_create_from_book' => 'converted book to shelf', - 'bookshelf_create_from_book_notification' => 'Book successfully converted to a shelf', - 'bookshelf_update' => 'updated shelf', - 'bookshelf_update_notification' => 'Shelf successfully updated', - 'bookshelf_delete' => 'deleted shelf', - 'bookshelf_delete_notification' => 'Shelf successfully deleted', + 'bookshelf_create' => 'apalategia sortuta', + 'bookshelf_create_notification' => 'Apalategia egoki sortuta', + 'bookshelf_create_from_book' => 'liburua apalategi bihurtuta', + 'bookshelf_create_from_book_notification' => 'Kapitulua ongi bilakatu da liburu', + 'bookshelf_update' => 'apalategia eguneratuta', + 'bookshelf_update_notification' => 'Erabiltzailea egoki eguneratua', + 'bookshelf_delete' => 'apalategia ezabatua', + 'bookshelf_delete_notification' => 'Apalategia egoki ezabatua', // Favourites 'favourite_add_notification' => '":name" zure gogoetara gehitua izan da', diff --git a/resources/lang/eu/preferences.php b/resources/lang/eu/preferences.php index e9a47461b..c3aa8a3ba 100644 --- a/resources/lang/eu/preferences.php +++ b/resources/lang/eu/preferences.php @@ -5,14 +5,14 @@ */ return [ - 'shortcuts' => 'Shortcuts', - 'shortcuts_interface' => 'Interface Keyboard Shortcuts', - 'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.', - 'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.', - 'shortcuts_toggle_label' => 'Keyboard shortcuts enabled', - 'shortcuts_section_navigation' => 'Navigation', - 'shortcuts_section_actions' => 'Common Actions', - 'shortcuts_save' => 'Save Shortcuts', - 'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.', - 'shortcuts_update_success' => 'Shortcut preferences have been updated!', + 'shortcuts' => 'Lastertekla', + 'shortcuts_interface' => 'Teklatuko lasterbideak ikusi', + 'shortcuts_toggle_desc' => 'Hemen, nabigaziorako eta ekintzetarako erabiltzen diren teklatu-sistemako lasterbideak gaitu edo desgaitu daitezke.', + 'shortcuts_customize_desc' => 'Beheko lasterbide bakoitza pertsonalizatu dezakezu. Sakatu nahi duzun tekla konbinazioa lasterbide baterako sarrera aukeratu ondoren.', + 'shortcuts_toggle_label' => 'Teklatu-lasterbideak aktibatuta', + 'shortcuts_section_navigation' => 'Nabigazioa', + 'shortcuts_section_actions' => 'Ohiko ekintzak', + 'shortcuts_save' => 'Gorde lasterbideak', + 'shortcuts_overlay_desc' => 'Oharra: Lasterbideak gaituta daudenean, "?" sakagailuaren bidez laguntzaileen gainjartze bat egongo da, eta horrek pantailan gaur egun ikus daitezkeen ekintzetarako dauden lasterbideak nabarmenduko ditu.', + 'shortcuts_update_success' => 'Zure lehentasunak gorde dira!', ]; \ No newline at end of file diff --git a/resources/lang/fa/entities.php b/resources/lang/fa/entities.php index d829ee4b5..a97ba694c 100644 --- a/resources/lang/fa/entities.php +++ b/resources/lang/fa/entities.php @@ -74,7 +74,7 @@ return [ 'search_date_options' => 'گزینه های تاریخ', 'search_updated_before' => 'قبلا به روز شده', 'search_updated_after' => 'پس از به روز رسانی', - 'search_created_before' => 'قبلا ایجاد شده است', + 'search_created_before' => 'ایجاد شده قبل از', 'search_created_after' => 'ایجاد شده پس از', 'search_set_date' => 'تنظیم تاریخ', 'search_update' => 'جستجو را به روز کنید', diff --git a/resources/lang/he/editor.php b/resources/lang/he/editor.php index c25846642..86594f6b5 100644 --- a/resources/lang/he/editor.php +++ b/resources/lang/he/editor.php @@ -113,7 +113,7 @@ return [ 'paste_row_after' => 'Paste row after', 'row_type' => 'Row type', 'row_type_header' => 'Header', - 'row_type_body' => 'Body', + 'row_type_body' => 'תוכן', 'row_type_footer' => 'Footer', 'alignment' => 'Alignment', 'cut_column' => 'Cut column', diff --git a/resources/lang/it/entities.php b/resources/lang/it/entities.php index 2e8f76e27..d93d6f015 100755 --- a/resources/lang/it/entities.php +++ b/resources/lang/it/entities.php @@ -224,8 +224,8 @@ return [ 'pages_md_insert_image' => 'Inserisci Immagina', 'pages_md_insert_link' => 'Inserisci Link Entità', 'pages_md_insert_drawing' => 'Inserisci Disegno', - 'pages_md_show_preview' => 'Show preview', - 'pages_md_sync_scroll' => 'Sync preview scroll', + 'pages_md_show_preview' => 'Visualizza anteprima', + 'pages_md_sync_scroll' => 'Sincronizza scorrimento anteprima', 'pages_not_in_chapter' => 'La pagina non è in un capitolo', 'pages_move' => 'Muovi Pagina', 'pages_move_success' => 'Pagina mossa in ":parentName"', diff --git a/resources/lang/ja/auth.php b/resources/lang/ja/auth.php index 98c49822c..c0c501b72 100644 --- a/resources/lang/ja/auth.php +++ b/resources/lang/ja/auth.php @@ -61,8 +61,8 @@ return [ 'email_confirm_send_error' => 'Eメールの確認が必要でしたが、システム上でEメールの送信ができませんでした。管理者に連絡し、Eメールが正しく設定されていることを確認してください。', 'email_confirm_success' => 'メールアドレスが確認されました!このメールアドレスでログインできるようになりました。', 'email_confirm_resent' => '確認メールを再送信しました。受信トレイを確認してください。', - 'email_confirm_thanks' => 'Thanks for confirming!', - 'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.', + 'email_confirm_thanks' => '確認いただきありがとうございます!', + 'email_confirm_thanks_desc' => '確認のため、しばらくお待ちください。3秒後にリダイレクトされない場合は、下の「続ける」リンクを押して次に進んでください。', 'email_not_confirmed' => 'Eメールアドレスが確認できていません', 'email_not_confirmed_text' => 'Eメールアドレスの確認が完了していません。', diff --git a/resources/lang/ja/common.php b/resources/lang/ja/common.php index 90b587d54..78454bfd8 100644 --- a/resources/lang/ja/common.php +++ b/resources/lang/ja/common.php @@ -25,7 +25,7 @@ return [ 'actions' => '実行', 'view' => '表示', 'view_all' => 'すべて表示', - 'new' => 'New', + 'new' => '新規作成', 'create' => '作成', 'update' => '更新', 'edit' => '編集', @@ -40,7 +40,7 @@ return [ 'reset' => 'リセット', 'remove' => '削除', 'add' => '追加', - 'configure' => 'Configure', + 'configure' => '設定', 'fullscreen' => '全画面', 'favourite' => 'お気に入り', 'unfavourite' => 'お気に入りから削除', @@ -81,14 +81,14 @@ return [ 'none' => 'なし', // Header - 'homepage' => 'Homepage', + 'homepage' => 'ホームページ', 'header_menu_expand' => 'ヘッダーメニューを展開', 'profile_menu' => 'プロフィールメニュー', 'view_profile' => 'プロフィール表示', 'edit_profile' => 'プロフィール編集', 'dark_mode' => 'ダークモード', 'light_mode' => 'ライトモード', - 'global_search' => 'Global Search', + 'global_search' => 'グローバル検索', // Layout tabs 'tab_info' => '情報', diff --git a/resources/lang/ja/editor.php b/resources/lang/ja/editor.php index d3a36019d..b47d638b4 100644 --- a/resources/lang/ja/editor.php +++ b/resources/lang/ja/editor.php @@ -66,7 +66,7 @@ return [ 'insert_link_title' => 'リンクの挿入・編集', 'insert_horizontal_line' => '水平線を挿入', 'insert_code_block' => 'コードブロックを挿入', - 'edit_code_block' => 'Edit code block', + 'edit_code_block' => 'コードブロックを編集', 'insert_drawing' => '描画を挿入・編集', 'drawing_manager' => '描画マネージャー', 'insert_media' => 'メディアの挿入・編集', @@ -144,11 +144,11 @@ return [ 'url' => 'リンク先URL', 'text_to_display' => 'リンク元テキスト', 'title' => 'タイトル', - 'open_link' => 'Open link', - 'open_link_in' => 'Open link in...', + 'open_link' => 'リンクを開く', + 'open_link_in' => 'リンク先の表示場所', 'open_link_current' => '同じウィンドウ', 'open_link_new' => '新規ウィンドウ', - 'remove_link' => 'Remove link', + 'remove_link' => 'リンクを削除', 'insert_collapsible' => '折りたたみブロックを追加', 'collapsible_unwrap' => 'ブロックの解除', 'edit_label' => 'ラベルを編集', diff --git a/resources/lang/ja/entities.php b/resources/lang/ja/entities.php index e3613c380..57a644c3f 100644 --- a/resources/lang/ja/entities.php +++ b/resources/lang/ja/entities.php @@ -42,15 +42,15 @@ return [ // Permissions and restrictions 'permissions' => '権限', - 'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.', - 'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.', - 'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.', + 'permissions_desc' => 'ユーザーの役割によって提供されるデフォルトの権限を上書きするため、ここで権限を設定します。', + 'permissions_book_cascade' => 'ブックに設定された権限は、子チャプターや子ページに独自の権限が定義されていない限り、自動的に子チャプターや子ページに継承されます。', + 'permissions_chapter_cascade' => 'チャプターに設定された権限は、子ページに独自の権限が定義されていない限り、自動的に子ページに継承されます。', 'permissions_save' => '権限を保存', 'permissions_owner' => '所有者', - 'permissions_role_everyone_else' => 'Everyone Else', - 'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.', - 'permissions_role_override' => 'Override permissions for role', - 'permissions_inherit_defaults' => 'Inherit defaults', + 'permissions_role_everyone_else' => 'その他の全員', + 'permissions_role_everyone_else_desc' => '明示的に上書きされていないすべての役割の権限を設定します。', + 'permissions_role_override' => '権限を上書きする役割', + 'permissions_inherit_defaults' => 'デフォルトを継承', // Search 'search_results' => '検索結果', @@ -224,8 +224,8 @@ return [ 'pages_md_insert_image' => '画像を挿入', 'pages_md_insert_link' => 'エンティティへのリンクを挿入', 'pages_md_insert_drawing' => '描画を追加', - 'pages_md_show_preview' => 'Show preview', - 'pages_md_sync_scroll' => 'Sync preview scroll', + 'pages_md_show_preview' => 'プレビューを表示', + 'pages_md_sync_scroll' => 'プレビューとスクロールを同期', 'pages_not_in_chapter' => 'チャプターが設定されていません', 'pages_move' => 'ページを移動', 'pages_move_success' => 'ページを ":parentName" へ移動しました', @@ -236,14 +236,14 @@ return [ 'pages_permissions_success' => 'ページの権限を更新しました', 'pages_revision' => '編集履歴', 'pages_revisions' => '編集履歴', - 'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.', + 'pages_revisions_desc' => '以下はこのページの過去の全リビジョンです。権限があれば、古いバージョンのページの見返しや比較、復元ができます。システムの設定によっては、古いリビジョンが自動削除されることがあるため、このページの全履歴がここに反映されないことがあります。', 'pages_revisions_named' => ':pageName のリビジョン', 'pages_revision_named' => ':pageName のリビジョン', 'pages_revision_restored_from' => '#:id :summary から復元', 'pages_revisions_created_by' => '作成者', 'pages_revisions_date' => '日付', 'pages_revisions_number' => '#', - 'pages_revisions_sort_number' => 'Revision Number', + 'pages_revisions_sort_number' => 'リビジョン番号', 'pages_revisions_numbered' => 'リビジョン #:id', 'pages_revisions_numbered_changes' => 'リビジョン #:id の変更', 'pages_revisions_editor' => 'エディタの種類', @@ -280,7 +280,7 @@ return [ 'shelf_tags' => '本棚のタグ', 'tag' => 'タグ', 'tags' => 'タグ', - 'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.', + 'tags_index_desc' => 'システム内のコンテンツにタグを適用して柔軟なカテゴリ分けを行うことができます。タグはキーと値の両方を持つことができ、値は任意です。タグを適用すると、タグの名前と値を使ってコンテンツを検索することができます。', 'tag_name' => 'タグの名前', 'tag_value' => '内容 (オプション)', 'tags_explain' => "タグを設定すると、コンテンツの管理が容易になります。\nより高度な管理をしたい場合、タグに内容を設定できます。", diff --git a/resources/lang/ja/preferences.php b/resources/lang/ja/preferences.php index e9a47461b..36a2f5a99 100644 --- a/resources/lang/ja/preferences.php +++ b/resources/lang/ja/preferences.php @@ -5,14 +5,14 @@ */ return [ - 'shortcuts' => 'Shortcuts', - 'shortcuts_interface' => 'Interface Keyboard Shortcuts', - 'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.', - 'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.', - 'shortcuts_toggle_label' => 'Keyboard shortcuts enabled', - 'shortcuts_section_navigation' => 'Navigation', - 'shortcuts_section_actions' => 'Common Actions', - 'shortcuts_save' => 'Save Shortcuts', - 'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.', - 'shortcuts_update_success' => 'Shortcut preferences have been updated!', + 'shortcuts' => 'ショートカット', + 'shortcuts_interface' => 'インターフェイスのキーボードショートカット', + 'shortcuts_toggle_desc' => 'ここでは、ナビゲーションやアクションに使用されるキーボードシステムインターフェイスのショートカットを有効または無効にすることができます。', + 'shortcuts_customize_desc' => '以下の各ショートカットをカスタマイズできます。ショートカットの入力を選択した後、希望のキーの組み合わせを押してください。', + 'shortcuts_toggle_label' => 'キーボードショートカットを有効にする', + 'shortcuts_section_navigation' => 'ナビゲーション', + 'shortcuts_section_actions' => '共通のアクション', + 'shortcuts_save' => 'ショートカットを保存', + 'shortcuts_overlay_desc' => '注:ショートカットが有効な場合はヘルパーオーバーレイが利用できます。「?」を押すと現在画面に表示されているアクションで利用可能なショートカットをハイライト表示します。', + 'shortcuts_update_success' => 'ショートカットの設定が更新されました!', ]; \ No newline at end of file diff --git a/resources/lang/ja/settings.php b/resources/lang/ja/settings.php index a36271949..61e6f69f1 100644 --- a/resources/lang/ja/settings.php +++ b/resources/lang/ja/settings.php @@ -133,9 +133,9 @@ return [ // Role Settings 'roles' => '役割', 'role_user_roles' => '役割', - 'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.', - 'roles_x_users_assigned' => '1 user assigned|:count users assigned', - 'roles_x_permissions_provided' => '1 permission|:count permissions', + 'roles_index_desc' => '役割は、ユーザーをグループ化しメンバーにシステム権限を与えるために使用されます。ユーザーが複数の役割のメンバーである場合、与えられた権限は積み重なり、ユーザーはすべての能力を継承します。', + 'roles_x_users_assigned' => '1人のユーザーに割り当て|:count人のユーザーに割り当て', + 'roles_x_permissions_provided' => '1件の権限|:count件の権限', 'roles_assigned_users' => 'Assigned Users', 'roles_permissions_provided' => 'Provided Permissions', 'role_create' => '役割を作成', @@ -177,7 +177,7 @@ return [ // Users 'users' => 'ユーザー', - 'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.', + 'users_index_desc' => 'システム内で個々のユーザーアカウントを作成し、管理します。ユーザーアカウントは、ログインおよびコンテンツとアクティビティの帰属のために使用されます。アクセス許可は主に役割ベースですが、ユーザーコンテンツの所有権やその他の要因も、許可とアクセスに影響する場合があります。', 'user_profile' => 'ユーザプロフィール', 'users_add_new' => 'ユーザーを追加', 'users_search' => 'ユーザー検索', @@ -247,8 +247,8 @@ return [ // Webhooks 'webhooks' => 'Webhook', - 'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.', - 'webhooks_x_trigger_events' => '1 trigger event|:count trigger events', + 'webhooks_index_desc' => 'Webhookは、システム内で特定のアクションやイベントが発生したときに外部URLにデータを送信する方法で、メッセージングシステムや通知システムなどの外部プラットフォームとのイベントベースの統合を可能にします。', + 'webhooks_x_trigger_events' => '1個のトリガーイベント|:count個のトリガーイベント', 'webhooks_create' => 'Webhookを作成', 'webhooks_none_created' => 'Webhookはまだ作成されていません。', 'webhooks_edit' => 'Webhookを編集', diff --git a/resources/lang/pl/auth.php b/resources/lang/pl/auth.php index d3e8c09b9..00cce1a2c 100644 --- a/resources/lang/pl/auth.php +++ b/resources/lang/pl/auth.php @@ -61,8 +61,8 @@ return [ 'email_confirm_send_error' => 'Wymagane jest potwierdzenie hasła, lecz wiadomość nie mogła zostać wysłana. Skontaktuj się z administratorem w celu upewnienia się, że skrzynka została skonfigurowana prawidłowo.', 'email_confirm_success' => 'Twój e-mail został potwierdzony! Powinieneś teraz mieć możliwość zalogowania się za pomocą tego adresu e-mail.', 'email_confirm_resent' => 'E-mail z potwierdzeniem został wysłany ponownie, sprawdź swoją skrzynkę odbiorczą.', - 'email_confirm_thanks' => 'Thanks for confirming!', - 'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.', + 'email_confirm_thanks' => 'Dzięki za potwierdzenie!', + 'email_confirm_thanks_desc' => 'Poczekaj chwilę, Twoje potwierdzenie jest obsługiwane. Jeśli nie zostaniesz przekierowany po 3 sekundach, naciśnij poniższy link "Kontynuuj", aby kontynuować.', 'email_not_confirmed' => 'Adres e-mail nie został potwierdzony', 'email_not_confirmed_text' => 'Twój adres e-mail nie został jeszcze potwierdzony.', diff --git a/resources/lang/pl/entities.php b/resources/lang/pl/entities.php index 58ae3ac7a..b4eed43fb 100644 --- a/resources/lang/pl/entities.php +++ b/resources/lang/pl/entities.php @@ -224,8 +224,8 @@ return [ 'pages_md_insert_image' => 'Wstaw obrazek', 'pages_md_insert_link' => 'Wstaw łącze do obiektu', 'pages_md_insert_drawing' => 'Wstaw rysunek', - 'pages_md_show_preview' => 'Show preview', - 'pages_md_sync_scroll' => 'Sync preview scroll', + 'pages_md_show_preview' => 'Pokaż podgląd', + 'pages_md_sync_scroll' => 'Synchronizuj przewijanie podglądu', 'pages_not_in_chapter' => 'Strona nie została umieszczona w rozdziale', 'pages_move' => 'Przenieś stronę', 'pages_move_success' => 'Strona przeniesiona do ":parentName"', diff --git a/resources/lang/pt_BR/entities.php b/resources/lang/pt_BR/entities.php index 20fd038e4..7852fe172 100644 --- a/resources/lang/pt_BR/entities.php +++ b/resources/lang/pt_BR/entities.php @@ -224,8 +224,8 @@ return [ 'pages_md_insert_image' => 'Inserir Imagem', 'pages_md_insert_link' => 'Inserir Link para Entidade', 'pages_md_insert_drawing' => 'Inserir Desenho', - 'pages_md_show_preview' => 'Show preview', - 'pages_md_sync_scroll' => 'Sync preview scroll', + 'pages_md_show_preview' => 'Mostrar pré-visualização', + 'pages_md_sync_scroll' => 'Sincronizar pré-visualização', 'pages_not_in_chapter' => 'Página não está dentro de um capítulo', 'pages_move' => 'Mover Página', 'pages_move_success' => 'Pagina movida para ":parentName"', diff --git a/resources/lang/zh_CN/auth.php b/resources/lang/zh_CN/auth.php index 3e944a7fa..d6dcd6219 100644 --- a/resources/lang/zh_CN/auth.php +++ b/resources/lang/zh_CN/auth.php @@ -24,7 +24,7 @@ return [ 'password_hint' => '必须至少有 8 个字符', 'forgot_password' => '忘记密码?', 'remember_me' => '记住我', - 'ldap_email_hint' => '请输入用于此帐户的电子邮件。', + 'ldap_email_hint' => '请输入用于此账户的电子邮件。', 'create_account' => '创建账户', 'already_have_account' => '已经有账号了?', 'dont_have_account' => '您还没有账号吗?', @@ -50,7 +50,7 @@ return [ 'reset_password_sent' => '重置密码的链接将通过您的电子邮箱发送:email。', 'reset_password_success' => '您的密码已成功重置。', 'email_reset_subject' => '重置您的:appName密码', - 'email_reset_text' => '您收到此电子邮件是因为我们收到了您的帐户的密码重置请求。', + 'email_reset_text' => '您收到此电子邮件是因为我们收到了您的账户的密码重置请求。', 'email_reset_not_requested' => '如果您没有要求重置密码,则不需要采取进一步的操作。', // Email Confirmation @@ -61,8 +61,8 @@ return [ 'email_confirm_send_error' => '需要Email验证,但系统无法发送电子邮件,请联系网站管理员。', 'email_confirm_success' => '您已成功验证电子邮件地址!您现在可以使用此电子邮件地址登录。', 'email_confirm_resent' => '验证邮件已重新发送,请检查收件箱。', - 'email_confirm_thanks' => 'Thanks for confirming!', - 'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.', + 'email_confirm_thanks' => '感谢您的确认!', + 'email_confirm_thanks_desc' => '请稍等,您的确认正在处理。如果您在3秒后未被重定向,请按下面的“继续“链接继续。', 'email_not_confirmed' => 'Email地址未验证', 'email_not_confirmed_text' => '您的电子邮件地址尚未确认。', @@ -72,11 +72,11 @@ return [ // User Invite 'user_invite_email_subject' => '您已受邀加入 :appName!', - 'user_invite_email_greeting' => ' :appName 已为您创建了一个帐户。', - 'user_invite_email_text' => '点击下面的按钮以设置帐户密码并获得访问权限:', - 'user_invite_email_action' => '设置帐号密码', + 'user_invite_email_greeting' => ':appName 已为您创建了一个账户。', + 'user_invite_email_text' => '点击下面的按钮以设置账户密码并获得访问权限:', + 'user_invite_email_action' => '设置账号密码', 'user_invite_page_welcome' => '欢迎来到 :appName!', - 'user_invite_page_text' => '要完成您的帐户并获得访问权限,您需要设置一个密码,该密码将在以后访问时用于登录 :appName。', + 'user_invite_page_text' => '要完成您的账户并获得访问权限,您需要设置一个密码,该密码将在以后访问时用于登录 :appName。', 'user_invite_page_confirm_button' => '确认密码', 'user_invite_success_login' => '密码已设置,您现在可以使用您设置的密码登录 :appName!', @@ -87,7 +87,7 @@ return [ 'mfa_setup_reconfigure' => '重新配置', 'mfa_setup_remove_confirmation' => '您确定想要移除多重身份认证吗?', 'mfa_setup_action' => '设置', - 'mfa_backup_codes_usage_limit_warning' => '您剩余的备用认证码少于 5 个,请在用完认证码之前生成并保存新的认证码,以防止您的帐户被锁定。', + 'mfa_backup_codes_usage_limit_warning' => '您剩余的备用认证码少于 5 个,请在用完认证码之前生成并保存新的认证码,以防止您的账户被锁定。', 'mfa_option_totp_title' => '移动设备 App', 'mfa_option_totp_desc' => '要使用多重身份认证功能,您需要一个支持 TOTP(基于时间的一次性密码算法) 的移动设备 App,如谷歌身份验证器(Google Authenticator)、Authy 或微软身份验证器(Microsoft Authenticator)。', 'mfa_option_backup_codes_title' => '备用认证码', diff --git a/resources/lang/zh_CN/common.php b/resources/lang/zh_CN/common.php index be36fceca..11d7144ab 100644 --- a/resources/lang/zh_CN/common.php +++ b/resources/lang/zh_CN/common.php @@ -25,7 +25,7 @@ return [ 'actions' => '操作', 'view' => '浏览', 'view_all' => '查看全部', - 'new' => 'New', + 'new' => '新', 'create' => '创建', 'update' => '更新', 'edit' => '编辑', @@ -81,14 +81,14 @@ return [ 'none' => '无', // Header - 'homepage' => 'Homepage', + 'homepage' => '主页', 'header_menu_expand' => '展开标头菜单', 'profile_menu' => '个人资料', 'view_profile' => '查看个人资料', 'edit_profile' => '编辑个人资料', 'dark_mode' => '夜间模式', 'light_mode' => '日间模式', - 'global_search' => 'Global Search', + 'global_search' => '全局搜索', // Layout tabs 'tab_info' => '信息', diff --git a/resources/lang/zh_CN/editor.php b/resources/lang/zh_CN/editor.php index 174ba1425..db65cb216 100644 --- a/resources/lang/zh_CN/editor.php +++ b/resources/lang/zh_CN/editor.php @@ -144,11 +144,11 @@ return [ 'url' => '网址', 'text_to_display' => '要显示的文本', 'title' => '标题', - 'open_link' => 'Open link', - 'open_link_in' => 'Open link in...', + 'open_link' => '打开链接', + 'open_link_in' => '打开链接于……', 'open_link_current' => '覆盖当前窗口', 'open_link_new' => '新建窗口', - 'remove_link' => 'Remove link', + 'remove_link' => '移除链接', 'insert_collapsible' => '插入可折叠块', 'collapsible_unwrap' => '展开', 'edit_label' => '编辑标签', diff --git a/resources/lang/zh_CN/entities.php b/resources/lang/zh_CN/entities.php index f3ff05700..20f068a9a 100644 --- a/resources/lang/zh_CN/entities.php +++ b/resources/lang/zh_CN/entities.php @@ -50,7 +50,7 @@ return [ 'permissions_role_everyone_else' => '其他所有人', 'permissions_role_everyone_else_desc' => '为所有未被特别覆盖的角色设置权限。', 'permissions_role_override' => '覆盖角色权限', - 'permissions_inherit_defaults' => 'Inherit defaults', + 'permissions_inherit_defaults' => '继承默认值', // Search 'search_results' => '搜索结果', @@ -224,8 +224,8 @@ return [ 'pages_md_insert_image' => '插入图片', 'pages_md_insert_link' => '插入项目链接', 'pages_md_insert_drawing' => '插入图表', - 'pages_md_show_preview' => 'Show preview', - 'pages_md_sync_scroll' => 'Sync preview scroll', + 'pages_md_show_preview' => '显示预览', + 'pages_md_sync_scroll' => '同步预览滚动', 'pages_not_in_chapter' => '本页面不在某章节中', 'pages_move' => '移动页面', 'pages_move_success' => '页面已移动到「:parentName」', @@ -236,14 +236,14 @@ return [ 'pages_permissions_success' => '页面权限已更新', 'pages_revision' => '修订', 'pages_revisions' => '页面修订', - 'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.', + 'pages_revisions_desc' => '下面列出的是该页面的所有过去修订。如果权限允许,您可以回顾、比较和恢复旧的页面版本。页面的完整历史可能不会在这里完全反映出来,因为根据系统配置,旧的修订可能会被自动删除。', 'pages_revisions_named' => '“:pageName”页面修订', 'pages_revision_named' => '“:pageName”页面修订', 'pages_revision_restored_from' => '恢复到 #:id :summary', 'pages_revisions_created_by' => '创建者', 'pages_revisions_date' => '修订日期', 'pages_revisions_number' => '#', - 'pages_revisions_sort_number' => 'Revision Number', + 'pages_revisions_sort_number' => '修订号', 'pages_revisions_numbered' => '修订 #:id', 'pages_revisions_numbered_changes' => '修改 #:id ', 'pages_revisions_editor' => '编辑器类型', @@ -280,7 +280,7 @@ return [ 'shelf_tags' => '书架标签', 'tag' => '标签', 'tags' => '标签', - 'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.', + 'tags_index_desc' => '标签是一种灵活的分类形式,可以应用于系统内的内容。标签可以有一个键和值,值是可选的。应用后就可以使用标签的名称和值来搜索内容。', 'tag_name' => '标签名称', 'tag_value' => '标签值 (可选)', 'tags_explain' => "添加一些标签以更好地对您的内容进行分类。\n您可以为标签分配一个值,以进行更好的进行管理。", diff --git a/resources/lang/zh_CN/errors.php b/resources/lang/zh_CN/errors.php index 66ee79a17..8a404cb42 100644 --- a/resources/lang/zh_CN/errors.php +++ b/resources/lang/zh_CN/errors.php @@ -30,14 +30,14 @@ return [ 'social_no_action_defined' => '没有定义行为', 'social_login_bad_response' => "在 :socialAccount 登录时遇到错误:\n:error", 'social_account_in_use' => ':socialAccount 账户已被使用,请尝试通过 :socialAccount 选项登录。', - 'social_account_email_in_use' => 'Email :email 已经被使用。如果您已有帐户,则可以在个人资料设置中绑定您的 :socialAccount。', + 'social_account_email_in_use' => 'Email :email 已经被使用。如果您已有账户,则可以在个人资料设置中绑定您的 :socialAccount。', 'social_account_existing' => ':socialAccount已经被绑定到您的账户。', 'social_account_already_used_existing' => ':socialAccount账户已经被其他用户使用。', 'social_account_not_used' => ':socialAccount账户没有绑定到任何用户,请在您的个人资料设置中绑定。', - 'social_account_register_instructions' => '如果您还没有帐户,您可以使用 :socialAccount 选项注册账户。', + 'social_account_register_instructions' => '如果您还没有账户,您可以使用 :socialAccount 选项注册账户。', 'social_driver_not_found' => '未找到社交驱动程序', 'social_driver_not_configured' => '您的:socialAccount社交设置不正确。', - 'invite_token_expired' => '此邀请链接已过期。 您可以尝试重置您的帐户密码。', + 'invite_token_expired' => '此邀请链接已过期。 您可以尝试重置您的账户密码。', // System 'path_not_writable' => '无法上传到文件路径“:filePath”,请确保它可写入服务器。', diff --git a/resources/lang/zh_CN/preferences.php b/resources/lang/zh_CN/preferences.php index e9a47461b..0e1f378aa 100644 --- a/resources/lang/zh_CN/preferences.php +++ b/resources/lang/zh_CN/preferences.php @@ -5,14 +5,14 @@ */ return [ - 'shortcuts' => 'Shortcuts', - 'shortcuts_interface' => 'Interface Keyboard Shortcuts', - 'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.', - 'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.', - 'shortcuts_toggle_label' => 'Keyboard shortcuts enabled', - 'shortcuts_section_navigation' => 'Navigation', - 'shortcuts_section_actions' => 'Common Actions', - 'shortcuts_save' => 'Save Shortcuts', - 'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.', - 'shortcuts_update_success' => 'Shortcut preferences have been updated!', + 'shortcuts' => '快捷键', + 'shortcuts_interface' => '界面键盘快捷键', + 'shortcuts_toggle_desc' => '你可以启用或禁用键盘系统界面快捷键,这些快捷键用于导航和操作。', + 'shortcuts_customize_desc' => '您可以自定义下面的每个快捷键。选择快捷方式输入后按下您想使用的按键组合即可。', + 'shortcuts_toggle_label' => '启用键盘快捷键', + 'shortcuts_section_navigation' => '导航', + 'shortcuts_section_actions' => '通用操作', + 'shortcuts_save' => '保存快捷键', + 'shortcuts_overlay_desc' => '注意:当快捷键启用时,可以按"?"键来打开帮助,它将突出显示当前屏幕上可见操作的快捷键。', + 'shortcuts_update_success' => '快捷键设置已更新!', ]; \ No newline at end of file diff --git a/resources/lang/zh_CN/settings.php b/resources/lang/zh_CN/settings.php index 22b626ed1..88dab170f 100755 --- a/resources/lang/zh_CN/settings.php +++ b/resources/lang/zh_CN/settings.php @@ -133,11 +133,11 @@ return [ // Role Settings 'roles' => '角色', 'role_user_roles' => '用户角色', - 'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.', - 'roles_x_users_assigned' => '1 user assigned|:count users assigned', - 'roles_x_permissions_provided' => '1 permission|:count permissions', - 'roles_assigned_users' => 'Assigned Users', - 'roles_permissions_provided' => 'Provided Permissions', + 'roles_index_desc' => '角色用于对用户进行分组并为其成员提供系统权限。当一个用户是多个角色的成员时,授予的权限将叠加,用户将继承所有角色的能力。', + 'roles_x_users_assigned' => '1 位用户已分配|:count 位用户已分配', + 'roles_x_permissions_provided' => '1 个权限|:count 个权限', + 'roles_assigned_users' => '已分配用户', + 'roles_permissions_provided' => '已提供权限', 'role_create' => '创建角色', 'role_create_success' => '角色创建成功', 'role_delete' => '删除角色', @@ -177,7 +177,7 @@ return [ // Users 'users' => '用户', - 'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.', + 'users_index_desc' => '在系统内创建和管理个人用户账户。用户账户用于登录和内容及活动的归属。访问权限主要是基于角色的,但用户的内容所有权以及其他因素,也可能影响到权限和访问。', 'user_profile' => '用户资料', 'users_add_new' => '添加用户', 'users_search' => '搜索用户', @@ -209,7 +209,7 @@ return [ 'users_preferred_language' => '语言', 'users_preferred_language_desc' => '此选项将更改用于应用程序用户界面的语言。 这不会影响任何用户创建的内容。', 'users_social_accounts' => '社交账户', - 'users_social_accounts_info' => '在这里,您可以绑定您的其他帐户,以便更快更轻松地登录。如果您选择解除绑定,之后将不能通过此社交账户登录,请设置社交账户来取消本App的访问权限。', + 'users_social_accounts_info' => '在这里,您可以绑定您的其他账户,以便更快更轻松地登录。如果您选择解除绑定,之后将不能通过此社交账户登录,请设置社交账户来取消本App的访问权限。', 'users_social_connect' => '绑定账户', 'users_social_disconnect' => '解除绑定账户', 'users_social_connected' => ':socialAccount 账户已经成功绑定到您的资料。', @@ -247,8 +247,8 @@ return [ // Webhooks 'webhooks' => 'Webhooks', - 'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.', - 'webhooks_x_trigger_events' => '1 trigger event|:count trigger events', + 'webhooks_index_desc' => 'Webhook 是一种在系统内发生某些操作和事件时将数据发送到外部 URL 的方法,它允许与外部平台(例如消息传递或通知系统)进行基于事件的集成。', + 'webhooks_x_trigger_events' => '1 个触发事件 |:count 个触发事件', 'webhooks_create' => '新建 Webhook', 'webhooks_none_created' => '尚未创建任何 Webhook。', 'webhooks_edit' => '编辑 Webhook', From 8468b632a14e0e27cdc13f4b21700d933126b580 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 16 Dec 2022 17:11:01 +0000 Subject: [PATCH 008/477] Updated crowdin config with PR title and labels Aligns to the title and labelling I already do manually. --- crowdin.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crowdin.yml b/crowdin.yml index 634a5772f..59f357e97 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,6 +1,9 @@ project_identifier: bookstack base_path: . preserve_hierarchy: false +pull_request_title: Updated translations with latest Crowdin changes +pull_request_labels: + - ":earth_africa: Translations" files: - source: /resources/lang/en/*.php translation: /resources/lang/%two_letters_code%/%original_file_name% From 0123d83fb27f286d8ad4a0a865d82974ad099a94 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 16 Dec 2022 17:44:13 +0000 Subject: [PATCH 009/477] Fixed not being able to remove all user roles User roles would only be actioned if they existed in the form request, hence removal of all roles would have no data to action upon. This adds a placeholder 0-id role to ensure there is always role data to send, even when no roles are selected. This field value is latter filtered out. Added test to cover. Likely related to #3922. --- app/Auth/UserRepo.php | 2 ++ .../views/form/role-checkboxes.blade.php | 1 + tests/User/UserManagementTest.php | 30 +++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/app/Auth/UserRepo.php b/app/Auth/UserRepo.php index 78bcb978e..2c27f34a7 100644 --- a/app/Auth/UserRepo.php +++ b/app/Auth/UserRepo.php @@ -234,6 +234,8 @@ class UserRepo */ protected function setUserRoles(User $user, array $roles) { + $roles = array_filter(array_values($roles)); + if ($this->demotingLastAdmin($user, $roles)) { throw new UserUpdateException(trans('errors.role_cannot_remove_only_admin'), $user->getEditUrl()); } diff --git a/resources/views/form/role-checkboxes.blade.php b/resources/views/form/role-checkboxes.blade.php index 7e5ca629a..b7b969d89 100644 --- a/resources/views/form/role-checkboxes.blade.php +++ b/resources/views/form/role-checkboxes.blade.php @@ -1,5 +1,6 @@
+ @foreach($roles as $role)
@include('form.custom-checkbox', [ diff --git a/tests/User/UserManagementTest.php b/tests/User/UserManagementTest.php index 4991e052a..b5cd764da 100644 --- a/tests/User/UserManagementTest.php +++ b/tests/User/UserManagementTest.php @@ -274,4 +274,34 @@ class UserManagementTest extends TestCase $resp->assertSessionHasErrors(['language' => 'The language may not be greater than 15 characters.']); $resp->assertSessionHasErrors(['language' => 'The language may only contain letters, numbers, dashes and underscores.']); } + + public function test_role_removal_on_user_edit_removes_all_role_assignments() + { + $user = $this->getEditor(); + + $this->assertEquals(1, $user->roles()->count()); + + // A roles[0] hidden fields is used to indicate the existence of role selection in the submission + // of the user edit form. We check that field is used and emulate its submission. + $resp = $this->asAdmin()->get("/settings/users/{$user->id}"); + $this->withHtml($resp)->assertElementExists('input[type="hidden"][name="roles[0]"][value="0"]'); + + $resp = $this->asAdmin()->put("/settings/users/{$user->id}", [ + 'name' => $user->name, + 'email' => $user->email, + 'roles' => ['0' => '0'], + ]); + $resp->assertRedirect("/settings/users"); + + $this->assertEquals(0, $user->roles()->count()); + } + + public function test_role_form_hidden_indicator_field_does_not_exist_where_roles_cannot_be_managed() + { + $user = $this->getEditor(); + $resp = $this->actingAs($user)->get("/settings/users/{$user->id}"); + $html = $this->withHtml($resp); + $html->assertElementExists('input[name="email"]'); + $html->assertElementNotExists('input[type="hidden"][name="roles[0]"]'); + } } From a521f418382aaec18ed6fce997a2b204a9a7eb56 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 16 Dec 2022 23:16:05 +0000 Subject: [PATCH 010/477] Fixed lack of scroll in editor toolbox contents For #2887 --- resources/sass/_pages.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/sass/_pages.scss b/resources/sass/_pages.scss index 720203a42..e037cd4c1 100755 --- a/resources/sass/_pages.scss +++ b/resources/sass/_pages.scss @@ -359,6 +359,7 @@ body.tox-fullscreen, body.markdown-fullscreen { .toolbox-tab-content { display: none; + overflow-y: auto; } .suggestion-box { From f5df811b155f31743d97f92b9c30d8ce2af6acc7 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 16 Dec 2022 23:21:24 +0000 Subject: [PATCH 011/477] Removed old unused style definition --- resources/sass/_pages.scss | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/resources/sass/_pages.scss b/resources/sass/_pages.scss index e037cd4c1..57718e647 100755 --- a/resources/sass/_pages.scss +++ b/resources/sass/_pages.scss @@ -318,14 +318,6 @@ body.tox-fullscreen, body.markdown-fullscreen { @include lightDark(color, #444, #EEE); background-color: rgba(0, 0, 0, 0.1); } - div[toolbox-tab-content] { - padding-bottom: 45px; - display: flex; - flex: 1; - flex-direction: column; - min-height: 0; - overflow-y: scroll; - } h4 { font-size: 24px; margin: $-m 0 0 0; @@ -360,6 +352,7 @@ body.tox-fullscreen, body.markdown-fullscreen { .toolbox-tab-content { display: none; overflow-y: auto; + padding-bottom: 45px; } .suggestion-box { From 5393465ea7caf45997026323bb30f9bc2c6c1213 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 16 Dec 2022 23:48:04 +0000 Subject: [PATCH 012/477] Updated translator attribution before release v22.11.1 --- .github/translators.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/translators.txt b/.github/translators.txt index e21d3c6e9..030569842 100644 --- a/.github/translators.txt +++ b/.github/translators.txt @@ -283,13 +283,13 @@ Kuchinashi Hoshikawa (kuchinashi) :: Chinese Simplified digilady :: Greek Linus (LinusOP) :: Swedish Felipe Cardoso (felipecardosoruff) :: Portuguese, Brazilian -RandomUser0815 :: German +RandomUser0815 :: German Informal; German Ismael Mesquita (mesquitoliveira) :: Portuguese, Brazilian 구인회 (laskdjlaskdj12) :: Korean LiZerui (CNLiZerui) :: Chinese Traditional Fabrice Boyer (FabriceBoyer) :: French mikael (bitcanon) :: Swedish -Matthias Mai (schnapsidee) :: German +Matthias Mai (schnapsidee) :: German; German Informal Ufuk Ayyıldız (ufukayyildiz) :: Turkish Jan Mitrof (jan.kachlik) :: Czech edwardsmirnov :: Russian @@ -298,3 +298,7 @@ shotu :: French Cesar_Lopez_Aguillon :: Spanish bdewoop :: German dina davoudi (dina.davoudi) :: Persian +Angelos Chouvardas (achouvardas) :: Greek +rndrss :: Portuguese, Brazilian +rirac294 :: Russian +David Furman (thefourCraft) :: Hebrew From 6070d804f8ac331c66dd689d8f55ba8a08fb8cf9 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 16 Jan 2023 16:54:53 +0000 Subject: [PATCH 013/477] Fixed incorrect pluralisation for de_informal Updated language system to only use initial part of locale for translation pluralisation to better match the hard-coded logic of the built-in MessageSelector. Extends and overrides Laravel's default for this system. Added test to cover. Related to #3976. --- app/Providers/TranslationServiceProvider.php | 31 ++++++++++++++++++++ app/Translation/MessageSelector.php | 19 ++++++++++++ tests/LanguageTest.php | 14 ++++++++- 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 app/Translation/MessageSelector.php diff --git a/app/Providers/TranslationServiceProvider.php b/app/Providers/TranslationServiceProvider.php index 3610a1e22..6bf57e021 100644 --- a/app/Providers/TranslationServiceProvider.php +++ b/app/Providers/TranslationServiceProvider.php @@ -3,10 +3,41 @@ namespace BookStack\Providers; use BookStack\Translation\FileLoader; +use BookStack\Translation\MessageSelector; use Illuminate\Translation\TranslationServiceProvider as BaseProvider; +use Illuminate\Translation\Translator; class TranslationServiceProvider extends BaseProvider { + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + $this->registerLoader(); + + // This is a tweak upon Laravel's based translation service registration to allow + // usage of a custom MessageSelector class + $this->app->singleton('translator', function ($app) { + $loader = $app['translation.loader']; + + // When registering the translator component, we'll need to set the default + // locale as well as the fallback locale. So, we'll grab the application + // configuration so we can easily get both of these values from there. + $locale = $app['config']['app.locale']; + + $trans = new Translator($loader, $locale); + $trans->setFallback($app['config']['app.fallback_locale']); + $trans->setSelector(new MessageSelector()); + + return $trans; + }); + } + + + /** * Register the translation line loader. * Overrides the default register action from Laravel so a custom loader can be used. diff --git a/app/Translation/MessageSelector.php b/app/Translation/MessageSelector.php new file mode 100644 index 000000000..1a4771b3f --- /dev/null +++ b/app/Translation/MessageSelector.php @@ -0,0 +1,19 @@ +get('/'); $this->assertTrue(config('app.rtl'), 'App RTL config should have been set to true by middleware'); } + + public function test_pluralisation_for_non_standard_locales() + { + $text = trans_choice('entities.x_pages', 1, [], 'de_informal'); + $this->assertEquals('1 Seite', $text); + + $text = trans_choice('entities.x_pages', 2, [], 'de_informal'); + $this->assertEquals('2 Seiten', $text); + + $text = trans_choice('entities.x_pages', 0, [], 'de_informal'); + $this->assertEquals('0 Seiten', $text); + } } From c724bfe4d37037e90a305b5ff9410070ccf90bb9 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 21 Jan 2023 11:08:34 +0000 Subject: [PATCH 014/477] Copied over work from user_permissions branch Only that relevant to the additional testing work. --- dev/docs/permission-scenario-testing.md | 421 ++++++++++++++++++ tests/Actions/AuditLogTest.php | 30 +- tests/Actions/WebhookCallTest.php | 4 +- tests/Actions/WebhookFormatTesting.php | 2 +- tests/Actions/WebhookManagementTest.php | 4 +- tests/Api/ApiAuthTest.php | 14 +- tests/Api/AttachmentsApiTest.php | 8 +- tests/Api/BooksApiTest.php | 2 +- tests/Api/ChaptersApiTest.php | 2 +- tests/Api/PagesApiTest.php | 4 +- tests/Api/RecycleBinApiTest.php | 14 +- tests/Api/TestsApi.php | 4 +- tests/Api/UsersApiTest.php | 8 +- tests/Auth/AuthTest.php | 6 +- tests/Auth/GroupSyncServiceTest.php | 8 +- tests/Auth/LdapTest.php | 4 +- tests/Auth/LoginAutoInitiateTest.php | 2 +- tests/Auth/MfaConfigurationTest.php | 18 +- tests/Auth/MfaVerificationTest.php | 10 +- tests/Auth/OidcTest.php | 6 +- tests/Auth/ResetPasswordTest.php | 2 +- tests/Auth/Saml2Test.php | 2 +- tests/Auth/SocialAuthTest.php | 6 +- tests/Auth/UserInviteTest.php | 10 +- tests/Commands/ClearActivityCommandTest.php | 2 +- tests/Commands/ClearViewsCommandTest.php | 4 +- .../CopyShelfPermissionsCommandTest.php | 8 +- .../RegeneratePermissionsCommandTest.php | 21 +- tests/Entity/BookShelfTest.php | 27 +- tests/Entity/BookTest.php | 14 +- tests/Entity/ChapterTest.php | 10 +- tests/Entity/ConvertTest.php | 16 +- tests/Entity/EntityAccessTest.php | 8 +- tests/Entity/EntitySearchTest.php | 16 +- tests/Entity/ExportTest.php | 7 +- tests/Entity/PageContentTest.php | 2 +- tests/Entity/PageDraftTest.php | 14 +- tests/Entity/PageRevisionTest.php | 8 +- tests/Entity/PageTemplateTest.php | 8 +- tests/Entity/PageTest.php | 22 +- tests/Entity/SortTest.php | 52 +-- tests/Entity/TagTest.php | 4 +- tests/ErrorTest.php | 6 +- tests/FavouriteTest.php | 14 +- tests/Helpers/EntityProvider.php | 40 -- tests/Helpers/PermissionsProvider.php | 136 ++++++ tests/Helpers/UserRoleProvider.php | 97 ++++ tests/HomepageTest.php | 10 +- tests/LanguageTest.php | 16 +- tests/Permissions/EntityPermissionsTest.php | 43 +- tests/Permissions/ExportPermissionsTest.php | 8 +- tests/Permissions/RolesTest.php | 90 ++-- .../Scenarios/EntityRolePermissionsTest.php | 201 +++++++++ .../Scenarios/EntityUserPermissionsTest.php | 209 +++++++++ .../Scenarios/PermissionScenarioTestCase.php | 38 ++ .../Scenarios/RoleContentPermissionsTest.php | 59 +++ tests/PublicActionTest.php | 4 +- tests/References/ReferencesTest.php | 2 +- tests/Settings/RecycleBinTest.php | 10 +- tests/Settings/RegenerateReferencesTest.php | 4 +- tests/Settings/TestEmailTest.php | 8 +- tests/TestCase.php | 114 +---- tests/ThemeTest.php | 2 +- tests/Unit/FrameworkAssumptionTest.php | 2 +- tests/Uploads/AttachmentTest.php | 10 +- tests/Uploads/DrawioTest.php | 6 +- tests/Uploads/ImageTest.php | 34 +- tests/User/UserApiTokenTest.php | 30 +- tests/User/UserManagementTest.php | 46 +- tests/User/UserPreferencesTest.php | 20 +- tests/User/UserProfileTest.php | 4 +- tests/User/UserSearchTest.php | 14 +- 72 files changed, 1566 insertions(+), 545 deletions(-) create mode 100644 dev/docs/permission-scenario-testing.md create mode 100644 tests/Helpers/PermissionsProvider.php create mode 100644 tests/Helpers/UserRoleProvider.php create mode 100644 tests/Permissions/Scenarios/EntityRolePermissionsTest.php create mode 100644 tests/Permissions/Scenarios/EntityUserPermissionsTest.php create mode 100644 tests/Permissions/Scenarios/PermissionScenarioTestCase.php create mode 100644 tests/Permissions/Scenarios/RoleContentPermissionsTest.php diff --git a/dev/docs/permission-scenario-testing.md b/dev/docs/permission-scenario-testing.md new file mode 100644 index 000000000..6d0935f09 --- /dev/null +++ b/dev/docs/permission-scenario-testing.md @@ -0,0 +1,421 @@ +# Permission Scenario Testing + +Due to complexity that can arise in the various combinations of permissions, this document details scenarios and their expected results. + +Test cases are written ability abstract, since all abilities should act the same in theory. Functional test cases may test abilities separate due to implementation differences. + +Tests are categorised by the most specific element involved in the scenario, where the below list is most specific to least: + +- User entity permissions. +- Role entity permissions. +- Fallback entity permissions. +- Role permissions. + +- TODO - Test fallback in the context of the above. + +## General Permission Logical Rules + +The below are some general rules we follow to standardise the behaviour of permissions in the platform: + +- Most specific permission application (as above) take priority and can deny less specific permissions. +- Parent user/role entity permissions that may be inherited, are considered to essentially be applied on the item they are inherited to unless a lower level has its own permission rule for an already specific role/user. +- Where both grant and deny exist at the same specificity, we side towards grant. + +## Cases + +### Content Role Permissions + +These are tests related to item/entity permissions that are set only at a role level. + +#### test_01_allow + +- Role A has role all-page permission. +- User has Role A. + +User granted page permission. + +#### test_02_deny + +- Role A has no page permission. +- User has Role A. + +User denied page permission. + +#### test_10_allow_on_own_with_own + +- Role A has role own-page permission. +- User has Role A. +- User is owner of page. + +User granted page permission. + +#### test_11_deny_on_other_with_own + +- Role A has role own-page permission. +- User has Role A. +- User is not owner of page. + +User denied page permission. + +#### test_20_multiple_role_conflicting_all + +- Role A has role all-page permission. +- Role B has no page permission. +- User has Role A & B. + +User granted page permission. + +#### test_21_multiple_role_conflicting_own + +- Role A has role own-page permission. +- Role B has no page permission. +- User has Role A & B. +- User is owner of page. + +User granted page permission. + +--- + +### Entity Role Permissions + +These are tests related to entity-level role-specific permission overrides. + +#### test_01_explicit_allow + +- Page permissions have inherit disabled. +- Role A has entity allow page permission. +- User has Role A. + +User granted page permission. + +#### test_02_explicit_deny + +- Page permissions have inherit disabled. +- Role A has entity deny page permission. +- User has Role A. + +User denied page permission. + +#### test_03_same_level_conflicting + +- Page permissions have inherit disabled. +- Role A has entity allow page permission. +- Role B has entity deny page permission. +- User has both Role A & B. + +User granted page permission. +Explicit grant overrides entity deny at same level. + +#### test_20_inherit_allow + +- Page permissions have inherit enabled. +- Chapter permissions has inherit disabled. +- Role A has entity allow chapter permission. +- User has Role A. + +User granted page permission. + +#### test_21_inherit_deny + +- Page permissions have inherit enabled. +- Chapter permissions has inherit disabled. +- Role A has entity deny chapter permission. +- User has Role A. + +User denied page permission. + +#### test_22_same_level_conflict_inherit + +- Page permissions have inherit enabled. +- Chapter permissions has inherit disabled. +- Role A has entity deny chapter permission. +- Role B has entity allow chapter permission. +- User has both Role A & B. + +User granted page permission. + +#### test_30_child_inherit_override_allow + +- Page permissions have inherit enabled. +- Chapter permissions has inherit disabled. +- Role A has entity deny chapter permission. +- Role A has entity allow page permission. +- User has Role A. + +User granted page permission. + +#### test_31_child_inherit_override_deny + +- Page permissions have inherit enabled. +- Chapter permissions has inherit disabled. +- Role A has entity allow chapter permission. +- Role A has entity deny page permission. +- User has Role A. + +User denied page permission. + +#### test_40_multi_role_inherit_conflict_override_deny + +- Page permissions have inherit enabled. +- Chapter permissions has inherit disabled. +- Role A has entity deny page permission. +- Role B has entity allow chapter permission. +- User has Role A & B. + +User granted page permission. + +#### test_41_multi_role_inherit_conflict_retain_allow + +- Page permissions have inherit enabled. +- Chapter permissions has inherit disabled. +- Role A has entity allow page permission. +- Role B has entity deny chapter permission. +- User has Role A & B. + +User granted page permission. + +#### test_50_role_override_allow + +- Page permissions have inherit enabled. +- Role A has no page role permission. +- Role A has entity allow page permission. +- User has Role A. + +User granted page permission. + +#### test_51_role_override_deny + +- Page permissions have inherit enabled. +- Role A has no page-view-all role permission. +- Role A has entity deny page permission. +- User has Role A. + +User denied page permission. + +#### test_60_inherited_role_override_allow + +- Page permissions have inherit enabled. +- Chapter permissions have inherit enabled. +- Role A has no page role permission. +- Role A has entity allow chapter permission. +- User has Role A. + +User granted page permission. + +#### test_61_inherited_role_override_deny + +- Page permissions have inherit enabled. +- Chapter permissions have inherit enabled. +- Role A has page role permission. +- Role A has entity denied chapter permission. +- User has Role A. + +User denied page permission. + +#### test_62_inherited_role_override_deny_on_own + +- Page permissions have inherit enabled. +- Chapter permissions have inherit enabled. +- Role A has own-page role permission. +- Role A has entity denied chapter permission. +- User has Role A. +- User owns Page. + +User denied page permission. + +#### test_70_multi_role_inheriting_deny + +- Page permissions have inherit enabled. +- Role A has all page role permission. +- Role B has entity denied page permission. +- User has Role A and B. + +User denied page permission. + +#### test_80_multi_role_inherited_deny_via_parent + +- Page permissions have inherit enabled. +- Chapter permissions have inherit enabled. +- Role A has all-pages role permission. +- Role B has entity denied chapter permission. +- User has Role A & B. + +User denied page permission. + +--- + +### Entity User Permissions + +These are tests related to entity-level user-specific permission overrides. + +#### test_01_explicit_allow + +- Page permissions have inherit disabled. +- User has entity allow page permission. + +User granted page permission. + +#### test_02_explicit_deny + +- Page permissions have inherit disabled. +- User has entity deny page permission. + +User denied page permission. + +#### test_10_allow_inherit + +- Page permissions have inherit enabled. +- Chapter permissions have inherit disabled. +- User has entity allow chapter permission. + +User granted page permission. + +#### test_11_deny_inherit + +- Page permissions have inherit enabled. +- Chapter permissions have inherit disabled. +- User has entity deny chapter permission. + +User denied page permission. + +#### test_12_allow_inherit_override + +- Page permissions have inherit enabled. +- Chapter permissions have inherit disabled. +- User has entity deny chapter permission. +- User has entity allow page permission. + +User granted page permission. + +#### test_13_deny_inherit_override + +- Page permissions have inherit enabled. +- Chapter permissions have inherit disabled. +- User has entity allow chapter permission. +- User has entity deny page permission. + +User denied page permission. + +#### test_40_entity_role_override_allow + +- Page permissions have inherit disabled. +- User has entity allow page permission. +- Role A has entity deny page permission. +- User has role A. + +User granted page permission. + +#### test_41_entity_role_override_deny + +- Page permissions have inherit disabled. +- User has entity deny page permission. +- Role A has entity allow page permission. +- User has role A. + +User denied page permission. + +#### test_42_entity_role_override_allow_via_inherit + +- Page permissions have inherit enabled. +- Chapter permissions have inherit disabled. +- User has entity allow chapter permission. +- Role A has entity deny page permission. +- User has role A. + +User granted page permission. + +#### test_43_entity_role_override_deny_via_inherit + +- Page permissions have inherit enabled. +- Chapter permissions have inherit disabled. +- User has entity deny chapter permission. +- Role A has entity allow page permission. +- User has role A. + +User denied page permission. + +#### test_50_role_override_allow + +- Page permissions have inherit enabled. +- Role A has no page role permission. +- User has entity allow page permission. +- User has Role A. + +User granted page permission. + +#### test_51_role_override_deny + +- Page permissions have inherit enabled. +- Role A has all-page role permission. +- User has entity deny page permission. +- User has Role A. + +User denied page permission. + +#### test_60_inherited_role_override_allow + +- Page permissions have inherit enabled. +- Role A has no page role permission. +- User has entity allow chapter permission. +- User has Role A. + +User granted page permission. + +#### test_61_inherited_role_override_deny + +- Page permissions have inherit enabled. +- Role A has view-all page role permission. +- User has entity deny chapter permission. +- User has Role A. + +User denied page permission. + +#### test_61_inherited_role_override_deny_on_own + +- Page permissions have inherit enabled. +- Role A has view-own page role permission. +- User has entity deny chapter permission. +- User has Role A. +- User owns Page. + +User denied page permission. + +#### test_70_all_override_allow + +- Page permissions have inherit enabled. +- Role A has no page role permission. +- Role A has entity deny page permission. +- User has entity allow page permission. +- User has Role A. + +User granted page permission. + +#### test_71_all_override_deny + +- Page permissions have inherit enabled. +- Role A has page-all role permission. +- Role A has entity allow page permission. +- User has entity deny page permission. +- User has Role A. + +User denied page permission. + +#### test_80_inherited_all_override_allow + +- Page permissions have inherit enabled. +- Role A has no page role permission. +- Role A has entity deny chapter permission. +- User has entity allow chapter permission. +- User has Role A. + +User granted page permission. + +#### test_81_inherited_all_override_deny + +- Page permissions have inherit enabled. +- Role A has view-all page role permission. +- Role A has entity allow chapter permission. +- User has entity deny chapter permission. +- User has Role A. + +User denied page permission. \ No newline at end of file diff --git a/tests/Actions/AuditLogTest.php b/tests/Actions/AuditLogTest.php index 25fa2b796..52b45e712 100644 --- a/tests/Actions/AuditLogTest.php +++ b/tests/Actions/AuditLogTest.php @@ -23,17 +23,17 @@ class AuditLogTest extends TestCase public function test_only_accessible_with_right_permissions() { - $viewer = $this->getViewer(); + $viewer = $this->users->viewer(); $this->actingAs($viewer); $resp = $this->get('/settings/audit'); $this->assertPermissionError($resp); - $this->giveUserPermissions($viewer, ['settings-manage']); + $this->permissions->grantUserRolePermissions($viewer, ['settings-manage']); $resp = $this->get('/settings/audit'); $this->assertPermissionError($resp); - $this->giveUserPermissions($viewer, ['users-manage']); + $this->permissions->grantUserRolePermissions($viewer, ['users-manage']); $resp = $this->get('/settings/audit'); $resp->assertStatus(200); $resp->assertSeeText('Audit Log'); @@ -41,7 +41,7 @@ class AuditLogTest extends TestCase public function test_shows_activity() { - $admin = $this->getAdmin(); + $admin = $this->users->admin(); $this->actingAs($admin); $page = $this->entities->page(); $this->activityService->add(ActivityType::PAGE_CREATE, $page); @@ -56,7 +56,7 @@ class AuditLogTest extends TestCase public function test_shows_name_for_deleted_items() { - $this->actingAs($this->getAdmin()); + $this->actingAs($this->users->admin()); $page = $this->entities->page(); $pageName = $page->name; $this->activityService->add(ActivityType::PAGE_CREATE, $page); @@ -71,12 +71,12 @@ class AuditLogTest extends TestCase public function test_shows_activity_for_deleted_users() { - $viewer = $this->getViewer(); + $viewer = $this->users->viewer(); $this->actingAs($viewer); $page = $this->entities->page(); $this->activityService->add(ActivityType::PAGE_CREATE, $page); - $this->actingAs($this->getAdmin()); + $this->actingAs($this->users->admin()); app(UserRepo::class)->destroy($viewer); $resp = $this->get('settings/audit'); @@ -85,7 +85,7 @@ class AuditLogTest extends TestCase public function test_filters_by_key() { - $this->actingAs($this->getAdmin()); + $this->actingAs($this->users->admin()); $page = $this->entities->page(); $this->activityService->add(ActivityType::PAGE_CREATE, $page); @@ -98,7 +98,7 @@ class AuditLogTest extends TestCase public function test_date_filters() { - $this->actingAs($this->getAdmin()); + $this->actingAs($this->users->admin()); $page = $this->entities->page(); $this->activityService->add(ActivityType::PAGE_CREATE, $page); @@ -120,8 +120,8 @@ class AuditLogTest extends TestCase public function test_user_filter() { - $admin = $this->getAdmin(); - $editor = $this->getEditor(); + $admin = $this->users->admin(); + $editor = $this->users->editor(); $this->actingAs($admin); $page = $this->entities->page(); $this->activityService->add(ActivityType::PAGE_CREATE, $page); @@ -142,7 +142,7 @@ class AuditLogTest extends TestCase public function test_ip_address_logged_and_visible() { config()->set('app.proxies', '*'); - $editor = $this->getEditor(); + $editor = $this->users->editor(); $page = $this->entities->page(); $this->actingAs($editor)->put($page->getUrl(), [ @@ -166,7 +166,7 @@ class AuditLogTest extends TestCase public function test_ip_address_is_searchable() { config()->set('app.proxies', '*'); - $editor = $this->getEditor(); + $editor = $this->users->editor(); $page = $this->entities->page(); $this->actingAs($editor)->put($page->getUrl(), [ @@ -192,7 +192,7 @@ class AuditLogTest extends TestCase { config()->set('app.proxies', '*'); config()->set('app.env', 'demo'); - $editor = $this->getEditor(); + $editor = $this->users->editor(); $page = $this->entities->page(); $this->actingAs($editor)->put($page->getUrl(), [ @@ -215,7 +215,7 @@ class AuditLogTest extends TestCase { config()->set('app.proxies', '*'); config()->set('app.ip_address_precision', 2); - $editor = $this->getEditor(); + $editor = $this->users->editor(); $page = $this->entities->page(); $this->actingAs($editor)->put($page->getUrl(), [ diff --git a/tests/Actions/WebhookCallTest.php b/tests/Actions/WebhookCallTest.php index 7ca190200..078b8bdf4 100644 --- a/tests/Actions/WebhookCallTest.php +++ b/tests/Actions/WebhookCallTest.php @@ -88,7 +88,7 @@ class WebhookCallTest extends TestCase ]); $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']); $page = $this->entities->page(); - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->runEvent(ActivityType::PAGE_UPDATE, $page, $editor); @@ -111,7 +111,7 @@ class WebhookCallTest extends TestCase protected function runEvent(string $event, $detail = '', ?User $user = null) { if (is_null($user)) { - $user = $this->getEditor(); + $user = $this->users->editor(); } $this->actingAs($user); diff --git a/tests/Actions/WebhookFormatTesting.php b/tests/Actions/WebhookFormatTesting.php index 07341c75b..be67d4d52 100644 --- a/tests/Actions/WebhookFormatTesting.php +++ b/tests/Actions/WebhookFormatTesting.php @@ -41,7 +41,7 @@ class WebhookFormatTesting extends TestCase protected function getWebhookData(string $event, $detail): array { $webhook = Webhook::factory()->make(); - $user = $this->getEditor(); + $user = $this->users->editor(); $formatter = WebhookFormatter::getDefault($event, $webhook, $detail, $user, time()); return $formatter->format(); diff --git a/tests/Actions/WebhookManagementTest.php b/tests/Actions/WebhookManagementTest.php index f106f303a..52838beca 100644 --- a/tests/Actions/WebhookManagementTest.php +++ b/tests/Actions/WebhookManagementTest.php @@ -135,7 +135,7 @@ class WebhookManagementTest extends TestCase public function test_settings_manage_permission_required_for_webhook_routes() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->actingAs($editor); $routes = [ @@ -153,7 +153,7 @@ class WebhookManagementTest extends TestCase $this->assertPermissionError($resp); } - $this->giveUserPermissions($editor, ['settings-manage']); + $this->permissions->grantUserRolePermissions($editor, ['settings-manage']); foreach ($routes as [$method, $endpoint]) { $resp = $this->call($method, $endpoint); diff --git a/tests/Api/ApiAuthTest.php b/tests/Api/ApiAuthTest.php index cc6818e27..038c4e067 100644 --- a/tests/Api/ApiAuthTest.php +++ b/tests/Api/ApiAuthTest.php @@ -16,8 +16,8 @@ class ApiAuthTest extends TestCase public function test_requests_succeed_with_default_auth() { - $viewer = $this->getViewer(); - $this->giveUserPermissions($viewer, ['access-api']); + $viewer = $this->users->viewer(); + $this->permissions->grantUserRolePermissions($viewer, ['access-api']); $resp = $this->get($this->endpoint); $resp->assertStatus(401); @@ -63,7 +63,7 @@ class ApiAuthTest extends TestCase auth()->logout(); $accessApiPermission = RolePermission::getByName('access-api'); - $editorRole = $this->getEditor()->roles()->first(); + $editorRole = $this->users->editor()->roles()->first(); $editorRole->detachPermission($accessApiPermission); $resp = $this->get($this->endpoint, $this->apiAuthHeader()); @@ -73,7 +73,7 @@ class ApiAuthTest extends TestCase public function test_api_access_permission_required_to_access_api_with_session_auth() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->actingAs($editor, 'standard'); $resp = $this->get($this->endpoint); @@ -81,7 +81,7 @@ class ApiAuthTest extends TestCase auth('standard')->logout(); $accessApiPermission = RolePermission::getByName('access-api'); - $editorRole = $this->getEditor()->roles()->first(); + $editorRole = $this->users->editor()->roles()->first(); $editorRole->detachPermission($accessApiPermission); $editor = User::query()->where('id', '=', $editor->id)->first(); @@ -114,7 +114,7 @@ class ApiAuthTest extends TestCase public function test_token_expiry_checked() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $token = $editor->apiTokens()->first(); $resp = $this->get($this->endpoint, $this->apiAuthHeader()); @@ -130,7 +130,7 @@ class ApiAuthTest extends TestCase public function test_email_confirmation_checked_using_api_auth() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $editor->email_confirmed = false; $editor->save(); diff --git a/tests/Api/AttachmentsApiTest.php b/tests/Api/AttachmentsApiTest.php index 4d1d3b340..b03f280ac 100644 --- a/tests/Api/AttachmentsApiTest.php +++ b/tests/Api/AttachmentsApiTest.php @@ -50,7 +50,7 @@ class AttachmentsApiTest extends TestCase ], ]]); - $this->entities->setPermissions($page, [], []); + $this->permissions->setEntityPermissions($page, [], []); $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id'); $resp->assertJsonMissing(['data' => [ @@ -246,13 +246,13 @@ class AttachmentsApiTest extends TestCase public function test_attachment_not_visible_on_other_users_draft() { $this->actingAsApiAdmin(); - $editor = $this->getEditor(); + $editor = $this->users->editor(); $page = $this->entities->page(); $page->draft = true; $page->owned_by = $editor->id; $page->save(); - $this->entities->regenPermissions($page); + $this->permissions->regenerateForEntity($page); $attachment = $this->createAttachmentForPage($page, [ 'name' => 'my attachment', @@ -342,7 +342,7 @@ class AttachmentsApiTest extends TestCase protected function createAttachmentForPage(Page $page, $attributes = []): Attachment { - $admin = $this->getAdmin(); + $admin = $this->users->admin(); /** @var Attachment $attachment */ $attachment = $page->attachments()->forceCreate(array_merge([ 'uploaded_to' => $page->id, diff --git a/tests/Api/BooksApiTest.php b/tests/Api/BooksApiTest.php index 614185c93..dd187672e 100644 --- a/tests/Api/BooksApiTest.php +++ b/tests/Api/BooksApiTest.php @@ -246,7 +246,7 @@ class BooksApiTest extends TestCase { $types = ['html', 'plaintext', 'pdf', 'markdown']; $this->actingAsApiEditor(); - $this->removePermissionFromUser($this->getEditor(), 'content-export'); + $this->permissions->removeUserRolePermissions($this->users->editor(), ['content-export']); $book = $this->entities->book(); foreach ($types as $type) { diff --git a/tests/Api/ChaptersApiTest.php b/tests/Api/ChaptersApiTest.php index d2db0313f..a48e3b026 100644 --- a/tests/Api/ChaptersApiTest.php +++ b/tests/Api/ChaptersApiTest.php @@ -221,7 +221,7 @@ class ChaptersApiTest extends TestCase { $types = ['html', 'plaintext', 'pdf', 'markdown']; $this->actingAsApiEditor(); - $this->removePermissionFromUser($this->getEditor(), 'content-export'); + $this->permissions->removeUserRolePermissions($this->users->editor(), ['content-export']); $chapter = Chapter::visible()->has('pages')->first(); foreach ($types as $type) { diff --git a/tests/Api/PagesApiTest.php b/tests/Api/PagesApiTest.php index 8c533680f..12b38bc07 100644 --- a/tests/Api/PagesApiTest.php +++ b/tests/Api/PagesApiTest.php @@ -209,7 +209,7 @@ class PagesApiTest extends TestCase $this->actingAsApiEditor(); $page = $this->entities->page(); $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first(); - $this->entities->setPermissions($chapter, ['view'], [$this->getEditor()->roles()->first()]); + $this->permissions->setEntityPermissions($chapter, ['view'], [$this->users->editor()->roles()->first()]); $details = [ 'name' => 'My updated API page', 'chapter_id' => $chapter->id, @@ -315,7 +315,7 @@ class PagesApiTest extends TestCase { $types = ['html', 'plaintext', 'pdf', 'markdown']; $this->actingAsApiEditor(); - $this->removePermissionFromUser($this->getEditor(), 'content-export'); + $this->permissions->removeUserRolePermissions($this->users->editor(), ['content-export']); $page = $this->entities->page(); foreach ($types as $type) { diff --git a/tests/Api/RecycleBinApiTest.php b/tests/Api/RecycleBinApiTest.php index bc7249987..d174838c2 100644 --- a/tests/Api/RecycleBinApiTest.php +++ b/tests/Api/RecycleBinApiTest.php @@ -21,8 +21,8 @@ class RecycleBinApiTest extends TestCase public function test_settings_manage_permission_needed_for_all_endpoints() { - $editor = $this->getEditor(); - $this->giveUserPermissions($editor, ['settings-manage']); + $editor = $this->users->editor(); + $this->permissions->grantUserRolePermissions($editor, ['settings-manage']); $this->actingAs($editor); foreach ($this->endpointMap as [$method, $uri]) { @@ -34,8 +34,8 @@ class RecycleBinApiTest extends TestCase public function test_restrictions_manage_all_permission_needed_for_all_endpoints() { - $editor = $this->getEditor(); - $this->giveUserPermissions($editor, ['restrictions-manage-all']); + $editor = $this->users->editor(); + $this->permissions->grantUserRolePermissions($editor, ['restrictions-manage-all']); $this->actingAs($editor); foreach ($this->endpointMap as [$method, $uri]) { @@ -47,7 +47,7 @@ class RecycleBinApiTest extends TestCase public function test_index_endpoint_returns_expected_page() { - $admin = $this->getAdmin(); + $admin = $this->users->admin(); $page = $this->entities->page(); $book = $this->entities->book(); @@ -82,7 +82,7 @@ class RecycleBinApiTest extends TestCase public function test_index_endpoint_returns_children_count() { - $admin = $this->getAdmin(); + $admin = $this->users->admin(); $book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first(); $this->actingAs($admin)->delete($book->getUrl()); @@ -109,7 +109,7 @@ class RecycleBinApiTest extends TestCase public function test_index_endpoint_returns_parent() { - $admin = $this->getAdmin(); + $admin = $this->users->admin(); $page = $this->entities->pageWithinChapter(); $this->actingAs($admin)->delete($page->getUrl()); diff --git a/tests/Api/TestsApi.php b/tests/Api/TestsApi.php index 0cdd93741..501f28754 100644 --- a/tests/Api/TestsApi.php +++ b/tests/Api/TestsApi.php @@ -12,7 +12,7 @@ trait TestsApi */ protected function actingAsApiEditor() { - $this->actingAs($this->getEditor(), 'api'); + $this->actingAs($this->users->editor(), 'api'); return $this; } @@ -22,7 +22,7 @@ trait TestsApi */ protected function actingAsApiAdmin() { - $this->actingAs($this->getAdmin(), 'api'); + $this->actingAs($this->users->admin(), 'api'); return $this; } diff --git a/tests/Api/UsersApiTest.php b/tests/Api/UsersApiTest.php index 739981f24..c89f9e6e3 100644 --- a/tests/Api/UsersApiTest.php +++ b/tests/Api/UsersApiTest.php @@ -175,7 +175,7 @@ class UsersApiTest extends TestCase { $this->actingAsApiAdmin(); /** @var User $user */ - $user = $this->getAdmin(); + $user = $this->users->admin(); $roles = Role::query()->pluck('id'); $resp = $this->putJson($this->baseEndpoint . "/{$user->id}", [ 'name' => 'My updated user', @@ -204,7 +204,7 @@ class UsersApiTest extends TestCase { $this->actingAsApiAdmin(); /** @var User $user */ - $user = $this->getAdmin(); + $user = $this->users->admin(); $roleCount = $user->roles()->count(); $resp = $this->putJson($this->baseEndpoint . "/{$user->id}", []); @@ -222,7 +222,7 @@ class UsersApiTest extends TestCase { $this->actingAsApiAdmin(); /** @var User $user */ - $user = User::query()->where('id', '!=', $this->getAdmin()->id) + $user = User::query()->where('id', '!=', $this->users->admin()->id) ->whereNull('system_name') ->first(); @@ -236,7 +236,7 @@ class UsersApiTest extends TestCase { $this->actingAsApiAdmin(); /** @var User $user */ - $user = User::query()->where('id', '!=', $this->getAdmin()->id) + $user = User::query()->where('id', '!=', $this->users->admin()->id) ->whereNull('system_name') ->first(); $entityChain = $this->entities->createChainBelongingToUser($user); diff --git a/tests/Auth/AuthTest.php b/tests/Auth/AuthTest.php index 3220b2aac..fe7e62568 100644 --- a/tests/Auth/AuthTest.php +++ b/tests/Auth/AuthTest.php @@ -44,7 +44,7 @@ class AuthTest extends TestCase public function test_mfa_session_cleared_on_logout() { - $user = $this->getEditor(); + $user = $this->users->editor(); $mfaSession = $this->app->make(MfaSession::class); $mfaSession->markVerifiedForUser($user); @@ -94,7 +94,7 @@ class AuthTest extends TestCase public function test_login_authenticates_nonadmins_on_default_guard_only() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $editor->password = bcrypt('password'); $editor->save(); @@ -120,7 +120,7 @@ class AuthTest extends TestCase public function test_logged_in_user_with_unconfirmed_email_is_logged_out() { $this->setSettings(['registration-confirmation' => 'true']); - $user = $this->getEditor(); + $user = $this->users->editor(); $user->email_confirmed = false; $user->save(); diff --git a/tests/Auth/GroupSyncServiceTest.php b/tests/Auth/GroupSyncServiceTest.php index 2fad53b26..dbf4110d8 100644 --- a/tests/Auth/GroupSyncServiceTest.php +++ b/tests/Auth/GroupSyncServiceTest.php @@ -11,7 +11,7 @@ class GroupSyncServiceTest extends TestCase { public function test_user_is_assigned_to_matching_roles() { - $user = $this->getViewer(); + $user = $this->users->viewer(); $roleA = Role::factory()->create(['display_name' => 'Wizards']); $roleB = Role::factory()->create(['display_name' => 'Gremlins']); @@ -33,7 +33,7 @@ class GroupSyncServiceTest extends TestCase public function test_multiple_values_in_role_external_auth_id_handled() { - $user = $this->getViewer(); + $user = $this->users->viewer(); $role = Role::factory()->create(['display_name' => 'ABC123', 'external_auth_id' => 'sales, engineering, developers, marketers']); $this->assertFalse($user->hasRole($role->id)); @@ -45,7 +45,7 @@ class GroupSyncServiceTest extends TestCase public function test_commas_can_be_used_in_external_auth_id_if_escaped() { - $user = $this->getViewer(); + $user = $this->users->viewer(); $role = Role::factory()->create(['display_name' => 'ABC123', 'external_auth_id' => 'sales\,-developers, marketers']); $this->assertFalse($user->hasRole($role->id)); @@ -57,7 +57,7 @@ class GroupSyncServiceTest extends TestCase public function test_external_auth_id_matches_ignoring_case() { - $user = $this->getViewer(); + $user = $this->users->viewer(); $role = Role::factory()->create(['display_name' => 'ABC123', 'external_auth_id' => 'WaRRioRs']); $this->assertFalse($user->hasRole($role->id)); diff --git a/tests/Auth/LdapTest.php b/tests/Auth/LdapTest.php index 978420f86..cac2ea5e1 100644 --- a/tests/Auth/LdapTest.php +++ b/tests/Auth/LdapTest.php @@ -235,7 +235,7 @@ class LdapTest extends TestCase public function test_user_edit_form() { - $editUser = $this->getNormalUser(); + $editUser = $this->users->viewer(); $editPage = $this->asAdmin()->get("/settings/users/{$editUser->id}"); $editPage->assertSee('Edit User'); $editPage->assertDontSee('Password'); @@ -257,7 +257,7 @@ class LdapTest extends TestCase public function test_non_admins_cannot_change_auth_id() { - $testUser = $this->getNormalUser(); + $testUser = $this->users->viewer(); $this->actingAs($testUser) ->get('/settings/users/' . $testUser->id) ->assertDontSee('External Authentication'); diff --git a/tests/Auth/LoginAutoInitiateTest.php b/tests/Auth/LoginAutoInitiateTest.php index 2d0384435..fcb4431af 100644 --- a/tests/Auth/LoginAutoInitiateTest.php +++ b/tests/Auth/LoginAutoInitiateTest.php @@ -70,7 +70,7 @@ class LoginAutoInitiateTest extends TestCase config()->set([ 'auth.method' => 'oidc', ]); - $this->actingAs($this->getEditor()); + $this->actingAs($this->users->editor()); $req = $this->post('/logout'); $req->assertRedirect('/login?prevent_auto_init=true'); diff --git a/tests/Auth/MfaConfigurationTest.php b/tests/Auth/MfaConfigurationTest.php index 3416263f3..fb941f00b 100644 --- a/tests/Auth/MfaConfigurationTest.php +++ b/tests/Auth/MfaConfigurationTest.php @@ -13,7 +13,7 @@ class MfaConfigurationTest extends TestCase { public function test_totp_setup() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->assertDatabaseMissing('mfa_values', ['user_id' => $editor->id]); // Setup page state @@ -66,7 +66,7 @@ class MfaConfigurationTest extends TestCase public function test_backup_codes_setup() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->assertDatabaseMissing('mfa_values', ['user_id' => $editor->id]); // Setup page state @@ -112,8 +112,8 @@ class MfaConfigurationTest extends TestCase public function test_mfa_method_count_is_visible_on_user_edit_page() { - $user = $this->getEditor(); - $resp = $this->actingAs($this->getAdmin())->get($user->getEditUrl()); + $user = $this->users->editor(); + $resp = $this->actingAs($this->users->admin())->get($user->getEditUrl()); $resp->assertSee('0 methods configured'); MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test'); @@ -127,17 +127,17 @@ class MfaConfigurationTest extends TestCase public function test_mfa_setup_link_only_shown_when_viewing_own_user_edit_page() { - $admin = $this->getAdmin(); + $admin = $this->users->admin(); $resp = $this->actingAs($admin)->get($admin->getEditUrl()); $this->withHtml($resp)->assertElementExists('a[href$="/mfa/setup"]'); - $resp = $this->actingAs($admin)->get($this->getEditor()->getEditUrl()); + $resp = $this->actingAs($admin)->get($this->users->editor()->getEditUrl()); $this->withHtml($resp)->assertElementNotExists('a[href$="/mfa/setup"]'); } public function test_mfa_indicator_shows_in_user_list() { - $admin = $this->getAdmin(); + $admin = $this->users->admin(); User::query()->where('id', '!=', $admin->id)->delete(); $resp = $this->actingAs($admin)->get('/settings/users'); @@ -150,7 +150,7 @@ class MfaConfigurationTest extends TestCase public function test_remove_mfa_method() { - $admin = $this->getAdmin(); + $admin = $this->users->admin(); MfaValue::upsertWithValue($admin, MfaValue::METHOD_TOTP, 'test'); $this->assertEquals(1, $admin->mfaValues()->count()); @@ -168,7 +168,7 @@ class MfaConfigurationTest extends TestCase public function test_totp_setup_url_shows_correct_user_when_setup_forced_upon_login() { - $admin = $this->getAdmin(); + $admin = $this->users->admin(); /** @var Role $role */ $role = $admin->roles()->first(); $role->mfa_enforced = true; diff --git a/tests/Auth/MfaVerificationTest.php b/tests/Auth/MfaVerificationTest.php index ba4c9b983..e23250314 100644 --- a/tests/Auth/MfaVerificationTest.php +++ b/tests/Auth/MfaVerificationTest.php @@ -140,7 +140,7 @@ class MfaVerificationTest extends TestCase public function test_both_mfa_options_available_if_set_on_profile() { - $user = $this->getEditor(); + $user = $this->users->editor(); $user->password = Hash::make('password'); $user->save(); @@ -165,7 +165,7 @@ class MfaVerificationTest extends TestCase public function test_mfa_required_with_no_methods_leads_to_setup() { - $user = $this->getEditor(); + $user = $this->users->editor(); $user->password = Hash::make('password'); $user->save(); /** @var Role $role */ @@ -222,7 +222,7 @@ class MfaVerificationTest extends TestCase // Attempted login user, who has configured mfa, access // Sets up user that has MFA required after attempted login. $loginService = $this->app->make(LoginService::class); - $user = $this->getEditor(); + $user = $this->users->editor(); /** @var Role $role */ $role = $user->roles->first(); $role->mfa_enforced = true; @@ -257,7 +257,7 @@ class MfaVerificationTest extends TestCase protected function startTotpLogin(): array { $secret = $this->app->make(TotpService::class)->generateSecret(); - $user = $this->getEditor(); + $user = $this->users->editor(); $user->password = Hash::make('password'); $user->save(); MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, $secret); @@ -274,7 +274,7 @@ class MfaVerificationTest extends TestCase */ protected function startBackupCodeLogin($codes = ['kzzu6-1pgll', 'bzxnf-plygd', 'bwdsp-ysl51', '1vo93-ioy7n', 'lf7nw-wdyka', 'xmtrd-oplac']): array { - $user = $this->getEditor(); + $user = $this->users->editor(); $user->password = Hash::make('password'); $user->save(); MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, json_encode($codes)); diff --git a/tests/Auth/OidcTest.php b/tests/Auth/OidcTest.php index db1f87bd5..32c2d4ae2 100644 --- a/tests/Auth/OidcTest.php +++ b/tests/Auth/OidcTest.php @@ -93,7 +93,7 @@ class OidcTest extends TestCase public function test_logout_route_functions() { - $this->actingAs($this->getEditor()); + $this->actingAs($this->users->editor()); $this->post('/logout'); $this->assertFalse(auth()->check()); } @@ -228,7 +228,7 @@ class OidcTest extends TestCase public function test_auth_login_as_existing_user() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $editor->external_auth_id = 'benny505'; $editor->save(); @@ -245,7 +245,7 @@ class OidcTest extends TestCase public function test_auth_login_as_existing_user_email_with_different_auth_id_fails() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $editor->external_auth_id = 'editor101'; $editor->save(); diff --git a/tests/Auth/ResetPasswordTest.php b/tests/Auth/ResetPasswordTest.php index 7b2d2e72b..72e26f10c 100644 --- a/tests/Auth/ResetPasswordTest.php +++ b/tests/Auth/ResetPasswordTest.php @@ -85,7 +85,7 @@ class ResetPasswordTest extends TestCase public function test_reset_request_is_throttled() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); Notification::fake(); $this->get('/password/email'); $this->followingRedirects()->post('/password/email', [ diff --git a/tests/Auth/Saml2Test.php b/tests/Auth/Saml2Test.php index 4c8d14dd5..0ee419610 100644 --- a/tests/Auth/Saml2Test.php +++ b/tests/Auth/Saml2Test.php @@ -170,7 +170,7 @@ class Saml2Test extends TestCase 'saml2.onelogin.strict' => false, ]); - $resp = $this->actingAs($this->getEditor())->get('/'); + $resp = $this->actingAs($this->users->editor())->get('/'); $this->withHtml($resp)->assertElementContains('form[action$="/saml2/logout"] button', 'Logout'); } diff --git a/tests/Auth/SocialAuthTest.php b/tests/Auth/SocialAuthTest.php index 67da771a5..24deedd5f 100644 --- a/tests/Auth/SocialAuthTest.php +++ b/tests/Auth/SocialAuthTest.php @@ -77,18 +77,18 @@ class SocialAuthTest extends TestCase // Test social callback with matching social account DB::table('social_accounts')->insert([ - 'user_id' => $this->getAdmin()->id, + 'user_id' => $this->users->admin()->id, 'driver' => 'github', 'driver_id' => 'logintest123', ]); $resp = $this->followingRedirects()->get('/login/service/github/callback'); $resp->assertDontSee('login-form'); - $this->assertActivityExists(ActivityType::AUTH_LOGIN, null, 'github; (' . $this->getAdmin()->id . ') ' . $this->getAdmin()->name); + $this->assertActivityExists(ActivityType::AUTH_LOGIN, null, 'github; (' . $this->users->admin()->id . ') ' . $this->users->admin()->name); } public function test_social_account_detach() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); config([ 'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc', 'APP_URL' => 'http://localhost', diff --git a/tests/Auth/UserInviteTest.php b/tests/Auth/UserInviteTest.php index ccbb538a6..e82ce4638 100644 --- a/tests/Auth/UserInviteTest.php +++ b/tests/Auth/UserInviteTest.php @@ -17,7 +17,7 @@ class UserInviteTest extends TestCase public function test_user_creation_creates_invite() { Notification::fake(); - $admin = $this->getAdmin(); + $admin = $this->users->admin(); $email = Str::random(16) . '@example.com'; $resp = $this->actingAs($admin)->post('/settings/users/create', [ @@ -38,7 +38,7 @@ class UserInviteTest extends TestCase public function test_user_invite_sent_in_selected_language() { Notification::fake(); - $admin = $this->getAdmin(); + $admin = $this->users->admin(); $email = Str::random(16) . '@example.com'; $resp = $this->actingAs($admin)->post('/settings/users/create', [ @@ -62,7 +62,7 @@ class UserInviteTest extends TestCase public function test_invite_set_password() { Notification::fake(); - $user = $this->getViewer(); + $user = $this->users->viewer(); $inviteService = app(UserInviteService::class); $inviteService->sendInvitation($user); @@ -91,7 +91,7 @@ class UserInviteTest extends TestCase public function test_invite_set_has_password_validation() { Notification::fake(); - $user = $this->getViewer(); + $user = $this->users->viewer(); $inviteService = app(UserInviteService::class); $inviteService->sendInvitation($user); @@ -126,7 +126,7 @@ class UserInviteTest extends TestCase public function test_token_expires_after_two_weeks() { Notification::fake(); - $user = $this->getViewer(); + $user = $this->users->viewer(); $inviteService = app(UserInviteService::class); $inviteService->sendInvitation($user); diff --git a/tests/Commands/ClearActivityCommandTest.php b/tests/Commands/ClearActivityCommandTest.php index cf2fba0d6..b2624e23d 100644 --- a/tests/Commands/ClearActivityCommandTest.php +++ b/tests/Commands/ClearActivityCommandTest.php @@ -19,7 +19,7 @@ class ClearActivityCommandTest extends TestCase $this->assertDatabaseHas('activities', [ 'type' => 'page_update', 'entity_id' => $page->id, - 'user_id' => $this->getEditor()->id, + 'user_id' => $this->users->editor()->id, ]); DB::rollBack(); diff --git a/tests/Commands/ClearViewsCommandTest.php b/tests/Commands/ClearViewsCommandTest.php index bbd06fa01..c9179089b 100644 --- a/tests/Commands/ClearViewsCommandTest.php +++ b/tests/Commands/ClearViewsCommandTest.php @@ -16,7 +16,7 @@ class ClearViewsCommandTest extends TestCase $this->get($page->getUrl()); $this->assertDatabaseHas('views', [ - 'user_id' => $this->getEditor()->id, + 'user_id' => $this->users->editor()->id, 'viewable_id' => $page->id, 'views' => 1, ]); @@ -27,7 +27,7 @@ class ClearViewsCommandTest extends TestCase $this->assertTrue($exitCode === 0, 'Command executed successfully'); $this->assertDatabaseMissing('views', [ - 'user_id' => $this->getEditor()->id, + 'user_id' => $this->users->editor()->id, ]); } } diff --git a/tests/Commands/CopyShelfPermissionsCommandTest.php b/tests/Commands/CopyShelfPermissionsCommandTest.php index cb9a845fd..c4b9fe6f3 100644 --- a/tests/Commands/CopyShelfPermissionsCommandTest.php +++ b/tests/Commands/CopyShelfPermissionsCommandTest.php @@ -18,11 +18,11 @@ class CopyShelfPermissionsCommandTest extends TestCase { $shelf = $this->entities->shelf(); $child = $shelf->books()->first(); - $editorRole = $this->getEditor()->roles()->first(); + $editorRole = $this->users->editor()->roles()->first(); $this->assertFalse($child->hasPermissions(), 'Child book should not be restricted by default'); $this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default'); - $this->entities->setPermissions($shelf, ['view', 'update'], [$editorRole]); + $this->permissions->setEntityPermissions($shelf, ['view', 'update'], [$editorRole]); $this->artisan('bookstack:copy-shelf-permissions', [ '--slug' => $shelf->slug, ]); @@ -43,11 +43,11 @@ class CopyShelfPermissionsCommandTest extends TestCase $shelf = $this->entities->shelf(); Bookshelf::query()->where('id', '!=', $shelf->id)->delete(); $child = $shelf->books()->first(); - $editorRole = $this->getEditor()->roles()->first(); + $editorRole = $this->users->editor()->roles()->first(); $this->assertFalse($child->hasPermissions(), 'Child book should not be restricted by default'); $this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default'); - $this->entities->setPermissions($shelf, ['view', 'update'], [$editorRole]); + $this->permissions->setEntityPermissions($shelf, ['view', 'update'], [$editorRole]); $this->artisan('bookstack:copy-shelf-permissions --all') ->expectsQuestion('Permission settings for all shelves will be cascaded. Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. Are you sure you want to proceed?', 'y'); $child = $shelf->books()->first(); diff --git a/tests/Commands/RegeneratePermissionsCommandTest.php b/tests/Commands/RegeneratePermissionsCommandTest.php index d514e5f9d..cc53b460d 100644 --- a/tests/Commands/RegeneratePermissionsCommandTest.php +++ b/tests/Commands/RegeneratePermissionsCommandTest.php @@ -2,8 +2,7 @@ namespace Tests\Commands; -use BookStack\Auth\Permissions\JointPermission; -use BookStack\Entities\Models\Page; +use BookStack\Auth\Permissions\CollapsedPermission; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; use Tests\TestCase; @@ -13,15 +12,23 @@ class RegeneratePermissionsCommandTest extends TestCase public function test_regen_permissions_command() { DB::rollBack(); - JointPermission::query()->truncate(); - $page = Page::first(); + $page = $this->entities->page(); + $editor = $this->users->editor(); + $this->permissions->addEntityPermission($page, ['view'], null, $editor); + CollapsedPermission::query()->truncate(); - $this->assertDatabaseMissing('joint_permissions', ['entity_id' => $page->id]); + $this->assertDatabaseMissing('entity_permissions_collapsed', ['entity_id' => $page->id]); $exitCode = Artisan::call('bookstack:regenerate-permissions'); $this->assertTrue($exitCode === 0, 'Command executed successfully'); - DB::beginTransaction(); - $this->assertDatabaseHas('joint_permissions', ['entity_id' => $page->id]); + $this->assertDatabaseHas('entity_permissions_collapsed', [ + 'entity_id' => $page->id, + 'user_id' => $editor->id, + 'view' => 1, + ]); + + CollapsedPermission::query()->truncate(); + DB::beginTransaction(); } } diff --git a/tests/Entity/BookShelfTest.php b/tests/Entity/BookShelfTest.php index 5d919f12b..5c6489281 100644 --- a/tests/Entity/BookShelfTest.php +++ b/tests/Entity/BookShelfTest.php @@ -16,21 +16,20 @@ class BookShelfTest extends TestCase public function test_shelves_shows_in_header_if_have_view_permissions() { - $viewer = $this->getViewer(); + $viewer = $this->users->viewer(); $resp = $this->actingAs($viewer)->get('/'); $this->withHtml($resp)->assertElementContains('header', 'Shelves'); $viewer->roles()->delete(); - $this->giveUserPermissions($viewer); $resp = $this->actingAs($viewer)->get('/'); $this->withHtml($resp)->assertElementNotContains('header', 'Shelves'); - $this->giveUserPermissions($viewer, ['bookshelf-view-all']); + $this->permissions->grantUserRolePermissions($viewer, ['bookshelf-view-all']); $resp = $this->actingAs($viewer)->get('/'); $this->withHtml($resp)->assertElementContains('header', 'Shelves'); $viewer->roles()->delete(); - $this->giveUserPermissions($viewer, ['bookshelf-view-own']); + $this->permissions->grantUserRolePermissions($viewer, ['bookshelf-view-own']); $resp = $this->actingAs($viewer)->get('/'); $this->withHtml($resp)->assertElementContains('header', 'Shelves'); } @@ -38,14 +37,14 @@ class BookShelfTest extends TestCase public function test_shelves_shows_in_header_if_have_any_shelve_view_permission() { $user = User::factory()->create(); - $this->giveUserPermissions($user, ['image-create-all']); + $this->permissions->grantUserRolePermissions($user, ['image-create-all']); $shelf = $this->entities->shelf(); $userRole = $user->roles()->first(); $resp = $this->actingAs($user)->get('/'); $this->withHtml($resp)->assertElementNotContains('header', 'Shelves'); - $this->entities->setPermissions($shelf, ['view'], [$userRole]); + $this->permissions->setEntityPermissions($shelf, ['view'], [$userRole]); $resp = $this->get('/'); $this->withHtml($resp)->assertElementContains('header', 'Shelves'); @@ -69,7 +68,7 @@ class BookShelfTest extends TestCase $resp->assertSee($book->name); $resp->assertSee($book->getUrl()); - $this->entities->setPermissions($book, []); + $this->permissions->setEntityPermissions($book, []); $resp = $this->asEditor()->get('/shelves'); $resp->assertDontSee($book->name); @@ -93,7 +92,7 @@ class BookShelfTest extends TestCase ], ])); $resp->assertRedirect(); - $editorId = $this->getEditor()->id; + $editorId = $this->users->editor()->id; $this->assertDatabaseHas('bookshelves', array_merge($shelfInfo, ['created_by' => $editorId, 'updated_by' => $editorId])); $shelf = Bookshelf::where('name', '=', $shelfInfo['name'])->first(); @@ -186,13 +185,13 @@ class BookShelfTest extends TestCase $this->withHtml($resp)->assertElementContains('.book-content a.grid-card:nth-child(1)', $books[0]->name); $this->withHtml($resp)->assertElementNotContains('.book-content a.grid-card:nth-child(3)', $books[0]->name); - setting()->putUser($this->getEditor(), 'shelf_books_sort_order', 'desc'); + setting()->putUser($this->users->editor(), 'shelf_books_sort_order', 'desc'); $resp = $this->asEditor()->get($shelf->getUrl()); $this->withHtml($resp)->assertElementNotContains('.book-content a.grid-card:nth-child(1)', $books[0]->name); $this->withHtml($resp)->assertElementContains('.book-content a.grid-card:nth-child(3)', $books[0]->name); - setting()->putUser($this->getEditor(), 'shelf_books_sort_order', 'desc'); - setting()->putUser($this->getEditor(), 'shelf_books_sort', 'name'); + setting()->putUser($this->users->editor(), 'shelf_books_sort_order', 'desc'); + setting()->putUser($this->users->editor(), 'shelf_books_sort', 'name'); $resp = $this->asEditor()->get($shelf->getUrl()); $this->withHtml($resp)->assertElementContains('.book-content a.grid-card:nth-child(1)', 'hdgfgdfg'); $this->withHtml($resp)->assertElementContains('.book-content a.grid-card:nth-child(2)', 'bsfsdfsdfsd'); @@ -224,7 +223,7 @@ class BookShelfTest extends TestCase $resp->assertRedirect($shelf->getUrl()); $this->assertSessionHas('success'); - $editorId = $this->getEditor()->id; + $editorId = $this->users->editor()->id; $this->assertDatabaseHas('bookshelves', array_merge($shelfInfo, ['id' => $shelf->id, 'created_by' => $editorId, 'updated_by' => $editorId])); $shelfPage = $this->get($shelf->getUrl()); @@ -294,11 +293,11 @@ class BookShelfTest extends TestCase $resp->assertSee("action=\"{$shelf->getUrl('/copy-permissions')}\"", false); $child = $shelf->books()->first(); - $editorRole = $this->getEditor()->roles()->first(); + $editorRole = $this->users->editor()->roles()->first(); $this->assertFalse($child->hasPermissions(), 'Child book should not be restricted by default'); $this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default'); - $this->entities->setPermissions($shelf, ['view', 'update'], [$editorRole]); + $this->permissions->setEntityPermissions($shelf, ['view', 'update'], [$editorRole]); $resp = $this->post($shelf->getUrl('/copy-permissions')); $child = $shelf->books()->first(); diff --git a/tests/Entity/BookTest.php b/tests/Entity/BookTest.php index 9e2750fd0..8435c534f 100644 --- a/tests/Entity/BookTest.php +++ b/tests/Entity/BookTest.php @@ -221,7 +221,7 @@ class BookTest extends TestCase public function test_books_view_shows_view_toggle_option() { /** @var Book $book */ - $editor = $this->getEditor(); + $editor = $this->users->editor(); setting()->putUser($editor, 'books_view_type', 'list'); $resp = $this->actingAs($editor)->get('/books'); @@ -304,7 +304,7 @@ class BookTest extends TestCase // Hide child content /** @var BookChild $page */ foreach ($book->getDirectChildren() as $child) { - $this->entities->setPermissions($child, [], []); + $this->permissions->setEntityPermissions($child, [], []); } $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']); @@ -318,8 +318,8 @@ class BookTest extends TestCase { /** @var Book $book */ $book = Book::query()->whereHas('chapters')->whereHas('directPages')->whereHas('chapters')->first(); - $viewer = $this->getViewer(); - $this->giveUserPermissions($viewer, ['book-create-all']); + $viewer = $this->users->viewer(); + $this->permissions->grantUserRolePermissions($viewer, ['book-create-all']); $this->actingAs($viewer)->post($book->getUrl('/copy'), ['name' => 'My copy book']); /** @var Book $copy */ @@ -354,9 +354,9 @@ class BookTest extends TestCase $shelfA->appendBook($book); $shelfB->appendBook($book); - $viewer = $this->getViewer(); - $this->giveUserPermissions($viewer, ['book-update-all', 'book-create-all', 'bookshelf-update-all']); - $this->entities->setPermissions($shelfB); + $viewer = $this->users->viewer(); + $this->permissions->grantUserRolePermissions($viewer, ['book-update-all', 'book-create-all', 'bookshelf-update-all']); + $this->permissions->setEntityPermissions($shelfB); $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']); diff --git a/tests/Entity/ChapterTest.php b/tests/Entity/ChapterTest.php index b726280c9..7fa32c252 100644 --- a/tests/Entity/ChapterTest.php +++ b/tests/Entity/ChapterTest.php @@ -101,7 +101,7 @@ class ChapterTest extends TestCase // Hide pages to all non-admin roles /** @var Page $page */ foreach ($chapter->pages as $page) { - $this->entities->setPermissions($page, [], []); + $this->permissions->setEntityPermissions($page, [], []); } $this->asEditor()->post($chapter->getUrl('/copy'), [ @@ -116,8 +116,8 @@ class ChapterTest extends TestCase public function test_copy_does_not_copy_pages_if_user_cant_page_create() { $chapter = $this->entities->chapterHasPages(); - $viewer = $this->getViewer(); - $this->giveUserPermissions($viewer, ['chapter-create-all']); + $viewer = $this->users->viewer(); + $this->permissions->grantUserRolePermissions($viewer, ['chapter-create-all']); // Lacking permission results in no copied pages $this->actingAs($viewer)->post($chapter->getUrl('/copy'), [ @@ -128,7 +128,7 @@ class ChapterTest extends TestCase $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first(); $this->assertEquals(0, $newChapter->pages()->count()); - $this->giveUserPermissions($viewer, ['page-create-all']); + $this->permissions->grantUserRolePermissions($viewer, ['page-create-all']); // Having permission rules in copied pages $this->actingAs($viewer)->post($chapter->getUrl('/copy'), [ @@ -144,7 +144,7 @@ class ChapterTest extends TestCase { $chapter = $this->entities->chapter(); - $resp = $this->actingAs($this->getViewer())->get($chapter->getUrl()); + $resp = $this->actingAs($this->users->viewer())->get($chapter->getUrl()); $this->withHtml($resp)->assertLinkNotExists($chapter->book->getUrl('sort')); $resp = $this->asEditor()->get($chapter->getUrl()); diff --git a/tests/Entity/ConvertTest.php b/tests/Entity/ConvertTest.php index 16dd89068..4beec7fa6 100644 --- a/tests/Entity/ConvertTest.php +++ b/tests/Entity/ConvertTest.php @@ -49,16 +49,16 @@ class ConvertTest extends TestCase public function test_convert_chapter_to_book_requires_permissions() { $chapter = $this->entities->chapter(); - $user = $this->getViewer(); + $user = $this->users->viewer(); $permissions = ['chapter-delete-all', 'book-create-all', 'chapter-update-all']; - $this->giveUserPermissions($user, $permissions); + $this->permissions->grantUserRolePermissions($user, $permissions); foreach ($permissions as $permission) { - $this->removePermissionFromUser($user, $permission); + $this->permissions->removeUserRolePermissions($user, [$permission]); $resp = $this->actingAs($user)->post($chapter->getUrl('/convert-to-book')); $this->assertPermissionError($resp); - $this->giveUserPermissions($user, [$permission]); + $this->permissions->grantUserRolePermissions($user, [$permission]); } $resp = $this->actingAs($user)->post($chapter->getUrl('/convert-to-book')); @@ -122,16 +122,16 @@ class ConvertTest extends TestCase public function test_book_convert_to_shelf_requires_permissions() { $book = $this->entities->book(); - $user = $this->getViewer(); + $user = $this->users->viewer(); $permissions = ['book-delete-all', 'bookshelf-create-all', 'book-update-all', 'book-create-all']; - $this->giveUserPermissions($user, $permissions); + $this->permissions->grantUserRolePermissions($user, $permissions); foreach ($permissions as $permission) { - $this->removePermissionFromUser($user, $permission); + $this->permissions->removeUserRolePermissions($user, [$permission]); $resp = $this->actingAs($user)->post($book->getUrl('/convert-to-shelf')); $this->assertPermissionError($resp); - $this->giveUserPermissions($user, [$permission]); + $this->permissions->grantUserRolePermissions($user, [$permission]); } $resp = $this->actingAs($user)->post($book->getUrl('/convert-to-shelf')); diff --git a/tests/Entity/EntityAccessTest.php b/tests/Entity/EntityAccessTest.php index 2bb32fde8..ab7587a3b 100644 --- a/tests/Entity/EntityAccessTest.php +++ b/tests/Entity/EntityAccessTest.php @@ -11,8 +11,8 @@ class EntityAccessTest extends TestCase public function test_entities_viewable_after_creator_deletion() { // Create required assets and revisions - $creator = $this->getEditor(); - $updater = $this->getViewer(); + $creator = $this->users->editor(); + $updater = $this->users->viewer(); $entities = $this->entities->createChainBelongingToUser($creator, $updater); app()->make(UserRepo::class)->destroy($creator); $this->entities->updatePage($entities['page'], ['html' => '

hello!

>']); @@ -23,8 +23,8 @@ class EntityAccessTest extends TestCase public function test_entities_viewable_after_updater_deletion() { // Create required assets and revisions - $creator = $this->getViewer(); - $updater = $this->getEditor(); + $creator = $this->users->viewer(); + $updater = $this->users->editor(); $entities = $this->entities->createChainBelongingToUser($creator, $updater); app()->make(UserRepo::class)->destroy($updater); $this->entities->updatePage($entities['page'], ['html' => '

Hello there!

']); diff --git a/tests/Entity/EntitySearchTest.php b/tests/Entity/EntitySearchTest.php index 2650b6743..4563fb651 100644 --- a/tests/Entity/EntitySearchTest.php +++ b/tests/Entity/EntitySearchTest.php @@ -132,7 +132,7 @@ class EntitySearchTest extends TestCase public function test_search_filters() { $page = $this->entities->newPage(['name' => 'My new test quaffleachits', 'html' => 'this is about an orange donkey danzorbhsing']); - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->actingAs($editor); // Viewed filter searches @@ -171,7 +171,7 @@ class EntitySearchTest extends TestCase // Restricted filter $this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertDontSee($page->name); - $this->entities->setPermissions($page, ['view'], [$editor->roles->first()]); + $this->permissions->setEntityPermissions($page, ['view'], [$editor->roles->first()]); $this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertSee($page->name); // Date filters @@ -235,7 +235,7 @@ class EntitySearchTest extends TestCase $this->withHtml($resp)->assertElementContains($baseSelector, $page->name); $this->withHtml($resp)->assertElementNotContains($baseSelector, "You don't have the required permissions to select this item"); - $resp = $this->actingAs($this->getViewer())->get($searchUrl); + $resp = $this->actingAs($this->users->viewer())->get($searchUrl); $this->withHtml($resp)->assertElementContains($baseSelector, $page->name); $this->withHtml($resp)->assertElementContains($baseSelector, "You don't have the required permissions to select this item"); } @@ -246,7 +246,7 @@ class EntitySearchTest extends TestCase $this->assertGreaterThan(2, count($chapter->pages), 'Ensure we\'re testing with at least 1 sibling'); $page = $chapter->pages->first(); - $search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page"); + $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page"); $search->assertSuccessful(); foreach ($chapter->pages as $page) { $search->assertSee($page->name); @@ -261,7 +261,7 @@ class EntitySearchTest extends TestCase $bookChildren = $page->book->getDirectChildren(); $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling'); - $search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page"); + $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page"); $search->assertSuccessful(); foreach ($bookChildren as $child) { $search->assertSee($child->name); @@ -276,7 +276,7 @@ class EntitySearchTest extends TestCase $bookChildren = $chapter->book->getDirectChildren(); $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling'); - $search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$chapter->id}&entity_type=chapter"); + $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$chapter->id}&entity_type=chapter"); $search->assertSuccessful(); foreach ($bookChildren as $child) { $search->assertSee($child->name); @@ -291,7 +291,7 @@ class EntitySearchTest extends TestCase $book = $books->first(); $this->assertGreaterThan(2, count($books), 'Ensure we\'re testing with at least 1 sibling'); - $search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$book->id}&entity_type=book"); + $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$book->id}&entity_type=book"); $search->assertSuccessful(); foreach ($books as $expectedBook) { $search->assertSee($expectedBook->name); @@ -304,7 +304,7 @@ class EntitySearchTest extends TestCase $shelf = $shelves->first(); $this->assertGreaterThan(2, count($shelves), 'Ensure we\'re testing with at least 1 sibling'); - $search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$shelf->id}&entity_type=bookshelf"); + $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$shelf->id}&entity_type=bookshelf"); $search->assertSuccessful(); foreach ($shelves as $expectedShelf) { $search->assertSee($expectedShelf->name); diff --git a/tests/Entity/ExportTest.php b/tests/Entity/ExportTest.php index 0f80bdd49..0f8d0f48c 100644 --- a/tests/Entity/ExportTest.php +++ b/tests/Entity/ExportTest.php @@ -275,7 +275,7 @@ class ExportTest extends TestCase public function test_page_export_with_deleted_creator_and_updater() { - $user = $this->getViewer(['name' => 'ExportWizardTheFifth']); + $user = $this->users->viewer(['name' => 'ExportWizardTheFifth']); $page = $this->entities->page(); $page->created_by = $user->id; $page->updated_by = $user->id; @@ -409,7 +409,7 @@ class ExportTest extends TestCase $chapter = $book->chapters()->first(); $page = $chapter->pages()->first(); $entities = [$book, $chapter, $page]; - $user = $this->getViewer(); + $user = $this->users->viewer(); $this->actingAs($user); foreach ($entities as $entity) { @@ -417,8 +417,7 @@ class ExportTest extends TestCase $resp->assertSee('/export/pdf'); } - /** @var Role $role */ - $this->removePermissionFromUser($user, 'content-export'); + $this->permissions->removeUserRolePermissions($user, ['content-export']); foreach ($entities as $entity) { $resp = $this->get($entity->getUrl()); diff --git a/tests/Entity/PageContentTest.php b/tests/Entity/PageContentTest.php index 0c9854206..e24ee4fb5 100644 --- a/tests/Entity/PageContentTest.php +++ b/tests/Entity/PageContentTest.php @@ -483,7 +483,7 @@ class PageContentTest extends TestCase { $page = $this->entities->page(); - $this->actingAs($this->getAdmin()) + $this->actingAs($this->users->admin()) ->put($page->getUrl(''), [ 'name' => 'Testing', 'html' => '

"Hello & welcome"

', diff --git a/tests/Entity/PageDraftTest.php b/tests/Entity/PageDraftTest.php index 010173852..75b1933ea 100644 --- a/tests/Entity/PageDraftTest.php +++ b/tests/Entity/PageDraftTest.php @@ -39,7 +39,7 @@ class PageDraftTest extends TestCase $this->withHtml($resp)->assertElementNotContains('[name="html"]', $addedContent); $newContent = $this->page->html . $addedContent; - $newUser = $this->getEditor(); + $newUser = $this->users->editor(); $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]); $resp = $this->actingAs($newUser)->get($this->page->getUrl('/edit')); @@ -62,7 +62,7 @@ class PageDraftTest extends TestCase $this->withHtml($resp)->assertElementNotContains('[name="html"]', $addedContent); $newContent = $this->page->html . $addedContent; - $newUser = $this->getEditor(); + $newUser = $this->users->editor(); $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]); $this->actingAs($newUser) @@ -75,8 +75,8 @@ class PageDraftTest extends TestCase public function test_draft_save_shows_alert_if_draft_older_than_last_page_update() { - $admin = $this->getAdmin(); - $editor = $this->getEditor(); + $admin = $this->users->admin(); + $editor = $this->users->editor(); $page = $this->entities->page(); $this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [ @@ -109,8 +109,8 @@ class PageDraftTest extends TestCase public function test_draft_save_shows_alert_if_draft_edit_started_by_someone_else() { - $admin = $this->getAdmin(); - $editor = $this->getEditor(); + $admin = $this->users->admin(); + $editor = $this->users->editor(); $page = $this->entities->page(); $this->actingAs($admin)->put('/ajax/page/' . $page->id . '/save-draft', [ @@ -143,7 +143,7 @@ class PageDraftTest extends TestCase { $book = $this->entities->book(); $chapter = $book->chapters->first(); - $newUser = $this->getEditor(); + $newUser = $this->users->editor(); $this->actingAs($newUser)->get($book->getUrl('/create-page')); $this->get($chapter->getUrl('/create-page')); diff --git a/tests/Entity/PageRevisionTest.php b/tests/Entity/PageRevisionTest.php index 0749888c8..0df37728e 100644 --- a/tests/Entity/PageRevisionTest.php +++ b/tests/Entity/PageRevisionTest.php @@ -208,13 +208,13 @@ class PageRevisionTest extends TestCase $page = $this->entities->page(); $this->createRevisions($page, 2); - $viewer = $this->getViewer(); + $viewer = $this->users->viewer(); $this->actingAs($viewer); $respHtml = $this->withHtml($this->get($page->getUrl('/revisions'))); $respHtml->assertElementNotContains('.actions a', 'Restore'); $respHtml->assertElementNotExists('form[action$="/restore"]'); - $this->giveUserPermissions($viewer, ['page-update-all']); + $this->permissions->grantUserRolePermissions($viewer, ['page-update-all']); $respHtml = $this->withHtml($this->get($page->getUrl('/revisions'))); $respHtml->assertElementContains('.actions a', 'Restore'); @@ -226,13 +226,13 @@ class PageRevisionTest extends TestCase $page = $this->entities->page(); $this->createRevisions($page, 2); - $viewer = $this->getViewer(); + $viewer = $this->users->viewer(); $this->actingAs($viewer); $respHtml = $this->withHtml($this->get($page->getUrl('/revisions'))); $respHtml->assertElementNotContains('.actions a', 'Delete'); $respHtml->assertElementNotExists('form[action$="/delete"]'); - $this->giveUserPermissions($viewer, ['page-delete-all']); + $this->permissions->grantUserRolePermissions($viewer, ['page-delete-all']); $respHtml = $this->withHtml($this->get($page->getUrl('/revisions'))); $respHtml->assertElementContains('.actions a', 'Delete'); diff --git a/tests/Entity/PageTemplateTest.php b/tests/Entity/PageTemplateTest.php index dc45fcfb8..6a68c3ab1 100644 --- a/tests/Entity/PageTemplateTest.php +++ b/tests/Entity/PageTemplateTest.php @@ -25,7 +25,7 @@ class PageTemplateTest extends TestCase public function test_manage_templates_permission_required_to_change_page_template_status() { $page = $this->entities->page(); - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->actingAs($editor); $pageUpdateData = [ @@ -40,7 +40,7 @@ class PageTemplateTest extends TestCase 'template' => false, ]); - $this->giveUserPermissions($editor, ['templates-manage']); + $this->permissions->grantUserRolePermissions($editor, ['templates-manage']); $this->put($page->getUrl(), $pageUpdateData); $this->assertDatabaseHas('pages', [ @@ -53,7 +53,7 @@ class PageTemplateTest extends TestCase { $content = '
my_custom_template_content
'; $page = $this->entities->page(); - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->actingAs($editor); $templateFetch = $this->get('/templates/' . $page->id); @@ -73,7 +73,7 @@ class PageTemplateTest extends TestCase public function test_template_endpoint_returns_paginated_list_of_templates() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->actingAs($editor); $toBeTemplates = Page::query()->orderBy('name', 'asc')->take(12)->get(); diff --git a/tests/Entity/PageTest.php b/tests/Entity/PageTest.php index f481ffb61..370c4381c 100644 --- a/tests/Entity/PageTest.php +++ b/tests/Entity/PageTest.php @@ -38,8 +38,8 @@ class PageTest extends TestCase public function test_page_view_when_creator_is_deleted_but_owner_exists() { $page = $this->entities->page(); - $user = $this->getViewer(); - $owner = $this->getEditor(); + $user = $this->users->viewer(); + $owner = $this->users->editor(); $page->created_by = $user->id; $page->owned_by = $owner->id; $page->save(); @@ -190,15 +190,15 @@ class PageTest extends TestCase $page = $this->entities->page(); $currentBook = $page->book; $newBook = Book::where('id', '!=', $currentBook->id)->first(); - $viewer = $this->getViewer(); + $viewer = $this->users->viewer(); $resp = $this->actingAs($viewer)->get($page->getUrl()); $resp->assertDontSee($page->getUrl('/copy')); $newBook->owned_by = $viewer->id; $newBook->save(); - $this->giveUserPermissions($viewer, ['page-create-own']); - $this->entities->regenPermissions($newBook); + $this->permissions->grantUserRolePermissions($viewer, ['page-create-own']); + $this->permissions->regenerateForEntity($newBook); $resp = $this->actingAs($viewer)->get($page->getUrl()); $resp->assertSee($page->getUrl('/copy')); @@ -249,7 +249,7 @@ class PageTest extends TestCase public function test_recently_updated_pages_view() { - $user = $this->getEditor(); + $user = $this->users->editor(); $content = $this->entities->createChainBelongingToUser($user); $resp = $this->asAdmin()->get('/pages/recently-updated'); @@ -258,7 +258,7 @@ class PageTest extends TestCase public function test_recently_updated_pages_view_shows_updated_by_details() { - $user = $this->getEditor(); + $user = $this->users->editor(); $page = $this->entities->page(); $this->actingAs($user)->put($page->getUrl(), [ @@ -272,7 +272,7 @@ class PageTest extends TestCase public function test_recently_updated_pages_view_shows_parent_chain() { - $user = $this->getEditor(); + $user = $this->users->editor(); $page = $this->entities->pageWithinChapter(); $this->actingAs($user)->put($page->getUrl(), [ @@ -287,7 +287,7 @@ class PageTest extends TestCase public function test_recently_updated_pages_view_does_not_show_parent_if_not_visible() { - $user = $this->getEditor(); + $user = $this->users->editor(); $page = $this->entities->pageWithinChapter(); $this->actingAs($user)->put($page->getUrl(), [ @@ -295,8 +295,8 @@ class PageTest extends TestCase 'html' => '

Updated content

', ]); - $this->entities->setPermissions($page->book); - $this->entities->setPermissions($page, ['view'], [$user->roles->first()]); + $this->permissions->setEntityPermissions($page->book); + $this->permissions->setEntityPermissions($page, ['view'], [$user->roles->first()]); $resp = $this->get('/pages/recently-updated'); $resp->assertDontSee($page->book->getShortName(42)); diff --git a/tests/Entity/SortTest.php b/tests/Entity/SortTest.php index f02e15d21..9a5a2fe17 100644 --- a/tests/Entity/SortTest.php +++ b/tests/Entity/SortTest.php @@ -53,7 +53,7 @@ class SortTest extends TestCase $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $newChapter = $newBook->chapters()->first(); - $movePageResp = $this->actingAs($this->getEditor())->put($page->getUrl('/move'), [ + $movePageResp = $this->actingAs($this->users->editor())->put($page->getUrl('/move'), [ 'entity_selection' => 'chapter:' . $newChapter->id, ]); $page->refresh(); @@ -71,7 +71,7 @@ class SortTest extends TestCase $page = $oldChapter->pages()->first(); $newBook = Book::query()->where('id', '!=', $oldChapter->book_id)->first(); - $movePageResp = $this->actingAs($this->getEditor())->put($page->getUrl('/move'), [ + $movePageResp = $this->actingAs($this->users->editor())->put($page->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, ]); $page->refresh(); @@ -89,16 +89,16 @@ class SortTest extends TestCase $page = $this->entities->page(); $currentBook = $page->book; $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); - $editor = $this->getEditor(); + $editor = $this->users->editor(); - $this->entities->setPermissions($newBook, ['view', 'update', 'delete'], $editor->roles->all()); + $this->permissions->setEntityPermissions($newBook, ['view', 'update', 'delete'], $editor->roles->all()); $movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, ]); $this->assertPermissionError($movePageResp); - $this->entities->setPermissions($newBook, ['view', 'update', 'delete', 'create'], $editor->roles->all()); + $this->permissions->setEntityPermissions($newBook, ['view', 'update', 'delete', 'create'], $editor->roles->all()); $movePageResp = $this->put($page->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, ]); @@ -114,10 +114,10 @@ class SortTest extends TestCase $page = $this->entities->page(); $currentBook = $page->book; $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); - $editor = $this->getEditor(); + $editor = $this->users->editor(); - $this->entities->setPermissions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all()); - $this->entities->setPermissions($page, ['view', 'update', 'create'], $editor->roles->all()); + $this->permissions->setEntityPermissions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all()); + $this->permissions->setEntityPermissions($page, ['view', 'update', 'create'], $editor->roles->all()); $movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, @@ -126,7 +126,7 @@ class SortTest extends TestCase $pageView = $this->get($page->getUrl()); $pageView->assertDontSee($page->getUrl('/move')); - $this->entities->setPermissions($page, ['view', 'update', 'create', 'delete'], $editor->roles->all()); + $this->permissions->setEntityPermissions($page, ['view', 'update', 'create', 'delete'], $editor->roles->all()); $movePageResp = $this->put($page->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, ]); @@ -169,10 +169,10 @@ class SortTest extends TestCase $chapter = $this->entities->chapter(); $currentBook = $chapter->book; $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); - $editor = $this->getEditor(); + $editor = $this->users->editor(); - $this->entities->setPermissions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all()); - $this->entities->setPermissions($chapter, ['view', 'update', 'create'], $editor->roles->all()); + $this->permissions->setEntityPermissions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all()); + $this->permissions->setEntityPermissions($chapter, ['view', 'update', 'create'], $editor->roles->all()); $moveChapterResp = $this->actingAs($editor)->put($chapter->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, @@ -181,7 +181,7 @@ class SortTest extends TestCase $pageView = $this->get($chapter->getUrl()); $pageView->assertDontSee($chapter->getUrl('/move')); - $this->entities->setPermissions($chapter, ['view', 'update', 'create', 'delete'], $editor->roles->all()); + $this->permissions->setEntityPermissions($chapter, ['view', 'update', 'create', 'delete'], $editor->roles->all()); $moveChapterResp = $this->put($chapter->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, ]); @@ -196,17 +196,17 @@ class SortTest extends TestCase $chapter = $this->entities->chapter(); $currentBook = $chapter->book; $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); - $editor = $this->getEditor(); + $editor = $this->users->editor(); - $this->entities->setPermissions($newBook, ['view', 'update', 'delete'], [$editor->roles->first()]); - $this->entities->setPermissions($chapter, ['view', 'update', 'create', 'delete'], [$editor->roles->first()]); + $this->permissions->setEntityPermissions($newBook, ['view', 'update', 'delete'], [$editor->roles->first()]); + $this->permissions->setEntityPermissions($chapter, ['view', 'update', 'create', 'delete'], [$editor->roles->first()]); $moveChapterResp = $this->actingAs($editor)->put($chapter->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, ]); $this->assertPermissionError($moveChapterResp); - $this->entities->setPermissions($newBook, ['view', 'update', 'create', 'delete'], [$editor->roles->first()]); + $this->permissions->setEntityPermissions($newBook, ['view', 'update', 'create', 'delete'], [$editor->roles->first()]); $moveChapterResp = $this->put($chapter->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, ]); @@ -313,7 +313,7 @@ class SortTest extends TestCase $page = $this->entities->pageWithinChapter(); /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); - $this->entities->setPermissions($otherChapter); + $this->permissions->setEntityPermissions($otherChapter); $sortData = [ 'id' => $page->id, @@ -334,8 +334,8 @@ class SortTest extends TestCase $page = $this->entities->pageWithinChapter(); /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); - $editor = $this->getEditor(); - $this->entities->setPermissions($otherChapter->book, ['update', 'delete'], [$editor->roles()->first()]); + $editor = $this->users->editor(); + $this->permissions->setEntityPermissions($otherChapter->book, ['update', 'delete'], [$editor->roles()->first()]); $sortData = [ 'id' => $page->id, @@ -356,8 +356,8 @@ class SortTest extends TestCase $page = $this->entities->pageWithinChapter(); /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); - $editor = $this->getEditor(); - $this->entities->setPermissions($otherChapter, ['view', 'delete'], [$editor->roles()->first()]); + $editor = $this->users->editor(); + $this->permissions->setEntityPermissions($otherChapter, ['view', 'delete'], [$editor->roles()->first()]); $sortData = [ 'id' => $page->id, @@ -378,8 +378,8 @@ class SortTest extends TestCase $page = $this->entities->pageWithinChapter(); /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); - $editor = $this->getEditor(); - $this->entities->setPermissions($page, ['view', 'delete'], [$editor->roles()->first()]); + $editor = $this->users->editor(); + $this->permissions->setEntityPermissions($page, ['view', 'delete'], [$editor->roles()->first()]); $sortData = [ 'id' => $page->id, @@ -400,8 +400,8 @@ class SortTest extends TestCase $page = $this->entities->pageWithinChapter(); /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); - $editor = $this->getEditor(); - $this->entities->setPermissions($page, ['view', 'update'], [$editor->roles()->first()]); + $editor = $this->users->editor(); + $this->permissions->setEntityPermissions($page, ['view', 'update'], [$editor->roles()->first()]); $sortData = [ 'id' => $page->id, diff --git a/tests/Entity/TagTest.php b/tests/Entity/TagTest.php index ab06686e0..7e6674959 100644 --- a/tests/Entity/TagTest.php +++ b/tests/Entity/TagTest.php @@ -75,7 +75,7 @@ class TagTest extends TestCase $this->asEditor()->get('/ajax/tags/suggest/names?search=co')->assertSimilarJson(['color', 'country']); // Set restricted permission the page - $this->entities->setPermissions($page, [], []); + $this->permissions->setEntityPermissions($page, [], []); $this->asAdmin()->get('/ajax/tags/suggest/names?search=co')->assertSimilarJson(['color', 'country']); $this->asEditor()->get('/ajax/tags/suggest/names?search=co')->assertSimilarJson([]); @@ -178,7 +178,7 @@ class TagTest extends TestCase $resp = $this->get('/tags?name=SuperCategory'); $resp->assertSee('GreatTestContent'); - $this->entities->setPermissions($page, [], []); + $this->permissions->setEntityPermissions($page, [], []); $resp = $this->asEditor()->get('/tags'); $resp->assertDontSee('SuperCategory'); diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index ebd9874d3..6ba01dd88 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -11,7 +11,7 @@ class ErrorTest extends TestCase // Due to middleware being handled differently this will not fail // if our custom, middleware-loaded handler fails but this is here // as a reminder and as a general check in the event of other issues. - $editor = $this->getEditor(); + $editor = $this->users->editor(); $editor->name = 'tester'; $editor->save(); @@ -24,7 +24,7 @@ class ErrorTest extends TestCase public function test_item_not_found_does_not_get_logged_to_file() { - $this->actingAs($this->getViewer()); + $this->actingAs($this->users->viewer()); $handler = $this->withTestLogger(); $book = $this->entities->book(); @@ -41,7 +41,7 @@ class ErrorTest extends TestCase public function test_access_to_non_existing_image_location_provides_404_response() { - $resp = $this->actingAs($this->getViewer())->get('/uploads/images/gallery/2021-05/anonexistingimage.png'); + $resp = $this->actingAs($this->users->viewer())->get('/uploads/images/gallery/2021-05/anonexistingimage.png'); $resp->assertStatus(404); $resp->assertSeeText('Image Not Found'); } diff --git a/tests/FavouriteTest.php b/tests/FavouriteTest.php index 456f2213c..7778aa8e9 100644 --- a/tests/FavouriteTest.php +++ b/tests/FavouriteTest.php @@ -10,7 +10,7 @@ class FavouriteTest extends TestCase public function test_page_add_favourite_flow() { $page = $this->entities->page(); - $editor = $this->getEditor(); + $editor = $this->users->editor(); $resp = $this->actingAs($editor)->get($page->getUrl()); $this->withHtml($resp)->assertElementContains('button', 'Favourite'); @@ -33,7 +33,7 @@ class FavouriteTest extends TestCase public function test_page_remove_favourite_flow() { $page = $this->entities->page(); - $editor = $this->getEditor(); + $editor = $this->users->editor(); Favourite::query()->forceCreate([ 'user_id' => $editor->id, 'favouritable_id' => $page->id, @@ -63,7 +63,7 @@ class FavouriteTest extends TestCase $book->owned_by = $user->id; $book->save(); - $this->giveUserPermissions($user, ['book-view-own']); + $this->permissions->grantUserRolePermissions($user, ['book-view-own']); $this->actingAs($user)->get($book->getUrl()); $resp = $this->post('/favourites/add', [ @@ -81,7 +81,7 @@ class FavouriteTest extends TestCase public function test_each_entity_type_shows_favourite_button() { - $this->actingAs($this->getEditor()); + $this->actingAs($this->users->editor()); foreach ($this->entities->all() as $entity) { $resp = $this->get($entity->getUrl()); @@ -94,13 +94,13 @@ class FavouriteTest extends TestCase $this->setSettings(['app-public' => 'true']); $resp = $this->get('/'); $this->withHtml($resp)->assertElementNotContains('header', 'My Favourites'); - $resp = $this->actingAs($this->getViewer())->get('/'); + $resp = $this->actingAs($this->users->viewer())->get('/'); $this->withHtml($resp)->assertElementContains('header a', 'My Favourites'); } public function test_favourites_shown_on_homepage() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $resp = $this->actingAs($editor)->get('/'); $this->withHtml($resp)->assertElementNotExists('#top-favourites'); @@ -116,7 +116,7 @@ class FavouriteTest extends TestCase public function test_favourites_list_page_shows_favourites_and_has_working_pagination() { $page = $this->entities->page(); - $editor = $this->getEditor(); + $editor = $this->users->editor(); $resp = $this->actingAs($editor)->get('/favourites'); $resp->assertDontSee($page->name); diff --git a/tests/Helpers/EntityProvider.php b/tests/Helpers/EntityProvider.php index 9e8cf0b73..d79015f75 100644 --- a/tests/Helpers/EntityProvider.php +++ b/tests/Helpers/EntityProvider.php @@ -2,8 +2,6 @@ namespace Tests\Helpers; -use BookStack\Auth\Permissions\EntityPermission; -use BookStack\Auth\Role; use BookStack\Auth\User; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; @@ -186,44 +184,6 @@ class EntityProvider return $pageRepo->publishDraft($draftPage, $input); } - /** - * Regenerate the permission for an entity. - * Centralised to manage clearing of cached elements between requests. - */ - public function regenPermissions(Entity $entity): void - { - $entity->rebuildPermissions(); - $entity->load('jointPermissions'); - } - - /** - * Set the given entity as having restricted permissions, and apply the given - * permissions for the given roles. - * @param string[] $actions - * @param Role[] $roles - */ - public function setPermissions(Entity $entity, array $actions = [], array $roles = []): void - { - $entity->permissions()->delete(); - - $permissions = [ - // Set default permissions to not allow actions so that only the provided role permissions are at play. - ['role_id' => 0, 'view' => false, 'create' => false, 'update' => false, 'delete' => false], - ]; - - foreach ($roles as $role) { - $permission = ['role_id' => $role->id]; - foreach (EntityPermission::PERMISSIONS as $possibleAction) { - $permission[$possibleAction] = in_array($possibleAction, $actions); - } - $permissions[] = $permission; - } - - $entity->permissions()->createMany($permissions); - $entity->load('permissions'); - $this->regenPermissions($entity); - } - /** * @param Entity|Entity[] $entities */ diff --git a/tests/Helpers/PermissionsProvider.php b/tests/Helpers/PermissionsProvider.php new file mode 100644 index 000000000..ac9a2a68a --- /dev/null +++ b/tests/Helpers/PermissionsProvider.php @@ -0,0 +1,136 @@ +userRoleProvider = $userRoleProvider; + } + + /** + * Grant role permissions to the provided user. + */ + public function grantUserRolePermissions(User $user, array $permissions): void + { + $newRole = $this->userRoleProvider->createRole($permissions); + $user->attachRole($newRole); + $user->load('roles'); + $user->clearPermissionCache(); + } + + /** + * Completely remove specific role permissions from the provided user. + */ + public function removeUserRolePermissions(User $user, array $permissions): void + { + foreach ($permissions as $permissionName) { + /** @var RolePermission $permission */ + $permission = RolePermission::query() + ->where('name', '=', $permissionName) + ->firstOrFail(); + + $roles = $user->roles()->whereHas('permissions', function ($query) use ($permission) { + $query->where('id', '=', $permission->id); + })->get(); + + /** @var Role $role */ + foreach ($roles as $role) { + $role->detachPermission($permission); + } + + $user->clearPermissionCache(); + } + } + + /** + * Change the owner of the given entity to the given user. + */ + public function changeEntityOwner(Entity $entity, User $newOwner): void + { + $entity->owned_by = $newOwner->id; + $entity->save(); + $entity->rebuildPermissions(); + } + + /** + * Regenerate the permission for an entity. + * Centralised to manage clearing of cached elements between requests. + */ + public function regenerateForEntity(Entity $entity): void + { + $entity->rebuildPermissions(); + } + + /** + * Set the given entity as having restricted permissions, and apply the given + * permissions for the given roles. + * @param string[] $actions + * @param Role[] $roles + */ + public function setEntityPermissions(Entity $entity, array $actions = [], array $roles = [], $inherit = false): void + { + $entity->permissions()->delete(); + + $permissions = []; + + if (!$inherit) { + // Set default permissions to not allow actions so that only the provided role permissions are at play. + $permissions[] = ['role_id' => null, 'user_id' => null, 'view' => false, 'create' => false, 'update' => false, 'delete' => false]; + } + + foreach ($roles as $role) { + $permissions[] = $this->actionListToEntityPermissionData($actions, $role->id); + } + + $this->addEntityPermissionEntries($entity, $permissions); + } + + public function addEntityPermission(Entity $entity, array $actionList, ?Role $role = null, ?User $user = null) + { + $permissionData = $this->actionListToEntityPermissionData($actionList, $role->id ?? null, $user->id ?? null); + $this->addEntityPermissionEntries($entity, [$permissionData]); + } + + /** + * Disable inherited permissions on the given entity. + * Effectively sets the "Other Users" UI permission option to not inherit, with no permissions. + */ + public function disableEntityInheritedPermissions(Entity $entity): void + { + $entity->permissions()->whereNull(['user_id', 'role_id'])->delete(); + $fallback = $this->actionListToEntityPermissionData([]); + $this->addEntityPermissionEntries($entity, [$fallback]); + } + + protected function addEntityPermissionEntries(Entity $entity, array $entityPermissionData): void + { + $entity->permissions()->createMany($entityPermissionData); + $entity->load('permissions'); + $this->regenerateForEntity($entity); + } + + /** + * For the given simple array of string actions (view, create, update, delete), convert + * the format to entity permission data, where permission is granted if the action is in the + * given actionList array. + */ + protected function actionListToEntityPermissionData(array $actionList, int $roleId = null, int $userId = null): array + { + $permissionData = ['role_id' => $roleId, 'user_id' => $userId]; + foreach (EntityPermission::PERMISSIONS as $possibleAction) { + $permissionData[$possibleAction] = in_array($possibleAction, $actionList); + } + + return $permissionData; + } +} diff --git a/tests/Helpers/UserRoleProvider.php b/tests/Helpers/UserRoleProvider.php new file mode 100644 index 000000000..355c1687c --- /dev/null +++ b/tests/Helpers/UserRoleProvider.php @@ -0,0 +1,97 @@ +admin)) { + $adminRole = Role::getSystemRole('admin'); + $this->admin = $adminRole->users->first(); + } + + return $this->admin; + } + + /** + * Get a typical "Editor" user. + */ + public function editor(): User + { + if ($this->editor === null) { + $editorRole = Role::getRole('editor'); + $this->editor = $editorRole->users->first(); + } + + return $this->editor; + } + + /** + * Get a typical "Viewer" user. + */ + public function viewer(array $attributes = []): User + { + $user = Role::getRole('viewer')->users()->first(); + if (!empty($attributes)) { + $user->forceFill($attributes)->save(); + } + + return $user; + } + + /** + * Create a new fresh user without any relations. + */ + public function newUser(array $attrs = []): User + { + return User::factory()->create($attrs); + } + + /** + * Create a new fresh user, with the given attrs, that has assigned a fresh role + * that has the given role permissions. + * Intended as a helper to create a blank slate baseline user and role. + * @return array{0: User, 1: Role} + */ + public function newUserWithRole(array $userAttrs = [], array $rolePermissions = []): array + { + $user = $this->newUser($userAttrs); + $role = $this->attachNewRole($user, $rolePermissions); + + return [$user, $role]; + } + + /** + * Attach a new role, with the given role permissions, to the given user + * and return that role. + */ + public function attachNewRole(User $user, array $rolePermissions = []): Role + { + $role = $this->createRole($rolePermissions); + $user->attachRole($role); + return $role; + } + + /** + * Create a new basic role with the given role permissions. + */ + public function createRole(array $rolePermissions = []): Role + { + $permissionRepo = app(PermissionsRepo::class); + $roleData = Role::factory()->make()->toArray(); + $roleData['permissions'] = array_flip($rolePermissions); + + return $permissionRepo->saveNewRole($roleData); + } +} diff --git a/tests/HomepageTest.php b/tests/HomepageTest.php index cf69425fb..c7e8b69bb 100644 --- a/tests/HomepageTest.php +++ b/tests/HomepageTest.php @@ -114,7 +114,7 @@ class HomepageTest extends TestCase public function test_set_book_homepage() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); setting()->putUser($editor, 'books_view_type', 'grid'); $this->setSettings(['app-homepage-type' => 'books']); @@ -133,7 +133,7 @@ class HomepageTest extends TestCase public function test_set_bookshelves_homepage() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); setting()->putUser($editor, 'bookshelves_view_type', 'grid'); $shelf = $this->entities->shelf(); @@ -152,7 +152,7 @@ class HomepageTest extends TestCase public function test_shelves_list_homepage_adheres_to_book_visibility_permissions() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); setting()->putUser($editor, 'bookshelves_view_type', 'list'); $this->setSettings(['app-homepage-type' => 'bookshelves']); $this->asEditor(); @@ -167,13 +167,13 @@ class HomepageTest extends TestCase // Ensure book no longer visible without view permission $editor->roles()->detach(); - $this->giveUserPermissions($editor, ['bookshelf-view-all']); + $this->permissions->grantUserRolePermissions($editor, ['bookshelf-view-all']); $homeVisit = $this->get('/'); $this->withHtml($homeVisit)->assertElementContains('.content-wrap', $shelf->name); $this->withHtml($homeVisit)->assertElementNotContains('.content-wrap', $book->name); // Ensure is visible again with entity-level view permission - $this->entities->setPermissions($book, ['view'], [$editor->roles()->first()]); + $this->permissions->setEntityPermissions($book, ['view'], [$editor->roles()->first()]); $homeVisit = $this->get('/'); $this->withHtml($homeVisit)->assertElementContains('.content-wrap', $shelf->name); $this->withHtml($homeVisit)->assertElementContains('.content-wrap', $book->name); diff --git a/tests/LanguageTest.php b/tests/LanguageTest.php index 27de5f875..ba522a74e 100644 --- a/tests/LanguageTest.php +++ b/tests/LanguageTest.php @@ -4,7 +4,7 @@ namespace Tests; class LanguageTest extends TestCase { - protected array $langs; + protected $langs; /** * LanguageTest constructor. @@ -77,20 +77,8 @@ class LanguageTest extends TestCase { $this->asEditor(); $this->assertFalse(config('app.rtl'), 'App RTL config should be false by default'); - setting()->putUser($this->getEditor(), 'language', 'ar'); + setting()->putUser($this->users->editor(), 'language', 'ar'); $this->get('/'); $this->assertTrue(config('app.rtl'), 'App RTL config should have been set to true by middleware'); } - - public function test_pluralisation_for_non_standard_locales() - { - $text = trans_choice('entities.x_pages', 1, [], 'de_informal'); - $this->assertEquals('1 Seite', $text); - - $text = trans_choice('entities.x_pages', 2, [], 'de_informal'); - $this->assertEquals('2 Seiten', $text); - - $text = trans_choice('entities.x_pages', 0, [], 'de_informal'); - $this->assertEquals('0 Seiten', $text); - } } diff --git a/tests/Permissions/EntityPermissionsTest.php b/tests/Permissions/EntityPermissionsTest.php index 4b613b49c..68a4ed244 100644 --- a/tests/Permissions/EntityPermissionsTest.php +++ b/tests/Permissions/EntityPermissionsTest.php @@ -2,6 +2,7 @@ namespace Tests\Permissions; +use BookStack\Auth\Role; use BookStack\Auth\User; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; @@ -20,8 +21,8 @@ class EntityPermissionsTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->user = $this->getEditor(); - $this->viewer = $this->getViewer(); + $this->user = $this->users->editor(); + $this->viewer = $this->users->viewer(); } protected function setRestrictionsForTestRoles(Entity $entity, array $actions = []) @@ -30,7 +31,7 @@ class EntityPermissionsTest extends TestCase $this->user->roles->first(), $this->viewer->roles->first(), ]; - $this->entities->setPermissions($entity, $actions, $roles); + $this->permissions->setEntityPermissions($entity, $actions, $roles); } public function test_bookshelf_view_restriction() @@ -378,8 +379,10 @@ class EntityPermissionsTest extends TestCase $this->put($modelInstance->getUrl('/permissions'), [ 'permissions' => [ - $roleId => [ - $permission => 'true', + 'role' => [ + $roleId => [ + $permission => 'true', + ], ], ], ]); @@ -655,6 +658,34 @@ class EntityPermissionsTest extends TestCase $resp->assertRedirect($book->getUrl('/page/test-page')); } + public function test_access_to_item_prevented_if_inheritance_active_but_permission_prevented_via_role() + { + $user = $this->users->viewer(); + $viewerRole = $user->roles->first(); + $chapter = $this->entities->chapter(); + $book = $chapter->book; + + $this->permissions->setEntityPermissions($book, ['edit'], [$viewerRole], false); + $this->permissions->setEntityPermissions($chapter, [], [$viewerRole], true); + + $this->assertFalse(userCan('chapter-update', $chapter)); + } + + public function test_access_to_item_allowed_if_inheritance_active_and_permission_prevented_via_role_but_allowed_via_parent() + { + $user = $this->users->viewer(); + $viewerRole = $user->roles->first(); + $editorRole = Role::getRole('Editor'); + $user->attachRole($editorRole); + $chapter = $this->entities->chapter(); + $book = $chapter->book; + + $this->permissions->setEntityPermissions($book, ['edit'], [$editorRole], false); + $this->permissions->setEntityPermissions($chapter, [], [$viewerRole], true); + + $this->assertTrue(userCan('chapter-update', $chapter)); + } + public function test_book_permissions_can_be_generated_without_error_if_child_chapter_is_in_recycle_bin() { $book = $this->entities->bookHasChaptersAndPages(); @@ -665,7 +696,7 @@ class EntityPermissionsTest extends TestCase $error = null; try { - $this->entities->setPermissions($book, ['view'], []); + $this->permissions->setEntityPermissions($book, ['view'], []); } catch (Exception $e) { $error = $e; } diff --git a/tests/Permissions/ExportPermissionsTest.php b/tests/Permissions/ExportPermissionsTest.php index 642cf1beb..8072221e5 100644 --- a/tests/Permissions/ExportPermissionsTest.php +++ b/tests/Permissions/ExportPermissionsTest.php @@ -14,7 +14,7 @@ class ExportPermissionsTest extends TestCase $pageContent = Str::random(48); $page->html = '

' . $pageContent . '

'; $page->save(); - $viewer = $this->getViewer(); + $viewer = $this->users->viewer(); $this->actingAs($viewer); $formats = ['html', 'plaintext']; @@ -25,7 +25,7 @@ class ExportPermissionsTest extends TestCase $resp->assertSee($pageContent); } - $this->entities->setPermissions($page, []); + $this->permissions->setEntityPermissions($page, []); foreach ($formats as $format) { $resp = $this->get($chapter->getUrl("export/{$format}")); @@ -42,7 +42,7 @@ class ExportPermissionsTest extends TestCase $pageContent = Str::random(48); $page->html = '

' . $pageContent . '

'; $page->save(); - $viewer = $this->getViewer(); + $viewer = $this->users->viewer(); $this->actingAs($viewer); $formats = ['html', 'plaintext']; @@ -53,7 +53,7 @@ class ExportPermissionsTest extends TestCase $resp->assertSee($pageContent); } - $this->entities->setPermissions($page, []); + $this->permissions->setEntityPermissions($page, []); foreach ($formats as $format) { $resp = $this->get($book->getUrl("export/{$format}")); diff --git a/tests/Permissions/RolesTest.php b/tests/Permissions/RolesTest.php index 88d400259..8bf700c07 100644 --- a/tests/Permissions/RolesTest.php +++ b/tests/Permissions/RolesTest.php @@ -22,7 +22,7 @@ class RolesTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->user = $this->getViewer(); + $this->user = $this->users->viewer(); } public function test_admin_can_see_settings() @@ -42,7 +42,7 @@ class RolesTest extends TestCase public function test_role_cannot_be_deleted_if_default() { - $newRole = $this->createNewRole(); + $newRole = $this->users->createRole(); $this->setSettings(['registration-role' => $newRole->id]); $deletePageUrl = '/settings/roles/delete/' . $newRole->id; @@ -121,11 +121,11 @@ class RolesTest extends TestCase { /** @var Role $adminRole */ $adminRole = Role::query()->where('system_name', '=', 'admin')->first(); - $adminUser = $this->getAdmin(); + $adminUser = $this->users->admin(); $adminRole->users()->where('id', '!=', $adminUser->id)->delete(); $this->assertEquals(1, $adminRole->users()->count()); - $viewerRole = $this->getViewer()->roles()->first(); + $viewerRole = $this->users->viewer()->roles()->first(); $editUrl = '/settings/users/' . $adminUser->id; $resp = $this->actingAs($adminUser)->put($editUrl, [ @@ -169,7 +169,7 @@ class RolesTest extends TestCase $roleA = Role::query()->create(['display_name' => 'Entity Permissions Delete Test']); $page = $this->entities->page(); - $this->entities->setPermissions($page, ['view'], [$roleA]); + $this->permissions->setEntityPermissions($page, ['view'], [$roleA]); $this->assertDatabaseHas('entity_permissions', [ 'role_id' => $roleA->id, @@ -214,7 +214,7 @@ class RolesTest extends TestCase public function test_manage_user_permission() { $this->actingAs($this->user)->get('/settings/users')->assertRedirect('/'); - $this->giveUserPermissions($this->user, ['users-manage']); + $this->permissions->grantUserRolePermissions($this->user, ['users-manage']); $this->actingAs($this->user)->get('/settings/users')->assertOk(); } @@ -222,9 +222,9 @@ class RolesTest extends TestCase { $usersLink = 'href="' . url('/settings/users') . '"'; $this->actingAs($this->user)->get('/')->assertDontSee($usersLink, false); - $this->giveUserPermissions($this->user, ['users-manage']); + $this->permissions->grantUserRolePermissions($this->user, ['users-manage']); $this->actingAs($this->user)->get('/')->assertSee($usersLink, false); - $this->giveUserPermissions($this->user, ['settings-manage', 'users-manage']); + $this->permissions->grantUserRolePermissions($this->user, ['settings-manage', 'users-manage']); $this->actingAs($this->user)->get('/')->assertDontSee($usersLink, false); } @@ -247,7 +247,7 @@ class RolesTest extends TestCase 'name' => 'my_new_name', ]); - $this->giveUserPermissions($this->user, ['users-manage']); + $this->permissions->grantUserRolePermissions($this->user, ['users-manage']); $resp = $this->get($userProfileUrl) ->assertOk(); @@ -269,7 +269,7 @@ class RolesTest extends TestCase { $this->actingAs($this->user)->get('/settings/roles')->assertRedirect('/'); $this->get('/settings/roles/1')->assertRedirect('/'); - $this->giveUserPermissions($this->user, ['user-roles-manage']); + $this->permissions->grantUserRolePermissions($this->user, ['user-roles-manage']); $this->actingAs($this->user)->get('/settings/roles')->assertOk(); $this->get('/settings/roles/1') ->assertOk() @@ -279,7 +279,7 @@ class RolesTest extends TestCase public function test_settings_manage_permission() { $this->actingAs($this->user)->get('/settings/features')->assertRedirect('/'); - $this->giveUserPermissions($this->user, ['settings-manage']); + $this->permissions->grantUserRolePermissions($this->user, ['settings-manage']); $this->get('/settings/features')->assertOk(); $resp = $this->post('/settings/features', []); @@ -295,7 +295,7 @@ class RolesTest extends TestCase $this->actingAs($this->user)->get($page->getUrl())->assertDontSee('Permissions'); $this->get($page->getUrl('/permissions'))->assertRedirect('/'); - $this->giveUserPermissions($this->user, ['restrictions-manage-all']); + $this->permissions->grantUserRolePermissions($this->user, ['restrictions-manage-all']); $this->actingAs($this->user)->get($page->getUrl())->assertSee('Permissions'); @@ -325,7 +325,7 @@ class RolesTest extends TestCase $this->actingAs($this->user)->get($page->getUrl())->assertDontSee('Permissions'); $this->get($page->getUrl('/permissions'))->assertRedirect('/'); - $this->giveUserPermissions($this->user, ['restrictions-manage-own']); + $this->permissions->grantUserRolePermissions($this->user, ['restrictions-manage-own']); // Check can't restrict other's content $this->actingAs($this->user)->get($otherUsersPage->getUrl())->assertDontSee('Permissions'); @@ -350,7 +350,7 @@ class RolesTest extends TestCase $this->withHtml($resp)->assertElementNotContains('.action-buttons', $text); } - $this->giveUserPermissions($this->user, [$permission]); + $this->permissions->grantUserRolePermissions($this->user, [$permission]); foreach ($accessUrls as $url) { $this->actingAs($this->user)->get($url)->assertOk(); @@ -380,7 +380,7 @@ class RolesTest extends TestCase $otherShelf = Bookshelf::query()->first(); $ownShelf = $this->entities->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']); $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save(); - $this->entities->regenPermissions($ownShelf); + $this->permissions->regenerateForEntity($ownShelf); $this->checkAccessPermission('bookshelf-update-own', [ $ownShelf->getUrl('/edit'), @@ -406,12 +406,12 @@ class RolesTest extends TestCase public function test_bookshelves_delete_own_permission() { - $this->giveUserPermissions($this->user, ['bookshelf-update-all']); + $this->permissions->grantUserRolePermissions($this->user, ['bookshelf-update-all']); /** @var Bookshelf $otherShelf */ $otherShelf = Bookshelf::query()->first(); $ownShelf = $this->entities->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']); $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save(); - $this->entities->regenPermissions($ownShelf); + $this->permissions->regenerateForEntity($ownShelf); $this->checkAccessPermission('bookshelf-delete-own', [ $ownShelf->getUrl('/delete'), @@ -430,7 +430,7 @@ class RolesTest extends TestCase public function test_bookshelves_delete_all_permission() { - $this->giveUserPermissions($this->user, ['bookshelf-update-all']); + $this->permissions->grantUserRolePermissions($this->user, ['bookshelf-update-all']); /** @var Bookshelf $otherShelf */ $otherShelf = Bookshelf::query()->first(); $this->checkAccessPermission('bookshelf-delete-all', [ @@ -486,7 +486,7 @@ class RolesTest extends TestCase public function test_books_delete_own_permission() { - $this->giveUserPermissions($this->user, ['book-update-all']); + $this->permissions->grantUserRolePermissions($this->user, ['book-update-all']); /** @var Book $otherBook */ $otherBook = Book::query()->take(1)->get()->first(); $ownBook = $this->entities->createChainBelongingToUser($this->user)['book']; @@ -506,7 +506,7 @@ class RolesTest extends TestCase public function test_books_delete_all_permission() { - $this->giveUserPermissions($this->user, ['book-update-all']); + $this->permissions->grantUserRolePermissions($this->user, ['book-update-all']); /** @var Book $otherBook */ $otherBook = Book::query()->take(1)->get()->first(); $this->checkAccessPermission('book-delete-all', [ @@ -585,7 +585,7 @@ class RolesTest extends TestCase public function test_chapter_delete_own_permission() { - $this->giveUserPermissions($this->user, ['chapter-update-all']); + $this->permissions->grantUserRolePermissions($this->user, ['chapter-update-all']); /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->first(); $ownChapter = $this->entities->createChainBelongingToUser($this->user)['chapter']; @@ -607,7 +607,7 @@ class RolesTest extends TestCase public function test_chapter_delete_all_permission() { - $this->giveUserPermissions($this->user, ['chapter-update-all']); + $this->permissions->grantUserRolePermissions($this->user, ['chapter-update-all']); /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->first(); $this->checkAccessPermission('chapter-delete-all', [ @@ -645,7 +645,7 @@ class RolesTest extends TestCase $ownChapter->getUrl() => 'New Page', ]); - $this->giveUserPermissions($this->user, ['page-create-own']); + $this->permissions->grantUserRolePermissions($this->user, ['page-create-own']); foreach ($accessUrls as $index => $url) { $resp = $this->actingAs($this->user)->get($url); @@ -688,7 +688,7 @@ class RolesTest extends TestCase $chapter->getUrl() => 'New Page', ]); - $this->giveUserPermissions($this->user, ['page-create-all']); + $this->permissions->grantUserRolePermissions($this->user, ['page-create-all']); foreach ($accessUrls as $index => $url) { $resp = $this->actingAs($this->user)->get($url); @@ -742,7 +742,7 @@ class RolesTest extends TestCase public function test_page_delete_own_permission() { - $this->giveUserPermissions($this->user, ['page-update-all']); + $this->permissions->grantUserRolePermissions($this->user, ['page-update-all']); /** @var Page $otherPage */ $otherPage = Page::query()->first(); $ownPage = $this->entities->createChainBelongingToUser($this->user)['page']; @@ -764,7 +764,7 @@ class RolesTest extends TestCase public function test_page_delete_all_permission() { - $this->giveUserPermissions($this->user, ['page-update-all']); + $this->permissions->grantUserRolePermissions($this->user, ['page-update-all']); /** @var Page $otherPage */ $otherPage = Page::query()->first(); @@ -823,7 +823,7 @@ class RolesTest extends TestCase public function test_image_delete_own_permission() { - $this->giveUserPermissions($this->user, ['image-update-all']); + $this->permissions->grantUserRolePermissions($this->user, ['image-update-all']); $page = $this->entities->page(); $image = Image::factory()->create([ 'uploaded_to' => $page->id, @@ -833,7 +833,7 @@ class RolesTest extends TestCase $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403); - $this->giveUserPermissions($this->user, ['image-delete-own']); + $this->permissions->grantUserRolePermissions($this->user, ['image-delete-own']); $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertOk(); $this->assertDatabaseMissing('images', ['id' => $image->id]); @@ -841,18 +841,18 @@ class RolesTest extends TestCase public function test_image_delete_all_permission() { - $this->giveUserPermissions($this->user, ['image-update-all']); - $admin = $this->getAdmin(); + $this->permissions->grantUserRolePermissions($this->user, ['image-update-all']); + $admin = $this->users->admin(); $page = $this->entities->page(); $image = Image::factory()->create(['uploaded_to' => $page->id, 'created_by' => $admin->id, 'updated_by' => $admin->id]); $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403); - $this->giveUserPermissions($this->user, ['image-delete-own']); + $this->permissions->grantUserRolePermissions($this->user, ['image-delete-own']); $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403); - $this->giveUserPermissions($this->user, ['image-delete-all']); + $this->permissions->grantUserRolePermissions($this->user, ['image-delete-all']); $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertOk(); $this->assertDatabaseMissing('images', ['id' => $image->id]); @@ -863,7 +863,7 @@ class RolesTest extends TestCase // To cover issue fixed in f99c8ff99aee9beb8c692f36d4b84dc6e651e50a. $page = $this->entities->page(); $viewerRole = Role::getRole('viewer'); - $viewer = $this->getViewer(); + $viewer = $this->users->viewer(); $this->actingAs($viewer)->get($page->getUrl())->assertOk(); $this->asAdmin()->put('/settings/roles/' . $viewerRole->id, [ @@ -877,18 +877,18 @@ class RolesTest extends TestCase public function test_empty_state_actions_not_visible_without_permission() { - $admin = $this->getAdmin(); + $admin = $this->users->admin(); // Book links $book = Book::factory()->create(['created_by' => $admin->id, 'updated_by' => $admin->id]); - $this->entities->regenPermissions($book); - $this->actingAs($this->getViewer())->get($book->getUrl()) + $this->permissions->regenerateForEntity($book); + $this->actingAs($this->users->viewer())->get($book->getUrl()) ->assertDontSee('Create a new page') ->assertDontSee('Add a chapter'); // Chapter links $chapter = Chapter::factory()->create(['created_by' => $admin->id, 'updated_by' => $admin->id, 'book_id' => $book->id]); - $this->entities->regenPermissions($chapter); - $this->actingAs($this->getViewer())->get($chapter->getUrl()) + $this->permissions->regenerateForEntity($chapter); + $this->actingAs($this->users->viewer())->get($chapter->getUrl()) ->assertDontSee('Create a new page') ->assertDontSee('Sort the current book'); } @@ -901,7 +901,7 @@ class RolesTest extends TestCase ->addComment($ownPage) ->assertStatus(403); - $this->giveUserPermissions($this->user, ['comment-create-all']); + $this->permissions->grantUserRolePermissions($this->user, ['comment-create-all']); $this->actingAs($this->user) ->addComment($ownPage) @@ -911,7 +911,7 @@ class RolesTest extends TestCase public function test_comment_update_own_permission() { $ownPage = $this->entities->createChainBelongingToUser($this->user)['page']; - $this->giveUserPermissions($this->user, ['comment-create-all']); + $this->permissions->grantUserRolePermissions($this->user, ['comment-create-all']); $this->actingAs($this->user)->addComment($ownPage); /** @var Comment $comment */ $comment = $ownPage->comments()->latest()->first(); @@ -919,7 +919,7 @@ class RolesTest extends TestCase // no comment-update-own $this->actingAs($this->user)->updateComment($comment)->assertStatus(403); - $this->giveUserPermissions($this->user, ['comment-update-own']); + $this->permissions->grantUserRolePermissions($this->user, ['comment-update-own']); // now has comment-update-own $this->actingAs($this->user)->updateComment($comment)->assertOk(); @@ -936,7 +936,7 @@ class RolesTest extends TestCase // no comment-update-all $this->actingAs($this->user)->updateComment($comment)->assertStatus(403); - $this->giveUserPermissions($this->user, ['comment-update-all']); + $this->permissions->grantUserRolePermissions($this->user, ['comment-update-all']); // now has comment-update-all $this->actingAs($this->user)->updateComment($comment)->assertOk(); @@ -946,7 +946,7 @@ class RolesTest extends TestCase { /** @var Page $ownPage */ $ownPage = $this->entities->createChainBelongingToUser($this->user)['page']; - $this->giveUserPermissions($this->user, ['comment-create-all']); + $this->permissions->grantUserRolePermissions($this->user, ['comment-create-all']); $this->actingAs($this->user)->addComment($ownPage); /** @var Comment $comment */ @@ -955,7 +955,7 @@ class RolesTest extends TestCase // no comment-delete-own $this->actingAs($this->user)->deleteComment($comment)->assertStatus(403); - $this->giveUserPermissions($this->user, ['comment-delete-own']); + $this->permissions->grantUserRolePermissions($this->user, ['comment-delete-own']); // now has comment-update-own $this->actingAs($this->user)->deleteComment($comment)->assertOk(); @@ -972,7 +972,7 @@ class RolesTest extends TestCase // no comment-delete-all $this->actingAs($this->user)->deleteComment($comment)->assertStatus(403); - $this->giveUserPermissions($this->user, ['comment-delete-all']); + $this->permissions->grantUserRolePermissions($this->user, ['comment-delete-all']); // now has comment-delete-all $this->actingAs($this->user)->deleteComment($comment)->assertOk(); diff --git a/tests/Permissions/Scenarios/EntityRolePermissionsTest.php b/tests/Permissions/Scenarios/EntityRolePermissionsTest.php new file mode 100644 index 000000000..b92ce620b --- /dev/null +++ b/tests/Permissions/Scenarios/EntityRolePermissionsTest.php @@ -0,0 +1,201 @@ +users->newUserWithRole(); + $page = $this->entities->page(); + $this->permissions->setEntityPermissions($page, ['view'], [$role], false); + + $this->assertVisibleToUser($page, $user); + } + + public function test_02_explicit_deny() + { + [$user, $role] = $this->users->newUserWithRole(); + $page = $this->entities->page(); + $this->permissions->setEntityPermissions($page, [], [$role], false); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_03_same_level_conflicting() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $roleB = $this->users->attachNewRole($user); + $page = $this->entities->page(); + + $this->permissions->disableEntityInheritedPermissions($page); + $this->permissions->addEntityPermission($page, [], $roleA); + $this->permissions->addEntityPermission($page, ['view'], $roleB); + + $this->assertVisibleToUser($page, $user); + } + + public function test_20_inherit_allow() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + + $this->permissions->disableEntityInheritedPermissions($chapter); + $this->permissions->addEntityPermission($chapter, ['view'], $roleA); + + $this->assertVisibleToUser($page, $user); + } + + public function test_21_inherit_deny() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + + $this->permissions->disableEntityInheritedPermissions($chapter); + $this->permissions->addEntityPermission($chapter, [], $roleA); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_22_same_level_conflict_inherit() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $roleB = $this->users->attachNewRole($user); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + + $this->permissions->disableEntityInheritedPermissions($chapter); + $this->permissions->addEntityPermission($chapter, [], $roleA); + $this->permissions->addEntityPermission($chapter, ['view'], $roleB); + + $this->assertVisibleToUser($page, $user); + } + + public function test_30_child_inherit_override_allow() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + + $this->permissions->disableEntityInheritedPermissions($chapter); + $this->permissions->addEntityPermission($chapter, [], $roleA); + $this->permissions->addEntityPermission($page, ['view'], $roleA); + + $this->assertVisibleToUser($page, $user); + } + + public function test_31_child_inherit_override_deny() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + + $this->permissions->disableEntityInheritedPermissions($chapter); + $this->permissions->addEntityPermission($chapter, ['view'], $roleA); + $this->permissions->addEntityPermission($page, [], $roleA); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_40_multi_role_inherit_conflict_override_deny() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $roleB = $this->users->attachNewRole($user); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + + $this->permissions->disableEntityInheritedPermissions($chapter); + $this->permissions->addEntityPermission($page, [], $roleA); + $this->permissions->addEntityPermission($chapter, ['view'], $roleB); + + $this->assertVisibleToUser($page, $user); + } + + public function test_41_multi_role_inherit_conflict_retain_allow() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $roleB = $this->users->attachNewRole($user); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + + $this->permissions->disableEntityInheritedPermissions($chapter); + $this->permissions->addEntityPermission($page, ['view'], $roleA); + $this->permissions->addEntityPermission($chapter, [], $roleB); + + $this->assertVisibleToUser($page, $user); + } + + public function test_50_role_override_allow() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $page = $this->entities->page(); + $this->permissions->addEntityPermission($page, ['view'], $roleA); + + $this->assertVisibleToUser($page, $user); + } + + public function test_51_role_override_deny() + { + [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']); + $page = $this->entities->page(); + $this->permissions->addEntityPermission($page, [], $roleA); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_60_inherited_role_override_allow() + { + [$user, $roleA] = $this->users->newUserWithRole([], []); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + $this->permissions->addEntityPermission($chapter, ['view'], $roleA); + + $this->assertVisibleToUser($page, $user); + } + + public function test_61_inherited_role_override_deny() + { + [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + $this->permissions->addEntityPermission($chapter, [], $roleA); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_62_inherited_role_override_deny_on_own() + { + [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-own']); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + $this->permissions->addEntityPermission($chapter, [], $roleA); + $this->permissions->changeEntityOwner($page, $user); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_70_multi_role_inheriting_deny() + { + [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']); + $roleB = $this->users->attachNewRole($user); + $page = $this->entities->page(); + + $this->permissions->addEntityPermission($page, [], $roleB); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_80_multi_role_inherited_deny_via_parent() + { + [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']); + $roleB = $this->users->attachNewRole($user); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + + $this->permissions->addEntityPermission($chapter, [], $roleB); + + $this->assertNotVisibleToUser($page, $user); + } +} diff --git a/tests/Permissions/Scenarios/EntityUserPermissionsTest.php b/tests/Permissions/Scenarios/EntityUserPermissionsTest.php new file mode 100644 index 000000000..4fa805805 --- /dev/null +++ b/tests/Permissions/Scenarios/EntityUserPermissionsTest.php @@ -0,0 +1,209 @@ +users->newUser(); + $page = $this->entities->page(); + $this->permissions->disableEntityInheritedPermissions($page); + $this->permissions->addEntityPermission($page, ['view'], null, $user); + + $this->assertVisibleToUser($page, $user); + } + + public function test_02_explicit_deny() + { + $user = $this->users->newUser(); + $page = $this->entities->page(); + $this->permissions->disableEntityInheritedPermissions($page); + $this->permissions->addEntityPermission($page, [], null, $user); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_10_allow_inherit() + { + $user = $this->users->newUser(); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + $this->permissions->disableEntityInheritedPermissions($chapter); + $this->permissions->addEntityPermission($chapter, ['view'], null, $user); + + $this->assertVisibleToUser($page, $user); + } + + public function test_11_deny_inherit() + { + $user = $this->users->newUser(); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + $this->permissions->disableEntityInheritedPermissions($chapter); + $this->permissions->addEntityPermission($chapter, [], null, $user); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_12_allow_inherit_override() + { + $user = $this->users->newUser(); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + $this->permissions->disableEntityInheritedPermissions($chapter); + $this->permissions->addEntityPermission($chapter, [], null, $user); + $this->permissions->addEntityPermission($page, ['view'], null, $user); + + $this->assertVisibleToUser($page, $user); + } + + public function test_13_deny_inherit_override() + { + $user = $this->users->newUser(); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + $this->permissions->disableEntityInheritedPermissions($chapter); + $this->permissions->addEntityPermission($chapter, ['view'], null, $user); + $this->permissions->addEntityPermission($page, ['deny'], null, $user); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_40_entity_role_override_allow() + { + [$user, $role] = $this->users->newUserWithRole(); + $page = $this->entities->page(); + $this->permissions->disableEntityInheritedPermissions($page); + $this->permissions->addEntityPermission($page, ['view'], null, $user); + $this->permissions->addEntityPermission($page, [], $role); + + $this->assertVisibleToUser($page, $user); + } + + public function test_41_entity_role_override_deny() + { + [$user, $role] = $this->users->newUserWithRole(); + $page = $this->entities->page(); + $this->permissions->disableEntityInheritedPermissions($page); + $this->permissions->addEntityPermission($page, [], null, $user); + $this->permissions->addEntityPermission($page, ['view'], $role); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_42_entity_role_override_allow_via_inherit() + { + [$user, $role] = $this->users->newUserWithRole(); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + $this->permissions->disableEntityInheritedPermissions($chapter); + $this->permissions->addEntityPermission($chapter, ['view'], null, $user); + $this->permissions->addEntityPermission($page, [], $role); + + $this->assertVisibleToUser($page, $user); + } + + public function test_43_entity_role_override_deny_via_inherit() + { + [$user, $role] = $this->users->newUserWithRole(); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + $this->permissions->disableEntityInheritedPermissions($chapter); + $this->permissions->addEntityPermission($chapter, [], null, $user); + $this->permissions->addEntityPermission($page, ['view'], $role); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_50_role_override_allow() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $page = $this->entities->page(); + $this->permissions->addEntityPermission($page, ['view'], null, $user); + + $this->assertVisibleToUser($page, $user); + } + + public function test_51_role_override_deny() + { + [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']); + $page = $this->entities->page(); + $this->permissions->addEntityPermission($page, [], null, $user); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_60_inherited_role_override_allow() + { + [$user, $roleA] = $this->users->newUserWithRole([], []); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + $this->permissions->addEntityPermission($chapter, ['view'], null, $user); + + $this->assertVisibleToUser($page, $user); + } + + public function test_61_inherited_role_override_deny() + { + [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + $this->permissions->addEntityPermission($chapter, [], null, $user); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_61_inherited_role_override_deny_on_own() + { + [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-own']); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + $this->permissions->addEntityPermission($chapter, [], null, $user); + $this->permissions->changeEntityOwner($page, $user); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_70_all_override_allow() + { + [$user, $roleA] = $this->users->newUserWithRole([], []); + $page = $this->entities->page(); + $this->permissions->addEntityPermission($page, [], $roleA, null); + $this->permissions->addEntityPermission($page, ['view'], null, $user); + + $this->assertVisibleToUser($page, $user); + } + + public function test_71_all_override_deny() + { + [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']); + $page = $this->entities->page(); + $this->permissions->addEntityPermission($page, ['view'], $roleA, null); + $this->permissions->addEntityPermission($page, [], null, $user); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_80_inherited_all_override_allow() + { + [$user, $roleA] = $this->users->newUserWithRole([], []); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + $this->permissions->addEntityPermission($chapter, [], $roleA, null); + $this->permissions->addEntityPermission($chapter, ['view'], null, $user); + + $this->assertVisibleToUser($page, $user); + } + + public function test_81_inherited_all_override_deny() + { + [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']); + $page = $this->entities->pageWithinChapter(); + $chapter = $page->chapter; + $this->permissions->addEntityPermission($chapter, ['view'], $roleA, null); + $this->permissions->addEntityPermission($chapter, [], null, $user); + + $this->assertNotVisibleToUser($page, $user); + } +} diff --git a/tests/Permissions/Scenarios/PermissionScenarioTestCase.php b/tests/Permissions/Scenarios/PermissionScenarioTestCase.php new file mode 100644 index 000000000..5352f468a --- /dev/null +++ b/tests/Permissions/Scenarios/PermissionScenarioTestCase.php @@ -0,0 +1,38 @@ +actingAs($user); + $funcView = userCan($entity->getMorphClass() . '-view', $entity); + $queryView = $entity->newQuery()->scopes(['visible'])->find($entity->id) !== null; + + $id = $entity->getMorphClass() . ':' . $entity->id; + $msg = "Item [{$id}] should be visible but was not found via "; + $msg .= implode(' and ', array_filter([!$funcView ? 'userCan' : '', !$queryView ? 'query' : ''])); + + static::assertTrue($funcView && $queryView, $msg); + } + + protected function assertNotVisibleToUser(Entity $entity, User $user) + { + $this->actingAs($user); + $funcView = userCan($entity->getMorphClass() . '-view', $entity); + $queryView = $entity->newQuery()->scopes(['visible'])->find($entity->id) !== null; + + $id = $entity->getMorphClass() . ':' . $entity->id; + $msg = "Item [{$id}] should not be visible but was found via "; + $msg .= implode(' and ', array_filter([$funcView ? 'userCan' : '', $queryView ? 'query' : ''])); + + static::assertTrue(!$funcView && !$queryView, $msg); + } +} diff --git a/tests/Permissions/Scenarios/RoleContentPermissionsTest.php b/tests/Permissions/Scenarios/RoleContentPermissionsTest.php new file mode 100644 index 000000000..8b8c9031c --- /dev/null +++ b/tests/Permissions/Scenarios/RoleContentPermissionsTest.php @@ -0,0 +1,59 @@ +users->newUserWithRole([], ['page-view-all']); + $page = $this->entities->page(); + + $this->assertVisibleToUser($page, $user); + } + + public function test_02_deny() + { + [$user] = $this->users->newUserWithRole([], []); + $page = $this->entities->page(); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_10_allow_on_own_with_own() + { + [$user] = $this->users->newUserWithRole([], ['page-view-own']); + $page = $this->entities->page(); + $this->permissions->changeEntityOwner($page, $user); + + $this->assertVisibleToUser($page, $user); + } + + public function test_11_deny_on_other_with_own() + { + [$user] = $this->users->newUserWithRole([], ['page-view-own']); + $page = $this->entities->page(); + $this->permissions->changeEntityOwner($page, $this->users->editor()); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_20_multiple_role_conflicting_all() + { + [$user] = $this->users->newUserWithRole([], ['page-view-all']); + $this->users->attachNewRole($user, []); + $page = $this->entities->page(); + + $this->assertVisibleToUser($page, $user); + } + + public function test_21_multiple_role_conflicting_own() + { + [$user] = $this->users->newUserWithRole([], ['page-view-own']); + $this->users->attachNewRole($user, []); + $page = $this->entities->page(); + $this->permissions->changeEntityOwner($page, $user); + + $this->assertVisibleToUser($page, $user); + } +} diff --git a/tests/PublicActionTest.php b/tests/PublicActionTest.php index 7e3f7be00..afc7fcef3 100644 --- a/tests/PublicActionTest.php +++ b/tests/PublicActionTest.php @@ -2,7 +2,6 @@ namespace Tests; -use BookStack\Auth\Permissions\JointPermissionBuilder; use BookStack\Auth\Permissions\RolePermission; use BookStack\Auth\Role; use BookStack\Auth\User; @@ -89,7 +88,6 @@ class PublicActionTest extends TestCase foreach (RolePermission::all() as $perm) { $publicRole->attachPermission($perm); } - $this->app->make(JointPermissionBuilder::class)->rebuildForRole($publicRole); user()->clearPermissionCache(); $chapter = $this->entities->chapter(); @@ -173,7 +171,7 @@ class PublicActionTest extends TestCase { $this->setSettings(['app-public' => 'true']); $book = $this->entities->book(); - $this->entities->setPermissions($book); + $this->permissions->setEntityPermissions($book); $resp = $this->get($book->getUrl()); $resp->assertSee('Book not found'); diff --git a/tests/References/ReferencesTest.php b/tests/References/ReferencesTest.php index 148b2197c..4330598ba 100644 --- a/tests/References/ReferencesTest.php +++ b/tests/References/ReferencesTest.php @@ -91,7 +91,7 @@ class ReferencesTest extends TestCase $pageB = $this->entities->page(); $this->createReference($pageB, $page); - $this->entities->setPermissions($pageB); + $this->permissions->setEntityPermissions($pageB); $this->asEditor()->get($page->getUrl('/references'))->assertDontSee($pageB->name); $this->asAdmin()->get($page->getUrl('/references'))->assertSee($pageB->name); diff --git a/tests/Settings/RecycleBinTest.php b/tests/Settings/RecycleBinTest.php index 990df607e..8adc92f25 100644 --- a/tests/Settings/RecycleBinTest.php +++ b/tests/Settings/RecycleBinTest.php @@ -14,7 +14,7 @@ class RecycleBinTest extends TestCase public function test_recycle_bin_routes_permissions() { $page = $this->entities->page(); - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->actingAs($editor)->delete($page->getUrl()); $deletion = Deletion::query()->firstOrFail(); @@ -33,7 +33,7 @@ class RecycleBinTest extends TestCase $this->assertPermissionError($resp); } - $this->giveUserPermissions($editor, ['restrictions-manage-all']); + $this->permissions->grantUserRolePermissions($editor, ['restrictions-manage-all']); foreach ($routes as $route) { [$method, $url] = explode(':', $route); @@ -41,7 +41,7 @@ class RecycleBinTest extends TestCase $this->assertPermissionError($resp); } - $this->giveUserPermissions($editor, ['settings-manage']); + $this->permissions->grantUserRolePermissions($editor, ['settings-manage']); foreach ($routes as $route) { DB::beginTransaction(); @@ -56,7 +56,7 @@ class RecycleBinTest extends TestCase { $page = $this->entities->page(); $book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first(); - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->actingAs($editor)->delete($page->getUrl()); $this->actingAs($editor)->delete($book->getUrl()); @@ -73,7 +73,7 @@ class RecycleBinTest extends TestCase { $page = $this->entities->page(); $book = Book::query()->where('id', '!=', $page->book_id)->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail(); - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->actingAs($editor)->delete($page->getUrl()); $this->actingAs($editor)->delete($book->getUrl()); diff --git a/tests/Settings/RegenerateReferencesTest.php b/tests/Settings/RegenerateReferencesTest.php index 0f3122074..239f50e76 100644 --- a/tests/Settings/RegenerateReferencesTest.php +++ b/tests/Settings/RegenerateReferencesTest.php @@ -32,11 +32,11 @@ class RegenerateReferencesTest extends TestCase public function test_settings_manage_permission_required() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $resp = $this->actingAs($editor)->post('/settings/maintenance/regenerate-references'); $this->assertPermissionError($resp); - $this->giveUserPermissions($editor, ['settings-manage']); + $this->permissions->grantUserRolePermissions($editor, ['settings-manage']); $resp = $this->actingAs($editor)->post('/settings/maintenance/regenerate-references'); $this->assertNotPermissionError($resp); diff --git a/tests/Settings/TestEmailTest.php b/tests/Settings/TestEmailTest.php index 31c51158f..322f90107 100644 --- a/tests/Settings/TestEmailTest.php +++ b/tests/Settings/TestEmailTest.php @@ -20,7 +20,7 @@ class TestEmailTest extends TestCase public function test_send_test_email_endpoint_sends_email_and_redirects_user_and_shows_notification() { Notification::fake(); - $admin = $this->getAdmin(); + $admin = $this->users->admin(); $sendReq = $this->actingAs($admin)->post('/settings/maintenance/send-test-email'); $sendReq->assertRedirect('/settings/maintenance#image-cleanup'); @@ -37,7 +37,7 @@ class TestEmailTest extends TestCase $exception = new \Exception('A random error occurred when testing an email'); $mockDispatcher->shouldReceive('sendNow')->andThrow($exception); - $admin = $this->getAdmin(); + $admin = $this->users->admin(); $sendReq = $this->actingAs($admin)->post('/settings/maintenance/send-test-email'); $sendReq->assertRedirect('/settings/maintenance#image-cleanup'); $this->assertSessionHas('error'); @@ -50,12 +50,12 @@ class TestEmailTest extends TestCase public function test_send_test_email_requires_settings_manage_permission() { Notification::fake(); - $user = $this->getViewer(); + $user = $this->users->viewer(); $sendReq = $this->actingAs($user)->post('/settings/maintenance/send-test-email'); Notification::assertNothingSent(); - $this->giveUserPermissions($user, ['settings-manage']); + $this->permissions->grantUserRolePermissions($user, ['settings-manage']); $sendReq = $this->actingAs($user)->post('/settings/maintenance/send-test-email'); Notification::assertSentTo($user, TestEmail::class); } diff --git a/tests/TestCase.php b/tests/TestCase.php index d0dd7d772..70fd0da1d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,11 +2,6 @@ namespace Tests; -use BookStack\Auth\Permissions\JointPermissionBuilder; -use BookStack\Auth\Permissions\PermissionsRepo; -use BookStack\Auth\Permissions\RolePermission; -use BookStack\Auth\Role; -use BookStack\Auth\User; use BookStack\Entities\Models\Entity; use BookStack\Settings\SettingService; use BookStack\Uploads\HttpFetcher; @@ -22,12 +17,15 @@ use Illuminate\Support\Env; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Testing\Assert as PHPUnit; +use Mockery; use Monolog\Handler\TestHandler; use Monolog\Logger; use Psr\Http\Client\ClientInterface; use Ssddanbrown\AssertHtml\TestsHtml; use Tests\Helpers\EntityProvider; +use Tests\Helpers\PermissionsProvider; use Tests\Helpers\TestServiceProvider; +use Tests\Helpers\UserRoleProvider; abstract class TestCase extends BaseTestCase { @@ -35,13 +33,16 @@ abstract class TestCase extends BaseTestCase use DatabaseTransactions; use TestsHtml; - protected ?User $admin = null; - protected ?User $editor = null; protected EntityProvider $entities; + protected UserRoleProvider $users; + protected PermissionsProvider $permissions; protected function setUp(): void { $this->entities = new EntityProvider(); + $this->users = new UserRoleProvider(); + $this->permissions = new PermissionsProvider($this->users); + parent::setUp(); } @@ -70,20 +71,7 @@ abstract class TestCase extends BaseTestCase */ public function asAdmin() { - return $this->actingAs($this->getAdmin()); - } - - /** - * Get the current admin user. - */ - public function getAdmin(): User - { - if (is_null($this->admin)) { - $adminRole = Role::getSystemRole('admin'); - $this->admin = $adminRole->users->first(); - } - - return $this->admin; + return $this->actingAs($this->users->admin()); } /** @@ -91,20 +79,7 @@ abstract class TestCase extends BaseTestCase */ public function asEditor() { - return $this->actingAs($this->getEditor()); - } - - /** - * Get a editor user. - */ - protected function getEditor(): User - { - if ($this->editor === null) { - $editorRole = Role::getRole('editor'); - $this->editor = $editorRole->users->first(); - } - - return $this->editor; + return $this->actingAs($this->users->editor()); } /** @@ -112,28 +87,7 @@ abstract class TestCase extends BaseTestCase */ public function asViewer() { - return $this->actingAs($this->getViewer()); - } - - /** - * Get an instance of a user with 'viewer' permissions. - */ - protected function getViewer(array $attributes = []): User - { - $user = Role::getRole('viewer')->users()->first(); - if (!empty($attributes)) { - $user->forceFill($attributes)->save(); - } - - return $user; - } - - /** - * Get a user that's not a system user such as the guest user. - */ - public function getNormalUser(): User - { - return User::query()->where('system_name', '=', null)->get()->last(); + return $this->actingAs($this->users->viewer()); } /** @@ -147,52 +101,6 @@ abstract class TestCase extends BaseTestCase } } - /** - * Give the given user some permissions. - */ - protected function giveUserPermissions(User $user, array $permissions = []): void - { - $newRole = $this->createNewRole($permissions); - $user->attachRole($newRole); - $user->load('roles'); - $user->clearPermissionCache(); - } - - /** - * Completely remove the given permission name from the given user. - */ - protected function removePermissionFromUser(User $user, string $permissionName) - { - $permissionBuilder = app()->make(JointPermissionBuilder::class); - - /** @var RolePermission $permission */ - $permission = RolePermission::query()->where('name', '=', $permissionName)->firstOrFail(); - - $roles = $user->roles()->whereHas('permissions', function ($query) use ($permission) { - $query->where('id', '=', $permission->id); - })->get(); - - /** @var Role $role */ - foreach ($roles as $role) { - $role->detachPermission($permission); - $permissionBuilder->rebuildForRole($role); - } - - $user->clearPermissionCache(); - } - - /** - * Create a new basic role for testing purposes. - */ - protected function createNewRole(array $permissions = []): Role - { - $permissionRepo = app(PermissionsRepo::class); - $roleData = Role::factory()->make()->toArray(); - $roleData['permissions'] = array_flip($permissions); - - return $permissionRepo->saveNewRole($roleData); - } - /** * Mock the HttpFetcher service and return the given data on fetch. */ diff --git a/tests/ThemeTest.php b/tests/ThemeTest.php index efab53379..ee4f20f30 100644 --- a/tests/ThemeTest.php +++ b/tests/ThemeTest.php @@ -36,7 +36,7 @@ class ThemeTest extends TestCase '; file_put_contents($translationPath . '/entities.php', $customTranslations); - $homeRequest = $this->actingAs($this->getViewer())->get('/'); + $homeRequest = $this->actingAs($this->users->viewer())->get('/'); $this->withHtml($homeRequest)->assertElementContains('header nav', 'Sandwiches'); }); } diff --git a/tests/Unit/FrameworkAssumptionTest.php b/tests/Unit/FrameworkAssumptionTest.php index 54d315de9..d4feff60c 100644 --- a/tests/Unit/FrameworkAssumptionTest.php +++ b/tests/Unit/FrameworkAssumptionTest.php @@ -25,7 +25,7 @@ class FrameworkAssumptionTest extends TestCase // Page has SoftDeletes trait by default, so we apply our custom scope and ensure // it stacks on the global scope to filter out deleted items. $query = Page::query()->scopes('visible')->toSql(); - $this->assertStringContainsString('joint_permissions', $query); + $this->assertStringContainsString('entity_permissions_collapsed', $query); $this->assertStringContainsString('`deleted_at` is null', $query); } } diff --git a/tests/Uploads/AttachmentTest.php b/tests/Uploads/AttachmentTest.php index b6fcb8f69..f2f30ff2e 100644 --- a/tests/Uploads/AttachmentTest.php +++ b/tests/Uploads/AttachmentTest.php @@ -75,7 +75,7 @@ class AttachmentTest extends TestCase { $page = $this->entities->page(); $this->asAdmin(); - $admin = $this->getAdmin(); + $admin = $this->users->admin(); $fileName = 'upload_test_file.txt'; $expectedResp = [ @@ -137,7 +137,7 @@ class AttachmentTest extends TestCase public function test_attaching_link_to_page() { $page = $this->entities->page(); - $admin = $this->getAdmin(); + $admin = $this->users->admin(); $this->asAdmin(); $linkReq = $this->call('POST', 'attachments/link', [ @@ -245,15 +245,15 @@ class AttachmentTest extends TestCase public function test_attachment_access_without_permission_shows_404() { - $admin = $this->getAdmin(); - $viewer = $this->getViewer(); + $admin = $this->users->admin(); + $viewer = $this->users->viewer(); $page = $this->entities->page(); /** @var Page $page */ $this->actingAs($admin); $fileName = 'permission_test.txt'; $this->uploadFile($fileName, $page->id); $attachment = Attachment::orderBy('id', 'desc')->take(1)->first(); - $this->entities->setPermissions($page, [], []); + $this->permissions->setEntityPermissions($page, [], []); $this->actingAs($viewer); $attachmentGet = $this->get($attachment->getUrl()); diff --git a/tests/Uploads/DrawioTest.php b/tests/Uploads/DrawioTest.php index 2ed4da7ca..080f05d74 100644 --- a/tests/Uploads/DrawioTest.php +++ b/tests/Uploads/DrawioTest.php @@ -30,7 +30,7 @@ class DrawioTest extends TestCase public function test_drawing_base64_upload() { $page = Page::first(); - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->actingAs($editor); $upload = $this->postJson('images/drawio', [ @@ -58,7 +58,7 @@ class DrawioTest extends TestCase { config()->set('services.drawio', 'http://cats.com?dog=tree'); $page = Page::first(); - $editor = $this->getEditor(); + $editor = $this->users->editor(); $resp = $this->actingAs($editor)->get($page->getUrl('/edit')); $resp->assertSee('drawio-url="http://cats.com?dog=tree"', false); @@ -68,7 +68,7 @@ class DrawioTest extends TestCase { config()->set('services.drawio', true); $page = Page::first(); - $editor = $this->getEditor(); + $editor = $this->users->editor(); $resp = $this->actingAs($editor)->get($page->getUrl('/edit')); $resp->assertSee('drawio-url="https://embed.diagrams.net/?embed=1&proto=json&spin=1&configure=1"', false); diff --git a/tests/Uploads/ImageTest.php b/tests/Uploads/ImageTest.php index 0e4065a82..c6e678ff2 100644 --- a/tests/Uploads/ImageTest.php +++ b/tests/Uploads/ImageTest.php @@ -16,7 +16,7 @@ class ImageTest extends TestCase public function test_image_upload() { $page = $this->entities->page(); - $admin = $this->getAdmin(); + $admin = $this->users->admin(); $this->actingAs($admin); $imgDetails = $this->uploadGalleryImage($page); @@ -40,7 +40,7 @@ class ImageTest extends TestCase public function test_image_display_thumbnail_generation_does_not_increase_image_size() { $page = $this->entities->page(); - $admin = $this->getAdmin(); + $admin = $this->users->admin(); $this->actingAs($admin); $originalFile = $this->getTestImageFilePath('compressed.png'); @@ -64,7 +64,7 @@ class ImageTest extends TestCase public function test_image_display_thumbnail_generation_for_apng_images_uses_original_file() { $page = $this->entities->page(); - $admin = $this->getAdmin(); + $admin = $this->users->admin(); $this->actingAs($admin); $imgDetails = $this->uploadGalleryImage($page, 'animated.png'); @@ -76,7 +76,7 @@ class ImageTest extends TestCase public function test_image_edit() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->actingAs($editor); $imgDetails = $this->uploadGalleryImage(); @@ -126,7 +126,7 @@ class ImageTest extends TestCase public function test_image_usage() { $page = $this->entities->page(); - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->actingAs($editor); $imgDetails = $this->uploadGalleryImage($page); @@ -146,7 +146,7 @@ class ImageTest extends TestCase public function test_php_files_cannot_be_uploaded() { $page = $this->entities->page(); - $admin = $this->getAdmin(); + $admin = $this->users->admin(); $this->actingAs($admin); $fileName = 'bad.php'; @@ -168,7 +168,7 @@ class ImageTest extends TestCase public function test_php_like_files_cannot_be_uploaded() { $page = $this->entities->page(); - $admin = $this->getAdmin(); + $admin = $this->users->admin(); $this->actingAs($admin); $fileName = 'bad.phtml'; @@ -185,7 +185,7 @@ class ImageTest extends TestCase public function test_files_with_double_extensions_will_get_sanitized() { $page = $this->entities->page(); - $admin = $this->getAdmin(); + $admin = $this->users->admin(); $this->actingAs($admin); $fileName = 'bad.phtml.png'; @@ -358,7 +358,7 @@ class ImageTest extends TestCase $this->get($expectedUrl)->assertOk(); - $this->entities->setPermissions($page, [], []); + $this->permissions->setEntityPermissions($page, [], []); $resp = $this->get($expectedUrl); $resp->assertNotFound(); @@ -382,7 +382,7 @@ class ImageTest extends TestCase $this->get($expectedUrl)->assertOk(); - $this->entities->setPermissions($page, [], []); + $this->permissions->setEntityPermissions($page, [], []); $resp = $this->get($expectedUrl); $resp->assertNotFound(); @@ -415,7 +415,7 @@ class ImageTest extends TestCase $export = $this->get($pageB->getUrl('/export/html')); $this->assertStringContainsString($encodedImageContent, $export->getContent()); - $this->entities->setPermissions($pageA, [], []); + $this->permissions->setEntityPermissions($pageA, [], []); $export = $this->get($pageB->getUrl('/export/html')); $this->assertStringNotContainsString($encodedImageContent, $export->getContent()); @@ -479,7 +479,7 @@ class ImageTest extends TestCase $imageName = 'first-image.png'; $relPath = $this->getTestImagePath('gallery', $imageName); $this->deleteImage($relPath); - $viewer = $this->getViewer(); + $viewer = $this->users->viewer(); $this->uploadImage($imageName, $page->id); $image = Image::first(); @@ -490,7 +490,7 @@ class ImageTest extends TestCase $resp = $this->actingAs($viewer)->get("/images/edit/{$image->id}"); $this->withHtml($resp)->assertElementNotExists('button#image-manager-delete[title="Delete"]'); - $this->giveUserPermissions($viewer, ['image-delete-all']); + $this->permissions->grantUserRolePermissions($viewer, ['image-delete-all']); $resp = $this->actingAs($viewer)->get("/images/edit/{$image->id}"); $this->withHtml($resp)->assertElementExists('button#image-manager-delete[title="Delete"]'); @@ -509,8 +509,8 @@ class ImageTest extends TestCase public function test_user_image_upload() { - $editor = $this->getEditor(); - $admin = $this->getAdmin(); + $editor = $this->users->editor(); + $admin = $this->users->admin(); $this->actingAs($admin); $file = $this->getTestProfileImage(); @@ -525,7 +525,7 @@ class ImageTest extends TestCase public function test_user_images_deleted_on_user_deletion() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->actingAs($editor); $file = $this->getTestProfileImage(); @@ -555,7 +555,7 @@ class ImageTest extends TestCase public function test_deleted_unused_images() { $page = $this->entities->page(); - $admin = $this->getAdmin(); + $admin = $this->users->admin(); $this->actingAs($admin); $imageName = 'unused-image.png'; diff --git a/tests/User/UserApiTokenTest.php b/tests/User/UserApiTokenTest.php index 716f3614c..93070b712 100644 --- a/tests/User/UserApiTokenTest.php +++ b/tests/User/UserApiTokenTest.php @@ -16,12 +16,12 @@ class UserApiTokenTest extends TestCase public function test_tokens_section_not_visible_without_access_api_permission() { - $user = $this->getViewer(); + $user = $this->users->viewer(); $resp = $this->actingAs($user)->get($user->getEditUrl()); $resp->assertDontSeeText('API Tokens'); - $this->giveUserPermissions($user, ['access-api']); + $this->permissions->grantUserRolePermissions($user, ['access-api']); $resp = $this->actingAs($user)->get($user->getEditUrl()); $resp->assertSeeText('API Tokens'); @@ -30,9 +30,9 @@ class UserApiTokenTest extends TestCase public function test_those_with_manage_users_can_view_other_user_tokens_but_not_create() { - $viewer = $this->getViewer(); - $editor = $this->getEditor(); - $this->giveUserPermissions($viewer, ['users-manage']); + $viewer = $this->users->viewer(); + $editor = $this->users->editor(); + $this->permissions->grantUserRolePermissions($viewer, ['users-manage']); $resp = $this->actingAs($viewer)->get($editor->getEditUrl()); $resp->assertSeeText('API Tokens'); @@ -41,7 +41,7 @@ class UserApiTokenTest extends TestCase public function test_create_api_token() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $resp = $this->asAdmin()->get($editor->getEditUrl('/create-api-token')); $resp->assertStatus(200); @@ -74,7 +74,7 @@ class UserApiTokenTest extends TestCase public function test_create_with_no_expiry_sets_expiry_hundred_years_away() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->asAdmin()->post($editor->getEditUrl('/create-api-token'), ['name' => 'No expiry token', 'expires_at' => '']); $token = ApiToken::query()->latest()->first(); @@ -88,7 +88,7 @@ class UserApiTokenTest extends TestCase public function test_created_token_displays_on_profile_page() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->asAdmin()->post($editor->getEditUrl('/create-api-token'), $this->testTokenData); $token = ApiToken::query()->latest()->first(); @@ -101,7 +101,7 @@ class UserApiTokenTest extends TestCase public function test_secret_shown_once_after_creation() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $resp = $this->asAdmin()->followingRedirects()->post($editor->getEditUrl('/create-api-token'), $this->testTokenData); $resp->assertSeeText('Token Secret'); @@ -114,7 +114,7 @@ class UserApiTokenTest extends TestCase public function test_token_update() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->asAdmin()->post($editor->getEditUrl('/create-api-token'), $this->testTokenData); $token = ApiToken::query()->latest()->first(); $updateData = [ @@ -132,7 +132,7 @@ class UserApiTokenTest extends TestCase public function test_token_update_with_blank_expiry_sets_to_hundred_years_away() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->asAdmin()->post($editor->getEditUrl('/create-api-token'), $this->testTokenData); $token = ApiToken::query()->latest()->first(); @@ -152,7 +152,7 @@ class UserApiTokenTest extends TestCase public function test_token_delete() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->asAdmin()->post($editor->getEditUrl('/create-api-token'), $this->testTokenData); $token = ApiToken::query()->latest()->first(); @@ -171,9 +171,9 @@ class UserApiTokenTest extends TestCase public function test_user_manage_can_delete_token_without_api_permission_themselves() { - $viewer = $this->getViewer(); - $editor = $this->getEditor(); - $this->giveUserPermissions($editor, ['users-manage']); + $viewer = $this->users->viewer(); + $editor = $this->users->editor(); + $this->permissions->grantUserRolePermissions($editor, ['users-manage']); $this->asAdmin()->post($viewer->getEditUrl('/create-api-token'), $this->testTokenData); $token = ApiToken::query()->latest()->first(); diff --git a/tests/User/UserManagementTest.php b/tests/User/UserManagementTest.php index b5cd764da..1c5c040da 100644 --- a/tests/User/UserManagementTest.php +++ b/tests/User/UserManagementTest.php @@ -46,7 +46,7 @@ class UserManagementTest extends TestCase public function test_user_updating() { - $user = $this->getNormalUser(); + $user = $this->users->viewer(); $password = $user->password; $resp = $this->asAdmin()->get('/settings/users/' . $user->id); @@ -65,7 +65,7 @@ class UserManagementTest extends TestCase public function test_user_password_update() { - $user = $this->getNormalUser(); + $user = $this->users->viewer(); $userProfilePage = '/settings/users/' . $user->id; $this->asAdmin()->get($userProfilePage); @@ -113,7 +113,7 @@ class UserManagementTest extends TestCase public function test_delete() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $resp = $this->asAdmin()->delete("settings/users/{$editor->id}"); $resp->assertRedirect('/settings/users'); $resp = $this->followRedirects($resp); @@ -126,7 +126,7 @@ class UserManagementTest extends TestCase public function test_delete_offers_migrate_option() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $resp = $this->asAdmin()->get("settings/users/{$editor->id}/delete"); $resp->assertSee('Migrate Ownership'); $resp->assertSee('new_owner_id'); @@ -134,13 +134,13 @@ class UserManagementTest extends TestCase public function test_migrate_option_hidden_if_user_cannot_manage_users() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $resp = $this->asEditor()->get("settings/users/{$editor->id}/delete"); $resp->assertDontSee('Migrate Ownership'); $resp->assertDontSee('new_owner_id'); - $this->giveUserPermissions($editor, ['users-manage']); + $this->permissions->grantUserRolePermissions($editor, ['users-manage']); $resp = $this->asEditor()->get("settings/users/{$editor->id}/delete"); $resp->assertSee('Migrate Ownership'); @@ -162,7 +162,7 @@ class UserManagementTest extends TestCase public function test_delete_removes_user_preferences() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); setting()->putUser($editor, 'dark-mode-enabled', 'true'); $this->assertDatabaseHas('settings', [ @@ -253,7 +253,7 @@ class UserManagementTest extends TestCase public function test_user_create_update_fails_if_locale_is_invalid() { - $user = $this->getEditor(); + $user = $this->users->editor(); // Too long $resp = $this->asAdmin()->put($user->getEditUrl(), ['language' => 'this_is_too_long']); @@ -274,34 +274,4 @@ class UserManagementTest extends TestCase $resp->assertSessionHasErrors(['language' => 'The language may not be greater than 15 characters.']); $resp->assertSessionHasErrors(['language' => 'The language may only contain letters, numbers, dashes and underscores.']); } - - public function test_role_removal_on_user_edit_removes_all_role_assignments() - { - $user = $this->getEditor(); - - $this->assertEquals(1, $user->roles()->count()); - - // A roles[0] hidden fields is used to indicate the existence of role selection in the submission - // of the user edit form. We check that field is used and emulate its submission. - $resp = $this->asAdmin()->get("/settings/users/{$user->id}"); - $this->withHtml($resp)->assertElementExists('input[type="hidden"][name="roles[0]"][value="0"]'); - - $resp = $this->asAdmin()->put("/settings/users/{$user->id}", [ - 'name' => $user->name, - 'email' => $user->email, - 'roles' => ['0' => '0'], - ]); - $resp->assertRedirect("/settings/users"); - - $this->assertEquals(0, $user->roles()->count()); - } - - public function test_role_form_hidden_indicator_field_does_not_exist_where_roles_cannot_be_managed() - { - $user = $this->getEditor(); - $resp = $this->actingAs($user)->get("/settings/users/{$user->id}"); - $html = $this->withHtml($resp); - $html->assertElementExists('input[name="email"]'); - $html->assertElementNotExists('input[type="hidden"][name="roles[0]"]'); - } } diff --git a/tests/User/UserPreferencesTest.php b/tests/User/UserPreferencesTest.php index 03dad7990..e47a259a5 100644 --- a/tests/User/UserPreferencesTest.php +++ b/tests/User/UserPreferencesTest.php @@ -36,7 +36,7 @@ class UserPreferencesTest extends TestCase public function test_body_has_shortcuts_component_when_active() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->actingAs($editor); $this->withHtml($this->get('/'))->assertElementNotExists('body[component="shortcuts"]'); @@ -47,7 +47,7 @@ class UserPreferencesTest extends TestCase public function test_update_sort_preference() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->actingAs($editor); $updateRequest = $this->patch('/preferences/change-sort/books', [ @@ -70,7 +70,7 @@ class UserPreferencesTest extends TestCase public function test_update_sort_bad_entity_type_handled() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->actingAs($editor); $updateRequest = $this->patch('/preferences/change-sort/dogs', [ @@ -85,7 +85,7 @@ class UserPreferencesTest extends TestCase public function test_update_expansion_preference() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $this->actingAs($editor); $updateRequest = $this->patch('/preferences/change-expansion/home-details', ['expand' => 'true']); @@ -103,7 +103,7 @@ class UserPreferencesTest extends TestCase public function test_toggle_dark_mode() { - $home = $this->actingAs($this->getEditor())->get('/'); + $home = $this->actingAs($this->users->editor())->get('/'); $home->assertSee('Dark Mode'); $this->withHtml($home)->assertElementNotExists('.dark-mode'); @@ -112,7 +112,7 @@ class UserPreferencesTest extends TestCase $prefChange->assertRedirect(); $this->assertEquals(true, setting()->getForCurrentUser('dark-mode-enabled')); - $home = $this->actingAs($this->getEditor())->get('/'); + $home = $this->actingAs($this->users->editor())->get('/'); $this->withHtml($home)->assertElementExists('.dark-mode'); $home->assertDontSee('Dark Mode'); $home->assertSee('Light Mode'); @@ -133,7 +133,7 @@ class UserPreferencesTest extends TestCase public function test_books_view_type_preferences_when_list() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); setting()->putUser($editor, 'books_view_type', 'list'); $resp = $this->actingAs($editor)->get('/books'); @@ -144,7 +144,7 @@ class UserPreferencesTest extends TestCase public function test_books_view_type_preferences_when_grid() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); setting()->putUser($editor, 'books_view_type', 'grid'); $resp = $this->actingAs($editor)->get('/books'); @@ -153,7 +153,7 @@ class UserPreferencesTest extends TestCase public function test_shelf_view_type_change() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $shelf = $this->entities->shelf(); setting()->putUser($editor, 'bookshelf_view_type', 'list'); @@ -175,7 +175,7 @@ class UserPreferencesTest extends TestCase public function test_update_code_language_favourite() { - $editor = $this->getEditor(); + $editor = $this->users->editor(); $page = $this->entities->page(); $this->actingAs($editor); diff --git a/tests/User/UserProfileTest.php b/tests/User/UserProfileTest.php index 77f1644a5..c507e8fa6 100644 --- a/tests/User/UserProfileTest.php +++ b/tests/User/UserProfileTest.php @@ -88,8 +88,8 @@ class UserProfileTest extends TestCase public function test_profile_has_search_links_in_created_entity_lists() { - $user = $this->getEditor(); - $resp = $this->actingAs($this->getAdmin())->get('/user/' . $user->slug); + $user = $this->users->editor(); + $resp = $this->actingAs($this->users->admin())->get('/user/' . $user->slug); $expectedLinks = [ '/search?term=%7Bcreated_by%3A' . $user->slug . '%7D+%7Btype%3Apage%7D', diff --git a/tests/User/UserSearchTest.php b/tests/User/UserSearchTest.php index 243af1186..1b3ca8a35 100644 --- a/tests/User/UserSearchTest.php +++ b/tests/User/UserSearchTest.php @@ -9,8 +9,8 @@ class UserSearchTest extends TestCase { public function test_select_search_matches_by_name() { - $viewer = $this->getViewer(); - $admin = $this->getAdmin(); + $viewer = $this->users->viewer(); + $admin = $this->users->admin(); $resp = $this->actingAs($admin)->get('/search/users/select?search=' . urlencode($viewer->name)); $resp->assertOk(); @@ -30,8 +30,8 @@ class UserSearchTest extends TestCase public function test_select_search_does_not_match_by_email() { - $viewer = $this->getViewer(); - $editor = $this->getEditor(); + $viewer = $this->users->viewer(); + $editor = $this->users->editor(); $resp = $this->actingAs($editor)->get('/search/users/select?search=' . urlencode($viewer->email)); $resp->assertDontSee($viewer->name); @@ -40,13 +40,13 @@ class UserSearchTest extends TestCase public function test_select_requires_right_permission() { $permissions = ['users-manage', 'restrictions-manage-own', 'restrictions-manage-all']; - $user = $this->getViewer(); + $user = $this->users->viewer(); foreach ($permissions as $permission) { $resp = $this->actingAs($user)->get('/search/users/select?search=a'); $this->assertPermissionError($resp); - $this->giveUserPermissions($user, [$permission]); + $this->permissions->grantUserRolePermissions($user, [$permission]); $resp = $this->actingAs($user)->get('/search/users/select?search=a'); $resp->assertOk(); $user->roles()->delete(); @@ -58,7 +58,7 @@ class UserSearchTest extends TestCase { $this->setSettings(['app-public' => true]); $defaultUser = User::getDefault(); - $this->giveUserPermissions($defaultUser, ['users-manage']); + $this->permissions->grantUserRolePermissions($defaultUser, ['users-manage']); $resp = $this->get('/search/users/select?search=a'); $this->assertPermissionError($resp); From e2a72d16aa496cef250986d9f7cc02dd9564e647 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 21 Jan 2023 13:03:47 +0000 Subject: [PATCH 015/477] Made adjustments to fit copied work into dev branch Ported non-compatible elements, Now all tests passing apart from some specific permission scenario tests which are probably correctly failing. Updates some tests to better avoid messing environment state. --- dev/docs/permission-scenario-testing.md | 183 +-------------- .../RegeneratePermissionsCommandTest.php | 20 +- tests/Entity/BookShelfTest.php | 1 + tests/Helpers/PermissionsProvider.php | 12 +- tests/Permissions/EntityPermissionsTest.php | 14 +- .../Scenarios/EntityUserPermissionsTest.php | 209 ------------------ tests/TestCase.php | 3 + tests/Unit/FrameworkAssumptionTest.php | 2 +- 8 files changed, 31 insertions(+), 413 deletions(-) delete mode 100644 tests/Permissions/Scenarios/EntityUserPermissionsTest.php diff --git a/dev/docs/permission-scenario-testing.md b/dev/docs/permission-scenario-testing.md index 6d0935f09..e738fe972 100644 --- a/dev/docs/permission-scenario-testing.md +++ b/dev/docs/permission-scenario-testing.md @@ -6,19 +6,16 @@ Test cases are written ability abstract, since all abilities should act the same Tests are categorised by the most specific element involved in the scenario, where the below list is most specific to least: -- User entity permissions. - Role entity permissions. - Fallback entity permissions. - Role permissions. -- TODO - Test fallback in the context of the above. - ## General Permission Logical Rules The below are some general rules we follow to standardise the behaviour of permissions in the platform: - Most specific permission application (as above) take priority and can deny less specific permissions. -- Parent user/role entity permissions that may be inherited, are considered to essentially be applied on the item they are inherited to unless a lower level has its own permission rule for an already specific role/user. +- Parent role entity permissions that may be inherited, are considered to essentially be applied on the item they are inherited to unless a lower level has its own permission rule for an already specific role. - Where both grant and deny exist at the same specificity, we side towards grant. ## Cases @@ -241,181 +238,3 @@ User denied page permission. - User has Role A & B. User denied page permission. - ---- - -### Entity User Permissions - -These are tests related to entity-level user-specific permission overrides. - -#### test_01_explicit_allow - -- Page permissions have inherit disabled. -- User has entity allow page permission. - -User granted page permission. - -#### test_02_explicit_deny - -- Page permissions have inherit disabled. -- User has entity deny page permission. - -User denied page permission. - -#### test_10_allow_inherit - -- Page permissions have inherit enabled. -- Chapter permissions have inherit disabled. -- User has entity allow chapter permission. - -User granted page permission. - -#### test_11_deny_inherit - -- Page permissions have inherit enabled. -- Chapter permissions have inherit disabled. -- User has entity deny chapter permission. - -User denied page permission. - -#### test_12_allow_inherit_override - -- Page permissions have inherit enabled. -- Chapter permissions have inherit disabled. -- User has entity deny chapter permission. -- User has entity allow page permission. - -User granted page permission. - -#### test_13_deny_inherit_override - -- Page permissions have inherit enabled. -- Chapter permissions have inherit disabled. -- User has entity allow chapter permission. -- User has entity deny page permission. - -User denied page permission. - -#### test_40_entity_role_override_allow - -- Page permissions have inherit disabled. -- User has entity allow page permission. -- Role A has entity deny page permission. -- User has role A. - -User granted page permission. - -#### test_41_entity_role_override_deny - -- Page permissions have inherit disabled. -- User has entity deny page permission. -- Role A has entity allow page permission. -- User has role A. - -User denied page permission. - -#### test_42_entity_role_override_allow_via_inherit - -- Page permissions have inherit enabled. -- Chapter permissions have inherit disabled. -- User has entity allow chapter permission. -- Role A has entity deny page permission. -- User has role A. - -User granted page permission. - -#### test_43_entity_role_override_deny_via_inherit - -- Page permissions have inherit enabled. -- Chapter permissions have inherit disabled. -- User has entity deny chapter permission. -- Role A has entity allow page permission. -- User has role A. - -User denied page permission. - -#### test_50_role_override_allow - -- Page permissions have inherit enabled. -- Role A has no page role permission. -- User has entity allow page permission. -- User has Role A. - -User granted page permission. - -#### test_51_role_override_deny - -- Page permissions have inherit enabled. -- Role A has all-page role permission. -- User has entity deny page permission. -- User has Role A. - -User denied page permission. - -#### test_60_inherited_role_override_allow - -- Page permissions have inherit enabled. -- Role A has no page role permission. -- User has entity allow chapter permission. -- User has Role A. - -User granted page permission. - -#### test_61_inherited_role_override_deny - -- Page permissions have inherit enabled. -- Role A has view-all page role permission. -- User has entity deny chapter permission. -- User has Role A. - -User denied page permission. - -#### test_61_inherited_role_override_deny_on_own - -- Page permissions have inherit enabled. -- Role A has view-own page role permission. -- User has entity deny chapter permission. -- User has Role A. -- User owns Page. - -User denied page permission. - -#### test_70_all_override_allow - -- Page permissions have inherit enabled. -- Role A has no page role permission. -- Role A has entity deny page permission. -- User has entity allow page permission. -- User has Role A. - -User granted page permission. - -#### test_71_all_override_deny - -- Page permissions have inherit enabled. -- Role A has page-all role permission. -- Role A has entity allow page permission. -- User has entity deny page permission. -- User has Role A. - -User denied page permission. - -#### test_80_inherited_all_override_allow - -- Page permissions have inherit enabled. -- Role A has no page role permission. -- Role A has entity deny chapter permission. -- User has entity allow chapter permission. -- User has Role A. - -User granted page permission. - -#### test_81_inherited_all_override_deny - -- Page permissions have inherit enabled. -- Role A has view-all page role permission. -- Role A has entity allow chapter permission. -- User has entity deny chapter permission. -- User has Role A. - -User denied page permission. \ No newline at end of file diff --git a/tests/Commands/RegeneratePermissionsCommandTest.php b/tests/Commands/RegeneratePermissionsCommandTest.php index cc53b460d..b916a8060 100644 --- a/tests/Commands/RegeneratePermissionsCommandTest.php +++ b/tests/Commands/RegeneratePermissionsCommandTest.php @@ -3,6 +3,8 @@ namespace Tests\Commands; use BookStack\Auth\Permissions\CollapsedPermission; +use BookStack\Auth\Permissions\EntityPermission; +use BookStack\Auth\Permissions\JointPermission; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; use Tests\TestCase; @@ -14,21 +16,25 @@ class RegeneratePermissionsCommandTest extends TestCase DB::rollBack(); $page = $this->entities->page(); $editor = $this->users->editor(); - $this->permissions->addEntityPermission($page, ['view'], null, $editor); - CollapsedPermission::query()->truncate(); + $role = $editor->roles()->first(); + $this->permissions->addEntityPermission($page, ['view'], $role); + JointPermission::query()->truncate(); - $this->assertDatabaseMissing('entity_permissions_collapsed', ['entity_id' => $page->id]); + $this->assertDatabaseMissing('joint_permissions', ['entity_id' => $page->id]); $exitCode = Artisan::call('bookstack:regenerate-permissions'); $this->assertTrue($exitCode === 0, 'Command executed successfully'); - $this->assertDatabaseHas('entity_permissions_collapsed', [ + $this->assertDatabaseHas('joint_permissions', [ 'entity_id' => $page->id, - 'user_id' => $editor->id, - 'view' => 1, + 'entity_type' => 'page', + 'role_id' => $role->id, + 'has_permission' => 1, ]); - CollapsedPermission::query()->truncate(); + $page->permissions()->delete(); + $page->rebuildPermissions(); + DB::beginTransaction(); } } diff --git a/tests/Entity/BookShelfTest.php b/tests/Entity/BookShelfTest.php index 5c6489281..d953f3692 100644 --- a/tests/Entity/BookShelfTest.php +++ b/tests/Entity/BookShelfTest.php @@ -21,6 +21,7 @@ class BookShelfTest extends TestCase $this->withHtml($resp)->assertElementContains('header', 'Shelves'); $viewer->roles()->delete(); + $this->permissions->grantUserRolePermissions($viewer, []); $resp = $this->actingAs($viewer)->get('/'); $this->withHtml($resp)->assertElementNotContains('header', 'Shelves'); diff --git a/tests/Helpers/PermissionsProvider.php b/tests/Helpers/PermissionsProvider.php index ac9a2a68a..2cbfb1af5 100644 --- a/tests/Helpers/PermissionsProvider.php +++ b/tests/Helpers/PermissionsProvider.php @@ -85,7 +85,7 @@ class PermissionsProvider if (!$inherit) { // Set default permissions to not allow actions so that only the provided role permissions are at play. - $permissions[] = ['role_id' => null, 'user_id' => null, 'view' => false, 'create' => false, 'update' => false, 'delete' => false]; + $permissions[] = ['role_id' => 0, 'view' => false, 'create' => false, 'update' => false, 'delete' => false]; } foreach ($roles as $role) { @@ -95,9 +95,9 @@ class PermissionsProvider $this->addEntityPermissionEntries($entity, $permissions); } - public function addEntityPermission(Entity $entity, array $actionList, ?Role $role = null, ?User $user = null) + public function addEntityPermission(Entity $entity, array $actionList, Role $role) { - $permissionData = $this->actionListToEntityPermissionData($actionList, $role->id ?? null, $user->id ?? null); + $permissionData = $this->actionListToEntityPermissionData($actionList, $role->id); $this->addEntityPermissionEntries($entity, [$permissionData]); } @@ -107,7 +107,7 @@ class PermissionsProvider */ public function disableEntityInheritedPermissions(Entity $entity): void { - $entity->permissions()->whereNull(['user_id', 'role_id'])->delete(); + $entity->permissions()->where('role_id', '=', 0)->delete(); $fallback = $this->actionListToEntityPermissionData([]); $this->addEntityPermissionEntries($entity, [$fallback]); } @@ -124,9 +124,9 @@ class PermissionsProvider * the format to entity permission data, where permission is granted if the action is in the * given actionList array. */ - protected function actionListToEntityPermissionData(array $actionList, int $roleId = null, int $userId = null): array + protected function actionListToEntityPermissionData(array $actionList, int $roleId = 0): array { - $permissionData = ['role_id' => $roleId, 'user_id' => $userId]; + $permissionData = ['role_id' => $roleId]; foreach (EntityPermission::PERMISSIONS as $possibleAction) { $permissionData[$possibleAction] = in_array($possibleAction, $actionList); } diff --git a/tests/Permissions/EntityPermissionsTest.php b/tests/Permissions/EntityPermissionsTest.php index 68a4ed244..ab8b1242d 100644 --- a/tests/Permissions/EntityPermissionsTest.php +++ b/tests/Permissions/EntityPermissionsTest.php @@ -379,19 +379,17 @@ class EntityPermissionsTest extends TestCase $this->put($modelInstance->getUrl('/permissions'), [ 'permissions' => [ - 'role' => [ - $roleId => [ - $permission => 'true', - ], + $roleId => [ + $permission => 'true', ], ], ]); $this->assertDatabaseHas('entity_permissions', [ - 'entity_id' => $modelInstance->id, - 'entity_type' => $modelInstance->getMorphClass(), - 'role_id' => $roleId, - $permission => true, + 'entity_id' => $modelInstance->id, + 'entity_type' => $modelInstance->getMorphClass(), + 'role_id' => $roleId, + $permission => true, ]); } diff --git a/tests/Permissions/Scenarios/EntityUserPermissionsTest.php b/tests/Permissions/Scenarios/EntityUserPermissionsTest.php deleted file mode 100644 index 4fa805805..000000000 --- a/tests/Permissions/Scenarios/EntityUserPermissionsTest.php +++ /dev/null @@ -1,209 +0,0 @@ -users->newUser(); - $page = $this->entities->page(); - $this->permissions->disableEntityInheritedPermissions($page); - $this->permissions->addEntityPermission($page, ['view'], null, $user); - - $this->assertVisibleToUser($page, $user); - } - - public function test_02_explicit_deny() - { - $user = $this->users->newUser(); - $page = $this->entities->page(); - $this->permissions->disableEntityInheritedPermissions($page); - $this->permissions->addEntityPermission($page, [], null, $user); - - $this->assertNotVisibleToUser($page, $user); - } - - public function test_10_allow_inherit() - { - $user = $this->users->newUser(); - $page = $this->entities->pageWithinChapter(); - $chapter = $page->chapter; - $this->permissions->disableEntityInheritedPermissions($chapter); - $this->permissions->addEntityPermission($chapter, ['view'], null, $user); - - $this->assertVisibleToUser($page, $user); - } - - public function test_11_deny_inherit() - { - $user = $this->users->newUser(); - $page = $this->entities->pageWithinChapter(); - $chapter = $page->chapter; - $this->permissions->disableEntityInheritedPermissions($chapter); - $this->permissions->addEntityPermission($chapter, [], null, $user); - - $this->assertNotVisibleToUser($page, $user); - } - - public function test_12_allow_inherit_override() - { - $user = $this->users->newUser(); - $page = $this->entities->pageWithinChapter(); - $chapter = $page->chapter; - $this->permissions->disableEntityInheritedPermissions($chapter); - $this->permissions->addEntityPermission($chapter, [], null, $user); - $this->permissions->addEntityPermission($page, ['view'], null, $user); - - $this->assertVisibleToUser($page, $user); - } - - public function test_13_deny_inherit_override() - { - $user = $this->users->newUser(); - $page = $this->entities->pageWithinChapter(); - $chapter = $page->chapter; - $this->permissions->disableEntityInheritedPermissions($chapter); - $this->permissions->addEntityPermission($chapter, ['view'], null, $user); - $this->permissions->addEntityPermission($page, ['deny'], null, $user); - - $this->assertNotVisibleToUser($page, $user); - } - - public function test_40_entity_role_override_allow() - { - [$user, $role] = $this->users->newUserWithRole(); - $page = $this->entities->page(); - $this->permissions->disableEntityInheritedPermissions($page); - $this->permissions->addEntityPermission($page, ['view'], null, $user); - $this->permissions->addEntityPermission($page, [], $role); - - $this->assertVisibleToUser($page, $user); - } - - public function test_41_entity_role_override_deny() - { - [$user, $role] = $this->users->newUserWithRole(); - $page = $this->entities->page(); - $this->permissions->disableEntityInheritedPermissions($page); - $this->permissions->addEntityPermission($page, [], null, $user); - $this->permissions->addEntityPermission($page, ['view'], $role); - - $this->assertNotVisibleToUser($page, $user); - } - - public function test_42_entity_role_override_allow_via_inherit() - { - [$user, $role] = $this->users->newUserWithRole(); - $page = $this->entities->pageWithinChapter(); - $chapter = $page->chapter; - $this->permissions->disableEntityInheritedPermissions($chapter); - $this->permissions->addEntityPermission($chapter, ['view'], null, $user); - $this->permissions->addEntityPermission($page, [], $role); - - $this->assertVisibleToUser($page, $user); - } - - public function test_43_entity_role_override_deny_via_inherit() - { - [$user, $role] = $this->users->newUserWithRole(); - $page = $this->entities->pageWithinChapter(); - $chapter = $page->chapter; - $this->permissions->disableEntityInheritedPermissions($chapter); - $this->permissions->addEntityPermission($chapter, [], null, $user); - $this->permissions->addEntityPermission($page, ['view'], $role); - - $this->assertNotVisibleToUser($page, $user); - } - - public function test_50_role_override_allow() - { - [$user, $roleA] = $this->users->newUserWithRole(); - $page = $this->entities->page(); - $this->permissions->addEntityPermission($page, ['view'], null, $user); - - $this->assertVisibleToUser($page, $user); - } - - public function test_51_role_override_deny() - { - [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']); - $page = $this->entities->page(); - $this->permissions->addEntityPermission($page, [], null, $user); - - $this->assertNotVisibleToUser($page, $user); - } - - public function test_60_inherited_role_override_allow() - { - [$user, $roleA] = $this->users->newUserWithRole([], []); - $page = $this->entities->pageWithinChapter(); - $chapter = $page->chapter; - $this->permissions->addEntityPermission($chapter, ['view'], null, $user); - - $this->assertVisibleToUser($page, $user); - } - - public function test_61_inherited_role_override_deny() - { - [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']); - $page = $this->entities->pageWithinChapter(); - $chapter = $page->chapter; - $this->permissions->addEntityPermission($chapter, [], null, $user); - - $this->assertNotVisibleToUser($page, $user); - } - - public function test_61_inherited_role_override_deny_on_own() - { - [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-own']); - $page = $this->entities->pageWithinChapter(); - $chapter = $page->chapter; - $this->permissions->addEntityPermission($chapter, [], null, $user); - $this->permissions->changeEntityOwner($page, $user); - - $this->assertNotVisibleToUser($page, $user); - } - - public function test_70_all_override_allow() - { - [$user, $roleA] = $this->users->newUserWithRole([], []); - $page = $this->entities->page(); - $this->permissions->addEntityPermission($page, [], $roleA, null); - $this->permissions->addEntityPermission($page, ['view'], null, $user); - - $this->assertVisibleToUser($page, $user); - } - - public function test_71_all_override_deny() - { - [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']); - $page = $this->entities->page(); - $this->permissions->addEntityPermission($page, ['view'], $roleA, null); - $this->permissions->addEntityPermission($page, [], null, $user); - - $this->assertNotVisibleToUser($page, $user); - } - - public function test_80_inherited_all_override_allow() - { - [$user, $roleA] = $this->users->newUserWithRole([], []); - $page = $this->entities->pageWithinChapter(); - $chapter = $page->chapter; - $this->permissions->addEntityPermission($chapter, [], $roleA, null); - $this->permissions->addEntityPermission($chapter, ['view'], null, $user); - - $this->assertVisibleToUser($page, $user); - } - - public function test_81_inherited_all_override_deny() - { - [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']); - $page = $this->entities->pageWithinChapter(); - $chapter = $page->chapter; - $this->permissions->addEntityPermission($chapter, ['view'], $roleA, null); - $this->permissions->addEntityPermission($chapter, [], null, $user); - - $this->assertNotVisibleToUser($page, $user); - } -} diff --git a/tests/TestCase.php b/tests/TestCase.php index 70fd0da1d..a5d75655c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -153,9 +153,12 @@ abstract class TestCase extends BaseTestCase DB::purge(); config()->set('database.connections.mysql_testing.database', $database); + DB::beginTransaction(); $callback(); + DB::rollBack(); + if (is_null($originalVal)) { unset($_SERVER[$name]); } else { diff --git a/tests/Unit/FrameworkAssumptionTest.php b/tests/Unit/FrameworkAssumptionTest.php index d4feff60c..54d315de9 100644 --- a/tests/Unit/FrameworkAssumptionTest.php +++ b/tests/Unit/FrameworkAssumptionTest.php @@ -25,7 +25,7 @@ class FrameworkAssumptionTest extends TestCase // Page has SoftDeletes trait by default, so we apply our custom scope and ensure // it stacks on the global scope to filter out deleted items. $query = Page::query()->scopes('visible')->toSql(); - $this->assertStringContainsString('entity_permissions_collapsed', $query); + $this->assertStringContainsString('joint_permissions', $query); $this->assertStringContainsString('`deleted_at` is null', $query); } } From 28dda39260260f8ba6e9cf0b4628d1f56f80aba2 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 21 Jan 2023 19:09:19 +0000 Subject: [PATCH 016/477] Updated PHP and JS depenencies --- composer.lock | 552 +++++++++++++---------- package-lock.json | 1076 ++++++++++++++++++++++----------------------- package.json | 8 +- 3 files changed, 855 insertions(+), 781 deletions(-) diff --git a/composer.lock b/composer.lock index c1a85651f..e58e2b109 100644 --- a/composer.lock +++ b/composer.lock @@ -58,16 +58,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.247.1", + "version": "3.257.5", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "337e447997148b9e5024c2d0ae69618b1cbf80d6" + "reference": "c600a07da531d6c29af791b9d2e8b6df796aa14b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/337e447997148b9e5024c2d0ae69618b1cbf80d6", - "reference": "337e447997148b9e5024c2d0ae69618b1cbf80d6", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c600a07da531d6c29af791b9d2e8b6df796aa14b", + "reference": "c600a07da531d6c29af791b9d2e8b6df796aa14b", "shasum": "" }, "require": { @@ -146,22 +146,22 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.247.1" + "source": "https://github.com/aws/aws-sdk-php/tree/3.257.5" }, - "time": "2022-11-22T19:23:34+00:00" + "time": "2023-01-20T19:34:14+00:00" }, { "name": "bacon/bacon-qr-code", - "version": "2.0.7", + "version": "2.0.8", "source": { "type": "git", "url": "https://github.com/Bacon/BaconQrCode.git", - "reference": "d70c840f68657ce49094b8d91f9ee0cc07fbf66c" + "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/d70c840f68657ce49094b8d91f9ee0cc07fbf66c", - "reference": "d70c840f68657ce49094b8d91f9ee0cc07fbf66c", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22", + "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22", "shasum": "" }, "require": { @@ -200,9 +200,9 @@ "homepage": "https://github.com/Bacon/BaconQrCode", "support": { "issues": "https://github.com/Bacon/BaconQrCode/issues", - "source": "https://github.com/Bacon/BaconQrCode/tree/2.0.7" + "source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8" }, - "time": "2022-03-14T02:02:36+00:00" + "time": "2022-12-07T17:46:57+00:00" }, { "name": "barryvdh/laravel-dompdf", @@ -561,16 +561,16 @@ }, { "name": "doctrine/dbal", - "version": "3.5.1", + "version": "3.5.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "f38ee8aaca2d58ee88653cb34a6a3880c23f38a5" + "reference": "88fa7e5189fd5ec6682477044264dc0ed4e3aa1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/f38ee8aaca2d58ee88653cb34a6a3880c23f38a5", - "reference": "f38ee8aaca2d58ee88653cb34a6a3880c23f38a5", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/88fa7e5189fd5ec6682477044264dc0ed4e3aa1e", + "reference": "88fa7e5189fd5ec6682477044264dc0ed4e3aa1e", "shasum": "" }, "require": { @@ -583,16 +583,16 @@ "psr/log": "^1|^2|^3" }, "require-dev": { - "doctrine/coding-standard": "10.0.0", - "jetbrains/phpstorm-stubs": "2022.2", - "phpstan/phpstan": "1.8.10", + "doctrine/coding-standard": "11.0.0", + "jetbrains/phpstorm-stubs": "2022.3", + "phpstan/phpstan": "1.9.4", "phpstan/phpstan-strict-rules": "^1.4", - "phpunit/phpunit": "9.5.25", - "psalm/plugin-phpunit": "0.17.0", + "phpunit/phpunit": "9.5.27", + "psalm/plugin-phpunit": "0.18.4", "squizlabs/php_codesniffer": "3.7.1", "symfony/cache": "^5.4|^6.0", "symfony/console": "^4.4|^5.4|^6.0", - "vimeo/psalm": "4.29.0" + "vimeo/psalm": "4.30.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -652,7 +652,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.5.1" + "source": "https://github.com/doctrine/dbal/tree/3.5.3" }, "funding": [ { @@ -668,7 +668,7 @@ "type": "tidelift" } ], - "time": "2022-10-24T07:26:18+00:00" + "time": "2023-01-12T10:21:44+00:00" }, { "name": "doctrine/deprecations", @@ -1787,16 +1787,16 @@ }, { "name": "laravel/framework", - "version": "v8.83.26", + "version": "v8.83.27", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "7411d9fa71c1b0fd73a33e225f14512b74e6c81e" + "reference": "e1afe088b4ca613fb96dc57e6d8dbcb8cc2c6b49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/7411d9fa71c1b0fd73a33e225f14512b74e6c81e", - "reference": "7411d9fa71c1b0fd73a33e225f14512b74e6c81e", + "url": "https://api.github.com/repos/laravel/framework/zipball/e1afe088b4ca613fb96dc57e6d8dbcb8cc2c6b49", + "reference": "e1afe088b4ca613fb96dc57e6d8dbcb8cc2c6b49", "shasum": "" }, "require": { @@ -1956,7 +1956,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-11-01T14:48:50+00:00" + "time": "2022-12-08T15:28:55+00:00" }, { "name": "laravel/serializable-closure", @@ -2020,30 +2020,30 @@ }, { "name": "laravel/socialite", - "version": "v5.5.6", + "version": "v5.6.0", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "1cd1682b709b8808a5b5dbb68179a58d1342aa7b" + "reference": "dae03ca4ecfe3badafcdfb81965d2279080051f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/1cd1682b709b8808a5b5dbb68179a58d1342aa7b", - "reference": "1cd1682b709b8808a5b5dbb68179a58d1342aa7b", + "url": "https://api.github.com/repos/laravel/socialite/zipball/dae03ca4ecfe3badafcdfb81965d2279080051f4", + "reference": "dae03ca4ecfe3badafcdfb81965d2279080051f4", "shasum": "" }, "require": { "ext-json": "*", "guzzlehttp/guzzle": "^6.0|^7.0", - "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0", - "illuminate/http": "^6.0|^7.0|^8.0|^9.0", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0", + "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0", "league/oauth1-client": "^1.10.1", "php": "^7.2|^8.0" }, "require-dev": { "mockery/mockery": "^1.0", - "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0", + "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0", "phpunit/phpunit": "^8.0|^9.3" }, "type": "library", @@ -2085,26 +2085,26 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2022-11-08T15:07:05+00:00" + "time": "2023-01-13T15:04:44+00:00" }, { "name": "laravel/tinker", - "version": "v2.7.3", + "version": "v2.8.0", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "5062061b4924af3392225dd482ca7b4d85d8b8ef" + "reference": "74d0b287cc4ae65d15c368dd697aae71d62a73ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/5062061b4924af3392225dd482ca7b4d85d8b8ef", - "reference": "5062061b4924af3392225dd482ca7b4d85d8b8ef", + "url": "https://api.github.com/repos/laravel/tinker/zipball/74d0b287cc4ae65d15c368dd697aae71d62a73ad", + "reference": "74d0b287cc4ae65d15c368dd697aae71d62a73ad", "shasum": "" }, "require": { - "illuminate/console": "^6.0|^7.0|^8.0|^9.0", - "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0", + "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0", "php": "^7.2.5|^8.0", "psy/psysh": "^0.10.4|^0.11.1", "symfony/var-dumper": "^4.3.4|^5.0|^6.0" @@ -2114,7 +2114,7 @@ "phpunit/phpunit": "^8.5.8|^9.3.3" }, "suggest": { - "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0)." + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0)." }, "type": "library", "extra": { @@ -2151,9 +2151,9 @@ ], "support": { "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v2.7.3" + "source": "https://github.com/laravel/tinker/tree/v2.8.0" }, - "time": "2022-11-09T15:11:38+00:00" + "time": "2023-01-10T18:03:30+00:00" }, { "name": "league/commonmark", @@ -2932,16 +2932,16 @@ }, { "name": "nesbot/carbon", - "version": "2.63.0", + "version": "2.65.0", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "ad35dd71a6a212b98e4b87e97389b6fa85f0e347" + "reference": "09acf64155c16dc6f580f36569ae89344e9734a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/ad35dd71a6a212b98e4b87e97389b6fa85f0e347", - "reference": "ad35dd71a6a212b98e4b87e97389b6fa85f0e347", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/09acf64155c16dc6f580f36569ae89344e9734a3", + "reference": "09acf64155c16dc6f580f36569ae89344e9734a3", "shasum": "" }, "require": { @@ -2952,7 +2952,7 @@ "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0" }, "require-dev": { - "doctrine/dbal": "^2.0 || ^3.0", + "doctrine/dbal": "^2.0 || ^3.1.4", "doctrine/orm": "^2.7", "friendsofphp/php-cs-fixer": "^3.0", "kylekatarnls/multi-tester": "^2.0", @@ -3030,20 +3030,20 @@ "type": "tidelift" } ], - "time": "2022-10-30T18:34:28+00:00" + "time": "2023-01-06T15:55:01+00:00" }, { "name": "nikic/php-parser", - "version": "v4.15.2", + "version": "v4.15.3", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc" + "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", - "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/570e980a201d8ed0236b0a62ddf2c9cbb2034039", + "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039", "shasum": "" }, "require": { @@ -3084,9 +3084,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.3" }, - "time": "2022-11-12T15:38:23+00:00" + "time": "2023-01-16T22:05:37+00:00" }, { "name": "onelogin/php-saml", @@ -3493,16 +3493,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.17", + "version": "3.0.18", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "dbc2307d5c69aeb22db136c52e91130d7f2ca761" + "reference": "f28693d38ba21bb0d9f0c411ee5dae2b178201da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/dbc2307d5c69aeb22db136c52e91130d7f2ca761", - "reference": "dbc2307d5c69aeb22db136c52e91130d7f2ca761", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/f28693d38ba21bb0d9f0c411ee5dae2b178201da", + "reference": "f28693d38ba21bb0d9f0c411ee5dae2b178201da", "shasum": "" }, "require": { @@ -3583,7 +3583,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.17" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.18" }, "funding": [ { @@ -3599,7 +3599,7 @@ "type": "tidelift" } ], - "time": "2022-10-24T10:51:50+00:00" + "time": "2022-12-17T18:26:50+00:00" }, { "name": "pragmarx/google2fa", @@ -4129,16 +4129,16 @@ }, { "name": "psy/psysh", - "version": "v0.11.9", + "version": "v0.11.10", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "1acec99d6684a54ff92f8b548a4e41b566963778" + "reference": "e9eadffbed9c9deb5426fd107faae0452bf20a36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/1acec99d6684a54ff92f8b548a4e41b566963778", - "reference": "1acec99d6684a54ff92f8b548a4e41b566963778", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/e9eadffbed9c9deb5426fd107faae0452bf20a36", + "reference": "e9eadffbed9c9deb5426fd107faae0452bf20a36", "shasum": "" }, "require": { @@ -4199,9 +4199,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.11.9" + "source": "https://github.com/bobthecow/psysh/tree/v0.11.10" }, - "time": "2022-11-06T15:29:46+00:00" + "time": "2022-12-23T17:47:18+00:00" }, { "name": "ralouphie/getallheaders", @@ -4249,42 +4249,53 @@ }, { "name": "ramsey/collection", - "version": "1.2.2", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/ramsey/collection.git", - "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a" + "reference": "ad7475d1c9e70b190ecffc58f2d989416af339b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/cccc74ee5e328031b15640b51056ee8d3bb66c0a", - "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a", + "url": "https://api.github.com/repos/ramsey/collection/zipball/ad7475d1c9e70b190ecffc58f2d989416af339b4", + "reference": "ad7475d1c9e70b190ecffc58f2d989416af339b4", "shasum": "" }, "require": { - "php": "^7.3 || ^8", + "php": "^7.4 || ^8.0", "symfony/polyfill-php81": "^1.23" }, "require-dev": { - "captainhook/captainhook": "^5.3", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "ergebnis/composer-normalize": "^2.6", - "fakerphp/faker": "^1.5", - "hamcrest/hamcrest-php": "^2", - "jangregor/phpstan-prophecy": "^0.8", - "mockery/mockery": "^1.3", + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.28.3", + "fakerphp/faker": "^1.21", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^1.0", + "mockery/mockery": "^1.5", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpcsstandards/phpcsutils": "^1.0.0-rc1", "phpspec/prophecy-phpunit": "^2.0", - "phpstan/extension-installer": "^1", - "phpstan/phpstan": "^0.12.32", - "phpstan/phpstan-mockery": "^0.12.5", - "phpstan/phpstan-phpunit": "^0.12.11", - "phpunit/phpunit": "^8.5 || ^9", - "psy/psysh": "^0.10.4", - "slevomat/coding-standard": "^6.3", - "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "^4.4" + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.18.4", + "ramsey/coding-standard": "^2.0.3", + "ramsey/conventional-commits": "^1.3", + "vimeo/psalm": "^5.4" }, "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, "autoload": { "psr-4": { "Ramsey\\Collection\\": "src/" @@ -4312,7 +4323,7 @@ ], "support": { "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/1.2.2" + "source": "https://github.com/ramsey/collection/tree/1.3.0" }, "funding": [ { @@ -4324,7 +4335,7 @@ "type": "tidelift" } ], - "time": "2021-10-10T03:01:02+00:00" + "time": "2022-12-27T19:12:24+00:00" }, { "name": "ramsey/uuid", @@ -4989,16 +5000,16 @@ }, { "name": "symfony/console", - "version": "v5.4.15", + "version": "v5.4.17", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "ea59bb0edfaf9f28d18d8791410ee0355f317669" + "reference": "58422fdcb0e715ed05b385f70d3e8b5ed4bbd45f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/ea59bb0edfaf9f28d18d8791410ee0355f317669", - "reference": "ea59bb0edfaf9f28d18d8791410ee0355f317669", + "url": "https://api.github.com/repos/symfony/console/zipball/58422fdcb0e715ed05b385f70d3e8b5ed4bbd45f", + "reference": "58422fdcb0e715ed05b385f70d3e8b5ed4bbd45f", "shasum": "" }, "require": { @@ -5068,7 +5079,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.15" + "source": "https://github.com/symfony/console/tree/v5.4.17" }, "funding": [ { @@ -5084,20 +5095,20 @@ "type": "tidelift" } ], - "time": "2022-10-26T21:41:52+00:00" + "time": "2022-12-28T14:15:31+00:00" }, { "name": "symfony/css-selector", - "version": "v5.4.11", + "version": "v5.4.17", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "c1681789f059ab756001052164726ae88512ae3d" + "reference": "052ef49b660f9ad2a3adb311c555c9bc11ba61f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/c1681789f059ab756001052164726ae88512ae3d", - "reference": "c1681789f059ab756001052164726ae88512ae3d", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/052ef49b660f9ad2a3adb311c555c9bc11ba61f4", + "reference": "052ef49b660f9ad2a3adb311c555c9bc11ba61f4", "shasum": "" }, "require": { @@ -5134,7 +5145,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v5.4.11" + "source": "https://github.com/symfony/css-selector/tree/v5.4.17" }, "funding": [ { @@ -5150,7 +5161,7 @@ "type": "tidelift" } ], - "time": "2022-06-27T16:58:25+00:00" + "time": "2022-12-23T11:40:44+00:00" }, { "name": "symfony/deprecation-contracts", @@ -5221,16 +5232,16 @@ }, { "name": "symfony/error-handler", - "version": "v5.4.15", + "version": "v5.4.17", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "539cf1428b8442303c6e876ad7bf5a7babd91091" + "reference": "b900446552833ad2f91ca7dd52aa8ffe78f66cb2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/539cf1428b8442303c6e876ad7bf5a7babd91091", - "reference": "539cf1428b8442303c6e876ad7bf5a7babd91091", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/b900446552833ad2f91ca7dd52aa8ffe78f66cb2", + "reference": "b900446552833ad2f91ca7dd52aa8ffe78f66cb2", "shasum": "" }, "require": { @@ -5272,7 +5283,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v5.4.15" + "source": "https://github.com/symfony/error-handler/tree/v5.4.17" }, "funding": [ { @@ -5288,20 +5299,20 @@ "type": "tidelift" } ], - "time": "2022-10-27T06:32:25+00:00" + "time": "2022-12-13T09:43:00+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v5.4.9", + "version": "v5.4.17", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc" + "reference": "8e18a9d559eb8ebc2220588f1faa726a2fcd31c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc", - "reference": "8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8e18a9d559eb8ebc2220588f1faa726a2fcd31c9", + "reference": "8e18a9d559eb8ebc2220588f1faa726a2fcd31c9", "shasum": "" }, "require": { @@ -5357,7 +5368,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.9" + "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.17" }, "funding": [ { @@ -5373,7 +5384,7 @@ "type": "tidelift" } ], - "time": "2022-05-05T16:45:39+00:00" + "time": "2022-12-12T15:54:21+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -5456,16 +5467,16 @@ }, { "name": "symfony/finder", - "version": "v5.4.11", + "version": "v5.4.17", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c" + "reference": "40c08632019838dfb3350f18cf5563b8080055fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/7872a66f57caffa2916a584db1aa7f12adc76f8c", - "reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c", + "url": "https://api.github.com/repos/symfony/finder/zipball/40c08632019838dfb3350f18cf5563b8080055fc", + "reference": "40c08632019838dfb3350f18cf5563b8080055fc", "shasum": "" }, "require": { @@ -5499,7 +5510,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.11" + "source": "https://github.com/symfony/finder/tree/v5.4.17" }, "funding": [ { @@ -5515,20 +5526,20 @@ "type": "tidelift" } ], - "time": "2022-07-29T07:37:50+00:00" + "time": "2022-12-22T10:31:03+00:00" }, { "name": "symfony/http-foundation", - "version": "v5.4.15", + "version": "v5.4.17", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "75bd663ff2db90141bfb733682459d5bbe9e29c3" + "reference": "b64a0e2df212d5849e4584cabff0cf09c5d6866a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/75bd663ff2db90141bfb733682459d5bbe9e29c3", - "reference": "75bd663ff2db90141bfb733682459d5bbe9e29c3", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/b64a0e2df212d5849e4584cabff0cf09c5d6866a", + "reference": "b64a0e2df212d5849e4584cabff0cf09c5d6866a", "shasum": "" }, "require": { @@ -5575,7 +5586,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v5.4.15" + "source": "https://github.com/symfony/http-foundation/tree/v5.4.17" }, "funding": [ { @@ -5591,20 +5602,20 @@ "type": "tidelift" } ], - "time": "2022-10-12T09:43:19+00:00" + "time": "2022-12-14T08:23:03+00:00" }, { "name": "symfony/http-kernel", - "version": "v5.4.15", + "version": "v5.4.18", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "fc63c8c3e1036d424820cc993a4ea163778dc5c7" + "reference": "5da6f57a13e5d7d77197443cf55697cdf65f1352" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/fc63c8c3e1036d424820cc993a4ea163778dc5c7", - "reference": "fc63c8c3e1036d424820cc993a4ea163778dc5c7", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/5da6f57a13e5d7d77197443cf55697cdf65f1352", + "reference": "5da6f57a13e5d7d77197443cf55697cdf65f1352", "shasum": "" }, "require": { @@ -5687,7 +5698,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v5.4.15" + "source": "https://github.com/symfony/http-kernel/tree/v5.4.18" }, "funding": [ { @@ -5703,20 +5714,20 @@ "type": "tidelift" } ], - "time": "2022-10-28T17:52:18+00:00" + "time": "2022-12-29T18:54:08+00:00" }, { "name": "symfony/mime", - "version": "v5.4.14", + "version": "v5.4.17", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "1c118b253bb3495d81e95a6e3ec6c2766a98a0c4" + "reference": "2a83d82efc91c3f03a23c8b47a896df168aa5c63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/1c118b253bb3495d81e95a6e3ec6c2766a98a0c4", - "reference": "1c118b253bb3495d81e95a6e3ec6c2766a98a0c4", + "url": "https://api.github.com/repos/symfony/mime/zipball/2a83d82efc91c3f03a23c8b47a896df168aa5c63", + "reference": "2a83d82efc91c3f03a23c8b47a896df168aa5c63", "shasum": "" }, "require": { @@ -5771,7 +5782,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v5.4.14" + "source": "https://github.com/symfony/mime/tree/v5.4.17" }, "funding": [ { @@ -5787,7 +5798,7 @@ "type": "tidelift" } ], - "time": "2022-10-07T08:01:20+00:00" + "time": "2022-12-13T09:59:55+00:00" }, { "name": "symfony/polyfill-ctype", @@ -6670,16 +6681,16 @@ }, { "name": "symfony/routing", - "version": "v5.4.15", + "version": "v5.4.17", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "5c9b129efe9abce9470e384bf65d8a7e262eee69" + "reference": "4ce2df9a469c19ba45ca6aca04fec1c358a6e791" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/5c9b129efe9abce9470e384bf65d8a7e262eee69", - "reference": "5c9b129efe9abce9470e384bf65d8a7e262eee69", + "url": "https://api.github.com/repos/symfony/routing/zipball/4ce2df9a469c19ba45ca6aca04fec1c358a6e791", + "reference": "4ce2df9a469c19ba45ca6aca04fec1c358a6e791", "shasum": "" }, "require": { @@ -6694,7 +6705,7 @@ "symfony/yaml": "<4.4" }, "require-dev": { - "doctrine/annotations": "^1.12", + "doctrine/annotations": "^1.12|^2", "psr/log": "^1|^2|^3", "symfony/config": "^5.3|^6.0", "symfony/dependency-injection": "^4.4|^5.0|^6.0", @@ -6740,7 +6751,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v5.4.15" + "source": "https://github.com/symfony/routing/tree/v5.4.17" }, "funding": [ { @@ -6756,7 +6767,7 @@ "type": "tidelift" } ], - "time": "2022-10-13T14:10:41+00:00" + "time": "2022-12-20T11:10:57+00:00" }, { "name": "symfony/service-contracts", @@ -6843,16 +6854,16 @@ }, { "name": "symfony/string", - "version": "v5.4.15", + "version": "v5.4.17", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "571334ce9f687e3e6af72db4d3b2a9431e4fd9ed" + "reference": "55733a8664b8853b003e70251c58bc8cb2d82a6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/571334ce9f687e3e6af72db4d3b2a9431e4fd9ed", - "reference": "571334ce9f687e3e6af72db4d3b2a9431e4fd9ed", + "url": "https://api.github.com/repos/symfony/string/zipball/55733a8664b8853b003e70251c58bc8cb2d82a6b", + "reference": "55733a8664b8853b003e70251c58bc8cb2d82a6b", "shasum": "" }, "require": { @@ -6909,7 +6920,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.15" + "source": "https://github.com/symfony/string/tree/v5.4.17" }, "funding": [ { @@ -6925,7 +6936,7 @@ "type": "tidelift" } ], - "time": "2022-10-05T15:16:54+00:00" + "time": "2022-12-12T15:54:21+00:00" }, { "name": "symfony/translation", @@ -7104,16 +7115,16 @@ }, { "name": "symfony/var-dumper", - "version": "v5.4.14", + "version": "v5.4.17", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "6894d06145fefebd9a4c7272baa026a1c394a430" + "reference": "ad74890513d07060255df2575703daf971de92c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/6894d06145fefebd9a4c7272baa026a1c394a430", - "reference": "6894d06145fefebd9a4c7272baa026a1c394a430", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/ad74890513d07060255df2575703daf971de92c7", + "reference": "ad74890513d07060255df2575703daf971de92c7", "shasum": "" }, "require": { @@ -7173,7 +7184,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v5.4.14" + "source": "https://github.com/symfony/var-dumper/tree/v5.4.17" }, "funding": [ { @@ -7189,20 +7200,20 @@ "type": "tidelift" } ], - "time": "2022-10-07T08:01:20+00:00" + "time": "2022-12-22T10:31:03+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", - "version": "2.2.5", + "version": "2.2.6", "source": { "type": "git", "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "4348a3a06651827a27d989ad1d13efec6bb49b19" + "reference": "c42125b83a4fa63b187fdf29f9c93cb7733da30c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/4348a3a06651827a27d989ad1d13efec6bb49b19", - "reference": "4348a3a06651827a27d989ad1d13efec6bb49b19", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/c42125b83a4fa63b187fdf29f9c93cb7733da30c", + "reference": "c42125b83a4fa63b187fdf29f9c93cb7733da30c", "shasum": "" }, "require": { @@ -7240,9 +7251,9 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/2.2.5" + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/2.2.6" }, - "time": "2022-09-12T13:28:28+00:00" + "time": "2023-01-03T09:29:04+00:00" }, { "name": "vlucas/phpdotenv", @@ -7464,16 +7475,16 @@ "packages-dev": [ { "name": "brianium/paratest", - "version": "v6.6.5", + "version": "v6.8.1", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "31fd5d69b41725f383c9a083831eefcc7ecd9061" + "reference": "168c1cfdf79e5b19b57cb03060fc9a6a79c5f582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/31fd5d69b41725f383c9a083831eefcc7ecd9061", - "reference": "31fd5d69b41725f383c9a083831eefcc7ecd9061", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/168c1cfdf79e5b19b57cb03060fc9a6a79c5f582", + "reference": "168c1cfdf79e5b19b57cb03060fc9a6a79c5f582", "shasum": "" }, "require": { @@ -7481,25 +7492,25 @@ "ext-pcre": "*", "ext-reflection": "*", "ext-simplexml": "*", + "fidry/cpu-core-counter": "^0.4.1", "jean85/pretty-package-versions": "^2.0.5", "php": "^7.3 || ^8.0", - "phpunit/php-code-coverage": "^9.2.17", + "phpunit/php-code-coverage": "^9.2.23", "phpunit/php-file-iterator": "^3.0.6", "phpunit/php-timer": "^5.0.3", - "phpunit/phpunit": "^9.5.24", + "phpunit/phpunit": "^9.5.28", "sebastian/environment": "^5.1.4", - "symfony/console": "^5.4.12 || ^6.1.4", - "symfony/process": "^5.4.11 || ^6.1.3" + "symfony/console": "^5.4.16 || ^6.2.3", + "symfony/process": "^5.4.11 || ^6.2" }, "require-dev": { "doctrine/coding-standard": "^10.0.0", "ext-pcov": "*", "ext-posix": "*", - "infection/infection": "^0.26.14", - "malukenho/mcbumpface": "^1.1.5", + "infection/infection": "^0.26.16", "squizlabs/php_codesniffer": "^3.7.1", - "symfony/filesystem": "^5.4.12 || ^6.1.4", - "vimeo/psalm": "^4.27.0" + "symfony/filesystem": "^5.4.13 || ^6.2", + "vimeo/psalm": "^5.4" }, "bin": [ "bin/paratest", @@ -7540,7 +7551,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v6.6.5" + "source": "https://github.com/paratestphp/paratest/tree/v6.8.1" }, "funding": [ { @@ -7552,20 +7563,20 @@ "type": "paypal" } ], - "time": "2022-10-28T12:22:26+00:00" + "time": "2023-01-17T10:08:49+00:00" }, { "name": "composer/ca-bundle", - "version": "1.3.4", + "version": "1.3.5", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "69098eca243998b53eed7a48d82dedd28b447cd5" + "reference": "74780ccf8c19d6acb8d65c5f39cd72110e132bbd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/69098eca243998b53eed7a48d82dedd28b447cd5", - "reference": "69098eca243998b53eed7a48d82dedd28b447cd5", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/74780ccf8c19d6acb8d65c5f39cd72110e132bbd", + "reference": "74780ccf8c19d6acb8d65c5f39cd72110e132bbd", "shasum": "" }, "require": { @@ -7612,7 +7623,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.3.4" + "source": "https://github.com/composer/ca-bundle/tree/1.3.5" }, "funding": [ { @@ -7628,7 +7639,7 @@ "type": "tidelift" } ], - "time": "2022-10-12T12:08:29+00:00" + "time": "2023-01-11T08:27:00+00:00" }, { "name": "composer/class-map-generator", @@ -7705,23 +7716,23 @@ }, { "name": "composer/composer", - "version": "2.4.4", + "version": "2.5.1", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "e8d9087229bcdbc5867594d3098091412f1130cf" + "reference": "923278ad13e1621946eb76ab2882655d2cc396a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/e8d9087229bcdbc5867594d3098091412f1130cf", - "reference": "e8d9087229bcdbc5867594d3098091412f1130cf", + "url": "https://api.github.com/repos/composer/composer/zipball/923278ad13e1621946eb76ab2882655d2cc396a4", + "reference": "923278ad13e1621946eb76ab2882655d2cc396a4", "shasum": "" }, "require": { "composer/ca-bundle": "^1.0", "composer/class-map-generator": "^1.0", "composer/metadata-minifier": "^1.0", - "composer/pcre": "^2 || ^3", + "composer/pcre": "^2.1 || ^3.1", "composer/semver": "^3.0", "composer/spdx-licenses": "^1.5.7", "composer/xdebug-handler": "^2.0.2 || ^3.0.3", @@ -7737,10 +7748,11 @@ "symfony/finder": "^5.4 || ^6.0", "symfony/polyfill-php73": "^1.24", "symfony/polyfill-php80": "^1.24", + "symfony/polyfill-php81": "^1.24", "symfony/process": "^5.4 || ^6.0" }, "require-dev": { - "phpstan/phpstan": "^1.4.1", + "phpstan/phpstan": "^1.9.3", "phpstan/phpstan-deprecation-rules": "^1", "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-strict-rules": "^1", @@ -7758,7 +7770,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.4-dev" + "dev-main": "2.5-dev" }, "phpstan": { "includes": [ @@ -7797,7 +7809,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", - "source": "https://github.com/composer/composer/tree/2.4.4" + "source": "https://github.com/composer/composer/tree/2.5.1" }, "funding": [ { @@ -7813,7 +7825,7 @@ "type": "tidelift" } ], - "time": "2022-10-27T12:39:29+00:00" + "time": "2022-12-22T14:33:54+00:00" }, { "name": "composer/metadata-minifier", @@ -8184,30 +8196,30 @@ }, { "name": "doctrine/instantiator", - "version": "1.4.1", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9", + "doctrine/coding-standard": "^9 || ^11", "ext-pdo": "*", "ext-phar": "*", "phpbench/phpbench": "^0.16 || ^1", "phpstan/phpstan": "^1.4", "phpstan/phpstan-phpunit": "^1", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.22" + "vimeo/psalm": "^4.30 || ^5.4" }, "type": "library", "autoload": { @@ -8234,7 +8246,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" }, "funding": [ { @@ -8250,7 +8262,7 @@ "type": "tidelift" } ], - "time": "2022-03-03T08:28:38+00:00" + "time": "2022-12-30T00:15:36+00:00" }, { "name": "facade/ignition-contracts", @@ -8307,20 +8319,20 @@ }, { "name": "fakerphp/faker", - "version": "v1.20.0", + "version": "v1.21.0", "source": { "type": "git", "url": "https://github.com/FakerPHP/Faker.git", - "reference": "37f751c67a5372d4e26353bd9384bc03744ec77b" + "reference": "92efad6a967f0b79c499705c69b662f738cc9e4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/37f751c67a5372d4e26353bd9384bc03744ec77b", - "reference": "37f751c67a5372d4e26353bd9384bc03744ec77b", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/92efad6a967f0b79c499705c69b662f738cc9e4d", + "reference": "92efad6a967f0b79c499705c69b662f738cc9e4d", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", + "php": "^7.4 || ^8.0", "psr/container": "^1.0 || ^2.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" }, @@ -8331,7 +8343,8 @@ "bamarni/composer-bin-plugin": "^1.4.1", "doctrine/persistence": "^1.3 || ^2.0", "ext-intl": "*", - "symfony/phpunit-bridge": "^4.4 || ^5.2" + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" }, "suggest": { "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", @@ -8343,7 +8356,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "v1.20-dev" + "dev-main": "v1.21-dev" } }, "autoload": { @@ -8368,9 +8381,70 @@ ], "support": { "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.20.0" + "source": "https://github.com/FakerPHP/Faker/tree/v1.21.0" }, - "time": "2022-07-20T13:12:54+00:00" + "time": "2022-12-13T13:54:32+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "0.4.1", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "79261cc280aded96d098e1b0e0ba0c4881b432c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/79261cc280aded96d098e1b0e0ba0c4881b432c2", + "reference": "79261cc280aded96d098e1b0e0ba0c4881b432c2", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^9.5.26 || ^8.5.31", + "theofidry/php-cs-fixer-config": "^1.0", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/0.4.1" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2022-12-16T22:01:02+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -8425,16 +8499,16 @@ }, { "name": "itsgoingd/clockwork", - "version": "v5.1.11", + "version": "v5.1.12", "source": { "type": "git", "url": "https://github.com/itsgoingd/clockwork.git", - "reference": "a790200347f0c6d07e2fca252ccb446df87520c6" + "reference": "c9dbdbb1f0efd19bb80f1080ef63f1b9b1bc3b1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/a790200347f0c6d07e2fca252ccb446df87520c6", - "reference": "a790200347f0c6d07e2fca252ccb446df87520c6", + "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/c9dbdbb1f0efd19bb80f1080ef63f1b9b1bc3b1b", + "reference": "c9dbdbb1f0efd19bb80f1080ef63f1b9b1bc3b1b", "shasum": "" }, "require": { @@ -8481,7 +8555,7 @@ ], "support": { "issues": "https://github.com/itsgoingd/clockwork/issues", - "source": "https://github.com/itsgoingd/clockwork/tree/v5.1.11" + "source": "https://github.com/itsgoingd/clockwork/tree/v5.1.12" }, "funding": [ { @@ -8489,7 +8563,7 @@ "type": "github" } ], - "time": "2022-11-02T21:11:04+00:00" + "time": "2022-12-13T00:04:12+00:00" }, { "name": "jean85/pretty-package-versions", @@ -9108,16 +9182,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.19", + "version": "9.2.23", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "c77b56b63e3d2031bd8997fcec43c1925ae46559" + "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c77b56b63e3d2031bd8997fcec43c1925ae46559", - "reference": "c77b56b63e3d2031bd8997fcec43c1925ae46559", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c", + "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c", "shasum": "" }, "require": { @@ -9173,7 +9247,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.19" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.23" }, "funding": [ { @@ -9181,7 +9255,7 @@ "type": "github" } ], - "time": "2022-11-18T07:47:47+00:00" + "time": "2022-12-28T12:41:10+00:00" }, { "name": "phpunit/php-file-iterator", @@ -9426,20 +9500,20 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.26", + "version": "9.5.28", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "851867efcbb6a1b992ec515c71cdcf20d895e9d2" + "reference": "954ca3113a03bf780d22f07bf055d883ee04b65e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/851867efcbb6a1b992ec515c71cdcf20d895e9d2", - "reference": "851867efcbb6a1b992ec515c71cdcf20d895e9d2", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/954ca3113a03bf780d22f07bf055d883ee04b65e", + "reference": "954ca3113a03bf780d22f07bf055d883ee04b65e", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1", + "doctrine/instantiator": "^1.3.1 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", @@ -9508,7 +9582,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.26" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.28" }, "funding": [ { @@ -9524,7 +9598,7 @@ "type": "tidelift" } ], - "time": "2022-10-28T06:00:21+00:00" + "time": "2023-01-14T12:32:24+00:00" }, { "name": "react/promise", @@ -10853,16 +10927,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v5.4.15", + "version": "v5.4.17", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "b8fd0ff9a0f00d944f1534f6d21e84f92eda7258" + "reference": "32a07d910edc138a1dd5508c17c6b9bc1eb27a1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/b8fd0ff9a0f00d944f1534f6d21e84f92eda7258", - "reference": "b8fd0ff9a0f00d944f1534f6d21e84f92eda7258", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/32a07d910edc138a1dd5508c17c6b9bc1eb27a1b", + "reference": "32a07d910edc138a1dd5508c17c6b9bc1eb27a1b", "shasum": "" }, "require": { @@ -10908,7 +10982,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v5.4.15" + "source": "https://github.com/symfony/dom-crawler/tree/v5.4.17" }, "funding": [ { @@ -10924,7 +10998,7 @@ "type": "tidelift" } ], - "time": "2022-10-27T08:04:35+00:00" + "time": "2022-12-22T10:31:03+00:00" }, { "name": "symfony/filesystem", diff --git a/package-lock.json b/package-lock.json index dbe0f90d2..eff4bea7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,22 +10,22 @@ "dropzone": "^5.9.3", "markdown-it": "^13.0.1", "markdown-it-task-lists": "^2.1.1", - "snabbdom": "^3.5.0", + "snabbdom": "^3.5.1", "sortablejs": "^1.15.0" }, "devDependencies": { "chokidar-cli": "^3.0", - "esbuild": "^0.15.12", + "esbuild": "^0.17.3", "livereload": "^0.9.3", "npm-run-all": "^4.1.5", - "punycode": "^2.1.1", - "sass": "^1.55.0" + "punycode": "^2.3.0", + "sass": "^1.57.0" } }, "node_modules/@esbuild/android-arm": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.12.tgz", - "integrity": "sha512-IC7TqIqiyE0MmvAhWkl/8AEzpOtbhRNDo7aph47We1NbE5w2bt/Q+giAhe0YYeVpYnIhGMcuZY92qDK6dQauvA==", + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.3.tgz", + "integrity": "sha512-1Mlz934GvbgdDmt26rTLmf03cAgLg5HyOgJN+ZGCeP3Q9ynYTNMn2/LQxIl7Uy+o4K6Rfi2OuLsr12JQQR8gNg==", "cpu": [ "arm" ], @@ -38,10 +38,154 @@ "node": ">=12" } }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.3.tgz", + "integrity": "sha512-XvJsYo3dO3Pi4kpalkyMvfQsjxPWHYjoX4MDiB/FUM4YMfWcXa5l4VCwFWVYI1+92yxqjuqrhNg0CZg3gSouyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.3.tgz", + "integrity": "sha512-nuV2CmLS07Gqh5/GrZLuqkU9Bm6H6vcCspM+zjp9TdQlxJtIe+qqEXQChmfc7nWdyr/yz3h45Utk1tUn8Cz5+A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.3.tgz", + "integrity": "sha512-01Hxaaat6m0Xp9AXGM8mjFtqqwDjzlMP0eQq9zll9U85ttVALGCGDuEvra5Feu/NbP5AEP1MaopPwzsTcUq1cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.3.tgz", + "integrity": "sha512-Eo2gq0Q/er2muf8Z83X21UFoB7EU6/m3GNKvrhACJkjVThd0uA+8RfKpfNhuMCl1bKRfBzKOk6xaYKQZ4lZqvA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.3.tgz", + "integrity": "sha512-CN62ESxaquP61n1ZjQP/jZte8CE09M6kNn3baos2SeUfdVBkWN5n6vGp2iKyb/bm/x4JQzEvJgRHLGd5F5b81w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.3.tgz", + "integrity": "sha512-feq+K8TxIznZE+zhdVurF3WNJ/Sa35dQNYbaqM/wsCbWdzXr5lyq+AaTUSER2cUR+SXPnd/EY75EPRjf4s1SLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.3.tgz", + "integrity": "sha512-CLP3EgyNuPcg2cshbwkqYy5bbAgK+VhyfMU7oIYyn+x4Y67xb5C5ylxsNUjRmr8BX+MW3YhVNm6Lq6FKtRTWHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.3.tgz", + "integrity": "sha512-JHeZXD4auLYBnrKn6JYJ0o5nWJI9PhChA/Nt0G4MvLaMrvXuWnY93R3a7PiXeJQphpL1nYsaMcoV2QtuvRnF/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.3.tgz", + "integrity": "sha512-FyXlD2ZjZqTFh0sOQxFDiWG1uQUEOLbEh9gKN/7pFxck5Vw0qjWSDqbn6C10GAa1rXJpwsntHcmLqydY9ST9ZA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/linux-loong64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.12.tgz", - "integrity": "sha512-tZEowDjvU7O7I04GYvWQOS4yyP9E/7YlsB0jjw1Ycukgr2ycEzKyIk5tms5WnLBymaewc6VmRKnn5IJWgK4eFw==", + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.3.tgz", + "integrity": "sha512-OrDGMvDBI2g7s04J8dh8/I7eSO+/E7nMDT2Z5IruBfUO/RiigF1OF6xoH33Dn4W/OwAWSUf1s2nXamb28ZklTA==", "cpu": [ "loong64" ], @@ -54,6 +198,182 @@ "node": ">=12" } }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.3.tgz", + "integrity": "sha512-DcnUpXnVCJvmv0TzuLwKBC2nsQHle8EIiAJiJ+PipEVC16wHXaPEKP0EqN8WnBe0TPvMITOUlP2aiL5YMld+CQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.3.tgz", + "integrity": "sha512-BDYf/l1WVhWE+FHAW3FzZPtVlk9QsrwsxGzABmN4g8bTjmhazsId3h127pliDRRu5674k1Y2RWejbpN46N9ZhQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.3.tgz", + "integrity": "sha512-WViAxWYMRIi+prTJTyV1wnqd2mS2cPqJlN85oscVhXdb/ZTFJdrpaqm/uDsZPGKHtbg5TuRX/ymKdOSk41YZow==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.3.tgz", + "integrity": "sha512-Iw8lkNHUC4oGP1O/KhumcVy77u2s6+KUjieUqzEU3XuWJqZ+AY7uVMrrCbAiwWTkpQHkr00BuXH5RpC6Sb/7Ug==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.3.tgz", + "integrity": "sha512-0AGkWQMzeoeAtXQRNB3s4J1/T2XbigM2/Mn2yU1tQSmQRmHIZdkGbVq2A3aDdNslPyhb9/lH0S5GMTZ4xsjBqg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.3.tgz", + "integrity": "sha512-4+rR/WHOxIVh53UIQIICryjdoKdHsFZFD4zLSonJ9RRw7bhKzVyXbnRPsWSfwybYqw9sB7ots/SYyufL1mBpEg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.3.tgz", + "integrity": "sha512-cVpWnkx9IYg99EjGxa5Gc0XmqumtAwK3aoz7O4Dii2vko+qXbkHoujWA68cqXjhh6TsLaQelfDO4MVnyr+ODeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.3.tgz", + "integrity": "sha512-RxmhKLbTCDAY2xOfrww6ieIZkZF+KBqG7S2Ako2SljKXRFi+0863PspK74QQ7JpmWwncChY25JTJSbVBYGQk2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.3.tgz", + "integrity": "sha512-0r36VeEJ4efwmofxVJRXDjVRP2jTmv877zc+i+Pc7MNsIr38NfsjkQj23AfF7l0WbB+RQ7VUb+LDiqC/KY/M/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.3.tgz", + "integrity": "sha512-wgO6rc7uGStH22nur4aLFcq7Wh86bE9cOFmfTr/yxN3BXvDEdCSXyKkO+U5JIt53eTOgC47v9k/C1bITWL/Teg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.3.tgz", + "integrity": "sha512-FdVl64OIuiKjgXBjwZaJLKp0eaEckifbhn10dXWhysMJkWblg3OEEGKSIyhiD5RSgAya8WzP3DNkngtIg3Nt7g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", @@ -391,9 +711,9 @@ } }, "node_modules/esbuild": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.12.tgz", - "integrity": "sha512-PcT+/wyDqJQsRVhaE9uX/Oq4XLrFh0ce/bs2TJh4CSaw9xuvI+xFrH2nAYOADbhQjUgAhNWC5LKoUsakm4dxng==", + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.3.tgz", + "integrity": "sha512-9n3AsBRe6sIyOc6kmoXg2ypCLgf3eZSraWFRpnkto+svt8cZNuKTkb1bhQcitBcvIqjNiK7K0J3KPmwGSfkA8g==", "dev": true, "hasInstallScript": true, "bin": { @@ -403,348 +723,28 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.15.12", - "@esbuild/linux-loong64": "0.15.12", - "esbuild-android-64": "0.15.12", - "esbuild-android-arm64": "0.15.12", - "esbuild-darwin-64": "0.15.12", - "esbuild-darwin-arm64": "0.15.12", - "esbuild-freebsd-64": "0.15.12", - "esbuild-freebsd-arm64": "0.15.12", - "esbuild-linux-32": "0.15.12", - "esbuild-linux-64": "0.15.12", - "esbuild-linux-arm": "0.15.12", - "esbuild-linux-arm64": "0.15.12", - "esbuild-linux-mips64le": "0.15.12", - "esbuild-linux-ppc64le": "0.15.12", - "esbuild-linux-riscv64": "0.15.12", - "esbuild-linux-s390x": "0.15.12", - "esbuild-netbsd-64": "0.15.12", - "esbuild-openbsd-64": "0.15.12", - "esbuild-sunos-64": "0.15.12", - "esbuild-windows-32": "0.15.12", - "esbuild-windows-64": "0.15.12", - "esbuild-windows-arm64": "0.15.12" - } - }, - "node_modules/esbuild-android-64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.12.tgz", - "integrity": "sha512-MJKXwvPY9g0rGps0+U65HlTsM1wUs9lbjt5CU19RESqycGFDRijMDQsh68MtbzkqWSRdEtiKS1mtPzKneaAI0Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-android-arm64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.12.tgz", - "integrity": "sha512-Hc9SEcZbIMhhLcvhr1DH+lrrec9SFTiRzfJ7EGSBZiiw994gfkVV6vG0sLWqQQ6DD7V4+OggB+Hn0IRUdDUqvA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-darwin-64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.12.tgz", - "integrity": "sha512-qkmqrTVYPFiePt5qFjP8w/S+GIUMbt6k8qmiPraECUWfPptaPJUGkCKrWEfYFRWB7bY23FV95rhvPyh/KARP8Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-darwin-arm64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.12.tgz", - "integrity": "sha512-z4zPX02tQ41kcXMyN3c/GfZpIjKoI/BzHrdKUwhC/Ki5BAhWv59A9M8H+iqaRbwpzYrYidTybBwiZAIWCLJAkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-freebsd-64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.12.tgz", - "integrity": "sha512-XFL7gKMCKXLDiAiBjhLG0XECliXaRLTZh6hsyzqUqPUf/PY4C6EJDTKIeqqPKXaVJ8+fzNek88285krSz1QECw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-freebsd-arm64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.12.tgz", - "integrity": "sha512-jwEIu5UCUk6TjiG1X+KQnCGISI+ILnXzIzt9yDVrhjug2fkYzlLbl0K43q96Q3KB66v6N1UFF0r5Ks4Xo7i72g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-32": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.12.tgz", - "integrity": "sha512-uSQuSEyF1kVzGzuIr4XM+v7TPKxHjBnLcwv2yPyCz8riV8VUCnO/C4BF3w5dHiVpCd5Z1cebBtZJNlC4anWpwA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.12.tgz", - "integrity": "sha512-QcgCKb7zfJxqT9o5z9ZUeGH1k8N6iX1Y7VNsEi5F9+HzN1OIx7ESxtQXDN9jbeUSPiRH1n9cw6gFT3H4qbdvcA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-arm": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.12.tgz", - "integrity": "sha512-Wf7T0aNylGcLu7hBnzMvsTfEXdEdJY/hY3u36Vla21aY66xR0MS5I1Hw8nVquXjTN0A6fk/vnr32tkC/C2lb0A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-arm64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.12.tgz", - "integrity": "sha512-HtNq5xm8fUpZKwWKS2/YGwSfTF+339L4aIA8yphNKYJckd5hVdhfdl6GM2P3HwLSCORS++++7++//ApEwXEuAQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-mips64le": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.12.tgz", - "integrity": "sha512-Qol3+AvivngUZkTVFgLpb0H6DT+N5/zM3V1YgTkryPYFeUvuT5JFNDR3ZiS6LxhyF8EE+fiNtzwlPqMDqVcc6A==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-ppc64le": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.12.tgz", - "integrity": "sha512-4D8qUCo+CFKaR0cGXtGyVsOI7w7k93Qxb3KFXWr75An0DHamYzq8lt7TNZKoOq/Gh8c40/aKaxvcZnTgQ0TJNg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-riscv64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.12.tgz", - "integrity": "sha512-G9w6NcuuCI6TUUxe6ka0enjZHDnSVK8bO+1qDhMOCtl7Tr78CcZilJj8SGLN00zO5iIlwNRZKHjdMpfFgNn1VA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-linux-s390x": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.12.tgz", - "integrity": "sha512-Lt6BDnuXbXeqSlVuuUM5z18GkJAZf3ERskGZbAWjrQoi9xbEIsj/hEzVnSAFLtkfLuy2DE4RwTcX02tZFunXww==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-netbsd-64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.12.tgz", - "integrity": "sha512-jlUxCiHO1dsqoURZDQts+HK100o0hXfi4t54MNRMCAqKGAV33JCVvMplLAa2FwviSojT/5ZG5HUfG3gstwAG8w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-openbsd-64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.12.tgz", - "integrity": "sha512-1o1uAfRTMIWNOmpf8v7iudND0L6zRBYSH45sofCZywrcf7NcZA+c7aFsS1YryU+yN7aRppTqdUK1PgbZVaB1Dw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-sunos-64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.12.tgz", - "integrity": "sha512-nkl251DpoWoBO9Eq9aFdoIt2yYmp4I3kvQjba3jFKlMXuqQ9A4q+JaqdkCouG3DHgAGnzshzaGu6xofGcXyPXg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-windows-32": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.12.tgz", - "integrity": "sha512-WlGeBZHgPC00O08luIp5B2SP4cNCp/PcS+3Pcg31kdcJPopHxLkdCXtadLU9J82LCfw4TVls21A6lilQ9mzHrw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-windows-64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.12.tgz", - "integrity": "sha512-VActO3WnWZSN//xjSfbiGOSyC+wkZtI8I4KlgrTo5oHJM6z3MZZBCuFaZHd8hzf/W9KPhF0lY8OqlmWC9HO5AA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-windows-arm64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.12.tgz", - "integrity": "sha512-Of3MIacva1OK/m4zCNIvBfz8VVROBmQT+gRX6pFTLPngFYcj6TFH/12VveAqq1k9VB2l28EoVMNMUCcmsfwyuA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "@esbuild/android-arm": "0.17.3", + "@esbuild/android-arm64": "0.17.3", + "@esbuild/android-x64": "0.17.3", + "@esbuild/darwin-arm64": "0.17.3", + "@esbuild/darwin-x64": "0.17.3", + "@esbuild/freebsd-arm64": "0.17.3", + "@esbuild/freebsd-x64": "0.17.3", + "@esbuild/linux-arm": "0.17.3", + "@esbuild/linux-arm64": "0.17.3", + "@esbuild/linux-ia32": "0.17.3", + "@esbuild/linux-loong64": "0.17.3", + "@esbuild/linux-mips64el": "0.17.3", + "@esbuild/linux-ppc64": "0.17.3", + "@esbuild/linux-riscv64": "0.17.3", + "@esbuild/linux-s390x": "0.17.3", + "@esbuild/linux-x64": "0.17.3", + "@esbuild/netbsd-x64": "0.17.3", + "@esbuild/openbsd-x64": "0.17.3", + "@esbuild/sunos-x64": "0.17.3", + "@esbuild/win32-arm64": "0.17.3", + "@esbuild/win32-ia32": "0.17.3", + "@esbuild/win32-x64": "0.17.3" } }, "node_modules/escape-string-regexp": { @@ -1551,9 +1551,9 @@ } }, "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true, "engines": { "node": ">=6" @@ -1649,9 +1649,9 @@ } }, "node_modules/sass": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz", - "integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==", + "version": "1.57.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz", + "integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -2040,16 +2040,156 @@ }, "dependencies": { "@esbuild/android-arm": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.12.tgz", - "integrity": "sha512-IC7TqIqiyE0MmvAhWkl/8AEzpOtbhRNDo7aph47We1NbE5w2bt/Q+giAhe0YYeVpYnIhGMcuZY92qDK6dQauvA==", + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.3.tgz", + "integrity": "sha512-1Mlz934GvbgdDmt26rTLmf03cAgLg5HyOgJN+ZGCeP3Q9ynYTNMn2/LQxIl7Uy+o4K6Rfi2OuLsr12JQQR8gNg==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.3.tgz", + "integrity": "sha512-XvJsYo3dO3Pi4kpalkyMvfQsjxPWHYjoX4MDiB/FUM4YMfWcXa5l4VCwFWVYI1+92yxqjuqrhNg0CZg3gSouyQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.3.tgz", + "integrity": "sha512-nuV2CmLS07Gqh5/GrZLuqkU9Bm6H6vcCspM+zjp9TdQlxJtIe+qqEXQChmfc7nWdyr/yz3h45Utk1tUn8Cz5+A==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.3.tgz", + "integrity": "sha512-01Hxaaat6m0Xp9AXGM8mjFtqqwDjzlMP0eQq9zll9U85ttVALGCGDuEvra5Feu/NbP5AEP1MaopPwzsTcUq1cw==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.3.tgz", + "integrity": "sha512-Eo2gq0Q/er2muf8Z83X21UFoB7EU6/m3GNKvrhACJkjVThd0uA+8RfKpfNhuMCl1bKRfBzKOk6xaYKQZ4lZqvA==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.3.tgz", + "integrity": "sha512-CN62ESxaquP61n1ZjQP/jZte8CE09M6kNn3baos2SeUfdVBkWN5n6vGp2iKyb/bm/x4JQzEvJgRHLGd5F5b81w==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.3.tgz", + "integrity": "sha512-feq+K8TxIznZE+zhdVurF3WNJ/Sa35dQNYbaqM/wsCbWdzXr5lyq+AaTUSER2cUR+SXPnd/EY75EPRjf4s1SLg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.3.tgz", + "integrity": "sha512-CLP3EgyNuPcg2cshbwkqYy5bbAgK+VhyfMU7oIYyn+x4Y67xb5C5ylxsNUjRmr8BX+MW3YhVNm6Lq6FKtRTWHQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.3.tgz", + "integrity": "sha512-JHeZXD4auLYBnrKn6JYJ0o5nWJI9PhChA/Nt0G4MvLaMrvXuWnY93R3a7PiXeJQphpL1nYsaMcoV2QtuvRnF/g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.3.tgz", + "integrity": "sha512-FyXlD2ZjZqTFh0sOQxFDiWG1uQUEOLbEh9gKN/7pFxck5Vw0qjWSDqbn6C10GAa1rXJpwsntHcmLqydY9ST9ZA==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.12.tgz", - "integrity": "sha512-tZEowDjvU7O7I04GYvWQOS4yyP9E/7YlsB0jjw1Ycukgr2ycEzKyIk5tms5WnLBymaewc6VmRKnn5IJWgK4eFw==", + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.3.tgz", + "integrity": "sha512-OrDGMvDBI2g7s04J8dh8/I7eSO+/E7nMDT2Z5IruBfUO/RiigF1OF6xoH33Dn4W/OwAWSUf1s2nXamb28ZklTA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.3.tgz", + "integrity": "sha512-DcnUpXnVCJvmv0TzuLwKBC2nsQHle8EIiAJiJ+PipEVC16wHXaPEKP0EqN8WnBe0TPvMITOUlP2aiL5YMld+CQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.3.tgz", + "integrity": "sha512-BDYf/l1WVhWE+FHAW3FzZPtVlk9QsrwsxGzABmN4g8bTjmhazsId3h127pliDRRu5674k1Y2RWejbpN46N9ZhQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.3.tgz", + "integrity": "sha512-WViAxWYMRIi+prTJTyV1wnqd2mS2cPqJlN85oscVhXdb/ZTFJdrpaqm/uDsZPGKHtbg5TuRX/ymKdOSk41YZow==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.3.tgz", + "integrity": "sha512-Iw8lkNHUC4oGP1O/KhumcVy77u2s6+KUjieUqzEU3XuWJqZ+AY7uVMrrCbAiwWTkpQHkr00BuXH5RpC6Sb/7Ug==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.3.tgz", + "integrity": "sha512-0AGkWQMzeoeAtXQRNB3s4J1/T2XbigM2/Mn2yU1tQSmQRmHIZdkGbVq2A3aDdNslPyhb9/lH0S5GMTZ4xsjBqg==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.3.tgz", + "integrity": "sha512-4+rR/WHOxIVh53UIQIICryjdoKdHsFZFD4zLSonJ9RRw7bhKzVyXbnRPsWSfwybYqw9sB7ots/SYyufL1mBpEg==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.3.tgz", + "integrity": "sha512-cVpWnkx9IYg99EjGxa5Gc0XmqumtAwK3aoz7O4Dii2vko+qXbkHoujWA68cqXjhh6TsLaQelfDO4MVnyr+ODeA==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.3.tgz", + "integrity": "sha512-RxmhKLbTCDAY2xOfrww6ieIZkZF+KBqG7S2Ako2SljKXRFi+0863PspK74QQ7JpmWwncChY25JTJSbVBYGQk2Q==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.3.tgz", + "integrity": "sha512-0r36VeEJ4efwmofxVJRXDjVRP2jTmv877zc+i+Pc7MNsIr38NfsjkQj23AfF7l0WbB+RQ7VUb+LDiqC/KY/M/A==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.3.tgz", + "integrity": "sha512-wgO6rc7uGStH22nur4aLFcq7Wh86bE9cOFmfTr/yxN3BXvDEdCSXyKkO+U5JIt53eTOgC47v9k/C1bITWL/Teg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.3.tgz", + "integrity": "sha512-FdVl64OIuiKjgXBjwZaJLKp0eaEckifbhn10dXWhysMJkWblg3OEEGKSIyhiD5RSgAya8WzP3DNkngtIg3Nt7g==", "dev": true, "optional": true }, @@ -2319,175 +2459,35 @@ } }, "esbuild": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.12.tgz", - "integrity": "sha512-PcT+/wyDqJQsRVhaE9uX/Oq4XLrFh0ce/bs2TJh4CSaw9xuvI+xFrH2nAYOADbhQjUgAhNWC5LKoUsakm4dxng==", + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.3.tgz", + "integrity": "sha512-9n3AsBRe6sIyOc6kmoXg2ypCLgf3eZSraWFRpnkto+svt8cZNuKTkb1bhQcitBcvIqjNiK7K0J3KPmwGSfkA8g==", "dev": true, "requires": { - "@esbuild/android-arm": "0.15.12", - "@esbuild/linux-loong64": "0.15.12", - "esbuild-android-64": "0.15.12", - "esbuild-android-arm64": "0.15.12", - "esbuild-darwin-64": "0.15.12", - "esbuild-darwin-arm64": "0.15.12", - "esbuild-freebsd-64": "0.15.12", - "esbuild-freebsd-arm64": "0.15.12", - "esbuild-linux-32": "0.15.12", - "esbuild-linux-64": "0.15.12", - "esbuild-linux-arm": "0.15.12", - "esbuild-linux-arm64": "0.15.12", - "esbuild-linux-mips64le": "0.15.12", - "esbuild-linux-ppc64le": "0.15.12", - "esbuild-linux-riscv64": "0.15.12", - "esbuild-linux-s390x": "0.15.12", - "esbuild-netbsd-64": "0.15.12", - "esbuild-openbsd-64": "0.15.12", - "esbuild-sunos-64": "0.15.12", - "esbuild-windows-32": "0.15.12", - "esbuild-windows-64": "0.15.12", - "esbuild-windows-arm64": "0.15.12" + "@esbuild/android-arm": "0.17.3", + "@esbuild/android-arm64": "0.17.3", + "@esbuild/android-x64": "0.17.3", + "@esbuild/darwin-arm64": "0.17.3", + "@esbuild/darwin-x64": "0.17.3", + "@esbuild/freebsd-arm64": "0.17.3", + "@esbuild/freebsd-x64": "0.17.3", + "@esbuild/linux-arm": "0.17.3", + "@esbuild/linux-arm64": "0.17.3", + "@esbuild/linux-ia32": "0.17.3", + "@esbuild/linux-loong64": "0.17.3", + "@esbuild/linux-mips64el": "0.17.3", + "@esbuild/linux-ppc64": "0.17.3", + "@esbuild/linux-riscv64": "0.17.3", + "@esbuild/linux-s390x": "0.17.3", + "@esbuild/linux-x64": "0.17.3", + "@esbuild/netbsd-x64": "0.17.3", + "@esbuild/openbsd-x64": "0.17.3", + "@esbuild/sunos-x64": "0.17.3", + "@esbuild/win32-arm64": "0.17.3", + "@esbuild/win32-ia32": "0.17.3", + "@esbuild/win32-x64": "0.17.3" } }, - "esbuild-android-64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.12.tgz", - "integrity": "sha512-MJKXwvPY9g0rGps0+U65HlTsM1wUs9lbjt5CU19RESqycGFDRijMDQsh68MtbzkqWSRdEtiKS1mtPzKneaAI0Q==", - "dev": true, - "optional": true - }, - "esbuild-android-arm64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.12.tgz", - "integrity": "sha512-Hc9SEcZbIMhhLcvhr1DH+lrrec9SFTiRzfJ7EGSBZiiw994gfkVV6vG0sLWqQQ6DD7V4+OggB+Hn0IRUdDUqvA==", - "dev": true, - "optional": true - }, - "esbuild-darwin-64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.12.tgz", - "integrity": "sha512-qkmqrTVYPFiePt5qFjP8w/S+GIUMbt6k8qmiPraECUWfPptaPJUGkCKrWEfYFRWB7bY23FV95rhvPyh/KARP8Q==", - "dev": true, - "optional": true - }, - "esbuild-darwin-arm64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.12.tgz", - "integrity": "sha512-z4zPX02tQ41kcXMyN3c/GfZpIjKoI/BzHrdKUwhC/Ki5BAhWv59A9M8H+iqaRbwpzYrYidTybBwiZAIWCLJAkw==", - "dev": true, - "optional": true - }, - "esbuild-freebsd-64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.12.tgz", - "integrity": "sha512-XFL7gKMCKXLDiAiBjhLG0XECliXaRLTZh6hsyzqUqPUf/PY4C6EJDTKIeqqPKXaVJ8+fzNek88285krSz1QECw==", - "dev": true, - "optional": true - }, - "esbuild-freebsd-arm64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.12.tgz", - "integrity": "sha512-jwEIu5UCUk6TjiG1X+KQnCGISI+ILnXzIzt9yDVrhjug2fkYzlLbl0K43q96Q3KB66v6N1UFF0r5Ks4Xo7i72g==", - "dev": true, - "optional": true - }, - "esbuild-linux-32": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.12.tgz", - "integrity": "sha512-uSQuSEyF1kVzGzuIr4XM+v7TPKxHjBnLcwv2yPyCz8riV8VUCnO/C4BF3w5dHiVpCd5Z1cebBtZJNlC4anWpwA==", - "dev": true, - "optional": true - }, - "esbuild-linux-64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.12.tgz", - "integrity": "sha512-QcgCKb7zfJxqT9o5z9ZUeGH1k8N6iX1Y7VNsEi5F9+HzN1OIx7ESxtQXDN9jbeUSPiRH1n9cw6gFT3H4qbdvcA==", - "dev": true, - "optional": true - }, - "esbuild-linux-arm": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.12.tgz", - "integrity": "sha512-Wf7T0aNylGcLu7hBnzMvsTfEXdEdJY/hY3u36Vla21aY66xR0MS5I1Hw8nVquXjTN0A6fk/vnr32tkC/C2lb0A==", - "dev": true, - "optional": true - }, - "esbuild-linux-arm64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.12.tgz", - "integrity": "sha512-HtNq5xm8fUpZKwWKS2/YGwSfTF+339L4aIA8yphNKYJckd5hVdhfdl6GM2P3HwLSCORS++++7++//ApEwXEuAQ==", - "dev": true, - "optional": true - }, - "esbuild-linux-mips64le": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.12.tgz", - "integrity": "sha512-Qol3+AvivngUZkTVFgLpb0H6DT+N5/zM3V1YgTkryPYFeUvuT5JFNDR3ZiS6LxhyF8EE+fiNtzwlPqMDqVcc6A==", - "dev": true, - "optional": true - }, - "esbuild-linux-ppc64le": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.12.tgz", - "integrity": "sha512-4D8qUCo+CFKaR0cGXtGyVsOI7w7k93Qxb3KFXWr75An0DHamYzq8lt7TNZKoOq/Gh8c40/aKaxvcZnTgQ0TJNg==", - "dev": true, - "optional": true - }, - "esbuild-linux-riscv64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.12.tgz", - "integrity": "sha512-G9w6NcuuCI6TUUxe6ka0enjZHDnSVK8bO+1qDhMOCtl7Tr78CcZilJj8SGLN00zO5iIlwNRZKHjdMpfFgNn1VA==", - "dev": true, - "optional": true - }, - "esbuild-linux-s390x": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.12.tgz", - "integrity": "sha512-Lt6BDnuXbXeqSlVuuUM5z18GkJAZf3ERskGZbAWjrQoi9xbEIsj/hEzVnSAFLtkfLuy2DE4RwTcX02tZFunXww==", - "dev": true, - "optional": true - }, - "esbuild-netbsd-64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.12.tgz", - "integrity": "sha512-jlUxCiHO1dsqoURZDQts+HK100o0hXfi4t54MNRMCAqKGAV33JCVvMplLAa2FwviSojT/5ZG5HUfG3gstwAG8w==", - "dev": true, - "optional": true - }, - "esbuild-openbsd-64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.12.tgz", - "integrity": "sha512-1o1uAfRTMIWNOmpf8v7iudND0L6zRBYSH45sofCZywrcf7NcZA+c7aFsS1YryU+yN7aRppTqdUK1PgbZVaB1Dw==", - "dev": true, - "optional": true - }, - "esbuild-sunos-64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.12.tgz", - "integrity": "sha512-nkl251DpoWoBO9Eq9aFdoIt2yYmp4I3kvQjba3jFKlMXuqQ9A4q+JaqdkCouG3DHgAGnzshzaGu6xofGcXyPXg==", - "dev": true, - "optional": true - }, - "esbuild-windows-32": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.12.tgz", - "integrity": "sha512-WlGeBZHgPC00O08luIp5B2SP4cNCp/PcS+3Pcg31kdcJPopHxLkdCXtadLU9J82LCfw4TVls21A6lilQ9mzHrw==", - "dev": true, - "optional": true - }, - "esbuild-windows-64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.12.tgz", - "integrity": "sha512-VActO3WnWZSN//xjSfbiGOSyC+wkZtI8I4KlgrTo5oHJM6z3MZZBCuFaZHd8hzf/W9KPhF0lY8OqlmWC9HO5AA==", - "dev": true, - "optional": true - }, - "esbuild-windows-arm64": { - "version": "0.15.12", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.12.tgz", - "integrity": "sha512-Of3MIacva1OK/m4zCNIvBfz8VVROBmQT+gRX6pFTLPngFYcj6TFH/12VveAqq1k9VB2l28EoVMNMUCcmsfwyuA==", - "dev": true, - "optional": true - }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -3067,9 +3067,9 @@ "dev": true }, "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true }, "read-pkg": { @@ -3138,9 +3138,9 @@ } }, "sass": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz", - "integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==", + "version": "1.57.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz", + "integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==", "dev": true, "requires": { "chokidar": ">=3.0.0 <4.0.0", diff --git a/package.json b/package.json index 9abdcc346..89fb07492 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,11 @@ }, "devDependencies": { "chokidar-cli": "^3.0", - "esbuild": "^0.15.12", + "esbuild": "^0.17.3", "livereload": "^0.9.3", "npm-run-all": "^4.1.5", - "punycode": "^2.1.1", - "sass": "^1.55.0" + "punycode": "^2.3.0", + "sass": "^1.57.0" }, "dependencies": { "clipboard": "^2.0.11", @@ -28,7 +28,7 @@ "dropzone": "^5.9.3", "markdown-it": "^13.0.1", "markdown-it-task-lists": "^2.1.1", - "snabbdom": "^3.5.0", + "snabbdom": "^3.5.1", "sortablejs": "^1.15.0" } } From 78ebcb6f38ee7a984b26cd56dff882ae9d7e9f95 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 21 Jan 2023 20:50:04 +0000 Subject: [PATCH 017/477] Addressed a range of deprecation warnings Closes #3969 --- app/Entities/Tools/Markdown/MarkdownToHtml.php | 4 ++-- app/Search/SearchIndex.php | 4 ++-- app/Util/CspService.php | 2 +- database/factories/Actions/TagFactory.php | 2 +- database/factories/Actions/WebhookFactory.php | 2 +- database/factories/Auth/UserFactory.php | 4 ++-- database/factories/Entities/Models/BookFactory.php | 4 ++-- database/factories/Entities/Models/ChapterFactory.php | 4 ++-- database/factories/Entities/Models/PageFactory.php | 2 +- database/factories/Uploads/ImageFactory.php | 6 +++--- dev/docs/development.md | 2 ++ resources/views/common/notifications.blade.php | 6 +++--- resources/views/exports/parts/meta.blade.php | 4 ++-- resources/views/pages/parts/revisions-index-row.blade.php | 2 +- tests/Entity/ExportTest.php | 4 ++-- tests/Entity/PageContentTest.php | 4 ++-- tests/TestCase.php | 4 ++++ 17 files changed, 33 insertions(+), 27 deletions(-) diff --git a/app/Entities/Tools/Markdown/MarkdownToHtml.php b/app/Entities/Tools/Markdown/MarkdownToHtml.php index f3cf7ab2f..06587ce1f 100644 --- a/app/Entities/Tools/Markdown/MarkdownToHtml.php +++ b/app/Entities/Tools/Markdown/MarkdownToHtml.php @@ -5,10 +5,10 @@ namespace BookStack\Entities\Tools\Markdown; use BookStack\Facades\Theme; use BookStack\Theming\ThemeEvents; use League\CommonMark\Block\Element\ListItem; -use League\CommonMark\CommonMarkConverter; use League\CommonMark\Environment; use League\CommonMark\Extension\Table\TableExtension; use League\CommonMark\Extension\TaskList\TaskListExtension; +use League\CommonMark\MarkdownConverter; class MarkdownToHtml { @@ -26,7 +26,7 @@ class MarkdownToHtml $environment->addExtension(new TaskListExtension()); $environment->addExtension(new CustomStrikeThroughExtension()); $environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment; - $converter = new CommonMarkConverter([], $environment); + $converter = new MarkdownConverter($environment); $environment->addBlockRenderer(ListItem::class, new CustomListItemRenderer(), 10); diff --git a/app/Search/SearchIndex.php b/app/Search/SearchIndex.php index 8c793a109..54ed95ebb 100644 --- a/app/Search/SearchIndex.php +++ b/app/Search/SearchIndex.php @@ -112,12 +112,12 @@ class SearchIndex * * @returns array */ - protected function generateTermScoreMapFromText(string $text, int $scoreAdjustment = 1): array + protected function generateTermScoreMapFromText(string $text, float $scoreAdjustment = 1): array { $termMap = $this->textToTermCountMap($text); foreach ($termMap as $term => $count) { - $termMap[$term] = $count * $scoreAdjustment; + $termMap[$term] = floor($count * $scoreAdjustment); } return $termMap; diff --git a/app/Util/CspService.php b/app/Util/CspService.php index f9ab666ac..227ec8e0b 100644 --- a/app/Util/CspService.php +++ b/app/Util/CspService.php @@ -126,7 +126,7 @@ class CspService protected function getAllowedIframeHosts(): array { - $hosts = config('app.iframe_hosts', ''); + $hosts = config('app.iframe_hosts') ?? ''; return array_filter(explode(' ', $hosts)); } diff --git a/database/factories/Actions/TagFactory.php b/database/factories/Actions/TagFactory.php index 8d5c77e09..8b9c529f2 100644 --- a/database/factories/Actions/TagFactory.php +++ b/database/factories/Actions/TagFactory.php @@ -21,7 +21,7 @@ class TagFactory extends Factory public function definition() { return [ - 'name' => $this->faker->city, + 'name' => $this->faker->city(), 'value' => $this->faker->sentence(3), ]; } diff --git a/database/factories/Actions/WebhookFactory.php b/database/factories/Actions/WebhookFactory.php index 662f64f8b..c7393b32c 100644 --- a/database/factories/Actions/WebhookFactory.php +++ b/database/factories/Actions/WebhookFactory.php @@ -18,7 +18,7 @@ class WebhookFactory extends Factory { return [ 'name' => 'My webhook for ' . $this->faker->country(), - 'endpoint' => $this->faker->url, + 'endpoint' => $this->faker->url(), 'active' => true, 'timeout' => 3, ]; diff --git a/database/factories/Auth/UserFactory.php b/database/factories/Auth/UserFactory.php index 805782fd8..6ff62a975 100644 --- a/database/factories/Auth/UserFactory.php +++ b/database/factories/Auth/UserFactory.php @@ -22,11 +22,11 @@ class UserFactory extends Factory */ public function definition() { - $name = $this->faker->name; + $name = $this->faker->name(); return [ 'name' => $name, - 'email' => $this->faker->email, + 'email' => $this->faker->email(), 'slug' => Str::slug($name . '-' . Str::random(5)), 'password' => Str::random(10), 'remember_token' => Str::random(10), diff --git a/database/factories/Entities/Models/BookFactory.php b/database/factories/Entities/Models/BookFactory.php index 0613800a1..3bf157786 100644 --- a/database/factories/Entities/Models/BookFactory.php +++ b/database/factories/Entities/Models/BookFactory.php @@ -22,9 +22,9 @@ class BookFactory extends Factory public function definition() { return [ - 'name' => $this->faker->sentence, + 'name' => $this->faker->sentence(), 'slug' => Str::random(10), - 'description' => $this->faker->paragraph, + 'description' => $this->faker->paragraph(), ]; } } diff --git a/database/factories/Entities/Models/ChapterFactory.php b/database/factories/Entities/Models/ChapterFactory.php index 4fcd69c39..36379866e 100644 --- a/database/factories/Entities/Models/ChapterFactory.php +++ b/database/factories/Entities/Models/ChapterFactory.php @@ -22,9 +22,9 @@ class ChapterFactory extends Factory public function definition() { return [ - 'name' => $this->faker->sentence, + 'name' => $this->faker->sentence(), 'slug' => Str::random(10), - 'description' => $this->faker->paragraph, + 'description' => $this->faker->paragraph(), ]; } } diff --git a/database/factories/Entities/Models/PageFactory.php b/database/factories/Entities/Models/PageFactory.php index c83e0f828..319d97880 100644 --- a/database/factories/Entities/Models/PageFactory.php +++ b/database/factories/Entities/Models/PageFactory.php @@ -24,7 +24,7 @@ class PageFactory extends Factory $html = '

' . implode('

', $this->faker->paragraphs(5)) . '

'; return [ - 'name' => $this->faker->sentence, + 'name' => $this->faker->sentence(), 'slug' => Str::random(10), 'html' => $html, 'text' => strip_tags($html), diff --git a/database/factories/Uploads/ImageFactory.php b/database/factories/Uploads/ImageFactory.php index c6d0e0801..b66c0a52c 100644 --- a/database/factories/Uploads/ImageFactory.php +++ b/database/factories/Uploads/ImageFactory.php @@ -21,9 +21,9 @@ class ImageFactory extends Factory public function definition() { return [ - 'name' => $this->faker->slug . '.jpg', - 'url' => $this->faker->url, - 'path' => $this->faker->url, + 'name' => $this->faker->slug() . '.jpg', + 'url' => $this->faker->url(), + 'path' => $this->faker->url(), 'type' => 'gallery', 'uploaded_to' => 0, ]; diff --git a/dev/docs/development.md b/dev/docs/development.md index 1611de578..b68f2664a 100644 --- a/dev/docs/development.md +++ b/dev/docs/development.md @@ -29,6 +29,8 @@ The testing database will also need migrating and seeding beforehand. This can b Once done you can run `composer test` in the application root directory to run all tests. Tests can be ran in parallel by running them via `composer t`. This will use Laravel's built-in parallel testing functionality, and attempt to create and seed a database instance for each testing thread. If required these parallel testing instances can be reset, before testing again, by running `composer t-reset`. +If the codebase needs to be tested with deprecations, this can be done via uncommenting the relevant line within the TestCase@setUp function. + ## Code Standards PHP code standards are managed by [using PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer). diff --git a/resources/views/common/notifications.blade.php b/resources/views/common/notifications.blade.php index e06bd5fd1..8b76a8dd5 100644 --- a/resources/views/common/notifications.blade.php +++ b/resources/views/common/notifications.blade.php @@ -5,7 +5,7 @@ style="display: none;" class="notification pos" role="alert"> - @icon('check-circle') {!! nl2br(htmlentities(session()->get('success'))) !!}
@icon('close')
+ @icon('check-circle') @if(session()->has('success')){!! nl2br(htmlentities(session()->get('success'))) !!}@endif
@icon('close')
\ No newline at end of file diff --git a/resources/views/exports/parts/meta.blade.php b/resources/views/exports/parts/meta.blade.php index 02a39e78c..d4128898b 100644 --- a/resources/views/exports/parts/meta.blade.php +++ b/resources/views/exports/parts/meta.blade.php @@ -4,13 +4,13 @@ @endif @icon('star'){!! trans('entities.meta_created' . ($entity->createdBy ? '_name' : ''), [ - 'timeLength' => $entity->created_at->formatLocalized('%e %B %Y %H:%M:%S'), + 'timeLength' => $entity->created_at->isoFormat('D MMMM Y HH:mm:ss'), 'user' => e($entity->createdBy->name ?? ''), ]) !!}
@icon('edit'){!! trans('entities.meta_updated' . ($entity->updatedBy ? '_name' : ''), [ - 'timeLength' => $entity->updated_at->formatLocalized('%e %B %Y %H:%M:%S'), + 'timeLength' => $entity->updated_at->isoFormat('D MMMM Y HH:mm:ss'), 'user' => e($entity->updatedBy->name ?? '') ]) !!}
\ No newline at end of file diff --git a/resources/views/pages/parts/revisions-index-row.blade.php b/resources/views/pages/parts/revisions-index-row.blade.php index 597b53234..fdc6a772d 100644 --- a/resources/views/pages/parts/revisions-index-row.blade.php +++ b/resources/views/pages/parts/revisions-index-row.blade.php @@ -17,7 +17,7 @@ @if($revision->createdBy) {{ $revision->createdBy->name }} @else {{ trans('common.deleted_user') }} @endif
- {{ $revision->created_at->formatLocalized('%e %B %Y %H:%M:%S') }} + {{ $revision->created_at->isoFormat('D MMMM Y HH:mm:ss') }} ({{ $revision->created_at->diffForHumans() }})
diff --git a/tests/Entity/ExportTest.php b/tests/Entity/ExportTest.php index 0f80bdd49..68c70e6c0 100644 --- a/tests/Entity/ExportTest.php +++ b/tests/Entity/ExportTest.php @@ -160,9 +160,9 @@ class ExportTest extends TestCase $page = $this->entities->page(); $resp = $this->asEditor()->get($page->getUrl('/export/html')); - $resp->assertSee($page->created_at->formatLocalized('%e %B %Y %H:%M:%S')); + $resp->assertSee($page->created_at->isoFormat('D MMMM Y HH:mm:ss')); $resp->assertDontSee($page->created_at->diffForHumans()); - $resp->assertSee($page->updated_at->formatLocalized('%e %B %Y %H:%M:%S')); + $resp->assertSee($page->updated_at->isoFormat('D MMMM Y HH:mm:ss')); $resp->assertDontSee($page->updated_at->diffForHumans()); } diff --git a/tests/Entity/PageContentTest.php b/tests/Entity/PageContentTest.php index 0c9854206..53107d14d 100644 --- a/tests/Entity/PageContentTest.php +++ b/tests/Entity/PageContentTest.php @@ -310,7 +310,7 @@ class PageContentTest extends TestCase { $this->asEditor(); $page = $this->entities->page(); - config()->push('app.allow_content_scripts', 'true'); + config()->set('app.allow_content_scripts', 'true'); $script = 'abc123abc123'; $page->html = "no escape {$script}"; @@ -355,7 +355,7 @@ class PageContentTest extends TestCase { $this->asEditor(); $page = $this->entities->page(); - config()->push('app.allow_content_scripts', 'true'); + config()->set('app.allow_content_scripts', 'true'); $script = '

Hello

'; $page->html = "escape {$script}"; diff --git a/tests/TestCase.php b/tests/TestCase.php index d0dd7d772..d9a614fc6 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -43,6 +43,10 @@ abstract class TestCase extends BaseTestCase { $this->entities = new EntityProvider(); parent::setUp(); + + // We can uncomment the below to run tests with failings upon deprecations. + // Can't leave on since some deprecations can only be fixed upstream. + // $this->withoutDeprecationHandling(); } /** From 1c2ae7bff6b00fbbba826a413a658978f87c8285 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 21 Jan 2023 21:34:39 +0000 Subject: [PATCH 018/477] Added gmp extension to test workflow If was not already enabled by default, should enable faster testing handling as it helps the phpseclib usage for OIDC tokens in test rocket through. --- .github/workflows/test-php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-php.yml b/.github/workflows/test-php.yml index 903b676cd..215f98741 100644 --- a/.github/workflows/test-php.yml +++ b/.github/workflows/test-php.yml @@ -16,7 +16,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: gd, mbstring, json, curl, xml, mysql, ldap + extensions: gd, mbstring, json, curl, xml, mysql, ldap, gmp - name: Get Composer Cache Directory id: composer-cache From f3f2a0c1d55681bbbf141f051b073a5b39100a51 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 23 Jan 2023 12:40:11 +0000 Subject: [PATCH 019/477] Updated userCan logic to meet expectations in tests Updated with similar logic to that used in the user_permissions branch, but all extracted to a seperate class for doing all fetch and collapse work. --- .../Permissions/EntityPermissionEvaluator.php | 131 ++++++++++++++++++ app/Auth/Permissions/PermissionApplicator.php | 46 +----- 2 files changed, 134 insertions(+), 43 deletions(-) create mode 100644 app/Auth/Permissions/EntityPermissionEvaluator.php diff --git a/app/Auth/Permissions/EntityPermissionEvaluator.php b/app/Auth/Permissions/EntityPermissionEvaluator.php new file mode 100644 index 000000000..91596d02a --- /dev/null +++ b/app/Auth/Permissions/EntityPermissionEvaluator.php @@ -0,0 +1,131 @@ +entity = $entity; + $this->userId = $userId; + $this->userRoleIds = $userRoleIds; + $this->action = $action; + } + + public function evaluate(): ?bool + { + if ($this->isUserSystemAdmin()) { + return true; + } + + $typeIdChain = $this->gatherEntityChainTypeIds(); + $relevantPermissions = $this->getRelevantPermissionsMapByTypeId($typeIdChain); + $permitsByType = $this->collapseAndCategorisePermissions($typeIdChain, $relevantPermissions); + + // Return grant or reject from role-level if exists + if (count($permitsByType['role']) > 0) { + return boolval(max($permitsByType['role'])); + } + + // Return fallback permission if exists + if (count($permitsByType['fallback']) > 0) { + return boolval($permitsByType['fallback'][0]); + } + + return null; + } + + /** + * @param string[] $typeIdChain + * @param array $permissionMapByTypeId + * @return array> + */ + protected function collapseAndCategorisePermissions(array $typeIdChain, array $permissionMapByTypeId): array + { + $permitsByType = ['fallback' => [], 'role' => []]; + + foreach ($typeIdChain as $typeId) { + $permissions = $permissionMapByTypeId[$typeId] ?? []; + foreach ($permissions as $permission) { + $roleId = $permission->role_id; + $type = $roleId === 0 ? 'fallback' : 'role'; + if (!isset($permitsByType[$type][$roleId])) { + $permitsByType[$type][$roleId] = $permission->{$this->action}; + } + } + } + + return $permitsByType; + } + + /** + * @param string[] $typeIdChain + * @return array + */ + protected function getRelevantPermissionsMapByTypeId(array $typeIdChain): array + { + $relevantPermissions = EntityPermission::query() + ->where(function (Builder $query) use ($typeIdChain) { + foreach ($typeIdChain as $typeId) { + $query->orWhere(function (Builder $query) use ($typeId) { + [$type, $id] = explode(':', $typeId); + $query->where('entity_type', '=', $type) + ->where('entity_id', '=', $id); + }); + } + })->where(function (Builder $query) { + $query->whereIn('role_id', [...$this->userRoleIds, 0]); + })->get(['entity_id', 'entity_type', 'role_id', $this->action]) + ->all(); + + $map = []; + foreach ($relevantPermissions as $permission) { + $key = $permission->entity_type . ':' . $permission->entity_id; + if (!isset($map[$key])) { + $map[$key] = []; + } + + $map[$key][] = $permission; + } + + return $map; + } + + /** + * @return string[] + */ + protected function gatherEntityChainTypeIds(): array + { + // The array order here is very important due to the fact we walk up the chain + // elsewhere in the class. Earlier items in the chain have higher priority. + + $chain = [$this->entity->getMorphClass() . ':' . $this->entity->id]; + + if ($this->entity instanceof Page && $this->entity->chapter_id) { + $chain[] = 'chapter:' . $this->entity->chapter_id; + } + + if ($this->entity instanceof Page || $this->entity instanceof Chapter) { + $chain[] = 'book:' . $this->entity->book_id; + } + + return $chain; + } + + protected function isUserSystemAdmin(): bool + { + $adminRoleId = Role::getSystemRole('admin')->id; + return in_array($adminRoleId, $this->userRoleIds); + } +} diff --git a/app/Auth/Permissions/PermissionApplicator.php b/app/Auth/Permissions/PermissionApplicator.php index af372cb74..3855a283b 100644 --- a/app/Auth/Permissions/PermissionApplicator.php +++ b/app/Auth/Permissions/PermissionApplicator.php @@ -4,7 +4,6 @@ namespace BookStack\Auth\Permissions; use BookStack\Auth\Role; use BookStack\Auth\User; -use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; use BookStack\Model; @@ -48,7 +47,7 @@ class PermissionApplicator return $hasRolePermission; } - $hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $action); + $hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $user->id, $userRoleIds, $action); return is_null($hasApplicableEntityPermissions) ? $hasRolePermission : $hasApplicableEntityPermissions; } @@ -57,50 +56,11 @@ class PermissionApplicator * Check if there are permissions that are applicable for the given entity item, action and roles. * Returns null when no entity permissions are in force. */ - protected function hasEntityPermission(Entity $entity, array $userRoleIds, string $action): ?bool + protected function hasEntityPermission(Entity $entity, int $userId, array $userRoleIds, string $action): ?bool { $this->ensureValidEntityAction($action); - $adminRoleId = Role::getSystemRole('admin')->id; - if (in_array($adminRoleId, $userRoleIds)) { - return true; - } - - // The chain order here is very important due to the fact we walk up the chain - // in the loop below. Earlier items in the chain have higher priority. - $chain = [$entity]; - if ($entity instanceof Page && $entity->chapter_id) { - $chain[] = $entity->chapter; - } - - if ($entity instanceof Page || $entity instanceof Chapter) { - $chain[] = $entity->book; - } - - foreach ($chain as $currentEntity) { - $allowedByRoleId = $currentEntity->permissions() - ->whereIn('role_id', [0, ...$userRoleIds]) - ->pluck($action, 'role_id'); - - // Continue up the chain if no applicable entity permission overrides. - if ($allowedByRoleId->isEmpty()) { - continue; - } - - // If we have user-role-specific permissions set, allow if any of those - // role permissions allow access. - $hasDefault = $allowedByRoleId->has(0); - if (!$hasDefault || $allowedByRoleId->count() > 1) { - return $allowedByRoleId->search(function (bool $allowed, int $roleId) { - return $roleId !== 0 && $allowed; - }) !== false; - } - - // Otherwise, return the default "Other roles" fallback value. - return $allowedByRoleId->get(0); - } - - return null; + return (new EntityPermissionEvaluator($entity, $userId, $userRoleIds, $action))->evaluate(); } /** From 91e613fe606777c0b036a2cfada94092b771dc22 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 23 Jan 2023 15:09:03 +0000 Subject: [PATCH 020/477] Shared entity permission logic across both query methods The runtime userCan() and the JointPermissionBuilder now share much of the same logic for handling entity permission resolution. --- .../Permissions/EntityPermissionEvaluator.php | 74 +++++---- .../Permissions/JointPermissionBuilder.php | 150 ++---------------- .../MassEntityPermissionEvaluator.php | 81 ++++++++++ app/Auth/Permissions/PermissionApplicator.php | 6 +- app/Auth/Permissions/SimpleEntityData.php | 16 ++ 5 files changed, 155 insertions(+), 172 deletions(-) create mode 100644 app/Auth/Permissions/MassEntityPermissionEvaluator.php diff --git a/app/Auth/Permissions/EntityPermissionEvaluator.php b/app/Auth/Permissions/EntityPermissionEvaluator.php index 91596d02a..99e87d769 100644 --- a/app/Auth/Permissions/EntityPermissionEvaluator.php +++ b/app/Auth/Permissions/EntityPermissionEvaluator.php @@ -3,36 +3,36 @@ namespace BookStack\Auth\Permissions; use BookStack\Auth\Role; -use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; -use BookStack\Entities\Models\Page; use Illuminate\Database\Eloquent\Builder; class EntityPermissionEvaluator { - protected Entity $entity; - protected array $userRoleIds; protected string $action; - protected int $userId; - public function __construct(Entity $entity, int $userId, array $userRoleIds, string $action) + public function __construct(string $action) { - $this->entity = $entity; - $this->userId = $userId; - $this->userRoleIds = $userRoleIds; $this->action = $action; } - public function evaluate(): ?bool + public function evaluateEntityForUser(Entity $entity, array $userRoleIds): ?bool { - if ($this->isUserSystemAdmin()) { + if ($this->isUserSystemAdmin($userRoleIds)) { return true; } - $typeIdChain = $this->gatherEntityChainTypeIds(); - $relevantPermissions = $this->getRelevantPermissionsMapByTypeId($typeIdChain); + $typeIdChain = $this->gatherEntityChainTypeIds(SimpleEntityData::fromEntity($entity)); + $relevantPermissions = $this->getPermissionsMapByTypeId($typeIdChain, [...$userRoleIds, 0]); $permitsByType = $this->collapseAndCategorisePermissions($typeIdChain, $relevantPermissions); + return $this->evaluatePermitsByType($permitsByType); + } + + /** + * @param array> $permitsByType + */ + protected function evaluatePermitsByType(array $permitsByType): ?bool + { // Return grant or reject from role-level if exists if (count($permitsByType['role']) > 0) { return boolval(max($permitsByType['role'])); @@ -73,21 +73,25 @@ class EntityPermissionEvaluator * @param string[] $typeIdChain * @return array */ - protected function getRelevantPermissionsMapByTypeId(array $typeIdChain): array + protected function getPermissionsMapByTypeId(array $typeIdChain, array $filterRoleIds): array { - $relevantPermissions = EntityPermission::query() - ->where(function (Builder $query) use ($typeIdChain) { - foreach ($typeIdChain as $typeId) { - $query->orWhere(function (Builder $query) use ($typeId) { - [$type, $id] = explode(':', $typeId); - $query->where('entity_type', '=', $type) - ->where('entity_id', '=', $id); - }); - } - })->where(function (Builder $query) { - $query->whereIn('role_id', [...$this->userRoleIds, 0]); - })->get(['entity_id', 'entity_type', 'role_id', $this->action]) - ->all(); + $query = EntityPermission::query()->where(function (Builder $query) use ($typeIdChain) { + foreach ($typeIdChain as $typeId) { + $query->orWhere(function (Builder $query) use ($typeId) { + [$type, $id] = explode(':', $typeId); + $query->where('entity_type', '=', $type) + ->where('entity_id', '=', $id); + }); + } + }); + + if (!empty($filterRoleIds)) { + $query->where(function (Builder $query) use ($filterRoleIds) { + $query->whereIn('role_id', [...$filterRoleIds, 0]); + }); + } + + $relevantPermissions = $query->get(['entity_id', 'entity_type', 'role_id', $this->action])->all(); $map = []; foreach ($relevantPermissions as $permission) { @@ -105,27 +109,27 @@ class EntityPermissionEvaluator /** * @return string[] */ - protected function gatherEntityChainTypeIds(): array + protected function gatherEntityChainTypeIds(SimpleEntityData $entity): array { // The array order here is very important due to the fact we walk up the chain // elsewhere in the class. Earlier items in the chain have higher priority. - $chain = [$this->entity->getMorphClass() . ':' . $this->entity->id]; + $chain = [$entity->type . ':' . $entity->id]; - if ($this->entity instanceof Page && $this->entity->chapter_id) { - $chain[] = 'chapter:' . $this->entity->chapter_id; + if ($entity->type === 'page' && $entity->chapter_id) { + $chain[] = 'chapter:' . $entity->chapter_id; } - if ($this->entity instanceof Page || $this->entity instanceof Chapter) { - $chain[] = 'book:' . $this->entity->book_id; + if ($entity->type === 'page' || $entity->type === 'chapter') { + $chain[] = 'book:' . $entity->book_id; } return $chain; } - protected function isUserSystemAdmin(): bool + protected function isUserSystemAdmin($userRoleIds): bool { $adminRoleId = Role::getSystemRole('admin')->id; - return in_array($adminRoleId, $this->userRoleIds); + return in_array($adminRoleId, $userRoleIds); } } diff --git a/app/Auth/Permissions/JointPermissionBuilder.php b/app/Auth/Permissions/JointPermissionBuilder.php index 114cff619..91207e3ab 100644 --- a/app/Auth/Permissions/JointPermissionBuilder.php +++ b/app/Auth/Permissions/JointPermissionBuilder.php @@ -19,11 +19,6 @@ use Illuminate\Support\Facades\DB; */ class JointPermissionBuilder { - /** - * @var array> - */ - protected array $entityCache; - /** * Re-generate all entity permission from scratch. */ @@ -98,40 +93,6 @@ class JointPermissionBuilder }); } - /** - * Prepare the local entity cache and ensure it's empty. - * - * @param SimpleEntityData[] $entities - */ - protected function readyEntityCache(array $entities) - { - $this->entityCache = []; - - foreach ($entities as $entity) { - if (!isset($this->entityCache[$entity->type])) { - $this->entityCache[$entity->type] = []; - } - - $this->entityCache[$entity->type][$entity->id] = $entity; - } - } - - /** - * Get a book via ID, Checks local cache. - */ - protected function getBook(int $bookId): SimpleEntityData - { - return $this->entityCache['book'][$bookId]; - } - - /** - * Get a chapter via ID, Checks local cache. - */ - protected function getChapter(int $chapterId): SimpleEntityData - { - return $this->entityCache['chapter'][$chapterId]; - } - /** * Get a query for fetching a book with its children. */ @@ -214,13 +175,7 @@ class JointPermissionBuilder $simpleEntities = []; foreach ($entities as $entity) { - $attrs = $entity->getAttributes(); - $simple = new SimpleEntityData(); - $simple->id = $attrs['id']; - $simple->type = $entity->getMorphClass(); - $simple->owned_by = $attrs['owned_by'] ?? 0; - $simple->book_id = $attrs['book_id'] ?? null; - $simple->chapter_id = $attrs['chapter_id'] ?? null; + $simple = SimpleEntityData::fromEntity($entity); $simpleEntities[] = $simple; } @@ -236,18 +191,10 @@ class JointPermissionBuilder protected function createManyJointPermissions(array $originalEntities, array $roles) { $entities = $this->entitiesToSimpleEntities($originalEntities); - $this->readyEntityCache($entities); $jointPermissions = []; // Fetch related entity permissions - $permissions = $this->getEntityPermissionsForEntities($entities); - - // Create a mapping of explicit entity permissions - $permissionMap = []; - foreach ($permissions as $permission) { - $key = $permission->entity_type . ':' . $permission->entity_id . ':' . $permission->role_id; - $permissionMap[$key] = $permission->view; - } + $permissions = new MassEntityPermissionEvaluator($entities, 'view'); // Create a mapping of role permissions $rolePermissionMap = []; @@ -260,13 +207,14 @@ class JointPermissionBuilder // Create Joint Permission Data foreach ($entities as $entity) { foreach ($roles as $role) { - $jointPermissions[] = $this->createJointPermissionData( + $jp = $this->createJointPermissionData( $entity, $role->getRawAttribute('id'), - $permissionMap, + $permissions, $rolePermissionMap, $role->system_name === 'admin' ); + $jointPermissions[] = $jp; } } @@ -300,94 +248,28 @@ class JointPermissionBuilder return $idsByType; } - /** - * Get the entity permissions for all the given entities. - * - * @param SimpleEntityData[] $entities - * - * @return EntityPermission[] - */ - protected function getEntityPermissionsForEntities(array $entities): array - { - $idsByType = $this->entitiesToTypeIdMap($entities); - $permissionFetch = EntityPermission::query() - ->where(function (Builder $query) use ($idsByType) { - foreach ($idsByType as $type => $ids) { - $query->orWhere(function (Builder $query) use ($type, $ids) { - $query->where('entity_type', '=', $type)->whereIn('entity_id', $ids); - }); - } - }); - - return $permissionFetch->get()->all(); - } - /** * Create entity permission data for an entity and role * for a particular action. */ - protected function createJointPermissionData(SimpleEntityData $entity, int $roleId, array $permissionMap, array $rolePermissionMap, bool $isAdminRole): array + protected function createJointPermissionData(SimpleEntityData $entity, int $roleId, MassEntityPermissionEvaluator $permissionMap, array $rolePermissionMap, bool $isAdminRole): array { - $permissionPrefix = $entity->type . '-view'; - $roleHasPermission = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-all']); - $roleHasPermissionOwn = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-own']); - + // Ensure system admin role retains permissions if ($isAdminRole) { return $this->createJointPermissionDataArray($entity, $roleId, true, true); } - if ($this->entityPermissionsActiveForRole($permissionMap, $entity, $roleId)) { - $hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $roleId); - - return $this->createJointPermissionDataArray($entity, $roleId, $hasAccess, $hasAccess); + // Return evaluated entity permission status if it has an affect. + $entityPermissionStatus = $permissionMap->evaluateEntityForRole($entity, $roleId); + if ($entityPermissionStatus !== null) { + return $this->createJointPermissionDataArray($entity, $roleId, $entityPermissionStatus, $entityPermissionStatus); } - if ($entity->type === 'book' || $entity->type === 'bookshelf') { - return $this->createJointPermissionDataArray($entity, $roleId, $roleHasPermission, $roleHasPermissionOwn); - } - - // For chapters and pages, Check if explicit permissions are set on the Book. - $book = $this->getBook($entity->book_id); - $hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $book, $roleId); - $hasPermissiveAccessToParents = !$this->entityPermissionsActiveForRole($permissionMap, $book, $roleId); - - // For pages with a chapter, Check if explicit permissions are set on the Chapter - if ($entity->type === 'page' && $entity->chapter_id !== 0) { - $chapter = $this->getChapter($entity->chapter_id); - $chapterRestricted = $this->entityPermissionsActiveForRole($permissionMap, $chapter, $roleId); - $hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapterRestricted; - if ($chapterRestricted) { - $hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $chapter, $roleId); - } - } - - return $this->createJointPermissionDataArray( - $entity, - $roleId, - ($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)), - ($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents)) - ); - } - - /** - * Check if entity permissions are defined within the given map, for the given entity and role. - * Checks for the default `role_id=0` backup option as a fallback. - */ - protected function entityPermissionsActiveForRole(array $permissionMap, SimpleEntityData $entity, int $roleId): bool - { - $keyPrefix = $entity->type . ':' . $entity->id . ':'; - return isset($permissionMap[$keyPrefix . $roleId]) || isset($permissionMap[$keyPrefix . '0']); - } - - /** - * Check for an active restriction in an entity map. - */ - protected function mapHasActiveRestriction(array $entityMap, SimpleEntityData $entity, int $roleId): bool - { - $roleKey = $entity->type . ':' . $entity->id . ':' . $roleId; - $defaultKey = $entity->type . ':' . $entity->id . ':0'; - - return $entityMap[$roleKey] ?? $entityMap[$defaultKey] ?? false; + // Otherwise default to the role-level permissions + $permissionPrefix = $entity->type . '-view'; + $roleHasPermission = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-all']); + $roleHasPermissionOwn = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-own']); + return $this->createJointPermissionDataArray($entity, $roleId, $roleHasPermission, $roleHasPermissionOwn); } /** diff --git a/app/Auth/Permissions/MassEntityPermissionEvaluator.php b/app/Auth/Permissions/MassEntityPermissionEvaluator.php new file mode 100644 index 000000000..1bd2ec44a --- /dev/null +++ b/app/Auth/Permissions/MassEntityPermissionEvaluator.php @@ -0,0 +1,81 @@ +entitiesInvolved = $entitiesInvolved; + parent::__construct($action); + } + + public function evaluateEntityForRole(SimpleEntityData $entity, int $roleId): ?bool + { + $typeIdChain = $this->gatherEntityChainTypeIds($entity); + $relevantPermissions = $this->getPermissionMapByTypeIdForChainAndRole($typeIdChain, $roleId); + $permitsByType = $this->collapseAndCategorisePermissions($typeIdChain, $relevantPermissions); + + return $this->evaluatePermitsByType($permitsByType); + } + + /** + * @param string[] $typeIdChain + * @return array + */ + protected function getPermissionMapByTypeIdForChainAndRole(array $typeIdChain, int $roleId): array + { + $allPermissions = $this->getPermissionMapByTypeIdAndRoleForAllInvolved(); + $relevantPermissions = []; + + // Filter down permissions to just those for current typeId + // and current roleID or fallback permissions. + foreach ($typeIdChain as $typeId) { + $relevantPermissions[$typeId] = [ + ...($allPermissions[$typeId][$roleId] ?? []), + ...($allPermissions[$typeId][0] ?? []) + ]; + } + + return $relevantPermissions; + } + + /** + * @return array> + */ + protected function getPermissionMapByTypeIdAndRoleForAllInvolved(): array + { + if (isset($this->permissionMapCache)) { + return $this->permissionMapCache; + } + + $entityTypeIdChain = []; + foreach ($this->entitiesInvolved as $entity) { + $entityTypeIdChain[] = $entity->type . ':' . $entity->id; + } + + $permissionMap = $this->getPermissionsMapByTypeId($entityTypeIdChain, []); + + // Manipulate permission map to also be keyed by roleId. + foreach ($permissionMap as $typeId => $permissions) { + $permissionMap[$typeId] = []; + foreach ($permissions as $permission) { + $roleId = $permission->getRawAttribute('role_id'); + if (!isset($permissionMap[$typeId][$roleId])) { + $permissionMap[$typeId][$roleId] = []; + } + $permissionMap[$typeId][$roleId][] = $permission; + } + } + + $this->permissionMapCache = $permissionMap; + + return $this->permissionMapCache; + } +} diff --git a/app/Auth/Permissions/PermissionApplicator.php b/app/Auth/Permissions/PermissionApplicator.php index 3855a283b..5326cc340 100644 --- a/app/Auth/Permissions/PermissionApplicator.php +++ b/app/Auth/Permissions/PermissionApplicator.php @@ -47,7 +47,7 @@ class PermissionApplicator return $hasRolePermission; } - $hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $user->id, $userRoleIds, $action); + $hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $action); return is_null($hasApplicableEntityPermissions) ? $hasRolePermission : $hasApplicableEntityPermissions; } @@ -56,11 +56,11 @@ class PermissionApplicator * Check if there are permissions that are applicable for the given entity item, action and roles. * Returns null when no entity permissions are in force. */ - protected function hasEntityPermission(Entity $entity, int $userId, array $userRoleIds, string $action): ?bool + protected function hasEntityPermission(Entity $entity, array $userRoleIds, string $action): ?bool { $this->ensureValidEntityAction($action); - return (new EntityPermissionEvaluator($entity, $userId, $userRoleIds, $action))->evaluate(); + return (new EntityPermissionEvaluator($action))->evaluateEntityForUser($entity, $userRoleIds); } /** diff --git a/app/Auth/Permissions/SimpleEntityData.php b/app/Auth/Permissions/SimpleEntityData.php index 62f5984f8..2128451fe 100644 --- a/app/Auth/Permissions/SimpleEntityData.php +++ b/app/Auth/Permissions/SimpleEntityData.php @@ -2,6 +2,8 @@ namespace BookStack\Auth\Permissions; +use BookStack\Entities\Models\Entity; + class SimpleEntityData { public int $id; @@ -9,4 +11,18 @@ class SimpleEntityData public int $owned_by; public ?int $book_id; public ?int $chapter_id; + + public static function fromEntity(Entity $entity): self + { + $attrs = $entity->getAttributes(); + $simple = new self(); + + $simple->id = $attrs['id']; + $simple->type = $entity->getMorphClass(); + $simple->owned_by = $attrs['owned_by'] ?? 0; + $simple->book_id = $attrs['book_id'] ?? null; + $simple->chapter_id = $attrs['chapter_id'] ?? null; + + return $simple; + } } From 7d74575eb86d9049cd420af6f0f1218b955dcfd4 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 24 Jan 2023 13:44:38 +0000 Subject: [PATCH 021/477] Found a sql having-style approach to permissions As a way to check aggregate queries for required changes to need to analyse across combined permission values. --- app/Auth/Permissions/PermissionApplicator.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/Auth/Permissions/PermissionApplicator.php b/app/Auth/Permissions/PermissionApplicator.php index 5326cc340..e4564ddf5 100644 --- a/app/Auth/Permissions/PermissionApplicator.php +++ b/app/Auth/Permissions/PermissionApplicator.php @@ -94,10 +94,14 @@ class PermissionApplicator { return $query->where(function (Builder $parentQuery) { $parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) { - $permissionQuery->whereIn('role_id', $this->getCurrentUserRoleIds()) - ->where(function (Builder $query) { - $this->addJointHasPermissionCheck($query, $this->currentUser()->id); - }); + $permissionQuery->select(['entity_id', 'entity_type']) + ->selectRaw('max(owned_by) as owned_by') + ->selectRaw('max(has_permission) as has_permission') + ->selectRaw('max(has_permission_own) as has_permission_own') + ->whereIn('role_id', $this->getCurrentUserRoleIds()) + ->groupBy(['entity_type', 'entity_id']) + ->havingRaw('has_permission > 0') + ->orHavingRaw('(has_permission_own > 0 and owned_by = ?)', [$this->currentUser()->id]); }); }); } From 2d1f1abce4a6372b6be1833d88354149cbc7e40c Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 24 Jan 2023 14:55:34 +0000 Subject: [PATCH 022/477] Implemented alternate approach to current joint_permissions Is a tweak upon the existing approach, mainly to store and query role permission access in a way that allows muli-level states that may override eachother. These states are represented in the new PermissionStatus class. This also simplifies how own permissions are stored and queried, to be part of a single column. --- app/Actions/Activity.php | 8 +++ app/Actions/Favourite.php | 8 +++ app/Actions/Tag.php | 8 +++ app/Actions/View.php | 8 +++ .../Permissions/JointPermissionBuilder.php | 23 ++++---- app/Auth/Permissions/PermissionApplicator.php | 33 ++++-------- app/Auth/Permissions/PermissionStatus.php | 11 ++++ app/References/Reference.php | 8 +++ app/References/ReferenceFetcher.php | 17 +++--- ...625_refactor_joint_permissions_storage.php | 52 +++++++++++++++++++ 10 files changed, 137 insertions(+), 39 deletions(-) create mode 100644 app/Auth/Permissions/PermissionStatus.php create mode 100644 database/migrations/2023_01_24_104625_refactor_joint_permissions_storage.php diff --git a/app/Actions/Activity.php b/app/Actions/Activity.php index 3b1408cb9..0789fe123 100644 --- a/app/Actions/Activity.php +++ b/app/Actions/Activity.php @@ -2,10 +2,12 @@ namespace BookStack\Actions; +use BookStack\Auth\Permissions\JointPermission; use BookStack\Auth\User; use BookStack\Entities\Models\Entity; use BookStack\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Support\Str; @@ -40,6 +42,12 @@ class Activity extends Model return $this->belongsTo(User::class); } + public function jointPermissions(): HasMany + { + return $this->hasMany(JointPermission::class, 'entity_id', 'entity_id') + ->whereColumn('activities.entity_type', '=', 'joint_permissions.entity_type'); + } + /** * Returns text from the language files, Looks up by using the activity key. */ diff --git a/app/Actions/Favourite.php b/app/Actions/Favourite.php index f45894182..c5d12a151 100644 --- a/app/Actions/Favourite.php +++ b/app/Actions/Favourite.php @@ -2,7 +2,9 @@ namespace BookStack\Actions; +use BookStack\Auth\Permissions\JointPermission; use BookStack\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphTo; class Favourite extends Model @@ -16,4 +18,10 @@ class Favourite extends Model { return $this->morphTo(); } + + public function jointPermissions(): HasMany + { + return $this->hasMany(JointPermission::class, 'entity_id', 'favouritable_id') + ->whereColumn('favourites.favouritable_type', '=', 'joint_permissions.entity_type'); + } } diff --git a/app/Actions/Tag.php b/app/Actions/Tag.php index 609c299ad..e173faea0 100644 --- a/app/Actions/Tag.php +++ b/app/Actions/Tag.php @@ -2,8 +2,10 @@ namespace BookStack\Actions; +use BookStack\Auth\Permissions\JointPermission; use BookStack\Model; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphTo; /** @@ -27,6 +29,12 @@ class Tag extends Model return $this->morphTo('entity'); } + public function jointPermissions(): HasMany + { + return $this->hasMany(JointPermission::class, 'entity_id', 'entity_id') + ->whereColumn('tags.entity_type', '=', 'joint_permissions.entity_type'); + } + /** * Get a full URL to start a tag name search for this tag name. */ diff --git a/app/Actions/View.php b/app/Actions/View.php index 16961bd91..706467133 100644 --- a/app/Actions/View.php +++ b/app/Actions/View.php @@ -2,8 +2,10 @@ namespace BookStack\Actions; +use BookStack\Auth\Permissions\JointPermission; use BookStack\Interfaces\Viewable; use BookStack\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphTo; /** @@ -28,6 +30,12 @@ class View extends Model return $this->morphTo(); } + public function jointPermissions(): HasMany + { + return $this->hasMany(JointPermission::class, 'entity_id', 'viewable_id') + ->whereColumn('views.viewable_type', '=', 'joint_permissions.entity_type'); + } + /** * Increment the current user's view count for the given viewable model. */ diff --git a/app/Auth/Permissions/JointPermissionBuilder.php b/app/Auth/Permissions/JointPermissionBuilder.php index 91207e3ab..bbdf4d6f8 100644 --- a/app/Auth/Permissions/JointPermissionBuilder.php +++ b/app/Auth/Permissions/JointPermissionBuilder.php @@ -256,35 +256,38 @@ class JointPermissionBuilder { // Ensure system admin role retains permissions if ($isAdminRole) { - return $this->createJointPermissionDataArray($entity, $roleId, true, true); + return $this->createJointPermissionDataArray($entity, $roleId, PermissionStatus::EXPLICIT_ALLOW, true); } // Return evaluated entity permission status if it has an affect. $entityPermissionStatus = $permissionMap->evaluateEntityForRole($entity, $roleId); if ($entityPermissionStatus !== null) { - return $this->createJointPermissionDataArray($entity, $roleId, $entityPermissionStatus, $entityPermissionStatus); + $status = $entityPermissionStatus ? PermissionStatus::EXPLICIT_ALLOW : PermissionStatus::EXPLICIT_DENY; + return $this->createJointPermissionDataArray($entity, $roleId, $status, $entityPermissionStatus); } // Otherwise default to the role-level permissions $permissionPrefix = $entity->type . '-view'; $roleHasPermission = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-all']); $roleHasPermissionOwn = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-own']); - return $this->createJointPermissionDataArray($entity, $roleId, $roleHasPermission, $roleHasPermissionOwn); + $status = $roleHasPermission ? PermissionStatus::IMPLICIT_ALLOW : PermissionStatus::IMPLICIT_DENY; + return $this->createJointPermissionDataArray($entity, $roleId, $status, $roleHasPermissionOwn); } /** * Create an array of data with the information of an entity jointPermissions. * Used to build data for bulk insertion. */ - protected function createJointPermissionDataArray(SimpleEntityData $entity, int $roleId, bool $permissionAll, bool $permissionOwn): array + protected function createJointPermissionDataArray(SimpleEntityData $entity, int $roleId, int $permissionStatus, bool $hasPermissionOwn): array { + $ownPermissionActive = ($hasPermissionOwn && $permissionStatus !== PermissionStatus::EXPLICIT_DENY && $entity->owned_by); + return [ - 'entity_id' => $entity->id, - 'entity_type' => $entity->type, - 'has_permission' => $permissionAll, - 'has_permission_own' => $permissionOwn, - 'owned_by' => $entity->owned_by, - 'role_id' => $roleId, + 'entity_id' => $entity->id, + 'entity_type' => $entity->type, + 'role_id' => $roleId, + 'status' => $permissionStatus, + 'owner_id' => $ownPermissionActive ? $entity->owned_by : null, ]; } } diff --git a/app/Auth/Permissions/PermissionApplicator.php b/app/Auth/Permissions/PermissionApplicator.php index e4564ddf5..4f95465af 100644 --- a/app/Auth/Permissions/PermissionApplicator.php +++ b/app/Auth/Permissions/PermissionApplicator.php @@ -95,13 +95,11 @@ class PermissionApplicator return $query->where(function (Builder $parentQuery) { $parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) { $permissionQuery->select(['entity_id', 'entity_type']) - ->selectRaw('max(owned_by) as owned_by') - ->selectRaw('max(has_permission) as has_permission') - ->selectRaw('max(has_permission_own) as has_permission_own') + ->selectRaw('max(owner_id) as owner_id') + ->selectRaw('max(status) as status') ->whereIn('role_id', $this->getCurrentUserRoleIds()) ->groupBy(['entity_type', 'entity_id']) - ->havingRaw('has_permission > 0') - ->orHavingRaw('(has_permission_own > 0 and owned_by = ?)', [$this->currentUser()->id]); + ->havingRaw('(status IN (1, 3) or owner_id = ?)', [$this->currentUser()->id]); }); }); } @@ -125,35 +123,23 @@ class PermissionApplicator * Filter items that have entities set as a polymorphic relation. * For simplicity, this will not return results attached to draft pages. * Draft pages should never really have related items though. - * - * @param Builder|QueryBuilder $query */ - public function restrictEntityRelationQuery($query, string $tableName, string $entityIdColumn, string $entityTypeColumn) + public function restrictEntityRelationQuery(Builder $query, string $tableName, string $entityIdColumn, string $entityTypeColumn): Builder { $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn]; $pageMorphClass = (new Page())->getMorphClass(); - $q = $query->whereExists(function ($permissionQuery) use (&$tableDetails) { - /** @var Builder $permissionQuery */ - $permissionQuery->select(['role_id'])->from('joint_permissions') - ->whereColumn('joint_permissions.entity_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) - ->whereColumn('joint_permissions.entity_type', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn']) - ->whereIn('joint_permissions.role_id', $this->getCurrentUserRoleIds()) - ->where(function (QueryBuilder $query) { - $this->addJointHasPermissionCheck($query, $this->currentUser()->id); - }); - })->where(function ($query) use ($tableDetails, $pageMorphClass) { - /** @var Builder $query */ - $query->where($tableDetails['entityTypeColumn'], '!=', $pageMorphClass) + return $this->restrictEntityQuery($query) + ->where(function ($query) use ($tableDetails, $pageMorphClass) { + /** @var Builder $query */ + $query->where($tableDetails['entityTypeColumn'], '!=', $pageMorphClass) ->orWhereExists(function (QueryBuilder $query) use ($tableDetails, $pageMorphClass) { $query->select('id')->from('pages') ->whereColumn('pages.id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) ->where($tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'], '=', $pageMorphClass) ->where('pages.draft', '=', false); }); - }); - - return $q; + }); } /** @@ -164,6 +150,7 @@ class PermissionApplicator */ public function restrictPageRelationQuery(Builder $query, string $tableName, string $pageIdColumn): Builder { + // TODO - Refactor $fullPageIdColumn = $tableName . '.' . $pageIdColumn; $morphClass = (new Page())->getMorphClass(); diff --git a/app/Auth/Permissions/PermissionStatus.php b/app/Auth/Permissions/PermissionStatus.php new file mode 100644 index 000000000..f8e55c20b --- /dev/null +++ b/app/Auth/Permissions/PermissionStatus.php @@ -0,0 +1,11 @@ +morphTo('to'); } + + public function jointPermissions(): HasMany + { + return $this->hasMany(JointPermission::class, 'entity_id', 'from_id') + ->whereColumn('references.from_type', '=', 'joint_permissions.entity_type'); + } } diff --git a/app/References/ReferenceFetcher.php b/app/References/ReferenceFetcher.php index a73463a95..415791857 100644 --- a/app/References/ReferenceFetcher.php +++ b/app/References/ReferenceFetcher.php @@ -5,6 +5,7 @@ namespace BookStack\References; use BookStack\Auth\Permissions\PermissionApplicator; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Relations\Relation; @@ -23,8 +24,7 @@ class ReferenceFetcher */ public function getPageReferencesToEntity(Entity $entity): Collection { - $baseQuery = $entity->referencesTo() - ->where('from_type', '=', (new Page())->getMorphClass()) + $baseQuery = $this->queryPageReferencesToEntity($entity) ->with([ 'from' => fn (Relation $query) => $query->select(Page::$listAttributes), 'from.book' => fn (Relation $query) => $query->scopes('visible'), @@ -47,11 +47,8 @@ class ReferenceFetcher */ public function getPageReferenceCountToEntity(Entity $entity): int { - $baseQuery = $entity->referencesTo() - ->where('from_type', '=', (new Page())->getMorphClass()); - $count = $this->permissions->restrictEntityRelationQuery( - $baseQuery, + $this->queryPageReferencesToEntity($entity), 'references', 'from_id', 'from_type' @@ -59,4 +56,12 @@ class ReferenceFetcher return $count; } + + protected function queryPageReferencesToEntity(Entity $entity): Builder + { + return Reference::query() + ->where('to_type', '=', $entity->getMorphClass()) + ->where('to_id', '=', $entity->id) + ->where('from_type', '=', (new Page())->getMorphClass()); + } } diff --git a/database/migrations/2023_01_24_104625_refactor_joint_permissions_storage.php b/database/migrations/2023_01_24_104625_refactor_joint_permissions_storage.php new file mode 100644 index 000000000..49ebf5c5a --- /dev/null +++ b/database/migrations/2023_01_24_104625_refactor_joint_permissions_storage.php @@ -0,0 +1,52 @@ +truncate(); + + if (Schema::hasColumn('joint_permissions', 'owned_by')) { + Schema::table('joint_permissions', function (Blueprint $table) { + $table->dropColumn(['has_permission', 'has_permission_own', 'owned_by']); + + $table->unsignedTinyInteger('status')->index(); + $table->unsignedInteger('owner_id')->nullable()->index(); + }); + } + + // Rebuild permissions + app(JointPermissionBuilder::class)->rebuildForAll(); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + DB::table('joint_permissions')->truncate(); + + Schema::table('joint_permissions', function (Blueprint $table) { + $table->dropColumn(['status', 'owner_id']); + + $table->boolean('has_permission')->index(); + $table->boolean('has_permission_own')->index(); + $table->unsignedInteger('created_by')->index(); + }); + } +} From 1660e72cc5f3420bb704a8f159a94f0632d0b25c Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 24 Jan 2023 19:04:32 +0000 Subject: [PATCH 023/477] Migrated remaining relation permission usages Now all tests are passing. Some level of manual checks to do. --- app/Auth/Permissions/PermissionApplicator.php | 51 +++---------------- app/Auth/User.php | 1 + app/Uploads/Attachment.php | 8 +++ app/Uploads/Image.php | 8 +++ .../RegeneratePermissionsCommandTest.php | 2 +- tests/Permissions/EntityPermissionsTest.php | 5 +- 6 files changed, 29 insertions(+), 46 deletions(-) diff --git a/app/Auth/Permissions/PermissionApplicator.php b/app/Auth/Permissions/PermissionApplicator.php index 4f95465af..437ddb0fb 100644 --- a/app/Auth/Permissions/PermissionApplicator.php +++ b/app/Auth/Permissions/PermissionApplicator.php @@ -150,51 +150,16 @@ class PermissionApplicator */ public function restrictPageRelationQuery(Builder $query, string $tableName, string $pageIdColumn): Builder { - // TODO - Refactor $fullPageIdColumn = $tableName . '.' . $pageIdColumn; - $morphClass = (new Page())->getMorphClass(); - - $existsQuery = function ($permissionQuery) use ($fullPageIdColumn, $morphClass) { - /** @var Builder $permissionQuery */ - $permissionQuery->select('joint_permissions.role_id')->from('joint_permissions') - ->whereColumn('joint_permissions.entity_id', '=', $fullPageIdColumn) - ->where('joint_permissions.entity_type', '=', $morphClass) - ->whereIn('joint_permissions.role_id', $this->getCurrentUserRoleIds()) - ->where(function (QueryBuilder $query) { - $this->addJointHasPermissionCheck($query, $this->currentUser()->id); + return $this->restrictEntityQuery($query) + ->where(function ($query) use ($fullPageIdColumn) { + /** @var Builder $query */ + $query->whereExists(function (QueryBuilder $query) use ($fullPageIdColumn) { + $query->select('id')->from('pages') + ->whereColumn('pages.id', '=', $fullPageIdColumn) + ->where('pages.draft', '=', false); }); - }; - - $q = $query->where(function ($query) use ($existsQuery, $fullPageIdColumn) { - $query->whereExists($existsQuery) - ->orWhere($fullPageIdColumn, '=', 0); - }); - - // Prevent visibility of non-owned draft pages - $q->whereExists(function (QueryBuilder $query) use ($fullPageIdColumn) { - $query->select('id')->from('pages') - ->whereColumn('pages.id', '=', $fullPageIdColumn) - ->where(function (QueryBuilder $query) { - $query->where('pages.draft', '=', false) - ->orWhere('pages.owned_by', '=', $this->currentUser()->id); - }); - }); - - return $q; - } - - /** - * Add the query for checking the given user id has permission - * within the join_permissions table. - * - * @param QueryBuilder|Builder $query - */ - protected function addJointHasPermissionCheck($query, int $userIdToCheck) - { - $query->where('joint_permissions.has_permission', '=', true)->orWhere(function ($query) use ($userIdToCheck) { - $query->where('joint_permissions.has_permission_own', '=', true) - ->where('joint_permissions.owned_by', '=', $userIdToCheck); - }); + }); } /** diff --git a/app/Auth/User.php b/app/Auth/User.php index 6e66bc808..cf9f20e52 100644 --- a/app/Auth/User.php +++ b/app/Auth/User.php @@ -200,6 +200,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon public function attachRole(Role $role) { $this->roles()->attach($role->id); + $this->unsetRelation('roles'); } /** diff --git a/app/Uploads/Attachment.php b/app/Uploads/Attachment.php index 6c7066ff9..fc86d36ea 100644 --- a/app/Uploads/Attachment.php +++ b/app/Uploads/Attachment.php @@ -2,6 +2,7 @@ namespace BookStack\Uploads; +use BookStack\Auth\Permissions\JointPermission; use BookStack\Auth\Permissions\PermissionApplicator; use BookStack\Auth\User; use BookStack\Entities\Models\Entity; @@ -10,6 +11,7 @@ use BookStack\Model; use BookStack\Traits\HasCreatorAndUpdater; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; /** * @property int $id @@ -56,6 +58,12 @@ class Attachment extends Model return $this->belongsTo(Page::class, 'uploaded_to'); } + public function jointPermissions(): HasMany + { + return $this->hasMany(JointPermission::class, 'entity_id', 'uploaded_to') + ->where('joint_permissions.entity_type', '=', 'page'); + } + /** * Get the url of this file. */ diff --git a/app/Uploads/Image.php b/app/Uploads/Image.php index bdf10f080..c21a3b03f 100644 --- a/app/Uploads/Image.php +++ b/app/Uploads/Image.php @@ -2,10 +2,12 @@ namespace BookStack\Uploads; +use BookStack\Auth\Permissions\JointPermission; use BookStack\Entities\Models\Page; use BookStack\Model; use BookStack\Traits\HasCreatorAndUpdater; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\HasMany; /** * @property int $id @@ -25,6 +27,12 @@ class Image extends Model protected $fillable = ['name']; protected $hidden = []; + public function jointPermissions(): HasMany + { + return $this->hasMany(JointPermission::class, 'entity_id', 'uploaded_to') + ->where('joint_permissions.entity_type', '=', 'page'); + } + /** * Get a thumbnail for this image. * diff --git a/tests/Commands/RegeneratePermissionsCommandTest.php b/tests/Commands/RegeneratePermissionsCommandTest.php index b916a8060..9cf7dec93 100644 --- a/tests/Commands/RegeneratePermissionsCommandTest.php +++ b/tests/Commands/RegeneratePermissionsCommandTest.php @@ -29,7 +29,7 @@ class RegeneratePermissionsCommandTest extends TestCase 'entity_id' => $page->id, 'entity_type' => 'page', 'role_id' => $role->id, - 'has_permission' => 1, + 'status' => 3, // Explicit allow ]); $page->permissions()->delete(); diff --git a/tests/Permissions/EntityPermissionsTest.php b/tests/Permissions/EntityPermissionsTest.php index ab8b1242d..99a8bd88c 100644 --- a/tests/Permissions/EntityPermissionsTest.php +++ b/tests/Permissions/EntityPermissionsTest.php @@ -663,7 +663,7 @@ class EntityPermissionsTest extends TestCase $chapter = $this->entities->chapter(); $book = $chapter->book; - $this->permissions->setEntityPermissions($book, ['edit'], [$viewerRole], false); + $this->permissions->setEntityPermissions($book, ['update'], [$viewerRole], false); $this->permissions->setEntityPermissions($chapter, [], [$viewerRole], true); $this->assertFalse(userCan('chapter-update', $chapter)); @@ -678,9 +678,10 @@ class EntityPermissionsTest extends TestCase $chapter = $this->entities->chapter(); $book = $chapter->book; - $this->permissions->setEntityPermissions($book, ['edit'], [$editorRole], false); + $this->permissions->setEntityPermissions($book, ['update'], [$editorRole], false); $this->permissions->setEntityPermissions($chapter, [], [$viewerRole], true); + $this->actingAs($user); $this->assertTrue(userCan('chapter-update', $chapter)); } From d1bd6d0e3919ccdf22c74ebf29282475288132bd Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 24 Jan 2023 19:21:23 +0000 Subject: [PATCH 024/477] Fixed incorrect field in down migration --- .../2023_01_24_104625_refactor_joint_permissions_storage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/2023_01_24_104625_refactor_joint_permissions_storage.php b/database/migrations/2023_01_24_104625_refactor_joint_permissions_storage.php index 49ebf5c5a..0f73f456b 100644 --- a/database/migrations/2023_01_24_104625_refactor_joint_permissions_storage.php +++ b/database/migrations/2023_01_24_104625_refactor_joint_permissions_storage.php @@ -46,7 +46,7 @@ class RefactorJointPermissionsStorage extends Migration $table->boolean('has_permission')->index(); $table->boolean('has_permission_own')->index(); - $table->unsignedInteger('created_by')->index(); + $table->unsignedInteger('owned_by')->index(); }); } } From 8be36455ab7007334ca26fff28ebb1a99886de65 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 24 Jan 2023 20:42:20 +0000 Subject: [PATCH 025/477] Addressed fallback override cases found during testing Had misalignment between query and usercan, The nuance between fallback and entity-role permissions was not taken into account by the query system. Now added with new test cases to cover. --- .../Permissions/EntityPermissionEvaluator.php | 10 +- .../Permissions/JointPermissionBuilder.php | 3 +- .../MassEntityPermissionEvaluator.php | 2 +- dev/docs/permission-scenario-testing.md | 82 +++++++++++++++- tests/Helpers/PermissionsProvider.php | 7 ++ .../Scenarios/EntityRolePermissionsTest.php | 97 ++++++++++++++++++- 6 files changed, 192 insertions(+), 9 deletions(-) diff --git a/app/Auth/Permissions/EntityPermissionEvaluator.php b/app/Auth/Permissions/EntityPermissionEvaluator.php index 99e87d769..f5e75be3e 100644 --- a/app/Auth/Permissions/EntityPermissionEvaluator.php +++ b/app/Auth/Permissions/EntityPermissionEvaluator.php @@ -25,22 +25,24 @@ class EntityPermissionEvaluator $relevantPermissions = $this->getPermissionsMapByTypeId($typeIdChain, [...$userRoleIds, 0]); $permitsByType = $this->collapseAndCategorisePermissions($typeIdChain, $relevantPermissions); - return $this->evaluatePermitsByType($permitsByType); + $status = $this->evaluatePermitsByType($permitsByType); + + return is_null($status) ? null : $status === PermissionStatus::IMPLICIT_ALLOW || $status === PermissionStatus::EXPLICIT_ALLOW; } /** * @param array> $permitsByType */ - protected function evaluatePermitsByType(array $permitsByType): ?bool + protected function evaluatePermitsByType(array $permitsByType): ?int { // Return grant or reject from role-level if exists if (count($permitsByType['role']) > 0) { - return boolval(max($permitsByType['role'])); + return max($permitsByType['role']) ? PermissionStatus::EXPLICIT_ALLOW : PermissionStatus::EXPLICIT_DENY; } // Return fallback permission if exists if (count($permitsByType['fallback']) > 0) { - return boolval($permitsByType['fallback'][0]); + return $permitsByType['fallback'][0] ? PermissionStatus::IMPLICIT_ALLOW : PermissionStatus::IMPLICIT_DENY; } return null; diff --git a/app/Auth/Permissions/JointPermissionBuilder.php b/app/Auth/Permissions/JointPermissionBuilder.php index bbdf4d6f8..4132a19af 100644 --- a/app/Auth/Permissions/JointPermissionBuilder.php +++ b/app/Auth/Permissions/JointPermissionBuilder.php @@ -262,8 +262,7 @@ class JointPermissionBuilder // Return evaluated entity permission status if it has an affect. $entityPermissionStatus = $permissionMap->evaluateEntityForRole($entity, $roleId); if ($entityPermissionStatus !== null) { - $status = $entityPermissionStatus ? PermissionStatus::EXPLICIT_ALLOW : PermissionStatus::EXPLICIT_DENY; - return $this->createJointPermissionDataArray($entity, $roleId, $status, $entityPermissionStatus); + return $this->createJointPermissionDataArray($entity, $roleId, $entityPermissionStatus, false); } // Otherwise default to the role-level permissions diff --git a/app/Auth/Permissions/MassEntityPermissionEvaluator.php b/app/Auth/Permissions/MassEntityPermissionEvaluator.php index 1bd2ec44a..a9deba16d 100644 --- a/app/Auth/Permissions/MassEntityPermissionEvaluator.php +++ b/app/Auth/Permissions/MassEntityPermissionEvaluator.php @@ -16,7 +16,7 @@ class MassEntityPermissionEvaluator extends EntityPermissionEvaluator parent::__construct($action); } - public function evaluateEntityForRole(SimpleEntityData $entity, int $roleId): ?bool + public function evaluateEntityForRole(SimpleEntityData $entity, int $roleId): ?int { $typeIdChain = $this->gatherEntityChainTypeIds($entity); $relevantPermissions = $this->getPermissionMapByTypeIdForChainAndRole($typeIdChain, $roleId); diff --git a/dev/docs/permission-scenario-testing.md b/dev/docs/permission-scenario-testing.md index e738fe972..54b1bcfe1 100644 --- a/dev/docs/permission-scenario-testing.md +++ b/dev/docs/permission-scenario-testing.md @@ -229,7 +229,7 @@ User denied page permission. User denied page permission. -#### test_80_multi_role_inherited_deny_via_parent +#### test_75_multi_role_inherited_deny_via_parent - Page permissions have inherit enabled. - Chapter permissions have inherit enabled. @@ -238,3 +238,83 @@ User denied page permission. - User has Role A & B. User denied page permission. + +#### test_80_fallback_override_allow + +- Page permissions have inherit disabled. +- Page fallback has entity deny permission. +- Role A has entity allow page permission. +- User has Role A. + +User granted page permission. + +#### test_81_fallback_override_deny + +- Page permissions have inherit disabled. +- Page fallback has entity allow permission. +- Role A has entity deny page permission. +- User has Role A. + +User denied page permission. + +#### test_84_fallback_override_allow_multi_role + +- Page permissions have inherit disabled. +- Page fallback has entity deny permission. +- Role A has entity allow page permission. +- Role B has no entity page permissions. +- User has Role A & B. + +User granted page permission. + +#### test_85_fallback_override_deny_multi_role + +- Page permissions have inherit disabled. +- Page fallback has entity allow permission. +- Role A has entity deny page permission. +- Role B has no entity page permissions. +- User has Role A & B. + +User denied page permission. + +#### test_86_fallback_override_allow_inherit + +- Chapter permissions have inherit disabled. +- Page permissions have inherit enabled. +- Chapter fallback has entity deny permission. +- Role A has entity allow chapter permission. +- User has Role A. + +User granted page permission. + +#### test_87_fallback_override_deny_inherit + +- Chapter permissions have inherit disabled. +- Page permissions have inherit enabled. +- Chapter fallback has entity allow permission. +- Role A has entity deny chapter permission. +- User has Role A. + +User denied page permission. + +#### test_88_fallback_override_allow_multi_role_inherit + +- Chapter permissions have inherit disabled. +- Page permissions have inherit enabled. +- Chapter fallback has entity deny permission. +- Role A has entity allow chapter permission. +- Role B has no entity chapter permissions. +- User has Role A & B. + +User granted page permission. + +#### test_89_fallback_override_deny_multi_role_inherit + +- Chapter permissions have inherit disabled. +- Page permissions have inherit enabled. +- Chapter fallback has entity allow permission. +- Role A has entity deny chapter permission. +- Role B has no entity chapter permissions. +- User has Role A & B. + +User denied page permission. \ No newline at end of file diff --git a/tests/Helpers/PermissionsProvider.php b/tests/Helpers/PermissionsProvider.php index 2cbfb1af5..b93c45e25 100644 --- a/tests/Helpers/PermissionsProvider.php +++ b/tests/Helpers/PermissionsProvider.php @@ -101,6 +101,13 @@ class PermissionsProvider $this->addEntityPermissionEntries($entity, [$permissionData]); } + public function setFallbackPermissions(Entity $entity, array $actionList) + { + $entity->permissions()->where('role_id', '=', 0)->delete(); + $permissionData = $this->actionListToEntityPermissionData($actionList, 0); + $this->addEntityPermissionEntries($entity, [$permissionData]); + } + /** * Disable inherited permissions on the given entity. * Effectively sets the "Other Users" UI permission option to not inherit, with no permissions. diff --git a/tests/Permissions/Scenarios/EntityRolePermissionsTest.php b/tests/Permissions/Scenarios/EntityRolePermissionsTest.php index b92ce620b..c8f1401e7 100644 --- a/tests/Permissions/Scenarios/EntityRolePermissionsTest.php +++ b/tests/Permissions/Scenarios/EntityRolePermissionsTest.php @@ -187,7 +187,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $this->assertNotVisibleToUser($page, $user); } - public function test_80_multi_role_inherited_deny_via_parent() + public function test_75_multi_role_inherited_deny_via_parent() { [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']); $roleB = $this->users->attachNewRole($user); @@ -198,4 +198,99 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $this->assertNotVisibleToUser($page, $user); } + + public function test_80_fallback_override_allow() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $page = $this->entities->page(); + + $this->permissions->setFallbackPermissions($page, []); + $this->permissions->addEntityPermission($page, ['view'], $roleA); + + $this->assertVisibleToUser($page, $user); + } + public function test_81_fallback_override_deny() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $page = $this->entities->page(); + + $this->permissions->setFallbackPermissions($page, ['view']); + $this->permissions->addEntityPermission($page, [], $roleA); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_84_fallback_override_allow_multi_role() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $roleB = $this->users->attachNewRole($user); + $page = $this->entities->page(); + + $this->permissions->setFallbackPermissions($page, []); + $this->permissions->addEntityPermission($page, ['view'], $roleA); + + $this->assertVisibleToUser($page, $user); + } + + public function test_85_fallback_override_deny_multi_role() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $roleB = $this->users->attachNewRole($user); + $page = $this->entities->page(); + + $this->permissions->setFallbackPermissions($page, ['view']); + $this->permissions->addEntityPermission($page, [], $roleA); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_86_fallback_override_allow_inherit() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $page = $this->entities->page(); + $chapter = $page->chapter; + + $this->permissions->setFallbackPermissions($chapter, []); + $this->permissions->addEntityPermission($chapter, ['view'], $roleA); + + $this->assertVisibleToUser($page, $user); + } + + public function test_87_fallback_override_deny_inherit() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $page = $this->entities->page(); + $chapter = $page->chapter; + + $this->permissions->setFallbackPermissions($chapter, ['view']); + $this->permissions->addEntityPermission($chapter, [], $roleA); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_88_fallback_override_allow_multi_role_inherit() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $roleB = $this->users->attachNewRole($user); + $page = $this->entities->page(); + $chapter = $page->chapter; + + $this->permissions->setFallbackPermissions($chapter, []); + $this->permissions->addEntityPermission($chapter, ['view'], $roleA); + + $this->assertVisibleToUser($page, $user); + } + + public function test_89_fallback_override_deny_multi_role_inherit() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $roleB = $this->users->attachNewRole($user); + $page = $this->entities->page(); + $chapter = $page->chapter; + + $this->permissions->setFallbackPermissions($chapter, ['view']); + $this->permissions->addEntityPermission($chapter, [], $roleA); + + $this->assertNotVisibleToUser($page, $user); + } } From 1fa5a3196060092b4771c3d6b775f503800434d8 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 24 Jan 2023 21:26:41 +0000 Subject: [PATCH 026/477] Fixed role entity permissions ignoring inheritance Added additional scnenario tests to cover --- .../Permissions/EntityPermissionEvaluator.php | 4 +++ dev/docs/permission-scenario-testing.md | 23 ++++++++++++++++ .../Scenarios/EntityRolePermissionsTest.php | 27 +++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/app/Auth/Permissions/EntityPermissionEvaluator.php b/app/Auth/Permissions/EntityPermissionEvaluator.php index f5e75be3e..51db45bbc 100644 --- a/app/Auth/Permissions/EntityPermissionEvaluator.php +++ b/app/Auth/Permissions/EntityPermissionEvaluator.php @@ -66,6 +66,10 @@ class EntityPermissionEvaluator $permitsByType[$type][$roleId] = $permission->{$this->action}; } } + + if (isset($permitsByType['fallback'][0])) { + break; + } } return $permitsByType; diff --git a/dev/docs/permission-scenario-testing.md b/dev/docs/permission-scenario-testing.md index 54b1bcfe1..7a9cc1126 100644 --- a/dev/docs/permission-scenario-testing.md +++ b/dev/docs/permission-scenario-testing.md @@ -317,4 +317,27 @@ User granted page permission. - Role B has no entity chapter permissions. - User has Role A & B. +User denied page permission. + +#### test_90_fallback_overrides_parent_entity_role_deny + +- Chapter permissions have inherit disabled. +- Page permissions have inherit disabled. +- Chapter fallback has entity deny permission. +- Page fallback has entity deny permission. +- Role A has entity allow chapter permission. +- User has Role A. + +User denied page permission. + +#### test_91_fallback_overrides_parent_entity_role_inherit + +- Book permissions have inherit disabled. +- Chapter permissions have inherit disabled. +- Page permissions have inherit enabled. +- Book fallback has entity deny permission. +- Chapter fallback has entity deny permission. +- Role A has entity allow book permission. +- User has Role A. + User denied page permission. \ No newline at end of file diff --git a/tests/Permissions/Scenarios/EntityRolePermissionsTest.php b/tests/Permissions/Scenarios/EntityRolePermissionsTest.php index c8f1401e7..bd5b31fdc 100644 --- a/tests/Permissions/Scenarios/EntityRolePermissionsTest.php +++ b/tests/Permissions/Scenarios/EntityRolePermissionsTest.php @@ -293,4 +293,31 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $this->assertNotVisibleToUser($page, $user); } + + public function test_90_fallback_overrides_parent_entity_role_deny() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $page = $this->entities->page(); + $chapter = $page->chapter; + + $this->permissions->setFallbackPermissions($chapter, []); + $this->permissions->setFallbackPermissions($page, []); + $this->permissions->addEntityPermission($chapter, ['view'], $roleA); + + $this->assertNotVisibleToUser($page, $user); + } + + public function test_91_fallback_overrides_parent_entity_role_inherit() + { + [$user, $roleA] = $this->users->newUserWithRole(); + $page = $this->entities->page(); + $chapter = $page->chapter; + $book = $page->book; + + $this->permissions->setFallbackPermissions($book, []); + $this->permissions->setFallbackPermissions($chapter, []); + $this->permissions->addEntityPermission($book, ['view'], $roleA); + + $this->assertNotVisibleToUser($page, $user); + } } From 55b6a7842ed8eda41637d3a26f0f7f23c0866478 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 25 Jan 2023 11:03:19 +0000 Subject: [PATCH 027/477] Added ability to control app icon (favicon) via settings --- app/Http/Controllers/SettingController.php | 36 +------ app/Settings/AppSettingsStore.php | 90 ++++++++++++++++++ app/Settings/SettingService.php | 12 +-- app/Uploads/ImageRepo.php | 10 +- public/icon-128.png | Bin 0 -> 3538 bytes public/icon-32.png | Bin 0 -> 1338 bytes public/icon-64.png | Bin 0 -> 1951 bytes public/icon.png | Bin 0 -> 6900 bytes resources/lang/en/settings.php | 2 +- resources/views/layouts/base.blade.php | 6 ++ .../views/settings/customization.blade.php | 19 ++++ 11 files changed, 132 insertions(+), 43 deletions(-) create mode 100644 app/Settings/AppSettingsStore.php create mode 100644 public/icon-128.png create mode 100644 public/icon-32.png create mode 100644 public/icon-64.png create mode 100644 public/icon.png diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php index f5e48ca4c..1e13d7cb7 100644 --- a/app/Http/Controllers/SettingController.php +++ b/app/Http/Controllers/SettingController.php @@ -4,20 +4,14 @@ namespace BookStack\Http\Controllers; use BookStack\Actions\ActivityType; use BookStack\Auth\User; +use BookStack\Settings\AppSettingsStore; use BookStack\Uploads\ImageRepo; use Illuminate\Http\Request; class SettingController extends Controller { - protected ImageRepo $imageRepo; - protected array $settingCategories = ['features', 'customization', 'registration']; - public function __construct(ImageRepo $imageRepo) - { - $this->imageRepo = $imageRepo; - } - /** * Handle requests to the settings index path. */ @@ -48,37 +42,17 @@ class SettingController extends Controller /** * Update the specified settings in storage. */ - public function update(Request $request, string $category) + public function update(Request $request, AppSettingsStore $store, string $category) { $this->ensureCategoryExists($category); $this->preventAccessInDemoMode(); $this->checkPermission('settings-manage'); $this->validate($request, [ - 'app_logo' => array_merge(['nullable'], $this->getImageValidationRules()), + 'app_logo' => ['nullable', ...$this->getImageValidationRules()], + 'app_icon' => ['nullable', ...$this->getImageValidationRules()], ]); - // Cycles through posted settings and update them - foreach ($request->all() as $name => $value) { - $key = str_replace('setting-', '', trim($name)); - if (strpos($name, 'setting-') !== 0) { - continue; - } - setting()->put($key, $value); - } - - // Update logo image if set - if ($category === 'customization' && $request->hasFile('app_logo')) { - $logoFile = $request->file('app_logo'); - $this->imageRepo->destroyByType('system'); - $image = $this->imageRepo->saveNew($logoFile, 'system', 0, null, 86); - setting()->put('app-logo', $image->url); - } - - // Clear logo image if requested - if ($category === 'customization' && $request->get('app_logo_reset', null)) { - $this->imageRepo->destroyByType('system'); - setting()->remove('app-logo'); - } + $store->storeFromUpdateRequest($request, $category); $this->logActivity(ActivityType::SETTINGS_UPDATE, $category); $this->showSuccessNotification(trans('settings.settings_save_success')); diff --git a/app/Settings/AppSettingsStore.php b/app/Settings/AppSettingsStore.php new file mode 100644 index 000000000..f2b6cdc52 --- /dev/null +++ b/app/Settings/AppSettingsStore.php @@ -0,0 +1,90 @@ +imageRepo = $imageRepo; + } + + public function storeFromUpdateRequest(Request $request, string $category) + { + $this->storeSimpleSettings($request); + if ($category === 'customization') { + $this->updateAppLogo($request); + $this->updateAppIcon($request); + } + } + + protected function updateAppIcon(Request $request): void + { + $sizes = [128, 64, 32]; + + // Update icon image if set + if ($request->hasFile('app_icon')) { + $iconFile = $request->file('app_icon'); + $this->destroyExistingSettingImage('app-icon'); + $image = $this->imageRepo->saveNew($iconFile, 'system', 0, 256, 256); + setting()->put('app-icon', $image->url); + + foreach ($sizes as $size) { + $icon = $this->imageRepo->saveNew($iconFile, 'system', 0, $size, $size); + setting()->put('app-icon-' . $size, $icon->url); + } + } + + // Clear icon image if requested + if ($request->get('app_icon_reset')) { + $this->destroyExistingSettingImage('app-icon'); + setting()->remove('app-icon'); + foreach ($sizes as $size) { + $this->destroyExistingSettingImage('app-icon-' . $size); + setting()->remove('app-icon-' . $size); + } + } + } + + protected function updateAppLogo(Request $request): void + { + // Update logo image if set + if ($request->hasFile('app_logo')) { + $logoFile = $request->file('app_logo'); + $this->destroyExistingSettingImage('app-logo'); + $image = $this->imageRepo->saveNew($logoFile, 'system', 0, null, 86); + setting()->put('app-logo', $image->url); + } + + // Clear logo image if requested + if ($request->get('app_logo_reset')) { + $this->destroyExistingSettingImage('app-logo'); + setting()->remove('app-logo'); + } + } + + protected function storeSimpleSettings(Request $request): void + { + foreach ($request->all() as $name => $value) { + if (strpos($name, 'setting-') !== 0) { + continue; + } + + $key = str_replace('setting-', '', trim($name)); + setting()->put($key, $value); + } + } + + protected function destroyExistingSettingImage(string $settingKey) + { + $existingVal = setting()->get($settingKey); + if ($existingVal) { + $this->imageRepo->destroyByUrlAndType($existingVal, 'system'); + } + } +} diff --git a/app/Settings/SettingService.php b/app/Settings/SettingService.php index 9f0a41ea2..d1bac164d 100644 --- a/app/Settings/SettingService.php +++ b/app/Settings/SettingService.php @@ -12,15 +12,11 @@ use Illuminate\Contracts\Cache\Repository as Cache; */ class SettingService { - protected $setting; - protected $cache; - protected $localCache = []; + protected Setting $setting; + protected Cache $cache; + protected array $localCache = []; + protected string $cachePrefix = 'setting-'; - protected $cachePrefix = 'setting-'; - - /** - * SettingService constructor. - */ public function __construct(Setting $setting, Cache $cache) { $this->setting = $setting; diff --git a/app/Uploads/ImageRepo.php b/app/Uploads/ImageRepo.php index 8770402ad..910248203 100644 --- a/app/Uploads/ImageRepo.php +++ b/app/Uploads/ImageRepo.php @@ -180,13 +180,17 @@ class ImageRepo } /** - * Destroy all images of a certain type. + * Destroy images that have a specific URL and type combination. * * @throws Exception */ - public function destroyByType(string $imageType): void + public function destroyByUrlAndType(string $url, string $imageType): void { - $images = Image::query()->where('type', '=', $imageType)->get(); + $images = Image::query() + ->where('url', '=', $url) + ->where('type', '=', $imageType) + ->get(); + foreach ($images as $image) { $this->destroyImage($image); } diff --git a/public/icon-128.png b/public/icon-128.png new file mode 100644 index 0000000000000000000000000000000000000000..46cc2811b5e7f6162e87b8e4e0cf3785f57a922c GIT binary patch literal 3538 zcmV;@4K4DCP)C0008|P)t-s03iSk zhtL3v(h-Q!0FBiYiP9H})D(@-8jIB&jMf*9(I1W18j#W~jL;mB(=LtCAClBCj?y8M z)-jLMBb3)OkkloW*fo*XCzjbZlGiws*ff>XIh5Hpmef0y+Cr4mH<;B#mDD+z)v)g|gm$wcm!b;D5H^hqU2< zx8jJk;(@s1iMHc{x#Wts5yXJ|z;E=iCjJoEAyyuF%;gP!Gi@oBKyyJ|% z}>6yamoWkRl#N?92>Yc;n zm&WCu#O9dC=by#rnaJp%#^{>K>7mEzo672=$m*oY>!-=(rONE6%I2xd=cdfa5S}v(4+R(CoC%?5@%6wa@LZ((b9$?zYhGxzOmb)9<>`>9o`7vefXq((1L; z>a*4IywmKq)$6^~?YGwLxYzE!*6z93@4wgYy4mo-*zmjC@xs~hyxj7`+VaNQ?7iLd z#M|@0-R;NQ?Y`dh!QSr3-0#BQ@X6iq!{G7C-to)d^334##^UqGGs*`_uA|D;Oh9@?DOI4`P=RJ;_Um}?)u*D z^ycmM;P3V4?)TyF_vr8V;_>+D@cHBN`Reid|N8&`{Qvv^|No39j~4&{00DGTPE!Ct=GbNc0004E zOGiWihy@);00009a7bBm001Cy001Cy0qf8I3jhELWJyFpRCwC$TTMtK*%t1isE`l} z2@=9{6GR$mW+M`mhJk=IO*1QL5XXfZM?^$qAPbR3X5*qkL}u}jmTqaJVWgV|@exFt znS~If9T*U?HNK=5CS#sRz(>pvA&aTH=iW+H-E(6lCc5dnnz6Ocse8`(zVlZP9(+6B zPLocBocor4Zz>ScrQCgK0I!}L@7pTehYBG6Vksa=o{^){ovT0u=HB%vl74)%U+>lx z!1#NEZiyZ_xSG8?6^Nj6azd3%M+qO?l?Fsib|olTPVjZ^ZbFiX$Zv)ei+FV=L7XDf zY8Alv2SXl_8q`*nVs?{zS42h)`84uz1mY6~OpMBVLPm_u%J}4G{<`^t@}b zE5Hk-X4|dC@7W-7POXSQG#hT}C4lkQI<3YJEM4k9ztri#cGPiVm?o(}9#h?T8wT|F+v|eE&iU=%9lW?;ZU@FfrOk4Wmw~$yyNR&;{UtSer=9K+EfAv@sTXmJT!i4s{n7d)u+^YlH&HN z?cG`uw&h}kSd2Y&OE(_>FE_XXCEa1q&CiltQ7uUQln~?Ns^sLz58u{s1xC6)N6(Vx zHK$*rgxDcV4o~2NPytH1T~41BhUBkuz1gcc6<@;*XvP(GIenJYo`w|pxyR{WxWd~7 zL6$)AWyC8vKkgm9KAKjY{=VD=wM%A?QvfI38_!@Ov*O(Nz88e2bL-u+b_Hmq+b!28 zUy}U+$Hv!6c;HqoT5SqI#^orC8$L=1ni!D2S`+}>jgP4<6fXx?F3sF(Qh<`~qQ8aW zB{f2lqU-mpcBv`=VZ6~Jw@|$MIpOKjL6uvX0<_W{Y^Cw#-j9T*lQSk8Qw7kBtI6U6 zivZOt7H%~wu-VG%S0_$^TW#G8P@uvbFwiRF50aVy;Z{R|1c}Y~n70+T-@QSqqSPGE zt#ed?;uEMMik}8t&6e+9$cVVrJE|K-zkp0Y^3R`(Qt8_qqjurB)itm~?p;?P*`{YG zkgTQ_nukShntdMg+{(m(0w*fexqBqBI38_|E3l*s<5nLSro?VQWtBeZDiwG7mD|g2 zpZgmMT0TYg{0M}&w`MW{>nah=t^~Pw9mafnX~7ku0y>4W2p>vB23-JCiN%{)u(Qf0$SgbJxG^BOwK!{)M#ik`6D1LgvV@5biiTWm2kiUDU z%P6NKJbw6J0=m8Klm+=fVHK!wjju?&!-r0a{=gEDE`p%y7-=UJqP(9e3ls_CzE0q# zzY!{=U8UT**cE8E7?a_C8Y_wjv2*o`DiBp_Q6Xc_jDV=MKPAS;{IfO_Zi;>$x!>mEEPRr7(D%vpdyd1n!4HZ+s^|C=sRQ?h{Mc@*c$ z*HnQ;BV5)=xTKNZ44T_%dJz}hMP!5_<3bvS8qyQI%Kn=<;cMfW?q@RofJ|bgBs9|k#0r|qB*44bQ!Pcr8r>ZgiQGk zMjCw2gXV1cXh@;Njzy;jumdH;M9S%kiQim!RmNpR5eU+tz$mfY;H0Lz}-B2Lv;$ec^a9*6& zXfhBRO(oHpIw?e`%bLQ%P#}u{C3_Xb$JCi*=^By2(n)lJhSfb)HpK9crO$EfqJJ9T zjSt;o!2}H*qL}Fz!#~e3xY}nYjk5p*54TWJouKixuc1H*dec0Wx6z*B#!V9dhAumg z6%i&TdAu^ha2yh*!y$LQRA?R19wB9A5QcRZ*kE~;0@0z$dMKFp(ib=uwvU;T=~=!C zZJRkOoA-?PXwnsUFKY<1gmsZ9tpODZ+81BWhduN7RQr#0*>p9uc;@HfDnPlGdeX~opG}K1 zE-@zB*Gg=qq0$ef!09+fX2ft;tGc_@mNEgcaOqP0zb=CeBemx|!Y8Wlp$ z7V-7N&?kxNc>!U86$!{{k!Gln4Y?`)Ax=-T+0yY95F#ccDbB8A*m9Njg9X75h7Pyc*MJSZ%zVWj{R zD!(HscWCPxo6`9gps-jAVueMi=AabQnK={E=5+`CqT;0(<27dYS*bOsJ{h2wj6vrxUQEU}aaWLJ+oP2OC;n6W3fczc4pCJ>q7jLTRhg=;9hivi(q~FzslS zSVkQeh`YhuK!v8_oFoCh>ZJyd0f{ z3R@7jUSXvRtXTJHmu~H(UI=?xP1)kD2=gjJt@teTxamv8+!8Y+7e|}OEHu2+?2laV zHE!L~s<4c0@+v#5Rqcdt9AD8ywgub#721cdDcFSt`V*H}388$_J*6?)&=2^`6Bu4IX?u-%fMRKkhgw!oC+g5dZ)H M07*qoM6N<$f;;1MtpET3 literal 0 HcmV?d00001 diff --git a/public/icon-32.png b/public/icon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..7307ed8f1e88f1e5ade85925d8aedee40d771f64 GIT binary patch literal 1338 zcmV-A1;zS_P)XIh5Hpmef0y+Cr4mH<;B#mDD+z)v)g|gm$wcm!b;D5H^hqU2< zx8jJk;(@s1iMHc{x#Wts5yXJ|z;E=iCjJoEAyyuF%;gP!Gi@oBKyyJ|% z}>6yamoWkRl#N?92>Yc;n zm&WCu#O9dC=by#rnaJp%#^{>K>7mEzo672=$m*oY>!-=(rONE6%I2xd=cdfa5S}v(4+R(CoC%?5@%6wa@LZ((b9$?zYhGxzOmb)9<>`>9o`7vefXq((1L; z>a*4IywmKq)$6^~?YGwLxYzE!*6z93@4wgYy4mo-*zmjC@xs~hyxj7`+VaNQ?7iLd z#M|@0-R;NQ?Y`dh!QSr3-0#BQ@X6iq!{G7C-to)d^334##^UqGGs*`_uA|D;Oh9@?DOI4`P=RJ;_Um}?)u*D z^ycmM;P3V4?)TyF_vr8V;_>+D@cHBN`Reid|N8&`{Qvv^|Nn}pJ+%M;00DGTPE!Ct=GbNc0004E zOGiWihy@);00009a7bBm001Cy001Cy0qf8I3jhEC%t=H+R5;6(Q$1+HKorhLuqYBr z5$PC;l&(_Tg;F|)Zmz+>-9buMhk`fg zTA^=AF5j1T-}~Nsn)d&QR|ny_dwezIf5N<%2TDPY?Wgdd1Qhz?Tcn-&9{i)OBjd6f zfD141c9SgHS?G7nJpqgst0!QMwkiCzKtEb+5;jp~ol}UYPWblaDvHb>6gLNBpcwJP z5x%2mf4^Q0{g~io$HcxcA6W&F#+RkNaIz6O28gg~L@nbtZU^24px-aFIm`~L?!bk? z*n+UMA8VYvEe?EZ3(mHsS@|B-MjD2qsB-vxwube3S(phTL4cB}M%gDb-%dC+!%MQJn#?AF@z>ObBYR(IVSw*3gG;H)pDiXH6^2U}unO#s{~~X8iEp^8fGp7Zf4e`k7Q;P5=M^07*qoM6N<$f&%O@y#N3J literal 0 HcmV?d00001 diff --git a/public/icon-64.png b/public/icon-64.png new file mode 100644 index 0000000000000000000000000000000000000000..854d80faacb4ddcbbef17a357a5e7c3ca6ed69b9 GIT binary patch literal 1951 zcmV;Q2VnS#P)XIh5Hpmef0y+Cr4mH<;B#mDD+z)v)g|gm$wcm!b;D5H^hqU2< zx8jJk;(@s1iMHc{x#Wts5yXJ|z;E=iCjJoEAyyuF%;gP!Gi@oBKyyJ|% z}>6yamoWkRl#N?92>Yc;n zm&WCu#O9dC=by#rnaJp%#^{>K>7mEzo672=$m*oY>!-=(rONE6%I2xd=cdfa5S}v(4+R(CoC%?5@%6wa@LZ((b9$?zYhGxzOmb)9<>`>9o`7vefXq((1L; z>a*4IywmKq)$6^~?YGwLxYzE!*6z93@4wgYy4mo-*zmjC@xs~hyxj7`+VaNQ?7iLd z#M|@0-R;NQ?Y`dh!QSr3-0#BQ@X6iq!{G7C-to)d^334##^UqGGs*`_uA|D;Oh9@?DOI4`P=RJ;_Um}?)u*D z^ycmM;P3V4?)TyF_vr8V;_>+D@cHBN`Reid|N8&`{Qvv^|Nn}pJ+%M;00DGTPE!Ct=GbNc0004E zOGiWihy@);00009a7bBm001Cy001Cy0qf8I3jhEFF-b&0R9M5+S3O80Q4sbK5)wiP zB0;cOB#3P|f>#FQunxi%7B)dd(prd!T?#?OGMFOBRU!mL5V^)e5YaA3j8|;6k+6u> z=4Xp3Zr;9qfBDfQ!oj^Nt~;|c-+VLQys4@G;sC(c%-dJ*le@sL-=>Li)|t=+fVmbR zcrw*V5&?KCM5#FmZNEZ$Tn_-9Q^PYS*&4J<#-THg3yk<7!^{^hUARl*kK{#nWEYrx zF6f!ZiVg>YkQH+@C;*&8mV9u-LLkD-oFvL^7$FaUZY9Yqma7s765ejY>-J3X6Big` z^#`+e_X0QogV3^DqyXd@<&moMJ3i z{h>r%#4+xdRUb(|8yIBnUN4H5)^4h%<+!j!f_V$<-7ry4qTK3p0Z_{Zw6_AGXaNc< zPtY#UlHQZL(Mt-5aJcU+@*Qj!?$iDMAirrPqgq3jm;o=1ZHmUg~u@utebwdsRL}<^4vJ)Bz*_&C95`b z;EeVCS?niKZFB%oX@U83R0P61HdZ_2|21!-11E-r5G?;B`L#MuCSL8BH=*Dyg3nE4 z!Stv0lzO7tfm{O|+lzokwyQ=q_)MR);6+rM(x{U3Fj(-h>A-V#xH%omvf5N9YAb#f zHaO{Jb@~q-%c9!R{Wq}kAzsE>x{n{dhmN58C4nR&R*Wk=XqOWlSDZoCMzF`+8_VlF z`ER%2{^m$h3|jk5Xzs}5x?&;{F_s-)pBRnmVrXP6bYKP&+C!Yf-HIXX>7VC@K+;w| zHhW4H7oPmE1eJocz=sTqdD^tMpa%7dlBP|wqTKQKLxQHTfY{q+K zG_xu_FOnRv{I&3gV_6qCjwIuO#BVT_l9ye0)Q}X!@i53~_#tkMoJuA7L<$_``0dG2h}5<3%XUpkW+ z4_JFIsO&w6d46RuI+A;~uS(FKk^DP~sr^$z8g26A>3SrQL>dWE6Z1gFZxI(OS&-O~ zwkl|<3bq`w6@~dETVB+XIh5Hpmef0y+Cr4mH<;B#mDD+z)v)g|gm$wcm!b;D5H^hqU2< zx8jJk;(@s1iMHc{x#Wts5yXJ|z;E=iCjJoEAyyuF%;gP!Gi@oBKyyJ|% z}>6yamoWkRl#N?92>Yc;n zm&WCu#O9dC=by#rnaJp%#^{>K>7mEzo672=$m*oY>!-=(rONE6%I2xd=cdfa5S}v(4+R(CoC%?5@%6wa@LZ((b9$?zYhGxzOmb)9<>`>9o`7vefXq((1L; z>a*4IywmKq)$6^~?YGwLxYzE!*6z93@4wgYy4mo-*zmjC@xs~hyxj7`+VaNQ?7iLd z#M|@0-R;NQ?Y`dh!QSr3-0#BQ@X6iq!{G7C-to)d^334##^UqGGs*`_uA|D;Oh9@?DOI4`P=RJ;_Um}?)u*D z^ycmM;P3V4?)TyF_vr8V;_>+D@cHBN`Reid|N8&`{Qvv^|Nn}pJ+%M;00DGTPE!Ct=GbNc0004E zOGiWihy@);00009a7bBm001Cy001Cy0qf8I3jhEYhDk(0RCwC$U2jZOS(dl@=A~Z+ zuqf$J9XhT7K45g541c1D(o9C}h?xw%xN+ld9JJdo>BuOp&PH3KwsBg`!WTP>qgk>n z%Vc+n-EO0X2xzyu8UpOHjE1BFlOZq|BW3kM*Ozyb`Y;q#!a4W-JMYyisG_)ENHkS- z&$;)Ud(Q9tTeWIst*n)`vR2m0T3N}o!jYBc4uE=7Wx|H-_0TTZ1Em86#OR9dB z4srl*|FeCv3og&G@#W3}B>bnmE@82}rs?G@2avXz=6vQ-FIIYI*{T9a7r$4kMSf(- z)kAz)vH+4lyzecS{_>uBvHVm4BtLk5HFFs)R^VS|)-ihe+6#@zpA~}PW#%Bjb5pZoeCPMSSFLDAiej%>MRGCge-Idja`V zjXr7Tlhn2fknry|6aF9F3PE#~cLb*6+WK*E2&+=P6# zwHuH>bKQ_#(!LbH%&^mICj14>;N@33x0tYN)=^G!%nS+t=^PWM*L!-BTVU7RXVxaO zD!{^DU?%+gk?{Y+YUUy=wyu*WsgHyZucuqhgulE;f$UmKBH!gcImwgMOGHlZ0W;yx zgTl|BmP;h^r9HzuNgZWm_nJDr87H9dk9{PQ$Y({j!KFzm(-1v<9Ytn#ZxwjWxH-G# zouz3QP(9M0JnsR#{R35IPVf4gQ21|cBntn55%4P$_3l|mc_cYl0FQsPF3Sw@Y6vcD zTj{GL3jfa@flCvN`j*Vs_F9`# zMR$~u@ITmWX7^U~acKAE(S+Z71|lVX17}ID8eUvg0B^s)#!UG0TOi`~U<*?d501ccJjNnVFXn_hM#oSb&}0LW_u(mv;Me zuL8xUJlRS46f3{C2&w?*cvYAQe<{TAW^NSHZoj8~6aq^JXfH;D+e3-I1PJlkZ?-XZ z9Ktuszt`>CRa)SaCB_V)zv@A7Xj~~?@KPLODcVs z7_0zAdbg1_c0rc`k=3u3Aa~saAb*1BEUD2(zw;cxkzS%!Dhe4fAXKb!mIh&j@T!lw z>@DWK5_DD-K=P;Q8)KgYX79S*HQ=>^^ui#pa(km)@}r*DR}QKIjChq;*ci*(f2N_i z{k#90nEL;aqJ$AQcq{lOmVFUAOV6q$SA2_A_9+f=06D!jHa7d{NwVkN4LkN7J^xRw z_x^WeJOAo&TXtN%77LY(bx#_ zdiJ60a&k$#2kc)0RNGMH)5W-d&1-BrsSv+nBK7E zDT)xTV=m_x%k8ki0POVUIlM7;Gy2Va5b^4*NCf#VmeUZj%m5_(56hk3*r%d(#791o z6G6U9{>Vm2qBW=2mJ(1f`BRs35_9}pd#$GvAmRT!F&mRdk&KxmUiQcrd}9&=u<$2l zW3yZP1|nXSQbK+MrQlE*0FVEEh2%=qI=UeI2NERwCa0JA{M$aCu3#|fX6@isoRyhi*15MXY+AO zV+J6^tI5YwnEZE|X2M#5I3ori;lERs!sH7@XD|?FzyNssgVk~hlP}brptz+j0}$L@ z?@49hFM&m8O7~Wz$aNTi3i?gcJd&Dx-_`HAfeWX}03`gK#i=a(a_tz5TV7DPa9Rwo zRNIi;$6pD7KTY#e%>d+$pYf*h?azlHB|flI%Zd{jfW7g;R3<;?LV!@7jv0W2|LM+@ z7XBJw_ww&`Q_m3t!nX2HlJfXVAHe*(Tl(ecIt-Y+l+t!DLcDmtQKmj*0QgK}MhYK4 zM!ZZ?&$Ssa>-g@KQdRgN;uQ`ym{@T-42YB}r=<78${#A4B?JTps4`W4mvVYQC^{P^ z#4S|};LvUSb!s6XStnrf?aPxCMW$jvFASXX!OGN81ybd6JE`YAm;t;_+6e=*iGpVn z@brBJxBZwHw{#zT@rD7hGhZJG8-fNc{MF?($kHwv?AN34{mzu{UD2ok(uz>plfP;F2ZJV|P|fK}qUlLBJV*~KZ8Ark%DHIJ6= zfbru*g=w82es7?)aP;rj zFiCSw1H{rtK2APWAb$c7uW;}pEpEy3f3K*+1wy3&o^H8yd(Ej#e>vL{fr}H(A7KUMb1$4?|}C4%CJ`x4>m2SDq#7r6p|18ocQ#i z#>_J}qWx(^28?<2MD94mnZ{4Kmx=`}1GQP2tfMIY4KGdLEHG+CK!hJM z)V(#ZD?_MZkukH`a}3#WdUyT-MF>%6Iu#=HFU;8Vz5+{_{`9NGd=lM<;W&Wmb&}Z>0cfu<^&#xrD0%w&n{cdBZhS#Opr1H0@eiTNN1L zS4bPW1UKYQa3B?d7krY)aD;e;`O{sR!mW-B^^)FqwjOo7O=Kuts_eBPa?FbmuaQG< z>wLS40ije(`d=6h0jvZtZRxr zr}ql8uyk$5MzkD0fTZlLNLHwpmg-h;{SsI@o4u{1~YtGZO)k zBHY%S+T2tA8+2sx6j#g&KLjtH6U)=3D&#C(Cw64^_G1S0%PwhKubps%_cMf49krWW z;zqrNqsS%U=YN7iI1fsR*4*A_I55JS<|eqnx?(=lKx}k|F|+4?ZATSIuvcC<)chVX z(hHH2Fty~8l`Ks2e8YghjcF4U6&(GT8ghhjYULZt5oNX!&KKpq#8904$)BJ}dNGHp zGGY1rLV`&2e>dH>wG}ibce=q+g|)zTYTdk40SjW3b|Tx|wPUD!KKkeT1yESpGYFlq ze_l&%bRDn}uAXBaMj0*=1<#(+mUi?B<@2!+Tej}A@`BjLL%_@)Zzb;NeQ5WFT8XN6 zH3|~g>&Ofl@a%1ToL8s04HR}4!ptb8_N)lDiB%r$AU?jR5PYhU=q#;)DNsfXh?thJ zF|BfsntX1Tg#^6*EJQa;v68!}3sJ6E!tg-sd-(L8fc>UU&_; z76CmZ%z)5Z9q;5Z!aayjZN{fiMZY?$c9gy6p`=Vuo!*jfxycMNZLYzYtufe3V1RCH z6m_-=hsfX*0yT#B%HlbK|DCz!vthxJ-zHYv1G&~y!nE9B{`dOM&V@-EZ9Q>QV-Dx* zu`*Q9-$~3naUYqm(~w;?corkPmT zm|2gw2GTo3K;(gBXmz89w{1NYGlPS`#%d$uMl$Nh;e8^Or?V0iA?z-vf)x-dn7+P_ z=_5kp#`Z35a`&2QdKEOo)Bc|-c`w@0HL{-(v|*UCGK3S}#rTp65-T?OHr(TMgc_+R z?S?wkZ?GfXuaN7;P)JWqUz3h|iS zS?h2ek$1m^X&$4*#2lPd!3Q+0adm=39nlzpV;jhhGVEqT?i7jBJjgT1vXRQvC~AMAe@NDe15^gDjv6&Z!x&kVI*AoO>`3PmSKpH8r04!e;~V zS{S@-wzr=>G)<1tV?e~dqPnC<;KMY2oT$(?HFo?X5%J2gQV6ccHN%oG&FC<>jqj{I z8$|Xj@m4dk`-Wza5z}IGK1WH^X<@3}6p-kMS@PreIGVU`F@p?&Y+)SLHHbn`mmN}pxx@6hm6 z5x<69`=Oa8i&BqRF~TZP6N+{_XnQzlKa)N(ebhz+YaxQsL)2hz*G{R+=CIQrq$bMv z@atnUeKzY5E(9mO$GMHDvdy|_KHonf9{U3)=RVnC{SrvK>=c5#vT9)zlWfIoxEWG~ z$4j+sDXInf{v%|8T1*gN8*jp2y-!Swm8wwE?u~paHo3rng}4CQ`jlu*8+dM0RuTl5 zuK~6`^izeuqyv@DlfAMLt=efA8IqAd6{JtX4%#X?mp_5mIB+4G8YF<_^L}y}QNK8B zDE0-J$f&*o$F`19{nw&f<~W4vLpuj;rFk5NX&!+Kt8F1Opplvo9v6_P{tBv4A|&eH zOzjw!j|!Uxs48c>9c<3!y`VL4OglfoHWmq^g5kht#<0KlJVXn}>ZpiUn;ovn`-e5> z3h3nrxa1?Ke4dI%`;5_OeW#ZicFaEg3YO24@p--ZqcQGhI~R`-O3rn(GoC(5E(FKP zF#)QEF1W5cU@8`*$<=;oP;25&>Pu6N<3Ab@uoyG?1yT67bwjSTZz3kM9vjymEEC&8 zfu5r=Xd_Vu$0WlO^+Hh%9)-%sYKalTLR&Z?j?(J%(&oE#@xOYM!h4#y*R?1r{S38f z6biwKv*hrIlv%D3?exuN!-u2)e%e5-U9**!P>S*ry5?n^ZGHfiIJJQ_QkfPqHO$9P zs~Rb3sDf{`X))WfU!9r4d^bWiB805$TU)t0zadN=j`b3ATc(9@Y0txm^5kKexsSrk zvsY(`0wr9+JyfUnIc2C8Ei~voKV?S%h3vFLLnwcYs8T(j{u%P|3^iY`gEo0Q_PzT| zEq#QUQ4?iw$vKpy@2C;NpAXr=34IOI)PYiZos?AHx&1rz2>*!MzRk}y(gVnd*DahJ zV8c2uW8#>_sL$NczXI^w0+Xc+0cJY{vDVhqu)u~VrM zqF1U!69h&NQsI>jRJ%h*yll^tR31~Kf-q5r2dOnKYpw?POFF;?z3M+wtG6opY|YeN zxMsOFvUbB{7}J|iY|n}3f|Bd^@>^_9RfdX&Xq){6)ww;~;$(jLJH5kkj1|YD1&RKdrygZ+o?7h$3ue#pg)PLZQwiIrUz{)Z zWiB}zHEi8XA)g};Hs;ybh`p)u_!f*FA?D2C&}?U%i88pzK%qvZJoV!|Zt_*Cpvp!l zfX`fuqvfR7Fp?_#d!IQ&zG~1Qu_DKF92J+HuvX zk>eG9@{|w3Ac>oN)x1G+{~}aA-?hnd`8=Kr`jl#t)=hAS$roABN9|zsG3sD-)zWMR zVJ@ium*hxqBFGm9uQ}Yq%)m{GDG_34E>_U$`0dx3H-DNKvB#L%Q(_^wm>ff_!dUb9 zYo0LjRq3--M|t}r=)kuU?hf13j^=`Hqo&3B6HC7MB;Kb2m?*jArP<^*oUTL133onG z5>38J6;Q9H?=ni1F?>c)x{lUdP=cr>kbY4WoTc{mKsAnix;guJD6m2e65tUj2_@fd zCOt0M(rGz~({=a&wH9>0e=*4yTkZ9f#NE;Q&7=8SI2Uy6Ff~H>n1>gaeo+-P#oN<- zZoRc1mCsX6?)REQi%-5N2h_HKtgI6S!2zf%l+#jM&}AN<$EMc!3 zrR$i^1)cY|zntbvOuh;;t0RMz@Uchb^Bu&J3^ea&>BtvYpbXF(-)NW5lN&}p=Sf1o zN)-^Jf~aNA8@rd6ia1r`J^6Rr1k|B1`zl?0@DEDa0j{u@ZsS9*zvmm5V0 zlZ1THI(}p|Y)wZOP>6^7g_LwQ!^=9VQW$gzrGy2B&M*MSxr+s zWyn{lf~t5E1ma8cNCH)GXG2`&^TnM?IMwJESTNmSd(;NYg}!s@kT0@;T5_&R4-i^@ zr`Uq3;K^?5Idd{91W!5gMeF#M#RLId2%eJk3zy(~E1fhVBT{(ElP|J>n&21dWk0q& z$XCJVq(O8zj+Wx&i&jCKsq4oIzE;A^iGJY{m^3zE*%yP$jeIMPvRvcm%aD9g4(@Gb zI*3=dvOLKbS)kdb5l0A@9sMfIOh^zA8a6FM@{LtNY9L-tV`lU7Oj-G3C7cfQiw;7x z%^l9HUf$%ZR6(7ac}`C9G$LPQ!C>9 'Any content added here will be inserted into the bottom of the section of every page. This is handy for overriding styles or adding analytics code.', 'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.', 'app_logo' => 'Application Logo', - 'app_logo_desc' => 'This image should be 43px in height.
Large images will be scaled down.', + 'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.', 'app_primary_color' => 'Application Primary Color', 'app_primary_color_desc' => 'Sets the primary color for the application including the banner, buttons, and links.', 'app_homepage' => 'Application Homepage', diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index 76d220952..b09a8dfe9 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -20,6 +20,12 @@ + + + + + + @yield('head') diff --git a/resources/views/settings/customization.blade.php b/resources/views/settings/customization.blade.php index 3748267df..847704007 100644 --- a/resources/views/settings/customization.blade.php +++ b/resources/views/settings/customization.blade.php @@ -53,6 +53,25 @@ +
+
+ +

+ This icon is used for browser tabs and shortcut icons. + This should be a 256px square PNG image. +

+
+
+ @include('form.image-picker', [ + 'removeValue' => 'none', + 'defaultImage' => url('/icon.png'), + 'currentImage' => setting('app-icon'), + 'name' => 'app_icon', + 'imageClass' => 'logo-image', + ]) +
+
+
From d8354255e7d28cefd538fcc05e671cbf99018637 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 25 Jan 2023 12:06:11 +0000 Subject: [PATCH 028/477] Added practicali to sponsor list --- readme.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index b8bd17232..8b3af303e 100644 --- a/readme.md +++ b/readme.md @@ -45,7 +45,7 @@ Note: Listed services are not tested, vetted nor supported by the official BookS Diagrams.net - Cloudabove + Cloudabove @@ -55,6 +55,9 @@ Note: Listed services are not tested, vetted nor supported by the official BookS Stellar Hosted + + Stellar Hosted + ## 🛣️ Road Map From 3c658e39abe32a7dc345f3c49ea012340d4427e2 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 25 Jan 2023 16:11:34 +0000 Subject: [PATCH 029/477] Extracted app icon text, fixed issues Tweaked sizes and meta tags based unpon ipad testing. Fixed reduced sizes not being cleaned up. --- app/Settings/AppSettingsStore.php | 3 ++- app/Uploads/ImageRepo.php | 5 ++++- resources/lang/en/settings.php | 2 ++ resources/views/layouts/base.blade.php | 13 ++++++++----- resources/views/settings/customization.blade.php | 7 ++----- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/app/Settings/AppSettingsStore.php b/app/Settings/AppSettingsStore.php index f2b6cdc52..8d7b73c1c 100644 --- a/app/Settings/AppSettingsStore.php +++ b/app/Settings/AppSettingsStore.php @@ -25,7 +25,7 @@ class AppSettingsStore protected function updateAppIcon(Request $request): void { - $sizes = [128, 64, 32]; + $sizes = [180, 128, 64, 32]; // Update icon image if set if ($request->hasFile('app_icon')) { @@ -35,6 +35,7 @@ class AppSettingsStore setting()->put('app-icon', $image->url); foreach ($sizes as $size) { + $this->destroyExistingSettingImage('app-icon-' . $size); $icon = $this->imageRepo->saveNew($iconFile, 'system', 0, $size, $size); setting()->put('app-icon-' . $size, $icon->url); } diff --git a/app/Uploads/ImageRepo.php b/app/Uploads/ImageRepo.php index 910248203..2c643a58b 100644 --- a/app/Uploads/ImageRepo.php +++ b/app/Uploads/ImageRepo.php @@ -123,7 +123,10 @@ class ImageRepo public function saveNew(UploadedFile $uploadFile, string $type, int $uploadedTo = 0, int $resizeWidth = null, int $resizeHeight = null, bool $keepRatio = true): Image { $image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo, $resizeWidth, $resizeHeight, $keepRatio); - $this->loadThumbs($image); + + if ($type !== 'system') { + $this->loadThumbs($image); + } return $image; } diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index 318dc0a52..023cf1beb 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -34,6 +34,8 @@ return [ 'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.', 'app_logo' => 'Application Logo', 'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.', + 'app_icon' => 'Application Icon', + 'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.', 'app_primary_color' => 'Application Primary Color', 'app_primary_color_desc' => 'Sets the primary color for the application including the banner, buttons, and links.', 'app_homepage' => 'Application Homepage', diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index b09a8dfe9..e0a6f46d0 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -6,10 +6,11 @@ {{ isset($pageTitle) ? $pageTitle . ' | ' : '' }}{{ setting('app-name') }} + - + @@ -21,10 +22,12 @@ - - - - + + + + + + @yield('head') diff --git a/resources/views/settings/customization.blade.php b/resources/views/settings/customization.blade.php index 847704007..aa37c30c9 100644 --- a/resources/views/settings/customization.blade.php +++ b/resources/views/settings/customization.blade.php @@ -55,11 +55,8 @@
- -

- This icon is used for browser tabs and shortcut icons. - This should be a 256px square PNG image. -

+ +

{{ trans('settings.app_icon_desc') }}

@include('form.image-picker', [ From a50b0ea1e5b3e3c02aacef653a8e3dd5d09eabd8 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 25 Jan 2023 16:41:41 +0000 Subject: [PATCH 030/477] Covered app icon setting with testing --- tests/Settings/SettingsTest.php | 46 +++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/Settings/SettingsTest.php b/tests/Settings/SettingsTest.php index e2ac6f27c..1161a466e 100644 --- a/tests/Settings/SettingsTest.php +++ b/tests/Settings/SettingsTest.php @@ -2,10 +2,14 @@ namespace Tests\Settings; +use Illuminate\Support\Facades\Storage; use Tests\TestCase; +use Tests\Uploads\UsesImages; class SettingsTest extends TestCase { + use UsesImages; + public function test_settings_endpoint_redirects_to_settings_view() { $resp = $this->asAdmin()->get('/settings'); @@ -40,4 +44,46 @@ class SettingsTest extends TestCase $resp->assertStatus(404); $resp->assertSee('Page Not Found'); } + + public function test_updating_and_removing_app_icon() + { + $this->asAdmin(); + $galleryFile = $this->getTestImage('my-app-icon.png'); + $expectedPath = public_path('uploads/images/system/' . date('Y-m') . '/my-app-icon.png'); + + $this->assertFalse(setting()->get('app-icon')); + $this->assertFalse(setting()->get('app-icon-180')); + $this->assertFalse(setting()->get('app-icon-128')); + $this->assertFalse(setting()->get('app-icon-64')); + $this->assertFalse(setting()->get('app-icon-32')); + + $prevFileCount = count(glob(dirname($expectedPath) . DIRECTORY_SEPARATOR . '*.png')); + + $upload = $this->call('POST', '/settings/customization', [], [], ['app_icon' => $galleryFile], []); + $upload->assertRedirect('/settings/customization'); + + $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: ' . $expectedPath); + $this->assertStringContainsString('my-app-icon', setting()->get('app-icon')); + $this->assertStringContainsString('my-app-icon', setting()->get('app-icon-180')); + $this->assertStringContainsString('my-app-icon', setting()->get('app-icon-128')); + $this->assertStringContainsString('my-app-icon', setting()->get('app-icon-64')); + $this->assertStringContainsString('my-app-icon', setting()->get('app-icon-32')); + + $newFileCount = count(glob(dirname($expectedPath) . DIRECTORY_SEPARATOR . '*.png')); + $this->assertEquals(5, $newFileCount - $prevFileCount); + + $resp = $this->get('/'); + $this->withHtml($resp)->assertElementCount('link[sizes][href*="my-app-icon"]', 6); + + $reset = $this->post('/settings/customization', ['app_icon_reset' => 'true']); + $reset->assertRedirect('/settings/customization'); + + $resetFileCount = count(glob(dirname($expectedPath) . DIRECTORY_SEPARATOR . '*.png')); + $this->assertEquals($prevFileCount, $resetFileCount); + $this->assertFalse(setting()->get('app-icon')); + $this->assertFalse(setting()->get('app-icon-180')); + $this->assertFalse(setting()->get('app-icon-128')); + $this->assertFalse(setting()->get('app-icon-64')); + $this->assertFalse(setting()->get('app-icon-32')); + } } From deda33174548c105e3470ecb361ea39e4f18f8f5 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 25 Jan 2023 21:46:26 +0000 Subject: [PATCH 031/477] Fixed global search preview click on safari Safari needs an element to be focusable to be able to use :focus-within. For #3926 --- resources/views/common/header.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/common/header.blade.php b/resources/views/common/header.blade.php index 71b73215b..a8b711595 100644 --- a/resources/views/common/header.blade.php +++ b/resources/views/common/header.blade.php @@ -19,7 +19,7 @@
@if (hasAppAccess()) -
\ No newline at end of file + \ No newline at end of file diff --git a/resources/views/books/sort.blade.php b/resources/views/books/sort.blade.php index 077da101d..b778398a8 100644 --- a/resources/views/books/sort.blade.php +++ b/resources/views/books/sort.blade.php @@ -16,8 +16,10 @@
-
-

{{ trans('entities.books_sort') }}

+
+

{{ trans('entities.books_sort') }}

+

{{ trans('entities.books_sort_desc') }}

+
@include('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren])
@@ -35,8 +37,9 @@
-
-

{{ trans('entities.books_sort_show_other') }}

+
+

{{ trans('entities.books_sort_show_other') }}

+

{{ trans('entities.books_sort_show_other_desc') }}

@include('entities.selector', ['name' => 'books_list', 'selectorSize' => 'compact', 'entityTypes' => 'book', 'entityPermission' => 'update', 'showAdd' => true]) From 7cacbaadf0dc2d9b94e163597bd2b3d2cc05be53 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 27 Jan 2023 13:08:35 +0000 Subject: [PATCH 039/477] Added functionality/logic for button-based sorting --- resources/js/components/book-sort.js | 167 ++++++++++++++++++ .../books/parts/sort-box-actions.blade.php | 12 ++ .../views/books/parts/sort-box.blade.php | 9 +- 3 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 resources/views/books/parts/sort-box-actions.blade.php diff --git a/resources/js/components/book-sort.js b/resources/js/components/book-sort.js index 2722eb586..3c849c5c6 100644 --- a/resources/js/components/book-sort.js +++ b/resources/js/components/book-sort.js @@ -37,6 +37,113 @@ const sortOperations = { }, }; +/** + * The available move actions. + * The active function indicates if the action is possible for the given item. + * The run function performs the move. + * @type {{up: {active(Element, ?Element, Element): boolean, run(Element, ?Element, Element)}}} + */ +const moveActions = { + up: { + active(elem, parent, book) { + return !(elem.previousElementSibling === null && !parent); + }, + run(elem, parent, book) { + const newSibling = elem.previousElementSibling || parent; + newSibling.insertAdjacentElement('beforebegin', elem); + } + }, + down: { + active(elem, parent, book) { + return !(elem.nextElementSibling === null && !parent); + }, + run(elem, parent, book) { + const newSibling = elem.nextElementSibling || parent; + newSibling.insertAdjacentElement('afterend', elem); + } + }, + next_book: { + active(elem, parent, book) { + return book.nextElementSibling !== null; + }, + run(elem, parent, book) { + const newList = book.nextElementSibling.querySelector('ul'); + newList.prepend(elem); + } + }, + prev_book: { + active(elem, parent, book) { + return book.previousElementSibling !== null; + }, + run(elem, parent, book) { + const newList = book.previousElementSibling.querySelector('ul'); + newList.appendChild(elem); + } + }, + next_chapter: { + active(elem, parent, book) { + return elem.dataset.type === 'page' && this.getNextChapter(elem, parent); + }, + run(elem, parent, book) { + const nextChapter = this.getNextChapter(elem, parent); + nextChapter.querySelector('ul').prepend(elem); + }, + getNextChapter(elem, parent) { + const topLevel = (parent || elem); + const topItems = Array.from(topLevel.parentElement.children); + const index = topItems.indexOf(topLevel); + return topItems.slice(index + 1).find(elem => elem.dataset.type === 'chapter'); + } + }, + prev_chapter: { + active(elem, parent, book) { + return elem.dataset.type === 'page' && this.getPrevChapter(elem, parent); + }, + run(elem, parent, book) { + const prevChapter = this.getPrevChapter(elem, parent); + prevChapter.querySelector('ul').append(elem); + }, + getPrevChapter(elem, parent) { + const topLevel = (parent || elem); + const topItems = Array.from(topLevel.parentElement.children); + const index = topItems.indexOf(topLevel); + return topItems.slice(0, index).reverse().find(elem => elem.dataset.type === 'chapter'); + } + }, + book_end: { + active(elem, parent, book) { + return parent || (parent === null && elem.nextElementSibling); + }, + run(elem, parent, book) { + book.querySelector('ul').append(elem); + } + }, + book_start: { + active(elem, parent, book) { + return parent || (parent === null && elem.previousElementSibling); + }, + run(elem, parent, book) { + book.querySelector('ul').prepend(elem); + } + }, + before_chapter: { + active(elem, parent, book) { + return parent; + }, + run(elem, parent, book) { + parent.insertAdjacentElement('beforebegin', elem); + } + }, + after_chapter: { + active(elem, parent, book) { + return parent; + }, + run(elem, parent, book) { + parent.insertAdjacentElement('afterend', elem); + } + }, +}; + export class BookSort extends Component { setup() { @@ -49,10 +156,35 @@ export class BookSort extends Component { const initialSortBox = this.container.querySelector('.sort-box'); this.setupBookSortable(initialSortBox); this.setupSortPresets(); + this.setupMoveActions(); window.$events.listen('entity-select-confirm', this.bookSelect.bind(this)); } + /** + * Setup the handlers for the item-level move buttons. + */ + setupMoveActions() { + // Handle move button click + this.container.addEventListener('click', event => { + if (event.target.matches('[data-move]')) { + const action = event.target.getAttribute('data-move'); + const sortItem = event.target.closest('[data-id]'); + this.runSortAction(sortItem, action); + } + }); + // TODO - Probably can remove this + // // Handle action updating on likely use + // this.container.addEventListener('focusin', event => { + // const sortItem = event.target.closest('[data-type="chapter"],[data-type="page"]'); + // if (sortItem) { + // this.updateMoveActionState(sortItem); + // } + // }); + + this.updateMoveActionStateForAll(); + } + /** * Setup the handlers for the preset sort type buttons. */ @@ -102,6 +234,7 @@ export class BookSort extends Component { const newBookContainer = htmlToDom(resp.data); this.sortContainer.append(newBookContainer); this.setupBookSortable(newBookContainer); + this.updateMoveActionStateForAll(); }); } @@ -204,4 +337,38 @@ export class BookSort extends Component { } } + /** + * Run the given sort action up the provided sort item. + * @param {Element} item + * @param {String} action + */ + runSortAction(item, action) { + const parentItem = item.parentElement.closest('li[data-id]'); + const parentBook = item.parentElement.closest('[data-type="book"]'); + moveActions[action].run(item, parentItem, parentBook); + this.updateMapInput(); + this.updateMoveActionStateForAll(); + item.scrollIntoView({behavior: 'smooth', block: 'nearest'}); + item.focus(); + } + + /** + * Update the state of the available move actions on this item. + * @param {Element} item + */ + updateMoveActionState(item) { + const parentItem = item.parentElement.closest('li[data-id]'); + const parentBook = item.parentElement.closest('[data-type="book"]'); + for (const [action, functions] of Object.entries(moveActions)) { + const moveButton = item.querySelector(`[data-move="${action}"]`); + moveButton.disabled = !functions.active(item, parentItem, parentBook); + } + } + + updateMoveActionStateForAll() { + const items = this.container.querySelectorAll('[data-type="chapter"],[data-type="page"]'); + for (const item of items) { + this.updateMoveActionState(item); + } + } } \ No newline at end of file diff --git a/resources/views/books/parts/sort-box-actions.blade.php b/resources/views/books/parts/sort-box-actions.blade.php new file mode 100644 index 000000000..0c91f42da --- /dev/null +++ b/resources/views/books/parts/sort-box-actions.blade.php @@ -0,0 +1,12 @@ +
+ + + + + + + + + + +
\ No newline at end of file diff --git a/resources/views/books/parts/sort-box.blade.php b/resources/views/books/parts/sort-box.blade.php index 819f1e063..77a03f831 100644 --- a/resources/views/books/parts/sort-box.blade.php +++ b/resources/views/books/parts/sort-box.blade.php @@ -23,7 +23,8 @@
  • + data-updated="{{ $bookChild->updated_at->timestamp }}" tabindex="0"> +
    @icon('grip')
    @icon($bookChild->getType())
    @@ -33,17 +34,21 @@
  • + @include('books.parts.sort-box-actions') @if($bookChild->isA('chapter'))
      @foreach($bookChild->visible_pages as $page)
    • + data-updated="{{ $page->updated_at->timestamp }}" + tabindex="0"> +
      @icon('grip')
      @icon('page') {{ $page->name }}
      + @include('books.parts.sort-box-actions')
    • @endforeach
    From 40e112fc5b4896c52d5aa09ea2ad2a9da6eccfa1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 27 Jan 2023 13:26:58 +0000 Subject: [PATCH 040/477] Extracted text & added dropdown for book sort move actions Primarily styling and testing left to do. --- resources/lang/en/entities.php | 11 ++++++- resources/sass/_lists.scss | 4 +-- .../books/parts/sort-box-actions.blade.php | 31 +++++++++++++------ .../views/books/parts/sort-box.blade.php | 2 +- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 5b019e848..834cfacba 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -150,7 +150,16 @@ return [ 'books_sort_chapters_last' => 'Chapters Last', 'books_sort_show_other' => 'Show Other Books', 'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.', - 'books_sort_save' => 'Save New Order', + 'books_sort_move_up' => 'Move Up', + 'books_sort_move_down' => 'Move Down', + 'books_sort_move_prev_book' => 'Move To Previous Book', + 'books_sort_move_next_book' => 'Move To Next Book', + 'books_sort_move_prev_chapter' => 'Move Into Previous Chapter', + 'books_sort_move_next_chapter' => 'Move Into Next Chapter', + 'books_sort_move_book_start' => 'Move To Start of Book', + 'books_sort_move_book_end' => 'Move To End of Book', + 'books_sort_move_before_chapter' => 'Move To Before Chapter', + 'books_sort_move_after_chapter' => 'Move To After Chapter', 'books_copy' => 'Copy Book', 'books_copy_success' => 'Book successfully copied', diff --git a/resources/sass/_lists.scss b/resources/sass/_lists.scss index 1ae801267..39b2afee6 100644 --- a/resources/sass/_lists.scss +++ b/resources/sass/_lists.scss @@ -232,7 +232,7 @@ } // Sortable Lists -.sortable-page-list, .sortable-page-list ul { +.sortable-page-list, .sortable-page-sublist { list-style: none; } .sort-box { @@ -278,7 +278,7 @@ > ul { margin-inline-start: 0; } - ul { + .sortable-page-sublist { margin-bottom: $-m; margin-top: 0; padding-inline-start: $-m; diff --git a/resources/views/books/parts/sort-box-actions.blade.php b/resources/views/books/parts/sort-box-actions.blade.php index 0c91f42da..c9a1d323e 100644 --- a/resources/views/books/parts/sort-box-actions.blade.php +++ b/resources/views/books/parts/sort-box-actions.blade.php @@ -1,12 +1,23 @@
    - - - - - - - - - - + + +
    \ No newline at end of file diff --git a/resources/views/books/parts/sort-box.blade.php b/resources/views/books/parts/sort-box.blade.php index 77a03f831..33448f483 100644 --- a/resources/views/books/parts/sort-box.blade.php +++ b/resources/views/books/parts/sort-box.blade.php @@ -36,7 +36,7 @@
    @include('books.parts.sort-box-actions') @if($bookChild->isA('chapter')) -
      +
        @foreach($bookChild->visible_pages as $page)
      • Date: Fri, 27 Jan 2023 16:25:06 +0000 Subject: [PATCH 041/477] Finished off design and fixing of sort buttons --- resources/js/components/book-sort.js | 18 ++++-------- resources/js/services/keyboard-navigation.js | 2 +- resources/lang/en/entities.php | 13 +++++---- resources/sass/_lists.scss | 19 +++++++++++- .../books/parts/sort-box-actions.blade.php | 8 +++-- .../views/books/parts/sort-box.blade.php | 29 +++++++++++-------- 6 files changed, 54 insertions(+), 35 deletions(-) diff --git a/resources/js/components/book-sort.js b/resources/js/components/book-sort.js index 3c849c5c6..e8ecd49a4 100644 --- a/resources/js/components/book-sort.js +++ b/resources/js/components/book-sort.js @@ -162,7 +162,7 @@ export class BookSort extends Component { } /** - * Setup the handlers for the item-level move buttons. + * Set up the handlers for the item-level move buttons. */ setupMoveActions() { // Handle move button click @@ -173,20 +173,12 @@ export class BookSort extends Component { this.runSortAction(sortItem, action); } }); - // TODO - Probably can remove this - // // Handle action updating on likely use - // this.container.addEventListener('focusin', event => { - // const sortItem = event.target.closest('[data-type="chapter"],[data-type="page"]'); - // if (sortItem) { - // this.updateMoveActionState(sortItem); - // } - // }); this.updateMoveActionStateForAll(); } /** - * Setup the handlers for the preset sort type buttons. + * Set up the handlers for the preset sort type buttons. */ setupSortPresets() { let lastSort = ''; @@ -235,6 +227,9 @@ export class BookSort extends Component { this.sortContainer.append(newBookContainer); this.setupBookSortable(newBookContainer); this.updateMoveActionStateForAll(); + + const summary = newBookContainer.querySelector('summary'); + summary.focus(); }); } @@ -243,8 +238,7 @@ export class BookSort extends Component { * @param {Element} bookContainer */ setupBookSortable(bookContainer) { - const sortElems = [bookContainer.querySelector('.sort-list')]; - sortElems.push(...bookContainer.querySelectorAll('.entity-list-item + ul')); + const sortElems = Array.from(bookContainer.querySelectorAll('.sort-list, .sortable-page-sublist')); const bookGroupConfig = { name: 'book', diff --git a/resources/js/services/keyboard-navigation.js b/resources/js/services/keyboard-navigation.js index 0e1dcf1a7..0f866ceaa 100644 --- a/resources/js/services/keyboard-navigation.js +++ b/resources/js/services/keyboard-navigation.js @@ -86,7 +86,7 @@ export class KeyboardNavigationHandler { */ #getFocusable() { const focusable = []; - const selector = '[tabindex]:not([tabindex="-1"]),[href],button:not([tabindex="-1"]),input:not([type=hidden])'; + const selector = '[tabindex]:not([tabindex="-1"]),[href],button:not([tabindex="-1"],[disabled]),input:not([type=hidden])'; for (const container of this.containers) { focusable.push(...container.querySelectorAll(selector)) } diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 834cfacba..8bf805774 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -149,17 +149,18 @@ return [ 'books_sort_chapters_first' => 'Chapters First', 'books_sort_chapters_last' => 'Chapters Last', 'books_sort_show_other' => 'Show Other Books', + 'books_sort_save' => 'Save New Order', 'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.', 'books_sort_move_up' => 'Move Up', 'books_sort_move_down' => 'Move Down', - 'books_sort_move_prev_book' => 'Move To Previous Book', - 'books_sort_move_next_book' => 'Move To Next Book', + 'books_sort_move_prev_book' => 'Move to Previous Book', + 'books_sort_move_next_book' => 'Move to Next Book', 'books_sort_move_prev_chapter' => 'Move Into Previous Chapter', 'books_sort_move_next_chapter' => 'Move Into Next Chapter', - 'books_sort_move_book_start' => 'Move To Start of Book', - 'books_sort_move_book_end' => 'Move To End of Book', - 'books_sort_move_before_chapter' => 'Move To Before Chapter', - 'books_sort_move_after_chapter' => 'Move To After Chapter', + 'books_sort_move_book_start' => 'Move to Start of Book', + 'books_sort_move_book_end' => 'Move to End of Book', + 'books_sort_move_before_chapter' => 'Move to Before Chapter', + 'books_sort_move_after_chapter' => 'Move to After Chapter', 'books_copy' => 'Copy Book', 'books_copy_success' => 'Book successfully copied', diff --git a/resources/sass/_lists.scss b/resources/sass/_lists.scss index 39b2afee6..33e500d6a 100644 --- a/resources/sass/_lists.scss +++ b/resources/sass/_lists.scss @@ -267,7 +267,7 @@ .entity-list-item > span:first-child { align-self: flex-start; } - .sortable-selected .entity-list-item, .sortable-selected .entity-list-item:hover { + .sortable-selected, .sortable-selected:hover { outline: 1px dotted var(--color-primary); background-color: var(--color-primary-light) !important; } @@ -284,6 +284,7 @@ padding-inline-start: $-m; } li { + @include lightDark(background-color, #FFF, #222); border: 1px solid; @include lightDark(border-color, #DDD, #666); margin-top: -1px; @@ -316,6 +317,22 @@ details.sort-box summary .caret-container svg { details.sort-box[open] summary .caret-container svg { transform: rotate(90deg); } +.sort-box-actions .icon-button { + opacity: .6; +} +.sort-box .flex-container-row:hover .sort-box-actions .icon-button, +.sort-box .flex-container-row:focus-within .sort-box-actions .icon-button { + opacity: 1; +} +.sort-box-actions .icon-button[disabled] { + visibility: hidden; +} +.sort-box-actions .dropdown-menu button[disabled] { + display: none; +} +.sort-list-handle { + cursor: grab; +} .activity-list-item { padding: $-s 0; diff --git a/resources/views/books/parts/sort-box-actions.blade.php b/resources/views/books/parts/sort-box-actions.blade.php index c9a1d323e..cd5a59e15 100644 --- a/resources/views/books/parts/sort-box-actions.blade.php +++ b/resources/views/books/parts/sort-box-actions.blade.php @@ -1,10 +1,12 @@ -
        - - +
        \ No newline at end of file From 87e371ffde35043b8d889d4012fee0fbdf5a0e36 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 27 Jan 2023 17:39:51 +0000 Subject: [PATCH 043/477] Added prevention of nested chapters on sort --- resources/js/components/book-sort.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/resources/js/components/book-sort.js b/resources/js/components/book-sort.js index 6e56e43a5..5ae283fd0 100644 --- a/resources/js/components/book-sort.js +++ b/resources/js/components/book-sort.js @@ -260,7 +260,8 @@ export class BookSort extends Component { animation: 150, fallbackOnBody: true, swapThreshold: 0.65, - onSort: () => { + onSort: (event) => { + this.ensureNoNestedChapters() this.updateMapInput(); this.updateMoveActionStateForAll(); }, @@ -273,6 +274,20 @@ export class BookSort extends Component { } } + /** + * Handle nested chapters by moving them to the parent book. + * Needed since sorting with multi-sort only checks group rules based on the active item, + * not all in group, therefore need to manually check after a sort. + * Must be done before updating the map input. + */ + ensureNoNestedChapters() { + const nestedChapters = this.container.querySelectorAll('[data-type="chapter"] [data-type="chapter"]'); + for (const chapter of nestedChapters) { + const parentChapter = chapter.parentElement.closest('[data-type="chapter"]'); + parentChapter.insertAdjacentElement('afterend', chapter); + } + } + /** * Update the input with our sort data. */ From 1f69965c1e61a6ef3d46eb46ef4290889ba34381 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 28 Jan 2023 11:50:46 +0000 Subject: [PATCH 044/477] Updated settings view to have dark-mode color options Also added link color option, not yet used. Cleaned up tabbed interface control design as part of this. --- app/Config/setting-defaults.php | 1 + resources/lang/en/settings.php | 10 ++-- resources/sass/_blocks.scss | 7 +++ resources/sass/_components.scss | 31 +++++----- .../views/pages/parts/image-manager.blade.php | 4 +- .../views/settings/customization.blade.php | 56 ++++++++----------- ...ade.php => setting-color-picker.blade.php} | 19 ++++--- .../parts/setting-color-scheme.blade.php | 25 +++++++++ 8 files changed, 92 insertions(+), 61 deletions(-) rename resources/views/settings/parts/{setting-entity-color-picker.blade.php => setting-color-picker.blade.php} (57%) create mode 100644 resources/views/settings/parts/setting-color-scheme.blade.php diff --git a/app/Config/setting-defaults.php b/app/Config/setting-defaults.php index 5e1e4348a..37270cf31 100644 --- a/app/Config/setting-defaults.php +++ b/app/Config/setting-defaults.php @@ -16,6 +16,7 @@ return [ 'app-editor' => 'wysiwyg', 'app-color' => '#206ea7', 'app-color-light' => 'rgba(32,110,167,0.15)', + 'link-color' => '#206ea7', 'bookshelf-color' => '#a94747', 'book-color' => '#077b70', 'chapter-color' => '#af4d0d', diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index 023cf1beb..f9abadc0c 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -36,8 +36,6 @@ return [ 'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.', 'app_icon' => 'Application Icon', 'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.', - 'app_primary_color' => 'Application Primary Color', - 'app_primary_color_desc' => 'Sets the primary color for the application including the banner, buttons, and links.', 'app_homepage' => 'Application Homepage', 'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.', 'app_homepage_select' => 'Select a page', @@ -51,8 +49,12 @@ return [ 'app_disable_comments_desc' => 'Disables comments across all pages in the application.
        Existing comments are not shown.', // Color settings - 'content_colors' => 'Content Colors', - 'content_colors_desc' => 'Sets colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.', + 'color_scheme' => 'Application Color Scheme', + 'color_scheme_desc' => 'Set the colors to use in the BookStack interface. Colors can be configured separately for dark and light mode to best fit the theme and ensure legibility.', + 'ui_colors_desc' => 'Set the primary and link colors used in BookStack. The primary color is mainly used for the header banner, buttons and UI decorations, in addition to a few other components.', + 'app_color' => 'Primary Color', + 'link_color' => 'Link Color', + 'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.', 'bookshelf_color' => 'Shelf Color', 'book_color' => 'Book Color', 'chapter_color' => 'Chapter Color', diff --git a/resources/sass/_blocks.scss b/resources/sass/_blocks.scss index 2794dd954..1d9bfc272 100644 --- a/resources/sass/_blocks.scss +++ b/resources/sass/_blocks.scss @@ -237,6 +237,13 @@ } } +.sub-card { + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); + border: 1.5px solid; + @include lightDark(border-color, #E2E2E2, #444); + border-radius: 4px; +} + .outline-hover { border: 1px solid transparent !important; &:hover { diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index ab1d506c7..b902220a7 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -608,36 +608,39 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { .tab-container .nav-tabs { + display: flex; + align-items: end; + justify-items: start; text-align: start; border-bottom: 1px solid #DDD; @include lightDark(border-color, #ddd, #444); margin-bottom: $-m; - .tab-item { - padding: $-s; - @include lightDark(color, #666, #999); - &.selected { - border-bottom-width: 3px; - } - } } .nav-tabs { text-align: center; - a, .tab-item { - padding: $-m; + .tab-item { display: inline-block; - @include lightDark(color, #666, #999); + padding: $-s; + @include lightDark(color, rgba(0, 0, 0, .5), rgba(255, 255, 255, .5)); cursor: pointer; - border-right: 1px solid rgba(0, 0, 0, 0.1); border-bottom: 2px solid transparent; + margin-bottom: -1px; &.selected { - border-bottom: 2px solid var(--color-primary); + color: var(--color-primary) !important; + border-bottom-color: var(--color-primary) !important; } - &:last-child { - border-right: 0; + &:hover, &:focus { + @include lightDark(color, rgba(0, 0, 0, .8), rgba(255, 255, 255, .8)); + @include lightDark(border-bottom-color, rgba(0, 0, 0, .2), rgba(255, 255, 255, .2)); } } } +.nav-tabs.controls-card { + margin-bottom: 0; + border-bottom: 0; + padding: 0 $-xs; +} .image-picker .none { display: none; diff --git a/resources/views/pages/parts/image-manager.blade.php b/resources/views/pages/parts/image-manager.blade.php index 50a0cd8c3..a21a5fdac 100644 --- a/resources/views/pages/parts/image-manager.blade.php +++ b/resources/views/pages/parts/image-manager.blade.php @@ -21,10 +21,10 @@ type="button" class="tab-item selected" title="{{ trans('components.image_all_title') }}">@icon('images') {{ trans('components.image_all') }} + type="button" class="tab-item" title="{{ trans('components.image_book_title') }}">@icon('book', ['class' => 'svg-icon']) {{ trans('entities.book') }} + type="button" class="tab-item" title="{{ trans('components.image_page_title') }}">@icon('page', ['class' => 'svg-icon']) {{ trans('entities.page') }}
    diff --git a/resources/views/settings/customization.blade.php b/resources/views/settings/customization.blade.php index aa37c30c9..d3c20c4b1 100644 --- a/resources/views/settings/customization.blade.php +++ b/resources/views/settings/customization.blade.php @@ -69,42 +69,32 @@
    - -
    -
    - -

    {!! trans('settings.app_primary_color_desc') !!}

    -
    -
    - - -
    - - | - -
    - -
    -
    - - +
    -
    - -

    {!! trans('settings.content_colors_desc') !!}

    +
    + +

    {{ trans('settings.color_scheme_desc') }}

    -
    -
    - @include('settings.parts.setting-entity-color-picker', ['type' => 'bookshelf']) - @include('settings.parts.setting-entity-color-picker', ['type' => 'book']) - @include('settings.parts.setting-entity-color-picker', ['type' => 'chapter']) + + @php + $darkMode = boolval(setting()->getForCurrentUser('dark-mode-enabled')); + @endphp +
    + -
    - @include('settings.parts.setting-entity-color-picker', ['type' => 'page']) - @include('settings.parts.setting-entity-color-picker', ['type' => 'page-draft']) +
    +
    + @include('settings.parts.setting-color-scheme', ['mode' => 'light']) +
    +
    + @include('settings.parts.setting-color-scheme', ['mode' => 'dark']) +
    diff --git a/resources/views/settings/parts/setting-entity-color-picker.blade.php b/resources/views/settings/parts/setting-color-picker.blade.php similarity index 57% rename from resources/views/settings/parts/setting-entity-color-picker.blade.php rename to resources/views/settings/parts/setting-color-picker.blade.php index e7bfc3fe9..d6707fb50 100644 --- a/resources/views/settings/parts/setting-entity-color-picker.blade.php +++ b/resources/views/settings/parts/setting-color-picker.blade.php @@ -1,12 +1,15 @@ {{-- - @type - Name of entity type + @type - Name of color setting --}} +@php + $keyAppends = ($mode === 'light' ? '' : '-' . $mode); +@endphp
    - + | @@ -14,10 +17,10 @@
    diff --git a/resources/views/settings/parts/setting-color-scheme.blade.php b/resources/views/settings/parts/setting-color-scheme.blade.php new file mode 100644 index 000000000..1b18a9a6a --- /dev/null +++ b/resources/views/settings/parts/setting-color-scheme.blade.php @@ -0,0 +1,25 @@ +{{-- + @mode - 'light' or 'dark'. +--}} +

    {{ trans('settings.ui_colors_desc') }}

    +
    +
    + @include('settings.parts.setting-color-picker', ['type' => 'app', 'mode' => $mode]) +
    +
    + @include('settings.parts.setting-color-picker', ['type' => 'link', 'mode' => $mode]) +
    +
    +
    +

    {!! trans('settings.content_colors_desc') !!}

    +
    +
    + @include('settings.parts.setting-color-picker', ['type' => 'bookshelf', 'mode' => $mode]) + @include('settings.parts.setting-color-picker', ['type' => 'book', 'mode' => $mode]) + @include('settings.parts.setting-color-picker', ['type' => 'chapter', 'mode' => $mode]) +
    +
    + @include('settings.parts.setting-color-picker', ['type' => 'page', 'mode' => $mode]) + @include('settings.parts.setting-color-picker', ['type' => 'page-draft', 'mode' => $mode]) +
    +
    \ No newline at end of file From e708ce93baa19715b0951c86c111320d4af2dc82 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 28 Jan 2023 12:50:51 +0000 Subject: [PATCH 045/477] Updated generic tab styles and js to force accessible usage Added use of more accessible tags to create tabbed-interfaces then updated css and JS to require use of those attributes rather than custom techniques. Updated relevant parts of app. Some custom parts using their own tabs though, something to improve in future. --- resources/js/components/attachments.js | 2 +- resources/js/components/image-manager.js | 7 +-- resources/js/components/tabs.js | 62 +++++++++---------- resources/sass/_components.scss | 36 +++++------ resources/views/attachments/manager.blade.php | 45 +++++++++++--- .../views/pages/parts/image-manager.blade.php | 10 ++- .../views/settings/customization.blade.php | 32 +++++++--- 7 files changed, 119 insertions(+), 75 deletions(-) diff --git a/resources/js/components/attachments.js b/resources/js/components/attachments.js index b4e400aeb..d8a506270 100644 --- a/resources/js/components/attachments.js +++ b/resources/js/components/attachments.js @@ -45,7 +45,7 @@ export class Attachments extends Component { this.stopEdit(); /** @var {Tabs} */ const tabs = window.$components.firstOnElement(this.mainTabs, 'tabs'); - tabs.show('items'); + tabs.show('attachment-panel-items'); window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => { this.list.innerHTML = resp.data; window.$components.init(this.list); diff --git a/resources/js/components/image-manager.js b/resources/js/components/image-manager.js index a44fffc1b..418b7c98a 100644 --- a/resources/js/components/image-manager.js +++ b/resources/js/components/image-manager.js @@ -140,10 +140,9 @@ export class ImageManager extends Component { } setActiveFilterTab(filterName) { - this.filterTabs.forEach(t => t.classList.remove('selected')); - const activeTab = this.filterTabs.find(t => t.dataset.filter === filterName); - if (activeTab) { - activeTab.classList.add('selected'); + for (const tab of this.filterTabs) { + const selected = tab.dataset.filter === filterName; + tab.setAttribute('aria-selected', selected ? 'true' : 'false'); } } diff --git a/resources/js/components/tabs.js b/resources/js/components/tabs.js index 46063d240..ebab4191c 100644 --- a/resources/js/components/tabs.js +++ b/resources/js/components/tabs.js @@ -1,48 +1,46 @@ -import {onSelect} from "../services/dom"; import {Component} from "./component"; /** * Tabs - * Works by matching 'tabToggle' with 'tabContent' sections. + * Uses accessible attributes to drive its functionality. + * On tab wrapping element: + * - role=tablist + * On tabs (Should be a button): + * - id + * - role=tab + * - aria-selected=true/false + * - aria-controls= + * On panels: + * - id + * - tabindex=0 + * - role=tabpanel + * - aria-labelledby= + * - hidden (If not shown by default). */ export class Tabs extends Component { setup() { - this.tabContentsByName = {}; - this.tabButtonsByName = {}; - this.allContents = []; - this.allButtons = []; + this.container = this.$el; + this.tabs = Array.from(this.container.querySelectorAll('[role="tab"]')); + this.panels = Array.from(this.container.querySelectorAll('[role="tabpanel"]')); - for (const [key, elems] of Object.entries(this.$manyRefs || {})) { - if (key.startsWith('toggle')) { - const cleanKey = key.replace('toggle', '').toLowerCase(); - onSelect(elems, e => this.show(cleanKey)); - this.allButtons.push(...elems); - this.tabButtonsByName[cleanKey] = elems; + this.container.addEventListener('click', event => { + const button = event.target.closest('[role="tab"]'); + if (button) { + this.show(button.getAttribute('aria-controls')); } - if (key.startsWith('content')) { - const cleanKey = key.replace('content', '').toLowerCase(); - this.tabContentsByName[cleanKey] = elems; - this.allContents.push(...elems); - } - } + }); } - show(key) { - this.allContents.forEach(c => { - c.classList.add('hidden'); - c.classList.remove('selected'); - }); - this.allButtons.forEach(b => b.classList.remove('selected')); + show(sectionId) { + for (const panel of this.panels) { + panel.toggleAttribute('hidden', panel.id !== sectionId); + } - const contents = this.tabContentsByName[key] || []; - const buttons = this.tabButtonsByName[key] || []; - if (contents.length > 0) { - contents.forEach(c => { - c.classList.remove('hidden') - c.classList.add('selected') - }); - buttons.forEach(b => b.classList.add('selected')); + for (const tab of this.tabs) { + const tabSection = tab.getAttribute('aria-controls'); + const selected = tabSection === sectionId; + tab.setAttribute('aria-selected', selected ? 'true' : 'false'); } } diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index b902220a7..c8ecd438d 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -607,7 +607,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { } -.tab-container .nav-tabs { +.tab-container [role="tablist"] { display: flex; align-items: end; justify-items: start; @@ -617,26 +617,24 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { margin-bottom: $-m; } -.nav-tabs { - text-align: center; - .tab-item { - display: inline-block; - padding: $-s; - @include lightDark(color, rgba(0, 0, 0, .5), rgba(255, 255, 255, .5)); - cursor: pointer; - border-bottom: 2px solid transparent; - margin-bottom: -1px; - &.selected { - color: var(--color-primary) !important; - border-bottom-color: var(--color-primary) !important; - } - &:hover, &:focus { - @include lightDark(color, rgba(0, 0, 0, .8), rgba(255, 255, 255, .8)); - @include lightDark(border-bottom-color, rgba(0, 0, 0, .2), rgba(255, 255, 255, .2)); - } +.tab-container [role="tablist"] button[role="tab"], +.image-manager [role="tablist"] button[role="tab"] { + display: inline-block; + padding: $-s; + @include lightDark(color, rgba(0, 0, 0, .5), rgba(255, 255, 255, .5)); + cursor: pointer; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + &[aria-selected="true"] { + color: var(--color-primary) !important; + border-bottom-color: var(--color-primary) !important; + } + &:hover, &:focus { + @include lightDark(color, rgba(0, 0, 0, .8), rgba(255, 255, 255, .8)); + @include lightDark(border-bottom-color, rgba(0, 0, 0, .2), rgba(255, 255, 255, .2)); } } -.nav-tabs.controls-card { +.tab-container [role="tablist"].controls-card { margin-bottom: 0; border-bottom: 0; padding: 0 $-xs; diff --git a/resources/views/attachments/manager.blade.php b/resources/views/attachments/manager.blade.php index 724ca9c8e..7d14d00e7 100644 --- a/resources/views/attachments/manager.blade.php +++ b/resources/views/attachments/manager.blade.php @@ -9,25 +9,54 @@
    -

    {{ trans('entities.attachments_explain') }} {{ trans('entities.attachments_explain_instant_save') }}

    +

    {{ trans('entities.attachments_explain') }} {{ trans('entities.attachments_explain_instant_save') }}

    -