Compare commits

..

103 Commits
0.5.0 ... 0.8.0

Author SHA1 Message Date
undergroundwires
cfedcd724c update screenshot 2020-11-01 18:55:40 +01:00
undergroundwires
fd28eaad06 hide scrollbars on code area when not overflowing 2020-11-01 18:49:27 +01:00
undergroundwires
8ce06facbd add support for shared functions #41 2020-11-01 18:36:55 +01:00
undergroundwires
1a9db31c77 add all dist folders in gitignore because of files auto-generated by vscode 2020-11-01 01:53:27 +01:00
undergroundwires
ac70b063b8 rework disabling metadata retrieval 2020-10-28 22:32:05 +01:00
undergroundwires
d0019c2c9b update recommendations to be safer and consistent 2020-10-27 16:41:56 +01:00
sopla4ever
4c68408f1e add scripts to increase cryptography, enable camera notifications and remove todo app (#36)
Co-authored-by: undergroundwires <undergroundwires@users.noreply.github.com>
2020-10-26 11:52:48 +01:00
undergroundwires
1072505219 show icons on cards during indeterminate and fully selected states 2020-10-25 12:55:40 +01:00
undergroundwires
07fc555324 change "download" button to "save" on desktop 2020-10-23 17:11:44 +01:00
undergroundwires
50fb29038a switch places of download and copy buttons 2020-10-22 17:02:38 +01:00
Charles Zwicker
3785c623f8 Add GroupMe and Spotify removal option (#34)
Co-authored-by: undergroundwires <undergroundwires@users.noreply.github.com>
Co-authored-by: Charles Zwicker <czwicker54@gmail.com>
2020-10-20 16:51:30 +02:00
undergroundwires
14be3017c5 add support for different recommendation levels: strict and standard 2020-10-19 15:12:03 +01:00
undergroundwires-bot
978bab0b81 ⬆️ bump everywhere to 0.7.6 2020-10-17 22:58:06 +00:00
undergroundwires
d9d7f62d81 run tests on all operating systems: macos, ubuntu, windows 2020-10-18 02:10:20 +01:00
undergroundwires
11e0613165 update dependencies to latest 2020-10-17 23:17:44 +01:00
undergroundwires
77c3d2bbb8 simplify "why" section 2020-09-23 20:42:05 +01:00
undergroundwires
784a67afff refactor to read more from package.json 2020-09-22 20:41:12 +01:00
undergroundwires
19a092dd31 add more reversibility 2020-09-21 23:05:31 +01:00
undergroundwires
4c2f74949b add robots.txt to explicitly allow indexing 2020-09-19 01:23:33 +01:00
undergroundwires
a3fc3782ef add docs for default0 pointing to github discussion (#30) 2020-09-20 13:58:19 +01:00
undergroundwires-bot
cdc93f032a ⬆️ bumped to 0.7.5 2020-09-19 13:41:58 +00:00
undergroundwires
7dd15ed064 fix typo 2020-09-19 15:39:48 +01:00
undergroundwires
d169434157 fix pasting in search bar after page load showing no results 2020-09-18 20:07:03 +01:00
undergroundwires
6efed72bf2 fix rendering issue in older edge/IE 2020-09-17 15:41:46 +01:00
Clayton Errington
15db311801 fix the recycling bin option (#32)
* update the recycling bin option

Powershell has a module in PS 5.1+ called Clear-Recyclebin that works better than the CMD method - rd /s %systemdrive%\$Recycle.bin

* update recycling bin delete command

one liner of ComObject in powershell instead of cmd asking for confirmation. adds better backwards compatibility.
2020-09-16 20:22:33 +00:00
undergroundwires
82d509129b fix tests and checks are not running on PRs 2020-09-16 19:10:25 +01:00
undergroundwires
939d838e35 fix reverting (reinstalling) capabilities not working 2020-09-16 02:09:23 +01:00
undergroundwires-bot
6de4ce58c4 ⬆️ bumped to 0.7.4 2020-09-14 13:57:39 +00:00
undergroundwires
ee66196d9a fix wrong path in clear all firefox user profile settings 2020-09-14 16:04:38 +01:00
undergroundwires
3c13a9e837 fix missing reg value in denying app access to account 2020-09-14 16:03:03 +01:00
undergroundwires
22b23a9ece fix spectre protection getting single lined #31 2020-09-14 16:00:20 +01:00
undergroundwires
4ae385b7fc fix checked checkbox has blue border 2020-09-13 18:42:19 +01:00
undergroundwires-bot
d9abc7f0b2 ⬆️ bumped to 0.7.3 2020-09-12 13:14:13 +00:00
undergroundwires
1f19b2528a fix typo in a test 2020-09-12 14:42:27 +01:00
undergroundwires
1f11c39773 add more detailed error message 2020-09-12 00:14:27 +01:00
undergroundwires
b6ccb5927a fix comment lines are being detected as duplicate in validation 2020-09-12 00:13:58 +01:00
undergroundwires
1d465ee318 add reversibility and more scripts to denying app access with better structure 2020-09-12 00:11:10 +01:00
undergroundwires
3ab48b1cf5 fix naming of firefox cleanup to mention profiles 2020-09-11 14:26:32 +01:00
undergroundwires
de4ac978bd fix wrong path to the main telemetry file 2020-09-10 12:52:29 +01:00
undergroundwires
8df5faf4ef improve CPU specific tweaks by conditional platform checks and reversibility 2020-09-09 13:55:21 +01:00
undergroundwires
99a2035fdb fix nvidia tweak error message, categorize and add reversibility 2020-09-08 19:41:03 +01:00
undergroundwires
a0d61728ea fix vscode settings file override and add more configs 2020-09-07 13:42:59 +01:00
undergroundwires-bot
312bf6102c ⬆️ bumped to 0.7.2 2020-09-06 18:10:12 +00:00
undergroundwires
f4885b6f1c add best practice suggestion to come back 2020-09-06 02:41:11 +01:00
undergroundwires
ca63a0979e fix wording in default text in text area 2020-09-06 02:37:47 +01:00
undergroundwires
1f266c3353 fix indeterminate state being lost 2020-09-06 15:26:19 +01:00
undergroundwires
c7b2a70312 add reversibility to removing bloatware 2020-09-06 19:03:36 +01:00
undergroundwires
255133af4d fix bad highlighting of selected nodes when using keyboard navigation 2020-09-04 01:24:35 +01:00
undergroundwires
db74531cd4 add reversibility for biometric disabling and do not recommend it 2020-09-06 16:39:48 +01:00
undergroundwires
f36d8bfc78 update onesync documentation and do not recommend it as it breaks other apps 2020-09-05 23:31:31 +01:00
undergroundwires-bot
3b31ace726 ⬆️ bumped to 0.7.1 2020-09-04 11:42:03 +00:00
undergroundwires
6badfef9da refactor unused imports 2020-09-04 13:29:42 +01:00
undergroundwires
8c38dd73d8 fix new/changed script higlighting not working on production builds 2020-09-04 13:26:35 +01:00
undergroundwires
b8682a852a rename screenshot image file 2020-09-04 13:25:36 +01:00
undergroundwires
8c17929151 fix some browsers (including firefox) downloading the script as a text file 2020-09-04 12:20:41 +01:00
undergroundwires-bot
bb92c9ec28 ⬆️ bumped to 0.7.0 2020-09-02 21:26:40 +00:00
undergroundwires
b4aacea2a3 update the screenshot to show off highlighting 2020-09-02 19:12:24 +01:00
undergroundwires
8bbe6ebf75 fix search (got broken in b789250) with tests and refactorings 2020-09-02 22:44:20 +01:00
undergroundwires
a23d28f2cf refactor unused imports & variables 2020-09-01 21:32:31 +01:00
undergroundwires
f51e8859ee add reversibility on category level 2020-09-01 21:18:16 +01:00
undergroundwires
d235dee955 exclude paint, wordpad and notepad from bloatware removal 2020-09-01 21:03:14 +01:00
undergroundwires
2afef4ea3d do not hardcode capability versions and make them reversible 2020-08-26 00:58:52 +01:00
undergroundwires
f709d6a566 fix "Configure Defender" being in wrong category #28 2020-09-01 20:06:44 +01:00
undergroundwires
532915b95d categorize, fix, make scripts reversible in "UI for privacy", "security improvements" and "configure browsers" 2020-09-01 20:03:43 +01:00
Francesco Saltori
456e40bedf Add disabling of PowerShell 7+ telemetry (#29) 2020-09-01 19:57:43 +02:00
undergroundwires
018b7e270f add disabling ccleaner telemetry 2020-08-20 00:29:35 +01:00
undergroundwires
f8b8b4c97a add disabling firefox telemetry 2020-08-30 17:42:05 +01:00
undergroundwires
978d7d0863 add more OneDrive cleanup scripts and categorize them 2020-08-30 16:05:02 +01:00
undergroundwires
594a14d6ca categorize, fix and extend windows log files cleanup 2020-08-24 21:21:21 +01:00
undergroundwires
c628aa9aef updated dependencies to latest and audit fixes (#25) 2020-08-28 16:46:09 +01:00
undergroundwires
3060ebf79c fix NTP script documentation is on wrong place 2020-08-28 14:16:11 +01:00
undergroundwires
1a34c7374b add more windows defender tweaks, categorization and reversibility 2020-08-27 20:32:19 +01:00
undergroundwires
c262681011 add removal of ghost (default0) telemetry user 2020-08-26 21:31:12 +01:00
undergroundwires
f8ba5c46e4 prompt admin priviliges automatically 2020-08-26 01:58:29 +01:00
undergroundwires
b789250cb8 add auto-highlighting of selected/updated code 2020-08-25 16:52:38 +01:00
undergroundwires
5df458739d move script generation to /generation 2020-08-25 16:44:47 +01:00
undergroundwires
d6fa9a2a03 [search] added clear/close button 2020-08-24 02:50:47 +01:00
undergroundwires
ec15af01dd [search] better (multilined) message when there are no results 2020-08-24 02:50:47 +01:00
undergroundwires-bot
7073336f81 ⬆️ bumped to 0.6.2 2020-08-16 16:16:50 +00:00
undergroundwires
3d3380f27e 🔥 removed disabling ClickToRun as it breaks office 2020-08-16 16:58:05 +02:00
undergroundwires
c69998c7cb 🐛 fixed changing time server not working 2020-08-16 16:48:37 +01:00
undergroundwires
1663bfeac7 added script to clear dotnet telemery 2020-08-16 14:19:44 +02:00
undergroundwires
afc3bfb3b8 ⚙️ enhanced tweak to disable for office telemetry 2020-08-16 14:18:20 +02:00
undergroundwires
b6bfc25727 🐛 fixed removing onedrive does not delete scheduled tasks 2020-08-10 19:12:04 +02:00
undergrounwdires
7fac0fe79f 🐛 fixed blank screen and icons on mac 2020-08-10 18:40:16 +01:00
undergrounwdires
5967347b80 🐛 fixed disabling error reporting for november 2019 update 2020-08-09 16:23:28 +02:00
undergroundwires-bot
855a445c1a ⬆️ bumped to 0.6.1 2020-08-09 01:03:43 +00:00
undergroundwires
1cc12195a3 fixed removing onedrive does not clean start menu / quick access 2020-08-09 03:00:19 +01:00
undergroundwires
66d4d39d5b refactorings 2020-08-09 03:00:18 +01:00
undergroundwires
a5dbe66fc1 tweaks to disable webcam, speech and compatibility telemetry 2020-08-09 03:00:18 +01:00
undergroundwires
4c8be45e28 fixed mac / linux download links 2020-08-09 03:00:18 +01:00
undergroundwires
6049a2b834 moved windows connect now to security & recommended 2020-08-09 03:00:18 +01:00
undergroundwires
831c014f97 more scripts can be reverted 2020-08-09 03:00:18 +01:00
undergroundwires
5c15a7a64a fixed typo in footer 2020-08-09 03:00:18 +01:00
dependabot[bot]
e43992b278 Bump elliptic from 6.5.2 to 6.5.3 (#23)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-08-09 03:00:18 +01:00
undergroundwires
5963d2bac5 updated documentation 2020-08-09 03:00:18 +01:00
undergroundwires
45816a2bcc updated dependencies to latest 2020-08-09 03:00:17 +01:00
undergroundwires
60a5a2aa40 reworked on footer & removed github icon 2020-08-09 03:00:17 +01:00
undergroundwires
04b9b59e14 support for desktop versions #20 2020-08-09 03:00:13 +01:00
undergroundwires
4ff4b52202 code area now shows "how" before "why" 2020-07-24 15:43:19 +01:00
undergroundwires
73c426844a runs tests on each push on the repository 2020-07-19 15:14:23 +01:00
undergroundwires
25ce236a77 fixed dead links in documentation 2020-07-19 02:37:54 +01:00
undergroundwires-bot
9b20175545 ⬆️ bumped to 0.5.0 2020-07-19 00:32:13 +00:00
163 changed files with 13748 additions and 3989 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
node_modules
dist
dist_electron
.vs
.vscode
.github
.git
docs
docker

View File

@@ -10,9 +10,8 @@ jobs:
if: github.event.base_ref == 'refs/heads/master' if: github.event.base_ref == 'refs/heads/master'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - uses: undergroundwires/bump-everywhere@master
uses: undergroundwires/bump-everywhere@master
with: with:
user: undergroundwires-bot user: undergroundwires-bot
release-token: ${{secrets.BUMP_GITHUB_PAT}} # Does not trigger release pipeline if we use default token: https://github.community/t5/GitHub-Actions/Github-Action-trigger-on-release-not-working-if-releases-was/td-p/34559 release-token: ${{ secrets.BUMP_GITHUB_PAT }} # Does not trigger release pipeline if we use default token: https://github.community/t5/GitHub-Actions/Github-Action-trigger-on-release-not-working-if-releases-was/td-p/34559
# GitHub does not inject secrets if pipeline runs from fork or a fork is merged to main repo. # GitHub does not inject secrets if pipeline runs from fork or a fork is merged to main repo.

32
.github/workflows/deploy-desktop.yaml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Deploy desktop
on:
release:
types: [created] # will be triggered when a NON-draft release is created and published.
jobs:
publish-desktop-app:
name: ${{ matrix.os }}
strategy:
matrix:
os: [macos, ubuntu, windows]
runs-on: ${{ matrix.os }}-latest
steps:
- uses: actions/checkout@v2
with:
ref: master # otherwise it defaults to the version tag missing bump commit
fetch-depth: 0 # fetch all history
- name: Checkout to bump commit
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
- name: Setup node
uses: actions/setup-node@v1
with:
node-version: '14.x'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:unit
- name: Publish desktop app
run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,11 +1,11 @@
name: Build & deploy name: Deploy site
on: on:
release: release:
types: [created] # will be triggered when a NON-draft release is created and published. types: [created] # will be triggered when a NON-draft release is created and published.
jobs: jobs:
build-and-deploy: aws-deploy: # see: https://github.com/undergroundwires/aws-static-site-with-cd
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- -
@@ -86,7 +86,7 @@ jobs:
node-version: '14.x' node-version: '14.x'
- -
name: "App: Install dependencies" name: "App: Install dependencies"
run: npm install run: npm ci
working-directory: site working-directory: site
- -
name: "App: Run tests" name: "App: Run tests"
@@ -114,4 +114,4 @@ jobs:
--web-stack-name privacysexy-web-stack --web-stack-cloudfront-arn-output-name CloudFrontDistributionArn \ --web-stack-name privacysexy-web-stack --web-stack-cloudfront-arn-output-name CloudFrontDistributionArn \
--role-arn ${{secrets.AWS_CLOUDFRONT_SITE_DEPLOYMENT_ROLE_ARN}} \ --role-arn ${{secrets.AWS_CLOUDFRONT_SITE_DEPLOYMENT_ROLE_ARN}} \
--region us-east-1 \ --region us-east-1 \
--profile user --session ${{ env.SESSION_NAME }} --profile user --session ${{ env.SESSION_NAME }}

View File

@@ -1,37 +1,26 @@
name: Quality checks name: Quality checks
on: on: [ push, pull_request ]
pull_request:
branches:
- master
jobs: jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
lint-command:
- npm run lint:vue
- npm run lint:yaml
- npm run lint:md
- npm run lint:md:relative-urls
- npm run lint:md:consistency
steps: steps:
- - name: Checkout
name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- - name: Setup node
name: Setup node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 14.x node-version: 14.x
- - name: Install dependencies
name: Install dependencies
run: npm ci run: npm ci
- - name: Lint
name: Lint vue run: ${{ matrix.lint-command }}
run: npm run lint:vue
-
name: Lint yaml
run: npm run lint:yaml
-
name: 'Validate md: Relative URLs'
run: npm run lint:md:relative-urls
-
name: 'Validate md: Enforce standards'
run: npm run lint:md
-
name: 'Validate md: Ensure consistency'
run: npm run lint:md:consistency

View File

@@ -1,9 +1,8 @@
name: Security checks name: Security checks
on: on:
push:
pull_request: pull_request:
branches:
- master
schedule: schedule:
- cron: '0 0 * * 0' - cron: '0 0 * * 0'

View File

@@ -1,13 +1,13 @@
name: Test name: Test
on: on: [ push, pull_request ]
pull_request:
branches:
- master
jobs: jobs:
run-tests: run-tests:
runs-on: ubuntu-latest strategy:
matrix:
os: [macos, ubuntu, windows]
runs-on: ${{ matrix.os }}-latest
steps: steps:
- -
name: Checkout name: Checkout

6
.gitignore vendored
View File

@@ -1,4 +1,6 @@
node_modules node_modules
/dist dist/
.vs .vs
.vscode .vscode
#Electron-builder output
/dist_electron

View File

@@ -1,8 +1,149 @@
# Changelog # Changelog
## 0.7.6 (2020-10-18)
* add docs for default0 pointing to github discussion (#30) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a3fc3782efd346b4c99d2a0b40df2eb0229f5b36)
* add robots.txt to explicitly allow indexing | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4c2f74949b0758d33049bdfa4f0124a28958f8ea)
* add more reversibility | [commit](https://github.com/undergroundwires/privacy.sexy/commit/19a092dd31fb3588277f1ab3120b409d98506752)
* refactor to read more from package.json | [commit](https://github.com/undergroundwires/privacy.sexy/commit/784a67afff681bc19147d03c947de0e165d97e87)
* simplify "why" section | [commit](https://github.com/undergroundwires/privacy.sexy/commit/77c3d2bbb8d13db86bb82ed0b5cbeaacfdea3db9)
* update dependencies to latest | [commit](https://github.com/undergroundwires/privacy.sexy/commit/11e06131655398db08faeeacff62062e46e0dddd)
* run tests on all operating systems: macos, ubuntu, windows | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d9d7f62d81d4d8f95104d33211e82641884d711f)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.5...0.7.6)
## 0.7.5 (2020-09-14)
* fix reverting (reinstalling) capabilities not working | [commit](https://github.com/undergroundwires/privacy.sexy/commit/939d838e3535bb1c9b00c8ea9dacb735ae41d700)
* fix tests and checks are not running on PRs | [commit](https://github.com/undergroundwires/privacy.sexy/commit/82d509129b4e4a5df4b84786a0d6842a7d26e888)
* fix the recycling bin option (#32) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/15db3118012a172a2191a2afad57084a65b34642)
* fix rendering issue in older edge/IE | [commit](https://github.com/undergroundwires/privacy.sexy/commit/6efed72bf25c2ddf0901caab7f22966ca13cd47a)
* fix pasting in search bar after page load showing no results | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d1694341578288eeaf8b80caf9296a38d76789f0)
* fix typo | [commit](https://github.com/undergroundwires/privacy.sexy/commit/7dd15ed06433e0e6583ab0fa46a683ce6554bbea)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.4...0.7.5)
## 0.7.4 (2020-09-12)
* fix checked checkbox has blue border | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4ae385b7fcea9014a68442714b7d99e2ee7df7d0)
* fix spectre protection getting single lined #31 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/22b23a9ece446c7f9abd4ede293051eb616ad50a)
* fix missing reg value in denying app access to account | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3c13a9e837e06e097450b31d7eb0c0e6bf20cefb)
* fix wrong path in clear all firefox user profile settings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ee66196d9a60f27d17ae7f62d02b4f119a47e6e0)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.3...0.7.4)
## 0.7.3 (2020-09-12)
* fix vscode settings file override and add more configs | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a0d61728ead04b4455437f85820121a848db9e00)
* fix nvidia tweak error message, categorize and add reversibility | [commit](https://github.com/undergroundwires/privacy.sexy/commit/99a2035fdb0766a4dfc2753133eab0d7666516cd)
* improve CPU specific tweaks by conditional platform checks and reversibility | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8df5faf4ef05a49da63973bd0fbb5c5d07d5bd93)
* fix wrong path to the main telemetry file | [commit](https://github.com/undergroundwires/privacy.sexy/commit/de4ac978bdda79573b36d355697b8a028d2c0beb)
* fix naming of firefox cleanup to mention profiles | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3ab48b1cf5f7f934f07e468ef2318ccee07f530c)
* add reversibility and more scripts to denying app access with better structure | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1d465ee3189d0e5a827453b3f0eb4361efe23770)
* fix comment lines are being detected as duplicate in validation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b6ccb5927a20412976a54fd2215eb645092f98a8)
* add more detailed error message | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1f11c39773c12eccfb3efb898b58c2f6f37ab9ca)
* fix typo in a test | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1f19b2528a69383e63e579d2885f01cd804abf6c)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.2...0.7.3)
## 0.7.2 (2020-09-06)
* update onesync documentation and do not recommend it as it breaks other apps | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f36d8bfc7848bb65ac0c641e318a689bf3816ccf)
* add reversibility for biometric disabling and do not recommend it | [commit](https://github.com/undergroundwires/privacy.sexy/commit/db74531cd4139615c6d595959217d3651f099019)
* fix bad highlighting of selected nodes when using keyboard navigation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/255133af4dfae40171406648a3e2920f16d71cb3)
* add reversibility to removing bloatware | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c7b2a703128470a05f12c9c6e8002444def37ef8)
* fix indeterminate state being lost | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1f266c33535f72b69c65985bf2eff27cd2c5a104)
* fix wording in default text in text area | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ca63a0979ef55d07d09d9443e5cea9aa888870a5)
* add best practice suggestion to come back | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f4885b6f1c82752f2143934e336d6d1b1af03015)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.1...0.7.2)
## 0.7.1 (2020-09-04)
* fix some browsers (including firefox) downloading the script as a text file | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8c17929151f9c4fa5f48564492bbf400ced95eea)
* rename screenshot image file | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b8682a852a14ed6cf49986695d9510b840ac9d3d)
* fix new/changed script higlighting not working on production builds | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8c38dd73d8c7b77d8d341c0389f4d7229f9b97fd)
* refactor unused imports | [commit](https://github.com/undergroundwires/privacy.sexy/commit/6badfef9daace0c5de3fd33652a82bfe22261b11)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.0...0.7.1)
## 0.7.0 (2020-09-02)
* [search] better (multilined) message when there are no results | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ec15af01dd020b364c2174fe562fd66227c2320c)
* [search] added clear/close button | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d6fa9a2a03c0ebe68b94f0b80cc52b4e200c9213)
* move script generation to /generation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5df458739d076719e350ba194c4f3f772884fcdb)
* add auto-highlighting of selected/updated code | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b789250cb89e2130b08e1a927df8181cf945dfeb)
* prompt admin priviliges automatically | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f8ba5c46e4923d9c35f200f8a08aa6437f7c0ecc)
* add removal of ghost (default0) telemetry user | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c262681011f39b4412669b6cf233476f676ca550)
* add more windows defender tweaks, categorization and reversibility | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1a34c7374ba56bafa0209bbb55c81b233bb419ed)
* fix NTP script documentation is on wrong place | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3060ebf79cf242370433495cc3e1878b7581b202)
* updated dependencies to latest and audit fixes (#25) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c628aa9aef8ab7c815661d3c1711e7fbc65c69a2)
* categorize, fix and extend windows log files cleanup | [commit](https://github.com/undergroundwires/privacy.sexy/commit/594a14d6ca76cbd27a21877b8c373c1930589ca6)
* add more OneDrive cleanup scripts and categorize them | [commit](https://github.com/undergroundwires/privacy.sexy/commit/978d7d08638dd161082f239ed088b12302f29458)
* add disabling firefox telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f8b8b4c97ab734d5ba7370894b694993924388da)
* add disabling ccleaner telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/018b7e270f207aac926cb12f8069ebfcdce193ce)
* Add disabling of PowerShell 7+ telemetry (#29) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/456e40bedf9afcc846f9b13f1ea144cef6115cf6)
* categorize, fix, make scripts reversible in "UI for privacy", "security improvements" and "configure browsers" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/532915b95da9fecd6b981d91bf489359e4e53caa)
* fix "Configure Defender" being in wrong category #28 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f709d6a566ed7846b677b383863deda9680a2a9c)
* do not hardcode capability versions and make them reversible | [commit](https://github.com/undergroundwires/privacy.sexy/commit/2afef4ea3d0d3d09aa1fa1eedba8493680bd8f10)
* exclude paint, wordpad and notepad from bloatware removal | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d235dee95514a01745aef9479d07f88ffb4b40b8)
* add reversibility on category level | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f51e8859eeb32c944126d692cfe03a0320c8b568)
* refactor unused imports & variables | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a23d28f2cfa2d64d45460697cf5ee9d6b5920752)
* fix search (got broken in b789250) with tests and refactorings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8bbe6ebf750f1a1cbab493fb99b5ea91f4e21609)
* update the screenshot to show off highlighting | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b4aacea2a3e0bbcf2d8a79ff67f51c0f19e888a6)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.6.2...0.7.0)
## 0.6.2 (2020-08-16)
* 🐛 fixed disabling error reporting for november 2019 update | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5967347b80976a519f6f4eb1972a62f3e600df2b)
* 🐛 fixed blank screen and icons on mac | [commit](https://github.com/undergroundwires/privacy.sexy/commit/7fac0fe79f252e8f9dda4f6f83cd6fa4ba2b539f)
* 🐛 fixed removing onedrive does not delete scheduled tasks | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b6bfc2572740c0cd46d3bc0058fa767dd5fa862e)
* ⚙️ enhanced tweak to disable for office telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/afc3bfb3b8896f332c9a196973ded3dce8fd21e4)
* ✨ added script to clear dotnet telemery | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1663bfeac7b6580b1335ca5fcf3587b69c080c72)
* 🐛 fixed changing time server not working | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c69998c7cb29ffcf40f0af03b73150736581da69)
* 🔥 removed disabling ClickToRun as it breaks office | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3d3380f27ebeea53f17f49974aaa89300ffaf2dd)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.6.1...0.6.2)
## 0.6.1 (2020-08-09)
* updated documentation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5963d2bac551083f9d16cce6b851abf0e8b88ce7)
* fixed typo in footer | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5c15a7a64aaf24578a32713dec491bf494216303)
* more scripts can be reverted | [commit](https://github.com/undergroundwires/privacy.sexy/commit/831c014f977515454ee6dc664d77a8c434495501)
* moved windows connect now to security & recommended | [commit](https://github.com/undergroundwires/privacy.sexy/commit/6049a2b834d8d17af741f8d8f8b07cd15153b001)
* fixed mac / linux download links | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4c8be45e287b5ea009d6f828f7f327f37850569e)
* tweaks to disable webcam, speech and compatibility telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a5dbe66fc175e39397f296ab2ff703e9b0ab4d7c)
* refactorings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/66d4d39d5bf3db305450514c6b6224654dafbfb2)
* fixed removing onedrive does not clean start menu / quick access | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1cc12195a3e9a11c590d3ed64d80299b50f74838)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.6.0...0.6.1)
## 0.6.0 (2020-07-26)
* fixed dead links in documentation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/25ce236a7737decaf2eb9b8c29a4c4f34d43f770)
* runs tests on each push on the repository | [commit](https://github.com/undergroundwires/privacy.sexy/commit/73c426844a0330718a9ab7de12b61ca05e853323)
* code area now shows "how" before "why" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4ff4b52202b1c5dbfe2b80580bbe7d93132ab05c)
* support for desktop versions #20 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/04b9b59e14766ccd251474ad3710baf1f682fd49)
* reworked on footer & removed github icon | [commit](https://github.com/undergroundwires/privacy.sexy/commit/60a5a2aa4026d384bef9e6a203f1b7514a269c33)
* updated dependencies to latest | [commit](https://github.com/undergroundwires/privacy.sexy/commit/45816a2bccb3d11a50e3f2bc19c0a6cc2587deaa)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.5.0...0.6.0)
## 0.5.0 (2020-07-19)
* added ability to revert (#21) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/9c063d59defa6297c64f50b49403e8bd10620de9)
* search placeholder shows total scripts | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1d5225de07186f853f4cf7aedd4998f5d00c107a)
* do not collapse card when on "Search" and "Select" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/dd7e1416b4df54bf71b719d4654db88769dc0994)
* opening a card scrolls to its content div | [commit](https://github.com/undergroundwires/privacy.sexy/commit/31d2067f076c3159483baec49975617dddbd158d)
* all cards in same line now have same height | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a9f9e9044385d9aed3b5551fc6c6823e813fd1e5)
* patched loadash vulnerability (#18) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/92a7118d1c5013312772e075b9ee5a79c93710b8)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.10...0.5.0)
## 0.4.10 (2020-07-15) ## 0.4.10 (2020-07-15)
* fixed script validation errors | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a34ae139d7fc1ba05b8ab9eb962da4ca0231ed5c) * fixed script errors & added tests | [commit](https://github.com/undergroundwires/privacy.sexy/commit/9e722ddfb3825fb29d6298025baaaa033120d017)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.9...0.4.10) [compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.9...0.4.10)

View File

@@ -20,25 +20,18 @@
6. Issue that pull request! 6. Issue that pull request!
- 🙏 DO - 🙏 DO
- Document your changes in the pull request - Document your changes in the pull request
- 💡 Check [developer notes](./docs/developer-notes.md) if you need help
- ❗ DON'T - ❗ DON'T
- Do not update the versions, current version is only [set by the maintainer](./docs/gitops.png) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) - Do not update the versions, current version is only [set by the maintainer](./img/architecture/gitops.png) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere)
## Guidelines ## Guidelines
### Extend scripts
- Create a [pull request](./../CONTRIBUTING.md#Pull+Request+Process) for [application.yaml](./../src/application/application.yaml)
- 🙏 For any new script, try to add `revertCode` that'll revert the changes caused by the script.
- See [typings](./../src/application/application.yaml.d.ts) for documentation as code.
### Handle the state in presentation layer ### Handle the state in presentation layer
- There are two types of components: - There are two types of components:
- **Stateless**, extends `Vue` - **Stateless**, extends `Vue`
- **Stateful**, extends [`StatefulVue`](./src/presentation/StatefulVue.ts) - **Stateful**, extends [`StatefulVue`](./src/presentation/StatefulVue.ts)
- The source of truth for the state lies in [application layer](./src/application/state) and must be updated from the views if they're mutating the state - The source of truth for the state lies in application layer (`./src/application/`) and must be updated from the views if they're mutating the state
- They mutate or/and reacts to changes in [application state](./src/application/state). - They mutate or/and reacts to changes in [application state](src/application/State/ApplicationState.ts).
- You can react by getting the state and listening to it and update the view accordingly in [`mounted()`](https://vuejs.org/v2/api/#mounted) method. - You can react by getting the state and listening to it and update the view accordingly in [`mounted()`](https://vuejs.org/v2/api/#mounted) method.
## License ## License

View File

@@ -1,6 +1,6 @@
# privacy.sexy # privacy.sexy
> Web tool to enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆 > Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](./CONTRIBUTING.md) [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](./CONTRIBUTING.md)
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/undergroundwires/privacy.sexy.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/undergroundwires/privacy.sexy.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
@@ -12,21 +12,29 @@
[![Deploy status](https://github.com/undergroundwires/privacy.sexy/workflows/Build%20&%20deploy/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions) [![Deploy status](https://github.com/undergroundwires/privacy.sexy/workflows/Build%20&%20deploy/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
[![Auto-versioned by bump-everywhere](https://github.com/undergroundwires/bump-everywhere/blob/master/badge.svg?raw=true)](https://github.com/undergroundwires/bump-everywhere) [![Auto-versioned by bump-everywhere](https://github.com/undergroundwires/bump-everywhere/blob/master/badge.svg?raw=true)](https://github.com/undergroundwires/bump-everywhere)
[https://privacy.sexy](https://privacy.sexy) ## Get started
- Online version: [https://privacy.sexy](https://privacy.sexy)
- or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.6/privacy.sexy-Setup-0.7.6.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.6/privacy.sexy-0.7.6.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.6/privacy.sexy-0.7.6.dmg)
- 💡 Come back regularly to apply latest version for stronger privacy and security.
[![privacy.sexy application](img/screenshot.png)](https://privacy.sexy)
## Why ## Why
- You don't need to run any compiled software that has access to your system, just run the generated scripts. - Rich tweak pool to harden security & privacy of the OS and other software on it
- It's open source, both application & infrastructure is 100% transparent - Free (both free as in beer and free as in speech)
- Fully automated C/CD pipeline to AWS for provisioning serverless infrastructure using GitHub actions. - No need to run any compiled software that has access to your system, just run the generated scripts
- Have full visibility into what the tweaks do as you enable them. - Have full visibility into what the tweaks do as you enable them
- Ability to revert applied scripts - Ability to revert (undo) applied scripts
- Everything is transparent: both application and its infrastructure are open-source and automated
- Easily extendable - Easily extendable
## Extend scripts ## Extend scripts
- Fork it & add more scripts in [application.yaml](src/application/application.yaml) and send a pull request 👌 - Fork it & add more scripts in [application.yaml](src/application/application.yaml) and send a pull request 👌
- 📖 More: [extend scripts | CONTRIBUTING.md](./CONTRIBUTING.md#extend-scripts) - 📖 If you're unsure about the syntax you can refer to the [application file | documentation](docs/application-file.md).
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
## Commands ## Commands
@@ -34,34 +42,38 @@
- Testing - Testing
- Run unit tests: `npm run test:unit` - Run unit tests: `npm run test:unit`
- Lint: `npm run lint` - Lint: `npm run lint`
- **Desktop app**
- Development: `npm run electron:serve`
- Production: `npm run electron:build` to build an executable
- **Webpage** - **Webpage**
- Development: `npm run serve` to compile & hot-reload for development. - Development: `npm run serve` to compile & hot-reload for development.
- Production: `npm run build` to prepare files for distribution. - Production: `npm run build` to prepare files for distribution.
- Or run using Docker: - Or run using Docker:
1. Build: `docker build -t undergroundwires/privacy.sexy:0.4.10 .` 1. Build: `docker build -t undergroundwires/privacy.sexy:0.7.6 .`
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.4.10 undergroundwires/privacy.sexy:0.4.10` 2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.7.6 undergroundwires/privacy.sexy:0.7.6`
## Architecture ## Architecture
### Application ### Application
- Powered by **TypeScript** + **Vue.js** 💪 - Powered by **TypeScript**, **Vue.js** and **Electron** 💪
- and driven by **Domain-driven design**, **Event-driven architecture**, **Data-driven programming** concepts. - and driven by **Domain-driven design**, **Event-driven architecture**, **Data-driven programming** concepts.
- Application uses highly decoupled models & services in different DDD layers. - Application uses highly decoupled models & services in different DDD layers.
- **Domain layer** is where the application is modelled with validation logic. - **Domain layer** is where the application is modelled with validation logic.
- **Presentation Layer** - **Presentation Layer**
- Consists of Vue.js components & UI stuff. - Consists of Vue.js components and other UI-related code.
- Desktop application is created using [Electron](https://www.electronjs.org/).
- Event driven as in components simply listens to events from the state and act accordingly. - Event driven as in components simply listens to events from the state and act accordingly.
- **Application Layer** - **Application Layer**
- Keeps the application state - Keeps the application state
- The [state](src/application/State/ApplicationState.ts) is a mutable singleton & event producer. - The [state](src/application/State/ApplicationState.ts) is a mutable singleton & event producer.
- The application is defined & controlled in a [single YAML file](src/application/application.yaml) (see [Data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming)) - The application is defined & controlled in a [single YAML file](src/application/application.yaml) (see [Data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming))
![DDD + vue.js](docs/app-ddd.png) ![DDD + vue.js](img/architecture/app-ddd.png)
### AWS Infrastructure ### AWS Infrastructure
[![AWS solution](docs/aws-solution.png)](https://github.com/undergroundwires/aws-static-site-with-cd) [![AWS solution](img/architecture/aws-solution.png)](https://github.com/undergroundwires/aws-static-site-with-cd)
- It uses infrastructure from the following repository: [aws-static-site-with-cd](https://github.com/undergroundwires/aws-static-site-with-cd) - It uses infrastructure from the following repository: [aws-static-site-with-cd](https://github.com/undergroundwires/aws-static-site-with-cd)
- Runs on AWS 100% serverless and automatically provisioned using [GitHub Actions](.github/workflows/). - Runs on AWS 100% serverless and automatically provisioned using [GitHub Actions](.github/workflows/).
@@ -73,4 +85,4 @@
- Versioning, tagging, creation of `CHANGELOG.md` and releasing is automated using [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) action - Versioning, tagging, creation of `CHANGELOG.md` and releasing is automated using [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) action
- Everything that's merged in the master goes directly to production. - Everything that's merged in the master goes directly to production.
[![CI/CD to AWS with GitHub Actions](docs/gitops.png)](.github/workflows/) [![CI/CD to AWS with GitHub Actions](img/architecture/gitops.png)](.github/workflows/)

5
build/README.md Normal file
View File

@@ -0,0 +1,5 @@
# build
- These are the file that are used by electron.
- Logos are created by from the [PNG icon](./../public/icon.png)
- by running `npx electron-icon-builder --input=./public/icon.png --output=build --flatten`

BIN
build/icons/1024x1024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

BIN
build/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
build/icons/16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B

BIN
build/icons/24x24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
build/icons/256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
build/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
build/icons/48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
build/icons/512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
build/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
build/icons/icon.icns Normal file

Binary file not shown.

BIN
build/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

139
docs/application-file.md Normal file
View File

@@ -0,0 +1,139 @@
# Application file
- privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from [`application.yaml`](./../src/application/application.yaml)
- 💡 Best practices
- If you repeat yourself, try to utilize [YAML-defined functions](#function)
- Always try to add documentation and a way to revert a tweak in [scripts](#script)
- 📖 Types in code: [`application.d.ts`](./../src/application/application.yaml.d.ts)
## Objects
### `Application`
- Application file simply defines different categories and their scripts in a tree structure.
- Application file also allows defining common [function](#function)s to be used throughout the application if you'd like different scripts to share same code.
#### `Application` syntax
- `actions: [` ***[`Category`](#Category)*** `, ... ]` **(required)**
- Each [category](#category) is rendered as different cards in card presentation.
- ❗ Application must consist of at least one category.
- `functions: [` ***[`Function`](#Function)*** `, ... ]`
- Functions are optionally defined to re-use the same code throughout different scripts.
### `Category`
- Category has a parent that has tree-like structure where it can have subcategories or subscripts.
- It's a logical grouping of different scripts and other categories.
#### `Category` syntax
- `category:` *`string`* (**required**)
- Name of the category
- ❗ Must be unique throughout the application
- `children: [` ***[`Category`](#category)*** `|` [***`Script`***](#Script) `, ... ]` (**required**)
- ❗ Category must consist of at least one subcategory or script.
- Children can be combination of scripts and subcategories.
### `Script`
- Script represents a single tweak.
- A script must include either:
- A `code` and `revertCode`
- Or `call` to call YAML-defined functions
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
#### `Script` syntax
- `name`: *`string`* (**required**)
- Name of the script
- ❗ Must be unique throughout the application
- E.g. `Disable targeted ads`
- `code`: *`string`* (may be **required**)
- Batch file commands that will be executed
- 💡 If defined, best practice to also define `revertCode`
- ❗ If not defined `call` must be defined, do not define if `call` is defined.
- `revertCode`: `string`
- Code that'll undo the change done by `code` property.
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
- ❗ Do not define if `call` is defined.
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
- A shared function or sequence of functions to call (called in order)
- ❗ If not defined `code` must be defined
- `docs`: *`string`* | `[`*`string`*`, ... ]`
- Single documentation URL or list of URLs for those who wants to learn more about the script
- E.g. `https://docs.microsoft.com/en-us/windows-server/`
- `recommend`: `"standard"` | `"strict"` | `undefined` (default)
- If not defined then the script will not be recommended
- If defined it can be either
- `standard`: Only non-breaking scripts without limiting OS functionality
- `strict`: Scripts that can break certain functionality in favor of privacy and security
### `FunctionCall`
- Describes a single call to a function by optionally providing values to its parameters.
- 👀 See [parameter substitution](#parameter-substitution) for an example usage
#### `FunctionCall` syntax
- `function`: *`string`* (**required**)
- Name of the function to call.
- ❗ Function with same name must defined in `functions` property of [Application](#application)
- `parameters`: `[ parameterName:` *`parameterValue`*`, ... ]`
- Defines key value dictionary for each parameter and its value
- E.g.
```yaml
parameters:
userDefinedParameterName: parameterValue
# ...
appName: Microsoft.WindowsFeedbackHub
```
### `Function`
- Functions allow re-usable code throughout the defined scripts.
- Functions are templates compiled by privacy.sexy and uses special expressions.
- Expressions are defined inside mustaches (double brackets, `{{` and `}}`)
- 👀 See [parameter substitution](#parameter-substitution) for an example usage
#### Parameter substitution
A simple function example
```yaml
function: EchoArgument
parameters: [ 'argument' ]
code: Hello {{ $argument }} !
```
It would print "Hello world" if it's called in a [script](#script) as following:
```yaml
script: Echo script
call:
function: EchoArgument
parameters:
argument: World
```
#### `Function` syntax
- `name`: *`string`* (**required**)
- Name of the function that scripts will use.
- Convention is to use camelCase, and be verbs.
- E.g. `uninstallStoreApp`
- ❗ Function names must be unique
- `parameters`: `[` *`string`* `, ... ]`
- Name of the parameters that the function has.
- Parameter values are provided by a [Script](#script) through a [FunctionCall](#functioncall)
- Parameter names must be defined to be used in expressions such as [parameter substitution](#parameter-substitution)
- ❗ Parameter names must be unique
`code`: *`string`* (**required**)
- Batch file commands that will be executed
- 💡 If defined, best practice to also define `revertCode`
- `revertCode`: *`string`*
- Code that'll undo the change done by `code` property.
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

File diff suppressed because one or more lines are too long

BIN
img/architecture/gitops.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

BIN
img/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

6612
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,54 +1,71 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.4.10", "version": "0.7.6",
"author": "undergroundwires",
"description": "Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆",
"homepage": "https://privacy.sexy",
"private": true, "private": true,
"repository": {
"type": "git",
"url": "https://github.com/undergroundwires/privacy.sexy.git"
},
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit", "test:unit": "vue-cli-service test:unit",
"lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency", "lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency",
"electron:build": "vue-cli-service electron:build",
"electron:serve": "vue-cli-service electron:serve",
"lint:md": "markdownlint **/*.md --ignore node_modules",
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
"lint:vue": "vue-cli-service lint --no-fix", "lint:vue": "vue-cli-service lint --no-fix",
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml", "lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
"lint:md": "markdownlint **/*.md --ignore node_modules", "postinstall": "electron-builder install-app-deps",
"lint:md:relative-urls": "remark . --frail --use remark-validate-links", "postuninstall": "electron-builder install-app-deps"
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent"
}, },
"main": "background.js",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.26", "@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-brands-svg-icons": "^5.12.0", "@fortawesome/free-brands-svg-icons": "^5.15.1",
"@fortawesome/free-regular-svg-icons": "^5.12.0", "@fortawesome/free-regular-svg-icons": "^5.15.1",
"@fortawesome/free-solid-svg-icons": "^5.12.0", "@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/vue-fontawesome": "^0.1.9", "@fortawesome/vue-fontawesome": "^2.0.0",
"ace-builds": "^1.4.7", "ace-builds": "^1.4.12",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"inversify": "^5.0.1", "inversify": "^5.0.1",
"liquor-tree": "^0.2.70", "liquor-tree": "^0.2.70",
"v-tooltip": "^2.0.2", "v-tooltip": "2.0.2",
"vue": "^2.6.11", "vue": "^2.6.12",
"vue-class-component": "^7.1.0", "vue-class-component": "^7.2.6",
"vue-js-modal": "^2.0.0-rc.3", "vue-js-modal": "^2.0.0-rc.6",
"vue-property-decorator": "^8.3.0" "vue-property-decorator": "^9.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/ace": "0.0.42", "@types/ace": "0.0.44",
"@types/chai": "^4.2.7", "@types/chai": "^4.2.14",
"@types/file-saver": "^2.0.1", "@types/file-saver": "^2.0.1",
"@types/mocha": "^5.2.7", "@types/mocha": "^8.0.3",
"@vue/cli-plugin-typescript": "^4.4.0", "@vue/cli-plugin-typescript": "^4.5.7",
"@vue/cli-plugin-unit-mocha": "^4.1.1", "@vue/cli-plugin-unit-mocha": "^4.5.7",
"@vue/cli-service": "^4.1.1", "@vue/cli-service": "^4.5.7",
"@vue/test-utils": "1.0.0-beta.30", "@vue/test-utils": "1.1.0",
"chai": "^4.2.0", "chai": "^4.2.0",
"electron": "^10.1.3",
"electron-devtools-installer": "^3.1.1",
"electron-log": "^4.2.4",
"electron-updater": "^4.3.5",
"js-yaml-loader": "^1.2.2", "js-yaml-loader": "^1.2.2",
"markdownlint-cli": "^0.23.1", "markdownlint-cli": "^0.24.0",
"remark-cli": "^8.0.0", "remark-cli": "^9.0.0",
"remark-lint-no-dead-urls": "^1.0.2", "remark-lint-no-dead-urls": "^1.1.0",
"remark-preset-lint-consistent": "^3.0.0", "remark-preset-lint-consistent": "^4.0.0",
"remark-validate-links": "^10.0.0", "remark-validate-links": "^10.0.2",
"sass": "^1.24.0", "sass": "^1.27.0",
"sass-loader": "^8.0.0", "sass-loader": "^10.0.3",
"typescript": "^3.7.4", "typescript": "^4.0.3",
"vue-template-compiler": "^2.6.11", "vue-cli-plugin-electron-builder": "^2.0.0-rc.4",
"vue-template-compiler": "^2.6.12",
"yaml-lint": "^1.2.4" "yaml-lint": "^1.2.4"
} }
} }

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -32,4 +32,4 @@
<div id="app"></div> <div id="app"></div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
</body> </body>
</html> </html>

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow:

View File

@@ -12,10 +12,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator'; import { Component, Vue } from 'vue-property-decorator';
import { ApplicationState } from '@/application/State/ApplicationState';
import TheHeader from '@/presentation/TheHeader.vue'; import TheHeader from '@/presentation/TheHeader.vue';
import TheFooter from '@/presentation/TheFooter.vue'; import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
import TheCodeArea from '@/presentation/TheCodeArea.vue'; import TheCodeArea from '@/presentation/TheCodeArea.vue';
import TheCodeButtons from '@/presentation/TheCodeButtons.vue'; import TheCodeButtons from '@/presentation/TheCodeButtons.vue';
import TheSearchBar from '@/presentation/TheSearchBar.vue'; import TheSearchBar from '@/presentation/TheSearchBar.vue';

View File

@@ -0,0 +1,54 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
import { DetectorBuilder } from './DetectorBuilder';
import { IBrowserOsDetector } from './IBrowserOsDetector';
export class BrowserOsDetector implements IBrowserOsDetector {
private readonly detectors = BrowserDetectors;
public detect(userAgent: string): OperatingSystem {
if (!userAgent) {
return OperatingSystem.Unknown;
}
for (const detector of this.detectors) {
const os = detector.detect(userAgent);
if (os !== OperatingSystem.Unknown) {
return os;
}
}
return OperatingSystem.Unknown;
}
}
// Reference: https://github.com/keithws/browser-report/blob/master/index.js#L304
const BrowserDetectors =
[
define(OperatingSystem.KaiOS, (b) =>
b.mustInclude('KAIOS')),
define(OperatingSystem.ChromeOS, (b) =>
b.mustInclude('CrOS')),
define(OperatingSystem.BlackBerryOS, (b) =>
b.mustInclude('BlackBerry')),
define(OperatingSystem.BlackBerryTabletOS, (b) =>
b.mustInclude('RIM Tablet OS')),
define(OperatingSystem.BlackBerry, (b) =>
b.mustInclude('BB10')),
define(OperatingSystem.Android, (b) =>
b.mustInclude('Android').mustNotInclude('Windows Phone')),
define(OperatingSystem.Android, (b) =>
b.mustInclude('Adr').mustNotInclude('Windows Phone')),
define(OperatingSystem.iOS, (b) =>
b.mustInclude('like Mac OS X')),
define(OperatingSystem.Linux, (b) =>
b.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
define(OperatingSystem.Windows, (b) =>
b.mustInclude('Windows').mustNotInclude('Windows Phone')),
define(OperatingSystem.WindowsPhone, (b) =>
b.mustInclude('Windows Phone')),
define(OperatingSystem.macOS, (b) =>
b.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
];
function define(os: OperatingSystem, applyRules: (builder: DetectorBuilder) => DetectorBuilder): IBrowserOsDetector {
const builder = new DetectorBuilder(os);
applyRules(builder);
return builder.build();
}

View File

@@ -0,0 +1,53 @@
import { IBrowserOsDetector } from './IBrowserOsDetector';
import { OperatingSystem } from '@/domain/OperatingSystem';
export class DetectorBuilder {
private readonly existingPartsInUserAgent = new Array<string>();
private readonly notExistingPartsInUserAgent = new Array<string>();
constructor(private readonly os: OperatingSystem) { }
public mustInclude(str: string): DetectorBuilder {
return this.add(str, this.existingPartsInUserAgent);
}
public mustNotInclude(str: string): DetectorBuilder {
return this.add(str, this.notExistingPartsInUserAgent);
}
public build(): IBrowserOsDetector {
if (!this.existingPartsInUserAgent.length) {
throw new Error('Must include at least a part');
}
return {
detect: (agent) => this.detect(agent),
};
}
private detect(userAgent: string): OperatingSystem {
if (!userAgent) {
throw new Error('User agent is null or undefined');
}
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
return OperatingSystem.Unknown;
}
if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) {
return OperatingSystem.Unknown;
}
return this.os;
}
private add(part: string, array: string[]): DetectorBuilder {
if (!part) {
throw new Error('part is empty or undefined');
}
if (this.existingPartsInUserAgent.includes(part)) {
throw new Error(`part ${part} is already included as existing part`);
}
if (this.notExistingPartsInUserAgent.includes(part)) {
throw new Error(`part ${part} is already included as not existing part`);
}
array.push(part);
return this;
}
}

View File

@@ -0,0 +1,5 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IBrowserOsDetector {
detect(userAgent: string): OperatingSystem;
}

View File

@@ -0,0 +1,80 @@
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
import { IEnvironment } from './IEnvironment';
import { OperatingSystem } from '@/domain/OperatingSystem';
interface IEnvironmentVariables {
readonly window: Window & typeof globalThis;
readonly process: NodeJS.Process;
readonly navigator: Navigator;
}
export class Environment implements IEnvironment {
public static readonly CurrentEnvironment: IEnvironment = new Environment({
window,
process,
navigator,
});
public readonly isDesktop: boolean;
public readonly os: OperatingSystem;
protected constructor(
variables: IEnvironmentVariables,
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector()) {
if (!variables) {
throw new Error('variables is null or empty');
}
this.isDesktop = isDesktop(variables);
this.os = this.isDesktop ?
getDesktopOsType(getProcessPlatform(variables))
: browserOsDetector.detect(getUserAgent(variables));
}
}
function getUserAgent(variables: IEnvironmentVariables): string {
if (!variables.window || !variables.window.navigator) {
return undefined;
}
return variables.window.navigator.userAgent;
}
function getProcessPlatform(variables: IEnvironmentVariables): string {
if (!variables.process || !variables.process.platform) {
return undefined;
}
return variables.process.platform;
}
function getDesktopOsType(processPlatform: string): OperatingSystem {
// https://nodejs.org/api/process.html#process_process_platform
if (processPlatform === 'darwin') {
return OperatingSystem.macOS;
} else if (processPlatform === 'win32') {
return OperatingSystem.Windows;
} else if (processPlatform === 'linux') {
return OperatingSystem.Linux;
}
return OperatingSystem.Unknown;
}
function isDesktop(variables: IEnvironmentVariables): boolean {
// More: https://github.com/electron/electron/issues/2288
// Renderer process
if (variables.window
&& variables.window.process
&& variables.window.process.type === 'renderer') {
return true;
}
// Main process
if (variables.process
&& variables.process.versions
&& Boolean(variables.process.versions.electron)) {
return true;
}
// Detect the user agent when the `nodeIntegration` option is set to true
if (variables.navigator
&& variables.navigator.userAgent
&& variables.navigator.userAgent.includes('Electron')) {
return true;
}
return false;
}

View File

@@ -0,0 +1,6 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IEnvironment {
isDesktop: boolean;
os: OperatingSystem;
}

View File

@@ -1,24 +1,37 @@
import { Category } from '@/domain/Category'; import { Category } from '@/domain/Category';
import { Application } from '@/domain/Application'; import { Application } from '@/domain/Application';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { IProjectInformation } from '@/domain/IProjectInformation';
import { ApplicationYaml } from 'js-yaml-loader!./../application.yaml'; import { ApplicationYaml } from 'js-yaml-loader!./../application.yaml';
import { parseCategory } from './CategoryParser'; import { parseCategory } from './CategoryParser';
import { ProjectInformation } from '@/domain/ProjectInformation';
import { ScriptCompiler } from './Compiler/ScriptCompiler';
export function parseApplication(content: ApplicationYaml): IApplication {
export function parseApplication(content: ApplicationYaml, env: NodeJS.ProcessEnv = process.env): IApplication {
validate(content); validate(content);
const compiler = new ScriptCompiler(content.functions);
const categories = new Array<Category>(); const categories = new Array<Category>();
for (const action of content.actions) { for (const action of content.actions) {
const category = parseCategory(action); const category = parseCategory(action, compiler);
categories.push(category); categories.push(category);
} }
const info = readAppInformation(env);
const app = new Application( const app = new Application(
content.name, info,
content.repositoryUrl,
process.env.VUE_APP_VERSION,
categories); categories);
return app; return app;
} }
function readAppInformation(environment: NodeJS.ProcessEnv): IProjectInformation {
return new ProjectInformation(
environment.VUE_APP_NAME,
environment.VUE_APP_VERSION,
environment.VUE_APP_REPOSITORY_URL,
environment.VUE_APP_HOMEPAGE_URL,
);
}
function validate(content: ApplicationYaml): void { function validate(content: ApplicationYaml): void {
if (!content) { if (!content) {
throw new Error('application is null or undefined'); throw new Error('application is null or undefined');

View File

@@ -3,6 +3,7 @@ import { Script } from '@/domain/Script';
import { Category } from '@/domain/Category'; import { Category } from '@/domain/Category';
import { parseDocUrls } from './DocumentationParser'; import { parseDocUrls } from './DocumentationParser';
import { parseScript } from './ScriptParser'; import { parseScript } from './ScriptParser';
import { IScriptCompiler } from './Compiler/IScriptCompiler';
let categoryIdCounter: number = 0; let categoryIdCounter: number = 0;
@@ -11,14 +12,17 @@ interface ICategoryChildren {
subScripts: Script[]; subScripts: Script[];
} }
export function parseCategory(category: YamlCategory): Category { export function parseCategory(category: YamlCategory, compiler: IScriptCompiler): Category {
if (!compiler) {
throw new Error('undefined compiler');
}
ensureValid(category); ensureValid(category);
const children: ICategoryChildren = { const children: ICategoryChildren = {
subCategories: new Array<Category>(), subCategories: new Array<Category>(),
subScripts: new Array<Script>(), subScripts: new Array<Script>(),
}; };
for (const categoryOrScript of category.children) { for (const categoryOrScript of category.children) {
parseCategoryChild(categoryOrScript, children, category); parseCategoryChild(categoryOrScript, children, category, compiler);
} }
return new Category( return new Category(
/*id*/ categoryIdCounter++, /*id*/ categoryIdCounter++,
@@ -42,22 +46,26 @@ function ensureValid(category: YamlCategory) {
} }
function parseCategoryChild( function parseCategoryChild(
categoryOrScript: any, children: ICategoryChildren, parent: YamlCategory) { categoryOrScript: any,
children: ICategoryChildren,
parent: YamlCategory,
compiler: IScriptCompiler) {
if (isCategory(categoryOrScript)) { if (isCategory(categoryOrScript)) {
const subCategory = parseCategory(categoryOrScript as YamlCategory); const subCategory = parseCategory(categoryOrScript as YamlCategory, compiler);
children.subCategories.push(subCategory); children.subCategories.push(subCategory);
} else if (isScript(categoryOrScript)) { } else if (isScript(categoryOrScript)) {
const yamlScript = categoryOrScript as YamlScript; const yamlScript = categoryOrScript as YamlScript;
const script = parseScript(yamlScript); const script = parseScript(yamlScript, compiler);
children.subScripts.push(script); children.subScripts.push(script);
} else { } else {
throw new Error(`Child element is neither a category or a script. throw new Error(`Child element is neither a category or a script.
Parent: ${parent.category}, element: ${categoryOrScript}`); Parent: ${parent.category}, element: ${JSON.stringify(categoryOrScript)}`);
} }
} }
function isScript(categoryOrScript: any): boolean { function isScript(categoryOrScript: any): boolean {
return categoryOrScript.code && categoryOrScript.code.length > 0; return (categoryOrScript.code && categoryOrScript.code.length > 0)
|| categoryOrScript.call;
} }
function isCategory(categoryOrScript: any): boolean { function isCategory(categoryOrScript: any): boolean {

View File

@@ -0,0 +1,7 @@
import { IScriptCode } from '@/domain/IScriptCode';
import { YamlScript } from 'js-yaml-loader!./application.yaml';
export interface IScriptCompiler {
canCompile(script: YamlScript): boolean;
compile(script: YamlScript): IScriptCode;
}

View File

@@ -0,0 +1,200 @@
import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCode } from '@/domain/ScriptCode';
import { YamlScript, YamlFunction, FunctionCall, ScriptFunctionCall, FunctionCallParameters } from 'js-yaml-loader!./application.yaml';
import { IScriptCompiler } from './IScriptCompiler';
interface ICompiledCode {
readonly code: string;
readonly revertCode: string;
}
export class ScriptCompiler implements IScriptCompiler {
constructor(private readonly functions: readonly YamlFunction[]) {
ensureValidFunctions(functions);
}
public canCompile(script: YamlScript): boolean {
if (!script.call) {
return false;
}
return true;
}
public compile(script: YamlScript): IScriptCode {
this.ensureCompilable(script.call);
const compiledCodes = new Array<ICompiledCode>();
const calls = getCallSequence(script.call);
calls.forEach((currentCall, currentCallIndex) => {
ensureValidCall(currentCall, script.name);
const commonFunction = this.getFunctionByName(currentCall.function);
let functionCode = compileCode(commonFunction, currentCall.parameters);
if (currentCallIndex !== calls.length - 1) {
functionCode = appendLine(functionCode);
}
compiledCodes.push(functionCode);
});
const scriptCode = merge(compiledCodes);
return new ScriptCode(script.name, scriptCode.code, scriptCode.revertCode);
}
private getFunctionByName(name: string): YamlFunction {
const func = this.functions.find((f) => f.name === name);
if (!func) {
throw new Error(`called function is not defined "${name}"`);
}
return func;
}
private ensureCompilable(call: ScriptFunctionCall) {
if (!this.functions || this.functions.length === 0) {
throw new Error('cannot compile without shared functions');
}
if (typeof call !== 'object') {
throw new Error('called function(s) must be an object');
}
}
}
function getDuplicates(texts: readonly string[]): string[] {
return texts.filter((item, index) => texts.indexOf(item) !== index);
}
function printList(list: readonly string[]): string {
return `"${list.join('","')}"`;
}
function ensureNoDuplicatesInFunctionNames(functions: readonly YamlFunction[]) {
const duplicateFunctionNames = getDuplicates(functions
.map((func) => func.name.toLowerCase()));
if (duplicateFunctionNames.length) {
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
}
}
function ensureNoDuplicatesInParameterNames(functions: readonly YamlFunction[]) {
const functionsWithParameters = functions
.filter((func) => func.parameters && func.parameters.length > 0);
for (const func of functionsWithParameters) {
const duplicateParameterNames = getDuplicates(func.parameters);
if (duplicateParameterNames.length) {
throw new Error(`"${func.name}": duplicate parameter name: ${printList(duplicateParameterNames)}`);
}
}
}
function ensureNoDuplicateCode(functions: readonly YamlFunction[]) {
const duplicateCodes = getDuplicates(functions.map((func) => func.code));
if (duplicateCodes.length > 0) {
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
}
const duplicateRevertCodes = getDuplicates(functions
.filter((func) => func.revertCode)
.map((func) => func.revertCode));
if (duplicateRevertCodes.length > 0) {
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
}
}
function ensureValidFunctions(functions: readonly YamlFunction[]) {
if (!functions) {
return;
}
ensureNoDuplicatesInFunctionNames(functions);
ensureNoDuplicatesInParameterNames(functions);
ensureNoDuplicateCode(functions);
}
function appendLine(code: ICompiledCode): ICompiledCode {
const appendLineIfNotEmpty = (str: string) => str ? `${str}\n` : str;
return {
code: appendLineIfNotEmpty(code.code),
revertCode: appendLineIfNotEmpty(code.revertCode),
};
}
function merge(codes: readonly ICompiledCode[]): ICompiledCode {
return {
code: codes.map((code) => code.code).join(''),
revertCode: codes.map((code) => code.revertCode).join(''),
};
}
function compileCode(func: YamlFunction, parameters: FunctionCallParameters): ICompiledCode {
return {
code: compileExpressions(func.code, parameters),
revertCode: compileExpressions(func.revertCode, parameters),
};
}
function compileExpressions(code: string, parameters: FunctionCallParameters): string {
let intermediateCode = compileToIL(code);
intermediateCode = substituteParameters(intermediateCode, parameters);
ensureNoExpressionLeft(intermediateCode);
return intermediateCode;
}
function substituteParameters(intermediateCode: string, parameters: FunctionCallParameters): string {
const parameterNames = getUniqueParameterNamesFromIL(intermediateCode);
if (parameterNames.length && !parameters) {
throw new Error(`no parameters defined, expected: ${printList(parameterNames)}`);
}
for (const parameterName of parameterNames) {
const parameterValue = parameters[parameterName];
intermediateCode = substituteParameter(intermediateCode, parameterName, parameterValue);
}
return intermediateCode;
}
function ensureValidCall(call: FunctionCall, scriptName: string) {
if (!call) {
throw new Error(`undefined function call in script "${scriptName}"`);
}
if (!call.function) {
throw new Error(`empty function name called in script "${scriptName}"`);
}
}
function getCallSequence(call: ScriptFunctionCall): FunctionCall[] {
if (call instanceof Array) {
return call as FunctionCall[];
}
return [ call as FunctionCall ];
}
function getDistinctValues(values: readonly string[]): string[] {
return values.filter((value, index, self) => {
return self.indexOf(value) === index;
});
}
// Trim each expression and put them inside "{{exp|}}" e.g. "{{ $hello }}" becomes "{{exp|$hello}}"
function compileToIL(code: string) {
return code.replace(/\{\{([\s]*[^;\s\{]+[\s]*)\}\}/g, (_, match) => {
return `\{\{exp|${match.trim()}\}\}`;
});
}
// Parses all distinct usages of {{exp|$parameterName}}
function getUniqueParameterNamesFromIL(ilCode: string) {
const allSubstitutions = ilCode.matchAll(/\{\{exp\|\$([^;\s\{]+[\s]*)\}\}/g);
const allParameters = Array.from(allSubstitutions, (match) => match[1]);
const uniqueParameterNames = getDistinctValues(allParameters);
return uniqueParameterNames;
}
// substitutes {{exp|$parameterName}} to value of the parameter
function substituteParameter(ilCode: string, parameterName: string, parameterValue: string) {
if (!parameterValue) {
throw Error(`parameter value is not provided for "${parameterName}" in function call`);
}
const pattern = `{{exp|$${parameterName}}}`;
return ilCode.split(pattern).join(parameterValue); // as .replaceAll() is not yet supported by TS
}
// finds all "{{exp|..}} left"
function ensureNoExpressionLeft(ilCode: string) {
const allSubstitutions = ilCode.matchAll(/\{\{exp\|(.*?)\}\}/g);
const allMatches = Array.from(allSubstitutions, (match) => match[1]);
const uniqueExpressions = getDistinctValues(allMatches);
if (uniqueExpressions.length > 0) {
throw new Error(`unknown expression: ${printList(uniqueExpressions)}`);
}
}

View File

@@ -1,37 +1,46 @@
import { YamlDocumentable } from 'js-yaml-loader!./application.yaml'; import { YamlDocumentable, DocumentationUrls } from 'js-yaml-loader!./application.yaml';
export function parseDocUrls(documentable: YamlDocumentable): ReadonlyArray<string> { export function parseDocUrls(documentable: YamlDocumentable): ReadonlyArray<string> {
if (!documentable) { if (!documentable) {
throw new Error('documentable is null or undefined'); throw new Error('documentable is null or undefined');
} }
const docs = documentable.docs; const docs = documentable.docs;
if (!docs) { if (!docs || !docs.length) {
return []; return [];
} }
const result = new DocumentationUrls(); let result = new DocumentationUrlContainer();
if (docs instanceof Array) { result = addDocs(docs, result);
for (const doc of docs) {
if (typeof doc !== 'string') {
throw new Error('Docs field (documentation url) must be an array of strings');
}
result.add(doc);
}
} else if (typeof docs === 'string') {
result.add(docs);
} else {
throw new Error('Docs field (documentation url) must a string or array of strings');
}
return result.getAll(); return result.getAll();
} }
class DocumentationUrls { function addDocs(docs: DocumentationUrls, urls: DocumentationUrlContainer): DocumentationUrlContainer {
if (docs instanceof Array) {
urls.addUrls(docs);
} else if (typeof docs === 'string') {
urls.addUrl(docs);
} else {
throw new Error('Docs field (documentation url) must a string or array of strings');
}
return urls;
}
class DocumentationUrlContainer {
private readonly urls = new Array<string>(); private readonly urls = new Array<string>();
public add(url: string) { public addUrl(url: string) {
validateUrl(url); validateUrl(url);
this.urls.push(url); this.urls.push(url);
} }
public addUrls(urls: any[]) {
for (const url of urls) {
if (typeof url !== 'string') {
throw new Error('Docs field (documentation url) must be an array of strings');
}
this.addUrl(url);
}
}
public getAll(): ReadonlyArray<string> { public getAll(): ReadonlyArray<string> {
return this.urls; return this.urls;
} }

View File

@@ -1,16 +1,61 @@
import { Script } from '@/domain/Script'; import { Script } from '@/domain/Script';
import { YamlScript } from 'js-yaml-loader!./application.yaml'; import { YamlScript } from 'js-yaml-loader!./application.yaml';
import { parseDocUrls } from './DocumentationParser'; import { parseDocUrls } from './DocumentationParser';
import { RecommendationLevelNames, RecommendationLevel } from '@/domain/RecommendationLevel';
import { IScriptCompiler } from './Compiler/IScriptCompiler';
import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCode } from '@/domain/ScriptCode';
export function parseScript(yamlScript: YamlScript): Script { export function parseScript(yamlScript: YamlScript, compiler: IScriptCompiler): Script {
if (!yamlScript) { validateScript(yamlScript);
throw new Error('script is null or undefined'); if (!compiler) {
throw new Error('undefined compiler');
} }
const script = new Script( const script = new Script(
/* name */ yamlScript.name, /* name */ yamlScript.name,
/* code */ yamlScript.code, /* code */ parseCode(yamlScript, compiler),
/* revertCode */ yamlScript.revertCode,
/* docs */ parseDocUrls(yamlScript), /* docs */ parseDocUrls(yamlScript),
/* isRecommended */ yamlScript.recommend); /* level */ getLevel(yamlScript.recommend));
return script; return script;
} }
function getLevel(level: string): RecommendationLevel | undefined {
if (!level) {
return undefined;
}
if (typeof level !== 'string') {
throw new Error(`level must be a string but it was ${typeof level}`);
}
const typedLevel = RecommendationLevelNames
.find((l) => l.toLowerCase() === level.toLowerCase());
if (!typedLevel) {
throw new Error(`unknown level: \"${level}\"`);
}
return RecommendationLevel[typedLevel as keyof typeof RecommendationLevel];
}
function parseCode(yamlScript: YamlScript, compiler: IScriptCompiler): IScriptCode {
if (compiler.canCompile(yamlScript)) {
return compiler.compile(yamlScript);
}
return new ScriptCode(yamlScript.name, yamlScript.code, yamlScript.revertCode);
}
function ensureNotBothCallAndCode(yamlScript: YamlScript) {
if (yamlScript.code && yamlScript.call) {
throw new Error('cannot define both "call" and "code"');
}
if (yamlScript.revertCode && yamlScript.call) {
throw new Error('cannot define "revertCode" if "call" is defined');
}
}
function validateScript(yamlScript: YamlScript) {
if (!yamlScript) {
throw new Error('undefined script');
}
if (!yamlScript.code && !yamlScript.call) {
throw new Error('must define either "call" or "code"');
}
ensureNotBothCallAndCode(yamlScript);
}

View File

@@ -11,6 +11,7 @@ import { Script } from '@/domain/Script';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { IApplicationCode } from './Code/IApplicationCode'; import { IApplicationCode } from './Code/IApplicationCode';
import applicationFile from 'js-yaml-loader!@/application/application.yaml'; import applicationFile from 'js-yaml-loader!@/application/application.yaml';
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
/** Mutatable singleton application state that's the single source of truth throughout the application */ /** Mutatable singleton application state that's the single source of truth throughout the application */
export class ApplicationState implements IApplicationState { export class ApplicationState implements IApplicationState {
@@ -37,8 +38,8 @@ export class ApplicationState implements IApplicationState {
public readonly app: IApplication, public readonly app: IApplication,
/** Initially selected scripts */ /** Initially selected scripts */
public readonly defaultScripts: Script[]) { public readonly defaultScripts: Script[]) {
this.selection = new UserSelection(app, defaultScripts); this.selection = new UserSelection(app, defaultScripts.map((script) => new SelectedScript(script, false)));
this.code = new ApplicationCode(this.selection, app.version); this.code = new ApplicationCode(this.selection, app.info.version);
this.filter = new UserFilter(app); this.filter = new UserFilter(app);
} }
} }

View File

@@ -1,30 +1,38 @@
import { CodeChangedEvent } from './Event/CodeChangedEvent';
import { CodePosition } from './Position/CodePosition';
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
import { SelectedScript } from '@/application/State/Selection/SelectedScript'; import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { IUserSelection } from '@/application/State/Selection/IUserSelection'; import { IUserSelection } from '@/application/State/Selection/IUserSelection';
import { UserScriptGenerator } from './UserScriptGenerator'; import { UserScriptGenerator } from './Generation/UserScriptGenerator';
import { Signal } from '@/infrastructure/Events/Signal'; import { Signal } from '@/infrastructure/Events/Signal';
import { IApplicationCode } from './IApplicationCode'; import { IApplicationCode } from './IApplicationCode';
import { IUserScriptGenerator } from './IUserScriptGenerator'; import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
export class ApplicationCode implements IApplicationCode { export class ApplicationCode implements IApplicationCode {
public readonly changed = new Signal<string>(); public readonly changed = new Signal<ICodeChangedEvent>();
public current: string; public current: string;
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(); private scriptPositions = new Map<SelectedScript, CodePosition>();
constructor( constructor(
userSelection: IUserSelection, userSelection: IUserSelection,
private readonly version: string) { private readonly version: string,
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) {
if (!userSelection) { throw new Error('userSelection is null or undefined'); } if (!userSelection) { throw new Error('userSelection is null or undefined'); }
if (!version) { throw new Error('version is null or undefined'); } if (!version) { throw new Error('version is null or undefined'); }
this.generator = new UserScriptGenerator(); if (!generator) { throw new Error('generator is null or undefined'); }
this.setCode(userSelection.selectedScripts); this.setCode(userSelection.selectedScripts);
userSelection.changed.on((scripts) => { userSelection.changed.on((scripts) => {
this.setCode(scripts); this.setCode(scripts);
}); });
} }
private setCode(scripts: ReadonlyArray<SelectedScript>) { private setCode(scripts: ReadonlyArray<SelectedScript>): void {
this.current = scripts.length === 0 ? '' : this.generator.buildCode(scripts, this.version); const oldScripts = Array.from(this.scriptPositions.keys());
this.changed.notify(this.current); const code = this.generator.buildCode(scripts, this.version);
this.current = code.code;
this.scriptPositions = code.scriptPositions;
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
this.changed.notify(event);
} }
} }

View File

@@ -0,0 +1,64 @@
import { ICodeChangedEvent } from './ICodeChangedEvent';
import { SelectedScript } from '../../Selection/SelectedScript';
import { IScript } from '@/domain/IScript';
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
export class CodeChangedEvent implements ICodeChangedEvent {
public readonly code: string;
public readonly addedScripts: ReadonlyArray<IScript>;
public readonly removedScripts: ReadonlyArray<IScript>;
public readonly changedScripts: ReadonlyArray<IScript>;
private readonly scripts: Map<IScript, ICodePosition>;
constructor(
code: string,
oldScripts: ReadonlyArray<SelectedScript>,
scripts: Map<SelectedScript, ICodePosition>) {
ensureAllPositionsExist(code, Array.from(scripts.values()));
this.code = code;
const newScripts = Array.from(scripts.keys());
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
this.changedScripts = getChangedScripts(oldScripts, newScripts);
this.scripts = new Map<IScript, ICodePosition>();
scripts.forEach((position, selection) => {
this.scripts.set(selection.script, position);
});
}
public isEmpty(): boolean {
return this.scripts.size === 0;
}
public getScriptPositionInCode(script: IScript): ICodePosition {
return this.scripts.get(script);
}
}
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
const totalLines = script.split(/\r\n|\r|\n/).length;
for (const position of positions) {
if (position.endLine > totalLines) {
throw new Error(`script end line (${position.endLine}) is out of range.` +
`(total code lines: ${totalLines}`);
}
}
}
function getChangedScripts(
oldScripts: ReadonlyArray<SelectedScript>,
newScripts: ReadonlyArray<SelectedScript>): ReadonlyArray<IScript> {
return newScripts
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
&& oldScript.revert !== newScript.revert ))
.map((selection) => selection.script);
}
function selectIfNotExists(
selectableContainer: ReadonlyArray<SelectedScript>,
test: ReadonlyArray<SelectedScript>) {
return selectableContainer
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
.map((selection) => selection.script);
}

View File

@@ -0,0 +1,11 @@
import { IScript } from '@/domain/IScript';
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
export interface ICodeChangedEvent {
readonly code: string;
addedScripts: ReadonlyArray<IScript>;
removedScripts: ReadonlyArray<IScript>;
changedScripts: ReadonlyArray<IScript>;
isEmpty(): boolean;
getScriptPositionInCode(script: IScript): ICodePosition;
}

View File

@@ -4,8 +4,20 @@ const TotalFunctionSeparatorChars = 58;
export class CodeBuilder { export class CodeBuilder {
private readonly lines = new Array<string>(); private readonly lines = new Array<string>();
// Returns current line starting from 0 (no lines), or 1 (have single line)
public get currentLine(): number {
return this.lines.length;
}
public appendLine(code?: string): CodeBuilder { public appendLine(code?: string): CodeBuilder {
this.lines.push(code); if (!code) {
this.lines.push('');
return this;
}
const lines = code.match(/[^\r\n]+/g);
for (const line of lines) {
this.lines.push(line);
}
return this; return this;
} }

View File

@@ -0,0 +1,7 @@
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
export interface IUserScript {
code: string;
scriptPositions: Map<SelectedScript, ICodePosition>;
}

View File

@@ -0,0 +1,7 @@
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { IUserScript } from './IUserScript';
export interface IUserScriptGenerator {
buildCode(
selectedScripts: ReadonlyArray<SelectedScript>,
version: string): IUserScript;
}

View File

@@ -0,0 +1,68 @@
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { IUserScriptGenerator } from './IUserScriptGenerator';
import { CodeBuilder } from './CodeBuilder';
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
import { CodePosition } from '../Position/CodePosition';
import { IUserScript } from './IUserScript';
export const adminRightsScript = {
name: 'Ensure admin privileges',
code: 'fltmc >nul 2>&1 || (\n' +
' echo Administrator privileges are required.\n' +
' PowerShell Start -Verb RunAs \'%0\' 2> nul || (\n' +
' echo Right-click on the script and select "Run as administrator".\n' +
' pause & exit 1\n' +
' )\n' +
' exit 0\n' +
')',
};
export class UserScriptGenerator implements IUserScriptGenerator {
public buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): IUserScript {
if (!selectedScripts) { throw new Error('scripts is undefined'); }
if (!version) { throw new Error('version is undefined'); }
let scriptPositions = new Map<SelectedScript, ICodePosition>();
if (!selectedScripts.length) {
return { code: '', scriptPositions };
}
const builder = initializeCode(version);
for (const selection of selectedScripts) {
scriptPositions = appendSelection(selection, scriptPositions, builder);
}
const code = finalizeCode(builder);
return { code, scriptPositions };
}
}
function initializeCode(version: string): CodeBuilder {
return new CodeBuilder()
.appendLine('@echo off')
.appendCommentLine(`https://privacy.sexy — v${version}${new Date().toUTCString()}`)
.appendFunction(adminRightsScript.name, adminRightsScript.code)
.appendLine();
}
function finalizeCode(builder: CodeBuilder): string {
return builder.appendLine()
.appendLine('pause')
.appendLine('exit /b 0')
.toString();
}
function appendSelection(
selection: SelectedScript,
scriptPositions: Map<SelectedScript, ICodePosition>,
builder: CodeBuilder): Map<SelectedScript, ICodePosition> {
const startPosition = builder.currentLine + 1;
appendCode(selection, builder);
const endPosition = builder.currentLine - 1;
builder.appendLine();
scriptPositions.set(selection, new CodePosition(startPosition, endPosition));
return scriptPositions;
}
function appendCode(selection: SelectedScript, builder: CodeBuilder) {
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute;
builder.appendFunction(name, scriptCode);
}

View File

@@ -1,6 +1,7 @@
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
import { ISignal } from '@/infrastructure/Events/ISignal'; import { ISignal } from '@/infrastructure/Events/ISignal';
export interface IApplicationCode { export interface IApplicationCode {
readonly changed: ISignal<string>; readonly changed: ISignal<ICodeChangedEvent>;
readonly current: string; readonly current: string;
} }

View File

@@ -1,5 +0,0 @@
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
export interface IUserScriptGenerator {
buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): string;
}

View File

@@ -0,0 +1,24 @@
import { ICodePosition } from './ICodePosition';
export class CodePosition implements ICodePosition {
public get totalLines(): number {
return this.endLine - this.startLine;
}
constructor(
public readonly startLine: number,
public readonly endLine: number) {
if (startLine < 0) {
throw new Error('Code cannot start in a negative line');
}
if (endLine < 0) {
throw new Error('Code cannot end in a negative line');
}
if (endLine === startLine) {
throw new Error('Empty code');
}
if (endLine < startLine) {
throw new Error('End line cannot be less than start line');
}
}
}

View File

@@ -0,0 +1,5 @@
export interface ICodePosition {
readonly startLine: number;
readonly endLine: number;
readonly totalLines: number;
}

View File

@@ -1,34 +0,0 @@
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { IUserScriptGenerator } from './IUserScriptGenerator';
import { CodeBuilder } from './CodeBuilder';
export const adminRightsScript = {
name: 'Ensure admin privileges',
code: 'fltmc >nul 2>&1 || (\n' +
' echo This batch script requires administrator privileges. Right-click on\n' +
' echo the script and select "Run as administrator".\n' +
' pause\n' +
' exit 1\n' +
')',
};
export class UserScriptGenerator implements IUserScriptGenerator {
public buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): string {
if (!selectedScripts) { throw new Error('scripts is undefined'); }
if (!selectedScripts.length) { throw new Error('scripts are empty'); }
if (!version) { throw new Error('version is undefined'); }
const builder = new CodeBuilder()
.appendLine('@echo off')
.appendCommentLine(`https://privacy.sexy — v${version}${new Date().toUTCString()}`)
.appendFunction(adminRightsScript.name, adminRightsScript.code).appendLine();
for (const selection of selectedScripts) {
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
const code = selection.revert ? selection.script.revertCode : selection.script.code;
builder.appendFunction(name, code).appendLine();
}
return builder.appendLine()
.appendLine('pause')
.appendLine('exit /b 0')
.toString();
}
}

View File

@@ -2,6 +2,7 @@ import { IFilterResult } from './IFilterResult';
import { ISignal } from '@/infrastructure/Events/Signal'; import { ISignal } from '@/infrastructure/Events/Signal';
export interface IUserFilter { export interface IUserFilter {
readonly currentFilter: IFilterResult | undefined;
readonly filtered: ISignal<IFilterResult>; readonly filtered: ISignal<IFilterResult>;
readonly filterRemoved: ISignal<void>; readonly filterRemoved: ISignal<void>;
setFilter(filter: string): void; setFilter(filter: string): void;

View File

@@ -8,6 +8,7 @@ import { Signal } from '@/infrastructure/Events/Signal';
export class UserFilter implements IUserFilter { export class UserFilter implements IUserFilter {
public readonly filtered = new Signal<IFilterResult>(); public readonly filtered = new Signal<IFilterResult>();
public readonly filterRemoved = new Signal<void>(); public readonly filterRemoved = new Signal<void>();
public currentFilter: IFilterResult | undefined;
constructor(private application: IApplication) { constructor(private application: IApplication) {
@@ -22,17 +23,17 @@ export class UserFilter implements IUserFilter {
(script) => isScriptAMatch(script, filterLowercase)); (script) => isScriptAMatch(script, filterLowercase));
const filteredCategories = this.application.getAllCategories().filter( const filteredCategories = this.application.getAllCategories().filter(
(category) => category.name.toLowerCase().includes(filterLowercase)); (category) => category.name.toLowerCase().includes(filterLowercase));
const matches = new FilterResult( const matches = new FilterResult(
filteredScripts, filteredScripts,
filteredCategories, filteredCategories,
filter, filter,
); );
this.currentFilter = matches;
this.filtered.notify(matches); this.filtered.notify(matches);
} }
public removeFilter(): void { public removeFilter(): void {
this.currentFilter = undefined;
this.filterRemoved.notify(); this.filterRemoved.notify();
} }
} }
@@ -41,11 +42,11 @@ function isScriptAMatch(script: IScript, filterLowercase: string) {
if (script.name.toLowerCase().includes(filterLowercase)) { if (script.name.toLowerCase().includes(filterLowercase)) {
return true; return true;
} }
if (script.code.toLowerCase().includes(filterLowercase)) { if (script.code.execute.toLowerCase().includes(filterLowercase)) {
return true; return true;
} }
if (script.revertCode) { if (script.code.revert) {
return script.revertCode.toLowerCase().includes(filterLowercase); return script.code.revert.toLowerCase().includes(filterLowercase);
} }
return false; return false;
} }

View File

@@ -1,16 +1,21 @@
import { SelectedScript } from './SelectedScript'; import { SelectedScript } from './SelectedScript';
import { ISignal } from '@/infrastructure/Events/Signal'; import { ISignal } from '@/infrastructure/Events/Signal';
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory';
export interface IUserSelection { export interface IUserSelection {
readonly changed: ISignal<ReadonlyArray<SelectedScript>>; readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
readonly selectedScripts: ReadonlyArray<SelectedScript>; readonly selectedScripts: ReadonlyArray<SelectedScript>;
readonly totalSelected: number; readonly totalSelected: number;
areAllSelected(category: ICategory): boolean;
isAnySelected(category: ICategory): boolean;
removeAllInCategory(categoryId: number): void;
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
addSelectedScript(scriptId: string, revert: boolean): void; addSelectedScript(scriptId: string, revert: boolean): void;
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void; addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
removeSelectedScript(scriptId: string): void; removeSelectedScript(scriptId: string): void;
selectOnly(scripts: ReadonlyArray<IScript>): void; selectOnly(scripts: ReadonlyArray<IScript>): void;
isSelected(script: IScript): boolean; isSelected(scriptId: string): boolean;
selectAll(): void; selectAll(): void;
deselectAll(): void; deselectAll(): void;
} }

View File

@@ -1,5 +1,5 @@
import { SelectedScript } from './SelectedScript'; import { SelectedScript } from './SelectedScript';
import { IApplication } from '@/domain/IApplication'; import { IApplication, ICategory } from '@/domain/IApplication';
import { IUserSelection } from './IUserSelection'; import { IUserSelection } from './IUserSelection';
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository'; import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
@@ -8,20 +8,67 @@ import { IRepository } from '@/infrastructure/Repository/IRepository';
export class UserSelection implements IUserSelection { export class UserSelection implements IUserSelection {
public readonly changed = new Signal<ReadonlyArray<SelectedScript>>(); public readonly changed = new Signal<ReadonlyArray<SelectedScript>>();
private readonly scripts: IRepository<string, SelectedScript> = new InMemoryRepository<string, SelectedScript>(); private readonly scripts: IRepository<string, SelectedScript>;
constructor( constructor(
private readonly app: IApplication, private readonly app: IApplication,
/** Initially selected scripts */ selectedScripts: ReadonlyArray<SelectedScript>) {
selectedScripts: ReadonlyArray<IScript>) { this.scripts = new InMemoryRepository<string, SelectedScript>();
if (selectedScripts && selectedScripts.length > 0) { if (selectedScripts && selectedScripts.length > 0) {
for (const script of selectedScripts) { for (const script of selectedScripts) {
const selected = new SelectedScript(script, false); this.scripts.addItem(script);
this.scripts.addItem(selected);
} }
} }
} }
public areAllSelected(category: ICategory): boolean {
if (this.selectedScripts.length === 0) {
return false;
}
const scripts = category.getAllScriptsRecursively();
if (this.selectedScripts.length < scripts.length) {
return false;
}
return scripts.every((script) => this.selectedScripts.some((selected) => selected.id === script.id));
}
public isAnySelected(category: ICategory): boolean {
if (this.selectedScripts.length === 0) {
return false;
}
return this.selectedScripts.some((s) => category.includes(s.script));
}
public removeAllInCategory(categoryId: number): void {
const category = this.app.findCategory(categoryId);
const scriptsToRemove = category.getAllScriptsRecursively()
.filter((script) => this.scripts.exists(script.id));
if (!scriptsToRemove.length) {
return;
}
for (const script of scriptsToRemove) {
this.scripts.removeItem(script.id);
}
this.changed.notify(this.scripts.getItems());
}
public addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void {
const category = this.app.findCategory(categoryId);
const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
.filter((script) =>
!this.scripts.exists(script.id)
|| this.scripts.getById(script.id).revert !== revert,
);
if (!scriptsToAddOrUpdate.length) {
return;
}
for (const script of scriptsToAddOrUpdate) {
const selectedScript = new SelectedScript(script, revert);
this.scripts.addOrUpdateItem(selectedScript);
}
this.changed.notify(this.scripts.getItems());
}
public addSelectedScript(scriptId: string, revert: boolean): void { public addSelectedScript(scriptId: string, revert: boolean): void {
const script = this.app.findScript(scriptId); const script = this.app.findScript(scriptId);
if (!script) { if (!script) {
@@ -44,8 +91,8 @@ export class UserSelection implements IUserSelection {
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
} }
public isSelected(script: IScript): boolean { public isSelected(scriptId: string): boolean {
return this.scripts.exists(script.id); return this.scripts.exists(scriptId);
} }
/** Get users scripts based on his/her selections */ /** Get users scripts based on his/her selections */

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,7 @@
declare module 'js-yaml-loader!*' { declare module 'js-yaml-loader!*' {
export type CategoryOrScript = YamlCategory | YamlScript; export interface ApplicationYaml {
type DocumentationUrls = ReadonlyArray<string> | string; actions: ReadonlyArray<YamlCategory>;
functions: ReadonlyArray<YamlFunction> | undefined;
export interface YamlDocumentable {
docs?: DocumentationUrls;
}
export interface YamlScript extends YamlDocumentable {
name: string;
code: string;
revertCode: string;
recommend: boolean;
} }
export interface YamlCategory extends YamlDocumentable { export interface YamlCategory extends YamlDocumentable {
@@ -18,10 +9,37 @@ declare module 'js-yaml-loader!*' {
category: string; category: string;
} }
export interface ApplicationYaml { export type CategoryOrScript = YamlCategory | YamlScript;
export type DocumentationUrls = ReadonlyArray<string> | string;
export interface YamlDocumentable {
docs?: DocumentationUrls;
}
export interface YamlFunction {
name: string; name: string;
repositoryUrl: string; code: string;
actions: ReadonlyArray<YamlCategory>; revertCode?: string;
parameters?: readonly string[];
}
export interface FunctionCallParameters {
[index: string]: string;
}
export interface FunctionCall {
function: string;
parameters?: FunctionCallParameters;
}
export type ScriptFunctionCall = readonly FunctionCall[] | FunctionCall | undefined;
export interface YamlScript extends YamlDocumentable {
name: string;
code: string | undefined;
revertCode: string | undefined;
call: ScriptFunctionCall;
recommend: string | undefined;
} }
const content: ApplicationYaml; const content: ApplicationYaml;

133
src/background.ts Normal file
View File

@@ -0,0 +1,133 @@
'use strict';
import { app, protocol, BrowserWindow, shell } from 'electron';
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
import path from 'path';
import { autoUpdater } from 'electron-updater';
import log from 'electron-log';
const isDevelopment = process.env.NODE_ENV !== 'production';
declare const __static: string; // https://github.com/electron-userland/electron-webpack/issues/172
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win: BrowserWindow | null;
// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } },
]);
// Setup logging
autoUpdater.logger = log; // https://www.electron.build/auto-update#debugging
log.transports.file.level = 'silly';
if (!process.env.IS_TEST) {
Object.assign(console, log.functions); // override console.log, console.warn etc.
}
function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 1350,
height: 955,
webPreferences: {
// Use pluginOptions.nodeIntegration, leave this alone
// See https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration
nodeIntegration: (process.env
.ELECTRON_NODE_INTEGRATION as unknown) as boolean,
},
// https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#set-tray-icon
icon: path.join(__static, 'icon.png'),
});
win.setMenuBarVisibility(false);
configureExternalsUrlsOpenBrowser(win);
loadApplication(win);
win.on('closed', () => {
win = null;
});
}
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (win === null) {
createWindow();
}
});
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', async () => {
if (isDevelopment && !process.env.IS_TEST) {
// Install Vue Devtools
try {
await installExtension(VUEJS_DEVTOOLS);
} catch (e) {
console.error('Vue Devtools failed to install:', e.toString()); // tslint:disable-line:no-console
}
}
createWindow();
});
// See electron-builder issue "checkForUpdatesAndNotify updates but does not notify on Windows 10"
// https://github.com/electron-userland/electron-builder/issues/2700
// https://github.com/electron/electron/issues/10864
if (process.platform === 'win32') {
// https://docs.microsoft.com/en-us/windows/win32/shell/appid#how-to-form-an-application-defined-appusermodelid
app.setAppUserModelId('Undergroundwires.PrivacySexy');
}
// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', (data) => {
if (data === 'graceful-exit') {
app.quit();
}
});
} else {
process.on('SIGTERM', () => {
app.quit();
});
}
}
function loadApplication(window: BrowserWindow) {
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string);
if (!process.env.IS_TEST) {
win.webContents.openDevTools();
}
} else {
createProtocol('app');
// Load the index.html when not in development
win.loadURL('app://./index.html');
// tslint:disable-next-line:max-line-length
autoUpdater.checkForUpdatesAndNotify(); // https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#check-for-updates-in-background-js-ts
}
}
function configureExternalsUrlsOpenBrowser(window: BrowserWindow) {
window.webContents.on('new-window', (event, url) => { // handle redirect
if (url !== win.webContents.getURL()) {
event.preventDefault();
shell.openExternal(url);
}
});
}

View File

@@ -2,53 +2,51 @@ import { IEntity } from '../infrastructure/Entity/IEntity';
import { ICategory } from './ICategory'; import { ICategory } from './ICategory';
import { IScript } from './IScript'; import { IScript } from './IScript';
import { IApplication } from './IApplication'; import { IApplication } from './IApplication';
import { IProjectInformation } from './IProjectInformation';
import { RecommendationLevel, RecommendationLevelNames, RecommendationLevels } from './RecommendationLevel';
export class Application implements IApplication { export class Application implements IApplication {
public get totalScripts(): number { return this.flattened.allScripts.length; } public get totalScripts(): number { return this.queryable.allScripts.length; }
public get totalCategories(): number { return this.flattened.allCategories.length; } public get totalCategories(): number { return this.queryable.allCategories.length; }
private readonly flattened: IFlattenedApplication; private readonly queryable: IQueryableApplication;
constructor( constructor(
public readonly name: string, public readonly info: IProjectInformation,
public readonly repositoryUrl: string,
public readonly version: string,
public readonly actions: ReadonlyArray<ICategory>) { public readonly actions: ReadonlyArray<ICategory>) {
if (!name) { throw Error('Application has no name'); } if (!info) {
if (!repositoryUrl) { throw Error('Application has no repository url'); } throw new Error('info is undefined');
if (!version) { throw Error('Version cannot be empty'); }
this.flattened = flatten(actions);
if (this.flattened.allCategories.length === 0) {
throw new Error('Application must consist of at least one category');
} }
if (this.flattened.allScripts.length === 0) { this.queryable = makeQueryable(actions);
throw new Error('Application must consist of at least one script'); ensureValid(this.queryable);
} ensureNoDuplicates(this.queryable.allCategories);
if (this.flattened.allScripts.filter((script) => script.isRecommended).length === 0) { ensureNoDuplicates(this.queryable.allScripts);
throw new Error('Application must consist of at least one recommended script');
}
ensureNoDuplicates(this.flattened.allCategories);
ensureNoDuplicates(this.flattened.allScripts);
} }
public findCategory(categoryId: number): ICategory | undefined { public findCategory(categoryId: number): ICategory | undefined {
return this.flattened.allCategories.find((category) => category.id === categoryId); return this.queryable.allCategories.find((category) => category.id === categoryId);
} }
public getRecommendedScripts(): readonly IScript[] { public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
return this.flattened.allScripts.filter((script) => script.isRecommended); if (isNaN(level)) {
throw new Error('undefined level');
}
if (!(level in RecommendationLevel)) {
throw new Error(`invalid level: ${level}`);
}
return this.queryable.scriptsByLevel.get(level);
} }
public findScript(scriptId: string): IScript | undefined { public findScript(scriptId: string): IScript | undefined {
return this.flattened.allScripts.find((script) => script.id === scriptId); return this.queryable.allScripts.find((script) => script.id === scriptId);
} }
public getAllScripts(): IScript[] { public getAllScripts(): IScript[] {
return this.flattened.allScripts; return this.queryable.allScripts;
} }
public getAllCategories(): ICategory[] { public getAllCategories(): ICategory[] {
return this.flattened.allCategories; return this.queryable.allCategories;
} }
} }
@@ -70,35 +68,85 @@ function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
} }
} }
interface IFlattenedApplication { interface IQueryableApplication {
allCategories: ICategory[]; allCategories: ICategory[];
allScripts: IScript[]; allScripts: IScript[];
scriptsByLevel: Map<RecommendationLevel, readonly IScript[]>;
} }
function flattenRecursive( function ensureValid(application: IQueryableApplication) {
categories: ReadonlyArray<ICategory>, ensureValidCategories(application.allCategories);
flattened: IFlattenedApplication) { ensureValidScripts(application.allScripts);
for (const category of categories) { }
flattened.allCategories.push(category);
if (category.scripts) { function ensureValidCategories(allCategories: readonly ICategory[]) {
for (const script of category.scripts) { if (!allCategories || allCategories.length === 0) {
flattened.allScripts.push(script); throw new Error('Application must consist of at least one category');
} }
} }
if (category.subCategories && category.subCategories.length > 0) {
flattenRecursive( function ensureValidScripts(allScripts: readonly IScript[]) {
category.subCategories as ReadonlyArray<ICategory>, if (!allScripts || allScripts.length === 0) {
flattened); throw new Error('Application must consist of at least one script');
}
for (const level of RecommendationLevels) {
if (allScripts.every((script) => script.level !== level)) {
throw new Error(`none of the scripts are recommended as ${RecommendationLevel[level]}`);
} }
} }
} }
function flatten( function flattenApplication(categories: ReadonlyArray<ICategory>): [ICategory[], IScript[]] {
categories: ReadonlyArray<ICategory>): IFlattenedApplication { const allCategories = new Array<ICategory>();
const flattened: IFlattenedApplication = { const allScripts = new Array<IScript>();
allCategories: new Array<ICategory>(), flattenCategories(categories, allCategories, allScripts);
allScripts: new Array<IScript>(), return [
}; allCategories,
flattenRecursive(categories, flattened); allScripts,
return flattened; ];
}
function flattenCategories(
categories: ReadonlyArray<ICategory>,
allCategories: ICategory[],
allScripts: IScript[]): IQueryableApplication {
if (!categories || categories.length === 0) {
return;
}
for (const category of categories) {
allCategories.push(category);
flattenScripts(category.scripts, allScripts);
flattenCategories(category.subCategories, allCategories, allScripts);
}
}
function flattenScripts(
scripts: ReadonlyArray<IScript>,
allScripts: IScript[]): IScript[] {
if (!scripts) {
return;
}
for (const script of scripts) {
allScripts.push(script);
}
}
function makeQueryable(
actions: ReadonlyArray<ICategory>): IQueryableApplication {
const flattened = flattenApplication(actions);
return {
allCategories: flattened[0],
allScripts: flattened[1],
scriptsByLevel: groupByLevel(flattened[1]),
};
}
function groupByLevel(allScripts: readonly IScript[]): Map<RecommendationLevel, readonly IScript[]> {
const map = new Map<RecommendationLevel, readonly IScript[]>();
for (const levelName of RecommendationLevelNames) {
const level = RecommendationLevel[levelName];
const scripts = allScripts.filter((script) => script.level !== undefined && script.level <= level);
map.set(level, scripts);
}
return map;
} }

View File

@@ -3,15 +3,7 @@ import { IScript } from './IScript';
import { ICategory } from './ICategory'; import { ICategory } from './ICategory';
export class Category extends BaseEntity<number> implements ICategory { export class Category extends BaseEntity<number> implements ICategory {
private static validate(category: ICategory) { private allSubScripts: ReadonlyArray<IScript> = undefined;
if (!category.name) {
throw new Error('name is null or empty');
}
if ((!category.subCategories || category.subCategories.length === 0) &&
(!category.scripts || category.scripts.length === 0)) {
throw new Error('A category must have at least one sub-category or scripts');
}
}
constructor( constructor(
id: number, id: number,
@@ -20,6 +12,31 @@ export class Category extends BaseEntity<number> implements ICategory {
public readonly subCategories?: ReadonlyArray<ICategory>, public readonly subCategories?: ReadonlyArray<ICategory>,
public readonly scripts?: ReadonlyArray<IScript>) { public readonly scripts?: ReadonlyArray<IScript>) {
super(id); super(id);
Category.validate(this); validateCategory(this);
}
public includes(script: IScript): boolean {
return this.getAllScriptsRecursively().some((childScript) => childScript.id === script.id);
}
public getAllScriptsRecursively(): readonly IScript[] {
return this.allSubScripts || (this.allSubScripts = parseScriptsRecursively(this));
}
}
function parseScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
return [
...category.scripts,
...category.subCategories.flatMap((c) => c.getAllScriptsRecursively()),
];
}
function validateCategory(category: ICategory) {
if (!category.name) {
throw new Error('undefined or empty name');
}
if ((!category.subCategories || category.subCategories.length === 0) &&
(!category.scripts || category.scripts.length === 0)) {
throw new Error('A category must have at least one sub-category or script');
} }
} }

View File

@@ -1,15 +1,15 @@
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory'; import { ICategory } from '@/domain/ICategory';
import { IProjectInformation } from './IProjectInformation';
import { RecommendationLevel } from './RecommendationLevel';
export interface IApplication { export interface IApplication {
readonly name: string; readonly info: IProjectInformation;
readonly repositoryUrl: string;
readonly version: string;
readonly totalScripts: number; readonly totalScripts: number;
readonly totalCategories: number; readonly totalCategories: number;
readonly actions: ReadonlyArray<ICategory>; readonly actions: ReadonlyArray<ICategory>;
getRecommendedScripts(): ReadonlyArray<IScript>; getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>;
findCategory(categoryId: number): ICategory | undefined; findCategory(categoryId: number): ICategory | undefined;
findScript(scriptId: string): IScript | undefined; findScript(scriptId: string): IScript | undefined;
getAllScripts(): ReadonlyArray<IScript>; getAllScripts(): ReadonlyArray<IScript>;

View File

@@ -7,6 +7,8 @@ export interface ICategory extends IEntity<number>, IDocumentable {
readonly name: string; readonly name: string;
readonly subCategories?: ReadonlyArray<ICategory>; readonly subCategories?: ReadonlyArray<ICategory>;
readonly scripts?: ReadonlyArray<IScript>; readonly scripts?: ReadonlyArray<IScript>;
includes(script: IScript): boolean;
getAllScriptsRecursively(): ReadonlyArray<IScript>;
} }
export { IEntity } from '../infrastructure/Entity/IEntity'; export { IEntity } from '../infrastructure/Entity/IEntity';

View File

@@ -0,0 +1,11 @@
import { OperatingSystem } from './OperatingSystem';
export interface IProjectInformation {
readonly name: string;
readonly version: string;
readonly repositoryUrl: string;
readonly homepage: string;
readonly feedbackUrl: string;
readonly releaseUrl: string;
readonly repositoryWebUrl: string;
getDownloadUrl(os: OperatingSystem): string;
}

View File

@@ -1,11 +1,12 @@
import { IEntity } from '../infrastructure/Entity/IEntity'; import { IEntity } from '../infrastructure/Entity/IEntity';
import { IDocumentable } from './IDocumentable'; import { IDocumentable } from './IDocumentable';
import { RecommendationLevel } from './RecommendationLevel';
import { IScriptCode } from './IScriptCode';
export interface IScript extends IEntity<string>, IDocumentable { export interface IScript extends IEntity<string>, IDocumentable {
readonly name: string; readonly name: string;
readonly isRecommended: boolean; readonly level?: RecommendationLevel;
readonly documentationUrls: ReadonlyArray<string>; readonly documentationUrls: ReadonlyArray<string>;
readonly code: string; readonly code: IScriptCode;
readonly revertCode: string;
canRevert(): boolean; canRevert(): boolean;
} }

View File

@@ -0,0 +1,4 @@
export interface IScriptCode {
readonly execute: string;
readonly revert: string;
}

View File

@@ -0,0 +1,14 @@
export enum OperatingSystem {
macOS,
Windows,
Linux,
KaiOS,
ChromeOS,
BlackBerryOS,
BlackBerry,
BlackBerryTabletOS,
Android,
iOS,
WindowsPhone,
Unknown,
}

View File

@@ -0,0 +1,55 @@
import { IProjectInformation } from './IProjectInformation';
import { OperatingSystem } from './OperatingSystem';
export class ProjectInformation implements IProjectInformation {
public readonly repositoryWebUrl: string;
constructor(
public readonly name: string,
public readonly version: string,
public readonly repositoryUrl: string,
public readonly homepage: string,
) {
if (!name) {
throw new Error('name is undefined');
}
if (!version || +version <= 0) {
throw new Error('version should be higher than zero');
}
if (!repositoryUrl) {
throw new Error('repositoryUrl is undefined');
}
if (!homepage) {
throw new Error('homepage is undefined');
}
this.repositoryWebUrl = getWebUrl(this.repositoryUrl);
}
public getDownloadUrl(os: OperatingSystem): string {
return `${this.repositoryWebUrl}/releases/download/${this.version}/${getFileName(os, this.version)}`;
}
public get feedbackUrl(): string {
return `${this.repositoryWebUrl}/issues`;
}
public get releaseUrl(): string {
return `${this.repositoryWebUrl}/releases/tag/${this.version}`;
}
}
function getWebUrl(gitUrl: string) {
if (gitUrl.endsWith('.git')) {
return gitUrl.substring(0, gitUrl.length - 4);
}
return gitUrl;
}
function getFileName(os: OperatingSystem, version: string): string {
switch (os) {
case OperatingSystem.Linux:
return `privacy.sexy-${version}.AppImage`;
case OperatingSystem.macOS:
return `privacy.sexy-${version}.dmg`;
case OperatingSystem.Windows:
return `privacy.sexy-Setup-${version}.exe`;
default:
throw new Error(`Unsupported os: ${OperatingSystem[os]}`);
}
}

View File

@@ -0,0 +1,11 @@
export enum RecommendationLevel {
Standard = 0,
Strict = 1,
}
export const RecommendationLevelNames = Object
.values(RecommendationLevel)
.filter((level) => typeof level === 'string') as string[];
export const RecommendationLevels = RecommendationLevelNames
.map((level) => RecommendationLevel[level]) as RecommendationLevel[];

View File

@@ -1,57 +1,27 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity'; import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import { IScript } from './IScript'; import { IScript } from './IScript';
import { RecommendationLevel } from './RecommendationLevel';
import { IScriptCode } from './IScriptCode';
export class Script extends BaseEntity<string> implements IScript { export class Script extends BaseEntity<string> implements IScript {
constructor( constructor(
public readonly name: string, public readonly name: string,
public readonly code: string, public readonly code: IScriptCode,
public readonly revertCode: string,
public readonly documentationUrls: ReadonlyArray<string>, public readonly documentationUrls: ReadonlyArray<string>,
public readonly isRecommended: boolean) { public readonly level?: RecommendationLevel) {
super(name); super(name);
validateCode(name, code); if (!code) {
if (revertCode) { throw new Error(`undefined code (script: ${name})`);
validateCode(name, revertCode);
if (code === revertCode) {
throw new Error(`${name}: Code itself and its reverting code cannot be the same`);
}
} }
validateLevel(level);
} }
public canRevert(): boolean { public canRevert(): boolean {
return Boolean(this.revertCode); return Boolean(this.code.revert);
} }
} }
function validateCode(name: string, code: string): void { function validateLevel(level?: RecommendationLevel) {
if (!code || code.length === 0) { if (level !== undefined && !(level in RecommendationLevel)) {
throw new Error(`Code of ${name} is empty or null`); throw new Error(`invalid level: ${level}`);
}
ensureCodeHasUniqueLines(name, code);
ensureNoEmptyLines(name, code);
}
function ensureNoEmptyLines(name: string, code: string): void {
if (code.split('\n').some((line) => line.trim().length === 0)) {
throw Error(`Script has empty lines "${name}"`);
}
}
function mayBeUniqueLine(codeLine: string): boolean {
const trimmed = codeLine.trim();
if (trimmed === ')' || trimmed === '(') { // "(" and ")" are used often in batch code
return false;
}
return true;
}
function ensureCodeHasUniqueLines(name: string, code: string): void {
const lines = code.split('\n')
.filter((line) => mayBeUniqueLine(line));
if (lines.length === 0) {
return;
}
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
if (duplicateLines.length !== 0) {
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
} }
} }

57
src/domain/ScriptCode.ts Normal file
View File

@@ -0,0 +1,57 @@
import { IScriptCode } from './IScriptCode';
export class ScriptCode implements IScriptCode {
constructor(
scriptName: string,
public readonly execute: string,
public readonly revert: string) {
if (!scriptName) {
throw new Error('script name is undefined');
}
validateCode(scriptName, execute);
if (revert) {
scriptName = `${scriptName} (revert)`;
validateCode(scriptName, revert);
if (execute === revert) {
throw new Error(`${scriptName}: Code itself and its reverting code cannot be the same`);
}
}
}
}
function validateCode(name: string, code: string): void {
if (!code || code.length === 0) {
throw new Error(`code of ${name} is empty or undefined`);
}
ensureCodeHasUniqueLines(name, code);
ensureNoEmptyLines(name, code);
}
function ensureNoEmptyLines(name: string, code: string): void {
if (code.split('\n').some((line) => line.trim().length === 0)) {
throw Error(`Script has empty lines "${name}"`);
}
}
function ensureCodeHasUniqueLines(name: string, code: string): void {
const lines = code.split('\n')
.filter((line) => mayBeUniqueLine(line));
if (lines.length === 0) {
return;
}
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
if (duplicateLines.length !== 0) {
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
}
}
function mayBeUniqueLine(codeLine: string): boolean {
const trimmed = codeLine.trim();
if (trimmed === ')' || trimmed === '(') { // "(" and ")" are used often in batch code
return false;
}
if (codeLine.startsWith(':: ') || codeLine.startsWith('REM ')) { // Is comment?
return false;
}
return true;
}

View File

@@ -3,6 +3,7 @@ import { IEntity } from '../Entity/IEntity';
export interface IRepository<TKey, TEntity extends IEntity<TKey>> { export interface IRepository<TKey, TEntity extends IEntity<TKey>> {
readonly length: number; readonly length: number;
getItems(predicate?: (entity: TEntity) => boolean): TEntity[]; getItems(predicate?: (entity: TEntity) => boolean): TEntity[];
getById(id: TKey): TEntity | undefined;
addItem(item: TEntity): void; addItem(item: TEntity): void;
addOrUpdateItem(item: TEntity): void; addOrUpdateItem(item: TEntity): void;
removeItem(id: TKey): void; removeItem(id: TKey): void;

View File

@@ -16,6 +16,14 @@ export class InMemoryRepository<TKey, TEntity extends IEntity<TKey>> implements
return predicate ? this.items.filter(predicate) : this.items; return predicate ? this.items.filter(predicate) : this.items;
} }
public getById(id: TKey): TEntity | undefined {
const items = this.getItems((entity) => entity.id === id);
if (!items.length) {
return undefined;
}
return items[0];
}
public addItem(item: TEntity): void { public addItem(item: TEntity): void {
if (!item) { if (!item) {
throw new Error('item is null or undefined'); throw new Error('item is null or undefined');

View File

@@ -1,9 +1,18 @@
import fileSaver from 'file-saver'; import fileSaver from 'file-saver';
export enum FileType {
BatchFile,
}
export class SaveFileDialog { export class SaveFileDialog {
public static saveText(text: string, fileName: string): void { public static saveFile(text: string, fileName: string, type: FileType): void {
this.saveBlob(text, 'text/plain;charset=utf-8', fileName); const mimeType = this.mimeTypes.get(type);
this.saveBlob(text, mimeType, fileName);
} }
private static readonly mimeTypes = new Map<FileType, string>([
// Some browsers (including firefox + IE) require right mime type
// otherwise they ignore extension and save the file as text.
[ FileType.BatchFile, 'application/bat' ], // https://en.wikipedia.org/wiki/Batch_file
]);
private static saveBlob(file: BlobPart, fileType: string, fileName: string): void { private static saveBlob(file: BlobPart, fileType: string, fileName: string): void {
try { try {

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'; import Vue from 'vue';
import App from './App.vue'; import App from './App.vue';
import { ApplicationBootstrapper } from './presentation/Bootstrapping/ApplicationBootstrapper'; import { ApplicationBootstrapper } from './presentation/Bootstrapping/ApplicationBootstrapper';
import 'core-js/fn/array/flat-map'; // Here until Vue 3 & CLI v4 https://github.com/vuejs/vue-cli/issues/3834
new ApplicationBootstrapper() new ApplicationBootstrapper()
.bootstrap(Vue); .bootstrap(Vue);

View File

@@ -4,21 +4,28 @@ import { faGithub } from '@fortawesome/free-brands-svg-icons';
/** BRAND ICONS (PREFIX: fab) */ /** BRAND ICONS (PREFIX: fab) */
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
/** REGULAR ICONS (PREFIX: far) */ /** REGULAR ICONS (PREFIX: far) */
import { faFolderOpen, faFolder } from '@fortawesome/free-regular-svg-icons'; import { faFolderOpen, faFolder, faSmile } from '@fortawesome/free-regular-svg-icons';
/** SOLID ICONS (PREFIX: fas (default)) */ /** SOLID ICONS (PREFIX: fas (default)) */
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle } from '@fortawesome/free-solid-svg-icons'; import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop,
faTag, faGlobe, faSave, faBatteryFull, faBatteryHalf } from '@fortawesome/free-solid-svg-icons';
export class IconBootstrapper implements IVueBootstrapper { export class IconBootstrapper implements IVueBootstrapper {
public bootstrap(vue: VueConstructor): void { public bootstrap(vue: VueConstructor): void {
library.add( library.add(
faGithub, faGithub,
faUserSecret,
faSmile,
faDesktop,
faGlobe,
faTag,
faFolderOpen, faFolderOpen,
faFolder, faFolder,
faTimes, faTimes,
faFileDownload, faFileDownload, faSave,
faCopy, faCopy,
faSearch, faSearch,
faBatteryFull, faBatteryHalf,
faInfoCircle); faInfoCircle);
vue.component('font-awesome-icon', FontAwesomeIcon); vue.component('font-awesome-icon', FontAwesomeIcon);
} }

View File

@@ -8,10 +8,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Emit } from 'vue-property-decorator'; import { Component, Prop, Emit } from 'vue-property-decorator';
import { StatefulVue } from './StatefulVue'; import { StatefulVue } from './StatefulVue';
import { SaveFileDialog } from './../infrastructure/SaveFileDialog';
import { Clipboard } from './../infrastructure/Clipboard';
@Component @Component
export default class IconButton extends StatefulVue { export default class IconButton extends StatefulVue {

View File

@@ -16,7 +16,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'; import { Component } from 'vue-property-decorator';
import CardListItem from './CardListItem.vue'; import CardListItem from './CardListItem.vue';
import { StatefulVue } from '@/presentation/StatefulVue'; import { StatefulVue } from '@/presentation/StatefulVue';
import { ICategory } from '@/domain/ICategory'; import { ICategory } from '@/domain/ICategory';

View File

@@ -8,9 +8,17 @@
}" }"
ref="cardElement"> ref="cardElement">
<div class="card__inner"> <div class="card__inner">
<span v-if="cardTitle && cardTitle.length > 0">{{cardTitle}}</span> <span v-if="cardTitle && cardTitle.length > 0">
<span>{{cardTitle}}</span>
</span>
<span v-else>Oh no 😢</span> <span v-else>Oh no 😢</span>
<!-- Expand icon -->
<font-awesome-icon :icon="['far', isExpanded ? 'folder-open' : 'folder']" class="card__inner__expand-icon" /> <font-awesome-icon :icon="['far', isExpanded ? 'folder-open' : 'folder']" class="card__inner__expand-icon" />
<!-- Indeterminate and full states -->
<div class="card__inner__state-icons">
<font-awesome-icon v-if="isAnyChildSelected && !areAllChildrenSelected" :icon="['fa', 'battery-half']" />
<font-awesome-icon v-if="areAllChildrenSelected" :icon="['fa', 'battery-full']" />
</div>
</div> </div>
<div class="card__expander" v-on:click.stop> <div class="card__expander" v-on:click.stop>
<div class="card__expander__content"> <div class="card__expander__content">
@@ -24,9 +32,11 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator'; import { Component, Prop, Watch, Emit } from 'vue-property-decorator';
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue'; import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
import { StatefulVue } from '@/presentation/StatefulVue'; import { StatefulVue } from '@/presentation/StatefulVue';
import { ICategory } from '@/domain/ICategory';
import { IUserSelection } from '@/application/State/IApplicationState';
@Component({ @Component({
components: { components: {
@@ -36,17 +46,21 @@ import { StatefulVue } from '@/presentation/StatefulVue';
export default class CardListItem extends StatefulVue { export default class CardListItem extends StatefulVue {
@Prop() public categoryId!: number; @Prop() public categoryId!: number;
@Prop() public activeCategoryId!: number; @Prop() public activeCategoryId!: number;
public cardTitle?: string = ''; public cardTitle = '';
public isExpanded: boolean = false; public isExpanded = false;
public isAnyChildSelected = false;
public areAllChildrenSelected = false;
@Emit('selected') @Emit('selected')
public onSelected(isExpanded: boolean) { public onSelected(isExpanded: boolean) {
this.isExpanded = isExpanded; this.isExpanded = isExpanded;
} }
@Watch('activeCategoryId') @Watch('activeCategoryId')
public async onActiveCategoryChanged(value: |number) { public async onActiveCategoryChanged(value: |number) {
this.isExpanded = value === this.categoryId; this.isExpanded = value === this.categoryId;
} }
@Watch('isExpanded') @Watch('isExpanded')
public async onExpansionChangedAsync(newValue: number, oldValue: number) { public async onExpansionChangedAsync(newValue: number, oldValue: number) {
if (!oldValue && newValue) { if (!oldValue && newValue) {
@@ -57,28 +71,28 @@ export default class CardListItem extends StatefulVue {
} }
public async mounted() { public async mounted() {
this.cardTitle = this.categoryId ? await this.getCardTitleAsync(this.categoryId) : undefined; const state = await this.getCurrentStateAsync();
state.selection.changed.on(() => {
this.updateStateAsync(this.categoryId);
});
this.updateStateAsync(this.categoryId);
} }
@Watch('categoryId') @Watch('categoryId')
public async onCategoryIdChanged(value: |number) { public async updateStateAsync(value: |number) {
this.cardTitle = value ? await this.getCardTitleAsync(value) : undefined; const state = await this.getCurrentStateAsync();
} const category = !value ? undefined : state.app.findCategory(this.categoryId);
this.cardTitle = category ? category.name : undefined;
private async getCardTitleAsync(categoryId: number): Promise<string | undefined> { this.isAnyChildSelected = category ? state.selection.isAnySelected(category) : false;
const state = await this.getCurrentStateAsync(); this.areAllChildrenSelected = category ? state.selection.areAllSelected(category) : false;
const category = state.app.findCategory(this.categoryId);
return category ? category.name : undefined;
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/presentation/styles/colors.scss"; @import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/media.scss";
$big-screen-width: 991px;
$medium-screen-width: 767px;
$small-screen-width: 380px;
$card-padding: 30px; $card-padding: 30px;
$card-margin: 15px; $card-margin: 15px;
@@ -96,7 +110,7 @@ $expanded-margin-top: 30px;
@media screen and (max-width: $small-screen-width) { width: 90%; } @media screen and (max-width: $small-screen-width) { width: 90%; }
&__inner { &__inner {
padding: $card-padding; padding: $card-padding $card-padding 0 $card-padding;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
background-color: $gray; background-color: $gray;
@@ -118,13 +132,21 @@ $expanded-margin-top: 30px;
&:after { &:after {
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
} }
&__state-icons {
height: $card-padding;
margin-right: -$card-padding;
padding-right: 10px;
display: flex;
justify-content: flex-end;
}
&__expand-icon { &__expand-icon {
width: 100%; width: 100%;
margin-top: .25em; margin-top: .25em;
vertical-align: middle;
} }
} }
&__expander { &__expander {
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
position: relative; position: relative;

View File

@@ -15,9 +15,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'; import { Component } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue'; import { StatefulVue } from '@/presentation/StatefulVue';
import { IApplicationState } from '@/application/State/IApplicationState';
import { Grouping } from './Grouping'; import { Grouping } from './Grouping';
const DefaultGrouping = Grouping.Cards; const DefaultGrouping = Grouping.Cards;

View File

@@ -1,6 +1,6 @@
import { IApplication } from './../../../domain/IApplication'; import { IApplication } from './../../../domain/IApplication';
import { ICategory, IScript } from '@/domain/ICategory'; import { ICategory, IScript } from '@/domain/ICategory';
import { INode } from './SelectableTree/INode'; import { INode, NodeType } from './SelectableTree/Node/INode';
export function parseAllCategories(app: IApplication): INode[] | undefined { export function parseAllCategories(app: IApplication): INode[] | undefined {
const nodes = new Array<INode>(); const nodes = new Array<INode>();
@@ -23,26 +23,45 @@ export function parseSingleCategory(categoryId: number, app: IApplication): INod
export function getScriptNodeId(script: IScript): string { export function getScriptNodeId(script: IScript): string {
return script.id; return script.id;
} }
export function getScriptId(nodeId: string): string {
return nodeId;
}
export function getCategoryId(nodeId: string): number {
return +nodeId;
}
export function getCategoryNodeId(category: ICategory): string { export function getCategoryNodeId(category: ICategory): string {
return `Category${category.id}`; return `${category.id}`;
} }
function parseCategoryRecursively( function parseCategoryRecursively(
parentCategory: ICategory): INode[] { parentCategory: ICategory): INode[] {
if (!parentCategory) { throw new Error('parentCategory is undefined'); } if (!parentCategory) {
throw new Error('parentCategory is undefined');
const nodes = new Array<INode>();
if (parentCategory.subCategories && parentCategory.subCategories.length > 0) {
for (const subCategory of parentCategory.subCategories) {
const subCategoryNodes = parseCategoryRecursively(subCategory);
nodes.push(convertCategoryToNode(subCategory, subCategoryNodes));
}
} }
if (parentCategory.scripts && parentCategory.scripts.length > 0) { let nodes = new Array<INode>();
for (const script of parentCategory.scripts) { nodes = addCategories(parentCategory.subCategories, nodes);
nodes.push(convertScriptToNode(script)); nodes = addScripts(parentCategory.scripts, nodes);
} return nodes;
}
function addScripts(scripts: ReadonlyArray<IScript>, nodes: INode[]): INode[] {
if (!scripts || scripts.length === 0) {
return nodes;
}
for (const script of scripts) {
nodes.push(convertScriptToNode(script));
}
return nodes;
}
function addCategories(categories: ReadonlyArray<ICategory>, nodes: INode[]): INode[] {
if (!categories || categories.length === 0) {
return nodes;
}
for (const category of categories) {
const subCategoryNodes = parseCategoryRecursively(category);
nodes.push(convertCategoryToNode(category, subCategoryNodes));
} }
return nodes; return nodes;
} }
@@ -51,16 +70,18 @@ function convertCategoryToNode(
category: ICategory, children: readonly INode[]): INode { category: ICategory, children: readonly INode[]): INode {
return { return {
id: getCategoryNodeId(category), id: getCategoryNodeId(category),
type: NodeType.Category,
text: category.name, text: category.name,
children, children,
documentationUrls: category.documentationUrls, documentationUrls: category.documentationUrls,
isReversible: false, isReversible: children && children.every((child) => child.isReversible),
}; };
} }
function convertScriptToNode(script: IScript): INode { function convertScriptToNode(script: IScript): INode {
return { return {
id: getScriptNodeId(script), id: getScriptNodeId(script),
type: NodeType.Script,
text: script.name, text: script.name,
children: undefined, children: undefined,
documentationUrls: script.documentationUrls, documentationUrls: script.documentationUrls,

View File

@@ -7,7 +7,6 @@
:filterPredicate="filterPredicate" :filterPredicate="filterPredicate"
:filterText="filterText" :filterText="filterText"
v-on:nodeSelected="toggleNodeSelectionAsync($event)" v-on:nodeSelected="toggleNodeSelectionAsync($event)"
v-on:nodeRevertToggled="handleNodeRevertToggleAsync($event)"
> >
</SelectableTree> </SelectableTree>
</span> </span>
@@ -16,18 +15,17 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; import { Component, Prop, Watch } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue'; import { StatefulVue } from '@/presentation/StatefulVue';
import { Category } from '@/domain/Category';
import { IRepository } from '@/infrastructure/Repository/IRepository';
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory'; import { ICategory } from '@/domain/ICategory';
import { IApplicationState, IUserSelection } from '@/application/State/IApplicationState'; import { IApplicationState } from '@/application/State/IApplicationState';
import { IFilterResult } from '@/application/State/Filter/IFilterResult'; import { IFilterResult } from '@/application/State/Filter/IFilterResult';
import { parseAllCategories, parseSingleCategory, getScriptNodeId, getCategoryNodeId } from './ScriptNodeParser'; import { parseAllCategories, parseSingleCategory, getScriptNodeId, getCategoryNodeId, getCategoryId, getScriptId } from './ScriptNodeParser';
import SelectableTree, { FilterPredicate } from './SelectableTree/SelectableTree.vue'; import SelectableTree from './SelectableTree/SelectableTree.vue';
import { INode } from './SelectableTree/INode'; import { INode, NodeType } from './SelectableTree/Node/INode';
import { SelectedScript } from '@/application/State/Selection/SelectedScript'; import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { INodeSelectedEvent } from './SelectableTree/INodeSelectedEvent';
@Component({ @Component({
components: { components: {
@@ -51,17 +49,20 @@
state.filter.filtered.on(this.handleFiltered); state.filter.filtered.on(this.handleFiltered);
// Update initial state // Update initial state
await this.initializeNodesAsync(this.categoryId); await this.initializeNodesAsync(this.categoryId);
await this.initializeFilter(state.filter.currentFilter);
} }
public async toggleNodeSelectionAsync(node: INode) { public async toggleNodeSelectionAsync(event: INodeSelectedEvent) {
if (node.children != null && node.children.length > 0) {
return; // only interested in script nodes
}
const state = await this.getCurrentStateAsync(); const state = await this.getCurrentStateAsync();
if (!this.selectedNodeIds.some((id) => id === node.id)) { switch (event.node.type) {
state.selection.addSelectedScript(node.id, false); case NodeType.Category:
} else { toggleCategoryNodeSelection(event, state);
state.selection.removeSelectedScript(node.id); break;
case NodeType.Script:
toggleScriptNodeSelection(event, state);
break;
default:
throw new Error(`Unknown node type: ${event.node.id}`);
} }
} }
@@ -84,6 +85,14 @@
(category: ICategory) => node.id === getCategoryNodeId(category)); (category: ICategory) => node.id === getCategoryNodeId(category));
} }
private initializeFilter(currentFilter: IFilterResult | undefined) {
if (!currentFilter) {
this.handleFilterRemoved();
} else {
this.handleFiltered(currentFilter);
}
}
private handleSelectionChanged(selectedScripts: ReadonlyArray<SelectedScript>): void { private handleSelectionChanged(selectedScripts: ReadonlyArray<SelectedScript>): void {
this.selectedNodeIds = selectedScripts this.selectedNodeIds = selectedScripts
.map((node) => node.id); .map((node) => node.id);
@@ -99,6 +108,24 @@
} }
} }
function toggleCategoryNodeSelection(event: INodeSelectedEvent, state: IApplicationState): void {
const categoryId = getCategoryId(event.node.id);
if (event.isSelected) {
state.selection.addOrUpdateAllInCategory(categoryId, false);
} else {
state.selection.removeAllInCategory(categoryId);
}
}
function toggleScriptNodeSelection(event: INodeSelectedEvent, state: IApplicationState): void {
const scriptId = getScriptId(event.node.id);
const actualToggleState = state.selection.isSelected(scriptId);
const targetToggleState = event.isSelected;
if (targetToggleState && !actualToggleState) {
state.selection.addSelectedScript(scriptId, false);
} else if (!targetToggleState && actualToggleState) {
state.selection.removeSelectedScript(scriptId);
}
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -0,0 +1,6 @@
import { INode } from './Node/INode';
export interface INodeSelectedEvent {
isSelected: boolean;
node: INode;
}

View File

@@ -1,4 +1,3 @@
// Two ways of typing other libraries: https://stackoverflow.com/a/53070501
declare module 'liquor-tree' { declare module 'liquor-tree' {
import { PluginObject } from 'vue'; import { PluginObject } from 'vue';
@@ -10,33 +9,44 @@ declare module 'liquor-tree' {
filter(query: string): void; filter(query: string): void;
clearFilter(): void; clearFilter(): void;
setModel(nodes: ReadonlyArray<ILiquorTreeNewNode>): void; setModel(nodes: ReadonlyArray<ILiquorTreeNewNode>): void;
// getNodeById(id: string): ILiquorTreeExistingNode;
recurseDown(fn: (node: ILiquorTreeExistingNode) => void): void;
} }
interface ICustomLiquorTreeData { export interface ICustomLiquorTreeData {
type: number;
documentationUrls: ReadonlyArray<string>; documentationUrls: ReadonlyArray<string>;
isReversible: boolean; isReversible: boolean;
} }
// https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
export interface ILiquorTreeNodeState {
checked: boolean;
indeterminate: boolean;
}
export interface ILiquorTreeNode {
id: string;
data: ICustomLiquorTreeData;
children: ReadonlyArray<ILiquorTreeNode> | undefined;
}
/** /**
* Returned from Node tree view events. * Returned from Node tree view events.
* See constructor in https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js * See constructor in https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
*/ */
export interface ILiquorTreeExistingNode { export interface ILiquorTreeExistingNode extends ILiquorTreeNode {
id: string;
data: ILiquorTreeNodeData; data: ILiquorTreeNodeData;
states: ILiquorTreeNodeState | undefined; states: ILiquorTreeNodeState | undefined;
children: ReadonlyArray<ILiquorTreeExistingNode> | undefined; children: ReadonlyArray<ILiquorTreeExistingNode> | undefined;
// expand(): void;
} }
/** /**
* Sent to liquor tree to define of new nodes. * Sent to liquor tree to define of new nodes.
* https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js * https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
*/ */
export interface ILiquorTreeNewNode { export interface ILiquorTreeNewNode extends ILiquorTreeNode {
id: string;
text: string; text: string;
state: ILiquorTreeNodeState | undefined; state: ILiquorTreeNodeState | undefined;
children: ReadonlyArray<ILiquorTreeNewNode> | undefined; children: ReadonlyArray<ILiquorTreeNewNode> | undefined;
data: ICustomLiquorTreeData;
} }
// https://amsik.github.io/liquor-tree/#Component-Options // https://amsik.github.io/liquor-tree/#Component-Options
@@ -47,29 +57,16 @@ declare module 'liquor-tree' {
autoCheckChildren: boolean; autoCheckChildren: boolean;
parentSelect: boolean; parentSelect: boolean;
keyboardNavigation: boolean; keyboardNavigation: boolean;
deletion: (node: ILiquorTreeExistingNode) => void;
filter: ILiquorTreeFilter; filter: ILiquorTreeFilter;
deletion(node: ILiquorTreeNode): boolean;
} }
// https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js export interface ILiquorTreeNodeData extends ICustomLiquorTreeData {
interface ILiquorTreeNodeState {
checked: boolean;
}
interface ILiquorTreeNodeData extends ICustomLiquorTreeData {
text: string; text: string;
} }
// https://github.com/amsik/liquor-tree/blob/master/src/components/TreeRoot.vue // https://github.com/amsik/liquor-tree/blob/master/src/components/TreeRoot.vue
interface ILiquorTreeOptions { export interface ILiquorTreeFilter {
checkbox: boolean;
checkOnSelect: boolean;
filter: ILiquorTreeFilter;
deletion(node: ILiquorTreeNewNode): boolean;
}
// https://github.com/amsik/liquor-tree/blob/master/src/components/TreeRoot.vue
interface ILiquorTreeFilter {
emptyText: string; emptyText: string;
matcher(query: string, node: ILiquorTreeExistingNode): boolean; matcher(query: string, node: ILiquorTreeExistingNode): boolean;
} }

View File

@@ -0,0 +1,23 @@
import { ILiquorTreeOptions, ILiquorTreeFilter, ILiquorTreeNode, ILiquorTreeExistingNode } from 'liquor-tree';
export class LiquorTreeOptions implements ILiquorTreeOptions {
public readonly multiple = true;
public readonly checkbox = true;
public readonly checkOnSelect = true;
/* For checkbox mode only. Children will have the same checked state as their parent.
⚠️ Setting this false, does not update indeterminate state of nodes.
This is false as it's handled manually to be able to batch select for performance + highlighting */
public readonly autoCheckChildren = false;
public readonly parentSelect = true;
public readonly keyboardNavigation = true;
public readonly filter = { // Wrap this in an arrow function as setting filter directly does not work JS APIs
emptyText: this.liquorTreeFilter.emptyText,
matcher: (query: string, node: ILiquorTreeExistingNode) => {
return this.liquorTreeFilter.matcher(query, node);
},
};
constructor(private readonly liquorTreeFilter: ILiquorTreeFilter) { }
public deletion(node: ILiquorTreeNode): boolean {
return false; // no op
}
}

View File

@@ -0,0 +1,17 @@
import { ILiquorTreeFilter, ILiquorTreeExistingNode } from 'liquor-tree';
import { convertExistingToNode } from './NodeTranslator';
import { INode } from './../../Node/INode';
export type FilterPredicate = (node: INode) => boolean;
export class NodePredicateFilter implements ILiquorTreeFilter {
public emptyText = ''; // Does not matter as a custom mesage is shown
constructor(private readonly filterPredicate: FilterPredicate) {
if (!filterPredicate) {
throw new Error('filterPredicate is undefined');
}
}
public matcher(query: string, node: ILiquorTreeExistingNode): boolean {
return this.filterPredicate(convertExistingToNode(node));
}
}

View File

@@ -0,0 +1,61 @@
import { ILiquorTreeNode, ILiquorTreeNodeState } from 'liquor-tree';
import { NodeType } from './../../Node/INode';
export function getNewState(
node: ILiquorTreeNode,
selectedNodeIds: ReadonlyArray<string>): ILiquorTreeNodeState {
const checked = getNewCheckedState(node, selectedNodeIds);
const indeterminate = !checked && getNewIndeterminateState(node, selectedNodeIds);
return {
checked, indeterminate,
};
}
function getNewIndeterminateState(
node: ILiquorTreeNode,
selectedNodeIds: ReadonlyArray<string>): boolean {
switch (node.data.type) {
case NodeType.Script:
return false;
case NodeType.Category:
return parseAllSubScriptIds(node).some((id) => selectedNodeIds.includes(id));
default:
throw new Error('Unknown node type');
}
}
function getNewCheckedState(
node: ILiquorTreeNode,
selectedNodeIds: ReadonlyArray<string>): boolean {
switch (node.data.type) {
case NodeType.Script:
return selectedNodeIds.some((id) => id === node.id);
case NodeType.Category:
return parseAllSubScriptIds(node).every((id) => selectedNodeIds.includes(id));
default:
throw new Error('Unknown node type');
}
}
function parseAllSubScriptIds(categoryNode: ILiquorTreeNode): ReadonlyArray<string> {
if (categoryNode.data.type !== NodeType.Category) {
throw new Error('Not a category node');
}
if (!categoryNode.children) {
return [];
}
return categoryNode
.children
.flatMap((child) => getNodeIds(child));
}
function getNodeIds(node: ILiquorTreeNode): ReadonlyArray<string> {
switch (node.data.type) {
case NodeType.Script:
return [ node.id ];
case NodeType.Category:
return parseAllSubScriptIds(node);
default:
throw new Error('Unknown node type');
}
}

View File

@@ -1,5 +1,5 @@
import { ILiquorTreeNewNode, ILiquorTreeExistingNode } from 'liquor-tree'; import { ILiquorTreeNewNode, ILiquorTreeExistingNode } from 'liquor-tree';
import { INode } from './INode'; import { INode } from './../../Node/INode';
// Functions to translate INode to LiqourTree models and vice versa for anti-corruption // Functions to translate INode to LiqourTree models and vice versa for anti-corruption
@@ -7,10 +7,10 @@ export function convertExistingToNode(liquorTreeNode: ILiquorTreeExistingNode):
if (!liquorTreeNode) { throw new Error('liquorTreeNode is undefined'); } if (!liquorTreeNode) { throw new Error('liquorTreeNode is undefined'); }
return { return {
id: liquorTreeNode.id, id: liquorTreeNode.id,
type: liquorTreeNode.data.type,
text: liquorTreeNode.data.text, text: liquorTreeNode.data.text,
// selected: liquorTreeNode.states && liquorTreeNode.states.checked, // selected: liquorTreeNode.states && liquorTreeNode.states.checked,
children: (!liquorTreeNode.children || liquorTreeNode.children.length === 0) children: convertChildren(liquorTreeNode.children, convertExistingToNode),
? [] : liquorTreeNode.children.map((childNode) => convertExistingToNode(childNode)),
documentationUrls: liquorTreeNode.data.documentationUrls, documentationUrls: liquorTreeNode.data.documentationUrls,
isReversible : liquorTreeNode.data.isReversible, isReversible : liquorTreeNode.data.isReversible,
}; };
@@ -23,12 +23,22 @@ export function toNewLiquorTreeNode(node: INode): ILiquorTreeNewNode {
text: node.text, text: node.text,
state: { state: {
checked: false, checked: false,
indeterminate: false,
}, },
children: (!node.children || node.children.length === 0) ? [] : children: convertChildren(node.children, toNewLiquorTreeNode),
node.children.map((childNode) => toNewLiquorTreeNode(childNode)),
data: { data: {
documentationUrls: node.documentationUrls, documentationUrls: node.documentationUrls,
isReversible: node.isReversible, isReversible: node.isReversible,
type: node.type,
}, },
}; };
} }
function convertChildren<TOldNode, TNewNode>(
oldChildren: readonly TOldNode[],
callback: (value: TOldNode) => TNewNode): TNewNode[] {
if (!oldChildren || oldChildren.length === 0) {
return [];
}
return oldChildren.map((childNode) => callback(childNode));
}

Some files were not shown because too many files have changed in this diff Show More