Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ada8d425c | ||
|
|
f03fc24098 | ||
|
|
756c736e21 | ||
|
|
e09db0f1bd | ||
|
|
c546a33eff | ||
|
|
da4be500da | ||
|
|
b404a91ada | ||
|
|
728584240c | ||
|
|
3b1a89ce86 | ||
|
|
c84a1bb74c | ||
|
|
bf7fb0732c | ||
|
|
dc30825232 | ||
|
|
40f5eb8334 | ||
|
|
fac72edd55 | ||
|
|
cdc32d1f12 | ||
|
|
8f4b34f8f1 | ||
|
|
86fde6d7dc | ||
|
|
2f06043559 | ||
|
|
fc9dd234e9 | ||
|
|
645c333787 | ||
|
|
efa05f42bc | ||
|
|
940febc3e8 | ||
|
|
3f62bb2d6e |
@@ -1,7 +1,11 @@
|
||||
root = true # Top-most EditorConfig file
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
|
||||
[*.{js,jsx,ts,tsx,vue,sh}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
max_line_length = 100
|
||||
@@ -9,3 +13,14 @@ max_line_length = 100
|
||||
[{Dockerfile}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.py]
|
||||
indent_size = 4 # PEP 8 (the official Python style guide) recommends using 4 spaces per indentation level
|
||||
indent_style = space
|
||||
max_line_length = 100
|
||||
|
||||
[*.ps1]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
@@ -9,14 +9,15 @@ module.exports = {
|
||||
es2022: true, // add globals and sets parserOptions.ecmaVersion to 2022
|
||||
},
|
||||
extends: [
|
||||
// Vue specific rules, eslint-plugin-vue
|
||||
// Vue specific base rules, `eslint-plugin-vue`
|
||||
'plugin:vue/vue3-recommended',
|
||||
|
||||
// Extends eslint-config-airbnb
|
||||
// Extends `eslint-config-airbnb`
|
||||
'@vue/eslint-config-airbnb-with-typescript',
|
||||
|
||||
// Extends @typescript-eslint/recommended
|
||||
// Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
||||
// - Sets base parser and plugin options.
|
||||
// - Includes `plugin:@typescript-eslint/recommended`. But incompatible with
|
||||
// `strict-type-checked` and `stylistic-type-checked`, see https://github.com/vuejs/eslint-config-typescript/issues/67.
|
||||
'@vue/typescript/recommended',
|
||||
],
|
||||
rules: {
|
||||
|
||||
@@ -5,71 +5,56 @@ labels: enhancement
|
||||
---
|
||||
|
||||
<!--
|
||||
Thank you for suggesting an script to make privacy better. 🤗
|
||||
Please fill in as much of the template below as you're able.
|
||||
You could alternatively send a PR directly (see CONTRIBUTING.md).
|
||||
Thank you for contributing to privacy.sexy! 🌟
|
||||
For guidance, see our script guidelines: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md.
|
||||
Consider submitting a PR for faster implementation: https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md#extend-scripts.
|
||||
-->
|
||||
|
||||
### OS
|
||||
### Operating system
|
||||
|
||||
<!--
|
||||
Which OS will the new script configure?
|
||||
One of the supported OSes: "Windows", "macOS" or "Linux".
|
||||
Specify the OS: Windows, macOS, or Linux.
|
||||
-->
|
||||
|
||||
### Name
|
||||
|
||||
<!--
|
||||
The name of the script.
|
||||
It should start with an imperative noun such as "disable", "turn off" , "clear"...
|
||||
E.g. "Disable webcam telemetry"
|
||||
Suggest a name for the script.
|
||||
Naming conventions: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#name.
|
||||
-->
|
||||
|
||||
### Script code
|
||||
### Code
|
||||
|
||||
<!--
|
||||
Code that will be executed when script is selected.
|
||||
Try to keep it as simple and backwards-compatible as possible.
|
||||
Allowed languages:
|
||||
- Windows: PowerShell (ps1) or batchfile
|
||||
- 💡 Prioritize the one that's simpler, batchfile if similar.
|
||||
- macOS: bash (sh)
|
||||
- Linux: bash (sh) or Python 3
|
||||
- 💡 Prioritize the one that's simpler, bash if similar.
|
||||
Provide or explain the code to execute when the script runs.
|
||||
Code guidelines: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#code.
|
||||
-->
|
||||
|
||||
### Revert code
|
||||
|
||||
<!--
|
||||
If applicable, add code that will revert the script code to its original (OS default) state.
|
||||
It may require additional time, but it's much appreciated by the community.
|
||||
Leave blank if the script is nonreversible (e.g. when clearing data without backup).
|
||||
Include code to revert changes to the default state.
|
||||
Leave blank for non-reversible scripts.
|
||||
-->
|
||||
|
||||
### Suggested category
|
||||
### Category
|
||||
|
||||
<!--
|
||||
If applicable, suggest one more multiple suitable parent category of script.
|
||||
A category is the item where the script will be presented under.
|
||||
Most likely there already is a category for the script, so check the existing categories.
|
||||
If you're unsure, leave blank and maintainer(s) will choose one.
|
||||
Suggest a category for the script.
|
||||
If unsure, leave blank for maintainers to decide.
|
||||
-->
|
||||
|
||||
### Suggested recommendation level
|
||||
### Recommendation level
|
||||
|
||||
<!--
|
||||
If applicable, suggest recommending the script or not recommending at all.
|
||||
A script should be only recommended if it'll be safe for your grandmother to run.
|
||||
So you have three options here:
|
||||
STANDARD: Non-breaking scripts that does not limit any functionality.
|
||||
STRICT: Scripts that can break certain functionality but not intrusive to common daily OS usage.
|
||||
NONE: Script is not recommended for newbies at all, only those who knows what's going on should select it.
|
||||
If you're unsure, leave blank and maintainer(s) will choose one.
|
||||
Suggest a recommendation level: STANDARD (non-breaking), STRICT (limits functionality), or NONE (for advanced users).
|
||||
If unsure, leave blank for maintainers to decide.
|
||||
-->
|
||||
|
||||
### Additional documentation/references
|
||||
### Documentation/References
|
||||
|
||||
<!--
|
||||
If applicable, refer to documentation that should show up on the script description.
|
||||
Sources (URLs) should be as high quality as possible e.g. vendor documentation is favored over user forums.
|
||||
Provide any relevant documentation or references.
|
||||
Prefer high-quality sources such as vendor documentation.
|
||||
Documentation guidelines: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#documentation.
|
||||
-->
|
||||
|
||||
4
.github/actions/setup-node/action.yml
vendored
4
.github/actions/setup-node/action.yml
vendored
@@ -3,6 +3,6 @@ runs:
|
||||
steps:
|
||||
-
|
||||
name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16.x
|
||||
node-version: 18.x
|
||||
|
||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -16,7 +16,8 @@
|
||||
// Scripting
|
||||
"timonwong.shellcheck", // Lints bash files.
|
||||
"ms-vscode.powershell", // Lints PowerShell files.
|
||||
"ms-python.python", // Lints Python files.
|
||||
"ms-python.python", // Python IntelliSense, debugging, and basic linting.
|
||||
"ms-python.pylint", // Lints Python files
|
||||
// Distribution
|
||||
"ms-azuretools.vscode-docker" // Adds Docker support.
|
||||
]
|
||||
|
||||
31
2
Normal file
31
2
Normal file
@@ -0,0 +1,31 @@
|
||||
Show error on AV removal on desktop $264, $304
|
||||
|
||||
This solves $264 where users do not get error messages when running
|
||||
script file fails due to antivirus intervention (it being blocking the
|
||||
script file as soon as privacy.sexy generates it to run it). Now if the
|
||||
desktop app users tries to save or run a script file and it afils due to
|
||||
antivirus removal, they'll get a special error message with guiding next
|
||||
steps.
|
||||
|
||||
- Add additional check to able to fail if the file writing fails. This
|
||||
includes trying to reading the written file back as suggested in $304.
|
||||
This successfully detects antivirus (Defender) intervation as read
|
||||
file operation triggers the antivirus scan that deletes the file.
|
||||
- Show directory and file path in error messages as suggested in $304.
|
||||
- Show an error message with more detailed information if an antivirus
|
||||
is detected.
|
||||
|
||||
# Please enter the commit message for your changes. Lines starting
|
||||
# with '#' will be ignored, and an empty message aborts the commit.
|
||||
#
|
||||
# Date: Tue Jan 16 16:23:08 2024 +0100
|
||||
#
|
||||
# On branch master
|
||||
# Your branch is ahead of 'origin/master' by 1 commit.
|
||||
# (use "git push" to publish your local commits)
|
||||
#
|
||||
# Changes to be committed:
|
||||
# modified: ../../application/CodeRunner/CodeRunner.ts
|
||||
# new file: NodeReliableFileWriter.ts
|
||||
# new file: ReliableFileWriter.ts
|
||||
#
|
||||
24
CHANGELOG.md
24
CHANGELOG.md
@@ -1,5 +1,29 @@
|
||||
# Changelog
|
||||
|
||||
## 0.12.9 (2023-12-16)
|
||||
|
||||
* win: improve docs and category of jump lists #146 | [40ae8a8](https://github.com/undergroundwires/privacy.sexy/commit/40ae8a8addaeb834ee26eabd330fda5cbb495324)
|
||||
* mac: improve clearing privacy permissions | [5a7d7d8](https://github.com/undergroundwires/privacy.sexy/commit/5a7d7d88ff2f3e8862b18c94d062f692ee4b690b)
|
||||
* win: fix logic for terminating processes | [807ae6a](https://github.com/undergroundwires/privacy.sexy/commit/807ae6a8f8ca724d781169f3ecb40f43ccd3fe10)
|
||||
* win: improve documentation for "Get Help" app #280 | [8f5d7ed](https://github.com/undergroundwires/privacy.sexy/commit/8f5d7ed3cfa57f66dded9b72374006c9b6df2ce9)
|
||||
* Centralize log file and refactor desktop logging | [08dbfea](https://github.com/undergroundwires/privacy.sexy/commit/08dbfead7ca7b55fe85f7dded01f2d4b88906c72)
|
||||
* win: fix revert and improve docs for SAM enum #255 | [25e23c8](https://github.com/undergroundwires/privacy.sexy/commit/25e23c89c3f86897d5661a24a774997c924d3b2d)
|
||||
* Improve security and reliability of macOS updates | [4765752](https://github.com/undergroundwires/privacy.sexy/commit/4765752ee3a36301b3d97317c570432424de8460)
|
||||
* win: fix Win 11 Windows Security app removal #195 | [daa6230](https://github.com/undergroundwires/privacy.sexy/commit/daa6230fc96f2cf7210bc8c165106c0d5544e5fb)
|
||||
* Improve security and privacy with strict meta tags | [ba5b29a](https://github.com/undergroundwires/privacy.sexy/commit/ba5b29a35dd7665aeea430aec4aaa8ff5ca811de)
|
||||
* win: document and discourage admin shares #249 | [e747ee5](https://github.com/undergroundwires/privacy.sexy/commit/e747ee5cbc7cf5f0fe28a87fe7d02457d777373e)
|
||||
* win: discourage XboxIdentityProvider #64, #79 #181 | [c72f9f5](https://github.com/undergroundwires/privacy.sexy/commit/c72f9f501680c1d880a0b560d02451a9e31063b4)
|
||||
* win: improve disabling update healing #272 | [47b4823](https://github.com/undergroundwires/privacy.sexy/commit/47b4823bc5e487188b12cbea67db2525260af497)
|
||||
* Fix tooltip overflow on smaller screens | [916c9d6](https://github.com/undergroundwires/privacy.sexy/commit/916c9d62d9fce27c3cd3feaf90c66df584d4f04a)
|
||||
* Fix touch state not being activated in iOS Safari | [a985127](https://github.com/undergroundwires/privacy.sexy/commit/a9851272ae14eb1b374767b0eed3eb68e6dd1560)
|
||||
* Fix tree view alignment and padding issues | [15134ea](https://github.com/undergroundwires/privacy.sexy/commit/15134ea04bc46e8cb13977d75b788f5ff71c800e)
|
||||
* win: improve disabling of Application Experience | [fe3de49](https://github.com/undergroundwires/privacy.sexy/commit/fe3de498c8a1394efd6517d436797a08f938bb57)
|
||||
* Fix OS switching not working on tree view UI | [3457fe1](https://github.com/undergroundwires/privacy.sexy/commit/3457fe18cf8193883f45b50ecbc9638c91ace2fb)
|
||||
* Fix touch-enabled Chromium highlight on tree nodes | [2063397](https://github.com/undergroundwires/privacy.sexy/commit/20633972e9b56bdc102357129e74df30a95cefa9)
|
||||
* win: add scripts to postpone auto-updates #272 | [e95b2ba](https://github.com/undergroundwires/privacy.sexy/commit/e95b2ba2179e40c0033a51b0087871dbfdc32d78)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.8...0.12.9)
|
||||
|
||||
## 0.12.8 (2023-11-27)
|
||||
|
||||
* Remove duplicated `index.html` file | [aab0f7e](https://github.com/undergroundwires/privacy.sexy/commit/aab0f7ea4680f377c610066bd0e99011eed8b506)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Contributing
|
||||
|
||||
Love your input! Contributing to this project should be as easy and transparent as possible, whether it's:
|
||||
Love your input ❤️! Contributing to this project should be as easy and transparent as possible, whether it's:
|
||||
|
||||
- reporting a bug,
|
||||
- discussing the current state of the code,
|
||||
@@ -16,7 +16,7 @@ Your pull requests are actively welcomed. We collaborate using [GitHub flow](htt
|
||||
|
||||
The steps:
|
||||
|
||||
1. Fork the repo and create your branch from master.
|
||||
1. Fork the repository and create your branch from `master`.
|
||||
2. If you've added code that requires testing, add tests. See [tests.md](./docs/tests.md).
|
||||
3. If you've done a major change, update the documentation. See [docs/](./docs/).
|
||||
4. Ensure the test suite passes. See [development.md | Testing](./docs/development.md#testing) for commands.
|
||||
@@ -37,16 +37,44 @@ Automated pipelines will run to control your PR and they will publish your code
|
||||
|
||||
## Extend scripts
|
||||
|
||||
Here's quick information for you who want to add more scripts.
|
||||
If you're interested in adding new scripts to privacy.sexy:
|
||||
|
||||
You have two alternatives:
|
||||
1. Read [guidelines for a good script](./docs/script-guidelines.md)
|
||||
2. Choose one of two ways to contribute:
|
||||
1. [Create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose) requesting the addition of a new script. This allows other contributors to develop and add it for you. This will take longer time.
|
||||
2. Submit a pull request with your script. This is the faster route to seeing your script included in the project. Add your scripts to the appropriate OS directory in the [collections](src/application/collections/) (for syntax guidance, see [collection-files.md](docs/collection-files.md)) folder, and follow the [pull request process](#pull-request-process).
|
||||
|
||||
1. [Create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose) and ask for someone else to add the script for you.
|
||||
2. Or send a PR yourself. This would make it faster to get your code into the project. You need to add scripts to related OS in [collections](src/application/collections/) folder. Then you'd sent a pull request, see [pull request process](#pull-request-process).
|
||||
- 💡 You should use existing shared functions for most of the operations, like `DisableService` for disabling services, to maintain code consistency and efficiency.
|
||||
- 📖 If you're unsure about the syntax, check [collection-files.md](docs/collection-files.md).
|
||||
- 📖 If you wish to use templates, use [templating.md](./docs/templating.md).
|
||||
## Commit conventions
|
||||
|
||||
- Adhere to the 50/72 rule:
|
||||
- Commit titles should not exceed 50 characters.
|
||||
- Limit description lines to 72 characters, except for code blocks or inline codes.
|
||||
- Avoid including delta (such as `git diff` information) or a list of changed files in the commit message. This information is redundant as it's already part of the commit.
|
||||
- Focus on explaining the WHY and HOW of the changes, rather than WHAT changes are.
|
||||
- Begin the commit message with a concise summary of what the commit accomplishes.
|
||||
- Use imperative language in the commit title. For example, use "add" instead of "added".
|
||||
- Commit prefixes:
|
||||
- Prefix bug fixes with `fix:` or `Fix ...`.
|
||||
- For commits affecting scripts of specific operating systems:
|
||||
- Prefix the commit title with an OS-specific tag such as `win:` for Windows scripts, `mac:` for macOS scripts, and `linux:` for Linux scripts.
|
||||
- Combine prefixes for commits affecting more than one operating system, e.g., `win, mac: ...`.
|
||||
|
||||
## Versioning
|
||||
|
||||
We base versioning on the release's content rather than strictly following semantic versioning.
|
||||
|
||||
There are two main types of releases:
|
||||
|
||||
1. **Patch Releases:** These focus on minor UI improvements, bug fixes, refactorings, dependency updates, and documentation updates. For scripts, they involve adjusting recommendation levels, enhancing functionality, and dividing scripts for more precise control. Patch releases may ship minor feature additions if they are essential for fixing a bug. For these updates, we increment the patch number in the `MAJOR.MINOR.PATCH`.
|
||||
|
||||
2. **Feature Releases:** These releases bring significant updates that change how users interact with privacy.sexy. They include major UI enhancements, the introduction of new scripts, and features. For these updates, we increment the minor number in the `MAJOR.MINOR.PATCH`.
|
||||
|
||||
Maintainers tag specific commits with a version number to trigger a release, and [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) automates the release process including updating version numbers throughout the project.
|
||||
|
||||
## Refactoring
|
||||
|
||||
Opportunistic refactoring is welcome. If you're adding a feature or fixing a bug, feel free to also clean up and optimize the related code. Your contributions should leave the code in a better state than when you found it.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your [GNU General Public License v3.0](./LICENSE) will be the license for your contributions.
|
||||
By contributing to this project, you agree that your contributions are licensed under the [GNU Affero General Public License](./LICENSE) as currently specified. Additionally, you expressly consent to the project maintainers having full authority to modify the licensing terms or relicense your contributions under different terms in the future.
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
## Get started
|
||||
|
||||
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
||||
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.8/privacy.sexy-Setup-0.12.8.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.8/privacy.sexy-0.12.8.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.8/privacy.sexy-0.12.8.AppImage). For more options, see [here](#additional-install-options).
|
||||
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.9/privacy.sexy-Setup-0.12.9.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.9/privacy.sexy-0.12.9.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.9/privacy.sexy-0.12.9.AppImage). For more options, see [here](#additional-install-options).
|
||||
|
||||
For a detailed comparison of features between the desktop and web versions of privacy.sexy, see [Desktop vs. Web Features](./docs/desktop-vs-web-features.md).
|
||||
|
||||
@@ -137,6 +137,7 @@ For a detailed comparison of features between the desktop and web versions of pr
|
||||
- **Transparent**. Have full visibility into what the tweaks do as you enable them.
|
||||
- **Reversible**. Revert if something feels wrong.
|
||||
- **Accessible**. No need to run any compiled software on your computer with web version.
|
||||
- **Secure**: Security is a top priority at privacy.sexy with [comprehensive safeguards](./SECURITY.md#security-practices) in place.
|
||||
- **Open**. What you see as code in this repository is what you get. The application itself, its infrastructure and deployments are open-source and automated thanks to [bump-everywhere](https://github.com/undergroundwires/bump-everywhere).
|
||||
- **Tested**. A lot of tests. Automated and manual. Community-testing and verification. Stability improvements comes before new features.
|
||||
- **Extensible**. Effortlessly [extend scripts](./CONTRIBUTING.md#extend-scripts) with a custom designed [templating language](./docs/templating.md).
|
||||
|
||||
17
SECURITY.md
17
SECURITY.md
@@ -31,9 +31,20 @@ privacy.sexy adopts a defense in depth strategy to protect users on multiple lay
|
||||
- **Content Security Policies (CSP):**
|
||||
privacy.sexy actively follows security guidelines from the Open Web Application Security Project (OWASP) at strictest level.
|
||||
This approach protects against attacks like Cross Site Scripting (XSS) and data injection.
|
||||
- **Context Isolation:**
|
||||
The desktop application isolates different code sections based on their access level.
|
||||
This separation prevents attackers from introducing harmful code into the app, known as injection attacks.
|
||||
- **Host System Access Control:**
|
||||
The desktop application segregates and isolates code sections based on their access levels through sandboxing.
|
||||
This provides a critical defense mechanism, prevents attackers from introducing harmful code into the app, known as injection attacks.
|
||||
- **Auditing and Transparency:**
|
||||
The desktop application improves security and transparency by logging application activities and retaining files of executed scripts
|
||||
This facilitates detailed auditability and effective troubleshooting, contributing to the integrity and reliability of the application.
|
||||
- **Privilege Management:**
|
||||
The desktop application operates without persistent administrative or `sudo` privileges, reinforcing its security posture. It requests
|
||||
elevation of privileges for system modifications with explicit user consent and logs every action taken with high privileges. This
|
||||
approach actively minimizes potential security risks by limiting privileged operations and aligning with the principle of least privilege.
|
||||
- **Secure Script Execution/Storage:**
|
||||
Before executing any script, the desktop application stores a copy to allow antivirus software to perform scans. This safeguards against
|
||||
any unwanted modifications. Furthermore, the application incorporates integrity checks for tamper protection. If the script file differs from
|
||||
the user's selected script, the application will not execute or save the script, ensuring the processing of authentic scripts.
|
||||
|
||||
### Update Security and Integrity
|
||||
|
||||
|
||||
@@ -27,13 +27,14 @@ Application uses highly decoupled models & services in different DDD layers:
|
||||
**Domain layer**:
|
||||
|
||||
- Serves as the system's core and central truth.
|
||||
- Facilitates communication between the application and presentation layers through the domain model.
|
||||
- It should be independent of other layers and encapsulate the core business concepts.
|
||||
|
||||
**Infrastructure layer**:
|
||||
|
||||
- Manages technical implementations without dependencies on other layers or domain knowledge.
|
||||
- Provides technical implementations.
|
||||
- Depends on the application and domain layers in terms of interfaces and contracts but should not include business logic.
|
||||
|
||||

|
||||

|
||||
|
||||
### Application state
|
||||
|
||||
|
||||
@@ -1,192 +1,164 @@
|
||||
# Collection files
|
||||
|
||||
- privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from yaml files in [`application/collections`](./../src/application/collections/)
|
||||
- 💡 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: [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts)
|
||||
privacy.sexy is a data-driven application that reads YAML files.
|
||||
This document details the structure and syntax of the YAML files located in [`application/collections`](./../src/application/collections/), which form the backbone of the application's data model.
|
||||
|
||||
Related documentation:
|
||||
|
||||
- 📖 [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts) outlines code types.
|
||||
- 📖 [Script Guidelines](./script-guidelines.md) provide guidance on script creation including best-practices.
|
||||
|
||||
## Objects
|
||||
|
||||
### `Collection`
|
||||
|
||||
- A collection simply defines:
|
||||
- different categories and their scripts in a tree structure
|
||||
- OS specific details
|
||||
- Also allows defining common [function](#function)s to be used throughout the collection if you'd like different scripts to share same code.
|
||||
- Defines categories, scripts, and OS-specific details in a tree structure.
|
||||
- Allows defining common [functions](#function) for code reuse.
|
||||
|
||||
#### `Collection` syntax
|
||||
|
||||
- `os:` *`string`* (**required**)
|
||||
- Operating system that the [Collection](#collection) is written for.
|
||||
- 📖 See [OperatingSystem.ts](./../src/domain/OperatingSystem.ts) enumeration for allowed values.
|
||||
- `os:` *`string`* **(required)**
|
||||
- Operating system for the collection.
|
||||
- 📖 See [`OperatingSystem.ts`](./../src/domain/OperatingSystem.ts) for possible values.
|
||||
- `actions: [` ***[`Category`](#category)*** `, ... ]` **(required)**
|
||||
- Each [category](#category) is rendered as different cards in card presentation.
|
||||
- Renders each parent category as cards in the user interface.
|
||||
- ❗ A [Collection](#collection) must consist of at least one category.
|
||||
- `functions: [` ***[`Function`](#function)*** `, ... ]`
|
||||
- Functions are optionally defined to re-use the same code throughout different scripts.
|
||||
- Optional for code reuse.
|
||||
- `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)**
|
||||
- Defines the scripting language that the code of other action uses.
|
||||
- Sets the scripting language for all inline code used within the collection.
|
||||
|
||||
### `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.
|
||||
Represents a logical group of scripts and subcategories.
|
||||
|
||||
#### `Category` syntax
|
||||
|
||||
- `category:` *`string`* (**required**)
|
||||
- Name of the category
|
||||
- ❗ Must be unique throughout the [Collection](#collection)
|
||||
- `children: [` ***[`Category`](#category)*** `|` [***`Script`***](#script) `, ... ]` (**required**)
|
||||
- `category:` *`string`* **(required)**
|
||||
- Name of the category.
|
||||
- ❗ Must be unique throughout the [collection](#collection).
|
||||
- `children: [` ***[`Category`](#category)*** `|` [***`Script`***](#script) `, ... ]` **(required)**
|
||||
- ❗ Category must consist of at least one subcategory or script.
|
||||
- Children can be combination of scripts and subcategories.
|
||||
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
||||
- Documentation pieces related to the category.
|
||||
- Rendered as markdown.
|
||||
- Markdown-formatted documentation related to the category.
|
||||
|
||||
### `Script`
|
||||
|
||||
- Script represents a single tweak.
|
||||
- A script can be of two different types (just like [functions](#function)):
|
||||
1. Inline script; a script with an inline code
|
||||
- Must define `code` property and optionally `revertCode` but not `call`
|
||||
2. Caller script; a script that calls other functions
|
||||
- Must define `call` property but not `code` or `revertCode`
|
||||
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
||||
Represents an individual tweak.
|
||||
|
||||
Types (like [functions](#function)):
|
||||
|
||||
1. Inline script:
|
||||
- Direct code.
|
||||
- ❗ Requires `code` and optional `revertCode`.
|
||||
2. Caller script:
|
||||
- Calls other [functions](#function).
|
||||
- ❗ Requires `call`, but not `code` or `revertCode`.
|
||||
|
||||
📖 For detailed guidelines, see [Script Guidelines](./script-guidelines.md).
|
||||
|
||||
#### `Script` syntax
|
||||
|
||||
- `name`: *`string`* (**required**)
|
||||
- Name of the script
|
||||
- ❗ Must be unique throughout the [Collection](#collection)
|
||||
- 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
|
||||
- `name`: *`string`* **(required)**
|
||||
- Script name.
|
||||
- ❗ Must be unique throughout the [Collection](#collection).
|
||||
- `code`: *`string`* **(conditionally required)**
|
||||
- Code to execute when the user selects the script.
|
||||
- 💡 If defined, it's best practice to also define `revertCode`.
|
||||
- ❗ Cannot co-exist with `call`, define either `code` with optional `revertCode` or `call`.
|
||||
- `revertCode`: *`string`*
|
||||
- Reverts changes made by `code`.
|
||||
- ❗ Cannot co-exist with `call`, define `revertCode` with `code` or `call`.
|
||||
- `call`: ***[`FunctionCall`](#functioncall)*** | `[` ***[`FunctionCall`](#functioncall)*** `, ... ]` **(conditionally required)**
|
||||
- A shared function or sequence of functions to call (called in order).
|
||||
- ❗ Cannot co-exist with `code` or `revertCode`, define `code` with optional `revertCode` or `call`.
|
||||
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
||||
- Documentation pieces related to the script.
|
||||
- Rendered as markdown.
|
||||
- `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
|
||||
- Markdown-formatted documentation related to the script.
|
||||
- `recommend`: *`"standard"`* | *`"strict"`* | *`undefined`* (default: `undefined`)
|
||||
- Sets recommendation level.
|
||||
- Application will not recommend the script if `undefined`.
|
||||
|
||||
📖 For detailed guidelines, see [Script Guidelines](./script-guidelines.md).
|
||||
|
||||
### `FunctionCall`
|
||||
|
||||
- Describes a single call to a function by optionally providing values to its parameters.
|
||||
- 👀 See [parameter substitution](./templating.md#parameter-substitution) for an example usage
|
||||
Specifies a function call. It may require providing argument values to its parameters.
|
||||
|
||||
#### `FunctionCall` syntax
|
||||
|
||||
- `function`: *`string`* (**required**)
|
||||
- `function`: *`string`* **(required)**
|
||||
- Name of the function to call.
|
||||
- ❗ Function with same name must defined in `functions` property of [Collection](#collection)
|
||||
- `parameters`: `[ parameterName:` *`parameterValue`*`, ... ]`
|
||||
- Defines key value dictionary for each parameter and its value
|
||||
- E.g.
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
userDefinedParameterName: parameterValue
|
||||
# ...
|
||||
appName: Microsoft.WindowsFeedbackHub
|
||||
```
|
||||
|
||||
- 💡 [Expressions (templating)](./templating.md#expressions) can be used as parameter value
|
||||
- ❗ Function with same name must defined in `functions` property of [Collection](#collection).
|
||||
- `parameters`: `[` *`parameterName: parameterValue`* `, ... ]`
|
||||
- Key-value pairs representing function parameters and their corresponding argument values.
|
||||
- 📖 See [parameter substitution](./templating.md#parameter-substitution) for an example usage.
|
||||
- 💡 You can use [expressions (templating)](./templating.md#expressions) when providing argument values for parameters.
|
||||
|
||||
### `Function`
|
||||
|
||||
- Functions allow re-usable code throughout the defined scripts.
|
||||
- Enables reusable code in scripts.
|
||||
- Functions are templates compiled by privacy.sexy and uses special expression expressions.
|
||||
- A function can be of two different types (just like [scripts](#script)):
|
||||
- A function can be of two different types (like [scripts](#script)):
|
||||
1. Inline function: a function with an inline code.
|
||||
- Must define `code` property and optionally `revertCode` but not `call`.
|
||||
- ❗ Requires `code` and optionally `revertCode`, but not `call`.
|
||||
2. Caller function: a function that calls other functions.
|
||||
- Must define `call` property but not `code` or `revertCode`.
|
||||
- 👀 Read more on [Templating](./templating.md) for function expressions and [example usages](./templating.md#parameter-substitution).
|
||||
- ❗ Requires `call`, but not `code` or `revertCode`.
|
||||
- 📖 Read about function expressions in [Templating](./templating.md) with [example usages](./templating.md#parameter-substitution).
|
||||
|
||||
#### `Function` syntax
|
||||
|
||||
- `name`: *`string`* (**required**)
|
||||
- `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`: `[` ***[`FunctionParameter`](#functionparameter)*** `, ... ]`
|
||||
- List of parameters that function code refers to.
|
||||
- ❗ Must be defined to be able use in [`FunctionCall`](#functioncall) or [expressions (templating)](./templating.md#expressions)
|
||||
`code`: *`string`* (**required** if `call` is undefined)
|
||||
- Batch file commands that will be executed
|
||||
- 💡 [Expressions (templating)](./templating.md#expressions) can be used in its value
|
||||
- 💡 If defined, best practice to also define `revertCode`
|
||||
- ❗ If not defined `call` must be defined
|
||||
- ❗ Function names must be unique.
|
||||
- ❗ Function names must follow camelCase and start with verbs (e.g., `uninstallStoreApp`).
|
||||
- `parameters`: `[` ***[`FunctionParameter`](#functionparameter)*** `, ... ]` **(conditionally required)**
|
||||
- Lists parameters used.
|
||||
- ❗ Required to be able use in [`FunctionCall`](#functioncall) or [expressions (templating)](./templating.md#expressions).
|
||||
- `code`: *`string`* **(conditionally required)**
|
||||
- Code to execute when the user selects the script.
|
||||
- 💡 You can use [expressions (templating)](./templating.md#expressions) in its value.
|
||||
- 💡 If defined, it's best practice to also define `revertCode`.
|
||||
- ❗ Cannot co-exist with `call`, define either `code` with optional `revertCode` or `call`.
|
||||
- `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`
|
||||
- 💡 [Expressions (templating)](./templating.md#expressions) can be used in code
|
||||
- `call`: ***[`FunctionCall`](#functioncall)*** | `[` ***[`FunctionCall`](#functioncall)*** `, ... ]` (may be **required**)
|
||||
- A shared function or sequence of functions to call (called in order)
|
||||
- The parameter values that are sent can use [expressions (templating)](./templating.md#expressions)
|
||||
- ❗ If not defined `code` must be defined
|
||||
- Reverts changes made by `code`.
|
||||
- 💡 You can use [expressions (templating)](./templating.md#expressions) in its value.
|
||||
- ❗ Cannot co-exist with `call`, define `revertCode` with `code` or `call`.
|
||||
- `call`: ***[`FunctionCall`](#functioncall)*** | `[` ***[`FunctionCall`](#functioncall)*** `, ... ]` **(conditionally required)**
|
||||
- A shared function or sequence of functions to call (called in order).
|
||||
- 💡 You can use [expressions (templating)](./templating.md#expressions) in argument values provided for parameters.
|
||||
- ❗ Cannot co-exist with `code` or `revertCode`, define `code` with optional `revertCode` or `call`.
|
||||
|
||||
### `FunctionParameter`
|
||||
|
||||
- Defines a parameter that function requires optionally or mandatory.
|
||||
- Its arguments are provided by a [Script](#script) through a [FunctionCall](#functioncall).
|
||||
- Defines a single parameter that may require an argument value optionally or mandatory.
|
||||
- A [`FunctionCall`](#functioncall) provides argument values by a caller.
|
||||
- A caller can be a [Script](#script) or [Function](#function).
|
||||
|
||||
#### `FunctionParameter` syntax
|
||||
|
||||
- `name`: *`string`* (**required**)
|
||||
- Name of the parameters that the function has.
|
||||
- Parameter names must be defined to be used in [expressions (templating)](./templating.md#expressions).
|
||||
- ❗ Parameter names must be unique and include alphanumeric characters only.
|
||||
- `name`: *`string`* **(required)**
|
||||
- Name of the parameter that the function has.
|
||||
- ❗ Required for [expressions (templating)](./templating.md#expressions).
|
||||
- ❗ Must be unique and consists of alphanumeric characters.
|
||||
- `optional`: *`boolean`* (default: `false`)
|
||||
- Specifies whether the caller [Script](#script) must provide any value for the parameter.
|
||||
- If set to `false` i.e. an argument value is not optional then it expects a non-empty value for the variable;
|
||||
- Otherwise it throws.
|
||||
- 💡 Set it to `true` if a parameter is used conditionally;
|
||||
- Indicates the caller must provide and argument value for the parameter.
|
||||
- 💡 If set to `false` i.e. an argument value is not optional then it expects a non-empty value for the variable.
|
||||
- E.g., in a [`with` expression](./templating.md#with).
|
||||
- 💡 Set it to `true` if you will use its argument value conditionally;
|
||||
- Or else set it to `false` for verbosity or do not define it as default value is `false` anyway.
|
||||
- 💡 Can be used in conjunction with [`with` expression](./templating.md#with).
|
||||
|
||||
### `ScriptingDefinition`
|
||||
|
||||
- Defines global properties for scripting that's used throughout its parent [Collection](#collection).
|
||||
Sets global scripting properties for a [Collection](#collection).
|
||||
|
||||
#### `ScriptingDefinition` syntax
|
||||
|
||||
- `language:` *`string`* (**required**)
|
||||
- 📖 See [ScriptingLanguage.ts](./../src/domain/ScriptingLanguage.ts) enumeration for allowed values.
|
||||
- `startCode:` *`string`* (**required**)
|
||||
- Code that'll be inserted on top of user created script.
|
||||
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](./templating.md#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
||||
- `endCode:` *`string`* (**required**)
|
||||
- Code that'll be inserted at the end of user created script.
|
||||
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](./templating.md#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
||||
|
||||
## Naming guidelines
|
||||
|
||||
- Prioritize consistency throughout all names.
|
||||
- Use an instruction format like "do this, do that" for clear, direct guidance. This approach reduces potential confusion and offers easy-to-follow steps. It provides specific, unambiguous instructions.
|
||||
- Ensure brand names adhere to their official casing.
|
||||
- Choose clear and uncomplicated language.
|
||||
- Favor the terms:
|
||||
- "Disable" over "Turn off"
|
||||
- "Configure" over "Set up"
|
||||
- "Clear" over "Erase" or "Clean"
|
||||
- "Minimize" over "Limit" or "Reduce" (when it enhances clarity)
|
||||
- "Remove" over "Uninstall"
|
||||
- Structure your phrases for clarity.
|
||||
- For instance, "Disable XX telemetry" or "Clear XX data" are preferred over "Clear data from XX", "Disable telemetry in XX", or "Clear data of XX".
|
||||
- Use sentence case rather than Title Case.
|
||||
- `language:` *`string`* **(required)**
|
||||
- 📖 See [`ScriptingLanguage.ts`](./../src/domain/ScriptingLanguage.ts) enumeration for allowed values.
|
||||
- `startCode:` *`string`* **(required)**
|
||||
- Prepends the given code to the generated script file.
|
||||
- 💡 You can use global variables such as `$homepage`, `$version`, `$date` via [parameter substitution](./templating.md#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`.
|
||||
- `endCode:` *`string`* **(required)**
|
||||
- Appends to the given code to the generated script file.
|
||||
- 💡 You can use global variables such as `$homepage`, `$version`, `$date` via [parameter substitution](./templating.md#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`.
|
||||
|
||||
@@ -9,6 +9,9 @@ This table highlights differences between the desktop and web versions of `priva
|
||||
| [Auto-updates](#auto-updates) | 🟢 Available | 🟢 Available |
|
||||
| [Logging](#logging) | 🟢 Available | 🔴 Not available |
|
||||
| [Script execution](#script-execution) | 🟢 Available | 🔴 Not available |
|
||||
| [Error handling](#error-handling) | 🟢 Advanced | 🟡 Limited |
|
||||
| [Native dialogs](#native-dialogs) | 🟢 Available | 🔴 Not available |
|
||||
| [Secure script execution/storage](#secure-script-executionstorage) | 🟢 Available | 🔴 Not available |
|
||||
|
||||
## Feature descriptions
|
||||
|
||||
@@ -50,5 +53,43 @@ Log file locations vary by operating system:
|
||||
|
||||
### Script execution
|
||||
|
||||
Direct execution of scripts is possible in the desktop version, offering a more integrated experience.
|
||||
This functionality is not present in the web version due to browser limitations.
|
||||
The desktop version of privacy.sexy enables direct script execution, providing a seamless and integrated experience.
|
||||
This direct execution capability isn't available in the web version due to inherent browser restrictions.
|
||||
|
||||
**Script execution history:**
|
||||
|
||||
For enhanced auditability and easier troubleshooting, the desktop version keeps a record of executed scripts in designated directories.
|
||||
These locations vary based on the operating system:
|
||||
|
||||
- macOS: `$HOME/Library/Application Support/privacy.sexy/runs`
|
||||
- Linux: `$HOME/.config/privacy.sexy/runs`
|
||||
- Windows: `%APPDATA%\privacy.sexy\runs`
|
||||
|
||||
### Error handling
|
||||
|
||||
The desktop version of privacy.sexy features advanced error handling capabilities.
|
||||
It employs robust and reliable execution strategies, including self-healing mechanisms, and provides guidance and troubleshooting information to resolve issues effectively.
|
||||
In contrast, the web version has more basic error handling due to browser limitations and the nature of web applications.
|
||||
|
||||
### Native dialogs
|
||||
|
||||
The desktop version uses native dialogs, offering more features and reliability compared to the browser's file system dialogs.
|
||||
These native dialogs provide a more integrated and user-friendly experience, aligning with the operating system's standard interface and functionalities.
|
||||
|
||||
### Secure script execution/storage
|
||||
|
||||
**Integrity checks:**
|
||||
|
||||
The desktop version of privacy.sexy implements robust integrity checks for both script execution and storage.
|
||||
Featuring tamper protection, the application actively verifies the integrity of script files before executing or saving them.
|
||||
If the actual contents of a script file do not align with the expected contents, the application refuses to execute or save the script.
|
||||
This proactive approach ensures only unaltered and verified scripts undergo processing, thereby enhancing both security and reliability.
|
||||
Due to browser constraints, this feature is absent in the web version.
|
||||
|
||||
**Error handling:**
|
||||
|
||||
In scenarios where script execution or storage encounters failure, the desktop application initiates automated troubleshooting and self-healing processes.
|
||||
It also guides users through potential issues with filesystem or third-party software, such as antivirus interventions.
|
||||
Specifically, the application is capable of identifying when antivirus software blocks or removes a script, providing users with tailored error messages
|
||||
and detailed resolution steps. This level of proactive error handling and user guidance enhances the application's security and reliability,
|
||||
offering a feature not achievable in the web version due to browser limitations.
|
||||
|
||||
@@ -13,8 +13,11 @@ See [ci-cd.md](./ci-cd.md) for more information.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Install Node >16.x.
|
||||
- Install Node.js:
|
||||
- Refer to [action.yml](./../.github/actions/setup-node/action.yml) for the minimum required version compatible with the automated workflows.
|
||||
- 💡 Recommended: Use [`nvm`](https://github.com/nvm-sh/nvm) CLI to install and switch between Node.js versions.
|
||||
- Install dependencies using `npm install` (or [`npm run install-deps`](#utility-scripts) for more options).
|
||||
- For Visual Studio Code users, running the configuration script is recommended to optimize the IDE settings, as detailed in [utility scripts](#utility-scripts).
|
||||
|
||||
### Testing
|
||||
|
||||
@@ -77,8 +80,8 @@ See [ci-cd.md](./ci-cd.md) for more information.
|
||||
- [**`npm run install-deps [-- <options>]`**](../scripts/npm-install.js):
|
||||
- Manages NPM dependency installation, it offers capabilities like doing a fresh install, retries on network errors, and other features.
|
||||
- For example, you can run `npm run install-deps -- --fresh` to do clean installation of dependencies.
|
||||
- [**`./scripts/configure-vscode.sh`**](../scripts/configure-vscode.sh):
|
||||
- This script checks and sets the necessary configurations for VSCode in `settings.json` file.
|
||||
- [**`python ./scripts/configure_vscode.py`**](../scripts/configure_vscode.py):
|
||||
- Optimizes Visual Studio Code settings and installs essential extensions, enhancing the development environment.
|
||||
|
||||
#### Automation scripts
|
||||
|
||||
@@ -94,3 +97,4 @@ See [ci-cd.md](./ci-cd.md) for more information.
|
||||
You should use EditorConfig to follow project style.
|
||||
|
||||
For Visual Studio Code, [`.vscode/extensions.json`](./../.vscode/extensions.json) includes list of recommended extensions.
|
||||
You can use [VSCode configuration script](#utility-scripts) to automatically install those.
|
||||
|
||||
@@ -79,19 +79,22 @@ To add a new dependency:
|
||||
|
||||
## Shared UI components
|
||||
|
||||
Shared UI components promote consistency and simplifies the creation of the front-end.
|
||||
Shared UI components ensure consistency and streamline front-end development.
|
||||
|
||||
In order to maintain portability and easy maintainability, the preference is towards using homegrown components over third-party ones or comprehensive UI frameworks like Quasar.
|
||||
We use homegrown components over third-party solutions or comprehensive UI frameworks like Quasar to maintain portability and easy maintenance.
|
||||
|
||||
Shared components include:
|
||||
|
||||
- [ModalDialog.vue](./../src/presentation/components/Shared/Modal/ModalDialog.vue) is utilized for rendering modal windows.
|
||||
- [TooltipWrapper.vue](./../src/presentation/components/Shared/TooltipWrapper.vue) acts as a wrapper for rendering tooltips.
|
||||
- [ModalDialog.vue](./../src/presentation/components/Shared/Modal/ModalDialog.vue): Renders modal windows.
|
||||
- [TooltipWrapper.vue](./../src/presentation/components/Shared/TooltipWrapper.vue): Provides tooltip functionality for improved information accessibility.
|
||||
- [FlatButton.vue](./../src/presentation/components/Shared/FlatButton.vue): Creates flat-style buttons for a unified and consistent user interface.
|
||||
|
||||
## Desktop builds
|
||||
|
||||
Desktop builds uses `electron-vite` to bundle the code, and `electron-builder` to build and publish the packages.
|
||||
|
||||
Host system access is strictly controlled. The [`preloader`](./../src/presentation/electron/preload/) isolates logic that interacts with the host system. These functionalities are then securely exposed to the renderer process (Vue application) using context-bridging. [`ApiContextBridge.ts`](./../src/presentation/electron/preload/ContextBridging/ApiContextBridge.ts) handles the configuration of the exposed APIs, ensuring a secure bridge between the Electron and Vue layers.
|
||||
|
||||
## Styles
|
||||
|
||||
### Style location
|
||||
|
||||
24
docs/research/README.md
Normal file
24
docs/research/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Research Documentation
|
||||
|
||||
Welcome to the research section of privacy.sexy.
|
||||
This area houses in-depth technical research and analyses, serving as a resource for developers, contributors, and technology enthusiasts.
|
||||
|
||||
**Structure:**
|
||||
|
||||
This folder organizes research into topic-specific subdirectories like `windows`, `linux`, etc.
|
||||
Each contains materials relevant to its subject.
|
||||
|
||||
**Contents:**
|
||||
|
||||
These documents offer comprehensive insights into the respective topics, supporting development and contributions.
|
||||
|
||||
**Contributing:**
|
||||
|
||||
Contributions to our research documentation are welcome.
|
||||
If your research aligns with privacy.sexy goals, please consider adding it here.
|
||||
See [`CONTRIBUTING.md`](./../../CONTRIBUTING.md) on more information about how to contribute.
|
||||
|
||||
**Usage:**
|
||||
|
||||
This information is available for educational and research purposes.
|
||||
We support knowledge sharing and aim to enhance understanding of privacy and security technologies.
|
||||
84
docs/research/windows/01-windows-10-1909-apps.txt
Normal file
84
docs/research/windows/01-windows-10-1909-apps.txt
Normal file
@@ -0,0 +1,84 @@
|
||||
|
||||
Name PublisherId Category NonRemovable
|
||||
---- ----------- -------- ------------
|
||||
1527c705-839a-4832-9118-54d4Bd6a0c89 cw5n1h2txyewy System True
|
||||
c5e2524a-ea46-4f67-841f-6a9465d9d515 cw5n1h2txyewy System True
|
||||
E2A4F912-2574-4A75-9BB0-0D023378592B cw5n1h2txyewy System True
|
||||
F46D4000-FD22-4DB4-AC8E-4E1DDDE828FE cw5n1h2txyewy System True
|
||||
InputApp cw5n1h2txyewy System True
|
||||
Microsoft.AAD.BrokerPlugin cw5n1h2txyewy System True
|
||||
Microsoft.AccountsControl cw5n1h2txyewy System True
|
||||
Microsoft.AsyncTextService 8wekyb3d8bbwe System True
|
||||
Microsoft.BingWeather 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.BioEnrollment cw5n1h2txyewy System True
|
||||
Microsoft.CredDialogHost cw5n1h2txyewy System True
|
||||
Microsoft.DesktopAppInstaller 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ECApp 8wekyb3d8bbwe System True
|
||||
Microsoft.GetHelp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Getstarted 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.HEIFImageExtension 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.LockApp cw5n1h2txyewy System True
|
||||
Microsoft.Messaging 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Microsoft3DViewer 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftEdge 8wekyb3d8bbwe System True
|
||||
Microsoft.MicrosoftEdgeDevToolsClient 8wekyb3d8bbwe System True
|
||||
Microsoft.MicrosoftOfficeHub 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftSolitaireCollection 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftStickyNotes 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MixedReality.Portal 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MSPaint 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Office.OneNote 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.OneConnect 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.People 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.PPIProjection cw5n1h2txyewy System True
|
||||
Microsoft.Print3D 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ScreenSketch 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.SkypeApp kzf8qxf38zg5c Provisioned False
|
||||
Microsoft.StorePurchaseApp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.VP9VideoExtensions 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Wallet 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WebMediaExtensions 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WebpImageExtension 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Win32WebViewHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Apprep.ChxApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.AssignedAccessLockApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CallingShellApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CapturePicker cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CloudExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ContentDeliveryManager cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Cortana cw5n1h2txyewy System True
|
||||
Microsoft.Windows.NarratorQuickStart 8wekyb3d8bbwe System True
|
||||
Microsoft.Windows.OOBENetworkCaptivePortal cw5n1h2txyewy System True
|
||||
Microsoft.Windows.OOBENetworkConnectionFlow cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ParentalControls cw5n1h2txyewy System True
|
||||
Microsoft.Windows.PeopleExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Photos 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Windows.PinningConfirmationDialog cw5n1h2txyewy System True
|
||||
Microsoft.Windows.SecHealthUI cw5n1h2txyewy System True
|
||||
Microsoft.Windows.SecureAssessmentBrowser cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ShellExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.StartMenuExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.XGpuEjectDialog cw5n1h2txyewy System True
|
||||
Microsoft.WindowsAlarms 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsCalculator 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsCamera 8wekyb3d8bbwe Provisioned False
|
||||
microsoft.windowscommunicationsapps 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsFeedbackHub 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsMaps 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsSoundRecorder 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsStore 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Xbox.TCUI 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxApp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxGameCallableUI cw5n1h2txyewy System True
|
||||
Microsoft.XboxGameOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxGamingOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxIdentityProvider 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxSpeechToTextOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.YourPhone 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ZuneMusic 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ZuneVideo 8wekyb3d8bbwe Provisioned False
|
||||
Windows.CBSPreview cw5n1h2txyewy System True
|
||||
windows.immersivecontrolpanel cw5n1h2txyewy System True
|
||||
Windows.PrintDialog cw5n1h2txyewy System True
|
||||
|
||||
|
||||
85
docs/research/windows/02-windows-10-20H2-apps.txt
Normal file
85
docs/research/windows/02-windows-10-20H2-apps.txt
Normal file
@@ -0,0 +1,85 @@
|
||||
|
||||
Name PublisherId Category NonRemovable
|
||||
---- ----------- -------- ------------
|
||||
1527c705-839a-4832-9118-54d4Bd6a0c89 cw5n1h2txyewy System True
|
||||
c5e2524a-ea46-4f67-841f-6a9465d9d515 cw5n1h2txyewy System True
|
||||
E2A4F912-2574-4A75-9BB0-0D023378592B cw5n1h2txyewy System True
|
||||
F46D4000-FD22-4DB4-AC8E-4E1DDDE828FE cw5n1h2txyewy System True
|
||||
Microsoft.549981C3F5F10 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.AAD.BrokerPlugin cw5n1h2txyewy System True
|
||||
Microsoft.AccountsControl cw5n1h2txyewy System True
|
||||
Microsoft.AsyncTextService 8wekyb3d8bbwe System True
|
||||
Microsoft.BingWeather 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.BioEnrollment cw5n1h2txyewy System True
|
||||
Microsoft.CredDialogHost cw5n1h2txyewy System True
|
||||
Microsoft.DesktopAppInstaller 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ECApp 8wekyb3d8bbwe System True
|
||||
Microsoft.GetHelp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Getstarted 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.HEIFImageExtension 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.LockApp cw5n1h2txyewy System True
|
||||
Microsoft.Microsoft3DViewer 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftEdge 8wekyb3d8bbwe System True
|
||||
Microsoft.MicrosoftEdge.Stable 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftEdgeDevToolsClient 8wekyb3d8bbwe System True
|
||||
Microsoft.MicrosoftOfficeHub 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftSolitaireCollection 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftStickyNotes 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MixedReality.Portal 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MSPaint 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Office.OneNote 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.People 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ScreenSketch 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.SkypeApp kzf8qxf38zg5c Provisioned False
|
||||
Microsoft.StorePurchaseApp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.VCLibs.140.00 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.VP9VideoExtensions 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Wallet 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WebMediaExtensions 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WebpImageExtension 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Win32WebViewHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Apprep.ChxApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.AssignedAccessLockApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CallingShellApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CapturePicker cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CloudExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ContentDeliveryManager cw5n1h2txyewy System True
|
||||
Microsoft.Windows.NarratorQuickStart 8wekyb3d8bbwe System True
|
||||
Microsoft.Windows.OOBENetworkCaptivePortal cw5n1h2txyewy System True
|
||||
Microsoft.Windows.OOBENetworkConnectionFlow cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ParentalControls cw5n1h2txyewy System True
|
||||
Microsoft.Windows.PeopleExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Photos 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Windows.PinningConfirmationDialog cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Search cw5n1h2txyewy System True
|
||||
Microsoft.Windows.SecHealthUI cw5n1h2txyewy System True
|
||||
Microsoft.Windows.SecureAssessmentBrowser cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ShellExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.StartMenuExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.XGpuEjectDialog cw5n1h2txyewy System True
|
||||
Microsoft.WindowsAlarms 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsCalculator 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsCamera 8wekyb3d8bbwe Provisioned False
|
||||
microsoft.windowscommunicationsapps 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsFeedbackHub 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsMaps 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsSoundRecorder 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsStore 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Xbox.TCUI 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxApp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxGameCallableUI cw5n1h2txyewy System True
|
||||
Microsoft.XboxGameOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxGamingOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxIdentityProvider 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxSpeechToTextOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.YourPhone 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ZuneMusic 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ZuneVideo 8wekyb3d8bbwe Provisioned False
|
||||
MicrosoftWindows.Client.CBS cw5n1h2txyewy System True
|
||||
MicrosoftWindows.UndockedDevKit cw5n1h2txyewy System True
|
||||
NcsiUwpApp 8wekyb3d8bbwe System True
|
||||
Windows.CBSPreview cw5n1h2txyewy System True
|
||||
windows.immersivecontrolpanel cw5n1h2txyewy System True
|
||||
Windows.PrintDialog cw5n1h2txyewy System True
|
||||
|
||||
|
||||
85
docs/research/windows/03-windows-10-21H2-apps.txt
Normal file
85
docs/research/windows/03-windows-10-21H2-apps.txt
Normal file
@@ -0,0 +1,85 @@
|
||||
|
||||
Name PublisherId Category NonRemovable
|
||||
---- ----------- -------- ------------
|
||||
1527c705-839a-4832-9118-54d4Bd6a0c89 cw5n1h2txyewy System True
|
||||
c5e2524a-ea46-4f67-841f-6a9465d9d515 cw5n1h2txyewy System True
|
||||
E2A4F912-2574-4A75-9BB0-0D023378592B cw5n1h2txyewy System True
|
||||
F46D4000-FD22-4DB4-AC8E-4E1DDDE828FE cw5n1h2txyewy System True
|
||||
Microsoft.549981C3F5F10 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.AAD.BrokerPlugin cw5n1h2txyewy System True
|
||||
Microsoft.AccountsControl cw5n1h2txyewy System True
|
||||
Microsoft.AsyncTextService 8wekyb3d8bbwe System True
|
||||
Microsoft.BingWeather 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.BioEnrollment cw5n1h2txyewy System True
|
||||
Microsoft.CredDialogHost cw5n1h2txyewy System True
|
||||
Microsoft.DesktopAppInstaller 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ECApp 8wekyb3d8bbwe System True
|
||||
Microsoft.GetHelp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Getstarted 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.HEIFImageExtension 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.LockApp cw5n1h2txyewy System True
|
||||
Microsoft.Microsoft3DViewer 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftEdge 8wekyb3d8bbwe System True
|
||||
Microsoft.MicrosoftEdge.Stable 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftEdgeDevToolsClient 8wekyb3d8bbwe System True
|
||||
Microsoft.MicrosoftOfficeHub 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftSolitaireCollection 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftStickyNotes 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MixedReality.Portal 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MSPaint 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Office.OneNote 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.People 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ScreenSketch 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.SkypeApp kzf8qxf38zg5c Provisioned False
|
||||
Microsoft.StorePurchaseApp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.VCLibs.140.00 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.VP9VideoExtensions 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Wallet 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WebMediaExtensions 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WebpImageExtension 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Win32WebViewHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Apprep.ChxApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.AssignedAccessLockApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CallingShellApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CapturePicker cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CloudExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ContentDeliveryManager cw5n1h2txyewy System True
|
||||
Microsoft.Windows.NarratorQuickStart 8wekyb3d8bbwe System True
|
||||
Microsoft.Windows.OOBENetworkCaptivePortal cw5n1h2txyewy System True
|
||||
Microsoft.Windows.OOBENetworkConnectionFlow cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ParentalControls cw5n1h2txyewy System True
|
||||
Microsoft.Windows.PeopleExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Photos 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Windows.PinningConfirmationDialog cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Search cw5n1h2txyewy System True
|
||||
Microsoft.Windows.SecHealthUI cw5n1h2txyewy System True
|
||||
Microsoft.Windows.SecureAssessmentBrowser cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ShellExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.StartMenuExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.XGpuEjectDialog cw5n1h2txyewy System True
|
||||
Microsoft.WindowsAlarms 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsCalculator 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsCamera 8wekyb3d8bbwe Provisioned False
|
||||
microsoft.windowscommunicationsapps 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsFeedbackHub 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsMaps 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsSoundRecorder 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsStore 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Xbox.TCUI 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxApp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxGameCallableUI cw5n1h2txyewy System True
|
||||
Microsoft.XboxGameOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxGamingOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxIdentityProvider 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxSpeechToTextOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.YourPhone 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ZuneMusic 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ZuneVideo 8wekyb3d8bbwe Provisioned False
|
||||
MicrosoftWindows.Client.CBS cw5n1h2txyewy System True
|
||||
MicrosoftWindows.UndockedDevKit cw5n1h2txyewy System True
|
||||
NcsiUwpApp 8wekyb3d8bbwe System True
|
||||
Windows.CBSPreview cw5n1h2txyewy System True
|
||||
windows.immersivecontrolpanel cw5n1h2txyewy System True
|
||||
Windows.PrintDialog cw5n1h2txyewy System True
|
||||
|
||||
|
||||
85
docs/research/windows/04-windows-10-22H2-apps.txt
Normal file
85
docs/research/windows/04-windows-10-22H2-apps.txt
Normal file
@@ -0,0 +1,85 @@
|
||||
|
||||
Name PublisherId Category NonRemovable
|
||||
---- ----------- -------- ------------
|
||||
1527c705-839a-4832-9118-54d4Bd6a0c89 cw5n1h2txyewy System True
|
||||
c5e2524a-ea46-4f67-841f-6a9465d9d515 cw5n1h2txyewy System True
|
||||
E2A4F912-2574-4A75-9BB0-0D023378592B cw5n1h2txyewy System True
|
||||
F46D4000-FD22-4DB4-AC8E-4E1DDDE828FE cw5n1h2txyewy System True
|
||||
Microsoft.549981C3F5F10 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.AAD.BrokerPlugin cw5n1h2txyewy System True
|
||||
Microsoft.AccountsControl cw5n1h2txyewy System True
|
||||
Microsoft.AsyncTextService 8wekyb3d8bbwe System True
|
||||
Microsoft.BingWeather 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.BioEnrollment cw5n1h2txyewy System True
|
||||
Microsoft.CredDialogHost cw5n1h2txyewy System True
|
||||
Microsoft.DesktopAppInstaller 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ECApp 8wekyb3d8bbwe System True
|
||||
Microsoft.GetHelp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Getstarted 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.HEIFImageExtension 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.LockApp cw5n1h2txyewy System True
|
||||
Microsoft.Microsoft3DViewer 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftEdge 8wekyb3d8bbwe System True
|
||||
Microsoft.MicrosoftEdge.Stable 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftEdgeDevToolsClient 8wekyb3d8bbwe System True
|
||||
Microsoft.MicrosoftOfficeHub 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftSolitaireCollection 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftStickyNotes 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MixedReality.Portal 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MSPaint 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Office.OneNote 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.People 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ScreenSketch 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.SkypeApp kzf8qxf38zg5c Provisioned False
|
||||
Microsoft.StorePurchaseApp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.VCLibs.140.00 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.VP9VideoExtensions 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Wallet 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WebMediaExtensions 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WebpImageExtension 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Win32WebViewHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Apprep.ChxApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.AssignedAccessLockApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CallingShellApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CapturePicker cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CloudExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ContentDeliveryManager cw5n1h2txyewy System True
|
||||
Microsoft.Windows.NarratorQuickStart 8wekyb3d8bbwe System True
|
||||
Microsoft.Windows.OOBENetworkCaptivePortal cw5n1h2txyewy System True
|
||||
Microsoft.Windows.OOBENetworkConnectionFlow cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ParentalControls cw5n1h2txyewy System True
|
||||
Microsoft.Windows.PeopleExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Photos 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Windows.PinningConfirmationDialog cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Search cw5n1h2txyewy System True
|
||||
Microsoft.Windows.SecHealthUI cw5n1h2txyewy System True
|
||||
Microsoft.Windows.SecureAssessmentBrowser cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ShellExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.StartMenuExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.XGpuEjectDialog cw5n1h2txyewy System True
|
||||
Microsoft.WindowsAlarms 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsCalculator 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsCamera 8wekyb3d8bbwe Provisioned False
|
||||
microsoft.windowscommunicationsapps 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsFeedbackHub 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsMaps 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsSoundRecorder 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsStore 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Xbox.TCUI 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxApp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxGameCallableUI cw5n1h2txyewy System True
|
||||
Microsoft.XboxGameOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxGamingOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxIdentityProvider 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxSpeechToTextOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.YourPhone 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ZuneMusic 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ZuneVideo 8wekyb3d8bbwe Provisioned False
|
||||
MicrosoftWindows.Client.CBS cw5n1h2txyewy System True
|
||||
MicrosoftWindows.UndockedDevKit cw5n1h2txyewy System True
|
||||
NcsiUwpApp 8wekyb3d8bbwe System True
|
||||
Windows.CBSPreview cw5n1h2txyewy System True
|
||||
windows.immersivecontrolpanel cw5n1h2txyewy System True
|
||||
Windows.PrintDialog cw5n1h2txyewy System True
|
||||
|
||||
|
||||
88
docs/research/windows/05-windows-11-21H2-apps.txt
Normal file
88
docs/research/windows/05-windows-11-21H2-apps.txt
Normal file
@@ -0,0 +1,88 @@
|
||||
|
||||
Name PublisherId Category NonRemovable
|
||||
---- ----------- -------- ------------
|
||||
1527c705-839a-4832-9118-54d4Bd6a0c89 cw5n1h2txyewy System True
|
||||
c5e2524a-ea46-4f67-841f-6a9465d9d515 cw5n1h2txyewy System True
|
||||
E2A4F912-2574-4A75-9BB0-0D023378592B cw5n1h2txyewy System True
|
||||
F46D4000-FD22-4DB4-AC8E-4E1DDDE828FE cw5n1h2txyewy System True
|
||||
Microsoft.549981C3F5F10 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.AAD.BrokerPlugin cw5n1h2txyewy System True
|
||||
Microsoft.AccountsControl cw5n1h2txyewy System True
|
||||
Microsoft.AsyncTextService 8wekyb3d8bbwe System True
|
||||
Microsoft.BingNews 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.BingWeather 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.BioEnrollment cw5n1h2txyewy System True
|
||||
Microsoft.CredDialogHost cw5n1h2txyewy System True
|
||||
Microsoft.DesktopAppInstaller 8wekyb3d8bbwe Provisioned True
|
||||
Microsoft.ECApp 8wekyb3d8bbwe System True
|
||||
Microsoft.GamingApp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.GetHelp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Getstarted 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.HEIFImageExtension 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.LockApp cw5n1h2txyewy System True
|
||||
Microsoft.MicrosoftEdge 8wekyb3d8bbwe System True
|
||||
Microsoft.MicrosoftEdge.Stable 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftEdgeDevToolsClient 8wekyb3d8bbwe System True
|
||||
Microsoft.MicrosoftOfficeHub 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftSolitaireCollection 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftStickyNotes 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.OneDriveSync 8wekyb3d8bbwe Installed False
|
||||
Microsoft.Paint 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.People 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.PowerAutomateDesktop 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ScreenSketch 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.SecHealthUI 8wekyb3d8bbwe Provisioned True
|
||||
Microsoft.StorePurchaseApp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Todos 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.UI.Xaml.2.4 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.VCLibs.140.00 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.VP9VideoExtensions 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WebMediaExtensions 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WebpImageExtension 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Win32WebViewHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Apprep.ChxApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.AssignedAccessLockApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CallingShellApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CapturePicker cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CloudExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ContentDeliveryManager cw5n1h2txyewy System True
|
||||
Microsoft.Windows.NarratorQuickStart 8wekyb3d8bbwe System True
|
||||
Microsoft.Windows.OOBENetworkCaptivePortal cw5n1h2txyewy System True
|
||||
Microsoft.Windows.OOBENetworkConnectionFlow cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ParentalControls cw5n1h2txyewy System True
|
||||
Microsoft.Windows.PeopleExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Photos 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Windows.PinningConfirmationDialog cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Search cw5n1h2txyewy System True
|
||||
Microsoft.Windows.SecureAssessmentBrowser cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ShellExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.StartMenuExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.XGpuEjectDialog cw5n1h2txyewy System True
|
||||
Microsoft.WindowsAlarms 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsCalculator 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsCamera 8wekyb3d8bbwe Provisioned False
|
||||
microsoft.windowscommunicationsapps 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsFeedbackHub 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsMaps 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsNotepad 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsSoundRecorder 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsStore 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsTerminal 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Xbox.TCUI 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxGameCallableUI cw5n1h2txyewy System True
|
||||
Microsoft.XboxGameOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxGamingOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxIdentityProvider 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxSpeechToTextOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.YourPhone 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ZuneMusic 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ZuneVideo 8wekyb3d8bbwe Provisioned False
|
||||
MicrosoftWindows.Client.CBS cw5n1h2txyewy System True
|
||||
MicrosoftWindows.Client.WebExperience cw5n1h2txyewy Provisioned False
|
||||
MicrosoftWindows.UndockedDevKit cw5n1h2txyewy System True
|
||||
NcsiUwpApp 8wekyb3d8bbwe System True
|
||||
Windows.CBSPreview cw5n1h2txyewy System True
|
||||
windows.immersivecontrolpanel cw5n1h2txyewy System True
|
||||
Windows.PrintDialog cw5n1h2txyewy System True
|
||||
|
||||
|
||||
91
docs/research/windows/06-windows-11-22H2-apps.txt
Normal file
91
docs/research/windows/06-windows-11-22H2-apps.txt
Normal file
@@ -0,0 +1,91 @@
|
||||
|
||||
Name PublisherId Category NonRemovable
|
||||
---- ----------- -------- ------------
|
||||
1527c705-839a-4832-9118-54d4Bd6a0c89 cw5n1h2txyewy System True
|
||||
c5e2524a-ea46-4f67-841f-6a9465d9d515 cw5n1h2txyewy System True
|
||||
Clipchamp.Clipchamp yxz26nhyzhsrt Provisioned False
|
||||
E2A4F912-2574-4A75-9BB0-0D023378592B cw5n1h2txyewy System True
|
||||
F46D4000-FD22-4DB4-AC8E-4E1DDDE828FE cw5n1h2txyewy System True
|
||||
Microsoft.549981C3F5F10 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.AAD.BrokerPlugin cw5n1h2txyewy System True
|
||||
Microsoft.AccountsControl cw5n1h2txyewy System True
|
||||
Microsoft.AsyncTextService 8wekyb3d8bbwe System True
|
||||
Microsoft.BingNews 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.BingWeather 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.BioEnrollment cw5n1h2txyewy System True
|
||||
Microsoft.CredDialogHost cw5n1h2txyewy System True
|
||||
Microsoft.DesktopAppInstaller 8wekyb3d8bbwe Provisioned True
|
||||
Microsoft.ECApp 8wekyb3d8bbwe System True
|
||||
Microsoft.GamingApp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.GetHelp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Getstarted 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.HEIFImageExtension 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.HEVCVideoExtension 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.LockApp cw5n1h2txyewy System True
|
||||
Microsoft.MicrosoftEdge 8wekyb3d8bbwe System True
|
||||
Microsoft.MicrosoftEdge.Stable 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftEdgeDevToolsClient 8wekyb3d8bbwe System True
|
||||
Microsoft.MicrosoftOfficeHub 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftSolitaireCollection 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftStickyNotes 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Paint 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.People 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.PowerAutomateDesktop 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.RawImageExtension 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ScreenSketch 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.SecHealthUI 8wekyb3d8bbwe Provisioned True
|
||||
Microsoft.StorePurchaseApp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Todos 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.VCLibs.140.00 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.VP9VideoExtensions 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WebMediaExtensions 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WebpImageExtension 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Win32WebViewHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Apprep.ChxApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.AssignedAccessLockApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CallingShellApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CapturePicker cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CloudExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ContentDeliveryManager cw5n1h2txyewy System True
|
||||
Microsoft.Windows.NarratorQuickStart 8wekyb3d8bbwe System True
|
||||
Microsoft.Windows.OOBENetworkCaptivePortal cw5n1h2txyewy System True
|
||||
Microsoft.Windows.OOBENetworkConnectionFlow cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ParentalControls cw5n1h2txyewy System True
|
||||
Microsoft.Windows.PeopleExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Photos 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Windows.PinningConfirmationDialog cw5n1h2txyewy System True
|
||||
Microsoft.Windows.PrintQueueActionCenter cw5n1h2txyewy System True
|
||||
Microsoft.Windows.SecureAssessmentBrowser cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ShellExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.StartMenuExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.XGpuEjectDialog cw5n1h2txyewy System True
|
||||
Microsoft.WindowsAlarms 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsCalculator 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsCamera 8wekyb3d8bbwe Provisioned False
|
||||
microsoft.windowscommunicationsapps 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsFeedbackHub 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsMaps 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsNotepad 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsSoundRecorder 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsStore 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsTerminal 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Xbox.TCUI 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxGameCallableUI cw5n1h2txyewy System True
|
||||
Microsoft.XboxGameOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxGamingOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxIdentityProvider 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxSpeechToTextOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.YourPhone 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ZuneMusic 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ZuneVideo 8wekyb3d8bbwe Provisioned False
|
||||
MicrosoftCorporationII.QuickAssist 8wekyb3d8bbwe Provisioned False
|
||||
MicrosoftWindows.Client.CBS cw5n1h2txyewy System True
|
||||
MicrosoftWindows.Client.Core cw5n1h2txyewy System True
|
||||
MicrosoftWindows.Client.WebExperience cw5n1h2txyewy Provisioned False
|
||||
MicrosoftWindows.UndockedDevKit cw5n1h2txyewy System True
|
||||
NcsiUwpApp 8wekyb3d8bbwe System True
|
||||
Windows.CBSPreview cw5n1h2txyewy System True
|
||||
windows.immersivecontrolpanel cw5n1h2txyewy System True
|
||||
Windows.PrintDialog cw5n1h2txyewy System True
|
||||
|
||||
|
||||
91
docs/research/windows/07-windows-11-23H2-apps.txt
Normal file
91
docs/research/windows/07-windows-11-23H2-apps.txt
Normal file
@@ -0,0 +1,91 @@
|
||||
|
||||
Name PublisherId Category NonRemovable
|
||||
---- ----------- -------- ------------
|
||||
1527c705-839a-4832-9118-54d4Bd6a0c89 cw5n1h2txyewy System True
|
||||
c5e2524a-ea46-4f67-841f-6a9465d9d515 cw5n1h2txyewy System True
|
||||
Clipchamp.Clipchamp yxz26nhyzhsrt Provisioned False
|
||||
E2A4F912-2574-4A75-9BB0-0D023378592B cw5n1h2txyewy System True
|
||||
F46D4000-FD22-4DB4-AC8E-4E1DDDE828FE cw5n1h2txyewy System True
|
||||
Microsoft.549981C3F5F10 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.AAD.BrokerPlugin cw5n1h2txyewy System True
|
||||
Microsoft.AccountsControl cw5n1h2txyewy System True
|
||||
Microsoft.AsyncTextService 8wekyb3d8bbwe System True
|
||||
Microsoft.BingNews 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.BingWeather 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.BioEnrollment cw5n1h2txyewy System True
|
||||
Microsoft.CredDialogHost cw5n1h2txyewy System True
|
||||
Microsoft.DesktopAppInstaller 8wekyb3d8bbwe Provisioned True
|
||||
Microsoft.ECApp 8wekyb3d8bbwe System True
|
||||
Microsoft.GamingApp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.GetHelp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Getstarted 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.HEIFImageExtension 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.HEVCVideoExtension 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.LockApp cw5n1h2txyewy System True
|
||||
Microsoft.MicrosoftEdge.Stable 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftEdgeDevToolsClient 8wekyb3d8bbwe System True
|
||||
Microsoft.MicrosoftOfficeHub 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftSolitaireCollection 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.MicrosoftStickyNotes 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Paint 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.People 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.PowerAutomateDesktop 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.RawImageExtension 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ScreenSketch 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.SecHealthUI 8wekyb3d8bbwe Provisioned True
|
||||
Microsoft.StorePurchaseApp 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Todos 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.VCLibs.140.00 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.VP9VideoExtensions 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WebMediaExtensions 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WebpImageExtension 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Win32WebViewHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Apprep.ChxApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.AssignedAccessLockApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CallingShellApp cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CapturePicker cw5n1h2txyewy System True
|
||||
Microsoft.Windows.CloudExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ContentDeliveryManager cw5n1h2txyewy System True
|
||||
Microsoft.Windows.NarratorQuickStart 8wekyb3d8bbwe System True
|
||||
Microsoft.Windows.OOBENetworkCaptivePortal cw5n1h2txyewy System True
|
||||
Microsoft.Windows.OOBENetworkConnectionFlow cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ParentalControls cw5n1h2txyewy System True
|
||||
Microsoft.Windows.PeopleExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.Photos 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Windows.PinningConfirmationDialog cw5n1h2txyewy System True
|
||||
Microsoft.Windows.PrintQueueActionCenter cw5n1h2txyewy System True
|
||||
Microsoft.Windows.SecureAssessmentBrowser cw5n1h2txyewy System True
|
||||
Microsoft.Windows.ShellExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.StartMenuExperienceHost cw5n1h2txyewy System True
|
||||
Microsoft.Windows.XGpuEjectDialog cw5n1h2txyewy System True
|
||||
Microsoft.WindowsAlarms 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsCalculator 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsCamera 8wekyb3d8bbwe Provisioned False
|
||||
microsoft.windowscommunicationsapps 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsFeedbackHub 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsMaps 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsNotepad 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsSoundRecorder 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsStore 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.WindowsTerminal 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.Xbox.TCUI 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxGameCallableUI cw5n1h2txyewy System True
|
||||
Microsoft.XboxGameOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxGamingOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxIdentityProvider 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.XboxSpeechToTextOverlay 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.YourPhone 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ZuneMusic 8wekyb3d8bbwe Provisioned False
|
||||
Microsoft.ZuneVideo 8wekyb3d8bbwe Provisioned False
|
||||
MicrosoftCorporationII.QuickAssist 8wekyb3d8bbwe Provisioned False
|
||||
MicrosoftWindows.Client.CBS cw5n1h2txyewy System True
|
||||
MicrosoftWindows.Client.Core cw5n1h2txyewy System True
|
||||
MicrosoftWindows.Client.FileExp cw5n1h2txyewy System True
|
||||
MicrosoftWindows.Client.WebExperience cw5n1h2txyewy Provisioned False
|
||||
MicrosoftWindows.UndockedDevKit cw5n1h2txyewy System True
|
||||
NcsiUwpApp 8wekyb3d8bbwe System True
|
||||
Windows.CBSPreview cw5n1h2txyewy System True
|
||||
windows.immersivecontrolpanel cw5n1h2txyewy System True
|
||||
Windows.PrintDialog cw5n1h2txyewy System True
|
||||
|
||||
|
||||
46
docs/research/windows/README.md
Normal file
46
docs/research/windows/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Research on Windows
|
||||
|
||||
In this section, we maintain a structured approach to our research on Windows.
|
||||
The use of `01` prefixed file names aids in organizing and retrieving search results effectively.
|
||||
|
||||
## Apps
|
||||
|
||||
The PowerShell script below serves as a method for gathering detailed information about Windows packages.
|
||||
|
||||
```ps1
|
||||
$allPackages = @()
|
||||
$provisionedPackages = Get-AppxProvisionedPackage -Online
|
||||
foreach ($installedPackage in Get-AppxPackage -AllUsers) {
|
||||
if ($installedPackage.IsFramework -eq $true) {
|
||||
continue
|
||||
}
|
||||
$allPackages += [PSCustomObject]@{
|
||||
Name = $installedPackage.Name
|
||||
PublisherId = $installedPackage.PublisherId
|
||||
Category = if ($installedPackage.SignatureKind -eq "System") {
|
||||
'System'
|
||||
} elseif ($provisionedPackages | Where-Object { $_.DisplayName -eq $installedPackage.Name }) {
|
||||
'Provisioned'
|
||||
} else {
|
||||
'Installed'
|
||||
}
|
||||
NonRemovable = $installedPackage.NonRemovable
|
||||
}
|
||||
}
|
||||
foreach ($provisionedPackage in $provisionedPackages) {
|
||||
if ($allPackages | Where-Object { $_.Name -eq $provisionedPackage.DisplayName }) {
|
||||
continue
|
||||
}
|
||||
$allPackages += [PSCustomObject]@{
|
||||
Name = $provisionedPackage.DisplayName
|
||||
PublisherId = $provisionedPackage.PackageName -split '_' | Select-Object -Last 1
|
||||
Category = 'Provisioned'
|
||||
NonRemovable = $false
|
||||
}
|
||||
}
|
||||
$allPackages `
|
||||
| Sort-Object Name `
|
||||
| Select-Object Name, PublisherId, Category, NonRemovable `
|
||||
| Format-Table `
|
||||
| Out-File -FilePath "$([System.Environment]::GetFolderPath('Desktop'))\apps.txt"
|
||||
```
|
||||
56
docs/script-guidelines.md
Normal file
56
docs/script-guidelines.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# privacy.sexy Script Guidelines
|
||||
|
||||
Create a script for privacy.sexy by submitting a PR or creating an issue (details in [Extend Scripts](./../CONTRIBUTING.md#extend-scripts)).
|
||||
As scripts are central to privacy.sexy and reach a global audience, their design is critical.
|
||||
|
||||
Key attributes of a good script:
|
||||
|
||||
- ✅ Well-referenced [documentation](#documentation).
|
||||
- ✅ Utilizes [shared functions](#shared-functions).
|
||||
- ✅ Has a [simple name](#name).
|
||||
|
||||
## Name
|
||||
|
||||
- Choose a title that is easy to understand for all users, regardless of technical skill, yet remains technically accurate.
|
||||
- Focus on privacy implications, avoiding complex or overly technical jargon.
|
||||
- Maintain consistency in naming, avoiding linguistic variations.
|
||||
- Use action-oriented language for clarity and directness. Use an instruction format like "do this, do that" for clear, direct guidance.
|
||||
- Respect the official casing of brand names.
|
||||
- Choose clear and uncomplicated language.
|
||||
- It should start with an imperative noun.
|
||||
- Start with action verbs like `Clear`, `Disable`, `Remove`, `Configure`, `Minimize`, `Maximize`. While exceptions exist, these prefixes help maintain naming consistency.
|
||||
- Favor the terms:
|
||||
- `Disable` over `Turn off`, `Stop`, `Prevent`
|
||||
- `Configure` over `Set up`
|
||||
- `Clear` over `Erase`, `Clean`
|
||||
- `Minimize` over `Limit`, `Reduce`
|
||||
- `Maximize` over `Extend`, `Delay`, `Postpone`, `Prolong`
|
||||
- `Remove` over `Uninstall`
|
||||
- Structure your phrases for clarity, examples:
|
||||
- Prefer `Disable XX telemetry` over `Disable telemetry in XX`
|
||||
- Prefer `Clear XX data` over `Clear data from XX`, or `Clear data of XX`.
|
||||
- Use sentence case rather than Title Case.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Use credible and reputable sources for references.
|
||||
- Use archived links by using [archive.org](https://archive.org) or [archive.today](https://archive.today).
|
||||
- Format archive.today links fully, for example: `https://archive.today/YYYYMMDDhhmmss/https://privacy.sexy`.
|
||||
- Explain the default behavior if the script is not executed.
|
||||
|
||||
## Shared functions
|
||||
|
||||
Use existing shared functions when possible, like `DisableService` for disabling services,.
|
||||
|
||||
- 📖 Learn about templates in [templating.md](./templating.md).
|
||||
- 📖 For syntax, see [collection-files.md](collection-files.md).
|
||||
|
||||
## Code
|
||||
|
||||
- Prefer [shared functions](#shared-functions); avoid custom code unless necessary.
|
||||
- Keep code simple and compatible with older systems.
|
||||
- Focus on reliability, ensuring the script is error-resistant, works on different locales and handles unexpected situations.
|
||||
- Language selection:
|
||||
- Windows: Use batch when simpler, otherwise PowerShell.
|
||||
- macOS/Linux: Use bash when simpler, otherwise Python.
|
||||
- Provide revert code to restore original/default settings when applicable.
|
||||
@@ -29,7 +29,9 @@ There are different types of tests executed:
|
||||
|
||||
- Evaluate individual components in isolation.
|
||||
- Located in [`./tests/unit`](./../tests/unit).
|
||||
- Achieve isolation using [stubs](./../tests/unit/shared/Stubs).
|
||||
- Achieve isolation using stubs where you place:
|
||||
- Common stubs in [`./shared/Stubs`](./../tests/unit/shared/Stubs),
|
||||
- Component-specific stubs in same folder as test file.
|
||||
- Include Vue component tests, enabled by `@vue/test-utils`.
|
||||
|
||||
#### Unit tests naming
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
|
||||
const { join } = require('path');
|
||||
const { join } = require('node:path');
|
||||
const { electronBundled, electronUnbundled } = require('./dist-dirs.json');
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolve } from 'path';
|
||||
import { resolve } from 'node:path';
|
||||
import { mergeConfig, UserConfig } from 'vite';
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
|
||||
import { getAliasesFromTsConfig, getClientEnvironmentVariables } from './vite-config-helper';
|
||||
import { getAliases, getClientEnvironmentVariables } from './vite-config-helper';
|
||||
import { createVueConfig } from './vite.config';
|
||||
import distDirs from './dist-dirs.json' assert { type: 'json' };
|
||||
|
||||
@@ -54,7 +54,9 @@ function getSharedElectronConfig(options: {
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: '[name].cjs', // This is needed so `type="module"` works
|
||||
// Mark: electron-esm-support
|
||||
// This is needed so `type="module"` works
|
||||
entryFileNames: '[name].cjs',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -64,7 +66,7 @@ function getSharedElectronConfig(options: {
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
...getAliasesFromTsConfig(),
|
||||
...getAliases(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<mxfile host="Electron" modified="2021-01-31T12:32:01.751Z" agent="5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.1.8 Chrome/87.0.4280.88 Electron/11.1.1 Safari/537.36" etag="OTbSPW1ZOLwiPL6mt-j9" version="14.1.8" type="device"><diagram id="rhL8jzEM8kVVyiS98U7u" name="Page-1">3VtZd6JKF/01/dhZjA6PREjCXRbGFpM2L70QCYIoLsUI/PpvnwKcMD3cazrJl6wEqfHU3vvsKoz5Infm6e3KWU5ZPPGiL5IwSb/I+hdJUsUmflNBVhQoDaEo8FfBpCgS9wWDIPfKwqrZJph466OGSRxHSbA8LnTjxcJzk6MyZ7WKt8fNnuPoeNal43u1goHrRPXSx2CSTIvSlirsy++8wJ9WM4tCWTN3qsZlwXrqTOLtQZFsfJE7qzhOilfztONFhF2FS9Hv5pXaXWArb5H8Tofr+9ZNO9J+6A+Ljvr89N2aP5tflSq4JKtW7E0AQHkbr5Jp7McLJzL2pdereLOYeDSsgLt9m24cL1EoojD0kiQr2XQ2SYyiaTKPylovDZLv1P1KLe9GBzV6Wo7Mb7LqZpGssoNOdDs6rNt343dVv3WyimdeJ47iFV+f3OZfqKkDWGK6jjcr1/sJapUQnZXvJT9pJxXtCNGDCUp6br147iFSNFh5kZMEL8eSc0rl+rt2e3LxouT3T7iW3oXrHW9HrO1J/IC8yZfmjXfVVisnO2iwjINFsj4Y+Z4K0KD0TrnMzdI51ZP0/nlrSVRPBFNMv5fPbh3/QVHyu7rHgXcIV5L6m/bxeWSofCj7KMd9caJNOdP9yltj8Zg7XtSEcEzzdhok3mDpcFy2OCkcU/ocRNEBxhPVa02Uc+i3pLHcaFCPeJFUOjuzFVbBeqvES38KXlnbaB6nT5lN2/2eL1c7+/Rgv69OMxdHW268Z2qJR6n1m5klHmeW9B6pJb1Xav07h28fm7YoH5n2L9tLzebbm7zc+ixHxPcU1MWPDP/JPaSaV2vLZRS4l7DqI+OtWfezSt/nyGjwr3KEg/Li62ck/b6Lq8cuvnsYO7Rx6YyNN9/MxtXPkjwf6flK/pQHJLmWdHo8d4LLHo3G4mTyfBZ3UWjKbe8Njkbtk6Rq15NKUs4klfpWSaXUgDYXzysHiGzcZLPyLgr4c8v1XPcc4OOWqqjCGwDePDmLNs4A3vqbLtaoAe69eOXJ5hWgxT8H+tlrnAd60myPBeEyO0TrBNtmHdvmGWilt4K2WYN25S3jdZDE5eCfGl75zAb8V+Ft1a3CWCRB8hmxPfWFdwe3XQM3ma48ZxIs/P8DeN/dGsT6KX5Of165rO2OBU/2GufAFbyW0GpdBlwcTo7BFZQ6uOqVeAZe6a3grZ/X8DKY/OoZ6aNCLKniVfvDgazWQF4nTvKzI9qf42vo9H0ZFE9MQKze/XhHCP/K6euSGJ6+mXoORPHcu6lvh2H9mOUcviEiRLEfuJ8HULG+Nf1dQHdr2AOazamXG0eR5xKqH1efHw/N+lZUA289dZb0MpjzTywcQkUrh5QjLQr8BcoSeptoV9p1xl50T48UJHVZH8dJEs/RIKKKa8ed+ZyYowMXfaEJn0xbL4tPVtBBy6lunoOUqLwu49GnSUIfydAICenGnSyUq8CNF88BKF9duZhRusHe6uBC5VDHDZIuXn91FpOv4xV+U5FKh54budH88bDxwvUPaoKDUOtqWZ0pL/pk3aa/UpxyryhXap38fenl6a9vkp+ffvmP6f/qTOKxtxOB1Gr9GDjr9QXpl9TWVVP9PQlI5zUgvZkI6tv85xeB+EsRjBdb/AbFKX7wCtsyFdNbausLsS7LyhHlcuOqVT+QSO0zht9+K7Lr55HPT7bwS7Kf45XvLb6uvZg+TXDTAC83y4C/Z7v+Gi+TYB7k/ER2wZzfvUtSsS/XqK/0cUh9VfZfqH/Y3Hrj5vegr7fv2awhLwLh8cyf5n7J/CWIPS+Z8+Qe8V/RzamUteJWulm/gJzrlM58nfs7S3rKrpXxY7pxcyFw7r4Jrh6/dOWJPMlUmWXqizt3X1iobVmnnU/mbmDeTZZPd9/i+4GZWfYoMG+nkfM4iSe6ELBwKJnBteQ8Psj9eVtBm62paz4v103RCkzMfX/r+k/zaD1Gj/G8vXkamMV9R8wmj2l0P/gnmswfNmPp28wMldZIirKRlEbm7dNyfLttmwHL+uE/N0wwMLtFs6Qs9Om1unt9Z7bN0Mh6HfPlPkwXB33Vb7OH62/hLCjLEnfxsH6yi1i8+UM2zniUd9fTya3vPyFK2zawfkVkuulbubGxwijs2qbUBS7WQBC6oZtbgSBYgSJaobthoZl27SGVSyzQUqujCMweJqhPcfWZrW0se5Z17f6G5SMZ2KDtKLcGGq6mYOpMxb1gzY0MV8nKtOoKPA2fdajdKMOcVC4yiSVWpsiWbSbMZhjbxNgzxDFCHKZIYzEepylYmZAjnhx1PuJRmO6jD9aUz1LiqBsaQq8jpKjLMJZv6RjH9uWubaTd0M+tx3MxaRTTpmcPJawb8/Zlby4kWBvKZqqpu2jLtpbEUo4jxwBz2kOxWLshW4GWsUDJe3bfZ/kM8ZBmGOZkSq+j5bQ+FjJoiWL1wYO76el+jv4y4tiygaaijYR+GBu42po0sk1wxfEtr4hzoG27wIjpmkj4WzraZVgvjU9zow/TXQXrABY+8Qeeh4SJivi2TGcJHz830MYgvMCvoIBrzkcP2Fq8DeFPPDDU9WX0zxnVvcqpkHfDmQAN4DpUvYArdzv6/i02b/ttcyakPYo3NGl9W8xHvFPcWyg+QY5izQblGcadIXcFgc+n+xwP8CsWmhymLBBIu+grEB8CcEW8Ptqwck0zWgv0CDz1IepGGLt/Hs8O8BwoWyuc+Wifs9wKGdeARtio4C4j3Vvh0IfuVHALfg1gPEOsBtbaV0x9RPFluE8pZ4ABrn3oxkQe+FuKE30zWgt+gP1MfY1bU+8Tf9Ay8kDvS6RDi17nlAsjhWvtkdY7pHFIY4h1poInmluxOhrmdIUe8gXYiozj7UNrTCy4nAms0KqAssQKSQt96Jj0MiIfkICpwmwXGoWOwdPrsRJOrgz8ZCt/CLkubZYjZ0n3AmLJMJZq6W5i0Rj5CJgMEYNJmMiESY+0w/EeIUcpTkMs8sYAf8CNuAk06Arj5CbnGXy/ih/4Jx3JRR4b4hkdKtxrMi3t6S7laT7KMa+tCUUu+6RB4lXqwc8QP7h3eU739D7mIe0apPuUUUz6kDREeKo9e5T0qL3OuEYseFGXvIzy5/bVvCFNy/C3jPN2W3ohjYs1kkd0aT2hKUKLaW+gKD3ySfJpvQ//MEhjyG3kAvyD8XgE0qxI8XA/DYeUx7S2DJiCY7STXo0HOsB6uIZd1ZKXCTiBRkzu9z19BLwoj42kyEsT+qdYfLHUYIo4Ea+m0N6CeAXue1inRdjkM65dC+OAb8xtSow8PSM9ukmP+1c/ezVXqd4eCZQnaC+McoPvNSzTuIfhWuwPpF/eZkQ5grWT12jEPfj0aayUv87dpPBt4piwYrTPUR5R/iiv40T+oEHPtA/McuSpCK1k2DtoPInZ/4RYM+FAuSgSLvBZeBe45X7F84G4zfg+m1VtuYbLMu20n1/1IzzIO7G/kW/tvL+o2/Xf9ava0rqrGE777eYF5zx/4Iamfj0lbjAW4uvnZZ7Bi4wyV3gcx/WYAzGU9dcN2+7THqLynON7Vz8d5aSZ4UE8/dN4hAMcgAnXu0px4QwjHuC2679fB28LXWrJHhPe73DMPaaFXgn7Au+BdgbvPTZFjgAdySjXDB/RWbVu0jr5vQLfOq2vtFPUL+Kgmyst9/ZGcDrXM5wcLcxH5wHSqkjnLkv/Fv4Rd3WshDpW7BQr9V9j1flbWGEs7rczn2so9zOce6CxmVLstxo8XeBeWJwrNRWemvL9Jx8mHMP8aQo/ynCu86trNU9RT35J50BXIo/r2eRptNe78B/yf9pvyZ/orGls6ZxSxK3RtYxBK2OqYtCKmIr85vtOEdNTwwxadFLvtBf34fblCUroyniiyRV6hrrMnz8l+Uo9fuNJlurPoY0zz6GNP34Oxe3+v8qKTwHv/zVPNv4H</diagram></mxfile>
|
||||
BIN
img/architecture/app-ddd.drawio.png
Normal file
BIN
img/architecture/app-ddd.drawio.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB |
1954
package-lock.json
generated
1954
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "privacy.sexy",
|
||||
"version": "0.12.8",
|
||||
"version": "0.12.9",
|
||||
"private": true,
|
||||
"slogan": "Now you have the choice",
|
||||
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
||||
@@ -36,7 +36,6 @@
|
||||
"@floating-ui/vue": "^1.0.2",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"ace-builds": "^1.30.0",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"electron-log": "^5.0.1",
|
||||
"electron-progressbar": "^2.1.0",
|
||||
"electron-updater": "^6.1.4",
|
||||
@@ -46,15 +45,15 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||
"@rushstack/eslint-patch": "^1.5.1",
|
||||
"@rushstack/eslint-patch": "^1.6.1",
|
||||
"@types/ace": "^0.0.49",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"@typescript-eslint/parser": "^6.17.0",
|
||||
"@vitejs/plugin-legacy": "^4.1.1",
|
||||
"@vitejs/plugin-vue": "^4.4.0",
|
||||
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/eslint-config-airbnb-with-typescript": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/test-utils": "^2.4.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"cypress": "^13.3.1",
|
||||
@@ -63,9 +62,9 @@
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-icon-builder": "^2.0.1",
|
||||
"electron-vite": "^1.0.28",
|
||||
"eslint": "^8.51.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"eslint-plugin-vuejs-accessibility": "^2.2.0",
|
||||
"icon-gen": "^4.0.0",
|
||||
"jsdom": "^22.1.0",
|
||||
@@ -87,11 +86,8 @@
|
||||
"yaml-lint": "^1.7.0"
|
||||
},
|
||||
"//devDependencies": {
|
||||
"terser": "Used by @vitejs/plugin-legacy for minification",
|
||||
"@rushstack/eslint-patch": "Needed by @vue/eslint-config-typescript",
|
||||
"@vue/eslint-config-typescript": "Cannot upgrade to 12.X.X due to @vue/eslint-config-airbnb-with-typescript, https://github.com/vuejs/eslint-config-airbnb/issues/58",
|
||||
"@typescript-eslint/eslint-plugin": "Cannot upgrade to 6.X.X due to @vue/eslint-config-airbnb-with-typescript, https://github.com/vuejs/eslint-config-airbnb/issues/58",
|
||||
"@typescript-eslint/parser": "Cannot upgrade to 6.X.X due to @vue/eslint-config-airbnb-with-typescript, https://github.com/vuejs/eslint-config-airbnb/issues/58"
|
||||
"terser": "Used by `@vitejs/plugin-legacy` for minification",
|
||||
"@rushstack/eslint-patch": "Needed by `@vue/eslint-config-typescript` and `@vue/eslint-config-airbnb-with-typescript`"
|
||||
},
|
||||
"homepage": "https://privacy.sexy",
|
||||
"repository": {
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script ensures that the '.vscode/settings.json' file exists and is configured correctly for ESLint validation on Vue and JavaScript files.
|
||||
# See https://web.archive.org/web/20230801024405/https://eslint.vuejs.org/user-guide/#visual-studio-code
|
||||
|
||||
declare -r SETTINGS_FILE='.vscode/settings.json'
|
||||
declare -ra CONFIG_KEYS=('vue' 'javascript' 'typescript')
|
||||
declare -r TEMP_FILE="tmp.$$.json"
|
||||
|
||||
main() {
|
||||
ensure_vscode_directory_exists
|
||||
create_or_update_settings
|
||||
}
|
||||
|
||||
ensure_vscode_directory_exists() {
|
||||
local dir_name
|
||||
dir_name=$(dirname "${SETTINGS_FILE}")
|
||||
if [[ ! -d ${dir_name} ]]; then
|
||||
mkdir -p "${dir_name}"
|
||||
echo "🎉 Created directory: ${dir_name}"
|
||||
fi
|
||||
}
|
||||
|
||||
create_or_update_settings() {
|
||||
if [[ ! -f ${SETTINGS_FILE} ]]; then
|
||||
create_default_settings
|
||||
else
|
||||
add_or_update_eslint_validate
|
||||
fi
|
||||
}
|
||||
|
||||
create_default_settings() {
|
||||
local default_validate
|
||||
default_validate=$(printf '%s' "${CONFIG_KEYS[*]}" | jq -R -s -c -M 'split(" ")')
|
||||
echo "{ \"eslint.validate\": ${default_validate} }" | jq '.' > "${SETTINGS_FILE}"
|
||||
echo "🎉 Created default ${SETTINGS_FILE}"
|
||||
}
|
||||
|
||||
add_or_update_eslint_validate() {
|
||||
if ! jq -e '.["eslint.validate"]' "${SETTINGS_FILE}" >/dev/null; then
|
||||
add_default_eslint_validate
|
||||
else
|
||||
update_eslint_validate
|
||||
fi
|
||||
}
|
||||
|
||||
add_default_eslint_validate() {
|
||||
jq --argjson keys "$(printf '%s' "${CONFIG_KEYS[*]}" \
|
||||
| jq -R -s -c 'split(" ")')" '. += {"eslint.validate": $keys}' "${SETTINGS_FILE}" > "${TEMP_FILE}"
|
||||
replace_and_confirm
|
||||
echo "🎉 Added default 'eslint.validate' to ${SETTINGS_FILE}"
|
||||
}
|
||||
|
||||
update_eslint_validate() {
|
||||
local existing_keys
|
||||
existing_keys=$(jq '.["eslint.validate"]' "${SETTINGS_FILE}")
|
||||
for key in "${CONFIG_KEYS[@]}"; do
|
||||
if ! echo "${existing_keys}" | jq 'index("'"${key}"'")' >/dev/null; then
|
||||
jq '.["eslint.validate"] += ["'"${key}"'"]' "${SETTINGS_FILE}" > "${TEMP_FILE}"
|
||||
mv "${TEMP_FILE}" "${SETTINGS_FILE}"
|
||||
echo "🎉 Updated 'eslint.validate' in ${SETTINGS_FILE} for ${key}"
|
||||
else
|
||||
echo "⏩️ No updated needed for ${key} ${SETTINGS_FILE}."
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
replace_and_confirm() {
|
||||
if mv "${TEMP_FILE}" "${SETTINGS_FILE}"; then
|
||||
echo "🎉 Updated ${SETTINGS_FILE}"
|
||||
fi
|
||||
}
|
||||
|
||||
main
|
||||
162
scripts/configure_vscode.py
Executable file
162
scripts/configure_vscode.py
Executable file
@@ -0,0 +1,162 @@
|
||||
"""
|
||||
This script configures project-level VSCode settings in '.vscode/settings.json' for
|
||||
development and installs recommended extensions from '.vscode/extensions.json'.
|
||||
"""
|
||||
# pylint: disable=missing-function-docstring
|
||||
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import re
|
||||
from typing import Any
|
||||
from shutil import which
|
||||
|
||||
VSCODE_SETTINGS_JSON_FILE: str = '.vscode/settings.json'
|
||||
VSCODE_EXTENSIONS_JSON_FILE: str = '.vscode/extensions.json'
|
||||
|
||||
def main() -> None:
|
||||
ensure_vscode_directory_exists()
|
||||
ensure_setting_file_exists()
|
||||
add_or_update_settings()
|
||||
install_recommended_extensions()
|
||||
|
||||
def ensure_vscode_directory_exists() -> None:
|
||||
vscode_directory_path = os.path.dirname(VSCODE_SETTINGS_JSON_FILE)
|
||||
try:
|
||||
os.makedirs(vscode_directory_path, exist_ok=True)
|
||||
print_success(f"Created or verified directory: {vscode_directory_path}")
|
||||
except OSError as error:
|
||||
print_error(f"Error handling directory {vscode_directory_path}: {error}")
|
||||
|
||||
def ensure_setting_file_exists() -> None:
|
||||
try:
|
||||
if os.path.isfile(VSCODE_SETTINGS_JSON_FILE):
|
||||
print_success(f"VSCode settings file exists: {VSCODE_SETTINGS_JSON_FILE}")
|
||||
return
|
||||
with open(VSCODE_SETTINGS_JSON_FILE, 'w', encoding='utf-8') as file:
|
||||
json.dump({}, file, indent=4)
|
||||
print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
||||
except IOError as error:
|
||||
print_error(f"Error creating file {VSCODE_SETTINGS_JSON_FILE}: {error}")
|
||||
print(f"📄 Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
||||
|
||||
def add_or_update_settings() -> None:
|
||||
configure_setting_key('eslint.validate', ['vue', 'javascript', 'typescript'])
|
||||
# Set ESLint validation for specific file types.
|
||||
# Details: # pylint: disable-next=line-too-long
|
||||
# - https://web.archive.org/web/20230801024405/https://eslint.vuejs.org/user-guide/#visual-studio-code
|
||||
|
||||
configure_setting_key('terminal.integrated.env.linux', {"GTK_PATH": ""})
|
||||
# Unset GTK_PATH on Linux for Electron development in sandboxed environments
|
||||
# like Snap or Flatpak VSCode installations, enabling script execution.
|
||||
# Details: # pylint: disable-next=line-too-long
|
||||
# - https://archive.ph/2024.01.06-003914/https://github.com/microsoft/vscode/issues/179274, https://web.archive.org/web/20240106003915/https://github.com/microsoft/vscode/issues/179274
|
||||
|
||||
def configure_setting_key(configuration_key: str, desired_value: Any) -> None:
|
||||
try:
|
||||
with open(VSCODE_SETTINGS_JSON_FILE, 'r+', encoding='utf-8') as file:
|
||||
settings: dict = json.load(file)
|
||||
if configuration_key in settings:
|
||||
actual_value = settings[configuration_key]
|
||||
if actual_value == desired_value:
|
||||
print_skip(f"Already configured as desired: \"{configuration_key}\"")
|
||||
return
|
||||
settings[configuration_key] = desired_value
|
||||
file.seek(0)
|
||||
json.dump(settings, file, indent=4)
|
||||
file.truncate()
|
||||
print_success(f"Added or updated configuration: {configuration_key}")
|
||||
except json.JSONDecodeError:
|
||||
print_error(f"Failed to update JSON for key {configuration_key}.")
|
||||
|
||||
def install_recommended_extensions() -> None:
|
||||
if not os.path.isfile(VSCODE_EXTENSIONS_JSON_FILE):
|
||||
print_error(
|
||||
f"The extensions.json file does not exist in the path: {VSCODE_EXTENSIONS_JSON_FILE}."
|
||||
)
|
||||
return
|
||||
with open(VSCODE_EXTENSIONS_JSON_FILE, 'r', encoding='utf-8') as file:
|
||||
json_content: str = remove_json_comments(file.read())
|
||||
try:
|
||||
data: dict = json.loads(json_content)
|
||||
extensions: list[str] = data.get("recommendations", [])
|
||||
if not extensions:
|
||||
print_skip(f"No recommendations found in the {VSCODE_EXTENSIONS_JSON_FILE} file.")
|
||||
return
|
||||
vscode_cli_path = which('code') # More reliable than using `code`, especially on Windows.
|
||||
if vscode_cli_path is None:
|
||||
print_error('Visual Studio Code CLI (`code`) tool not found.')
|
||||
return
|
||||
install_vscode_extensions(vscode_cli_path, extensions)
|
||||
except json.JSONDecodeError:
|
||||
print_error(f"Invalid JSON in {VSCODE_EXTENSIONS_JSON_FILE}")
|
||||
|
||||
def remove_json_comments(json_like: str) -> str:
|
||||
pattern: str = r'(?:"(?:\\.|[^"\\])*"|/\*[\s\S]*?\*/|//.*)|([^:]//.*$)'
|
||||
return re.sub(
|
||||
pattern,
|
||||
lambda m: '' if m.group(1) else m.group(0), json_like, flags=re.MULTILINE,
|
||||
)
|
||||
|
||||
def install_vscode_extensions(vscode_cli_path: str, extensions: list[str]) -> None:
|
||||
successful_installations = 0
|
||||
for ext in extensions:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[vscode_cli_path, "--install-extension", ext],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if "already installed" in result.stdout:
|
||||
print_skip(f"Created or verified directory: {ext}")
|
||||
else:
|
||||
print_success(f"Installed extension: {ext}")
|
||||
successful_installations += 1
|
||||
print_subprocess_output(result)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print_subprocess_output(e)
|
||||
print_error(f"Failed to install extension: {ext}")
|
||||
except FileNotFoundError:
|
||||
print_error(' '.join([
|
||||
f"Visual Studio Code CLI tool not found: {vscode_cli_path}."
|
||||
f"Could not install extension: {ext}",
|
||||
]))
|
||||
total_extensions = len(extensions)
|
||||
print_installation_results(successful_installations, total_extensions)
|
||||
|
||||
def print_subprocess_output(result: subprocess.CompletedProcess[str]) -> None:
|
||||
output = '\n'.join([text.strip() for text in [result.stdout, result.stderr] if text])
|
||||
if not output:
|
||||
return
|
||||
formatted_output = '\t' + output.strip().replace('\n', '\n\t')
|
||||
print(formatted_output)
|
||||
|
||||
def print_installation_results(successful_installations: int, total_extensions: int) -> None:
|
||||
if successful_installations == total_extensions:
|
||||
print_success(
|
||||
f"Successfully installed or verified all {total_extensions} recommended extensions."
|
||||
)
|
||||
elif successful_installations > 0:
|
||||
print_warning(
|
||||
f"Partially successful: Installed or verified {successful_installations} "
|
||||
f"out of {total_extensions} recommended extensions."
|
||||
)
|
||||
else:
|
||||
print_error("Failed to install any of the recommended extensions.")
|
||||
|
||||
def print_error(message: str) -> None:
|
||||
print(f"💀 Error: {message}", file=sys.stderr)
|
||||
|
||||
def print_success(message: str) -> None:
|
||||
print(f"✅ Success: {message}")
|
||||
|
||||
def print_skip(message: str) -> None:
|
||||
print(f"⏩ Skipped: {message}")
|
||||
|
||||
def print_warning(message: str) -> None:
|
||||
print(f"⚠️ Warning: {message}", file=sys.stderr)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
import { resolve, join } from 'path';
|
||||
import { rm, mkdtemp, stat } from 'fs/promises';
|
||||
import { spawn } from 'child_process';
|
||||
import { URL, fileURLToPath } from 'url';
|
||||
import { resolve, join } from 'node:path';
|
||||
import { rm, mkdtemp, stat } from 'node:fs/promises';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { URL, fileURLToPath } from 'node:url';
|
||||
|
||||
class Paths {
|
||||
constructor(selfDirectory) {
|
||||
|
||||
@@ -35,10 +35,10 @@ Note:
|
||||
Example: npm run install-deps -- --fresh --non-deterministic
|
||||
*/
|
||||
|
||||
import { exec } from 'child_process';
|
||||
import { resolve } from 'path';
|
||||
import { access, rm, unlink } from 'fs/promises';
|
||||
import { constants } from 'fs';
|
||||
import { exec } from 'node:child_process';
|
||||
import { resolve } from 'node:path';
|
||||
import { access, rm, unlink } from 'node:fs/promises';
|
||||
import { constants } from 'node:fs';
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
const RETRY_DELAY_IN_MS = 5 /* seconds */ * 1000;
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
* --web Path for the web application
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
const DIST_DIRS_JSON_FILE_PATH = resolve(process.cwd(), 'dist-dirs.json'); // cannot statically import because ESLint does not support it https://github.com/eslint/eslint/discussions/15305
|
||||
const CLI_ARGUMENTS = process.argv.slice(2);
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
* --web Verify artifacts for the web application.
|
||||
*/
|
||||
|
||||
import { access, readdir } from 'fs/promises';
|
||||
import { exec } from 'child_process';
|
||||
import { resolve } from 'path';
|
||||
import { access, readdir } from 'node:fs/promises';
|
||||
import { exec } from 'node:child_process';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const PROCESS_ARGUMENTS = process.argv.slice(2);
|
||||
const PRINT_DIST_DIR_SCRIPT_BASE_COMMAND = 'node scripts/print-dist-dir';
|
||||
|
||||
@@ -14,3 +14,35 @@ export type ConstructorArguments<T> =
|
||||
export type FunctionKeys<T> = {
|
||||
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
export function isString(value: unknown): value is string {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
export function isNumber(value: unknown): value is number {
|
||||
return typeof value === 'number';
|
||||
}
|
||||
|
||||
export function isBoolean(value: unknown): value is boolean {
|
||||
return typeof value === 'boolean';
|
||||
}
|
||||
|
||||
export function isFunction(value: unknown): value is (...args: unknown[]) => unknown {
|
||||
return typeof value === 'function';
|
||||
}
|
||||
|
||||
export function isArray(value: unknown): value is Array<unknown> {
|
||||
return Array.isArray(value);
|
||||
}
|
||||
|
||||
export function isPlainObject(
|
||||
variable: unknown,
|
||||
): variable is object & Record<string, unknown> {
|
||||
return Boolean(variable) // the data type of null is an object
|
||||
&& typeof variable === 'object'
|
||||
&& !Array.isArray(variable);
|
||||
}
|
||||
|
||||
export function isNullOrUndefined(value: unknown): value is (null | undefined) {
|
||||
return typeof value === 'undefined' || value === null;
|
||||
}
|
||||
|
||||
37
src/application/CodeRunner/CodeRunner.ts
Normal file
37
src/application/CodeRunner/CodeRunner.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export interface CodeRunner {
|
||||
runCode(
|
||||
code: string,
|
||||
fileExtension: string,
|
||||
): Promise<CodeRunOutcome>;
|
||||
}
|
||||
|
||||
export type CodeRunOutcome = SuccessfulCodeRun | FailedCodeRun;
|
||||
|
||||
export type CodeRunErrorType =
|
||||
| 'FileWriteError'
|
||||
| 'FileReadbackVerificationError'
|
||||
| 'FilePathGenerationError'
|
||||
| 'UnsupportedOperatingSystem'
|
||||
| 'FileExecutionError'
|
||||
| 'DirectoryCreationError'
|
||||
| 'UnexpectedError';
|
||||
|
||||
interface CodeRunStatus {
|
||||
readonly success: boolean;
|
||||
readonly error?: CodeRunError;
|
||||
}
|
||||
|
||||
interface SuccessfulCodeRun extends CodeRunStatus {
|
||||
readonly success: true;
|
||||
readonly error?: undefined;
|
||||
}
|
||||
|
||||
export interface FailedCodeRun extends CodeRunStatus {
|
||||
readonly success: false;
|
||||
readonly error: CodeRunError;
|
||||
}
|
||||
|
||||
export interface CodeRunError {
|
||||
readonly type: CodeRunErrorType;
|
||||
readonly message: string;
|
||||
}
|
||||
1
src/application/CodeRunner/ScriptFilename.ts
Normal file
1
src/application/CodeRunner/ScriptFilename.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const ScriptFilename = 'privacy-script' as const;
|
||||
@@ -1,3 +1,5 @@
|
||||
import { isFunction } from '@/TypeHelpers';
|
||||
|
||||
/*
|
||||
Provides a unified and resilient way to extend errors across platforms.
|
||||
|
||||
@@ -50,8 +52,3 @@ function ensureStackTrace(target: Error) {
|
||||
}
|
||||
captureStackTrace(target, target.constructor);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
function isFunction(func: unknown): func is Function {
|
||||
return typeof func === 'function';
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { isString } from '@/TypeHelpers';
|
||||
|
||||
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
|
||||
export type EnumType = number | string;
|
||||
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType>
|
||||
@@ -23,7 +25,7 @@ function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
||||
if (!value) {
|
||||
throw new Error(`missing ${enumName}`);
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
if (!isString(value)) {
|
||||
throw new Error(`unexpected type of ${enumName}: "${typeof value}"`);
|
||||
}
|
||||
const casedValue = getEnumNames(enumVariable)
|
||||
@@ -40,7 +42,7 @@ export function getEnumNames
|
||||
): string[] {
|
||||
return Object
|
||||
.values(enumVariable)
|
||||
.filter((enumMember) => typeof enumMember === 'string') as string[];
|
||||
.filter((enumMember): enumMember is string => isString(enumMember));
|
||||
}
|
||||
|
||||
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Timer, TimeoutType } from './Timer';
|
||||
import { PlatformTimer } from './PlatformTimer';
|
||||
|
||||
export type CallbackType = (..._: unknown[]) => void;
|
||||
export type CallbackType = (..._: readonly unknown[]) => void;
|
||||
|
||||
export function throttle(
|
||||
callback: CallbackType,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import { IApplicationFactory } from '../IApplicationFactory';
|
||||
import { ApplicationFactory } from '../ApplicationFactory';
|
||||
import { ApplicationContext } from './ApplicationContext';
|
||||
|
||||
export async function buildContext(
|
||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||
environment = RuntimeEnvironment.CurrentEnvironment,
|
||||
environment = CurrentEnvironment,
|
||||
): Promise<IApplicationContext> {
|
||||
const app = await factory.getApp();
|
||||
const os = getInitialOs(app, environment.os);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
||||
import { isString, isArray } from '@/TypeHelpers';
|
||||
|
||||
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
||||
const { docs } = documentable;
|
||||
@@ -14,11 +15,9 @@ function addDocs(
|
||||
docs: DocumentationData,
|
||||
container: DocumentationContainer,
|
||||
): DocumentationContainer {
|
||||
if (docs instanceof Array) {
|
||||
if (docs.length > 0) {
|
||||
container.addParts(docs);
|
||||
}
|
||||
} else if (typeof docs === 'string') {
|
||||
if (isArray(docs)) {
|
||||
docs.forEach((doc) => container.addPart(doc));
|
||||
} else if (isString(docs)) {
|
||||
container.addPart(docs);
|
||||
} else {
|
||||
throwInvalidType();
|
||||
@@ -29,27 +28,21 @@ function addDocs(
|
||||
class DocumentationContainer {
|
||||
private readonly parts = new Array<string>();
|
||||
|
||||
public addPart(documentation: string) {
|
||||
public addPart(documentation: unknown): void {
|
||||
if (!documentation) {
|
||||
throw Error('missing documentation');
|
||||
}
|
||||
if (typeof documentation !== 'string') {
|
||||
if (!isString(documentation)) {
|
||||
throwInvalidType();
|
||||
}
|
||||
this.parts.push(documentation);
|
||||
}
|
||||
|
||||
public addParts(parts: readonly string[]) {
|
||||
for (const part of parts) {
|
||||
this.addPart(part);
|
||||
}
|
||||
}
|
||||
|
||||
public getAll(): ReadonlyArray<string> {
|
||||
return this.parts;
|
||||
}
|
||||
}
|
||||
|
||||
function throwInvalidType() {
|
||||
function throwInvalidType(): never {
|
||||
throw new Error('docs field (documentation) must be an array of strings');
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isString } from '@/TypeHelpers';
|
||||
import { INodeDataErrorContext, NodeDataError } from './NodeDataError';
|
||||
import { NodeData } from './NodeData';
|
||||
|
||||
@@ -13,7 +14,7 @@ export class NodeValidator {
|
||||
'missing name',
|
||||
)
|
||||
.assert(
|
||||
() => typeof nameValue === 'string',
|
||||
() => isString(nameValue),
|
||||
`Name (${JSON.stringify(nameValue)}) is not a string but ${typeof nameValue}.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from '@/application/collections/';
|
||||
import { isArray, isPlainObject } from '@/TypeHelpers';
|
||||
import { FunctionCall } from './FunctionCall';
|
||||
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
|
||||
import { FunctionCallArgument } from './Argument/FunctionCallArgument';
|
||||
@@ -10,13 +11,13 @@ export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] {
|
||||
}
|
||||
|
||||
function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
|
||||
if (typeof calls !== 'object') {
|
||||
throw new Error('called function(s) must be an object');
|
||||
if (!isPlainObject(calls) && !isArray(calls)) {
|
||||
throw new Error('called function(s) must be an object or array');
|
||||
}
|
||||
if (calls instanceof Array) {
|
||||
if (isArray(calls)) {
|
||||
return calls as FunctionCallData[];
|
||||
}
|
||||
const singleCall = calls;
|
||||
const singleCall = calls as FunctionCallData;
|
||||
return [singleCall];
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValida
|
||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
||||
import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||
import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers';
|
||||
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
||||
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||
@@ -121,8 +122,11 @@ function ensureEitherCallOrCodeIsDefined(holders: readonly FunctionData[]) {
|
||||
}
|
||||
|
||||
function ensureExpectedParametersType(functions: readonly FunctionData[]) {
|
||||
const hasValidParameters = (
|
||||
func: FunctionData,
|
||||
) => isNullOrUndefined(func.parameters) || isArrayOfObjects(func.parameters);
|
||||
const unexpectedFunctions = functions
|
||||
.filter((func) => func.parameters && !isArrayOfObjects(func.parameters));
|
||||
.filter((func) => !hasValidParameters(func));
|
||||
if (unexpectedFunctions.length) {
|
||||
const errorMessage = `parameters must be an array of objects in function(s) ${printNames(unexpectedFunctions)}`;
|
||||
throw new Error(errorMessage);
|
||||
@@ -130,8 +134,7 @@ function ensureExpectedParametersType(functions: readonly FunctionData[]) {
|
||||
}
|
||||
|
||||
function isArrayOfObjects(value: unknown): boolean {
|
||||
return Array.isArray(value)
|
||||
&& value.every((item) => typeof item === 'object');
|
||||
return isArray(value) && value.every((item) => isPlainObject(item));
|
||||
}
|
||||
|
||||
function printNames(holders: readonly FunctionData[]) {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export interface ScriptDiagnosticsCollector {
|
||||
collectDiagnosticInformation(): Promise<ScriptDiagnosticData>;
|
||||
}
|
||||
|
||||
export interface ScriptDiagnosticData {
|
||||
readonly scriptsDirectoryAbsolutePath?: string;
|
||||
readonly currentOperatingSystem?: OperatingSystem;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,46 +0,0 @@
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { getWindowInjectedSystemOperations } from './SystemOperations/WindowInjectedSystemOperations';
|
||||
|
||||
export class CodeRunner {
|
||||
constructor(
|
||||
private readonly system = getWindowInjectedSystemOperations(),
|
||||
private readonly environment = RuntimeEnvironment.CurrentEnvironment,
|
||||
) { }
|
||||
|
||||
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
|
||||
const { os } = this.environment;
|
||||
if (os === undefined) {
|
||||
throw new Error('Unidentified operating system');
|
||||
}
|
||||
const dir = this.system.location.combinePaths(
|
||||
this.system.operatingSystem.getTempDirectory(),
|
||||
folderName,
|
||||
);
|
||||
await this.system.fileSystem.createDirectory(dir, true);
|
||||
const filePath = this.system.location.combinePaths(dir, `run.${fileExtension}`);
|
||||
await this.system.fileSystem.writeToFile(filePath, code);
|
||||
await this.system.fileSystem.setFilePermissions(filePath, '755');
|
||||
const command = getExecuteCommand(filePath, os);
|
||||
this.system.command.execute(command);
|
||||
}
|
||||
}
|
||||
|
||||
function getExecuteCommand(
|
||||
scriptPath: string,
|
||||
currentOperatingSystem: OperatingSystem,
|
||||
): string {
|
||||
switch (currentOperatingSystem) {
|
||||
case OperatingSystem.Linux:
|
||||
return `x-terminal-emulator -e '${scriptPath}'`;
|
||||
case OperatingSystem.macOS:
|
||||
return `open -a Terminal.app ${scriptPath}`;
|
||||
// Another option with graphical sudo would be
|
||||
// `osascript -e "do shell script \\"${scriptPath}\\" with administrator privileges"`
|
||||
// However it runs in background
|
||||
case OperatingSystem.Windows:
|
||||
return scriptPath;
|
||||
default:
|
||||
throw Error(`unsupported os: ${OperatingSystem[currentOperatingSystem]}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import { CodeRunError, CodeRunErrorType } from '@/application/CodeRunner/CodeRunner';
|
||||
import { SystemOperations } from '../../System/SystemOperations';
|
||||
import { NodeElectronSystemOperations } from '../../System/NodeElectronSystemOperations';
|
||||
import { ScriptDirectoryOutcome, ScriptDirectoryProvider } from './ScriptDirectoryProvider';
|
||||
|
||||
export const ExecutionSubdirectory = 'runs';
|
||||
|
||||
/**
|
||||
* Provides a dedicated directory for script execution.
|
||||
* Benefits of using a persistent directory:
|
||||
* - Antivirus Exclusions: Easier antivirus configuration.
|
||||
* - Auditability: Stores script execution history for troubleshooting.
|
||||
* - Reliability: Avoids issues with directory clean-ups during execution,
|
||||
* seen in Windows Pro Azure VMs when stored on Windows temporary directory.
|
||||
*/
|
||||
export class PersistentDirectoryProvider implements ScriptDirectoryProvider {
|
||||
constructor(
|
||||
private readonly system: SystemOperations = new NodeElectronSystemOperations(),
|
||||
private readonly logger: Logger = ElectronLogger,
|
||||
) { }
|
||||
|
||||
public async provideScriptDirectory(): Promise<ScriptDirectoryOutcome> {
|
||||
const {
|
||||
success: isPathConstructed,
|
||||
error: pathConstructionError,
|
||||
directoryPath,
|
||||
} = this.constructScriptDirectoryPath();
|
||||
if (!isPathConstructed) {
|
||||
return {
|
||||
success: false,
|
||||
error: pathConstructionError,
|
||||
};
|
||||
}
|
||||
const {
|
||||
success: isDirectoryCreated,
|
||||
error: directoryCreationError,
|
||||
} = await this.createDirectory(directoryPath);
|
||||
if (!isDirectoryCreated) {
|
||||
return {
|
||||
success: false,
|
||||
error: directoryCreationError,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
directoryAbsolutePath: directoryPath,
|
||||
};
|
||||
}
|
||||
|
||||
private async createDirectory(directoryPath: string): Promise<DirectoryPathCreationOutcome> {
|
||||
try {
|
||||
this.logger.info(`Attempting to create script directory at path: ${directoryPath}`);
|
||||
await this.system.fileSystem.createDirectory(directoryPath, true);
|
||||
this.logger.info(`Script directory successfully created at: ${directoryPath}`);
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: this.handleException(error, 'DirectoryCreationError'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private constructScriptDirectoryPath(): DirectoryPathConstructionOutcome {
|
||||
try {
|
||||
const parentDirectory = this.system.operatingSystem.getUserDataDirectory();
|
||||
const scriptDirectory = this.system.location.combinePaths(
|
||||
parentDirectory,
|
||||
ExecutionSubdirectory,
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
directoryPath: scriptDirectory,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: this.handleException(error, 'DirectoryCreationError'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private handleException(
|
||||
exception: Error,
|
||||
errorType: CodeRunErrorType,
|
||||
): CodeRunError {
|
||||
const errorMessage = 'Error during script directory creation';
|
||||
this.logger.error(errorType, errorMessage, exception);
|
||||
return {
|
||||
type: errorType,
|
||||
message: `${errorMessage}: ${exception.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type DirectoryPathConstructionOutcome = {
|
||||
readonly success: false;
|
||||
readonly error: CodeRunError;
|
||||
readonly directoryPath?: undefined;
|
||||
} | {
|
||||
readonly success: true;
|
||||
readonly directoryPath: string;
|
||||
readonly error?: undefined;
|
||||
};
|
||||
|
||||
type DirectoryPathCreationOutcome = {
|
||||
readonly success: false;
|
||||
readonly error: CodeRunError;
|
||||
} | {
|
||||
readonly success: true;
|
||||
readonly error?: undefined;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { CodeRunError } from '@/application/CodeRunner/CodeRunner';
|
||||
|
||||
export interface ScriptDirectoryProvider {
|
||||
provideScriptDirectory(): Promise<ScriptDirectoryOutcome>;
|
||||
}
|
||||
|
||||
export type ScriptDirectoryOutcome = SuccessfulDirectoryCreation | FailedDirectoryCreation;
|
||||
|
||||
interface ScriptDirectoryCreationStatus {
|
||||
readonly success: boolean;
|
||||
readonly directoryAbsolutePath?: string;
|
||||
readonly error?: CodeRunError;
|
||||
}
|
||||
|
||||
interface SuccessfulDirectoryCreation extends ScriptDirectoryCreationStatus {
|
||||
readonly success: true;
|
||||
readonly directoryAbsolutePath: string;
|
||||
}
|
||||
|
||||
interface FailedDirectoryCreation extends ScriptDirectoryCreationStatus {
|
||||
readonly success: false;
|
||||
readonly error: CodeRunError;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ScriptFilenameParts } from '../ScriptFileCreator';
|
||||
|
||||
export interface FilenameGenerator {
|
||||
generateFilename(scriptFilenameParts: ScriptFilenameParts): string;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ScriptFilenameParts } from '../ScriptFileCreator';
|
||||
import { FilenameGenerator } from './FilenameGenerator';
|
||||
|
||||
export class TimestampedFilenameGenerator implements FilenameGenerator {
|
||||
public generateFilename(
|
||||
scriptFilenameParts: ScriptFilenameParts,
|
||||
date = new Date(),
|
||||
): string {
|
||||
validateScriptFilenameParts(scriptFilenameParts);
|
||||
const baseFilename = `${createTimeStampForFile(date)}-${scriptFilenameParts.scriptName}`;
|
||||
return scriptFilenameParts.scriptFileExtension ? `${baseFilename}.${scriptFilenameParts.scriptFileExtension}` : baseFilename;
|
||||
}
|
||||
}
|
||||
|
||||
/** Generates a timestamp for the filename in 'YYYY-MM-DD_HH-MM-SS' format. */
|
||||
function createTimeStampForFile(date: Date): string {
|
||||
return date
|
||||
.toISOString()
|
||||
.replace(/T/, '_')
|
||||
.replace(/:/g, '-')
|
||||
.replace(/\..+/, '');
|
||||
}
|
||||
|
||||
function validateScriptFilenameParts(scriptFilenameParts: ScriptFilenameParts) {
|
||||
if (!scriptFilenameParts.scriptName) {
|
||||
throw new Error('Script name is required but not provided.');
|
||||
}
|
||||
if (scriptFilenameParts.scriptFileExtension?.startsWith('.')) {
|
||||
throw new Error('File extension should not start with a dot.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { CodeRunError, CodeRunErrorType } from '@/application/CodeRunner/CodeRunner';
|
||||
import { FileReadbackVerificationErrors, ReadbackFileWriter } from '@/infrastructure/ReadbackFileWriter/ReadbackFileWriter';
|
||||
import { NodeReadbackFileWriter } from '@/infrastructure/ReadbackFileWriter/NodeReadbackFileWriter';
|
||||
import { SystemOperations } from '../System/SystemOperations';
|
||||
import { NodeElectronSystemOperations } from '../System/NodeElectronSystemOperations';
|
||||
import { FilenameGenerator } from './Filename/FilenameGenerator';
|
||||
import { ScriptFilenameParts, ScriptFileCreator, ScriptFileCreationOutcome } from './ScriptFileCreator';
|
||||
import { TimestampedFilenameGenerator } from './Filename/TimestampedFilenameGenerator';
|
||||
import { ScriptDirectoryProvider } from './Directory/ScriptDirectoryProvider';
|
||||
import { PersistentDirectoryProvider } from './Directory/PersistentDirectoryProvider';
|
||||
|
||||
export class ScriptFileCreationOrchestrator implements ScriptFileCreator {
|
||||
constructor(
|
||||
private readonly system: SystemOperations = new NodeElectronSystemOperations(),
|
||||
private readonly filenameGenerator: FilenameGenerator = new TimestampedFilenameGenerator(),
|
||||
private readonly directoryProvider: ScriptDirectoryProvider = new PersistentDirectoryProvider(),
|
||||
private readonly fileWriter: ReadbackFileWriter = new NodeReadbackFileWriter(),
|
||||
private readonly logger: Logger = ElectronLogger,
|
||||
) { }
|
||||
|
||||
public async createScriptFile(
|
||||
contents: string,
|
||||
scriptFilenameParts: ScriptFilenameParts,
|
||||
): Promise<ScriptFileCreationOutcome> {
|
||||
const {
|
||||
success: isDirectoryCreated, error: directoryCreationError, directoryAbsolutePath,
|
||||
} = await this.directoryProvider.provideScriptDirectory();
|
||||
if (!isDirectoryCreated) {
|
||||
return createFailure(directoryCreationError);
|
||||
}
|
||||
const {
|
||||
success: isFilePathConstructed, error: filePathGenerationError, filePath,
|
||||
} = this.constructFilePath(scriptFilenameParts, directoryAbsolutePath);
|
||||
if (!isFilePathConstructed) {
|
||||
return createFailure(filePathGenerationError);
|
||||
}
|
||||
const {
|
||||
success: isFileCreated, error: fileCreationError,
|
||||
} = await this.writeFile(filePath, contents);
|
||||
if (!isFileCreated) {
|
||||
return createFailure(fileCreationError);
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
scriptFileAbsolutePath: filePath,
|
||||
};
|
||||
}
|
||||
|
||||
private constructFilePath(
|
||||
scriptFilenameParts: ScriptFilenameParts,
|
||||
directoryPath: string,
|
||||
): FilePathConstructionOutcome {
|
||||
try {
|
||||
const filename = this.filenameGenerator.generateFilename(scriptFilenameParts);
|
||||
const filePath = this.system.location.combinePaths(directoryPath, filename);
|
||||
return { success: true, filePath };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: this.handleException(error, 'FilePathGenerationError'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async writeFile(
|
||||
filePath: string,
|
||||
contents: string,
|
||||
): Promise<FileWriteOutcome> {
|
||||
const {
|
||||
success, error,
|
||||
} = await this.fileWriter.writeAndVerifyFile(filePath, contents);
|
||||
if (success) {
|
||||
return { success: true };
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: error.message,
|
||||
type: FileReadbackVerificationErrors.find((e) => e === error.type) ? 'FileReadbackVerificationError' : 'FileWriteError',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private handleException(
|
||||
exception: Error,
|
||||
errorType: CodeRunErrorType,
|
||||
): CodeRunError {
|
||||
const errorMessage = 'Error during script file operation';
|
||||
this.logger.error(errorType, errorMessage, exception);
|
||||
return {
|
||||
type: errorType,
|
||||
message: `${errorMessage}: ${exception.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createFailure(error: CodeRunError): ScriptFileCreationOutcome {
|
||||
return {
|
||||
success: false,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
type FileWriteOutcome = {
|
||||
readonly success: true;
|
||||
readonly error?: undefined;
|
||||
} | {
|
||||
readonly success: false;
|
||||
readonly error: CodeRunError;
|
||||
};
|
||||
|
||||
type FilePathConstructionOutcome = {
|
||||
readonly success: true;
|
||||
readonly filePath: string;
|
||||
readonly error?: undefined;
|
||||
} | {
|
||||
readonly success: false;
|
||||
readonly filePath?: undefined;
|
||||
readonly error: CodeRunError;
|
||||
};
|
||||
31
src/infrastructure/CodeRunner/Creation/ScriptFileCreator.ts
Normal file
31
src/infrastructure/CodeRunner/Creation/ScriptFileCreator.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { CodeRunError } from '@/application/CodeRunner/CodeRunner';
|
||||
|
||||
export interface ScriptFileCreator {
|
||||
createScriptFile(
|
||||
contents: string,
|
||||
scriptFilenameParts: ScriptFilenameParts,
|
||||
): Promise<ScriptFileCreationOutcome>;
|
||||
}
|
||||
|
||||
export interface ScriptFilenameParts {
|
||||
readonly scriptName: string;
|
||||
readonly scriptFileExtension: string | undefined;
|
||||
}
|
||||
|
||||
export type ScriptFileCreationOutcome = SuccessfulScriptCreation | FailedScriptCreation;
|
||||
|
||||
interface ScriptFileCreationStatus {
|
||||
readonly success: boolean;
|
||||
readonly error?: CodeRunError;
|
||||
readonly scriptFileAbsolutePath?: string;
|
||||
}
|
||||
|
||||
interface SuccessfulScriptCreation extends ScriptFileCreationStatus {
|
||||
readonly success: true;
|
||||
readonly scriptFileAbsolutePath: string;
|
||||
}
|
||||
|
||||
interface FailedScriptCreation extends ScriptFileCreationStatus {
|
||||
readonly success: false;
|
||||
readonly error: CodeRunError;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { CodeRunError } from '@/application/CodeRunner/CodeRunner';
|
||||
|
||||
export interface ScriptFileExecutor {
|
||||
executeScriptFile(filePath: string): Promise<ScriptFileExecutionOutcome>;
|
||||
}
|
||||
|
||||
export type ScriptFileExecutionOutcome = SuccessfulScriptFileExecution | FailedScriptFileExecution;
|
||||
|
||||
interface ScriptFileExecutionStatus {
|
||||
readonly success: boolean;
|
||||
readonly error?: CodeRunError;
|
||||
}
|
||||
|
||||
interface SuccessfulScriptFileExecution extends ScriptFileExecutionStatus {
|
||||
readonly success: true;
|
||||
readonly error?: undefined;
|
||||
}
|
||||
|
||||
export interface FailedScriptFileExecution extends ScriptFileExecutionStatus {
|
||||
readonly success: false;
|
||||
readonly error: CodeRunError;
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { CommandOps, SystemOperations } from '@/infrastructure/CodeRunner/System/SystemOperations';
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { NodeElectronSystemOperations } from '@/infrastructure/CodeRunner/System/NodeElectronSystemOperations';
|
||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import { CodeRunErrorType } from '@/application/CodeRunner/CodeRunner';
|
||||
import { isString } from '@/TypeHelpers';
|
||||
import { FailedScriptFileExecution, ScriptFileExecutionOutcome, ScriptFileExecutor } from './ScriptFileExecutor';
|
||||
|
||||
export class VisibleTerminalScriptExecutor implements ScriptFileExecutor {
|
||||
constructor(
|
||||
private readonly system: SystemOperations = new NodeElectronSystemOperations(),
|
||||
private readonly logger: Logger = ElectronLogger,
|
||||
private readonly environment: RuntimeEnvironment = CurrentEnvironment,
|
||||
) { }
|
||||
|
||||
public async executeScriptFile(filePath: string): Promise<ScriptFileExecutionOutcome> {
|
||||
const { os } = this.environment;
|
||||
if (os === undefined) {
|
||||
return this.handleError('UnsupportedOperatingSystem', 'Operating system could not be identified from environment.');
|
||||
}
|
||||
const filePermissionsResult = await this.setFileExecutablePermissions(filePath);
|
||||
if (!filePermissionsResult.success) {
|
||||
return filePermissionsResult;
|
||||
}
|
||||
const scriptExecutionResult = await this.runFileWithRunner(filePath, os);
|
||||
if (!scriptExecutionResult.success) {
|
||||
return scriptExecutionResult;
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
private async setFileExecutablePermissions(
|
||||
filePath: string,
|
||||
): Promise<ScriptFileExecutionOutcome> {
|
||||
/*
|
||||
This is required on macOS and Linux otherwise the terminal emulators will refuse to
|
||||
execute the script. It's not needed on Windows.
|
||||
*/
|
||||
try {
|
||||
this.logger.info(`Setting execution permissions for file at ${filePath}`);
|
||||
await this.system.fileSystem.setFilePermissions(filePath, '755');
|
||||
this.logger.info(`Execution permissions set successfully for ${filePath}`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return this.handleError('FileExecutionError', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async runFileWithRunner(
|
||||
filePath: string,
|
||||
os: OperatingSystem,
|
||||
): Promise<ScriptFileExecutionOutcome> {
|
||||
this.logger.info(`Executing script file: ${filePath} on ${OperatingSystem[os]}.`);
|
||||
const runner = TerminalRunners[os];
|
||||
if (!runner) {
|
||||
return this.handleError('UnsupportedOperatingSystem', `Unsupported operating system: ${OperatingSystem[os]}`);
|
||||
}
|
||||
const context: TerminalExecutionContext = {
|
||||
scriptFilePath: filePath,
|
||||
commandOps: this.system.command,
|
||||
logger: this.logger,
|
||||
};
|
||||
try {
|
||||
await runner(context);
|
||||
this.logger.info('Command script file successfully.');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return this.handleError('FileExecutionError', error);
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(
|
||||
type: CodeRunErrorType,
|
||||
error: Error | string,
|
||||
): FailedScriptFileExecution {
|
||||
const errorMessage = 'Error during script file execution';
|
||||
this.logger.error([type, errorMessage, ...(error ? [error] : [])]);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
type,
|
||||
message: `${errorMessage}: ${isString(error) ? error : errorMessage}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface TerminalExecutionContext {
|
||||
readonly scriptFilePath: string;
|
||||
readonly commandOps: CommandOps;
|
||||
readonly logger: Logger;
|
||||
}
|
||||
|
||||
type TerminalRunner = (context: TerminalExecutionContext) => Promise<void>;
|
||||
|
||||
export const LinuxTerminalEmulator = 'x-terminal-emulator';
|
||||
|
||||
const TerminalRunners: Partial<Record<OperatingSystem, TerminalRunner>> = {
|
||||
[OperatingSystem.Windows]: async (context) => {
|
||||
const command = [
|
||||
'PowerShell',
|
||||
'Start-Process',
|
||||
'-Verb RunAs', // Run as administrator with GUI sudo prompt
|
||||
`-FilePath ${cmdShellPathArgumentEscape(context.scriptFilePath)}`,
|
||||
].join(' ');
|
||||
/*
|
||||
📝 Options:
|
||||
`child_process.execFile()`
|
||||
"path", `cmd.exe /c "path"`
|
||||
❌ Script execution in the background without a visible terminal.
|
||||
This occurs only when the user runs the application as administrator, as seen
|
||||
in Windows Pro VMs on Azure.
|
||||
`PowerShell Start -Verb RunAs "path"`
|
||||
✅ Visible terminal window
|
||||
✅ GUI sudo prompt (through `RunAs` option)
|
||||
`PowerShell Start "path"`
|
||||
`explorer.exe "path"`
|
||||
`electron.shell.openPath`
|
||||
`start cmd.exe /c "$path"`
|
||||
✅ Visible terminal window
|
||||
✅ GUI sudo prompt (through `RunAs` option)
|
||||
👍 Among all options `start` command is the most explicit one, being the most resilient
|
||||
against the potential changes in Windows or Electron framework (e.g. https://github.com/electron/electron/issues/36765).
|
||||
`%COMSPEC%` environment variable should be checked before defaulting to `cmd.exe.
|
||||
Related docs: https://web.archive.org/web/20240106002357/https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows
|
||||
*/
|
||||
await runCommand(command, context);
|
||||
},
|
||||
[OperatingSystem.Linux]: async (context) => {
|
||||
const command = `${LinuxTerminalEmulator} -e ${posixShellPathArgumentEscape(context.scriptFilePath)}`;
|
||||
/*
|
||||
🤔 Potential improvements:
|
||||
Use user-friendly GUI sudo prompt (not terminal-based).
|
||||
If `pkexec` exists, we could do `x-terminal-emulator -e pkexec 'path'`, which always
|
||||
prompts with user-friendly GUI sudo prompt.
|
||||
📝 Options:
|
||||
`x-terminal-emulator -e 'path'`:
|
||||
✅ Visible terminal window
|
||||
❌ Terminal-based (not GUI) sudo prompt.
|
||||
`x-terminal-emulator -e pkexec 'path'
|
||||
✅ Visible terminal window
|
||||
✅ Always prompts with user-friendly GUI sudo prompt.
|
||||
🤔 Not using `pkexec` as it is not in all Linux distributions. It should have smarter
|
||||
logic to handle if it does not exist.
|
||||
`electron.shell.openPath`:
|
||||
❌ Opens the script in the default text editor, verified on
|
||||
Debian/Ubuntu-based distributions.
|
||||
`child_process.execFile()`:
|
||||
❌ Script execution in the background without a visible terminal.
|
||||
*/
|
||||
await runCommand(command, context);
|
||||
},
|
||||
[OperatingSystem.macOS]: async (context) => {
|
||||
const command = `open -a Terminal.app ${posixShellPathArgumentEscape(context.scriptFilePath)}`;
|
||||
// -a Specifies the application to use for opening the file
|
||||
/* eslint-disable vue/max-len */
|
||||
/*
|
||||
🤔 Potential improvements:
|
||||
Use user-friendly GUI sudo prompt for running the script.
|
||||
📝 Options:
|
||||
`open -a Terminal.app 'path'`
|
||||
✅ Visible terminal window
|
||||
❌ Terminal-based (not GUI) sudo prompt.
|
||||
❌ Terminal app requires many privileges to execute the script, this prompts user
|
||||
to grant privileges to the Terminal app.
|
||||
`osascript -e 'do shell script "'/tmp/test.sh'" with administrator privileges'`
|
||||
✅ Script as root
|
||||
✅ GUI sudo prompt.
|
||||
❌ Script execution in the background without a visible terminal.
|
||||
`osascript -e 'do shell script "open -a 'Terminal.app' '/tmp/test.sh'" with administrator privileges'`
|
||||
❌ Script as user, not root
|
||||
✅ GUI sudo prompt.
|
||||
✅ Visible terminal window
|
||||
`osascript -e 'do shell script "/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal '/tmp/test.sh'" with administrator privileges'`
|
||||
✅ Script as root
|
||||
✅ GUI sudo prompt.
|
||||
✅ Visible terminal window
|
||||
Useful resources about `do shell script .. with administrator privileges`:
|
||||
- Change "osascript wants to make changes" prompt: https://web.archive.org/web/20240109191128/https://apple.stackexchange.com/questions/283353/how-to-rename-osascript-in-the-administrator-privileges-dialog
|
||||
- More about `do shell script`: https://web.archive.org/web/20100906222226/http://developer.apple.com/mac/library/technotes/tn2002/tn2065.html
|
||||
*/
|
||||
/* eslint-enable vue/max-len */
|
||||
await runCommand(command, context);
|
||||
},
|
||||
} as const;
|
||||
|
||||
async function runCommand(command: string, context: TerminalExecutionContext): Promise<void> {
|
||||
context.logger.info(`Executing command:\n${command}`);
|
||||
await context.commandOps.exec(command);
|
||||
context.logger.info('Executed command successfully.');
|
||||
}
|
||||
|
||||
function posixShellPathArgumentEscape(pathArgument: string): string {
|
||||
/*
|
||||
- Wraps the path in single quotes, which is a standard practice in POSIX shells
|
||||
(like bash and zsh) found on macOS/Linux to ensure that characters like spaces, '*', and
|
||||
'?' are treated as literals, not as special characters.
|
||||
- Escapes any single quotes within the path itself. This allows paths containing single
|
||||
quotes to be correctly interpreted in POSIX-compliant systems, such as Linux and macOS.
|
||||
*/
|
||||
return `'${pathArgument.replaceAll('\'', '\'\\\'\'')}'`;
|
||||
}
|
||||
|
||||
function cmdShellPathArgumentEscape(pathArgument: string): string {
|
||||
// - Encloses the path in double quotes, which is necessary for Windows command line (cmd.exe)
|
||||
// to correctly handle paths containing spaces.
|
||||
// - Paths in Windows cannot include double quotes `"` themselves, so these are not escaped.
|
||||
return `"${pathArgument}"`;
|
||||
}
|
||||
57
src/infrastructure/CodeRunner/ScriptFileCodeRunner.ts
Normal file
57
src/infrastructure/CodeRunner/ScriptFileCodeRunner.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { ScriptFilename } from '@/application/CodeRunner/ScriptFilename';
|
||||
import {
|
||||
CodeRunError, CodeRunOutcome, CodeRunner, FailedCodeRun,
|
||||
} from '@/application/CodeRunner/CodeRunner';
|
||||
import { ElectronLogger } from '../Log/ElectronLogger';
|
||||
import { ScriptFileExecutor } from './Execution/ScriptFileExecutor';
|
||||
import { ScriptFileCreator } from './Creation/ScriptFileCreator';
|
||||
import { ScriptFileCreationOrchestrator } from './Creation/ScriptFileCreationOrchestrator';
|
||||
import { VisibleTerminalScriptExecutor } from './Execution/VisibleTerminalScriptFileExecutor';
|
||||
|
||||
export class ScriptFileCodeRunner implements CodeRunner {
|
||||
constructor(
|
||||
private readonly scriptFileExecutor
|
||||
: ScriptFileExecutor = new VisibleTerminalScriptExecutor(),
|
||||
private readonly scriptFileCreator: ScriptFileCreator = new ScriptFileCreationOrchestrator(),
|
||||
private readonly logger: Logger = ElectronLogger,
|
||||
) { }
|
||||
|
||||
public async runCode(
|
||||
code: string,
|
||||
fileExtension: string,
|
||||
): Promise<CodeRunOutcome> {
|
||||
this.logger.info('Initiating script running process.');
|
||||
const {
|
||||
success: isFileCreated, scriptFileAbsolutePath, error: fileCreationError,
|
||||
} = await this.scriptFileCreator.createScriptFile(code, {
|
||||
scriptName: ScriptFilename,
|
||||
scriptFileExtension: fileExtension,
|
||||
});
|
||||
if (!isFileCreated) {
|
||||
return createFailure(fileCreationError);
|
||||
}
|
||||
const {
|
||||
success: isFileSuccessfullyExecuted,
|
||||
error: fileExecutionError,
|
||||
} = await this.scriptFileExecutor.executeScriptFile(
|
||||
scriptFileAbsolutePath,
|
||||
);
|
||||
if (!isFileSuccessfullyExecuted) {
|
||||
return createFailure(fileExecutionError);
|
||||
}
|
||||
this.logger.info(`Successfully ran script at ${scriptFileAbsolutePath}`);
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createFailure(
|
||||
error: CodeRunError,
|
||||
): FailedCodeRun {
|
||||
return {
|
||||
success: false,
|
||||
error,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { join } from 'node:path';
|
||||
import { chmod, mkdir } from 'node:fs/promises';
|
||||
import { exec } from 'node:child_process';
|
||||
import { app } from 'electron/main';
|
||||
import {
|
||||
CommandOps, FileSystemOps, LocationOps, OperatingSystemOps, SystemOperations,
|
||||
} from './SystemOperations';
|
||||
|
||||
export class NodeElectronSystemOperations implements SystemOperations {
|
||||
public readonly operatingSystem: OperatingSystemOps = {
|
||||
/*
|
||||
This method returns the directory for storing app's configuration files.
|
||||
It appends your app's name to the default appData directory.
|
||||
Conventionally, you should store user data files in this directory.
|
||||
However, avoid writing large files here as some environments might back up this directory
|
||||
to cloud storage, potentially causing issues with file size.
|
||||
|
||||
Based on tests it returns:
|
||||
|
||||
- Windows: `%APPDATA%\privacy.sexy`
|
||||
- Linux: `$HOME/.config/privacy.sexy/runs`
|
||||
- macOS: `$HOME/Library/Application Support/privacy.sexy/runs`
|
||||
|
||||
For more details, refer to the Electron documentation: https://web.archive.org/web/20240104154857/https://www.electronjs.org/docs/latest/api/app#appgetpathname
|
||||
*/
|
||||
getUserDataDirectory: () => {
|
||||
return app.getPath('userData');
|
||||
},
|
||||
};
|
||||
|
||||
public readonly location: LocationOps = {
|
||||
combinePaths: (...pathSegments) => join(...pathSegments),
|
||||
};
|
||||
|
||||
public readonly fileSystem: FileSystemOps = {
|
||||
setFilePermissions: (
|
||||
filePath: string,
|
||||
mode: string | number,
|
||||
) => chmod(filePath, mode),
|
||||
createDirectory: async (
|
||||
directoryPath: string,
|
||||
isRecursive?: boolean,
|
||||
) => {
|
||||
await mkdir(directoryPath, { recursive: isRecursive });
|
||||
// Ignoring the return value from `mkdir`, which is the first directory created
|
||||
// when `recursive` is true, or empty return value.
|
||||
// See https://github.com/nodejs/node/pull/31530
|
||||
},
|
||||
};
|
||||
|
||||
public readonly command: CommandOps = {
|
||||
exec: (command) => new Promise((resolve, reject) => {
|
||||
exec(command, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
};
|
||||
}
|
||||
23
src/infrastructure/CodeRunner/System/SystemOperations.ts
Normal file
23
src/infrastructure/CodeRunner/System/SystemOperations.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export interface SystemOperations {
|
||||
readonly operatingSystem: OperatingSystemOps;
|
||||
readonly location: LocationOps;
|
||||
readonly fileSystem: FileSystemOps;
|
||||
readonly command: CommandOps;
|
||||
}
|
||||
|
||||
export interface OperatingSystemOps {
|
||||
getUserDataDirectory(): string;
|
||||
}
|
||||
|
||||
export interface LocationOps {
|
||||
combinePaths(...pathSegments: string[]): string;
|
||||
}
|
||||
|
||||
export interface CommandOps {
|
||||
exec(command: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface FileSystemOps {
|
||||
setFilePermissions(filePath: string, mode: string | number): Promise<void>;
|
||||
createDirectory(directoryPath: string, isRecursive?: boolean): Promise<void>;
|
||||
}
|
||||
30
src/infrastructure/Dialog/Browser/BrowserDialog.ts
Normal file
30
src/infrastructure/Dialog/Browser/BrowserDialog.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Dialog, FileType, SaveFileOutcome } from '@/presentation/common/Dialog';
|
||||
import { FileSaverDialog } from './FileSaverDialog';
|
||||
import { BrowserSaveFileDialog } from './BrowserSaveFileDialog';
|
||||
|
||||
export class BrowserDialog implements Dialog {
|
||||
constructor(
|
||||
private readonly window: WindowDialogAccessor = globalThis.window,
|
||||
private readonly saveFileDialog: BrowserSaveFileDialog = new FileSaverDialog(),
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
public showError(title: string, message: string): void {
|
||||
this.window.alert(`❌ ${title}\n\n${message}`);
|
||||
}
|
||||
|
||||
public saveFile(
|
||||
fileContents: string,
|
||||
defaultFilename: string,
|
||||
type: FileType,
|
||||
): Promise<SaveFileOutcome> {
|
||||
return Promise.resolve(
|
||||
this.saveFileDialog.saveFile(fileContents, defaultFilename, type),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface WindowDialogAccessor {
|
||||
readonly alert: typeof window.alert;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { FileType, SaveFileOutcome } from '@/presentation/common/Dialog';
|
||||
|
||||
export interface BrowserSaveFileDialog {
|
||||
saveFile(
|
||||
fileContents: string,
|
||||
defaultFilename: string,
|
||||
fileType: FileType,
|
||||
): SaveFileOutcome;
|
||||
}
|
||||
42
src/infrastructure/Dialog/Browser/FileSaverDialog.ts
Normal file
42
src/infrastructure/Dialog/Browser/FileSaverDialog.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import fileSaver from 'file-saver';
|
||||
import { FileType, SaveFileOutcome } from '@/presentation/common/Dialog';
|
||||
import { BrowserSaveFileDialog } from './BrowserSaveFileDialog';
|
||||
|
||||
export type SaveAsFunction = (data: Blob, filename?: string) => void;
|
||||
|
||||
export type WindowOpenFunction = (url: string, target: string, features: string) => void;
|
||||
|
||||
export class FileSaverDialog implements BrowserSaveFileDialog {
|
||||
constructor(
|
||||
private readonly fileSaverSaveAs: SaveAsFunction = fileSaver.saveAs,
|
||||
private readonly windowOpen: WindowOpenFunction = window.open.bind(window),
|
||||
) { }
|
||||
|
||||
public saveFile(
|
||||
fileContents: string,
|
||||
defaultFilename: string,
|
||||
fileType: FileType,
|
||||
): SaveFileOutcome {
|
||||
const mimeType = MimeTypes[fileType];
|
||||
this.saveBlob(fileContents, mimeType, defaultFilename);
|
||||
return {
|
||||
success: true, // Exceptions are handled internally
|
||||
};
|
||||
}
|
||||
|
||||
private saveBlob(file: BlobPart, mimeType: string, defaultFilename: string): void {
|
||||
try {
|
||||
const blob = new Blob([file], { type: mimeType });
|
||||
this.fileSaverSaveAs(blob, defaultFilename);
|
||||
} catch (e) {
|
||||
this.windowOpen(`data:${mimeType},${encodeURIComponent(file.toString())}`, '_blank', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MimeTypes: Record<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
|
||||
[FileType.ShellScript]: 'text/x-shellscript', // https://de.wikipedia.org/wiki/Shellskript#MIME-Typ
|
||||
} as const;
|
||||
29
src/infrastructure/Dialog/Electron/ElectronDialog.ts
Normal file
29
src/infrastructure/Dialog/Electron/ElectronDialog.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { dialog } from 'electron/main';
|
||||
import { Dialog, FileType, SaveFileOutcome } from '@/presentation/common/Dialog';
|
||||
import { NodeElectronSaveFileDialog } from './NodeElectronSaveFileDialog';
|
||||
import { ElectronSaveFileDialog } from './ElectronSaveFileDialog';
|
||||
|
||||
export class ElectronDialog implements Dialog {
|
||||
constructor(
|
||||
private readonly saveFileDialog: ElectronSaveFileDialog = new NodeElectronSaveFileDialog(),
|
||||
private readonly electron: ElectronDialogAccessor = {
|
||||
showErrorBox: dialog.showErrorBox.bind(dialog),
|
||||
},
|
||||
) { }
|
||||
|
||||
public saveFile(
|
||||
fileContents: string,
|
||||
defaultFilename: string,
|
||||
type: FileType,
|
||||
): Promise<SaveFileOutcome> {
|
||||
return this.saveFileDialog.saveFile(fileContents, defaultFilename, type);
|
||||
}
|
||||
|
||||
public showError(title: string, message: string): void {
|
||||
this.electron.showErrorBox(title, message);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ElectronDialogAccessor {
|
||||
readonly showErrorBox: typeof dialog.showErrorBox;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { FileType, SaveFileOutcome } from '@/presentation/common/Dialog';
|
||||
|
||||
export interface ElectronSaveFileDialog {
|
||||
saveFile(
|
||||
fileContents: string,
|
||||
defaultFilename: string,
|
||||
type: FileType,
|
||||
): Promise<SaveFileOutcome>;
|
||||
}
|
||||
176
src/infrastructure/Dialog/Electron/NodeElectronSaveFileDialog.ts
Normal file
176
src/infrastructure/Dialog/Electron/NodeElectronSaveFileDialog.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { join } from 'node:path';
|
||||
import { app, dialog } from 'electron/main';
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import {
|
||||
FileType, SaveFileError, SaveFileErrorType, SaveFileOutcome,
|
||||
} from '@/presentation/common/Dialog';
|
||||
import { FileReadbackVerificationErrors, ReadbackFileWriter } from '@/infrastructure/ReadbackFileWriter/ReadbackFileWriter';
|
||||
import { NodeReadbackFileWriter } from '@/infrastructure/ReadbackFileWriter/NodeReadbackFileWriter';
|
||||
import { ElectronSaveFileDialog } from './ElectronSaveFileDialog';
|
||||
|
||||
export class NodeElectronSaveFileDialog implements ElectronSaveFileDialog {
|
||||
constructor(
|
||||
private readonly logger: Logger = ElectronLogger,
|
||||
private readonly electron: ElectronFileDialogOperations = {
|
||||
getUserDownloadsPath: () => app.getPath('downloads'),
|
||||
showSaveDialog: dialog.showSaveDialog.bind(dialog),
|
||||
},
|
||||
private readonly node: NodeFileOperations = { join },
|
||||
private readonly fileWriter: ReadbackFileWriter = new NodeReadbackFileWriter(),
|
||||
) { }
|
||||
|
||||
public async saveFile(
|
||||
fileContents: string,
|
||||
defaultFilename: string,
|
||||
type: FileType,
|
||||
): Promise<SaveFileOutcome> {
|
||||
const {
|
||||
success: isPathConstructed,
|
||||
filePath: defaultFilePath,
|
||||
error: pathConstructionError,
|
||||
} = this.constructDefaultFilePath(defaultFilename);
|
||||
if (!isPathConstructed) {
|
||||
return { success: false, error: pathConstructionError };
|
||||
}
|
||||
const fileDialog = await this.showSaveFileDialog(defaultFilename, defaultFilePath, type);
|
||||
if (!fileDialog.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: fileDialog.error,
|
||||
};
|
||||
}
|
||||
if (fileDialog.canceled) {
|
||||
this.logger.info(`File save cancelled by user: ${defaultFilename}`);
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
const result = await this.writeFile(fileDialog.filePath, fileContents);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async writeFile(
|
||||
filePath: string,
|
||||
fileContents: string,
|
||||
): Promise<SaveFileOutcome> {
|
||||
const {
|
||||
success, error,
|
||||
} = await this.fileWriter.writeAndVerifyFile(filePath, fileContents);
|
||||
if (success) {
|
||||
return { success: true };
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: error.message,
|
||||
type: FileReadbackVerificationErrors.find((e) => e === error.type) ? 'FileReadbackVerificationError' : 'FileCreationError',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async showSaveFileDialog(
|
||||
defaultFilename: string,
|
||||
defaultFilePath: string,
|
||||
type: FileType,
|
||||
): Promise<SaveDialogOutcome> {
|
||||
try {
|
||||
const dialogResult = await this.electron.showSaveDialog({
|
||||
title: defaultFilename,
|
||||
defaultPath: defaultFilePath,
|
||||
filters: getDialogFileFilters(type),
|
||||
properties: [
|
||||
'createDirectory', // Enables directory creation on macOS.
|
||||
'showOverwriteConfirmation', // Shows overwrite confirmation on Linux.
|
||||
],
|
||||
});
|
||||
if (dialogResult.canceled) {
|
||||
return { success: true, canceled: true };
|
||||
}
|
||||
if (!dialogResult.filePath) {
|
||||
return {
|
||||
success: false,
|
||||
error: { type: 'DialogDisplayError', message: 'Unexpected Error: File path is undefined after save dialog completion.' },
|
||||
};
|
||||
}
|
||||
return { success: true, filePath: dialogResult.filePath };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: this.handleException(error, 'DialogDisplayError'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private constructDefaultFilePath(defaultFilename: string): DefaultFilePathConstructionOutcome {
|
||||
try {
|
||||
const downloadsFolder = this.electron.getUserDownloadsPath();
|
||||
const defaultFilePath = this.node.join(downloadsFolder, defaultFilename);
|
||||
return {
|
||||
success: true,
|
||||
filePath: defaultFilePath,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
error: this.handleException(err, 'DialogDisplayError'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private handleException(
|
||||
exception: Error,
|
||||
errorType: SaveFileErrorType,
|
||||
): SaveFileError {
|
||||
const errorMessage = 'Error during saving script file.';
|
||||
this.logger.error(errorType, errorMessage, exception);
|
||||
return {
|
||||
type: errorType,
|
||||
message: `${errorMessage}: ${exception.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface ElectronFileDialogOperations {
|
||||
getUserDownloadsPath(): string;
|
||||
showSaveDialog(options: Electron.SaveDialogOptions): Promise<Electron.SaveDialogReturnValue>;
|
||||
}
|
||||
|
||||
export interface NodeFileOperations {
|
||||
readonly join: typeof join;
|
||||
}
|
||||
|
||||
function getDialogFileFilters(fileType: FileType): Electron.FileFilter[] {
|
||||
const filters = FileTypeSpecificFilters[fileType];
|
||||
return [
|
||||
...filters,
|
||||
{
|
||||
name: 'All Files',
|
||||
extensions: ['*'],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const FileTypeSpecificFilters: Record<FileType, Electron.FileFilter[]> = {
|
||||
[FileType.BatchFile]: [
|
||||
{
|
||||
name: 'Batch Files',
|
||||
extensions: ['bat', 'cmd'],
|
||||
},
|
||||
],
|
||||
[FileType.ShellScript]: [
|
||||
{
|
||||
name: 'Shell Scripts',
|
||||
extensions: ['sh', 'bash', 'zsh'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
type SaveDialogOutcome =
|
||||
| { readonly success: true; readonly filePath: string; readonly canceled?: false }
|
||||
| { readonly success: true; readonly canceled: true }
|
||||
| { readonly success: false; readonly error: SaveFileError; readonly canceled?: false };
|
||||
|
||||
type DefaultFilePathConstructionOutcome =
|
||||
| { readonly success: true; readonly filePath: string; readonly error?: undefined; }
|
||||
| { readonly success: false; readonly filePath?: undefined; readonly error: SaveFileError; };
|
||||
36
src/infrastructure/Dialog/LoggingDialogDecorator.ts
Normal file
36
src/infrastructure/Dialog/LoggingDialogDecorator.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { Dialog, FileType } from '@/presentation/common/Dialog';
|
||||
|
||||
export function decorateWithLogging(
|
||||
dialog: Dialog,
|
||||
logger: Logger,
|
||||
): Dialog {
|
||||
return new LoggingDialogDecorator(dialog, logger);
|
||||
}
|
||||
|
||||
class LoggingDialogDecorator implements Dialog {
|
||||
constructor(
|
||||
private readonly dialog: Dialog,
|
||||
private readonly logger: Logger,
|
||||
) { }
|
||||
|
||||
public async saveFile(
|
||||
fileContents: string,
|
||||
defaultFilename: string,
|
||||
fileType: FileType,
|
||||
) {
|
||||
this.logger.info(`Opening save file dialog with default filename: ${defaultFilename}.`);
|
||||
const dialogResult = await this.dialog.saveFile(fileContents, defaultFilename, fileType);
|
||||
if (dialogResult.success) {
|
||||
this.logger.info('File saving process completed successfully.');
|
||||
} else {
|
||||
this.logger.error('Error encountered while saving the file.', dialogResult.error);
|
||||
}
|
||||
return dialogResult;
|
||||
}
|
||||
|
||||
public showError(title: string, message: string) {
|
||||
this.logger.error(`Showing error dialog: ${title} - ${message}`);
|
||||
this.dialog.showError(title, message);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { isNumber } from '@/TypeHelpers';
|
||||
import { IEntity } from './IEntity';
|
||||
|
||||
export abstract class BaseEntity<TId> implements IEntity<TId> {
|
||||
protected constructor(public id: TId) {
|
||||
if (typeof id !== 'number' && !id) {
|
||||
if (!isNumber(id) && !id) {
|
||||
throw new Error('Id cannot be null or empty');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isBoolean, isFunction } from '@/TypeHelpers';
|
||||
import { IEnvironmentVariables } from './IEnvironmentVariables';
|
||||
|
||||
/* Validation is externalized to keep the environment objects simple */
|
||||
@@ -15,7 +16,7 @@ export function validateEnvironmentVariables(environment: IEnvironmentVariables)
|
||||
function getKeysMissingValues(keyValuePairs: Record<string, unknown>): string[] {
|
||||
return Object.entries(keyValuePairs)
|
||||
.reduce((acc, [key, value]) => {
|
||||
if (!value && typeof value !== 'boolean') {
|
||||
if (!value && !isBoolean(value)) {
|
||||
acc.push(key);
|
||||
}
|
||||
return acc;
|
||||
@@ -38,7 +39,7 @@ function capturePropertyValues(instance: object): Record<string, unknown> {
|
||||
|
||||
// Capture getter properties from the instance's prototype
|
||||
for (const [key, descriptor] of Object.entries(descriptors)) {
|
||||
if (typeof descriptor.get === 'function') {
|
||||
if (isFunction(descriptor.get)) {
|
||||
obj[key] = descriptor.get.call(instance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,8 @@ import log from 'electron-log/main';
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import type { LogFunctions } from 'electron-log';
|
||||
|
||||
// Using plain-function rather than class so it can be used in Electron's context-bridging.
|
||||
export function createElectronLogger(logger: LogFunctions = log): Logger {
|
||||
return {
|
||||
info: (...params) => logger.info(...params),
|
||||
debug: (...params) => logger.debug(...params),
|
||||
warn: (...params) => logger.warn(...params),
|
||||
error: (...params) => logger.error(...params),
|
||||
};
|
||||
return logger;
|
||||
}
|
||||
|
||||
export const ElectronLogger = createElectronLogger();
|
||||
|
||||
115
src/infrastructure/ReadbackFileWriter/NodeReadbackFileWriter.ts
Normal file
115
src/infrastructure/ReadbackFileWriter/NodeReadbackFileWriter.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { writeFile, access, readFile } from 'node:fs/promises';
|
||||
import { constants } from 'node:fs';
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { ElectronLogger } from '../Log/ElectronLogger';
|
||||
import {
|
||||
FailedFileWrite, ReadbackFileWriter, FileWriteErrorType,
|
||||
FileWriteOutcome, SuccessfulFileWrite,
|
||||
} from './ReadbackFileWriter';
|
||||
|
||||
const FILE_ENCODING: NodeJS.BufferEncoding = 'utf-8';
|
||||
|
||||
export class NodeReadbackFileWriter implements ReadbackFileWriter {
|
||||
constructor(
|
||||
private readonly logger: Logger = ElectronLogger,
|
||||
private readonly fileSystem: FileReadWriteOperations = {
|
||||
writeFile,
|
||||
readFile: (path, encoding) => readFile(path, encoding),
|
||||
access,
|
||||
},
|
||||
) { }
|
||||
|
||||
public async writeAndVerifyFile(
|
||||
filePath: string,
|
||||
fileContents: string,
|
||||
): Promise<FileWriteOutcome> {
|
||||
this.logger.info(`Starting file write and verification process for: ${filePath}`);
|
||||
const fileWritePipelineActions: ReadonlyArray<() => Promise<FileWriteOutcome>> = [
|
||||
() => this.createOrOverwriteFile(filePath, fileContents),
|
||||
() => this.verifyFileExistsWithoutReading(filePath),
|
||||
() => this.verifyFileContentsByReading(filePath, fileContents),
|
||||
];
|
||||
for (const action of fileWritePipelineActions) {
|
||||
const actionOutcome = await action(); // eslint-disable-line no-await-in-loop
|
||||
if (!actionOutcome.success) {
|
||||
return actionOutcome;
|
||||
}
|
||||
}
|
||||
return this.reportSuccess(`File successfully written and verified: ${filePath}`);
|
||||
}
|
||||
|
||||
private async createOrOverwriteFile(
|
||||
filePath: string,
|
||||
fileContents: string,
|
||||
): Promise<FileWriteOutcome> {
|
||||
try {
|
||||
this.logger.info(`Creating file at ${filePath}, size: ${fileContents.length} characters`);
|
||||
await this.fileSystem.writeFile(filePath, fileContents, FILE_ENCODING);
|
||||
return this.reportSuccess('Created file.');
|
||||
} catch (error) {
|
||||
return this.reportFailure('WriteOperationFailed', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async verifyFileExistsWithoutReading(
|
||||
filePath: string,
|
||||
): Promise<FileWriteOutcome> {
|
||||
try {
|
||||
await this.fileSystem.access(filePath, constants.F_OK);
|
||||
return this.reportSuccess('Verified file existence without reading.');
|
||||
} catch (error) {
|
||||
return this.reportFailure('FileExistenceVerificationFailed', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async verifyFileContentsByReading(
|
||||
filePath: string,
|
||||
expectedFileContents: string,
|
||||
): Promise<FileWriteOutcome> {
|
||||
try {
|
||||
const actualFileContents = await this.fileSystem.readFile(filePath, FILE_ENCODING);
|
||||
if (actualFileContents !== expectedFileContents) {
|
||||
return this.reportFailure(
|
||||
'ContentVerificationFailed',
|
||||
[
|
||||
'The contents of the written file do not match the expected contents.',
|
||||
'Written file contents do not match the expected file contents',
|
||||
`File path: ${filePath}`,
|
||||
`Expected total characters: ${actualFileContents.length}`,
|
||||
`Actual total characters: ${expectedFileContents.length}`,
|
||||
].join('\n'),
|
||||
);
|
||||
}
|
||||
return this.reportSuccess('Verified file content by reading.');
|
||||
} catch (error) {
|
||||
return this.reportFailure('ReadVerificationFailed', error);
|
||||
}
|
||||
}
|
||||
|
||||
private reportFailure(
|
||||
errorType: FileWriteErrorType,
|
||||
error: Error | string,
|
||||
): FailedFileWrite {
|
||||
this.logger.error('Error saving file', errorType, error);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
type: errorType,
|
||||
message: typeof error === 'string' ? error : error.message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private reportSuccess(successAction: string): SuccessfulFileWrite {
|
||||
this.logger.info(`Successful file save: ${successAction}`);
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface FileReadWriteOperations {
|
||||
readonly writeFile: typeof writeFile;
|
||||
readonly access: typeof access;
|
||||
readFile: (filePath: string, encoding: NodeJS.BufferEncoding) => Promise<string>;
|
||||
}
|
||||
59
src/infrastructure/ReadbackFileWriter/ReadbackFileWriter.ts
Normal file
59
src/infrastructure/ReadbackFileWriter/ReadbackFileWriter.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* It defines the contract for file writing operations with an added layer of
|
||||
* verification. This approach is useful in environments where file write operations
|
||||
* might be silently intercepted or manipulated by external factors, such as antivirus software.
|
||||
*
|
||||
* This additional verification provides a more reliable and transparent file writing
|
||||
* process, enhancing the application's resilience against external disruptions and
|
||||
* improving the overall user experience. It enables the application to notify users
|
||||
* of potential issues, such as antivirus interventions, and offer guidance on how to
|
||||
* resolve them.
|
||||
*/
|
||||
export interface ReadbackFileWriter {
|
||||
writeAndVerifyFile(filePath: string, fileContents: string): Promise<FileWriteOutcome>;
|
||||
}
|
||||
|
||||
export type FileWriteOutcome = SuccessfulFileWrite | FailedFileWrite;
|
||||
|
||||
export type FileWriteErrorType =
|
||||
| UnionOfConstArray<typeof FileWriteOperationErrors>
|
||||
| UnionOfConstArray<typeof FileReadbackVerificationErrors>;
|
||||
|
||||
export const FileWriteOperationErrors = [
|
||||
'WriteOperationFailed',
|
||||
] as const;
|
||||
|
||||
export const FileReadbackVerificationErrors = [
|
||||
'FileExistenceVerificationFailed',
|
||||
'ContentVerificationFailed',
|
||||
|
||||
/*
|
||||
This error indicates a failure in verifying the contents of a written file.
|
||||
This error often occurs when antivirus software falsely identifies a script as harmful and
|
||||
either alters or removes it during the readback process. This verification step is crucial
|
||||
for detecting and handling such antivirus interventions.
|
||||
*/
|
||||
'ReadVerificationFailed',
|
||||
] as const;
|
||||
|
||||
interface FileWriteStatus {
|
||||
readonly success: boolean;
|
||||
readonly error?: FileWriteError;
|
||||
}
|
||||
|
||||
export interface SuccessfulFileWrite extends FileWriteStatus {
|
||||
readonly success: true;
|
||||
readonly error?: undefined;
|
||||
}
|
||||
|
||||
export interface FailedFileWrite extends FileWriteStatus {
|
||||
readonly success: false;
|
||||
readonly error: FileWriteError;
|
||||
}
|
||||
|
||||
export interface FileWriteError {
|
||||
readonly type: FileWriteErrorType;
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
type UnionOfConstArray<T extends ReadonlyArray<unknown>> = T[number];
|
||||
@@ -57,6 +57,7 @@ export const BrowserConditions: readonly BrowserCondition[] = [
|
||||
{
|
||||
operatingSystem: OperatingSystem.iPadOS,
|
||||
existingPartsInSameUserAgent: ['Macintosh'], // Reported by Safari on iPads running ≥ iPadOS 13
|
||||
notExistingPartsInUserAgent: ['Electron'], // Electron supports only macOS, not iPadOS
|
||||
touchSupport: TouchSupportExpectation.MustExist, // Safari same user agent as desktop macOS
|
||||
},
|
||||
{
|
||||
@@ -83,4 +84,23 @@ export const BrowserConditions: readonly BrowserCondition[] = [
|
||||
notExistingPartsInUserAgent: ['like Mac OS X'], // Eliminate iOS and iPadOS for Safari
|
||||
touchSupport: TouchSupportExpectation.MustNotExist, // Distinguish from iPadOS for Safari
|
||||
},
|
||||
...generateJsdomBrowserConditions(),
|
||||
] as const;
|
||||
|
||||
function generateJsdomBrowserConditions(): readonly BrowserCondition[] {
|
||||
// jsdom user agent format: `Mozilla/5.0 (${process.platform || "unknown OS"}) ...` (https://archive.ph/2023.02.14-193200/https://github.com/jsdom/jsdom#advanced-configuration)
|
||||
const operatingSystemPlatformMap: Partial<Record<
|
||||
OperatingSystem,
|
||||
NodeJS.Platform> // Enforce right platform constants at compile time
|
||||
> = {
|
||||
[OperatingSystem.Linux]: 'linux',
|
||||
[OperatingSystem.Windows]: 'win32',
|
||||
[OperatingSystem.macOS]: 'darwin',
|
||||
} as const;
|
||||
return Object
|
||||
.entries(operatingSystemPlatformMap)
|
||||
.map(([operatingSystemKey, platformString]): BrowserCondition => ({
|
||||
operatingSystem: Number(operatingSystemKey),
|
||||
existingPartsInSameUserAgent: ['jsdom', platformString],
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
|
||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||
import { RuntimeEnvironment } from '../RuntimeEnvironment';
|
||||
import { ConditionBasedOsDetector } from './BrowserOs/ConditionBasedOsDetector';
|
||||
import { BrowserEnvironment, BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||
import { isTouchEnabledDevice } from './TouchSupportDetection';
|
||||
|
||||
export class BrowserRuntimeEnvironment implements RuntimeEnvironment {
|
||||
public readonly isRunningAsDesktopApplication: boolean;
|
||||
|
||||
public readonly os: OperatingSystem | undefined;
|
||||
|
||||
public readonly isNonProduction: boolean;
|
||||
|
||||
public constructor(
|
||||
window: Partial<Window>,
|
||||
environmentVariables: IEnvironmentVariables = EnvironmentVariablesFactory.Current.instance,
|
||||
browserOsDetector: BrowserOsDetector = new ConditionBasedOsDetector(),
|
||||
touchDetector: TouchDetector = isTouchEnabledDevice,
|
||||
) {
|
||||
if (!window) { throw new Error('missing window'); } // do not trust strictNullChecks for global objects
|
||||
this.isNonProduction = environmentVariables.isNonProduction;
|
||||
this.isRunningAsDesktopApplication = isElectronRendererProcess(window);
|
||||
this.os = determineOperatingSystem(window, touchDetector, browserOsDetector);
|
||||
}
|
||||
}
|
||||
|
||||
function isElectronRendererProcess(globalWindow: Partial<Window>): boolean {
|
||||
return globalWindow.isRunningAsDesktopApplication === true; // Preloader injects this
|
||||
// We could also do `globalWindow?.navigator?.userAgent?.includes('Electron') === true;`
|
||||
}
|
||||
|
||||
function determineOperatingSystem(
|
||||
globalWindow: Partial<Window>,
|
||||
touchDetector: TouchDetector,
|
||||
browserOsDetector: BrowserOsDetector,
|
||||
): OperatingSystem | undefined {
|
||||
const userAgent = globalWindow?.navigator?.userAgent;
|
||||
if (!userAgent) {
|
||||
return undefined;
|
||||
}
|
||||
const browserEnvironment: BrowserEnvironment = {
|
||||
userAgent,
|
||||
isTouchSupported: touchDetector(),
|
||||
};
|
||||
return browserOsDetector.detect(browserEnvironment);
|
||||
}
|
||||
|
||||
type TouchDetector = () => boolean;
|
||||
@@ -0,0 +1,57 @@
|
||||
export function isTouchEnabledDevice(
|
||||
browserTouchAccessor: BrowserTouchSupportAccessor = GlobalTouchSupportAccessor,
|
||||
): boolean {
|
||||
return TouchSupportChecks.some(
|
||||
(check) => check(browserTouchAccessor),
|
||||
);
|
||||
}
|
||||
|
||||
export interface BrowserTouchSupportAccessor {
|
||||
navigatorMaxTouchPoints: () => number | undefined;
|
||||
windowMatchMediaMatches: (query: string) => boolean;
|
||||
documentOntouchend: () => undefined | unknown;
|
||||
}
|
||||
|
||||
/*
|
||||
Touch support checks are inconsistent across different browsers and OS.
|
||||
`✅` and `❌` indicate correct and incorrect detections, respectively.
|
||||
*/
|
||||
const TouchSupportChecks: ReadonlyArray<(accessor: BrowserTouchSupportAccessor) => boolean> = [
|
||||
/*
|
||||
Mobile (iOS & Android): ✅ Chrome, ✅ Safari, ✅ Firefox
|
||||
Touch-enabled Windows laptop: ❌ Chrome (reports no touch), ❌ Firefox (reports no touch)
|
||||
Chromium has removed ontouch* events on desktop since Chrome 70+
|
||||
Non-touch macOS: ✅ Firefox, ✅ Safari, ✅ Chromium
|
||||
*/
|
||||
(accessor) => accessor.documentOntouchend() !== undefined,
|
||||
/*
|
||||
Mobile (iOS & Android): ✅ Chrome, ✅ Safari, ✅ Firefox
|
||||
Touch-enabled Windows laptop: ✅ Chrome, ❌ Firefox (reports no touch)
|
||||
Non-touch macOS: ✅ Firefox, ✅ Safari, ✅ Chromium
|
||||
*/
|
||||
(accessor) => {
|
||||
const maxTouchPoints = accessor.navigatorMaxTouchPoints();
|
||||
return maxTouchPoints !== undefined && maxTouchPoints > 0;
|
||||
},
|
||||
/*
|
||||
Mobile (iOS & Android): ✅ Chrome, ✅ Safari, ✅ Firefox
|
||||
Touch-enabled Windows laptop: ✅ Chrome, ❌ Firefox (reports no touch)
|
||||
Non-touch macOS: ✅ Firefox, ✅ Safari, ✅ Chromium
|
||||
*/
|
||||
(accessor) => accessor.windowMatchMediaMatches('(any-pointer: coarse)'),
|
||||
|
||||
/*
|
||||
Do not check window.TouchEvent === undefined, as it incorrectly
|
||||
reports touch support on Chromium macOS even though there is no
|
||||
touch support.
|
||||
Mobile (iOS & Android): ✅ Chrome, ✅ Safari, ✅ Firefox
|
||||
Touch-enabled Windows laptop: ✅ Chrome, ❌ Firefox (reports no touch)
|
||||
Non-touch macOS: ✅ Firefox, ✅ Safari, ❌ Chromium (reports touch)
|
||||
*/
|
||||
];
|
||||
|
||||
const GlobalTouchSupportAccessor: BrowserTouchSupportAccessor = {
|
||||
navigatorMaxTouchPoints: () => navigator.maxTouchPoints,
|
||||
windowMatchMediaMatches: (query: string) => window.matchMedia(query)?.matches,
|
||||
documentOntouchend: () => document.ontouchend,
|
||||
} as const;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ElectronEnvironmentDetector, ElectronProcessType } from './ElectronEnvironmentDetector';
|
||||
|
||||
export class ContextIsolatedElectronDetector implements ElectronEnvironmentDetector {
|
||||
constructor(
|
||||
private readonly nodeProcessAccessor: NodeProcessAccessor = () => globalThis?.process,
|
||||
private readonly userAgentAccessor: UserAgentAccessor = () => globalThis?.navigator?.userAgent,
|
||||
) { }
|
||||
|
||||
public isRunningInsideElectron(): boolean {
|
||||
return isNodeProcessElectronBased(this.nodeProcessAccessor)
|
||||
|| isUserAgentElectronBased(this.userAgentAccessor);
|
||||
}
|
||||
|
||||
public determineElectronProcessType(): ElectronProcessType {
|
||||
const isNodeAccessible = isNodeProcessElectronBased(this.nodeProcessAccessor);
|
||||
const isBrowserAccessible = isUserAgentElectronBased(this.userAgentAccessor);
|
||||
if (!isNodeAccessible && !isBrowserAccessible) {
|
||||
throw new Error('Unable to determine the Electron process type. Neither Node.js nor browser-based Electron contexts were detected.');
|
||||
}
|
||||
if (isNodeAccessible && isBrowserAccessible) {
|
||||
return 'preloader'; // Only preloader can access both Node.js and browser contexts in Electron with context isolation.
|
||||
}
|
||||
if (isNodeAccessible) {
|
||||
return 'main';
|
||||
}
|
||||
return 'renderer';
|
||||
}
|
||||
}
|
||||
|
||||
export type NodeProcessAccessor = () => NodeJS.Process | undefined;
|
||||
|
||||
function isNodeProcessElectronBased(nodeProcessAccessor: NodeProcessAccessor): boolean {
|
||||
const nodeProcess = nodeProcessAccessor();
|
||||
if (!nodeProcess) {
|
||||
return false;
|
||||
}
|
||||
if (nodeProcess.versions.electron) {
|
||||
// Electron populates `nodeProcess.versions.electron` with its version, see https://web.archive.org/web/20240113162837/https://www.electronjs.org/docs/latest/api/process#processversionselectron-readonly.
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export type UserAgentAccessor = () => string | undefined;
|
||||
|
||||
function isUserAgentElectronBased(
|
||||
userAgentAccessor: UserAgentAccessor,
|
||||
): boolean {
|
||||
const userAgent = userAgentAccessor();
|
||||
if (userAgent?.includes('Electron')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface ElectronEnvironmentDetector {
|
||||
isRunningInsideElectron(): boolean;
|
||||
determineElectronProcessType(): ElectronProcessType;
|
||||
}
|
||||
|
||||
export type ElectronProcessType = 'main' | 'preloader' | 'renderer';
|
||||
@@ -1,7 +0,0 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export interface IRuntimeEnvironment {
|
||||
readonly isDesktop: boolean;
|
||||
readonly os: OperatingSystem | undefined;
|
||||
readonly isNonProduction: boolean;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export function convertPlatformToOs(platform: NodeJS.Platform): OperatingSystem | undefined {
|
||||
export function convertPlatformToOs(
|
||||
platform: NodeJS.Platform,
|
||||
): OperatingSystem | undefined {
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
return OperatingSystem.macOS;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { RuntimeEnvironment } from '../RuntimeEnvironment';
|
||||
import { convertPlatformToOs } from './NodeOsMapper';
|
||||
|
||||
export class NodeRuntimeEnvironment implements RuntimeEnvironment {
|
||||
public readonly isRunningAsDesktopApplication: boolean;
|
||||
|
||||
public readonly os: OperatingSystem | undefined;
|
||||
|
||||
public readonly isNonProduction: boolean;
|
||||
|
||||
constructor(
|
||||
nodeProcess: NodeJSProcessAccessor = globalThis.process,
|
||||
convertToOs: PlatformToOperatingSystemConverter = convertPlatformToOs,
|
||||
) {
|
||||
if (!nodeProcess) { throw new Error('missing process'); } // do not trust strictNullChecks for global objects
|
||||
this.isRunningAsDesktopApplication = true;
|
||||
this.os = convertToOs(nodeProcess.platform);
|
||||
this.isNonProduction = nodeProcess.env.NODE_ENV !== 'production'; // populated by Vite
|
||||
}
|
||||
}
|
||||
|
||||
export interface NodeJSProcessAccessor {
|
||||
readonly platform: NodeJS.Platform;
|
||||
readonly env: NodeJS.ProcessEnv;
|
||||
}
|
||||
|
||||
export type PlatformToOperatingSystemConverter = typeof convertPlatformToOs;
|
||||
@@ -1,50 +1,7 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
|
||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||
import { ConditionBasedOsDetector } from './BrowserOs/ConditionBasedOsDetector';
|
||||
import { BrowserEnvironment, BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||
import { IRuntimeEnvironment } from './IRuntimeEnvironment';
|
||||
import { isTouchEnabledDevice } from './TouchSupportDetection';
|
||||
|
||||
export class RuntimeEnvironment implements IRuntimeEnvironment {
|
||||
public static readonly CurrentEnvironment: IRuntimeEnvironment = new RuntimeEnvironment(window);
|
||||
|
||||
public readonly isDesktop: boolean;
|
||||
|
||||
public readonly os: OperatingSystem | undefined;
|
||||
|
||||
public readonly isNonProduction: boolean;
|
||||
|
||||
protected constructor(
|
||||
window: Partial<Window>,
|
||||
environmentVariables: IEnvironmentVariables = EnvironmentVariablesFactory.Current.instance,
|
||||
browserOsDetector: BrowserOsDetector = new ConditionBasedOsDetector(),
|
||||
touchDetector = isTouchEnabledDevice,
|
||||
) {
|
||||
if (!window) { throw new Error('missing window'); } // do not trust strictNullChecks for global objects
|
||||
this.isNonProduction = environmentVariables.isNonProduction;
|
||||
this.isDesktop = isDesktop(window);
|
||||
if (this.isDesktop) {
|
||||
this.os = window?.os;
|
||||
} else {
|
||||
this.os = undefined;
|
||||
const userAgent = getUserAgent(window);
|
||||
if (userAgent) {
|
||||
const browserEnvironment: BrowserEnvironment = {
|
||||
userAgent,
|
||||
isTouchSupported: touchDetector(),
|
||||
};
|
||||
this.os = browserOsDetector.detect(browserEnvironment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getUserAgent(window: Partial<Window>): string | undefined {
|
||||
return window?.navigator?.userAgent;
|
||||
}
|
||||
|
||||
function isDesktop(window: Partial<WindowVariables>): boolean {
|
||||
return window?.isDesktop === true;
|
||||
export interface RuntimeEnvironment {
|
||||
readonly isRunningAsDesktopApplication: boolean;
|
||||
readonly os: OperatingSystem | undefined;
|
||||
readonly isNonProduction: boolean;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ElectronEnvironmentDetector } from './Electron/ElectronEnvironmentDetector';
|
||||
import { BrowserRuntimeEnvironment } from './Browser/BrowserRuntimeEnvironment';
|
||||
import { NodeRuntimeEnvironment } from './Node/NodeRuntimeEnvironment';
|
||||
import { RuntimeEnvironment } from './RuntimeEnvironment';
|
||||
import { ContextIsolatedElectronDetector } from './Electron/ContextIsolatedElectronDetector';
|
||||
|
||||
export const CurrentEnvironment = determineAndCreateRuntimeEnvironment(globalThis.window);
|
||||
|
||||
export function determineAndCreateRuntimeEnvironment(
|
||||
globalWindow: Window | undefined | null = globalThis.window,
|
||||
electronDetector: ElectronEnvironmentDetector = new ContextIsolatedElectronDetector(),
|
||||
browserEnvironmentFactory: BrowserRuntimeEnvironmentFactory = (
|
||||
window,
|
||||
) => new BrowserRuntimeEnvironment(window),
|
||||
nodeEnvironmentFactory: NodeRuntimeEnvironmentFactory = () => new NodeRuntimeEnvironment(),
|
||||
): RuntimeEnvironment {
|
||||
if (
|
||||
electronDetector.isRunningInsideElectron()
|
||||
&& electronDetector.determineElectronProcessType() === 'main') {
|
||||
return nodeEnvironmentFactory();
|
||||
}
|
||||
if (!globalWindow) {
|
||||
throw new Error('Unsupported runtime environment: The current context is neither a recognized browser nor a desktop environment.');
|
||||
}
|
||||
return browserEnvironmentFactory(globalWindow);
|
||||
}
|
||||
|
||||
export type BrowserRuntimeEnvironmentFactory = (window: Window) => RuntimeEnvironment;
|
||||
|
||||
export type NodeRuntimeEnvironmentFactory = () => NodeRuntimeEnvironment;
|
||||
|
||||
export type GlobalWindowAccessor = Window | undefined;
|
||||
@@ -1,52 +0,0 @@
|
||||
export function isTouchEnabledDevice(
|
||||
browserTouchAccessor: BrowserTouchSupportAccessor = GlobalTouchSupportAccessor,
|
||||
): boolean {
|
||||
return TouchSupportChecks.some(
|
||||
(check) => check(browserTouchAccessor),
|
||||
);
|
||||
}
|
||||
|
||||
export interface BrowserTouchSupportAccessor {
|
||||
navigatorMaxTouchPoints: () => number | undefined;
|
||||
windowMatchMediaMatches: (query: string) => boolean;
|
||||
documentOntouchend: () => undefined | unknown;
|
||||
windowTouchEvent: () => undefined | unknown;
|
||||
}
|
||||
|
||||
const TouchSupportChecks: ReadonlyArray<(accessor: BrowserTouchSupportAccessor) => boolean> = [
|
||||
/*
|
||||
✅ Mobile: Chrome, Safari, Firefox on iOS and Android
|
||||
❌ Touch-enabled Windows laptop: Chrome
|
||||
(Chromium has removed ontouch* events on desktop since Chrome 70+.)
|
||||
❌ Touch-enabled Windows laptop: Firefox
|
||||
*/
|
||||
(accessor) => accessor.documentOntouchend() !== undefined,
|
||||
/*
|
||||
✅ Mobile: Chrome, Safari, Firefox on iOS and Android
|
||||
✅ Touch-enabled Windows laptop: Chrome
|
||||
❌ Touch-enabled Windows laptop: Firefox
|
||||
*/
|
||||
(accessor) => {
|
||||
const maxTouchPoints = accessor.navigatorMaxTouchPoints();
|
||||
return maxTouchPoints !== undefined && maxTouchPoints > 0;
|
||||
},
|
||||
/*
|
||||
✅ Mobile: Chrome, Safari, Firefox on iOS and Android
|
||||
✅ Touch-enabled Windows laptop: Chrome
|
||||
❌ Touch-enabled Windows laptop: Firefox
|
||||
*/
|
||||
(accessor) => accessor.windowMatchMediaMatches('(any-pointer: coarse)'),
|
||||
/*
|
||||
✅ Mobile: Chrome, Safari, Firefox on iOS and Android
|
||||
✅ Touch-enabled Windows laptop: Chrome
|
||||
❌ Touch-enabled Windows laptop: Firefox
|
||||
*/
|
||||
(accessor) => accessor.windowTouchEvent() !== undefined,
|
||||
];
|
||||
|
||||
const GlobalTouchSupportAccessor: BrowserTouchSupportAccessor = {
|
||||
navigatorMaxTouchPoints: () => navigator.maxTouchPoints,
|
||||
windowMatchMediaMatches: (query: string) => window.matchMedia(query)?.matches,
|
||||
documentOntouchend: () => document.ontouchend,
|
||||
windowTouchEvent: () => window.TouchEvent,
|
||||
} as const;
|
||||
@@ -1,33 +0,0 @@
|
||||
import fileSaver from 'file-saver';
|
||||
|
||||
export enum FileType {
|
||||
BatchFile,
|
||||
ShellScript,
|
||||
}
|
||||
|
||||
export class SaveFileDialog {
|
||||
public static saveFile(
|
||||
text: string,
|
||||
fileName: string,
|
||||
type: FileType,
|
||||
): void {
|
||||
const mimeType = this.mimeTypes[type];
|
||||
this.saveBlob(text, mimeType, fileName);
|
||||
}
|
||||
|
||||
private static readonly mimeTypes: Record<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
|
||||
[FileType.ShellScript]: 'text/x-shellscript', // https://de.wikipedia.org/wiki/Shellskript#MIME-Typ
|
||||
};
|
||||
|
||||
private static saveBlob(file: BlobPart, fileType: string, fileName: string): void {
|
||||
try {
|
||||
const blob = new Blob([file], { type: fileType });
|
||||
fileSaver.saveAs(blob, fileName);
|
||||
} catch (e) {
|
||||
window.open(`data:${fileType},${encodeURIComponent(file.toString())}`, '_blank', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ScriptDiagnosticData, ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import { PersistentDirectoryProvider } from '@/infrastructure/CodeRunner/Creation/Directory/PersistentDirectoryProvider';
|
||||
import { ScriptDirectoryProvider } from '../CodeRunner/Creation/Directory/ScriptDirectoryProvider';
|
||||
|
||||
export class ScriptEnvironmentDiagnosticsCollector implements ScriptDiagnosticsCollector {
|
||||
constructor(
|
||||
private readonly directoryProvider: ScriptDirectoryProvider = new PersistentDirectoryProvider(),
|
||||
private readonly environment: RuntimeEnvironment = CurrentEnvironment,
|
||||
) { }
|
||||
|
||||
public async collectDiagnosticInformation(): Promise<ScriptDiagnosticData> {
|
||||
const { directoryAbsolutePath } = await this.directoryProvider.provideScriptDirectory();
|
||||
return {
|
||||
scriptsDirectoryAbsolutePath: directoryAbsolutePath,
|
||||
currentOperatingSystem: this.environment.os,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
export interface ISystemOperations {
|
||||
readonly operatingSystem: IOperatingSystemOps;
|
||||
readonly location: ILocationOps;
|
||||
readonly fileSystem: IFileSystemOps;
|
||||
readonly command: ICommandOps;
|
||||
}
|
||||
|
||||
export interface IOperatingSystemOps {
|
||||
getTempDirectory(): string;
|
||||
}
|
||||
|
||||
export interface ILocationOps {
|
||||
combinePaths(...pathSegments: string[]): string;
|
||||
}
|
||||
|
||||
export interface ICommandOps {
|
||||
execute(command: string): void;
|
||||
}
|
||||
|
||||
export interface IFileSystemOps {
|
||||
setFilePermissions(filePath: string, mode: string | number): Promise<void>;
|
||||
createDirectory(directoryPath: string, isRecursive?: boolean): Promise<void>;
|
||||
writeToFile(filePath: string, data: string): Promise<void>;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { chmod, mkdir, writeFile } from 'fs/promises';
|
||||
import { exec } from 'child_process';
|
||||
import { ISystemOperations } from './ISystemOperations';
|
||||
|
||||
export function createNodeSystemOperations(): ISystemOperations {
|
||||
return {
|
||||
operatingSystem: {
|
||||
getTempDirectory: () => tmpdir(),
|
||||
},
|
||||
location: {
|
||||
combinePaths: (...pathSegments) => join(...pathSegments),
|
||||
},
|
||||
fileSystem: {
|
||||
setFilePermissions: (
|
||||
filePath: string,
|
||||
mode: string | number,
|
||||
) => chmod(filePath, mode),
|
||||
createDirectory: async (
|
||||
directoryPath: string,
|
||||
isRecursive?: boolean,
|
||||
) => {
|
||||
await mkdir(directoryPath, { recursive: isRecursive });
|
||||
// Ignoring the return value from `mkdir`, which is the first directory created
|
||||
// when `recursive` is true. The function contract is to not return any value,
|
||||
// and we avoid handling this inconsistent behavior.
|
||||
// See https://github.com/nodejs/node/pull/31530
|
||||
},
|
||||
writeToFile: (
|
||||
filePath: string,
|
||||
data: string,
|
||||
) => writeFile(filePath, data),
|
||||
},
|
||||
command: {
|
||||
execute: (command) => exec(command),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { WindowVariables } from '../WindowVariables/WindowVariables';
|
||||
import { ISystemOperations } from './ISystemOperations';
|
||||
|
||||
export function getWindowInjectedSystemOperations(
|
||||
windowVariables: Partial<WindowVariables> = window,
|
||||
): ISystemOperations {
|
||||
if (!windowVariables) {
|
||||
throw new Error('missing window');
|
||||
}
|
||||
if (!windowVariables.system) {
|
||||
throw new Error('missing system');
|
||||
}
|
||||
return windowVariables.system;
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations';
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { CodeRunner } from '@/application/CodeRunner/CodeRunner';
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import { ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
|
||||
|
||||
/* Primary entry point for platform-specific injections */
|
||||
export interface WindowVariables {
|
||||
readonly isDesktop: boolean;
|
||||
readonly system?: ISystemOperations;
|
||||
readonly isRunningAsDesktopApplication?: true;
|
||||
readonly codeRunner?: CodeRunner;
|
||||
readonly os?: OperatingSystem;
|
||||
readonly log: Logger;
|
||||
readonly log?: Logger;
|
||||
readonly dialog?: Dialog;
|
||||
readonly scriptDiagnosticsCollector?: ScriptDiagnosticsCollector;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import { ContextIsolatedElectronDetector } from '@/infrastructure/RuntimeEnvironment/Electron/ContextIsolatedElectronDetector';
|
||||
import { ElectronEnvironmentDetector } from '@/infrastructure/RuntimeEnvironment/Electron/ElectronEnvironmentDetector';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { PropertyKeys } from '@/TypeHelpers';
|
||||
import {
|
||||
PropertyKeys, isBoolean, isFunction, isNumber, isPlainObject,
|
||||
} from '@/TypeHelpers';
|
||||
import { WindowVariables } from './WindowVariables';
|
||||
|
||||
/**
|
||||
* Checks for consistency in runtime environment properties injected by Electron preloader.
|
||||
*/
|
||||
export function validateWindowVariables(variables: Partial<WindowVariables>) {
|
||||
if (!isObject(variables)) {
|
||||
export function validateWindowVariables(
|
||||
variables: Partial<WindowVariables>,
|
||||
electronDetector: ElectronEnvironmentDetector = new ContextIsolatedElectronDetector(),
|
||||
) {
|
||||
if (!electronDetector.isRunningInsideElectron()
|
||||
|| electronDetector.determineElectronProcessType() !== 'renderer') {
|
||||
return;
|
||||
}
|
||||
if (!isPlainObject(variables)) {
|
||||
throw new Error('window is not an object');
|
||||
}
|
||||
const errors = [...testEveryProperty(variables)];
|
||||
@@ -16,13 +27,13 @@ export function validateWindowVariables(variables: Partial<WindowVariables>) {
|
||||
}
|
||||
|
||||
function* testEveryProperty(variables: Partial<WindowVariables>): Iterable<string> {
|
||||
const tests: {
|
||||
[K in PropertyKeys<Required<WindowVariables>>]: boolean;
|
||||
} = {
|
||||
const tests: Record<PropertyKeys<Required<WindowVariables>>, boolean> = {
|
||||
os: testOperatingSystem(variables.os),
|
||||
isDesktop: testIsDesktop(variables.isDesktop),
|
||||
system: testSystem(variables),
|
||||
isRunningAsDesktopApplication: testIsRunningAsDesktopApplication(variables),
|
||||
codeRunner: testCodeRunner(variables),
|
||||
log: testLogger(variables),
|
||||
dialog: testDialog(variables),
|
||||
scriptDiagnosticsCollector: testScriptDiagnosticsCollector(variables),
|
||||
};
|
||||
|
||||
for (const [propertyName, testResult] of Object.entries(tests)) {
|
||||
@@ -46,36 +57,30 @@ function testOperatingSystem(os: unknown): boolean {
|
||||
}
|
||||
|
||||
function testLogger(variables: Partial<WindowVariables>): boolean {
|
||||
if (!variables.isDesktop) {
|
||||
return true;
|
||||
}
|
||||
return isObject(variables.log);
|
||||
return isPlainObject(variables.log)
|
||||
&& isFunction(variables.log.debug)
|
||||
&& isFunction(variables.log.info)
|
||||
&& isFunction(variables.log.error)
|
||||
&& isFunction(variables.log.warn);
|
||||
}
|
||||
|
||||
function testSystem(variables: Partial<WindowVariables>): boolean {
|
||||
if (!variables.isDesktop) {
|
||||
return true;
|
||||
}
|
||||
return isObject(variables.system);
|
||||
function testCodeRunner(variables: Partial<WindowVariables>): boolean {
|
||||
return isPlainObject(variables.codeRunner)
|
||||
&& isFunction(variables.codeRunner.runCode);
|
||||
}
|
||||
|
||||
function testIsDesktop(isDesktop: unknown): boolean {
|
||||
if (isDesktop === undefined) {
|
||||
return true;
|
||||
}
|
||||
return isBoolean(isDesktop);
|
||||
function testIsRunningAsDesktopApplication(variables: Partial<WindowVariables>): boolean {
|
||||
return isBoolean(variables.isRunningAsDesktopApplication)
|
||||
&& variables.isRunningAsDesktopApplication === true;
|
||||
}
|
||||
|
||||
function isNumber(variable: unknown): variable is number {
|
||||
return typeof variable === 'number';
|
||||
function testDialog(variables: Partial<WindowVariables>): boolean {
|
||||
return isPlainObject(variables.dialog)
|
||||
&& isFunction(variables.dialog.saveFile)
|
||||
&& isFunction(variables.dialog.showError);
|
||||
}
|
||||
|
||||
function isBoolean(variable: unknown): variable is boolean {
|
||||
return typeof variable === 'boolean';
|
||||
}
|
||||
|
||||
function isObject(variable: unknown): variable is object {
|
||||
return Boolean(variable) // the data type of null is an object
|
||||
&& typeof variable === 'object'
|
||||
&& !Array.isArray(variable);
|
||||
function testScriptDiagnosticsCollector(variables: Partial<WindowVariables>): boolean {
|
||||
return isPlainObject(variables.scriptDiagnosticsCollector)
|
||||
&& isFunction(variables.scriptDiagnosticsCollector.collectDiagnosticInformation);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ $color-on-surface : #4d5156;
|
||||
// Background | Appears behind scrollable content.
|
||||
$color-background : #e6ecf4;
|
||||
|
||||
|
||||
/*
|
||||
Application-specific colors:
|
||||
These are tailored to the specific needs of the application and derived from the above theme colors.
|
||||
@@ -38,3 +37,4 @@ $color-background : #e6ecf4;
|
||||
This approach maintains a cohesive look and feel and simplifies theme adjustments.
|
||||
*/
|
||||
$color-scripts-bg: $color-primary-darker;
|
||||
$color-highlight: $color-primary;
|
||||
|
||||
@@ -11,14 +11,10 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
$globals-color-hover: $color-primary;
|
||||
a {
|
||||
color:inherit;
|
||||
text-decoration: underline;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
@include hover-or-touch {
|
||||
color: $globals-color-hover;
|
||||
}
|
||||
@include flat-button($disabled: false);
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@use "@/presentation/assets/styles/colors" as *;
|
||||
|
||||
@mixin hover-or-touch($selector-suffix: '', $selector-prefix: '&') {
|
||||
@media (hover: hover) {
|
||||
/*
|
||||
@@ -65,3 +67,47 @@
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@mixin reset-button {
|
||||
margin: 0;
|
||||
padding-block: 0;
|
||||
padding-inline: 0;
|
||||
font: unset;
|
||||
border: unset;
|
||||
background: unset;
|
||||
align-items: unset;
|
||||
text-align: unset;
|
||||
text-shadow: unset;
|
||||
text-rendering: unset;
|
||||
color: inherit;
|
||||
writing-mode: unset;
|
||||
letter-spacing: unset;
|
||||
word-spacing: unset;
|
||||
line-height: unset;
|
||||
text-transform: unset;
|
||||
text-indent: unset;
|
||||
appearance: unset;
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
@mixin flat-button($disabled: false) {
|
||||
@include reset-button;
|
||||
|
||||
@if $disabled {
|
||||
color: $color-primary-light;
|
||||
} @else {
|
||||
color: inherit;
|
||||
@include clickable;
|
||||
@include hover-or-touch {
|
||||
text-decoration: underline;
|
||||
color: $color-highlight;
|
||||
/*
|
||||
Using color change and underlining and as hover cues instead of bold text,
|
||||
due to inconsistent bold rendering in macOS browsers:
|
||||
- Safari: Renders bold, causes layout shift.
|
||||
- Firefox: Renders bold correctly, no layout shift.
|
||||
- Chromium-based browsers (including Electron app): Do not render bold, no layout shift.
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user