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 = " \
-
\
- \
- Deine Notenübersicht
\
- \
-
\
-
\
-
Bestandene Prüfung
\
-
\
-
\
-
Nicht bestandene Prüfung
\
-
\
-
\
- \
- \
- {{header_text}} \
- \
- \
- \
- {{td}} \
- \
- \
- \
-
"
-
-// 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