diff --git a/.eslintrc.js b/.eslintrc.js index 5fb4533b..fa43f07b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,8 +6,15 @@ module.exports = { jquery: true }, extends: ['standard'], - plugins: ['promise'], + parser: '@typescript-eslint/parser', + plugins: ['promise', '@typescript-eslint'], rules: { - 'prefer-arrow-callback': 'warn' + 'prefer-arrow-callback': 'warn', + 'no-unused-vars': 'warn', // This should be no error! + semi: ['warn', 'never'], + quotes: ['warn', 'single', { avoidEscape: true }], + 'spaced-comment': 'warn', + 'no-multiple-empty-lines': 'warn', + 'eol-last': 'warn' } } diff --git a/package-lock.json b/package-lock.json index e6de60aa..e2df9173 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,18 @@ "name": "tufast_tud", "version": "6.0.0", "license": "GPL-3.0", + "dependencies": { + "chart.js": "^3.8.0", + "simple-datatables": "^3.2.0" + }, "devDependencies": { "@snowpack/plugin-sass": "^1.4.0", + "@types/chart.js": "^2.9.37", + "@types/chrome": "^0.0.184", + "@types/firefox": "^0.0.31", + "@types/simple-datatables": "^3.2.0", + "@typescript-eslint/eslint-plugin": "^5.27.0", + "@typescript-eslint/parser": "^5.27.0", "eslint": "^7.32.0", "eslint-config-standard": "^16.0.3", "eslint-plugin-import": "^2.25.2", @@ -136,6 +146,41 @@ "integrity": "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==", "dev": true }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@npmcli/arborist": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.10.0.tgz", @@ -535,18 +580,70 @@ "@types/responselike": "*" } }, + "node_modules/@types/chart.js": { + "version": "2.9.37", + "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.37.tgz", + "integrity": "sha512-9bosRfHhkXxKYfrw94EmyDQcdjMaQPkU1fH2tDxu8DWXxf1mjzWQAV4laJF51ZbC2ycYwNDvIm1rGez8Bug0vg==", + "dev": true, + "dependencies": { + "moment": "^2.10.2" + } + }, + "node_modules/@types/chrome": { + "version": "0.0.184", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.184.tgz", + "integrity": "sha512-Wvawa0L2jUyLd6RNd0mx/1z0R/RZXkGKDI77+twsbcmhLlsGA64xrXQYxFr/F7yu6yGLEqKpEmrrJ1tveuVkIQ==", + "dev": true, + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, "node_modules/@types/estree": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "dev": true }, + "node_modules/@types/filesystem": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.32.tgz", + "integrity": "sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==", + "dev": true, + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.29.tgz", + "integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==", + "dev": true + }, + "node_modules/@types/firefox": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/@types/firefox/-/firefox-0.0.31.tgz", + "integrity": "sha512-GFamkvAUHpv41OvKSCbFX8HVqIFr8wIWhrUzD/7weQv4dFAdxoZfTj0mB+DJtxTlnw3oJyp1ZNt69TL4PP7ZMg==", + "dev": true + }, + "node_modules/@types/har-format": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.8.tgz", + "integrity": "sha512-OP6L9VuZNdskgNN3zFQQ54ceYD8OLq5IbqO4VK91ORLfOm7WdT/CiT/pHEBSQEqCInJ2y3O6iCm/zGtPElpgJQ==", + "dev": true + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", "dev": true }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -592,6 +689,205 @@ "@types/node": "*" } }, + "node_modules/@types/simple-datatables": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/simple-datatables/-/simple-datatables-3.2.0.tgz", + "integrity": "sha512-Kws/O3CNPN30AR2o5aFMWXBFYqeR5B6O2EcjEg5Sy2w/0AzDaYc5MjVyVC5FtPJ4tX5HcgWiVhGBxx6shwa/3g==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.27.0.tgz", + "integrity": "sha512-DDrIA7GXtmHXr1VCcx9HivA39eprYBIFxbQEHI6NyraRDxCGpxAFiYQAT/1Y0vh1C+o2vfBiy4IuPoXxtTZCAQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.27.0", + "@typescript-eslint/type-utils": "5.27.0", + "@typescript-eslint/utils": "5.27.0", + "debug": "^4.3.4", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.27.0.tgz", + "integrity": "sha512-8oGjQF46c52l7fMiPPvX4It3u3V3JipssqDfHQ2hcR0AeR8Zge+OYyKUCm5b70X72N1qXt0qgHenwN6Gc2SXZA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.27.0", + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/typescript-estree": "5.27.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.27.0.tgz", + "integrity": "sha512-VnykheBQ/sHd1Vt0LJ1JLrMH1GzHO+SzX6VTXuStISIsvRiurue/eRkTqSrG0CexHQgKG8shyJfR4o5VYioB9g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/visitor-keys": "5.27.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.27.0.tgz", + "integrity": "sha512-vpTvRRchaf628Hb/Xzfek+85o//zEUotr1SmexKvTfs7czXfYjXVT/a5yDbpzLBX1rhbqxjDdr1Gyo0x1Fc64g==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "5.27.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.27.0.tgz", + "integrity": "sha512-lY6C7oGm9a/GWhmUDOs3xAVRz4ty/XKlQ2fOLr8GAIryGn0+UBOoJDWyHer3UgrHkenorwvBnphhP+zPmzmw0A==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.27.0.tgz", + "integrity": "sha512-QywPMFvgZ+MHSLRofLI7BDL+UczFFHyj0vF5ibeChDAJgdTV8k4xgEwF0geFhVlPc1p8r70eYewzpo6ps+9LJQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/visitor-keys": "5.27.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.27.0.tgz", + "integrity": "sha512-nZvCrkIJppym7cIbP3pOwIkAefXOmfGPnCM0LQfzNaKxJHI6VjI8NC662uoiPlaf5f6ymkTy9C3NQXev2mdXmA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.27.0", + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/typescript-estree": "5.27.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.27.0.tgz", + "integrity": "sha512-46cYrteA2MrIAjv9ai44OQDUoCZyHeGIc4lsjCUX2WT6r4C+kidz1bNiR4017wHOPUythYeH+Sc7/cFP97KEAA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.27.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -619,6 +915,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/address": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/address/-/address-1.1.2.tgz", @@ -757,6 +1062,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/array.prototype.flat": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz", @@ -1143,6 +1457,11 @@ "node": ">=8" } }, + "node_modules/chart.js": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.8.0.tgz", + "integrity": "sha512-cr8xhrXjLIXVLOBZPkBZVF6NDeiVIrPLHcMhnON7UufudL+CNeRrD+wpYanswlm8NpudMdrt3CHoLMQMxJhHRg==" + }, "node_modules/cheerio": { "version": "1.0.0-rc.10", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", @@ -1431,10 +1750,15 @@ "node": ">=0.10" } }, + "node_modules/dayjs": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", + "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" + }, "node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -1609,6 +1933,18 @@ "wrappy": "1" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2178,6 +2514,24 @@ "node": ">=8.0.0" } }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, "node_modules/eslint-visitor-keys": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", @@ -2381,6 +2735,22 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2393,6 +2763,15 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-5.1.0.tgz", @@ -2790,6 +3169,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/got": { "version": "11.8.2", "resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz", @@ -3058,9 +3457,9 @@ } }, "node_modules/ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true, "engines": { "node": ">= 4" @@ -3521,9 +3920,9 @@ "dev": true }, "node_modules/json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "dev": true }, "node_modules/json-schema-traverse": { @@ -3584,18 +3983,18 @@ } }, "node_modules/jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "dev": true, - "engines": [ - "node >=0.6.0" - ], "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", - "json-schema": "0.2.3", + "json-schema": "0.4.0", "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" } }, "node_modules/just-diff": { @@ -3785,6 +4184,15 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/meriyah": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/meriyah/-/meriyah-3.1.6.tgz", @@ -3794,6 +4202,19 @@ "node": ">=10.4.0" } }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.50.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", @@ -3846,9 +4267,9 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "node_modules/minipass": { @@ -3977,6 +4398,15 @@ "node": ">=10" } }, + "node_modules/moment": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3984,9 +4414,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.1.30", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", - "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", "dev": true, "bin": { "nanoid": "bin/nanoid.cjs" @@ -4600,9 +5030,9 @@ "dev": true }, "node_modules/picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "engines": { "node": ">=8.6" @@ -4848,6 +5278,26 @@ "node": ">=0.6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -5017,6 +5467,16 @@ "node": ">= 4" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -5071,6 +5531,29 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -5099,9 +5582,9 @@ } }, "node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -5160,6 +5643,14 @@ "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", "dev": true }, + "node_modules/simple-datatables": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/simple-datatables/-/simple-datatables-3.2.0.tgz", + "integrity": "sha512-lWwrH7SbTHp4SBQmvGty3NFzruzcbz/URUkkcwd8AdnZrQUtSitGwKeJ64qUVv2JXWP+RLOe1xpN8bW4Z6gjSQ==", + "dependencies": { + "dayjs": "^1.10.7" + } + }, "node_modules/skypack": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/skypack/-/skypack-0.3.2.tgz", @@ -5794,6 +6285,27 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", "dev": true }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -5992,10 +6504,14 @@ "dev": true }, "node_modules/vm2": { - "version": "3.9.5", - "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.5.tgz", - "integrity": "sha512-LuCAHZN75H9tdrAiLFf030oW7nJV5xwNMuk1ymOZwopmuK3d2H4L1Kv4+GFHgarKiLfXXLFU+7LDABHnwOkWng==", + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.9.tgz", + "integrity": "sha512-xwTm7NLh/uOjARRBs8/95H0e8fT3Ukw5D/JJWhxMbhKzNh1Nu981jQKvkep9iKYNxzlVrdzD0mlBGkDKZWprlw==", "dev": true, + "dependencies": { + "acorn": "^8.7.0", + "acorn-walk": "^8.2.0" + }, "bin": { "vm2": "bin/vm2" }, @@ -6003,6 +6519,18 @@ "node": ">=6.0" } }, + "node_modules/vm2/node_modules/acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/walk-up-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-1.0.0.tgz", @@ -6234,6 +6762,32 @@ "integrity": "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==", "dev": true }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, "@npmcli/arborist": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.10.0.tgz", @@ -6553,18 +7107,70 @@ "@types/responselike": "*" } }, + "@types/chart.js": { + "version": "2.9.37", + "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.37.tgz", + "integrity": "sha512-9bosRfHhkXxKYfrw94EmyDQcdjMaQPkU1fH2tDxu8DWXxf1mjzWQAV4laJF51ZbC2ycYwNDvIm1rGez8Bug0vg==", + "dev": true, + "requires": { + "moment": "^2.10.2" + } + }, + "@types/chrome": { + "version": "0.0.184", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.184.tgz", + "integrity": "sha512-Wvawa0L2jUyLd6RNd0mx/1z0R/RZXkGKDI77+twsbcmhLlsGA64xrXQYxFr/F7yu6yGLEqKpEmrrJ1tveuVkIQ==", + "dev": true, + "requires": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, "@types/estree": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "dev": true }, + "@types/filesystem": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.32.tgz", + "integrity": "sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==", + "dev": true, + "requires": { + "@types/filewriter": "*" + } + }, + "@types/filewriter": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.29.tgz", + "integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==", + "dev": true + }, + "@types/firefox": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/@types/firefox/-/firefox-0.0.31.tgz", + "integrity": "sha512-GFamkvAUHpv41OvKSCbFX8HVqIFr8wIWhrUzD/7weQv4dFAdxoZfTj0mB+DJtxTlnw3oJyp1ZNt69TL4PP7ZMg==", + "dev": true + }, + "@types/har-format": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.8.tgz", + "integrity": "sha512-OP6L9VuZNdskgNN3zFQQ54ceYD8OLq5IbqO4VK91ORLfOm7WdT/CiT/pHEBSQEqCInJ2y3O6iCm/zGtPElpgJQ==", + "dev": true + }, "@types/http-cache-semantics": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", "dev": true }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, "@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -6610,6 +7216,115 @@ "@types/node": "*" } }, + "@types/simple-datatables": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/simple-datatables/-/simple-datatables-3.2.0.tgz", + "integrity": "sha512-Kws/O3CNPN30AR2o5aFMWXBFYqeR5B6O2EcjEg5Sy2w/0AzDaYc5MjVyVC5FtPJ4tX5HcgWiVhGBxx6shwa/3g==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.27.0.tgz", + "integrity": "sha512-DDrIA7GXtmHXr1VCcx9HivA39eprYBIFxbQEHI6NyraRDxCGpxAFiYQAT/1Y0vh1C+o2vfBiy4IuPoXxtTZCAQ==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.27.0", + "@typescript-eslint/type-utils": "5.27.0", + "@typescript-eslint/utils": "5.27.0", + "debug": "^4.3.4", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/parser": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.27.0.tgz", + "integrity": "sha512-8oGjQF46c52l7fMiPPvX4It3u3V3JipssqDfHQ2hcR0AeR8Zge+OYyKUCm5b70X72N1qXt0qgHenwN6Gc2SXZA==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.27.0", + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/typescript-estree": "5.27.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.27.0.tgz", + "integrity": "sha512-VnykheBQ/sHd1Vt0LJ1JLrMH1GzHO+SzX6VTXuStISIsvRiurue/eRkTqSrG0CexHQgKG8shyJfR4o5VYioB9g==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/visitor-keys": "5.27.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.27.0.tgz", + "integrity": "sha512-vpTvRRchaf628Hb/Xzfek+85o//zEUotr1SmexKvTfs7czXfYjXVT/a5yDbpzLBX1rhbqxjDdr1Gyo0x1Fc64g==", + "dev": true, + "requires": { + "@typescript-eslint/utils": "5.27.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/types": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.27.0.tgz", + "integrity": "sha512-lY6C7oGm9a/GWhmUDOs3xAVRz4ty/XKlQ2fOLr8GAIryGn0+UBOoJDWyHer3UgrHkenorwvBnphhP+zPmzmw0A==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.27.0.tgz", + "integrity": "sha512-QywPMFvgZ+MHSLRofLI7BDL+UczFFHyj0vF5ibeChDAJgdTV8k4xgEwF0geFhVlPc1p8r70eYewzpo6ps+9LJQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/visitor-keys": "5.27.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/utils": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.27.0.tgz", + "integrity": "sha512-nZvCrkIJppym7cIbP3pOwIkAefXOmfGPnCM0LQfzNaKxJHI6VjI8NC662uoiPlaf5f6ymkTy9C3NQXev2mdXmA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.27.0", + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/typescript-estree": "5.27.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.27.0.tgz", + "integrity": "sha512-46cYrteA2MrIAjv9ai44OQDUoCZyHeGIc4lsjCUX2WT6r4C+kidz1bNiR4017wHOPUythYeH+Sc7/cFP97KEAA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.27.0", + "eslint-visitor-keys": "^3.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true + } + } + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -6629,6 +7344,12 @@ "dev": true, "requires": {} }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, "address": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/address/-/address-1.1.2.tgz", @@ -6736,6 +7457,12 @@ "is-string": "^1.0.7" } }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, "array.prototype.flat": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz", @@ -7041,6 +7768,11 @@ } } }, + "chart.js": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.8.0.tgz", + "integrity": "sha512-cr8xhrXjLIXVLOBZPkBZVF6NDeiVIrPLHcMhnON7UufudL+CNeRrD+wpYanswlm8NpudMdrt3CHoLMQMxJhHRg==" + }, "cheerio": { "version": "1.0.0-rc.10", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", @@ -7266,10 +7998,15 @@ "assert-plus": "^1.0.0" } }, + "dayjs": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", + "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" + }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -7397,6 +8134,15 @@ "wrappy": "1" } }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -7864,6 +8610,15 @@ "estraverse": "^4.1.1" } }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + } + }, "eslint-visitor-keys": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", @@ -7994,6 +8749,19 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -8006,6 +8774,15 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, "fdir": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-5.1.0.tgz", @@ -8307,6 +9084,20 @@ "type-fest": "^0.20.2" } }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, "got": { "version": "11.8.2", "resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz", @@ -8504,9 +9295,9 @@ "requires": {} }, "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true }, "ignore-walk": { @@ -8844,9 +9635,9 @@ "dev": true }, "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "dev": true }, "json-schema-traverse": { @@ -8895,14 +9686,14 @@ "dev": true }, "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "dev": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", - "json-schema": "0.2.3", + "json-schema": "0.4.0", "verror": "1.10.0" } }, @@ -9065,12 +9856,28 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, "meriyah": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/meriyah/-/meriyah-3.1.6.tgz", "integrity": "sha512-JDOSi6DIItDc33U5N52UdV6P8v+gn+fqZKfbAfHzdWApRQyQWdcvxPvAr9t01bI2rBxGvSrKRQSCg3SkZC1qeg==", "dev": true }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, "mime-db": { "version": "1.50.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", @@ -9108,9 +9915,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "minipass": { @@ -9207,6 +10014,12 @@ "mkdirp": "^1.0.3" } }, + "moment": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -9214,9 +10027,9 @@ "dev": true }, "nanoid": { - "version": "3.1.30", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", - "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", "dev": true }, "natural-compare": { @@ -9672,9 +10485,9 @@ "dev": true }, "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, "pify": { @@ -9851,6 +10664,12 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, "quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -9986,6 +10805,12 @@ "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", "dev": true }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -10022,6 +10847,15 @@ "@rollup/plugin-inject": "^4.0.0" } }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -10044,9 +10878,9 @@ } }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -10090,6 +10924,14 @@ "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", "dev": true }, + "simple-datatables": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/simple-datatables/-/simple-datatables-3.2.0.tgz", + "integrity": "sha512-lWwrH7SbTHp4SBQmvGty3NFzruzcbz/URUkkcwd8AdnZrQUtSitGwKeJ64qUVv2JXWP+RLOe1xpN8bW4Z6gjSQ==", + "requires": { + "dayjs": "^1.10.7" + } + }, "skypack": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/skypack/-/skypack-0.3.2.tgz", @@ -10571,6 +11413,23 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", "dev": true }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -10737,10 +11596,22 @@ } }, "vm2": { - "version": "3.9.5", - "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.5.tgz", - "integrity": "sha512-LuCAHZN75H9tdrAiLFf030oW7nJV5xwNMuk1ymOZwopmuK3d2H4L1Kv4+GFHgarKiLfXXLFU+7LDABHnwOkWng==", - "dev": true + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.9.tgz", + "integrity": "sha512-xwTm7NLh/uOjARRBs8/95H0e8fT3Ukw5D/JJWhxMbhKzNh1Nu981jQKvkep9iKYNxzlVrdzD0mlBGkDKZWprlw==", + "dev": true, + "requires": { + "acorn": "^8.7.0", + "acorn-walk": "^8.2.0" + }, + "dependencies": { + "acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "dev": true + } + } }, "walk-up-path": { "version": "1.0.0", diff --git a/package.json b/package.json index ce3f260c..0e475101 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Browser Extension for higher productivity with TU Dresden IT-Services 🚀", "main": "index.js", "scripts": { - "lint": "npx eslint --fix .", + "lint": "npx eslint --ext .ts --ext .js --fix .", "test": "npm run lint && npm run build", "dev": "snowpack build --watch", "build": "snowpack build" @@ -36,6 +36,12 @@ "homepage": "https://github.com/TUfast-TUD/TUfast_TUD#readme", "devDependencies": { "@snowpack/plugin-sass": "^1.4.0", + "@types/chart.js": "^2.9.37", + "@types/chrome": "^0.0.184", + "@types/firefox": "^0.0.31", + "@types/simple-datatables": "^3.2.0", + "@typescript-eslint/eslint-plugin": "^5.27.0", + "@typescript-eslint/parser": "^5.27.0", "eslint": "^7.32.0", "eslint-config-standard": "^16.0.3", "eslint-plugin-import": "^2.25.2", @@ -44,5 +50,9 @@ "sass": "^1.42.1", "snowpack": "^3.8.8", "typescript": "^4.4.3" + }, + "dependencies": { + "chart.js": "^3.8.0", + "simple-datatables": "^3.2.0" } } diff --git a/snowpack.config.js b/snowpack.config.js index ba62abf2..d12b27b6 100644 --- a/snowpack.config.js +++ b/snowpack.config.js @@ -10,7 +10,10 @@ module.exports = { '@snowpack/plugin-sass', ], packageOptions: { - /* ... */ + knownEntrypoints: [ + 'dayjs', + 'dayjs/plugin/customParseFormat' + ] }, devOptions: { /* ... */ diff --git a/src/background.html b/src/background.html new file mode 100644 index 00000000..ce956529 --- /dev/null +++ b/src/background.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/background.js b/src/background.js deleted file mode 100644 index bc110bbb..00000000 --- a/src/background.js +++ /dev/null @@ -1,878 +0,0 @@ -'use strict' - -// eslint-disable-next-line no-unused-vars -const isChrome = navigator.userAgent.includes('Chrome/') // attention: no failsave browser detection | also for new edge! -const isFirefox = navigator.userAgent.includes('Firefox/') // attention: no failsave browser detection - -// start fetchOWA if activated and user data exists -chrome.storage.local.get(['enabledOWAFetch', 'NumberOfUnreadMails'], async (resp) => { - if (await userDataExists() && resp.enabledOWAFetch) { - await setBadgeUnreadMails(resp.NumberOfUnreadMails) // read number of unread mails from storage and display badge - await enableOWAFetch() // start owa fetch - console.log('Activated OWA fetch.') - } else console.log('No OWAfetch registered') -}) - -// disable star rating -chrome.storage.local.set({ ratingEnabledFlag: false }) - -// reset banner for gOPAL -const d = new Date() -const month = d.getMonth() + 1 // starts at 0 -const day = d.getDate() -if (month === 10 && day > 20) { - chrome.storage.local.set({ closedMsg1: false }) -} - -// DOESNT WORK IN RELEASE VERSION -chrome.storage.local.get(['openSettingsOnReload'], async (resp) => { - if (resp.openSettingsOnReload) await openSettingsPage() - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.set({ openSettingsOnReload: false }, resolve)) -}) - -// set browserIcon -chrome.storage.local.get(['selectedRocketIcon'], (resp) => { - try { - const r = JSON.parse(resp.selectedRocketIcon) - chrome.browserAction.setIcon({ - path: r.link - }) - } catch (e) { console.log('Cannot set rocket icon: ' + e) } -}) - -console.log('Loaded TUfast') -chrome.storage.local.set({ - loggedOutSelma: false, - loggedOutElearningMED: false, - loggedOutTumed: false, - loggedOutQis: false, - loggedOutOpal: false, - loggedOutOwa: false, - loggedOutMagma: false, - loggedOutJexam: false, - loggedOutCloudstore: false, - loggedOutTex: false, - loggedOutGitlab: false -}) -chrome.storage.local.get(['pdfInNewTab'], (result) => { - if (result.pdfInNewTab) { - enableHeaderListener(true) - } -}) - -chrome.runtime.onInstalled.addListener(async (details) => { - const reason = details.reason - switch (reason) { - case 'install': { - console.log('TUfast installed.') - - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.set({ - installed: true, - showed_50_clicks: false, - showed_100_clicks: false, - isEnabled: false, - fwdEnabled: true, - mostLiklySubmittedReview: false, - removedReviewBanner: false, - neverShowedReviewBanner: true, - encryption_level: 3, - meine_kurse: false, - favoriten: false, - // openSettingsPageParam: false - seenInOpalAfterDashbaordUpdate: 0, - dashboardDisplay: 'favoriten', - additionalNotificationOnNewMail: false, - // NumberOfUnreadMails: 'undefined', - removedOpalBanner: false, - nameIsTUfast: true, - enabledOWAFetch: false, - colorfulRocket: 'black', - PRObadge: false, - flakeState: false, - availableRockets: ['RI_default'], - foundEasteregg: false, - hisqisPimpedTable: true, - openSettingsOnReload: false, - selectedRocketIcon: '{"id": "RI_default", "link": "assets/icons/RocketIcons/default_128px.png"}', - pdfInInline: false, - pdfInNewTab: false, - studiengang: 'general', - updateCustomizeStudiengang: false, - TUfastCampInvite1: false, - theme: 'system' - }, resolve)) - - await openSettingsPage('first_visit') // open settings page - break - } - case 'update': { - // Promisified until usage of Manifest V3 - const settings = await new Promise((resolve) => chrome.storage.local.get([ - 'encryption_level', - 'dashboardDisplay', - 'mostLiklySubmittedReview', - 'removedReviewBanner', - 'neverShowedReviewBanner', - 'seenInOpalAfterDashbaordUpdate', - 'enabledOWAFetch', - 'flakeState', - 'showedFirefoxBanner', - 'showedUnreadMailCounterBanner', - 'openSettingsOnReload', - 'availableRockets', - 'selectedRocketIcon', - 'hisqisPimpedTable', - 'Rocket', 'foundEasteregg', 'saved_click_counter', 'availableRockets', - 'updateCustomizeStudiengang', - 'studiengang', - 'theme' - // TUfastCampInvite1 - ], resolve)) - - const updateObj = {} - - // check if encryption is already on level 3 - if (settings.encryption_level !== 3) { - switch (settings.encryption_level) { - case 1: { - // This branch probably will not be called anymore... - console.log('Upgrading encryption standard from level 1 to level 3...') - // Promisified until usage of Manifest V3 - const userData = await new Promise((resolve) => chrome.storage.local.get(['asdf', 'fdsa'], resolve)) - await setUserData({ user: atob(userData.asdf), pass: atob(userData.fdsa) }, 'zih') - break - } - case 2: { - const { asdf: user, fdsa: pass } = await getUserDataLagacy() - await setUserData({ user, pass }, 'zih') - // Delete old user data - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.remove(['Data'], resolve)) - } - } - updateObj.encryption_level = 3 - } - - // check if the type of courses is selected which should be display in the dasbhaord. If not, set to default - if (!settings.dashboardDisplay) updateObj.dashboardDisplay = 'favoriten' - - // check if mostLiklySubmittedReview - if (!settings.mostLiklySubmittedReview && typeof settings.neverShowedReviewBanner !== 'boolean') updateObj.mostLiklySubmittedReview = false - - // check if removedReviewBanner - if (!settings.removedReviewBanner && typeof settings.neverShowedReviewBanner !== 'boolean') updateObj.removedReviewBanner = false - - // check if neverShowedReviewBanner - if (!settings.neverShowedReviewBanner && typeof settings.neverShowedReviewBanner !== 'boolean') updateObj.neverShowedReviewBanner = true - - // check if seenInOpalAfterDashbaordUpdate exists - if (!settings.seenInOpalAfterDashbaordUpdate && typeof settings.seenInOpalAfterDashbaordUpdate !== 'number') updateObj.seenInOpalAfterDashbaordUpdate = 0 - - // check if enabledOWAFetch exists - if (!settings.enabledOWAFetch && typeof settings.enabledOWAFetch !== 'boolean') { - updateObj.enabledOWAFetch = false - updateObj.NumberOfUnreadMails = undefined - updateObj.additionalNotificationOnNewMail = false - } - - // check, whether flake state exists. If not, initialize with false. - if (!settings.flakeState && typeof settings.flakeState !== 'boolean') updateObj.flakeState = false - - // check if ShowedFirefoxBanner - if (!settings.showedFirefoxBanner && typeof settings.showedFirefoxBanner !== 'boolean') updateObj.showedFirefoxBanner = false - - // check if showedUnreadMailCounterBanner - if (!settings.showedUnreadMailCounterBanner && typeof settings.showedUnreadMailCounterBanner !== 'boolean') updateObj.showedUnreadMailCounterBanner = false - - // check if openSettingsOnReload - if (!settings.openSettingsOnReload && typeof settings.openSettingsOnReload !== 'boolean') updateObj.openSettingsOnReload = false - - // check if availableRockets - if (!settings.availableRockets) updateObj.availableRockets = ['RI_default'] - - // check if selectedRocketIcon - if (!settings.selectedRocketIcon) updateObj.selectedRocketIcon = '{"id": "RI_default", "link": "assets/icons/RocketIcons/default_128px.png"}' - - // check if hisqisPimpedTable - if (!settings.hisqisPimpedTable && typeof settings.hisqisPimpedTable !== 'boolean') updateObj.hisqisPimpedTable = true - - // if easteregg was discovered in an earlier version: enable and select specific rocket! - const avRockets = settings.availableRockets || [] - if (settings.saved_click_counter > 250 && !avRockets.includes('RI4')) avRockets.push('RI4') - if (settings.saved_click_counter > 2500 && !avRockets.includes('RI5')) avRockets.push('RI5') - if (settings.Rocket === 'colorful' && settings.foundEasteregg === undefined) { - updateObj.foundEasteregg = true - updateObj.selectedRocketIcon = '{"id": "RI3", "link": "assets/icons/RocketIcons/3_120px.png"}' - avRockets.push('RI3') - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.browserAction.setIcon({ path: 'assets/icons/RocketIcons/3_120px.png' }, resolve)) - } - updateObj.availableRockets = avRockets - - // seen customized studiengang - if (!settings.updateCustomizeStudiengang && typeof settings.updateCustomizeStudiengang !== 'boolean') updateObj.updateCustomizeStudiengang = false - - // selected studiengang - if (!settings.studiengang) updateObj.studiengang = 'general' - - // selected theme - if (!settings.theme) updateObj.theme = 'system' - - // if not yet invite shown: show, and set shown to true - // if(!settings.TUfastCampInvite1) { - // const today = new Date() - // const max_invite_date = new Date(2021, 8, 30) // 27.09.2021; month is zero based - // if (today < max_invite_date) { - // updateObj.TUfastCampInvite1 = true - // Promisified until usage of Manifest V3 - // await new Promise((resolve) => chrome.tabs.create({ url: 'TUfastCamp.html' }, resolve)) - // } - // } - - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.set(updateObj, resolve)) - break - } - default: - console.log('Other install events within the browser for TUfast.') - break - } -}) - -// checks, if user currently uses owa in browser -async function owaIsOpened () { - const uri = 'msx.tu-dresden.de' - const tabs = await getAllChromeTabs() - // Find element with msx in uri, -1 if none found - if (tabs.findIndex((element) => element.url.includes(uri)) >= 0) { - console.log('currently opened owa') - return true - } else return false -} - -function getAllChromeTabs () { - // Promisified until usage of Manifest V3 - return new Promise((resolve) => chrome.tabs.query({}, resolve)) -} - -// check if user stored login data -// eslint-disable-next-line no-unused-vars -async function loginDataExists (platform = 'zih') { - const { user, pass } = await getUserData(platform) - return !!(user && pass) -} - -// start OWA fetch funtion based on interval -async function enableOWAFetch () { - // first, clear all alarms - console.log('starting to fetch from owa...') - await owaFetch() - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.alarms.clear('fetchOWAAlarm', resolve)) - chrome.alarms.create('fetchOWAAlarm', { delayInMinutes: 1, periodInMinutes: 5 }) - chrome.alarms.onAlarm.addListener(async (alarm) => { - if (alarm.name === 'fetchOWAAlarm') await owaFetch() - }) -} - -async function owaFetch () { - // dont logout if user is currently using owa in browser - const logout = !(await owaIsOpened()) - console.log('executing fetch ...') - - // get user data - const { user, pass } = await getUserData('zih') - // call fetch - const mailInfoJson = await fetchOWA(user, pass, logout) - // check # of unread mails - const numberUnreadMails = countUnreadMsg(mailInfoJson) - console.log('Unread mails in OWA: ' + numberUnreadMails) - - // alert on new Mail - // Promisified until usage of Manifest V3 - const result = await new Promise((resolve) => chrome.storage.local.get(['NumberOfUnreadMails', 'additionalNotificationOnNewMail'], resolve)) - if (!result.NumberOfUnreadMails !== undefined && result.additionalNotificationOnNewMail) { - if (result.NumberOfUnreadMails < numberUnreadMails) { - if (confirm("Neue Mail in deinem TU Dresden Postfach!\nDruecke 'Ok' um OWA zu oeffnen.")) { - const url = 'https://msx.tu-dresden.de/owa/auth/logon.aspx?url=https%3a%2f%2fmsx.tu-dresden.de%2fowa&reason=0' - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.tabs.create({ url }, resolve)) - } - } - } - - // set badge and local storage - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.set({ NumberOfUnreadMails: numberUnreadMails }, resolve)) - await setBadgeUnreadMails(numberUnreadMails) -} - -async function disableOwaFetch () { - console.log('stopped owa connection') - await setBadgeUnreadMails(0) - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.remove(['NumberOfUnreadMails'], resolve)) - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.alarms.clear('fetchOWAAlarm', resolve)) -} - -async function readMailOWA (NrUnreadMails) { - // set badge and local storage - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.set({ NumberOfUnreadMails: NrUnreadMails }, resolve)) - await setBadgeUnreadMails(NrUnreadMails) -} - -async function setBadgeUnreadMails (numberUnreadMails) { - // set badge - if (!numberUnreadMails) { - await showBadge('', '#4cb749') - } else if (numberUnreadMails > 99) { - await showBadge('99+', '#4cb749') - } else { - await showBadge(numberUnreadMails.toString(), '#4cb749') - } -} - -// command listener -// this listener behaves weirdly with an async function so it just calls async functions and returns true -chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => { - switch (request.cmd) { - case 'show_ok_badge': - // show_badge('Login', '#4cb749', request.timeout) - break - case 'no_login_data': - // alert("Bitte gib deinen Nutzernamen und Passwort in der TUfast Erweiterung an! Klicke dafür auf das Erweiterungssymbol oben rechts.") - // show_badge("Error", '#ff0000', 10000) - break - case 'perform_login': - break - case 'clear_badge': - // show_badge("", "#ffffff", 0) - break - case 'save_clicks': - saveClicks(request.click_count) - break - case 'get_user_data': { - const platform = request.platform || 'zih' - getUserData(platform).then(userData => sendResponse(userData)) - break - } - case 'set_user_data': { - const platform = request.platform || 'zih' - setUserData(request.userData, platform).then(() => sendResponse(true)) - break - } - case 'check_user_data': { - userDataExists(request.platform).then(response => sendResponse(response)) - break - } - case 'read_mail_owa': - readMailOWA(request.NrUnreadMails) - break - case 'logged_out': - loggedOut(request.portal) - break - case 'disable_owa_fetch': - disableOwaFetch() - break - case 'reload_extension': - chrome.runtime.reload() - break - case 'save_courses': - saveCourses(request.course_list) - break - case 'open_settings_page': - openSettingsPage(request.params) - break - case 'open_share_page': - openSharePage() - break - case 'open_shortcut_settings': - if (isFirefox) { - chrome.tabs.create({ url: 'https://support.mozilla.org/de/kb/tastenkombinationen-fur-erweiterungen-verwalten' }) - } else { - // for chrome and everything else - chrome.tabs.create({ url: 'chrome://extensions/shortcuts' }) - } - break - case 'toggle_pdf_inline_setting': - enableHeaderListener(request.enabled) - break - case 'update_rocket_logo_easteregg': - chrome.browserAction.setIcon({ path: 'assets/icons/RocketIcons/3_120px.png' }) - break - default: - console.log('Cmd not found!') - break - } - return true // required for async sendResponse -}) - -// register hotkeys -chrome.commands.onCommand.addListener(async (command) => { - console.log('Detected command: ' + command) - switch (command) { - case 'open_opal_hotkey': - chrome.tabs.update({ url: 'https://bildungsportal.sachsen.de/opal/home/' }) - await saveClicks(2) - break - case 'open_owa_hotkey': - chrome.tabs.update({ url: 'https://msx.tu-dresden.de/owa/' }) - await saveClicks(2) - break - case 'open_jexam_hotkey': - chrome.tabs.update({ url: 'https://jexam.inf.tu-dresden.de/' }) - await saveClicks(2) - break - } -}) - -/** - * enable or disable the header listener - * modify http header from opal, to view pdf in browser without the need to download it - * @param {true} enabled flag to enable/ disable listener - */ -function enableHeaderListener (enabled) { - if (enabled) { - chrome.webRequest.onHeadersReceived.addListener( - headerListenerFunc, - { - urls: [ - 'https://bildungsportal.sachsen.de/opal/downloadering*', - 'https://bildungsportal.sachsen.de/opal/*.pdf' - ] - }, - ['blocking', 'responseHeaders'] - ) - } else { - chrome.webRequest.onHeadersReceived.removeListener(headerListenerFunc) - } -} - -function headerListenerFunc (details) { - const header = details.responseHeaders.find( - e => e.name.toLowerCase() === 'content-disposition' - ) - if (!header?.value.includes('.pdf')) return // only for pdf - header.value = 'inline' - return { responseHeaders: details.responseHeaders } -} - -// open settings (=options) page, if required set params -async function openSettingsPage (params) { - if (params) { - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.set({ openSettingsPageParam: params }, resolve)) - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.runtime.openOptionsPage(resolve)) - } else { - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.runtime.openOptionsPage(resolve)) - } -} - -async function openSharePage () { - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.tabs.create({ url: 'share.html' }, resolve)) -} - -// timeout is 2000 default -async function loggedOut (portal) { - const timeout = portal === 'loggedOutCloudstore' ? 7000 : 2000 - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.set({ [portal]: true }, resolve)) - setTimeout(async () => { - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.set({ [portal]: false }, resolve)) - }, timeout) -} - -// show badge -async function showBadge (text, color, _timeout) { - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.browserAction.setBadgeText({ text }, resolve)) - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.browserAction.setBadgeBackgroundColor({ color }, resolve)) - // setTimeout(() => { - // chrome.browserAction.setBadgeText({text: ""}); - // }, timeout); -} - -// save_click_counter -async function saveClicks (counter) { - // await new Promise((resolve) => { - // load number of saved clicks and add counter! - let savedClicks = 0 - // Promisified until usage of Manifest V3 - const result = await new Promise((resolve) => chrome.storage.local.get(['saved_click_counter'], resolve)) - savedClicks = (result.saved_click_counter === undefined) ? 0 : result.saved_click_counter - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.set({ saved_click_counter: savedClicks + counter }, () => { - console.log('You just saved yourself ' + counter + ' clicks!') - resolve() - })) - // make rocketIcons available if appropriate - // Promisified until usage of Manifest V3 - const resp = await new Promise((resolve) => chrome.storage.local.get(['availableRockets'], resolve)) - const avRockets = resp.availableRockets - if (result.saved_click_counter > 250 && !avRockets.includes('RI4')) avRockets.push('RI4') - if (result.saved_click_counter > 2500 && !avRockets.includes('RI5')) avRockets.push('RI5') - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.set({ availableRockets: avRockets }, resolve)) -} - -/// ///////////// FUNCTIONS FOR ENCRYPTION AND USERDATA HANDLING //////////////// -// info: user = username | pass = password - -// create hash from input-string (can also be json of course) -// output hash is always of same length and is of type buffer -async function hashDigest (string) { - return await crypto.subtle.digest('SHA-256', (new TextEncoder()).encode(string)) -} - -// get key for encryption (format: buffer) -async function getKeyBuffer () { - // async fetch of system information - // Promisified until usage of Manifest V3 - const sysInfo = await new Promise((resolve) => { - let sysInfo = '' - - // key differs between browsers, because different APIs - if (isFirefox) { - sysInfo += window.navigator.hardwareConcurrency - chrome.runtime.getPlatformInfo((info) => { - sysInfo += JSON.stringify(info) - resolve(sysInfo) - }) - } else { - // chrome, edge and everything else - chrome.system.cpu.getInfo((info) => { - delete info.processors - delete info.temperatures - sysInfo += JSON.stringify(info) - chrome.runtime.getPlatformInfo((info) => { - sysInfo += JSON.stringify(info) - resolve(sysInfo) - }) - }) - } - }) - - // create key - return await crypto.subtle.importKey('raw', await hashDigest(sysInfo), - { name: 'AES-CBC' }, - false, - ['encrypt', 'decrypt']) -} - -// this functions saved user login-data locally. -// user data is encrypted using the crpyto-js library (aes-cbc). The encryption key is created from pc-information with system.cpu -// a lot of encoding and transforming needs to be done, in order to provide all values in the right format. -async function setUserData (userData, platform = 'zih') { - if (!userData || !userData.user || !userData.pass || !platform) return - - // local function so it's not easily called from elsewhere - const encode = async (decoded) => { - const dataEncoded = (new TextEncoder()).encode(decoded) - const keyBuffer = await getKeyBuffer() - let iv = crypto.getRandomValues(new Uint8Array(16)) - - // encrypt - let dataEnc = await crypto.subtle.encrypt( - { - name: 'AES-CBC', - iv: iv - }, - keyBuffer, - dataEncoded - ) - - // adjust format to save encrypted data in local storage - dataEnc = Array.from(new Uint8Array(dataEnc)) - dataEnc = dataEnc.map(byte => String.fromCharCode(byte)).join('') - dataEnc = btoa(dataEnc) - iv = Array.from(iv).map(b => ('00' + b.toString(16)).slice(-2)).join('') - return iv + dataEnc - } - - const user = await encode(userData.user) - const pass = await encode(userData.pass) - - let dataObj - try { - // Promisified until usage of Manifest V3 - const data = await new Promise((resolve) => chrome.storage.local.get(['udata'], (data) => resolve(data.udata))) - if (typeof data !== 'string') throw Error() - dataObj = JSON.parse(data) - } catch { - // data field is undefined or broken -> reset it - dataObj = {} - } - dataObj[platform] = { user, pass } - - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.set({ udata: JSON.stringify(dataObj) }, resolve)) -} - -// check if username, password exist -async function userDataExists (platform) { - if (typeof platform === 'string') { - // Query for a specific platform - const { user, pass } = await getUserData(platform) - return !!(user && pass) - } else { - // Query for any platform - const data = await new Promise((resolve) => chrome.storage.local.get(['udata'], (data) => resolve(data.udata))) - if (typeof data !== 'string') return false - - try { - const dataJson = JSON.parse(data) - for (const platform of Object.keys(dataJson)) { - const { user, pass } = await getUserData(platform) - if (user && pass) return true - } - } catch {} - } - return false -} - -// return {user: string, pass: string} -// decrypt and return user data -// a lot of encoding and transforming needs to be done, in order to provide all values in the right format -async function getUserData (platform = 'zih') { - // get required data for decryption - const keyBuffer = await getKeyBuffer() - // async fetch of user data - // Promisified until usage of Manifest V3 - const data = await new Promise((resolve) => chrome.storage.local.get(['udata'], (data) => resolve(data.udata))) - - // check if Data exists, else return - if (typeof data !== 'string' || !platform) { - return ({ user: undefined, pass: undefined }) - } - - // local function so it's not easily called from elsewhere - const decode = async (encoded) => { - if (!encoded) return undefined - let iv = encoded.slice(0, 32).match(/.{2}/g).map(byte => parseInt(byte, 16)) - iv = new Uint8Array(iv) - let dataEncrypted = atob(encoded.slice(32)) - dataEncrypted = new Uint8Array(dataEncrypted.match(/[\s\S]/g).map(ch => ch.charCodeAt(0))) - - // decrypt - const decoded = await crypto.subtle.decrypt( - { - name: 'AES-CBC', - iv: iv - }, - keyBuffer, - dataEncrypted - ) - - // adjust to useable format - return new TextDecoder().decode(decoded) - } - - try { - const userDataJson = JSON.parse(data) - const encUser = userDataJson[platform].user - const encPass = userDataJson[platform].pass - return { user: await decode(encUser), pass: await decode(encPass) } - } catch { - return { user: undefined, pass: undefined } - } -} - -// return {user: string, pass: string} -// This is the old method to get the user data. It will be preserved until probably every installation uses the new format -async function getUserDataLagacy () { - // get required data for decryption - const keyBuffer = await getKeyBuffer() - // async fetch of user data - // Promisified until usage of Manifest V3 - const data = await new Promise((resolve) => chrome.storage.local.get(['Data'], (data) => resolve(data.Data))) - - // check if Data exists, else return - if (data === undefined || data === 'undefined') { - return ({ asdf: undefined, fdsa: undefined }) - } - let iv = data.slice(0, 32).match(/.{2}/g).map(byte => parseInt(byte, 16)) - iv = new Uint8Array(iv) - let userDataEncrypted = atob(data.slice(32)) - userDataEncrypted = new Uint8Array(userDataEncrypted.match(/[\s\S]/g).map(ch => ch.charCodeAt(0))) - - // decrypt - let userData = await crypto.subtle.decrypt( - { - name: 'AES-CBC', - iv: iv - }, - keyBuffer, - userDataEncrypted - ) - - // adjust to useable format - userData = new TextDecoder().decode(userData) - userData = userData.split('@@@@@') - return ({ asdf: userData[0], fdsa: userData[1] }) -} - -/// ///////////// END FUNCTIONS FOR ENCRYPTION AND USERDATA HANDLING //////////////// - -// save parsed courses -// course_list = {type:"", list:[{link:link, name: name}, ...]} -async function saveCourses (courseList) { - courseList.list.sort((a, b) => (a.name > b.name) ? 1 : -1) - switch (courseList.type) { - case 'favoriten': - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.set({ favoriten: JSON.stringify(courseList.list) }, resolve)) - console.log('saved Favoriten in TUfast') - break - case 'meine_kurse': - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.set({ meine_kurse: JSON.stringify(courseList.list) }, resolve)) - console.log('saved Meine Kurse in TUfast') - break - } -} - -// get parsed courses -// return course_list = [{link:link, name: name}, ...] -// function loadCourses (type) { -// switch (type) { -// case 'favoriten': -// chrome.storage.local.get(['favoriten'], (result) => { -// console.log(JSON.parse(result.favoriten)) -// }) -// break -// case 'meine_kurse': -// chrome.storage.local.get(['meine_kurse'], (result) => { -// console.log(JSON.parse(result.meine_kurse)) -// }) -// break -// default: -// break -// } -// } - -// function for custom URIEncoding -function customURIEncoding (string) { - string = encodeURIComponent(string) - string = string.replace('!', '%21').replace("'", '%27').replace('(', '%28').replace(')', '%29').replace('~', '%7E') - return string -} - -// function to log msx.tu-dresden.de/owa/ and retrieve the .json containing information about EMails -async function fetchOWA (username, password, logout) { - // encodeURIComponent and encodeURI are not working for all chars. See documentation. Thats why I implemented custom encoding. - username = customURIEncoding(username) - password = customURIEncoding(password) - - // login - await fetch('https://msx.tu-dresden.de/owa/auth.owa', { - headers: { - accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', - 'accept-language': 'de-DE,de;q=0.9,en-DE;q=0.8,en-GB;q=0.7,en-US;q=0.6,en;q=0.5', - 'cache-control': 'max-age=0', - 'content-type': 'application/x-www-form-urlencoded', - 'Access-Control-Allow-Origin': '*', - 'sec-fetch-dest': 'document', - 'sec-fetch-mode': 'navigate', - 'sec-fetch-site': 'same-origin', - 'sec-fetch-user': '?1', - 'upgrade-insecure-requests': '1' - }, - referrer: 'https://msx.tu-dresden.de/owa/auth/logon.aspx?replaceCurrent=1&url=https%3a%2f%2fmsx.tu-dresden.de%2fowa%2f%23authRedirect%3dtrue', - referrerPolicy: 'strict-origin-when-cross-origin', - 'Access-Control-Allow-Origin': '*', - body: 'destination=https%3A%2F%2Fmsx.tu-dresden.de%2Fowa%2F%23authRedirect%3Dtrue&flags=4&forcedownlevel=0&username=' + username + '%40msx.tu-dresden.de&password=' + password + '&passwordText=&isUtf8=1', - method: 'POST', - mode: 'no-cors', - credentials: 'include' - }) - - const owaResp = await fetch('https://msx.tu-dresden.de/owa/', { - headers: { - accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', - 'accept-language': 'de-DE,de;q=0.9,en-DE;q=0.8,en-GB;q=0.7,en-US;q=0.6,en;q=0.5', - 'cache-control': 'max-age=0', - 'sec-fetch-dest': 'document', - 'sec-fetch-mode': 'navigate', - 'Access-Control-Allow-Origin': '*', - 'sec-fetch-site': 'same-origin', - 'sec-fetch-user': '?1', - 'upgrade-insecure-requests': '1' - }, - referrer: 'https://msx.tu-dresden.de/owa/auth/logon.aspx?replaceCurrent=1&url=https%3a%2f%2fmsx.tu-dresden.de%2fowa', - referrerPolicy: 'strict-origin-when-cross-origin', - body: null, - method: 'GET', - 'Access-Control-Allow-Origin': '*', - mode: 'cors', - credentials: 'include' - }) - - const respText = await owaResp.text() - const tmp = respText.split("window.clientId = '")[1] - const clientId = tmp.split("'")[0] - const corrId = clientId + '_' + (new Date()).getTime() - console.log('corrID: ' + corrId) - - const mailInfoRsp = await fetch('https://msx.tu-dresden.de/owa/sessiondata.ashx?appcacheclient=0', { - headers: { - accept: '*/*', - 'accept-language': 'de-DE,de;q=0.9,en-DE;q=0.8,en-GB;q=0.7,en-US;q=0.6,en;q=0.5', - 'sec-fetch-dest': 'empty', - 'sec-fetch-mode': 'cors', - 'Access-Control-Allow-Origin': '*', - 'sec-fetch-site': 'same-origin', - 'x-owa-correlationid': corrId, - 'x-owa-smimeinstalled': '1' - }, - referrer: 'https://msx.tu-dresden.de/owa/', - referrerPolicy: 'strict-origin-when-cross-origin', - 'Access-Control-Allow-Origin': '*', - body: null, - method: 'POST', - mode: 'cors', - credentials: 'include' - }) - - const mailInfoJson = await mailInfoRsp.json() - - // only logout, if user is not using owa in browser session - if (logout) { - console.log('Logging out from owa..') - await fetch('https://msx.tu-dresden.de/owa/logoff.owa', { - headers: { - accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', - 'accept-language': 'de-DE,de;q=0.9,en-DE;q=0.8,en-GB;q=0.7,en-US;q=0.6,en;q=0.5', - 'sec-fetch-dest': 'document', - 'Access-Control-Allow-Origin': '*', - 'sec-fetch-mode': 'navigate', - 'sec-fetch-site': 'same-origin', - 'sec-fetch-user': '?1', - 'upgrade-insecure-requests': '1' - }, - referrer: 'https://msx.tu-dresden.de/owa/', - referrerPolicy: 'strict-origin-when-cross-origin', - 'Access-Control-Allow-Origin': '*', - body: null, - method: 'GET', - mode: 'cors', - credentials: 'include' - }) - } - - return mailInfoJson -} - -// extract number of unread messages in owa -function countUnreadMsg (json) { - const folder = json.findFolders.Body.ResponseMessages.Items[0].RootFolder.Folders.find(obj => obj.DisplayName === 'Inbox' || obj.DisplayName === 'Posteingang') - return folder.UnreadCount -} diff --git a/src/background.ts b/src/background.ts new file mode 100644 index 00000000..b25a212f --- /dev/null +++ b/src/background.ts @@ -0,0 +1,392 @@ +'use strict' +import * as credentials from './modules/credentials' +import * as owaFetch from './modules/owaFetch' + +// eslint-disable-next-line no-unused-vars +const isFirefox = !!(typeof globalThis.browser !== 'undefined' && globalThis.browser.runtime && globalThis.browser.runtime.getBrowserInfo) + +// On installed/updated function +chrome.runtime.onInstalled.addListener(async (details) => { + const reason = details.reason + switch (reason) { + case 'install': + console.log('TUfast installed') + await chrome.storage.local.set({ + dashboardDisplay: 'favoriten', + fwdEnabled: true, + encryptionLevel: 3, + availableRockets: ['RI_default'], + selectedRocketIcon: '{"id": "RI_default", "link": "assets/icons/RocketIcons/default_128px.png"}', + theme: 'system', + studiengang: 'general' + }) + await openSettingsPage('first_visit') + break + case 'update': { + // Promisified until usage of Manifest V3 + const currentSettings = await new Promise((resolve) => chrome.storage.local.get([ + 'dashboardDisplay', + 'fwdEnabled', + 'encryptionLevel', + 'encryption_level', // legacy + 'availableRockets', + 'selectedRocketIcon', + 'theme', + 'studiengang', + 'hisqisPimpedTable', + 'savedClickCounter', + 'saved_click_counter', // legacy + 'Rocket', // legacy + 'foundEasteregg', + 'bannersShown', // new banners + // Old opal banners + 'showedUnreadMailCounterBanner', + 'removedUnlockRocketsBanner', + 'showedOpalCustomizeBanner', + 'removedReviewBanner', + 'showedKeyboardBanner2' + ], resolve)) + + const updateObj: any = {} + + // Setting the defaults if keys do not exist + if (typeof currentSettings.dashboardDisplay === 'undefined') updateObj.dashboardDisplay = 'favoriten' + if (typeof currentSettings.fwdEnabled === 'undefined') updateObj.fwdEnabled = true + if (typeof currentSettings.hisqisPimpedTable === 'undefined') updateObj.hisqisPimpedTable = true + if (typeof currentSettings.theme === 'undefined') updateObj.theme = 'system' + if (typeof currentSettings.studiengang === 'undefined') updateObj.studiengang = 'general' + if (typeof currentSettings.selectedRocketIcon === 'undefined') updateObj.selectedRocketIcon = '{"id": "RI_default", "link": "assets/icons/RocketIcons/default_128px.png"}' + + // Upgrading encryption + // Currently "encryptionLevel" can't be lower than 3, but "encryption_level" can + if (currentSettings.encryption_level !== 3) { + switch (currentSettings.encryption_level) { + case 1: { + // This branch probably/hopefully will not be called anymore... + console.log('Upgrading encryption standard from level 1 to level 3...') + // Promisified until usage of Manifest V3 + const userData = await new Promise((resolve) => chrome.storage.local.get(['asdf', 'fdsa'], resolve)) + await credentials.setUserData({ user: atob(userData.asdf), pass: atob(userData.fdsa) }, 'zih') + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.storage.local.remove(['asdf', 'fdsa'], resolve)) + break + } + case 2: { + const { user, pass } = await credentials.getUserDataLagacy() + await credentials.setUserData({ user, pass }, 'zih') + // Delete old user data + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.storage.local.remove(['Data'], resolve)) + break + } + } + updateObj.encryptionLevel = 3 + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.storage.local.remove(['encryption_level'], resolve)) + } + + // Upgrading saved_clicks_counter to savedClicksCounter + const savedClicks = currentSettings.savedClickCounter || currentSettings.saved_click_counter + if (typeof currentSettings.savedClickCounter === 'undefined' && typeof currentSettings.saved_click_counter !== 'undefined') { + updateObj.savedClickCounter = savedClicks + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.storage.local.remove(['saved_click_counter'], resolve)) + } + + // Upgrading availableRockets + const avRockets = currentSettings.availableRockets || ['RI_default'] + if (savedClicks > 250 && !avRockets.includes('RI4')) avRockets.push('RI4') + if (savedClicks > 2500 && !avRockets.includes('RI5')) avRockets.push('RI5') + if (currentSettings.Rocket === 'colorful' && currentSettings.foundEasteregg === undefined) { + updateObj.foundEasteregg = true + updateObj.selectedRocketIcon = '{"id": "RI3", "link": "assets/icons/RocketIcons/3_120px.png"}' + avRockets.push('RI3') + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.browserAction.setIcon({ path: 'assets/icons/RocketIcons/3_128px.png' }, resolve)) + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.storage.local.remove(['Rocket'], resolve)) + } + updateObj.availableRockets = avRockets + + // Migrating which opal banners where already shown + const bannersShown: string[] = currentSettings.bannersShown || [] + if (currentSettings.showedUnreadMailCounterBanner && !bannersShown.includes('mailCount')) bannersShown.push('mailCount') + if (currentSettings.removedUnlockRocketsBanner && !bannersShown.includes('customizeRockets')) bannersShown.push('customizeRockets') + if (currentSettings.showedOpalCustomizeBanner && !bannersShown.includes('customizeOpal')) bannersShown.push('customizeOpal') + if (currentSettings.removedReviewBanner && !bannersShown.includes('submitReview')) bannersShown.push('submitReview') + if (currentSettings.showedKeyboardBanner2 && !bannersShown.includes('keyboardShortcuts')) bannersShown.push('keyboardShortcuts') + updateObj.bannersShown = bannersShown + + // Write back to storage + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.storage.local.set(updateObj, resolve)) + break + } + } +}) + +// register hotkeys +chrome.commands.onCommand.addListener(async (command) => { + console.log('Detected command: ' + command) + switch (command) { + case 'open_opal_hotkey': + chrome.tabs.update({ url: 'https://bildungsportal.sachsen.de/opal/home/' }) + await saveClicks(2) + break + case 'open_owa_hotkey': + chrome.tabs.update({ url: 'https://msx.tu-dresden.de/owa/' }) + await saveClicks(2) + break + case 'open_jexam_hotkey': + chrome.tabs.update({ url: 'https://jexam.inf.tu-dresden.de/' }) + await saveClicks(2) + break + } +}) + +// Set icon on startup +chrome.storage.local.get(['selectedRocketIcon'], (resp) => { + try { + const r = JSON.parse(resp.selectedRocketIcon) + chrome.browserAction.setIcon({ + path: r.link + }) + } catch (e) { + console.log(`Cannot parse rocket icon: ${resp}`) + chrome.browserAction.setIcon({ + path: 'assets/icons/RocketIcons/default_128px.png' + }) + } +}) + +// start fetchOWA if activated and user data exists +chrome.storage.local.get(['enabledOWAFetch', 'numberOfUnreadMails', 'additionalNotificationOnNewMail'], async (result: any) => { + if (await credentials.userDataExists('zih') && result.enabledOWAFetch) { + await owaFetch.enableOWAFetch() + } + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.permissions.contains({ permissions: ['notifications'] }, (granted: boolean) => { + if (granted && result.additionalNotificationOnNewMail) { + // register listener for owaFetch notifications + chrome.notifications.onClicked.addListener(async (id) => { + if (id === 'tuFastNewEmailNotification') { + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.tabs.create({ url: 'https://msx.tu-dresden.de/owa/' }, resolve)) + } + }) + } + resolve() + })) +}) + +// Register header listener +chrome.storage.local.get(['pdfInNewTab'], (result) => { + if (result.pdfInNewTab) { + enableHeaderListener(true) + } +}) + +// reset banner for gOPAL on 20. 10. +const d = new Date(new Date().getFullYear(), 10, 20) +if (d.getTime() - Date.now() < 0) d.setFullYear(d.getFullYear() + 1) +chrome.alarms.create('resetGOpalBanner', { when: d.getTime() }) +chrome.alarms.onAlarm.addListener(async (alarm) => { + if (alarm.name === 'resetGOpalBanner') { + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.storage.local.set({ closedMsg1: false }, resolve)) + } +}) + +// DOESNT WORK IN RELEASE VERSION +chrome.storage.local.get(['openSettingsOnReload'], async (resp) => { + if (resp.openSettingsOnReload) await openSettingsPage() + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.storage.local.set({ openSettingsOnReload: false }, resolve)) +}) + +// command listener +// This listener can send async responses. If this is desired it must return true. +chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => { + switch (request.cmd) { + case 'save_clicks': + // The first one is legacy and should not be used anymore + saveClicks(request.click_count || request.clickCount) + break + case 'get_user_data': + // Asynchronous response + credentials.getUserData(request.platform || 'zih').then(sendResponse) + return true // required for async sendResponse + case 'set_user_data': + // Asynchronous response + credentials.setUserData(request.userData, request.platform || 'zih').then(() => sendResponse(true)) + return true // required for async sendResponse + case 'check_user_data': + // Asynchronous response + credentials.userDataExists(request.platform).then(sendResponse) + return true // required for async sendResponse + case 'read_mail_owa': + owaFetch.readMailOWA(request.nrOfUnreadMail || 0) + break + case 'disable_owa_fetch': + owaFetch.disableOwaFetch() + break + case 'reload_extension': + chrome.runtime.reload() + break + case 'open_settings_page': + openSettingsPage(request.params) + break + case 'open_share_page': + openSharePage() + break + case 'open_shortcut_settings': + if (isFirefox) { + chrome.tabs.create({ url: 'https://support.mozilla.org/de/kb/tastenkombinationen-fur-erweiterungen-verwalten' }) + } else { + // for chrome and everything else + chrome.tabs.create({ url: 'chrome://extensions/shortcuts' }) + } + break + case 'toggle_pdf_inline_setting': + enableHeaderListener(request.enabled) + break + case 'update_rocket_logo_easteregg': + chrome.browserAction.setIcon({ path: 'assets/icons/RocketIcons/3_120px.png' }) + break + case 'logout_idp': + logoutIdp(request.logoutDuration) + break + case 'easteregg_found': + eastereggFound() + break + default: + console.log(`Cmd not found "${request.cmd}"!`) + break + } + return false // no async sendResponse will be fired +}) + +/** + * enable or disable the header listener + * modify http header from opal, to view pdf in browser without the need to download it + * @param {boolean} enabled flag to enable/ disable listener + */ +function enableHeaderListener (enabled: boolean) { + if (enabled) { + chrome.webRequest.onHeadersReceived.addListener( + headerListenerFunc, + { + urls: [ + 'https://bildungsportal.sachsen.de/opal/downloadering*', + 'https://bildungsportal.sachsen.de/opal/*.pdf' + ] + }, + ['blocking', 'responseHeaders'] + ) + } else { + chrome.webRequest.onHeadersReceived.removeListener(headerListenerFunc) + } +} + +function headerListenerFunc (details: chrome.webRequest.WebResponseHeadersDetails) { + if (!details.responseHeaders) return + const header = details.responseHeaders.find( + e => e.name.toLowerCase() === 'content-disposition' + ) + if (!header?.value?.includes('.pdf')) return // only for pdf + header.value = 'inline' + return { responseHeaders: details.responseHeaders } +} + +// open settings (=options) page, if required set params +async function openSettingsPage (params?: string) { + if (params) { + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.storage.local.set({ openSettingsPageParam: params }, resolve)) + } + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.runtime.openOptionsPage(resolve)) +} + +async function openSharePage () { + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.tabs.create({ url: 'share.html' }, resolve)) +} + +// save_click_counter +async function saveClicks (counter: number) { + // load number of saved clicks and add counter! + // Promisified until usage of Manifest V3 + const result = await new Promise((resolve) => chrome.storage.local.get(['savedClickCounter'], resolve)) + const savedClickCounter = (typeof result.savedClickCounter === 'undefined') ? counter : result.savedClickCounter + counter + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.storage.local.set({ savedClickCounter }, () => { + console.log('Saved ' + counter + ' clicks!') + resolve() + })) + // make rocketIcons available if appropriate + // Promisified until usage of Manifest V3 + const { availableRockets } = await new Promise((resolve) => chrome.storage.local.get(['availableRockets'], resolve)) + if (savedClickCounter > 250 && !availableRockets.includes('RI4')) availableRockets.push('RI4') + if (savedClickCounter > 2500 && !availableRockets.includes('RI5')) availableRockets.push('RI5') + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.storage.local.set({ availableRockets }, resolve)) +} + +// logout function for idp +async function logoutIdp (logoutDuration: number = 5) { + // Promisified until usage of Manifest V3 + const granted = await new Promise((resolve) => chrome.permissions.request({ permissions: ['cookies'] }, resolve)) + if (!granted) return + + // Set the logout cookie for idp + const date = new Date() + date.setMinutes(date.getMinutes() + logoutDuration) + await new Promise((resolve) => chrome.cookies.set({ + url: 'https://idp.tu-dresden.de', + name: 'idpLoggedOut', + value: 'true', + secure: true, + expirationDate: date.getTime() / 1000 + }, resolve)) + + // Log out + // Promisified until usage of Manifest V3 + const { idpLogoutEnabled } = await new Promise((resolve) => chrome.storage.local.get(['idpLogoutEnabled'], resolve)) + if (!idpLogoutEnabled) return + + // get session cookie + const sessionCookie = await new Promise((resolve) => chrome.cookies.get({ + url: 'https://idp.tu-dresden.de', + name: 'JSESSIONID' + }, resolve)) + if (!sessionCookie) return + + const redirect = await fetch('https://idp.tu-dresden.de/idp/profile/Logout', { + headers: { + Cookie: `JSESSIONID=${sessionCookie.value}` + } + }) + await fetch(redirect.url, { + headers: { + Cookie: `JSESSIONID=${sessionCookie.value}` + }, + method: 'POST' + }) +} + +// Function called when the easteregg is found +async function eastereggFound () { + // Promisified until usage of Manifest V3 + const { availableRockets } = await new Promise((resolve) => chrome.storage.local.get(['availableRockets'], resolve)) + availableRockets.push('RI3') + + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.storage.local.set({ + foundEasteregg: true, + selectedRocketIcon: '{"id": "RI3", "link": "/assets/icons/RocketIcons/7_128px.png"}', + availableRockets + }, resolve)) + + chrome.browserAction.setIcon({ path: '/assets/icons/RocketIcons/7_128px.png' }) +} diff --git a/src/contentScripts/bildungsportal.js b/src/contentScripts/bildungsportal.js deleted file mode 100644 index 5ef2cd48..00000000 --- a/src/contentScripts/bildungsportal.js +++ /dev/null @@ -1,33 +0,0 @@ -chrome.storage.local.get(['isEnabled', 'loggedOutOpal'], (result) => { - if (result.isEnabled && !(result.loggedOutOpal)) { - // when pop-up shows - document.addEventListener('DOMNodeInserted', () => { - // select TU Dresden from selector - if (document.getElementsByName('content:container:login:shibAuthForm:wayfselection') && document.getElementsByName('content:container:login:shibAuthForm:wayfselection')[0]) { - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - const selectionList = document.getElementsByName('content:container:login:shibAuthForm:wayfselection')[0] - // The following spread operator is needed because HTMLCollection has no "find" - const element = [...selectionList].find((el) => el.textContent === 'TU Dresden' || el.textContent === 'Technsiche Universität Dresden') - document.getElementsByName('content:container:login:shibAuthForm:wayfselection')[0].value = element.value - } - // submit selected - if (document.getElementsByName('content:container:login:shibAuthForm:shibLogin')[0]) { - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - chrome.runtime.sendMessage({ cmd: 'show_ok_badge', timeout: 4000 }) - document.getElementsByName('content:container:login:shibAuthForm:shibLogin')[0].click() - } - }, false) - - // start login process - window.addEventListener('load', () => { - if (document.getElementsByClassName('btn btn-sm')[1].innerText.includes('Login')) { - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - chrome.runtime.sendMessage({ cmd: 'show_ok_badge', timeout: 4000 }) - document.getElementsByClassName('btn btn-sm')[1].click() - } - }, true) - console.log('Auto Login to Opal.') - } else if (result.loggedOutOpal) { - chrome.storage.local.set({ loggedOutOpal: false }) - } -}) diff --git a/src/contentScripts/bildungsportal_insertLogo.js b/src/contentScripts/bildungsportal_insertLogo.js deleted file mode 100644 index a1c88a6b..00000000 --- a/src/contentScripts/bildungsportal_insertLogo.js +++ /dev/null @@ -1,315 +0,0 @@ -/* - { selectedRocketIcon: '{"id": "RI_default", "link": "RocketIcons/default_128px"}' } -*/ -// No async await on top level -chrome.storage.local.get(['isEnabled', 'fwdEnabled', 'PRObadge', 'flakeState', 'selectedRocketIcon', 'foundEasteregg'], (result) => { - if (result.isEnabled || result.fwdEnabled) { - // parse selectedRocketIcon - const selectedRocketIcon = JSON.parse(result.selectedRocketIcon) - - // decide which overlay to show - let christmasTime = false - const d = new Date() - const month = d.getMonth() + 1 // starts at 0 - const day = d.getDate() - if (month === 12 && day > 15 && day < 27) christmasTime = true - - // switch flakeState to false in november - if (month === 11) chrome.storage.local.set({ flakeState: false }) - - if (christmasTime) { - // on load - document.addEventListener('DOMNodeInserted', () => { - if (!document.getElementById('flake')) insertFlakeSwitch(result.flakeState) - }) - // on document changes - window.addEventListener('load', () => { - if (!document.getElementById('flake')) insertFlakeSwitch(result.flakeState) - if (!document.getElementById('snowflakes') && result.flakeState) insertFlakes() - }, true) - // standard rocket logo - } else { - // on load - document.addEventListener('DOMNodeInserted', () => { - if (!document.getElementById('TUFastLogo')) { insertRocket(selectedRocketIcon, result.PRObadge, result.foundEasteregg) } - }) - // on document changes - window.addEventListener('load', () => { - if (!document.getElementById('TUFastLogo')) { insertRocket(selectedRocketIcon, result.PRObadge, result.foundEasteregg) } - }, true) - } - } -}) - -// variables required for rocket logo easteregg -let globalCounter = 0 // count clicks on icon -let coutdownRemoveScreenOverlay // timer -let timeout = 1000 // timeout for timer -let blocker = false // block icon click -let timeUp = true // true, when time is up -let displayValue // number of text, which shows on screen -let typeOfMsg = '' // type of message which is displayed - -async function updateRocketLogo (iconPath) { - const timestamp = new Date().getTime() - document.querySelectorAll('#TUFastLogo img')[0].src = chrome.runtime.getURL('' + iconPath) + '?t =' + timestamp - chrome.runtime.sendMessage({ cmd: 'update_rocket_logo_easteregg' }) -} - -// function setProBadge() { -// document.getElementById('TUFastLogo').parentNode.removeChild(document.getElementById('TUFastLogo')) -// chrome.storage.local.set({ PRObadge: 'PRO' }, function () { }) -// insertRocket('colorful', 'PRO') -// } - -function insertScreenOverlay () { - try { - if (!document.getElementById('counter')) { - const body = document.getElementsByTagName('body')[0] - const counter = document.createElement('div') - const container = document.createElement('div') - container.style.position = 'relative' - counter.id = 'counter' - counter.style.opacity = '1' - counter.style.fontSize = '150px' - counter.style.position = 'absolute' - counter.style.color = '#000000' - counter.style.top = '50%' - counter.style.left = '50%' - counter.style.marginRight = '-50%' - counter.style.transform = 'translate(-50%, -50%)' - counter.style.zIndex = '99' - - container.appendChild(counter) - body.prepend(counter) - } - } catch (e) { console.log('cannot insert overlay:' + e) } -} - -async function logoOnClickEasteregg () { - // block counting up when text is promted - if (blocker && !timeUp) return - - globalCounter++ - - // show screen overlay - if (!document.getElementById('counter')) { - // insert overlay - insertScreenOverlay() - } else { - // remove existing timeout - clearTimeout(coutdownRemoveScreenOverlay) - } - - let counter = document.getElementById('counter') - counter.style.color = funnyColor(counter.style.color, 80) - - // trigger actions based on counter - switch (globalCounter) { - case 10: { - // easteregg finished - displayValue = '🚀 🚀 🚀' - typeOfMsg = 'text' - // enable rocketIcon, set selected rocketIcon (RI3) - // live-update the logo - updateRocketLogo('../assets/icons/RocketIcons/7_128px.png') - // change the onclick function - document.getElementById('TUFastLogo').onclick = logoOnClick - // Promisified until usage of Manifest V3 - const availableRockets = await new Promise((resolve) => chrome.storage.local.get(['availableRockets'], resp => resolve(resp.availableRockets))) - availableRockets.push('RI3') - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.set({ - foundEasteregg: true, - selectedRocketIcon: '{"id": "RI3", "link": "../assets/icons/RocketIcons/7_128px.png"}', - availableRockets - }, resolve)) - break - } - default: - typeOfMsg = 'number' - displayValue = globalCounter - } - - // decide how to show text - switch (typeOfMsg) { - case 'text': - counter.style.fontSize = '100px' - timeout = 3000 - blocker = true - break - default: - timeout = 1000 - blocker = false - counter.style.fontSize = '150px' - } - - // populate screen overlay with value - counter.innerHTML = displayValue - - timeUp = false - - coutdownRemoveScreenOverlay = setTimeout(() => { - counter = document.getElementById('counter') - counter.parentNode.removeChild(counter) - timeUp = true - }, timeout) -} - -async function logoOnClick () { - // console.log('here') - if (timeUp) { - chrome.runtime.sendMessage({ cmd: 'open_settings_page', params: 'rocket_icons_settings' }) - } -} - -function funnyColor (color, step) { - const rgb = color.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/) - rgb[1] = parseInt(rgb[1]) - rgb[2] = parseInt(rgb[2]) - rgb[3] = parseInt(rgb[3]) - if (rgb[1] < 255 - step && rgb[2] < 255 - step && rgb[3] < 150 - step) rgb[1] += step - else if (rgb[2] < 255 - step && rgb[3] < 150 - step) rgb[2] += step - else if (rgb[3] < 150 - step) rgb[3] += step - else if (rgb[1] > 0 + step) rgb[1] -= step - else if (rgb[2] > 0 + step) rgb[2] -= step - else if (rgb[3] > 0 + step) rgb[3] -= step - color = 'rgb(' + rgb[1] + ',' + rgb[2] + ',' + rgb[3] + ')' - return color -} - -function insertRocket (selectedRocketIcon, PRObadge = false, foundEasteregg) { - let imgUrl, header, logoNode, logoLink, logoImg, badge - try { - if (document.getElementsByClassName('page-header')[0] !== undefined) { - header = document.getElementsByClassName('page-header')[0] - logoNode = document.createElement('h1') - logoLink = document.createElement('a') - logoImg = document.createElement('img') - logoLink.href = 'javascript:void(0)' - logoNode.id = 'TUFastLogo' - logoLink.title = 'powered by TUFast. Enjoy :)' - - // onclick function depends on whether easteregg was already found! - if (foundEasteregg) { - logoNode.onclick = logoOnClick - } else { - logoNode.onclick = logoOnClickEasteregg - } - - // create rocket icon - imgUrl = chrome.runtime.getURL('../' + selectedRocketIcon.link) - logoImg.style.display = 'inline-block' - logoImg.style.width = '37px' - logoImg.src = imgUrl - logoLink.appendChild(logoImg) - - // add badge - switch (PRObadge) { - case 'PRO': - logoLink.style.position = 'relative' - badge = document.createElement('span') - badge.classList.add('badge') - badge.innerHTML = 'PRO' - badge.style.fontSize = '0.3em' - badge.style.position = 'absolute' - badge.style.bottom = '0px' - badge.style.left = '20px' - logoLink.appendChild(badge) - break - default: - break - } - - // append to header - logoNode.appendChild(logoLink) - header.append(logoNode) - } - } catch (e) { - console.log('Error inserting logo: ' + e) - } -} - -// toggle flake state -async function flakesSwitchOnClick () { - // Promisified until usage of Manifest V3 - const flakeState = await new Promise((resolve) => chrome.storage.local.get(['flakeState'], resp => resolve(resp.flakeState))) - - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.set({ flakeState: !flakeState }, resolve)) - // careful: this has to be negated, as its toggled - if (!flakeState) { - document.getElementById('flakeLink').style.color = 'black' - insertFlakes() - } else { - document.getElementById('flakeLink').style.color = 'grey' - removeFlakes() - } -} - -function removeFlakes () { - try { - document.getElementById('snowflakes').parentNode.removeChild(document.getElementById('snowflakes')) - } catch (e) { console.log('No snowflakes!: ' + e) } -} - -function insertFlakes () { - try { - if (!document.getElementById('snowflakes')) { - // create snowflake css - const snowflakeCss = '.snowflake {color: #fff;font-size: 1em;font-family: Arial, sans-serif;text-shadow: 0 0 5px #000;pointer-events: none;}@-webkit-keyframes snowflakes-fall{0%{top:-10%}100%{top:100%}}@-webkit-keyframes snowflakes-shake{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}50%{-webkit-transform:translateX(80px);transform:translateX(80px)}}@keyframes snowflakes-fall{0%{top:-10%}100%{top:100%}}@keyframes snowflakes-shake{0%,100%{transform:translateX(0)}50%{transform:translateX(80px)}}.snowflake{position:fixed;top:-10%;z-index:9999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default;-webkit-animation-name:snowflakes-fall,snowflakes-shake;-webkit-animation-duration:10s,3s;-webkit-animation-timing-function:linear,ease-in-out;-webkit-animation-iteration-count:infinite,infinite;-webkit-animation-play-state:running,running;animation-name:snowflakes-fall,snowflakes-shake;animation-duration:10s,3s;animation-timing-function:linear,ease-in-out;animation-iteration-count:infinite,infinite;animation-play-state:running,running}.snowflake:nth-of-type(0){left:1%;-webkit-animation-delay:0s,0s;animation-delay:0s,0s}.snowflake:nth-of-type(1){left:10%;-webkit-animation-delay:1s,1s;animation-delay:1s,1s}.snowflake:nth-of-type(2){left:20%;-webkit-animation-delay:6s,.5s;animation-delay:6s,.5s}.snowflake:nth-of-type(3){left:30%;-webkit-animation-delay:4s,2s;animation-delay:4s,2s}.snowflake:nth-of-type(4){left:40%;-webkit-animation-delay:2s,2s;animation-delay:2s,2s}.snowflake:nth-of-type(5){left:50%;-webkit-animation-delay:8s,3s;animation-delay:8s,3s}.snowflake:nth-of-type(6){left:60%;-webkit-animation-delay:6s,2s;animation-delay:6s,2s}.snowflake:nth-of-type(7){left:70%;-webkit-animation-delay:2.5s,1s;animation-delay:2.5s,1s}.snowflake:nth-of-type(8){left:80%;-webkit-animation-delay:1s,0s;animation-delay:1s,0s}.snowflake:nth-of-type(9){left:90%;-webkit-animation-delay:3s,1.5s;animation-delay:3s,1.5s}.snowflake:nth-of-type(10){left:25%;-webkit-animation-delay:2s,0s;animation-delay:2s,0s}.snowflake:nth-of-type(11){left:65%;-webkit-animation-delay:4s,2.5s;animation-delay:4s,2.5s}' - const snowflakeStyle = document.createElement('style') - - // add css to snowflage tag - if (snowflakeStyle.styleSheet) { - snowflakeStyle.styleSheet.cssText = snowflakeCss - } else { - snowflakeStyle.appendChild(document.createTextNode(snowflakeCss)) - } - - // add snowflage style tag to website head - document.getElementsByTagName('head')[0].appendChild(snowflakeStyle) - - // create snowflake div - const snowflakes = document.createElement('div') - snowflakes.classList.add('snowflakes') - snowflakes.id = 'snowflakes' - snowflakes.setAttribute('aria-hidden', 'true') - snowflakes.innerHTML = '
' - - // add snowflake div to website body - document.getElementsByTagName('body')[0].prepend(snowflakes) - } - } catch (e) { console.log('cannot insert snowFlakes: ' + e) } -} - -function insertFlakeSwitch (currentlyActivated) { - if (currentlyActivated === undefined) currentlyActivated = true - try { - if (document.getElementsByClassName('page-header')[0] !== undefined) { - const header = document.getElementsByClassName('page-header')[0] - const flake = document.createElement('h1') - const flakeLink = document.createElement('a') - flakeLink.id = 'flakeLink' - flakeLink.style.textDecoration = 'none' - flakeLink.href = 'javascript:void(0)' - flake.id = 'flake' - flakeLink.title = 'Click me. Winter powered by TUfast.' - flake.onclick = flakesSwitchOnClick - flake.style.paddingTop = '2px' - flake.style.paddingLeft = '3px' - - if (currentlyActivated) flakeLink.style.color = 'black' - if (!currentlyActivated) flakeLink.style.color = 'Grey' - - flakeLink.innerHTML = '❅' - flakeLink.fontSize = '30px' - - // append to header - flake.appendChild(flakeLink) - header.append(flake) - } - } catch (e) { - console.log('Error inserting flakeSwitch: ' + e) - } -} diff --git a/src/contentScripts/bildungsportal_main.js b/src/contentScripts/bildungsportal_main.js deleted file mode 100644 index ecbf2c49..00000000 --- a/src/contentScripts/bildungsportal_main.js +++ /dev/null @@ -1,27 +0,0 @@ -function loginOpal () { - // select TU Dresden from selector - if (document.getElementsByName('wayfselection')[0]) { - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - const selectionList = document.getElementsByName('wayfselection')[0] - // The following spread operator is needed because HTMLCollection has no "find" - const element = [...selectionList].find((el) => el.textContent === 'TU Dresden' || el.textContent === 'Technsiche Universität Dresden') - document.getElementsByName('wayfselection')[0].value = element.value - } - chrome.runtime.sendMessage({ cmd: 'show_ok_badge', timeout: 4000 }) - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - document.getElementsByClassName('btn-highlight')[0].click() - - console.log('Auto Login to Opal.') -} - -chrome.storage.local.get(['isEnabled', 'loggedOutOpal'], (result) => { - if (result.isEnabled && !(result.loggedOutOpal)) { - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', loginOpal) - } else { - loginOpal() - } - } else if (result.loggedOutOpal) { - chrome.storage.local.set({ loggedOutOpal: false }) - } -}) diff --git a/src/contentScripts/bildungsportal_other.js b/src/contentScripts/bildungsportal_other.js deleted file mode 100644 index 41d99b44..00000000 --- a/src/contentScripts/bildungsportal_other.js +++ /dev/null @@ -1,287 +0,0 @@ -function addLogoutButtonListener () { - if (document.querySelectorAll('.btn.btn-sm[title="Abmelden"]')[0]) { - document.querySelectorAll('.btn.btn-sm[title="Abmelden"]')[0].addEventListener('click', () => { - chrome.runtime.sendMessage({ cmd: 'logged_out', portal: 'loggedOutOpal' }) - }) - } -} - -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', addLogoutButtonListener) -} else { - addLogoutButtonListener() -} - -chrome.storage.local.get(['isEnabled', 'availableRockets', 'removedUnlockRocketsBanner', 'showedOpalCustomizeBanner', 'unlockRocketsFirstPrompt', 'saved_click_counter', 'showedUnreadMailCounterBanner', 'showedFirefoxBanner', 'mostLiklySubmittedReview', 'removedReviewBanner', 'neverShowedReviewBanner', 'showedKeyboardBanner2', 'nameIsTUfast'], (result) => { - // decide whether to show review banner - const showReviewBanner = false - let showKeyboardUpdate = false - // let showImplementationForFirefox = false - let showUnreadMailCounter = false - let showUnlockRocketsFirstPrompt = false - let showOpalCustomize = false - const showUnlockRockets = false - - // const mod200Clicks = result.saved_click_counter % 200 - const mod100Clicks = result.saved_click_counter % 100 - // const isFirefox = navigator.userAgent.includes('Firefox/') // attention: no failsave browser detection - - // not shown right now - // reviews only required in FF - // if (isFirefox && !result.mostLiklySubmittedReview && mod200Clicks < 15 && !result.removedReviewBanner && result.saved_click_counter > 200) { - // showReviewBanner = true - // } - // if (mod200Clicks > 15) { - // chrome.storage.local.set({ removedReviewBanner: false }, function () { }) - // } - // if (isFirefox && result.neverShowedReviewBanner && result.saved_click_counter > 300) { - // showReviewBanner = true - // } - - // show implementationForFirefox not longer required - // if (!showKeyboardUpdate && !showReviewBanner && !result.showedFirefoxBanner && result.saved_click_counter > 50) { - // let isChrome = navigator.userAgent.includes("Chrome/") //attention: no failsave browser detection | also for new edge! - // if(isChrome) showImplementationForFirefox = true - // } - - if (result.saved_click_counter > 50 && !result.showedUnreadMailCounterBanner) { - showUnreadMailCounter = true - } else if (result.saved_click_counter > 100 && !result.showedKeyboardBanner2) { - showKeyboardUpdate = true - } else if (result.saved_click_counter > 150 && !result.showedOpalCustomizeBanner) { - showOpalCustomize = true - } else if (result.saved_click_counter > 250 && !result.unlockRocketsFirstPrompt) { - showUnlockRocketsFirstPrompt = true - } - /* - else if (result.saved_click_counter > 300 && result.unlockRocketsFirstPrompt && mod100Clicks < 7 && !result.removedUnlockRocketsBanner) { - //dont show banner, if all rockets are already unlocked - if (!result.availableRockets.includes("RI1") || !result.availableRockets.includes("RI2")) { - showUnlockRockets = true - } - } - */ - - // reset unlock rockets banner - if (mod100Clicks > 7) { chrome.storage.local.set({ removedUnlockRocketsBanner: false }) } - - window.addEventListener('load', async function () { - if (showReviewBanner) { showLeaveReviewBanner() } - if (showKeyboardUpdate) { showKeyboardShortcutUpdate() } - // if (showImplementationForFirefox) { showImplementationForFirefoxBanner() } - if (showUnreadMailCounter) { showUnreadMailCounterBanner() } - if (showUnlockRocketsFirstPrompt) { showUnlockRocketsFirstBanner() } - if (showOpalCustomize) { showOpalCustomizeBanner() } - if (showUnlockRockets) { showUnlockRocketsBanner() } - - if (this.document.getElementById('removeReviewBanner')) { - this.document.getElementById('removeReviewBanner').onclick = removeReviewBanner - } - if (this.document.getElementById('webstoreLink')) { - this.document.getElementById('webstoreLink').onclick = clickedWebstoreLink - } - if (this.document.getElementById('openKeyboardShortcutSettings')) { - this.document.getElementById('openKeyboardShortcutSettings').onclick = openKeyboardShortcutSettings - } - if (this.document.getElementById('OpenOpalCustomizeSettings')) { - this.document.getElementById('OpenOpalCustomizeSettings').onclick = openOpalCustomizeSettings - } - if (this.document.getElementById('removeKeyboardShortcutSettings')) { - this.document.getElementById('removeKeyboardShortcutSettings').onclick = removeKeyboardShortcutSettings - } - if (this.document.getElementById('RemoveShowOpalCustomizeBanner')) { - this.document.getElementById('RemoveShowOpalCustomizeBanner').onclick = removeOpenOpalCustomizeSettings - } - if (this.document.getElementById('removeNameBanner')) { - this.document.getElementById('removeNameBanner').onclick = removeNameBanner - } - if (this.document.getElementById('removeShowImplementationForFirefoxBanner')) { - this.document.getElementById('removeShowImplementationForFirefoxBanner').onclick = removeShowImplementationForFirefoxBanner - this.document.getElementById('LinkShowImplementationForFirefoxBanner').onclick = removeShowImplementationForFirefoxBanner - } - if (this.document.getElementById('showUnreadMailCounterBanner')) { - this.document.getElementById('OpenUnreadMailCounterSettings').onclick = openSettingsUnreadMail - this.document.getElementById('RemoveShowUnreadMailCounterBanner').onclick = RemoveShowUnreadMailCounterBanner - } - if (this.document.getElementById('unlockRocketsFirstPrompt')) { - this.document.getElementById('openMoreRocketIcons').onclick = openMoreRocketIconsSettingsFirstPrompt - document.getElementById('removeMoreRocketIconsFirst').onclick = removeFirstRocketBanner - } - if (this.document.getElementById('unlockRocketsPrompt')) { - this.document.getElementById('openMoreRocketIcons').onclick = openMoreRocketIconsSettings - this.document.getElementById('removeMoreRocketIcons').onclick = RemoveMoreRocketIcons - } - }) -}) - -function RemoveMoreRocketIcons () { - if (document.getElementById('unlockRocketsPrompt')) { - document.getElementById('unlockRocketsPrompt').remove() - chrome.storage.local.set({ removedUnlockRocketsBanner: true }) - } -} - -function showUnlockRocketsBanner () { - const imgUrl = chrome.runtime.getURL('../assets/images/tufast48.png') - const banner = this.document.createElement('div') - banner.id = 'unlockRocketsPrompt' - banner.style = 'font-size:22px; height:55px; line-height:55px;text-align:center' - banner.innerHTML = 'Unterstütze TUfast und schalte coole neue Raketen frei! 🔥🔥🔥 Nein' - this.document.body.insertBefore(banner, document.body.childNodes[0]) -} - -function RemoveShowUnreadMailCounterBanner () { - if (document.getElementById('showUnreadMailCounterBanner')) { - document.getElementById('showUnreadMailCounterBanner').remove() - chrome.storage.local.set({ showedUnreadMailCounterBanner: true }) - } -} - -function openSettingsUnreadMail () { - chrome.runtime.sendMessage({ cmd: 'open_settings_page', params: 'mailFetchSettings' }) - if (document.getElementById('showUnreadMailCounterBanner')) { - document.getElementById('showUnreadMailCounterBanner').remove() - chrome.storage.local.set({ showedUnreadMailCounterBanner: true }) - } -} - -function removeReviewBanner () { - if (document.getElementById('reviewBanner')) { - document.getElementById('reviewBanner').remove() - chrome.storage.local.set({ removedReviewBanner: true }) - chrome.storage.local.set({ neverShowedReviewBanner: false }) - } -} - -function removeShowImplementationForFirefoxBanner () { - chrome.storage.local.set({ showedFirefoxBanner: true }) - if (document.getElementById('showImplementationForFirefoxBanner')) { - document.getElementById('showImplementationForFirefoxBanner').remove() - } -} - -function openKeyboardShortcutSettings () { - if (document.getElementById('keyboardBanner')) { - chrome.runtime.sendMessage({ cmd: 'open_shortcut_settings' }) - } -} - -function removeFirstRocketBanner () { - if (document.getElementById('unlockRocketsFirstPrompt')) { - document.getElementById('unlockRocketsFirstPrompt').remove() - chrome.storage.local.set({ unlockRocketsFirstPrompt: true }) - } -} - -function openMoreRocketIconsSettingsFirstPrompt () { - if (document.getElementById('unlockRocketsFirstPrompt')) { - chrome.runtime.sendMessage({ cmd: 'open_settings_page', params: 'rocket_icons_settings' }) - document.getElementById('unlockRocketsFirstPrompt').remove() - chrome.storage.local.set({ unlockRocketsFirstPrompt: true }) - } -} - -function openMoreRocketIconsSettings () { - if (document.getElementById('unlockRocketsPrompt')) { - chrome.runtime.sendMessage({ cmd: 'open_settings_page', params: 'rocket_icons_settings' }) - document.getElementById('unlockRocketsPrompt').remove() - chrome.storage.local.set({ removedUnlockRocketsBanner: true }) - } -} - -function openOpalCustomizeSettings () { - chrome.runtime.sendMessage({ cmd: 'open_settings_page', params: 'opalCustomize' }) - if (document.getElementById('showOpalCustomizeBanner')) { - document.getElementById('showOpalCustomizeBanner').remove() - chrome.storage.local.set({ showedOpalCustomizeBanner: true }) - } -} - -function removeKeyboardShortcutSettings () { - chrome.storage.local.set({ showedKeyboardBanner2: true }) - if (document.getElementById('keyboardBanner')) { - document.getElementById('keyboardBanner').remove() - } -} - -function removeOpenOpalCustomizeSettings () { - chrome.storage.local.set({ showedOpalCustomizeBanner: true }) - if (document.getElementById('showOpalCustomizeBanner')) { - document.getElementById('showOpalCustomizeBanner').remove() - } -} - -function removeNameBanner () { - chrome.storage.local.set({ nameIsTUfast: true }) - if (document.getElementById('nameBanner')) { - document.getElementById('nameBanner').remove() - } -} - -function clickedWebstoreLink () { - if (document.getElementById('reviewBanner')) { - document.getElementById('reviewBanner').remove() - chrome.storage.local.set({ mostLiklySubmittedReview: true }) - chrome.storage.local.set({ neverShowedReviewBanner: false }) - } -} - -function showKeyboardShortcutUpdate () { - const imgUrl = chrome.runtime.getURL('../assets/images/tufast48.png') - const banner = this.document.createElement('div') - banner.id = 'keyboardBanner' - banner.style = 'font-size:22px; height:55px; line-height:55px;text-align:center' - banner.innerHTML = ' Supergeil: TUfast Shortcuts! Öffne z.B. das Dashboard mit Alt+QAlle Shortcuts ansehenX' - this.document.body.insertBefore(banner, document.body.childNodes[0]) -} - -function showUnlockRocketsFirstBanner () { - const imgUrl = chrome.runtime.getURL('../assets/images/tufast48.png') - const banner = this.document.createElement('div') - banner.id = 'unlockRocketsFirstPrompt' - banner.style = 'font-size:22px; height:55px; line-height:55px;text-align:center' - banner.innerHTML = 'Dir gefällt TUfast ? Unterstütze das Projekt, empfehle es deinen Freunden und schalte coole neue Raketen frei! 🔥🔥🔥 Nein' - this.document.body.insertBefore(banner, document.body.childNodes[0]) -} - -// function showImplementationForFirefoxBanner () { -// const imgUrl = chrome.runtime.getURL('../assets/images/tufast48.png') -// const banner = this.document.createElement('div') -// banner.id = 'showImplementationForFirefoxBanner' -// banner.style = 'font-size:22px; height:55px; line-height:55px;text-align:center' -// banner.innerHTML = 'Supergeil und Brandneu: TUfast für Firefox! 🔥🔥🔥Schließen' -// this.document.body.insertBefore(banner, document.body.childNodes[0]) -// } - -function showOpalCustomizeBanner () { - const imgUrl = chrome.runtime.getURL('../assets/images/tufast48.png') - const banner = this.document.createElement('div') - banner.id = 'showOpalCustomizeBanner' - banner.style = 'font-size:22px; height:55px; line-height:55px;text-align:center' - banner.innerHTML = 'Mit TUfast kannst du OPAL Personalisieren und Verbessern. Gleich ausprobieren! 🔥🔥🔥Schließen X' - this.document.body.insertBefore(banner, document.body.childNodes[0]) -} - -function showUnreadMailCounterBanner () { - const imgUrl = chrome.runtime.getURL('../assets/images/tufast48.png') - const banner = this.document.createElement('div') - banner.id = 'showUnreadMailCounterBanner' - banner.style = 'font-size:22px; height:55px; line-height:55px;text-align:center' - banner.innerHTML = 'Neu: mit TUfast verpasst du keine Mails aus deinem TU Dresden Postfach. Jetzt probieren.Schließen' - this.document.body.insertBefore(banner, document.body.childNodes[0]) -} - -function showLeaveReviewBanner () { - // webstore link depends on browser! - const isChrome = navigator.userAgent.includes('Chrome/') // attention: no failsave browser detection | also for new edge! - const isFirefox = navigator.userAgent.includes('Firefox/') // attention: no failsave browser detection - let webstoreLink - if (isChrome) { webstoreLink = 'https://chrome.google.com/webstore/detail/tufast-tu-dresden/aheogihliekaafikeepfjngfegbnimbk?hl=de' } else if (isFirefox) { webstoreLink = 'https://addons.mozilla.org/de/firefox/addon/tufast/?utm_source=addons.mozilla.org&utm_medium=referral&utm_content=search' } else { webstoreLink = 'https://www.tu-fast.de' } - - const imgUrl = chrome.runtime.getURL('../assets/images/tufast48.png') - const banner = this.document.createElement('div') - banner.id = 'reviewBanner' - banner.style = 'font-size:22px; height:55px; line-height:55px;text-align:center' - banner.innerHTML = ' Dir gefällt TUfast ⭐⭐⭐⭐⭐ ? Hinterlasse uns eine Bewertung im Webstore!Nein, danke :(' - this.document.body.insertBefore(banner, document.body.childNodes[0]) -} diff --git a/src/contentScripts/bildungsportal_viewPDFinBrowser.js b/src/contentScripts/bildungsportal_viewPDFinBrowser.js deleted file mode 100644 index cddd2490..00000000 --- a/src/contentScripts/bildungsportal_viewPDFinBrowser.js +++ /dev/null @@ -1,59 +0,0 @@ -chrome.storage.local.get(['pdfInNewTab'], (result) => { - if (result.pdfInNewTab) { - // on load - document.addEventListener('DOMNodeInserted', () => { - modifyPdfLinks() - }) - // on document loaded - window.addEventListener( - 'load', - () => { - modifyPdfLinks() - pdfButtonExternalReload() - }, - true - ) - } -}) - -function modifyPdfLinks () { - // Modify js so that link is opened in new tab - const links = document.getElementsByTagName('a') - for (const link of links) { - if (link.href.includes('.pdf')) { - link.onclick = function (event) { - event.stopImmediatePropagation() // prevents OPAL to load in the same tab - window.open(this.href, '_blank') - return false - } - } - } -} - -function pdfButtonExternalReload () { - // Only run logic if URL path includes 'downloadering'. - // This is the page Opal requests on click on - let's call it - a 'document button'. - if (document.documentURI.includes('downloadering')) { - // The fiber code gets auto genereted, it seems kind of random. - // It doesn't identify a document, I saw two codes for the same PDF. - - const fibercodeQuery = 'fibercode=' - const fibercodePos = document.documentURI.lastIndexOf(fibercodeQuery) - - const fibercode = document.documentURI.slice(fibercodePos + fibercodeQuery.length) - - // check if fibercode is already present to not reload the same page forever - chrome.storage.local.get(['fibercode'], (result) => { - if (result.fibercode === fibercode) { - // remove fibercode again after page opened in new tab (runs in new tab) - chrome.storage.local.set({ fibercode: '' }) - } else { - // set the fibercode if it doesn't exist in db (runs on button click) - chrome.storage.local.set({ fibercode: fibercode }, () => { - open(document.documentURI, '_blank') - history.back() - }) - } - }) - } -} diff --git a/src/contentScripts/cloudstore.js b/src/contentScripts/cloudstore.js deleted file mode 100644 index 70b78624..00000000 --- a/src/contentScripts/cloudstore.js +++ /dev/null @@ -1,36 +0,0 @@ -function loginCloudstore () { - if (document.getElementById('user') && document.getElementById('password')) { - chrome.runtime.sendMessage({ cmd: 'get_user_data' }, async (result) => { - await result - if (result.user && result.pass) { - chrome.runtime.sendMessage({ cmd: 'show_ok_badge', timeout: 2000 }) - chrome.runtime.sendMessage({ cmd: 'perform_login' }) - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - document.getElementById('user').value = result.user - document.getElementById('password').value = result.pass - document.getElementById('submit-form').click() - } else { - chrome.runtime.sendMessage({ cmd: 'no_login_data' }) - } - }) - } - if (document.querySelectorAll('[data-id="logout"] > a')[0]) { - document.querySelectorAll('[data-id="logout"] > a')[0].addEventListener('click', () => { - chrome.runtime.sendMessage({ cmd: 'logged_out', portal: 'loggedOutCloudstore' }) - }) - } - - console.log('Auto Login to Cloudstore.') -} - -chrome.storage.local.get(['isEnabled', 'loggedOutCloudstore'], (result) => { - if (result.isEnabled && !result.loggedOutCloudstore) { - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', loginCloudstore) - } else { - loginCloudstore() - } - } else if (result.loggedOutCloudstore) { - chrome.storage.local.set({ loggedOutCloudstore: false }, () => { }) - } -}) diff --git a/src/contentScripts/elearningMED.js b/src/contentScripts/elearningMED.js deleted file mode 100644 index e8c518d3..00000000 --- a/src/contentScripts/elearningMED.js +++ /dev/null @@ -1,38 +0,0 @@ -function loginElearningMED () { - if (document.getElementsByTagName('a')[0].textContent === 'Moodle' && location.href === 'https://elearning.med.tu-dresden.de/') { - document.getElementsByTagName('a')[0].click() - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - } else if (location.href === 'https://elearning.med.tu-dresden.de/moodle/academiLogin.html' && document.querySelectorAll('[href="https://elearning.med.tu-dresden.de/moodle/auth/shibboleth/index.php"]')[0]) { - document.querySelectorAll('[href="https://elearning.med.tu-dresden.de/moodle/auth/shibboleth/index.php"]')[0].click() - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - } - - // second login screen (or it was just changed?!) - if (location.href.includes('elearning.med.tu-dresden.de/moodle/login')) { - if (document.querySelectorAll("a[title='ZIH-Login']")[0].href === 'https://elearning.med.tu-dresden.de/moodle/auth/shibboleth/index.php') { - document.querySelectorAll("a[title='ZIH-Login']")[0].click() - } - } - - // detecting logout - if (document.getElementById('actionmenuaction-6')) { - document.getElementById('actionmenuaction-6').addEventListener('click', () => { - console.log('logout detected!') - chrome.runtime.sendMessage({ cmd: 'logged_out', portal: 'loggedOutElearningMED' }) - }) - } - - console.log('Auto Login to elearning.med.') -} - -chrome.storage.local.get(['isEnabled', 'loggedOutElearningMED'], (result) => { - if (result.isEnabled && !result.loggedOutElearningMED) { - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => { - loginElearningMED() - }) - } else { - loginElearningMED() - } - } -}) diff --git a/src/contentScripts/forward/jexam.ts b/src/contentScripts/forward/jexam.ts new file mode 100644 index 00000000..41bc38a8 --- /dev/null +++ b/src/contentScripts/forward/jexam.ts @@ -0,0 +1,6 @@ +import type { SENamespace } from './searchEngines/common' + +(async () => { + const common: SENamespace = await import(chrome.runtime.getURL('contentScripts/forward/searchEngines/common.js')) + common.forward('jexam') +})() diff --git a/src/contentScripts/forward/opal.ts b/src/contentScripts/forward/opal.ts new file mode 100644 index 00000000..ab79a217 --- /dev/null +++ b/src/contentScripts/forward/opal.ts @@ -0,0 +1,7 @@ +import type { SENamespace } from './searchEngines/common' + +(async () => { + if (location.href.includes('exam')) return // No exam.opal domains + const common: SENamespace = await import(chrome.runtime.getURL('contentScripts/forward/searchEngines/common.js')) + common.forward('opal') +})() diff --git a/src/contentScripts/forward/searchEngines/common.ts b/src/contentScripts/forward/searchEngines/common.ts new file mode 100644 index 00000000..b966dd7c --- /dev/null +++ b/src/contentScripts/forward/searchEngines/common.ts @@ -0,0 +1,38 @@ +const sites = { + hisqis: 'https://qis.dez.tu-dresden.de/qisserver/servlet/de.his.servlet.RequestDispatcherServlet?state=template&template=user/news', + jexam: 'https://jexam.inf.tu-dresden.de/de.jexam.web.v4.5/spring/welcome', + opal: 'https://bildungsportal.sachsen.de/opal/shiblogin?0', + selma: 'https://selma.tu-dresden.de/APP/EXTERNALPAGES/-N000000000000001,-N000155,-AEXT_willkommen', + slub: 'https://www.slub-dresden.de/', + tucloud: 'https://cloudstore.zih.tu-dresden.de/index.php/login', + tudmail: 'https://msx.tu-dresden.de/owa/#path=/mail', + tumail: 'https://msx.tu-dresden.de/owa/#path=/mail', + tumatrix: 'https://matrix.tu-dresden.de/#/', + tumed: 'https://eportal.med.tu-dresden.de/login', + tustore: 'https://cloudstore.zih.tu-dresden.de/index.php/login', + videocampus: 'https://videocampus.sachsen.de/' +} + +export async function fwdEnabled () { + // Promisified until usage of Manifest V3 + const { fwdEnabled } = await new Promise((resolve) => chrome.storage.local.get(['fwdEnabled'], resolve)) + return !!fwdEnabled +} + +export async function forward (query: string): Promise { + if (!fwdEnabled() || !query) return false + + const url = sites[query.toLowerCase()] + if (url) { + console.log(`Forwarding to ${query} (${url})`) + chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) // We don't need to wait for any (useless) response + window.location.replace(url) + return true // This probably will never be reached, but we can use it + } + return false +} + +export interface SENamespace { + fwdEnabled: typeof fwdEnabled + forward: typeof forward +} diff --git a/src/contentScripts/forward/searchEngines/generic.ts b/src/contentScripts/forward/searchEngines/generic.ts new file mode 100644 index 00000000..5d5852a3 --- /dev/null +++ b/src/contentScripts/forward/searchEngines/generic.ts @@ -0,0 +1,12 @@ +import type { SENamespace } from './common' + +// This fetches the q-query from the URL +// Basically every SE just uses this parameter when using GET +// Startpage sometimes uses 'query' instead of 'q' but we can handle this here too +const params = new URLSearchParams(window.location.search) +const keyword = decodeURI(params.get('q') || '') || decodeURI(params.get('query') || ''); + +(async () => { + const common: SENamespace = await import(chrome.runtime.getURL('contentScripts/forward/searchEngines/common.js')) + common.forward(keyword) +})() diff --git a/src/contentScripts/forward/searchEngines/qwant.ts b/src/contentScripts/forward/searchEngines/qwant.ts new file mode 100644 index 00000000..404d2865 --- /dev/null +++ b/src/contentScripts/forward/searchEngines/qwant.ts @@ -0,0 +1,43 @@ +import type { SENamespace } from './common' + +// See if we have a query param - if so we don't want to do anything here +function shouldAct (): boolean { + const params = new URLSearchParams(window.location.search) + const hasQuery = params.has('q') || params.has('query') + return !hasQuery +} + +(async () => { + const common: SENamespace = await import(chrome.runtime.getURL('contentScripts/forward/searchEngines/common.js')) + + // If we have GET query or no fwdEnabled, do nothing + if (!shouldAct() || !(await common.fwdEnabled())) return + + // Check the searchbox first, if there's something in there + const sb = document.querySelector('input[name="q"][type="search"]') as HTMLInputElement + // If there's no searchbox thats weird, but we can't do anything about it + if (!sb) return + + // Check the content of the box + if (await common.forward(sb.value)) return + + // If we get here, the searchquery was useless but that could change so we register a listener + // First get the search form + const sf = document.querySelector('form[data-testid="mainSearchBar"]') as HTMLFormElement + + // Create a listener function + const onSubmit = (e: Event) => { + e.preventDefault() // We want to do our own stuff + // Call the forward function + common.forward(sb.value).then((forwarded:boolean) => { + if (!forwarded) { + // When we didn't forward, the user still wants to search + e.target?.removeEventListener('submit', onSubmit); + (e.target as HTMLFormElement).submit() + } + }) + } + + // Register the listener + sf.addEventListener('submit', onSubmit) +})() diff --git a/src/contentScripts/forward/searchEngines/startpage.ts b/src/contentScripts/forward/searchEngines/startpage.ts new file mode 100644 index 00000000..ef187cb9 --- /dev/null +++ b/src/contentScripts/forward/searchEngines/startpage.ts @@ -0,0 +1,43 @@ +import type { SENamespace } from './common' + +// See if we have a query param - if so we don't want to do anything here +function shouldAct (): boolean { + const params = new URLSearchParams(window.location.search) + const hasQuery = params.has('q') || params.has('query') + return !hasQuery +} + +(async () => { + const common: SENamespace = await import(chrome.runtime.getURL('contentScripts/forward/searchEngines/common.js')) + + // If we have GET query or no fwdEnabled, do nothing + if (!shouldAct() || !(await common.fwdEnabled())) return + + // Check the searchbox first, if there's something in there + const sb = document.getElementById('q') as HTMLInputElement + // If there's no searchbox thats weird, but we can't do anything about it + if (!sb) return + + // Check the content of the box + if (await common.forward(sb.value)) return + + // If we get here, the searchquery was useless but that could change so we register a listener + // First get the search form + const sf = document.getElementById('search') as HTMLFormElement + + // Create a listener function + const onSubmit = (e: Event) => { + e.preventDefault() // We want to do our own stuff + // Call the forward function + common.forward(sb.value).then((forwarded:boolean) => { + if (!forwarded) { + // When we didn't forward, the user still wants to search + e.target?.removeEventListener('submit', onSubmit); + (e.target as HTMLFormElement).submit() + } + }) + } + + // Register the listener + sf.addEventListener('submit', onSubmit) +})() diff --git a/src/contentScripts/fwd_cloudstore.js b/src/contentScripts/fwd_cloudstore.js deleted file mode 100644 index 380dd1e2..00000000 --- a/src/contentScripts/fwd_cloudstore.js +++ /dev/null @@ -1,7 +0,0 @@ -chrome.storage.local.get(['fwdEnabled'], async (result) => { - if (result.fwdEnabled) { - console.log('fwd to cloudstore') - await new Promise((resolve) => chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }, resolve)) - window.location.replace('https://cloudstore.zih.tu-dresden.de/index.php/login') - } -}) diff --git a/src/contentScripts/fwd_hisqis.js b/src/contentScripts/fwd_hisqis.js deleted file mode 100644 index 75d8abaa..00000000 --- a/src/contentScripts/fwd_hisqis.js +++ /dev/null @@ -1,7 +0,0 @@ -chrome.storage.local.get(['fwdEnabled'], async (result) => { - if (result.fwdEnabled) { - console.log('fwd to hisqis') - await new Promise((resolve) => chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }, resolve)) - window.location.replace('https://qis.dez.tu-dresden.de/qisserver/servlet/de.his.servlet.RequestDispatcherServlet?state=template&template=user/news') - } -}) diff --git a/src/contentScripts/fwd_jexam.js b/src/contentScripts/fwd_jexam.js deleted file mode 100644 index 53dcf600..00000000 --- a/src/contentScripts/fwd_jexam.js +++ /dev/null @@ -1,7 +0,0 @@ -chrome.storage.local.get(['fwdEnabled'], async (result) => { - if (result.fwdEnabled) { - console.log('fwd to jexam') - await new Promise((resolve) => chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }, resolve)) - window.location.replace('https://jexam.inf.tu-dresden.de/de.jexam.web.v4.5/spring/welcome') - } -}) diff --git a/src/contentScripts/fwd_magma.js b/src/contentScripts/fwd_magma.js deleted file mode 100644 index f6cf4ab9..00000000 --- a/src/contentScripts/fwd_magma.js +++ /dev/null @@ -1,7 +0,0 @@ -chrome.storage.local.get(['fwdEnabled'], async (result) => { - if (result.fwdEnabled) { - console.log('fwd to magma') - await new Promise((resolve) => chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }, resolve)) - window.location.replace('https://bildungsportal.sachsen.de/magma/') - } -}) diff --git a/src/contentScripts/fwd_matrix.js b/src/contentScripts/fwd_matrix.js deleted file mode 100644 index a1a9faa4..00000000 --- a/src/contentScripts/fwd_matrix.js +++ /dev/null @@ -1,13 +0,0 @@ -// current Auto-Login functionality doesnt allow for session verification. So only forward to https://matrix.tu-dresden.de/#/ -chrome.storage.local.get(['fwdEnabled'], async (result) => { - if (result.fwdEnabled) { - // console.log('fwd to matrix') - // chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - // window.location.replace('https://matrix.tu-dresden.de/#/login') - // } else if (result.fwdEnabled && !result.isEnabled) { - console.log('fwd to matrix') - await new Promise((resolve) => chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }, resolve)) - window.location.replace('https://matrix.tu-dresden.de/#/') - // } - } -}) diff --git a/src/contentScripts/fwd_opal.js b/src/contentScripts/fwd_opal.js deleted file mode 100644 index 22c500a6..00000000 --- a/src/contentScripts/fwd_opal.js +++ /dev/null @@ -1,7 +0,0 @@ -chrome.storage.local.get(['fwdEnabled'], async (result) => { - if (result.fwdEnabled & !(location.href.includes('exam'))) { // dont fwd if user wants to get to exam.zih.tu-dresden.de - console.log('fwd to opal') - await new Promise((resolve) => chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }, resolve)) - window.location.replace('https://bildungsportal.sachsen.de/opal/shiblogin?0') - } -}) diff --git a/src/contentScripts/fwd_opalError.js b/src/contentScripts/fwd_opalError.js deleted file mode 100644 index c736f944..00000000 --- a/src/contentScripts/fwd_opalError.js +++ /dev/null @@ -1,10 +0,0 @@ -chrome.storage.local.get(['fwdEnabled'], async (result) => { - if (result.fwdEnabled) { - console.log('fwd to opal from opal error') - await new Promise((resolve) => chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }, resolve)) - // click on forward to opal, which should be first element in list - if (document.querySelectorAll('li>a')[0].text.includes('OPAL')) { - document.querySelectorAll('li>a')[0].click() - } - } -}) diff --git a/src/contentScripts/fwd_owa.js b/src/contentScripts/fwd_owa.js deleted file mode 100644 index bed40df6..00000000 --- a/src/contentScripts/fwd_owa.js +++ /dev/null @@ -1,7 +0,0 @@ -chrome.storage.local.get(['fwdEnabled'], async (result) => { - if (result.fwdEnabled) { - console.log('fwd to owa') - await new Promise((resolve) => chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }, resolve)) - window.location.replace('https://msx.tu-dresden.de/owa/#path=/mail') - } -}) diff --git a/src/contentScripts/fwd_qwant.js b/src/contentScripts/fwd_qwant.js deleted file mode 100644 index b04f859d..00000000 --- a/src/contentScripts/fwd_qwant.js +++ /dev/null @@ -1,54 +0,0 @@ -chrome.storage.local.get(['fwdEnabled', 'isEnabled'], (result) => { - if (result.fwdEnabled) { - console.log('register fwds in qwant') - const searchForm = document.querySelector('form[data-testid="mainSearchBar"]') - const searchInput = searchForm.querySelector('input[name="q"]') - searchForm.addEventListener('submit', event => fwdOnSearch(event, searchInput.value, result.isEnabled)) - } -}) - -function fwdOnSearch (event, value, isEnabled) { - console.log(value) - console.log(isEnabled) - let link = '' - let fwd = true - switch (value) { - case 'opal': - link = 'https://bildungsportal.sachsen.de/opal/shiblogin?0' - break - case 'jexam': - link = 'https://jexam.inf.tu-dresden.de/de.jexam.web.v4.5/spring/welcome' - break - case 'tustore': - case 'tucloud': - link = 'https://cloudstore.zih.tu-dresden.de/index.php/login' - break - case 'tumail': - case 'tudmail': - link = 'https://msx.tu-dresden.de/owa/#path=/mail' - break - case 'hisqis': - link = 'https://qis.dez.tu-dresden.de/qisserver/servlet/de.his.servlet.RequestDispatcherServlet?state=template&template=user/news' - break - case 'tumed': - link = 'https://eportal.med.tu-dresden.de/login' - break - case 'selma': - link = 'https://selma.tu-dresden.de/APP/EXTERNALPAGES/-N000000000000001,-N000155,-AEXT_willkommen' - break - case 'tumatrix': - link = isEnabled ? 'https://matrix.tu-dresden.de/#/login' : 'https://matrix.tu-dresden.de/#/' - break - case 'magma': - link = 'https://bildungsportal.sachsen.de/magma/' - break - default: - fwd = false - } - if (fwd) { - event.preventDefault() - console.log(`fwd to ${link}`) - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - window.location.replace(link) - } -} diff --git a/src/contentScripts/fwd_selma.js b/src/contentScripts/fwd_selma.js deleted file mode 100644 index c1c34c26..00000000 --- a/src/contentScripts/fwd_selma.js +++ /dev/null @@ -1,8 +0,0 @@ -chrome.storage.local.get(['fwdEnabled'], async (result) => { - if (result.fwdEnabled) { - console.log('fwd to selma') - await new Promise((resolve) => chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }, resolve)) - // There is a unique session url. That is why user always has to be fwd to login page :/ - window.location.replace('https://selma.tu-dresden.de/APP/EXTERNALPAGES/-N000000000000001,-N000155,-AEXT_willkommen') - } -}) diff --git a/src/contentScripts/fwd_startpage.js b/src/contentScripts/fwd_startpage.js deleted file mode 100644 index a2fbbc1c..00000000 --- a/src/contentScripts/fwd_startpage.js +++ /dev/null @@ -1,54 +0,0 @@ -chrome.storage.local.get(['fwdEnabled', 'isEnabled'], (result) => { - if (result.fwdEnabled) { - console.log('register fwds in startpage') - const searchForm = document.getElementById('search') - const searchInput = document.getElementById('q') - searchForm.addEventListener('submit', event => fwdOnSearch(event, searchInput.value, result.isEnabled)) - } -}) - -function fwdOnSearch (event, value, isEnabled) { - console.log(value) - console.log(isEnabled) - let link = '' - let fwd = true - switch (value) { - case 'opal': - link = 'https://bildungsportal.sachsen.de/opal/shiblogin?0' - break - case 'jexam': - link = 'https://jexam.inf.tu-dresden.de/de.jexam.web.v4.5/spring/welcome' - break - case 'tustore': - case 'tucloud': - link = 'https://cloudstore.zih.tu-dresden.de/index.php/login' - break - case 'tumail': - case 'tudmail': - link = 'https://msx.tu-dresden.de/owa/#path=/mail' - break - case 'hisqis': - link = 'https://qis.dez.tu-dresden.de/qisserver/servlet/de.his.servlet.RequestDispatcherServlet?state=template&template=user/news' - break - case 'tumed': - link = 'https://eportal.med.tu-dresden.de/login' - break - case 'selma': - link = 'https://selma.tu-dresden.de/APP/EXTERNALPAGES/-N000000000000001,-N000155,-AEXT_willkommen' - break - case 'tumatrix': - link = isEnabled ? 'https://matrix.tu-dresden.de/#/login' : 'https://matrix.tu-dresden.de/#/' - break - case 'magma': - link = 'https://bildungsportal.sachsen.de/magma/' - break - default: - fwd = false - } - if (fwd) { - event.preventDefault() - console.log(`fwd to ${link}`) - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - window.location.replace(link) - } -} diff --git a/src/contentScripts/fwd_tumed.js b/src/contentScripts/fwd_tumed.js deleted file mode 100644 index 83acf5c1..00000000 --- a/src/contentScripts/fwd_tumed.js +++ /dev/null @@ -1,7 +0,0 @@ -chrome.storage.local.get(['fwdEnabled'], async (result) => { - if (result.fwdEnabled) { - console.log('fwd to https://eportal.med.tu-dresden.de/login') - await new Promise((resolve) => chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }, resolve)) - window.location.replace('https://eportal.med.tu-dresden.de/login') - } -}) diff --git a/src/contentScripts/gitlab.js b/src/contentScripts/gitlab.js deleted file mode 100644 index 54305cda..00000000 --- a/src/contentScripts/gitlab.js +++ /dev/null @@ -1,35 +0,0 @@ -function loginGitlab () { - if (document.getElementById('username') && document.getElementById('password')) { - chrome.runtime.sendMessage({ cmd: 'get_user_data' }, async (result) => { - await result - if (result.user && result.pass) { - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - chrome.runtime.sendMessage({ cmd: 'perform_login' }) - document.getElementById('username').value = result.user - document.getElementById('password').value = result.pass - document.getElementsByTagName('input')[4].click() - } else { - chrome.runtime.sendMessage({ cmd: 'no_login_data' }) - } - }) - } - if (document.querySelectorAll('.sign-out-link')[0]) { - document.querySelectorAll('.sign-out-link')[0].addEventListener('click', () => { - chrome.runtime.sendMessage({ cmd: 'logged_out', portal: 'loggedOutGitlab' }) - }) - } - - console.log('Auto Login to gitlab.') -} - -chrome.storage.local.get(['isEnabled', 'loggedOutGitlab'], (result) => { - if (result.isEnabled && !result.loggedOutGitlab) { - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', loginGitlab) - } else { - loginGitlab() - } - } else if (result.loggedOutGitlab) { - chrome.storage.local.set({ loggedOutGitlab: false }) - } -}) diff --git a/src/contentScripts/idp.js b/src/contentScripts/idp.js deleted file mode 100644 index 35fe1d41..00000000 --- a/src/contentScripts/idp.js +++ /dev/null @@ -1,36 +0,0 @@ -function loginIdp () { - if (document.getElementById('username')) { - chrome.runtime.sendMessage({ cmd: 'get_user_data' }, async (result) => { - await result - if (result.user && result.pass) { - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - document.getElementById('username').value = result.user - document.getElementById('password').value = result.pass - document.getElementsByName('_eventId_proceed')[0].click() - } else { - chrome.runtime.sendMessage({ cmd: 'no_login_data' }) - } - }) - } else if (document.getElementsByName('_eventId_proceed')[0]) { - document.getElementsByName('_eventId_proceed')[0].click() - chrome.runtime.sendMessage({ cmd: 'perform_login' }) - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - } else if (document.querySelectorAll('body > div:nth-child(2) > div:nth-child(1) > b')[0]) { - if (document.querySelectorAll('body > div:nth-child(2) > div:nth-child(1) > b')[0].innerHTML === 'TUD - TU Dresden - Single Sign On - Veraltete Anfrage' || - document.querySelectorAll('body > div:nth-child(2) > div:nth-child(1) > b')[0].innerHTML === 'TUD - TU Dresden - Single Sign On - Stale Request') { - window.location.replace('https://bildungsportal.sachsen.de/opal/login') - } - } - - console.log('Auto Login to TU Dresden Auth.') -} - -chrome.storage.local.get(['isEnabled'], (result) => { - if (!result.isEnabled) return - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', loginIdp) - } else { - loginIdp() - } -}) diff --git a/src/contentScripts/jexam.js b/src/contentScripts/jexam.js deleted file mode 100644 index 95ad75f7..00000000 --- a/src/contentScripts/jexam.js +++ /dev/null @@ -1,36 +0,0 @@ -function loginJexam () { - if (document.getElementById('username') && document.getElementById('password')) { - chrome.runtime.sendMessage({ cmd: 'get_user_data' }, async (result) => { - await result - if (result.user && result.pass) { - chrome.runtime.sendMessage({ cmd: 'show_ok_badge', timeout: 2000 }) - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - chrome.runtime.sendMessage({ cmd: 'perform_login' }) - document.getElementById('username').value = result.user - document.getElementById('password').value = result.pass - document.getElementsByTagName('input')[2].click() - } else { - chrome.runtime.sendMessage({ cmd: 'no_login_data' }) - } - }) - } - if (document.getElementsByClassName('logout nav-entry animate-fade-in')[0]) { - document.getElementsByClassName('logout nav-entry animate-fade-in')[0].addEventListener('click', () => { - chrome.runtime.sendMessage({ cmd: 'logged_out', portal: 'loggedOutJexam' }) - }) - } - - console.log('Auto Login to jexam.') -} - -chrome.storage.local.get(['isEnabled', 'loggedOutJexam'], (result) => { - if (result.isEnabled && !result.loggedOutJexam) { - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', loginJexam) - } else { - loginJexam() - } - } else if (result.loggedOutJexam) { - chrome.storage.local.set({ loggedOutJexam: false }) - } -}) diff --git a/src/contentScripts/login/cloudstore.ts b/src/contentScripts/login/cloudstore.ts new file mode 100644 index 00000000..935684eb --- /dev/null +++ b/src/contentScripts/login/cloudstore.ts @@ -0,0 +1,42 @@ +// Although we can't use the ESM import statements in content scripts we can import types. +import type { CookieSettings, LoginFields, LoginNamespace } from './common' + +// "Quicksettings" +const platform = 'zih' +const cookieSettings: CookieSettings = { + portalName: 'cloudstore', + domain: 'cloudstore.zih.tu-dresden.de' +}; + +(async () => { + const common: LoginNamespace = await import(chrome.runtime.getURL('contentScripts/login/common.js')) + + // For better syntax highlighting import the "Login" type from the common module and change it to "common.Login" when you're done. + class CloudstoreLogin extends common.Login { + constructor () { + super(platform, cookieSettings) + } + + async additionalFunctionsPreCheck (): Promise { } + + async additionalFunctionsPostCheck (): Promise { } + + async findCredentialsError (): Promise { + return document.getElementsByClassName('wrongPasswordMsg')[0] + } + + async loginFieldsAvailable (): Promise { + return { + usernameField: document.getElementById('user') as HTMLInputElement, + passwordField: document.getElementById('password') as HTMLInputElement, + submitButton: document.getElementById('submit-form') as HTMLInputElement + } + } + + async findLogoutButtons (): Promise<(HTMLElement|Element|null)[] | NodeList | null> { + return [document.querySelector('[data-id="logout"] > a')] + } + } + + await (new CloudstoreLogin()).start() +})() diff --git a/src/contentScripts/login/common.ts b/src/contentScripts/login/common.ts new file mode 100644 index 00000000..6a8bad97 --- /dev/null +++ b/src/contentScripts/login/common.ts @@ -0,0 +1,222 @@ +// This is the brain for all login activities. +// It is designed to remove redundand behavior in the login scripts +// while maintaining individuality. + +// Typescript interfaces and types +export interface UserData { + user: string; + pass: string; +} + +export type LoginCheckResponse = UserData | false; + +export interface CookieSettings { + portalName: string; + domain?: string; + logoutDuration?: number; + usesIdp?: boolean; +} + +export interface LoginFields { + usernameField: HTMLInputElement; + passwordField: HTMLInputElement; + submitButton?: HTMLElement; +} + +// This is the default lifetime for the logout cookie in minutes. +const defaultLogoutDuration = 5 + +// Abstract Loginclass +// This should be extended by the login scripts. +export abstract class Login { + platform: string; + cookieSettings: CookieSettings; + savedClickCount: number; + + // Constructor + // Nothing fancy here + constructor (platform: string, cookieSettings: CookieSettings, savedClickCount: number = 1) { + this.platform = platform || 'zih' + this.cookieSettings = cookieSettings + this.savedClickCount = savedClickCount + } + + // Abstract methods + // All these need to be implemented by the login scripts. + // This function is called on every page load no matter if userdata is available etc + abstract additionalFunctionsPreCheck(): Promise + // This function is called after cheking if we even have valid data and should act. + abstract additionalFunctionsPostCheck(userData: UserData): Promise + // This function should be used if login fields are loaded. It can return a simple boolean (no login will happen on "false") or an LoginFields object. + // If user- or pass- input are null no login try will happen. + abstract loginFieldsAvailable(): Promise + // This function should return all candidates for logout buttons. + // An onClick listener will be added to set a "loggedOut" cookie + abstract findLogoutButtons(): Promise<(HTMLElement|Element|null)[] | NodeList | null> + + // The following methods should be implemented where necessery or possible. + // The actual login function. It has access to credentials and - if the function above returns them - the input fields. + async login (userData: UserData, loginFields?: LoginFields): Promise { + if (!loginFields || !loginFields.usernameField || !loginFields.passwordField) return + + this.fakeInput(loginFields.usernameField, userData.user) + this.fakeInput(loginFields.passwordField, userData.pass) + loginFields.submitButton?.click() + } + + // This function should be used to find if an error dialog is shown for invalid credentaials. + // When the return value is not null it means that the error dialog is shown. + // There is a default implementation here but it should be used where possible. + async findCredentialsError (): Promise { return false } + + // The main function the only only one that should be actually called from outside. + async start () { + // .catch(() => { }) because we don't care about user implemented errors. + await this.additionalFunctionsPreCheck().catch(() => { }) + + const userData = await this.loginCheckAndData() + if (!userData) return + + await this.additionalFunctionsPostCheck(userData).catch(() => { }) + + await this.tryLogin(userData) + + const buttons = await this.findLogoutButtons() + this.registerLogoutButtonsListener(buttons) + } + + registerLogoutButtonsListener (buttons: (HTMLElement|Element|null)[] | NodeList | null) { + if (buttons) { + for (const button of buttons) { + if (button) button.addEventListener('click', this.setLoggedOutCookie.bind(this)) + } + } + } + + async loginCheckAndData (): Promise { + // The fastest and first check is for loggedOutCookie + if (this.isLoggedOutCookie()) return false + + // Check if auto login is enabled + // Promisified until usage of Manifest V3 + const { isEnabled } = await new Promise((resolve) => chrome.storage.local.get('isEnabled', resolve)) + if (!isEnabled) return false + + // Promissified fetch of userdata + // Chances are this also has to be used in Manifest V3 + const userData: UserData = await new Promise((resolve) => chrome.runtime.sendMessage({ cmd: 'get_user_data', platform: this.platform }, resolve)) + + if (!userData || !userData.user || !userData.pass) return false + else return userData + } + + // The if the platformLoggedOut cookie is set + isLoggedOutCookie (): boolean { + return document.cookie.includes(`${this.cookieSettings.portalName}LoggedOut`) + } + + // Function to set the platformLoggedOut cookie + setLoggedOutCookie (): void { + if (!this.cookieSettings.domain) return + + // The next line could be confusing + // Either we use the duration set in the cookieSettings object or we read it from local storage (if there is a default) or we set it to 5 minutes. + const logoutDuration: number = this.cookieSettings.logoutDuration || defaultLogoutDuration || 5 + + const date = new Date() + date.setMinutes(date.getMinutes() + logoutDuration) + const domain = this.cookieSettings.domain.startsWith('.') ? this.cookieSettings.domain : `.${this.cookieSettings.domain}` + document.cookie = `${this.cookieSettings.portalName}LoggedOut=true; expires=${date.toUTCString()}; path=/; domain=${domain}; secure` + + // If we use IDP we need to logout we can ask the backgroundscript to log us out of there too + if (this.cookieSettings.usesIdp) chrome.runtime.sendMessage({ cmd: 'logout_idp', logoutDuration }) + } + + // This function is for additional triggers that should happen on login. + // For example we need to add a click to the savedClickCounter. + // In future this can be used to add more functions. + async onLogin (): Promise { + // I don't know if await even works but there is no reason to await any response anyway + await chrome.runtime.sendMessage({ cmd: 'save_clicks', clickCount: this.savedClickCount }) + } + + // This method finds the login fields, checks for the error dialog and tries to login. + async tryLogin (userData: UserData) { + const errorDialog = await this.findCredentialsError() + if (errorDialog) return + + let loginFields: LoginFields | undefined + + const avail = await this.loginFieldsAvailable().catch(() => { }) + if (typeof avail === 'boolean' && !avail) return + if (typeof avail === 'object') { + if (!avail.usernameField || !avail.passwordField) return + else loginFields = avail + } + + await this.onLogin() + await this.login(userData, loginFields) + } + + fakeInput (input: HTMLInputElement, value: string) { + // Inspired by how the Bitwarden extension does it + // https://github.com/bitwarden/clients/blob/master/apps/browser/src/content/autofill.js#L346 + input.getBoundingClientRect() + + // Click it + input.click() + + // Focus it + input.focus() + + // Sending empty keypresses + // Making it a local function so we can use it again later + const sendEmptyPresses = () => { + input.dispatchEvent(new KeyboardEvent('keydown', { + bubbles: true, + cancelable: false, + charCode: 0, + keyCode: 0, + which: 0 + })) + input.dispatchEvent(new KeyboardEvent('keypress', { + bubbles: true, + cancelable: false, + charCode: 0, + keyCode: 0, + which: 0 + })) + input.dispatchEvent(new KeyboardEvent('keyup', { + bubbles: true, + cancelable: false, + charCode: 0, + keyCode: 0, + which: 0 + })) + } + sendEmptyPresses() + + // Set value + input.value = value + + // Click again + input.click() + + // Send empty keypresses again + sendEmptyPresses() + + // Other events + input.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })) + input.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })) + + // Blur it + input.blur() + + // Set value again + input.value = value + } +} + +export interface LoginNamespace { + Login: typeof Login; +} diff --git a/src/contentScripts/login/gitlabMN.ts b/src/contentScripts/login/gitlabMN.ts new file mode 100644 index 00000000..442e24b4 --- /dev/null +++ b/src/contentScripts/login/gitlabMN.ts @@ -0,0 +1,51 @@ +// Although we can't use the ESM import statements in content scripts we can import types. +import type { CookieSettings, UserData, LoginFields, LoginNamespace } from './common' + +// "Quicksettings" +const platform = 'zih' +const cookieSettings: CookieSettings = { + portalName: 'gitlabMn', + domain: 'gitlab.mn.tu-dresden.de' +}; + +(async () => { + const common: LoginNamespace = await import(chrome.runtime.getURL('contentScripts/login/common.js')) + + // For better syntax highlighting import the "Login" type from the common module and change it to "common.Login" when you're done. + class GlMnLogin extends common.Login { + constructor () { + super(platform, cookieSettings) + } + + async additionalFunctionsPreCheck (): Promise { } + + async additionalFunctionsPostCheck (): Promise { } + + async findCredentialsError (): Promise { + return document.querySelector('.flash-alert[data-testid="alert-danger"]') + } + + async loginFieldsAvailable (): Promise { + return { + usernameField: document.getElementById('username') as HTMLInputElement, + passwordField: document.getElementById('password') as HTMLInputElement, + submitButton: document.querySelector('input[type="submit"]') as HTMLInputElement + } + } + + async findLogoutButtons (): Promise<(HTMLElement|Element|null)[] | NodeList | null> { + return document.querySelectorAll('a.sign-out-link') + } + + async login (userData: UserData, loginFields?: LoginFields): Promise { + if (!loginFields) return + + this.fakeInput(loginFields.usernameField, userData.user) + this.fakeInput(loginFields.passwordField, userData.pass); + (document.getElementById('remember_me') as HTMLInputElement).checked = true + loginFields.submitButton?.click() + } + } + + await (new GlMnLogin()).start() +})() diff --git a/src/contentScripts/login/idp.ts b/src/contentScripts/login/idp.ts new file mode 100644 index 00000000..12e4969a --- /dev/null +++ b/src/contentScripts/login/idp.ts @@ -0,0 +1,61 @@ +// Although we can't use the ESM import statements in content scripts we can import types. +import type { CookieSettings, LoginFields, LoginNamespace } from './common' + +// "Quicksettings" +const platform = 'zih' +const cookieSettings: CookieSettings = { + portalName: 'idp', + domain: 'idp.tu-dresden.de' +}; + +(async () => { + const common: LoginNamespace = await import(chrome.runtime.getURL('contentScripts/login/common.js')) + + // For better syntax highlighting import the "Login" type from the common module and change it to "common.Login" when you're done. + class IdpLogin extends common.Login { + constructor () { + super(platform, cookieSettings) + } + + async additionalFunctionsPreCheck (): Promise { } + + async additionalFunctionsPostCheck (): Promise { + this.confirmData() + this.outdatedRequest() + } + + confirmData () { + // Check if this is the consense page + if (!document.getElementById('generalConsentDiv')) return + + // Click the button + const button = document.querySelector('input[type="submit"][name="_eventId_proceed"]') + if (button) (button as HTMLInputElement).click() + } + + outdatedRequest () { + // Check if this is the outdated request page + // TODO: Decide whether we should really do this? + // This hint isn't there for no reason can can be reached by "wrong" user choices. + // We don't know where the user tried to login, so we can't jsut redirect to Opal/etc + } + + async findCredentialsError (): Promise { + return document.querySelector('.content p font[color="red"]') + } + + async loginFieldsAvailable (): Promise { + return { + usernameField: document.getElementById('username') as HTMLInputElement, + passwordField: document.getElementById('password') as HTMLInputElement, + submitButton: document.querySelector('button[name="_eventId_proceed"][value="Login"]') as HTMLButtonElement + } + } + + async findLogoutButtons (): Promise<(HTMLElement|Element|null)[] | NodeList | null> { + return null + } + } + + await (new IdpLogin()).start() +})() diff --git a/src/contentScripts/login/jexam.ts b/src/contentScripts/login/jexam.ts new file mode 100644 index 00000000..b1a3c1ee --- /dev/null +++ b/src/contentScripts/login/jexam.ts @@ -0,0 +1,45 @@ +// Although we can't use the ESM import statements in content scripts we can import types. +import type { CookieSettings, LoginFields, LoginNamespace } from './common' + +// "Quicksettings" +const platform = 'zih' +const cookieSettings: CookieSettings = { + portalName: 'jexam', + domain: 'jexam.inf.tu-dresden.de' +}; + +(async () => { + const common: LoginNamespace = await import(chrome.runtime.getURL('contentScripts/login/common.js')) + + // For better syntax highlighting import the "Login" type from the common module and change it to "common.Login" when you're done. + class JExamLogin extends common.Login { + constructor () { + super(platform, cookieSettings) + } + + async additionalFunctionsPreCheck (): Promise { } + + async additionalFunctionsPostCheck (): Promise { } + + async findCredentialsError (): Promise { + const params = new URLSearchParams(window.location.search) + return params.get('error') === 'badcredentials' + } + + async loginFieldsAvailable (): Promise { + return { + usernameField: document.getElementById('username') as HTMLInputElement, + passwordField: document.getElementById('password') as HTMLInputElement, + // Currently there is no english login page but if that changes, this should also catch the english button + submitButton: document.querySelector('input[type="submit"]') as HTMLInputElement + } + } + + async findLogoutButtons (): Promise<(HTMLElement|Element|null)[] | NodeList | null> { + // There should only be one button but let's be safe + return document.querySelectorAll('a[href$="/logout"]') + } + } + + await (new JExamLogin()).start() +})() diff --git a/src/contentScripts/login/lskonline.ts b/src/contentScripts/login/lskonline.ts new file mode 100644 index 00000000..25d23e64 --- /dev/null +++ b/src/contentScripts/login/lskonline.ts @@ -0,0 +1,49 @@ +// Although we can't use the ESM import statements in content scripts we can import types. +import type { CookieSettings, LoginFields, LoginNamespace } from './common' + +// "Quicksettings" +const platform = 'zih' +const cookieSettings: CookieSettings = { + portalName: 'lskonline', + domain: 'lskonline.tu-dresden.de' +}; + +(async () => { + const common: LoginNamespace = await import(chrome.runtime.getURL('contentScripts/login/common.js')) + + // For better syntax highlighting import the "Login" type from the common module and change it to "common.Login" when you're done. + class LSKLogin extends common.Login { + constructor () { + super(platform, cookieSettings) + } + + async additionalFunctionsPreCheck (): Promise { } + + async additionalFunctionsPostCheck (): Promise { + this.clickLogin() + } + + clickLogin () { + const link = Array.from(document.getElementsByTagName('a')).find(link => link?.innerText === 'Login') + if (link && !link.id.includes('selected')) link.click() + } + + // We don't need this check for wrong credentials here as it's a nother page where "Login" is selected + // but no input fields are available. + + async loginFieldsAvailable (): Promise { + return { + usernameField: document.querySelector('input[name="j_username"]') as HTMLInputElement, + passwordField: document.querySelector('input[name="j_password"]') as HTMLInputElement, + submitButton: document.querySelector('input[type="submit"]') as HTMLInputElement + } + } + + async findLogoutButtons (): Promise<(HTMLElement|Element|null)[] | NodeList | null> { + // There should only be one button but let's be safe + return Array.from(document.getElementsByTagName('a')).filter(link => link?.innerText === 'Logout') + } + } + + await (new LSKLogin()).start() +})() diff --git a/src/contentScripts/login/matrix.ts b/src/contentScripts/login/matrix.ts new file mode 100644 index 00000000..18974003 --- /dev/null +++ b/src/contentScripts/login/matrix.ts @@ -0,0 +1,67 @@ +// Although we can't use the ESM import statements in content scripts we can import types. +import type { CookieSettings, UserData, LoginFields, LoginNamespace } from './common' + +// "Quicksettings" +const platform = 'zih' +const cookieSettings: CookieSettings = { + portalName: 'matrix', + domain: 'matrix.tu-dresden.de' +}; + +(async () => { + const common: LoginNamespace = await import(chrome.runtime.getURL('contentScripts/login/common.js')) + + // For better syntax highlighting import the "Login" type from the common module and change it to "common.Login" when you're done. + class MatrixLogin extends common.Login { + constructor () { + super(platform, cookieSettings) + } + + async additionalFunctionsPreCheck (): Promise { } + + async additionalFunctionsPostCheck (): Promise { + this.clickLogin() + } + + clickLogin () { + (document.querySelector('a[href="#/login"]') as HTMLAnchorElement|null)?.click() + } + + async findCredentialsError (): Promise { + return document.getElementsByClassName('mx_Login_error')[0] + } + + async loginFieldsAvailable (): Promise { + const hash = window.location.hash + if (hash !== '#/login') return false + + return { + usernameField: document.getElementById('mx_LoginForm_username') as HTMLInputElement, + passwordField: document.getElementById('mx_LoginForm_password') as HTMLInputElement, + submitButton: document.querySelector('input.mx_Login_submit[type="submit"]') as HTMLInputElement + } + } + + async findLogoutButtons (): Promise<(HTMLElement|Element|null)[] | NodeList | null> { + return [document.getElementsByClassName('mx_UserMenu_iconSignOut')[0]?.parentElement] + } + + async login (userData: UserData, loginFields?: LoginFields): Promise { + if (!loginFields || !loginFields.submitButton) return + + // Fake the input on fields + this.fakeInput(loginFields.usernameField, userData.user) + this.fakeInput(loginFields.passwordField, userData.pass) + loginFields.submitButton.click() + } + } + + const login = new MatrixLogin() + + // As the logout button is injected dynmically we need to wait for it to be available + const oberserver = new MutationObserver(async (_records, _observer) => { + await login.start() + }) + + oberserver.observe(document.body, { subtree: true, childList: true }) +})() diff --git a/src/contentScripts/login/medELearning.ts b/src/contentScripts/login/medELearning.ts new file mode 100644 index 00000000..3f841e25 --- /dev/null +++ b/src/contentScripts/login/medELearning.ts @@ -0,0 +1,44 @@ +// Although we can't use the ESM import statements in content scripts we can import types. +import type { CookieSettings, UserData, LoginFields, LoginNamespace } from './common' + +// "Quicksettings" +const platform = 'zih' +const cookieSettings: CookieSettings = { + portalName: 'elearningMed', + domain: 'elearning.med.tu-dresden.de', + usesIdp: true +}; + +(async () => { + const common: LoginNamespace = await import(chrome.runtime.getURL('contentScripts/login/common.js')) + + // For better syntax highlighting import the "Login" type from the common module and change it to "common.Login" when you're done. + class MedLogin extends common.Login { + constructor () { + super(platform, cookieSettings) + } + + async additionalFunctionsPreCheck (): Promise { } + + async additionalFunctionsPostCheck (): Promise { + this.chooseZIHLogin() + } + + chooseZIHLogin () { + if (window.location.pathname !== '/moodle/login/index.php') return + (document.querySelector('a[href*="auth/shibboleth"]') as HTMLAnchorElement | null)?.click() + } + + async loginFieldsAvailable (): Promise { + return false + } + + async findLogoutButtons (): Promise<(HTMLElement|Element|null)[] | NodeList | null> { + return document.querySelectorAll('a[href*="moodle/login/logout.php"]') + } + + async login (_userData: UserData, _loginFields?: LoginFields): Promise { } + } + + await (new MedLogin()).start() +})() diff --git a/src/contentScripts/login/medEPortal.ts b/src/contentScripts/login/medEPortal.ts new file mode 100644 index 00000000..d90ef856 --- /dev/null +++ b/src/contentScripts/login/medEPortal.ts @@ -0,0 +1,43 @@ +// Although we can't use the ESM import statements in content scripts we can import types. +import type { CookieSettings, UserData, LoginFields, LoginNamespace } from './common' + +// "Quicksettings" +const platform = 'zih' +const cookieSettings: CookieSettings = { + portalName: 'eportalMed', + domain: 'eportal.med.tu-dresden.de', + usesIdp: true +}; + +(async () => { + const common: LoginNamespace = await import(chrome.runtime.getURL('contentScripts/login/common.js')) + + // For better syntax highlighting import the "Login" type from the common module and change it to "common.Login" when you're done. + class MedLogin extends common.Login { + constructor () { + super(platform, cookieSettings) + } + + async additionalFunctionsPreCheck (): Promise { } + + async additionalFunctionsPostCheck (): Promise { + this.clickLogin() + } + + clickLogin () { + (document.getElementById('personaltools-login') as HTMLAnchorElement | null)?.click() + } + + async loginFieldsAvailable (): Promise { + return false + } + + async findLogoutButtons (): Promise<(HTMLElement|Element|null)[] | NodeList | null> { + return [document.getElementById('personaltools-logout')] + } + + async login (_userData: UserData, _loginFields?: LoginFields): Promise { } + } + + await (new MedLogin()).start() +})() diff --git a/src/contentScripts/login/opalHome.ts b/src/contentScripts/login/opalHome.ts new file mode 100644 index 00000000..8ab730a3 --- /dev/null +++ b/src/contentScripts/login/opalHome.ts @@ -0,0 +1,59 @@ +// Although we can't use the ESM import statements in content scripts we can import types. +import type { CookieSettings, UserData, LoginFields, LoginNamespace } from './common' + +// "Quicksettings" +const platform = 'zih' +const cookieSettings: CookieSettings = { + portalName: 'opal', + domain: 'bildungsportal.sachsen.de', + usesIdp: true +}; + +(async () => { + const common: LoginNamespace = await import(chrome.runtime.getURL('contentScripts/login/common.js')) + + // For better syntax highlighting import the "Login" type from the common module and change it to "common.Login" when you're done. + class OpalLogin extends common.Login { + constructor () { + super(platform, cookieSettings) + } + + async additionalFunctionsPreCheck (): Promise { } + + async additionalFunctionsPostCheck (): Promise { + const observer = new MutationObserver(this.selectTU.bind(this)) + observer.observe(document.body, { subtree: true, childList: true }) + this.clickLogin() + } + + selectTU (_records: MutationRecord[], observer: MutationObserver) { + // "id4b" seems to be random generated, so we should probably not use it + const select = document.querySelector('select[name="content:container:login:shibAuthForm:wayfselection"]') as HTMLSelectElement + if (!select) return + const value = Array.from(select.options).find(option => option.innerText === 'TU Dresden' || option.innerText === 'Technische Universität Dresden')?.value + if (value) { + observer.disconnect() + select.value = value; + // same here for "id51" + (document.querySelector('button[name="content:container:login:shibAuthForm:shibLogin"]') as HTMLButtonElement | null)?.click() + } + } + + clickLogin () { + (document.querySelector('a[title="Login"]') as HTMLAnchorElement|null)?.click() + } + + async loginFieldsAvailable (): Promise { + return false + } + + async findLogoutButtons (): Promise<(HTMLElement|Element|null)[] | NodeList | null> { + // The title actually isn't translated when using Opal in Englisch. But for the future it's here. + return [document.querySelector('a[title="Abmelden"]'), document.querySelector('a[title="Logout"]')] + } + + async login (_userData: UserData, _loginFields?: LoginFields): Promise { } + } + + await (new OpalLogin()).start() +})() diff --git a/src/contentScripts/login/opalMain.ts b/src/contentScripts/login/opalMain.ts new file mode 100644 index 00000000..d0784e61 --- /dev/null +++ b/src/contentScripts/login/opalMain.ts @@ -0,0 +1,50 @@ +// Although we can't use the ESM import statements in content scripts we can import types. +import type { CookieSettings, UserData, LoginFields, LoginNamespace } from './common' + +// "Quicksettings" +const platform = 'zih' +const cookieSettings: CookieSettings = { + portalName: 'opal', + domain: 'bildungsportal.sachsen.de' +}; + +(async () => { + const common: LoginNamespace = await import(chrome.runtime.getURL('contentScripts/login/common.js')) + + // For better syntax highlighting import the "Login" type from the common module and change it to "common.Login" when you're done. + class OpalLogin extends common.Login { + constructor () { + super(platform, cookieSettings) + } + + async additionalFunctionsPreCheck (): Promise { } + + async additionalFunctionsPostCheck (): Promise { + this.selectTU() + } + + selectTU () { + // "id2" seems to be random generated, so we should probably not use it + const select = document.querySelector('select[name="wayfselection"]') as HTMLSelectElement | null + if (!select) return + const value = Array.from(select.options).find(option => option.innerText === 'TU Dresden' || option.innerText === 'Technische Universität Dresden')?.value + if (value) { + select.value = value; + // same here for "id11" + (document.querySelector('button[name="shibLogin"]') as HTMLButtonElement | null)?.click() + } + } + + async loginFieldsAvailable (): Promise { + return false + } + + async findLogoutButtons (): Promise<(HTMLElement|Element|null)[] | NodeList | null> { + return [document.getElementById('logOut_btn')] + } + + async login (_userData: UserData, _loginFields?: LoginFields): Promise { } + } + + await (new OpalLogin()).start() +})() diff --git a/src/contentScripts/login/owa.ts b/src/contentScripts/login/owa.ts new file mode 100644 index 00000000..63638ce0 --- /dev/null +++ b/src/contentScripts/login/owa.ts @@ -0,0 +1,60 @@ +// Although we can't use the ESM import statements in content scripts we can import types. +import type { CookieSettings, LoginFields, LoginNamespace } from './common' + +// "Quicksettings" +const platform = 'zih' +const cookieSettings: CookieSettings = { + portalName: 'owa', + domain: 'msx.tu-dresden.de' +}; + +(async () => { + const common: LoginNamespace = await import(chrome.runtime.getURL('contentScripts/login/common.js')) + + // For better syntax highlighting import the "Login" type from the common module and change it to "common.Login" when you're done. + class OWALogin extends common.Login { + constructor () { + super(platform, cookieSettings) + } + + async additionalFunctionsPreCheck (): Promise { } + + async additionalFunctionsPostCheck (): Promise { } + + async findCredentialsError (): Promise { + return document.getElementById('signInErrorDiv') + } + + async loginFieldsAvailable (): Promise { + const submit = document.querySelector('div.signInEnter div') as HTMLDivElement + + return { + usernameField: document.getElementById('username') as HTMLInputElement, + passwordField: document.getElementById('password') as HTMLInputElement, + submitButton: submit || undefined + } + } + + async findLogoutButtons (): Promise<(HTMLElement|Element|null)[] | NodeList | null> { + // Light Version, we need more advanced stuff for the others + return [document.getElementById('lo')] + } + } + + const login = new OWALogin() + await login.start() + + // As the logout button is injected dynmically we need to wait for it to be available + const observer = new MutationObserver((_records, _observer) => { + // old owa version + const oldBtn = document.querySelector('[aria-label="Abmelden"]') + + // new owa version + const allNewBtns = document.querySelectorAll('div[role=menu] button') + const newBtns = (Array.from(allNewBtns) as HTMLButtonElement[]).filter((btn) => btn.innerText === 'Abmelden' || btn.innerText === 'Logout') + + login.registerLogoutButtonsListener([...newBtns, oldBtn]) + }) + + observer.observe(document.body, { subtree: true, childList: true }) +})() diff --git a/src/contentScripts/login/qis.ts b/src/contentScripts/login/qis.ts new file mode 100644 index 00000000..70c5d726 --- /dev/null +++ b/src/contentScripts/login/qis.ts @@ -0,0 +1,51 @@ +// Although we can't use the ESM import statements in content scripts we can import types. +import type { CookieSettings, LoginFields, LoginNamespace } from './common' + +// "Quicksettings" +const platform = 'zih' +const cookieSettings: CookieSettings = { + portalName: 'hisqis', + domain: 'qis.dez.tu-dresden.de' +}; + +(async () => { + const common: LoginNamespace = await import(chrome.runtime.getURL('contentScripts/login/common.js')) + + // For better syntax highlighting import the "Login" type from the common module and change it to "common.Login" when you're done. + class HisqisLogin extends common.Login { + constructor () { + super(platform, cookieSettings) + } + + async additionalFunctionsPreCheck (): Promise { + this.acceptConditions() + } + + acceptConditions () { + const link = Array.from(document.querySelectorAll('a')).find(element => element.innerText.includes('>>>')) + if (link) { + (link as HTMLAnchorElement).click() + } + } + + async additionalFunctionsPostCheck (): Promise {} + + async findCredentialsError (): Promise { + return document.getElementsByClassName('newSessionMsg')[0] + } + + async loginFieldsAvailable (): Promise { + return { + usernameField: document.getElementById('asdf') as HTMLInputElement, + passwordField: document.getElementById('fdsa') as HTMLInputElement, + submitButton: document.querySelector('input[type="submit"]') as HTMLInputElement + } + } + + async findLogoutButtons (): Promise<(HTMLElement|Element|null)[] | NodeList | null> { + return [document.querySelector('a[title="Abmelden"]'), document.querySelector('a[title="Logout"]')] + } + } + + await (new HisqisLogin()).start() +})() diff --git a/src/contentScripts/login/selma.ts b/src/contentScripts/login/selma.ts new file mode 100644 index 00000000..8281262f --- /dev/null +++ b/src/contentScripts/login/selma.ts @@ -0,0 +1,49 @@ +// Although we can't use the ESM import statements in content scripts we can import types. +import type { CookieSettings, LoginFields, LoginNamespace } from './common' + +// "Quicksettings" +const platform = 'zih' +const cookieSettings: CookieSettings = { + portalName: 'selma', + domain: 'selma.tu-dresden.de' +}; + +(async () => { + const common: LoginNamespace = await import(chrome.runtime.getURL('contentScripts/login/common.js')) + + // For better syntax highlighting import the "Login" type from the common module and change it to "common.Login" when you're done. + class SelmaLogin extends common.Login { + constructor () { + super(platform, cookieSettings) + } + + async additionalFunctionsPreCheck (): Promise { } + + async additionalFunctionsPostCheck (): Promise { } + + async findCredentialsError (): Promise { + const header = document.getElementsByTagName('h1')[0] + if (!header) return false + + const ger = header.innerText === 'Benutzername oder Passwort falsch' + // Currently the error message is not localized. + // But here's a blindport to the German error message. + const eng = header.innerText === 'Username or password is wrong' + return ger || eng + } + + async loginFieldsAvailable (): Promise { + return { + usernameField: document.getElementById('field_user') as HTMLInputElement, + passwordField: document.getElementById('field_pass') as HTMLInputElement, + submitButton: document.getElementById('logIn_btn') as HTMLInputElement + } + } + + async findLogoutButtons (): Promise<(HTMLElement|Element|null)[] | NodeList | null> { + return [document.getElementById('logOut_btn')] + } + } + + await (new SelmaLogin()).start() +})() diff --git a/src/contentScripts/login/slub.ts b/src/contentScripts/login/slub.ts new file mode 100644 index 00000000..a7b323df --- /dev/null +++ b/src/contentScripts/login/slub.ts @@ -0,0 +1,42 @@ +// Although we can't use the ESM import statements in content scripts we can import types. +import type { CookieSettings, LoginFields, LoginNamespace } from './common' + +// "Quicksettings" +const platform = 'slub' +const cookieSettings: CookieSettings = { + portalName: 'slub', + domain: 'slub-dresden.de' +}; + +(async () => { + const common: LoginNamespace = await import(chrome.runtime.getURL('contentScripts/login/common.js')) + + // For better syntax highlighting import the "Login" type from the common module and change it to "common.Login" when you're done. + class SlubLogin extends common.Login { + constructor () { + super(platform, cookieSettings) + } + + async additionalFunctionsPreCheck (): Promise { } + + async additionalFunctionsPostCheck (): Promise {} + + async findCredentialsError (): Promise { + return document.getElementsByClassName('form-error')[0] + } + + async loginFieldsAvailable (): Promise { + return { + usernameField: document.getElementById('username') as HTMLInputElement, + passwordField: document.getElementById('password') as HTMLInputElement, + submitButton: document.querySelector('input[type="submit"]') as HTMLInputElement + } + } + + async findLogoutButtons (): Promise<(HTMLElement|Element|null)[] | NodeList | null> { + return document.querySelectorAll('a[href^="https://www.slub-dresden.de/Shibboleth.sso/Logout"]') + } + } + + await (new SlubLogin()).start() +})() diff --git a/src/contentScripts/login/tex.ts b/src/contentScripts/login/tex.ts new file mode 100644 index 00000000..284b1797 --- /dev/null +++ b/src/contentScripts/login/tex.ts @@ -0,0 +1,43 @@ +// Although we can't use the ESM import statements in content scripts we can import types. +import type { CookieSettings, UserData, LoginFields, LoginNamespace } from './common' + +// "Quicksettings" +const platform = 'zih' +const cookieSettings: CookieSettings = { + portalName: 'tex', + domain: 'tex.zih.tu-dresden.de', + usesIdp: true +}; + +(async () => { + const common: LoginNamespace = await import(chrome.runtime.getURL('contentScripts/login/common.js')) + + // For better syntax highlighting import the "Login" type from the common module and change it to "common.Login" when you're done. + class TexLogin extends common.Login { + constructor () { + super(platform, cookieSettings) + } + + async additionalFunctionsPreCheck (): Promise { } + + async additionalFunctionsPostCheck (): Promise { + this.clickLogin() + } + + clickLogin () { + (document.querySelector('a[href="/saml/login/go"]') as HTMLAnchorElement | null)?.click() + } + + async loginFieldsAvailable (): Promise { + return false + } + + async findLogoutButtons (): Promise<(HTMLElement|Element|null)[] | NodeList | null> { + return [document.querySelector('form[action="/logout"] button')] + } + + async login (_userData: UserData, _loginFields?: LoginFields): Promise { } + } + + await (new TexLogin()).start() +})() diff --git a/src/contentScripts/login/videocampus.ts b/src/contentScripts/login/videocampus.ts new file mode 100644 index 00000000..11394510 --- /dev/null +++ b/src/contentScripts/login/videocampus.ts @@ -0,0 +1,57 @@ +import type { CookieSettings, UserData, LoginFields, LoginNamespace } from './common' + +// "Quicksettings" +const platform = 'zih' +const cookieSettings: CookieSettings = { + portalName: 'videocampus', + domain: 'videocampus.sachsen.de', + usesIdp: true +}; + +(async () => { + const common: LoginNamespace = await import(chrome.runtime.getURL('contentScripts/login/common.js')) + + // For better syntax highlighting import the "Login" type from the common module and change it to "common.Login" when you're done. + class VideocampusLogin extends common.Login { + constructor () { + super(platform, cookieSettings) + } + + async additionalFunctionsPreCheck (): Promise { } + + async additionalFunctionsPostCheck (): Promise { + this.selectIdpAndClick() + } + + selectIdpAndClick () { + const [select] = document.getElementsByName('idp') + if (!select) return + + // Get all options and find TU Dresden + const optionsArr = Array.from((select as HTMLSelectElement).options) + const idpValue = optionsArr.find((option) => option.innerText === 'TU Dresden' || option.innerText === 'Technische Universität Dresden')?.value + if (typeof idpValue === 'undefined') return + (select as HTMLSelectElement).value = idpValue; + + // We need to trigger the onchange event manually + (select as HTMLSelectElement).dispatchEvent(new Event('change')) + + // Click the submit button + document.getElementById('samlLogin')?.click() + } + + // Login fields are never available + async loginFieldsAvailable (): Promise { + return false + } + + async findLogoutButtons (): Promise<(HTMLElement|Element|null)[] | NodeList | null> { + return [document.querySelector('a.dropdown-item[href="/logout"]')] + } + + // We never login but forward to the idp + async login (_userData: UserData, _loginFields?: LoginFields): Promise { } + } + + await (new VideocampusLogin()).start() +})() diff --git a/src/contentScripts/lskonline.js b/src/contentScripts/lskonline.js deleted file mode 100644 index a9d16cb9..00000000 --- a/src/contentScripts/lskonline.js +++ /dev/null @@ -1,46 +0,0 @@ -function loginLsk (logoutDuration) { - // abmelden button - document.querySelectorAll('a[href="103.0"]').forEach((logoutBtn) => { - if (logoutBtn.innerHTML !== 'Logout') return - logoutBtn.addEventListener('click', () => { - const date = new Date() - date.setMinutes(date.getMinutes() + logoutDuration) - document.cookie = `lskLoggedOut; expires=${date.toUTCString()}; path=/; domain=.lskonline.tu-dresden.de; secure` - }) - }) - - if (document.querySelector('form[name="loginForm"]')) { - // The login form manipulation has to be first else we will always click on "login" - chrome.runtime.sendMessage({ cmd: 'get_user_data', platform: 'zih' }, async (result) => { - await result - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - chrome.runtime.sendMessage({ cmd: 'show_ok_badge', timeout: 2000 }) - chrome.runtime.sendMessage({ cmd: 'perform_login' }) - document.querySelector('input[name="j_username"]').value = result.user - document.querySelector('input[name="j_password"]').value = result.pass - document.querySelector('input[type="submit"]').click() - }) - } else if (document.querySelector('a[href="102.0.1"]')) { - const loginLink = document.querySelector('a[href="102.0.1"]') - if (loginLink.innerHTML !== 'Login') return - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - document.querySelector('a[href="102.0.1"]').click() - } - - console.log('Auto Login to lsk.') -} - -chrome.storage.local.get(['isEnabled', 'logoutDuration'], (result) => { - if (result.isEnabled && !document.cookie.includes('lskLoggedOut')) { - const logoutDuration = result.logoutDuration || 5 - chrome.runtime.sendMessage({ cmd: 'check_user_data', platform: 'zih' }, async (result) => { - await result - if (!result) return - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => loginLsk(logoutDuration)) - } else { - loginLsk(logoutDuration) - } - }) - } -}) diff --git a/src/contentScripts/magma.js b/src/contentScripts/magma.js deleted file mode 100644 index 9b8b58ec..00000000 --- a/src/contentScripts/magma.js +++ /dev/null @@ -1,24 +0,0 @@ -chrome.storage.local.get(['isEnabled', 'loggedOutMagma'], (result) => { - if (result.isEnabled && !result.loggedOutMagma) { - document.addEventListener('DOMContentLoaded', () => { - if (document.getElementsByName('loginButton')[0] && document.getElementsByName('wayfSelect')[0]) { - chrome.runtime.sendMessage({ cmd: 'show_ok_badge', timeout: 2000 }) - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - document.getElementsByName('wayfSelect')[0].value = '11' - document.getElementsByName('loginButton')[0].click() - } - if (document.querySelectorAll('#page > div:nth-child(3) > section > div:nth-child(3) > form > button')[0]) { - document.querySelectorAll('#page > div:nth-child(3) > section > div:nth-child(3) > form > button')[0].click() - } - // logout - if (document.querySelectorAll('#page > header > div:nth-child(2) > div > a')[0]) { - document.querySelectorAll('#page > header > div:nth-child(2) > div > a')[0].addEventListener('click', () => { - chrome.runtime.sendMessage({ cmd: 'logged_out', portal: 'loggedOutMagma' }) - }) - } - }) - console.log('Auto Login to magma.') - } else if (result.loggedOutMagma) { - chrome.storage.local.set({ loggedOutMagma: false }) - } -}) diff --git a/src/contentScripts/matrix.js b/src/contentScripts/matrix.js deleted file mode 100644 index 77415aca..00000000 --- a/src/contentScripts/matrix.js +++ /dev/null @@ -1,64 +0,0 @@ -// current Auto-Login functionality doesnt allow for session verification. So only forward to https://matrix.tu-dresden.de/#/ - -// Mainly contributed by Daniel: https://github.com/C0ntroller - -// function loginMatrix () { -// // check if already loked in. -// if (localStorage.getItem('mx_access_token')) { -// const hash = window.location.hash -// // forward to home page, if already logged in -// if (hash === '#/login') { -// console.log('Already logged into matrix. Fwd to home page') -// chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) -// // window.location.replace("https://matrix.tu-dresden.de/") -// window.location.hash = '#/home' -// location.reload() -// } -// return -// } -// chrome.runtime.sendMessage({ cmd: 'get_user_data' }, function (result) { -// if (!(result.asdf === undefined || result.fdsa === undefined)) { -// const url = 'https://matrix.tu-dresden.de/_matrix/client/r0/login' -// const params = '{"type":"m.login.password","password":"' + (result.fdsa) + '","identifier":{"type":"m.id.user","user":"' + (result.asdf) + '"},"initial_device_display_name":"Chrome Autologin"}' - -// const http = new XMLHttpRequest() -// http.open('POST', url, true) -// http.setRequestHeader('Content-type', 'application/json; charset=utf-8') -// http.setRequestHeader('authority', 'matrix.tu-dresden.de') -// http.setRequestHeader('accept', 'application/json') -// http.setRequestHeader('content-type', 'application/json') - -// http.onreadystatechange = function () { -// if (http.readyState === 4 && http.status === 200) { -// chrome.runtime.sendMessage({ cmd: 'show_ok_badge', timeout: 2000 }) -// chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) -// console.log('Auto Login to matrix') - -// const response = JSON.parse(http.responseText) -// localStorage.setItem('mx_hs_url', 'https://matrix.tu-dresden.de/') -// localStorage.setItem('mx_is_url', 'https://matrix.tu-dresden.de/') -// localStorage.setItem('mx_device_id', response.device_id) -// localStorage.setItem('mx_user_id', response.user_id) -// localStorage.setItem('mx_access_token', response.access_token) -// localStorage.setItem('mx_is_guest', 'false') - -// window.location.replace('https://matrix.tu-dresden.de/') -// } -// } -// http.send(params) -// } else { -// chrome.runtime.sendMessage({ cmd: 'no_login_data' }) -// } -// }) -// } - -// chrome.storage.local.get(['isEnabled'], function (result) { -// if (result.isEnabled) { -// const ctx = document.getElementById('matrixchat') -// ctx.addEventListener('DOMSubtreeModified', function () { -// // eslint-disable-next-line no-caller -// this.removeEventListener('DOMSubtreeModified', arguments.callee) -// loginMatrix() -// }) -// } -// }) diff --git a/src/contentScripts/other/hisqis/gradeChart.ts b/src/contentScripts/other/hisqis/gradeChart.ts new file mode 100644 index 00000000..664b3260 --- /dev/null +++ b/src/contentScripts/other/hisqis/gradeChart.ts @@ -0,0 +1,129 @@ +import Chart from 'chart.js/auto' + +type ExamGrades = Record +type ModuleGrades = Record + +interface ParsedGrades { + exams: ExamGrades + modules: ModuleGrades +} + +function parseGrades (): ParsedGrades | null { + const examGrades: ExamGrades = {} + const moduleGrades: ModuleGrades = {} + + const tableRows = document.querySelectorAll('form table:not([summary="Liste der Stammdaten des Studierenden"]) > tbody > tr') + if (tableRows.length === 0) return null + + for (const row of tableRows) { + const cells = row.getElementsByTagName('td') + if (cells.length < 11) continue + if (cells[0].bgColor === '#ADADAD') continue + + const isModule = cells[0].bgColor === '#DDDDDD' + + const entitiyNumber = Number.parseInt(cells[0].textContent?.trim() || '') + if (Number.isNaN(entitiyNumber)) continue + + const entityGrade = Number.parseFloat(cells[3].textContent?.trim().replace(',', '.') || '') + if (Number.isNaN(entityGrade)) continue + + if (isModule) { + const cp = Number.parseInt(cells[7].textContent?.trim() || '') || 0 + moduleGrades[entitiyNumber] = { grade: entityGrade, cp } + } else { + if (examGrades[entitiyNumber]) { + // If it exists in the list we overwrite it when it's better (we don't count 5.0) + if (entityGrade < examGrades[entitiyNumber]) examGrades[entitiyNumber] = entityGrade + } else { + // If it doesn't exist we create it + examGrades[entitiyNumber] = entityGrade + } + } + } + + return { exams: examGrades, modules: moduleGrades } +} + +function countGrades (grades: ExamGrades): number[] { + const gradesArr = [0, 0, 0, 0, 0] + Object.values(grades).forEach(g => { + const grade = Math.round(g) + gradesArr[grade - 1] += 1 + }) + + return gradesArr +} + +function getWeightedAverage (grades: ModuleGrades): number { + let sum = 0 + let count = 0 + for (const { grade, cp } of Object.values(grades)) { + sum += grade * cp + count += cp + } + return sum / count +} + +(async () => { + const container = document.getElementById('TUfastGradeContainer') + if (!container) return + + const grades = parseGrades() + if (!grades) return + + const canvas = document.createElement('canvas') + + const statistik: HTMLParagraphElement[] = [] + for (let i = 0; i < 3; i++) { + const p = document.createElement('p') + p.className = 'info' + statistik.push(p) + } + + statistik[0].innerHTML = `Deine Durchschnittnote (nach CP gewichtet): ${getWeightedAverage(grades.modules).toFixed(2)}` + statistik[1].innerHTML = `Anzahl Module: ${Object.keys(grades.modules).length}` + statistik[2].innerHTML = `Anzahl Prüfungen: ${Object.keys(grades.exams).length}` + + container.append(canvas, ...statistik) + + const ctx = canvas.getContext('2d') + if (!ctx) return + ctx.canvas.width = 500 + ctx.canvas.height = 250 + // eslint-disable-next-line no-unused-vars + const gradeChart = new Chart(ctx, { + type: 'bar', + data: { + labels: ['1', '2', '3', '4', 'nicht bestanden'], + datasets: [{ + data: countGrades(grades.exams), + backgroundColor: [ + '#0b2a51', + '#0b2a51', + '#0b2a51', + '#0b2a51', + '#0b2a51' + ], + borderColor: [ + '#0b2a51' + ], + borderWidth: 1 + }] + }, + options: { + responsive: false, + maintainAspectRatio: false, + scales: { + y: { + beginAtZero: true + } + }, + plugins: { + legend: { + display: false + } + } + } + }) +})() diff --git a/src/contentScripts/other/hisqis/newTable.ts b/src/contentScripts/other/hisqis/newTable.ts new file mode 100644 index 00000000..aad15af6 --- /dev/null +++ b/src/contentScripts/other/hisqis/newTable.ts @@ -0,0 +1,121 @@ +import { DataTable } from 'simple-datatables' + +(async () => { + // Get the container for the information + // Currently this means there's the link located for switching the tables + const tableInfoContainer = document.getElementById('TUfastTableInfo') + if (!tableInfoContainer) return + + // The old table and it's content + const oldTable = document.getElementsByTagName('table')[2] + if (!oldTable) return + oldTable.id = 'oldGradeTable' + const oldTableRows = (oldTable as HTMLTableElement).querySelectorAll('tr') + + // Create a new table + const newTable = document.createElement('table') + newTable.id = 'gradeTable' + newTable.style.display = 'none' // Hide by default so there's less flickering + + // Header for our new table + const caption = document.createElement('caption') + const tableHeader = document.createElement('div') + tableHeader.classList.add('table-header') + caption.append(tableHeader) + + // title element + const title = document.createElement('h3') + title.classList.add('table-header__title') + title.innerText = 'Deine Notenübersicht' + + // flex div to display small color helpers + const colorHelpers = document.createElement('div') + colorHelpers.classList.add('table-header__helpers') + + // create small color helpers + for (const [i, descriptor] of ['Modul', 'Bestandene Prüfung', 'Verhauene Prüfung'].entries()) { + const colorHelper = document.createElement('div') + colorHelper.classList.add('table-header__helper') + colorHelper.classList.add(`table-header__helper--${i}`) + + const color = document.createElement('div') + color.classList.add('table-header__color') + color.classList.add(`table-header__color--${i}`) + colorHelper.append(color) + + const colorText = document.createElement('div') + colorText.classList.add('table-header__color-text') + colorText.classList.add(`table-header__color-text--${i}`) + colorText.innerText = descriptor + colorHelper.append(colorText) + + colorHelpers.append(colorHelper) + } + + tableHeader.append(title, colorHelpers) + + // Header for the new table + const newTableHead = document.createElement('thead') + const newTableHeadRow = document.createElement('tr') + for (const th of oldTableRows[1].getElementsByTagName('th')) { + const newTh = document.createElement('th') + newTh.style.textAlign = th.style.textAlign + newTh.style.width = th.style.width + newTh.innerText = th.innerText.trim() + newTableHeadRow.append(newTh) + } + newTableHead.appendChild(newTableHeadRow) + + // Body for the new table + const newTableBody = document.createElement('tbody') + // Create rows from the old table + for (const oldRow of oldTableRows) { + const cells = oldRow.getElementsByTagName('td') + if (cells.length < 11) continue + + const newRow = document.createElement('tr') + for (const cell of cells) { + const newCell = document.createElement('td') + newCell.style.textAlign = cell.style.textAlign + newCell.style.width = cell.style.width + newCell.innerText = cell.innerText + newRow.appendChild(newCell) + } + + const computedStyleCell = window.getComputedStyle(cells[0]) + const backgroundColorCell = computedStyleCell.getPropertyValue('background-color') + switch (true) { + case backgroundColorCell === 'rgb(173, 173, 173)': + newRow.className = 'meta' + break + case backgroundColorCell === 'rgb(221, 221, 221)': + newRow.className = 'module' + break + case Number.parseFloat(cells[3].textContent?.trim().replace(',', '.') || '') === 5: + newRow.className = 'exam-nopass' + break + default: + newRow.className = 'exam' + } + newTableBody.appendChild(newRow) + } + + // Append everything together + newTable.append(caption, newTableHead, newTableBody) + + /* let { hisqisPimpedTable } = { hisqisPimpedTable: true } // await new Promise((resolve) => chrome.storage.local.get(['hisqisPimpedTable'], resolve)) + if (hisqisPimpedTable) oldTable.style.display = 'none' + else newTable.style.display = 'none' */ + + oldTable.parentNode?.insertBefore(newTable, oldTable) + + // eslint-disable-next-line no-unused-vars + const _dataTable = new DataTable(newTable, { + sortable: true, + searchable: false, + paging: false, + columns: [ + { select: 10, type: 'date', format: 'DD.MM.YYYY' } + ] + }) +})() diff --git a/src/contentScripts/other/hisqis/pimpAndInjectModules.ts b/src/contentScripts/other/hisqis/pimpAndInjectModules.ts new file mode 100644 index 00000000..a4c1a526 --- /dev/null +++ b/src/contentScripts/other/hisqis/pimpAndInjectModules.ts @@ -0,0 +1,66 @@ +(async () => { + const form = document.getElementsByTagName('form')[0] + const table = document.querySelector('table[summary="Liste der Stammdaten des Studierenden"]') + const afterTable = table?.nextElementSibling + if (!form || !table || !afterTable) return + + form.insertBefore(document.createElement('p'), afterTable) // Spacer + + const gradeContainer = document.createElement('div') + gradeContainer.id = 'TUfastGradeContainer' + form.insertBefore(gradeContainer, afterTable) + + const tableInfoContainer = document.createElement('div') + tableInfoContainer.id = 'TUfastTableInfo' + form.insertBefore(tableInfoContainer, afterTable) + + const imgUrl = chrome.runtime.getURL('/assets/images/tufast48.png') + const credits = document.createElement('p') + credits.id = 'TUfastCredits' + credits.innerHTML = `Powered by TUfast (entwickelt von Noxdor & C0ntroller)` + form.insertBefore(credits, afterTable) + + // Insert grade script + const gradeScript = document.createElement('script') + gradeScript.type = 'module' + gradeScript.src = chrome.extension.getURL('/contentScripts/other/hisqis/gradeChart.js') + + // Insert table script + const tableScript = document.createElement('script') + tableScript.type = 'module' + tableScript.src = chrome.extension.getURL('/contentScripts/other/hisqis/newTable.js') + + // Because the table script needs access to the storage we need to add the toggle here. + // We also need to set the initial state to the script. + tableScript.addEventListener('load', async () => { + const newTable = document.getElementById('gradeTable') + const oldTable = document.getElementById('oldGradeTable') // This is set in the other script for convinience + + if (!newTable || !oldTable) return + + const { hisqisPimpedTable } = await new Promise((resolve) => chrome.storage.local.get(['hisqisPimpedTable'], resolve)) + + newTable.style.display = hisqisPimpedTable ? 'table' : 'none' + oldTable.style.display = hisqisPimpedTable ? 'none' : 'table' + + // Now we need the link to switch the tables + const p = document.createElement('p') + p.className = 'info' + + const changeLink = document.createElement('a') + changeLink.style.cursor = 'pointer' + changeLink.innerText = hisqisPimpedTable ? 'langweiligen, alten Tabelle...' : 'neuen, coolen TUfast-Tabelle 🔥' + changeLink.addEventListener('click', async () => { + const hisqisPimpedTable = !(newTable.style.display === 'table') // We toggle + newTable.style.display = hisqisPimpedTable ? 'table' : 'none' + oldTable.style.display = hisqisPimpedTable ? 'none' : 'table' + changeLink.innerText = hisqisPimpedTable ? 'langweiligen, alten Tabelle...' : 'neuen, coolen TUfast-Tabelle 🔥' + await new Promise((resolve) => chrome.storage.local.set({ hisqisPimpedTable }, resolve)) + }) + + p.append(document.createTextNode(' Weiter zur '), changeLink) + tableInfoContainer.appendChild(p) + }) + + document.head.append(gradeScript, tableScript) +})() diff --git a/src/contentScripts/other/notification.ts b/src/contentScripts/other/notification.ts new file mode 100644 index 00000000..74538890 --- /dev/null +++ b/src/contentScripts/other/notification.ts @@ -0,0 +1,42 @@ +// create notification container +const notificationContainer = document.createElement('div') +notificationContainer.classList.add('notifications') + +document.body.append(notificationContainer) + +export const notify = (msg: string) => { + // create a notification element, a close button and TUfast logo + const notification = document.createElement('div') + const closeButton = document.createElement('div') + const logo = document.createElement('img') + + // configure notification + notification.classList.add('notifications__notification') + notification.innerHTML = `${msg}` + + // configure close button + closeButton.classList.add('notifications__close-button') + closeButton.innerText = 'X' + closeButton.onclick = () => { + notification.classList.add('fade-out') + setTimeout(() => notification.remove(), 500) + } + + // configure logo + logo.src = chrome.runtime.getURL('/assets/images/tufast48.png') + + // apend close icon & logo to notification and notification to container + notification.prepend(logo, closeButton) + notificationContainer.prepend(notification) + + // set timeout for notification to be removed automatically + setTimeout(() => { + notification.classList.add('fade-out') + // console.log("added fade-out") + setTimeout(() => notification.remove(), 500) + }, 50000) +} + +export interface NotificationNamespace { + notify: typeof notify; +} diff --git a/src/contentScripts/other/opal/insertBanner.ts b/src/contentScripts/other/opal/insertBanner.ts new file mode 100644 index 00000000..c0cd1b33 --- /dev/null +++ b/src/contentScripts/other/opal/insertBanner.ts @@ -0,0 +1,108 @@ + +(async () => { + const { bannersShown, savedClickCounter, /* enabledOWAFetch, */ mostLikelySubmittedReview } = await new Promise((resolve) => chrome.storage.local.get(['bannersShown', 'savedClickCounter', 'enabledOWAFetch', 'mostLikelySubmittedReview'], resolve)) + + const bannerArr = Array.isArray(bannersShown) ? bannersShown : [] + + function insertBanner (bannerName: string, title: string, otherElements: Node[]) { + const banner = document.createElement('div') + banner.id = 'TUfastBanner' + + const img = document.createElement('img') + img.src = chrome.runtime.getURL('/assets/images/tufast48.png') + banner.appendChild(img) + + const titleE = document.createElement('b') + titleE.innerText = ` ${title} ` + + banner.append(titleE, ...otherElements) + + const closeLink = document.createElement('span') + closeLink.className = 'closeLink' + closeLink.innerText = 'X' + closeLink.addEventListener('click', async () => { + document.body.removeChild(banner) + bannerArr.push(bannerName) + await new Promise((resolve) => chrome.storage.local.set({ bannersShown: bannerArr }, resolve)) + }) + + banner.appendChild(closeLink) + + document.body.prepend(banner) + } + + switch (true) { + /* case !bannerArr.includes('mailCount') && savedClickCounter > 50 && !enabledOWAFetch: { + const text = document.createTextNode('Mit TUfast verpasst du keine Mails aus deinem TU Dresden Postfach! ') + const interact = document.createElement('span') + interact.className = 'interactLink' + interact.textContent = 'Jetzt probieren!' + interact.addEventListener('click', () => { + chrome.runtime.sendMessage({ cmd: 'open_settings_page', params: 'mailFetchSettings' }) + }) + insertBanner('mailCount', 'Noch faster:', [text, interact]) + break + } + case !bannerArr.includes('keyboardShortcuts') && savedClickCounter > 100: { + const text = document.createElement('span') + text.innerHTML = 'Öffne z.B. das Dashboard mit Alt+Q. ' + const interact = document.createElement('span') + interact.className = 'interactLink' + interact.textContent = 'Alle Shortcuts ansehen!' + interact.addEventListener('click', () => { + chrome.runtime.sendMessage({ cmd: 'open_shortcut_settings' }) + }) + insertBanner('keyboardShortcuts', 'Supergeil: TUfast Shortcuts!', [text, interact]) + break + } + case !bannerArr.includes('customizeOpal') && savedClickCounter > 150: { + const text = document.createElement('span') + text.innerHTML = 'Mit TUfast kannst du OPAL personalisieren und verbessern! ' + const interact = document.createElement('span') + interact.className = 'interactLink' + interact.textContent = 'Gleich ausprobieren' + interact.addEventListener('click', () => { + chrome.runtime.sendMessage({ cmd: 'open_settings_page', params: 'opalCustomize' }) + }) + insertBanner('customizeOpal', 'Wie du willst:', [text, interact]) + break + } */ + case !bannerArr.includes('helpWanted'): { + const text = document.createElement('span') + text.innerHTML = 'Du hast Bock TUfast weiterzuentwickeln? ' + const interact = document.createElement('span') + interact.className = 'interactLink' + interact.textContent = 'Wir suchen dich!' + interact.addEventListener('click', () => window.open('https://tu-fast.de/jobs', '_blank')) + insertBanner('helpWanted', 'Verstärkung gesucht:', [text, interact]) + break + } + case !bannerArr.includes('customizeRockets') && savedClickCounter > 250: { + const text = document.createElement('span') + text.innerHTML = 'TUfast empfehlen und neue Icons freischalten! ' + const interact = document.createElement('span') + interact.className = 'interactLink' + interact.textContent = 'Los gehts!' + interact.addEventListener('click', () => { + chrome.runtime.sendMessage({ cmd: 'open_settings_page', params: 'rocket_icons_settings' }) + }) + insertBanner('customizeRockets', 'Schnapp\' sie dir alle!', [text, interact]) + break + } + case !bannerArr.includes('submitReview') && !mostLikelySubmittedReview && savedClickCounter > 500: { + const text = document.createElement('span') + text.innerHTML = 'Dann hau\' mal ne gute Bewertung im Store raus! ' + const interact = document.createElement('span') + interact.className = 'interactLink' + interact.textContent = 'Hier geht\'s lang!' + interact.addEventListener('click', async () => { + const isFirefox = navigator.userAgent.includes('Firefox/') // checking window.browser etc does not work here + const webstoreLink = isFirefox ? 'https://addons.mozilla.org/de/firefox/addon/tufast/' : 'https://chrome.google.com/webstore/detail/tufast-tu-dresden/aheogihliekaafikeepfjngfegbnimbk' + window.open(webstoreLink, '_blank') + await new Promise((resolve) => chrome.storage.local.set({ mostLikelySubmittedReview: true }, resolve)) + }) + insertBanner('submitReview', 'Gefällt\'s dir?', [text, interact]) + break + } + } +})() diff --git a/src/contentScripts/other/opal/insertLogo.ts b/src/contentScripts/other/opal/insertLogo.ts new file mode 100644 index 00000000..613f565b --- /dev/null +++ b/src/contentScripts/other/opal/insertLogo.ts @@ -0,0 +1,124 @@ +// Step through the color wheel +function colorStep (noOfSteps: number = 10) { + // Get color variable + const color = document.documentElement.style.getPropertyValue('--counter-color') + + // When no color is set on the element we set the first one: red + if (!color) { + document.documentElement.style.setProperty('--counter-color', 'hsl(0, 100%, 50%)') + return + } + + // Else we will step through the color wheel + const hsl = color.trim().match(/^hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)$/)?.slice(1).map((n) => parseInt(n, 10)) + if (hsl?.length !== 3) return + hsl[0] += Math.round(360 / noOfSteps) % 360 // 10 would be red again but 10 are the rockets so no +1 here + document.documentElement.style.setProperty('--counter-color', `hsl(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%)`) +} + +function resetColor () { + document.documentElement.style.removeProperty('--counter-color') +} + +// Main function +(async () => { + // Get initial values + // Promisified until usage of Manifest V3 + const { selectedRocketIcon, isEnabled, fwdEnabled, foundEasteregg } = await new Promise((resolve) => chrome.storage.local.get(['selectedRocketIcon', 'isEnabled', 'fwdEnabled', 'foundEasteregg'], resolve)) + if (!isEnabled && !fwdEnabled) return + + // Looks weird but I like this more than having everything in a try/catch block + const iconPath = (() => { + try { + const parsed = JSON.parse(selectedRocketIcon) + return parsed && parsed.link ? parsed.link : 'RocketIcons/default_128px' + } catch (e) { + return 'RocketIcons/default_128px' + } + })() + + const iconURL = chrome.runtime.getURL(iconPath) + + // Using an object because the values are mutable and we don't need 10 global mutable values + const onClickSettings: { + counter: number + screenOverlayTimeout: any // NodeJS.Timeout and number do not work, so idc + blocker: boolean + timeUp: boolean + overlay: HTMLDivElement | undefined + } = { + counter: 0, + screenOverlayTimeout: undefined, + blocker: false, + timeUp: true, + overlay: undefined + } + + // Create image node + const logo = document.createElement('img') + logo.src = iconURL + logo.id = 'TUfastIcon' + logo.title = 'Powered by TUFast. Enjoy :)' + document.getElementsByClassName('page-header')[0]?.appendChild(document.createElement('h1')).appendChild(logo) + + // What to do onclick + const onClickWhenFound = () => { + if (onClickSettings.timeUp) chrome.runtime.sendMessage({ cmd: 'open_settings_page', params: 'rocket_icons_settings' }) + } + + if (foundEasteregg) { + logo.addEventListener('click', onClickWhenFound) + return + } + + logo.addEventListener('click', () => { + if (onClickSettings.blocker && !onClickSettings.timeUp) return + onClickSettings.counter++ + + // show screen overlay + if (!onClickSettings.overlay) { + // insert overlay + onClickSettings.overlay = document.createElement('div') + onClickSettings.overlay.id = 'counter' + + document.body.prepend(onClickSettings.overlay) + } else { + // remove existing timeout + if (onClickSettings.screenOverlayTimeout) clearTimeout(onClickSettings.screenOverlayTimeout) + } + + colorStep() + + let timeout: number + + // trigger actions based on counter + if (onClickSettings.counter === 10) { + // live-update the logo + logo.src = chrome.runtime.getURL('assets/icons/RocketIcons/7_128px.png') + // change the onclick function + logo.onclick = onClickWhenFound + // Unlock easteregg + chrome.runtime.sendMessage({ cmd: 'easteregg_found' }) + + onClickSettings.overlay.style.fontSize = '100px' + timeout = 3000 + onClickSettings.blocker = true + onClickSettings.overlay.innerHTML = '🚀 🚀 🚀' + } else { + onClickSettings.overlay.style.fontSize = '150px' + timeout = 1000 + onClickSettings.blocker = false + onClickSettings.overlay.innerHTML = onClickSettings.counter.toString() + } + + onClickSettings.timeUp = false + + onClickSettings.screenOverlayTimeout = setTimeout(() => { + if (onClickSettings.overlay) document.body.removeChild(onClickSettings.overlay) + onClickSettings.overlay = undefined + onClickSettings.counter = 0 + onClickSettings.timeUp = true + resetColor() + }, timeout) + }) +})() diff --git a/src/contentScripts/other/opal/parseCourses.ts b/src/contentScripts/other/opal/parseCourses.ts new file mode 100644 index 00000000..fbbaba1b --- /dev/null +++ b/src/contentScripts/other/opal/parseCourses.ts @@ -0,0 +1,152 @@ +import type { NotificationNamespace } from '../notification' + +interface Course { + name: string; + link: string; +} + +interface ParseResult { + courses: Course[]; + favorites: Course[]; +} + +function parseTable (tbody: HTMLTableSectionElement): ParseResult { + if (!tbody) throw new Error('Cannot parse table') + + // Get the current courses + const favorites: Course[] = [] + const courses: Course[] = [] + + const tableRows: HTMLCollection = tbody.getElementsByTagName('tr') + for (const row of tableRows) { + const linkElement: HTMLAnchorElement = row.getElementsByTagName('a')[0] as HTMLAnchorElement + + if (!linkElement || !linkElement.href || !linkElement.textContent) continue + if (linkElement.textContent.trim().endsWith('[beendet]') || linkElement.textContent.trim().endsWith('[finished]')) continue // Course is finished + + const c = { + link: linkElement.href, + name: linkElement.textContent + } + courses.push(c) + + if (row.getElementsByClassName('icon-star-filled').length > 0) favorites.push(c) + } + + return { courses, favorites } +} + +function parseList (previewContainer: HTMLDivElement): ParseResult { + const courses: Course[] = [] + const favorites: Course[] = [] + + const listItems: HTMLCollection = previewContainer.getElementsByClassName('content-preview') + + for (const item of listItems) { + const linkElement: HTMLAnchorElement = item.querySelector('.content-preview > a') as HTMLAnchorElement + const titleElement = item.querySelector('.content-preview-main .content-preview-title') as HTMLHeadingElement + + if (!linkElement || !linkElement.href || !titleElement || !titleElement.textContent) continue + if (titleElement.textContent.trim().endsWith('[beendet]') || titleElement.textContent.trim().endsWith('[finished]')) continue // Course is finished + + const c = { + link: linkElement.href, + name: titleElement.textContent + } + courses.push(c) + + if (item.getElementsByClassName('icon-star-filled').length > 0) favorites.push(c) + } + + return { courses, favorites } +} + +(async () => { + const notification: NotificationNamespace = await import(chrome.runtime.getURL('contentScripts/other/notification.js')) + + const mainFunction = async () => { + // We are only interested in these two pages + if (window.location.pathname !== '/opal/auth/resource/courses' && window.location.pathname !== '/opal/auth/resource/favorites') return + + // We know one of the two pages is loaded so we only need to check which of those two + const currentPage = window.location.pathname === '/opal/auth/resource/courses' ? 'meine_kurse' : 'favoriten' + + // Show all courses + // If this is possible we don't need to do anything else because the MutationObserver will fire again + const pages = document.querySelectorAll('li.page').length + if (pages > 1) { + (document.getElementsByClassName('pager-showall')[0] as HTMLAnchorElement | undefined)?.click() + return + } + + const tablePanel = document.getElementsByClassName('table-panel')[0] + if (!tablePanel) return + + const previewContainer = tablePanel.getElementsByClassName('content-preview-container')[0] + + const { courses, favorites } = previewContainer ? parseList(previewContainer as HTMLDivElement) : parseTable(tablePanel.getElementsByTagName('tbody')[0]) + + // If the user has no courses - nothing to do here anymore (favorites can only be a subset of courses, so no check needed) + if (courses.length === 0) return + + // Sort them by name + courses.sort((a, b) => a.name.localeCompare(b.name)) + + // Get the old data to check if something changed + const { meine_kurse: currentCoursesStr, favoriten: currentFavouritesStr } = await new Promise((resolve) => chrome.storage.local.get(['meine_kurse', 'favoriten'], resolve)) + // Make an object out of it but in a scoped function so we can handle the error better + const parseJson = (input: string) => { + try { + return JSON.parse(input) + } catch { + return undefined + } + } + + const currentCourses: Course[] = parseJson(currentCoursesStr) + const currentFavourites: Course[] = parseJson(currentFavouritesStr) + + const firstTime = currentCourses === undefined + + // Compare those lists: + // If they are the same we don't need to do anything + const arraysAreSame = (array1: any[], array2: any[]) => { + // When lengths are different we know something changed + if (array1.length !== array2.length) return false + + // We need to match every course from one list to another + // We only need one way because we know the lists are the same size. + return array1.every((course) => { + return !!array2.find(c => c.name === course.name && c.link === course.link) + }) + } + + // We don't want to update the course list on the favorites only page + const coursesChanged = currentPage === 'meine_kurse' && !arraysAreSame(currentCourses || [], courses) + const favouritesChanged = !arraysAreSame(currentFavourites || [], favorites) + + // eslint-disable-next-line camelcase + const updateObj: {meine_kurse?: string, favoriten?: string} = {} + if (coursesChanged) updateObj.meine_kurse = JSON.stringify(courses) + if (favouritesChanged) updateObj.favoriten = JSON.stringify(favorites) + + if (Object.keys(updateObj).length > 0) { + await new Promise((resolve) => chrome.storage.local.set(updateObj, resolve)) + } + + if (firstTime && updateObj.meine_kurse) { + notification.notify('Kurse wurden erfolgreich in TUfast gespeichert! Drücke jetzt Alt + Q um deine Kurse zu sehen!') + } else if (coursesChanged || favouritesChanged) { + notification.notify('Deine Kurse wurden erfolgreich in TUfast geupdatet!') + } + } + + // When the content changes we need to rerun as the tab is not getting reloaded + const content = document.getElementsByClassName('content-container')[0] + if (!content) return + + new MutationObserver(mainFunction).observe(content, { subtree: true, childList: true }) + + // Run the function a first time + mainFunction() +})() diff --git a/src/contentScripts/other/opal/snowflakes.ts b/src/contentScripts/other/opal/snowflakes.ts new file mode 100644 index 00000000..72a2b1e9 --- /dev/null +++ b/src/contentScripts/other/opal/snowflakes.ts @@ -0,0 +1,75 @@ +(async () => { + // All december is christmas time + if ((new Date()).getMonth() !== 11) return + + // Promisified until usage of Manifest V3 + const { flakeState } = await new Promise((resolve) => chrome.storage.local.get(['flakeState'], resolve)) + + const snowflakeSettings: { + container: HTMLDivElement | undefined + switch: HTMLHeadingElement | undefined + currentState: boolean + } = { + container: undefined, + switch: undefined, + currentState: flakeState + } + + // If there is undefined or something wrong in there we set it to true + if (typeof snowflakeSettings.currentState !== 'boolean') snowflakeSettings.currentState = true + + function removeFlakes () { + if (!snowflakeSettings.container) return + try { + const sf = document.getElementById('snowflakes') + if (sf) document.body.removeChild(sf) + snowflakeSettings.container = undefined + } catch (e) {} + } + + function insertFlakes () { + if (snowflakeSettings.container) return + + snowflakeSettings.container = document.createElement('div') + snowflakeSettings.container.classList.add('snowflakes') + snowflakeSettings.container.id = 'snowflakes' + snowflakeSettings.container.setAttribute('aria-hidden', 'true') + for (let i = 0; i < 12; i++) { + const flake = document.createElement('div') + flake.className = 'snowflake' + flake.innerText = '❅' + snowflakeSettings.container.appendChild(flake) + } + // snowflakeNodes.container.innerHTML = '
' + + // add snowflake div to website body + document.body.prepend(snowflakeSettings.container) + } + + // toggle flake state + async function flakesSwitchOnClick (e: MouseEvent) { + snowflakeSettings.currentState = !snowflakeSettings.currentState + + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.storage.local.set({ flakeState: snowflakeSettings.currentState }, resolve)) + + if (snowflakeSettings.currentState) { + (e.target as HTMLHeadingElement).style.color = 'black' + insertFlakes() + } else { + (e.target as HTMLSpanElement).style.color = 'grey' + removeFlakes() + } + } + + const flakeSwitch = document.createElement('h1') + flakeSwitch.id = 'flakeSwitch' + flakeSwitch.title = 'Click me. Winter powered by TUfast.' + flakeSwitch.style.color = snowflakeSettings.currentState ? 'black' : 'grey' + flakeSwitch.textContent = '❅' + flakeSwitch.addEventListener('click', flakesSwitchOnClick) + + document.getElementsByClassName('page-header')[0]?.appendChild(flakeSwitch) + + snowflakeSettings.currentState ? insertFlakes() : removeFlakes() +})() diff --git a/src/contentScripts/other/opal/viewPdfInNewTab.ts b/src/contentScripts/other/opal/viewPdfInNewTab.ts new file mode 100644 index 00000000..495fa43a --- /dev/null +++ b/src/contentScripts/other/opal/viewPdfInNewTab.ts @@ -0,0 +1,47 @@ +function reloadInNewTab () { + // When opening a pdf we get to ".../downloadering?fibercode=xxx" + // We could save the fibrecode to not reopen the same document over and over + // or we could go the simpler way and see if there even is a history for this tab. + // When opening something in a new tab it doesn't have a history. + if (!window.location.pathname.includes('downloadering') || window.history.length < 2) return + + window.open(window.location.href, '_blank') + window.history.back() +} + +(async () => { + const { pdfInNewTab } = await new Promise((resolve) => chrome.storage.local.get(['pdfInNewTab'], resolve)) + if (!pdfInNewTab) return + + // If we get loaded on a pdf we open it in a new tab + reloadInNewTab() + + // The following code is for changing all the links to open in a new tab. + // In combination that won't work, as a new tab is opened but the pdf is loaded in the same tab and then in the new one. + // So in the end there are two new tabs, one is wrong. + + // When only using the method above it works fine for pdf files hosted on Opal. + + // This method modifies any anchor nodes it gets to open in a new tab. + /* const modifyLinks = (nodeList: NodeList|HTMLCollectionOf) => { + for (const node of nodeList) { + if (node.nodeName.toLowerCase() === 'a' && (node as HTMLAnchorElement).href.includes('.pdf')) { + node.addEventListener('click', (e: Event) => { + e.stopImmediatePropagation() + window.open((node as HTMLAnchorElement).href, '_blank') + return false + }) + } + } + } + + // The mutation observer is used to modify links when they are added to the DOM + new MutationObserver((mutations, _observer) => { + for (const mutation of mutations) { + modifyLinks(mutation.addedNodes) + } + }).observe(document.body, { childList: true, subtree: true }) + + // Because the Mutation observer is only running when the DOM is changed, we need to run it manually once + modifyLinks(document.getElementsByTagName('a')) */ +})() diff --git a/src/contentScripts/other/owaUnreadMailcount.ts b/src/contentScripts/other/owaUnreadMailcount.ts new file mode 100644 index 00000000..e256f6b0 --- /dev/null +++ b/src/contentScripts/other/owaUnreadMailcount.ts @@ -0,0 +1,26 @@ +function findUnreadCount (): HTMLDivElement | null { + const nodeList = document.querySelectorAll('div[role="treeitem"]') + for (const node of nodeList) { + const nameNode = node.querySelector('span[title]') + if (!nameNode) continue + const name = nameNode.getAttribute('title') + + if (name === 'Inbox' || name === 'Posteingang') { + return nameNode.nextElementSibling as HTMLDivElement + } + } + return null +} + +function onCharacterChanged (mutationRecord: MutationRecord[], _observer: MutationObserver) { + const count = Number.parseInt(mutationRecord[0]?.target?.textContent || '') || 0 + chrome.runtime.sendMessage({ cmd: 'read_mail_owa', nrOfUnreadMail: count }) +} + +const checkInterval = setInterval(() => { + const unreadCountNode = findUnreadCount() + if (unreadCountNode) { + clearInterval(checkInterval) + new MutationObserver(onCharacterChanged).observe(unreadCountNode, { subtree: true, characterData: true }) + } +}, 100) diff --git a/src/contentScripts/owa.js b/src/contentScripts/owa.js deleted file mode 100644 index ee83aeb8..00000000 --- a/src/contentScripts/owa.js +++ /dev/null @@ -1,110 +0,0 @@ -function loginOWA () { - if (document.getElementById('username') && document.getElementById('password')) { - chrome.runtime.sendMessage({ cmd: 'get_user_data' }, async (result) => { - await result - if (result.user && result.pass) { - chrome.runtime.sendMessage({ cmd: 'show_ok_badge', timeout: 2000 }) - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - chrome.runtime.sendMessage({ cmd: 'perform_login' }) - document.getElementById('username').value = result.user + '@msx.tu-dresden.de' - document.getElementById('password').value = result.pass - document.getElementsByClassName('signinbutton')[0].click() - chrome.runtime.sendMessage({ cmd: 'logged_out', portal: 'loggedOutOwa' }) - } else { - chrome.runtime.sendMessage({ cmd: 'no_login_data' }) - } - }) - } - - // detecting logout - if (document.querySelectorAll('[aria-label="Abmelden"]')[0]) { - document.querySelectorAll('[aria-label="Abmelden"]')[0].addEventListener('click', () => { - chrome.runtime.sendMessage({ cmd: 'logged_out', portal: 'loggedOutOwa' }) - }) - } - - console.log('Auto Login to OWA.') -} - -chrome.storage.local.get(['isEnabled', 'loggedOutOwa'], (result) => { - if (result.isEnabled && !result.loggedOutOwa) { - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', loginOWA) - } else { - loginOWA() - } - } - // sometimes it reloades the page, sometimes it doesnt... - // else if(result.loggedOutOwa) { - // chrome.storage.local.set({loggedOutOwa: undefined}, function() {}) - // setTimeout(() => { chrome.storage.local.set({loggedOutOwa: false}, function() {}) }, 2000); - // } else if(result.loggedOutOwa === undefined) { - // chrome.storage.local.set({loggedOutOwa: false}, function() {}) - // } -}) - -// detecting logout -document.addEventListener('DOMNodeInserted', () => { - // old owa version - if (document.querySelectorAll('[aria-label="Abmelden"]')[0]) { - document.querySelectorAll('[aria-label="Abmelden"]')[0].addEventListener('click', () => { - chrome.runtime.sendMessage({ cmd: 'logged_out', portal: 'loggedOutOwa' }) - }) - } - // new owa version - if (document.querySelectorAll("[autoid='_ho2_2']")[1] && document.querySelectorAll("[autoid='_ho2_2']")[1].innerHTML === 'Abmelden') { - document.querySelectorAll('[aria-label="Abmelden"]')[1].addEventListener('click', () => { - chrome.runtime.sendMessage({ cmd: 'logged_out', portal: 'loggedOutOwa' }) - }) - } -}, false) - -window.onload = () => { - chrome.storage.local.get(['enabledOWAFetch'], (resp) => { - if (resp.enabledOWAFetch) { - // check if all mails are loaded | also check for new owa version - const checkForNode = setInterval(() => { - this.console.log('checking') - if ((document.querySelectorAll("[autoid='_n_x1']")[1] && document.querySelectorAll("[autoid='_n_x1']")[1].textContent !== '') || - (document.querySelectorAll("[autoid='_n_41']")[1] && document.querySelectorAll("[autoid='_n_41']")[1].textContent !== '')) { - readMailObserver() - clearInterval(checkForNode) - } - }, 100) - } - }) -} - -function readMailObserver () { - // use mutation observer to detect page changes - const config = { attributes: true, childList: true, subtree: true, characterData: true } - let nrUnreadMails - const callback = (_mutationsList, _observer) => { - const chrome = this.chrome - // check again, if enabled - chrome.storage.local.get(['enabledOWAFetch'], (resp) => { - if (resp.enabledOWAFetch) { - // get number of unread messages | also check for new owa version - - try { - nrUnreadMails = parseInt(document.querySelectorAll("[autoid='_n_x1']")[1].textContent) - } catch { - nrUnreadMails = parseInt(document.querySelectorAll("[autoid='_n_41']")[1].textContent) - } - - if (isNaN(nrUnreadMails)) nrUnreadMails = 0 - - console.log('Number of unread mails: ' + nrUnreadMails) - - chrome.runtime.sendMessage({ cmd: 'read_mail_owa', NrUnreadMails: nrUnreadMails }) - } - }) - } - - // node containing unreadCount | also check new owa version - let unreadCountNode = document.querySelectorAll("[autoid='_n_41']")[1] - if (!unreadCountNode) unreadCountNode = document.querySelectorAll("[autoid='_n_c']")[0] - - const observer = new MutationObserver(callback) - observer.observe(unreadCountNode, config) -} diff --git a/src/contentScripts/parseOpal.js b/src/contentScripts/parseOpal.js deleted file mode 100644 index f906db61..00000000 --- a/src/contentScripts/parseOpal.js +++ /dev/null @@ -1,138 +0,0 @@ -chrome.storage.local.get(['isEnabled', 'seenInOpalAfterDashbaordUpdate', 'removedOpalBanner', 'saved_click_counter', 'mostLiklySubmittedReview', 'removedReviewBanner', 'neverShowedReviewBanner'], (result) => { - // decide whether to show dashbaord banner - const showDashboardBanner = result.seenInOpalAfterDashbaordUpdate < 5 && !result.removedOpalBanner - chrome.storage.local.set({ seenInOpalAfterDashbaordUpdate: result.seenInOpalAfterDashbaordUpdate + 1 }) - - // wait until full page is loaded - window.addEventListener('load', async () => { - let oldLocationHref = location.href - let parsedCourses = false - - // show banner - if (showDashboardBanner) { showDashboardBannerFunc() } - // - - // if all courses loaded --> parse - if (!document.getElementsByClassName('pager-showall')[0]) { - chrome.runtime.sendMessage({ cmd: 'save_courses', course_list: parseCoursesFromWebPage() }) - parsedCourses = true - // if not --> load all courses - } else { - document.getElementsByClassName('pager-showall')[0].click() - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - parsedCourses = false - } - - // close banner buttons - if (this.document.getElementById('closeOpalBanner')) { - this.document.getElementById('closeOpalBanner').onclick = closeOpalBanner - } - - // use mutation observer to detect page changes - const config = { attributes: true, childList: true, subtree: true } - const callback = (_mutationsList, _observer) => { - const chrome = this.chrome - // detect new page - if (location.href !== oldLocationHref) { - oldLocationHref = location.href - // all courses loaded already --> parse directly - if (!document.getElementsByClassName('pager-showall')[0]) { - const courseList = parseCoursesFromWebPage() - chrome.runtime.sendMessage({ cmd: 'save_courses', course_list: courseList }) - parsedCourses = true - } - // not all courses loaded already --> load all courses - if (document.getElementsByClassName('pager-showall')[0]?.innerText === 'alle anzeigen') { - document.getElementsByClassName('pager-showall')[0].click() - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - parsedCourses = false - } - } - - // parse courses - if (document.getElementsByClassName('pager-showall')[0]) { - if (document.getElementsByClassName('pager-showall')[0].innerText === 'Seiten' && !parsedCourses) { - chrome.runtime.sendMessage({ cmd: 'save_courses', course_list: parseCoursesFromWebPage() }) - parsedCourses = true - } - } - } - const observer = new MutationObserver(callback) - observer.observe(document.body, config) - }, true) -}) - -function closeOpalBanner () { - if (document.getElementById('opalBanner')) { - document.getElementById('opalBanner').remove() - chrome.storage.local.set({ removedOpalBanner: true }, () => { }) - } -} - -// CSS found in banner.css -function showDashboardBannerFunc () { - // create banner div - const banner = document.createElement('div') - banner.classList.add('banner') - // create image for rocket - const img = document.createElement('img') - img.classList.add('banner__icon') - img.src = chrome.runtime.getURL('../assets/icons/RocketIcons/default_128px.png') - // create title - const title = document.createElement('h2') - title.classList.add('banner__title') - title.innerHTML = 'Hier Dashboard öffnen oder Alt + Q drücken' - // create close button - const button = document.createElement('button') - button.classList.add('banner__close') - button.innerHTML = '×' - button.addEventListener('click', () => { - banner.remove() - chrome.storage.local.set({ removedOpalBanner: true }) - }) - - banner.appendChild(img) - banner.appendChild(title) - banner.appendChild(button) - document.body.appendChild(banner) -} - -function parseCoursesFromWebPage () { - let courseList = { type: '', list: [] } - if (location.pathname === '/opal/auth/resource/courses') { courseList.type = 'meine_kurse' } - if (location.pathname === '/opal/auth/resource/favorites') { courseList.type = 'favoriten' } - // there are two options, how the coursse-overview table can be build. - // They are simply tried out - try { - const tableEntries = document.querySelector('.table-panel tbody')?.children - for (const item of tableEntries) { - const name = item.children[2].children[0].getAttribute('title') - const link = item.children[2].children[0].getAttribute('href') - courseList.list.push({ name: name, link: link }) - } - // There is a reported case, where no error is thrown and course_list has entries, but all of them are empty - // Of course this is not wanted. So in this case, also throw an error - const getAllNullEntries = courseList.list.filter(el => !el.link && !el.name) // this contains all entries, where link and name is false (i.e. null) - if (getAllNullEntries.length > 2) { // when more than two null-entries: most likely unwanted case - courseList = { type: '', list: [] } // reset course list - throw new Error('most likely parsing error') // throw error - } - } catch { - const tableEntries = document.querySelectorAll('.table-panel .content-preview-container .list-unstyled .content-preview.content-preview-horizontal') - for (const item of tableEntries) { - try { - const name = item.getElementsByClassName('content-preview-title')[0].innerHTML - const link = item.children[3].getAttribute('href') - courseList.list.push({ name: name, link: link }) - } catch (e) { console.log('Error in parsing course list. Could not parse course list: ' + e) } - } - } - - // alert, if still null-entries found - const getAllNullEntriesFinal = courseList.list.filter(el => !el.link && !el.name) - if (getAllNullEntriesFinal.length > 0) { console.log('Possible Error in parsing courses. Found null entries.') } - // if present, remove all null entries - courseList.list = courseList.list.filter(el => !(!el.link && !el.name)) - - return courseList -} diff --git a/src/contentScripts/pimpHISQIS.js b/src/contentScripts/pimpHISQIS.js deleted file mode 100644 index 7378a509..00000000 --- a/src/contentScripts/pimpHISQIS.js +++ /dev/null @@ -1,128 +0,0 @@ -// returns: [{grade: X.X, isModule: true}] -// only count subjects (not modules!) -function parseGrades () { - const grades = [] - - const tableRows = document.querySelector('form table:not([summary="Liste der Stammdaten des Studierenden"]) > tbody').children - for (const row of tableRows) { - if (row.children.length < 7) continue - - const background = row.children[1].getAttribute('bgcolor') - if (background === '#ADADAD') continue - const grade = Number.parseFloat(row.children[3].innerHTML.trim().replace(',', '.')) - if (Number.isNaN(grade)) continue - const isModule = background === '#DDDDDD' - let weight = isModule ? Number.parseInt(row.children[7].innerHTML.trim()) : 0 - if (Number.isNaN(weight)) weight = 0 - const exam = row.children[1].innerHTML.trim() - if (exam === '' || exam === 'Gesamtnote Zwischenprüfung') continue - - grades.push({ grade, isModule, weight }) - } - - return grades -} - -// return counted number of rounded grades for display -// also showing failed exams which were passed later -function countGrades (rawGrades) { - const gradesCount = [0, 0, 0, 0, 0] - rawGrades.forEach((info) => { - const grade = Math.round(info.grade) - switch (grade) { - case 1: - gradesCount[0] = gradesCount[0] + 1 - break - case 2: - gradesCount[1] = gradesCount[1] + 1 - break - case 3: - gradesCount[2] = gradesCount[2] + 1 - break - case 4: - gradesCount[3] = gradesCount[3] + 1 - break - case 5: - gradesCount[4] = gradesCount[4] + 1 - break - default: - break - } - }) - return gradesCount -} - -// return arithmetic grade average -// not counting failed exams! -// function getArithAverage (rawGrades) { -// // first get all grade-objects that aren't failed, then map it directly as number -// const grades = rawGrades.filter(x => x.grade !== 5.0 && !x.isModule).map(x => x.grade) -// return grades.length ? (grades.reduce((acc, value) => acc + value, 0) / grades.length).toFixed(1) : 0 -// } - -// return weighted grade average -// not counting failed modules! -function getWeightedAverage (rawGrades) { - const grades = rawGrades.filter(x => x.grade !== 5.0 && x.isModule) - const totalWeight = grades.reduce((acc, value) => acc + value.weight, 0) // BUG: - return totalWeight ? (grades.reduce((acc, value) => acc + value.grade * value.weight, 0) / totalWeight).toFixed(1) : 0 -} - - -console.log('Pimping up hisqis...') - -const imgUrl = chrome.runtime.getURL('../assets/images/tufast48.png') -const rawGrades = parseGrades() -const table = document.querySelector('table[summary="Liste der Stammdaten des Studierenden"]') -const notenStatistik = `

- -

Deine Durchschnittnote (nach CP gewichtet): ${getWeightedAverage(rawGrades)}

-

Anzahl Module: ${rawGrades.filter(x => x.isModule).length}

-

Anzahl Prüfungen: ${rawGrades.filter(x => !x.isModule).length}

-

powered by TUfast (entwickelt von Noxdor, C0ntroller)

-

Wechsle zur ... nocht nicht für Firefox!

` -table.insertAdjacentHTML('afterend', notenStatistik) -const ctx = document.getElementById('myChart').getContext('2d') -ctx.canvas.width = 500 -ctx.canvas.height = 250 -// eslint-disable-next-line no-unused-vars, no-undef -const myChart = new Chart(ctx, { - type: 'bar', - data: { - labels: ['1', '2', '3', '4', 'nicht bestanden'], - datasets: [{ - data: countGrades(rawGrades.filter(x => !x.isModule)), - backgroundColor: [ - '#0b2a51', - '#0b2a51', - '#0b2a51', - '#0b2a51', - '#0b2a51' - ], - borderColor: [ - '#0b2a51' - ], - borderWidth: 1 - }] - }, - options: { - responsive: false, - maintainAspectRatio: false, - legend: { - display: false - }, - scales: { - yAxes: [{ - ticks: { - beginAtZero: true - } - }], - xAxes: [{ - scaleLabel: { - // display: true, - // labelString: "here" - } - }] - } - } -}) diff --git a/src/contentScripts/pimpHISQIS_table.js b/src/contentScripts/pimpHISQIS_table.js deleted file mode 100644 index 1e519711..00000000 --- a/src/contentScripts/pimpHISQIS_table.js +++ /dev/null @@ -1,138 +0,0 @@ -/* eslint-disable no-multi-str */ -console.log('pimping table ... maybe :)') - -function setPimpedTable () { - oldTable.style.display = 'none' - pimpedTable.style.display = 'block' - changeTableLink.innerHTML = 'langweiligen, alten Tabelle.' -} - -function setOldTable () { - oldTable.style.display = 'block' - pimpedTable.style.display = 'none' - changeTableLink.innerHTML = 'neuen, coolen TUfast-Tabelle 🔥 (Beta).' -} - -function getGradesFromTable () { - // create container for vuejs table and insert it after the old table - const container = document.createElement('div') - container.id = 'container' - oldTable.insertAdjacentElement('afterend', container) - container.innerHTML = tableHtml - - const table = [] - // second table is the grade table - // first table row index with useful information: 2 - const tableRows = [...document.getElementsByTagName('tbody')[2].getElementsByTagName('tr')] - - // collect all data from the table - tableRows.forEach((row) => { - const newRow = []; - [...row.cells].forEach((tableData) => { - if (tableData.lastElementChild === null) { - newRow.push(tableData.innerHTML.trim().replace(/&.*;/, '')) - } else { - newRow.push(tableData.lastElementChild.innerHTML.trim().replace(/&.*;/, '')) - } - }) - table.push(newRow) - }) - - // if first row is Prüfungsnr., then we need to add dummy row[0] - // this is required, because there are two different views of the hisqis table, dependent on how you navigate there - if (table[0][0] === 'Prüfungsnr.') table.unshift(['dummy']) - - // remove that ugly table from the page - oldTable.style.display = 'none' - - const levels = { - mainLevel: [], - moduleLevel: [], - examLevel: [] - } - - // Logic to figure out which row is a section, module or exam - table.filter((row, index) => row[0][1] === '0' || parseInt(row[0]) < 1000 ? levels.mainLevel.push(index) : []) - table.filter((row, index) => row[0].slice(-2)[0] === '0' && levels.mainLevel.indexOf(index) < 0 ? levels.moduleLevel.push(index) : []) - table.filter((_row, index) => levels.mainLevel.indexOf(index) < 0 && levels.moduleLevel.indexOf(index) < 0 && index > 2 ? levels.examLevel.push(index) : []) - - runVue(table, levels) -} - -// Vue.js logic, attaches Vue to the new container under the old table and draws the new table -function runVue (table, levels) { - // eslint-disable-next-line no-new, no-undef - new Vue({ - el: '#container', - data: { - table, - levels - }, - methods: { - getColour (rowIndex, row) { - rowIndex += 2 - const passedText = row[5] - return this.levels.mainLevel.indexOf(rowIndex) > -1 - ? 'dark' - : this.levels.moduleLevel.indexOf(rowIndex) > -1 - ? 'primary' - : passedText === '' - ? 'dark' - : passedText === 'bestanden' - ? 'success' - : passedText === 'in Bearbeitung' ? 'warn' : 'danger' - } - } - }) -} - -// eslint-disable-next-line no-template-curly-in-string -const tableHtml = "" - -// this needs to be done first -const oldTable = document.getElementsByTagName('table')[2] -const changeTableLink = document.getElementById('changeTableLink') - -// insert pimped table with style display:none -getGradesFromTable() -const pimpedTable = document.getElementById('pimpedTable') - -// check if hisqisPimpedTable is activated -chrome.storage.local.get(['hisqisPimpedTable'], (result) => { - result.hisqisPimpedTable ? setPimpedTable() : setOldTable() -}) - -// listen for event for switching table -changeTableLink.onclick = () => { - const pimpedTableActivated = pimpedTable.style.display !== 'none' - - // switch table - pimpedTableActivated ? setOldTable() : setPimpedTable() - // store permanently - chrome.storage.local.set({ hisqisPimpedTable: !pimpedTableActivated }) -} diff --git a/src/contentScripts/qis.js b/src/contentScripts/qis.js deleted file mode 100644 index eb782a04..00000000 --- a/src/contentScripts/qis.js +++ /dev/null @@ -1,42 +0,0 @@ -function loginQis (isEnabled) { - if (document.getElementsByTagName('a')[4].innerText === 'Ich habe die Nutzungsbedingungen gelesen, verstanden und akzeptiert. >>>') { - document.getElementsByTagName('a')[4].click() - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - } else if (document.getElementById('asdf') && isEnabled) { - chrome.runtime.sendMessage({ cmd: 'get_user_data' }, async (result) => { - await result - if (result.user && result.pass) { - chrome.runtime.sendMessage({ cmd: 'show_ok_badge', timeout: 2000 }) - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - chrome.runtime.sendMessage({ cmd: 'perform_login' }) - document.getElementById('asdf').value = result.user - document.getElementById('fdsa').value = result.pass - document.getElementsByName('submit')[0].click() - } else { - chrome.runtime.sendMessage({ cmd: 'no_login_data' }) - } - }) - } - // abmelden button - if (document.querySelectorAll('#visual-footer-wrapper :nth-child(5)')[0]) { - document.querySelectorAll('#visual-footer-wrapper :nth-child(5)')[0].addEventListener('click', () => { - chrome.runtime.sendMessage({ cmd: 'logged_out', portal: 'loggedOutQis' }) - }) - } - - console.log('Auto Login to hisqis.') -} - -chrome.storage.local.get(['isEnabled', 'loggedOutQis'], (result) => { - if (!result.loggedOutQis) { - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => { - loginQis(result.isEnabled) - }) - } else { - loginQis(result.isEnabled) - } - } else if (result.loggedOutQis) { - chrome.storage.local.set({ loggedOutQis: false }) - } -}) diff --git a/src/contentScripts/selma.js b/src/contentScripts/selma.js deleted file mode 100644 index 34a0eb56..00000000 --- a/src/contentScripts/selma.js +++ /dev/null @@ -1,40 +0,0 @@ -function loginSelma () { - if (document.getElementById('field_user')) { - chrome.runtime.sendMessage({ cmd: 'get_user_data' }, async (response) => { - await response - if (response.user && response.pass) { - chrome.runtime.sendMessage({ cmd: 'show_ok_badge', timeout: 2000 }) - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - chrome.runtime.sendMessage({ cmd: 'perform_login' }) - document.getElementById('field_user').value = response.user - document.getElementById('field_pass').value = response.pass - document.getElementById('logIn_btn').click() - } else { - chrome.runtime.sendMessage({ cmd: 'no_login_data' }) - } - }) - } - // abmelden button - if (document.getElementById('logOut_btn')) { - document.getElementById('logOut_btn').addEventListener('click', () => { - chrome.runtime.sendMessage({ cmd: 'logged_out', portal: 'loggedOutSelma' }) - }) - } - - console.log('Auto Login to Selma.') -} - -chrome.storage.local.get(['isEnabled', 'loggedOutSelma'], (result) => { - if (result.isEnabled && !result.loggedOutSelma) { - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', loginSelma) - } else { - loginSelma() - } - // page is reloaded two times - } else if (result.loggedOutSelma) { - chrome.storage.local.set({ loggedOutSelma: undefined }) - } else if (result.loggedOutSelma === undefined) { - chrome.storage.local.set({ loggedOutSelma: false }) - } -}) diff --git a/src/contentScripts/slub.js b/src/contentScripts/slub.js deleted file mode 100644 index e25d0663..00000000 --- a/src/contentScripts/slub.js +++ /dev/null @@ -1,55 +0,0 @@ -function loginSlub (logoutDuration) { - if (document.getElementsByClassName('login')[0]) { - document.getElementsByClassName('login')[0].click() - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - } else if (document.getElementById('username')) { - chrome.runtime.sendMessage({ cmd: 'get_user_data', platform: 'slub' }, async (result) => { - await result - if (result.user && result.pass && document.getElementsByClassName('form-error').length === 0) { - chrome.runtime.sendMessage({ cmd: 'show_ok_badge', timeout: 2000 }) - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - chrome.runtime.sendMessage({ cmd: 'perform_login' }) - document.getElementById('username').value = result.user - document.getElementById('password').value = result.pass - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - document.querySelector('input.slubbutton[type="submit"]').click() - } else { - chrome.runtime.sendMessage({ cmd: 'no_login_data' }) - } - }) - } - - // abmelden button - if (document.getElementsByClassName('logout')[0]) { - document.getElementsByClassName('logout')[0].addEventListener('click', () => { - const date = new Date() - date.setMinutes(date.getMinutes() + logoutDuration) - document.cookie = `slubLoggedOut; expires=${date.toUTCString()}; path=/; domain=.slub-dresden.de; secure` - }) - } - // abmelden button no 2 - if (document.querySelector('.user a')) { - document.querySelector('.user a').addEventListener('click', () => { - const date = new Date() - date.setMinutes(date.getMinutes() + logoutDuration) - document.cookie = `slubLoggedOut; expires=${date.toUTCString()}; path=/; domain=.slub-dresden.de; secure` - }) - } - - console.log('Auto Login to slub.') -} - -chrome.storage.local.get(['isEnabled', 'logoutDuration'], (result) => { - if (result.isEnabled && !document.cookie.includes('slubLoggedOut')) { - const logoutDuration = result.logoutDuration || 5 - chrome.runtime.sendMessage({ cmd: 'check_user_data', platform: 'slub' }, async (result) => { - await result - if (!result) return - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => loginSlub(logoutDuration)) - } else { - loginSlub(logoutDuration) - } - }) - } -}) diff --git a/src/contentScripts/tex.js b/src/contentScripts/tex.js deleted file mode 100644 index 58b37622..00000000 --- a/src/contentScripts/tex.js +++ /dev/null @@ -1,42 +0,0 @@ -function loginTex () { - document.querySelectorAll("a[href='/saml/login/go']")[0].click() - console.log('Auto Login to tex.') -} - -chrome.storage.local.get(['loggedOutTex'], (result) => { - if (!result.loggedOutTex) { - // there is only a button and no reason to not click - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', loginTex) - } else { - loginTex() - } - } else { - chrome.storage.local.set({ loggedOutTex: false }) - } -}) - -function addLogoutButtonListener () { - const buttons = document.querySelectorAll( - 'button.btn-link.text-left.dropdown-menu-button' - ) - for (const button of buttons) { - if (button.innerHTML.indexOf('Log Out') !== -1) { - // listen for click - button.addEventListener('click', () => { - chrome.runtime.sendMessage({ - cmd: 'logged_out', - portal: 'loggedOutTex' - }) - console.log('Logged out Tex') - }) - } - } -} - -// add event listener for log-out button -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', addLogoutButtonListener) -} else { - addLogoutButtonListener() -} \ No newline at end of file diff --git a/src/contentScripts/tumed.js b/src/contentScripts/tumed.js deleted file mode 100644 index f34d18d7..00000000 --- a/src/contentScripts/tumed.js +++ /dev/null @@ -1,47 +0,0 @@ -function loginTumed () { - // that is the old e-portal. Leave it for now - if (document.querySelectorAll('label[for=__ac_name]')[0] && document.querySelectorAll('label[for=__ac_password]')[0] && document.getElementById('__ac_name') && document.getElementById('__ac_password')) { - chrome.runtime.sendMessage({ cmd: 'get_user_data' }, async (result) => { - await result - if (result.user && result.pass) { - chrome.runtime.sendMessage({ cmd: 'show_ok_badge', timeout: 2000 }) - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 2 }) - chrome.runtime.sendMessage({ cmd: 'perform_login' }) - document.getElementById('__ac_name').value = result.user - document.getElementById('__ac_password').value = result.pass - document.querySelectorAll('input[value=Anmelden]')[0].click() - } else { - chrome.runtime.sendMessage({ cmd: 'no_login_data' }) - } - }) - } - - // abmelden button (SAME FOR OLD AND NEW EPORTAL!) - if (document.getElementById('personaltools-logout')) { - console.log('registered logout button') - document.getElementById('personaltools-logout').addEventListener('click', () => { - console.log('detected logout') - chrome.runtime.sendMessage({ cmd: 'logged_out', portal: 'loggedOutTumed' }) - }) - } - - // this is the new eportal - if (document.getElementById('personaltools-login')) { - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 2 }) - document.getElementById('personaltools-login').click() - } - - console.log('Auto Login to eportal med.') -} - -chrome.storage.local.get(['isEnabled', 'loggedOutTumed'], (result) => { - if (result.isEnabled && !result.loggedOutTumed) { - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', loginTumed) - } else { - loginTumed() - } - } else if (result.loggedOutTumed) { - chrome.storage.local.set({ loggedOutTumed: false }) - } -}) diff --git a/src/contentScripts/videocampus.js b/src/contentScripts/videocampus.js deleted file mode 100644 index ad5c5a5e..00000000 --- a/src/contentScripts/videocampus.js +++ /dev/null @@ -1,37 +0,0 @@ -function loginVideoCampus (logoutDuration) { - if (document.querySelector('#login .loginOptions .form-control[name="entityID"]')) { - // The login form manipulation has to be first else we will always click on "login" - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 3 }) - chrome.runtime.sendMessage({ cmd: 'show_ok_badge', timeout: 2000 }) - chrome.runtime.sendMessage({ cmd: 'perform_login' }) - document.querySelector('#login .loginOptions .form-control[name="entityID"]').value = 'https://idp.tu-dresden.de/idp/shibboleth' - document.querySelector('#login .loginOptions input[type="submit"]').click() - } else if (document.querySelector('.nav-link[href="/login"]')) { - chrome.runtime.sendMessage({ cmd: 'save_clicks', click_count: 1 }) - document.querySelector('.nav-link[href="/login"]').click() - } else if (document.querySelector('.dropdown-item[href="/logout"]')) { - // abmelden button - document.querySelector('.dropdown-item[href="/logout"]').addEventListener('click', () => { - const date = new Date() - date.setMinutes(date.getMinutes() + logoutDuration) - document.cookie = `vcLoggedOut; expires=${date.toUTCString()}; path=/; domain=.videocampus.sachsen.de; secure` - }) - } - - console.log('Auto Login to videocampus.') -} - -chrome.storage.local.get(['isEnabled', 'logoutDuration'], (result) => { - if (result.isEnabled && !document.cookie.includes('vcLoggedOut')) { - const logoutDuration = result.logoutDuration || 5 - chrome.runtime.sendMessage({ cmd: 'check_user_data', platform: 'zih' }, async (result) => { - await result - if (!result) return - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => loginVideoCampus(logoutDuration)) - } else { - loginVideoCampus(logoutDuration) - } - }) - } -}) diff --git a/src/freshContent/popup/popup.js b/src/freshContent/popup/popup.js index 4f4dc068..c3808182 100644 --- a/src/freshContent/popup/popup.js +++ b/src/freshContent/popup/popup.js @@ -139,7 +139,7 @@ window.onload = async () => { const result = await new Promise((resolve) => chrome.storage.local.get([ 'dashboardDisplay', 'ratingEnabledFlag', - 'saved_click_counter', + 'savedClickCounter', 'studiengang', 'closedIntro1', 'ratedCourses', @@ -178,9 +178,8 @@ window.onload = async () => { listSearchFunction() // display saved clicks - if (result.saved_click_counter === undefined) { result.saved_click_counter = 0 } - const time = clicksToTime(result.saved_click_counter) - document.getElementById('saved_clicks').innerHTML = `${result.saved_click_counter} Klicks gespart: ${time}` + const time = clicksToTime(result.savedClickCounter || 0) + document.getElementById('saved_clicks').innerHTML = `${result.savedClickCounter || 0} Klicks gespart: ${time}` document.getElementById('time').onclick = openSettingsTimeSection // display banana at each end of semester for two weeks! @@ -189,7 +188,7 @@ window.onload = async () => { const month = d.getMonth() + 1 // starts at 0 const day = d.getDate() if ((month === 7 && day < 15) || (month === 1 && day > 15)) bananaTime = true - if (result.saved_click_counter > 100 && bananaTime) { + if (result.savedClickCounter > 100 && bananaTime) { document.getElementById('banana').innerHTML = bananaHTML } @@ -243,8 +242,8 @@ window.onload = async () => { // highlight studiengang selection (only once) // Promisified until usage of Manifest V3 - const studiengangResult = await new Promise((resolve) => chrome.storage.local.get(['updateCustomizeStudiengang', 'saved_click_counter'], resolve)) - if (studiengangResult.updateCustomizeStudiengang !== dropdownUpdateId && dropdownUpdateId !== false && studiengangResult.saved_click_counter > -1) { + const studiengangResult = await new Promise((resolve) => chrome.storage.local.get(['updateCustomizeStudiengang', 'savedClickCounter'], resolve)) + if (studiengangResult.updateCustomizeStudiengang !== dropdownUpdateId && dropdownUpdateId !== false && studiengangResult.savedClickCounter > -1) { document.getElementById('select_studiengang_dropdown_id').style.border = '2px solid red' } diff --git a/src/freshContent/settings/settings.html b/src/freshContent/settings/settings.html index 10211e6c..17b10db6 100644 --- a/src/freshContent/settings/settings.html +++ b/src/freshContent/settings/settings.html @@ -184,14 +184,15 @@

  • tumail → Outlook Web App
  • -
  • OPAL → OPAL
  • +
  • opal → OPAL
  • tucloud → Cloudstore TU Dresden
  • hisqis → Hisqis TU Dresden
  • selma → selma TU Dresden
  • jexam → jExam
  • tumatrix → Matrix-Chat TU Dresden
  • -
  • magma → Magma TU Dresden
  • tumed → eportal.med.tu-dresden
  • +
  • videocampus → Videocampus Sachsen
  • +
  • slub → SLUB

diff --git a/src/freshContent/settings/settings.js b/src/freshContent/settings/settings.js index 23006528..dd3b1b89 100644 --- a/src/freshContent/settings/settings.js +++ b/src/freshContent/settings/settings.js @@ -162,7 +162,7 @@ async function displayEnabled () { document.getElementById('switch_pdf_newtab_block').style.visibility = 'hidden' } - document.getElementById('switch_pdf_newtab').checked = result.pdfInNewTab + document.getElementById('switch_pdf_newtab').checked = !!result.pdfInNewTab await updatePlatformStatus(document.getElementById('status_platform').value) /* @@ -288,7 +288,7 @@ async function enableOWAFetch () { async function getAvailableRockets () { // Promisified until usage of Manifest V3 const availableRockets = await new Promise((resolve) => chrome.storage.local.get(['availableRockets'], (resp) => resolve(resp.availableRockets))) - return availableRockets || {} // To prevent errors when undefined + return availableRockets || [] // To prevent errors when undefined } const rocketIconsConfig = { @@ -428,7 +428,7 @@ window.onload = async () => { // apply initial theme // Promisified until usage of Manifest V3 - const theme = await new Promise((resolve) => chrome.storage.local.get('theme', (res) => resolve(res.theme))) + const theme = await new Promise((resolve) => chrome.storage.local.get(['theme'], (res) => resolve(res.theme))) await applyTheme(theme) // prevent transition on page load @@ -464,8 +464,14 @@ window.onload = async () => { // Promisified until usage of Manifest V3 const enabledOWAFetch = await new Promise((resolve) => chrome.storage.local.get(['enabledOWAFetch'], (resp) => resolve(resp.enabledOWAFetch))) if (enabledOWAFetch) { - // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.set({ additionalNotificationOnNewMail: true }, resolve)) + chrome.permissions.request({ permissions: ['notifications'] }, (granted) => { + if (granted) { + chrome.storage.local.set({ additionalNotificationOnNewMail: true }) + } else { + document.getElementById('owa_fetch_msg').innerHTML = 'Für dieses Feature musst du zulassen, dass TUfast Benachrichtigungen senden darf.' + document.getElementById('additionalNotification').checked = false + } + }) } else { document.getElementById('owa_fetch_msg').innerHTML = 'Für dieses Feature musst der Button auf \'Ein\' stehen.' document.getElementById('additionalNotification').checked = false @@ -575,7 +581,7 @@ window.onload = async () => { } // get things from storage// Promisified until usage of Manifest V3 - const result = await new Promise((resolve) => chrome.storage.local.get(['saved_click_counter', 'openSettingsPageParam', 'isEnabled'], resolve)) + const result = await new Promise((resolve) => chrome.storage.local.get(['savedClickCounter', 'openSettingsPageParam', 'isEnabled'], resolve)) await updateSavedStatus() // update saved clicks // see if any params are available @@ -593,7 +599,7 @@ window.onload = async () => { setTimeout(() => window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }), 500) } - this.document.getElementById('settings_comment').innerHTML = 'Bereits ' + clicksToTimeNoIcon(result.saved_click_counter || 0) + this.document.getElementById('settings_comment').innerHTML = 'Bereits ' + clicksToTimeNoIcon(result.savedClickCounter || 0) // Promisified until usage of Manifest V3 - await new Promise((resolve) => chrome.storage.local.set({ openSettingsPageParam: false }, resolve)) + await new Promise((resolve) => chrome.storage.local.remove(['openSettingsPageParam'], resolve)) } diff --git a/src/manifest.json b/src/manifest.json index 9c51f2c8..3108f6b6 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,6 +1,6 @@ { "name": "TUfast TU Dresden", - "version": "6.1.0.0", + "version": "6.2.0.0", "description": "Das Produktivitäts-Tool für TU Dresden Studierende 🚀", "permissions": [ "storage", @@ -12,38 +12,29 @@ "optional_permissions": [ "tabs", "webRequest", - "webRequestBlocking" + "webRequestBlocking", + "notifications", + "cookies" ], "background": { - "scripts": [ - "background.js" - ], + "page": "background.html", "persistent": true }, "content_scripts": [ { "js": [ - "contentScripts/elearningMED.js" + "contentScripts/login/medELearning.js" ], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ "https://elearning.med.tu-dresden.de/*" ] }, { "js": [ - "contentScripts/fwd_opalError.js" + "contentScripts/login/idp.js" ], - "run_at": "document_start", - "matches": [ - "https://bildungsportal.sachsen.de/cgi-bin/forward.cgi" - ] - }, - { - "js": [ - "contentScripts/idp.js" - ], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ "https://idp.tu-dresden.de/*", "https://idp2.tu-dresden.de/*" @@ -51,96 +42,49 @@ }, { "js": [ - "contentScripts/fwd_jexam.js" + "contentScripts/forward/jexam.js" ], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ - "https://jexam.inf.tu-dresden.de/", - "https://www.google.de/search?*q=jexam*", - "https://www.google.com/search?*q=jexam*", - "https://duckduckgo.com/?*q=jexam*", - "https://www.ecosia.org/search?*q=jexam*", - "https://www.bing.com/search?*q=jexam*", - "https://www.startpage.com/do/search?*q=jexam*", - "https://www.startpage.com/do/search?*query=jexam*", - "https://www.qwant.com/?*q=jexam*", - "https://search.brave.com/search?*q=jexam*" + "https://jexam.inf.tu-dresden.de/" ] }, { "js": [ - "contentScripts/fwd_cloudstore.js" - ], - "run_at": "document_start", - "matches": [ - "https://www.google.de/search?*q=tustore*", - "https://www.google.com/search?*q=tustore*", - "https://duckduckgo.com/?*q=tustore*", - "https://www.ecosia.org/search?*q=tustore*", - "https://www.bing.com/search?*q=tustore*", - "https://www.startpage.com/do/search?*q=tustore*", - "https://www.startpage.com/do/search?*query=tustore*", - "https://www.qwant.com/?*q=tustore*", - "https://search.brave.com/search?*q=tustore*", - "https://www.google.de/search?*q=tucloud*", - "https://www.google.com/search?*q=tucloud*", - "https://duckduckgo.com/?*q=tucloud*", - "https://www.ecosia.org/search?*q=tucloud*", - "https://www.bing.com/search?*q=tucloud*", - "https://www.startpage.com/do/search?*q=tucloud*", - "https://www.startpage.com/do/search?*query=tucloud*", - "https://www.qwant.com/?*q=tucloud*", - "https://search.brave.com/search?*q=tucloud*" - ] - }, - { - "js": [ - "contentScripts/fwd_owa.js" - ], + "contentScripts/forward/searchEngines/generic.js" + ], "run_at": "document_start", "matches": [ - "https://www.google.de/search?*q=tumail*", - "https://www.google.com/search?*q=tumail*", - "https://duckduckgo.com/?*q=tumail*", - "https://www.ecosia.org/search?*q=tumail*", - "https://www.bing.com/search?*q=tumail*", - "https://www.startpage.com/do/search?*q=tumail*", - "https://www.startpage.com/do/search?*query=tumail*", - "https://www.qwant.com/?*q=tumail*", - "https://search.brave.com/search?*q=tumail*", - "https://www.google.de/search?*q=tudmail*", - "https://www.google.com/search?*q=tudmail*", - "https://duckduckgo.com/?*q=tudmail*", - "https://www.ecosia.org/search?*q=tudmail*", - "https://www.bing.com/search?*q=tudmail*", - "https://www.startpage.com/do/search?*q=tudmail*", - "https://www.startpage.com/do/search?*query=tudmail*", - "https://www.qwant.com/?*q=tudmail*", - "https://search.brave.com/search?*q=tudmail*" + "https://www.google.de/search?*q=*", + "https://www.google.com/search?*q=*", + "https://duckduckgo.com/?*q=*", + "https://www.ecosia.org/search?*q=*", + "https://www.bing.com/search?*q=*", + "https://www.startpage.com/do/search?*q=*", + "https://www.startpage.com/do/search?*query=*", + "https://www.qwant.com/?*q=*", + "https://search.brave.com/search?*q=*" ] }, { "js": [ - "contentScripts/jexam.js" + "contentScripts/login/jexam.js" ], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ "https://jexam.inf.tu-dresden.de/*" ] }, { "js": [ - "thirdParty/Chart.bundle.min.js", - "contentScripts/pimpHISQIS.js", - "thirdParty/vue.js", - "thirdParty/vuesax.js", - "contentScripts/pimpHISQIS_table.js" + "contentScripts/other/hisqis/pimpAndInjectModules.js" ], "css": [ "thirdParty/vuesax.css", - "styles/components/gradeTable.css" + "styles/components/gradeTable.css", + "styles/contentScripts/hisqis.css" ], - "run_at": "document_end", + "run_at": "document_idle", "matches": [ "https://qis.dez.tu-dresden.de/qisserver/servlet/*" ], @@ -150,96 +94,37 @@ }, { "js": [ - "contentScripts/fwd_opal.js" + "contentScripts/forward/opal.js" ], "run_at": "document_start", "matches": [ "*://opal.de/*", - "https://www.google.de/search?*q=opal*", - "https://www.google.com/search?*q=opal*", - "https://duckduckgo.com/?*q=opal*", - "https://www.ecosia.org/search?*q=opal*", - "https://www.bing.com/search?*q=opal*", - "https://www.startpage.com/do/search?*q=opal*", - "https://www.startpage.com/do/search?*query=opal*", - "https://www.qwant.com/?*q=opal*", - "https://search.brave.com/search?*q=opal*" + "https://bildungsportal.sachsen.de/cgi-bin/forward.cgi" ] }, { "js": [ - "contentScripts/selma.js" + "contentScripts/login/selma.js" ], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ "https://selma.tu-dresden.de/*" ] }, { "js": [ - "contentScripts/fwd_hisqis.js" - ], - "run_at": "document_start", - "matches": [ - "https://www.google.de/search?*q=hisqis*", - "https://www.google.com/search?*q=hisqis*", - "https://duckduckgo.com/?*q=hisqis*", - "https://www.ecosia.org/search?*q=hisqis*", - "https://www.bing.com/search?*q=hisqis*", - "https://www.startpage.com/do/search?*q=hisqis*", - "https://www.startpage.com/do/search?*query=hisqis*", - "https://www.qwant.com/?*q=hisqis*", - "https://search.brave.com/search?*q=hisqis*" - ] - }, - { - "js": [ - "contentScripts/fwd_tumed.js" - ], - "run_at": "document_start", - "matches": [ - "https://www.google.de/search?*q=tumed*", - "https://www.google.com/search?*q=tumed*", - "https://duckduckgo.com/?*q=tumed*", - "https://www.ecosia.org/search?*q=tumed*", - "https://www.bing.com/search?*q=tumed*", - "https://www.startpage.com/do/search?*q=tumed*", - "https://www.startpage.com/do/search?*query=tumed*", - "https://www.qwant.com/?*q=tumed*", - "https://search.brave.com/search?*q=tumed*" - ] - }, - { - "js": [ - "contentScripts/fwd_selma.js" - ], - "run_at": "document_start", - "matches": [ - "https://www.google.de/search?*q=selma*", - "https://www.google.com/search?*q=selma*", - "https://duckduckgo.com/?*q=selma*", - "https://www.ecosia.org/search?*q=selma*", - "https://www.bing.com/search?*q=selma*", - "https://www.startpage.com/do/search?*q=selma*", - "https://www.startpage.com/do/search?*query=selma*", - "https://www.qwant.com/?*q=selma*", - "https://search.brave.com/search?*q=selma*" - ] - }, - { - "js": [ - "contentScripts/qis.js" + "contentScripts/login/qis.js" ], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ "https://qis.dez.tu-dresden.de/qisserver/servlet*" ] }, { "js": [ - "contentScripts/bildungsportal.js" + "contentScripts/login/opalHome.js" ], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ "https://bildungsportal.sachsen.de/opal/resource*", "https://bildungsportal.sachsen.de/opal/home*", @@ -248,26 +133,9 @@ }, { "js": [ - "contentScripts/fwd_matrix.js" - ], - "run_at": "document_start", - "matches": [ - "https://www.google.com/search?*q=tumatrix*", - "https://www.google.de/search?*q=tumatrix*", - "https://duckduckgo.com/?*q=tumatrix*", - "https://www.ecosia.org/search?*q=tumatrix*", - "https://www.bing.com/search?*q=tumatrix*", - "https://www.startpage.com/do/search?*q=tumatrix*", - "https://www.startpage.com/do/search?*query=tumatrix*", - "https://www.qwant.com/?*q=tumatrix*", - "https://search.brave.com/search?*q=tumatrix*" - ] - }, - { - "js": [ - "contentScripts/matrix.js" + "contentScripts/login/matrix.js" ], - "run_at": "document_end", + "run_at": "document_idle", "matches": [ "https://matrix.tu-dresden.de/*" ], @@ -279,27 +147,28 @@ }, { "js": [ - "contentScripts/owa.js" + "contentScripts/other/owaUnreadMailcount.js", + "contentScripts/login/owa.js" ], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ "https://msx.tu-dresden.de/*" ] }, { "js": [ - "contentScripts/cloudstore.js" + "contentScripts/login/cloudstore.js" ], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ "https://cloudstore.zih.tu-dresden.de/*" ] }, { "js": [ - "contentScripts/bildungsportal_main.js" + "contentScripts/login/opalMain.js" ], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ "https://bildungsportal.sachsen.de/opal/shiblogin*", "https://bildungsportal.sachsen.de/opal/login*" @@ -307,75 +176,53 @@ }, { "js": [ - "contentScripts/bildungsportal_insertLogo.js", - "contentScripts/bildungsportal_viewPDFinBrowser.js" - ], - "run_at": "document_start", - "matches": [ - "https://bildungsportal.sachsen.de/opal/*" - ] - }, - { - "js": [ - "contentScripts/bildungsportal_other.js" - ], - "run_at": "document_start", - "matches": [ - "https://bildungsportal.sachsen.de/opal*" - ] - }, - { - "js": [ - "contentScripts/parseOpal.js" + "contentScripts/other/opal/insertLogo.js", + "contentScripts/other/opal/snowflakes.js", + "contentScripts/other/opal/insertBanner.js" ], "css": [ - "styles/components/banner.css" + "styles/contentScripts/opal/logo.css", + "styles/contentScripts/opal/snowflakes.css", + "styles/contentScripts/opal/banner.css" ], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ - "https://bildungsportal.sachsen.de/opal/auth/resource/favorites*", - "https://bildungsportal.sachsen.de/opal/auth/resource/courses*", - "https://bildungsportal.sachsen.de/opal/auth/resource/groups*" + "https://bildungsportal.sachsen.de/opal/*" ] }, { "js": [ - "contentScripts/magma.js" + "contentScripts/other/opal/viewPdfInNewTab.js" ], "run_at": "document_start", "matches": [ - "https://bildungsportal.sachsen.de/magma*" + "https://bildungsportal.sachsen.de/opal/*" ] }, { "js": [ - "contentScripts/fwd_magma.js" + "contentScripts/other/opal/parseCourses.js" ], - "run_at": "document_start", + "css" : [ + "styles/components/notification.css" + ], + "run_at": "document_idle", "matches": [ - "https://www.google.com/search?*q=magma*", - "https://www.google.de/search?*q=magma*", - "https://duckduckgo.com/?*q=magma*", - "https://www.ecosia.org/search?*q=magma*", - "https://www.bing.com/search?*q=magma*", - "https://www.startpage.com/do/search?*q=magma*", - "https://www.startpage.com/do/search?*query=magma*", - "https://www.qwant.com/?*q=magma*", - "https://search.brave.com/search?*q=magma*" + "https://bildungsportal.sachsen.de/opal/auth/resource/*" ] }, { "js": [ - "contentScripts/tumed.js" + "contentScripts/login/medEPortal.js" ], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ "https://eportal.med.tu-dresden.de/*" ] }, { "js": [ - "contentScripts/lskonline.js" + "contentScripts/login/lskonline.js" ], "run_at": "document_start", "matches": [ @@ -384,54 +231,54 @@ }, { "js": [ - "contentScripts/tex.js" + "contentScripts/login/tex.js" ], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ "https://tex.zih.tu-dresden.de/*" ] }, { "js": [ - "contentScripts/videocampus.js" + "contentScripts/login/videocampus.js" ], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ "https://videocampus.sachsen.de/*" ] }, { "js": [ - "contentScripts/gitlab.js" + "contentScripts/login/gitlabMN.js" ], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ "https://gitlab.mn.tu-dresden.de/*" ] }, { "js": [ - "contentScripts/slub.js" + "contentScripts/login/slub.js" ], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ "https://*.slub-dresden.de/*" ] }, { "js": [ - "contentScripts/fwd_qwant.js" + "contentScripts/forward/searchEngines/qwant.js" ], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ "https://www.qwant.com/*" ] }, { "js": [ - "contentScripts/fwd_startpage.js" + "contentScripts/forward/searchEngines/startpage.js" ], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ "https://www.startpage.com/*" ] @@ -452,7 +299,12 @@ }, "web_accessible_resources": [ "assets/*", - "assets/icons/RocketIcons/default_128px.png" + "assets/icons/RocketIcons/default_128px.png", + "contentScripts/forward/searchEngines/common.js", + "contentScripts/login/common.js", + "contentScripts/other/hisqis/*", + "contentScripts/other/notification.js", + "snowpack/pkg/*" ], "manifest_version": 2, "commands": { diff --git a/src/modules/credentials.ts b/src/modules/credentials.ts new file mode 100644 index 00000000..6b684843 --- /dev/null +++ b/src/modules/credentials.ts @@ -0,0 +1,205 @@ +// info: user = username | pass = password +export interface UserData { + user: string | undefined; + pass: string | undefined; +} + +interface UserDataStore { + [platform: string]: UserData; +} + +const isFirefox = !!(typeof globalThis.browser !== 'undefined' && globalThis.browser.runtime && globalThis.browser.runtime.getBrowserInfo) + +// create hash from input-string (can also be json of course) +// output hash is always of same length and is of type buffer +async function hashDigest (str: string) { + return await crypto.subtle.digest('SHA-256', (new TextEncoder()).encode(str)) +} + +// get key for encryption +async function getKeyBuffer () { + // async fetch of system information + // Promisified until usage of Manifest V3 + const sysInfo = await new Promise((resolve) => { + let sysInfo: string = '' + + // key differs between browsers, because different APIs + if (isFirefox) { + sysInfo += window.navigator.hardwareConcurrency + } else { + // chrome, edge and everything else + chrome.system.cpu.getInfo((info: any) => { + delete info.processors + if (info.temperatures) delete info.temperatures // Chrome OS only + sysInfo += JSON.stringify(info) + }) + } + + chrome.runtime.getPlatformInfo((info) => { + sysInfo += JSON.stringify(info) + resolve(sysInfo) + }) + }) + + // create key + return await crypto.subtle.importKey('raw', await hashDigest(sysInfo), + { name: 'AES-CBC' }, + false, + ['encrypt', 'decrypt']) +} + +export async function setUserData (userData: UserData, platform = 'zih') { + if (!userData || !userData.user || !userData.pass || !platform) return + + // local function so it's not easily called from elsewhere + const encode = async (decoded: string) => { + const dataEncoded = (new TextEncoder()).encode(decoded) + const keyBuffer = await getKeyBuffer() + const iv = crypto.getRandomValues(new Uint8Array(16)) + + // encrypt + let dataEnc = await crypto.subtle.encrypt( + { + name: 'AES-CBC', + iv: iv + }, + keyBuffer, + dataEncoded + ) + + // adjust format to save encrypted data in local storage + dataEnc = Array.from(new Uint8Array(dataEnc)) + dataEnc = dataEnc.map(byte => String.fromCharCode(byte)).join('') + dataEnc = btoa(dataEnc) + const ivStr = Array.from(iv).map(b => ('00' + b.toString(16)).slice(-2)).join('') + return ivStr + dataEnc + } + + const user = await encode(userData.user) + const pass = await encode(userData.pass) + + let dataObj: UserDataStore + try { + // Promisified until usage of Manifest V3 + const data = await new Promise((resolve) => chrome.storage.local.get(['udata'], (data) => resolve(data.udata))) + if (typeof data !== 'string') throw Error() + dataObj = JSON.parse(data) + } catch { + // data field is undefined or broken -> reset it + dataObj = {} + } + dataObj[platform] = { user, pass } + + // Promisified until usage of Manifest V3 + await chrome.storage.local.set({ udata: JSON.stringify(dataObj) }) +} + +// check if username, password exist +export async function userDataExists (platform: string | undefined) { + if (typeof platform === 'string') { + // Query for a specific platform + const { user, pass } = await getUserData(platform) + return !!(user && pass) + } else { + // Query for any platform + // Promisified until usage of Manifest V3 + const data = await new Promise((resolve) => chrome.storage.local.get(['udata'], (data) => resolve(data.udata))) + if (typeof data !== 'string') return false + + try { + const dataJson = JSON.parse(data) + for (const platform of Object.keys(dataJson)) { + const { user, pass } = await getUserData(platform) + if (user && pass) return true + } + } catch { } + } + return false +} + +// Legacy +export const loginDataExists = (platform = 'zih') => userDataExists(platform) + +// return {user: string, pass: string} +// decrypt and return user data +// a lot of encoding and transforming needs to be done, in order to provide all values in the right format +export async function getUserData (platform: string = 'zih'): Promise { + // get required data for decryption + const keyBuffer = await getKeyBuffer() + // Promisified until usage of Manifest V3 + const data = await new Promise((resolve) => chrome.storage.local.get(['udata'], (data) => resolve(data.udata))) + + // check if data exists, else return + if (typeof data !== 'string' || !platform) { + return ({ user: undefined, pass: undefined }) + } + + // local function so it's not easily called from elsewhere + const decode = async (encoded: string) => { + if (!encoded) return undefined + const ivArr = encoded.slice(0, 32).match(/.{2}/g)?.map(byte => parseInt(byte, 16)) + if (!ivArr) return undefined + const iv = new Uint8Array(ivArr) + const dataEncryptedStr = atob(encoded.slice(32)) + const dataEncrypted = new Uint8Array(dataEncryptedStr.match(/[\s\S]/g)?.map(ch => ch.charCodeAt(0)) || []) + if (dataEncrypted.length === 0) return undefined + + // decrypt + const decoded = await crypto.subtle.decrypt( + { + name: 'AES-CBC', + iv: iv + }, + keyBuffer, + dataEncrypted + ) + + // adjust to useable format + return new TextDecoder().decode(decoded) + } + + try { + const userDataJson = JSON.parse(data) + const { user: encUser, pass: encPass } = userDataJson[platform] + return { user: await decode(encUser), pass: await decode(encPass) } + } catch { + return { user: undefined, pass: undefined } + } +} + +// return {user: string, pass: string} +// This is the old method to get the user data. It will be preserved until probably every installation uses the new format +export async function getUserDataLagacy (): Promise { + // get required data for decryption + const keyBuffer = await getKeyBuffer() + // async fetch of user data + // Promisified until usage of Manifest V3 + const data = await new Promise((resolve) => chrome.storage.local.get(['Data'], (data) => resolve(data.Data))) + + // check if Data exists, else return + if (data === undefined || data === 'undefined') { + return ({ user: undefined, pass: undefined }) + } + const ivSlice = data.slice(0, 32).match(/.{2}/g)?.map(byte => parseInt(byte, 16)) + if (!ivSlice) return ({ user: undefined, pass: undefined }) + + const iv = new Uint8Array(ivSlice) + const userDataEncryptedStr = atob(data.slice(32)) + const userDataEncrypted = new Uint8Array(userDataEncryptedStr.match(/[\s\S]/g)?.map(ch => ch.charCodeAt(0)) || []) + if (userDataEncrypted.length === 0) return ({ user: undefined, pass: undefined }) + + // decrypt + let userData = await crypto.subtle.decrypt( + { + name: 'AES-CBC', + iv: iv + }, + keyBuffer, + userDataEncrypted + ) + + // adjust to useable format + userData = new TextDecoder().decode(userData) + userData = userData.split('@@@@@') + return ({ user: userData[0], pass: userData[1] }) +} diff --git a/src/modules/owaFetch.ts b/src/modules/owaFetch.ts new file mode 100644 index 00000000..0c8b559e --- /dev/null +++ b/src/modules/owaFetch.ts @@ -0,0 +1,251 @@ +import { getUserData } from './credentials' + +// function for custom URIEncoding +function customURIEncoding (str: string) { + str = encodeURIComponent(str) + str = str + .replace('!', '%21') + .replace("'", '%27') + .replace('(', '%28') + .replace(')', '%29') + .replace('~', '%7E') + return str +} + +// function to log msx.tu-dresden.de/owa/ and retrieve the .json containing information about EMails +export async function fetchOWA (username: string, password: string, logout: boolean) { + // encodeURIComponent and encodeURI are not working for all chars. See documentation. Thats why I implemented custom encoding. + username = customURIEncoding(username) + password = customURIEncoding(password) + + // login + await fetch('https://msx.tu-dresden.de/owa/auth.owa', { + headers: { + accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + 'accept-language': + 'de-DE,de;q=0.9,en-DE;q=0.8,en-GB;q=0.7,en-US;q=0.6,en;q=0.5', + 'cache-control': 'max-age=0', + 'content-type': 'application/x-www-form-urlencoded', + 'Access-Control-Allow-Origin': '*', + 'sec-fetch-dest': 'document', + 'sec-fetch-mode': 'navigate', + 'sec-fetch-site': 'same-origin', + 'sec-fetch-user': '?1', + 'upgrade-insecure-requests': '1' + }, + referrer: + 'https://msx.tu-dresden.de/owa/auth/logon.aspx?replaceCurrent=1&url=https%3a%2f%2fmsx.tu-dresden.de%2fowa%2f%23authRedirect%3dtrue', + referrerPolicy: 'strict-origin-when-cross-origin', + body: `destination=https%3A%2F%2Fmsx.tu-dresden.de%2Fowa%2F%23authRedirect%3Dtrue&flags=4&forcedownlevel=0&username=${username}%40msx.tu-dresden.de&password=${password}&passwordText=&isUtf8=1`, + method: 'POST', + mode: 'no-cors', + credentials: 'include' + }) + + const owaResp = await fetch('https://msx.tu-dresden.de/owa/', { + headers: { + accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + 'accept-language': + 'de-DE,de;q=0.9,en-DE;q=0.8,en-GB;q=0.7,en-US;q=0.6,en;q=0.5', + 'cache-control': 'max-age=0', + 'sec-fetch-dest': 'document', + 'sec-fetch-mode': 'navigate', + 'Access-Control-Allow-Origin': '*', + 'sec-fetch-site': 'same-origin', + 'sec-fetch-user': '?1', + 'upgrade-insecure-requests': '1' + }, + referrer: + 'https://msx.tu-dresden.de/owa/auth/logon.aspx?replaceCurrent=1&url=https%3a%2f%2fmsx.tu-dresden.de%2fowa', + referrerPolicy: 'strict-origin-when-cross-origin', + body: null, + method: 'GET', + mode: 'cors', + credentials: 'include' + }) + + const respText = await owaResp.text() + const searchString = "window.clientId = '" + const idxStart = respText.indexOf(searchString) + const idxEnd = respText.indexOf("'", idxStart + searchString.length) + if (idxStart === -1 || idxEnd === -1) { + // console.error(respText) + return + } + const clientId = respText.substring(idxStart + 1, idxEnd) + const corrId = clientId + '_' + new Date().getTime() + + const mailInfoRsp = await fetch( + 'https://msx.tu-dresden.de/owa/sessiondata.ashx?appcacheclient=0', + { + headers: { + accept: '*/*', + 'accept-language': + 'de-DE,de;q=0.9,en-DE;q=0.8,en-GB;q=0.7,en-US;q=0.6,en;q=0.5', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'Access-Control-Allow-Origin': '*', + 'sec-fetch-site': 'same-origin', + 'x-owa-correlationid': corrId, + 'x-owa-smimeinstalled': '1' + }, + referrer: 'https://msx.tu-dresden.de/owa/', + referrerPolicy: 'strict-origin-when-cross-origin', + body: null, + method: 'POST', + mode: 'cors', + credentials: 'include' + } + ) + + const mailInfoJson = await mailInfoRsp.json() + + // only logout, if user is not using owa in browser session + if (logout) { + // console.log('Logging out from owa..') + await fetch('https://msx.tu-dresden.de/owa/logoff.owa', { + headers: { + accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + 'accept-language': + 'de-DE,de;q=0.9,en-DE;q=0.8,en-GB;q=0.7,en-US;q=0.6,en;q=0.5', + 'sec-fetch-dest': 'document', + 'Access-Control-Allow-Origin': '*', + 'sec-fetch-mode': 'navigate', + 'sec-fetch-site': 'same-origin', + 'sec-fetch-user': '?1', + 'upgrade-insecure-requests': '1' + }, + referrer: 'https://msx.tu-dresden.de/owa/', + referrerPolicy: 'strict-origin-when-cross-origin', + body: null, + method: 'GET', + mode: 'cors', + credentials: 'include' + }) + } + + return mailInfoJson +} + +// extract number of unread messages in owa +export function countUnreadMsg (json: any) { + // console.log(json) + const folder = json.findFolders.Body.ResponseMessages.Items[0].RootFolder.Folders.find( + (obj) => obj.DisplayName === 'Inbox' || obj.DisplayName === 'Posteingang' + ) + return folder.UnreadCount +} + +// checks, if user currently uses owa in browser +export async function owaIsOpened () { + const uri = 'msx.tu-dresden.de' + const tabs = await getAllChromeTabs() + // Find element with msx in uri, -1 if none found + if (tabs.findIndex((element) => element.url?.includes(uri)) >= 0) { + // console.log('currently opened owa') + return true + } else return false +} + +function getAllChromeTabs (): Promise { + // Promisified until usage of Manifest V3 + return new Promise((resolve) => chrome.tabs.query({}, resolve)) +} + +// start OWA fetch funtion based on interval +export async function enableOWAFetch () { + // console.log('starting to fetch from owa...'); + await owaFetch() + chrome.alarms.create('fetchOWAAlarm', { delayInMinutes: 1, periodInMinutes: 5 }) + chrome.alarms.onAlarm.addListener(async (alarm) => { + if (alarm.name === 'fetchOWAAlarm') await owaFetch() + }) +} + +export async function disableOwaFetch () { + // console.log('stopped owa connection') + await setBadgeUnreadMails(0) + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.alarms.clear('fetchOWAAlarm', resolve)) + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.storage.local.remove(['numberOfUnreadMails'], resolve)) +} + +export async function readMailOWA (numberOfUnreadMails: number) { + // set badge and local storage + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.storage.local.set({ numberOfUnreadMails }, resolve)) + await setBadgeUnreadMails(numberOfUnreadMails) +} + +export async function setBadgeUnreadMails (numberOfUnreadMails: number) { + // set badge + if (!numberOfUnreadMails) { + await showBadge('', '#4cb749') + } else if (numberOfUnreadMails > 99) { + await showBadge('99+', '#4cb749') + } else { + await showBadge(numberOfUnreadMails.toString(), '#4cb749') + } +} + +// show badge +async function showBadge (text: string, color: string) { + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.browserAction.setBadgeText({ text }, resolve)) + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.browserAction.setBadgeBackgroundColor({ color }, resolve)) +} + +export async function owaFetch () { + // dont logout if user is currently using owa in browser + const logout = !(await owaIsOpened()) + console.log('Executing OWA fetch ...') + + // get user data + const { user, pass } = await getUserData('zih') + if (!user || !pass) return + // call fetch + const mailInfoJson = await fetchOWA(user, pass, logout).catch(() => {}) + if (!mailInfoJson) return + // check # of unread mails + const numberOfUnreadMails = countUnreadMsg(mailInfoJson) + // console.log('Unread mails in OWA: ' + numberOfUnreadMails) + + // alert on new Mail + // Promisified until usage of Manifest V3 + const result = await new Promise((resolve) => chrome.storage.local.get(['numberOfUnreadMails', 'additionalNotificationOnNewMail'], resolve)) + // Promisified until usage of Manifest V3 + const notificationGranted = await new Promise((resolve) => chrome.permissions.contains({ permissions: ['notifications'] }, resolve)) + + if (result.additionalNotificationOnNewMail && typeof result.numberOfUnreadMails !== 'undefined' && result.numberOfUnreadMails < numberOfUnreadMails) { + if (notificationGranted) { + // Promissified notification + await new Promise((resolve) => chrome.notifications.create( + 'tuFastNewEmailNotification', + { + type: 'basic', + message: `Du hast ${numberOfUnreadMails} neue E-Mail${numberOfUnreadMails > 1 ? 's' : ''}`, + title: 'Neue E-Mails', + iconUrl: 'assets/icons/RocketIcons/default_128px.png' + }, + (_id) => resolve(undefined) + )) + } else { + // Fallback + // Maybe we should keep it for compatibility? + if (confirm(`Du hast ${numberOfUnreadMails} neue E-Mail${numberOfUnreadMails > 1 ? 's' : ''} in deinem TU Dresden Postfach!\nDr\u00FCcke 'Ok' um OWA zu \u00F6ffnen.`)) { + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.tabs.create({ url: 'https://msx.tu-dresden.de/owa/' }, resolve)) + } + } + } + + // set badge and local storage + // Promisified until usage of Manifest V3 + await new Promise((resolve) => chrome.storage.local.set({ numberOfUnreadMails }, resolve)) + await setBadgeUnreadMails(numberOfUnreadMails) +} diff --git a/src/styles/components/gradeTable.scss b/src/styles/components/gradeTable.scss index c2dab23d..55d25aae 100644 --- a/src/styles/components/gradeTable.scss +++ b/src/styles/components/gradeTable.scss @@ -1,67 +1,71 @@ -.tr-table-state-dark .td { - background-color: #0B2A51DD; - color: white; -} - -.tr-table-state-danger .td, .red { - background-color: #91BFD666; -} +$clr-blue-dark: rgba(11, 42, 81); +$clr-blue-light: rgba(0, 86, 127); +$clr-green: rgba(56, 199, 158); +$clr-grey: rgba(231, 220, 220); -.tr-table-state-success .td, .green { - background-color: #38C79E66; -} +#gradeTable { + .table-header { + display: grid; + grid-template-columns: 2fr 1fr 2fr; -.tr-table-state-primary .td, .blue { - background-color: #00567FAA; - color: white; -} + &__title { + grid-column: 2; + font-size: 1.2rem; + } + &__helpers { + grid-column: 3; + display: flex; + gap: 1rem; + justify-content: flex-end; + align-items: center; + margin-right: 1rem; + } + &__helper { + display: flex; + align-items: center; + gap: .5rem; + } + &__color { + height: 24px; + width: 24px; + border-radius: 4px; + &--0 { background-color: $clr-blue-light; } + &--1 { background-color: $clr-green; } + &--2 { background-color: $clr-grey; } + } + } -.tr-table-state-warn .td { - background-color: #E5EE1B99; -} + background-color: $clr-blue-dark; + thead tr th { + background-color: $clr-blue-dark; + .dataTable-sorter { + color: white; + } + } -.tr-table-state-success, .tr-table-state-primary, .tr-table-state-danger, .tr-table-state-warning { - color: black; -} - -.info-row { - display: flex; - flex-direction: row; - justify-content: space-around; - width: 50%; -} - -.square { - width: 20px; - height: 20px; - margin: 0 8px; -} - -.info-row__info { - display: flex; - flex-direction: row; - align-items: center; - width: 33% -} - -#container { - display: flex; - align-items: center; - justify-content: center; -} - -#pimpedTable { - width: 95vw; - -} - -#pimpedTable .td { - font-size: inherit; - padding: 8px 5px; + tbody { + .meta td { + background-color: $clr-blue-light; + color: white; + } + .module td { + background-color: lighten($clr-blue-light, 10%); + color: white; + } + .exam td { + background-color: $clr-green; + color: black; + } + .exam-nopass td { + background-color: $clr-grey; + color: black; + } + } } /* fix original css */ #portal-footer { height: inherit !important; -} \ No newline at end of file +} + diff --git a/src/styles/components/notification.scss b/src/styles/components/notification.scss new file mode 100644 index 00000000..026610c2 --- /dev/null +++ b/src/styles/components/notification.scss @@ -0,0 +1,69 @@ +.notifications { + position: fixed; + bottom: .8rem; + right: .8rem; + width: 25vw; + height: 50vh; + z-index: 50; + + display: flex; + flex-direction: column; + gap: 1rem; + justify-content: flex-end; + + &__notification { + animation: 500ms fade-in ease-in-out; + border-radius: .8rem; + background-color: #47BB5D; + flex: 0 0 100px; + color: white; + font-size: 1.4rem; + font-weight: bigger; + position: relative; + user-select: none; + + display: flex; + gap: .8rem; + align-items: center; + padding-inline: 1.2rem; + } + + &__close-button { + position: absolute; + top: 2px; + right: 8px; + + &:hover { + cursor: pointer; + color: red; + } + } +} + +.fade-out { + animation: 500ms fade-out ease-in; +} + +@keyframes fade-in { + from { + transform: translateY(50px); + opacity: 0; + } + + to { + transform: translateY(0px); + opacity: 1; + } +} + +@keyframes fade-out { + from { + opacity: 1; + transform: translateX(0px); + } + + to { + opacity: 0; + transform: translateX(500px); + } +} diff --git a/src/styles/contentScripts/hisqis.scss b/src/styles/contentScripts/hisqis.scss new file mode 100644 index 00000000..3732e596 --- /dev/null +++ b/src/styles/contentScripts/hisqis.scss @@ -0,0 +1,52 @@ +#TUfastGradeContainer, #TUfastTableInfo { + padding-left: 10px; + + canvas, .info { + margin: 0 auto; + } + .info { + font-size: 1rem; + font-weight: bold; + } + +} + +#TUfastCredits { + font-size: 1rem; + font-weight: bold; + padding-left: 10px; +} + +#gradeTable { + font-size: 1rem; + caption-side: top; + + thead { + font-weight: bold; + font-size: 1.2em; + + th { + background: #8b8b8b; + padding: 10px; + } + } + + td { + padding: 10px; + } + + .meta { + background: #969696; + font-size: 1.1em; + font-weight: bold; + } + .module { + background: #3b69be; + } + .exam { + background: #20a32b; + } + .exam-nopass { + background: #eb9090; + } +} \ No newline at end of file diff --git a/src/styles/contentScripts/opal/banner.css b/src/styles/contentScripts/opal/banner.css new file mode 100644 index 00000000..97a03a40 --- /dev/null +++ b/src/styles/contentScripts/opal/banner.css @@ -0,0 +1,38 @@ +#TUfastBanner { + font-size: 22px; + min-height: 55px; + line-height: 55px; + text-align: center; + padding: 10px; + padding-right: 30px; + /*background: red; + z-index: 5000;*/ +} + +#TUfastBanner img { + position: relative; + right: 2px; + height: 33px; +} + +#TUfastBanner .closeLink { + position: absolute; + right: 10px; + font-size: 30px; + color: #888; + z-index: 10; + cursor: pointer; +} + +#TUfastBanner .interactLink { + cursor: pointer; + text-decoration: underline; +} + +#TUfastBanner .interactLink::before { + content: "> "; +} + +#TUfastBanner .interactLink::after { + content: " <"; +} \ No newline at end of file diff --git a/src/styles/contentScripts/opal/logo.css b/src/styles/contentScripts/opal/logo.css new file mode 100644 index 00000000..5d5d9ae9 --- /dev/null +++ b/src/styles/contentScripts/opal/logo.css @@ -0,0 +1,19 @@ +#counter { + color: var(--counter-color); + opacity: 1; + position: fixed; + top: 50%; + left: 50%; + margin-right: -50%; + transform: translate(-50%, -50%); + z-index: 999; +} + +#counterContainer { + position: relative; +} + +#TUfastIcon { + max-height: 0.9em; + cursor: pointer; +} \ No newline at end of file diff --git a/src/styles/contentScripts/opal/snowflakes.css b/src/styles/contentScripts/opal/snowflakes.css new file mode 100644 index 00000000..f9efe2b8 --- /dev/null +++ b/src/styles/contentScripts/opal/snowflakes.css @@ -0,0 +1,150 @@ +@-webkit-keyframes snowflakes-fall { + 0% { + top: -10% + } + + 100% { + top: 100% + } +} + +@-webkit-keyframes snowflakes-shake { + + 0%, + 100% { + -webkit-transform: translateX(0); + transform: translateX(0) + } + + 50% { + -webkit-transform: translateX(80px); + transform: translateX(80px) + } +} + +@keyframes snowflakes-fall { + 0% { + top: -10% + } + + 100% { + top: 100% + } +} + +@keyframes snowflakes-shake { + + 0%, + 100% { + transform: translateX(0) + } + + 50% { + transform: translateX(80px) + } +} + +.snowflake { + color: #fff; + font-size: 1em; + font-family: Arial, sans-serif; + text-shadow: 0 0 5px #000; + pointer-events: none; + position: fixed; + top: -10%; + z-index: 9999; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: default; + -webkit-animation-name: snowflakes-fall, snowflakes-shake; + -webkit-animation-duration: 10s, 3s; + -webkit-animation-timing-function: linear, ease-in-out; + -webkit-animation-iteration-count: infinite, infinite; + -webkit-animation-play-state: running, running; + animation-name: snowflakes-fall, snowflakes-shake; + animation-duration: 10s, 3s; + animation-timing-function: linear, ease-in-out; + animation-iteration-count: infinite, infinite; + animation-play-state: running, running +} + +.snowflake:nth-of-type(0) { + left: 1%; + -webkit-animation-delay: 0s, 0s; + animation-delay: 0s, 0s +} + +.snowflake:nth-of-type(1) { + left: 10%; + -webkit-animation-delay: 1s, 1s; + animation-delay: 1s, 1s +} + +.snowflake:nth-of-type(2) { + left: 20%; + -webkit-animation-delay: 6s, .5s; + animation-delay: 6s, .5s +} + +.snowflake:nth-of-type(3) { + left: 30%; + -webkit-animation-delay: 4s, 2s; + animation-delay: 4s, 2s +} + +.snowflake:nth-of-type(4) { + left: 40%; + -webkit-animation-delay: 2s, 2s; + animation-delay: 2s, 2s +} + +.snowflake:nth-of-type(5) { + left: 50%; + -webkit-animation-delay: 8s, 3s; + animation-delay: 8s, 3s +} + +.snowflake:nth-of-type(6) { + left: 60%; + -webkit-animation-delay: 6s, 2s; + animation-delay: 6s, 2s +} + +.snowflake:nth-of-type(7) { + left: 70%; + -webkit-animation-delay: 2.5s, 1s; + animation-delay: 2.5s, 1s +} + +.snowflake:nth-of-type(8) { + left: 80%; + -webkit-animation-delay: 1s, 0s; + animation-delay: 1s, 0s +} + +.snowflake:nth-of-type(9) { + left: 90%; + -webkit-animation-delay: 3s, 1.5s; + animation-delay: 3s, 1.5s +} + +.snowflake:nth-of-type(10) { + left: 25%; + -webkit-animation-delay: 2s, 0s; + animation-delay: 2s, 0s +} + +.snowflake:nth-of-type(11) { + left: 65%; + -webkit-animation-delay: 4s, 2.5s; + animation-delay: 4s, 2.5s +} + +#flakeSwitch { + padding-top: 2px; + padding-left: 3px; + cursor: pointer; + font-size: 30px; +} \ No newline at end of file