Compare commits

...

65 Commits

Author SHA1 Message Date
undergroundwires
0db8cc4206 Fix website not loading on Safari
It's caused by lookahead regex used in dash comment regex for inlining
PowerShell. This commit changes dash comment inlining.

- Change regex to one without lookahead.
- Add more test cases for inlining dash comment in tricky situations.
- Refactor makeInlineComment to be it's own function to easily test
  other regex options.
- Document all regex alternatives.
- Remove redundant null check (`||`) with adding safe navigation
  operator  (`?`) to allow variable before check to be null instead of
  throwing exception.
2021-11-04 18:42:44 +01:00
undergroundwires
97ddc027cb Fix dead URLs and use forks as GitHub references
Change all GitHub URLs with forks so they survive if their maintainer
decides to remove them.

Fix dead URLs in:
  - "Windows Push Notification Service" (#101)
  - "Limit CPU usage during scans to minimum"
  - "Disable NVIDIA telemetry"
2021-11-03 20:08:56 +01:00
undergroundwires
82c43ba2e3 Refactor to remove "Async" function name suffix
Remove convention where Async suffix is added to functions that returns
a Promise. It was a habit from C#, but is not widely used in JavaScript
/ TypeScript world, also bloats the code. The code is more consistent
with third party dependencies/frameworks without the suffix.
2021-11-01 19:02:22 +01:00
undergroundwires
799fb091b8 Fix failing URL status checking integration tests
Implement following redirects over `fetch` supporting cookies.
`node-fetch` does not support sending cookies during redirect. However,
this is needed to not end-up in a redirect loop for a sign-in callback.

Fix integration tests failing due to redirects and 403 errors:
  - Many redirects from `answers.microsoft.com` was throwing: throwing
    `FetchError: maximum redirect reached` error. It was caused by not
    having cookies when following redirects therefore having an infinite
    sign-in callback for the webpage.
  - Fixes integration tests failing due to additional referer header being
    sent by the application. It adds support for making exceptions to
    additional header sending through a list of regexes.

Add in-depth documentation for URL status checking.
2021-10-30 16:19:10 +01:00
undergroundwires
5ead1a087d Fix, document, unrecommend Windows browser cleanup
The main goal is to highlight and exclude scripts that clears user data
(such as Chrome bookmarks) from standard recommendation, thus allowing
more granular and intentional user selection. Because scripts that are
recommended as "standard" should be non-breaking.

Standard: Recommend only clearing data that would not be noticable by
user. E.g. caches and logs.
Strict	: Recommend clearing data that may be noticable by user, but
does not affect stored consciously data by user. E.g. cookies.
Do not recommend if data is stored consciously by user. E.g. favorites
/ bookmarks.

[General]
  - Change wording from "Clear xx traces" to "Clean xx history" to make
  it more clear and unify the naming with macOS scripts.
  - More documentation both in code and both as more references.

[Chrome]
  - Unrecommend deleting Chrome user profile.
  - Document what each chrome clean-up script is doing in more detail.

[Internet Explorer]
  - Document IE scripts better.
  - For Cookie cleanup, add solutions for later Windows version.
  - Unrecommend some from standard.
  - Remove undocumented `Local Settings\Traces` folder.
  - Take ownership before deleting Temporary Internet Files. Fixes
    permission error.
  - Remove `INetCookies\PrivacIE` script because it's undocumented and
    we already have cleanup for its parent folder (`INetCookies`).
  - Remove "%USERPROFILE%\Local Settings\Traces" due to lack of
    documentation.

[Safari]
  - Remove cleanup for undocumented traces folders `Safari\Traces`.
  - Document with subcategories and references.
  - Fix clearing all data not pointing to `localappdata`.
  - Unrecomend clearing all data.

[Opera]
  - Rename to "Clear all.." to show intent.
  - Unrecommend as it removes everything.
2021-10-28 17:43:04 +01:00
undergroundwires
64631a4552 Update dependencies
- Bump dependencies to latest.
- Remove unused inversify dependency.
- Lock sass-loader to a version that's compatible to 10. Because later
  versions (>=11) require Webpack v5 while Vue CLI v4 uses Webpack v4.
- Changes slashes as division to `math.div` as it's depreciated by SASS
  https://sass-lang.com/documentation/breaking-changes/slash
2021-10-23 20:25:03 +01:00
undergroundwires-bot
f47cb04860 ⬆️ bump everywhere to 0.11.0 2021-10-21 14:57:54 +00:00
undergroundwires
504fa056d7 Update screenshot 2021-10-21 15:48:43 +01:00
undergroundwires
739287ac71 Fix minor issues with Defender scripts
Suppress errors:
  - when deleting registry keys because the condition where key does
    not exist is not really an error,
  - when disabling a task that does not exist because absence of a task
    when trying to disable it is still a positive condition for goal to
    get rid of it,
  - when stopping or starting a service because goal is to
    disable/enable the service.

> Turn off SmartScreen App Install Control feature
- Add missing `/f` flag

> Remove "Scan with Windows Defender" option from context menu
- Add missing `/f` flag
- Remove redundant delete line in revert code
- Add missing whitespace before `/v` flag
- Instead of deleting with `/v ""`, use `/va` for explicit indent

> Disable Malicious Software Reporting tool diagnostic data
- Fix typo in revert scripts

> Turn off tamper protection
> Restrict threat history to administrators
Fix permission errors using TrustedInstaller session

> `Set-MpPreference` scripts
- Show warning when Set-MpPreference parameter is missing instead of an
  error The reasons include:
    - If the parameter is missing, functionality may be missing i.e.
      there's nothing to disable
    - In most cases different way of disabling registry is provided
    - Error is not clear and too verbose and should be implemented in a
      better way
- Add back "Limiting Defender definition updates" script due to being
  able to handle if aparameter is missing
- Add documentation for command in its script

> Disable the Potentially Unwanted Application (PUA) feature
- Fix revert code disabling another functionality

> Disable bidirectional scanning of incoming
- Fix typo

> Disable Microsoft Defender Antivirus
- Correct wrong comment regarding deprecation

> Disable Windows Defender Security Center Service
- Fix disabling not working on Windows 11
2021-10-21 15:37:09 +01:00
undergroundwires
ab8bce7686 Support disabling of protected services #74
Add new ways to disable Defender on Windows:
  1. Disable through renaming required files
  2. Disable using registry changes
  3. Disable using TrustedInstaller user

Add support for running code as TrustedInstaller 🥳. It allows running
commands in OS-protected areas. It is written in PowerShell and it uses
PowerShell syntax like backticks that are inlined in special way. So the
commit extends inlining support and allows writing PowerShell using:
  - Comments
  - Here-strings
  - Backticks

Add disabling of more Defender service

Improve documentation and categorization of services.
2021-10-20 21:12:47 +02:00
undergroundwires
e6152fa76f Improve security hardening for macOS
- Moves security improvements to its own action.
- Add more scripts and documentation for security improvements
- Move "Disable Spotlight indexing" out of wrong firewall category
- Add more documentation
2021-10-19 22:16:40 +02:00
undergroundwires
a8031d18d5 Change theme colors
Change almost all colors for better look.
2021-10-18 21:23:04 +01:00
undergroundwires
9aa8166891 Change PowerShell double quotes escape
It changes the way privacy.sexy escape double quotes inside batch
command when running PowerShell scripts as an argument to
PowerShell.exe. It uses more robust and stable way offering support for
wider use-cases.
2021-10-17 15:37:06 +01:00
undergroundwires
236a0f6c82 Add privacy over security scripts for macOS #83
It adds scripts to:
  - Disable OS security modules.
  - Clean quarantine data.
  - Disable auto-updates.
2021-10-16 19:49:41 +02:00
undergroundwires
2492f2d814 Add more ways to disable and clean Defender #74
Adds more scripts to:
 - disable Windows Defender functions,
 - remove it from the user interface,
 - clean its scan history.

Improves the documentation:
 - Adds more documentation reference URLs.
 - Restructures scripts in a way to better document their behavior. All
   Defender scripts are now under "Disable Windows Defender" and
   "Privacy over security".

Changes recommendations, and simply recommends less:
 - Defender cloud configurations are now only documented on "Strict"
   mode.
 - Watson event sending with "DisableGenericReports" is disabled due to
   lack of documentation and depreciation.
2021-10-15 17:21:37 +01:00
undergroundwires
410bcd8244 Add semi-automatic update support for macOS
For fully automatic macOS updates, electron-updater requires:
  1. Distributing macOS file as .zip (electron-userland/electron-builder#2199)
  2. Code signing for the application

privacy.sexy as of today lacks both the distribution and code signing.
This commit introduces auto-updates through automatically checking for
updates, downloading them but requiring user to drag application icons
to Applications by opening dmg file.

This commit also fixes:
  1. Progress state in update progress bar not being shown.
  2. Downloading updates were being triggered even though it was not
desired as downloads are being handled using different based on OS and
user choice.

In the end it refactors the code for handling updates of two different
kinds, and making message dialog use enums for results instead of
response integers as well as setting default and cancel button behavior.
Refactorings make behaviors more explicit and extends the control.
2021-10-13 21:25:09 +01:00
undergroundwires
b08a6b5cec Use a consistent color system
1. Renames color names in palette. Using names such as "primary" and
"secondary" that are in consistent with designs such as material,
bootstrap and metro UI palettes. It adds `color-` prefix on color
variables in line with Vue Design System.
2. Introduces necessary changes to follow the system color system
   everywhere without using any other color:
     - It changes tooltip background from black to darker primary
     colors.
     - It overrides unset styles from tree component
     - It ensures footer has same color as top menu.
3. Removes opacity CSS changes to have better control on choices. To
   achieve that:
     - It introduces new "light" variants of main colors
     - It switches to colors with different variants (e.g. in Dialogs it
       uses primary color as button as it has variants that can be
       activated on hover meanwhile on-surface color is single).
4. Styles a tags (anchor elements) globally for consistency
2021-10-11 19:33:34 +02:00
undergroundwires
37ad26a082 Remove integration tests from deployments #90
Integration tests may depend on third parties and can fail from time to
time. In some situations failing tests can be acceptable to go forward
with deployments. They should not be a requirement that blocks
deployments. They may lead to unintended lack of distributed packages as
seen in #90.
2021-10-09 12:39:05 +01:00
undergroundwires
0696ed8396 Improve disabling of SmartScreen #74
This commit renames "smart screen" to "SmartScreen" which is the
official name from Microsoft.

It categorizes scripts to document the behavior in a more clear way. It
adds structured depth. It moves all SmartScreen scripts under Defender,
as it's now part of Defender offering since latest branding.

In addition, the commit adds more documentation and more scripts such as
disabling SmartScreen for Edge.
2021-10-08 22:38:41 +02:00
undergroundwires
9942df16c8 Increase default screen width on desktop app
Goal is to show 3 card in a row as default. It gives more consistent
look to privacy.sexy across web and desktop.
2021-10-05 22:25:59 +01:00
undergroundwires
20b7d283b0 Add support for more depth in function calls
It allow pipes to be used in nested functions. Before, pipes were added
to a variable before variable content was evaluated/compiled by
another function. This commit ensures that the commits are evaluted in
expected order.

The issue is solved by stopping precompiling functions. It makes code
less complex. It adds to compile time of the script file but nothing
noticable and something optimizable.

The problem was that the call trees we're not executed in expected
order. E.g. let's say we have functionA that outputs something like
"Hello {{ $name| pipe }}", and we have function B calling with "name:
dear {{ $firstName}}", and at last we have a script that's calling
function B with "firstName: undergroundwires". Before, expressions were
evaluated directly, meaning that function A would become:
"Hello Dear {{ $firstName}}", as you see the pipe in function A
is lost here after being applied to function B and not reaching
$firstTime input value. Parsing expressions in the end allows for pipes
etc. to not get lost.

The commit also does necessary name refactorings and folder refactorings
to reflect logical changes. `FunctionCompiler` is renamed to
`SharedFunctionsParser` as precompiling is removed and it just simply
parses now. `/FunctionCall/` is moved to `/Function/Call`.

Finally, it improves documentation and adds more tests.
2021-10-04 18:13:25 +01:00
undergroundwires
f39ee76c0c Add script to remove Meet Now icon in Windows 2021-09-24 19:22:09 +01:00
undergroundwires
4b2390736a Support disabling per-user services in Windows #16
Some services in Windows have random characters appended to them. This
commit fixes the scripts that has been trying to disable them but
failing in newer Windows versions where they become per-user.
2021-09-20 23:05:15 +01:00
undergroundwires
c8cb7a5c28 Improve alignment, padding/margin issues on UI
1. It vertically centers top script menu (including selectors for view,
   OS and recommendation levels). Before, it did not utilize the empty
   space on smaller screens when of the menu items overflowed to a new
   line. This commit fixes it, also adds margin on top selectors on
   small screens.
2. It adds vertical margin between slider items on vertical view. It
   also refactors slider component so that the `v-deep` is no longer
   used, instead style is set through properties.
3. It ensures symmetrical margin on both sides of the handle in slider
   during horizontal view. Before, the left margin did not exist and
   right margin was too wide. This commit balances right and left margin
   of the arrow.
4. It changes the way margining is done for the card list. It removes
   internal margin from cards, because when they have them they also add
   that to the outer card list. This commit solves it in a way that
   unifies setting gap between cards and setting gap between cards.
   The styles are controlled on card list instead. This way same margins
   and paddings is also applied to non-card view (i.e. scripts tree).
   Before margining was done separately and those views looked
   diferently.
5. It improves styling of cards. It uses variables instead of hardcoded
   values and also refactors and renames variables for simpler
   understanding.
2021-09-19 15:33:40 +01:00
undergroundwires
5217b0b758 Add pipes to write pretty PowerShell #53
This commit introduces two pipes: `inlinePowerShell`,
`escapeDoubleQuotes`. The types when used together allows writing adding
clean and real PowerShell scripts as they are (without inlinining or
escaping them), removing the need to have hard-coded inlining/escaping.

It enables writing better PowerShell, makes it easier to maintain and
extend PowerShell scripts. Also allows writing more stable code with
less "unseen" bugs due to manual escaping/inlining. This commit
naturally reveals and fixes double quotes not being escaped in "Empty
trash bin" script.

This is solved by unifying the use of RunPowerShell function by all
scripts using PowerShell. The function inlines and escapes the scripts
as compile time to be send them to PowerShell.exe as an argument and
then invokes PowerShell.exe with generated ugly code.
2021-09-13 21:23:57 +01:00
undergroundwires
ddf417a16a Add new UX for optionally downloading updates
Before we used native method from electron for updating and notifying
(`checkForUpdatesAndNotify`). It simply checked if there's an update,
downloaded it, applied in the background and showed OS notification.

The flow is now updated. Updates will be checked, user will be asked to
confirm about whether to download and apply the updates, then a UI with
progress bar will be shown and user will be asked to restart the
application.

This commit also moves electron related logic to `/electron/` folder (as
there are now multiple files) to keep them structured. Also the electon
entrypoint `background.ts` is renamed to `main.ts`. The reason it was
named  `background.ts` by vue-cli-plugin-electron-builder was to remove
the confusion between `main.ts` of Vue itself. However, as they are
kept in different folders, but this is not the case for us.

Better than `checkForUpdatesAndNotify`.
Organizes electron desktop app logic in same folder to allow using
multiple files in a structured manner.
2021-09-11 11:04:08 +01:00
undergroundwires
2f0321f315 Bump node environment to 15.x
It updates node version to 15.x to be able to use
`String.prototype.replaceAll()`
2021-09-09 22:12:59 +01:00
undergroundwires
4d7ff7edc5 Add support for pipes in templates #53
The goal is to be able to modify values of variables used in templates.
It enables future functionality such as escaping, inlining etc.

It adds support applying predefined pipes to variables. Pipes
can be applied to variable substitution in with and parameter
substitution expressions. They work in similar way to piping in Unix
where each pipe applied to the compiled result of pipe before.

It adds support for using pipes in `with` and parameter substitution
expressions. It also refactors how their regex is build to reuse more of
the logic by abstracting regex building into a new class.

Finally, it separates and extends documentation for templating.
2021-09-08 18:58:30 +01:00
undergroundwires
862914b06e Add "with" expression for templating #53
Allows optionally rendering content if an argument is given. The
expression is designed to be used with `optional` parameters.

Goal is to allow using `RunPowerShell` function on every function that
consists of PowerShell code. Before this commit, they were all required
to provide revertCode, or none of them could be able to have it. It
would not work because some scripts can be reverted, meanwhile some are
one-way scripts that cannot be reverted (such as cleaning scripts). In
this case a way to optionally render revertCode was required. `with`
expression give each callee script ability to turn off `revertCode` if
not needed, therefore enables using `RunPowerShell` everywhere.

This commit also improves error message for script code for better
debugging and refactors parser tests for more code reuse. It also adds
more tests to parameter substitution, and renames some tests of both
expressions for consistency.
2021-09-06 21:02:41 +01:00
undergroundwires
6c3c2e6709 Improve macOS scripts for cleaning OS logs
Categorizes existing scripts on deeper level and adds more
documentation, also adds new scripts for other system log artifacts.
2021-09-05 21:51:33 +02:00
undergroundwires
c92dc1e253 Add scripts to disable, hide and opt-out from Siri 2021-09-04 23:39:14 +02:00
undergroundwires
e73c0ad1bf Do not collapse cards on links and code area #88
Detects clickable elements automatically and exempts them from
collapsing cards, also interacting with code area does no longer
collapse cards.

This commit also fixes subscribing to clicks on document every time card
list is loaded, but never unsubscribing. This impacts performance and
causes memory leaks. Now, registered event listener is removed every
time card list component is destroyed.
2021-09-03 19:44:44 +02:00
undergroundwires
6a89c6224b Add optionality for parameters
This commit allows for parameters that does not require any arguments to
be provided in function calls. It changes collection syntax where
parameters are list of objects instead of primitive strings. A
parameter has now 'name' and 'optional' properties. 'name' is required
and used in same way as older strings as parameter definitions.
'Optional' property is optional, 'false' is the default behavior if
undefined. It also adds additional validation to restrict parameter
names to alphanumeric strings to have a clear syntax in expressions.
2021-09-02 18:59:25 +01:00
undergroundwires
dcccb61781 Tighten parameter substitution tolerance
In collection templating syntax, do not tolerate whitespace after dollar sign. So while `{{ $param }}` is valid `{{ $ param }}` will be ignored.
2021-08-30 18:57:05 +01:00
undergroundwires
c0c475ff56 Change "grouping" to "view"
1. *Grouping* becomes *view*. Because *view* is more clear and extensible than *grouping*. It increases flexibility to extend by e.g. adding *flat* as a new view as discussed in #50, in this case "flat *view*" would make more sense than "flat *grouping*".
2. *None* becomes *tree*. Because *tree* is more descriptive than *none*.

Updates labels on top menu. As labels are updated, the file structure/names are refactored to follow the same concept. `TheScriptsList` is renamed to `TheScriptsView`. Also refactors `ViewChanger` so view types are presented in same way.
2021-08-29 11:33:16 +01:00
undergroundwires-bot
6dc768817f ⬆️ bump everywhere to 0.10.3 2021-08-28 10:22:47 +00:00
undergroundwires
439cd303ff Fix dead URLs 2021-08-27 21:53:13 +01:00
undergroundwires
ec0c972d34 Fix excessive highlighting on hover
It fixes whitespace on left when being highlighted when hovering on macOS (OS selection button on top)
The commit also unifies the way top menu buttons are displayed by reusing `MenuOptionListItem`s (renamed from `SelectableOption`) and `MenuOptionList`. This ensures right and consistent behavior.
Finally it fixes `enabled` property in menu option setting disabled state instead.
2021-08-26 21:08:38 +01:00
undergroundwires
2a08855e5d Fix tests for ParameterSubstitutionParser
Fix nested mocha "it"s and "different parameters" test having wrong expectation
and add a test for whitespace tolerance.
2021-08-25 17:15:54 +01:00
undergroundwires
1c6b3057ea Fix select options being clickable when disabled 2021-08-24 21:18:52 +01:00
undergroundwires
ea5f9ec27d Fix infinitely subscribing to state changes 2021-08-23 17:38:14 +01:00
undergroundwires
f2935e4008 Improve issue templates
- Use same multi-lined comment convention
- Highlight that "additional information" in a bug report is optional
- Remove recommendation for pasting script in a bug report as it's too long
- Rename feature request issue file to follow same naming convention
- Document also creating a issue as a way to extend scripts
- Add reproduction steps in script bug reports
- Use names instead of commands in heading
2021-08-22 15:59:44 +01:00
Bram Ceulemans
487001af48 Fix typo on main page (#82) 2021-08-21 19:07:27 +00:00
Marc05
71e70e50c5 Fix NTP configuration before running the service (#72)
Co-authored-by: Marc05 <git@marc05.net>
2021-08-20 17:21:08 +00:00
undergroundwires
0a857aa09e bump dependencies to latest #75, #69 2021-05-08 23:46:59 +02:00
undergroundwires
b976b92031 fix hiding recent files in quick access
It fixes x64 / x86 conditions in "Do not show recently used files in Quick Access" script. Wow6432Node only exists in x64 systems for x86 application data which is also consumed by explorer.exe. So it should only be edited in x64 systems. The rest of the registry settings ("ShowRecent" and default "DelegateFolders") applies to both x64 and x86 systems.
2021-05-07 16:49:07 +02:00
undergroundwires
db62ed7f3a fix broken URLs and automate broken URL checks #70
This commit:
- Fixes broken URLs using archive.org or other references.
- Replaces tenforums.com URLs with better documentation as they tend to return HTTP status code 403 to tests and also are low quality source.
- Changes all insecure http sources to https alternatives
- Adds integration tests to check for broken URLs
  - There's logic implemented for having a delay inbetween when sending requests to same domains, however it's not used as the sources can respond to totally parallelized requests.
- Run test pipeline weekly to get notified about broken URls without commits
2021-05-05 23:57:41 +02:00
undergroundwires
36f0805590 unify usage of sleepAsync and add tests
The tests mock JS setTimeout API. However promise.resolve() is not working without flushing the promise queue (which could be done just by awaiting Promise.resolve()), similar issue has been discussed in facebook/jest#2157.
2021-05-04 19:10:23 +02:00
undergroundwires
49600c5f37 add initial integration tests
Integration tests are executed using vue-cli-service with double quotes as following: `vue-cli-service test:unit "tests/integration/**/*.spec.ts"`. Using single quotes (mochajs/mocha#1828) works on macOS and Ubuntu but does not on Windows (tests are not found). Double quotes is the only portable way that works on all three platforms (mochajs/mocha#3136).
2021-05-03 15:48:01 +02:00
Marc05
eb9ac35a92 fix incorrect modification of Desktop folder on ThisPC (#71)
Fixes hiding "Desktop" from This PC and Dialog boxes not working as expected
2021-04-25 16:02:36 +02:00
undergroundwires
77148980e0 unrecommend VSS and document its breaking behavior 2021-04-21 20:07:38 +02:00
undergroundwires-bot
b3d2e82025 ⬆️ bump everywhere to 0.10.2 2021-04-20 16:47:11 +00:00
undergroundwires
b25b8cc805 fix vue warning for undefined property during render
currentOs is not recognized as reactive property as it's set to "undefined". JavaScript does not accept "undefined" as valid value to initialize. A property needs to be initialized with a non-undefined value to become reactive in a class-based component. Otherwise Vue warns: Property or method "currentOs" is not defined on the instance but referenced during render.
2021-04-19 18:21:06 +02:00
Marc05
8141a01ef7 fix typo and dead URL in Windows scripts (#70)
Co-authored-by: Marc05 <git@marc05.net>
2021-04-18 19:12:50 +02:00
undergroundwires
a2f10857e2 fix script revert activating recommendation level
Reverting any single of the scripts from standard recommendation pool
shows "Standard" selection as selected which is wrong. This commit fixes
it, refactors selection handling in a separate class and it also adds
missing tests. It removes UserSelection.totalSelected propertty in favor of using
UserSelection.selectedScripts.length to provide unified way of accessing
the information.
2021-04-17 14:34:29 +01:00
undergroundwires
aea04e5f7c document chromium warning for policy changes
Chromium shows "Your browser is managed" or "Your browser is managed by an organization" warnings when its behavior is manipulated using policies. This message confuses some users, so the commit marks this behavior to let users know why the box appears.
Read more:
- https://chromium.googlesource.com/chromium/src/+/refs/tags/92.0.4475.1/chrome/browser/ui/managed_ui.cc#67
- https://support.google.com/chrome/thread/3262871
2021-04-16 17:56:31 +02:00
undergroundwires
60c80611ea add module alias '@tests/'
Alias would remove unnecessary repetitions and less relative paths make changes easier when moving around files. This commit cleans also up some relative paths ('../../../') by using the alias and orders imports. It updates both path alias in tsconfig and module alias in Vue CLI's bundler (vuejs/vue-cli#2398).
2021-04-15 18:34:40 +02:00
undergroundwires
b1ed3ce55f document breaking behavior in script name #64
Removing Cloud Experience Hosting app breaks Microsoft cloud and
corporate sign in. It's now documented more cleary in the name of the
script.
2021-04-14 15:44:22 +01:00
undergroundwires
040ed2701c improve disabling ads and marketing #65
This commit documents the behavior better with more granularity of
choice and also adds options to revert the code.
2021-04-13 16:39:44 +01:00
undergroundwires
00d8e551db refactor extra code, duplicates, complexity
- refactor array equality check and add tests
- remove OperatingSystem.Unknown causing extra logic, return undefined instead
- refactor enum validation to share same logic
- refactor scripting language factories to share same logic
- refactor too many args in runCodeAsync
- refactor ScriptCode constructor to reduce complexity
- fix writing useless write to member object since another property write always override it
2021-04-11 14:37:02 +01:00
dependabot[bot]
3e9c99f5f8 Bump y18n from 3.2.1 to 3.2.2 (#66)
Bumps [y18n](https://github.com/yargs/y18n) from 3.2.1 to 3.2.2.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-06 20:29:15 +02:00
undergroundwires
02bdc4cf04 fix desktop initial window size being bigger than current display size on smaller Linux/Windows screens 2021-04-05 14:31:31 +01:00
undergroundwires
5c43965f0b in CI/CD, run other tests/check even if one of them fails 2021-03-28 14:26:20 +01:00
dependabot[bot]
b2376ecc30 Bump elliptic from 6.5.3 to 6.5.4 (#62)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-27 17:15:30 +01:00
undergroundwires-bot
aeaa6deeb4 ⬆️ bump everywhere to 0.10.1 2021-03-26 14:57:03 +00:00
271 changed files with 40634 additions and 4494 deletions

View File

@@ -11,9 +11,11 @@ Please fill in as much of the template below as you're able.
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient. As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
--> -->
### Describe the bug ### Description
<!-- A clear and concise description of what the bug is. --> <!--
A clear and concise description of what the bug is.
-->
### OS ### OS
@@ -23,14 +25,32 @@ On Windows you can find it using "Start button" > "Settings" > "System" > "About
On macOS you can find it using "Apple menu (top left corner)" > "About This Mac". On macOS you can find it using "Apple menu (top left corner)" > "About This Mac".
--> -->
### Screenshots ### Reproduction steps
<!-- If applicable, add screenshots to help explain your problem. --> <!--
How can the bug be recreated?
It's the most important information in the bug report. Bugs that cannot be reproduced cannot be fixed and verified.
E.g.
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
-->
### Scripts ### Scripts
<!-- Which scripts did you execute? If applicable, please paste the executed scripts or attach the generated privacy.sexy file . --> <!--
If applicable, please attach the generated privacy.sexy file instead of copy pasting which becomes too long.
-->
### Screenshots
<!--
If applicable, add screenshots to help explain your problem.
-->
### Additional information ### Additional information
<!-- Add any other context about the problem here. --> <!--
If applicable, add any other context about the problem here.
-->

View File

@@ -11,13 +11,16 @@ Please fill in as much of the template below as you're able.
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient. As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
--> -->
### Describe the bug ### Description
<!-- A clear and concise description of what the bug is. --> <!--
A clear and concise description of what the bug is.
-->
### To Reproduce ### Reproduction steps
<!-- <!--
It's the most important information in the bug report. Bugs that cannot be reproduced cannot be fixed and verified.
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
@@ -41,12 +44,12 @@ If applicable, add screenshots to help explain your problem.
<!-- <!--
If applicable, mention how you were using privacy.sexy when the bug was encountered: If applicable, mention how you were using privacy.sexy when the bug was encountered:
- Web (on Desktop or mobile?) - Web (on Desktop or mobile?)
- Or desktop (Windows, macOS or Linux?) - Or desktop (Windows, macOS or Linux?)
--> -->
### Additional context ### Additional context
<!-- <!--
Add any other context about the problem here. If applicable, add any other context about the problem here.
--> -->

View File

@@ -0,0 +1,36 @@
---
name: Feature request
about: Suggest an idea for privacy.sexy
labels: enhancement
---
<!--
Thank you for suggesting an idea to improve privacy better 🤗.
Please fill in as much of the template below as you're able.
-->
### Problem description
<!--
What are we trying to solve?
Please add a clear and concise description of the problem you are seeking to solve with this feature request.
E.g. I'm always frustrated when [...]
-->
### Proposed solution
<!--
Describe the solution you'd like in a clear and concise manner.
-->
### Alternatives considered
<!--
A clear and concise description of any alternative solutions or features you've considered.
-->
### Additional information
<!--
If applicable, add any other context or screenshots about the feature request here.
-->

View File

@@ -1,27 +0,0 @@
---
name: Feature request
about: Suggest an idea for privacy.sexy
labels: enhancement
---
<!--
Thank you for suggesting an idea to make privacy better. 🤗
Please fill in as much of the template below as you're able.
-->
### Problem Description
<!-- Please add a clear and concise description of the problem you are seeking to solve with this feature request. Ex. I'm always frustrated when [...] -->
### Proposed solution
<!-- Describe the solution you'd like in a clear and concise manner. -->
### Alternatives considered
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
### Additional information
<!-- Add any other context or screenshots about the feature request here. -->

View File

@@ -0,0 +1,73 @@
---
name: New script suggestion
about: Suggest a new script for privacy.sexy
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).
-->
### OS
<!--
Which OS will the new script configure?
Either "Windows" or "macOS".
-->
### Name
<!--
The name of the script.
It should start with an imperative noun such as "disable", "turn off" , "clear"...
E.g. "Disable webcam telemetry"
-->
### Script code
<!--
Code that will be executed when script is selected.
Try to keep it as simple and backwards-compatible as possible.
Allowed languages:
- macOS: bash (sh)
- Windows: PowerShell (ps1) or batchfile
- 💡 Prioritize the one that's simpler, batchfile if similar.
-->
### 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).
-->
### Suggested 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.
-->
### Suggested 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.
-->
### Additional 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.
-->

View File

@@ -22,10 +22,10 @@ jobs:
- name: Setup node - name: Setup node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: '14.x' node-version: 15.x
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Run tests - name: Run unit tests
run: npm run test:unit run: npm run test:unit
- name: Publish desktop app - name: Publish desktop app
run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github

View File

@@ -83,13 +83,13 @@ jobs:
name: "App: Setup node" name: "App: Setup node"
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: '14.x' node-version: 15.x
- -
name: "App: Install dependencies" name: "App: Install dependencies"
run: npm ci run: npm ci
working-directory: site working-directory: site
- -
name: "App: Run tests" name: "App: Run unit tests"
run: npm run test:unit run: npm run test:unit
working-directory: site working-directory: site
- -

View File

@@ -13,13 +13,14 @@ jobs:
- npm run lint:md - npm run lint:md
- npm run lint:md:relative-urls - npm run lint:md:relative-urls
- npm run lint:md:consistency - npm run lint:md:consistency
fail-fast: false # So it continues with other commands if one fails
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup node - name: Setup node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 14.x node-version: 15.x
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Lint - name: Lint

View File

@@ -5,7 +5,7 @@ on:
pull_request: pull_request:
paths: [ '/package.json', '/package-lock.json' ] # Allow PRs to be green if they do not introduce dependency change paths: [ '/package.json', '/package-lock.json' ] # Allow PRs to be green if they do not introduce dependency change
schedule: schedule:
- cron: '0 0 * * 0' - cron: '0 0 * * 0' # at 00:00 on every Sunday
jobs: jobs:
npm-audit: npm-audit:
@@ -18,7 +18,7 @@ jobs:
name: Setup node name: Setup node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 14.x node-version: 15.x
- -
name: NPM audit name: NPM audit
run: npm audit run: exit "$(npm audit)" # Since node 15.x, it does not fail with error if we don't explicitly exit

View File

@@ -1,12 +1,17 @@
name: Test name: Test
on: [ push, pull_request ] on:
push:
pull_request:
schedule: # for integration tests
- cron: '0 0 * * 0' # at 00:00 on every Sunday
jobs: jobs:
run-tests: run-tests:
strategy: strategy:
matrix: matrix:
os: [macos, ubuntu, windows] os: [macos, ubuntu, windows]
fail-fast: false # So it still runs on other OSes if one of them fails
runs-on: ${{ matrix.os }}-latest runs-on: ${{ matrix.os }}-latest
steps: steps:
- -
@@ -16,10 +21,13 @@ jobs:
name: Setup node name: Setup node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: '14.x' node-version: 15.x
- -
name: Install dependencies name: Install dependencies
run: npm ci run: npm ci
- -
name: Run tests name: Run unit tests
run: npm run test:unit run: npm run test:unit
-
name: Run integration tests
run: npm run test:integration

View File

@@ -1,5 +1,88 @@
# Changelog # Changelog
## 0.11.0 (2021-10-21)
* Change "grouping" to "view" | [c0c475f](https://github.com/undergroundwires/privacy.sexy/commit/c0c475ff564b23a4dabcc03ac2909207a8eb61ce)
* Tighten parameter substitution tolerance | [dcccb61](https://github.com/undergroundwires/privacy.sexy/commit/dcccb617813625c224a28242c5b965bb4cd6f189)
* Add optionality for parameters | [6a89c62](https://github.com/undergroundwires/privacy.sexy/commit/6a89c6224bdef5eb96980471f3b3935b9351b197)
* Do not collapse cards on links and code area #88 | [e73c0ad](https://github.com/undergroundwires/privacy.sexy/commit/e73c0ad1bf922b1dd3360fc5aafc3434951fa63c)
* Add scripts to disable, hide and opt-out from Siri | [c92dc1e](https://github.com/undergroundwires/privacy.sexy/commit/c92dc1e25387c65a3a41ca64d2a23cf8131b4c86)
* Improve macOS scripts for cleaning OS logs | [6c3c2e6](https://github.com/undergroundwires/privacy.sexy/commit/6c3c2e6709ec84f8e0411f19c024bab2c7e5753b)
* Add "with" expression for templating #53 | [862914b](https://github.com/undergroundwires/privacy.sexy/commit/862914b06ea9ef74c4b58a9a4164a10a38273638)
* Add support for pipes in templates #53 | [4d7ff7e](https://github.com/undergroundwires/privacy.sexy/commit/4d7ff7edc5a96cc0d99d3c1ca4fdf9bbdace3fd2)
* Bump node environment to 15.x | [2f0321f](https://github.com/undergroundwires/privacy.sexy/commit/2f0321f315ac0da8c713dd50e37032f1de194942)
* Add new UX for optionally downloading updates | [ddf417a](https://github.com/undergroundwires/privacy.sexy/commit/ddf417a16a79551b43576befab0541ea08487969)
* Add pipes to write pretty PowerShell #53 | [5217b0b](https://github.com/undergroundwires/privacy.sexy/commit/5217b0b7587ccfe509ba8adc3a7748b9bae14d7a)
* Improve alignment, padding/margin issues on UI | [c8cb7a5](https://github.com/undergroundwires/privacy.sexy/commit/c8cb7a5c28420557319606da82f56b011e88f470)
* Support disabling per-user services in Windows #16 | [4b23907](https://github.com/undergroundwires/privacy.sexy/commit/4b2390736ac1f9de2d5176b7b07da0e827112f9a)
* Add script to remove Meet Now icon in Windows | [f39ee76](https://github.com/undergroundwires/privacy.sexy/commit/f39ee76c0cda95f54502b19d5c49390fd0f12b5e)
* Add support for more depth in function calls | [20b7d28](https://github.com/undergroundwires/privacy.sexy/commit/20b7d283b02dd751dfbde18ef1fe334c6bf76e2b)
* Increase default screen width on desktop app | [9942df1](https://github.com/undergroundwires/privacy.sexy/commit/9942df16c8334ff041fb92f432a3a29e351c88df)
* Improve disabling of SmartScreen #74 | [0696ed8](https://github.com/undergroundwires/privacy.sexy/commit/0696ed8396e298a358bec17adb91c9145dd90418)
* Remove integration tests from deployments #90 | [37ad26a](https://github.com/undergroundwires/privacy.sexy/commit/37ad26a082851c02497c36e7fce40555b9480e11)
* Use a consistent color system | [b08a6b5](https://github.com/undergroundwires/privacy.sexy/commit/b08a6b5cecf4a53023053695292146edbd24b960)
* Add semi-automatic update support for macOS | [410bcd8](https://github.com/undergroundwires/privacy.sexy/commit/410bcd82445097c29c9fcf0eabf7af9ebcb93c1e)
* Add more ways to disable and clean Defender #74 | [2492f2d](https://github.com/undergroundwires/privacy.sexy/commit/2492f2d8141b3abdf590ccad59680b1f50ecb59e)
* Add privacy over security scripts for macOS #83 | [236a0f6](https://github.com/undergroundwires/privacy.sexy/commit/236a0f6c8241294fc397194cd1b20bdeccbbb50b)
* Change PowerShell double quotes escape | [9aa8166](https://github.com/undergroundwires/privacy.sexy/commit/9aa816689146ee6cd86d8262112677c38651c6bd)
* Change theme colors | [a8031d1](https://github.com/undergroundwires/privacy.sexy/commit/a8031d18d520dd3b0567f7b8cfe2dcd694b65073)
* Improve security hardening for macOS | [e6152fa](https://github.com/undergroundwires/privacy.sexy/commit/e6152fa76f5e7d23b0f79d5dd98713daaecbff90)
* Support disabling of protected services #74 | [ab8bce7](https://github.com/undergroundwires/privacy.sexy/commit/ab8bce768650a10677f0a13b3a9fae93c83802ff)
* Fix minor issues with Defender scripts | [739287a](https://github.com/undergroundwires/privacy.sexy/commit/739287ac71b3f8b04348fc101f1fa06f2d7d86a2)
* Update screenshot | [504fa05](https://github.com/undergroundwires/privacy.sexy/commit/504fa056d7d8b17fc20afd398f9a557495fca7e8)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.3...0.11.0)
## 0.10.3 (2021-08-27)
* unrecommend VSS and document its breaking behavior | [7714898](https://github.com/undergroundwires/privacy.sexy/commit/77148980e08859f89c15c6604e55b56ce4f74358)
* fix incorrect modification of Desktop folder on ThisPC (#71) | [eb9ac35](https://github.com/undergroundwires/privacy.sexy/commit/eb9ac35a923325cc2c9983ef71c0d904337a58f5)
* add initial integration tests | [49600c5](https://github.com/undergroundwires/privacy.sexy/commit/49600c5f37ca33c1687885fdf02a71ef7d3e6e8c)
* unify usage of sleepAsync and add tests | [36f0805](https://github.com/undergroundwires/privacy.sexy/commit/36f08055909f371fd9cbe3480ea813b963aea22b)
* fix broken URLs and automate broken URL checks #70 | [db62ed7](https://github.com/undergroundwires/privacy.sexy/commit/db62ed7f3ac63e9f2d762eb946060595eb9f5626)
* fix hiding recent files in quick access | [b976b92](https://github.com/undergroundwires/privacy.sexy/commit/b976b920318dba55b32d39f148fdca4f6be3cce3)
* bump dependencies to latest #75, #69 | [0a857aa](https://github.com/undergroundwires/privacy.sexy/commit/0a857aa09ee703d34ad0422bd1731158017a9a58)
* Fix NTP configuration before running the service (#72) | [71e70e5](https://github.com/undergroundwires/privacy.sexy/commit/71e70e50c51249bb10f6203414948b325acc2b2a)
* Fix typo on main page (#82) | [487001a](https://github.com/undergroundwires/privacy.sexy/commit/487001af485fdbb958615d7b52c09c2e386ddaf2)
* Improve issue templates | [f2935e4](https://github.com/undergroundwires/privacy.sexy/commit/f2935e4008f1231ef174f8932290e11715564d20)
* Fix infinitely subscribing to state changes | [ea5f9ec](https://github.com/undergroundwires/privacy.sexy/commit/ea5f9ec27df7cec6ac575e23fef18948d2b8e68a)
* Fix select options being clickable when disabled | [1c6b305](https://github.com/undergroundwires/privacy.sexy/commit/1c6b3057ea6e45125cadf374f20a905712ccdf3c)
* Fix tests for `ParameterSubstitutionParser` | [2a08855](https://github.com/undergroundwires/privacy.sexy/commit/2a08855e5d1bdf74354fd692cbfebd1a48e495ac)
* Fix excessive highlighting on hover | [ec0c972](https://github.com/undergroundwires/privacy.sexy/commit/ec0c972d348ffd5897f115d201031b704875b56a)
* Fix dead URLs | [439cd30](https://github.com/undergroundwires/privacy.sexy/commit/439cd303ff3db96a53664e5f44fefe12b95c5e6c)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.2...0.10.3)
## 0.10.2 (2021-04-19)
* in CI/CD, run other tests/check even if one of them fails | [5c43965](https://github.com/undergroundwires/privacy.sexy/commit/5c43965f0bc44f991ada7d3bad68937a80665dc3)
* fix desktop initial window size being bigger than current display size on smaller Linux/Windows screens | [02bdc4c](https://github.com/undergroundwires/privacy.sexy/commit/02bdc4cf0426c452f3fc9af52b819ca9b0757290)
* refactor extra code, duplicates, complexity | [00d8e55](https://github.com/undergroundwires/privacy.sexy/commit/00d8e551db001247fadfb6f6af7a4c5ce19a9e64)
* improve disabling ads and marketing #65 | [040ed27](https://github.com/undergroundwires/privacy.sexy/commit/040ed2701c4a468749901f4c5369b221bc0973c4)
* document breaking behavior in script name #64 | [b1ed3ce](https://github.com/undergroundwires/privacy.sexy/commit/b1ed3ce55f2d003cad1ead23e674aa66d4eb5802)
* add module alias '@tests/' | [60c8061](https://github.com/undergroundwires/privacy.sexy/commit/60c80611eab227791fabb883caf93418cef5fd00)
* document chromium warning for policy changes | [aea04e5](https://github.com/undergroundwires/privacy.sexy/commit/aea04e5f7cd48fbb9b407b68ade75575a6064c82)
* fix script revert activating recommendation level | [a2f1085](https://github.com/undergroundwires/privacy.sexy/commit/a2f10857e2a8debb3ce01f79b0dfbe8649ea9a17)
* fix typo and dead URL in Windows scripts (#70) | [8141a01](https://github.com/undergroundwires/privacy.sexy/commit/8141a01ef798331b4d82f5ca95f7b18df4f6f912)
* fix vue warning for undefined property during render | [b25b8cc](https://github.com/undergroundwires/privacy.sexy/commit/b25b8cc8052655af70b0695c6c3085974d783bb6)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.1...0.10.2)
## 0.10.1 (2021-03-25)
* refactor script compilation to make it easy to add new expressions #41 #53 | [646db90](https://github.com/undergroundwires/privacy.sexy/commit/646db9058541cebd0af437554de04fdc6bb63a6e)
* restructure presentation layer | [f3c7413](https://github.com/undergroundwires/privacy.sexy/commit/f3c7413f529be4a00dba7b0ab23904b48ea13a35)
* fix a test where "it" is not used inside "describe" | [1a5f920](https://github.com/undergroundwires/privacy.sexy/commit/1a5f92021f7423cd039f8f5326cd6f99b355c962)
* bump dependencies to latest | [1f515e7](https://github.com/undergroundwires/privacy.sexy/commit/1f515e7be525291c960ccb71db05312db6da53f5)
* fix throttle function not being able to run with argument(s) | [1935db1](https://github.com/undergroundwires/privacy.sexy/commit/1935db10192051401ab00ca2cd767955d0d3b866)
* fix fs module hanging not allowing code to run | [5f527a0](https://github.com/undergroundwires/privacy.sexy/commit/5f527a00cf225d3e74b3f6577d6e2456e919de24)
* refactor all modals to use same dialog component | [6f46cdb](https://github.com/undergroundwires/privacy.sexy/commit/6f46cdb4ed49a8941c6c0dde5c5e2a816c06daef)
* fix safari cleanup scripts that are not working on modern versions | [05932c5](https://github.com/undergroundwires/privacy.sexy/commit/05932c5a36446d551c5bc811165e3295fbe15e3f)
* refactor features to use shared functions #41 | [ac2249f](https://github.com/undergroundwires/privacy.sexy/commit/ac2249f25664827d8a6d2c7ebd659ccf126b0cde)
* increase performance by polyfilling ResizeObserver only if required | [448e378](https://github.com/undergroundwires/privacy.sexy/commit/448e378dc4501f9de69af63634c87d0e5060bf52)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.0...0.10.1)
## 0.10.0 (2021-03-02) ## 0.10.0 (2021-03-02)
* allow functions to call other functions #53 | [7661575](https://github.com/undergroundwires/privacy.sexy/commit/7661575573c6d3e8f4bc28bfa7a124a764c72ef9) * allow functions to call other functions #53 | [7661575](https://github.com/undergroundwires/privacy.sexy/commit/7661575573c6d3e8f4bc28bfa7a124a764c72ef9)

View File

@@ -16,7 +16,7 @@
- Online version at [https://privacy.sexy](https://privacy.sexy) - Online version at [https://privacy.sexy](https://privacy.sexy)
- 💡 No need to run any compiled software on your computer. - 💡 No need to run any compiled software on your computer.
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.0/privacy.sexy-Setup-0.10.0.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.0/privacy.sexy-0.10.0.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.0/privacy.sexy-0.10.0.AppImage). - Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.0/privacy.sexy-Setup-0.11.0.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.0/privacy.sexy-0.11.0.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.0/privacy.sexy-0.11.0.AppImage).
- 💡 Single click to execute your script. - 💡 Single click to execute your script.
- ❗ Come back regularly to apply latest version for stronger privacy and security. - ❗ Come back regularly to apply latest version for stronger privacy and security.
@@ -30,21 +30,25 @@
- Have full visibility into what the tweaks do as you enable them - Have full visibility into what the tweaks do as you enable them
- Ability to revert (undo) applied scripts - Ability to revert (undo) applied scripts
- Everything is transparent: both application and its infrastructure are open-source and automated - Everything is transparent: both application and its infrastructure are open-source and automated
- Easily extendable - Easily extendable with [own powerful templating language](./docs/templating.md)
- Each script is independently executable without cross-dependencies
## Extend scripts ## Extend scripts
1. Fork the repository - You can either [create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose)
2. Add more scripts in respective script collection in [collections](src/application/collections/) folder. - Or send a PR:
- 📖 If you're unsure about the syntax you can refer to the [collection files | documentation](docs/collection-files.md). 1. Fork the repository
- 🙏 For any new script, please add `revertCode` and `docs` values if possible. 2. Add more scripts in respective script collection in [collections](src/application/collections/) folder.
3. Send a pull request 👌 - 📖 If you're unsure about the syntax you can refer to the [collection files | documentation](docs/collection-files.md).
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
3. Send a pull request 👌
## Commands ## Commands
- Project setup: `npm install` - Project setup: `npm install`
- Testing - Testing
- Run unit tests: `npm run test:unit` - Run unit tests: `npm run test:unit`
- Run integration tests: `npm run test:integration`
- Lint: `npm run lint` - Lint: `npm run lint`
- **Desktop app** - **Desktop app**
- Development: `npm run electron:serve` - Development: `npm run electron:serve`
@@ -53,8 +57,8 @@
- Development: `npm run serve` to compile & hot-reload for development. - Development: `npm run serve` to compile & hot-reload for development.
- Production: `npm run build` to prepare files for distribution. - Production: `npm run build` to prepare files for distribution.
- Or run using Docker: - Or run using Docker:
1. Build: `docker build -t undergroundwires/privacy.sexy:0.10.0 .` 1. Build: `docker build -t undergroundwires/privacy.sexy:0.11.0 .`
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.10.0 undergroundwires/privacy.sexy:0.10.0` 2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.11.0 undergroundwires/privacy.sexy:0.11.0`
## Architecture overview ## Architecture overview

View File

@@ -3,6 +3,14 @@
- It's mainly responsible for - It's mainly responsible for
- creating and event based [application state](#application-state) - creating and event based [application state](#application-state)
- [parsing](#parsing) and [compiling](#compiling) [application data](#application-data) - [parsing](#parsing) and [compiling](#compiling) [application data](#application-data)
- Consumed by [presentation layer](./presentation.md)
## Structure
- [`/src/` **`application/`**](./../src/application/): Contains all application related code.
- [**`collections/`**](./../src/application/collections/): Holds [collection files](./collection-files.md)
- [**`Common/`**](./../src/application/Common/): Contains common functionality that is shared in application layer.
- `..`: other classes are categorized using folders-by-feature structure
## Application state ## Application state

View File

@@ -45,9 +45,11 @@
### `Script` ### `Script`
- Script represents a single tweak. - Script represents a single tweak.
- A script must include either: - A script can be of two different types (just like [functions](#function)):
- A `code` and `revertCode` 1. Inline script; a script with an inline code
- Or `call` to call YAML-defined functions - 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. - 🙏 For any new script, please add `revertCode` and `docs` values if possible.
#### `Script` syntax #### `Script` syntax
@@ -80,7 +82,7 @@
### `FunctionCall` ### `FunctionCall`
- Describes a single call to a function by optionally providing values to its parameters. - Describes a single call to a function by optionally providing values to its parameters.
- 👀 See [parameter substitution](#parameter-substitution) for an example usage - 👀 See [parameter substitution](./templating.md#parameter-substitution) for an example usage
#### `FunctionCall` syntax #### `FunctionCall` syntax
@@ -98,52 +100,18 @@
appName: Microsoft.WindowsFeedbackHub appName: Microsoft.WindowsFeedbackHub
``` ```
- 💡 [Expressions (templating)](./templating.md#expressions) can be used as parameter value
### `Function` ### `Function`
- Functions allow re-usable code throughout the defined scripts. - Functions allow re-usable code throughout the defined scripts.
- Functions are templates compiled by privacy.sexy and uses special [expressions](#expressions). - Functions are templates compiled by privacy.sexy and uses special expression expressions.
- Functions can call other functions by defining `call` property instead of `code` - A function can be of two different types (just like [scripts](#script)):
- 👀 See [parameter substitution](#parameter-substitution) for an example usage 1. Inline function: a function with an inline code.
- Must define `code` property and optionally `revertCode` but not `call`.
#### Expressions 2. Caller function: a function that calls other functions.
- Must define `call` property but not `code` or `revertCode`.
- Expressions are defined inside mustaches (double brackets, `{{` and `}}`) - 👀 Read more on [Templating](./templating.md) for function expressions and [example usages](./templating.md#parameter-substitution).
##### Parameter substitution
A simple function example
```yaml
function: EchoArgument
parameters: [ 'argument' ]
code: Hello {{ $argument }} !
```
It would print "Hello world" if it's called in a [script](#script) as following:
```yaml
script: Echo script
call:
function: EchoArgument
parameters:
argument: World
```
A function can call other functions such as:
```yaml
-
function: CallerFunction
parameters: [ 'value' ]
call:
function: EchoArgument
parameters:
argument: {{ $value }}
-
function: EchoArgument
parameters: [ 'argument' ]
code: Hello {{ $argument }} !
```
#### `Function` syntax #### `Function` syntax
@@ -152,24 +120,43 @@ A function can call other functions such as:
- Convention is to use camelCase, and be verbs. - Convention is to use camelCase, and be verbs.
- E.g. `uninstallStoreApp` - E.g. `uninstallStoreApp`
- ❗ Function names must be unique - ❗ Function names must be unique
- `parameters`: `[` *`string`* `, ... ]` - `parameters`: `[` ***[`FunctionParameter`](#FunctionParameter)*** `, ... ]`
- Name of the parameters that the function has. - List of parameters that function code refers to.
- Parameter values are provided by a [Script](#script) through a [FunctionCall](#FunctionCall) - ❗ Must be defined to be able use in [`FunctionCall`](#functioncall) or [expressions (templating)](./templating.md#expressions)
- Parameter names must be defined to be used in [expressions](#expressions)
- ❗ Parameter names must be unique
`code`: *`string`* (**required** if `call` is undefined) `code`: *`string`* (**required** if `call` is undefined)
- Batch file commands that will be executed - 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 defined, best practice to also define `revertCode`
- ❗ If not defined `call` must be defined - ❗ If not defined `call` must be defined
- `revertCode`: *`string`* - `revertCode`: *`string`*
- Code that'll undo the change done by `code` property. - 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` - 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` - 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**) - `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
- A shared function or sequence of functions to call (called in order) - A shared function or sequence of functions to call (called in order)
- The parameter values that are sent can use [expressions](#expressions) - The parameter values that are sent can use [expressions (templating)](./templating.md#expressions)
- ❗ If not defined `code` must be defined - ❗ If not defined `code` must be defined
### `FunctionParameter`
- Defines a parameter that function requires optionally or mandatory.
- Its arguments are provided by a [Script](#script) through a [FunctionCall](#FunctionCall).
#### `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.
- `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;
- 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` ### `ScriptingDefinition`
- Defines global properties for scripting that's used throughout its parent [Collection](#collection). - Defines global properties for scripting that's used throughout its parent [Collection](#collection).
@@ -180,7 +167,7 @@ A function can call other functions such as:
- 📖 See [ScriptingLanguage.ts](./../src/domain/ScriptingLanguage.ts) enumeration for allowed values. - 📖 See [ScriptingLanguage.ts](./../src/domain/ScriptingLanguage.ts) enumeration for allowed values.
- `startCode:` *`string`* (**required**) - `startCode:` *`string`* (**required**)
- Code that'll be inserted on top of user created script. - Code that'll be inserted on top of user created script.
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!` - 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**) - `endCode:` *`string`* (**required**)
- Code that'll be inserted at the end of user created script. - 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](#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!` - Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](./templating.md#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`

View File

@@ -12,7 +12,8 @@
- [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that are shared across other components. - [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that are shared across other components.
- [**`styles/`**](./../src/presentation/styles/): Contains shared styles used throughout different components. - [**`styles/`**](./../src/presentation/styles/): Contains shared styles used throughout different components.
- [**`main.ts`**](./../src/presentation/main.ts): Application entry point that mounts and starts Vue application. - [**`main.ts`**](./../src/presentation/main.ts): Application entry point that mounts and starts Vue application.
- [**`background.ts`**](./../src/presentation/background.ts): Main process of Electron, started as first thing when app starts. - [`electron/`](./../src/presentation/electron/): Electron configuration for the desktop application.
- [**`main.ts`**](./../src/presentation/main.ts): Main process of Electron, started as first thing when app starts.
- [**`/public/`**](./../public/): Contains static assets that will simply be copied and not go through webpack. - [**`/public/`**](./../public/): Contains static assets that will simply be copied and not go through webpack.
- [**`/vue.config.js`**](./../vue.config.js): Global Vue CLI configurations loaded by `@vue/cli-service` - [**`/vue.config.js`**](./../vue.config.js): Global Vue CLI configurations loaded by `@vue/cli-service`
- [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations that are used by Vue CLI internally - [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations that are used by Vue CLI internally

88
docs/templating.md Normal file
View File

@@ -0,0 +1,88 @@
# Templating
## Benefits of templating
- Generating scripts by sharing code to increase best-practice usage and maintainability.
- Creating self-contained scripts without depending on each other that can be easily shared.
- Use of pipes for writing cleaner code and letting pipes do dirty work.
## Expressions
- Expressions in the language are defined inside mustaches (double brackets, `{{` and `}}`).
- Expression syntax is inspired mainly by [Go Templates](https://pkg.go.dev/text/template).
- Expressions are used in and enabled by functions where they can be used.
- In script definition parts of a function, see [`Function`](./collection-files.md#Function).
- When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function).
### Parameter substitution
A simple function example:
```yaml
function: EchoArgument
parameters:
- name: 'argument'
code: Hello {{ $argument }} !
```
It would print "Hello world" if it's called in a [script](./collection-files.md#script) as following:
```yaml
script: Echo script
call:
function: EchoArgument
parameters:
argument: World
```
A function can call other functions such as:
```yaml
-
function: CallerFunction
parameters:
- name: 'value'
call:
function: EchoArgument
parameters:
argument: {{ $value }}
-
function: EchoArgument
parameters:
- name: 'argument'
code: Hello {{ $argument }} !
```
### with
- Skips the block if the variable is absent or empty.
- Binds its context (`.`) value of provided argument for the parameter if provided one.
- A block is defined as `{{ with $parameterName }} Parameter value is {{ . }} here {{ end }}`.
- The parameters used for `with` condition should be declared as optional, otherwise `with` block becomes redundant.
- Example:
```yaml
function: FunctionThatOutputsConditionally
parameters:
- name: 'argument'
optional: true
code: |-
{{ with $argument }}
Value is: {{ . }}
{{ end }}
```
### Pipes
- Pipes are set of functions available for handling text in privacy.sexy.
- Allows stacking actions one after another also known as "chaining".
- Just like [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)), the concept is simple: each pipeline's output becomes the input of the following pipe.
- Pipes are provided and defined by the compiler and consumed by collection files.
- Pipes can be combined with [parameter substitution](#parameter-substitution) and [with](#with).
- ❗ Pipe names must be camelCase without any space or special characters.
- **Existing pipes**
- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
- `escapeDoubleQuotes`: Escapes `"` characters to be used inside double quotes (`"`)
- **Example usages**
- `{{ with $code }} echo "{{ . | inlinePowerShell }}" {{ end }}`
- `{{ with $code }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}`

View File

@@ -1,16 +1,24 @@
# Unit tests # Tests
- Unit tests are defined in [`./tests`](./../tests) - There are two different types of tests executed:
1. [Unit tests](#unit-tests)
2. [Integration tests](#integration-tests)
- 💡 You can use path/module alias `@/tests` in import statements.
## Unit tests
- Tests each component in isolation
- Defined in [`./tests/unit`](./../tests/unit)
- They follow same folder structure as [`./src`](./../src) - They follow same folder structure as [`./src`](./../src)
## Naming ### Naming
- Each test suite first describe the system under test - Each test suite first describe the system under test
- E.g. tests for class `Application` is categorized under `Application` - E.g. tests for class `Application` is categorized under `Application`
- Tests for specific methods are categorized under method name (if applicable) - Tests for specific methods are categorized under method name (if applicable)
- E.g. test for `run()` is categorized under `run` - E.g. test for `run()` is categorized under `run`
## Act, arrange, assert ### Act, arrange, assert
- Tests use act, arrange and assert (AAA) pattern when applicable - Tests use act, arrange and assert (AAA) pattern when applicable
- **Arrange** - **Arrange**
@@ -23,7 +31,13 @@
- Should elicit some sort of response - Should elicit some sort of response
- Starts with comment line `// assert` - Starts with comment line `// assert`
## Stubs ### Stubs
- Stubs are defined in [`./tests/stubs`](./../tests/unit/stubs) - Stubs are defined in [`./tests/stubs`](./../tests/unit/stubs)
- They implement dummy behavior to be functional - They implement dummy behavior to be functional
## Integration tests
- Tests functionality of a component in combination with others (not isolated)
- Ensure dependencies to third parties work as expected
- Defined in [`./tests/integration`](./../tests/integration)

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 483 KiB

After

Width:  |  Height:  |  Size: 579 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 98 KiB

31929
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.10.0", "version": "0.11.0",
"private": true, "private": true,
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆", "description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
"author": "undergroundwires", "author": "undergroundwires",
@@ -8,6 +8,7 @@
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit", "test:unit": "vue-cli-service test:unit",
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\"",
"lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency", "lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency",
"electron:build": "vue-cli-service electron:build", "electron:build": "vue-cli-service electron:build",
"electron:serve": "vue-cli-service electron:serve", "electron:serve": "vue-cli-service electron:serve",
@@ -21,50 +22,53 @@
}, },
"main": "background.js", "main": "background.js",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.34", "@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.2", "@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-regular-svg-icons": "^5.15.2", "@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.2", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^2.0.2", "@fortawesome/vue-fontawesome": "^2.0.6",
"@juggle/resize-observer": "^3.3.0", "@juggle/resize-observer": "^3.3.1",
"ace-builds": "^1.4.12", "ace-builds": "^1.4.13",
"core-js": "^3.9.1", "core-js": "^3.18.3",
"cross-fetch": "^3.1.4",
"electron-progressbar": "^2.0.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"inversify": "^5.0.5", "install": "^0.13.0",
"liquor-tree": "^0.2.70", "liquor-tree": "^0.2.70",
"v-tooltip": "2.1.2", "npm": "^8.1.1",
"vue": "^2.6.12", "v-tooltip": "2.1.3",
"vue": "^2.6.14",
"vue-class-component": "^7.2.6", "vue-class-component": "^7.2.6",
"vue-js-modal": "^2.0.0-rc.6", "vue-js-modal": "^2.0.1",
"vue-property-decorator": "^9.1.2" "vue-property-decorator": "^9.1.2"
}, },
"devDependencies": { "devDependencies": {
"@types/ace": "0.0.45", "@types/ace": "0.0.47",
"@types/chai": "^4.2.15", "@types/chai": "^4.2.22",
"@types/file-saver": "^2.0.1", "@types/file-saver": "^2.0.3",
"@types/mocha": "^8.2.1", "@types/mocha": "^9.0.0",
"@vue/cli-plugin-babel": "^4.5.11", "@vue/cli-plugin-babel": "^4.5.14",
"@vue/cli-plugin-typescript": "^4.5.11", "@vue/cli-plugin-typescript": "^4.5.14",
"@vue/cli-plugin-unit-mocha": "^4.5.11", "@vue/cli-plugin-unit-mocha": "^4.5.14",
"@vue/cli-service": "^4.5.11", "@vue/cli-service": "^4.5.14",
"@vue/test-utils": "1.1.3", "@vue/test-utils": "1.2.2",
"chai": "^4.3.3", "chai": "^4.3.4",
"electron": "^12.0.1", "electron": "^15.3.0",
"electron-devtools-installer": "^3.1.1", "electron-devtools-installer": "^3.2.0",
"electron-log": "^4.3.2", "electron-log": "^4.4.1",
"electron-updater": "^4.3.8", "electron-updater": "^4.3.9",
"js-yaml-loader": "^1.2.2", "js-yaml-loader": "^1.2.2",
"markdownlint-cli": "^0.27.1", "markdownlint-cli": "^0.29.0",
"remark-cli": "^9.0.0", "remark-cli": "^10.0.0",
"remark-lint-no-dead-urls": "^1.1.0", "remark-lint-no-dead-urls": "^1.1.0",
"remark-preset-lint-consistent": "^4.0.0", "remark-preset-lint-consistent": "^5.1.0",
"remark-validate-links": "^10.0.3", "remark-validate-links": "^11.0.1",
"sass": "^1.32.8", "sass": "^1.43.3",
"sass-loader": "^10.1.1", "sass-loader": "10.2.0",
"tslib": "^2.1.0", "tslib": "^2.3.1",
"typescript": "^4.2.3", "typescript": "^4.4.4",
"vue-cli-plugin-electron-builder": "^2.0.0-rc.6", "vue-cli-plugin-electron-builder": "^2.1.1",
"vue-template-compiler": "^2.6.12", "vue-template-compiler": "^2.6.14",
"yaml-lint": "^1.2.4" "yaml-lint": "^1.2.4"
}, },
"homepage": "https://privacy.sexy", "homepage": "https://privacy.sexy",

View File

@@ -15,7 +15,7 @@ export class ApplicationFactory implements IApplicationFactory {
} }
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter())); this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
} }
public getAppAsync(): Promise<IApplication> { public getApp(): Promise<IApplication> {
return this.getter.getValueAsync(); return this.getter.getValue();
} }
} }

View File

@@ -0,0 +1,21 @@
// Compares to Array<T> objects for equality, ignoring order
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
if (!array1) { throw new Error('undefined first array'); }
if (!array2) { throw new Error('undefined second array'); }
const sortedArray1 = sort(array1);
const sortedArray2 = sort(array2);
return sequenceEqual(sortedArray1, sortedArray2);
function sort(array: readonly T[]) {
return array.slice().sort();
}
}
// Compares to Array<T> objects for equality in same order
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
if (!array1) { throw new Error('undefined first array'); }
if (!array2) { throw new Error('undefined second array'); }
if (array1.length !== array2.length) {
return false;
}
return array1.every((val, index) => val === array2[index]);
}

View File

@@ -1,6 +1,6 @@
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611 // Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
type EnumType = number | string; export type EnumType = number | string;
type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue }; export type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue };
export interface IEnumParser<TEnum> { export interface IEnumParser<TEnum> {
parseEnum(value: string, propertyName: string): TEnum; parseEnum(value: string, propertyName: string): TEnum;
@@ -41,3 +41,14 @@ export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
return getEnumNames(enumVariable) return getEnumNames(enumVariable)
.map((level) => enumVariable[level]) as TEnumValue[]; .map((level) => enumVariable[level]) as TEnumValue[];
} }
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
value: TEnumValue,
enumVariable: EnumVariable<T, TEnumValue>) {
if (value === undefined) {
throw new Error('undefined enum value');
}
if (!(value in enumVariable)) {
throw new RangeError(`enum value "${value}" is out of range`);
}
}

View File

@@ -0,0 +1,5 @@
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
export interface IScriptingLanguageFactory<T> {
create(language: ScriptingLanguage): T;
}

View File

@@ -0,0 +1,31 @@
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
import { assertInRange } from '@/application/Common/Enum';
type Getter<T> = () => T;
export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageFactory<T> {
private readonly getters = new Map<ScriptingLanguage, Getter<T>>();
public create(language: ScriptingLanguage): T {
assertInRange(language, ScriptingLanguage);
if (!this.getters.has(language)) {
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
}
const getter = this.getters.get(language);
const instance = getter();
return instance;
}
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
assertInRange(language, ScriptingLanguage);
if (!getter) {
throw new Error('undefined getter');
}
if (this.getters.has(language)) {
throw new Error(`${ScriptingLanguage[language]} is already registered`);
}
this.getters.set(language, getter);
}
}

View File

@@ -5,6 +5,7 @@ import { IApplication } from '@/domain/IApplication';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { EventSource } from '@/infrastructure/Events/EventSource'; import { EventSource } from '@/infrastructure/Events/EventSource';
import { assertInRange } from '@/application/Common/Enum';
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>; type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
@@ -22,7 +23,7 @@ export class ApplicationContext implements IApplicationContext {
public readonly app: IApplication, public readonly app: IApplication,
initialContext: OperatingSystem) { initialContext: OperatingSystem) {
validateApp(app); validateApp(app);
validateOs(initialContext); assertInRange(initialContext, OperatingSystem);
this.states = initializeStates(app); this.states = initializeStates(app);
this.changeContext(initialContext); this.changeContext(initialContext);
} }
@@ -50,18 +51,6 @@ function validateApp(app: IApplication) {
} }
} }
function validateOs(os: OperatingSystem) {
if (os === undefined) {
throw new Error('undefined os');
}
if (os === OperatingSystem.Unknown) {
throw new Error('unknown os');
}
if (!(os in OperatingSystem)) {
throw new Error(`os "${os}" is out of range`);
}
}
function initializeStates(app: IApplication): StateMachine { function initializeStates(app: IApplication): StateMachine {
const machine = new Map<OperatingSystem, ICategoryCollectionState>(); const machine = new Map<OperatingSystem, ICategoryCollectionState>();
for (const collection of app.collections) { for (const collection of app.collections) {

View File

@@ -7,12 +7,12 @@ import { IEnvironment } from '../Environment/IEnvironment';
import { IApplicationFactory } from '../IApplicationFactory'; import { IApplicationFactory } from '../IApplicationFactory';
import { ApplicationFactory } from '../ApplicationFactory'; import { ApplicationFactory } from '../ApplicationFactory';
export async function buildContextAsync( export async function buildContext(
factory: IApplicationFactory = ApplicationFactory.Current, factory: IApplicationFactory = ApplicationFactory.Current,
environment = Environment.CurrentEnvironment): Promise<IApplicationContext> { environment = Environment.CurrentEnvironment): Promise<IApplicationContext> {
if (!factory) { throw new Error('undefined factory'); } if (!factory) { throw new Error('undefined factory'); }
if (!environment) { throw new Error('undefined environment'); } if (!environment) { throw new Error('undefined environment'); }
const app = await factory.getAppAsync(); const app = await factory.getApp();
const os = getInitialOs(app, environment); const os = getInitialOs(app, environment);
return new ApplicationContext(app, os); return new ApplicationContext(app, os);
} }

View File

@@ -1,15 +1,14 @@
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ICodeBuilder } from './ICodeBuilder'; import { ICodeBuilder } from './ICodeBuilder';
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
import { BatchBuilder } from './Languages/BatchBuilder'; import { BatchBuilder } from './Languages/BatchBuilder';
import { ShellBuilder } from './Languages/ShellBuilder'; import { ShellBuilder } from './Languages/ShellBuilder';
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
export class CodeBuilderFactory implements ICodeBuilderFactory { export class CodeBuilderFactory extends ScriptingLanguageFactory<ICodeBuilder> implements ICodeBuilderFactory {
public create(language: ScriptingLanguage): ICodeBuilder { constructor() {
switch (language) { super();
case ScriptingLanguage.shellscript: return new ShellBuilder(); this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());
case ScriptingLanguage.batchfile: return new BatchBuilder(); this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder());
default: throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
}
} }
} }

View File

@@ -1,6 +1,5 @@
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ICodeBuilder } from './ICodeBuilder'; import { ICodeBuilder } from './ICodeBuilder';
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
export interface ICodeBuilderFactory { export interface ICodeBuilderFactory extends IScriptingLanguageFactory<ICodeBuilder> {
create(language: ScriptingLanguage): ICodeBuilder;
} }

View File

@@ -6,7 +6,6 @@ import { IEventSource } from '@/infrastructure/Events/IEventSource';
export interface IUserSelection { export interface IUserSelection {
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>; readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
readonly selectedScripts: ReadonlyArray<SelectedScript>; readonly selectedScripts: ReadonlyArray<SelectedScript>;
readonly totalSelected: number;
areAllSelected(category: ICategory): boolean; areAllSelected(category: ICategory): boolean;
isAnySelected(category: ICategory): boolean; isAnySelected(category: ICategory): boolean;
removeAllInCategory(categoryId: number): void; removeAllInCategory(categoryId: number): void;

View File

@@ -101,10 +101,6 @@ export class UserSelection implements IUserSelection {
return this.scripts.getItems(); return this.scripts.getItems();
} }
public get totalSelected(): number {
return this.scripts.getItems().length;
}
public selectAll(): void { public selectAll(): void {
for (const script of this.collection.getAllScripts()) { for (const script of this.collection.getAllScripts()) {
if (!this.scripts.exists(script.id)) { if (!this.scripts.exists(script.id)) {

View File

@@ -4,17 +4,17 @@ import { IBrowserOsDetector } from './IBrowserOsDetector';
export class BrowserOsDetector implements IBrowserOsDetector { export class BrowserOsDetector implements IBrowserOsDetector {
private readonly detectors = BrowserDetectors; private readonly detectors = BrowserDetectors;
public detect(userAgent: string): OperatingSystem { public detect(userAgent: string): OperatingSystem | undefined {
if (!userAgent) { if (!userAgent) {
return OperatingSystem.Unknown; return undefined;
} }
for (const detector of this.detectors) { for (const detector of this.detectors) {
const os = detector.detect(userAgent); const os = detector.detect(userAgent);
if (os !== OperatingSystem.Unknown) { if (os !== undefined) {
return os; return os;
} }
} }
return OperatingSystem.Unknown; return undefined;
} }
} }

View File

@@ -29,10 +29,10 @@ export class DetectorBuilder {
throw new Error('User agent is null or undefined'); throw new Error('User agent is null or undefined');
} }
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) { if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
return OperatingSystem.Unknown; return undefined;
} }
if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) { if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) {
return OperatingSystem.Unknown; return undefined;
} }
return this.os; return this.os;
} }

View File

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

View File

@@ -44,7 +44,7 @@ function getProcessPlatform(variables: IEnvironmentVariables): string {
return variables.process.platform; return variables.process.platform;
} }
function getDesktopOsType(processPlatform: string): OperatingSystem { function getDesktopOsType(processPlatform: string): OperatingSystem | undefined {
// https://nodejs.org/api/process.html#process_process_platform // https://nodejs.org/api/process.html#process_process_platform
if (processPlatform === 'darwin') { if (processPlatform === 'darwin') {
return OperatingSystem.macOS; return OperatingSystem.macOS;
@@ -53,7 +53,7 @@ function getDesktopOsType(processPlatform: string): OperatingSystem {
} else if (processPlatform === 'linux') { } else if (processPlatform === 'linux') {
return OperatingSystem.Linux; return OperatingSystem.Linux;
} }
return OperatingSystem.Unknown; return undefined;
} }
function isDesktop(variables: IEnvironmentVariables): boolean { function isDesktop(variables: IEnvironmentVariables): boolean {

View File

@@ -1,5 +1,5 @@
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
export interface IApplicationFactory { export interface IApplicationFactory {
getAppAsync(): Promise<IApplication>; getApp(): Promise<IApplication>;
} }

View File

@@ -1,6 +1,6 @@
import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ILanguageSyntax } from '@/domain/ScriptCode'; import { ILanguageSyntax } from '@/domain/ScriptCode';
import { FunctionData } from 'js-yaml-loader!*'; import { FunctionData } from 'js-yaml-loader!@/*';
import { IScriptCompiler } from './Compiler/IScriptCompiler'; import { IScriptCompiler } from './Compiler/IScriptCompiler';
import { ScriptCompiler } from './Compiler/ScriptCompiler'; import { ScriptCompiler } from './Compiler/ScriptCompiler';
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext'; import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';

View File

@@ -1,12 +1,18 @@
import { ExpressionPosition } from './ExpressionPosition'; import { ExpressionPosition } from './ExpressionPosition';
import { ExpressionArguments, IExpression } from './IExpression'; import { IExpression } from './IExpression';
import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
import { ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
export type ExpressionEvaluator = (args?: ExpressionArguments) => string; export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
export class Expression implements IExpression { export class Expression implements IExpression {
constructor( constructor(
public readonly position: ExpressionPosition, public readonly position: ExpressionPosition,
public readonly evaluator: ExpressionEvaluator, public readonly evaluator: ExpressionEvaluator,
public readonly parameters: readonly string[] = new Array<string>()) { public readonly parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection()) {
if (!position) { if (!position) {
throw new Error('undefined position'); throw new Error('undefined position');
} }
@@ -14,22 +20,43 @@ export class Expression implements IExpression {
throw new Error('undefined evaluator'); throw new Error('undefined evaluator');
} }
} }
public evaluate(args?: ExpressionArguments): string { public evaluate(context: IExpressionEvaluationContext): string {
args = filterUnusedArguments(this.parameters, args); if (!context) {
return this.evaluator(args); throw new Error('undefined context');
}
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
const args = filterUnusedArguments(this.parameters, context.args);
context = new ExpressionEvaluationContext(args, context.pipelineCompiler);
return this.evaluator(context);
}
}
function validateThatAllRequiredParametersAreSatisfied(
parameters: IReadOnlyFunctionParameterCollection,
args: IReadOnlyFunctionCallArgumentCollection,
) {
const requiredParameterNames = parameters
.all
.filter((parameter) => !parameter.isOptional)
.map((parameter) => parameter.name);
const missingParameterNames = requiredParameterNames
.filter((parameterName) => !args.hasArgument(parameterName));
if (missingParameterNames.length) {
throw new Error(
`argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`);
} }
} }
function filterUnusedArguments( function filterUnusedArguments(
parameters: readonly string[], args: ExpressionArguments): ExpressionArguments { parameters: IReadOnlyFunctionParameterCollection,
let result: ExpressionArguments = {}; allFunctionArgs: IReadOnlyFunctionCallArgumentCollection): IReadOnlyFunctionCallArgumentCollection {
for (const parameter of Object.keys(args)) { const specificCallArgs = new FunctionCallArgumentCollection();
if (parameters.includes(parameter)) { for (const parameter of parameters.all) {
result = { if (parameter.isOptional && !allFunctionArgs.hasArgument(parameter.name)) {
...result, continue; // Optional parameter is not necessarily provided
[parameter]: args[parameter],
};
} }
const arg = allFunctionArgs.getArgument(parameter.name);
specificCallArgs.addArgument(arg);
} }
return result; return specificCallArgs;
} }

View File

@@ -0,0 +1,18 @@
import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
import { IPipelineCompiler } from '../Pipes/IPipelineCompiler';
import { PipelineCompiler } from '../Pipes/PipelineCompiler';
export interface IExpressionEvaluationContext {
readonly args: IReadOnlyFunctionCallArgumentCollection;
readonly pipelineCompiler: IPipelineCompiler;
}
export class ExpressionEvaluationContext implements IExpressionEvaluationContext {
constructor(
public readonly args: IReadOnlyFunctionCallArgumentCollection,
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler()) {
if (!args) {
throw new Error('undefined args, send empty collection instead');
}
}
}

View File

@@ -1,12 +1,9 @@
import { ExpressionPosition } from './ExpressionPosition'; import { ExpressionPosition } from './ExpressionPosition';
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
export interface IExpression { export interface IExpression {
readonly position: ExpressionPosition; readonly position: ExpressionPosition;
readonly parameters?: readonly string[]; readonly parameters: IReadOnlyFunctionParameterCollection;
evaluate(args?: ExpressionArguments): string; evaluate(context: IExpressionEvaluationContext): string;
} }
export interface ExpressionArguments {
readonly [parameter: string]: string;
}

View File

@@ -1,30 +1,42 @@
import { IExpressionsCompiler, ParameterValueDictionary } from './IExpressionsCompiler'; import { IExpressionsCompiler } from './IExpressionsCompiler';
import { IExpression } from './Expression/IExpression'; import { IExpression } from './Expression/IExpression';
import { IExpressionParser } from './Parser/IExpressionParser'; import { IExpressionParser } from './Parser/IExpressionParser';
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser'; import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
import { ExpressionEvaluationContext } from './Expression/ExpressionEvaluationContext';
import { IExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
export class ExpressionsCompiler implements IExpressionsCompiler { export class ExpressionsCompiler implements IExpressionsCompiler {
public constructor(private readonly extractor: IExpressionParser = new CompositeExpressionParser()) { } public constructor(
public compileExpressions(code: string, parameters?: ParameterValueDictionary): string { private readonly extractor: IExpressionParser = new CompositeExpressionParser()) { }
public compileExpressions(
code: string,
args: IReadOnlyFunctionCallArgumentCollection): string {
if (!args) {
throw new Error('undefined args, send empty collection instead');
}
const expressions = this.extractor.findExpressions(code); const expressions = this.extractor.findExpressions(code);
const requiredParameterNames = expressions.map((e) => e.parameters).filter((p) => p).flat(); ensureParamsUsedInCodeHasArgsProvided(expressions, args);
const uniqueParameterNames = Array.from(new Set(requiredParameterNames)); const context = new ExpressionEvaluationContext(args);
ensureRequiredArgsProvided(uniqueParameterNames, parameters); const compiledCode = compileExpressions(expressions, code, context);
return compileExpressions(expressions, code, parameters); return compiledCode;
} }
} }
function compileExpressions(expressions: IExpression[], code: string, parameters?: ParameterValueDictionary) { function compileExpressions(
expressions: readonly IExpression[],
code: string,
context: IExpressionEvaluationContext) {
let compiledCode = ''; let compiledCode = '';
expressions = expressions const sortedExpressions = expressions
.slice() // copy the array to not mutate the parameter .slice() // copy the array to not mutate the parameter
.sort((a, b) => b.position.start - a.position.start); .sort((a, b) => b.position.start - a.position.start);
let index = 0; let index = 0;
while (index !== code.length) { while (index !== code.length) {
const nextExpression = expressions.pop(); const nextExpression = sortedExpressions.pop();
if (nextExpression) { if (nextExpression) {
compiledCode += code.substring(index, nextExpression.position.start); compiledCode += code.substring(index, nextExpression.position.start);
const expressionCode = nextExpression.evaluate(parameters); const expressionCode = nextExpression.evaluate(context);
compiledCode += expressionCode; compiledCode += expressionCode;
index = nextExpression.position.end; index = nextExpression.position.end;
} else { } else {
@@ -35,15 +47,29 @@ function compileExpressions(expressions: IExpression[], code: string, parameters
return compiledCode; return compiledCode;
} }
function ensureRequiredArgsProvided(parameters: readonly string[], args: ParameterValueDictionary) { function extractRequiredParameterNames(
parameters = parameters || []; expressions: readonly IExpression[]): string[] {
args = args || {}; const usedParameterNames = expressions
if (!parameters.length) { .map((e) => e.parameters.all
.filter((p) => !p.isOptional)
.map((p) => p.name))
.filter((p) => p)
.flat();
const uniqueParameterNames = Array.from(new Set(usedParameterNames));
return uniqueParameterNames;
}
function ensureParamsUsedInCodeHasArgsProvided(
expressions: readonly IExpression[],
providedArgs: IReadOnlyFunctionCallArgumentCollection): void {
const usedParameterNames = extractRequiredParameterNames(expressions);
if (!usedParameterNames?.length) {
return; return;
} }
const notProvidedParameters = parameters.filter((parameter) => !Boolean(args[parameter])); const notProvidedParameters = usedParameterNames
.filter((parameterName) => !providedArgs.hasArgument(parameterName));
if (notProvidedParameters.length) { if (notProvidedParameters.length) {
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)}`); throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)} but used in code`);
} }
} }

View File

@@ -1,5 +1,7 @@
export interface ParameterValueDictionary { [parameterName: string]: string; } import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
export interface IExpressionsCompiler { export interface IExpressionsCompiler {
compileExpressions(code: string, parameters?: ParameterValueDictionary): string; compileExpressions(
code: string,
args: IReadOnlyFunctionCallArgumentCollection): string;
} }

View File

@@ -1,14 +1,18 @@
import { IExpression } from '../Expression/IExpression'; import { IExpression } from '../Expression/IExpression';
import { IExpressionParser } from './IExpressionParser'; import { IExpressionParser } from './IExpressionParser';
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser'; import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
import { WithParser } from '../SyntaxParsers/WithParser';
const parsers = [ const Parsers = [
new ParameterSubstitutionParser(), new ParameterSubstitutionParser(),
new WithParser(),
]; ];
export class CompositeExpressionParser implements IExpressionParser { export class CompositeExpressionParser implements IExpressionParser {
public constructor(private readonly leafs: readonly IExpressionParser[] = parsers) { public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
if (leafs.some((leaf) => !leaf)) { throw new Error('undefined leaf'); } if (leafs.some((leaf) => !leaf)) {
throw new Error('undefined leaf');
}
} }
public findExpressions(code: string): IExpression[] { public findExpressions(code: string): IExpression[] {
const expressions = new Array<IExpression>(); const expressions = new Array<IExpression>();

View File

@@ -0,0 +1,59 @@
export class ExpressionRegexBuilder {
private readonly parts = new Array<string>();
public expectCharacters(characters: string) {
return this.addRawRegex(
characters
.replaceAll('$', '\\$')
.replaceAll('.', '\\.'),
);
}
public expectOneOrMoreWhitespaces() {
return this
.addRawRegex('\\s+');
}
public matchPipeline() {
return this
.expectZeroOrMoreWhitespaces()
.addRawRegex('(\\|\\s*.+?)?');
}
public matchUntilFirstWhitespace() {
return this
.addRawRegex('([^|\\s]+)');
}
public matchAnythingExceptSurroundingWhitespaces() {
return this
.expectZeroOrMoreWhitespaces()
.addRawRegex('(.+?)')
.expectZeroOrMoreWhitespaces();
}
public expectExpressionStart() {
return this
.expectCharacters('{{')
.expectZeroOrMoreWhitespaces();
}
public expectExpressionEnd() {
return this
.expectZeroOrMoreWhitespaces()
.expectCharacters('}}');
}
public buildRegExp(): RegExp {
return new RegExp(this.parts.join(''), 'g');
}
private expectZeroOrMoreWhitespaces() {
return this
.addRawRegex('\\s*');
}
private addRawRegex(regex: string) {
this.parts.push(regex);
return this;
}
}

View File

@@ -1,10 +1,13 @@
import { IExpressionParser } from './IExpressionParser'; import { IExpressionParser } from '../IExpressionParser';
import { ExpressionPosition } from '../Expression/ExpressionPosition'; import { ExpressionPosition } from '../../Expression/ExpressionPosition';
import { IExpression } from '../Expression/IExpression'; import { IExpression } from '../../Expression/IExpression';
import { Expression, ExpressionEvaluator } from '../Expression/Expression'; import { Expression, ExpressionEvaluator } from '../../Expression/Expression';
import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
export abstract class RegexParser implements IExpressionParser { export abstract class RegexParser implements IExpressionParser {
protected abstract readonly regex: RegExp; protected abstract readonly regex: RegExp;
public findExpressions(code: string): IExpression[] { public findExpressions(code: string): IExpression[] {
return Array.from(this.findRegexExpressions(code)); return Array.from(this.findRegexExpressions(code));
} }
@@ -23,7 +26,8 @@ export abstract class RegexParser implements IExpressionParser {
throw new Error(`[${this.constructor.name}] invalid script position: ${error.message}\nRegex ${this.regex}\nCode: ${code}`); throw new Error(`[${this.constructor.name}] invalid script position: ${error.message}\nRegex ${this.regex}\nCode: ${code}`);
} }
const primitiveExpression = this.buildExpression(match); const primitiveExpression = this.buildExpression(match);
const expression = new Expression(position, primitiveExpression.evaluator, primitiveExpression.parameters); const parameters = getParameters(primitiveExpression);
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
yield expression; yield expression;
} }
} }
@@ -31,5 +35,14 @@ export abstract class RegexParser implements IExpressionParser {
export interface IPrimitiveExpression { export interface IPrimitiveExpression {
evaluator: ExpressionEvaluator; evaluator: ExpressionEvaluator;
parameters?: readonly string[]; parameters?: readonly IFunctionParameter[];
}
function getParameters(
expression: IPrimitiveExpression): FunctionParameterCollection {
const parameters = new FunctionParameterCollection();
for (const parameter of expression.parameters || []) {
parameters.addParameter(parameter);
}
return parameters;
} }

View File

@@ -0,0 +1,4 @@
export interface IPipe {
readonly name: string;
apply(input: string): string;
}

View File

@@ -0,0 +1,3 @@
export interface IPipelineCompiler {
compile(value: string, pipeline: string): string;
}

View File

@@ -0,0 +1,27 @@
import { IPipe } from '../IPipe';
export class EscapeDoubleQuotes implements IPipe {
public readonly name: string = 'escapeDoubleQuotes';
public apply(raw: string): string {
return raw?.replaceAll('"', '"^""');
/*
"^"" is the most robust and stable choice.
Other options:
""
Breaks, because it is fundamentally unsupported
""""
Does not work with consecutive double quotes.
E.g. PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"
Works when using: PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"
\"
May break as they are interpreted by cmd.exe as metacharacters breaking the command
E.g. PowerShell -Command "Write-Host 'Hello \"w&orld\"'" does not work due to unescaped "&"
Works when using: PowerShell -Command "Write-Host 'Hello "^""w&orld"^""'"
\""
Normalizes interior whitespace
E.g. PowerShell -Command "\""a& c\"".length", outputs 4 and discards one of two whitespaces
Works when using "^"": PowerShell -Command ""^""a& c"^"".length"
A good explanation: https://stackoverflow.com/a/31413730
*/
}
}

View File

@@ -0,0 +1,155 @@
import { IPipe } from '../IPipe';
export class InlinePowerShell implements IPipe {
public readonly name: string = 'inlinePowerShell';
public apply(code: string): string {
if (!code || !hasLines(code)) {
return code;
}
code = inlineComments(code);
code = mergeLinesWithBacktick(code);
code = mergeHereStrings(code);
const lines = getLines(code)
.map((line) => line.trim())
.filter((line) => line.length > 0);
return lines
.join('; ');
}
}
function hasLines(text: string) {
return text.includes('\n') || text.includes('\r');
}
/*
Line comments using "#" are replaced with inline comment syntax <# comment.. #>
Otherwise single # comments out rest of the code
*/
function inlineComments(code: string): string {
const makeInlineComment = (comment: string) => {
const value = comment?.trim();
if (!value) {
return '<##>';
}
return `<# ${value} #>`;
};
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
if (captureComment === undefined) {
return match;
}
return makeInlineComment(captureComment);
});
/*
Other alternatives considered:
--------------------------
/#(?<!<#)(?![<>])(.*)$/gm
-------------------------
✅ Simple, yet matches and captures only what's necessary
❌ Fails to match some cases
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
❌ `Write-Host "hi" #>Comment starting like inline comment end but not one`
❌ Uses lookbehind
Safari does not yet support lookbehind and syntax, leading application to not
load and throw "Invalid regular expression: invalid group specifier name"
https://caniuse.com/js-regexp-lookbehind
⏩ Usage
return code.replaceAll(/#(?<!<#)(?![<>])(.*)$/gm, (match, captureComment) => {
return makeInlineComment(captureComment)
});
----------------
/<#.*?#>|#(.*)/g
----------------
✅ Simple yet affective
❌ Matches all comments, but only captures dash comments
❌ Fails to match some cases
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
⏩ Usage
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
if (captureComment === undefined) {
return match;
}
return makeInlineComment(captureComment);
});
------------------------------------
/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm
------------------------------------
✅ Covers all cases
❌ Matches every line, three capture groups are used to build result
⏩ Usage
return code.replaceAll(/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm,
(match, captureLeft, captureDash, captureComment) => {
if (!captureDash) {
return match;
}
return captureLeft + makeInlineComment(captureComment);
});
*/
}
function getLines(code: string): string [] {
return (code?.split(/\r\n|\r|\n/) || []);
}
/*
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules#here-strings
*/
function mergeHereStrings(code: string) {
const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g;
return code.replaceAll(regex, (_$, quotes, scope) => {
const newString = getHereStringHandler(quotes);
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
const lines = getLines(escaped);
const inlined = lines.join(newString.separator);
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
return quoted;
});
}
interface IInlinedHereString {
readonly quotesAround: string;
readonly escapedQuotes: string;
readonly separator: string;
}
// We handle @' and @" differently so single quotes are interpreted literally and doubles are expandable
function getHereStringHandler(quotes: string): IInlinedHereString {
const expandableNewLine = '`r`n';
switch (quotes) {
case '\'':
return {
quotesAround: '\'',
escapedQuotes: '\'\'',
separator: `\'+"${expandableNewLine}"+\'`,
};
case '"':
return {
quotesAround: '"',
escapedQuotes: '`"',
separator: expandableNewLine,
};
default:
throw new Error(`expected quotes: ${quotes}`);
}
}
/*
Input ->
Get-Service * `
Sort-Object StartType `
Format-Table Name, ServiceType, Status -AutoSize
Output ->
Get-Service * | Sort-Object StartType | Format-Table -AutoSize
*/
function mergeLinesWithBacktick(code: string) {
/*
The regex actually wraps any whitespace character after backtick and before newline
However, this is not always the case for PowerShell.
I see two behaviors:
1. If inside string, it's accepted (inside " or ')
2. If part of a command, PowerShell throws "An empty pipe element is not allowed"
However we don't need to be so robust and handle this complexity (yet), so for easier regex
we wrap it anyway
*/
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
}

View File

@@ -0,0 +1,47 @@
import { IPipe } from './IPipe';
import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
const RegisteredPipes = [
new EscapeDoubleQuotes(),
new InlinePowerShell(),
];
export interface IPipeFactory {
get(pipeName: string): IPipe;
}
export class PipeFactory implements IPipeFactory {
private readonly pipes = new Map<string, IPipe>();
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
if (pipes.some((pipe) => !pipe)) {
throw new Error('undefined pipe in list');
}
for (const pipe of pipes) {
this.registerPipe(pipe);
}
}
public get(pipeName: string): IPipe {
validatePipeName(pipeName);
if (!this.pipes.has(pipeName)) {
throw new Error(`Unknown pipe: "${pipeName}"`);
}
return this.pipes.get(pipeName);
}
private registerPipe(pipe: IPipe): void {
validatePipeName(pipe.name);
if (this.pipes.has(pipe.name)) {
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
}
this.pipes.set(pipe.name, pipe);
}
}
function validatePipeName(name: string) {
if (!name) {
throw new Error('empty pipe name');
}
if (!/^[a-z][A-Za-z]*$/.test(name)) {
throw new Error(`Pipe name should be camelCase: "${name}"`);
}
}

View File

@@ -0,0 +1,31 @@
import { IPipeFactory, PipeFactory } from './PipeFactory';
import { IPipelineCompiler } from './IPipelineCompiler';
export class PipelineCompiler implements IPipelineCompiler {
constructor(private readonly factory: IPipeFactory = new PipeFactory()) { }
public compile(value: string, pipeline: string): string {
ensureValidArguments(value, pipeline);
const pipeNames = extractPipeNames(pipeline);
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
for (const pipe of pipes) {
value = pipe.apply(value);
}
return value;
}
}
function extractPipeNames(pipeline: string): string[] {
return pipeline
.trim()
.split('|')
.slice(1)
.map((p) => p.trim());
}
function ensureValidArguments(value: string, pipeline: string) {
if (!value) { throw new Error('undefined value'); }
if (!pipeline) { throw new Error('undefined pipeline'); }
if (!pipeline.trimStart().startsWith('|')) {
throw new Error('pipeline does not start with pipe');
}
}

View File

@@ -1,12 +1,28 @@
import { RegexParser, IPrimitiveExpression } from '../Parser/RegexParser'; import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
export class ParameterSubstitutionParser extends RegexParser { export class ParameterSubstitutionParser extends RegexParser {
protected readonly regex = /{{\s*\$\s*([^}| ]+)\s*}}/g; protected readonly regex = new ExpressionRegexBuilder()
.expectExpressionStart()
.expectCharacters('$')
.matchUntilFirstWhitespace() // First match: Parameter name
.matchPipeline() // Second match: Pipeline
.expectExpressionEnd()
.buildRegExp();
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression { protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
const parameterName = match[1]; const parameterName = match[1];
const pipeline = match[2];
return { return {
parameters: [ parameterName ], parameters: [ new FunctionParameter(parameterName, false) ],
evaluator: (args) => args[parameterName], evaluator: (context) => {
const argumentValue = context.args.getArgument(parameterName).argumentValue;
if (!pipeline) {
return argumentValue;
}
return context.pipelineCompiler.compile(argumentValue, pipeline);
},
}; };
} }
} }

View File

@@ -0,0 +1,58 @@
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
export class WithParser extends RegexParser {
protected readonly regex = new ExpressionRegexBuilder()
// {{ with $parameterName }}
.expectExpressionStart()
.expectCharacters('with')
.expectOneOrMoreWhitespaces()
.expectCharacters('$')
.matchUntilFirstWhitespace() // First match: parameter name
.expectExpressionEnd()
// ...
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
// {{ end }}
.expectExpressionStart()
.expectCharacters('end')
.expectExpressionEnd()
.buildRegExp();
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
const parameterName = match[1];
const scopeText = match[2];
return {
parameters: [ new FunctionParameter(parameterName, true) ],
evaluator: (context) => {
const argumentValue = context.args.hasArgument(parameterName) ?
context.args.getArgument(parameterName).argumentValue
: undefined;
if (!argumentValue) {
return '';
}
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
if (!pipeline) {
return argumentValue;
}
return context.pipelineCompiler.compile(argumentValue, pipeline);
});
},
};
}
}
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
// {{ . | pipeName }}
.expectExpressionStart()
.expectCharacters('.')
.matchPipeline() // First match: pipeline
.expectExpressionEnd()
.buildRegExp();
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets, but let pipeline compiler fail on those
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1 ) => {
return replacer(match1);
});
}

View File

@@ -0,0 +1,13 @@
import { IFunctionCallArgument } from './IFunctionCallArgument';
import { ensureValidParameterName } from '../../Shared/ParameterNameValidator';
export class FunctionCallArgument implements IFunctionCallArgument {
constructor(
public readonly parameterName: string,
public readonly argumentValue: string) {
ensureValidParameterName(parameterName);
if (!argumentValue) {
throw new Error(`undefined argument value for "${parameterName}"`);
}
}
}

View File

@@ -0,0 +1,34 @@
import { IFunctionCallArgument } from './IFunctionCallArgument';
import { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection';
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
private readonly arguments = new Map<string, IFunctionCallArgument>();
public addArgument(argument: IFunctionCallArgument): void {
if (!argument) {
throw new Error('undefined argument');
}
if (this.hasArgument(argument.parameterName)) {
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
}
this.arguments.set(argument.parameterName, argument);
}
public getAllParameterNames(): string[] {
return Array.from(this.arguments.keys());
}
public hasArgument(parameterName: string): boolean {
if (!parameterName) {
throw new Error('undefined parameter name');
}
return this.arguments.has(parameterName);
}
public getArgument(parameterName: string): IFunctionCallArgument {
if (!parameterName) {
throw new Error('undefined parameter name');
}
const arg = this.arguments.get(parameterName);
if (!arg) {
throw new Error(`parameter does not exist: ${parameterName}`);
}
return arg;
}
}

View File

@@ -0,0 +1,4 @@
export interface IFunctionCallArgument {
readonly parameterName: string;
readonly argumentValue: string;
}

View File

@@ -0,0 +1,11 @@
import { IFunctionCallArgument } from './IFunctionCallArgument';
export interface IReadOnlyFunctionCallArgumentCollection {
getArgument(parameterName: string): IFunctionCallArgument;
getAllParameterNames(): string[];
hasArgument(parameterName: string): boolean;
}
export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection {
addArgument(argument: IFunctionCallArgument): void;
}

View File

@@ -0,0 +1,139 @@
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
import { ICompiledCode } from './ICompiledCode';
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler';
import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler';
import { ISharedFunction, IFunctionCode } from '../../ISharedFunction';
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCall } from '../FunctionCall';
import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection';
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
export class FunctionCallCompiler implements IFunctionCallCompiler {
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
protected constructor(
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler()) {
}
public compileCall(
calls: IFunctionCall[],
functions: ISharedFunctionCollection): ICompiledCode {
if (!functions) { throw new Error('undefined functions'); }
if (!calls) { throw new Error('undefined calls'); }
if (calls.some((f) => !f)) { throw new Error('undefined function call'); }
const context: ICompilationContext = {
allFunctions: functions,
callSequence: calls,
expressionsCompiler: this.expressionsCompiler,
};
const code = compileCallSequence(context);
return code;
}
}
interface ICompilationContext {
allFunctions: ISharedFunctionCollection;
callSequence: readonly IFunctionCall[];
expressionsCompiler: IExpressionsCompiler;
}
interface ICompiledFunctionCall {
readonly code: string;
readonly revertCode: string;
}
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
const compiledFunctions = new Array<ICompiledFunctionCall>();
for (const call of context.callSequence) {
const compiledCode = compileSingleCall(call, context);
compiledFunctions.push(...compiledCode);
}
return {
code: merge(compiledFunctions.map((f) => f.code)),
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
};
}
function compileSingleCall(call: IFunctionCall, context: ICompilationContext): ICompiledFunctionCall[] {
const func = context.allFunctions.getFunctionByName(call.functionName);
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
if (func.body.code) { // Function with inline code
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
return [ compiledCode ];
} else { // Function with inner calls
return func.body.calls
.map((innerCall) => {
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs);
return compileSingleCall(compiledCall, context);
})
.flat();
}
}
function compileCode(
code: IFunctionCode,
args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler): ICompiledFunctionCall {
return {
code: compiler.compileExpressions(code.do, args),
revertCode: compiler.compileExpressions(code.revert, args),
};
}
function compileArgs(
argsToCompile: IReadOnlyFunctionCallArgumentCollection,
args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler,
): IReadOnlyFunctionCallArgumentCollection {
const compiledArgs = new FunctionCallArgumentCollection();
for (const parameterName of argsToCompile.getAllParameterNames()) {
const argumentValue = argsToCompile.getArgument(parameterName).argumentValue;
const compiledValue = compiler.compileExpressions(argumentValue, args);
const newArgument = new FunctionCallArgument(parameterName, compiledValue);
compiledArgs.addArgument(newArgument);
}
return compiledArgs;
}
function merge(codeParts: readonly string[]): string {
return codeParts
.filter((part) => part?.length > 0)
.join('\n');
}
function ensureThatCallArgumentsExistInParameterDefinition(
func: ISharedFunction,
args: IReadOnlyFunctionCallArgumentCollection): void {
const callArgumentNames = args.getAllParameterNames();
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
throwIfNotEmpty(func.name, unexpectedParameters, functionParameterNames);
}
function findUnexpectedParameters(
callArgumentNames: string[],
functionParameterNames: string[]): string[] {
if (!callArgumentNames.length && !functionParameterNames.length) {
return [];
}
return callArgumentNames
.filter((callParam) => !functionParameterNames.includes(callParam));
}
function throwIfNotEmpty(
functionName: string,
unexpectedParameters: string[],
expectedParameters: string[]) {
if (!unexpectedParameters.length) {
return;
}
throw new Error(
`Function "${functionName}" has unexpected parameter(s) provided: ` +
`"${unexpectedParameters.join('", "')}"` +
'. Expected parameter(s): ' +
(expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
);
}

View File

@@ -0,0 +1,9 @@
import { ICompiledCode } from './ICompiledCode';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { IFunctionCall } from '../IFunctionCall';
export interface IFunctionCallCompiler {
compileCall(
calls: IFunctionCall[],
functions: ISharedFunctionCollection): ICompiledCode;
}

View File

@@ -0,0 +1,15 @@
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
import { IFunctionCall } from './IFunctionCall';
export class FunctionCall implements IFunctionCall {
constructor(
public readonly functionName: string,
public readonly args: IReadOnlyFunctionCallArgumentCollection) {
if (!functionName) {
throw new Error('empty function name in function call');
}
if (!args) {
throw new Error('undefined args');
}
}
}

View File

@@ -0,0 +1,35 @@
import { FunctionCallData, FunctionCallsData } from 'js-yaml-loader!@/*';
import { IFunctionCall } from './IFunctionCall';
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
import { FunctionCallArgument } from './Argument/FunctionCallArgument';
import { FunctionCall } from './FunctionCall';
export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] {
if (!calls) {
throw new Error('undefined call data');
}
const sequence = getCallSequence(calls);
return sequence.map((call) => parseFunctionCall(call));
}
function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
if (typeof calls !== 'object') {
throw new Error('called function(s) must be an object');
}
if (calls instanceof Array) {
return calls as FunctionCallData[];
}
return [ calls as FunctionCallData ];
}
function parseFunctionCall(call: FunctionCallData): IFunctionCall {
if (!call) {
throw new Error(`undefined function call`);
}
const args = new FunctionCallArgumentCollection();
for (const parameterName of Object.keys(call.parameters || {})) {
const arg = new FunctionCallArgument(parameterName, call.parameters[parameterName]);
args.addArgument(arg);
}
return new FunctionCall(call.function, args);
}

View File

@@ -0,0 +1,6 @@
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
export interface IFunctionCall {
readonly functionName: string;
readonly args: IReadOnlyFunctionCallArgumentCollection;
}

View File

@@ -1,6 +0,0 @@
import { FunctionData } from 'js-yaml-loader!*';
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
export interface IFunctionCompiler {
compileFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection;
}

View File

@@ -1,6 +1,24 @@
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
import { IFunctionCall } from '../Function/Call/IFunctionCall';
export interface ISharedFunction { export interface ISharedFunction {
readonly name: string; readonly name: string;
readonly parameters?: readonly string[]; readonly parameters: IReadOnlyFunctionParameterCollection;
readonly code: string; readonly body: ISharedFunctionBody;
readonly revertCode?: string; }
export interface ISharedFunctionBody {
readonly type: FunctionBodyType;
readonly code: IFunctionCode;
readonly calls: readonly IFunctionCall[];
}
export enum FunctionBodyType {
Code,
Calls,
}
export interface IFunctionCode {
readonly do: string;
readonly revert?: string;
} }

View File

@@ -0,0 +1,6 @@
import { FunctionData } from 'js-yaml-loader!@/*';
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
export interface ISharedFunctionsParser {
parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection;
}

View File

@@ -0,0 +1,10 @@
import { IFunctionParameter } from './IFunctionParameter';
import { ensureValidParameterName } from '../Shared/ParameterNameValidator';
export class FunctionParameter implements IFunctionParameter {
constructor(
public readonly name: string,
public readonly isOptional: boolean) {
ensureValidParameterName(name);
}
}

View File

@@ -0,0 +1,26 @@
import { IFunctionParameterCollection } from './IFunctionParameterCollection';
import { IFunctionParameter } from './IFunctionParameter';
export class FunctionParameterCollection implements IFunctionParameterCollection {
private parameters = new Array<IFunctionParameter>();
public get all(): readonly IFunctionParameter[] {
return this.parameters;
}
public addParameter(parameter: IFunctionParameter) {
this.ensureValidParameter(parameter);
this.parameters.push(parameter);
}
private includesName(name: string) {
return this.parameters.find((existingParameter) => existingParameter.name === name);
}
private ensureValidParameter(parameter: IFunctionParameter) {
if (!parameter) {
throw new Error('undefined parameter');
}
if (this.includesName(parameter.name)) {
throw new Error(`duplicate parameter name: "${parameter.name}"`);
}
}
}

View File

@@ -0,0 +1,4 @@
export interface IFunctionParameter {
readonly name: string;
readonly isOptional: boolean;
}

View File

@@ -0,0 +1,9 @@
import { IFunctionParameter } from './IFunctionParameter';
export interface IReadOnlyFunctionParameterCollection {
readonly all: readonly IFunctionParameter[];
}
export interface IFunctionParameterCollection extends IReadOnlyFunctionParameterCollection {
addParameter(parameter: IFunctionParameter): void;
}

View File

@@ -0,0 +1,8 @@
export function ensureValidParameterName(parameterName: string) {
if (!parameterName) {
throw new Error('undefined parameter name');
}
if (!parameterName.match(/^[0-9a-zA-Z]+$/)) {
throw new Error(`parameter name must be alphanumeric but it was "${parameterName}"`);
}
}

View File

@@ -1,14 +1,49 @@
import { ISharedFunction } from './ISharedFunction'; import { IFunctionCall } from '../Function/Call/IFunctionCall';
import { FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody } from './ISharedFunction';
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
export class SharedFunction implements ISharedFunction { export function createCallerFunction(
name: string,
parameters: IReadOnlyFunctionParameterCollection,
callSequence: readonly IFunctionCall[]): ISharedFunction {
if (!callSequence) {
throw new Error(`undefined call sequence in function "${name}"`);
}
if (!callSequence.length) {
throw new Error(`empty call sequence in function "${name}"`);
}
return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls);
}
export function createFunctionWithInlineCode(
name: string,
parameters: IReadOnlyFunctionParameterCollection,
code: string,
revertCode?: string): ISharedFunction {
if (!code) {
throw new Error(`undefined code in function "${name}"`);
}
const content: IFunctionCode = {
do: code,
revert: revertCode,
};
return new SharedFunction(name, parameters, content, FunctionBodyType.Code);
}
class SharedFunction implements ISharedFunction {
public readonly body: ISharedFunctionBody;
constructor( constructor(
public readonly name: string, public readonly name: string,
public readonly parameters: readonly string[], public readonly parameters: IReadOnlyFunctionParameterCollection,
public readonly code: string, content: IFunctionCode | readonly IFunctionCall[],
public readonly revertCode: string, bodyType: FunctionBodyType,
) { ) {
if (!name) { throw new Error('undefined function name'); } if (!name) { throw new Error('undefined function name'); }
if (!code) { throw new Error(`undefined function ("${name}") code`); } if (!parameters) { throw new Error(`undefined parameters`); }
this.parameters = parameters || []; this.body = {
type: bodyType,
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
calls: bodyType === FunctionBodyType.Calls ? content as readonly IFunctionCall[] : undefined,
};
} }
} }

View File

@@ -6,7 +6,7 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
public addFunction(func: ISharedFunction): void { public addFunction(func: ISharedFunction): void {
if (!func) { throw new Error('undefined function'); } if (!func) { throw new Error('undefined function'); }
if (this.functionsByName.has(func.name)) { if (this.has(func.name)) {
throw new Error(`function with name ${func.name} already exists`); throw new Error(`function with name ${func.name} already exists`);
} }
this.functionsByName.set(func.name, func); this.functionsByName.set(func.name, func);
@@ -20,4 +20,8 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
} }
return func; return func;
} }
private has(functionName: string) {
return this.functionsByName.has(functionName);
}
} }

View File

@@ -1,39 +1,59 @@
import { FunctionData, InstructionHolder } from 'js-yaml-loader!*'; import { FunctionData, InstructionHolder } from 'js-yaml-loader!@/*';
import { SharedFunction } from './SharedFunction'; import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
import { SharedFunctionCollection } from './SharedFunctionCollection'; import { SharedFunctionCollection } from './SharedFunctionCollection';
import { ISharedFunctionCollection } from './ISharedFunctionCollection'; import { ISharedFunctionCollection } from './ISharedFunctionCollection';
import { IFunctionCompiler } from './IFunctionCompiler'; import { ISharedFunctionsParser } from './ISharedFunctionsParser';
import { IFunctionCallCompiler } from '../FunctionCall/IFunctionCallCompiler'; import { FunctionParameter } from './Parameter/FunctionParameter';
import { FunctionCallCompiler } from '../FunctionCall/FunctionCallCompiler'; import { FunctionParameterCollection } from './Parameter/FunctionParameterCollection';
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
import { ISharedFunction } from './ISharedFunction';
import { parseFunctionCalls } from './Call/FunctionCallParser';
export class FunctionCompiler implements IFunctionCompiler { export class SharedFunctionsParser implements ISharedFunctionsParser {
public static readonly instance: IFunctionCompiler = new FunctionCompiler(); public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
protected constructor( public parseFunctions(
private readonly functionCallCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance) { functions: readonly FunctionData[]): ISharedFunctionCollection {
}
public compileFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection {
const collection = new SharedFunctionCollection(); const collection = new SharedFunctionCollection();
if (!functions || !functions.length) { if (!functions || !functions.length) {
return collection; return collection;
} }
ensureValidFunctions(functions); ensureValidFunctions(functions);
functions for (const func of functions) {
.filter((func) => hasCode(func)) const sharedFunction = parseFunction(func);
.forEach((func) => { collection.addFunction(sharedFunction);
const shared = new SharedFunction(func.name, func.parameters, func.code, func.revertCode); }
collection.addFunction(shared);
});
functions
.filter((func) => hasCall(func))
.forEach((func) => {
const code = this.functionCallCompiler.compileCall(func.call, collection);
const shared = new SharedFunction(func.name, func.parameters, code.code, code.revertCode);
collection.addFunction(shared);
});
return collection; return collection;
} }
} }
function parseFunction(data: FunctionData): ISharedFunction {
const name = data.name;
const parameters = parseParameters(data);
if (hasCode(data)) {
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
} else { // has call
const calls = parseFunctionCalls(data.call);
return createCallerFunction(name, parameters, calls);
}
}
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
const parameters = new FunctionParameterCollection();
if (!data.parameters) {
return parameters;
}
for (const parameterData of data.parameters) {
const isOptional = parameterData.optional || false;
try {
const parameter = new FunctionParameter(parameterData.name, isOptional);
parameters.addParameter(parameter);
} catch (err) {
throw new Error(`"${data.name}": ${err.message}`);
}
}
return parameters;
}
function hasCode(data: FunctionData): boolean { function hasCode(data: FunctionData): boolean {
return Boolean(data.code); return Boolean(data.code);
} }
@@ -42,14 +62,12 @@ function hasCall(data: FunctionData): boolean {
return Boolean(data.call); return Boolean(data.call);
} }
function ensureValidFunctions(functions: readonly FunctionData[]) { function ensureValidFunctions(functions: readonly FunctionData[]) {
ensureNoUndefinedItem(functions); ensureNoUndefinedItem(functions);
ensureNoDuplicatesInFunctionNames(functions); ensureNoDuplicatesInFunctionNames(functions);
ensureNoDuplicatesInParameterNames(functions);
ensureNoDuplicateCode(functions); ensureNoDuplicateCode(functions);
ensureEitherCallOrCodeIsDefined(functions); ensureEitherCallOrCodeIsDefined(functions);
ensureExpectedParameterNameTypes(functions); ensureExpectedParametersType(functions);
} }
function printList(list: readonly string[]): string { function printList(list: readonly string[]): string {
@@ -69,14 +87,18 @@ function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[])
} }
} }
function ensureExpectedParameterNameTypes(functions: readonly FunctionData[]) { function ensureExpectedParametersType(functions: readonly FunctionData[]) {
const unexpectedFunctions = functions.filter((func) => func.parameters && !isArrayOfStrings(func.parameters)); const unexpectedFunctions = functions
.filter((func) => func.parameters && !isArrayOfObjects(func.parameters));
if (unexpectedFunctions.length) { if (unexpectedFunctions.length) {
throw new Error(`unexpected parameter name type in ${printNames(unexpectedFunctions)}`); const errorMessage = `parameters must be an array of objects in function(s) ${printNames(unexpectedFunctions)}`;
throw new Error(errorMessage);
} }
function isArrayOfStrings(value: any): boolean { }
return Array.isArray(value) && value.every((item) => typeof item === 'string');
} function isArrayOfObjects(value: any): boolean {
return Array.isArray(value)
&& value.every((item) => typeof item === 'object');
} }
function printNames(holders: readonly InstructionHolder[]) { function printNames(holders: readonly InstructionHolder[]) {
@@ -90,21 +112,13 @@ function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`); throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
} }
} }
function ensureNoUndefinedItem(functions: readonly FunctionData[]) { function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
if (functions.some((func) => !func)) { if (functions.some((func) => !func)) {
throw new Error(`some functions are undefined`); throw new Error(`some functions are undefined`);
} }
} }
function ensureNoDuplicatesInParameterNames(functions: readonly FunctionData[]) {
const functionsWithParameters = functions
.filter((func) => func.parameters && func.parameters.length > 0);
for (const func of functionsWithParameters) {
const duplicateParameterNames = getDuplicates(func.parameters);
if (duplicateParameterNames.length) {
throw new Error(`"${func.name}": duplicate parameter name: ${printList(duplicateParameterNames)}`);
}
}
}
function ensureNoDuplicateCode(functions: readonly FunctionData[]) { function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
const duplicateCodes = getDuplicates(functions const duplicateCodes = getDuplicates(functions
.map((func) => func.code) .map((func) => func.code)

View File

@@ -1,89 +0,0 @@
import { FunctionCallData, FunctionCallParametersData, FunctionData, ScriptFunctionCallData } from 'js-yaml-loader!*';
import { ICompiledCode } from './ICompiledCode';
import { ISharedFunctionCollection } from '../Function/ISharedFunctionCollection';
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
import { IExpressionsCompiler } from '../Expressions/IExpressionsCompiler';
import { ExpressionsCompiler } from '../Expressions/ExpressionsCompiler';
export class FunctionCallCompiler implements IFunctionCallCompiler {
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
protected constructor(
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler()) { }
public compileCall(
call: ScriptFunctionCallData,
functions: ISharedFunctionCollection): ICompiledCode {
if (!functions) { throw new Error('undefined functions'); }
if (!call) { throw new Error('undefined call'); }
const compiledCodes = new Array<ICompiledCode>();
const calls = getCallSequence(call);
calls.forEach((currentCall, currentCallIndex) => {
ensureValidCall(currentCall);
const commonFunction = functions.getFunctionByName(currentCall.function);
ensureExpectedParameters(commonFunction, currentCall);
let functionCode = compileCode(commonFunction, currentCall.parameters, this.expressionsCompiler);
if (currentCallIndex !== calls.length - 1) {
functionCode = appendLine(functionCode);
}
compiledCodes.push(functionCode);
});
const compiledCode = merge(compiledCodes);
return compiledCode;
}
}
function ensureExpectedParameters(func: FunctionData, call: FunctionCallData) {
const actual = Object.keys(call.parameters || {});
const expected = func.parameters || [];
if (!actual.length && !expected.length) {
return;
}
const unexpectedParameters = actual.filter((callParam) => !expected.includes(callParam));
if (unexpectedParameters.length) {
throw new Error(
`function "${func.name}" has unexpected parameter(s) provided: "${unexpectedParameters.join('", "')}"`);
}
}
function merge(codes: readonly ICompiledCode[]): ICompiledCode {
return {
code: codes.map((code) => code.code).join(''),
revertCode: codes.map((code) => code.revertCode).join(''),
};
}
function compileCode(
func: FunctionData,
parameters: FunctionCallParametersData,
compiler: IExpressionsCompiler): ICompiledCode {
return {
code: compiler.compileExpressions(func.code, parameters),
revertCode: compiler.compileExpressions(func.revertCode, parameters),
};
}
function getCallSequence(call: ScriptFunctionCallData): FunctionCallData[] {
if (typeof call !== 'object') {
throw new Error('called function(s) must be an object');
}
if (call instanceof Array) {
return call as FunctionCallData[];
}
return [ call as FunctionCallData ];
}
function ensureValidCall(call: FunctionCallData) {
if (!call) {
throw new Error(`undefined function call`);
}
if (!call.function) {
throw new Error(`empty function name called`);
}
}
function appendLine(code: ICompiledCode): ICompiledCode {
const appendLineIfNotEmpty = (str: string) => str ? `${str}\n` : str;
return {
code: appendLineIfNotEmpty(code.code),
revertCode: appendLineIfNotEmpty(code.revertCode),
};
}

View File

@@ -1,9 +0,0 @@
import { ScriptFunctionCallData } from 'js-yaml-loader!*';
import { ICompiledCode } from './ICompiledCode';
import { ISharedFunctionCollection } from '../Function/ISharedFunctionCollection';
export interface IFunctionCallCompiler {
compileCall(
call: ScriptFunctionCallData,
functions: ISharedFunctionCollection): ICompiledCode;
}

View File

@@ -1,24 +1,25 @@
import { IScriptCode } from '@/domain/IScriptCode'; import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCode } from '@/domain/ScriptCode'; import { ScriptCode } from '@/domain/ScriptCode';
import { ILanguageSyntax } from '@/domain/ScriptCode';
import { FunctionData, ScriptData } from 'js-yaml-loader!@/*'; import { FunctionData, ScriptData } from 'js-yaml-loader!@/*';
import { IScriptCompiler } from './IScriptCompiler'; import { IScriptCompiler } from './IScriptCompiler';
import { ILanguageSyntax } from '@/domain/ScriptCode';
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection'; import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
import { IFunctionCallCompiler } from './FunctionCall/IFunctionCallCompiler'; import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler';
import { FunctionCallCompiler } from './FunctionCall/FunctionCallCompiler'; import { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompiler';
import { IFunctionCompiler } from './Function/IFunctionCompiler'; import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser';
import { FunctionCompiler } from './Function/FunctionCompiler'; import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
export class ScriptCompiler implements IScriptCompiler { export class ScriptCompiler implements IScriptCompiler {
private readonly functions: ISharedFunctionCollection; private readonly functions: ISharedFunctionCollection;
constructor( constructor(
functions: readonly FunctionData[] | undefined, functions: readonly FunctionData[] | undefined,
private readonly syntax: ILanguageSyntax, private readonly syntax: ILanguageSyntax,
functionCompiler: IFunctionCompiler = FunctionCompiler.instance, sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance, private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
) { ) {
if (!syntax) { throw new Error('undefined syntax'); } if (!syntax) { throw new Error('undefined syntax'); }
this.functions = functionCompiler.compileFunctions(functions); this.functions = sharedFunctionsParser.parseFunctions(functions);
} }
public canCompile(script: ScriptData): boolean { public canCompile(script: ScriptData): boolean {
if (!script) { throw new Error('undefined script'); } if (!script) { throw new Error('undefined script'); }
@@ -30,7 +31,8 @@ export class ScriptCompiler implements IScriptCompiler {
public compile(script: ScriptData): IScriptCode { public compile(script: ScriptData): IScriptCode {
if (!script) { throw new Error('undefined script'); } if (!script) { throw new Error('undefined script'); }
try { try {
const compiledCode = this.callCompiler.compileCall(script.call, this.functions); const calls = parseFunctionCalls(script.call);
const compiledCode = this.callCompiler.compileCall(calls, this.functions);
return new ScriptCode( return new ScriptCode(
compiledCode.code, compiledCode.code,
compiledCode.revertCode, compiledCode.revertCode,

View File

@@ -1,6 +1,10 @@
import { ILanguageSyntax } from '@/domain/ScriptCode'; import { ILanguageSyntax } from '@/domain/ScriptCode';
const BatchFileCommonCodeParts = [ '(', ')', 'else', '||' ];
const PowerShellCommonCodeParts = [ '{', '}' ];
export class BatchFileSyntax implements ILanguageSyntax { export class BatchFileSyntax implements ILanguageSyntax {
public readonly commentDelimiters = [ 'REM', '::' ]; public readonly commentDelimiters = [ 'REM', '::' ];
public readonly commonCodeParts = [ '(', ')', 'else' ]; public readonly commonCodeParts = [ ...BatchFileCommonCodeParts, ...PowerShellCommonCodeParts ];
} }

View File

@@ -1,6 +1,5 @@
import { ILanguageSyntax } from '@/domain/ScriptCode'; import { ILanguageSyntax } from '@/domain/ScriptCode';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
export interface ISyntaxFactory { export interface ISyntaxFactory extends IScriptingLanguageFactory<ILanguageSyntax> {
create(language: ScriptingLanguage): ILanguageSyntax;
} }

View File

@@ -2,5 +2,5 @@ import { ILanguageSyntax } from '@/domain/ScriptCode';
export class ShellScriptSyntax implements ILanguageSyntax { export class ShellScriptSyntax implements ILanguageSyntax {
public readonly commentDelimiters = [ '#' ]; public readonly commentDelimiters = [ '#' ];
public readonly commonCodeParts = [ '(', ')', 'else' ]; public readonly commonCodeParts = [ '(', ')', 'else', 'fi' ];
} }

View File

@@ -1,15 +1,14 @@
import { ILanguageSyntax } from '@/domain/ScriptCode'; import { ILanguageSyntax } from '@/domain/ScriptCode';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ISyntaxFactory } from './ISyntaxFactory'; import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
import { BatchFileSyntax } from './BatchFileSyntax'; import { BatchFileSyntax } from './BatchFileSyntax';
import { ShellScriptSyntax } from './ShellScriptSyntax'; import { ShellScriptSyntax } from './ShellScriptSyntax';
import { ISyntaxFactory } from './ISyntaxFactory';
export class SyntaxFactory implements ISyntaxFactory { export class SyntaxFactory extends ScriptingLanguageFactory<ILanguageSyntax> implements ISyntaxFactory {
public create(language: ScriptingLanguage): ILanguageSyntax { constructor() {
switch (language) { super();
case ScriptingLanguage.batchfile: return new BatchFileSyntax(); this.registerGetter(ScriptingLanguage.batchfile, () => new BatchFileSyntax());
case ScriptingLanguage.shellscript: return new ShellScriptSyntax(); this.registerGetter(ScriptingLanguage.shellscript, () => new ShellScriptSyntax());
default: throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
}
} }
} }

View File

@@ -1,9 +1,11 @@
import { IExpressionsCompiler, ParameterValueDictionary } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler'; import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { ParameterSubstitutionParser } from '@/application/Parser/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser'; import { ParameterSubstitutionParser } from '@/application/Parser/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser';
import { CompositeExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser'; import { CompositeExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser';
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler'; import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
import { IProjectInformation } from '@/domain/IProjectInformation'; import { IProjectInformation } from '@/domain/IProjectInformation';
import { ICodeSubstituter } from './ICodeSubstituter'; import { ICodeSubstituter } from './ICodeSubstituter';
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
export class CodeSubstituter implements ICodeSubstituter { export class CodeSubstituter implements ICodeSubstituter {
constructor( constructor(
@@ -15,12 +17,13 @@ export class CodeSubstituter implements ICodeSubstituter {
public substitute(code: string, info: IProjectInformation): string { public substitute(code: string, info: IProjectInformation): string {
if (!code) { throw new Error('undefined code'); } if (!code) { throw new Error('undefined code'); }
if (!info) { throw new Error('undefined info'); } if (!info) { throw new Error('undefined info'); }
const parameters: ParameterValueDictionary = { const args = new FunctionCallArgumentCollection();
homepage: info.homepage, const substitute = (name: string, value: string) =>
version: info.version, args.addArgument(new FunctionCallArgument(name, value));
date: this.date.toUTCString(), substitute('homepage', info.homepage);
}; substitute('version', info.version);
const compiledCode = this.compiler.compileExpressions(code, parameters); substitute('date', this.date.toUTCString());
const compiledCode = this.compiler.compileExpressions(code, args);
return compiledCode; return compiledCode;
} }
} }

View File

@@ -1,4 +1,4 @@
declare module 'js-yaml-loader!*' { declare module 'js-yaml-loader!@/*' {
export interface CollectionData { export interface CollectionData {
readonly os: string; readonly os: string;
readonly scripting: ScriptingDefinitionData; readonly scripting: ScriptingDefinitionData;
@@ -24,11 +24,16 @@ declare module 'js-yaml-loader!*' {
readonly code?: string; readonly code?: string;
readonly revertCode?: string; readonly revertCode?: string;
readonly call?: ScriptFunctionCallData; readonly call?: FunctionCallsData;
}
export interface ParameterDefinitionData {
readonly name: string;
readonly optional?: boolean;
} }
export interface FunctionData extends InstructionHolder { export interface FunctionData extends InstructionHolder {
readonly parameters?: readonly string[]; readonly parameters?: readonly ParameterDefinitionData[];
} }
export interface FunctionCallParametersData { export interface FunctionCallParametersData {
@@ -40,7 +45,7 @@ declare module 'js-yaml-loader!*' {
readonly parameters?: FunctionCallParametersData; readonly parameters?: FunctionCallParametersData;
} }
export type ScriptFunctionCallData = readonly FunctionCallData[] | FunctionCallData | undefined; export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined;
export interface ScriptData extends InstructionHolder, DocumentableData { export interface ScriptData extends InstructionHolder, DocumentableData {
readonly name: string; readonly name: string;

View File

@@ -1,4 +1,4 @@
# Structure documented in "docs/collections.md" # Structure documented in "docs/collection-files.md"
os: macos os: macos
scripting: scripting:
language: shellscript language: shellscript
@@ -55,15 +55,81 @@ actions:
sudo rm -rfv /System/Library/Caches/* &>/dev/null sudo rm -rfv /System/Library/Caches/* &>/dev/null
sudo rm -rfv ~/Library/Caches/* &>/dev/null sudo rm -rfv ~/Library/Caches/* &>/dev/null
- -
name: Clear system log files category: Clear OS logs
recommend: strict recommend: strict
code: |- children:
sudo rm -rfv /private/var/log/asl/*.asl &>/dev/null -
sudo rm -rfv /Library/Logs/DiagnosticReports/* &>/dev/null category: Clear unified logs (diagnostics)
sudo rm -rfv /Library/Logs/Adobe/* &>/dev/null docs: https://developer.apple.com/documentation/os/logging
rm -rfv ~/Library/Containers/com.apple.mail/Data/Library/Logs/Mail/* &>/dev/null children:
rm -rfv ~/Library/Logs/CoreSimulator/* &>/dev/null -
sudo rm -rfv /var/log/* name: Clear diagnostics logs
docs: https://eclecticlight.co/2017/10/10/inside-the-macos-log-logd-and-the-files-that-it-manages/
code: |-
sudo rm -rfv /private/var/db/diagnostics/*
sudo rm -rfv /var/db/diagnostics/*
-
name: Clear shared-cache strings data
docs:
- https://eclecticlight.co/2017/09/23/sierras-unified-log-evolves-more-persistent-and-a-valuable-log-log/
- https://github.com/privacysexy-forks/dtformats/blob/main/documentation/Apple%20Unified%20Logging%20and%20Activity%20Tracing%20formats.asciidoc
code: |-
sudo rm -rfv /private/var/db/uuidtext/
sudo rm -rfv /var/db/uuidtext/
-
category: Clear system logs (/var/log/)
children:
-
name: Clear Apple System Logs (ASL)
docs:
- https://papers.put.as/papers/macosx/2012/Mac_Log_Analysis_Sarah_Edwards_DFIRSummit2012.pdf
- https://apple.stackexchange.com/questions/98197/is-it-safe-to-delete-system-logs
code: |-
sudo rm -rfv /private/var/log/asl/*
sudo rm -rfv /var/log/asl/*
sudo rm -fv /var/log/asl.log # Legacy ASL (10.4)
sudo rm -fv /var/log/asl.db
-
name: Clear install logs
docs: https://discussions.apple.com/thread/1829842
code: sudo rm -fv /var/log/install.log
-
name: Clear all system logs
docs: https://www.howtogeek.com/356942/how-to-view-the-system-log-on-a-mac/
code: sudo rm -rfv /var/log/* # Clears including /var/log/system.log
-
name: Clear system application logs
docs: https://papers.put.as/papers/macosx/2012/Mac_Log_Analysis_Sarah_Edwards_DFIRSummit2012.pdf
code: sudo rm -rfv /Library/Logs/*
-
name: Clear Mail logs
code: rm -rfv ~/Library/Containers/com.apple.mail/Data/Library/Logs/Mail/*
-
name: Clear audit logs (login, logout, authentication and other user activity)
docs:
- https://papers.put.as/papers/macosx/2012/Mac_Log_Analysis_Sarah_Edwards_DFIRSummit2012.pdf
- http://macadmins.psu.edu/wp-content/uploads/sites/24696/2016/06/psumac2016-19-osxlogs_macadmins_2016.pdf
code: |-
sudo rm -rfv /var/audit/*
sudo rm -rfv /private/var/audit/*
-
name: Clear user logs (user reports)
docs:
- https://www.howtogeek.com/356942/how-to-view-the-system-log-on-a-mac/
- https://apple.stackexchange.com/questions/272929/is-it-safe-to-delete-the-content-of-library-logs
code: sudo rm -rfv ~/Library/Logs/*
-
name: Clear daily logs
docs: https://salt4n6.com/2018/12/11/mac-os-daily-logs/
code: sudo rm -fv /System/Library/LaunchDaemons/com.apple.periodic-*.plist
-
name: Clear receipt logs for installed packages/apps
docs:
- https://apple.stackexchange.com/questions/327174/whats-the-purpose-of-directory-private-var-db-receipts
- https://papers.put.as/papers/macosx/2012/Mac_Log_Analysis_Sarah_Edwards_DFIRSummit2012.pdf
code: |-
sudo rm -rfv /var/db/receipts/*
sudo rm -vf /Library/Receipts/InstallHistory.plist
- -
category: Clear browser history category: Clear browser history
children: children:
@@ -185,7 +251,7 @@ actions:
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/sessionstore-backups/upgrade.js*-20* rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/sessionstore-backups/upgrade.js*-20*
- -
name: Delete Firefox passwords name: Delete Firefox passwords
docs: http://kb.mozillazine.org/Password_Manager docs: https://web.archive.org/web/20210425202923/http://kb.mozillazine.org/Password_Manager
code: |- code: |-
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/signons.txt rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/signons.txt
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/signons2.txt rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/signons2.txt
@@ -392,7 +458,7 @@ actions:
- -
name: Disable Firefox telemetry name: Disable Firefox telemetry
recommend: standard recommend: standard
docs: https://github.com/mozilla/policy-templates/blob/master/README.md docs: https://github.com/privacysexy-forks/policy-templates/blob/master/README.md
code: |- code: |-
# Enable Firefox policies so the telemetry can be configured. # Enable Firefox policies so the telemetry can be configured.
sudo defaults write /Library/Preferences/org.mozilla.firefox EnterprisePoliciesEnabled -bool TRUE sudo defaults write /Library/Preferences/org.mozilla.firefox EnterprisePoliciesEnabled -bool TRUE
@@ -437,7 +503,7 @@ actions:
- -
name: Disable PowerShell Core telemetry name: Disable PowerShell Core telemetry
recommend: standard recommend: standard
docs: https://github.com/PowerShell/PowerShell/tree/release/v7.1.1#telemetry docs: https://github.com/privacysexy-forks/PowerShell/blob/v7.1.5/README.md#telemetry
call: call:
- -
function: PersistUserEnvironmentConfiguration function: PersistUserEnvironmentConfiguration
@@ -479,46 +545,6 @@ actions:
recommend: standard recommend: standard
code: defaults write NSGlobalDomain NSDocumentSaveNewDocumentsToCloud -bool false code: defaults write NSGlobalDomain NSDocumentSaveNewDocumentsToCloud -bool false
revertCode: defaults delete NSGlobalDomain NSDocumentSaveNewDocumentsToCloud revertCode: defaults delete NSGlobalDomain NSDocumentSaveNewDocumentsToCloud
-
category: Security improvements
children:
-
category: Configure macOS Application Firewall
children:
-
name: Enable firewall
recommend: standard
docs: https://www.stigviewer.com/stig/apple_os_x_10.13/2018-10-01/finding/V-81681
code: /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on
revertCode: /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate off
-
name: Turn on firewall logging
recommend: standard
docs: https://www.stigviewer.com/stig/apple_os_x_10.13/2018-10-01/finding/V-81671
code: /usr/libexec/ApplicationFirewall/socketfilterfw --setloggingmode on
revertCode: /usr/libexec/ApplicationFirewall/socketfilterfw --setloggingmode off
-
name: Turn on stealth mode
recommend: standard
docs: https://www.stigviewer.com/stig/apple_os_x_10.8_mountain_lion_workstation/2015-02-10/finding/V-51327
code: /usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode on
revertCode: /usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode off
-
name: Disable Spotlight indexing
code: sudo mdutil -i off -d /
revertCode: sudo mdutil -i on /
-
name: Disable Captive portal
docs:
- https://web.archive.org/web/20171008071031if_/http://blog.erratasec.com/2010/09/apples-secret-wispr-request.html#.WdnPa5OyL6Y
- https://web.archive.org/web/20130407200745/http://www.divertednetworks.net/apple-captiveportal.html
- https://web.archive.org/web/20170622064304/https://grpugh.wordpress.com/2014/10/29/an-undocumented-change-to-captive-network-assistant-settings-in-os-x-10-10-yosemite/
code: sudo defaults write /Library/Preferences/SystemConfiguration/com.apple.captive.control.plist Active -bool false
revertCode: sudo defaults delete /Library/Preferences/SystemConfiguration/com.apple.captive.control.plist Active
-
name: Require a password to wake the computer from sleep or screen saver
code: defaults write /Library/Preferences/com.apple.screensaver askForPassword -bool true
revertCode: sudo defaults delete /Library/Preferences/com.apple.screensaver askForPassword
- -
name: Do not show recent items on dock name: Do not show recent items on dock
docs: https://developer.apple.com/documentation/devicemanagement/dock docs: https://developer.apple.com/documentation/devicemanagement/dock
@@ -529,10 +555,532 @@ actions:
recommend: strict recommend: strict
code: defaults write com.apple.NetworkBrowser DisableAirDrop -bool true code: defaults write com.apple.NetworkBrowser DisableAirDrop -bool true
revertCode: defaults write com.apple.NetworkBrowser DisableAirDrop -bool false revertCode: defaults write com.apple.NetworkBrowser DisableAirDrop -bool false
-
category: Configure Siri
children:
-
name: Opt-out from Siri data collection
recommend: standard
code: defaults write com.apple.assistant.support 'Siri Data Sharing Opt-In Status' -int 2
revertCode: defaults delete com.apple.assistant.support 'Siri Data Sharing Opt-In Status'
-
category: Disable Siri
children:
-
name: Disable "Ask Siri"
recommend: strict
docs: https://derflounder.wordpress.com/2016/09/20/blocking-siri-on-macos-sierra/
code: defaults write com.apple.assistant.support 'Assistant Enabled' -bool false
revertCode: defaults write com.apple.assistant.support 'Assistant Enabled' -bool true
-
name: Disable Siri voice feedback
recommend: strict
docs:
- https://github.com/privacysexy-forks/starter/blob/master/system/siri.sh
- https://machippie.github.io/system/
code: defaults write com.apple.assistant.backedup 'Use device speaker for TTS' -int 3
revertCode: defaults write com.apple.assistant.backedup 'Use device speaker for TTS' -int 2
-
name: Disable Siri services (Siri and assistantd)
recommend: strict
docs:
- https://apple.stackexchange.com/questions/57514/what-is-assistantd
- https://www.jamf.com/jamf-nation/discussions/22757/kill-siri#responseChild137563
- https://apple.stackexchange.com/a/370426
# To see status: • `launchctl print-disabled system` • `launchctl print-disabled user/$UID` • `launchctl print-disabled gui/$UID`
code: |-
launchctl disable "user/$UID/com.apple.assistantd"
launchctl disable "gui/$UID/com.apple.assistantd"
sudo launchctl disable 'system/com.apple.assistantd'
launchctl disable "user/$UID/com.apple.Siri.agent"
launchctl disable "gui/$UID/com.apple.Siri.agent"
sudo launchctl disable 'system/com.apple.Siri.agent'
if [ $(/usr/bin/csrutil status | awk '/status/ {print $5}' | sed 's/\.$//') = "enabled" ]; then
>&2 echo 'This script requires SIP to be disabled. Read more: https://developer.apple.com/documentation/security/disabling_and_enabling_system_integrity_protection'
fi
revertCode: |-
launchctl enable "user/$UID/com.apple.assistantd"
launchctl enable "gui/$UID/com.apple.assistantd"
sudo launchctl enable 'system/com.apple.assistantd'
launchctl enable "user/$UID/com.apple.Siri.agent"
launchctl enable "gui/$UID/com.apple.Siri.agent"
sudo launchctl enable 'system/com.apple.Siri.agent'
if [ $(/usr/bin/csrutil status | awk '/status/ {print $5}' | sed 's/\.$//') = "enabled" ]; then
>&2 echo 'This script requires SIP to be disabled. Read more: https://developer.apple.com/documentation/security/disabling_and_enabling_system_integrity_protection''
fi
-
name: Disable "Do you want to enable Siri?" pop-up
docs:
- https://discussions.apple.com/thread/7694127?answerId=30752577022#30752577022
- https://windowsreport.com/mac/siri-keeps-popping-up/
- https://www.jamf.com/jamf-nation/discussions/21783/disable-siri-setup-assistant-in-macos-sierra#responseChild131588
code: defaults write com.apple.SetupAssistant 'DidSeeSiriSetup' -bool True
revertCode: defaults delete com.apple.SetupAssistant 'DidSeeSiriSetup'
-
category: Hide Siri
children:
-
name: Hide Siri from menu bar
recommend: strict
code: defaults write com.apple.systemuiserver 'NSStatusItem Visible Siri' 0
revertCode: defaults write com.apple.systemuiserver 'NSStatusItem Visible Siri' 1
-
name: Hide Siri from status menu
recommend: strict
docs: https://derflounder.wordpress.com/2016/09/20/blocking-siri-on-macos-sierra/
code: |-
defaults write com.apple.Siri 'StatusMenuVisible' -bool false
defaults write com.apple.Siri 'UserHasDeclinedEnable' -bool true
revertCode: |-
defaults delete com.apple.Siri 'StatusMenuVisible'
defaults delete com.apple.Siri 'UserHasDeclinedEnable'
-
name: Disable Spotlight indexing
code: sudo mdutil -i off -d /
revertCode: sudo mdutil -i on /
-
category: Security improvements
children:
-
category: Configure macOS Application Firewall
children:
-
name: Enable application firewall
recommend: standard
docs:
- https://www.stigviewer.com/stig/apple_os_x_10.13/2018-10-01/finding/V-81681
- https://daiderd.com/nix-darwin/manual/index.html
- https://developer.apple.com/documentation/devicemanagement/firewall
code: |-
/usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on
sudo defaults write /Library/Preferences/com.apple.alf globalstate -bool true
defaults write com.apple.security.firewall EnableFirewall -bool true
revertCode: |-
/usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate off
sudo defaults write /Library/Preferences/com.apple.alf globalstate -bool false
defaults write com.apple.security.firewall EnableFirewall -bool false
-
name: Turn on firewall logging
recommend: standard
docs:
- https://www.stigviewer.com/stig/apple_os_x_10.13/2018-10-01/finding/V-81671
- https://daiderd.com/nix-darwin/manual/index.html
code: |-
/usr/libexec/ApplicationFirewall/socketfilterfw --setloggingmode on
sudo defaults write /Library/Preferences/com.apple.alf loggingenabled -bool true
revertCode: |-
/usr/libexec/ApplicationFirewall/socketfilterfw --setloggingmode off
sudo defaults write /Library/Preferences/com.apple.alf loggingenabled -bool false
-
name: Turn on stealth mode
recommend: standard
docs:
- https://www.stigviewer.com/stig/apple_os_x_10.8_mountain_lion_workstation/2015-02-10/finding/V-51327
- https://daiderd.com/nix-darwin/manual/index.html
- https://developer.apple.com/documentation/devicemanagement/firewall
code: |-
/usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode on
sudo defaults write /Library/Preferences/com.apple.alf stealthenabled -bool true
defaults write com.apple.security.firewall EnableStealthMode -bool true
revertCode: |-
/usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode off
sudo defaults write /Library/Preferences/com.apple.alf stealthenabled -bool false
defaults write com.apple.security.firewall EnableStealthMode -bool false
-
category: Disable auto-permitting incoming traffic for apps
children:
-
name: Prevent automatically allowing incoming connections to signed apps
docs: https://daiderd.com/nix-darwin/manual/index.html
recommend: strict
code: sudo defaults write /Library/Preferences/com.apple.alf allowsignedenabled -bool false
revertCode: sudo defaults write /Library/Preferences/com.apple.alf allowsignedenabled -bool true
-
name: Prevent automatically allowing incoming connections to downloaded signed apps
docs: https://daiderd.com/nix-darwin/manual/index.html
recommend: strict
code: sudo defaults write /Library/Preferences/com.apple.alf allowdownloadsignedenabled -bool false
revertCode: sudo defaults write /Library/Preferences/com.apple.alf allowdownloadsignedenabled -bool true
-
name: Disable Captive portal
# An attacker could trigger the utility and direct a Mac to a site with malware without user interaction,
# so it's best to disable this feature and log in to captive portals using regular Web browser instead.
recommend: standard
docs:
# Risks with captive portals:
- https://www.eff.org/deeplinks/2017/08/how-captive-portals-interfere-wireless-security-and-privacy
# More about apple Captive portal:
- https://web.archive.org/web/20171008071031if_/http://blog.erratasec.com/2010/09/apples-secret-wispr-request.html#.WdnPa5OyL6Y
- https://web.archive.org/web/20130407200745/http://www.divertednetworks.net/apple-captiveportal.html
- https://web.archive.org/web/20170622064304/https://grpugh.wordpress.com/2014/10/29/an-undocumented-change-to-captive-network-assistant-settings-in-os-x-10-10-yosemite/
code: sudo defaults write /Library/Preferences/SystemConfiguration/com.apple.captive.control.plist Active -bool false
revertCode: sudo defaults delete /Library/Preferences/SystemConfiguration/com.apple.captive.control.plist Active
-
category: Use screen saver for protection
children:
-
name: Require a password to wake the computer from sleep or screen saver
# The screen saver acts as a session lock and prevents unauthorized users from accessing the current user's account.
docs: https://www.stigviewer.com/stig/apple_macos_11_big_sur/2020-11-27/finding/V-230744
code: sudo defaults write /Library/Preferences/com.apple.screensaver askForPassword -bool true
revertCode: sudo defaults delete /Library/Preferences/com.apple.screensaver askForPassword
-
name: Initiate session lock five seconds after screen saver is started
docs: https://www.stigviewer.com/stig/apple_macos_11_big_sur/2020-11-27/finding/V-230745
# An unattended system with an excessive grace period is vulnerable to a malicious user.
code: sudo defaults write /Library/Preferences/com.apple.screensaver 'askForPasswordDelay' -int 5
revertCode: sudo defaults delete /Library/Preferences/com.apple.screensaver 'askForPasswordDelay'
-
category: Disable guest accounts
docs:
- https://www.stigviewer.com/stig/apple_macos_11_big_sur/2021-06-16/finding/V-230823
- https://www.stigviewer.com/stig/apple_os_x_10.13/2018-10-01/finding/V-81615
children:
-
name: Disables signing in as Guest from the login screen
code: sudo defaults write /Library/Preferences/com.apple.loginwindow GuestEnabled -bool NO
revetCode: sudo defaults write /Library/Preferences/com.apple.loginwindow GuestEnabled -bool YES
-
name: Disables Guest access to file shares over AF
code: sudo defaults write /Library/Preferences/SystemConfiguration/com.apple.smb.server AllowGuestAccess -bool NO
revetCode: sudo defaults write /Library/Preferences/SystemConfiguration/com.apple.smb.server AllowGuestAccess -bool YES
-
name: Disables Guest access to file shares over SMB
code: sudo defaults write /Library/Preferences/com.apple.AppleFileServer guestAccess -bool NO
revetCode: sudo defaults write /Library/Preferences/com.apple.AppleFileServer guestAccess -bool YES
-
category: Prevent unauthorized connections
children:
-
name: Disable remote login (incoming SSH and SFTP connections)
recommend: standard
docs: https://osxdaily.com/2016/08/16/enable-ssh-mac-command-line/
# Check if enabled: sudo systemsetup -getremotelogin, returns "Remote Login: On" or "Off"
code: echo 'yes' | sudo systemsetup -setremotelogin off
revertCode: sudo systemsetup -setremotelogin on
-
name: Disable insecure TFTP service
recommend: standard
# If the system does not require Trivial File Transfer Protocol (TFTP), then support for
# it is non-essential and should be disabled. The information system should be configured to
# provide only essential capabilities. Disabling TFTP helps prevent the unauthorized connection
# of devices and the unauthorized transfer of information.
docs: https://www.stigviewer.com/stig/apple_macos_11_big_sur/2021-06-16/finding/V-230813
code: sudo launchctl disable 'system/com.apple.tftpd'
revertCode: sudo launchctl enable 'system/com.apple.tftpd'
-
name: Disable Bonjour multicast advertising
recommend: standard
docs: https://www.stigviewer.com/stig/apple_os_x_10.11/2017-04-06/finding/V-67593
code: sudo defaults write /Library/Preferences/com.apple.mDNSResponder.plist NoMulticastAdvertisements -bool true
revertCode: sudo defaults write /Library/Preferences/com.apple.mDNSResponder.plist NoMulticastAdvertisements -bool false
-
name: Disable insecure telnet protocol
recommend: standard
docs:
- https://www.stigviewer.com/stig/apple_os_x_10.13/2020-09-11/finding/V-214882
- https://www.stigviewer.com/stig/apple_os_x_10.10_yosemite_workstation/2017-04-06/finding/V-59671
code: sudo launchctl disable system/com.apple.telnetd
revertCode: sudo launchctl enable system/com.apple.telnetd
-
category: Disable printer sharing (IPP, LDP, SMB and Bonjour protocols)
# Used typically for servers
# By default, the CUPS only listens to requests from the machine that it's running on
# cupsctl is a tool to manage the configuration of the CUPS daemon
docs:
- https://www.cups.org/doc/sharing.html
- https://www.cups.org/doc/security.html # Security risks
children:
-
name: Disable sharing of local printers with other computers
recommend: standard
docs: https://www.cups.org/doc/man-cupsctl.html
code: cupsctl --no-share-printers
revertCode: cupsctl --share-printers
-
name: Disable printing from any address including the Internet
recommend: standard
docs: https://www.cups.org/doc/man-cupsctl.html
code: cupsctl --no-remote-any
revertCode: cupsctl --remote-any
-
name: Disable remote printer administration
recommend: standard
docs: https://www.cups.org/doc/man-cupsctl.html
code: cupsctl --no-remote-admin
revertCode: cupsctl --remote-admin
-
category: Privacy over security
children:
-
category: Disable File Quarantine (tracks downloaded files and warns)
# OS tracks downloaded files with help of quarantine-aware applications
# (such as Safari, Chrome) adding quarantine extended attributes to files.
# then OS warns and asks if you really want to open it
docs: https://support.apple.com/en-gb/HT202491
children:
-
category: Clean File Quarantine from downloaded files
children:
-
name: Clear File Quarantine logs of all downloaded files
recommend: strict
docs:
- https://www.macobserver.com/tips/how-to/your-mac-remembers-everything-you-download-heres-how-to-clear-download-history/
- https://eclecticlight.co/2019/04/25/%F0%9F%8E%97-quarantine-apps/
- https://eclecticlight.co/2017/12/11/xattr-com-apple-quarantine-the-quarantine-flag/
- https://eclecticlight.co/2017/08/14/show-me-your-metadata-extended-attributes-in-macos-sierra/
# Query entries using:
# sqlite3 ~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2 'select DISTINCT LSQuarantineDataURLString from LSQuarantineEvent'
code: |-
db_file=~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2
db_query='delete from LSQuarantineEvent'
if [ -f "$db_file" ]; then
echo "Database exists at \"$db_file\""
if ls -lO "$db_file" | grep --silent 'schg'; then
sudo chflags noschg "$db_file"
echo "Found and removed system immutable flag"
has_sytem_immutable_flag=true
fi
if ls -lO "$db_file" | grep --silent 'uchg'; then
sudo chflags nouchg "$db_file"
echo "Found and removed user immutable flag"
has_user_immutable_flag=true
fi
sqlite3 "$db_file" "$db_query"
echo "Executed the query \"$db_query\""
if [ "$has_sytem_immutable_flag" = true ] ; then
sudo chflags schg "$db_file"
echo "Added system immutable flag back"
fi
if [ "$has_user_immutable_flag" = true ] ; then
sudo chflags uchg "$db_file"
echo "Added user immutable flag back"
fi
else
echo "No action needed, database does not exist at \"$db_file\""
fi
-
name: Clear File Quarantine attribute from downloaded files
docs: https://superuser.com/questions/28384/what-should-i-do-about-com-apple-quarantine
code: |-
find ~/Downloads \
-type f \
-exec \
sh -c \
'
attr="com.apple.quarantine"
file="{}"
if [[ $(xattr "$file") = *$attr* ]]; then
if xattr -d "$attr" "$file" 2>/dev/null; then
echo "🧹 Cleaned attribute from \"$file\""
else
>&2 echo "❌ Failed to clean attribute from \"$file\""
fi
else
echo "No attribute in \"$file\""
fi
' \
{} \;
-
category: Disable File Quarantine from tracking downloaded files
children:
-
name: Prevent quarantine from logging downloaded files
docs:
- https://eclecticlight.co/2019/04/25/%F0%9F%8E%97-quarantine-apps/
- https://eclecticlight.co/2017/12/11/xattr-com-apple-quarantine-the-quarantine-flag/
- https://eclecticlight.co/2017/08/14/show-me-your-metadata-extended-attributes-in-macos-sierra/
recommend: strict
code: |-
file_to_lock=~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2
if [ -f "$file_to_lock" ]; then
sudo chflags schg "$file_to_lock"
echo "Made file immutable at \"$file_to_lock\""
else
echo "No action is needed, file does not exist at \"$file_to_lock\""
fi
revertCode: |-
file_to_lock=~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2
if [ -f "$file_to_lock" ]; then
sudo chflags noschg "$file_to_lock"
echo "Successfully reverted immutability from \"$file_to_lock\""
else
>&2 echo "Cannot revert immutability, file does not exist at\"$file_to_lock\""
fi
-
name: Disable using extended quarantine attribute on downloaded files (disables warning)
# Disables dialogs shown when opening an application for the first time
# i.e. "Application Downloaded from Internet" quarantine warning.
docs:
- https://apple.stackexchange.com/questions/373176/disable-the-use-of-the-com-apple-quarantine-extended-attribute-on-mojave
- https://superuser.com/questions/266176/is-there-some-way-to-disable-the-dialogs-shown-when-opening-an-application-for-t
- https://macos-defaults.com/misc/lsquarantine.html
code: sudo defaults write com.apple.LaunchServices 'LSQuarantine' -bool NO
revertCode: sudo defaults delete com.apple.LaunchServices 'LSQuarantine'
-
category: Disable Gatekeeper (enforces code-signing)
# Built on top of File Quarantine, requires code-signing for apps.
# Warns user if a file is not signed by it's developer with certificate issued by Apple.
# Can protect against unknown threats.
children:
-
name: Prevent Gatekeeper from automatically reactivating itself
docs:
- https://osxdaily.com/2015/11/05/stop-gatekeeper-auto-rearm-mac-os-x/
- https://www.cnet.com/tech/computing/how-to-disable-gatekeeper-permanently-on-os-x/
code: sudo defaults write /Library/Preferences/com.apple.security GKAutoRearm -bool true
revertCode: sudo defaults write /Library/Preferences/com.apple.security GKAutoRearm -bool false
-
name: Disable Gatekeeper
docs:
# References for spctl --master-disable
- https://www.manpagez.com/man/8/spctl/
# References for /var/db/SystemPolicy-prefs.plist
- https://krypted.com/mac-security/manage-gatekeeper-from-the-command-line-in-mountain-lion/
- https://community.jamf.com/t5/jamf-pro/users-can-t-change-password-greyed-out/m-p/54228
code: |-
os_major_ver=$(sw_vers -productVersion | awk -F "." '{print $1}')
os_minor_ver=$(sw_vers -productVersion | awk -F "." '{print $2}')
if [[ $os_major_ver -le 10 \
|| ( $os_major_ver -eq 10 && $os_minor_ver -lt 7 ) \
]]; then
echo "No action needed, Gatekeeper is not available this OS version"
else
gatekeeper_status="$(spctl --status | awk '/assessments/ {print $2}')"
if [ $gatekeeper_status = "disabled" ]; then
echo "No action needed, Gatekeeper is already disabled"
elif [ $gatekeeper_status = "enabled" ]; then
sudo spctl --master-disable
sudo defaults write '/var/db/SystemPolicy-prefs' 'enabled' -string 'no'
echo "Disabled Gatekeeper"
else
>&2 echo "Unknown gatekeeper status: $gatekeeper_status"
fi
fi
revertCode: |-
os_major_ver=$(sw_vers -productVersion | awk -F "." '{print $1}')
os_minor_ver=$(sw_vers -productVersion | awk -F "." '{print $2}')
if [[ $os_major_ver -le 10 \
|| ( $os_major_ver -eq 10 && $os_minor_ver -lt 7 ) \
]]; then
>&2 echo "Gatekeeper is not available in this OS version"
else
gatekeeper_status="$(spctl --status | awk '/assessments/ {print $2}')"
if [ $gatekeeper_status = "disabled" ]; then
sudo spctl --master-enable
sudo defaults write '/var/db/SystemPolicy-prefs' 'enabled' -string 'yes'
echo "Enabled Gatekeeper"
elif [ $gatekeeper_status = "enabled" ]; then
echo "No action needed, Gatekeeper is already enabled"
else
>&2 echo "Unknown Gatekeeper status: $gatekeeper_status"
fi
fi
-
name: Disable Library Validation Entitlement (checks signature of libraries)
docs:
- https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_disable-library-validation
- https://www.macenhance.com/docs/general/sip-library-validation.html
- https://www.naut.ca/blog/2020/11/13/forbidden-commands-to-liberate-macos/
code: sudo defaults write /Library/Preferences/com.apple.security.libraryvalidation.plist 'DisableLibraryValidation' -bool true
revertCode: sudo defaults write /Library/Preferences/com.apple.security.libraryvalidation.plist 'DisableLibraryValidation' -bool false
-
category: Disable automatic updates
docs:
- https://developer.apple.com/documentation/devicemanagement/deviceinformationresponse/queryresponses/osupdatesettings
- https://macadminsdoc.readthedocs.io/en/master/Profiles-and-Settings/OS-X-Updates.html
children:
-
name: Disable automatically checking for updates
docs: https://developer.apple.com/documentation/devicemanagement/softwareupdate
code: |-
# For OS X Yosemite and later (>= 10.10)
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticCheckEnabled' -bool false
revertCode: |-
# For OS X Yosemite and later (>= 10.10)
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticCheckEnabled' -bool true
-
name: Disable automatically downloading new updates when available
docs: https://developer.apple.com/documentation/devicemanagement/softwareupdate
code: |-
# For OS X Yosemite and later (>= 10.10)
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticDownload' -bool false
revertCode: |-
# For OS X Yosemite and later (>= 10.10)
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticDownload' -bool true
-
name: Disable automatically installing macOS updates
docs:
# References for AutoUpdateRestartRequired
- https://kb.vmware.com/s/article/2960635
- https://derflounder.wordpress.com/2018/12/28/enabling-automatic-macos-software-updates-for-os-x-yosemite-through-macos-mojave/
# References for AutomaticallyInstallMacOSUpdates
- https://developer.apple.com/documentation/devicemanagement/softwareupdate
code: |-
# For OS X Yosemite through macOS High Sierra (>= 10.10 && < 10.14)
sudo defaults write /Library/Preferences/com.apple.commerce 'AutoUpdateRestartRequired' -bool false
# For Mojave and later (>= 10.14)
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticallyInstallMacOSUpdates' -bool false
revertCode: |-
# For OS X Yosemite through macOS High Sierra (>= 10.10 && < 10.14)
sudo defaults write /Library/Preferences/com.apple.commerce 'AutoUpdateRestartRequired' -bool true
# For Mojave and later (>= 10.14)
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticallyInstallMacOSUpdates' -bool true
-
name: Disable automatically updating app from the App Store
docs:
- https://kb.vmware.com/s/article/2960635
- https://derflounder.wordpress.com/2018/12/28/enabling-automatic-macos-software-updates-for-os-x-yosemite-through-macos-mojave/
code: |-
# For OS X Yosemite and later (>= 10.10)
sudo defaults write /Library/Preferences/com.apple.commerce 'AutoUpdate' -bool false
# For Mojave and later (>= 10.14)
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticallyInstallAppUpdates' -bool false
revertCode: |-
# For OS X Yosemite and later
sudo defaults write /Library/Preferences/com.apple.commerce 'AutoUpdate' -bool true
# For Mojave and later (>= 10.14)
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticallyInstallAppUpdates' -bool true
-
name: Disable installation of macOS beta releases
docs: https://support.apple.com/en-gb/HT203018
code: |-
# For OS X Yosemite and later (>= 10.10)
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AllowPreReleaseInstallation' -bool false
revertCode: |-
# For OS X Yosemite and later (>= 10.10)
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AllowPreReleaseInstallation' -bool true
-
name: Disable automatically installing configuration data (e.g. XProtect, Gatekeeper, MRT)
docs: https://derflounder.wordpress.com/2018/12/28/enabling-automatic-macos-software-updates-for-os-x-yosemite-through-macos-mojave/
code: |-
# For OS X Yosemite and later (>= 10.10)
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'ConfigDataInstall' -bool false
revertCode: |-
# For OS X Yosemite and later (>= 10.10)
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'ConfigDataInstall' -bool true
-
name: Disable automatically installing system data files and security updates
docs:
# References for CriticalUpdateInstall
- https://derflounder.wordpress.com/2014/12/24/managing-os-xs-automatic-security-updates/
- https://developer.apple.com/documentation/devicemanagement/softwareupdate
# References for softwareupdate --background-critical
- https://managingosx.wordpress.com/2013/04/30/undocumented-options/
code: |-
# For OS X Yosemite and later (>= 10.10)
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'CriticalUpdateInstall' -bool false
revertCode: |-
# For OS X Yosemite and later (>= 10.10)
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'CriticalUpdateInstall' -bool true
# Trigger background check with normal scan (critical updates only)
sudo softwareupdate --background-critical
functions: functions:
- -
name: PersistUserEnvironmentConfiguration name: PersistUserEnvironmentConfiguration
parameters: [ configuration ] parameters:
- name: configuration
code: |- code: |-
command='{{ $configuration }}' command='{{ $configuration }}'
declare -a profile_files=("$HOME/.bash_profile" "$HOME/.zprofile") declare -a profile_files=("$HOME/.bash_profile" "$HOME/.zprofile")
@@ -557,4 +1105,4 @@ functions:
else else
echo "[$profile_file] No need for any action, configuration does not exist" echo "[$profile_file] No need for any action, configuration does not exist"
fi fi
done done

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import { getEnumNames, getEnumValues } from '@/application/Common/Enum'; import { getEnumNames, getEnumValues, assertInRange } from '@/application/Common/Enum';
import { IEntity } from '../infrastructure/Entity/IEntity'; import { IEntity } from '../infrastructure/Entity/IEntity';
import { ICategory } from './ICategory'; import { ICategory } from './ICategory';
import { IScript } from './IScript'; import { IScript } from './IScript';
@@ -21,7 +21,7 @@ export class CategoryCollection implements ICategoryCollection {
throw new Error('undefined scripting definition'); throw new Error('undefined scripting definition');
} }
this.queryable = makeQueryable(actions); this.queryable = makeQueryable(actions);
ensureValidOs(os); assertInRange(os, OperatingSystem);
ensureValid(this.queryable); ensureValid(this.queryable);
ensureNoDuplicates(this.queryable.allCategories); ensureNoDuplicates(this.queryable.allCategories);
ensureNoDuplicates(this.queryable.allScripts); ensureNoDuplicates(this.queryable.allScripts);
@@ -54,18 +54,6 @@ export class CategoryCollection implements ICategoryCollection {
} }
} }
function ensureValidOs(os: OperatingSystem): void {
if (os === undefined) {
throw new Error('undefined os');
}
if (os === OperatingSystem.Unknown) {
throw new Error('unknown os');
}
if (!(os in OperatingSystem)) {
throw new Error(`os "${os}" is out of range`);
}
}
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) { function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
const totalOccurrencesById = new Map<TKey, number>(); const totalOccurrencesById = new Map<TKey, number>();
for (const entity of entities) { for (const entity of entities) {

View File

@@ -10,5 +10,4 @@ export enum OperatingSystem {
Android, Android,
iOS, iOS,
WindowsPhone, WindowsPhone,
Unknown,
} }

View File

@@ -1,5 +1,6 @@
import { IProjectInformation } from './IProjectInformation'; import { IProjectInformation } from './IProjectInformation';
import { OperatingSystem } from './OperatingSystem'; import { OperatingSystem } from './OperatingSystem';
import { assertInRange } from '@/application/Common/Enum';
export class ProjectInformation implements IProjectInformation { export class ProjectInformation implements IProjectInformation {
public readonly repositoryWebUrl: string; public readonly repositoryWebUrl: string;
@@ -42,6 +43,7 @@ function getWebUrl(gitUrl: string) {
} }
function getFileName(os: OperatingSystem, version: string): string { function getFileName(os: OperatingSystem, version: string): string {
assertInRange(os, OperatingSystem);
switch (os) { switch (os) {
case OperatingSystem.Linux: case OperatingSystem.Linux:
return `privacy.sexy-${version}.AppImage`; return `privacy.sexy-${version}.AppImage`;
@@ -50,6 +52,6 @@ function getFileName(os: OperatingSystem, version: string): string {
case OperatingSystem.Windows: case OperatingSystem.Windows:
return `privacy.sexy-Setup-${version}.exe`; return `privacy.sexy-Setup-${version}.exe`;
default: default:
throw new Error(`Unsupported os: ${OperatingSystem[os]}`); throw new RangeError(`Unsupported os: ${OperatingSystem[os]}`);
} }
} }

View File

@@ -7,16 +7,7 @@ export class ScriptCode implements IScriptCode {
syntax: ILanguageSyntax) { syntax: ILanguageSyntax) {
if (!syntax) { throw new Error('undefined syntax'); } if (!syntax) { throw new Error('undefined syntax'); }
validateCode(execute, syntax); validateCode(execute, syntax);
if (revert) { validateRevertCode(revert, execute, syntax);
try {
validateCode(revert, syntax);
if (execute === revert) {
throw new Error(`Code itself and its reverting code cannot be the same`);
}
} catch (err) {
throw Error(`(revert): ${err.message}`);
}
}
} }
} }
@@ -25,6 +16,20 @@ export interface ILanguageSyntax {
readonly commonCodeParts: string[]; readonly commonCodeParts: string[];
} }
function validateRevertCode(revertCode: string, execute: string, syntax: ILanguageSyntax) {
if (!revertCode) {
return;
}
try {
validateCode(revertCode, syntax);
if (execute === revertCode) {
throw new Error(`Code itself and its reverting code cannot be the same`);
}
} catch (err) {
throw Error(`(revert): ${err.message}`);
}
}
function validateCode(code: string, syntax: ILanguageSyntax): void { function validateCode(code: string, syntax: ILanguageSyntax): void {
if (!code || code.length === 0) { if (!code || code.length === 0) {
throw new Error(`code is empty or undefined`); throw new Error(`code is empty or undefined`);
@@ -34,23 +39,37 @@ function validateCode(code: string, syntax: ILanguageSyntax): void {
} }
function ensureNoEmptyLines(code: string): void { function ensureNoEmptyLines(code: string): void {
if (code.split('\n').some((line) => line.trim().length === 0)) { const lines = code.split(/\r\n|\r|\n/);
throw Error(`script has empty lines`); if (lines.some((line) => line.trim().length === 0)) {
throw Error(`Script has empty lines:\n${lines.map((part, index) => `\n (${index}) ${part || '❌'}`).join('')}`);
} }
} }
function ensureCodeHasUniqueLines(code: string, syntax: ILanguageSyntax): void { function ensureCodeHasUniqueLines(code: string, syntax: ILanguageSyntax): void {
const lines = code.split('\n') const allLines = code.split(/\r\n|\r|\n/);
.filter((line) => !shouldIgnoreLine(line, syntax)); const checkedLines = allLines.filter((line) => !shouldIgnoreLine(line, syntax));
if (lines.length === 0) { if (checkedLines.length === 0) {
return; return;
} }
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i); const duplicateLines = checkedLines.filter((e, i, a) => a.indexOf(e) !== i);
if (duplicateLines.length !== 0) { if (duplicateLines.length !== 0) {
throw Error(`Duplicates detected in script:\n${duplicateLines.map((line, index) => `(${index}) - ${line}`).join('\n')}`); throw Error(`Duplicates detected in script:\n${printDuplicatedLines(allLines)}`);
} }
} }
function printDuplicatedLines(allLines: string[]) {
return allLines
.map((line, index) => {
const occurrenceIndices = allLines
.map((e, i) => e === line ? i : '')
.filter(String);
const isDuplicate = occurrenceIndices.length > 1;
const indicator = isDuplicate ? `❌ (${occurrenceIndices.join(',')})\t` : '✅ ';
return `${indicator}[${index}] ${line}`;
})
.join('\n');
}
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean { function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
codeLine = codeLine.toLowerCase(); codeLine = codeLine.toLowerCase();
const isCommentLine = () => syntax.commentDelimiters.some((delimiter) => codeLine.startsWith(delimiter)); const isCommentLine = () => syntax.commentDelimiters.some((delimiter) => codeLine.startsWith(delimiter));

View File

@@ -5,16 +5,20 @@ import fs from 'fs';
import child_process from 'child_process'; import child_process from 'child_process';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
export async function runCodeAsync( export class CodeRunner {
code: string, folderName: string, fileExtension: string, constructor(
node = getNodeJs(), environment = Environment.CurrentEnvironment): Promise<void> { private readonly node = getNodeJs(),
const dir = node.path.join(node.os.tmpdir(), folderName); private readonly environment = Environment.CurrentEnvironment) {
await node.fs.promises.mkdir(dir, {recursive: true}); }
const filePath = node.path.join(dir, `run.${fileExtension}`); public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
await node.fs.promises.writeFile(filePath, code); const dir = this.node.path.join(this.node.os.tmpdir(), folderName);
await node.fs.promises.chmod(filePath, '755'); await this.node.fs.promises.mkdir(dir, {recursive: true});
const command = getExecuteCommand(filePath, environment); const filePath = this.node.path.join(dir, `run.${fileExtension}`);
node.child_process.exec(command); await this.node.fs.promises.writeFile(filePath, code);
await this.node.fs.promises.chmod(filePath, '755');
const command = getExecuteCommand(filePath, this.environment);
this.node.child_process.exec(command);
}
} }
function getExecuteCommand(scriptPath: string, environment: Environment): string { function getExecuteCommand(scriptPath: string, environment: Environment): string {

View File

@@ -12,7 +12,7 @@ export class AsyncLazy<T> {
this.valueFactory = valueFactory; this.valueFactory = valueFactory;
} }
public async getValueAsync(): Promise<T> { public async getValue(): Promise<T> {
// If value is already created, return the value directly // If value is already created, return the value directly
if (this.isValueCreated) { if (this.isValueCreated) {
return Promise.resolve(this.value); return Promise.resolve(this.value);

View File

@@ -0,0 +1,5 @@
export type SchedulerType = (callback: (...args: any[]) => void, ms: number) => void;
export function sleep(time: number, scheduler: SchedulerType = setTimeout) {
return new Promise((resolve) => scheduler(() => resolve(undefined), time));
}

View File

@@ -1,10 +1,10 @@
<template> <template>
<div id="app"> <div id="app">
<div class="wrapper"> <div class="app__wrapper">
<TheHeader class="row" /> <TheHeader class="app__row" />
<TheSearchBar class="row" /> <TheSearchBar class="app__row" />
<TheScriptArea class="row" /> <TheScriptArea class="app__row" />
<TheCodeButtons class="row code-buttons" /> <TheCodeButtons class="app__row app__code-buttons" />
<TheFooter /> <TheFooter />
</div> </div>
</div> </div>
@@ -41,27 +41,36 @@ export default class App extends Vue {
box-sizing: border-box; box-sizing: border-box;
} }
a {
color:inherit;
text-decoration: underline;
cursor: pointer;
&:hover {
color: $color-primary;
}
}
body { body {
background: $light-gray; background: $color-background;
font-family: $main-font; font-family: $main-font;
color: $slate;
} }
#app { #app {
margin-right: auto; margin-right: auto;
margin-left: auto; margin-left: auto;
max-width: 1600px; max-width: 1600px;
.wrapper { .app__wrapper {
margin: 0% 2% 0% 2%; margin: 0% 2% 0% 2%;
background-color: white; background-color: $color-surface;
color: $color-on-surface;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.06); box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.06);
padding: 2%; padding: 2%;
display:flex; display:flex;
flex-direction: column; flex-direction: column;
.row { .app__row {
margin-bottom: 10px; margin-bottom: 10px;
} }
.code-buttons { .app__code-buttons {
padding-bottom: 10px; padding-bottom: 10px;
} }
} }

View File

@@ -32,8 +32,8 @@ export default class Code extends Vue {
white-space: nowrap; white-space: nowrap;
justify-content: space-between; justify-content: space-between;
font-family: $normal-font; font-family: $normal-font;
background-color: $slate; background-color: $color-primary-darker;
color: $light-gray; color: $color-on-primary;
padding-left: 0.3rem; padding-left: 0.3rem;
padding-right: 0.3rem; padding-right: 0.3rem;
.dollar { .dollar {
@@ -45,7 +45,7 @@ export default class Code extends Vue {
margin-left: 1rem; margin-left: 1rem;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
opacity: 0.8; color: $color-primary;
} }
} }
code { code {

View File

@@ -32,25 +32,22 @@ export default class IconButton extends Vue {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
background-color: $accent; background-color: $color-secondary;
color: $color-on-secondary;
border: none; border: none;
color: $white;
padding:20px; padding:20px;
transition-duration: 0.4s; transition-duration: 0.4s;
overflow: hidden; overflow: hidden;
box-shadow: 0 3px 9px $dark-slate; box-shadow: 0 3px 9px $color-primary-darkest;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
// border: 0.1em solid $slate;
// border-radius: 80px;
// padding: 0.5em;
width: 10%; width: 10%;
min-width: 90px; min-width: 90px;
&:hover { &:hover {
background: $white; background: $color-surface;
box-shadow: 0px 2px 10px 5px $accent; box-shadow: 0px 2px 10px 5px $color-secondary;
color: $black;
} }
&:hover>&__text { &:hover>&__text {
display: block; display: block;
@@ -62,7 +59,7 @@ export default class IconButton extends Vue {
display: none; display: none;
font-family: $artistic-font; font-family: $artistic-font;
font-size: 1.5em; font-size: 1.5em;
color: $gray; color: $color-primary;
font-weight: 500; font-weight: 500;
line-height: 1.1; line-height: 1.1;
} }

View File

@@ -96,7 +96,7 @@ export default class MacOsInstructions extends Vue {
public macOsDownloadUrl = ''; public macOsDownloadUrl = '';
public async created() { public async created() {
const app = await ApplicationFactory.Current.getAppAsync(); const app = await ApplicationFactory.Current.getApp();
this.appName = app.info.name; this.appName = app.info.name;
this.macOsDownloadUrl = app.info.getDownloadUrl(OperatingSystem.macOS); this.macOsDownloadUrl = app.info.getDownloadUrl(OperatingSystem.macOS);
} }

View File

@@ -3,18 +3,18 @@
<IconButton <IconButton
v-if="this.canRun" v-if="this.canRun"
text="Run" text="Run"
v-on:click="executeCodeAsync" v-on:click="executeCode"
icon-prefix="fas" icon-name="play"> icon-prefix="fas" icon-name="play">
</IconButton> </IconButton>
<IconButton <IconButton
:text="this.isDesktopVersion ? 'Save' : 'Download'" :text="this.isDesktopVersion ? 'Save' : 'Download'"
v-on:click="saveCodeAsync" v-on:click="saveCode"
icon-prefix="fas" icon-prefix="fas"
:icon-name="this.isDesktopVersion ? 'save' : 'file-download'"> :icon-name="this.isDesktopVersion ? 'save' : 'file-download'">
</IconButton> </IconButton>
<IconButton <IconButton
text="Copy" text="Copy"
v-on:click="copyCodeAsync" v-on:click="copyCode"
icon-prefix="fas" icon-name="copy"> icon-prefix="fas" icon-name="copy">
</IconButton> </IconButton>
<Dialog v-if="this.isMacOsCollection" ref="instructionsDialog"> <Dialog v-if="this.isMacOsCollection" ref="instructionsDialog">
@@ -37,7 +37,7 @@ import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode'; import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { runCodeAsync } from '@/infrastructure/CodeRunner'; import { CodeRunner } from '@/infrastructure/CodeRunner';
import { IApplicationContext } from '@/application/Context/IApplicationContext'; import { IApplicationContext } from '@/application/Context/IApplicationContext';
@Component({ @Component({
@@ -54,20 +54,20 @@ export default class TheCodeButtons extends StatefulVue {
public isMacOsCollection = false; public isMacOsCollection = false;
public fileName = ''; public fileName = '';
public async copyCodeAsync() { public async copyCode() {
const code = await this.getCurrentCodeAsync(); const code = await this.getCurrentCode();
Clipboard.copyText(code.current); Clipboard.copyText(code.current);
} }
public async saveCodeAsync() { public async saveCode() {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
saveCode(this.fileName, context.state); saveCode(this.fileName, context.state);
if (this.isMacOsCollection) { if (this.isMacOsCollection) {
(this.$refs.instructionsDialog as any).show(); (this.$refs.instructionsDialog as any).show();
} }
} }
public async executeCodeAsync() { public async executeCode() {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
await executeCodeAsync(context); await executeCode(context);
} }
protected handleCollectionState(newState: ICategoryCollectionState): void { protected handleCollectionState(newState: ICategoryCollectionState): void {
@@ -77,8 +77,8 @@ export default class TheCodeButtons extends StatefulVue {
this.react(newState.code); this.react(newState.code);
} }
private async getCurrentCodeAsync(): Promise<IApplicationCode> { private async getCurrentCode(): Promise<IApplicationCode> {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
const code = context.state.code; const code = context.state.code;
return code; return code;
} }
@@ -115,12 +115,13 @@ function buildFileName(scripting: IScriptingDefinition) {
return fileName; return fileName;
} }
async function executeCodeAsync(context: IApplicationContext) { async function executeCode(context: IApplicationContext) {
await runCodeAsync( const runner = new CodeRunner();
/*code*/ context.state.code.current, await runner.runCode(
/*appName*/ context.app.info.name, /*code*/ context.state.code.current,
/*fileExtension*/ context.state.collection.scripting.fileExtension, /*appName*/ context.app.info.name,
); /*fileExtension*/ context.state.collection.scripting.fileExtension,
);
} }
</script> </script>

View File

@@ -1,5 +1,7 @@
<template> <template>
<Responsive v-on:sizeChanged="sizeChanged()"> <Responsive
v-on:sizeChanged="sizeChanged()"
v-non-collapsing>
<div <div
:id="editorId" :id="editorId"
class="code-area" class="code-area"
@@ -18,11 +20,13 @@ import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory'; import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory';
import Responsive from '@/presentation/components/Shared/Responsive.vue'; import Responsive from '@/presentation/components/Shared/Responsive.vue';
import { NonCollapsing } from '@/presentation/components/Scripts/View/Cards/NonCollapsingDirective';
@Component({ @Component({
components: { components: {
Responsive, Responsive,
}, },
directives: { NonCollapsing },
}) })
export default class TheCodeArea extends StatefulVue { export default class TheCodeArea extends StatefulVue {
public readonly editorId = 'codeEditor'; public readonly editorId = 'codeEditor';
@@ -47,13 +51,13 @@ export default class TheCodeArea extends StatefulVue {
const appCode = newState.code; const appCode = newState.code;
this.editor.setValue(appCode.current || getDefaultCode(newState.collection.scripting.language), 1); this.editor.setValue(appCode.current || getDefaultCode(newState.collection.scripting.language), 1);
this.events.unsubscribeAll(); this.events.unsubscribeAll();
this.events.register(appCode.changed.on((code) => this.updateCodeAsync(code))); this.events.register(appCode.changed.on((code) => this.updateCode(code)));
} }
private async updateCodeAsync(event: ICodeChangedEvent) { private async updateCode(event: ICodeChangedEvent) {
this.removeCurrentHighlighting(); this.removeCurrentHighlighting();
if (event.isEmpty()) { if (event.isEmpty()) {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContext();
const defaultCode = getDefaultCode(context.state.collection.scripting.language); const defaultCode = getDefaultCode(context.state.collection.scripting.language);
this.editor.setValue(defaultCode, 1); this.editor.setValue(defaultCode, 1);
return; return;
@@ -153,8 +157,7 @@ function getDefaultCode(language: ScriptingLanguage): string {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
&__highlight { &__highlight {
background-color: $accent; background-color: $color-secondary-light;
opacity: 0.2; // having procent fails in production (minified) build
position: absolute; position: absolute;
} }
} }

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