Compare commits

..

46 Commits

Author SHA1 Message Date
undergroundwires
d328f08952 Fix incorrect URL rendering in documentation texts
This commit addresses incorrect URL rendering within documentation text
by improving auto-linkified URL labels, handling `+` symbols as spaces,
enhancing readability of encoded path segments and manually updating
some of the documetation.

Key improvements:

- Parse `+` as whitespace in URLs for accurate script labeling.
- Interpret multiple whitespaces as single for robustness.
- Decode path segments for clearer links.
- Refactor markdown renderer.
- Expand unit tests for comprehensive coverage.

Documentation has been updated to fix inline URL references and improve
linkification across several scripts, ensuring more readable and
user-friendly content.

Affected files and documentation sections have been adjusted
accordingly, including script and category names for consistency and
clarity.

Some of the script/category documentation changing fixing URL rendering
includes:

- 'Disable sending information to Customer Experience Improvement
  Program':
  - Fix reference URLs being inlined.
- 'Disable "Secure boot" button in "Windows Security"':
  - Fix rendering issue due to auto-linkification of `markdown-it`.
- 'Clear Internet Explorer DOMStore':
  - Fix rendering issue due to auto-linkification of `markdown-it`.
- 'Disable "Windows Defender Firewall" service':
  - Fix rendering issue due to auto-linkification of `markdown-it`.
  - Convert YAML comments to markdown comments visible by users.
  - Add breaking behavior to script name, changing script name to.
- 'Disable Microsoft Defender Firewall services and drivers':
  - Remove information about breaking behavior to avoid duplication and
    be consistent with the documentation of the rest of the collections.
- Use consistent styling for warning texts starting with `Caution:`.
- Rename 'Remove extensions' category to 'Remove extension apps' for
  consistency with names of its sibling categories.
2023-11-27 05:17:58 +01:00
undergroundwires
bcad357017 linux: fix Firefox settings not reverting #282
Improve the revert process for Firefox settings by extending
modifications to also include `prefs.js`.

- Validate profile directories similarly to execution script.
- Check and warn if Firefox is running during revert to prevent
  `prefs.js` from being overriden.
- Clarify output messages for execution and revert scripts.
- Add flowchart diagram for visual documentation.
- Improve documentation for consistency and precision.
- Update `.gitignore` to account for temporary draw.io files.
2023-11-26 01:20:21 +01:00
undergroundwires
9845a7cd68 Fix rendering of inline code blocks for docs
Styling of codeblocks:

- Uniform margins as other documentation elements.
- Add small margin for inline code-blocks.
- Use different background color for inline code-blocks.
- Introduce `inline-code` and `code-block` mixins for clarity in
  styling.

Overflowing of codeblocks:

- Improve flex layout of the tree component to be handle overflowing
  content and providing maximum available width. To be able to correctly
  provide maximum available width in card content, card expansion layout
  is changed so both close button and the content gets their full width.
- Other refactorings to support this:
  - Introduce separate Vue component for checkboxes of nodes for better
    separation of concerns and improved maintainability.
  - Refactor `LeafTreeNode` to make it simpler, separating layout concerns
    from other styling.
  - `ScriptsTree.vue`: Prefer `<div>`s instead of `<span>`s as they
    represent large content.
  - Remove unnecessary `<div>`s and use `<template>`s to reduce HTML
    complexity.
  - Update script documentation to not include unnecessary left padding
    on script code blocks.
  - Refactor SCSS variable names in `DocumentationText.vue` for clarity.
2023-11-25 11:03:33 +01:00
undergroundwires
7c632f7388 win: fix system app removal affecting updates #287
This commit fixes an issue where removing systems apps could disrupt
Windows Cumulative updates as reported in #287.

The fix involves removing the `EndOfLife` registry key after the app is
removed. Keeping the key at
`HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Appx\AppxAllUserStore\EndOfLife`
was identified as the cause for update failures in #287.

This commit also refactors the registry key creation/removal logic to be
owned by separate functions for easier readability and reusability.
2023-11-24 14:52:52 +01:00
undergroundwires
1442f62633 Fix spacing in documentation for readability
This commit improves the readability of the script/category
documentations by refining the vertical and horizontal spacing. The
adjustments aim to create more visually consistent layout.

Styling changes include:

- Add more and consistent spacing between text parts (such as lists,
  tables, paragraphs etc.).
- Remove top/bottom spacing at the start and end of the text.
- Add more horizontal spacing (padding on left) for lists.
- Improve blockquote styling with a border and similar spacing as other
  parts.
2023-11-23 07:39:11 +01:00
undergroundwires
7f7a84e3ba win: discourage IntelliCode disabling #267, #286
This commit fixes issues #267 and #286 where users reported that
disabling IntelliCode data collection causes Visual Studio 2022 to hang
or become unresponsive.

The script has been updated to remove its recommendation status and
include a warning about these issues. As Microsoft did not respond to
inconsistencies with the official documentation in
MicrosoftDocs/intellicode#510, this commit prevents further disruptions
for privacy.sexy users.
2023-11-22 11:59:35 +01:00
undergroundwires
dee3279f85 win: fix persistent update disabling /w tasks #272
This patch improves the existing functionality for disabling Windows
Updates. It ensures that the disabling of automatic updates is more
persistent, addressing previous shortcomings.

This commit introduces the "Disable Windows Update scheduled tasks"
category, enabling users to persistently turn off automatic background
updates.

Supporting changes include:

- Improve `DisableScheduledTask`:
  - Add the ability to elevate privileges.
  - Add the ability to disable tasks upon script reversion to match the
    correct default operating system state.
  - Fix warning output not being correctly formatted upon script
    reversion.
- Add the ability to disable tasks upon script reversion in
  `DisableScheduledTask` to match the correct default operating system
  state.
- Add a comment to clarify the rationale behind not disabling certain
  Windows services.
- Ensure consistent casing (all uppercase) for Windows environment
  variables in documentation.
- Ensure consistent and right casing of Windows folder names in scripts
  and their documentation.
2023-11-21 05:07:43 +01:00
undergroundwires
094dbb01b8 win: fix and improve Store app categorization #190
- Remove incorrect categories and flatten their children.
- Simplify user interface by removing "installed" and "provisioned" app
  categories, listing the apps directly.
- Indent comments to be easily collapse parent category in IDE.
- Improve some of the existing documentation.
2023-11-20 11:26:14 +01:00
undergroundwires
e299d40fa1 Fix layout jumps/shifts and overflow on modals
This commit improves the user interface in modal display.

- Prevent layout shifts caused by background scrollbars when modals are
  active.
- Fix unintended overflow of modals on small screens, preventing parts
  of the modal from being cut off on the right side.
- Refactor DOM manipulation, enhancing modularity, reusability,
  extensibility, and separation of concerns.
- Centralize viewport test scenarios for different sizes in a single
  definition for E2E tests.
2023-11-19 23:51:25 +01:00
undergroundwires
cb42f11b97 Fix code highlighting and optimize category select
This commit introduces a batched debounce mechanism for managing user
selection state changes. It effectively reduces unnecessary processing
during rapid script checking, preventing multiple triggers for code
compilation and UI rendering.

Key improvements include:

- Enhanced performance, especially noticeable when selecting large
  categories. This update resolves minor UI freezes experienced when
  selecting categories with numerous scripts.
- Correction of a bug where the code area only highlighted the last
  selected script when multiple scripts were chosen.

Other changes include:

- Timing functions:
  - Create a `Timing` folder for `throttle` and the new
    `batchedDebounce` functions.
  - Move these functions to the application layer from the presentation
    layer, reflecting their application-wide use.
  - Refactor existing code for improved clarity, naming consistency, and
    adherence to new naming conventions.
  - Add missing unit tests.
- `UserSelection`:
  - State modifications in `UserSelection` now utilize a singular object
    inspired by the CQRS pattern, enabling batch updates and flexible
    change configurations, thereby simplifying change management.
- Remove the `I` prefix from related interfaces to align with new coding
  standards.
- Refactor related code for better testability in isolation with
  dependency injection.
- Repository:
  - Move repository abstractions to the application layer.
  - Improve repository abstraction to combine `ReadonlyRepository` and
    `MutableRepository` interfaces.
- E2E testing:
  - Introduce E2E tests to validate the correct batch selection
    behavior.
  - Add a specialized data attribute in `TheCodeArea.vue` for improved
    testability.
  - Reorganize shared Cypress functions for a more idiomatic Cypress
    approach.
  - Improve test documentation with related information.
- `SelectedScript`:
  - Create an abstraction for simplified testability.
  - Introduce `SelectedScriptStub` in tests as a substitute for the
    actual object.
2023-11-18 22:23:27 +01:00
undergroundwires
4531645b4c Refactor to Vue 3 recommended ESLint rules
These updates ensure better adherence to Vue 3 standards and improve
overall code quality and readability.

- Update ESLint configuration from Vue 2.x to Vue 3 rules.
- Switch from "essential" to strictest "recommended" ESLint ruleset.
- Adjust ESLint script to treat warnings as errors by using
  `--max-warnings=0` flag. This enforces stricter code quality controls
  provided by Vue 3 rules.
2023-11-17 13:57:13 +01:00
undergroundwires
bf3426f91b Fix card list UI layout shifts (jumps) on load
This commit fixes layout shifts that occur on card list part of the page
when the page is initially loaded.

- Resolve issue where card list starts with minimal width, leading
  to jumps in UI until correct width is calculated on medium and
  big screens.
- Dispose of existing `ResizeObserver` properly before creating a new
  one. This prevents leaks and incorrect width calculations if
  `containerElement` changes.
- Throttle resize events to minimize width/height calculation changes,
  enhancing performance and reducing the chances for layout shifts.

Supporting CI/CD improvements:

- Enable artifact upload in CI/CD even if E2E tests fail.
- Distinguish uploaded artifacts by operating system for clarity.
2023-11-16 16:06:33 +01:00
undergroundwires
3864f04218 win: improve disabling of scheduled tasks
This commit:

- Reduces false-positive error messages when disabling scheduled tasks.
  E.g., `ERROR: The specified task name ... does not exist in the system.`
- Centralizes and unifies the logic for disabling scheduled tasks.
- Adds additional documentation, including the existence status of tasks
  on default installations.
- Updates and improves the scripts that disable scheduled tasks.
- Improves consistency of headers in documentation text by removing the
  top margin introduces by headers.

Introduces `DisableScheduledTask` templating function:

- It provides a unified way of disabling scheduled tasks.
- It displays user-friendly messages if a task cannot be found.
- It can now handle multiple tasks found matching a pattern.
- The script now exits with the correct error code.
- It skips enable/disable actions if it's not necessary.

Improve existing scripts:

- 'Disable Google update services':
  - Rename to 'Disable Google background automatic updates'.
  - Add missing scheduled tasks observed in newer versions of Chrome.
  - Change the recommendation for disabling certain tasks to `Strict`,
    as they may interfere with Google Credential Provider as
    side-effect.
  - Separate into more categories/scripts for better granularity and
    documentation.
- 'Disable Adobe Acrobat update services':
  - Rename to 'Disable Adobe background automatic updates'.
  - Separate into more categories/scripts for enhanced granularity and
    documentation.
  - Remove end-of-life `Adobe Flash Player Updater` scheduled task and
    `adobeflashplayerupdatesvc`.
- 'Disable Dropbox automatic update services':
  - Rename to 'Disable Dropbox background automatic updates'.
  - Seperate into more categories/scripts.
- 'Disable Webcam Telemetry (`devicecensus.exe`)':
  - Rename to 'Disable census data collection'.
  - Add the disabling of the "Device User" task under it.
- 'Disable `devicecensus.exe` (telemetry) process':
  - Rename to 'Disable device and configuration data collection tool'.
- 'Disable Nvidia telemetry services':
  - Rename to 'Disable Nvidia telemetry scheduled tasks'.
  - Converted into a category for better granularity.
  - Improve documentation.
- 'Disable Defender tasks':
  - Rename to 'Disable Defender scheduled tasks'.
- 'Disable "Windows Defender ExploitGuard" task':
  - Rename to 'Disable "ExploitGuard MDM policy Refresh" task'.
- 'Remove Nvidia telemetry tasks':
  - Rename to 'Remove Nvidia telemetry packages', as "tasks" often
    refers to scheduled tasks.
- 'Disable Microsoft Office Subscription Heartbeat'
  - Rename to 'Disable "Microsoft Office Subscription Heartbeat" task'.
  - Remove disabling of the undocumented `Office 16 Subscription
    Heartbeat` task.
- 'Disable OneDrive scheduled tasks':
  - Improve documentation.
  - Add disabling of 'OneDrive Per-Machine Standalone Update' task.
- 'Disable Customer Experience Improvement Program'
  - Rename to 'Disable "Customer Experience Improvement Program"
    scheduled tasks' for clarity.
2023-11-15 21:30:42 +01:00
undergroundwires
e541a35e86 Fix mobile layout overflow caused by tooltips
This commit fixes an issue where tooltips create unwanted horizontal
overflow on mobile devices.

An overlay has been added to contain the tooltip within the viewport,
ensuring it doesn't disrupt the page layout.

The changes include adjustments to CSS visibility and pointer event
handling for the tooltip container and its children.

Changes:

- Introduce an overlay that spans the entire viewport for the tooltip
  container.
- Add CSS rules to ensure the tooltip and its children maintain correct
  pointer events and overflow behavior.
- Add a Cypress end-to-end test that verifies the absence of the
  unintended horizontal overflow on small screens.
- Uploads videos/screenshots as artifacts during CI/CD to provide easier
  troubleshooting. This change is supported by creating
  `cypress-dirs.json` to be able to share directory information with
  CI/CD runners and cypress configuration file.
2023-11-14 13:46:53 +01:00
undergroundwires
bd383ed273 Fix icon tooltip alignment on instructions modal
This commit fixes a UI misalignment issue for toolips in the download
instructions modal.

The existing margin on icons caused tooltips to be misaligned upon hover
or touch.

Changes include:

- Introduce wrapper elements to encapsulate the margin, ensuring
  tooltips align with the corresponding icons.
- Extract the information incon into a separate Vue component to adhere
  to the single-responsibility principle and improve maintainability.
- Remove redundant newline at the end of 'Open terminal' tooltip to
  reduce the visual clutter.
2023-11-13 19:02:14 +01:00
undergroundwires
949fac1a7c Refactor to enforce strictNullChecks
This commit applies `strictNullChecks` to the entire codebase to improve
maintainability and type safety. Key changes include:

- Remove some explicit null-checks where unnecessary.
- Add necessary null-checks.
- Refactor static factory functions for a more functional approach.
- Improve some test names and contexts for better debugging.
- Add unit tests for any additional logic introduced.
- Refactor `createPositionFromRegexFullMatch` to its own function as the
  logic is reused.
- Prefer `find` prefix on functions that may return `undefined` and
  `get` prefix for those that always return a value.
2023-11-12 22:54:00 +01:00
undergroundwires
7ab16ecccb Refactor watch sources for reliability
This commit changes `WatchSource` signatures into `Readonly<Ref>`s.

It provides two important benefits:

1. Eliminates the possibility of `undefined` states, that's result of
   using `WatchSource`s. This previously required additional null checks.
   By using `Readonly<Ref>`, the state handling becomes simpler and less
   susceptible to null errors.
2. Optimizes performance by using references:
   - Avoids the reactive layer of `computed` references when not needed.
   - The `watch` syntax, such as `watch(() => ref.value)`, can introduce
     side effects. For example, it does not account for `triggerRef` in
     scenarios where the value remains unchanged, preventing the watcher
     from running (vuejs/core#9579).
2023-11-11 13:55:21 +01:00
undergroundwires
58cd551a30 Refactor user selection state handling using hook
This commit introduces `useUserSelectionState` compositional hook. it
centralizes and allows reusing the logic for tracking and mutation user
selection state across the application.

The change aims to increase code reusability, simplify the code, improve
testability, and adhere to the single responsibility principle. It makes
the code more reliable against race conditions and removes the need for
tracking deep changes.

Other supporting changes:

- Introduce `CardStateIndicator` component for displaying the card state
  indicator icon, improving the testability and separation of concerns.
- Refactor `SelectionTypeHandler` to use functional code with more clear
  interfaces to simplify the code. It reduces complexity, increases
  maintainability and increase readability by explicitly separating
  mutating functions.
- Add new unit tests and extend improving ones to cover the new logic
  introduced in this commit. Remove the need to mount a wrapper
  component to simplify and optimize some tests, using parameter
  injection to inject dependencies intead.
2023-11-10 13:16:53 +01:00
undergroundwires
7770a9b521 Refactor DI for simplicity and type safety
This commit improves the dependency injection mechanism by introducing a
custom `injectKey` function.

Key improvements are:

- Enforced type consistency during dependency registration and
  instantiation.
- Simplified injection process, abstracting away the complexity with a
  uniform API, regardless of the dependency's lifetime.
- Eliminated the possibility of `undefined` returns during dependency
  injection, promoting fail-fast behavior.
- Removed the necessity for type casting to `symbol` for injection keys
  in unit tests by using existing types.
- Consalidated imports, combining keys and injection functions in one
  `import` statement.
2023-11-09 13:17:38 +01:00
undergroundwires
aab0f7ea46 Remove duplicated index.html file
This commit removes the redundant `index.html` file from the
`src/presentation/public` directory. This file was initially created
there with Vue CLI before migration to Vite. The existence of this file
is now unnecessary as Vite requires `index.html` to be at the project
root.

The deletion of this duplicate file simplifies the project and
eliminates potential confusion regarding the entry point of the
application.

Changes:

- Update `docs/presentation.md` to clarify the location of `index.html`.
- Remove the `src/presentation/public/index.html` file, which was
  duplicate of the project root `index.html`.

These changes ensure compliance with Vite's configuration and project
structure clarity.
2023-11-08 17:06:53 +01:00
undergroundwires-bot
ea41f4f503 ⬆️ bump everywhere to 0.12.7 2023-11-07 01:06:52 +00:00
undergroundwires
af7219f6e1 Fix tree node check states not being updated
This commit fixes a bug where the check states of tree nodes were not
correctly updated upon selecting predefined groups like "Standard",
"Strict", "None" and "All".

It resolves the issue by manually triggering of updates for mutated
array containing selected scripts.

It enhances test coverage to prevent regression and verify the correct
behavior of state updates.

This bug was introduced in commit
4995e49c46, which optimized reactivity by
removing deep state tracking.
2023-11-07 01:14:38 +01:00
undergroundwires
8ccaec7af6 Fix unresponsive copy button on instructions modal
This commit fixes the bug where the "Copy" button does not copy when
clicked on download instructions modal (on Linux and macOS).

This commit also introduces several improvements to the UI components
related to copy action and their interaction with the clipboard feature.

It adds more tests to avoid regression of the bugs and improves
maintainability, testability and adherence to Vue's reactive principles.

Changes include:

- Fix non-responsive copy button in the download instructions modal by
  triggering a `click` event in `AppIcon.vue`.
- Improve `TheCodeButtons.vue`:
  - Remove redundant `getCurrentCode` function.
  - Separate components for each button for better separation of
    concerns and higher testability.
  - Use the `gap` property in the flexbox layout, replacing the less
    explicit sibling combinator approach.
- Add `useClipboard` compositional hook for more idiomatic Vue approach
  to interacting with the clipboard.
- Add `useCurrentCode` compositional hook to handle current code state
  more effectively with unified logic.
- Abstract clipboard operations to an interface to isolate
  responsibilities.
- Switch clipboard implementation to the `navigator.clipboard` API,
  moving away from the deprecated `document.execCommand`.
- Move clipboard logic to the presentation layer to conform to
  separation of concerns and domain-driven design principles.
- Improve `IconButton.vue` component to increase reusability with
  consistent sizing.
2023-11-06 21:55:43 +01:00
undergroundwires
b2ffc90da7 Add winget download instructions 2023-11-05 17:05:17 +01:00
undergroundwires-bot
72e4d0b896 ⬆️ bump everywhere to 0.12.6 2023-11-04 12:14:46 +00:00
undergroundwires
5bb13e34f8 win: fix store revert for multiple installs #260
This commit improves the revert script for store apps to handle
scenarios where `Get-AppxPackage` returns multiple packages. Instead of
relying on a single package result, the script now iterates over all
found packages and attempts installation using the `AppxManifest.xml`
for each. This ensures that even if multiple versions or instances of a
package are found, the script will robustly handle and attempt to install
each one until successful.

Other changes:

- Add better message with suggestion if the revert code fails, as
  discussed in #270.
- Improve robustness of finding manifest path by using `Join-Path`
  instead of basic string concatenation. This resolves wrong paths being
  built due to missing `\` in file path.
- Add check for null or empty `InstallLocation` before accessing
  manifest path. It prevents errors when accessing `AppxManifest.xml`,
  enhancing script robustness and reliability.
- Improve error handling in manifest file existence check with try-catch
  block to catch and log exceptions, ensuring uninterrupted script
  execution in edge cases such as when the script lacks access to read
  the directory.
- Add verification of package installation before attempting to install
  the package for increased robustness.
- Add documentation for revertCode.
2023-11-03 15:24:15 +01:00
undergroundwires
0466b86f10 win, linux: unify & improve Firefox clean-up #273
This commit unifies some of the logic, documentation and naming for
Firefox clean-up with improvements on both Linux and Windows platforms.

Windows:

- 'Clear browsing history and cache':
  - Not recommend.
  - Align script name and logic with Linux implementation.
  - New documentation and not including the script in recommendation
    provides safety against unintended data loss as discussed in #273.
- 'Clear Firefox user profiles, settings, and data':
  - Rename to 'Clear all Firefox user information and preferences' for
    improved clarity.
  - Add more documentation.

Linux:

- Replace `DeleteFromFirefoxProfiles` with
  `DeleteFilesFromFirefoxProfiles`.
- Migrate implementation to Python:
  - Add more user-friendly outputs.
  - Exclude removing directory itself for additional safety.

Both Linux and Windows:

- Improve documentation for:
  - 'Clear Firefox user profiles, settings, and data'
  - 'Clear Firefox history'
2023-11-02 13:18:54 +01:00
undergroundwires
ca81f68ff1 Migrate to Vue 3.0 #230
- Migrate from "Vue 2.X" to "Vue 3.X"
- Migrate from "Vue Test Utils v1" to "Vue Test Utils v2"

Changes in detail:

- Change `inserted` to `mounted`.
- Change `::v-deep` to `:deep`.
- Change to Vue 3.0 `v-modal` syntax.
- Remove old Vue 2.0 transition name, keep the ones for Vue 3.0.
- Use new global mounting API `createApp`.
- Change `destroy` to `unmount`.
- Bootstrapping:
  - Move `provide`s for global dependencies to a bootsrapper from
    `App.vue`.
  - Remove `productionTip` setting (not in Vue 3).
  - Change `IVueBootstrapper` for simplicity and Vue 3 compatible API.
  - Add missing tests.
- Remove `.text` access on `VNode` as it's now internal API of Vue.
- Import `CSSProperties` from `vue` instead of `jsx` package.
- Shims:
  - Remove unused `shims-tsx.d.ts`.
  - Remove `shims-vue.d.ts` that's missing in quickstart template.
- Unit tests:
  - Remove old typing workaround for mounting components.
  - Rename `propsData` to `props`.
  - Remove unneeded `any` cast workarounds.
  - Move stubs and `provide`s under `global` object.

Other changes:

- Add `dmg-license` dependency explicitly due to failing electron builds
  on macOS (electron-userland/electron-builder#6520,
  electron-userland/electron-builder#6489). This was a side-effect of
  updating dependencies for this commit.
2023-11-01 13:39:39 +01:00
undergroundwires
4995e49c46 Improve UI performance by optimizing reactivity
- Replace `ref`s with `shallowRef` when deep reactivity is not needed.
- Replace `readonly`s with `shallowReadonly` where the goal is to only
  prevent `.value` mutation.
- Remove redundant `ref` in `SizeObserver.vue`.
- Remove redundant nested `ref` in `TooltipWrapper.vue`.
- Remove redundant `events` export from `UseCollectionState.ts`.
- Remove redundant `computed` from `UseCollectionState.ts`.
- Remove `timestamp` from `TreeViewFilterEvent` that becomes unnecessary
  after using `shallowRef`.
- Add missing unit tests for `UseTreeViewFilterEvent`.
- Add missing stub for `FilterChangeDetails`.
2023-10-31 13:57:57 +01:00
undergroundwires
77123d8c92 win: change system app removal to hard delete #260
This commit changes the system app removal functionality in privacy.sexy
to perform a hard delete, while preserving the soft-delete logic for
handling residual files.

It improves in-code documentation to facilitate a clearer understanding
of the code execution flow, as the logic for removing system apps has
grown in complexity and length.

Transitioning to a hard-delete approach resolves issues related to
residual links to soft-deleted apps:

- Resolves issue with Edge remaining in the installed apps list (#236).
- Resolves issue with Edge remaining in the programs list (#194).
- Resolves issue with Edge shortcuts persisting in the start menu (#73).

Other changes:

- `RunPowerShell`:
  - Introduce `codeComment` and `revertCodeComment` parameters for
    improved in-code documentation.
- `CommentCode`:
  - Simplify naming to `Comment`.
  - Rename `comment` to `codeComment` for clarity.
  - Add functionality to comment on revert with the `revertCodeComment`
    parameter.
2023-10-30 12:39:10 +01:00
undergroundwires
e72c1c13ea win: improve file delete
This commit unifies the way the files are being deleted by introducing
the `DeleteFiles` function. It refactors existing scripts that are
deleting files to use the new function, to improve their documentation
and increase their safety.

Script changes:

- 'Clear Software Reporter Tool logs':
  - Rename to: 'Clear Google's "Software Reporter Tool" logs'
- 'Clear credentials in Windows Credential Manager':
  - Migrate code to PowerShell, removing the need to delete files.
  - Improve error messages and robustness of the implementation.
- 'Clear Nvidia residual telemetry files':
  - Extract to two scripts for more granularity and better
    documentation:
      1. 'Disable Nvidia telemetry components'
      2. 'Disable Nvidia telemetry drivers'
  - Change the logic so instead of clearing directory contents and
    deleting drivers, it conducts a soft delete for reversibility to
    prioritize user safety.
- 'Remove OneDrive residual files':
  - Improve documentation
- 'Clear primary Windows telemetry file':
  - Rename to 'Clear diagnostics tracking logs'.
  - Add missing file paths seen on modern versions of Windows.
  - Add more documentation.
- 'Clear Windows Update History (`WUAgent`) system logs':
  - Rename to 'Clear Windows update files'.
  - Add more documentation.
- 'Clear Cryptographic Services diagnostic traces':
  - Rename to 'Clear "Cryptographic Services" diagnostic traces'.
  - Add more documentation.

Other changes:

- Improve `DeleteGlob`:
  - Add iteration callbacks for its reusability.
  - Improve its documentation.
  - Make recursion optional.
  - Improve sanity check (verification) logic for given glob when
    granting permissions.
  - Fix granting permissions using wrong variable to find out parent
    directory.
- Improve `IterateGlob`:
  - Use `Get-Item` to get results. This fixes `DeleteDirectory` not
    being able to delete directory itself but just its contents.
  - Introduce and use `recurse` parameter to provide optional recursive
    search logic (to search in subdirectories) using `Get-ChildItem`.
  - Fix wrong PowerShell syntax for `$revert` variable value for
    `revertCode`: replace `true` with `$true`.
  - Order iterated paths based on their length to process the deepest
    item first.
  - Improve handling of missing files with correct/informative outputs
    when granting permissions.
- Improve `SoftDeleteFiles`:
  - Introduce and use `recurse` parameter for explicitness.
  - Fix undefined `$backupFilePath` by replacing it with correct
    `$originalFilePath`.
  - Improve documentation.
- Ensure consistent use of instructive language in code comments.
2023-10-29 18:42:41 +01:00
undergroundwires
e775d68a9b linux: fix string formatting of Firefox configs
This commit fixes some configurations being set wrong values to wrong
YAML notation used for string values.
2023-10-28 13:58:41 +02:00
undergroundwires
f8e5f1a5a2 Fix incorrect tooltip position after window resize
This commit fixes an issue where the tooltip position becomes inaccurate
after resizing the window.

The solution uses `autoUpdate` functionality of `floating-ui` to update
the position automatically on resize events. This function depends on
browser APIs: `IntersectionObserver` and `ResizeObserver`. The official
documentation recommends polyfilling those to support old browsers.

Polyfilling `ResizeObserver` is already part of the codebase, used by
`SizeObserver.vue`. This commit refactors polyfill logic to be reusable
across different components, and reuses it on `TooltipWrapper.vue`.

Polyfilling `IntersectionObserver` is ignored due to this API being
older and more widely supported.
2023-10-27 20:58:07 +02:00
undergroundwires
f4a74f058d win: improve soft file/app delete security #260
This commit improves soft file delete logic:

- Unify logic for soft deleting single files and system apps.
- Rename `RenameSystemFile` templating function to `SoftDeleteFiles` so
  new name gives clarity to:
   - It's not necessarily single file being renamed but can be multiple
     files.
   - It's not necessarily system files being renamed, but can also work
     without granting extra permissions.
- Grant permissions for only files that will be backed up, skipping
  unnecessarily granting permissions to folders/other files. Both
  `SeRestorePrivilege` and `SeTakeownershipPrivileges` are claimed and
  revoked as necessary.
- Make granting permissions optional through `grantPermissions`
  parameter. Do not take permissions if not needed.
- Restore permissions to system default after file is renamed. Before
  both deletion of system apps and renaming system files did not restore
  their original permissions. This might leave user computers
  vulnerable, which is fixed in this commit. It ensures that the
  system's original security posture is preserved.
- Deleting system apps is now independent of `Get-AppxPackage`,
  improving its robustness and enabling their execution once system apps
  are hard-deleted (#260)
- Introduce common way to share glob iteration logic of how the
  directories are being cleaned up. It reuses most of the logic from
  former `DeleteGlob` with some improvements:
  - Simplify call to `Get-ChildItem` by avoiding `-Filter` parameter.
  - Improve reliability of getting parent directory in `DeleteGlob`
    sanity check to use .NET's `[System.IO.Path]` methods.
2023-10-26 18:35:39 +02:00
undergroundwires
80821fca07 Fix compiler failing with nested with expression
The previous implementation of `WithParser` used regex, which struggles
with parsing nested structures correctly. This commit improves
`WithParser` to track and parse all nested `with` expressions.

Other improvements:

- Throw meaningful errors when syntax is wrong. Replacing the prior
  behavior of silently ignoring such issues.
- Remove `I` prefix from related interfaces to align with newer code
  conventions.
- Add more unit tests for `with` expression.
- Improve documentation for templating.
- `ExpressionRegexBuilder`:
  - Use words `capture` and `match` correctly.
  - Fix minor issues revealed by new and improved tests:
     - Change regex for matching anything except surrounding
       whitespaces. The new regex ensures that it works even without
       having any preceeding text.
     - Change regex for capturing pipelines. The old regex was only
       matching (non-greedy) first character of the pipeline in tests,
       new regex matches the full pipeline.
- `ExpressionRegexBuilder.spec.ts`:
  - Ensure consistent way to define `describe` and `it` blocks.
  - Replace `expectRegex` tests, regex expectations test internal
    behavior of the class, not the external.
  - Simplified tests by eliminating the need for UUID suffixes/prefixes.
2023-10-25 19:39:12 +02:00
undergroundwires
dfd4451561 win: improve script environment robustness #221
This commit ensures the script functions as expected, even when invoked
from unexpected environments.

Using `setlocal` initializes a distinct environment for privacy.sexy.
It's strategically placed after the admin privilege check to avoid
unnecessary setup in case of a relaunch. The script concludes with
`endlocal` right before the exit, maintaining a clean environment
throughout its execution and ensuring no unintentional global
environment modifications.

Changes:

- Enhance script's environment robustness.
- Add descriptive comments for script start/end sequences.
2023-10-24 16:56:54 +02:00
undergroundwires
8570b02dde win: prevent updates from reinstalling apps #260
This commit addresses the issue of unwanted applications being
reinstalled during a Windows update. By adding a specific registry
entry, this commit ensures that Windows apps, once removed, do not
return with subsequent updates.

This change ensures more control over the applications present on a
Windows system, particularly after an update, enhancing user experience
and systeam cleanliness.
2023-10-23 16:52:52 +02:00
undergroundwires
d6da406c61 Centralize Electron entry file path configuration
This commit refactors configuration to use centrally defined Electron
entry file path to improve maintainability and reduce duplication.

- Replace the hardcoded file path in the `main` field of `package.json`
  with a reference to the `ELECTRON_ENTRY` environment variable, managed
  by `electron-vite`.
- Update `electron-vite` to version 1.0.28, enabling the use of
  `ELECTRON_ENTRY` environment variable feature (details in
  alex8088/electron-vite#270).
2023-10-22 15:03:58 +02:00
undergroundwires
060e789662 win: improve directory cleanup security
This commit improves the security, reliability, and robustness of
directory cleanup operations on Windows.

The focus is shifted from deleting entire directories to purging their
contents, addressing potential unintended side effects. Previously,
numerous directories were removed, which could destabilize system
behavior.

This improvement has crucial security implications. The prior approach
involved changing ownership and assigning permissions to the directory
itself, leading to an altered and potentially less secure OS security
posture.

Directory removal improvements include:

- Output user-friendly messages.
- Improved ownership and permission handling for file deletion.
- Explicit shared functions for enhanced reliability/security.
- Centralized way to delete glob (wildcard) patterns in Windows.
Notable script improvements:

- 'Clear Steam dumps, logs, and traces':
  - Convert the script to a category to provide more granularity.
  - Improve cache cleaning, ensuring the entire cache directory is
    cleared, not just the log files.
- 'Clear "Temporary Internet Files" (browser cache)':
  - Add more documentation.
  - Grant necessary permissions to folders, fixing errors due to
    lack of permissions before.
- 'Clear Windows Update Medic Service logs':
  - Remove redundant permission grants, as they are unnecessary in
    recent Windows versions.
- 'Clear Server-initiated Healing Events system logs',
  'Clear Windows Update events logs':
  - Merge due to identical functionalities.
  - Add more documentation.
- 'Clear Defender scan (protection) history':
  - Remove the execution with `TrustedInstallerPrivileges`, uniformly
    using `grantPermissions` as with other scripts. This addresses the
    false-positive alerts from Microsoft Defender, as discussed in #264.
- 'Clear "Temporary Internet Files" (browser cache)':
  - Retain `INetCache` and `Temporary Internet Files` directories,
    purging only their contents. This approach aims to resolve the issue
    mentioned in #145, where the absence of these folders could prevent
    Microsoft Office applications from launching.
2023-10-21 17:41:37 +02:00
undergroundwires
e40b9a3cf5 win: fix Microsoft Advertising app removal #200
This commit fixes the issue where the Microsoft Advertising app fails to
be removed using the standard script. The problem is due to Microsoft
Advertising SDK (`Microsoft.Advertising.Xaml`) acting as a framework
package. Such packages are automatically installed when a specific
application requires them, and they cannot be individually removed if
there are applications that depend on them. The only way to effectively
remove this library is by uninstalling the dependent applications.

Key findings:

- On Windows 11 22H2, the issue does not arise as the package does not
  exist.
- On Windows 10 22H2, the user is prompted to delete dependent
  applications, like MSN Weather and Mail And Calendar apps. Once these
  apps are removed, the Microsoft Advertising app is automatically
  removed.

Given the nuances and potential for confusion, this script is removed.
This means that the app will no longer be removed directly but will be
handled as part of the removal of its dependencies.
2023-10-20 16:04:25 +02:00
undergroundwires
237d9944f9 Fix YAML error for site release in CI/CD
Fix the syntax error in the GitHub action script that was caused by
improper multi-line YAML notation. This correction ensures the action
can successfully parse and execute.
2023-10-19 12:33:26 +02:00
undergroundwires
79b46bf210 Improve performance of rendering during search
Optimize the tree view rendering during searches by enhancing the render
queue ordering. This update changes the rendering order to prioritize
visible nodes, leading to faster appearance of these nodes during
searches. The ordering logic now ignores the depth in the hierarchy and
instead focused on the node order. The collapsed check for the node
itself is removed, ensuring that visible collapsed parents are first
while their invisible children are rendered later.
2023-10-18 16:44:49 +02:00
undergroundwires
98a26f9ae4 win: improve system app uninstall /w fallback #260
This commit improves soft deletion of system apps. Before if the package
was missing, it failed to recover or delete system apps. Now, it works
even though if `Get-AppxPackage` returns null (i.e. package is
non-existing), so it can be executed even after a hard delete. This
allows safely introducing hard-delete of system apps (as discussed in
 #260) with still keeping a robust soft-delete as complement.

Before, the script was dependent on `Get-AppxPackage.InstallLocation`,
however a system app can only be located in one of these folders:

- C:\Windows\SystemApps\{PackageFamilyName}
- C:\Windows\{ShortAppName}

To ensure resilience, this commit adjust the script to rename the files
within these directories if Get-AppxPackage fails, this provides a
fallback.
2023-10-17 13:56:32 +02:00
undergroundwires
dbe3c5cfb9 win: improve system app uninstall cleanup #73
- Add documentation about folders.
- Add more user-friendly logging.
- Continue uninstallation if single folder fails (remove throw).
- Continue uninstallation if renaming single file fails.
- Add handling of `Metadata` folder as suggested in #73.
2023-10-16 18:42:29 +02:00
undergroundwires
25d7f7b2a4 Bump dependencies to latest
This commit updates various dependencies to their latest versions.

Other changes include:

- Moved the following from `devDependencies` to `dependencies`:
  - `electron-log`
  - `electron-updater`
- Remove `npm` dependency.
- Code changes:
  - Add type casting in several places to align with the latest
    `typescript` version.
  - Adopt to new return type of `setTimeout`.
- Dependencies not upgraded due to
  `@vue/eslint-config-airbnb-with-typescript` not supporting
  `@eslint-typescript` V6 (see vuejs/eslint-config-airbnb#58):
  - `vue/eslint-config-typescript`
  - `@typescript-eslint/eslint-plugin`
  - `@typescript-eslint/parser`
- Enable video recording for cypress as it's disabled by default since
  13.X.X.
2023-10-16 02:06:19 +02:00
undergroundwires-bot
b76e99ac0f ⬆️ bump everywhere to 0.12.5 2023-10-14 14:17:25 +00:00
494 changed files with 20988 additions and 15598 deletions

View File

@@ -10,7 +10,7 @@ module.exports = {
},
extends: [
// Vue specific rules, eslint-plugin-vue
'plugin:vue/essential',
'plugin:vue/vue3-recommended',
// Extends eslint-config-airbnb
'@vue/eslint-config-airbnb-with-typescript',

View File

@@ -3,6 +3,6 @@ runs:
steps:
-
name: Setup node
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: 16.x

View File

@@ -21,7 +21,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node
@@ -49,7 +49,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node
@@ -78,7 +78,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Install Docker on macOS
if: matrix.os == 'macos' # macOS runner is missing Docker

View File

@@ -15,7 +15,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node

View File

@@ -10,7 +10,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node

View File

@@ -18,7 +18,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node

View File

@@ -14,7 +14,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node
@@ -42,7 +42,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node

View File

@@ -13,7 +13,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node

View File

@@ -25,7 +25,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
-
name: Initialize CodeQL
uses: github/codeql-action/init@v2

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ${{ matrix.os }}-latest
steps:
-
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
ref: master # otherwise it defaults to the version tag missing bump commit
fetch-depth: 0 # fetch all history

View File

@@ -10,7 +10,7 @@ jobs:
steps:
-
name: "Infrastructure: Checkout"
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
path: aws
repository: undergroundwires/aws-static-site-with-cd
@@ -75,7 +75,7 @@ jobs:
working-directory: aws
-
name: "App: Checkout"
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
path: app
ref: master # otherwise we don't get version bump commit
@@ -102,7 +102,7 @@ jobs:
-
name: "App: Deploy to S3"
shell: bash
run: >-
run: |-
declare web_output_dir
if ! web_output_dir=$(cd app && node scripts/print-dist-dir.js --web); then
echo 'Error: Could not determine distribution directory.'

View File

@@ -14,7 +14,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node
@@ -24,3 +24,41 @@ jobs:
-
name: Run e2e tests
run: npm run test:cy:run
-
name: Output artifact directories
id: artifacts
if: always() # Run even if previous steps fail because test run video is always captured
shell: bash
run: |-
declare -r dirs_json_file='cypress-dirs.json'
if [ ! -f "${dirs_json_file}" ]; then
echo "${dirs_json_file} does not exist"
exit 1
fi
SCREENSHOTS_DIR=$(jq -r '.screenshots' "${dirs_json_file}")
VIDEOS_DIR=$(jq -r '.videos' "${dirs_json_file}")
for dir in "${SCREENSHOTS_DIR}" "${VIDEOS_DIR}"; do
if [ "${dir}" = 'null' ] || [ -z "${dir}" ]; then
echo "One or more directories are null or not specified in cypress-dirs.json"
exit 1
fi
done
echo "SCREENSHOTS_DIR=${SCREENSHOTS_DIR}" >> "${GITHUB_OUTPUT}"
echo "VIDEOS_DIR=${VIDEOS_DIR}" >> "${GITHUB_OUTPUT}"
-
name: Upload screenshots
if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed
uses: actions/upload-artifact@v3
with:
name: e2e-screenshots-${{ matrix.os }}
path: ${{ steps.artifacts.outputs.SCREENSHOTS_DIR }}
-
name: Upload videos
if: always() # Run even if previous steps fail because test run video is always captured
uses: actions/upload-artifact@v3
with:
name: e2e-videos-${{ matrix.os }}
path: ${{ steps.artifacts.outputs.VIDEOS_DIR }}

View File

@@ -16,7 +16,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node

View File

@@ -14,7 +14,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
-
name: Set-up node
uses: ./.github/actions/setup-node

14
.gitignore vendored
View File

@@ -1,5 +1,13 @@
node_modules
# Application build artifacts
/dist-*/
.vs
# npm
node_modules
# Visual Studio Code
.vscode/**/*
!.vscode/extensions.json
!.vscode/extensions.json
# draw.io
*.bkp
*.dtmp

View File

@@ -1,5 +1,59 @@
# Changelog
## 0.12.7 (2023-11-07)
* Add winget download instructions | [b2ffc90](https://github.com/undergroundwires/privacy.sexy/commit/b2ffc90da70367b9e65c82556e8f440f865ceb98)
* Fix unresponsive copy button on instructions modal | [8ccaec7](https://github.com/undergroundwires/privacy.sexy/commit/8ccaec7af6ea3ecfd46bab5c13b90f71d55e32c1)
* Fix tree node check states not being updated | [af7219f](https://github.com/undergroundwires/privacy.sexy/commit/af7219f6e12ab4a65ce07190f691cf3234e87e35)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.6...0.12.7)
## 0.12.6 (2023-11-03)
* Bump dependencies to latest | [25d7f7b](https://github.com/undergroundwires/privacy.sexy/commit/25d7f7b2a479e51e092881cc2751e67a7d3f179f)
* win: improve system app uninstall cleanup #73 | [dbe3c5c](https://github.com/undergroundwires/privacy.sexy/commit/dbe3c5cfb91ba8a1657838b69117858843c8fbc8)
* win: improve system app uninstall /w fallback #260 | [98a26f9](https://github.com/undergroundwires/privacy.sexy/commit/98a26f9ae47af2668aa53f39d1768983036048ce)
* Improve performance of rendering during search | [79b46bf](https://github.com/undergroundwires/privacy.sexy/commit/79b46bf21004d96d31551439e5db5d698a3f71f3)
* Fix YAML error for site release in CI/CD | [237d994](https://github.com/undergroundwires/privacy.sexy/commit/237d9944f900f5172366868d75219224ff0542b0)
* win: fix Microsoft Advertising app removal #200 | [e40b9a3](https://github.com/undergroundwires/privacy.sexy/commit/e40b9a3cf53c341f2e84023a9f0e9680ac08f3fa)
* win: improve directory cleanup security | [060e789](https://github.com/undergroundwires/privacy.sexy/commit/060e7896624309aebd25e8b190c127282de177e8)
* Centralize Electron entry file path configuration | [d6da406](https://github.com/undergroundwires/privacy.sexy/commit/d6da406c61e5b9f5408851d1302d6d7398157a2e)
* win: prevent updates from reinstalling apps #260 | [8570b02](https://github.com/undergroundwires/privacy.sexy/commit/8570b02dde14ffad64863f614682c3fc1f87b6c2)
* win: improve script environment robustness #221 | [dfd4451](https://github.com/undergroundwires/privacy.sexy/commit/dfd44515613f38abe5a806bda36f44e7b715b50b)
* Fix compiler failing with nested `with` expression | [80821fc](https://github.com/undergroundwires/privacy.sexy/commit/80821fca0769e5fd2c6338918fbdcea12fbe83d2)
* win: improve soft file/app delete security #260 | [f4a74f0](https://github.com/undergroundwires/privacy.sexy/commit/f4a74f058db9b5bcbcbe438785db5ec88ecc1657)
* Fix incorrect tooltip position after window resize | [f8e5f1a](https://github.com/undergroundwires/privacy.sexy/commit/f8e5f1a5a2afa1f18567e6d965359b6a1f082367)
* linux: fix string formatting of Firefox configs | [e775d68](https://github.com/undergroundwires/privacy.sexy/commit/e775d68a9b4a5f9e893ff0e3500dade036185193)
* win: improve file delete | [e72c1c1](https://github.com/undergroundwires/privacy.sexy/commit/e72c1c13ea2d73ebfc7a8da5a21254fdfc0e5b59)
* win: change system app removal to hard delete #260 | [77123d8](https://github.com/undergroundwires/privacy.sexy/commit/77123d8c929d23676a9cb21d7b697703fd1b6e82)
* Improve UI performance by optimizing reactivity | [4995e49](https://github.com/undergroundwires/privacy.sexy/commit/4995e49c469211404dac9fcb79b75eb121f80bce)
* Migrate to Vue 3.0 #230 | [ca81f68](https://github.com/undergroundwires/privacy.sexy/commit/ca81f68ff1c3bbe5b22981096ae9220b0b5851c7)
* win, linux: unify & improve Firefox clean-up #273 | [0466b86](https://github.com/undergroundwires/privacy.sexy/commit/0466b86f1013341c966a9bbf6513990337b16598)
* win: fix store revert for multiple installs #260 | [5bb13e3](https://github.com/undergroundwires/privacy.sexy/commit/5bb13e34f8de2e2a7ba943ff72b12c0569435e62)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.5...0.12.6)
## 0.12.5 (2023-10-13)
* Fix Docker build and improve checks #220 | [7669985](https://github.com/undergroundwires/privacy.sexy/commit/7669985f8e1446e726a95626ecf35b3ce6b60a16)
* Add SAST security checks with SECURITY.md #178 | [3e5239f](https://github.com/undergroundwires/privacy.sexy/commit/3e5239f7d35e57749c01adf3dbbcd365aebb39c8)
* Add Scoop download instructions #174 | [cf55ca9](https://github.com/undergroundwires/privacy.sexy/commit/cf55ca9e28b064fa7a516077a9da23e3a8e3f534)
* win: fix and improve temp dir cleanup #176, #89 | [d457504](https://github.com/undergroundwires/privacy.sexy/commit/d45750428cca010daf2721b33a8ae3a01b28813b)
* win, linux: improve VSCode setting robustness #196 | [e8a52f7](https://github.com/undergroundwires/privacy.sexy/commit/e8a52f717dc799b34ceeb1c27c2b8219391dff6a)
* linux: fix obsolete Firefox DPI script #239 | [e5f6edf](https://github.com/undergroundwires/privacy.sexy/commit/e5f6edf405bcec7c29ea4d7932d1910620fa15f8)
* win: add removal of Edge assocations #64 | [888c916](https://github.com/undergroundwires/privacy.sexy/commit/888c9166fc66a2094137fa8be739cc21bafef5f6)
* win: improve Edge & OneDrive shortcut removal #73 | [8501495](https://github.com/undergroundwires/privacy.sexy/commit/8501495c170af61913288a63dbd369db5bbc5003)
* win: relocate and document SecHealthUI #190 | [2862951](https://github.com/undergroundwires/privacy.sexy/commit/286295128d0179358e0c6b7b6415d752175a1aed)
* Add developer toolkit UI component | [2147eae](https://github.com/undergroundwires/privacy.sexy/commit/2147eae687b82d05bc43bb4605d9068f148bb92a)
* win: fix and improve network data usage reset #265 | [5e359c2](https://github.com/undergroundwires/privacy.sexy/commit/5e359c2fb82a08e6acf7159b70ca86a8234b359b)
* win: improve app reversion and docs #260 | [a3f11df](https://github.com/undergroundwires/privacy.sexy/commit/a3f11dff187c821a00910c20dac05e285cda9073)
* Fix working directory in CI/CD web release | [698b570](https://github.com/undergroundwires/privacy.sexy/commit/698b570ee6e300d6703015464f4345b5e706f1cb)
* Implement new UI component for icons #230 | [48730bc](https://github.com/undergroundwires/privacy.sexy/commit/48730bca0506120bca4bf3a23545d59f2b1a9009)
* win: fix and improve AppCompat disabling #255 | [bab6316](https://github.com/undergroundwires/privacy.sexy/commit/bab6316e7625230cf4a4cf67c3aca417347db75c)
* win, linux, mac: fix typos and improve naming | [67c3677](https://github.com/undergroundwires/privacy.sexy/commit/67c3677621b201525a813e8a26f07d607176e89b)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.4...0.12.5)
## 0.12.4 (2023-09-25)
* win: fix Windows spotlight revert, docs, recommend | [659fea7](https://github.com/undergroundwires/privacy.sexy/commit/659fea7afcabcd0ea273cfdcc8c4bae190c126f3)

View File

@@ -43,6 +43,7 @@ You have two alternatives:
1. [Create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose) and ask for someone else to add the script for you.
2. Or send a PR yourself. This would make it faster to get your code into the project. You need to add scripts to related OS in [collections](src/application/collections/) folder. Then you'd sent a pull request, see [pull request process](#pull-request-process).
- 💡 You should use existing shared functions for most of the operations, like `DisableService` for disabling services, to maintain code consistency and efficiency.
- 📖 If you're unsure about the syntax, check [collection-files.md](docs/collection-files.md).
- 📖 If you wish to use templates, use [templating.md](./docs/templating.md).

View File

@@ -122,7 +122,7 @@
## Get started
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.4/privacy.sexy-Setup-0.12.4.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.4/privacy.sexy-0.12.4.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.4/privacy.sexy-0.12.4.AppImage). For more options, see [here](#additional-install-options).
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.7/privacy.sexy-Setup-0.12.7.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.7/privacy.sexy-0.12.7.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.7/privacy.sexy-0.12.7.AppImage). For more options, see [here](#additional-install-options).
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
@@ -153,12 +153,21 @@ Online version does not require to run any software on your computer. Offline ve
## Additional Install Options
- Check the [releases page](https://github.com/undergroundwires/privacy.sexy/releases) for all available versions.
- Using [Scoop](https://scoop.sh/#/apps?q=privacy.sexy&s=2&d=1&o=true) package manager on Windows:
- Other unofficial channels (not maintained by privacy.sexy) for Windows include:
- [Scoop 🥄](https://scoop.sh/#/apps?q=privacy.sexy&s=2&d=1&o=true) (latest version):
```powershell
scoop bucket add extras
scoop install privacy.sexy
```
```powershell
scoop bucket add extras
scoop install privacy.sexy
```
- [winget 🪟](https://winget.run/pkg/undergroundwires/privacy.sexy) (may be outdated):
```powershell
winget install -e --id undergroundwires.privacy.sexy
```
With winget, updates require manual submission; the auto-update feature within privacy.sexy will notify you of new releases post-installation.
## Development

5
cypress-dirs.json Normal file
View File

@@ -0,0 +1,5 @@
{
"base": "tests/e2e",
"videos": "tests/e2e/videos",
"screenshots": "tests/e2e/videos"
}

View File

@@ -1,15 +1,31 @@
import { defineConfig } from 'cypress';
import ViteConfig from './vite.config';
const CYPRESS_BASE_DIR = 'tests/e2e/';
import cypressDirs from './cypress-dirs.json' assert { type: 'json' };
export default defineConfig({
fixturesFolder: `${CYPRESS_BASE_DIR}/fixtures`,
screenshotsFolder: `${CYPRESS_BASE_DIR}/screenshots`,
videosFolder: `${CYPRESS_BASE_DIR}/videos`,
fixturesFolder: `${cypressDirs.base}/fixtures`,
screenshotsFolder: cypressDirs.screenshots,
video: true,
videosFolder: cypressDirs.videos,
e2e: {
baseUrl: `http://localhost:${ViteConfig.server.port}/`,
specPattern: `${CYPRESS_BASE_DIR}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
supportFile: `${CYPRESS_BASE_DIR}/support/e2e.ts`,
baseUrl: `http://localhost:${getApplicationPort()}/`,
specPattern: `${cypressDirs.base}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
supportFile: `${cypressDirs.base}/support/e2e.ts`,
},
/*
Disabling Chrome's web security to allow for faster DOM queries to access DOM earlier than
`cy.get()`. It bypasses the usual same-origin policy constraints
*/
chromeWebSecurity: false,
});
function getApplicationPort(): number {
const port = ViteConfig.server?.port;
if (port === undefined) {
throw new Error('Unknown application port');
}
return port;
}

View File

@@ -11,6 +11,8 @@ The presentation layer uses an event-driven architecture for bidirectional react
## Structure
- [`/src/` **`presentation/`**](./../src/presentation/): Contains Vue and Electron code.
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
- [**`index.html`**](./../src/presentation/index.html): The `index.html` entry file, located at the root of the project as required by Vite
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers.
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
@@ -20,8 +22,7 @@ The presentation layer uses an event-driven architecture for bidirectional react
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles coupled to Vue components.
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint.
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint..
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
@@ -70,10 +71,11 @@ To add a new dependency:
1. **Define its symbol**: Define an associated symbol for every dependency in [`injectionSymbols.ts`](./../src/presentation/injectionSymbols.ts). Symbols are grouped into:
- **Singletons**: Shared across components, instantiated once.
- **Transients**: Factories yielding a new instance on every access.
2. **Provide the dependency**: Modify the [`provideDependencies`](./../src/presentation/bootstrapping/DependencyProvider.ts) function to include the new dependency. [`App.vue`](./../src/presentation/components/App.vue) calls this function within its `setup()` hook to register the dependencies.
3. **Inject the dependency**: Use Vue's `inject` method alongside the defined symbol to incorporate the dependency into components.
- For singletons, invoke the factory method: `inject(symbolKey)()`.
- For transients, directly inject: `inject(symbolKey)`.
2. **Provide the dependency**:
Modify the [`provideDependencies`](./../src/presentation/bootstrapping/DependencyProvider.ts) function to include the new dependency.
[`App.vue`](./../src/presentation/components/App.vue) calls this function within its `setup()` hook to register the dependencies.
3. **Inject the dependency**: Use `injectKey` to inject a dependency. Pass a selector function to `injectKey` that retrieves the appropriate symbol from the provided dependencies.
- Example usage: `injectKey((keys) => keys.useCollectionState)`;
## Shared UI components

View File

@@ -2,79 +2,142 @@
## Benefits of templating
- Generating scripts by sharing code to increase best-practice usage and maintainability.
- Creating self-contained scripts without cross-dependencies.
- Use of pipes for writing cleaner code and letting pipes do dirty work.
- **Code sharing:** Share code across scripts for consistent practices and easier maintenance.
- **Script independence:** Generate self-contained scripts, eliminating the need for external code.
- **Cleaner code:** Use pipes for complex operations, resulting in more readable and streamlined code.
## Expressions
- Expressions start and end with mustaches (double brackets, `{{` and `}}`).
- E.g. `Hello {{ $name }} !`
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) but not the same.
- Functions enables usage of expressions.
- 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).
- Expressions inside expressions (nested templates) are supported.
- An expression can output another expression that will also be compiled.
- E.g. following would compile first [with expression](#with), and then [parameter substitution](#parameter-substitution) in its output.
**Syntax:**
```go
{{ with $condition }}
echo {{ $text }}
{{ end }}
```
Expressions are enclosed within `{{` and `}}`.
Example: `Hello {{ $name }}!`.
They are a core component of templating, enhancing scripts with dynamic capabilities and functionality.
**Syntax similarity:**
The syntax shares similarities with [Go Templates ❤️](https://pkg.go.dev/text/template), but with some differences:
**Function definitions:**
You can use expressions in function definition.
Refer to [Function](./collection-files.md#function) for more details.
Example usage:
```yaml
name: GreetFunction
parameters:
- name: name
code: Hello {{ $name }}!
```
If you assign `name` the value `world`, invoking `GreetFunction` would result in `Hello world!`.
**Function arguments:**
You can also use expressions in arguments in nested function calls.
Refer to [`Function | collection-files.md`](./collection-files.md#functioncall) for more details.
Example with nested function calls:
```yaml
-
name: PrintMessageFunction
parameters:
- name: message
code: echo "{{ $message }}"
-
name: GreetUserFunction
parameters:
- name: userName
call:
name: PrintMessageFunction
parameters:
argument: 'Hello, {{ $userName }}!'
```
Here, if `userName` is `Alice`, invoking `GreetUserFunction` would execute `echo "Hello, Alice!"`.
**Nested templates:**
You can nest expressions inside expressions (also called "nested templates").
This means that an expression can output another expression where compiler will compile both.
For example, following would compile first [with expression](#with), and then [parameter substitution](#parameter-substitution) in its output:
```go
{{ with $condition }}
echo {{ $text }}
{{ end }}
```
### Parameter substitution
A simple function example:
Parameter substitution dynamically replaces variable references with their corresponding values in the script.
**Example function:**
```yaml
function: EchoArgument
name: DisplayTextFunction
parameters:
- name: 'argument'
code: Hello {{ $argument }} !
- name: 'text'
code: echo {{ $text }}
```
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 }} !
```
Invoking `DisplayTextFunction` with `text` set to `"Hello, world!"` would result in `echo "Hello, World!"`.
### with
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions.
E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}` would only output `Hi, I'm a block!` if `parameterName` has any value..
The `with` expression enables conditional rendering and provides a context variable for simpler code.
It binds its context (value of the provided parameter value) as arbitrary `.` value. It allows you to use the argument value of the given parameter when it is provided and not empty such as:
**Optional block rendering:**
If the provided variable is falsy (`false`, `null`, or empty), the compiler skips the enclosed block of code.
A "block" lies between the with start (`{{ with .. }}`) and end (`{{ end }}`) expressions, defining its boundaries.
Example:
```go
{{ with $optionalVariable }}
Hello
{{ end }}
```
This would display `Hello` if `$optionalVariable` is truthy.
**Parameter declaration:**
You should set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
Declare parameters used for `with` condition as optional such as:
```yaml
name: ConditionalOutputFunction
parameters:
- name: 'data'
optional: true
code: |-
{{ with $data }}
Data is: {{ . }}
{{ end }}
```
**Context variable:**
`with` statement binds its context (value of the provided parameter value) as arbitrary `.` value.
`{{ . }}` syntax gives you access to the context variable.
This is optional to use, and not required to use `with` expressions.
For example:
```go
{{ with $parameterName }}Parameter value is {{ . }} here {{ end }}
```
It supports multiline text inside the block. You can have something like:
**Multiline text:**
It supports multiline text inside the block. You can write something like:
```go
{{ with $argument }}
@@ -83,7 +146,9 @@ It supports multiline text inside the block. You can have something like:
{{ end }}
```
You can also use other expressions inside its block, such as [parameter substitution](#parameter-substitution):
**Inner expressions:**
You can also embed other expressions inside its block, such as [parameter substitution](#parameter-substitution):
```go
{{ with $condition }}
@@ -91,32 +156,44 @@ You can also use other expressions inside its block, such as [parameter substitu
{{ end }}
```
💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
This also includes nesting `with` statements:
Example:
```yaml
function: FunctionThatOutputsConditionally
parameters:
- name: 'argument'
optional: true
code: |-
{{ with $argument }}
Value is: {{ . }}
```go
{{ with $condition1 }}
Value of $condition1: {{ . }}
{{ with $condition2 }}
Value of $condition2: {{ . }}
{{ end }}
{{ end }}
```
### Pipes
- Pipes are functions available for handling text.
- Allows stacking actions one after another also known as "chaining".
- 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.
- You cannot create pipes. [A dedicated compiler](./application.md#parsing-and-compiling) provides pre-defined pipes to consume in collection files.
- You can combine pipes with other expressions such as [parameter substitution](#parameter-substitution) and [with](#with) syntax.
- ❗ 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, allows you to use them inside double quotes (`"`).
- **Example usages**
- `{{ with $code }} echo "{{ . | inlinePowerShell }}" {{ end }}`
- `{{ with $code }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}`
Pipes are functions designed for text manipulation.
They allow for a sequential application of operations resembling [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)), also known as "chaining".
Each pipeline's output becomes the input of the following pipe.
**Pre-defined**:
Pipes are pre-defined by the system.
You cannot create pipes in [collection files](./collection-files.md).
[A dedicated compiler](./application.md#parsing-and-compiling) provides pre-defined pipes to consume in collection files.
**Compatibility:**
You can combine pipes with other expressions such as [parameter substitution](#parameter-substitution) and [with](#with) syntax.
For example:
```go
{{ with $script }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}
```
**Naming:**
❗ Pipe names must be camelCase without any space or special characters.
**Available pipes:**
- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
- `escapeDoubleQuotes`: Escapes `"` characters for batch command execution, allows you to use them inside double quotes (`"`).

View File

@@ -68,21 +68,23 @@ These checks validate various qualities like runtime execution, building process
- [`./src/`](./../src/): Contains the code subject to testing.
- [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories.
- [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests.
- [`Assertions/`](./../tests/shared/Assertions/): Contains common assertion functions, prefixed with `expect`.
- [`./tests/unit/`](./../tests/unit/)
- Stores unit test code.
- The directory structure mirrors [`./src/`](./../src).
- E.g., tests for [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) reside in [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts).
- [`shared/`](./../tests/unit/shared/)
- Contains shared unit test functionalities.
- [`Assertions/`](./../tests/unit/shared/Assertions): Contains common assertion functions, prefixed with `expect`.
- [`TestCases/`](./../tests/unit/shared/TestCases/)
- Shared test cases.
- Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix.
- [`Stubs/`](./../tests/unit/shared/Stubs): Maintains stubs for component isolation, equipped with basic functionalities and, when necessary, spying or mocking capabilities.
- [`./tests/integration/`](./../tests/integration/): Contains integration test files.
- [`cypress.config.ts`](./../cypress.config.ts): Cypress (E2E tests) configuration file.
- [`cypress-dirs.json`](./../cypress-dirs.json): A central definition of directories used by Cypress, designed for reuse across different configurations.
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder, includes tests with `.cy.ts` extension.
- [`/support/e2e.ts`](./../tests/e2e/support/e2e.ts): Support file, runs before every single spec file.
- [`/tsconfig.json`]: TypeScript configuration for file Cypress code, improves IDE support, recommended to have by official documentation.
- *(git ignored)* `/videos`: Asset folder for videos taken during tests.
- *(git ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
- [`/support/e2e.ts`](./../tests/e2e/support/e2e.ts): Support file, runs before every single test file.
- [`/support/interactions/`](./../tests/e2e/support/interactions/): Contains reusable functions for simulating user interactions, enhancing test readability and maintainability.

View File

@@ -8,15 +8,21 @@ import distDirs from './dist-dirs.json' assert { type: 'json' };
const MAIN_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/main/index.ts');
const PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts');
const WEB_INDEX_HTML_PATH = resolvePathFromProjectRoot('src/presentation/index.html');
const DIST_DIR = resolvePathFromProjectRoot(distDirs.electronUnbundled);
const ELECTRON_DIST_SUBDIRECTORIES = {
main: resolveElectronDistSubdirectory('main'),
preload: resolveElectronDistSubdirectory('preload'),
renderer: resolveElectronDistSubdirectory('renderer'),
};
process.env.ELECTRON_ENTRY = resolve(ELECTRON_DIST_SUBDIRECTORIES.main, 'index.cjs');
export default defineConfig({
main: getSharedElectronConfig({
distDirSubfolder: 'main',
distDirSubfolder: ELECTRON_DIST_SUBDIRECTORIES.main,
entryFilePath: MAIN_ENTRY_FILE,
}),
preload: getSharedElectronConfig({
distDirSubfolder: 'preload',
distDirSubfolder: ELECTRON_DIST_SUBDIRECTORIES.preload,
entryFilePath: PRELOAD_ENTRY_FILE,
}),
renderer: mergeConfig(
@@ -25,7 +31,7 @@ export default defineConfig({
}),
{
build: {
outDir: resolve(DIST_DIR, 'renderer'),
outDir: ELECTRON_DIST_SUBDIRECTORIES.renderer,
rollupOptions: {
input: {
index: WEB_INDEX_HTML_PATH,
@@ -42,7 +48,7 @@ function getSharedElectronConfig(options: {
}): UserConfig {
return {
build: {
outDir: resolve(DIST_DIR, options.distDirSubfolder),
outDir: options.distDirSubfolder,
lib: {
entry: options.entryFilePath,
},
@@ -64,6 +70,11 @@ function getSharedElectronConfig(options: {
};
}
function resolvePathFromProjectRoot(pathSegment: string) {
function resolvePathFromProjectRoot(pathSegment: string): string {
return resolve(__dirname, pathSegment);
}
function resolveElectronDistSubdirectory(subDirectory: string): string {
const electronDistDir = resolvePathFromProjectRoot(distDirs.electronUnbundled);
return resolve(electronDistDir, subDirectory);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

10439
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,11 @@
{
"name": "privacy.sexy",
"version": "0.12.4",
"version": "0.12.7",
"private": true,
"slogan": "Now you have the choice",
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
"author": "undergroundwires",
"type": "module",
"main": "./dist-electron-unbundled/main/index.cjs",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
@@ -25,7 +24,7 @@
"electron:preview": "electron-vite preview",
"electron:prebuild": "electron-vite build",
"electron:build": "electron-builder",
"lint:eslint": "eslint . --ignore-path .gitignore",
"lint:eslint": "eslint . --max-warnings=0 --ignore-path .gitignore",
"lint:md": "markdownlint **/*.md --ignore node_modules",
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
@@ -36,71 +35,73 @@
"dependencies": {
"@floating-ui/vue": "^1.0.2",
"@juggle/resize-observer": "^3.4.0",
"ace-builds": "^1.23.4",
"ace-builds": "^1.30.0",
"cross-fetch": "^4.0.0",
"electron-log": "^4.4.8",
"electron-progressbar": "^2.1.0",
"electron-updater": "^6.1.4",
"file-saver": "^2.0.5",
"markdown-it": "^13.0.1",
"npm": "^9.8.1",
"vue": "^2.7.14"
"markdown-it": "^13.0.2",
"vue": "^3.3.7"
},
"devDependencies": {
"@modyfi/vite-plugin-yaml": "^1.0.4",
"@rushstack/eslint-patch": "^1.3.2",
"@types/ace": "^0.0.48",
"@rushstack/eslint-patch": "^1.5.1",
"@types/ace": "^0.0.49",
"@types/file-saver": "^2.0.5",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@vitejs/plugin-legacy": "^4.1.1",
"@vitejs/plugin-vue2": "^2.2.0",
"@vitejs/plugin-vue": "^4.4.0",
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/test-utils": "^1.3.6",
"autoprefixer": "^10.4.15",
"cypress": "^12.17.2",
"electron": "^25.3.2",
"electron-builder": "^24.6.3",
"@vue/test-utils": "^2.4.1",
"autoprefixer": "^10.4.16",
"cypress": "^13.3.1",
"electron": "^27.0.0",
"electron-builder": "^24.6.4",
"electron-devtools-installer": "^3.2.0",
"electron-icon-builder": "^2.0.1",
"electron-log": "^4.4.8",
"electron-updater": "^6.1.4",
"electron-vite": "^1.0.27",
"eslint": "^8.46.0",
"eslint-plugin-cypress": "^2.14.0",
"eslint-plugin-vue": "^9.6.0",
"eslint-plugin-vuejs-accessibility": "^1.2.0",
"icon-gen": "^3.0.1",
"electron-vite": "^1.0.28",
"eslint": "^8.51.0",
"eslint-plugin-cypress": "^2.15.1",
"eslint-plugin-vue": "^9.17.0",
"eslint-plugin-vuejs-accessibility": "^2.2.0",
"icon-gen": "^4.0.0",
"jsdom": "^22.1.0",
"markdownlint-cli": "^0.35.0",
"postcss": "^8.4.28",
"remark-cli": "^11.0.0",
"markdownlint-cli": "^0.37.0",
"postcss": "^8.4.31",
"remark-cli": "^12.0.0",
"remark-lint-no-dead-urls": "^1.1.0",
"remark-preset-lint-consistent": "^5.1.2",
"remark-validate-links": "^12.1.1",
"sass": "^1.64.1",
"start-server-and-test": "^2.0.0",
"remark-validate-links": "^13.0.0",
"sass": "^1.69.3",
"start-server-and-test": "^2.0.1",
"svgexport": "^0.4.2",
"terser": "^5.19.2",
"tslib": "~2.4.0",
"typescript": "~4.6.2",
"vite": "^4.4.9",
"vitest": "^0.34.2",
"vue-tsc": "^1.8.8",
"terser": "^5.21.0",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"vite": "^4.4.11",
"vitest": "^0.34.6",
"vue-tsc": "^1.8.19",
"yaml-lint": "^1.7.0"
},
"//devDependencies": {
"terser": "Used by @vitejs/plugin-legacy for minification",
"typescript": [
"Cannot upgrade to 5.X.X due to unmaintained @vue/cli-plugin-typescript, https://github.com/vuejs/vue-cli/issues/7401",
"Cannot upgrade to > 4.6.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252"
],
"tslib": "Cannot upgrade to > 2.4.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252",
"@typescript-eslint/eslint-plugin": "Cannot upgrade to 6.X.X due to @vue/eslint-config-typescript, https://github.com/vuejs/eslint-config-typescript/pull/60",
"@typescript-eslint/parser": "Cannot upgrade to 6.X.X due to @vue/eslint-config-typescript, https://github.com/vuejs/eslint-config-typescript/pull/60"
"@rushstack/eslint-patch": "Needed by @vue/eslint-config-typescript",
"@vue/eslint-config-typescript": "Cannot upgrade to 12.X.X due to @vue/eslint-config-airbnb-with-typescript, https://github.com/vuejs/eslint-config-airbnb/issues/58",
"@typescript-eslint/eslint-plugin": "Cannot upgrade to 6.X.X due to @vue/eslint-config-airbnb-with-typescript, https://github.com/vuejs/eslint-config-airbnb/issues/58",
"@typescript-eslint/parser": "Cannot upgrade to 6.X.X due to @vue/eslint-config-airbnb-with-typescript, https://github.com/vuejs/eslint-config-airbnb/issues/58"
},
"homepage": "https://privacy.sexy",
"repository": {
"type": "git",
"url": "https://github.com/undergroundwires/privacy.sexy.git"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"
},
"//optionalDependencies": {
"dmg-license": "Required by `electron-builder` for DMG builds on macOS, https://github.com/electron-userland/electron-builder/issues/6489, https://github.com/electron-userland/electron-builder/issues/6520"
}
}

View File

@@ -12,9 +12,6 @@ export class ApplicationFactory implements IApplicationFactory {
private readonly getter: AsyncLazy<IApplication>;
protected constructor(costlyGetter: ApplicationGetterType) {
if (!costlyGetter) {
throw new Error('missing getter');
}
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
}

View File

@@ -1,7 +1,5 @@
// Compares to Array<T> objects for equality, ignoring order
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
if (!array1) { throw new Error('missing first array'); }
if (!array2) { throw new Error('missing second array'); }
const sortedArray1 = sort(array1);
const sortedArray2 = sort(array2);
return sequenceEqual(sortedArray1, sortedArray2);
@@ -12,8 +10,6 @@ export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
// 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('missing first array'); }
if (!array2) { throw new Error('missing second array'); }
if (array1.length !== array2.length) {
return false;
}

View File

@@ -20,23 +20,30 @@ export abstract class CustomError extends Error {
}
}
export const Environment = {
interface ErrorPrototypeManipulation {
getSetPrototypeOf: () => (typeof Object.setPrototypeOf | undefined);
getCaptureStackTrace: () => (typeof Error.captureStackTrace | undefined);
}
export const PlatformErrorPrototypeManipulation: ErrorPrototypeManipulation = {
getSetPrototypeOf: () => Object.setPrototypeOf,
getCaptureStackTrace: () => Error.captureStackTrace,
};
function fixPrototype(target: Error, prototype: CustomError) {
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
const setPrototypeOf = Environment.getSetPrototypeOf();
if (!functionExists(setPrototypeOf)) {
// This is recommended by TypeScript guidelines.
// Source: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
// Snapshots: https://web.archive.org/web/20231111234849/https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget, https://archive.ph/tr7cX#support-for-newtarget
const setPrototypeOf = PlatformErrorPrototypeManipulation.getSetPrototypeOf();
if (!isFunction(setPrototypeOf)) {
return;
}
setPrototypeOf(target, prototype);
}
function ensureStackTrace(target: Error) {
const captureStackTrace = Environment.getCaptureStackTrace();
if (!functionExists(captureStackTrace)) {
const captureStackTrace = PlatformErrorPrototypeManipulation.getCaptureStackTrace();
if (!isFunction(captureStackTrace)) {
// captureStackTrace is only available on V8, if it's not available
// modern JS engines will usually generate a stack trace on error objects when they're thrown.
return;
@@ -44,7 +51,7 @@ function ensureStackTrace(target: Error) {
captureStackTrace(target, target.constructor);
}
function functionExists(func: unknown): boolean {
// Not doing truthy/falsy check i.e. if(func) as most values are truthy in JS for robustness
// eslint-disable-next-line @typescript-eslint/ban-types
function isFunction(func: unknown): func is Function {
return typeof func === 'function';
}

View File

@@ -54,9 +54,6 @@ export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
value: TEnumValue,
enumVariable: EnumVariable<T, TEnumValue>,
) {
if (value === undefined || value === null) {
throw new Error('absent enum value');
}
if (!(value in enumVariable)) {
throw new RangeError(`enum value "${value}" is out of range`);
}

View File

@@ -9,19 +9,16 @@ export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageF
public create(language: ScriptingLanguage): T {
assertInRange(language, ScriptingLanguage);
if (!this.getters.has(language)) {
const getter = this.getters.get(language);
if (!getter) {
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('missing getter');
}
if (this.getters.has(language)) {
throw new Error(`${ScriptingLanguage[language]} is already registered`);
}

View File

@@ -0,0 +1,27 @@
import { PlatformTimer } from './PlatformTimer';
import { TimeoutType, Timer } from './Timer';
export function batchedDebounce<T>(
callback: (batches: readonly T[]) => void,
waitInMs: number,
timer: Timer = PlatformTimer,
): (arg: T) => void {
let lastTimeoutId: TimeoutType | undefined;
let batches: Array<T> = [];
return (arg: T) => {
batches.push(arg);
const later = () => {
callback(batches);
batches = [];
lastTimeoutId = undefined;
};
if (lastTimeoutId !== undefined) {
timer.clearTimeout(lastTimeoutId);
}
lastTimeoutId = timer.setTimeout(later, waitInMs);
};
}

View File

@@ -0,0 +1,7 @@
import { Timer } from './Timer';
export const PlatformTimer: Timer = {
setTimeout: (callback, ms) => setTimeout(callback, ms),
clearTimeout: (timeoutId) => clearTimeout(timeoutId),
dateNow: () => Date.now(),
};

View File

@@ -1,47 +1,29 @@
import { Timer, TimeoutType } from './Timer';
import { PlatformTimer } from './PlatformTimer';
export type CallbackType = (..._: unknown[]) => void;
export function throttle(
callback: CallbackType,
waitInMs: number,
timer: ITimer = NodeTimer,
timer: Timer = PlatformTimer,
): CallbackType {
const throttler = new Throttler(timer, waitInMs, callback);
return (...args: unknown[]) => throttler.invoke(...args);
}
// Allows aligning with both NodeJs (NodeJs.Timeout) and Window type (number)
export type TimeoutType = ReturnType<typeof setTimeout>;
export interface ITimer {
setTimeout: (callback: () => void, ms: number) => TimeoutType;
clearTimeout: (timeoutId: TimeoutType) => void;
dateNow(): number;
}
const NodeTimer: ITimer = {
setTimeout: (callback, ms) => setTimeout(callback, ms),
clearTimeout: (timeoutId) => clearTimeout(timeoutId),
dateNow: () => Date.now(),
};
interface IThrottler {
invoke: CallbackType;
}
class Throttler implements IThrottler {
private queuedExecutionId: TimeoutType;
class Throttler {
private queuedExecutionId: TimeoutType | undefined;
private previouslyRun: number;
constructor(
private readonly timer: ITimer,
private readonly timer: Timer,
private readonly waitInMs: number,
private readonly callback: CallbackType,
) {
if (!timer) { throw new Error('missing timer'); }
if (!waitInMs) { throw new Error('missing delay'); }
if (waitInMs < 0) { throw new Error('negative delay'); }
if (!callback) { throw new Error('missing callback'); }
}
public invoke(...args: unknown[]): void {

View File

@@ -0,0 +1,8 @@
// Allows aligning with both NodeJs (NodeJs.Timeout) and Window type (number)
export type TimeoutType = ReturnType<typeof setTimeout>;
export interface Timer {
setTimeout: (callback: () => void, ms: number) => TimeoutType;
clearTimeout: (timeoutId: TimeoutType) => void;
dateNow(): number;
}

View File

@@ -26,7 +26,6 @@ export class ApplicationContext implements IApplicationContext {
public readonly app: IApplication,
initialContext: OperatingSystem,
) {
validateApp(app);
this.states = initializeStates(app);
this.changeContext(initialContext);
}
@@ -36,10 +35,8 @@ export class ApplicationContext implements IApplicationContext {
if (this.currentOs === os) {
return;
}
this.collection = this.app.getCollection(os);
if (!this.collection) {
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
}
const collection = this.app.getCollection(os);
this.collection = collection;
const event: IApplicationContextChangedEvent = {
newState: this.states[os],
oldState: this.states[this.currentOs],
@@ -49,12 +46,6 @@ export class ApplicationContext implements IApplicationContext {
}
}
function validateApp(app: IApplication) {
if (!app) {
throw new Error('missing app');
}
}
function initializeStates(app: IApplication): StateMachine {
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
for (const collection of app.collections) {

View File

@@ -10,18 +10,23 @@ export async function buildContext(
factory: IApplicationFactory = ApplicationFactory.Current,
environment = RuntimeEnvironment.CurrentEnvironment,
): Promise<IApplicationContext> {
if (!factory) { throw new Error('missing factory'); }
if (!environment) { throw new Error('missing environment'); }
const app = await factory.getApp();
const os = getInitialOs(app, environment.os);
return new ApplicationContext(app, os);
}
function getInitialOs(app: IApplication, currentOs: OperatingSystem): OperatingSystem {
function getInitialOs(
app: IApplication,
currentOs: OperatingSystem | undefined,
): OperatingSystem {
const supportedOsList = app.getSupportedOsList();
if (supportedOsList.includes(currentOs)) {
if (currentOs !== undefined && supportedOsList.includes(currentOs)) {
return currentOs;
}
return getMostSupportedOs(supportedOsList, app);
}
function getMostSupportedOs(supportedOsList: OperatingSystem[], app: IApplication) {
supportedOsList.sort((os1, os2) => {
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
return getPriority(os2) - getPriority(os1);

View File

@@ -4,23 +4,48 @@ import { UserFilter } from './Filter/UserFilter';
import { IUserFilter } from './Filter/IUserFilter';
import { ApplicationCode } from './Code/ApplicationCode';
import { UserSelection } from './Selection/UserSelection';
import { IUserSelection } from './Selection/IUserSelection';
import { ICategoryCollectionState } from './ICategoryCollectionState';
import { IApplicationCode } from './Code/IApplicationCode';
import { UserSelectionFacade } from './Selection/UserSelectionFacade';
export class CategoryCollectionState implements ICategoryCollectionState {
public readonly os: OperatingSystem;
public readonly code: IApplicationCode;
public readonly selection: IUserSelection;
public readonly selection: UserSelection;
public readonly filter: IUserFilter;
public constructor(readonly collection: ICategoryCollection) {
this.selection = new UserSelection(collection, []);
this.code = new ApplicationCode(this.selection, collection.scripting);
this.filter = new UserFilter(collection);
public constructor(
public readonly collection: ICategoryCollection,
selectionFactory = DefaultSelectionFactory,
codeFactory = DefaultCodeFactory,
filterFactory = DefaultFilterFactory,
) {
this.selection = selectionFactory(collection, []);
this.code = codeFactory(this.selection.scripts, collection.scripting);
this.filter = filterFactory(collection);
this.os = collection.os;
}
}
export type CodeFactory = (
...params: ConstructorParameters<typeof ApplicationCode>
) => IApplicationCode;
const DefaultCodeFactory: CodeFactory = (...params) => new ApplicationCode(...params);
export type SelectionFactory = (
...params: ConstructorParameters<typeof UserSelectionFacade>
) => UserSelection;
const DefaultSelectionFactory: SelectionFactory = (
...params
) => new UserSelectionFacade(...params);
export type FilterFactory = (
...params: ConstructorParameters<typeof UserFilter>
) => IUserFilter;
const DefaultFilterFactory: FilterFactory = (...params) => new UserFilter(...params);

View File

@@ -1,7 +1,7 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IReadOnlyUserSelection } from '@/application/Context/State/Selection/IUserSelection';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { ReadonlyScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection';
import { CodeChangedEvent } from './Event/CodeChangedEvent';
import { CodePosition } from './Position/CodePosition';
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
@@ -17,15 +17,12 @@ export class ApplicationCode implements IApplicationCode {
private scriptPositions = new Map<SelectedScript, CodePosition>();
constructor(
userSelection: IReadOnlyUserSelection,
selection: ReadonlyScriptSelection,
private readonly scriptingDefinition: IScriptingDefinition,
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
) {
if (!userSelection) { throw new Error('missing userSelection'); }
if (!scriptingDefinition) { throw new Error('missing scriptingDefinition'); }
if (!generator) { throw new Error('missing generator'); }
this.setCode(userSelection.selectedScripts);
userSelection.changed.on((scripts) => {
this.setCode(selection.selectedScripts);
selection.changed.on((scripts) => {
this.setCode(scripts);
});
}

View File

@@ -1,6 +1,6 @@
import { IScript } from '@/domain/IScript';
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import { SelectedScript } from '../../Selection/SelectedScript';
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { ICodeChangedEvent } from './ICodeChangedEvent';
export class CodeChangedEvent implements ICodeChangedEvent {
@@ -36,7 +36,18 @@ export class CodeChangedEvent implements ICodeChangedEvent {
}
public getScriptPositionInCode(script: IScript): ICodePosition {
return this.scripts.get(script);
return this.getPositionById(script.id);
}
private getPositionById(scriptId: string): ICodePosition {
const position = [...this.scripts.entries()]
.filter(([s]) => s.id === scriptId)
.map(([, pos]) => pos)
.at(0);
if (!position) {
throw new Error('Unknown script: Position could not be found for the script');
}
return position;
}
}

View File

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

View File

@@ -16,7 +16,9 @@ export abstract class CodeBuilder implements ICodeBuilder {
return this;
}
const lines = code.match(/[^\r\n]+/g);
this.lines.push(...lines);
if (lines) {
this.lines.push(...lines);
}
return this;
}

View File

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

View File

@@ -1,9 +1,10 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { IUserScript } from './IUserScript';
export interface IUserScriptGenerator {
buildCode(
selectedScripts: ReadonlyArray<SelectedScript>,
scriptingDefinition: IScriptingDefinition): IUserScript;
scriptingDefinition: IScriptingDefinition,
): IUserScript;
}

View File

@@ -1,6 +1,6 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { CodePosition } from '../Position/CodePosition';
import { IUserScriptGenerator } from './IUserScriptGenerator';
import { IUserScript } from './IUserScript';
@@ -17,8 +17,6 @@ export class UserScriptGenerator implements IUserScriptGenerator {
selectedScripts: ReadonlyArray<SelectedScript>,
scriptingDefinition: IScriptingDefinition,
): IUserScript {
if (!selectedScripts) { throw new Error('missing scripts'); }
if (!scriptingDefinition) { throw new Error('missing definition'); }
if (!selectedScripts.length) {
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
}
@@ -68,8 +66,19 @@ function appendSelection(
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
const { script } = selection;
const name = selection.revert ? `${script.name} (revert)` : script.name;
const scriptCode = selection.revert ? script.code.revert : script.code.execute;
const scriptCode = getSelectedCode(selection);
return builder
.appendLine()
.appendFunction(name, scriptCode);
}
function getSelectedCode(selection: SelectedScript): string {
const { code } = selection.script;
if (!selection.revert) {
return code.execute;
}
if (!code.revert) {
throw new Error('Reverted script lacks revert code.');
}
return code.revert;
}

View File

@@ -1,37 +1,37 @@
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
import { FilterActionType } from './FilterActionType';
import { IFilterChangeDetails, IFilterChangeDetailsVisitor } from './IFilterChangeDetails';
import {
IFilterChangeDetails, IFilterChangeDetailsVisitor,
ApplyFilterAction, ClearFilterAction,
} from './IFilterChangeDetails';
export class FilterChange implements IFilterChangeDetails {
public static forApply(filter: IFilterResult) {
if (!filter) {
throw new Error('missing filter');
}
return new FilterChange(FilterActionType.Apply, filter);
public static forApply(
filter: IFilterResult,
): IFilterChangeDetails {
return new FilterChange({ type: FilterActionType.Apply, filter });
}
public static forClear() {
return new FilterChange(FilterActionType.Clear);
public static forClear(): IFilterChangeDetails {
return new FilterChange({ type: FilterActionType.Clear });
}
private constructor(
public readonly actionType: FilterActionType,
public readonly filter?: IFilterResult,
) { }
private constructor(public readonly action: ApplyFilterAction | ClearFilterAction) { }
public visit(visitor: IFilterChangeDetailsVisitor): void {
if (!visitor) {
throw new Error('missing visitor');
}
switch (this.actionType) {
switch (this.action.type) {
case FilterActionType.Apply:
visitor.onApply(this.filter);
if (visitor.onApply) {
visitor.onApply(this.action.filter);
}
break;
case FilterActionType.Clear:
visitor.onClear();
if (visitor.onClear) {
visitor.onClear();
}
break;
default:
throw new Error(`Unknown action type: ${this.actionType}`);
throw new Error(`Unknown action: ${this.action}`);
}
}
}

View File

@@ -2,13 +2,22 @@ import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'
import { FilterActionType } from './FilterActionType';
export interface IFilterChangeDetails {
readonly actionType: FilterActionType;
readonly filter?: IFilterResult;
readonly action: FilterAction;
visit(visitor: IFilterChangeDetailsVisitor): void;
}
export interface IFilterChangeDetailsVisitor {
onClear(): void;
onApply(filter: IFilterResult): void;
readonly onClear?: () => void;
readonly onApply?: (filter: IFilterResult) => void;
}
export type ApplyFilterAction = {
readonly type: FilterActionType.Apply,
readonly filter: IFilterResult;
};
export type ClearFilterAction = {
readonly type: FilterActionType.Clear,
};
export type FilterAction = ApplyFilterAction | ClearFilterAction;

View File

@@ -9,8 +9,6 @@ export class FilterResult implements IFilterResult {
public readonly query: string,
) {
if (!query) { throw new Error('Query is empty or undefined'); }
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
if (!categoryMatches) { throw new Error('Category matches is undefined'); }
}
public hasAnyMatches(): boolean {

View File

@@ -1,18 +1,18 @@
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter';
import { IReadOnlyUserSelection, IUserSelection } from './Selection/IUserSelection';
import { ReadonlyUserSelection, UserSelection } from './Selection/UserSelection';
import { IApplicationCode } from './Code/IApplicationCode';
export interface IReadOnlyCategoryCollectionState {
readonly code: IApplicationCode;
readonly os: OperatingSystem;
readonly filter: IReadOnlyUserFilter;
readonly selection: IReadOnlyUserSelection;
readonly selection: ReadonlyUserSelection;
readonly collection: ICategoryCollection;
}
export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState {
readonly filter: IUserFilter;
readonly selection: IUserSelection;
readonly selection: UserSelection;
}

View File

@@ -0,0 +1,11 @@
import { ICategory } from '@/domain/ICategory';
import { CategorySelectionChangeCommand } from './CategorySelectionChange';
export interface ReadonlyCategorySelection {
areAllScriptsSelected(category: ICategory): boolean;
isAnyScriptSelected(category: ICategory): boolean;
}
export interface CategorySelection extends ReadonlyCategorySelection {
processChanges(action: CategorySelectionChangeCommand): void;
}

View File

@@ -0,0 +1,15 @@
type CategorySelectionStatus = {
readonly isSelected: true;
readonly isReverted: boolean;
} | {
readonly isSelected: false;
};
export interface CategorySelectionChange {
readonly categoryId: number;
readonly newStatus: CategorySelectionStatus;
}
export interface CategorySelectionChangeCommand {
readonly changes: readonly CategorySelectionChange[];
}

View File

@@ -0,0 +1,60 @@
import { ICategory } from '@/domain/ICategory';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { ScriptSelection } from '../Script/ScriptSelection';
import { ScriptSelectionChange } from '../Script/ScriptSelectionChange';
import { CategorySelection } from './CategorySelection';
import { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
export class ScriptToCategorySelectionMapper implements CategorySelection {
constructor(
private readonly scriptSelection: ScriptSelection,
private readonly collection: ICategoryCollection,
) {
}
public areAllScriptsSelected(category: ICategory): boolean {
const { selectedScripts } = this.scriptSelection;
if (selectedScripts.length === 0) {
return false;
}
const scripts = category.getAllScriptsRecursively();
if (selectedScripts.length < scripts.length) {
return false;
}
return scripts.every(
(script) => selectedScripts.some((selected) => selected.id === script.id),
);
}
public isAnyScriptSelected(category: ICategory): boolean {
const { selectedScripts } = this.scriptSelection;
if (selectedScripts.length === 0) {
return false;
}
return selectedScripts.some((s) => category.includes(s.script));
}
public processChanges(action: CategorySelectionChangeCommand): void {
const scriptChanges = action.changes.reduce((changes, change) => {
changes.push(...this.collectScriptChanges(change));
return changes;
}, new Array<ScriptSelectionChange>());
this.scriptSelection.processChanges({
changes: scriptChanges,
});
}
private collectScriptChanges(change: CategorySelectionChange): ScriptSelectionChange[] {
const category = this.collection.getCategory(change.categoryId);
const scripts = category.getAllScriptsRecursively();
const scriptsChangesInCategory = scripts
.map((script): ScriptSelectionChange => ({
scriptId: script.id,
newStatus: {
...change.newStatus,
},
}));
return scriptsChangesInCategory;
}
}

View File

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

View File

@@ -0,0 +1,171 @@
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import { IScript } from '@/domain/IScript';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
import { ScriptSelection } from './ScriptSelection';
import { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange';
import { SelectedScript } from './SelectedScript';
import { UserSelectedScript } from './UserSelectedScript';
const DEBOUNCE_DELAY_IN_MS = 100;
export type DebounceFunction = typeof batchedDebounce<ScriptSelectionChangeCommand>;
export class DebouncedScriptSelection implements ScriptSelection {
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
private readonly scripts: Repository<string, SelectedScript>;
public readonly processChanges: ScriptSelection['processChanges'];
constructor(
private readonly collection: ICategoryCollection,
selectedScripts: ReadonlyArray<SelectedScript>,
debounce: DebounceFunction = batchedDebounce,
) {
this.scripts = new InMemoryRepository<string, SelectedScript>();
for (const script of selectedScripts) {
this.scripts.addItem(script);
}
this.processChanges = debounce(
(batchedRequests: readonly ScriptSelectionChangeCommand[]) => {
const consolidatedChanges = batchedRequests.flatMap((request) => request.changes);
this.processScriptChanges(consolidatedChanges);
},
DEBOUNCE_DELAY_IN_MS,
);
}
public isSelected(scriptId: string): boolean {
return this.scripts.exists(scriptId);
}
public get selectedScripts(): readonly SelectedScript[] {
return this.scripts.getItems();
}
public selectAll(): void {
const scriptsToSelect = this.collection
.getAllScripts()
.filter((script) => !this.scripts.exists(script.id))
.map((script) => new UserSelectedScript(script, false));
if (scriptsToSelect.length === 0) {
return;
}
this.processChanges({
changes: scriptsToSelect.map((script): ScriptSelectionChange => ({
scriptId: script.id,
newStatus: {
isSelected: true,
isReverted: false,
},
})),
});
}
public deselectAll(): void {
if (this.scripts.length === 0) {
return;
}
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
this.processChanges({
changes: selectedScriptIds.map((scriptId): ScriptSelectionChange => ({
scriptId,
newStatus: {
isSelected: false,
},
})),
});
}
public selectOnly(scripts: readonly IScript[]): void {
if (scripts.length === 0) {
throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.');
}
this.processChanges({
changes: [
...getScriptIdsToBeDeselected(this.scripts, scripts)
.map((scriptId): ScriptSelectionChange => ({
scriptId,
newStatus: {
isSelected: false,
},
})),
...getScriptIdsToBeSelected(this.scripts, scripts)
.map((scriptId): ScriptSelectionChange => ({
scriptId,
newStatus: {
isSelected: true,
isReverted: false,
},
})),
],
});
}
private processScriptChanges(changes: readonly ScriptSelectionChange[]): void {
let totalChanged = 0;
for (const change of changes) {
totalChanged += this.applyChange(change);
}
if (totalChanged > 0) {
this.changed.notify(this.scripts.getItems());
}
}
private applyChange(change: ScriptSelectionChange): number {
const script = this.collection.getScript(change.scriptId);
if (change.newStatus.isSelected) {
return this.addOrUpdateScript(script.id, change.newStatus.isReverted);
}
return this.removeScript(script.id);
}
private addOrUpdateScript(scriptId: string, revert: boolean): number {
const script = this.collection.getScript(scriptId);
const selectedScript = new UserSelectedScript(script, revert);
if (!this.scripts.exists(selectedScript.id)) {
this.scripts.addItem(selectedScript);
return 1;
}
const existingSelectedScript = this.scripts.getById(selectedScript.id);
if (equals(selectedScript, existingSelectedScript)) {
return 0;
}
this.scripts.addOrUpdateItem(selectedScript);
return 1;
}
private removeScript(scriptId: string): number {
if (!this.scripts.exists(scriptId)) {
return 0;
}
this.scripts.removeItem(scriptId);
return 1;
}
}
function getScriptIdsToBeSelected(
existingItems: ReadonlyRepository<string, SelectedScript>,
desiredScripts: readonly IScript[],
): string[] {
return desiredScripts
.filter((script) => !existingItems.exists(script.id))
.map((script) => script.id);
}
function getScriptIdsToBeDeselected(
existingItems: ReadonlyRepository<string, SelectedScript>,
desiredScripts: readonly IScript[],
): string[] {
return existingItems
.getItems()
.filter((existing) => !desiredScripts.some((script) => existing.id === script.id))
.map((script) => script.id);
}
function equals(a: SelectedScript, b: SelectedScript): boolean {
return a.script.equals(b.script.id) && a.revert === b.revert;
}

View File

@@ -0,0 +1,17 @@
import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { IScript } from '@/domain/IScript';
import { SelectedScript } from './SelectedScript';
import { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
export interface ReadonlyScriptSelection {
readonly changed: IEventSource<readonly SelectedScript[]>;
readonly selectedScripts: readonly SelectedScript[];
isSelected(scriptId: string): boolean;
}
export interface ScriptSelection extends ReadonlyScriptSelection {
selectOnly(scripts: readonly IScript[]): void;
selectAll(): void;
deselectAll(): void;
processChanges(action: ScriptSelectionChangeCommand): void;
}

View File

@@ -0,0 +1,15 @@
export type ScriptSelectionStatus = {
readonly isSelected: true;
readonly isReverted: boolean;
} | {
readonly isSelected: false;
};
export interface ScriptSelectionChange {
readonly scriptId: string;
readonly newStatus: ScriptSelectionStatus;
}
export interface ScriptSelectionChangeCommand {
readonly changes: ReadonlyArray<ScriptSelectionChange>;
}

View File

@@ -0,0 +1,9 @@
import { IEntity } from '@/infrastructure/Entity/IEntity';
import { IScript } from '@/domain/IScript';
type ScriptId = IScript['id'];
export interface SelectedScript extends IEntity<ScriptId> {
readonly script: IScript;
readonly revert: boolean;
}

View File

@@ -1,14 +1,17 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import { IScript } from '@/domain/IScript';
import { SelectedScript } from './SelectedScript';
export class SelectedScript extends BaseEntity<string> {
type SelectedScriptId = SelectedScript['id'];
export class UserSelectedScript extends BaseEntity<SelectedScriptId> {
constructor(
public readonly script: IScript,
public readonly revert: boolean,
) {
super(script.id);
if (revert && !script.canRevert()) {
throw new Error('cannot revert an irreversible script');
throw new Error(`The script with ID '${script.id}' is not reversible and cannot be reverted.`);
}
}
}

View File

@@ -1,167 +1,12 @@
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import { IScript } from '@/domain/IScript';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { IRepository } from '@/infrastructure/Repository/IRepository';
import { ICategory } from '@/domain/ICategory';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { IUserSelection } from './IUserSelection';
import { SelectedScript } from './SelectedScript';
import { CategorySelection, ReadonlyCategorySelection } from './Category/CategorySelection';
import { ReadonlyScriptSelection, ScriptSelection } from './Script/ScriptSelection';
export class UserSelection implements IUserSelection {
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
private readonly scripts: IRepository<string, SelectedScript>;
constructor(
private readonly collection: ICategoryCollection,
selectedScripts: ReadonlyArray<SelectedScript>,
) {
this.scripts = new InMemoryRepository<string, SelectedScript>();
for (const script of selectedScripts) {
this.scripts.addItem(script);
}
}
public areAllSelected(category: ICategory): boolean {
if (this.selectedScripts.length === 0) {
return false;
}
const scripts = category.getAllScriptsRecursively();
if (this.selectedScripts.length < scripts.length) {
return false;
}
return scripts.every(
(script) => this.selectedScripts.some((selected) => selected.id === script.id),
);
}
public isAnySelected(category: ICategory): boolean {
if (this.selectedScripts.length === 0) {
return false;
}
return this.selectedScripts.some((s) => category.includes(s.script));
}
public removeAllInCategory(categoryId: number): void {
const category = this.collection.findCategory(categoryId);
const scriptsToRemove = category.getAllScriptsRecursively()
.filter((script) => this.scripts.exists(script.id));
if (!scriptsToRemove.length) {
return;
}
for (const script of scriptsToRemove) {
this.scripts.removeItem(script.id);
}
this.changed.notify(this.scripts.getItems());
}
public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
const scriptsToAddOrUpdate = this.collection
.findCategory(categoryId)
.getAllScriptsRecursively()
.filter(
(script) => !this.scripts.exists(script.id)
|| this.scripts.getById(script.id).revert !== revert,
)
.map((script) => new SelectedScript(script, revert));
if (!scriptsToAddOrUpdate.length) {
return;
}
for (const script of scriptsToAddOrUpdate) {
this.scripts.addOrUpdateItem(script);
}
this.changed.notify(this.scripts.getItems());
}
public addSelectedScript(scriptId: string, revert: boolean): void {
const script = this.collection.findScript(scriptId);
if (!script) {
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
}
const selectedScript = new SelectedScript(script, revert);
this.scripts.addItem(selectedScript);
this.changed.notify(this.scripts.getItems());
}
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
const script = this.collection.findScript(scriptId);
const selectedScript = new SelectedScript(script, revert);
this.scripts.addOrUpdateItem(selectedScript);
this.changed.notify(this.scripts.getItems());
}
public removeSelectedScript(scriptId: string): void {
this.scripts.removeItem(scriptId);
this.changed.notify(this.scripts.getItems());
}
public isSelected(scriptId: string): boolean {
return this.scripts.exists(scriptId);
}
/** Get users scripts based on his/her selections */
public get selectedScripts(): ReadonlyArray<SelectedScript> {
return this.scripts.getItems();
}
public selectAll(): void {
const scriptsToSelect = this.collection
.getAllScripts()
.filter((script) => !this.scripts.exists(script.id))
.map((script) => new SelectedScript(script, false));
if (scriptsToSelect.length === 0) {
return;
}
for (const script of scriptsToSelect) {
this.scripts.addItem(script);
}
this.changed.notify(this.scripts.getItems());
}
public deselectAll(): void {
if (this.scripts.length === 0) {
return;
}
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
for (const scriptId of selectedScriptIds) {
this.scripts.removeItem(scriptId);
}
this.changed.notify([]);
}
public selectOnly(scripts: readonly IScript[]): void {
if (!scripts || scripts.length === 0) {
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
}
let totalChanged = 0;
totalChanged += this.unselectMissingWithoutNotifying(scripts);
totalChanged += this.selectNewWithoutNotifying(scripts);
if (totalChanged > 0) {
this.changed.notify(this.scripts.getItems());
}
}
private unselectMissingWithoutNotifying(scripts: readonly IScript[]): number {
if (this.scripts.length === 0 || scripts.length === 0) {
return 0;
}
const existingItems = this.scripts.getItems();
const missingIds = existingItems
.filter((existing) => !scripts.some((script) => existing.id === script.id))
.map((script) => script.id);
for (const id of missingIds) {
this.scripts.removeItem(id);
}
return missingIds.length;
}
private selectNewWithoutNotifying(scripts: readonly IScript[]): number {
const unselectedScripts = scripts
.filter((script) => !this.scripts.exists(script.id))
.map((script) => new SelectedScript(script, false));
for (const newScript of unselectedScripts) {
this.scripts.addItem(newScript);
}
return unselectedScripts.length;
}
export interface ReadonlyUserSelection {
readonly categories: ReadonlyCategorySelection;
readonly scripts: ReadonlyScriptSelection;
}
export interface UserSelection extends ReadonlyUserSelection {
readonly categories: CategorySelection;
readonly scripts: ScriptSelection;
}

View File

@@ -0,0 +1,39 @@
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { CategorySelection } from './Category/CategorySelection';
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
import { ScriptSelection } from './Script/ScriptSelection';
import { UserSelection } from './UserSelection';
import { SelectedScript } from './Script/SelectedScript';
export class UserSelectionFacade implements UserSelection {
public readonly categories: CategorySelection;
public readonly scripts: ScriptSelection;
constructor(
collection: ICategoryCollection,
selectedScripts: readonly SelectedScript[],
scriptsFactory = DefaultScriptsFactory,
categoriesFactory = DefaultCategoriesFactory,
) {
this.scripts = scriptsFactory(collection, selectedScripts);
this.categories = categoriesFactory(this.scripts, collection);
}
}
export type ScriptsFactory = (
...params: ConstructorParameters<typeof DebouncedScriptSelection>
) => ScriptSelection;
const DefaultScriptsFactory: ScriptsFactory = (
...params
) => new DebouncedScriptSelection(...params);
export type CategoriesFactory = (
...params: ConstructorParameters<typeof ScriptToCategorySelectionMapper>
) => CategorySelection;
const DefaultCategoriesFactory: CategoriesFactory = (
...params
) => new ScriptToCategorySelectionMapper(...params);

View File

@@ -32,10 +32,7 @@ const PreParsedCollections: readonly CollectionData [] = [
];
function validateCollectionsData(collections: readonly CollectionData[]) {
if (!collections?.length) {
if (!collections.length) {
throw new Error('missing collections');
}
if (collections.some((collection) => !collection)) {
throw new Error('missing collection provided');
}
}

View File

@@ -28,10 +28,7 @@ export function parseCategoryCollection(
}
function validate(content: CollectionData): void {
if (!content) {
throw new Error('missing content');
}
if (!content.actions || content.actions.length <= 0) {
if (!content.actions.length) {
throw new Error('content does not define any action');
}
}

View File

@@ -1,5 +1,5 @@
import type {
CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder,
CategoryData, ScriptData, CategoryOrScriptData,
} from '@/application/collections/';
import { Script } from '@/domain/Script';
import { Category } from '@/domain/Category';
@@ -16,7 +16,6 @@ export function parseCategory(
context: ICategoryCollectionParseContext,
factory: CategoryFactoryType = CategoryFactory,
): Category {
if (!context) { throw new Error('missing context'); }
return parseCategoryRecursively({
categoryData: category,
context,
@@ -30,8 +29,8 @@ interface ICategoryParseContext {
readonly factory: CategoryFactoryType,
readonly parentCategory?: CategoryData,
}
// eslint-disable-next-line consistent-return
function parseCategoryRecursively(context: ICategoryParseContext): Category {
function parseCategoryRecursively(context: ICategoryParseContext): Category | never {
ensureValidCategory(context.categoryData, context.parentCategory);
const children: ICategoryChildren = {
subCategories: new Array<Category>(),
@@ -55,7 +54,7 @@ function parseCategoryRecursively(context: ICategoryParseContext): Category {
/* scripts: */ children.subScripts,
);
} catch (err) {
new NodeValidator({
return new NodeValidator({
type: NodeType.Category,
selfNode: context.categoryData,
parentNode: context.parentCategory,
@@ -72,7 +71,7 @@ function ensureValidCategory(category: CategoryData, parentCategory?: CategoryDa
.assertDefined(category)
.assertValidName(category.category)
.assert(
() => category.children && category.children.length > 0,
() => category.children.length > 0,
`"${category.category}" has no children.`,
);
}
@@ -94,14 +93,14 @@ function parseNode(context: INodeParseContext) {
validator.assertDefined(context.nodeData);
if (isCategory(context.nodeData)) {
const subCategory = parseCategoryRecursively({
categoryData: context.nodeData as CategoryData,
categoryData: context.nodeData,
context: context.context,
factory: context.factory,
parentCategory: context.parent,
});
context.children.subCategories.push(subCategory);
} else if (isScript(context.nodeData)) {
const script = parseScript(context.nodeData as ScriptData, context.context);
const script = parseScript(context.nodeData, context.context);
context.children.subScripts.push(script);
} else {
validator.throw('Node is neither a category or a script.');
@@ -109,19 +108,18 @@ function parseNode(context: INodeParseContext) {
}
function isScript(data: CategoryOrScriptData): data is ScriptData {
const holder = (data as InstructionHolder);
return hasCode(holder) || hasCall(holder);
return hasCode(data) || hasCall(data);
}
function isCategory(data: CategoryOrScriptData): data is CategoryData {
return hasProperty(data, 'category');
}
function hasCode(data: InstructionHolder): boolean {
function hasCode(data: unknown): boolean {
return hasProperty(data, 'code');
}
function hasCall(data: InstructionHolder) {
function hasCall(data: unknown) {
return hasProperty(data, 'call');
}

View File

@@ -1,9 +1,6 @@
import type { DocumentableData, DocumentationData } from '@/application/collections/';
export function parseDocs(documentable: DocumentableData): readonly string[] {
if (!documentable) {
throw new Error('missing documentable');
}
const { docs } = documentable;
if (!docs) {
return [];

View File

@@ -32,7 +32,7 @@ export class NodeValidator {
return this;
}
public throw(errorMessage: string) {
public throw(errorMessage: string): never {
throw new NodeDataError(errorMessage, this.context);
}
}

View File

@@ -17,8 +17,7 @@ export class CategoryCollectionParseContext implements ICategoryCollectionParseC
scripting: IScriptingDefinition,
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
) {
if (!scripting) { throw new Error('missing scripting'); }
this.syntax = syntaxFactory.create(scripting.language);
this.compiler = new ScriptCompiler(functionsData, this.syntax);
this.compiler = new ScriptCompiler(functionsData ?? [], this.syntax);
}
}

View File

@@ -15,19 +15,10 @@ export class Expression implements IExpression {
public readonly evaluator: ExpressionEvaluator,
parameters?: IReadOnlyFunctionParameterCollection,
) {
if (!position) {
throw new Error('missing position');
}
if (!evaluator) {
throw new Error('missing evaluator');
}
this.parameters = parameters ?? new FunctionParameterCollection();
}
public evaluate(context: IExpressionEvaluationContext): string {
if (!context) {
throw new Error('missing context');
}
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
const args = filterUnusedArguments(this.parameters, context.args);
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);

View File

@@ -12,8 +12,5 @@ export class ExpressionEvaluationContext implements IExpressionEvaluationContext
public readonly args: IReadOnlyFunctionCallArgumentCollection,
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
) {
if (!args) {
throw new Error('missing args, send empty collection instead.');
}
}
}

View File

@@ -0,0 +1,16 @@
import { ExpressionPosition } from './ExpressionPosition';
export function createPositionFromRegexFullMatch(
match: RegExpMatchArray,
): ExpressionPosition {
const startPos = match.index;
if (startPos === undefined) {
throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`);
}
const fullMatch = match[0];
if (!fullMatch.length) {
throw new Error(`Regex match is empty: ${JSON.stringify(match)}`);
}
const endPos = startPos + fullMatch.length;
return new ExpressionPosition(startPos, endPos);
}

View File

@@ -11,14 +11,11 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
) { }
public compileExpressions(
code: string | undefined,
code: string,
args: IReadOnlyFunctionCallArgumentCollection,
): string {
if (!args) {
throw new Error('missing args, send empty collection instead.');
}
if (!code) {
return code;
return '';
}
const context = new ExpressionEvaluationContext(args);
const compiledCode = compileRecursively(code, context, this.extractor);
@@ -145,7 +142,7 @@ function ensureParamsUsedInCodeHasArgsProvided(
providedArgs: IReadOnlyFunctionCallArgumentCollection,
): void {
const usedParameterNames = extractRequiredParameterNames(expressions);
if (!usedParameterNames?.length) {
if (!usedParameterNames.length) {
return;
}
const notProvidedParameters = usedParameterNames

View File

@@ -2,6 +2,7 @@ import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argume
export interface IExpressionsCompiler {
compileExpressions(
code: string | undefined,
args: IReadOnlyFunctionCallArgumentCollection): string;
code: string,
args: IReadOnlyFunctionCallArgumentCollection,
): string;
}

View File

@@ -10,12 +10,9 @@ const Parsers = [
export class CompositeExpressionParser implements IExpressionParser {
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
if (!leafs) {
if (!leafs.length) {
throw new Error('missing leafs');
}
if (leafs.some((leaf) => !leaf)) {
throw new Error('missing leaf');
}
}
public findExpressions(code: string): IExpression[] {

View File

@@ -14,45 +14,44 @@ export class ExpressionRegexBuilder {
.addRawRegex('\\s+');
}
public matchPipeline() {
public captureOptionalPipeline() {
return this
.expectZeroOrMoreWhitespaces()
.addRawRegex('(\\|\\s*.+?)?');
.addRawRegex('((?:\\|\\s*\\b[a-zA-Z]+\\b\\s*)*)');
}
public matchUntilFirstWhitespace() {
public captureUntilWhitespaceOrPipe() {
return this
.addRawRegex('([^|\\s]+)');
}
public matchMultilineAnythingExceptSurroundingWhitespaces() {
public captureMultilineAnythingExceptSurroundingWhitespaces() {
return this
.expectZeroOrMoreWhitespaces()
.addRawRegex('([\\S\\s]+?)')
.expectZeroOrMoreWhitespaces();
.expectOptionalWhitespaces()
.addRawRegex('([\\s\\S]*\\S)')
.expectOptionalWhitespaces();
}
public expectExpressionStart() {
return this
.expectCharacters('{{')
.expectZeroOrMoreWhitespaces();
.expectOptionalWhitespaces();
}
public expectExpressionEnd() {
return this
.expectZeroOrMoreWhitespaces()
.expectOptionalWhitespaces()
.expectCharacters('}}');
}
public expectOptionalWhitespaces() {
return this
.addRawRegex('\\s*');
}
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,9 +1,9 @@
import { IExpressionParser } from '../IExpressionParser';
import { ExpressionPosition } from '../../Expression/ExpressionPosition';
import { IExpression } from '../../Expression/IExpression';
import { Expression, ExpressionEvaluator } from '../../Expression/Expression';
import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
import { createPositionFromRegexFullMatch } from '../../Expression/ExpressionPositionFactory';
export abstract class RegexParser implements IExpressionParser {
protected abstract readonly regex: RegExp;
@@ -21,7 +21,7 @@ export abstract class RegexParser implements IExpressionParser {
const matches = code.matchAll(this.regex);
for (const match of matches) {
const primitiveExpression = this.buildExpression(match);
const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code);
const position = this.doOrRethrow(() => createPositionFromRegexFullMatch(match), 'invalid script position', code);
const parameters = createParameters(primitiveExpression);
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
yield expression;
@@ -37,12 +37,6 @@ export abstract class RegexParser implements IExpressionParser {
}
}
function createPosition(match: RegExpMatchArray): ExpressionPosition {
const startPos = match.index;
const endPos = startPos + match[0].length;
return new ExpressionPosition(startPos, endPos);
}
function createParameters(
expression: IPrimitiveExpression,
): FunctionParameterCollection {

View File

@@ -28,7 +28,7 @@ function hasLines(text: string) {
*/
function inlineComments(code: string): string {
const makeInlineComment = (comment: string) => {
const value = comment?.trim();
const value = comment.trim();
if (!value) {
return '<##>';
}

View File

@@ -15,12 +15,6 @@ export class PipeFactory implements IPipeFactory {
private readonly pipes = new Map<string, IPipe>();
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
if (!pipes) {
throw new Error('missing pipes');
}
if (pipes.some((pipe) => !pipe)) {
throw new Error('missing pipe in list');
}
for (const pipe of pipes) {
this.registerPipe(pipe);
}
@@ -28,10 +22,11 @@ export class PipeFactory implements IPipeFactory {
public get(pipeName: string): IPipe {
validatePipeName(pipeName);
if (!this.pipes.has(pipeName)) {
const pipe = this.pipes.get(pipeName);
if (!pipe) {
throw new Error(`Unknown pipe: "${pipeName}"`);
}
return this.pipes.get(pipeName);
return pipe;
}
private registerPipe(pipe: IPipe): void {

View File

@@ -6,8 +6,9 @@ export class ParameterSubstitutionParser extends RegexParser {
protected readonly regex = new ExpressionRegexBuilder()
.expectExpressionStart()
.expectCharacters('$')
.matchUntilFirstWhitespace() // First match: Parameter name
.matchPipeline() // Second match: Pipeline
.captureUntilWhitespaceOrPipe() // First capture: Parameter name
.expectOptionalWhitespaces()
.captureOptionalPipeline() // Second capture: Pipeline
.expectExpressionEnd()
.buildRegExp();

View File

@@ -1,59 +1,220 @@
// eslint-disable-next-line max-classes-per-file
import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { IExpression } from '../Expression/IExpression';
import { ExpressionPosition } from '../Expression/ExpressionPosition';
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory';
export class WithParser extends RegexParser {
protected readonly regex = new ExpressionRegexBuilder()
// {{ with $parameterName }}
.expectExpressionStart()
.expectCharacters('with')
.expectOneOrMoreWhitespaces()
.expectCharacters('$')
.matchUntilFirstWhitespace() // First match: parameter name
.expectExpressionEnd()
// ...
.matchMultilineAnythingExceptSurroundingWhitespaces() // Second match: Scope text
// {{ end }}
.expectExpressionStart()
.expectCharacters('end')
.expectExpressionEnd()
.buildRegExp();
export class WithParser implements IExpressionParser {
public findExpressions(code: string): IExpression[] {
if (!code) {
throw new Error('missing code');
}
return parseWithExpressions(code);
}
}
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
const parameterName = match[1];
const scopeText = match[2];
enum WithStatementType {
Start,
End,
ContextVariable,
}
type WithStatement = {
readonly type: WithStatementType.Start;
readonly parameterName: string;
readonly position: ExpressionPosition;
} | {
readonly type: WithStatementType.End;
readonly position: ExpressionPosition;
} | {
readonly type: WithStatementType.ContextVariable;
readonly position: ExpressionPosition;
readonly pipeline: string | undefined;
};
function parseAllWithExpressions(
input: string,
): WithStatement[] {
const expressions = new Array<WithStatement>();
for (const match of input.matchAll(WithStatementStartRegEx)) {
expressions.push({
type: WithStatementType.Start,
parameterName: match[1],
position: createPositionFromRegexFullMatch(match),
});
}
for (const match of input.matchAll(WithStatementEndRegEx)) {
expressions.push({
type: WithStatementType.End,
position: createPositionFromRegexFullMatch(match),
});
}
for (const match of input.matchAll(ContextVariableWithPipelineRegEx)) {
expressions.push({
type: WithStatementType.ContextVariable,
position: createPositionFromRegexFullMatch(match),
pipeline: match[1],
});
}
return expressions;
}
class WithStatementBuilder {
private readonly contextVariables = new Array<{
readonly positionInScope: ExpressionPosition;
readonly pipeline: string | undefined;
}>();
public addContextVariable(
absolutePosition: ExpressionPosition,
pipeline: string | undefined,
): void {
const positionInScope = new ExpressionPosition(
absolutePosition.start - this.startExpressionPosition.end,
absolutePosition.end - this.startExpressionPosition.end,
);
this.contextVariables.push({
positionInScope,
pipeline,
});
}
public buildExpression(endExpressionPosition: ExpressionPosition, input: string): IExpression {
const parameters = new FunctionParameterCollection();
parameters.addParameter(new FunctionParameter(this.parameterName, true));
const position = new ExpressionPosition(
this.startExpressionPosition.start,
endExpressionPosition.end,
);
const scope = input.substring(this.startExpressionPosition.end, endExpressionPosition.start);
return {
parameters: [new FunctionParameter(parameterName, true)],
evaluator: (context) => {
const argumentValue = context.args.hasArgument(parameterName)
? context.args.getArgument(parameterName).argumentValue
parameters,
position,
evaluate: (context) => {
const argumentValue = context.args.hasArgument(this.parameterName)
? context.args.getArgument(this.parameterName).argumentValue
: undefined;
if (!argumentValue) {
return '';
}
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
const substitutedScope = this.substituteContextVariables(scope, (pipeline) => {
if (!pipeline) {
return argumentValue;
}
return context.pipelineCompiler.compile(argumentValue, pipeline);
});
return substitutedScope;
},
};
}
constructor(
private readonly startExpressionPosition: ExpressionPosition,
private readonly parameterName: string,
) {
}
private substituteContextVariables(
scope: string,
substituter: (pipeline?: string) => string,
): string {
if (!this.contextVariables.length) {
return scope;
}
let substitutedScope = '';
let scopeSubstrIndex = 0;
for (const contextVariable of this.contextVariables) {
substitutedScope += scope.substring(scopeSubstrIndex, contextVariable.positionInScope.start);
substitutedScope += substituter(contextVariable.pipeline);
scopeSubstrIndex = contextVariable.positionInScope.end;
}
substitutedScope += scope.substring(scopeSubstrIndex, scope.length);
return substitutedScope;
}
}
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
function buildErrorContext(code: string, statements: readonly WithStatement[]): string {
const formattedStatements = statements.map((s) => `- [${s.position.start}, ${s.position.end}] ${WithStatementType[s.type]}`).join('\n');
return [
'Code:', '---', code, '---',
'nStatements:', '---', formattedStatements, '---',
].join('\n');
}
function parseWithExpressions(input: string): IExpression[] {
const allStatements = parseAllWithExpressions(input);
const sortedStatements = allStatements
.slice()
.sort((a, b) => b.position.start - a.position.start);
const expressions = new Array<IExpression>();
const builders = new Array<WithStatementBuilder>();
const throwWithContext = (message: string): never => {
throw new Error(`${message}\n${buildErrorContext(input, allStatements)}}`);
};
while (sortedStatements.length > 0) {
const statement = sortedStatements.pop();
if (!statement) {
break;
}
switch (statement.type) { // eslint-disable-line default-case
case WithStatementType.Start:
builders.push(new WithStatementBuilder(
statement.position,
statement.parameterName,
));
break;
case WithStatementType.ContextVariable:
if (builders.length === 0) {
throwWithContext('Context variable before `with` statement.');
}
builders[builders.length - 1].addContextVariable(statement.position, statement.pipeline);
break;
case WithStatementType.End: {
const builder = builders.pop();
if (!builder) {
throwWithContext('Redundant `end` statement, missing `with`?');
break;
}
expressions.push(builder.buildExpression(statement.position, input));
break;
}
}
}
if (builders.length > 0) {
throwWithContext('Missing `end` statement, forgot `{{ end }}?');
}
return expressions;
}
const ContextVariableWithPipelineRegEx = new ExpressionRegexBuilder()
// {{ . | pipeName }}
.expectExpressionStart()
.expectCharacters('.')
.matchPipeline() // First match: pipeline
.expectOptionalWhitespaces()
.captureOptionalPipeline() // First capture: pipeline
.expectExpressionEnd()
.buildRegExp();
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets,
// but instead letting the pipeline compiler to fail on those.
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1) => {
return replacer(match1);
});
}
const WithStatementStartRegEx = new ExpressionRegexBuilder()
// {{ with $parameterName }}
.expectExpressionStart()
.expectCharacters('with')
.expectOneOrMoreWhitespaces()
.expectCharacters('$')
.captureUntilWhitespaceOrPipe() // First capture: parameter name
.expectExpressionEnd()
.expectOptionalWhitespaces()
.buildRegExp();
const WithStatementEndRegEx = new ExpressionRegexBuilder()
// {{ end }}
.expectOptionalWhitespaces()
.expectExpressionStart()
.expectCharacters('end')
.expectOptionalWhitespaces()
.expectExpressionEnd()
.buildRegExp();

View File

@@ -5,9 +5,6 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl
private readonly arguments = new Map<string, IFunctionCallArgument>();
public addArgument(argument: IFunctionCallArgument): void {
if (!argument) {
throw new Error('missing argument');
}
if (this.hasArgument(argument.parameterName)) {
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
}

View File

@@ -3,18 +3,22 @@ import { CodeSegmentMerger } from './CodeSegmentMerger';
export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
if (!codeSegments?.length) {
if (!codeSegments.length) {
throw new Error('missing segments');
}
return {
code: joinCodeParts(codeSegments.map((f) => f.code)),
revertCode: joinCodeParts(codeSegments.map((f) => f.revertCode)),
revertCode: joinCodeParts(
codeSegments
.map((f) => f.revertCode)
.filter((code): code is string => Boolean(code)),
),
};
}
}
function joinCodeParts(codeSegments: readonly string[]): string {
return codeSegments
.filter((segment) => segment?.length > 0)
.filter((segment) => segment.length > 0)
.join('\n');
}

View File

@@ -21,9 +21,7 @@ export class FunctionCallSequenceCompiler implements FunctionCallCompiler {
calls: readonly FunctionCall[],
functions: ISharedFunctionCollection,
): CompiledCode {
if (!functions) { throw new Error('missing functions'); }
if (!calls?.length) { throw new Error('missing calls'); }
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
if (!calls.length) { throw new Error('missing calls'); }
const context: FunctionCallCompilationContext = {
allFunctions: functions,
rootCallSequence: calls,

View File

@@ -1,6 +1,6 @@
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
@@ -12,19 +12,33 @@ export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {
}
public canCompile(func: ISharedFunction): boolean {
return func.body.code !== undefined;
return func.body.type === FunctionBodyType.Code;
}
public compileFunction(
calledFunction: ISharedFunction,
callToFunction: FunctionCall,
): CompiledCode[] {
if (calledFunction.body.type !== FunctionBodyType.Code) {
throw new Error([
'Unexpected function body type.',
`\tExpected: "${FunctionBodyType[FunctionBodyType.Code]}"`,
`\tActual: "${FunctionBodyType[calledFunction.body.type]}"`,
'Function:',
`\t${JSON.stringify(callToFunction)}`,
].join('\n'));
}
const { code } = calledFunction.body;
const { args } = callToFunction;
return [
{
code: this.expressionsCompiler.compileExpressions(code.execute, args),
revertCode: this.expressionsCompiler.compileExpressions(code.revert, args),
revertCode: (() => {
if (!code.revert) {
return undefined;
}
return this.expressionsCompiler.compileExpressions(code.revert, args);
})(),
},
];
}

View File

@@ -1,4 +1,4 @@
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { CallFunctionBody, FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
@@ -13,7 +13,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
}
public canCompile(func: ISharedFunction): boolean {
return func.body.calls !== undefined;
return func.body.type === FunctionBodyType.Calls;
}
public compileFunction(
@@ -21,7 +21,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
callToFunction: FunctionCall,
context: FunctionCallCompilationContext,
): CompiledCode[] {
const nestedCalls = calledFunction.body.calls;
const nestedCalls = (calledFunction.body as CallFunctionBody).calls;
return nestedCalls.map((nestedCall) => {
try {
const compiledParentCall = this.argumentCompiler

View File

@@ -5,9 +5,6 @@ import { FunctionCallArgument } from './Argument/FunctionCallArgument';
import { ParsedFunctionCall } from './ParsedFunctionCall';
export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] {
if (calls === undefined) {
throw new Error('missing call data');
}
const sequence = getCallSequence(calls);
return sequence.map((call) => parseFunctionCall(call));
}
@@ -19,22 +16,21 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
if (calls instanceof Array) {
return calls as FunctionCallData[];
}
return [calls as FunctionCallData];
const singleCall = calls;
return [singleCall];
}
function parseFunctionCall(call: FunctionCallData): FunctionCall {
if (!call) {
throw new Error('missing call data');
}
const callArgs = parseArgs(call.parameters);
return new ParsedFunctionCall(call.function, callArgs);
}
function parseArgs(
parameters: FunctionCallParametersData,
parameters: FunctionCallParametersData | undefined,
): FunctionCallArgumentCollection {
return Object.keys(parameters || {})
.map((parameterName) => new FunctionCallArgument(parameterName, parameters[parameterName]))
const parametersMap = parameters ?? {};
return Object.keys(parametersMap)
.map((parameterName) => new FunctionCallArgument(parameterName, parametersMap[parameterName]))
.reduce((args, arg) => {
args.addArgument(arg);
return args;

View File

@@ -9,8 +9,5 @@ export class ParsedFunctionCall implements FunctionCall {
if (!functionName) {
throw new Error('missing function name in function call');
}
if (!args) {
throw new Error('missing args');
}
}
}

View File

@@ -4,15 +4,21 @@ import { FunctionCall } from './Call/FunctionCall';
export interface ISharedFunction {
readonly name: string;
readonly parameters: IReadOnlyFunctionParameterCollection;
readonly body: ISharedFunctionBody;
readonly body: SharedFunctionBody;
}
export interface ISharedFunctionBody {
readonly type: FunctionBodyType;
readonly code: IFunctionCode | undefined;
readonly calls: readonly FunctionCall[] | undefined;
export interface CallFunctionBody {
readonly type: FunctionBodyType.Calls,
readonly calls: readonly FunctionCall[],
}
export interface CodeFunctionBody {
readonly type: FunctionBodyType.Code;
readonly code: IFunctionCode,
}
export type SharedFunctionBody = CallFunctionBody | CodeFunctionBody;
export enum FunctionBodyType {
Code,
Calls,

View File

@@ -18,9 +18,6 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
}
private ensureValidParameter(parameter: IFunctionParameter) {
if (!parameter) {
throw new Error('missing parameter');
}
if (this.includesName(parameter.name)) {
throw new Error(`duplicate parameter name: "${parameter.name}"`);
}

View File

@@ -1,7 +1,7 @@
import { FunctionCall } from './Call/FunctionCall';
import {
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
FunctionBodyType, IFunctionCode, ISharedFunction, SharedFunctionBody,
} from './ISharedFunction';
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
@@ -10,7 +10,7 @@ export function createCallerFunction(
parameters: IReadOnlyFunctionParameterCollection,
callSequence: readonly FunctionCall[],
): ISharedFunction {
if (!callSequence || !callSequence.length) {
if (!callSequence.length) {
throw new Error(`missing call sequence in function "${name}"`);
}
return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls);
@@ -33,7 +33,7 @@ export function createFunctionWithInlineCode(
}
class SharedFunction implements ISharedFunction {
public readonly body: ISharedFunctionBody;
public readonly body: SharedFunctionBody;
constructor(
public readonly name: string,
@@ -42,11 +42,22 @@ class SharedFunction implements ISharedFunction {
bodyType: FunctionBodyType,
) {
if (!name) { throw new Error('missing function name'); }
if (!parameters) { throw new Error('missing parameters'); }
this.body = {
type: bodyType,
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
calls: bodyType === FunctionBodyType.Calls ? content as readonly FunctionCall[] : undefined,
};
switch (bodyType) {
case FunctionBodyType.Code:
this.body = {
type: FunctionBodyType.Code,
code: content as IFunctionCode,
};
break;
case FunctionBodyType.Calls:
this.body = {
type: FunctionBodyType.Calls,
calls: content as readonly FunctionCall[],
};
break;
default:
throw new Error(`unknown body type: ${FunctionBodyType[bodyType]}`);
}
}
}

View File

@@ -5,7 +5,6 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
private readonly functionsByName = new Map<string, ISharedFunction>();
public addFunction(func: ISharedFunction): void {
if (!func) { throw new Error('missing function'); }
if (this.has(func.name)) {
throw new Error(`function with name ${func.name} already exists`);
}

View File

@@ -1,4 +1,6 @@
import type { FunctionData, InstructionHolder } from '@/application/collections/';
import type {
FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData, CallInstruction,
} from '@/application/collections/';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
@@ -23,9 +25,8 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
functions: readonly FunctionData[],
syntax: ILanguageSyntax,
): ISharedFunctionCollection {
if (!syntax) { throw new Error('missing syntax'); }
const collection = new SharedFunctionCollection();
if (!functions || !functions.length) {
if (!functions.length) {
return collection;
}
ensureValidFunctions(functions);
@@ -55,16 +56,18 @@ function parseFunction(
}
function validateCode(
data: FunctionData,
data: CodeFunctionData,
syntax: ILanguageSyntax,
validator: ICodeValidator,
): void {
[data.code, data.revertCode].forEach(
(code) => validator.throwIfInvalid(
code,
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
),
);
[data.code, data.revertCode]
.filter((code): code is string => Boolean(code))
.forEach(
(code) => validator.throwIfInvalid(
code,
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
),
);
}
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
@@ -85,19 +88,18 @@ function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollecti
}, new FunctionParameterCollection());
}
function hasCode(data: FunctionData): boolean {
return Boolean(data.code);
function hasCode(data: FunctionData): data is CodeFunctionData {
return (data as CodeInstruction).code !== undefined;
}
function hasCall(data: FunctionData): boolean {
return Boolean(data.call);
function hasCall(data: FunctionData): data is CallFunctionData {
return (data as CallInstruction).call !== undefined;
}
function ensureValidFunctions(functions: readonly FunctionData[]) {
ensureNoUndefinedItem(functions);
ensureNoDuplicatesInFunctionNames(functions);
ensureNoDuplicateCode(functions);
ensureEitherCallOrCodeIsDefined(functions);
ensureNoDuplicateCode(functions);
ensureExpectedParametersType(functions);
}
@@ -105,7 +107,7 @@ function printList(list: readonly string[]): string {
return `"${list.join('","')}"`;
}
function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[]) {
function ensureEitherCallOrCodeIsDefined(holders: readonly FunctionData[]) {
// Ensure functions do not define both call and code
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
if (withBothCallAndCode.length) {
@@ -132,7 +134,7 @@ function isArrayOfObjects(value: unknown): boolean {
&& value.every((item) => typeof item === 'object');
}
function printNames(holders: readonly InstructionHolder[]) {
function printNames(holders: readonly FunctionData[]) {
return printList(holders.map((holder) => holder.name));
}
@@ -144,22 +146,19 @@ function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
}
}
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
if (functions.some((func) => !func)) {
throw new Error('some functions are undefined');
}
}
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
const duplicateCodes = getDuplicates(functions
const callFunctions = functions
.filter((func) => hasCode(func))
.map((func) => func as CodeFunctionData);
const duplicateCodes = getDuplicates(callFunctions
.map((func) => func.code)
.filter((code) => code));
if (duplicateCodes.length > 0) {
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
}
const duplicateRevertCodes = getDuplicates(functions
.filter((func) => func.revertCode)
.map((func) => func.revertCode));
const duplicateRevertCodes = getDuplicates(callFunctions
.map((func) => func.revertCode)
.filter((code): code is string => Boolean(code)));
if (duplicateRevertCodes.length > 0) {
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
}

View File

@@ -1,4 +1,4 @@
import type { FunctionData, ScriptData } from '@/application/collections/';
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/';
import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCode } from '@/domain/ScriptCode';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
@@ -18,27 +18,24 @@ export class ScriptCompiler implements IScriptCompiler {
private readonly functions: ISharedFunctionCollection;
constructor(
functions: readonly FunctionData[] | undefined,
functions: readonly FunctionData[],
syntax: ILanguageSyntax,
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
) {
if (!syntax) { throw new Error('missing syntax'); }
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
}
public canCompile(script: ScriptData): boolean {
if (!script) { throw new Error('missing script'); }
if (!script.call) {
return false;
}
return true;
return hasCall(script);
}
public compile(script: ScriptData): IScriptCode {
if (!script) { throw new Error('missing script'); }
try {
if (!hasCall(script)) {
throw new Error('Script does include any calls.');
}
const calls = parseFunctionCalls(script.call);
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
validateCompiledCode(compiledCode, this.codeValidator);
@@ -53,7 +50,17 @@ export class ScriptCompiler implements IScriptCompiler {
}
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
[compiledCode.code, compiledCode.revertCode].forEach(
(code) => validator.throwIfInvalid(code, [new NoEmptyLines()]),
);
[compiledCode.code, compiledCode.revertCode]
.filter((code): code is string => Boolean(code))
.map((code) => code as string)
.forEach(
(code) => validator.throwIfInvalid(
code,
[new NoEmptyLines()],
),
);
}
function hasCall(data: ScriptData): data is ScriptData & CallInstruction {
return (data as CallInstruction).call !== undefined;
}

View File

@@ -1,4 +1,4 @@
import type { ScriptData } from '@/application/collections/';
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/';
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
import { Script } from '@/domain/Script';
@@ -14,7 +14,6 @@ import { ICategoryCollectionParseContext } from './ICategoryCollectionParseConte
import { CodeValidator } from './Validation/CodeValidator';
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
// eslint-disable-next-line consistent-return
export function parseScript(
data: ScriptData,
context: ICategoryCollectionParseContext,
@@ -24,7 +23,6 @@ export function parseScript(
): Script {
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
validateScript(data, validator);
if (!context) { throw new Error('missing context'); }
try {
const script = scriptFactory(
/* name: */ data.name,
@@ -34,12 +32,12 @@ export function parseScript(
);
return script;
} catch (err) {
validator.throw(err.message);
return validator.throw(err.message);
}
}
function parseLevel(
level: string,
level: string | undefined,
parser: IEnumParser<RecommendationLevel>,
): RecommendationLevel | undefined {
if (!level) {
@@ -56,39 +54,45 @@ function parseCode(
if (context.compiler.canCompile(script)) {
return context.compiler.compile(script);
}
const code = new ScriptCode(script.code, script.revertCode);
const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled
const code = new ScriptCode(codeScript.code, codeScript.revertCode);
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
return code;
}
function validateHardcodedCodeWithoutCalls(
scriptCode: ScriptCode,
codeValidator: ICodeValidator,
validator: ICodeValidator,
syntax: ILanguageSyntax,
) {
[scriptCode.execute, scriptCode.revert].forEach(
(code) => codeValidator.throwIfInvalid(
code,
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
),
);
[scriptCode.execute, scriptCode.revert]
.filter((code): code is string => Boolean(code))
.forEach(
(code) => validator.throwIfInvalid(
code,
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
),
);
}
function validateScript(script: ScriptData, validator: NodeValidator) {
function validateScript(
script: ScriptData,
validator: NodeValidator,
): asserts script is NonNullable<ScriptData> {
validator
.assertDefined(script)
.assertValidName(script.name)
.assert(
() => Boolean(script.code || script.call),
'Must define either "call" or "code".',
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
'Neither "call" or "code" is defined.',
)
.assert(
() => !(script.code && script.call),
'Cannot define both "call" and "code".',
() => !((script as CodeScriptData).code && (script as CallScriptData).call),
'Both "call" and "code" are defined.',
)
.assert(
() => !(script.revertCode && script.call),
'Cannot define "revertCode" if "call" is defined.',
() => !((script as CodeScriptData).revertCode && (script as CallScriptData).call),
'Both "call" and "revertCode" are defined.',
);
}

View File

@@ -9,7 +9,7 @@ export class CodeValidator implements ICodeValidator {
code: string,
rules: readonly ICodeValidationRule[],
): void {
if (!rules || rules.length === 0) { throw new Error('missing rules'); }
if (rules.length === 0) { throw new Error('missing rules'); }
if (!code) {
return;
}

View File

@@ -3,9 +3,7 @@ import { ICodeLine } from '../ICodeLine';
import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
export class NoDuplicatedLines implements ICodeValidationRule {
constructor(private readonly syntax: ILanguageSyntax) {
if (!syntax) { throw new Error('missing syntax'); }
}
constructor(private readonly syntax: ILanguageSyntax) { }
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
return lines

View File

@@ -17,7 +17,6 @@ export class CodeSubstituter implements ICodeSubstituter {
public substitute(code: string, info: IProjectInformation): string {
if (!code) { throw new Error('missing code'); }
if (!info) { throw new Error('missing info'); }
const args = new FunctionCallArgumentCollection();
const substitute = (name: string, value: string) => args
.addArgument(new FunctionCallArgument(name, value));

View File

@@ -18,8 +18,6 @@ export class ScriptingDefinitionParser {
definition: ScriptingDefinitionData,
info: IProjectInformation,
): IScriptingDefinition {
if (!info) { throw new Error('missing info'); }
if (!definition) { throw new Error('missing definition'); }
const language = this.languageParser.parseEnum(definition.language, 'language');
const startCode = this.codeSubstituter.substitute(definition.startCode, info);
const endCode = this.codeSubstituter.substitute(definition.endCode, info);

View File

@@ -0,0 +1,17 @@
import { IEntity } from '@/infrastructure/Entity/IEntity';
export interface ReadonlyRepository<TKey, TEntity extends IEntity<TKey>> {
readonly length: number;
getItems(predicate?: (entity: TEntity) => boolean): readonly TEntity[];
getById(id: TKey): TEntity;
exists(id: TKey): boolean;
}
export interface MutableRepository<TKey, TEntity extends IEntity<TKey>> {
addItem(item: TEntity): void;
addOrUpdateItem(item: TEntity): void;
removeItem(id: TKey): void;
}
export interface Repository<TKey, TEntity extends IEntity<TKey>>
extends ReadonlyRepository<TKey, TEntity>, MutableRepository<TKey, TEntity> { }

View File

@@ -12,29 +12,38 @@ declare module '@/application/collections/*' {
}
export type CategoryOrScriptData = CategoryData | ScriptData;
export type DocumentationData = ReadonlyArray<string> | string;
export type DocumentationData = ReadonlyArray<string> | string | undefined;
export interface DocumentableData {
readonly docs?: DocumentationData;
}
export interface InstructionHolder {
readonly name: string;
readonly code?: string;
export interface CodeInstruction {
readonly code: string;
readonly revertCode?: string;
readonly call?: FunctionCallsData;
}
export interface CallInstruction {
readonly call: FunctionCallsData;
}
export type InstructionHolder = CodeInstruction | CallInstruction;
export interface ParameterDefinitionData {
readonly name: string;
readonly optional?: boolean;
}
export interface FunctionData extends InstructionHolder {
export type FunctionDefinition = {
readonly name: string;
readonly parameters?: readonly ParameterDefinitionData[];
}
};
export type CodeFunctionData = FunctionDefinition & CodeInstruction;
export type CallFunctionData = FunctionDefinition & CallInstruction;
export type FunctionData = CodeFunctionData | CallFunctionData;
export interface FunctionCallParametersData {
readonly [index: string]: string;
@@ -47,10 +56,16 @@ declare module '@/application/collections/*' {
export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined;
export interface ScriptData extends InstructionHolder, DocumentableData {
export type ScriptDefinition = DocumentableData & {
readonly name: string;
readonly recommend?: string;
}
};
export type CodeScriptData = ScriptDefinition & CodeInstruction;
export type CallScriptData = ScriptDefinition & CallInstruction;
export type ScriptData = CodeScriptData | CallScriptData;
export interface ScriptingDefinitionData {
readonly language: string;

View File

@@ -707,15 +707,9 @@ actions:
-
category: Clear Firefox history
docs: |-
Mozilla Firefox, or simply Firefox, is a free and open-source web browser developed by the Mozilla Foundation and
its subsidiary the Mozilla Corporation [1].
This category encompasses a series of scripts aimed at helping users manage and delete their browsing history and related data in Mozilla Firefox.
Firefox stores user-related data in user profiles [2].
See also [the Firefox homepage](https://web.archive.org/web/20221029214632/https://www.mozilla.org/en-US/firefox/).
[1]: https://web.archive.org/web/20221029145113/https://en.wikipedia.org/wiki/Firefox "Firefox | Wikipedia | en.wikipedia.org"
[2]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
The scripts are designed to target different aspects of user data stored by Firefox, providing users options for maintaining privacy and freeing up disk space.
children:
-
name: Clear Firefox cache
@@ -755,9 +749,13 @@ actions:
# Snap installation
rm -rfv ~/snap/firefox/common/.mozilla/firefox/Crash\ Reports/*
-
function: DeleteFromFirefoxProfiles
function: DeleteFilesFromFirefoxProfiles
parameters:
path: crashes/
pathGlob: crashes/*
-
function: DeleteFilesFromFirefoxProfiles
parameters:
pathGlob: crashes/events/*
-
name: Clear Firefox cookies
docs: |-
@@ -765,41 +763,37 @@ actions:
[1]: https://web.archive.org/web/20221029140816/https://kb.mozillazine.org/Cookies.sqlite "Cookies.sqlite - MozillaZine Knowledge Base | kb.mozillazine.org"
call:
function: DeleteFromFirefoxProfiles
function: DeleteFilesFromFirefoxProfiles
parameters:
path: cookies.sqlite
pathGlob: cookies.sqlite
-
name: Clear Firefox browsing history (URLs, downloads, bookmarks, visits, etc.)
# This script (name, documentation and code) is same in Linux and Windows collections.
# Changes should be done at both places.
# Marked: refactor-with-partials
docs: |-
The file "places.sqlite" stores the annotations, bookmarks, favorite icons, input history, keywords, and browsing history (a record of visited pages) [1].
The tables include [1]:
- `moz_anno_attributes`: Annotation Attributes
- `moz_annos`: Annotations
- `moz_bookmarks`: Bookmarks
- `moz_bookmarks_roots`: Bookmark roots i.e. places, menu, toolbar, tags, unfiled
- `moz_favicons`: Favorite icons - including URL of icon
- `moz_historyvisits`: A history of the number of times a site has been visited
- `moz_inputhistory`: A history of URLs typed by the user
- `moz_items_annos`: Item annotations
- `moz_keywords`: Keywords
- `moz_places`: Places/Sites visited - referenced by `moz_historyvisits`
URL data is stored in the `moz_places` table. However, this table is connected to `moz_annos`, `moz_bookmarks`, and `moz_inputhistory` and `moz_historyvisits`.
As these entries are connected to each other, we'll delete all of them at the same time [2].
This script targets the Firefox browsing history, including URLs, downloads, bookmarks, and site visits, by deleting specific database entries.
**Bookmarks**:
Firefox bookmarks are stored in tables such as `moz_bookmarks`, `moz_bookmarks_folders`, `moz_bookmarks_roots` [3].
There are also not very well documented tables, such as `moz_bookmarks_deleted` [4].
Firefox stores various user data in a file named `places.sqlite`. This file includes:
**Downloads:**
Firefox downloads are stored in the 'places.sqlite' database, within the 'moz_annos' table [5].
The entries in `moz_annos` are linked to `moz_places` that store the actual history entry (`moz_places.id = moz_annos.place_id`) [6].
Associated URL information is stored within the 'moz_places' table [5].
Downloads have been historically stored in `downloads.rdf` for Firefox 2.x and below [7].
Starting with Firefox 3.x they're stored in `downloads.sqlite` [7].
- Annotations, bookmarks, and favorite icons (`moz_anno_attributes`, `moz_annos`, `moz_favicons`) [1]
- Browsing history, a record of pages visited (`moz_places`, `moz_historyvisits`) [1]
- Keywords and typed URLs (`moz_keywords`, `moz_inputhistory`) [1]
- Item annotations (`moz_items_annos`) [1]
- Bookmark roots such as places, menu, toolbar, tags, unfiled (`moz_bookmarks_roots`) [1]
**Favicons:**
Firefox favicons are stored in the `favicons.sqlite` database, within the `moz_icons` table [5].
Older versions of Firefox stored Favicons in the 'places.sqlite' database, within the `moz_favicons` table [5].
The `moz_places` table holds URL data, connecting to various other tables like `moz_annos`, `moz_bookmarks`, `moz_inputhistory`, and `moz_historyvisits` [2].
Due to these connections, the script removes entries from all relevant tables simultaneously to maintain database integrity.
**Bookmarks**: Stored across several tables (`moz_bookmarks`, `moz_bookmarks_folders`, `moz_bookmarks_roots`) [3], with additional undocumented tables like `moz_bookmarks_deleted` [4].
**Downloads**: Stored in the 'places.sqlite' database, within the 'moz_annos' table [5]. The entries in `moz_annos` are linked to `moz_places` that store the actual history entry
(`moz_places.id = moz_annos.place_id`) [6]. Associated URL information is stored within the 'moz_places' table [5]. Downloads have been historically stored in `downloads.rdf` for Firefox 2.x
and below [7], and `downloads.sqlite` later on [7].
**Favicons**: Older Firefox versions stored favicons in `places.sqlite` within the `moz_favicons` table [5], while newer versions use `favicons.sqlite` and the `moz_icons` table [5].
By executing this script, users can ensure their Firefox browsing history, bookmarks, and downloads are thoroughly removed, contributing to a cleaner and more private browsing experience.
[1]: https://web.archive.org/web/20221029141626/https://kb.mozillazine.org/Places.sqlite "Places.sqlite - MozillaZine Knowledge Base | kb.mozillazine.org"
[2]: https://web.archive.org/web/20221030160803/https://wiki.mozilla.org/images/0/08/Places.sqlite.schema.pdf "Places.sqlite.schema.pdf | Mozilla Wiki"
@@ -810,21 +804,21 @@ actions:
[7]: https://web.archive.org/web/20221029145712/https://kb.mozillazine.org/Downloads.rdf "Downloads.rdf | MozillaZine Knowledge Base | kb.mozillazine.org"
call:
-
function: DeleteFromFirefoxProfiles
function: DeleteFilesFromFirefoxProfiles
parameters:
path: downloads.rdf
pathGlob: downloads.rdf
-
function: DeleteFromFirefoxProfiles
function: DeleteFilesFromFirefoxProfiles
parameters:
path: downloads.sqlite
pathGlob: downloads.sqlite
-
function: DeleteFromFirefoxProfiles
function: DeleteFilesFromFirefoxProfiles
parameters:
path: places.sqlite
pathGlob: places.sqlite
-
function: DeleteFromFirefoxProfiles
function: DeleteFilesFromFirefoxProfiles
parameters:
path: favicons.sqlite
pathGlob: favicons.sqlite
-
name: Clear Firefox logins
docs: |-
@@ -837,17 +831,17 @@ actions:
[2]: https://web.archive.org/web/20221029145757/https://bugzilla.mozilla.org/show_bug.cgi?id=1593467 "1593467 - Automatically restore from logins-backup.json when logins.json is missing or corrupt | Bugzilla | mozilla.org | bugzilla.mozilla.org"
call:
-
function: DeleteFromFirefoxProfiles
function: DeleteFilesFromFirefoxProfiles
parameters:
path: logins.json
pathGlob: logins.json
-
function: DeleteFromFirefoxProfiles
function: DeleteFilesFromFirefoxProfiles
parameters:
path: logins-backup.json
pathGlob: logins-backup.json
-
function: DeleteFromFirefoxProfiles
function: DeleteFilesFromFirefoxProfiles
parameters:
path: signons.sqlite
pathGlob: signons.sqlite
-
name: Clear Firefox autocomplete history
docs: |-
@@ -856,9 +850,9 @@ actions:
[1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
call:
function: DeleteFromFirefoxProfiles
function: DeleteFilesFromFirefoxProfiles
parameters:
path: formhistory.sqlite
pathGlob: formhistory.sqlite
-
name: Clear Firefox "Multi-Account Containers" data
docs: |-
@@ -866,9 +860,9 @@ actions:
[1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
call:
function: DeleteFromFirefoxProfiles
function: DeleteFilesFromFirefoxProfiles
parameters:
path: containers.json
pathGlob: containers.json
-
name: Clear Firefox open tabs and windows data
docs: |-
@@ -878,9 +872,9 @@ actions:
[1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
call:
function: DeleteFromFirefoxProfiles
function: DeleteFilesFromFirefoxProfiles
parameters:
path: sessionstore.jsonlz4
pathGlob: sessionstore.jsonlz4
-
category: Clear system and kernel usage data
docs: |-
@@ -2911,7 +2905,8 @@ actions:
function: AddFirefoxPrefs
parameters:
prefName: toolkit.telemetry.log.level
jsonValue: 'Fatal'
jsonValue: >-
"Fatal"
-
name: Disable Firefox telemetry log output
recommend: standard
@@ -2924,7 +2919,8 @@ actions:
function: AddFirefoxPrefs
parameters:
prefName: toolkit.telemetry.log.dump
jsonValue: 'Fatal'
jsonValue: >-
"Fatal"
-
name: Clear Firefox telemetry user ID
recommend: standard
@@ -3491,16 +3487,66 @@ functions:
>&2 echo "Failed, $service does not exist."
fi
-
name: DeleteFromFirefoxProfiles
name: Comment
# 💡 Purpose:
# Adds a comment in the executed code for better readability and debugging.
# This function does not affect the execution flow but helps in understanding the purpose of subsequent code.
parameters:
- name: path # file or folder in profile file
code: |-
# {{ $path }}: Global installation
rm -rfv ~/.mozilla/firefox/*/{{ $path }}
# {{ $path }}: Flatpak installation
rm -rfv ~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/{{ $path }}
# {{ $path }}: Snap installation
rm -rfv ~/snap/firefox/common/.mozilla/firefox/*/{{ $path }}
- name: codeComment
optional: true
- name: revertCodeComment
optional: true
call:
function: RunInlineCode
parameters:
code: '{{ with $codeComment }}# {{ . }}{{ end }}'
revertCode: '{{ with $revertCodeComment }}# {{ . }}{{ end }}'
-
name: DeleteFiles
parameters:
- name: fileGlob
call:
-
function: Comment
parameters:
codeComment: >-
Delete files matching pattern: "{{ $fileGlob }}"
-
function: RunPython3Code
parameters:
code: |-
import glob
import os
path = '{{ $fileGlob }}'
expanded_path = os.path.expandvars(os.path.expanduser(path))
print(f'Deleting files matching pattern: {expanded_path}')
paths = glob.glob(expanded_path)
if not paths:
print('Skipping, no paths found.')
for path in paths:
if not os.path.isfile(path):
print(f'Skipping folder: "{path}".')
continue
os.remove(path)
print(f'Successfully delete file: "{path}".')
print(f'Successfully deleted {len(paths)} file(s).')
-
name: DeleteFilesFromFirefoxProfiles
parameters:
- name: pathGlob # file or folder in profile file
call:
- # Global installation
function: DeleteFiles
parameters:
fileGlob: ~/.mozilla/firefox/*/{{ $pathGlob }}
- # Flatpak installation
function: DeleteFiles
parameters:
fileGlob: ~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/{{ $pathGlob }}
- # Snap installation
function: DeleteFiles
parameters:
fileGlob: ~/snap/firefox/common/.mozilla/firefox/*/{{ $pathGlob }}
-
name: CleanTableFromFirefoxProfileDatabase
parameters:
@@ -3693,7 +3739,7 @@ functions:
- name: prefName
- name: jsonValue
docs: |-
This script either creates or updates the `user.js` file to set specific Mozilla Firefox preferences.
This script modifies the `user.js` file in Firefox profiles to set specific preferences.
The `user.js` file can be found in a Firefox profile folder [1] and its location depends on the type of installation:
@@ -3701,12 +3747,18 @@ functions:
- Flatpak: `~/.var/app/org.mozilla.firefox/.mozilla/firefox/<profile-name>/user.js`
- Snap: `~/snap/firefox/common/.mozilla/firefox/<profile-name>/user.js`
While the `user.js` file is optional [2], if it's present, the Firefox application will prioritize its settings over
those in `prefs.js` upon startup [1][2]. To prevent potential profile corruption, Mozilla advises against editing
`prefs.js` directly [2].
While the `user.js` file is optional [2], if it's present, the Firefox will prioritize its settings over
those in `prefs.js` upon startup [1] [2]. It's recommended not to directly edit `prefs.js` to avoid profile corruption [2].
When `user.js` is modified or deleted, corresponding changes in `prefs.js` are necessary for reversion, as Firefox
doesn't automatically revert these changes [3].
This script safely modifies `user.js` and ensures changes are reflected in `prefs.js` during reversion, addressing
issues with preference persistence [3].
[1]: https://web.archive.org/web/20230811005205/https://kb.mozillazine.org/User.js_file "User.js file - MozillaZine Knowledge Base"
[2]: https://web.archive.org/web/20221029211757/https://kb.mozillazine.org/Prefs.js_file "Prefs.js file - MozillaZine Knowledge Base"
[3]: https://github.com/undergroundwires/privacy.sexy/issues/282 "[BUG]: Reverting Firefox settings do not work on Linux · Issue #282 · undergroundwires/privacy.sexy | github.com"
code: |-
pref_name='{{ $prefName }}'
pref_value='{{ $jsonValue }}'
@@ -3746,12 +3798,16 @@ functions:
if [ "$total_profiles_found" -eq 0 ]; then
echo 'No profile folders are found, no changes are made.'
else
echo "Preferences verified in $total_profiles_found profiles."
echo "Successfully verified preferences in $total_profiles_found profiles."
fi
revertCode: |-
pref_name='{{ $prefName }}'
pref_value='{{ $jsonValue }}'
echo "Reverting preference: \"$pref_name\" to its default."
if command -v 'ps' &> /dev/null && ps aux | grep -i "[f]irefox" > /dev/null; then
>&2 echo -e "\e[33mWarning: Firefox is currently running. Please close Firefox before executing the revert script to ensure changes are applied effectively.\e[0m"
fi
declare -a files_to_modify=('prefs.js' 'user.js')
declare -a profile_paths=(
~/.mozilla/firefox/*/
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/
@@ -3759,31 +3815,39 @@ functions:
)
declare -i total_profiles_found=0
for profile_dir in "${profile_paths[@]}"; do
user_js_file="${profile_dir}user.js"
if [ ! -f "$user_js_file" ]; then
if [ ! -d "$profile_dir" ]; then
continue
fi
((total_profiles_found++))
echo "$user_js_file:"
pref_start="user_pref(\"$pref_name\","
pref_line="user_pref(\"$pref_name\", $pref_value);"
if ! grep --quiet "^$pref_start" "${user_js_file}"; then
echo $'\t''Skipping, preference was not configured before.'
elif grep --quiet "^$pref_line$" "${user_js_file}"; then
sed --in-place "/^$pref_line/d" "$user_js_file"
echo $'\t''Successfully reverted preference to default.'
if ! grep --quiet '[^[:space:]]' "$user_js_file"; then
rm "$user_js_file"
echo $'\t''Removed user.js file as it became empty.'
fi
else
echo $'\t''Skipping, the preference has value that is not configured by privacy.sexy.'
if [[ ! "$(basename "$profile_dir")" =~ ^[a-z0-9]{8}\..+ ]]; then
continue # Not a profile folder
fi
((total_profiles_found++))
for file_to_modify in "${files_to_modify[@]}"; do
config_file_path="${profile_dir}${file_to_modify}"
if [ ! -f "$config_file_path" ]; then
continue
fi
echo "$config_file_path:"
pref_start="user_pref(\"$pref_name\","
pref_line="user_pref(\"$pref_name\", $pref_value);"
if ! grep --quiet "^$pref_start" "${config_file_path}"; then
echo $'\t''Skipping, preference was not configured before.'
elif grep --quiet "^$pref_line$" "${config_file_path}"; then
sed --in-place "/^$pref_line/d" "$config_file_path"
echo $'\t''Successfully reverted preference to default.'
if ! grep --quiet '[^[:space:]]' "$config_file_path"; then
rm "$config_file_path"
echo $'\t'"Removed the file as it became empty."
fi
else
echo $'\t''Skipping, the preference has value that is not configured by privacy.sexy.'
fi
done
done
if [ "$total_profiles_found" -eq 0 ]; then
echo 'No reversion was necessary.'
else
echo "Preferences verified in $total_profiles_found profiles."
echo "Successfully verified preferences in $total_profiles_found profiles."
fi
-
name: RenameFile

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