diff --git a/.github/actions/asana-add-comment/action.yml b/.github/actions/asana-add-comment/action.yml new file mode 100644 index 0000000000..73f442c5be --- /dev/null +++ b/.github/actions/asana-add-comment/action.yml @@ -0,0 +1,78 @@ +name: Add a Comment to Asana Task +description: Adds a comment to the Asana task. +inputs: + access-token: + description: "Asana access token" + required: true + type: string + task-url: + description: "Task URL" + required: false + type: string + task-id: + description: "Task ID" + required: false + type: string + comment: + description: "Comment to add to the Asana task" + required: false + type: string + template-name: + description: | + Name of a template file (without extension) for the comment, relative to 'templates' subdirectory of the action. + The file is processed by envsubst before being sent to Asana. + required: false + type: string +runs: + using: "composite" + steps: + - id: extract-task-id + if: ${{ inputs.task-url }} + uses: ./.github/actions/asana-extract-task-id + with: + task-url: ${{ inputs.task-url }} + access-token: ${{ inputs.access-token }} + + - id: process-template-payload + if: ${{ inputs.template-name }} + shell: bash + env: + TEMPLATE_PATH: ${{ github.action_path }}/templates/${{ inputs.template-name }}.yml + run: | + if [ ! -f $TEMPLATE_PATH ]; then + echo "::error::Template file not found at $TEMPLATE_PATH" + exit 1 + fi + + # Process the template file with envsubst, turn into JSON, remove leading spaces and newlines on non-empty lines, and compact the JSON + payload="$(envsubst < $TEMPLATE_PATH | yq -o=j | sed -E 's/\\n( *)([^\\n])/\2/g' | jq -c)" + echo "payload-base64=$(base64 <<< $payload)" >> $GITHUB_OUTPUT + + - id: process-comment-payload + if: ${{ inputs.comment }} + shell: bash + env: + COMMENT: ${{ inputs.comment }} + WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + payload="{ \"data\": { \"text\": \"${COMMENT}\n\nWorkflow URL: ${WORKFLOW_URL}\" } }" + echo "payload-base64=$(base64 <<< $payload)" >> $GITHUB_OUTPUT + + - id: add-comment + shell: bash + env: + ASANA_ACCESS_TOKEN: ${{ inputs.access-token }} + TASK_ID: ${{ inputs.task-id || steps.extract-task-id.outputs.task-id }} + PAYLOAD_BASE64: ${{ steps.process-template-payload.outputs.payload-base64 || steps.process-comment-payload.outputs.payload-base64 }} + run: | + return_code=$(curl -fLSs "https://app.asana.com/api/1.0/tasks/${TASK_ID}/stories" \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + -H 'content-type: application/json' \ + --write-out '%{http_code}' \ + --output /dev/null \ + -d "$(base64 -d <<< $PAYLOAD_BASE64)") + + if [ $return_code -ne 201 ]; then + echo "::error::Failed to add a comment to the Asana task" + exit 1 + fi diff --git a/.github/actions/asana-add-comment/templates/appcast-failed.yml b/.github/actions/asana-add-comment/templates/appcast-failed.yml new file mode 100644 index 0000000000..b20642af47 --- /dev/null +++ b/.github/actions/asana-add-comment/templates/appcast-failed.yml @@ -0,0 +1,9 @@ +data: + html_text: | + +

[ACTION NEEDED] Publishing ${TAG} internal release to Sparkle failed

+ , please proceed with generating appcast2.xml and uploading files to S3 from your local machine, according to instructions. + + + 🔗 Workflow URL: ${WORKFLOW_URL}. + \ No newline at end of file diff --git a/.github/actions/asana-add-comment/templates/debug-symbols-uploaded.yml b/.github/actions/asana-add-comment/templates/debug-symbols-uploaded.yml new file mode 100644 index 0000000000..dd02a3b80a --- /dev/null +++ b/.github/actions/asana-add-comment/templates/debug-symbols-uploaded.yml @@ -0,0 +1,8 @@ +data: + html_text: | + + 🐛 Debug symbols archive for ${TAG} build is uploaded to ${DSYM_S3_PATH}. + + + 🔗 Workflow URL: ${WORKFLOW_URL}. + diff --git a/.github/actions/asana-add-comment/templates/dmg-uploaded.yml b/.github/actions/asana-add-comment/templates/dmg-uploaded.yml new file mode 100644 index 0000000000..a5bc0978de --- /dev/null +++ b/.github/actions/asana-add-comment/templates/dmg-uploaded.yml @@ -0,0 +1,8 @@ +data: + html_text: | + + 📥 DMG for ${TAG} is available from ${DMG_URL}. + + + 🔗 Workflow URL: ${WORKFLOW_URL}. + diff --git a/.github/actions/asana-add-comment/templates/internal-release-complete.yml b/.github/actions/asana-add-comment/templates/internal-release-complete.yml new file mode 100644 index 0000000000..3f6773c3d2 --- /dev/null +++ b/.github/actions/asana-add-comment/templates/internal-release-complete.yml @@ -0,0 +1,8 @@ +data: + html_text: | + + Build ${TAG} is now available for internal testing through Sparkle and TestFlight. + + + 📥 DMG download link + diff --git a/.github/actions/asana-add-comment/templates/internal-release-ready-merge-failed.yml b/.github/actions/asana-add-comment/templates/internal-release-ready-merge-failed.yml new file mode 100644 index 0000000000..733d97340b --- /dev/null +++ b/.github/actions/asana-add-comment/templates/internal-release-ready-merge-failed.yml @@ -0,0 +1,17 @@ +data: + # yq -o=j | sed -E 's/\\n( *)([^\\n])/\2/g' + html_text: | + +

[ACTION NEEDED] Internal release build ${TAG} ready

+ + + 🔗 Workflow URL: ${WORKFLOW_URL}. + diff --git a/.github/actions/asana-add-comment/templates/internal-release-ready-tag-failed.yml b/.github/actions/asana-add-comment/templates/internal-release-ready-tag-failed.yml new file mode 100644 index 0000000000..f98641fa62 --- /dev/null +++ b/.github/actions/asana-add-comment/templates/internal-release-ready-tag-failed.yml @@ -0,0 +1,16 @@ +data: + html_text: | + +

[ACTION NEEDED] Internal release build ${TAG} ready

+ + + , please proceed with manual tagging and merging according to instructions. + + + 🔗 Workflow URL: ${WORKFLOW_URL}. + diff --git a/.github/actions/asana-add-comment/templates/internal-release-ready.yml b/.github/actions/asana-add-comment/templates/internal-release-ready.yml new file mode 100644 index 0000000000..d089b0d551 --- /dev/null +++ b/.github/actions/asana-add-comment/templates/internal-release-ready.yml @@ -0,0 +1,14 @@ +data: + # yq -o=j | sed -E 's/\\n( *)([^\\n])/\2/g' + html_text: | + +

Internal release build ${TAG} ready ✅

+ + + 🔗 Workflow URL: ${WORKFLOW_URL}. + diff --git a/.github/actions/asana-add-comment/templates/validate-check-for-updates-internal.yml b/.github/actions/asana-add-comment/templates/validate-check-for-updates-internal.yml new file mode 100644 index 0000000000..f76a5cc72d --- /dev/null +++ b/.github/actions/asana-add-comment/templates/validate-check-for-updates-internal.yml @@ -0,0 +1,11 @@ +data: + html_text: | + +

Build ${TAG} is available for internal testing through Sparkle 🚀

+ + + 🔗 Workflow URL: ${WORKFLOW_URL}. + diff --git a/.github/actions/asana-create-action-item/templates/appcast-failed.yml b/.github/actions/asana-create-action-item/templates/appcast-failed.yml new file mode 100644 index 0000000000..668083791a --- /dev/null +++ b/.github/actions/asana-create-action-item/templates/appcast-failed.yml @@ -0,0 +1,37 @@ +data: + name: Generate appcast2.xml for ${TAG} internal release and upload assets to S3 + assignee: "${ASSIGNEE_ID}" + html_notes: | + + Publishing internal release ${TAG} failed in CI. Please follow the steps to generate the appcast file and upload files to S3 from your local machine. + +
    +
  1. Download the DMG for ${TAG} release.
  2. +
  3. Create a new file called release-notes.txt on your disk. +
  4. +
  5. Run appcastManager: +
  6. +
  7. Verify that the new build is in the appcast file with the following internal channel tag: +
  8. +
  9. Run upload_to_s3.sh script: +
  10. +
+ When done, please verify that "Check for Updates" works correctly: +
    +
  1. Launch a debug version of the app with an old version number.
  2. +
  3. Identify as an internal user in the app.
  4. +
  5. Go to Main Menu → DuckDuckGo → Check for Updates...
  6. +
  7. Verify that you're being offered to update to ${TAG}.
  8. +
  9. Verify that the update works.
  10. +
+ + 🔗 Workflow URL: ${WORKFLOW_URL}. + diff --git a/.github/actions/asana-create-action-item/templates/validate-check-for-updates-internal.yml b/.github/actions/asana-create-action-item/templates/validate-check-for-updates-internal.yml new file mode 100644 index 0000000000..61d5b86207 --- /dev/null +++ b/.github/actions/asana-create-action-item/templates/validate-check-for-updates-internal.yml @@ -0,0 +1,41 @@ +data: + name: Validate that 'Check For Updates' upgrades to ${TAG} for internal users + assignee: "${ASSIGNEE_ID}" + html_notes: | + +

Build ${TAG} has been released internally via Sparkle 🎉

+ Please verify that "Check for Updates" works correctly: +
    +
  1. Launch a debug version of the app with an old version number.
  2. +
  3. Identify as an internal user in the app.
  4. +
  5. Go to Main Menu → DuckDuckGo → Check for Updates...
  6. +
  7. Verify that you're being offered to update to ${TAG}.
  8. +
  9. Verify that the update works.
  10. +
+

🚨In case "Check for Updates" is broken

+ You can restore previous version of the appcast2.xml: +
    +
  1. Download the ${OLD_APPCAST_NAME} file attached to this task.
  2. +
  3. Log in to AWS session: +
  4. +
  5. Overwrite appcast2.xml with the old version: +
  6. +
+ +
+

Summary of automated changes

+

Changes to appcast2.xml

+ See the attached ${APPCAST_PATCH_NAME} file. +

Release notes

+ See the attached ${RELEASE_NOTES_FILE} file for release notes extracted automatically from the release task description. +

List of files uploaded to S3

+
    + ${FILES_UPLOADED} +
+ + 🔗 Workflow URL: ${WORKFLOW_URL}. + diff --git a/.github/actions/asana-log-message/action.yml b/.github/actions/asana-log-message/action.yml index be0d695ce0..966d7a4605 100644 --- a/.github/actions/asana-log-message/action.yml +++ b/.github/actions/asana-log-message/action.yml @@ -47,46 +47,10 @@ runs: exit 1 fi - - id: process-template-payload - if: ${{ inputs.template-name }} - shell: bash - env: - TEMPLATE_PATH: ${{ github.action_path }}/templates/${{ inputs.template-name }}.yml - run: | - if [ ! -f $TEMPLATE_PATH ]; then - echo "::error::Template file not found at $TEMPLATE_PATH" - exit 1 - fi - - # Process the template file with envsubst, turn into JSON, remove leading spaces and newlines on non-empty lines, and compact the JSON - payload="$(envsubst < $TEMPLATE_PATH | yq -o=j | sed -E 's/\\n( *)([^\\n])/\2/g' | jq -c)" - echo "payload-base64=$(base64 <<< $payload)" >> $GITHUB_OUTPUT - - - id: process-comment-payload - if: ${{ inputs.comment }} - shell: bash - env: - COMMENT: ${{ inputs.comment }} - WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} - run: | - payload="{ \"data\": { \"text\": \"${COMMENT}\n\nWorkflow URL: ${WORKFLOW_URL}\" } }" - echo "payload-base64=$(base64 <<< $payload)" >> $GITHUB_OUTPUT - - id: add-comment - shell: bash - env: - ASANA_ACCESS_TOKEN: ${{ inputs.access-token }} - TASK_ID: ${{ steps.get-automation-subtask.outputs.automation-task-id }} - PAYLOAD_BASE64: ${{ steps.process-template-payload.outputs.payload-base64 || steps.process-comment-payload.outputs.payload-base64 }} - run: | - return_code=$(curl -fLSs "https://app.asana.com/api/1.0/tasks/${TASK_ID}/stories" \ - -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ - -H 'content-type: application/json' \ - --write-out '%{http_code}' \ - --output /dev/null \ - -d "$(base64 -d <<< $PAYLOAD_BASE64)") - - if [ $return_code -ne 201 ]; then - echo "::error::Failed to add a comment to the Asana task" - exit 1 - fi + uses: ./.github/actions/asana-add-comment + with: + access-token: ${{ inputs.access-token }} + task-id: ${{ steps.get-automation-subtask.outputs.automation-task-id }} + comment: ${{ inputs.comment }} + template-name: ${{ inputs.template-name }} diff --git a/.github/actions/asana-upload/action.yml b/.github/actions/asana-upload/action.yml index d54584236a..405216a35f 100644 --- a/.github/actions/asana-upload/action.yml +++ b/.github/actions/asana-upload/action.yml @@ -17,7 +17,7 @@ runs: using: "composite" steps: - run: | - curl -s "https://app.asana.com/api/1.0/tasks/${{ inputs.task-id }}/attachments" \ + curl -fLSs "https://app.asana.com/api/1.0/tasks/${{ inputs.task-id }}/attachments" \ -H "Authorization: Bearer ${{ inputs.access-token }}" \ --form "file=@${{ inputs.file-name }}" shell: bash diff --git a/.github/workflows/build_appstore.yml b/.github/workflows/build_appstore.yml index fc5108c64a..89ea1d2d61 100644 --- a/.github/workflows/build_appstore.yml +++ b/.github/workflows/build_appstore.yml @@ -82,7 +82,7 @@ jobs: ${{ secrets.SSH_PRIVATE_KEY_FASTLANE_MATCH }} - name: Check out the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive ref: ${{ inputs.branch || github.ref_name }} @@ -149,6 +149,7 @@ jobs: template-name: debug-symbols-uploaded - name: Send Mattermost message + if: success() || failure() # Don't execute when cancelled env: WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} DESTINATION: ${{ env.destination }} @@ -158,8 +159,7 @@ jobs: if [[ -z "${MM_USER_HANDLE}" ]]; then echo "Mattermost user handle not known for ${{ github.actor }}, skipping sending message" else - curl -s -H 'Content-type: application/json' \ - -d "$(envsubst < ./scripts/assets/appstore-release-mm-template.json)" \ + -d "$(envsubst < ./scripts/assets/appstore-release-mm-template.json | jq ".${{ job.status }}")" \ ${{ secrets.MM_WEBHOOK_URL }} fi diff --git a/.github/workflows/build_notarized.yml b/.github/workflows/build_notarized.yml index 010df3bcc6..8144e8517c 100644 --- a/.github/workflows/build_notarized.yml +++ b/.github/workflows/build_notarized.yml @@ -108,7 +108,7 @@ jobs: steps: - name: Check out the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive ref: ${{ env.branch }} @@ -296,7 +296,8 @@ jobs: run: | aws s3 cp \ ${{ github.workspace }}/${{ steps.create-dmg.outputs.dmg }} \ - s3://${{ env.RELEASE_BUCKET_NAME }}/${{ env.RELEASE_BUCKET_PREFIX }}/ + s3://${{ env.RELEASE_BUCKET_NAME }}/${{ env.RELEASE_BUCKET_PREFIX }}/ \ + --acl public-read - name: Report success if: ${{ env.upload-to == 's3' }} @@ -330,12 +331,17 @@ jobs: name: Send Mattermost message needs: [export-notarized-app, create-dmg] - if: ${{ always() && (needs.export-notarized-app.result == 'success') && (needs.create-dmg.result == 'success' || needs.create-dmg.result == 'skipped') }} + if: always() runs-on: ubuntu-latest + env: + success: ${{ (needs.export-notarized-app.result == 'success') && (needs.create-dmg.result == 'success' || needs.create-dmg.result == 'skipped') }} + failure: ${{ (needs.export-notarized-app.result == 'failure') || (needs.create-dmg.result == 'failure') }} + steps: - name: Send Mattermost message + if: ${{ env.success || env.failure }} # Don't execute when cancelled env: ASANA_TASK_URL: ${{ github.event.inputs.asana-task-url || inputs.asana-task-url }} GH_TOKEN: ${{ github.token }} @@ -355,7 +361,12 @@ jobs: export ASANA_LINK=" | [:asana: Asana task](${ASANA_TASK_URL})" fi + if [[ "${{ env.success }}" == "true" ]]; then + status="success" + else + status="failure" + fi curl -s -H 'Content-type: application/json' \ - -d "$(envsubst < message-template.json)" \ + -d "$(envsubst < message-template.json | jq ".${status}")" \ ${{ secrets.MM_WEBHOOK_URL }} fi diff --git a/.github/workflows/bump_internal_release.yml b/.github/workflows/bump_internal_release.yml index 223162dbc6..49b67e1bd2 100644 --- a/.github/workflows/bump_internal_release.yml +++ b/.github/workflows/bump_internal_release.yml @@ -14,11 +14,11 @@ on: jobs: - update_embedded_files: + assert_release_branch: - name: Update Embedded Files + name: Assert Release Branch - runs-on: macos-13-xlarge + runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -30,32 +30,11 @@ jobs: *) echo "👎 Not a release branch"; exit 1 ;; esac - - name: Check out the code - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Select Xcode - run: sudo xcode-select -s /Applications/Xcode_$(<.xcode-version).app/Contents/Developer - - - name: Prepare fastlane - run: bundle install - - - name: Update embedded files - env: - APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} - APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} - APPLE_API_KEY_ISSUER: ${{ secrets.APPLE_API_KEY_ISSUER }} - run: | - git config --global user.name "Dax the Duck" - git config --global user.email "dax@duckduckgo.com" - bundle exec fastlane update_embedded_files - run_tests: name: Run Tests - needs: update_embedded_files + needs: assert_release_branch uses: ./.github/workflows/pr.yml secrets: ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} @@ -71,10 +50,11 @@ jobs: steps: - name: Check out the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: - submodules: recursive + fetch-depth: 0 # Fetch all history and tags in order to extract Asana task URLs from git log ref: ${{ github.ref_name }} + submodules: recursive - name: Select Xcode run: sudo xcode-select -s /Applications/Xcode_$(<.xcode-version).app/Contents/Developer @@ -92,6 +72,21 @@ jobs: git config --global user.email "dax@duckduckgo.com" bundle exec fastlane bump_internal_release update_embedded_files:false + - name: Extract Asana Task ID + id: task-id + uses: ./.github/actions/asana-extract-task-id + with: + task-url: ${{ github.event.inputs.asana-task-url }} + + - name: Update Asana tasks for the release + env: + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + GH_TOKEN: ${{ github.token }} + BRANCH: ${{ github.ref_name }} + run: | + version="$(cut -d '/' -f 2 <<< "$BRANCH")" + ./scripts/update_asana_for_release.sh ${{ steps.task-id.outputs.task-id }} "${version}" ${{ vars.MACOS_APP_BOARD_VALIDATION_SECTION_ID }} + prepare_release: name: Prepare Release needs: increment_build_number @@ -119,7 +114,9 @@ jobs: MM_HANDLES_BASE64: ${{ secrets.MM_HANDLES_BASE64 }} MM_WEBHOOK_URL: ${{ secrets.MM_WEBHOOK_URL }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_ACCESS_KEY_ID_RELEASE_S3: ${{ secrets.AWS_ACCESS_KEY_ID_RELEASE_S3 }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY_RELEASE_S3: ${{ secrets.AWS_SECRET_ACCESS_KEY_RELEASE_S3 }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} SSH_PRIVATE_KEY_FASTLANE_MATCH: ${{ secrets.SSH_PRIVATE_KEY_FASTLANE_MATCH }} diff --git a/.github/workflows/code_freeze.yml b/.github/workflows/code_freeze.yml index d4f475d290..6806f405c0 100644 --- a/.github/workflows/code_freeze.yml +++ b/.github/workflows/code_freeze.yml @@ -26,8 +26,9 @@ jobs: fi - name: Check out the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: + fetch-depth: 0 # Fetch all history and tags in order to extract Asana task URLs from git log submodules: recursive - name: Prepare fastlane @@ -56,6 +57,8 @@ jobs: -H "Content-Type: application/json" \ -d "{ \"data\": { \"name\": \"$task_name\" }}" \ | jq -r .data.new_task.gid)" + echo "marketing_version=${version}" >> $GITHUB_OUTPUT + echo "asana_task_id=${asana_task_id}" >> $GITHUB_OUTPUT echo "asana_task_url=https://app.asana.com/0/0/${asana_task_id}/f" >> $GITHUB_OUTPUT curl -fLSs -X POST "https://app.asana.com/api/1.0/sections/${{ vars.MACOS_APP_DEVELOPMENT_RELEASE_SECTION_ID }}/addTask" \ @@ -73,6 +76,16 @@ jobs: --output /dev/null \ -d "{ \"data\": { \"assignee\": \"$assignee_id\" }}" + - name: Update Asana tasks for the release + env: + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + GH_TOKEN: ${{ github.token }} + run: | + ./scripts/update_asana_for_release.sh \ + ${{ steps.create_release_task.outputs.asana_task_id }} \ + ${{ steps.create_release_task.outputs.marketing_version }} \ + ${{ vars.MACOS_APP_BOARD_VALIDATION_SECTION_ID }} + run_tests: name: Run Tests @@ -95,7 +108,7 @@ jobs: steps: - name: Check out the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive ref: ${{ needs.create_release_branch.outputs.release_branch_name }} diff --git a/.github/workflows/create_variants.yml b/.github/workflows/create_variants.yml index ac303b8937..2ec2ebbc33 100644 --- a/.github/workflows/create_variants.yml +++ b/.github/workflows/create_variants.yml @@ -150,8 +150,13 @@ jobs: runs-on: ubuntu-latest + env: + success: ${{ needs.create-atb-variants.result == 'success' }} + failure: ${{ needs.create-atb-variants.result == 'failure' }} + steps: - name: Send Mattermost message + if: ${{ env.success || env.failure }} # Don't execute when cancelled env: GH_TOKEN: ${{ github.token }} WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} @@ -164,7 +169,13 @@ jobs: if [[ -z "${MM_USER_HANDLE}" ]]; then echo "Mattermost user handle not known for ${{ github.actor }}, skipping sending message" else + + if [[ "${{ env.success }}" == "true" ]]; then + status="success" + else + status="failure" + fi curl -s -H 'Content-type: application/json' \ - -d "$(envsubst < message-template.json)" \ + -d "$(envsubst < message-template.json | jq ".${status}")" \ ${{ secrets.MM_WEBHOOK_URL }} fi diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5b3ad38fbb..bb7a657499 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: SwiftLint uses: docker://norionomura/swiftlint:0.54.0 with: @@ -37,11 +37,11 @@ jobs: steps: - name: Check out the code if: github.event_name == 'pull_request' || github.event_name == 'push' - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Check out the code if: github.event_name != 'pull_request' && github.event_name != 'push' - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ inputs.branch || github.ref_name }} @@ -88,13 +88,13 @@ jobs: steps: - name: Check out the code if: github.event_name == 'pull_request' || github.event_name == 'push' - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive - name: Check out the code if: github.event_name != 'pull_request' && github.event_name != 'push' - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive ref: ${{ inputs.branch || github.ref_name }} @@ -265,7 +265,7 @@ jobs: steps: - name: Check out the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive @@ -331,7 +331,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version: 16 diff --git a/.github/workflows/publish_dmg_release.yml b/.github/workflows/publish_dmg_release.yml new file mode 100644 index 0000000000..8ccbe6905b --- /dev/null +++ b/.github/workflows/publish_dmg_release.yml @@ -0,0 +1,210 @@ +name: Publish DMG Release + +on: + workflow_dispatch: + inputs: + asana-task-url: + description: "Asana release task URL" + required: true + type: string + tag: + description: "Tag to publish" + required: true + type: string + +jobs: + + publish-to-sparkle: + + name: Publish internal release to Sparkle + + runs-on: macos-13-xlarge + timeout-minutes: 10 + + env: + SPARKLE_DIR: ${{ github.workspace }}/sparkle-updates + + steps: + + - name: Verify the tag + id: verify-tag + env: + tag: ${{ github.event.inputs.tag }} + run: | + tag_regex='^[0-9]+\.[0-9]+\.[0-9]+-[0-9]+$' + + if [[ ! "$tag" =~ $tag_regex ]]; then + echo "::error::The provided tag ($tag) has incorrect format (attempted to match ${tag_regex})." + exit 1 + fi + echo "tag-in-filename=${tag//-/.}" >> $GITHUB_OUTPUT + + - name: Check out the code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Sparkle tools + env: + SPARKLE_URL: https://github.com/sparkle-project/Sparkle/releases/download/${{ vars.SPARKLE_VERSION }}/Sparkle-${{ vars.SPARKLE_VERSION }}.tar.xz + run: | + curl -fLSs $SPARKLE_URL | tar xJ bin + echo "${{ github.workspace }}/bin" >> $GITHUB_PATH + + - name: Fetch DMG + id: fetch-dmg + env: + DMG_NAME: duckduckgo-${{ steps.verify-tag.outputs.tag-in-filename }}.dmg + run: | + DMG_URL="${{ vars.DMG_URL_ROOT }}${DMG_NAME}" + curl -fLSs -o "$DMG_NAME" "$DMG_URL" + echo "dmg-name=$DMG_NAME" >> $GITHUB_OUTPUT + echo "dmg-path=$DMG_NAME" >> $GITHUB_OUTPUT + + - name: Extract Asana Task ID + id: task-id + uses: ./.github/actions/asana-extract-task-id + with: + task-url: ${{ github.event.inputs.asana-task-url }} + + - name: Fetch release notes + env: + TASK_ID: ${{ steps.task-id.outputs.task-id }} + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + run: | + curl -fLSs "https://app.asana.com/api/1.0/tasks/${TASK_ID}?opt_fields=notes" \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + | jq -r .data.notes \ + | ./scripts/extract_release_notes.sh > release_notes.txt + echo "RELEASE_NOTES_FILE=release_notes.txt" >> $GITHUB_ENV + + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode_$(<.xcode-version).app/Contents/Developer + + - name: Generate appcast + id: appcast + env: + DMG_PATH: ${{ steps.fetch-dmg.outputs.dmg-path }} + SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} + run: | + echo -n "$SPARKLE_PRIVATE_KEY" > sparkle_private_key + chmod 600 sparkle_private_key + + ./scripts/appcast_manager/appcastManager.swift \ + --release-to-internal-channel \ + --dmg ${DMG_PATH} \ + --release-notes release_notes.txt \ + --key sparkle_private_key + + appcast_patch_name="appcast2-${{ steps.verify-tag.outputs.tag-in-filename }}.patch" + mv -f ${{ env.SPARKLE_DIR }}/appcast_diff.txt ${{ env.SPARKLE_DIR }}/${appcast_patch_name} + echo "appcast-patch-name=${appcast_patch_name}" >> $GITHUB_OUTPUT + + - name: Upload appcast diff artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.appcast.outputs.appcast-patch-name }} + path: ${{ env.SPARKLE_DIR }}/${{ steps.appcast.outputs.appcast-patch-name }} + + - name: Upload to S3 + id: upload + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_RELEASE_S3 }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_RELEASE_S3 }} + AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} + run: | + # Back up existing appcast2.xml + OLD_APPCAST_NAME=appcast2_old.xml + echo "OLD_APPCAST_NAME=${OLD_APPCAST_NAME}" >> $GITHUB_ENV + curl -fLSs "${{ vars.DMG_URL_ROOT }}appcast2.xml" --output "${OLD_APPCAST_NAME}" + + # Upload files to S3 + ./scripts/upload_to_s3/upload_to_s3.sh --run --force + + if [[ -f "${{ env.SPARKLE_DIR }}/uploaded_files_list.txt" ]]; then + echo "FILES_UPLOADED=$(awk '{ print "
  • "$1"
  • "; }' < ${{ env.SPARKLE_DIR }}/uploaded_files_list.txt | tr '\n' ' ')" >> $GITHUB_ENV + else + echo "FILES_UPLOADED='No files uploaded.'" >> $GITHUB_ENV + fi + + - name: Set common environment variables + if: always() + env: + DMG_NAME: ${{ steps.fetch-dmg.outputs.dmg-name }} + run: | + echo "APPCAST_PATCH_NAME=${{ steps.appcast.outputs.appcast-patch-name }}" >> $GITHUB_ENV + echo "DMG_NAME=${DMG_NAME}" >> $GITHUB_ENV + echo "DMG_URL=${{ vars.DMG_URL_ROOT }}${DMG_NAME}" >> $GITHUB_ENV + echo "RELEASE_BUCKET_NAME=${{ vars.RELEASE_BUCKET_NAME }}" >> $GITHUB_ENV + echo "RELEASE_BUCKET_PREFIX=${{ vars.RELEASE_BUCKET_PREFIX }}" >> $GITHUB_ENV + echo "RELEASE_TASK_ID=${{ steps.task-id.outputs.task-id }}" >> $GITHUB_ENV + echo "TAG=${{ github.event.inputs.tag }}" >> $GITHUB_ENV + echo "WORKFLOW_URL=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_ENV + + - name: Set up Asana templates + if: always() + id: asana-templates + run: | + if [[ ${{ steps.upload.outcome }} == "success" ]]; then + echo "task-template=validate-check-for-updates-internal" >> $GITHUB_OUTPUT + echo "comment-template=validate-check-for-updates-internal" >> $GITHUB_OUTPUT + echo "release-task-comment-template=internal-release-complete" >> $GITHUB_OUTPUT + else + echo "task-template=appcast-failed" >> $GITHUB_OUTPUT + echo "comment-template=appcast-failed" >> $GITHUB_OUTPUT + fi + + - name: Create Asana task + id: create-task + if: always() + uses: ./.github/actions/asana-create-action-item + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + release-task-url: ${{ github.event.inputs.asana-task-url }} + template-name: ${{ steps.asana-templates.outputs.task-template }} + + - name: Upload patch to the Asana task + id: upload-patch + if: success() + uses: ./.github/actions/asana-upload + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + file-name: ${{ env.SPARKLE_DIR }}/${{ steps.appcast.outputs.appcast-patch-name }} + task-id: ${{ steps.create-task.outputs.new-task-id }} + + - name: Upload old appcast file to the Asana task + id: upload-old-appcast + if: success() + uses: ./.github/actions/asana-upload + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + file-name: ${{ env.OLD_APPCAST_NAME }} + task-id: ${{ steps.create-task.outputs.new-task-id }} + + - name: Upload release notes to the Asana task + id: upload-release-notes + if: success() + uses: ./.github/actions/asana-upload + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + file-name: ${{ env.RELEASE_NOTES_FILE }} + task-id: ${{ steps.create-task.outputs.new-task-id }} + + - name: Report status + if: always() + uses: ./.github/actions/asana-log-message + env: + ASSIGNEE_ID: ${{ steps.create-task.outputs.assignee-id }} + TASK_ID: ${{ steps.create-task.outputs.new-task-id }} + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + task-url: ${{ github.event.inputs.asana-task-url }} + template-name: ${{ steps.asana-templates.outputs.comment-template }} + + - name: Add a comment to the release task + if: success() + uses: ./.github/actions/asana-add-comment + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + task-url: ${{ github.event.inputs.asana-task-url }} + template-name: internal-release-complete diff --git a/.github/workflows/sparkle_internal.yml b/.github/workflows/sparkle_internal.yml new file mode 100644 index 0000000000..bfc7293c92 --- /dev/null +++ b/.github/workflows/sparkle_internal.yml @@ -0,0 +1,210 @@ +name: Publish Internal Release to Sparkle + +on: + workflow_dispatch: + inputs: + asana-task-url: + description: "Asana release task URL" + required: true + type: string + tag: + description: "Tag to publish" + required: true + type: string + +jobs: + + publish-to-sparkle: + + name: Publish internal release to Sparkle + + runs-on: macos-13-xlarge + timeout-minutes: 10 + + env: + SPARKLE_DIR: ${{ github.workspace }}/sparkle-updates + + steps: + + - name: Verify the tag + id: verify-tag + env: + tag: ${{ github.event.inputs.tag }} + run: | + tag_regex='^[0-9]+\.[0-9]+\.[0-9]+-[0-9]+$' + + if [[ ! "$tag" =~ $tag_regex ]]; then + echo "::error::The provided tag ($tag) has incorrect format (attempted to match ${tag_regex})." + exit 1 + fi + echo "tag-in-filename=${tag//-/.}" >> $GITHUB_OUTPUT + + - name: Check out the code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Sparkle tools + env: + SPARKLE_URL: https://github.com/sparkle-project/Sparkle/releases/download/${{ vars.SPARKLE_VERSION }}/Sparkle-${{ vars.SPARKLE_VERSION }}.tar.xz + run: | + curl -fLSs $SPARKLE_URL | tar xJ bin + echo "${{ github.workspace }}/bin" >> $GITHUB_PATH + + - name: Fetch DMG + id: fetch-dmg + env: + DMG_NAME: duckduckgo-${{ steps.verify-tag.outputs.tag-in-filename }}.dmg + run: | + DMG_URL="${{ vars.DMG_URL_ROOT }}${DMG_NAME}" + curl -fLSs -o "$DMG_NAME" "$DMG_URL" + echo "dmg-name=$DMG_NAME" >> $GITHUB_OUTPUT + echo "dmg-path=$DMG_NAME" >> $GITHUB_OUTPUT + + - name: Extract Asana Task ID + id: task-id + uses: ./.github/actions/asana-extract-task-id + with: + task-url: ${{ github.event.inputs.asana-task-url }} + + - name: Fetch release notes + env: + TASK_ID: ${{ steps.task-id.outputs.task-id }} + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + run: | + curl -fLSs "https://app.asana.com/api/1.0/tasks/${TASK_ID}?opt_fields=notes" \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + | jq -r .data.notes \ + | ./scripts/extract_release_notes.sh > release_notes.txt + echo "RELEASE_NOTES_FILE=release_notes.txt" >> $GITHUB_ENV + + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode_$(<.xcode-version).app/Contents/Developer + + - name: Generate appcast + id: appcast + env: + DMG_PATH: ${{ steps.fetch-dmg.outputs.dmg-path }} + SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} + run: | + echo -n "$SPARKLE_PRIVATE_KEY" > sparkle_private_key + chmod 600 sparkle_private_key + + ./scripts/appcast_manager/appcastManager.swift \ + --release-to-internal-channel \ + --dmg ${DMG_PATH} \ + --release-notes release_notes.txt \ + --key sparkle_private_key + + appcast_patch_name="appcast2-${{ steps.verify-tag.outputs.tag-in-filename }}.patch" + mv -f ${{ env.SPARKLE_DIR }}/appcast_diff.txt ${{ env.SPARKLE_DIR }}/${appcast_patch_name} + echo "appcast-patch-name=${appcast_patch_name}" >> $GITHUB_OUTPUT + + - name: Upload appcast diff artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.appcast.outputs.appcast-patch-name }} + path: ${{ env.SPARKLE_DIR }}/${{ steps.appcast.outputs.appcast-patch-name }} + + - name: Upload to S3 + id: upload + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} + run: | + # Back up existing appcast2.xml + OLD_APPCAST_NAME=appcast2_old.xml + echo "OLD_APPCAST_NAME=${OLD_APPCAST_NAME}" >> $GITHUB_ENV + curl -fLSs "${{ vars.DMG_URL_ROOT }}appcast2.xml" --output "${OLD_APPCAST_NAME}" + + # Upload files to S3 + ./scripts/upload_to_s3/upload_to_s3.sh --run --force + + if [[ -f "${{ env.SPARKLE_DIR }}/uploaded_files_list.txt" ]]; then + echo "FILES_UPLOADED=$(awk '{ print "
  • "$1"
  • "; }' < ${{ env.SPARKLE_DIR }}/uploaded_files_list.txt | tr '\n' ' ')" >> $GITHUB_ENV + else + echo "FILES_UPLOADED='No files uploaded.'" >> $GITHUB_ENV + fi + + - name: Set common environment variables + if: always() + env: + DMG_NAME: ${{ steps.fetch-dmg.outputs.dmg-name }} + run: | + echo "APPCAST_PATCH_NAME=${{ steps.appcast.outputs.appcast-patch-name }}" >> $GITHUB_ENV + echo "DMG_NAME=${DMG_NAME}" >> $GITHUB_ENV + echo "DMG_URL=${{ vars.DMG_URL_ROOT }}${DMG_NAME}" >> $GITHUB_ENV + echo "RELEASE_BUCKET_NAME=${{ vars.RELEASE_BUCKET_NAME }}" >> $GITHUB_ENV + echo "RELEASE_BUCKET_PREFIX=${{ vars.RELEASE_BUCKET_PREFIX }}" >> $GITHUB_ENV + echo "RELEASE_TASK_ID=${{ steps.task-id.outputs.task-id }}" >> $GITHUB_ENV + echo "TAG=${{ github.event.inputs.tag }}" >> $GITHUB_ENV + echo "WORKFLOW_URL=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_ENV + + - name: Set up Asana templates + if: always() + id: asana-templates + run: | + if [[ ${{ steps.upload.outcome }} == "success" ]]; then + echo "task-template=validate-check-for-updates-internal" >> $GITHUB_OUTPUT + echo "comment-template=validate-check-for-updates-internal" >> $GITHUB_OUTPUT + echo "release-task-comment-template=internal-release-complete" >> $GITHUB_OUTPUT + else + echo "task-template=appcast-failed" >> $GITHUB_OUTPUT + echo "comment-template=appcast-failed" >> $GITHUB_OUTPUT + fi + + - name: Create Asana task + id: create-task + if: always() + uses: ./.github/actions/asana-create-action-item + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + release-task-url: ${{ github.event.inputs.asana-task-url }} + template-name: ${{ steps.asana-templates.outputs.task-template }} + + - name: Upload patch to the Asana task + id: upload-patch + if: success() + uses: ./.github/actions/asana-upload + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + file-name: ${{ env.SPARKLE_DIR }}/${{ steps.appcast.outputs.appcast-patch-name }} + task-id: ${{ steps.create-task.outputs.new-task-id }} + + - name: Upload old appcast file to the Asana task + id: upload-old-appcast + if: success() + uses: ./.github/actions/asana-upload + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + file-name: ${{ env.OLD_APPCAST_NAME }} + task-id: ${{ steps.create-task.outputs.new-task-id }} + + - name: Upload release notes to the Asana task + id: upload-release-notes + if: success() + uses: ./.github/actions/asana-upload + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + file-name: ${{ env.RELEASE_NOTES_FILE }} + task-id: ${{ steps.create-task.outputs.new-task-id }} + + - name: Report status + if: always() + uses: ./.github/actions/asana-log-message + env: + ASSIGNEE_ID: ${{ steps.create-task.outputs.assignee-id }} + TASK_ID: ${{ steps.create-task.outputs.new-task-id }} + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + task-url: ${{ github.event.inputs.asana-task-url }} + template-name: ${{ steps.asana-templates.outputs.comment-template }} + + - name: Add a comment to the release task + if: success() + uses: ./.github/actions/asana-add-comment + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + task-url: ${{ github.event.inputs.asana-task-url }} + template-name: internal-release-complete diff --git a/.github/workflows/tag_and_merge.yml b/.github/workflows/tag_and_merge.yml index 8798981f8b..4004a55ac0 100644 --- a/.github/workflows/tag_and_merge.yml +++ b/.github/workflows/tag_and_merge.yml @@ -84,10 +84,11 @@ jobs: if: always() env: GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.create-tag.outputs.tag }} run: | - echo "TAG=${{ steps.create-tag.outputs.tag }}" >> $GITHUB_ENV + echo "TAG=$TAG" >> $GITHUB_ENV echo "WORKFLOW_URL=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_ENV - echo "DMG_URL=${{ vars.DMG_URL_ROOT }}duckduckgo-${{ steps.create-tag.outputs.tag }}.dmg" >> $GITHUB_ENV + echo "DMG_URL=${{ vars.DMG_URL_ROOT }}duckduckgo-${TAG//-/.}.dmg" >> $GITHUB_ENV echo "RELEASE_URL=https://github.com/${{ github.repository }}/releases/tag/${{ steps.create-tag.outputs.tag }}" >> $GITHUB_ENV if [[ ${{ steps.create-tag.outputs.tag-created }} == "false" ]]; then last_release_tag=$(gh api /repos/${{ github.repository }}/releases/latest --jq '.tag_name') diff --git a/Configuration/App/DuckDuckGoAppStore.xcconfig b/Configuration/App/DuckDuckGoAppStore.xcconfig index 3ee212ad5e..904caca8c5 100644 --- a/Configuration/App/DuckDuckGoAppStore.xcconfig +++ b/Configuration/App/DuckDuckGoAppStore.xcconfig @@ -17,11 +17,6 @@ #include "../AppStore.xcconfig" #include "ManualAppStoreRelease.xcconfig" -AGENT_BUNDLE_ID[sdk=*] = com.duckduckgo.mobile.ios.vpn.agent -AGENT_BUNDLE_ID[config=Debug][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug -AGENT_BUNDLE_ID[config=CI][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug -AGENT_BUNDLE_ID[config=Review][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.review - PRODUCT_BUNDLE_IDENTIFIER = $(MAIN_BUNDLE_IDENTIFIER) CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAppStore.entitlements diff --git a/Configuration/AppStore.xcconfig b/Configuration/AppStore.xcconfig index af55b7fa5d..0ad3f9f6b5 100644 --- a/Configuration/AppStore.xcconfig +++ b/Configuration/AppStore.xcconfig @@ -31,9 +31,9 @@ GCC_PREPROCESSOR_DEFINITIONS[config=Review][arch=*][sdk=*] = APPSTORE=1 REVIEW=1 MACOSX_DEPLOYMENT_TARGET = 12.3 SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*][sdk=*] = APPSTORE $(FEATURE_FLAGS) -SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=CI][arch=*][sdk=*] = APPSTORE DEBUG NETWORK_PROTECTION DBP CI $(FEATURE_FLAGS) -SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug][arch=*][sdk=*] = APPSTORE DEBUG NETWORK_PROTECTION DBP $(FEATURE_FLAGS) -SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Review][arch=*][sdk=*] = APPSTORE REVIEW NETWORK_PROTECTION DBP $(FEATURE_FLAGS) +SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=CI][arch=*][sdk=*] = APPSTORE DEBUG CI $(FEATURE_FLAGS) +SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug][arch=*][sdk=*] = APPSTORE DEBUG $(FEATURE_FLAGS) +SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Review][arch=*][sdk=*] = APPSTORE REVIEW $(FEATURE_FLAGS) NETP_BASE_APP_GROUP = $(DEVELOPMENT_TEAM).com.duckduckgo.macos.browser.network-protection NETP_APP_GROUP[config=CI][sdk=macos*] = $(NETP_BASE_APP_GROUP).debug @@ -50,21 +50,23 @@ AGENT_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).review AGENT_PRODUCT_NAME = DuckDuckGo VPN App Store AGENT_RELEASE_PRODUCT_NAME = DuckDuckGo VPN -SYSEX_BUNDLE_ID[sdk=*] = com.duckduckgo.mobile.ios.vpn.agent -SYSEX_BUNDLE_ID[config=Debug][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug.network-protection-extension -SYSEX_BUNDLE_ID[config=CI][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug.network-protection-extension -SYSEX_BUNDLE_ID[config=Review][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension -SYSEX_BUNDLE_ID[config=Release][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension +// Extensions -// Distributed Notifications Prefix +PROXY_EXTENSION_BUNDLE_ID[sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(AGENT_BUNDLE_ID).proxy + +TUNNEL_EXTENSION_BUNDLE_ID[sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension -SYSEX_BUNDLE_ID_BASE[sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Release][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension +// Distributed Notifications Prefix -DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE = $(SYSEX_BUNDLE_ID_BASE) +DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE = $(AGENT_BUNDLE_ID_BASE).network-extension DISTRIBUTED_NOTIFICATIONS_PREFIX[config=CI][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).ci DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Review][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).review diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 321e492669..550b71a3ee 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 118 +CURRENT_PROJECT_VERSION = 123 diff --git a/Configuration/Common.xcconfig b/Configuration/Common.xcconfig index e83abad09c..5c38d4e8e4 100644 --- a/Configuration/Common.xcconfig +++ b/Configuration/Common.xcconfig @@ -21,7 +21,7 @@ COMBINE_HIDPI_IMAGES = YES DEVELOPMENT_TEAM = HKE973VLUW DEVELOPMENT_TEAM[config=CI][sdk=*] = -FEATURE_FLAGS = FEEDBACK +FEATURE_FLAGS = FEEDBACK DBP NETWORK_PROTECTION GCC_PREPROCESSOR_DEFINITIONS[config=CI][arch=*][sdk=*] = DEBUG=1 CI=1 $(inherited) GCC_PREPROCESSOR_DEFINITIONS[config=Debug][arch=*][sdk=*] = DEBUG=1 $(inherited) diff --git a/Configuration/DeveloperID.xcconfig b/Configuration/DeveloperID.xcconfig index 0bfb9bb8cb..b66acc76d2 100644 --- a/Configuration/DeveloperID.xcconfig +++ b/Configuration/DeveloperID.xcconfig @@ -65,6 +65,20 @@ AGENT_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).debug AGENT_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).review AGENT_PRODUCT_NAME = DuckDuckGo VPN +// Extensions + +PROXY_EXTENSION_BUNDLE_ID[sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(SYSEX_BUNDLE_ID) + +TUNNEL_EXTENSION_BUNDLE_ID[sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(SYSEX_BUNDLE_ID) + // DBP DBP_BACKGROUND_AGENT_PRODUCT_NAME = DuckDuckGo Personal Information Removal diff --git a/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig b/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig index 60ad407569..2fb095fc56 100644 --- a/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig +++ b/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig @@ -14,10 +14,7 @@ // #include "../ExtensionBase.xcconfig" - -// Since we're using nonstandard bundle IDs we'll just define them here, but we should consider -// standardizing the bundle IDs so we can just define BUNDLE_IDENTIFIER_PREFIX -BUNDLE_IDENTIFIER_PREFIX = com.duckduckgo.mobile.ios.vpn.agent +#include "../../AppStore.xcconfig" CODE_SIGN_ENTITLEMENTS[config=CI][sdk=macosx*] = CODE_SIGN_ENTITLEMENTS[config=Debug][sdk=macosx*] = DuckDuckGo/NetworkProtectionAppExtension.entitlements @@ -38,17 +35,11 @@ FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION -NETP_BASE_APP_GROUP = $(DEVELOPMENT_TEAM).com.duckduckgo.macos.browser.network-protection -NETP_APP_GROUP[config=CI][sdk=macos*] = $(NETP_BASE_APP_GROUP).debug -NETP_APP_GROUP[config=Review][sdk=macos*] = $(NETP_BASE_APP_GROUP).review -NETP_APP_GROUP[config=Debug][sdk=macos*] = $(NETP_BASE_APP_GROUP).debug -NETP_APP_GROUP[config=Release][sdk=macos*] = $(NETP_BASE_APP_GROUP) - PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = -PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).debug.network-protection-extension -PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).debug.network-protection-extension -PRODUCT_BUNDLE_IDENTIFIER[config=Release][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).network-protection-extension -PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).review.network-protection-extension +PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Release][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) PROVISIONING_PROFILE_SPECIFIER[config=CI][sdk=macosx*] = PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = match AppStore com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension macos @@ -59,24 +50,3 @@ SKIP_INSTALL = YES SWIFT_EMIT_LOC_STRINGS = YES LD_RUNPATH_SEARCH_PATHS = @executable_path/../Frameworks @executable_path/../../../../Frameworks - -// Distributed Notifications: - -AGENT_BUNDLE_ID_BASE[sdk=*] = com.duckduckgo.mobile.ios.vpn.agent -AGENT_BUNDLE_ID[sdk=*] = $(AGENT_BUNDLE_ID_BASE) -AGENT_BUNDLE_ID[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID_BASE).debug -AGENT_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).debug -AGENT_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).review - -SYSEX_BUNDLE_ID_BASE[sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Release][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension - -DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE = $(SYSEX_BUNDLE_ID_BASE) - -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=CI][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).ci -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Review][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).review -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Debug][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).debug -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Release][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE) diff --git a/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig b/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig new file mode 100644 index 0000000000..5f70d87091 --- /dev/null +++ b/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig @@ -0,0 +1,52 @@ +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "../ExtensionBase.xcconfig" +#include "../../AppStore.xcconfig" + +CODE_SIGN_ENTITLEMENTS[config=CI][sdk=macosx*] = +CODE_SIGN_ENTITLEMENTS[config=Debug][sdk=macosx*] = VPNProxyExtension/VPNProxyExtension.entitlements +CODE_SIGN_ENTITLEMENTS[config=Release][sdk=macosx*] = VPNProxyExtension/VPNProxyExtension.entitlements +CODE_SIGN_ENTITLEMENTS[config=Review][sdk=macosx*] = VPNProxyExtension/VPNProxyExtension.entitlements +CODE_SIGN_STYLE[config=Debug][sdk=*] = Automatic + +CODE_SIGN_IDENTITY[sdk=macosx*] = 3rd Party Mac Developer Application +CODE_SIGN_IDENTITY[config=Debug][sdk=macosx*] = Apple Development +CODE_SIGN_IDENTITY[config=CI][sdk=macosx*] = + +GENERATE_INFOPLIST_FILE = YES +INFOPLIST_FILE = VPNProxyExtension/Info.plist +INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2023 DuckDuckGo. All rights reserved. + +FEATURE_FLAGS[arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION + +PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = +PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Release][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) + +PROVISIONING_PROFILE_SPECIFIER[config=CI][sdk=macosx*] = +PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = match AppStore $(AGENT_BUNDLE_ID).proxy macos +PROVISIONING_PROFILE_SPECIFIER[config=Review][sdk=macosx*] = match AppStore $(AGENT_BUNDLE_ID).proxy macos + +SDKROOT = macosx +SKIP_INSTALL = YES +SWIFT_EMIT_LOC_STRINGS = YES + +LD_RUNPATH_SEARCH_PATHS = @executable_path/../Frameworks @executable_path/../../../../Frameworks diff --git a/Configuration/Tests/IntegrationTests.xcconfig b/Configuration/Tests/IntegrationTests.xcconfig index 6100fbe474..cee1523e37 100644 --- a/Configuration/Tests/IntegrationTests.xcconfig +++ b/Configuration/Tests/IntegrationTests.xcconfig @@ -22,4 +22,6 @@ FEATURE_FLAGS = FEEDBACK NETWORK_PROTECTION DBP INFOPLIST_FILE = IntegrationTests/Info.plist PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.Integration-Tests +SWIFT_OBJC_BRIDGING_HEADER = $(SRCROOT)/IntegrationTests/Common/IntegrationTestsBridging.h + TEST_HOST=$(BUILT_PRODUCTS_DIR)/DuckDuckGo.app/Contents/MacOS/DuckDuckGo diff --git a/Configuration/Tests/UnitTestsAppStore.xcconfig b/Configuration/Tests/UnitTestsAppStore.xcconfig index fb90843360..a885a868ed 100644 --- a/Configuration/Tests/UnitTestsAppStore.xcconfig +++ b/Configuration/Tests/UnitTestsAppStore.xcconfig @@ -16,7 +16,7 @@ #include "UnitTests.xcconfig" #include "../AppStore.xcconfig" -FEATURE_FLAGS = FEEDBACK +FEATURE_FLAGS = FEEDBACK NETWORK_PROTECTION DBP PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.mobile.ios.DuckDuckGoTests diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 39ca579f82..bb31eb4eda 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.75.0 +MARKETING_VERSION = 1.76.0 diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 834ada47ce..cc17b146e8 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -265,7 +265,7 @@ 3706FAE1293F65D500E42796 /* HomePageFavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85589E8627BBB8F20038AD11 /* HomePageFavoritesModel.swift */; }; 3706FAE2293F65D500E42796 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4925B7B690006F6B06 /* SequenceExtensions.swift */; }; 3706FAE3293F65D500E42796 /* ChromiumDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B59023B26B35F3600489384 /* ChromiumDataImporter.swift */; }; - 3706FAE5293F65D500E42796 /* WKBackForwardListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC3B25337FAB0079BC96 /* WKBackForwardListItemViewModel.swift */; }; + 3706FAE5293F65D500E42796 /* BackForwardListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC3B25337FAB0079BC96 /* BackForwardListItemViewModel.swift */; }; 3706FAE6293F65D500E42796 /* BWNotRespondingAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D43EB3329297D760065E5D6 /* BWNotRespondingAlert.swift */; }; 3706FAE7293F65D500E42796 /* DebugUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4425B7B55C006F6B06 /* DebugUserScript.swift */; }; 3706FAE8293F65D500E42796 /* RecentlyClosedTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC6881828626BF800D54247 /* RecentlyClosedTab.swift */; }; @@ -666,7 +666,6 @@ 3706FCC0293F65D500E42796 /* FindInPage.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85A0117325AF2EDF00FA6A0C /* FindInPage.storyboard */; }; 3706FCC3293F65D500E42796 /* userscript.js in Resources */ = {isa = PBXBuildFile; fileRef = B31055BE27A1BA1D001AC618 /* userscript.js */; }; 3706FCC4293F65D500E42796 /* fb-tds.json in Resources */ = {isa = PBXBuildFile; fileRef = EA4617EF273A28A700F110A2 /* fb-tds.json */; }; - 3706FCC5293F65D500E42796 /* TabPreview.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AAE8B101258A41C000E81239 /* TabPreview.storyboard */; }; 3706FCC6293F65D500E42796 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = AA68C3D62490F821001B8783 /* README.md */; }; 3706FCC8293F65D500E42796 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA585D85248FD31400E9A3E2 /* Assets.xcassets */; }; 3706FCC9293F65D500E42796 /* NavigationBar.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85589E8C27BBBB870038AD11 /* NavigationBar.storyboard */; }; @@ -699,7 +698,6 @@ 3706FCEF293F65D500E42796 /* macos-config.json in Resources */ = {isa = PBXBuildFile; fileRef = 026ADE1326C3010C002518EE /* macos-config.json */; }; 3706FCF0293F65D500E42796 /* httpsMobileV2BloomSpec.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B677427255DBEB800025BD8 /* httpsMobileV2BloomSpec.json */; }; 3706FCF1293F65D500E42796 /* TabBarFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA2CB12C2587BB5600AA6FBE /* TabBarFooter.xib */; }; - 3706FCF2293F65D500E42796 /* BrowserTab.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA80EC69256C4691007083E7 /* BrowserTab.storyboard */; }; 3706FCF3293F65D500E42796 /* FirePopoverCollectionViewItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = AAE246F22709EF3B00BEEAEE /* FirePopoverCollectionViewItem.xib */; }; 3706FCF4293F65D500E42796 /* ProximaNova-Bold-webfont.woff2 in Resources */ = {isa = PBXBuildFile; fileRef = EAA29AE7278D2E43007070CF /* ProximaNova-Bold-webfont.woff2 */; }; 3706FCF5293F65D500E42796 /* dark-shield-dot.json in Resources */ = {isa = PBXBuildFile; fileRef = AA34396E2754D4E900B241FA /* dark-shield-dot.json */; }; @@ -1101,12 +1099,11 @@ 4B25375B2A11BE7300610219 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; 4B2537722A11BF8B00610219 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B25376F2A11BF8B00610219 /* main.swift */; }; 4B2537772A11BFE100610219 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2537762A11BFE100610219 /* PixelKit */; }; - 4B2537782A11C00F00610219 /* NetworkProtectionExtensionMachService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */; }; 4B25377A2A11C01700610219 /* UserText+NetworkProtectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */; }; 4B29759728281F0900187C4E /* FirefoxEncryptionKeyReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B29759628281F0900187C4E /* FirefoxEncryptionKeyReader.swift */; }; 4B2975992828285900187C4E /* FirefoxKeyReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2975982828285900187C4E /* FirefoxKeyReaderTests.swift */; }; 4B2AAAF529E70DEA0026AFC0 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2AAAF429E70DEA0026AFC0 /* Lottie */; }; - 4B2D06292A11C0C900DE1F49 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B2D06292A11C0C900DE1F49 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B2D062A2A11C0C900DE1F49 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 4B2D062C2A11C0E100DE1F49 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2D062B2A11C0E100DE1F49 /* Networking */; }; 4B2D062D2A11C12300DE1F49 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; @@ -1170,17 +1167,16 @@ 4B44FEF52B1FEF5A000619D8 /* FocusableTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B44FEF22B1FEF5A000619D8 /* FocusableTextEditor.swift */; }; 4B4BEC3D2A11B56B001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4BEC382A11B509001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift */; }; 4B4BEC3E2A11B56E001D9AC5 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4BEC322A11B509001D9AC5 /* Logging.swift */; }; - 4B4BEC402A11B5B5001D9AC5 /* NetworkProtectionExtensionMachService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */; }; 4B4BEC412A11B5BD001D9AC5 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */; }; 4B4BEC422A11B5C7001D9AC5 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; - 4B4BEC432A11B5C7001D9AC5 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B4BEC432A11B5C7001D9AC5 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B4BEC452A11B5EE001D9AC5 /* UserText+NetworkProtectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */; }; 4B4BEC482A11B61F001D9AC5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B4BEC342A11B509001D9AC5 /* Assets.xcassets */; }; 4B4D603F2A0B290200BCD287 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; 4B4D60892A0B2A1C00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */; }; 4B4D60982A0B2A5C00BCD287 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4B4D60972A0B2A5C00BCD287 /* PixelKit */; }; 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; - 4B4D60A02A0B2D5B00BCD287 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B4D60A02A0B2D5B00BCD287 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 4B4D60A52A0B2EC000BCD287 /* UserText+NetworkProtectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */; }; 4B4D60AC2A0C804B00BCD287 /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; @@ -1204,7 +1200,7 @@ 4B4D60D42A0C84F700BCD287 /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; 4B4D60DD2A0C875E00BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 4B4D60DF2A0C875F00BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; - 4B4D60E02A0C875F00BCD287 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B4D60E22A0C883A00BCD287 /* AppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60E12A0C883A00BCD287 /* AppMain.swift */; }; 4B4D60E32A0C883A00BCD287 /* AppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60E12A0C883A00BCD287 /* AppMain.swift */; }; 4B4F72EC266B2ED300814C60 /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4F72EB266B2ED300814C60 /* CollectionExtension.swift */; }; @@ -1402,7 +1398,7 @@ 4B9579BB2AC7AE700062CA31 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4925B7B690006F6B06 /* SequenceExtensions.swift */; }; 4B9579BC2AC7AE700062CA31 /* WKBackForwardListExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B602E7CE2A93A5FF00F12201 /* WKBackForwardListExtension.swift */; }; 4B9579BD2AC7AE700062CA31 /* ChromiumDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B59023B26B35F3600489384 /* ChromiumDataImporter.swift */; }; - 4B9579BE2AC7AE700062CA31 /* WKBackForwardListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC3B25337FAB0079BC96 /* WKBackForwardListItemViewModel.swift */; }; + 4B9579BE2AC7AE700062CA31 /* BackForwardListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC3B25337FAB0079BC96 /* BackForwardListItemViewModel.swift */; }; 4B9579BF2AC7AE700062CA31 /* BWNotRespondingAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D43EB3329297D760065E5D6 /* BWNotRespondingAlert.swift */; }; 4B9579C02AC7AE700062CA31 /* DebugUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4425B7B55C006F6B06 /* DebugUserScript.swift */; }; 4B9579C12AC7AE700062CA31 /* RecentlyClosedTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC6881828626BF800D54247 /* RecentlyClosedTab.swift */; }; @@ -1530,7 +1526,7 @@ 4B957A422AC7AE700062CA31 /* SafariFaviconsReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0AACAD28BC6FD0001038AC /* SafariFaviconsReader.swift */; }; 4B957A432AC7AE700062CA31 /* NSScreenExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B3E0DC2657E9CF0040E0A2 /* NSScreenExtension.swift */; }; 4B957A442AC7AE700062CA31 /* NSBezierPathExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65E6B9F26D9F10600095F96 /* NSBezierPathExtension.swift */; }; - 4B957A452AC7AE700062CA31 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B957A452AC7AE700062CA31 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B957A462AC7AE700062CA31 /* WebsiteDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6820E325502F19005ED0D5 /* WebsiteDataStore.swift */; }; 4B957A472AC7AE700062CA31 /* NetworkProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */; }; 4B957A482AC7AE700062CA31 /* PermissionContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C852926942AC90048FEBE /* PermissionContextMenu.swift */; }; @@ -1951,7 +1947,6 @@ 4B957BFD2AC7AE700062CA31 /* JSAlert.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = EEC111E3294D06020086524F /* JSAlert.storyboard */; }; 4B957C002AC7AE700062CA31 /* userscript.js in Resources */ = {isa = PBXBuildFile; fileRef = B31055BE27A1BA1D001AC618 /* userscript.js */; }; 4B957C012AC7AE700062CA31 /* fb-tds.json in Resources */ = {isa = PBXBuildFile; fileRef = EA4617EF273A28A700F110A2 /* fb-tds.json */; }; - 4B957C022AC7AE700062CA31 /* TabPreview.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AAE8B101258A41C000E81239 /* TabPreview.storyboard */; }; 4B957C032AC7AE700062CA31 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = AA68C3D62490F821001B8783 /* README.md */; }; 4B957C042AC7AE700062CA31 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA585D85248FD31400E9A3E2 /* Assets.xcassets */; }; 4B957C052AC7AE700062CA31 /* NavigationBar.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85589E8C27BBBB870038AD11 /* NavigationBar.storyboard */; }; @@ -1984,7 +1979,6 @@ 4B957C282AC7AE700062CA31 /* macos-config.json in Resources */ = {isa = PBXBuildFile; fileRef = 026ADE1326C3010C002518EE /* macos-config.json */; }; 4B957C292AC7AE700062CA31 /* httpsMobileV2BloomSpec.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B677427255DBEB800025BD8 /* httpsMobileV2BloomSpec.json */; }; 4B957C2A2AC7AE700062CA31 /* TabBarFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA2CB12C2587BB5600AA6FBE /* TabBarFooter.xib */; }; - 4B957C2B2AC7AE700062CA31 /* BrowserTab.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA80EC69256C4691007083E7 /* BrowserTab.storyboard */; }; 4B957C2C2AC7AE700062CA31 /* FirePopoverCollectionViewItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = AAE246F22709EF3B00BEEAEE /* FirePopoverCollectionViewItem.xib */; }; 4B957C2D2AC7AE700062CA31 /* ProximaNova-Bold-webfont.woff2 in Resources */ = {isa = PBXBuildFile; fileRef = EAA29AE7278D2E43007070CF /* ProximaNova-Bold-webfont.woff2 */; }; 4B957C2E2AC7AE700062CA31 /* dark-shield-dot.json in Resources */ = {isa = PBXBuildFile; fileRef = AA34396E2754D4E900B241FA /* dark-shield-dot.json */; }; @@ -2053,7 +2047,7 @@ 4BA7C4DA2B3F639800AFE511 /* NetworkProtectionTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8F52402A18326600BE7131 /* NetworkProtectionTunnelController.swift */; }; 4BA7C4DB2B3F63AE00AFE511 /* NetworkExtensionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */; }; 4BA7C4DD2B3F64E500AFE511 /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 4BA7C4DC2B3F64E500AFE511 /* LoginItems */; }; - 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in CopyFiles */ = {isa = PBXBuildFile; fileRef = 4B4D603D2A0B290200BCD287 /* NetworkProtectionAppExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in Embed Network Extensions */ = {isa = PBXBuildFile; fileRef = 4B4D603D2A0B290200BCD287 /* NetworkProtectionAppExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4BB6CE5F26B77ED000EC5860 /* Cryptography.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB6CE5E26B77ED000EC5860 /* Cryptography.swift */; }; 4BB88B4525B7B55C006F6B06 /* DebugUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4425B7B55C006F6B06 /* DebugUserScript.swift */; }; 4BB88B4A25B7B690006F6B06 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4925B7B690006F6B06 /* SequenceExtensions.swift */; }; @@ -2137,7 +2131,7 @@ 4BF97AD62B43C45800EB4240 /* NetworkProtectionNavBarPopoverManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3618C12ADE75C8000D6154 /* NetworkProtectionNavBarPopoverManager.swift */; }; 4BF97AD72B43C53D00EB4240 /* NetworkProtectionIPCTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */; }; 4BF97AD82B43C5B300EB4240 /* NetworkProtectionAppEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2DDCF72A93A8BB0039D884 /* NetworkProtectionAppEvents.swift */; }; - 4BF97AD92B43C5C000EB4240 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4BF97AD92B43C5C000EB4240 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4BF97ADA2B43C5DC00EB4240 /* VPNFeedbackCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0526632B1D55D80054955A /* VPNFeedbackCategory.swift */; }; 4BF97ADB2B43C5E000EB4240 /* VPNMetadataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B05265D2B1AE5C70054955A /* VPNMetadataCollector.swift */; }; 4BF97ADC2B43C5E200EB4240 /* VPNFeedbackSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0526602B1D55320054955A /* VPNFeedbackSender.swift */; }; @@ -2178,6 +2172,14 @@ 56D145F229E6F06D00E3488A /* MockBookmarkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D145F029E6F06D00E3488A /* MockBookmarkManager.swift */; }; 56D6A3D629DB2BAB0055215A /* ContinueSetUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */; }; 56D6A3D729DB2BAB0055215A /* ContinueSetUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */; }; + 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */; }; + 7B00997D2B6508B700FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */; }; + 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */; }; + 7B0099822B65C6B300FE7C31 /* MacTransparentProxyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */; }; + 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */; }; + 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */; }; + 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */; }; + 7B1459572B7D43E500047F2C /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */; }; 7B1E819E27C8874900FF0E60 /* ContentOverlayPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */; }; 7B1E819F27C8874900FF0E60 /* ContentOverlay.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */; }; 7B1E81A027C8874900FF0E60 /* ContentOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */; }; @@ -2194,15 +2196,25 @@ 7B4CE8E726F02135009134B1 /* TabBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4CE8E626F02134009134B1 /* TabBarTests.swift */; }; 7B5DD69A2AE51FFA001DE99C /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B5DD6992AE51FFA001DE99C /* PixelKit */; }; 7B5F9A752AE2BE4E002AEBC0 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B5F9A742AE2BE4E002AEBC0 /* PixelKit */; }; + 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; + 7B7DFB222B7E7473009EA1A3 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 7B7DFB212B7E7473009EA1A3 /* Networking */; }; 7B8C083C2AE1268E00F4C67F /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B8C083B2AE1268E00F4C67F /* PixelKit */; }; 7B8DB31A2B504D7500EC16DA /* VPNAppEventsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8DB3192B504D7500EC16DA /* VPNAppEventsHandler.swift */; }; 7B934C412A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */; }; + 7B94E1652B7ED95100E32B96 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */; }; + 7B97CD592B7E0B57004FEF43 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */; }; + 7B97CD5B2B7E0B85004FEF43 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD5A2B7E0B85004FEF43 /* Common */; }; + 7B97CD5C2B7E0BBB004FEF43 /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */; }; + 7B97CD5D2B7E0BCE004FEF43 /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; + 7B97CD5E2B7E0BEA004FEF43 /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; + 7B97CD5F2B7E0BF7004FEF43 /* NSApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C8F622591021700748EB7 /* NSApplicationExtension.swift */; }; + 7B97CD602B7E0C2E004FEF43 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; + 7B97CD622B7E0C4B004FEF43 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD612B7E0C4B004FEF43 /* PixelKit */; }; + 7BA076BB2B65D61400D7FB72 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */; }; 7BA4727D26F01BC400EAA165 /* CoreDataTestUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292C42667104B00AD2C21 /* CoreDataTestUtilities.swift */; }; 7BA59C9B2AE18B49009A97B1 /* SystemExtensionManager in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA59C9A2AE18B49009A97B1 /* SystemExtensionManager */; }; 7BA7CC392AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */; }; 7BA7CC3A2AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */; }; - 7BA7CC3B2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */; }; - 7BA7CC3C2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */; }; 7BA7CC3D2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */; }; 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */; }; 7BA7CC3F2AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC132AD11DC80042E5CE /* AppLauncher+DefaultInitializer.swift */; }; @@ -2218,8 +2230,8 @@ 7BA7CC4C2AD11EC70042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606A2A0B29FA00BCD287 /* NetworkProtectionControllerErrorStore.swift */; }; 7BA7CC4E2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */; }; 7BA7CC502AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */; }; - 7BA7CC532AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; - 7BA7CC542AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 7BA7CC532AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; + 7BA7CC542AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 7BA7CC552AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 7BA7CC562AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 7BA7CC582AD1203A0042E5CE /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; @@ -2237,9 +2249,13 @@ 7BBD44282AD730A400D0A064 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7BBD44272AD730A400D0A064 /* PixelKit */; }; 7BBD45B12A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; 7BBD45B22A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; + 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */; }; 7BD01C192AD8319C0088B32E /* IPCServiceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */; }; 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */; }; 7BD3AF5D2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */; }; + 7BDA36E62B7E037100AD5388 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; + 7BDA36F52B7E055800AD5388 /* MacTransparentProxyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */; }; + 7BDA36F92B7E084A00AD5388 /* VPNProxyExtension.appex in Embed Network Extensions */ = {isa = PBXBuildFile; fileRef = 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 7BE146072A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */; }; 7BE146082A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */; }; 7BEC182F2AD5D8DC00D30536 /* SystemExtensionManager in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */; }; @@ -2486,7 +2502,6 @@ AA7EB6EB27E880AE00036718 /* dark-shield-mouse-over.json in Resources */ = {isa = PBXBuildFile; fileRef = AA7EB6EA27E880AE00036718 /* dark-shield-mouse-over.json */; }; AA7EB6ED27E880B600036718 /* dark-shield-dot-mouse-over.json in Resources */ = {isa = PBXBuildFile; fileRef = AA7EB6EC27E880B600036718 /* dark-shield-dot-mouse-over.json */; }; AA80EC54256BE3BC007083E7 /* UserText.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA80EC53256BE3BC007083E7 /* UserText.swift */; }; - AA80EC67256C4691007083E7 /* BrowserTab.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA80EC69256C4691007083E7 /* BrowserTab.storyboard */; }; AA80EC73256C46A2007083E7 /* Suggestion.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA80EC75256C46A2007083E7 /* Suggestion.storyboard */; }; AA80EC79256C46AA007083E7 /* TabBar.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA80EC7B256C46AA007083E7 /* TabBar.storyboard */; }; AA840A9827319D1600E63CDD /* FirePopoverWrapperViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA840A9727319D1600E63CDD /* FirePopoverWrapperViewController.swift */; }; @@ -2505,7 +2520,7 @@ AA9FF95D24A1FA1C0039E328 /* TabCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9FF95C24A1FA1C0039E328 /* TabCollection.swift */; }; AA9FF95F24A1FB690039E328 /* TabCollectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9FF95E24A1FB680039E328 /* TabCollectionViewModel.swift */; }; AAA0CC33252F181A0079BC96 /* NavigationButtonMenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC32252F181A0079BC96 /* NavigationButtonMenuDelegate.swift */; }; - AAA0CC3C25337FAB0079BC96 /* WKBackForwardListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC3B25337FAB0079BC96 /* WKBackForwardListItemViewModel.swift */; }; + AAA0CC3C25337FAB0079BC96 /* BackForwardListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC3B25337FAB0079BC96 /* BackForwardListItemViewModel.swift */; }; AAA0CC472533833C0079BC96 /* MoreOptionsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC462533833C0079BC96 /* MoreOptionsMenu.swift */; }; AAA0CC572539EBC90079BC96 /* FaviconUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC562539EBC90079BC96 /* FaviconUserScript.swift */; }; AAA0CC6A253CC43C0079BC96 /* WKUserContentControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC69253CC43C0079BC96 /* WKUserContentControllerExtension.swift */; }; @@ -2557,7 +2572,6 @@ AAE7527C263B056C00B973F8 /* HistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527B263B056C00B973F8 /* HistoryStore.swift */; }; AAE7527E263B05C600B973F8 /* HistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527D263B05C600B973F8 /* HistoryEntry.swift */; }; AAE75280263B0A4D00B973F8 /* HistoryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527F263B0A4D00B973F8 /* HistoryCoordinator.swift */; }; - AAE8B102258A41C000E81239 /* TabPreview.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AAE8B101258A41C000E81239 /* TabPreview.storyboard */; }; AAE8B110258A456C00E81239 /* TabPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */; }; AAE99B8927088A19008B6BD9 /* FirePopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE99B8827088A19008B6BD9 /* FirePopover.swift */; }; AAEC74B22642C57200C2EFBC /* HistoryCoordinatingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEC74B12642C57200C2EFBC /* HistoryCoordinatingMock.swift */; }; @@ -2788,6 +2802,17 @@ B68172AE269EB43F006D1092 /* GeolocationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68172AD269EB43F006D1092 /* GeolocationServiceTests.swift */; }; B6830961274CDE99004B46BB /* FireproofDomainsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6830960274CDE99004B46BB /* FireproofDomainsContainer.swift */; }; B6830963274CDEC7004B46BB /* FireproofDomainsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6830962274CDEC7004B46BB /* FireproofDomainsStore.swift */; }; + B68412142B694BA10092F66A /* NSObject+performSelector.m in Sources */ = {isa = PBXBuildFile; fileRef = B68412132B694BA10092F66A /* NSObject+performSelector.m */; }; + B68412152B694BA10092F66A /* NSObject+performSelector.m in Sources */ = {isa = PBXBuildFile; fileRef = B68412132B694BA10092F66A /* NSObject+performSelector.m */; }; + B68412162B694BA10092F66A /* NSObject+performSelector.m in Sources */ = {isa = PBXBuildFile; fileRef = B68412132B694BA10092F66A /* NSObject+performSelector.m */; }; + B684121C2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B684121B2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift */; }; + B684121D2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B684121B2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift */; }; + B684121E2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B684121B2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift */; }; + B68412202B6A30680092F66A /* StringExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B684121F2B6A30680092F66A /* StringExtensionTests.swift */; }; + B68412212B6A30680092F66A /* StringExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B684121F2B6A30680092F66A /* StringExtensionTests.swift */; }; + B68412272B6A68C10092F66A /* WKBackForwardListItemExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68412242B6A67920092F66A /* WKBackForwardListItemExtension.swift */; }; + B68412282B6A68C20092F66A /* WKBackForwardListItemExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68412242B6A67920092F66A /* WKBackForwardListItemExtension.swift */; }; + B68412292B6A68C90092F66A /* WKBackForwardListItemExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68412242B6A67920092F66A /* WKBackForwardListItemExtension.swift */; }; B68458B025C7E76A00DC17B6 /* WindowManager+StateRestoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68458AF25C7E76A00DC17B6 /* WindowManager+StateRestoration.swift */; }; B68458B825C7E8B200DC17B6 /* Tab+NSSecureCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68458B725C7E8B200DC17B6 /* Tab+NSSecureCoding.swift */; }; B68458C025C7E9E000DC17B6 /* TabCollectionViewModel+NSSecureCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68458BF25C7E9E000DC17B6 /* TabCollectionViewModel+NSSecureCoding.swift */; }; @@ -2818,6 +2843,8 @@ B690152C2ACBF4DA00AD0BAB /* MenuPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B690152B2ACBF4DA00AD0BAB /* MenuPreview.swift */; }; B690152D2ACBF4DA00AD0BAB /* MenuPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B690152B2ACBF4DA00AD0BAB /* MenuPreview.swift */; }; B690152F2ACBF4DA00AD0BAB /* MenuPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B690152B2ACBF4DA00AD0BAB /* MenuPreview.swift */; }; + B693766E2B6B5F27005BD9D4 /* ErrorPageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693766D2B6B5F26005BD9D4 /* ErrorPageTests.swift */; }; + B693766F2B6B5F27005BD9D4 /* ErrorPageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693766D2B6B5F26005BD9D4 /* ErrorPageTests.swift */; }; B693954B26F04BEB0015B914 /* MouseOverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693953D26F04BE70015B914 /* MouseOverView.swift */; }; B693954C26F04BEB0015B914 /* FocusRingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693953E26F04BE70015B914 /* FocusRingView.swift */; }; B693954E26F04BEB0015B914 /* LoadingProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693954026F04BE80015B914 /* LoadingProgressView.swift */; }; @@ -3097,7 +3124,6 @@ EEC8EB402982CD550065AA39 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; EECE10E529DD77E60044D027 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECE10E429DD77E60044D027 /* FeatureFlag.swift */; }; EECE10E629DD77E60044D027 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECE10E429DD77E60044D027 /* FeatureFlag.swift */; }; - EEF12E6E2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; EEF53E182950CED5002D78F4 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; F41D174125CB131900472416 /* NSColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D174025CB131900472416 /* NSColorExtension.swift */; }; @@ -3178,6 +3204,13 @@ remoteGlobalIDString = AA585D7D248FD31100E9A3E2; remoteInfo = "DuckDuckGo Privacy Browser"; }; + 7BDA36F72B7E082100AD5388 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AA585D76248FD31100E9A3E2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7BDA36E42B7E037100AD5388; + remoteInfo = VPNProxyExtension; + }; 7BEC18302AD5DA3300D30536 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = AA585D76248FD31100E9A3E2 /* Project object */; @@ -3235,14 +3268,16 @@ name = "Embed Login Items"; runOnlyForDeploymentPostprocessing = 0; }; - 4BA7C4E02B3F6F7500AFE511 /* CopyFiles */ = { + 4BA7C4E02B3F6F7500AFE511 /* Embed Network Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( - 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in CopyFiles */, + 7BDA36F92B7E084A00AD5388 /* VPNProxyExtension.appex in Embed Network Extensions */, + 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in Embed Network Extensions */, ); + name = "Embed Network Extensions"; runOnlyForDeploymentPostprocessing = 0; }; B6EC37E629B5DA2A001ACE79 /* CopyFiles */ = { @@ -3539,7 +3574,7 @@ 4B4D604F2A0B293C00BCD287 /* NetworkProtectionSystemExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetworkProtectionSystemExtension.xcconfig; sourceTree = ""; }; 4B4D60502A0B293C00BCD287 /* NetworkProtectionAppExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetworkProtectionAppExtension.xcconfig; sourceTree = ""; }; 4B4D60512A0B293C00BCD287 /* ExtensionBase.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ExtensionBase.xcconfig; sourceTree = ""; }; - 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionBundle.swift; sourceTree = ""; }; + 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+VPN.swift"; sourceTree = ""; }; 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOptionKeyExtension.swift; sourceTree = ""; }; 4B4D60652A0B29FA00BCD287 /* NetworkProtectionNavBarButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionNavBarButtonModel.swift; sourceTree = ""; }; 4B4D60692A0B29FA00BCD287 /* NetworkProtection+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtection+ConvenienceInitializers.swift"; sourceTree = ""; }; @@ -3549,10 +3584,7 @@ 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteCodeViewModel.swift; sourceTree = ""; }; 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventMapping+NetworkProtectionError.swift"; sourceTree = ""; }; 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionUNNotificationsPresenter.swift; sourceTree = ""; }; - 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionExtensionMachService.swift; sourceTree = ""; }; 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserText+NetworkProtectionExtensions.swift"; sourceTree = ""; }; - 4B4D609C2A0B2C2300BCD287 /* DuckDuckGo_NetP_Release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGo_NetP_Release.entitlements; sourceTree = ""; }; - 4B4D609E2A0B2C2300BCD287 /* DuckDuckGo_NetP_Debug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGo_NetP_Debug.entitlements; sourceTree = ""; }; 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserText+NetworkProtection.swift"; sourceTree = ""; }; 4B4D60E12A0C883A00BCD287 /* AppMain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMain.swift; sourceTree = ""; }; 4B4F72EB266B2ED300814C60 /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = ""; }; @@ -3759,7 +3791,10 @@ 56D145ED29E6DAD900E3488A /* DataImportProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportProviderTests.swift; sourceTree = ""; }; 56D145F029E6F06D00E3488A /* MockBookmarkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBookmarkManager.swift; sourceTree = ""; }; 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueSetUpView.swift; sourceTree = ""; }; + 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWindowManager.swift; sourceTree = ""; }; + 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTransparentProxyProvider.swift; sourceTree = ""; }; 7B05829D2A812AC000AC3F7C /* NetworkProtectionOnboardingMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOnboardingMenu.swift; sourceTree = ""; }; + 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNProxyLauncher.swift; sourceTree = ""; }; 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentOverlayPopover.swift; sourceTree = ""; }; 7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ContentOverlay.storyboard; sourceTree = ""; }; 7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentOverlayViewController.swift; sourceTree = ""; }; @@ -3783,7 +3818,6 @@ 7BA7CC0B2AD11D1E0042E5CE /* DuckDuckGoVPNAppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DuckDuckGoVPNAppStore.xcconfig; sourceTree = ""; }; 7BA7CC0C2AD11D1E0042E5CE /* DuckDuckGoVPN.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DuckDuckGoVPN.xcconfig; sourceTree = ""; }; 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckDuckGoVPNAppDelegate.swift; sourceTree = ""; }; - 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Configuration.swift"; sourceTree = ""; }; 7BA7CC102AD11DC80042E5CE /* Info-AppStore.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-AppStore.plist"; sourceTree = ""; }; 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelControllerIPCService.swift; sourceTree = ""; }; 7BA7CC122AD11DC80042E5CE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -3802,6 +3836,10 @@ 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkExtensionController.swift; sourceTree = ""; }; 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeychainType+ClientDefault.swift"; sourceTree = ""; }; 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFeatureVisibility.swift; sourceTree = ""; }; + 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = VPNProxyExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 7BDA36EA2B7E037200AD5388 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7BDA36EB2B7E037200AD5388 /* VPNProxyExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VPNProxyExtension.entitlements; sourceTree = ""; }; + 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = VPNProxyExtension.xcconfig; sourceTree = ""; }; 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugMenu.swift; sourceTree = ""; }; 7BEC182D2AD5D89C00D30536 /* SystemExtensionManager */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SystemExtensionManager; sourceTree = ""; }; 7BEC20402B0F505F00243D3E /* AddBookmarkPopoverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBookmarkPopoverView.swift; sourceTree = ""; }; @@ -4016,7 +4054,6 @@ AA7EB6EA27E880AE00036718 /* dark-shield-mouse-over.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "dark-shield-mouse-over.json"; sourceTree = ""; }; AA7EB6EC27E880B600036718 /* dark-shield-dot-mouse-over.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "dark-shield-dot-mouse-over.json"; sourceTree = ""; }; AA80EC53256BE3BC007083E7 /* UserText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserText.swift; sourceTree = ""; }; - AA80EC68256C4691007083E7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/BrowserTab.storyboard; sourceTree = ""; }; AA80EC74256C46A2007083E7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Suggestion.storyboard; sourceTree = ""; }; AA80EC7A256C46AA007083E7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/TabBar.storyboard; sourceTree = ""; }; AA840A9727319D1600E63CDD /* FirePopoverWrapperViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopoverWrapperViewController.swift; sourceTree = ""; }; @@ -4035,7 +4072,7 @@ AA9FF95C24A1FA1C0039E328 /* TabCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCollection.swift; sourceTree = ""; }; AA9FF95E24A1FB680039E328 /* TabCollectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCollectionViewModel.swift; sourceTree = ""; }; AAA0CC32252F181A0079BC96 /* NavigationButtonMenuDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationButtonMenuDelegate.swift; sourceTree = ""; }; - AAA0CC3B25337FAB0079BC96 /* WKBackForwardListItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKBackForwardListItemViewModel.swift; sourceTree = ""; }; + AAA0CC3B25337FAB0079BC96 /* BackForwardListItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackForwardListItemViewModel.swift; sourceTree = ""; }; AAA0CC462533833C0079BC96 /* MoreOptionsMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreOptionsMenu.swift; sourceTree = ""; }; AAA0CC562539EBC90079BC96 /* FaviconUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconUserScript.swift; sourceTree = ""; }; AAA0CC69253CC43C0079BC96 /* WKUserContentControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKUserContentControllerExtension.swift; sourceTree = ""; }; @@ -4089,7 +4126,6 @@ AAE7527B263B056C00B973F8 /* HistoryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryStore.swift; sourceTree = ""; }; AAE7527D263B05C600B973F8 /* HistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryEntry.swift; sourceTree = ""; }; AAE7527F263B0A4D00B973F8 /* HistoryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCoordinator.swift; sourceTree = ""; }; - AAE8B101258A41C000E81239 /* TabPreview.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = TabPreview.storyboard; sourceTree = ""; }; AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPreviewViewController.swift; sourceTree = ""; }; AAE99B8827088A19008B6BD9 /* FirePopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopover.swift; sourceTree = ""; }; AAEC74B12642C57200C2EFBC /* HistoryCoordinatingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCoordinatingMock.swift; sourceTree = ""; }; @@ -4225,6 +4261,11 @@ B68172AD269EB43F006D1092 /* GeolocationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeolocationServiceTests.swift; sourceTree = ""; }; B6830960274CDE99004B46BB /* FireproofDomainsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireproofDomainsContainer.swift; sourceTree = ""; }; B6830962274CDEC7004B46BB /* FireproofDomainsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireproofDomainsStore.swift; sourceTree = ""; }; + B68412122B694BA10092F66A /* NSObject+performSelector.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSObject+performSelector.h"; sourceTree = ""; }; + B68412132B694BA10092F66A /* NSObject+performSelector.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSObject+performSelector.m"; sourceTree = ""; }; + B684121B2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPageHTMLTemplate.swift; sourceTree = ""; }; + B684121F2B6A30680092F66A /* StringExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensionTests.swift; sourceTree = ""; }; + B68412242B6A67920092F66A /* WKBackForwardListItemExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKBackForwardListItemExtension.swift; sourceTree = ""; }; B68458AF25C7E76A00DC17B6 /* WindowManager+StateRestoration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WindowManager+StateRestoration.swift"; sourceTree = ""; }; B68458B725C7E8B200DC17B6 /* Tab+NSSecureCoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tab+NSSecureCoding.swift"; sourceTree = ""; }; B68458BF25C7E9E000DC17B6 /* TabCollectionViewModel+NSSecureCoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabCollectionViewModel+NSSecureCoding.swift"; sourceTree = ""; }; @@ -4243,6 +4284,7 @@ B68C92C0274E3EF4002AC6B0 /* PopUpWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopUpWindow.swift; sourceTree = ""; }; B68C92C32750EF76002AC6B0 /* PixelDataRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelDataRecord.swift; sourceTree = ""; }; B690152B2ACBF4DA00AD0BAB /* MenuPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuPreview.swift; sourceTree = ""; }; + B693766D2B6B5F26005BD9D4 /* ErrorPageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorPageTests.swift; sourceTree = ""; }; B693953D26F04BE70015B914 /* MouseOverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MouseOverView.swift; sourceTree = ""; }; B693953E26F04BE70015B914 /* FocusRingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FocusRingView.swift; sourceTree = ""; }; B693954026F04BE80015B914 /* LoadingProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingProgressView.swift; sourceTree = ""; }; @@ -4289,6 +4331,8 @@ B6A5A27D25B9403E00AA7ADA /* FileStoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileStoreMock.swift; sourceTree = ""; }; B6A5A29F25B96E8300AA7ADA /* AppStateChangePublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateChangePublisherTests.swift; sourceTree = ""; }; B6A5A2A725BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowManagerStateRestorationTests.swift; sourceTree = ""; }; + B6A60E4F2B73C3B800FD4968 /* WKURLSchemeTask+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WKURLSchemeTask+Private.h"; sourceTree = ""; }; + B6A60E502B73C46B00FD4968 /* IntegrationTestsBridging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IntegrationTestsBridging.h; sourceTree = ""; }; B6A924D82664C72D001A28CA /* WebKitDownloadTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebKitDownloadTask.swift; sourceTree = ""; }; B6A9E45226142B070067D1B9 /* Pixel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pixel.swift; sourceTree = ""; }; B6A9E46A2614618A0067D1B9 /* OperatingSystemVersionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperatingSystemVersionExtension.swift; sourceTree = ""; }; @@ -4430,6 +4474,7 @@ 373FB4B32B4D6C4B004C88D6 /* PreferencesViews in Frameworks */, 7B5F9A752AE2BE4E002AEBC0 /* PixelKit in Frameworks */, 4BF97AD32B43C43F00EB4240 /* NetworkProtectionUI in Frameworks */, + 7B1459572B7D43E500047F2C /* NetworkProtectionProxy in Frameworks */, B6F7128229F6820A00594A45 /* QuickLookUI.framework in Frameworks */, 984FD3BF299ACF35007334DD /* Bookmarks in Frameworks */, 37A5E2F0298AA1B20047046B /* Persistence in Frameworks */, @@ -4497,6 +4542,7 @@ 37269F012B332FC8005E8E46 /* Common in Frameworks */, EE7295E92A545BC4008C0991 /* NetworkProtection in Frameworks */, 4B2537772A11BFE100610219 /* PixelKit in Frameworks */, + 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */, 4B2D062C2A11C0E100DE1F49 /* Networking in Frameworks */, 4B25375B2A11BE7300610219 /* NetworkExtension.framework in Frameworks */, ); @@ -4507,6 +4553,7 @@ buildActionMask = 2147483647; files = ( 4B41EDAB2B1544B2001EEDF4 /* LoginItems in Frameworks */, + 7B00997D2B6508B700FE7C31 /* NetworkProtectionProxy in Frameworks */, 7BEEA5122AD1235B00A9E72B /* NetworkProtectionIPC in Frameworks */, 7BA7CC5F2AD1210C0042E5CE /* Networking in Frameworks */, 7BEEA5162AD1236E00A9E72B /* NetworkProtectionUI in Frameworks */, @@ -4523,6 +4570,7 @@ 7BFCB7502ADE7E2300DA3EA7 /* PixelKit in Frameworks */, 7BA7CC612AD1211C0042E5CE /* Networking in Frameworks */, 7BEEA5142AD1236300A9E72B /* NetworkProtectionIPC in Frameworks */, + 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */, EE7295EF2A545C12008C0991 /* NetworkProtection in Frameworks */, 4B2D067F2A1334D700DE1F49 /* NetworkProtectionUI in Frameworks */, 4BA7C4DD2B3F64E500AFE511 /* LoginItems in Frameworks */, @@ -4558,6 +4606,7 @@ 3143C8792B0D1F3D00382627 /* DataBrokerProtection in Frameworks */, 372217842B33380E00B8E9C2 /* TestUtils in Frameworks */, 4B957BD62AC7AE700062CA31 /* LoginItems in Frameworks */, + 7B94E1652B7ED95100E32B96 /* NetworkProtectionProxy in Frameworks */, 4B957BD72AC7AE700062CA31 /* NetworkProtection in Frameworks */, 4B957BD82AC7AE700062CA31 /* BrowserServicesKit in Frameworks */, 4B957BDA2AC7AE700062CA31 /* Bookmarks in Frameworks */, @@ -4600,6 +4649,18 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7BDA36E22B7E037100AD5388 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7BDA36E62B7E037100AD5388 /* NetworkExtension.framework in Frameworks */, + 7B97CD592B7E0B57004FEF43 /* NetworkProtectionProxy in Frameworks */, + 7B97CD622B7E0C4B004FEF43 /* PixelKit in Frameworks */, + 7B7DFB222B7E7473009EA1A3 /* Networking in Frameworks */, + 7B97CD5B2B7E0B85004FEF43 /* Common in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9D9AE8C62AAA39A70026E7DC /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -4648,6 +4709,7 @@ 37DF000529F9C056002B7D3E /* SyncDataProviders in Frameworks */, 37BA812D29B3CD690053F1A3 /* SyncUI in Frameworks */, 372217802B3337FE00B8E9C2 /* TestUtils in Frameworks */, + 7BA076BB2B65D61400D7FB72 /* NetworkProtectionProxy in Frameworks */, 4B4D60B12A0C83B900BCD287 /* NetworkProtectionUI in Frameworks */, 98A50964294B691800D10880 /* Persistence in Frameworks */, ); @@ -5227,8 +5289,9 @@ 4B18E32C2A1ECF1F005D0AAA /* NetworkProtection */ = { isa = PBXGroup; children = ( - 4B4D604F2A0B293C00BCD287 /* NetworkProtectionSystemExtension.xcconfig */, 4B4D60502A0B293C00BCD287 /* NetworkProtectionAppExtension.xcconfig */, + 4B4D604F2A0B293C00BCD287 /* NetworkProtectionSystemExtension.xcconfig */, + 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */, ); path = NetworkProtection; sourceTree = ""; @@ -5243,7 +5306,7 @@ B603973229BEF84900902A34 /* HTTPSUpgrade */, B62A233A29C322A000D22475 /* NavigationProtection */, B603973629BF0E9400902A34 /* PrivacyDashboard */, - B644B43C29D56811003FA9AB /* TabExtensions */, + B644B43C29D56811003FA9AB /* Tab */, 4B1AD91625FC46FB00261379 /* CoreDataEncryptionTests.swift */, 4BA1A6EA258C288C00F6F690 /* EncryptionKeyStoreTests.swift */, 4B1AD8A125FC27E200261379 /* Info.plist */, @@ -5366,7 +5429,7 @@ 4B4D605D2A0B29FA00BCD287 /* AppAndExtensionAndNotificationTargets */ = { isa = PBXGroup; children = ( - 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */, + 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */, 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */, B602E8152A1E2570006D261F /* URL+NetworkProtection.swift */, ); @@ -5450,7 +5513,6 @@ isa = PBXGroup; children = ( B602E81F2A1E2603006D261F /* Bundle+NetworkProtectionExtensions.swift */, - 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */, ); path = SystemExtensionAndNotificationTargets; sourceTree = ""; @@ -5468,6 +5530,7 @@ children = ( 4B41ED9F2B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift */, EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */, + 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */, ); path = NetworkExtensionTargets; sourceTree = ""; @@ -6113,11 +6176,11 @@ isa = PBXGroup; children = ( 7BA7CC132AD11DC80042E5CE /* AppLauncher+DefaultInitializer.swift */, - 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */, 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */, 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */, 7BA7CC152AD11DC80042E5CE /* NetworkProtectionBouncer.swift */, 7B8DB3192B504D7500EC16DA /* VPNAppEventsHandler.swift */, + 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */, 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */, 7BA7CC172AD11DC80042E5CE /* UserText.swift */, 7BA7CC122AD11DC80042E5CE /* Assets.xcassets */, @@ -6147,6 +6210,15 @@ path = LetsMove1.25; sourceTree = ""; }; + 7BDA36E72B7E037200AD5388 /* VPNProxyExtension */ = { + isa = PBXGroup; + children = ( + 7BDA36EA2B7E037200AD5388 /* Info.plist */, + 7BDA36EB2B7E037200AD5388 /* VPNProxyExtension.entitlements */, + ); + path = VPNProxyExtension; + sourceTree = ""; + }; 853014D425E6709500FB8205 /* Support */ = { isa = PBXGroup; children = ( @@ -6481,6 +6553,7 @@ B6DA06E02913AEDB00225DE2 /* TestNavigationDelegate.swift */, B60C6F8029B1B4AD007BFAA8 /* TestRunHelper.swift */, B60C6F7D29B1B41D007BFAA8 /* TestRunHelperInitializer.m */, + B6A60E4F2B73C3B800FD4968 /* WKURLSchemeTask+Private.h */, ); path = Common; sourceTree = ""; @@ -6509,6 +6582,7 @@ 9D9AE9152AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift */, 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */, 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */, + 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */, 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */, 9D9AE9172AAA3B450026E7DC /* UserText.swift */, 9D9AE9162AAA3B450026E7DC /* Assets.xcassets */, @@ -6632,6 +6706,7 @@ B6EC37E929B5DA2A001ACE79 /* tests-server */, 7B96D0D02ADFDA7F007E02C8 /* DuckDuckGoDBPTests */, 4B5F14F72A148B230060320F /* NetworkProtectionAppExtension */, + 7BDA36E72B7E037200AD5388 /* VPNProxyExtension */, 4B25375C2A11BE7500610219 /* NetworkProtectionSystemExtension */, 9D9AE9132AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgent */, 7BA7CC0D2AD11DC80042E5CE /* DuckDuckGoVPN */, @@ -6662,6 +6737,7 @@ 4B957C412AC7AE700062CA31 /* DuckDuckGo Privacy Pro.app */, 565E46DD2B2725DC0013AC2A /* SyncE2EUITests.xctest */, 376113D42B29CD5B00E794BB /* SyncE2EUITests App Store.xctest */, + 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */, ); name = Products; sourceTree = ""; @@ -6670,21 +6746,21 @@ isa = PBXGroup; children = ( B658BAB52B0F845D00D1F2C7 /* Localizable.xcstrings */, - 3192EC862A4DCF0E001E97A5 /* DBP */, - EEAEA3F4294D05CF00D04DF3 /* JSAlert */, + AA4D700525545EDE00C3411E /* Application */, B31055BB27A1BA0E001AC618 /* Autoconsent */, 7B1E819A27C8874900FF0E60 /* Autofill */, - AA4D700525545EDE00C3411E /* Application */, AAC5E4C025D6A6A9007F5990 /* Bookmarks */, 4BFD356E283ADE8B00CE9234 /* BookmarksBar */, AA86491324D831B9001BABEE /* Common */, 85D33F1025C82E93002B91A6 /* Configuration */, 4B6160D125B14E5E007DE5B2 /* ContentBlocker */, AAC30A24268DF93500D2D9CD /* CrashReports */, - 4B723DEA26B0002B00E14D75 /* DataImport */, 4B723DF826B0002B00E14D75 /* DataExport */, + 4B723DEA26B0002B00E14D75 /* DataImport */, + 3192EC862A4DCF0E001E97A5 /* DBP */, 4B379C1C27BDB7EA008A968E /* DeviceAuthentication */, 4B65143C26392483005B46EB /* Email */, + B68412192B6A16030092F66A /* ErrorPage */, AA5FA695275F823900DCE9C9 /* Favicons */, 1D36E651298A84F600AA485D /* FeatureFlagging */, AA3863C227A1E1C000749AB5 /* Feedback */, @@ -6694,13 +6770,13 @@ 4B02197B25E05FAC00ED7DEA /* Fireproofing */, B65536902684409300085A79 /* Geolocation */, AAE75275263B036300B973F8 /* History */, + AAE71DB225F66A0900D74437 /* HomePage */, + EEAEA3F4294D05CF00D04DF3 /* JSAlert */, 9D03F5A22AA74829001A50E8 /* LoginItems */, AA585DB02490E6FA00E9A3E2 /* MainWindow */, - AAE71DB225F66A0900D74437 /* HomePage */, AA97BF4425135CB60014931A /* Menus */, 85378D9A274E618C007C5CBF /* MessageViews */, AA86491524D83384001BABEE /* NavigationBar */, - 4B41EDAC2B168A66001EEDF4 /* VPNFeedbackForm */, 4B4D60542A0B29FA00BCD287 /* NetworkProtection */, 85B7184727677A7D00B4277F /* Onboarding */, 1D074B252909A371006E4AC3 /* PasswordManager */, @@ -6721,6 +6797,7 @@ AAE8B0FD258A416F00E81239 /* TabPreview */, B6040859274B8C5200680351 /* UnprotectedDomains */, AACF6FD426BC35C200CF09F9 /* UserAgent */, + 4B41EDAC2B168A66001EEDF4 /* VPNFeedbackForm */, 4B9DB0062A983B23000927DB /* Waitlist */, AA6EF9AE25066F99004754E6 /* Windows */, 31F28C4B28C8EE9000119F70 /* YoutubePlayer */, @@ -6731,8 +6808,6 @@ 4B5F15032A1570F10060320F /* DuckDuckGoDebug.entitlements */, 37D9BBA329376EE8000B99F9 /* DuckDuckGoAppStore.entitlements */, 377E54382937B7C400780A0A /* DuckDuckGoAppStoreCI.entitlements */, - 4B4D609E2A0B2C2300BCD287 /* DuckDuckGo_NetP_Debug.entitlements */, - 4B4D609C2A0B2C2300BCD287 /* DuckDuckGo_NetP_Release.entitlements */, 4B2D06642A132F3A00DE1F49 /* NetworkProtectionAppExtension.entitlements */, 4B5F14C42A145D6A0060320F /* NetworkProtectionVPNController.entitlements */, 56CEE9092B7A66C500CF10AA /* Info.plist */, @@ -7109,7 +7184,6 @@ AA86491C24D83868001BABEE /* View */ = { isa = PBXGroup; children = ( - AA80EC69256C4691007083E7 /* BrowserTab.storyboard */, B6C0BB6929AF1C7000AE8E3C /* BrowserTabView.swift */, AA585D83248FD31100E9A3E2 /* BrowserTabViewController.swift */, AA6FFB4524DC3B5A0028F4D0 /* WebView.swift */, @@ -7255,7 +7329,7 @@ AAA0CC3A25337F990079BC96 /* ViewModel */ = { isa = PBXGroup; children = ( - AAA0CC3B25337FAB0079BC96 /* WKBackForwardListItemViewModel.swift */, + AAA0CC3B25337FAB0079BC96 /* BackForwardListItemViewModel.swift */, B689ECD426C247DB006FB0C5 /* BackForwardListItem.swift */, AA75A0AD26F3500C0086B667 /* PrivacyIconViewModel.swift */, ); @@ -7524,6 +7598,8 @@ AA6EF9B2250785D5004754E6 /* NSMenuExtension.swift */, AA72D5FD25FFF94E00C77619 /* NSMenuItemExtension.swift */, 4B980E202817604000282EE1 /* NSNotificationName+Debug.swift */, + B68412122B694BA10092F66A /* NSObject+performSelector.h */, + B68412132B694BA10092F66A /* NSObject+performSelector.m */, 4B0511DF262CAA8600F6079C /* NSOpenPanelExtensions.swift */, 4B0135CD2729F1AA00D54834 /* NSPasteboardExtension.swift */, 4B85A47F28821CC500FC4C39 /* NSPasteboardItemExtension.swift */, @@ -7556,6 +7632,7 @@ AA88D14A252A557100980B4E /* URLRequestExtension.swift */, B6DB3AEE278D5C370024C5C4 /* URLSessionExtension.swift */, B602E7CE2A93A5FF00F12201 /* WKBackForwardListExtension.swift */, + B68412242B6A67920092F66A /* WKBackForwardListItemExtension.swift */, B6DA06E7291401D700225DE2 /* WKMenuItemIdentifier.swift */, B645D8F529FA95440024461F /* WKProcessPoolExtension.swift */, AAA0CC69253CC43C0079BC96 /* WKUserContentControllerExtension.swift */, @@ -7635,7 +7712,6 @@ AAE8B0FD258A416F00E81239 /* TabPreview */ = { isa = PBXGroup; children = ( - AAE8B101258A41C000E81239 /* TabPreview.storyboard */, AAC82C5F258B6CB5009B6B42 /* TabPreviewWindowController.swift */, AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */, 1DB67F272B6FE21D003DF243 /* Model */, @@ -7683,6 +7759,7 @@ 85F69B3B25EDE81F00978E59 /* URLExtensionTests.swift */, 4B8AD0B027A86D9200AE44D6 /* WKWebsiteDataStoreExtensionTests.swift */, B6AA64722994B43300D99CD6 /* FutureExtensionTests.swift */, + B684121F2B6A30680092F66A /* StringExtensionTests.swift */, ); path = Extensions; sourceTree = ""; @@ -7727,6 +7804,7 @@ B603972A29BEDF0F00902A34 /* Common */ = { isa = PBXGroup; children = ( + B6A60E502B73C46B00FD4968 /* IntegrationTestsBridging.h */, B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */, ); path = Common; @@ -7799,12 +7877,13 @@ path = History; sourceTree = ""; }; - B644B43C29D56811003FA9AB /* TabExtensions */ = { + B644B43C29D56811003FA9AB /* Tab */ = { isa = PBXGroup; children = ( B644B43929D565DB003FA9AB /* SearchNonexistentDomainTests.swift */, + B693766D2B6B5F26005BD9D4 /* ErrorPageTests.swift */, ); - path = TabExtensions; + path = Tab; sourceTree = ""; }; B647EFB32922539400BA628D /* TabExtensions */ = { @@ -7900,6 +7979,14 @@ path = Database; sourceTree = ""; }; + B68412192B6A16030092F66A /* ErrorPage */ = { + isa = PBXGroup; + children = ( + B684121B2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift */, + ); + path = ErrorPage; + sourceTree = ""; + }; B68458AE25C7E75100DC17B6 /* StateRestoration */ = { isa = PBXGroup; children = ( @@ -8253,6 +8340,7 @@ 4BF97AD42B43C43F00EB4240 /* NetworkProtection */, 373FB4B22B4D6C4B004C88D6 /* PreferencesViews */, 312978892B64131200B67619 /* DataBrokerProtection */, + 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */, ); productName = DuckDuckGo; productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */; @@ -8368,6 +8456,7 @@ 4B2D062B2A11C0E100DE1F49 /* Networking */, EE7295E82A545BC4008C0991 /* NetworkProtection */, 37269F002B332FC8005E8E46 /* Common */, + 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */, ); productName = NetworkProtectionSystemExtension; productReference = 4B25375A2A11BE7300610219 /* com.duckduckgo.macos.vpn.network-extension.debug.systemextension */; @@ -8398,6 +8487,7 @@ 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */, 7BFCB74D2ADE7E1A00DA3EA7 /* PixelKit */, 4B41EDAA2B1544B2001EEDF4 /* LoginItems */, + 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */, ); productName = DuckDuckGoAgent; productReference = 4B2D06392A11CFBB00DE1F49 /* DuckDuckGo VPN.app */; @@ -8411,11 +8501,12 @@ 4B2D06662A13318400DE1F49 /* Frameworks */, 4B2D06672A13318400DE1F49 /* Resources */, 4B2D067D2A13341200DE1F49 /* ShellScript */, - 4BA7C4E02B3F6F7500AFE511 /* CopyFiles */, + 4BA7C4E02B3F6F7500AFE511 /* Embed Network Extensions */, ); buildRules = ( ); dependencies = ( + 7BDA36F82B7E082100AD5388 /* PBXTargetDependency */, 4BA7C4DF2B3F6F4900AFE511 /* PBXTargetDependency */, B6080BA52B20AF8800B418EF /* PBXTargetDependency */, ); @@ -8426,6 +8517,7 @@ 7BA7CC602AD1211C0042E5CE /* Networking */, 7BEEA5132AD1236300A9E72B /* NetworkProtectionIPC */, 7BFCB74F2ADE7E2300DA3EA7 /* PixelKit */, + 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */, 4BA7C4DC2B3F64E500AFE511 /* LoginItems */, ); productName = DuckDuckGoAgentAppStore; @@ -8525,6 +8617,7 @@ 372217832B33380E00B8E9C2 /* TestUtils */, 373FB4B42B4D6C57004C88D6 /* PreferencesViews */, 1E21F8E22B73E48600FB272E /* Subscription */, + 7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */, ); productName = DuckDuckGo; productReference = 4B957C412AC7AE700062CA31 /* DuckDuckGo Privacy Pro.app */; @@ -8569,6 +8662,29 @@ productReference = 7B4CE8DA26F02108009134B1 /* UI Tests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + 7BDA36E42B7E037100AD5388 /* VPNProxyExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7BDA36F42B7E037200AD5388 /* Build configuration list for PBXNativeTarget "VPNProxyExtension" */; + buildPhases = ( + 7BDA36E12B7E037100AD5388 /* Sources */, + 7BDA36E22B7E037100AD5388 /* Frameworks */, + 7BDA36E32B7E037100AD5388 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = VPNProxyExtension; + packageProductDependencies = ( + 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */, + 7B97CD5A2B7E0B85004FEF43 /* Common */, + 7B97CD612B7E0C4B004FEF43 /* PixelKit */, + 7B7DFB212B7E7473009EA1A3 /* Networking */, + ); + productName = VPNProxyExtension; + productReference = 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; 9D9AE8B22AAA39A70026E7DC /* DuckDuckGoDBPBackgroundAgent */ = { isa = PBXNativeTarget; buildConfigurationList = 9D9AE8CC2AAA39A70026E7DC /* Build configuration list for PBXNativeTarget "DuckDuckGoDBPBackgroundAgent" */; @@ -8663,6 +8779,7 @@ 37269EFA2B332F9E005E8E46 /* Common */, 3722177F2B3337FE00B8E9C2 /* TestUtils */, 373FB4B02B4D6C42004C88D6 /* PreferencesViews */, + 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */, ); productName = DuckDuckGo; productReference = AA585D7E248FD31100E9A3E2 /* DuckDuckGo.app */; @@ -8719,7 +8836,7 @@ AA585D76248FD31100E9A3E2 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1500; + LastSwiftUpdateCheck = 1520; LastUpgradeCheck = 1400; ORGANIZATIONNAME = DuckDuckGo; TargetAttributes = { @@ -8761,6 +8878,9 @@ CreatedOnToolsVersion = 12.5.1; TestTargetID = AA585D7D248FD31100E9A3E2; }; + 7BDA36E42B7E037100AD5388 = { + CreatedOnToolsVersion = 15.2; + }; AA585D7D248FD31100E9A3E2 = { CreatedOnToolsVersion = 11.5; }; @@ -8806,6 +8926,7 @@ 3706FE9B293F662100E42796 /* Integration Tests App Store */, B6EC37E729B5DA2A001ACE79 /* tests-server */, 4B4D603C2A0B290200BCD287 /* NetworkProtectionAppExtension */, + 7BDA36E42B7E037100AD5388 /* VPNProxyExtension */, 4B2537592A11BE7300610219 /* NetworkProtectionSystemExtension */, 4B4BEC1F2A11B4E2001D9AC5 /* DuckDuckGoNotifications */, 4B2D06382A11CFBA00DE1F49 /* DuckDuckGoVPN */, @@ -8841,7 +8962,6 @@ EEC8EB3F2982CA440065AA39 /* JSAlert.storyboard in Resources */, 3706FCC3293F65D500E42796 /* userscript.js in Resources */, 3706FCC4293F65D500E42796 /* fb-tds.json in Resources */, - 3706FCC5293F65D500E42796 /* TabPreview.storyboard in Resources */, 3706FCC6293F65D500E42796 /* README.md in Resources */, 3706FCC8293F65D500E42796 /* Assets.xcassets in Resources */, 3706FCC9293F65D500E42796 /* NavigationBar.storyboard in Resources */, @@ -8875,7 +8995,6 @@ 3706FCEF293F65D500E42796 /* macos-config.json in Resources */, 3706FCF0293F65D500E42796 /* httpsMobileV2BloomSpec.json in Resources */, 3706FCF1293F65D500E42796 /* TabBarFooter.xib in Resources */, - 3706FCF2293F65D500E42796 /* BrowserTab.storyboard in Resources */, 3706FCF3293F65D500E42796 /* FirePopoverCollectionViewItem.xib in Resources */, 3706FCF4293F65D500E42796 /* ProximaNova-Bold-webfont.woff2 in Resources */, 3706FCF5293F65D500E42796 /* dark-shield-dot.json in Resources */, @@ -8969,7 +9088,6 @@ 4B957BFD2AC7AE700062CA31 /* JSAlert.storyboard in Resources */, 4B957C002AC7AE700062CA31 /* userscript.js in Resources */, 4B957C012AC7AE700062CA31 /* fb-tds.json in Resources */, - 4B957C022AC7AE700062CA31 /* TabPreview.storyboard in Resources */, 4B957C032AC7AE700062CA31 /* README.md in Resources */, 4B957C042AC7AE700062CA31 /* Assets.xcassets in Resources */, 4B957C052AC7AE700062CA31 /* NavigationBar.storyboard in Resources */, @@ -9003,7 +9121,6 @@ 4B957C282AC7AE700062CA31 /* macos-config.json in Resources */, 4B957C292AC7AE700062CA31 /* httpsMobileV2BloomSpec.json in Resources */, 4B957C2A2AC7AE700062CA31 /* TabBarFooter.xib in Resources */, - 4B957C2B2AC7AE700062CA31 /* BrowserTab.storyboard in Resources */, 4B957C2C2AC7AE700062CA31 /* FirePopoverCollectionViewItem.xib in Resources */, 4B957C2D2AC7AE700062CA31 /* ProximaNova-Bold-webfont.woff2 in Resources */, 4B957C2E2AC7AE700062CA31 /* dark-shield-dot.json in Resources */, @@ -9027,6 +9144,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7BDA36E32B7E037100AD5388 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9D9AE8C92AAA39A70026E7DC /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -9066,7 +9190,6 @@ EEC111E4294D06020086524F /* JSAlert.storyboard in Resources */, B31055C627A1BA1D001AC618 /* userscript.js in Resources */, EA4617F0273A28A700F110A2 /* fb-tds.json in Resources */, - AAE8B102258A41C000E81239 /* TabPreview.storyboard in Resources */, AA68C3D72490F821001B8783 /* README.md in Resources */, AA585D86248FD31400E9A3E2 /* Assets.xcassets in Resources */, 85589E8D27BBBB870038AD11 /* NavigationBar.storyboard in Resources */, @@ -9100,7 +9223,6 @@ 026ADE1426C3010C002518EE /* macos-config.json in Resources */, 4B677432255DBEB800025BD8 /* httpsMobileV2BloomSpec.json in Resources */, AA2CB12D2587BB5600AA6FBE /* TabBarFooter.xib in Resources */, - AA80EC67256C4691007083E7 /* BrowserTab.storyboard in Resources */, AAE246F42709EF3B00BEEAEE /* FirePopoverCollectionViewItem.xib in Resources */, EAA29AE9278D2E43007070CF /* ProximaNova-Bold-webfont.woff2 in Resources */, AA3439702754D4E900B241FA /* dark-shield-dot.json in Resources */, @@ -9315,7 +9437,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Embeds login items for the App Store build.\n\n# Skip login item embedding for release builds until they're ready to go live.\nif [ \"${CONFIGURATION}\" = \"Release\" ]; then\n echo \"Skipping login item embedding for release build.\"\n exit 0\n \n VPN_AGENT_NAME=\"${AGENT_RELEASE_PRODUCT_NAME}\"\n PIR_AGENT_NAME=\"${DBP_BACKGROUND_AGENT_RELEASE_PRODUCT_NAME}\"\nelse\n VPN_AGENT_NAME=\"${AGENT_PRODUCT_NAME}\"\n PIR_AGENT_NAME=\"${DBP_BACKGROUND_AGENT_PRODUCT_NAME}\"\nfi\n\nVPN_AGENT_ORIGIN=$(readlink -f \"${CONFIGURATION_BUILD_DIR}/${VPN_AGENT_NAME}.app\")\nPIR_AGENT_ORIGIN=$(readlink -f \"${CONFIGURATION_BUILD_DIR}/${PIR_AGENT_NAME}.app\")\nAGENT_DESTINATION=\"${CONFIGURATION_BUILD_DIR}/${CONTENTS_FOLDER_PATH}/Library/LoginItems\"\n \n# Make sure that Library/LoginItems exists before copying\nmkdir -p \"$AGENT_DESTINATION\"\n \necho \"Copying VPN agent from $VPN_AGENT_ORIGIN to $AGENT_DESTINATION\"\nrsync -r --links \"$VPN_AGENT_ORIGIN\" \"$AGENT_DESTINATION\"\n \necho \"Copying Personal Information Removal agent from $PIR_AGENT_ORIGIN to $AGENT_DESTINATION\"\nrsync -r --links \"$PIR_AGENT_ORIGIN\" \"$AGENT_DESTINATION\"\n"; + shellScript = "# Embeds login items for the App Store build.\n\n# Skip login item embedding for release builds until they're ready to go live.\nif [ \"${CONFIGURATION}\" = \"Release\" ]; then\n VPN_AGENT_NAME=\"${AGENT_RELEASE_PRODUCT_NAME}\"\n PIR_AGENT_NAME=\"${DBP_BACKGROUND_AGENT_RELEASE_PRODUCT_NAME}\"\nelse\n VPN_AGENT_NAME=\"${AGENT_PRODUCT_NAME}\"\n PIR_AGENT_NAME=\"${DBP_BACKGROUND_AGENT_PRODUCT_NAME}\"\nfi\n\nVPN_AGENT_ORIGIN=$(readlink -f \"${CONFIGURATION_BUILD_DIR}/${VPN_AGENT_NAME}.app\")\nPIR_AGENT_ORIGIN=$(readlink -f \"${CONFIGURATION_BUILD_DIR}/${PIR_AGENT_NAME}.app\")\nAGENT_DESTINATION=\"${CONFIGURATION_BUILD_DIR}/${CONTENTS_FOLDER_PATH}/Library/LoginItems\"\n \n# Make sure that Library/LoginItems exists before copying\nmkdir -p \"$AGENT_DESTINATION\"\n \necho \"Copying VPN agent from $VPN_AGENT_ORIGIN to $AGENT_DESTINATION\"\nrsync -r --links \"$VPN_AGENT_ORIGIN\" \"$AGENT_DESTINATION\"\n \necho \"Copying Personal Information Removal agent from $PIR_AGENT_ORIGIN to $AGENT_DESTINATION\"\nrsync -r --links \"$PIR_AGENT_ORIGIN\" \"$AGENT_DESTINATION\"\n"; }; 7B31FD922AD126C40086AA24 /* Embed System Network Extension */ = { isa = PBXShellScriptBuildPhase; @@ -9568,7 +9690,7 @@ 3706FAE1293F65D500E42796 /* HomePageFavoritesModel.swift in Sources */, 3706FAE2293F65D500E42796 /* SequenceExtensions.swift in Sources */, 3706FAE3293F65D500E42796 /* ChromiumDataImporter.swift in Sources */, - 3706FAE5293F65D500E42796 /* WKBackForwardListItemViewModel.swift in Sources */, + 3706FAE5293F65D500E42796 /* BackForwardListItemViewModel.swift in Sources */, 3706FAE6293F65D500E42796 /* BWNotRespondingAlert.swift in Sources */, 3706FAE7293F65D500E42796 /* DebugUserScript.swift in Sources */, 3706FAE8293F65D500E42796 /* RecentlyClosedTab.swift in Sources */, @@ -9840,7 +9962,7 @@ B66260E829ACD0C900E9E3EE /* DuckPlayerTabExtension.swift in Sources */, 3706FBAA293F65D500E42796 /* HoverUserScript.swift in Sources */, 3706FBAC293F65D500E42796 /* MainMenuActions.swift in Sources */, - 4BF97AD92B43C5C000EB4240 /* NetworkProtectionBundle.swift in Sources */, + 4BF97AD92B43C5C000EB4240 /* Bundle+VPN.swift in Sources */, 3706FBAE293F65D500E42796 /* DataImport.swift in Sources */, 3706FBAF293F65D500E42796 /* FireproofDomains.xcdatamodeld in Sources */, B626A7552991413000053070 /* SerpHeadersNavigationResponder.swift in Sources */, @@ -9884,6 +10006,8 @@ 3706FBD0293F65D500E42796 /* WebKitVersionProvider.swift in Sources */, 3706FBD1293F65D500E42796 /* NSCoderExtensions.swift in Sources */, B6D6A5DD2982A4CE001F5F11 /* Tab+Navigation.swift in Sources */, + B68412282B6A68C20092F66A /* WKBackForwardListItemExtension.swift in Sources */, + B684121D2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift in Sources */, 3706FBD2293F65D500E42796 /* RunningApplicationCheck.swift in Sources */, 3706FBD3293F65D500E42796 /* StatePersistenceService.swift in Sources */, 3706FBD4293F65D500E42796 /* WindowManager+StateRestoration.swift in Sources */, @@ -10037,6 +10161,7 @@ 3706FC3C293F65D500E42796 /* HistoryCoordinator.swift in Sources */, 3706FC3E293F65D500E42796 /* VariantManager.swift in Sources */, 3706FC3F293F65D500E42796 /* ApplicationDockMenu.swift in Sources */, + B68412152B694BA10092F66A /* NSObject+performSelector.m in Sources */, 4B9DB03C2A983B24000927DB /* InvitedToWaitlistView.swift in Sources */, 3706FC40293F65D500E42796 /* SaveIdentityViewController.swift in Sources */, 3706FC41293F65D500E42796 /* FileStore.swift in Sources */, @@ -10242,6 +10367,7 @@ 3706FE13293F661700E42796 /* ConfigurationStorageTests.swift in Sources */, 3706FE14293F661700E42796 /* DownloadListStoreMock.swift in Sources */, 3706FE15293F661700E42796 /* PrivacyIconViewModelTests.swift in Sources */, + B68412212B6A30680092F66A /* StringExtensionTests.swift in Sources */, 1D8C2FEE2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift in Sources */, 56D145F229E6F06D00E3488A /* MockBookmarkManager.swift in Sources */, 3706FE16293F661700E42796 /* CSVImporterTests.swift in Sources */, @@ -10434,6 +10560,7 @@ B603972D29BEDF2100902A34 /* ExpectedNavigationExtension.swift in Sources */, 3706FEA6293F662100E42796 /* EncryptionKeyStoreTests.swift in Sources */, B6F5656A299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */, + B693766F2B6B5F27005BD9D4 /* ErrorPageTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10474,6 +10601,7 @@ B603972C29BEDF2100902A34 /* ExpectedNavigationExtension.swift in Sources */, 4B1AD8D525FC38DD00261379 /* EncryptionKeyStoreTests.swift in Sources */, B6F56568299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */, + B693766E2B6B5F27005BD9D4 /* ErrorPageTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10493,11 +10621,11 @@ 4B2D06332A11C1E300DE1F49 /* OptionalExtension.swift in Sources */, 4BF0E50B2AD2552200FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 4B41EDA12B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, - 4B2537782A11C00F00610219 /* NetworkProtectionExtensionMachService.swift in Sources */, + 7B0099822B65C6B300FE7C31 /* MacTransparentProxyProvider.swift in Sources */, B65DA5F32A77D3C700CBEE8D /* UserDefaultsWrapper.swift in Sources */, 4B2537722A11BF8B00610219 /* main.swift in Sources */, EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */, - 4B2D06292A11C0C900DE1F49 /* NetworkProtectionBundle.swift in Sources */, + 4B2D06292A11C0C900DE1F49 /* Bundle+VPN.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10516,14 +10644,14 @@ 7BA7CC4C2AD11EC70042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, B6F92BAC2A6937B3002ABA6B /* OptionalExtension.swift in Sources */, 7B8DB31A2B504D7500EC16DA /* VPNAppEventsHandler.swift in Sources */, - 7BA7CC532AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */, - 7BA7CC3C2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */, + 7BA7CC532AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */, 7BFE95562A9DF29B0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, 7BA7CC5D2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, 7BA7CC4A2AD11EA00042E5CE /* NetworkProtectionTunnelController.swift in Sources */, 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */, 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, + 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */, EEC589DB2A4F1CE700BCD60C /* AppLauncher.swift in Sources */, B65DA5EF2A77CC3A00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, 4BF0E5072AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, @@ -10542,10 +10670,10 @@ B6F92BA32A691583002ABA6B /* UserDefaultsWrapper.swift in Sources */, 4BA7C4DB2B3F63AE00AFE511 /* NetworkExtensionController.swift in Sources */, 4B2D067C2A13340900DE1F49 /* Logging.swift in Sources */, + 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */, B6F92BAD2A6937B5002ABA6B /* OptionalExtension.swift in Sources */, 4BA7C4D92B3F61FB00AFE511 /* BundleExtension.swift in Sources */, 7BA7CC5A2AD120640042E5CE /* NetworkProtection+ConvenienceInitializers.swift in Sources */, - 7BA7CC3B2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */, EEC589DC2A4F1CE800BCD60C /* AppLauncher.swift in Sources */, 7BA7CC3F2AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 4B0EF7292B5780EB009D6481 /* VPNAppEventsHandler.swift in Sources */, @@ -10563,7 +10691,7 @@ 7BA7CC3D2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 4BA7C4DA2B3F639800AFE511 /* NetworkProtectionTunnelController.swift in Sources */, 7BA7CC432AD11E480042E5CE /* UserText.swift in Sources */, - 7BA7CC542AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */, + 7BA7CC542AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10574,12 +10702,11 @@ 4B4BEC3D2A11B56B001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift in Sources */, 4B4BEC3E2A11B56E001D9AC5 /* Logging.swift in Sources */, 4B4BEC412A11B5BD001D9AC5 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */, - 4B4BEC432A11B5C7001D9AC5 /* NetworkProtectionBundle.swift in Sources */, + 4B4BEC432A11B5C7001D9AC5 /* Bundle+VPN.swift in Sources */, 4B4BEC452A11B5EE001D9AC5 /* UserText+NetworkProtectionExtensions.swift in Sources */, 4B4BEC422A11B5C7001D9AC5 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8222A1E2603006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, B602E81A2A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, - 4B4BEC402A11B5B5001D9AC5 /* NetworkProtectionExtensionMachService.swift in Sources */, EEAD7A7C2A1D3E20002A24E7 /* AppLauncher.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -10590,14 +10717,14 @@ files = ( 4B41EDA02B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */, + 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */, 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8182A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, B65DA5F52A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, 4B4D60892A0B2A1C00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */, - 4B4D60A02A0B2D5B00BCD287 /* NetworkProtectionBundle.swift in Sources */, + 4B4D60A02A0B2D5B00BCD287 /* Bundle+VPN.swift in Sources */, 4B4D60AD2A0C807300BCD287 /* NSApplicationExtension.swift in Sources */, 4B4D60A52A0B2EC000BCD287 /* UserText+NetworkProtectionExtensions.swift in Sources */, - EEF12E6E2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */, 4BF0E50C2AD2552300FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 4B4D60AC2A0C804B00BCD287 /* OptionalExtension.swift in Sources */, B65DA5F22A77D3C600CBEE8D /* UserDefaultsWrapper.swift in Sources */, @@ -10735,7 +10862,7 @@ 4B9579BB2AC7AE700062CA31 /* SequenceExtensions.swift in Sources */, 4B9579BC2AC7AE700062CA31 /* WKBackForwardListExtension.swift in Sources */, 4B9579BD2AC7AE700062CA31 /* ChromiumDataImporter.swift in Sources */, - 4B9579BE2AC7AE700062CA31 /* WKBackForwardListItemViewModel.swift in Sources */, + 4B9579BE2AC7AE700062CA31 /* BackForwardListItemViewModel.swift in Sources */, 4B9579BF2AC7AE700062CA31 /* BWNotRespondingAlert.swift in Sources */, 4B9579C02AC7AE700062CA31 /* DebugUserScript.swift in Sources */, 1DC669722B6CF0D700AA0645 /* TabSnapshotStore.swift in Sources */, @@ -10882,7 +11009,7 @@ 4B957A422AC7AE700062CA31 /* SafariFaviconsReader.swift in Sources */, 4B957A432AC7AE700062CA31 /* NSScreenExtension.swift in Sources */, 4B957A442AC7AE700062CA31 /* NSBezierPathExtension.swift in Sources */, - 4B957A452AC7AE700062CA31 /* NetworkProtectionBundle.swift in Sources */, + 4B957A452AC7AE700062CA31 /* Bundle+VPN.swift in Sources */, B68D21CA2ACBC971002DA3C2 /* MockPrivacyConfiguration.swift in Sources */, 4B957A462AC7AE700062CA31 /* WebsiteDataStore.swift in Sources */, 4B957A472AC7AE700062CA31 /* NetworkProtectionFeatureVisibility.swift in Sources */, @@ -10959,6 +11086,7 @@ 4B957A842AC7AE700062CA31 /* PreferencesDownloadsView.swift in Sources */, 4B957A852AC7AE700062CA31 /* QRSharingService.swift in Sources */, 4B957A862AC7AE700062CA31 /* ProcessExtension.swift in Sources */, + B68412162B694BA10092F66A /* NSObject+performSelector.m in Sources */, 4B957A872AC7AE700062CA31 /* PermissionAuthorizationQuery.swift in Sources */, 4B957A882AC7AE700062CA31 /* BadgeAnimationView.swift in Sources */, 4B957A892AC7AE700062CA31 /* BrowserTabSelectionDelegate.swift in Sources */, @@ -11212,6 +11340,7 @@ 4B957B6C2AC7AE700062CA31 /* MouseOverAnimationButton.swift in Sources */, 4B957B6D2AC7AE700062CA31 /* TabBarScrollView.swift in Sources */, B6B5F5822B024105008DB58A /* DataImportSummaryView.swift in Sources */, + B684121E2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift in Sources */, 4B957B6E2AC7AE700062CA31 /* BookmarkListTreeControllerDataSource.swift in Sources */, 4B957B6F2AC7AE700062CA31 /* AddressBarViewController.swift in Sources */, 4B957B702AC7AE700062CA31 /* Permissions.swift in Sources */, @@ -11226,6 +11355,7 @@ 4B957B782AC7AE700062CA31 /* ContentOverlayViewController.swift in Sources */, 4B957B792AC7AE700062CA31 /* ContentBlockingTabExtension.swift in Sources */, 4B957B7A2AC7AE700062CA31 /* OnboardingViewController.swift in Sources */, + B68412292B6A68C90092F66A /* WKBackForwardListItemExtension.swift in Sources */, 4B957B7B2AC7AE700062CA31 /* DeviceAuthenticator.swift in Sources */, 4B957B7C2AC7AE700062CA31 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */, 4B957B7D2AC7AE700062CA31 /* TabBarCollectionView.swift in Sources */, @@ -11348,11 +11478,25 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7BDA36E12B7E037100AD5388 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7B97CD5C2B7E0BBB004FEF43 /* UserDefaultsWrapper.swift in Sources */, + 7B97CD602B7E0C2E004FEF43 /* Logging.swift in Sources */, + 7B97CD5E2B7E0BEA004FEF43 /* OptionalExtension.swift in Sources */, + 7B97CD5F2B7E0BF7004FEF43 /* NSApplicationExtension.swift in Sources */, + 7BDA36F52B7E055800AD5388 /* MacTransparentProxyProvider.swift in Sources */, + 7B97CD5D2B7E0BCE004FEF43 /* BundleExtension.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9D9AE8B62AAA39A70026E7DC /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, + 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */, 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, 9D9AE91D2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9212AAA3B450026E7DC /* UserText.swift in Sources */, @@ -11364,6 +11508,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */, 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, 9D9AE91E2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, @@ -11436,6 +11581,7 @@ 4BF0E5122AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, AA6EF9AD25066F42004754E6 /* WindowsManager.swift in Sources */, 1D43EB3A292B63B00065E5D6 /* BWRequest.swift in Sources */, + B684121C2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift in Sources */, B68458CD25C7EB9000DC17B6 /* WKWebViewConfigurationExtensions.swift in Sources */, 85AC7ADD27BEB6EE00FFB69B /* HomePageDefaultBrowserModel.swift in Sources */, B6619EFB2B111CC500CD9186 /* InstructionsFormatParser.swift in Sources */, @@ -11506,7 +11652,7 @@ B602E7CF2A93A5FF00F12201 /* WKBackForwardListExtension.swift in Sources */, 4B59024026B35F3600489384 /* ChromiumDataImporter.swift in Sources */, B62B48562ADE730D000DECE5 /* FileImportView.swift in Sources */, - AAA0CC3C25337FAB0079BC96 /* WKBackForwardListItemViewModel.swift in Sources */, + AAA0CC3C25337FAB0079BC96 /* BackForwardListItemViewModel.swift in Sources */, 1D43EB3429297D760065E5D6 /* BWNotRespondingAlert.swift in Sources */, 4BB88B4525B7B55C006F6B06 /* DebugUserScript.swift in Sources */, AAC6881928626BF800D54247 /* RecentlyClosedTab.swift in Sources */, @@ -11585,6 +11731,7 @@ 1D36E658298AA3BA00AA485D /* InternalUserDeciderStore.swift in Sources */, B634DBE5293C944700C3C99E /* NewWindowPolicy.swift in Sources */, 31CF3432288B0B1B0087244B /* NavigationBarBadgeAnimator.swift in Sources */, + B68412272B6A68C10092F66A /* WKBackForwardListItemExtension.swift in Sources */, 858A798526A8BB5D00A75A42 /* NSTextViewExtension.swift in Sources */, B634DBE7293C98C500C3C99E /* FutureExtension.swift in Sources */, B634DBE3293C900000C3C99E /* UserDialogRequest.swift in Sources */, @@ -11645,7 +11792,7 @@ 4B0AACAE28BC6FD0001038AC /* SafariFaviconsReader.swift in Sources */, B6B3E0E12657EA7A0040E0A2 /* NSScreenExtension.swift in Sources */, B65E6BA026D9F10600095F96 /* NSBezierPathExtension.swift in Sources */, - 4B4D60E02A0C875F00BCD287 /* NetworkProtectionBundle.swift in Sources */, + 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */, AA6820E425502F19005ED0D5 /* WebsiteDataStore.swift in Sources */, B6F9BDDC2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */, 4B67854A2AA8DE75008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */, @@ -11665,6 +11812,7 @@ 4BA1A6BD258B082300F6F690 /* EncryptionKeyStore.swift in Sources */, B6C00ED7292FB4B4009C73A6 /* TabExtensionsBuilder.swift in Sources */, 4BE65474271FCD40008D1D63 /* PasswordManagementIdentityItemView.swift in Sources */, + B68412142B694BA10092F66A /* NSObject+performSelector.m in Sources */, B6F41031264D2B23003DA42C /* ProgressExtension.swift in Sources */, 4B723E0F26B0006500E14D75 /* CSVParser.swift in Sources */, B6DA44082616B30600DD1EC2 /* PixelDataModel.xcdatamodeld in Sources */, @@ -12184,6 +12332,7 @@ 4B3F641E27A8D3BD00E0C118 /* BrowserProfileTests.swift in Sources */, B6106BA026A7BE0B0013B453 /* PermissionManagerTests.swift in Sources */, 4B59CC8C290083240058F2F6 /* ConnectBitwardenViewModelTests.swift in Sources */, + B68412202B6A30680092F66A /* StringExtensionTests.swift in Sources */, B662D3D92755D7AD0035D4D6 /* PixelStoreTests.swift in Sources */, B6106BB526A809E60013B453 /* GeolocationProviderTests.swift in Sources */, B6A5A2A025B96E8300AA7ADA /* AppStateChangePublisherTests.swift in Sources */, @@ -12415,6 +12564,11 @@ target = AA585D7D248FD31100E9A3E2 /* DuckDuckGo Privacy Browser */; targetProxy = 7B4CE8DF26F02108009134B1 /* PBXContainerItemProxy */; }; + 7BDA36F82B7E082100AD5388 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7BDA36E42B7E037100AD5388 /* VPNProxyExtension */; + targetProxy = 7BDA36F72B7E082100AD5388 /* PBXContainerItemProxy */; + }; 7BEC18312AD5DA3300D30536 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 4B2537592A11BE7300610219 /* NetworkProtectionSystemExtension */; @@ -12494,14 +12648,6 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - AA80EC69256C4691007083E7 /* BrowserTab.storyboard */ = { - isa = PBXVariantGroup; - children = ( - AA80EC68256C4691007083E7 /* Base */, - ); - name = BrowserTab.storyboard; - sourceTree = ""; - }; AA80EC75256C46A2007083E7 /* Suggestion.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -12892,6 +13038,34 @@ }; name = Release; }; + 7BDA36F02B7E037200AD5388 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + 7BDA36F12B7E037200AD5388 /* CI */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = CI; + }; + 7BDA36F22B7E037200AD5388 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 7BDA36F32B7E037200AD5388 /* Review */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = Review; + }; 9D9AE8CD2AAA39A70026E7DC /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7B6EC5E52AE2D8AF004FE6DF /* DuckDuckGoDBPAgent.xcconfig */; @@ -13199,6 +13373,17 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 7BDA36F42B7E037200AD5388 /* Build configuration list for PBXNativeTarget "VPNProxyExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7BDA36F02B7E037200AD5388 /* Debug */, + 7BDA36F12B7E037200AD5388 /* CI */, + 7BDA36F22B7E037200AD5388 /* Release */, + 7BDA36F32B7E037200AD5388 /* Review */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 9D9AE8CC2AAA39A70026E7DC /* Build configuration list for PBXNativeTarget "DuckDuckGoDBPBackgroundAgent" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -13742,6 +13927,18 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = NetworkProtection; }; + 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; 7B31FD8B2AD125620086AA24 /* NetworkProtectionIPC */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtectionIPC; @@ -13758,10 +13955,36 @@ isa = XCSwiftPackageProductDependency; productName = PixelKit; }; + 7B7DFB212B7E7473009EA1A3 /* Networking */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Networking; + }; 7B8C083B2AE1268E00F4C67F /* PixelKit */ = { isa = XCSwiftPackageProductDependency; productName = PixelKit; }; + 7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B97CD5A2B7E0B85004FEF43 /* Common */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Common; + }; + 7B97CD612B7E0C4B004FEF43 /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + productName = PixelKit; + }; + 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; 7BA59C9A2AE18B49009A97B1 /* SystemExtensionManager */ = { isa = XCSwiftPackageProductDependency; productName = SystemExtensionManager; @@ -13780,6 +14003,10 @@ isa = XCSwiftPackageProductDependency; productName = PixelKit; }; + 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */ = { isa = XCSwiftPackageProductDependency; productName = SystemExtensionManager; diff --git a/DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json new file mode 100644 index 0000000000..3fe9b59242 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json new file mode 100644 index 0000000000..802fa68a4c --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Alert-Circle-Color-16.imageset/Alert-Medium-Multicolor-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Alert-Circle-Color-16.imageset/Alert-Medium-Multicolor-16.pdf new file mode 100644 index 0000000000..9d34524e44 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/Alert-Circle-Color-16.imageset/Alert-Medium-Multicolor-16.pdf differ diff --git a/DuckDuckGo/Assets.xcassets/Images/Alert-Circle-Color-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Alert-Circle-Color-16.imageset/Contents.json new file mode 100644 index 0000000000..55f0057de9 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Alert-Circle-Color-16.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Alert-Medium-Multicolor-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Audio-Mute-12.pdf b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Audio-Mute-12.pdf new file mode 100644 index 0000000000..de1f4718ed Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Audio-Mute-12.pdf differ diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json new file mode 100644 index 0000000000..b609317961 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Audio-Mute-12.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Audio-12.pdf b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Audio-12.pdf new file mode 100644 index 0000000000..635e45f874 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Audio-12.pdf differ diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json new file mode 100644 index 0000000000..35d4dda319 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Audio-12.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/DefaultFavicon.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DefaultFavicon.imageset/Contents.json deleted file mode 100644 index 1d9b14c5fe..0000000000 --- a/DuckDuckGo/Assets.xcassets/Images/DefaultFavicon.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "default-fav.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/DuckDuckGo/Assets.xcassets/Images/DefaultFavicon.imageset/default-fav.png b/DuckDuckGo/Assets.xcassets/Images/DefaultFavicon.imageset/default-fav.png deleted file mode 100644 index 566b201475..0000000000 Binary files a/DuckDuckGo/Assets.xcassets/Images/DefaultFavicon.imageset/default-fav.png and /dev/null differ diff --git a/DuckDuckGo/Assets.xcassets/Images/Globe-Multicolor-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Globe-Multicolor-16.imageset/Contents.json new file mode 100644 index 0000000000..771bdd6276 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Globe-Multicolor-16.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Globe-Multicolor-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Globe-Multicolor-16.imageset/Globe-Multicolor-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Globe-Multicolor-16.imageset/Globe-Multicolor-16.pdf new file mode 100644 index 0000000000..35a5fd2eea Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/Globe-Multicolor-16.imageset/Globe-Multicolor-16.pdf differ diff --git a/DuckDuckGo/Bridging.h b/DuckDuckGo/Bridging.h index 8edc3aeb9e..a5d8f3cb44 100644 --- a/DuckDuckGo/Bridging.h +++ b/DuckDuckGo/Bridging.h @@ -6,6 +6,7 @@ #import "WKWebView+Private.h" #import "NSException+Catch.h" +#import "NSObject+performSelector.h" #import "WKGeolocationProvider.h" #ifndef APPSTORE diff --git a/DuckDuckGo/Common/Extensions/NSAlertExtension.swift b/DuckDuckGo/Common/Extensions/NSAlertExtension.swift index 938977500c..616df15e44 100644 --- a/DuckDuckGo/Common/Extensions/NSAlertExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSAlertExtension.swift @@ -148,6 +148,16 @@ extension NSAlert { return alert } + static func removeAllDBPStateAndDataAlert() -> NSAlert { + let alert = NSAlert() + alert.messageText = "Uninstall Personal Information Removal Login Item?" + alert.informativeText = "This will remove the Personal Information Removal Login Item, delete all your data and reset the waitlist state." + alert.alertStyle = .warning + alert.addButton(withTitle: "Uninstall") + alert.addButton(withTitle: UserText.cancel) + return alert + } + static func noAccessToDownloads() -> NSAlert { let alert = NSAlert() alert.messageText = UserText.noAccessToDownloadsFolderHeader diff --git a/DuckDuckGo/Common/Extensions/NSObject+performSelector.h b/DuckDuckGo/Common/Extensions/NSObject+performSelector.h new file mode 100644 index 0000000000..5c93f950ff --- /dev/null +++ b/DuckDuckGo/Common/Extensions/NSObject+performSelector.h @@ -0,0 +1,27 @@ +// +// NSObject+performSelector.h +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSObject (performSelector) +- (id)performSelector:(SEL)selector withArguments:(NSArray *)arguments; +@end + +NS_ASSUME_NONNULL_END diff --git a/DuckDuckGo/Common/Extensions/NSObject+performSelector.m b/DuckDuckGo/Common/Extensions/NSObject+performSelector.m new file mode 100644 index 0000000000..d5b7a7bb08 --- /dev/null +++ b/DuckDuckGo/Common/Extensions/NSObject+performSelector.m @@ -0,0 +1,52 @@ +// +// NSObject+performSelector.m +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "NSObject+performSelector.h" + +@implementation NSObject (performSelector) + +- (id)performSelector:(SEL)selector withArguments:(NSArray *)arguments { + NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector]; + + if (!methodSignature) { + [[[NSException alloc] initWithName:@"InvalidSelectorOrTarget" reason:[NSString stringWithFormat:@"Could not get method signature for selector %@ on %@", NSStringFromSelector(selector), self] userInfo:nil] raise]; + } + + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [invocation setSelector:selector]; + [invocation setTarget:self]; + + for (NSInteger i = 0; i < arguments.count; i++) { + id argument = arguments[i]; + [invocation setArgument:&argument atIndex:i + 2]; // Indices 0 and 1 are reserved for target and selector + } + + [invocation invoke]; + + if (methodSignature.methodReturnLength > 0) { + id returnValue; + [invocation getReturnValue:&returnValue]; + + return returnValue; + } + + return nil; +} + + +@end diff --git a/DuckDuckGo/Common/Extensions/StringExtension.swift b/DuckDuckGo/Common/Extensions/StringExtension.swift index 53cd3148a2..b6d6affbb0 100644 --- a/DuckDuckGo/Common/Extensions/StringExtension.swift +++ b/DuckDuckGo/Common/Extensions/StringExtension.swift @@ -16,8 +16,9 @@ // limitations under the License. // -import Foundation import BrowserServicesKit +import Common +import Foundation import UniformTypeIdentifiers extension String { @@ -32,6 +33,42 @@ extension String { self.replacingOccurrences(of: "\\", with: "\\\\") .replacingOccurrences(of: "\"", with: "\\\"") .replacingOccurrences(of: "'", with: "\\'") + .replacingOccurrences(of: "\n", with: "\\n") + } + + private static let unicodeHtmlCharactersMapping: [Character: String] = [ + "&": "&", + "\"": """, + "'": "'", + "<": "<", + ">": ">", + "/": "/", + "!": "!", + "$": "$", + "%": "%", + "=": "=", + "#": "#", + "@": "@", + "[": "[", + "\\": "\", + "]": "]", + "^": "^", + "`": "a", + "{": "{", + "}": "}", + ] + func escapedUnicodeHtmlString() -> String { + var result = "" + + for character in self { + if let mapped = Self.unicodeHtmlCharactersMapping[character] { + result.append(mapped) + } else { + result.append(character) + } + } + + return result } init(_ staticString: StaticString) { diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index 7ab5a0deff..51144f4e3d 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -130,6 +130,8 @@ extension URL { static let welcome = URL(string: "duck://welcome")! static let settings = URL(string: "duck://settings")! static let bookmarks = URL(string: "duck://bookmarks")! + // base url for Error Page Alternate HTML loaded into Web View + static let error = URL(string: "duck://error")! static let dataBrokerProtection = URL(string: "duck://dbp")! diff --git a/DuckDuckGo/Common/Extensions/WKBackForwardListItemExtension.swift b/DuckDuckGo/Common/Extensions/WKBackForwardListItemExtension.swift new file mode 100644 index 0000000000..909d3bcc9c --- /dev/null +++ b/DuckDuckGo/Common/Extensions/WKBackForwardListItemExtension.swift @@ -0,0 +1,35 @@ +// +// WKBackForwardListItemExtension.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import WebKit + +extension WKBackForwardListItem { + + // sometimes WKBackForwardListItem returns wrong or outdated title + private static let tabTitleKey = UnsafeRawPointer(bitPattern: "tabTitleKey".hashValue)! + var tabTitle: String? { + get { + objc_getAssociatedObject(self, Self.tabTitleKey) as? String + } + set { + objc_setAssociatedObject(self, Self.tabTitleKey, newValue, .OBJC_ASSOCIATION_RETAIN) + } + } + +} diff --git a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift index 01df6a149a..4c70d4bacc 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import Common import Navigation import WebKit @@ -28,6 +29,12 @@ extension WKWebView { return false } + enum AudioState { + case muted + case unmuted + case notSupported + } + enum CaptureState { case none case active @@ -128,6 +135,48 @@ extension WKWebView { } #endif + func muteOrUnmute() { +#if !APPSTORE + guard self.responds(to: #selector(WKWebView._setPageMuted(_:))) else { + assertionFailure("WKWebView does not respond to selector _stopMediaCapture") + return + } + let mutedState: _WKMediaMutedState = { + guard self.responds(to: #selector(WKWebView._mediaMutedState)) else { return [] } + return self._mediaMutedState() + }() + var newState = mutedState + + if newState == .audioMuted { + newState.remove(.audioMuted) + } else { + newState.insert(.audioMuted) + } + guard newState != mutedState else { return } + self._setPageMuted(newState) +#endif + } + + /// Returns the audio state of the WKWebView. + /// + /// - Returns: `muted` if the web view is muted + /// `unmuted` if the web view is unmuted + /// `notSupported` if the web view does not support fetching the current audio state + func audioState() -> AudioState { +#if APPSTORE + return .notSupported +#else + guard self.responds(to: #selector(WKWebView._mediaMutedState)) else { + assertionFailure("WKWebView does not respond to selector _mediaMutedState") + return .notSupported + } + + let mutedState = self._mediaMutedState() + + return mutedState.contains(.audioMuted) ? .muted : .unmuted +#endif + } + func stopMediaCapture() { guard #available(macOS 12.0, *) else { #if !APPSTORE @@ -231,6 +280,21 @@ extension WKWebView { self.evaluateJavaScript("window.open(\(urlEnc), '_blank', 'noopener, noreferrer')") } + func loadAlternateHTML(_ html: String, baseURL: URL, forUnreachableURL failingURL: URL) { + guard responds(to: Selector.loadAlternateHTMLString) else { + if #available(macOS 12.0, *) { + os_log(.error, log: .navigation, "WKWebView._loadAlternateHTMLString not available") + loadSimulatedRequest(URLRequest(url: failingURL), responseHTML: html) + } + return + } + self.perform(Selector.loadAlternateHTMLString, withArguments: [html, baseURL, failingURL]) + } + + func setDocumentHtml(_ html: String) { + self.evaluateJavaScript("document.open(); document.write('\(html.escapedJavaScriptString())'); document.close()", in: nil, in: .defaultClient) + } + @MainActor var mimeType: String? { get async { @@ -285,6 +349,7 @@ extension WKWebView { enum Selector { static let fullScreenPlaceholderView = NSSelectorFromString("_fullScreenPlaceholderView") static let printOperationWithPrintInfoForFrame = NSSelectorFromString("_printOperationWithPrintInfo:forFrame:") + static let loadAlternateHTMLString = NSSelectorFromString("_loadAlternateHTMLString:baseURL:forUnreachableURL:") } } diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index e590978dc5..eee5b3f4e5 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -17,6 +17,7 @@ // import Foundation +import Navigation struct UserText { @@ -76,7 +77,7 @@ struct UserText { // MARK: - Main Menu -> -File static let mainMenuFile = NSLocalizedString("main-menu.file", value:"File", comment: "Main Menu File") static let mainMenuFileNewTab = NSLocalizedString("main-menu.file.new-tab", value:"New Tab", comment: "Main Menu File item") - static let mainMenuFileOpenLocation = NSLocalizedString("main-menu.file.open-location", value:"Open Location…", comment: "Main Menu File item") + static let mainMenuFileOpenLocation = NSLocalizedString("main-menu.file.open-location", value:"Open Location…", comment: "Main Menu File item- Menu option that allows the user to connect to an address (type an address) on click the address bar of the browser is selected and the user can type.") static let mainMenuFileCloseWindow = NSLocalizedString("main-menu.file.close-window", value:"Close Window", comment: "Main Menu File item") static let mainMenuFileCloseAllWindows = NSLocalizedString("main-menu.file.close-all-windows", value:"Close All Windows", comment: "Main Menu File item") static let mainMenuFileSaveAs = NSLocalizedString("main-menu.file.save-as", value:"Save As…", comment: "Main Menu File item") @@ -92,7 +93,7 @@ struct UserText { static let mainMenuEditCut = NSLocalizedString("main-menu.edit.cut", value:"Cut", comment: "Main Menu Edit item") static let mainMenuEditCopy = NSLocalizedString("main-menu.edit.copy", value:"Copy", comment: "Main Menu Edit item") static let mainMenuEditPaste = NSLocalizedString("main-menu.edit.paste", value:"Paste", comment: "Main Menu Edit item") - static let mainMenuEditPasteAndMatchStyle = NSLocalizedString("main-menu.edit.paste-and-match-style", value:"Paste and Match Style", comment: "Main Menu Edit item") + static let mainMenuEditPasteAndMatchStyle = NSLocalizedString("main-menu.edit.paste-and-match-style", value:"Paste and Match Style", comment: "Main Menu Edit item - Action that allows the user to paste copy into a target document and the target document's style will be retained (instead of the source style)") static let mainMenuEditDelete = NSLocalizedString("main-menu.edit.delete", value:"Delete", comment: "Main Menu Edit item") static let mainMenuEditSelectAll = NSLocalizedString("main-menu.edit.select-all", value:"Select All", comment: "Main Menu Edit item") @@ -198,6 +199,8 @@ struct UserText { static let pinTab = NSLocalizedString("pin.tab", value: "Pin Tab", comment: "Menu item. Pin as a verb") static let unpinTab = NSLocalizedString("unpin.tab", value: "Unpin Tab", comment: "Menu item. Unpin as a verb") static let closeTab = NSLocalizedString("close.tab", value: "Close Tab", comment: "Menu item") + static let muteTab = NSLocalizedString("mute.tab", value: "Mute Tab", comment: "Menu item. Mute tab") + static let unmuteTab = NSLocalizedString("unmute.tab", value: "Unmute Tab", comment: "Menu item. Unmute tab") static let closeOtherTabs = NSLocalizedString("close.other.tabs", value: "Close Other Tabs", comment: "Menu item") static let closeTabsToTheRight = NSLocalizedString("close.tabs.to.the.right", value: "Close Tabs to the Right", comment: "Menu item") static let openInNewTab = NSLocalizedString("open.in.new.tab", value: "Open in New Tab", comment: "Menu item that opens the link in a new tab") @@ -212,12 +215,15 @@ struct UserText { static let tabPreferencesTitle = NSLocalizedString("tab.preferences.title", value: "Settings", comment: "Tab preferences title") static let tabBookmarksTitle = NSLocalizedString("tab.bookmarks.title", value: "Bookmarks", comment: "Tab bookmarks title") static let tabOnboardingTitle = NSLocalizedString("tab.onboarding.title", value: "Welcome", comment: "Tab onboarding title") - static let tabErrorTitle = NSLocalizedString("tab.error.title", value: "Oops!", comment: "Tab error title") + static let tabErrorTitle = NSLocalizedString("tab.error.title", value: "Failed to open page", comment: "Tab error title") + static let errorPageHeader = NSLocalizedString("page.error.header", value: "DuckDuckGo can’t load this page.", comment: "Error page heading text") + static let webProcessCrashPageHeader = NSLocalizedString("page.crash.header", value: "This webpage has crashed.", comment: "Error page heading text shown when a Web Page process had crashed") + static let webProcessCrashPageMessage = NSLocalizedString("page.crash.message", value: "Try reloading the page or come back later.", comment: "Error page message text shown when a Web Page process had crashed") + static let openSystemPreferences = NSLocalizedString("open.preferences", value: "Open System Preferences", comment: "Open System Preferences (to re-enable permission for the App) (up to and including macOS 12") - static let openSystemSettings = NSLocalizedString("open.settings", value: "Open System Settings…", comment: "") + static let openSystemSettings = NSLocalizedString("open.settings", value: "Open System Settings…", comment: "This string represents a prompt or button label prompting the user to open system settings") static let checkForUpdate = NSLocalizedString("check.for.update", value: "Check for Update", comment: "Button users can use to check for a new update") - static let unknownErrorMessage = NSLocalizedString("error.unknown", value: "An unknown error has occurred", comment: "Error page subtitle") static let unknownErrorTryAgainMessage = NSLocalizedString("error.unknown.try.again", value: "An unknown error has occurred", comment: "Generic error message on a dialog for when the cause is not known.") static let moveTabToNewWindow = NSLocalizedString("options.menu.move.tab.to.new.window", @@ -284,19 +290,16 @@ struct UserText { static let selectedDomainsDescription = NSLocalizedString("fire.selected-domains.description", value: "Clear data only for selected domains", comment: "Description of the 'Current Window' configuration option for the fire button") static let selectSiteToClear = NSLocalizedString("fire.select-site-to-clear", value: "Select a site to clear its data.", comment: "Info label in the fire button popover") static func activeTabsInfo(tabs: Int, sites: Int) -> String { - let siteString = sites == 1 ? "site" : "sites" - let tabsString = tabs == 1 ? "tab" : "tabs" let localized = NSLocalizedString("fire.active-tabs-info", - value: "Close %d active %@ and clear all browsing history and cookies (%d %@).", + value: "Close active tabs (%d) and clear all browsing history and cookies (sites: %d).", comment: "Info in the Fire Button popover") - return String(format: localized, tabs, tabsString, sites, siteString) + return String(format: localized, tabs, sites) } static func oneTabInfo(sites: Int) -> String { - let siteString = sites == 1 ? "site" : "sites" let localized = NSLocalizedString("fire.one-tab-info", - value: "Close this tab and clear its browsing history and cookies (%d %@).", + value: "Close this tab and clear its browsing history and cookies (sites: %d).", comment: "Info in the Fire Button popover") - return String(format: localized, sites, siteString) + return String(format: localized, sites) } static let fireDialogDetails = NSLocalizedString("fire.dialog.details", value: "Details", comment: "Button to show more details") static let fireDialogWindowWillClose = NSLocalizedString("fire.dialog.window-will-close", value: "Current window will close", comment: "Warning label shown in an expanded view of the fire popover") @@ -346,7 +349,7 @@ struct UserText { static let autofillPasswordManagerBitwarden = NSLocalizedString("autofill.password-manager.bitwarden", value: "Bitwarden", comment: "Autofill password manager row title") static let autofillPasswordManagerBitwardenDisclaimer = NSLocalizedString("autofill.password-manager.bitwarden.disclaimer", value: "Setup requires installing the Bitwarden app.", comment: "Autofill password manager Bitwarden disclaimer") static let restartBitwarden = NSLocalizedString("restart.bitwarden", value: "Restart Bitwarden", comment: "Button to restart Bitwarden application") - static let restartBitwardenInfo = NSLocalizedString("restart.bitwarden.info", value: "Bitwarden is not responding. Please restart it to initiate the communication again", comment: "") + static let restartBitwardenInfo = NSLocalizedString("restart.bitwarden.info", value: "Bitwarden is not responding. Please restart it to initiate the communication again", comment: "This string represents a message informing the user that Bitwarden is not responding and prompts them to restart the application to initiate communication again.") static let autofillViewContentButton = NSLocalizedString("autofill.view-autofill-content", value: "View Autofill Content…", comment: "View Autofill Content Button name in the autofill settings") static let autofillAskToSave = NSLocalizedString("autofill.ask-to-save", value: "Save and Autofill", comment: "Autofill settings section title") @@ -444,8 +447,8 @@ struct UserText { static let emailOptionsMenuItem = NSLocalizedString("email.optionsMenu", value: "Email Protection", comment: "Menu item email feature") static let emailOptionsMenuCreateAddressSubItem = NSLocalizedString("email.optionsMenu.createAddress", value: "Generate Private Duck Address", comment: "Create an email alias sub menu item") static let emailOptionsMenuTurnOffSubItem = NSLocalizedString("email.optionsMenu.turnOff", value: "Disable Email Protection Autofill", comment: "Disable email sub menu item") - static let emailOptionsMenuTurnOnSubItem = NSLocalizedString("email.optionsMenu.turnOn", value: "Enable Email Protection", comment: "Enable email sub menu item") - static let privateEmailCopiedToClipboard = NSLocalizedString("email.copied", value: "New address copied to your clipboard", comment: "Private email address was copied to clipboard message") + static let emailOptionsMenuTurnOnSubItem = NSLocalizedString("email.optionsMenu.turnOn", value: "Enable Email Protection", comment: "Sub menu item to enable Email Protection") + static let privateEmailCopiedToClipboard = NSLocalizedString("email.copied", value: "New address copied to your clipboard", comment: "Notification that the Private email address was copied to clipboard after the user generated a new address") static let emailOptionsMenuManageAccountSubItem = NSLocalizedString("email.optionsMenu.manageAccount", value: "Manage Account", comment: "Manage private email account sub menu item") static let newFolder = NSLocalizedString("folder.optionsMenu.newFolder", value: "New Folder", comment: "Option for creating a new folder") @@ -456,7 +459,7 @@ struct UserText { static let updateBookmark = NSLocalizedString("bookmark.update", value: "Update Bookmark", comment: "Option for updating a bookmark") - static let failedToOpenExternally = NSLocalizedString("open.externally.failed", value: "The app required to open that link can’t be found", comment: "’Link’ is link on a website") + static let failedToOpenExternally = NSLocalizedString("open.externally.failed", value: "The app required to open that link can’t be found", comment: "’Link’ is link on a website, it couldn't be opened due to the required app not being found") // MARK: Permission static let devicePermissionAuthorizationFormat = NSLocalizedString("permission.authorization.format", @@ -493,7 +496,7 @@ struct UserText { static let permissionGeolocationServicesDisabled = NSLocalizedString("permission.disabled.system", value: "System location services are disabled", comment: "Geolocation Services are disabled in System Preferences") static let permissionOpenSystemSettings = NSLocalizedString("permission.open.settings", value: "Open System Settings", comment: "Open System Settings (to re-enable permission for the App) (macOS 13 and above)") - static let permissionPopupTitle = NSLocalizedString("permission.popup.title", value: "Blocked Pop-ups", comment: "List of blocked popups Title") + static let permissionPopupTitle = NSLocalizedString("permission.popup.title", value: "Blocked Pop-ups", comment: "Title of a popup that has a list of blocked popups") static let permissionPopupOpenFormat = NSLocalizedString("permission.popup.open.format", value: "%@", comment: "Open %@ URL Pop-up") static let permissionExternalSchemeOpenFormat = NSLocalizedString("permission.externalScheme.open.format", value: "Open %@", comment: "Open %@ App Name") @@ -530,7 +533,7 @@ struct UserText { static let downloads = NSLocalizedString("preferences.downloads", value: "Downloads", comment: "Show downloads browser preferences") static let isDefaultBrowser = NSLocalizedString("preferences.default-browser.active", value: "DuckDuckGo is your default browser", comment: "Indicate that the browser is the default") static let isNotDefaultBrowser = NSLocalizedString("preferences.default-browser.inactive", value: "DuckDuckGo is not your default browser.", comment: "Indicate that the browser is not the default") - static let makeDefaultBrowser = NSLocalizedString("preferences.default-browser.button.make-default", value: "Make DuckDuckGo Default…", comment: "") + static let makeDefaultBrowser = NSLocalizedString("preferences.default-browser.button.make-default", value: "Make DuckDuckGo Default…", comment: "represents a prompt message asking the user to make DuckDuckGo their default browser.") static let onStartup = NSLocalizedString("preferences.on-startup", value: "On Startup", comment: "Name of the preferences section related to app startup") static let reopenAllWindowsFromLastSession = NSLocalizedString("preferences.reopen-windows", value: "Reopen all windows from last session", comment: "Option to control session restoration") static let showHomePage = NSLocalizedString("preferences.show-home", value: "Open a new window", comment: "Option to control session startup") @@ -569,18 +572,18 @@ struct UserText { static let aboutDuckDuckGo = NSLocalizedString("preferences.about.about-duckduckgo", value: "About DuckDuckGo", comment: "About screen") static let privacySimplified = NSLocalizedString("preferences.about.privacy-simplified", value: "Privacy, simplified.", comment: "About screen") - static let aboutUnsupportedDeviceInfo1 = NSLocalizedString("preferences.about.unsupported-device-info1", value: "DuckDuckGo is no longer providing browser updates for your version of macOS.", comment: "") + static let aboutUnsupportedDeviceInfo1 = NSLocalizedString("preferences.about.unsupported-device-info1", value: "DuckDuckGo is no longer providing browser updates for your version of macOS.", comment: "This string represents a message informing the user that DuckDuckGo is no longer providing browser updates for their version of macOS") static func aboutUnsupportedDeviceInfo2(version: String) -> String { - let localized = NSLocalizedString("preferences.about.unsupported-device-info2", value: "Please update to macOS %@ or later to use the most recent version", comment: "Link to the about page") + let localized = NSLocalizedString("preferences.about.unsupported-device-info2", value: "Please update to macOS %@ or later to use the most recent version of DuckDuckGo. You can also keep using your current version of the browser, but it will not receive further updates.", comment: "Copy in section that tells the user to update their macOS version since their current version is unsupported") return String(format: localized, version) } - static let aboutUnsupportedDeviceInfo2Part1 = NSLocalizedString("preferences.about.unsupported-device-info2-part1", value: "Please", comment: "Second paragraph of unsupported device info - sentence part 1") + static let aboutUnsupportedDeviceInfo2Part1 = "Please" static func aboutUnsupportedDeviceInfo2Part2(version: String) -> String { - return String(format: NSLocalizedString("preferences.about.unsupported-device-info2-part2", value: "update to macOS %@", comment: "Second paragraph of unsupported device info - sentence part 2 (underlined)"), version) + return String(format: "update to macOS %@", version) } - static let aboutUnsupportedDeviceInfo2Part3 = NSLocalizedString("preferences.about.unsupported-device-info2-part3", value: "or later to use the most recent version", comment: "Second paragraph of unsupported device info - sentence part 3") - static let aboutUnsupportedDeviceInfo2Part4 = NSLocalizedString("preferences.about.unsupported-device-info2-part4", value: "of DuckDuckGo. You can also keep using your current version of the browser, but it will not receive further updates.", comment: "Second paragraph of unsupported device info - sentence part 4") - static let unsupportedDeviceInfoAlertHeader = NSLocalizedString("unsupported.device.info.alert.header", value: "Your version of macOS is no longer supported.", comment: "") + static let aboutUnsupportedDeviceInfo2Part3 = "or later to use the most recent version" + static let aboutUnsupportedDeviceInfo2Part4 = "of DuckDuckGo. You can also keep using your current version of the browser, but it will not receive further updates." + static let unsupportedDeviceInfoAlertHeader = NSLocalizedString("unsupported.device.info.alert.header", value: "Your version of macOS is no longer supported.", comment: "his string represents the header for an alert informing the user that their version of macOS is no longer supported") static func moreAt(url: String) -> String { @@ -590,10 +593,10 @@ struct UserText { static let sendFeedback = NSLocalizedString("preferences.about.send-feedback", value: "Send Feedback", comment: "Feedback button in the about preferences page") - static let feedbackDisclaimer = NSLocalizedString("feedback.disclaimer", value: "Reports sent to DuckDuckGo are 100% anonymous and only include your message, the DuckDuckGo app version, and your macOS version.", comment: "Disclaimer in breakage form") + static let feedbackDisclaimer = NSLocalizedString("feedback.disclaimer", value: "Reports sent to DuckDuckGo are 100% anonymous and only include your message, the DuckDuckGo app version, and your macOS version.", comment: "Disclaimer in breakage form - a form that users can submit to say that a website is not working properly in DuckDuckGo") - static let feedbackBugDescription = NSLocalizedString("feedback.bug.description", value: "Please describe the problem in as much detail as possible:", comment: "Label in the feedback form") - static let feedbackFeatureRequestDescription = NSLocalizedString("feedback.feature.request.description", value: "What feature would you like to see?", comment: "Label in the feedback form") + static let feedbackBugDescription = NSLocalizedString("feedback.bug.description", value: "Please describe the problem in as much detail as possible:", comment: "Label in the feedback form that users can submit to say that a website is not working properly in DuckDuckGo") + static let feedbackFeatureRequestDescription = NSLocalizedString("feedback.feature.request.description", value: "What feature would you like to see?", comment: "Label in the feedback form for feature requests.") static let feedbackOtherDescription = NSLocalizedString("feedback.other.description", value: "Please give us your feedback:", comment: "Label in the feedback form") static func versionLabel(version: String, build: String) -> String { @@ -697,7 +700,7 @@ struct UserText { static func importingBookmarks(_ numberOfBookmarks: Int?) -> String { if let numberOfBookmarks, numberOfBookmarks > 0 { - let localized = NSLocalizedString("import.bookmarks.number.progress.text", value: "Importing %d bookmarks…", comment: "Operation progress info message about %d number of bookmarks being imported") + let localized = NSLocalizedString("import.bookmarks.number.progress.text", value: "Importing bookmarks (%d)…", comment: "Operation progress info message about %d number of bookmarks being imported") return String(format: localized, numberOfBookmarks) } else { return NSLocalizedString("import.bookmarks.indefinite.progress.text", value: "Importing bookmarks…", comment: "Operation progress info message about indefinite number of bookmarks being imported") @@ -706,7 +709,7 @@ struct UserText { static func importingPasswords(_ numberOfPasswords: Int?) -> String { if let numberOfPasswords, numberOfPasswords > 0 { - let localized = NSLocalizedString("import.passwords.number.progress.text", value: "Importing %d passwords…", comment: "Operation progress info message about %d number of passwords being imported") + let localized = NSLocalizedString("import.passwords.number.progress.text", value: "Importing passwords (%d)…", comment: "Operation progress info message about %d number of passwords being imported") return String(format: localized, numberOfPasswords) } else { return NSLocalizedString("import.passwords.indefinite.progress.text", value: "Importing passwords…", comment: "Operation progress info message about indefinite number of passwords being imported") @@ -716,22 +719,22 @@ struct UserText { static let moreOrLessCollapse = NSLocalizedString("more.or.less.collapse", value: "Show Less", comment: "For collapsing views to show less.") static let moreOrLessExpand = NSLocalizedString("more.or.less.expand", value: "Show More", comment: "For expanding views to show more.") - static let defaultBrowserPromptMessage = NSLocalizedString("default.browser.prompt.message", value: "Make DuckDuckGo your default browser", comment: "") - static let defaultBrowserPromptButton = NSLocalizedString("default.browser.prompt.button", value: "Set Default…", comment: "") + static let defaultBrowserPromptMessage = NSLocalizedString("default.browser.prompt.message", value: "Make DuckDuckGo your default browser", comment: "represents a prompt message asking the user to make DuckDuckGo their default browser.") + static let defaultBrowserPromptButton = NSLocalizedString("default.browser.prompt.button", value: "Set Default…", comment: "represents a prompt message asking the user to make DuckDuckGo their default browser.") - static let homePageProtectionSummaryInfo = NSLocalizedString("home.page.protection.summary.info", value: "No recent activity", comment: "") + static let homePageProtectionSummaryInfo = NSLocalizedString("home.page.protection.summary.info", value: "No recent activity", comment: "This string represents a message in the protection summary on the home page, indicating that there is no recent activity") static func homePageProtectionSummaryMessage(numberOfTrackersBlocked: Int) -> String { let localized = NSLocalizedString("home.page.protection.summary.message", value: "%@ tracking attempts blocked", - comment: "") + comment: "The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@") return String(format: localized, NumberFormatter.localizedString(from: NSNumber(value: numberOfTrackersBlocked), number: .decimal)) } static let homePageProtectionDurationInfo = NSLocalizedString("home.page.protection.duration", value: "PAST 7 DAYS", comment: "Past 7 days in uppercase.") - static let homePageEmptyStateItemTitle = NSLocalizedString("home.page.empty.state.item.title", value: "Recently visited sites appear here", comment: "") - static let homePageEmptyStateItemMessage = NSLocalizedString("home.page.empty.state.item.message", value: "Keep browsing to see how many trackers were blocked", comment: "") - static let homePageNoTrackersFound = NSLocalizedString("home.page.no.trackers.found", value: "No trackers found", comment: "") - static let homePageNoTrackersBlocked = NSLocalizedString("home.page.no.trackers.blocked", value: "No trackers blocked", comment: "") + static let homePageEmptyStateItemTitle = NSLocalizedString("home.page.empty.state.item.title", value: "Recently visited sites appear here", comment: "This string represents the title for an empty state item on the home page, indicating that recently visited sites will appear here") + static let homePageEmptyStateItemMessage = NSLocalizedString("home.page.empty.state.item.message", value: "Keep browsing to see how many trackers were blocked", comment: "This string represents the message for an empty state item on the home page, encouraging the user to keep browsing to see how many trackers were blocked") + static let homePageNoTrackersFound = NSLocalizedString("home.page.no.trackers.found", value: "No trackers found", comment: "This string represents a message on the home page indicating that no trackers were found") + static let homePageNoTrackersBlocked = NSLocalizedString("home.page.no.trackers.blocked", value: "No trackers blocked", comment: "This string represents a message on the home page indicating that no trackers were blocked") static let homePageBurnFireproofSiteAlert = NSLocalizedString("home.page.burn.fireproof.site.alert", value: "History will be cleared for this site, but related data will remain, because this site is Fireproof", comment: "Message for an alert displayed when trying to burn a fireproof website") static let homePageClearHistory = NSLocalizedString("home.page.clear.history", value: "Clear History", comment: "Button caption for the burn fireproof website alert") @@ -740,21 +743,20 @@ struct UserText { static func tooltipClearHistoryAndData(domain: String) -> String { let localized = NSLocalizedString("tooltip.clearHistoryAndData", value: "Clear browsing history and data for %@", - comment: "Tooltip for burn button") + comment: "Tooltip for burn button where %@ is the domain") return String(format: localized, domain) } static func tooltipClearHistory(domain: String) -> String { let localized = NSLocalizedString("tooltip.clearHistory", value: "Clear browsing history for %@", - comment: "Tooltip for burn button") + comment: "Tooltip for burn button where %@ is the domain") return String(format: localized, domain) } - static let recentlyClosedMenuItemSuffixOne = NSLocalizedString("one.more.tab", value: " (and 1 more tab)", comment: "suffix of string in Recently Closed menu") - static let recentlyClosedMenuItemSuffixMultiple = NSLocalizedString("n.more.tabs", value: " (and %d more tabs)", comment: "suffix of string in Recently Closed menu") + static let recentlyClosedWindowMenuItem = NSLocalizedString("n.more.tabs", value: "Window with multiple tabs (%d)", comment: "String in Recently Closed menu item for recently closed browser window and number of tabs contained in the closed window") - static let reopenLastClosedTab = NSLocalizedString("reopen.last.closed.tab", value: "Reopen Last Closed Tab", comment: "") - static let reopenLastClosedWindow = NSLocalizedString("reopen.last.closed.window", value: "Reopen Last Closed Window", comment: "") + static let reopenLastClosedTab = NSLocalizedString("reopen.last.closed.tab", value: "Reopen Last Closed Tab", comment: "This string represents an action to reopen the last closed tab in the browser") + static let reopenLastClosedWindow = NSLocalizedString("reopen.last.closed.window", value: "Reopen Last Closed Window", comment: "This string represents an action to reopen the last closed window in the browser") static let cookiePopupManagedNotification = NSLocalizedString("notification.badge.cookiesmanaged", value: "Cookies Managed", comment: "Notification that appears when browser automatically handle cookies") static let cookiePopupHiddenNotification = NSLocalizedString("notification.badge.popuphidden", value: "Pop-up Hidden", comment: "Notification that appears when browser cosmetically hides a cookie popup") @@ -795,25 +797,25 @@ struct UserText { // MARK: - Bitwarden static let passwordManager = NSLocalizedString("password.manager", value: "Password Manager", comment: "Section header") - static let bitwardenPreferencesUnableToConnect = NSLocalizedString("bitwarden.preferences.unable-to-connect", value: "Unable to find or connect to Bitwarden", comment: "") - static let bitwardenPreferencesCompleteSetup = NSLocalizedString("bitwarden.preferences.complete-setup", value: "Complete Setup…", comment: "") - static let bitwardenPreferencesOpenBitwarden = NSLocalizedString("bitwarden.preferences.open-bitwarden", value: "Open Bitwarden", comment: "") - static let bitwardenPreferencesUnlock = NSLocalizedString("bitwarden.preferences.unlock", value: "Unlock Bitwarden", comment: "") - static let bitwardenPreferencesRun = NSLocalizedString("bitwarden.preferences.run", value: "Bitwarden app not running", comment: "") - static let bitwardenError = NSLocalizedString("bitwarden.error", value: "Unable to find or connect to Bitwarden", comment: "") + static let bitwardenPreferencesUnableToConnect = NSLocalizedString("bitwarden.preferences.unable-to-connect", value: "Unable to find or connect to Bitwarden", comment: "Dialog telling the user Bitwarden (a password manager) is not available") + static let bitwardenPreferencesCompleteSetup = NSLocalizedString("bitwarden.preferences.complete-setup", value: "Complete Setup…", comment: "action option that prompts the user to complete the setup process in Bitwarden preferences") + static let bitwardenPreferencesOpenBitwarden = NSLocalizedString("bitwarden.preferences.open-bitwarden", value: "Open Bitwarden", comment: "Button to open Bitwarden app") + static let bitwardenPreferencesUnlock = NSLocalizedString("bitwarden.preferences.unlock", value: "Unlock Bitwarden", comment: "Asks the user to unlock the password manager Bitwarden") + static let bitwardenPreferencesRun = NSLocalizedString("bitwarden.preferences.run", value: "Bitwarden app not running", comment: "Warns user that the password manager Bitwarden app is not running") + static let bitwardenError = NSLocalizedString("bitwarden.error", value: "Unable to find or connect to Bitwarden", comment: "This message appears when the application is unable to find or connect to Bitwarden, indicating a connection issue.") static let bitwardenNotInstalled = NSLocalizedString("bitwarden.not.installed", value: "Bitwarden app is not installed", comment: "") - static let bitwardenOldVersion = NSLocalizedString("bitwarden.old.version", value: "Please update Bitwarden to the latest version", comment: "") - static let bitwardenIntegrationNotApproved = NSLocalizedString("bitwarden.integration.not.approved", value: "Integration with DuckDuckGo is not approved in Bitwarden app", comment: "") - static let bitwardenMissingHandshake = NSLocalizedString("bitwarden.missing.handshake", value: "Missing handshake", comment: "") - static let bitwardenWaitingForHandshake = NSLocalizedString("bitwarden.waiting.for.handshake", value: "Waiting for the handshake approval in Bitwarden app", comment: "") - static let bitwardenCantAccessContainer = NSLocalizedString("bitwarden.cant.access.container", value: "DuckDuckGo needs permission to access Bitwarden. You can grant DuckDuckGo Full Disk Access in System Settings, or switch back to the built-in password manager.", comment: "") - static let bitwardenHanshakeNotApproved = NSLocalizedString("bitwarden.handshake.not.approved", value: "Handshake not approved in Bitwarden app", comment: "") - static let bitwardenConnecting = NSLocalizedString("bitwarden.connecting", value: "Connecting to Bitwarden", comment: "") - static let bitwardenWaitingForStatusResponse = NSLocalizedString("bitwarden.waiting.for.status.response", value: "Waiting for the status response from Bitwarden", comment: "") + static let bitwardenOldVersion = NSLocalizedString("bitwarden.old.version", value: "Please update Bitwarden to the latest version", comment: "Message that warns user they need to update their password manager Bitwarden app vesion") + static let bitwardenIntegrationNotApproved = NSLocalizedString("bitwarden.integration.not.approved", value: "Integration with DuckDuckGo is not approved in Bitwarden app", comment: "While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates that the integration with DuckDuckGo has not been approved in the Bitwarden app.") + static let bitwardenMissingHandshake = NSLocalizedString("bitwarden.missing.handshake", value: "Missing handshake", comment: "While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates a missing handshake (a way for two devices or systems to say hello to each other and agree to communicate or exchange information).") + static let bitwardenWaitingForHandshake = NSLocalizedString("bitwarden.waiting.for.handshake", value: "Waiting for the handshake approval in Bitwarden app", comment: "While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates the system is waiting for the handshake (a way for two devices or systems to say hello to each other and agree to communicate or exchange information).") + static let bitwardenCantAccessContainer = NSLocalizedString("bitwarden.cant.access.container", value: "DuckDuckGo needs permission to access Bitwarden. You can grant DuckDuckGo Full Disk Access in System Settings, or switch back to the built-in password manager.", comment: "Requests user Full Disk access in order to access password manager Birwarden") + static let bitwardenHanshakeNotApproved = NSLocalizedString("bitwarden.handshake.not.approved", value: "Handshake not approved in Bitwarden app", comment: "It appears in a dialog when the users are connecting to Bitwardern and shows the status of the action. This message indicates that the handshake process was not approved in the Bitwarden app.") + static let bitwardenConnecting = NSLocalizedString("bitwarden.connecting", value: "Connecting to Bitwarden", comment: "It appears in a dialog when the users are connecting to Bitwardern and shows the status of the action, in this case we are in the progress of connecting the browser to the Bitwarden password maanger.") + static let bitwardenWaitingForStatusResponse = NSLocalizedString("bitwarden.waiting.for.status.response", value: "Waiting for the status response from Bitwarden", comment: "It appears in a dialog when the users are connecting to Bitwardern and shows the status of the action, in this case that the application is currently waiting for a response from the Bitwarden service.") static let connectToBitwarden = NSLocalizedString("bitwarden.connect.title", value: "Connect to Bitwarden", comment: "Title for the Bitwarden onboarding flow") - static let connectToBitwardenDescription = NSLocalizedString("bitwarden.connect.description", value: "We’ll walk you through connecting to Bitwarden, so you can use it in DuckDuckGo.", comment: "") + static let connectToBitwardenDescription = NSLocalizedString("bitwarden.connect.description", value: "We’ll walk you through connecting to Bitwarden, so you can use it in DuckDuckGo.", comment: "Description for when the user wants to connect the browser to the password manager Bitwarned.") static let connectToBitwardenPrivacy = NSLocalizedString("bitwarden.connect.privacy", value: "Privacy", comment: "") static let installBitwarden = NSLocalizedString("bitwarden.install", value: "Install Bitwarden", comment: "Button to install Bitwarden app") @@ -833,8 +835,8 @@ struct UserText { static let bitwardenIntegrationComplete = NSLocalizedString("bitwarden.integration.complete", value: "Bitwarden integration complete!", comment: "Setup of the integration with Bitwarden app") static let bitwardenIntegrationCompleteInfo = NSLocalizedString("bitwarden.integration.complete.info", value: "You are now using Bitwarden as your password manager.", comment: "Setup of the integration with Bitwarden app") - static let bitwardenCommunicationInfo = NSLocalizedString("bitwarden.connect.communication-info", value: "All communication between Bitwarden and DuckDuckGo is encrypted and the data never leaves your device.", comment: "") - static let bitwardenHistoryInfo = NSLocalizedString("bitwarden.connect.history-info", value: "Bitwarden will have access to your browsing history.", comment: "") + static let bitwardenCommunicationInfo = NSLocalizedString("bitwarden.connect.communication-info", value: "All communication between Bitwarden and DuckDuckGo is encrypted and the data never leaves your device.", comment: "Warns users that all communication between the DuckDuckGo browser and the password manager Bitwarden is encrypted and doesn't leave the user device") + static let bitwardenHistoryInfo = NSLocalizedString("bitwarden.connect.history-info", value: "Bitwarden will have access to your browsing history.", comment: "Warn users that the password Manager Bitwarden will have access to their browsing history") static let showAutofillShortcut = NSLocalizedString("pinning.show-autofill-shortcut", value: "Show Autofill Shortcut", comment: "Menu item for showing the autofill shortcut") static let hideAutofillShortcut = NSLocalizedString("pinning.hide-autofill-shortcut", value: "Hide Autofill Shortcut", comment: "Menu item for hiding the autofill shortcut") @@ -931,7 +933,7 @@ struct UserText { return String(format: localized, domain) } - static let noAccessToDownloadsFolderHeader = NSLocalizedString("no.access.to.downloads.folder.header", value: "DuckDuckGo needs permission to access your Downloads folder", comment: "Header of the alert dialog informing user about failed download") + static let noAccessToDownloadsFolderHeader = NSLocalizedString("no.access.to.downloads.folder.header", value: "DuckDuckGo needs permission to access your Downloads folder", comment: "Header of the alert dialog warning the user they need to give the browser permission to access the Downloads folder") private static let noAccessToDownloadsFolderLegacy = NSLocalizedString("no.access.to.downloads.folder.legacy", value: "Grant access in Security & Privacy preferences in System Settings.", comment: "Alert presented to user if the app doesn't have rights to access Downloads folder. This is used for macOS version 12 and below") private static let noAccessToDownloadsFolderModern = NSLocalizedString("no.access.to.downloads.folder.modern", value: "Grant access in Privacy & Security preferences in System Settings.", comment: "Alert presented to user if the app doesn't have rights to access Downloads folder. This is used for macOS version 13 and above") @@ -987,10 +989,10 @@ struct UserText { static let newTabRecentActivitySectionTitle = NSLocalizedString("newTab.recent.activity.section.title", value: "Recent Activity", comment: "Title of the RecentActivity section in the home page") static let burnerWindowHeader = NSLocalizedString("burner.window.header", value: "Fire Window", comment: "Header shown on the hompage of the Fire Window") static let burnerTabHomeTitle = NSLocalizedString("burner.tab.home.title", value: "New Fire Tab", comment: "Tab title for Fire Tab") - static let burnerHomepageDescription1 = NSLocalizedString("burner.homepage.description.1", value: "Browse without saving local history", comment: "") - static let burnerHomepageDescription2 = NSLocalizedString("burner.homepage.description.2", value: "Sign in to a site with a different account", comment: "") - static let burnerHomepageDescription3 = NSLocalizedString("burner.homepage.description.3", value: "Troubleshoot websites", comment: "") - static let burnerHomepageDescription4 = NSLocalizedString("burner.homepage.description.4", value: "Fire windows are isolated from other browser data, and their data is burned when you close them. They have the same tracking protection as other windows.", comment: "") + static let burnerHomepageDescription1 = NSLocalizedString("burner.homepage.description.1", value: "Browse without saving local history", comment: "Descriptions of features Fire page. Provides information about browsing functionalities such as browsing without saving local history, signing in to a site with a different account, and troubleshooting websites.") + static let burnerHomepageDescription2 = NSLocalizedString("burner.homepage.description.2", value: "Sign in to a site with a different account", comment: "Descriptions of features Fire page. Provides information about browsing functionalities such as browsing without saving local history, signing in to a site with a different account, and troubleshooting websites.") + static let burnerHomepageDescription3 = NSLocalizedString("burner.homepage.description.3", value: "Troubleshoot websites", comment: "Descriptions of features Fire page. Provides information about browsing functionalities such as browsing without saving local history, signing in to a site with a different account, and troubleshooting websites.") + static let burnerHomepageDescription4 = NSLocalizedString("burner.homepage.description.4", value: "Fire windows are isolated from other browser data, and their data is burned when you close them. They have the same tracking protection as other windows.", comment: "This describes the functionality of one of out browser feature Fire Window, highlighting their isolation from other browser data and the automatic deletion of their data upon closure. Additionally, it emphasizes that fire windows offer the same level of tracking protection as other browsing windows.") // Email Protection Management static let disableEmailProtectionTitle = NSLocalizedString("disable.email.protection.title", value: "Disable Email Protection Autofill?", comment: "Title for alert shown when user disables email protection") @@ -1058,11 +1060,24 @@ struct UserText { } #if SUBSCRIPTION - static let subscriptionOptionsMenuItem = NSLocalizedString("subscription.menu.item", value: "Privacy Pro", comment: "Title for Subscription item in the options menu") - static let subscription = NSLocalizedString("preferences.subscription", value: "Privacy Pro", comment: "Show subscription preferences") + // Key: "subscription.menu.item" + // Comment: "Title for Subscription item in the options menu" + static let subscriptionOptionsMenuItem = "Privacy Pro" + + // Key: "preferences.subscription" + // Comment: "Show subscription preferences" + static let subscription = "Privacy Pro" + + // Key: "subscription.progress.view.purchasing.subscription" + // Comment: "Progress view title when starting the purchase" + static let purchasingSubscriptionTitle = "Purchase in progress..." + + // Key: "subscription.progress.view.restoring.subscription" + // Comment: "Progress view title when restoring past subscription purchase" + static let restoringSubscriptionTitle = "Restoring subscription..." - static let purchasingSubscriptionTitle = NSLocalizedString("subscription.progress.view.purchasing.subscription", value: "Purchase in progress...", comment: "Progress view title when starting the purchase") - static let restoringSubscriptionTitle = NSLocalizedString("subscription.progress.view.restoring.subscription", value: "Restoring subscription...", comment: "Progress view title when restoring past subscription purchase") - static let completingPurchaseTitle = NSLocalizedString("subscription.progress.view.completing.purchase", value: "Completing purchase...", comment: "Progress view title when completing the purchase") + // Key: "subscription.progress.view.completing.purchase" + // Comment: "Progress view title when completing the purchase" + static let completingPurchaseTitle = "Completing purchase..." #endif } diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 286d5842fa..722a4fca75 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -69,6 +69,7 @@ public struct UserDefaultsWrapper { case askToSaveAddresses = "preferences.ask-to-save.addresses" case askToSavePaymentMethods = "preferences.ask-to-save.payment-methods" case autolockLocksFormFilling = "preferences.lock-autofill-form-fill" + case autofillDebugScriptEnabled = "preferences.enable-autofill-debug-script" case saveAsPreferredFileType = "saveAs.selected.filetype" diff --git a/DuckDuckGo/Common/View/AppKit/ColorView.swift b/DuckDuckGo/Common/View/AppKit/ColorView.swift index 07e5a383f5..dd48e068e1 100644 --- a/DuckDuckGo/Common/View/AppKit/ColorView.swift +++ b/DuckDuckGo/Common/View/AppKit/ColorView.swift @@ -29,6 +29,7 @@ internal class ColorView: NSView { init(frame: NSRect, backgroundColor: NSColor? = nil, cornerRadius: CGFloat = 0, borderColor: NSColor? = nil, borderWidth: CGFloat = 0, interceptClickEvents: Bool = false) { super.init(frame: frame) + self.translatesAutoresizingMaskIntoConstraints = false self.backgroundColor = backgroundColor self.cornerRadius = cornerRadius self.borderColor = borderColor diff --git a/DuckDuckGo/Common/View/SwiftUI/FaviconView.swift b/DuckDuckGo/Common/View/SwiftUI/FaviconView.swift index 8249a26e71..9bf9832bd8 100644 --- a/DuckDuckGo/Common/View/SwiftUI/FaviconView.swift +++ b/DuckDuckGo/Common/View/SwiftUI/FaviconView.swift @@ -26,6 +26,7 @@ struct FaviconView: View { let url: URL? let size: CGFloat let onFaviconMissing: (() -> Void)? + private var letterPaddingModifier: CGFloat var domain: String { url?.host ?? "" @@ -34,9 +35,12 @@ struct FaviconView: View { @State var image: NSImage? @State private var timer = Timer.publish(every: 0.1, tolerance: 0, on: .main, in: .default, options: nil).autoconnect() - init(url: URL?, size: CGFloat = 32, onFaviconMissing: (() -> Void)? = nil) { + /// Initializes a `FaviconView` + /// Note: The `letterPaddingModifier` parameter is only used when a `LetterIconView` is displayed instead of a Favicon image + init(url: URL?, size: CGFloat = 32, letterPaddingModifier: CGFloat = 0.33, onFaviconMissing: (() -> Void)? = nil) { self.url = url self.size = size + self.letterPaddingModifier = letterPaddingModifier self.onFaviconMissing = onFaviconMissing } @@ -73,18 +77,7 @@ struct FaviconView: View { timer.upstream.connect().cancel() } } else { - - ZStack { - let eTLDplus1 = ContentBlocking.shared.tld.eTLDplus1(domain) ?? domain - Rectangle() - .foregroundColor(Color.forString(eTLDplus1)) - Text(String(eTLDplus1.capitalized.first ?? "?")) - .font(.title) - .foregroundColor(Color.white) - } - .frame(width: size, height: size) - .cornerRadius(4.0) - + LetterIconView(title: ContentBlocking.shared.tld.eTLDplus1(domain) ?? domain, size: size, paddingModifier: letterPaddingModifier) } }.onAppear { refreshImage() diff --git a/DuckDuckGo/Common/View/SwiftUI/LoginFaviconView.swift b/DuckDuckGo/Common/View/SwiftUI/LoginFaviconView.swift index a2c5557f6e..f7532478c1 100644 --- a/DuckDuckGo/Common/View/SwiftUI/LoginFaviconView.swift +++ b/DuckDuckGo/Common/View/SwiftUI/LoginFaviconView.swift @@ -35,7 +35,8 @@ struct LoginFaviconView: View { .cornerRadius(4.0) .padding(.leading, 6) } else { - LetterIconView(title: generatedIconLetters) + LetterIconView(title: generatedIconLetters, font: .system(size: 32, weight: .semibold)) + .padding(.leading, 8) } } } diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index a8052699b3..e35137c46b 100644 --- a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift +++ b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"80ac4a1898be3d2e52a76d841bdd21b5\"" - public static let embeddedDataSHA = "c61d9c08e6b54aabddb6a5043bf3dcd940afe996dca0c1f7f1e105156e77a9b8" + public static let embeddedDataETag = "\"3f7639dcb62ac27e380627dc7391ebf3\"" + public static let embeddedDataSHA = "7c4c52a8d470962fd3d90a1ad9dbbbd26387d92034b001028c1da46f054d482b" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift b/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift index e7557059a5..8088be236e 100644 --- a/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift +++ b/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppTrackerDataSetProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"144361b3801e3d4c33c5aff8d8de3c6b\"" - public static let embeddedDataSHA = "0cf5a43c234d54c3168cc28a65c19b0c5804c15e87aae3e8368d2b2f775a1a8b" + public static let embeddedDataETag = "\"0b6a7a2629abc170a505b92aebd67017\"" + public static let embeddedDataSHA = "32cd805f6be415e77affdf51929494c7add6363234cef58ea8b53ca3a08c86d4" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift b/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift index eeb484e27c..4013498fff 100644 --- a/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift +++ b/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift @@ -85,7 +85,8 @@ struct ScriptSourceProvider: ScriptSourceProviding { return DefaultAutofillSourceProvider.Builder(privacyConfigurationManager: privacyConfigurationManager, properties: ContentScopeProperties(gpcEnabled: privacySettings.gpcEnabled, sessionKey: self.sessionKey ?? "", - featureToggles: ContentScopeFeatureToggles.supportedFeaturesOnMacOS(privacyConfig))) + featureToggles: ContentScopeFeatureToggles.supportedFeaturesOnMacOS(privacyConfig)), + isDebug: AutofillPreferences().debugScriptEnabled) .withJSLoading() .build() } diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index 478a07e8ea..db536115a3 100644 --- a/DuckDuckGo/ContentBlocker/macos-config.json +++ b/DuckDuckGo/ContentBlocker/macos-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1707497831317, + "version": 1708360600128, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -257,9 +257,6 @@ { "domain": "youtube.com" }, - { - "domain": "tuc.org.uk" - }, { "domain": "newsmax.com" }, @@ -279,11 +276,11 @@ "settings": { "disabledCMPs": [ "generic-cosmetic", - "EZoic" + "termsfeed3" ] }, "state": "enabled", - "hash": "df49b4912fc62299b3cc20b5538e3b89" + "hash": "ce906469aa6ac0fb5ecf3f39710ef05b" }, "autofill": { "exceptions": [ @@ -1068,6 +1065,18 @@ { "domain": "zalando.fr", "reason": "https://github.com/duckduckgo/privacy-configuration/issues/667" + }, + { + "domain": "www.canva.com", + "reason": "https://github.com/duckduckgo/privacy-configuration/issues/1818" + }, + { + "domain": "53.com", + "reason": "https://github.com/duckduckgo/privacy-configuration/issues/1824" + }, + { + "domain": "www.evernote.com", + "reason": "https://github.com/duckduckgo/privacy-configuration/issues/1827" } ], "webViewDefault": [ @@ -1091,7 +1100,7 @@ }, "exceptions": [], "state": "enabled", - "hash": "96ca986dfe68dbb73a36dd7f3adbb53e" + "hash": "653b8d161df5462850be095097e85143" }, "dbp": { "state": "enabled", @@ -1105,6 +1114,9 @@ "steps": [ { "percent": 10 + }, + { + "percent": 50 } ] } @@ -1112,7 +1124,7 @@ }, "exceptions": [], "minSupportedVersion": "1.70.0", - "hash": "c90b300b1e5ab20c29a726c8de9fd2da" + "hash": "b405d032a1519de3059db2e833e81c9a" }, "duckPlayer": { "exceptions": [], @@ -4337,9 +4349,6 @@ { "domain": "tirerack.com" }, - { - "domain": "sephora.com" - }, { "domain": "earth.google.com" }, @@ -4362,7 +4371,7 @@ "privacy-test-pages.site" ] }, - "hash": "0e4353dbfbf35914b784ef12e0073447" + "hash": "1a1373bcf16647d63220659fce650a83" }, "harmfulApis": { "settings": { @@ -4874,6 +4883,16 @@ } ] }, + "adlightning.com": { + "rules": [ + { + "rule": "publisher.adlightning.com/user-api/session/", + "domains": [ + "boltive.com" + ] + } + ] + }, "ads-twitter.com": { "rules": [ { @@ -5220,7 +5239,19 @@ "cloudflare.com": { "rules": [ { - "rule": "cdnjs.cloudflare.com/cdn-cgi/scripts/.*/cloudflare-static/rocket-loader.min.js", + "rule": "cloudflare.com/cdn-cgi/scripts/7089c43e/cloudflare-static/rocket-loader.min.js", + "domains": [ + "" + ] + }, + { + "rule": "cloudflare.com/cdn-cgi/scripts/7d0fa10a/cloudflare-static/rocket-loader.min.js", + "domains": [ + "" + ] + }, + { + "rule": "cloudflare.com/cdn-cgi/scripts/1680307200/cloudflare-static/rocket-loader.min.js", "domains": [ "" ] @@ -5950,6 +5981,7 @@ "doterra.com", "easyjet.com", "edx.org", + "saplinglearning.com", "worlddutyfree.com" ] }, @@ -6122,7 +6154,9 @@ "rule": "googletagmanager.com/gtag/js", "domains": [ "abril.com.br", + "algomalegalclinic.com", "cosmicbook.news", + "eatroyo.com", "thesimsresource.com", "tradersync.com" ] @@ -7080,6 +7114,16 @@ } ] }, + "pubnation.com": { + "rules": [ + { + "rule": "scripts.pubnation.com/tags/", + "domains": [ + "n4g.com" + ] + } + ] + }, "qualtrics.com": { "rules": [ { @@ -7339,6 +7383,16 @@ } ] }, + "sumo.com": { + "rules": [ + { + "rule": "load.sumo.com", + "domains": [ + "glennbeck.com" + ] + } + ] + }, "taboola.com": { "rules": [ { @@ -7765,7 +7819,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "f5cba883ae4a666f7b6135d3406c2797" + "hash": "63057921f3dafa389fd7c132ea62393d" }, "trackingCookies1p": { "settings": { @@ -8152,10 +8206,20 @@ "value": "enabled" } ] + }, + { + "domain": "myhome.experian.co.uk", + "patchSettings": [ + { + "op": "replace", + "path": "/messageHandlers/state", + "value": "enabled" + } + ] } ] }, - "hash": "1a7df2f0fbf4e5838e7c4b21627d2086" + "hash": "151d7ee40451c4aac4badfcc829ea0b5" }, "windowsPermissionUsage": { "exceptions": [], diff --git a/DuckDuckGo/ContentBlocker/trackerData.json b/DuckDuckGo/ContentBlocker/trackerData.json index 0d4648236e..2e842e28c9 100644 --- a/DuckDuckGo/ContentBlocker/trackerData.json +++ b/DuckDuckGo/ContentBlocker/trackerData.json @@ -1,6 +1,6 @@ { "_builtWith": { - "tracker-radar": "56bc133a7354c326d8afcb10b905e6cf865390022e9f2fc69045315332db9afd-4013b4e91930c643394cb31c6c745356f133b04f", + "tracker-radar": "9b6a3c1e62c8a97f63db77d2aef4f185129f05aad0f770425bcb54cbf8e0db84-4013b4e91930c643394cb31c6c745356f133b04f", "tracker-surrogates": "ba0d8cefe4432723ec75b998241efd2454dff35a" }, "readme": "https://github.com/duckduckgo/tracker-blocklists", @@ -418,7 +418,20 @@ "fingerprinting": 1, "cookies": 0, "categories": [], - "default": "block" + "default": "ignore", + "rules": [ + { + "rule": "a2z\\.com\\/resource\\/00000179-3cdd-d40f-a779-bedf7f820000\\/styleguide\\/All\\.min\\.7cb0b3550a4bbcbe1dbaee0522794cc9\\.gz\\.js", + "fingerprinting": 1, + "cookies": 0 + }, + { + "rule": "a2z\\.com\\/x\\.png", + "fingerprinting": 0, + "cookies": 0, + "comment": "pixel" + } + ] }, "aamsitecertifier.com": { "domain": "aamsitecertifier.com", @@ -451,7 +464,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -462,7 +475,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -508,7 +521,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2396,7 +2409,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2407,7 +2420,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2451,7 +2464,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2462,7 +2475,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2577,7 +2590,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2711,7 +2724,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2722,7 +2735,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3041,7 +3054,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3168,7 +3181,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3292,7 +3305,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3676,7 +3689,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3687,7 +3700,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3698,7 +3711,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3709,7 +3722,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3775,7 +3788,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3827,7 +3840,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3838,7 +3851,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3975,7 +3988,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -4326,7 +4339,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -4361,7 +4374,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -4807,7 +4820,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -4818,7 +4831,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5020,7 +5033,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5062,7 +5075,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5085,7 +5098,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5120,7 +5133,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5149,7 +5162,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5437,7 +5450,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5554,7 +5567,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5565,7 +5578,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5576,7 +5589,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5587,7 +5600,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5624,7 +5637,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5679,7 +5692,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -6215,7 +6228,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -6369,7 +6382,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -6685,7 +6698,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -6696,7 +6709,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -6816,7 +6829,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7012,7 +7025,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7048,7 +7061,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7059,7 +7072,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7070,7 +7083,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7653,7 +7666,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7664,7 +7677,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7675,7 +7688,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7756,7 +7769,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7879,7 +7892,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7890,7 +7903,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7968,7 +7981,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7979,7 +7992,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -8094,7 +8107,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -8245,7 +8258,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -8256,7 +8269,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -8747,7 +8760,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -8758,7 +8771,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -8799,7 +8812,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9215,7 +9228,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9820,7 +9833,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9831,7 +9844,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9842,7 +9855,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9882,7 +9895,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9924,7 +9937,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9978,7 +9991,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10046,7 +10059,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10057,7 +10070,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10105,7 +10118,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10231,7 +10244,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10391,7 +10404,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10402,7 +10415,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10413,7 +10426,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10440,7 +10453,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10490,7 +10503,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10537,7 +10550,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -11015,7 +11028,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -11055,7 +11068,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -11791,7 +11804,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -11880,7 +11893,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -11891,7 +11904,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12113,7 +12126,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12153,7 +12166,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12164,7 +12177,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12175,7 +12188,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12186,7 +12199,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12512,7 +12525,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12523,7 +12536,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12534,7 +12547,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -14442,7 +14455,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -15127,7 +15140,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -15202,7 +15215,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -15253,7 +15266,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16112,7 +16125,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16123,7 +16136,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16152,7 +16165,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16371,7 +16384,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16680,7 +16693,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16691,7 +16704,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16876,7 +16889,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16925,7 +16938,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -17199,7 +17212,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -17210,7 +17223,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -17773,7 +17786,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -18121,7 +18134,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -18260,7 +18273,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -18619,7 +18632,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -18830,7 +18843,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20007,7 +20020,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20228,7 +20241,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20239,7 +20252,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20250,7 +20263,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20390,7 +20403,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20877,7 +20890,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20906,7 +20919,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20917,7 +20930,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20928,7 +20941,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20992,7 +21005,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21072,7 +21085,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21120,7 +21133,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21131,7 +21144,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21171,7 +21184,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21182,7 +21195,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21217,7 +21230,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21228,7 +21241,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21361,7 +21374,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21445,7 +21458,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21916,7 +21929,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21927,7 +21940,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21938,7 +21951,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22008,7 +22021,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22019,7 +22032,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22172,7 +22185,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22183,7 +22196,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22225,7 +22238,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22236,7 +22249,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22247,7 +22260,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22517,7 +22530,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22528,7 +22541,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22589,7 +22602,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22647,7 +22660,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22658,7 +22671,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22758,7 +22771,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22781,7 +22794,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22792,7 +22805,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23149,7 +23162,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23253,7 +23266,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23389,7 +23402,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23464,7 +23477,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23475,7 +23488,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23519,7 +23532,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23530,7 +23543,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23541,7 +23554,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23552,7 +23565,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23632,7 +23645,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23665,7 +23678,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23716,7 +23729,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23852,7 +23865,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23981,7 +23994,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23992,7 +24005,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24239,7 +24252,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24250,7 +24263,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24261,7 +24274,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24542,7 +24555,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24577,7 +24590,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24618,7 +24631,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24734,7 +24747,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24745,7 +24758,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24774,7 +24787,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24870,7 +24883,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24927,7 +24940,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24938,7 +24951,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25059,7 +25072,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25216,7 +25229,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25242,7 +25255,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25253,7 +25266,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25316,7 +25329,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25327,7 +25340,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25338,7 +25351,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25569,7 +25582,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25596,7 +25609,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25607,7 +25620,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25618,7 +25631,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25648,7 +25661,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25659,7 +25672,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25687,7 +25700,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25698,7 +25711,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25771,7 +25784,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25819,7 +25832,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25830,7 +25843,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25841,7 +25854,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25852,7 +25865,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25863,7 +25876,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25874,7 +25887,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25946,7 +25959,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25957,7 +25970,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26011,7 +26024,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26213,7 +26226,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26521,7 +26534,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26532,7 +26545,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26543,7 +26556,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26794,7 +26807,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26805,7 +26818,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -27223,7 +27236,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28080,7 +28093,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28210,7 +28223,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28221,7 +28234,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28354,7 +28367,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28437,7 +28450,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28448,7 +28461,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28809,7 +28822,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -29116,7 +29129,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -29127,7 +29140,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -29253,7 +29266,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -29264,7 +29277,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -31006,7 +31019,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -31311,7 +31324,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32471,7 +32484,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32482,7 +32495,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32493,7 +32506,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32504,7 +32517,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32515,7 +32528,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32526,7 +32539,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32537,7 +32550,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32548,7 +32561,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32559,7 +32572,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32570,7 +32583,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32581,7 +32594,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32592,7 +32605,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32603,7 +32616,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32614,7 +32627,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32625,7 +32638,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32636,7 +32649,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32647,7 +32660,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32658,7 +32671,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32669,7 +32682,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32680,7 +32693,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "ambientdusk.com": { + "domain": "ambientdusk.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32691,7 +32715,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "ambrosialsummit.com": { + "domain": "ambrosialsummit.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32702,7 +32737,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32713,7 +32748,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32724,7 +32759,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "analyzecorona.com": { + "domain": "analyzecorona.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32735,7 +32781,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32746,7 +32792,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32757,7 +32803,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32768,7 +32814,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32779,7 +32825,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32790,7 +32836,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32801,7 +32847,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32812,7 +32858,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32823,7 +32869,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32834,7 +32880,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32845,7 +32891,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32856,7 +32902,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32867,7 +32913,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32878,7 +32924,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32889,7 +32935,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32900,7 +32946,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32911,7 +32957,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32922,7 +32968,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32933,7 +32979,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32944,7 +32990,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32955,7 +33001,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32966,7 +33012,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32977,7 +33023,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32988,7 +33034,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32999,7 +33045,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33010,7 +33056,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33021,7 +33067,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33032,7 +33078,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33043,7 +33089,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33054,7 +33100,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33065,7 +33111,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33076,7 +33122,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33087,7 +33133,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33098,7 +33144,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33109,7 +33155,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33120,7 +33166,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33131,7 +33177,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33142,7 +33188,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33153,7 +33199,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33164,7 +33210,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33175,7 +33221,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33186,7 +33232,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33197,7 +33243,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33208,7 +33254,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33219,7 +33265,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33230,7 +33276,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33241,7 +33287,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33252,7 +33298,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33263,7 +33309,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33274,7 +33320,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33285,7 +33331,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33296,7 +33342,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33307,7 +33353,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33318,7 +33364,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33329,7 +33375,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33340,7 +33386,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33351,7 +33397,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33362,7 +33408,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33373,7 +33419,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "celestialquasar.com": { + "domain": "celestialquasar.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33384,7 +33441,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33395,7 +33452,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33406,7 +33463,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33417,7 +33474,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33428,7 +33485,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33439,7 +33496,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33450,7 +33507,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33461,7 +33518,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33472,7 +33529,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33483,7 +33540,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33494,7 +33551,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33505,7 +33562,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33516,7 +33573,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33527,7 +33584,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33538,7 +33595,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33549,7 +33606,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33560,7 +33617,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33571,7 +33628,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33582,7 +33639,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33593,7 +33650,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33604,7 +33661,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33615,7 +33672,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33626,7 +33683,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33637,7 +33694,40 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "coordinatedcoat.com": { + "domain": "coordinatedcoat.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "copycarpenter.com": { + "domain": "copycarpenter.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "cosmicsculptor.com": { + "domain": "cosmicsculptor.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33648,7 +33738,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33659,7 +33749,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33670,7 +33760,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33681,7 +33771,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33692,7 +33782,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33703,7 +33793,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33714,7 +33804,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33725,7 +33815,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33736,7 +33826,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33747,7 +33837,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33758,7 +33848,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33769,7 +33859,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33780,7 +33870,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33791,7 +33881,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33802,7 +33892,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33813,7 +33903,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33824,7 +33914,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33835,7 +33925,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33846,7 +33936,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33857,7 +33947,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33868,7 +33958,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33879,7 +33969,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33890,7 +33980,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33901,7 +33991,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33912,7 +34002,29 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "deliciousducks.com": { + "domain": "deliciousducks.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "dependenttrip.com": { + "domain": "dependenttrip.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33923,7 +34035,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33934,7 +34046,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33945,7 +34057,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33956,7 +34068,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33967,7 +34079,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33978,7 +34090,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33989,7 +34101,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34000,7 +34112,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34011,7 +34123,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34022,7 +34134,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34033,7 +34145,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "effervescentvista.com": { + "domain": "effervescentvista.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34044,7 +34167,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34055,7 +34178,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34066,7 +34189,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "enchantingmystique.com": { + "domain": "enchantingmystique.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34077,7 +34211,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34088,7 +34222,29 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "enigmaticcanyon.com": { + "domain": "enigmaticcanyon.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "enigmaticvoyage.com": { + "domain": "enigmaticvoyage.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34099,7 +34255,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34110,7 +34266,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34121,7 +34277,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34132,7 +34288,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "evasivejar.com": { + "domain": "evasivejar.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34143,7 +34310,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34154,7 +34321,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34165,7 +34332,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34176,7 +34343,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34187,7 +34354,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34198,7 +34365,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34209,7 +34376,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34220,7 +34387,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34231,7 +34398,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34242,7 +34409,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34253,7 +34420,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34264,7 +34431,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34275,7 +34442,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34286,7 +34453,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34297,7 +34464,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34308,7 +34475,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34319,7 +34486,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34330,7 +34497,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34341,7 +34508,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34352,7 +34519,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34363,7 +34530,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34374,7 +34541,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34385,7 +34552,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34396,7 +34563,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34407,7 +34574,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34418,7 +34585,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34429,7 +34596,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34440,7 +34607,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34451,7 +34618,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34462,7 +34629,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "friendlycrayon.com": { + "domain": "friendlycrayon.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34473,7 +34651,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34484,7 +34662,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34495,7 +34673,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34506,7 +34684,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34517,7 +34695,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34528,7 +34706,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34539,7 +34717,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34550,7 +34728,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34561,7 +34739,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34572,7 +34750,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34583,7 +34761,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34594,7 +34772,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34605,7 +34783,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34616,7 +34794,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "gracefulmilk.com": { + "domain": "gracefulmilk.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34627,7 +34816,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34638,7 +34827,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34649,7 +34838,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34660,7 +34849,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34671,7 +34860,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34682,7 +34871,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34693,7 +34882,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34704,7 +34893,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34715,7 +34904,29 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "halcyoncanyon.com": { + "domain": "halcyoncanyon.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "halcyonsculpture.com": { + "domain": "halcyonsculpture.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34726,7 +34937,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34737,7 +34948,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34748,7 +34959,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34759,7 +34970,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34770,7 +34981,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34781,7 +34992,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34792,7 +35003,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34803,7 +35014,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34814,7 +35025,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34825,7 +35036,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34836,7 +35047,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34847,7 +35058,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34858,7 +35069,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34869,7 +35080,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34880,7 +35091,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34891,7 +35102,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34902,7 +35113,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34913,7 +35124,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34924,7 +35135,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34935,7 +35146,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34946,7 +35157,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34957,7 +35168,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34968,7 +35179,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34979,7 +35190,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34990,7 +35201,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35001,7 +35212,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35012,7 +35223,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35023,7 +35234,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35034,7 +35245,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35045,7 +35256,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35056,7 +35267,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35067,7 +35278,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35078,7 +35289,40 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "jubilantcascade.com": { + "domain": "jubilantcascade.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "jubilantglimmer.com": { + "domain": "jubilantglimmer.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "jubilantwhisper.com": { + "domain": "jubilantwhisper.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35089,7 +35333,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35100,7 +35344,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35111,7 +35355,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35122,7 +35366,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35133,7 +35377,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35144,7 +35388,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35155,7 +35399,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35166,7 +35410,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35177,7 +35421,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35188,7 +35432,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35199,7 +35443,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35210,7 +35454,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "loadsurprise.com": { + "domain": "loadsurprise.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35221,7 +35476,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35232,7 +35487,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35243,7 +35498,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35254,7 +35509,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35265,7 +35520,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "luminouscatalyst.com": { + "domain": "luminouscatalyst.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35276,7 +35542,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "lustroushaven.com": { + "domain": "lustroushaven.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35287,7 +35564,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35298,7 +35575,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35309,7 +35586,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35320,7 +35597,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35331,7 +35608,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35342,7 +35619,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35353,7 +35630,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35364,7 +35641,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35375,7 +35652,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35386,7 +35663,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35397,7 +35674,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35408,7 +35685,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35419,7 +35696,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35430,7 +35707,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35441,7 +35718,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35452,7 +35729,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35463,7 +35740,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35474,7 +35751,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35485,7 +35762,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35496,7 +35773,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35507,7 +35784,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "mysticalagoon.com": { + "domain": "mysticalagoon.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35518,7 +35806,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35529,7 +35817,29 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "nebulacrescent.com": { + "domain": "nebulacrescent.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "nebulajubilee.com": { + "domain": "nebulajubilee.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35540,7 +35850,40 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "nebulousgarden.com": { + "domain": "nebulousgarden.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "nebulousquasar.com": { + "domain": "nebulousquasar.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "nebulousripple.com": { + "domain": "nebulousripple.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35551,7 +35894,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "niftyhospital.com": { + "domain": "niftyhospital.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35562,7 +35916,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35573,7 +35927,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35584,7 +35938,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35595,7 +35949,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35606,7 +35960,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35617,7 +35971,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35628,7 +35982,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35639,7 +35993,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35650,7 +36004,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35661,7 +36015,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35672,7 +36026,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35683,7 +36037,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35694,7 +36048,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35705,7 +36059,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35716,7 +36070,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35727,7 +36081,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "parallelbulb.com": { + "domain": "parallelbulb.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35738,7 +36103,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35749,7 +36114,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35760,7 +36125,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35771,7 +36136,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "piquantvortex.com": { + "domain": "piquantvortex.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35782,7 +36158,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35793,7 +36169,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35804,7 +36180,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35815,7 +36191,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35826,7 +36202,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35837,7 +36213,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "pointlessprofit.com": { + "domain": "pointlessprofit.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35848,7 +36235,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35859,7 +36246,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35870,7 +36257,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35881,7 +36268,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35892,7 +36279,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35903,7 +36290,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35914,7 +36301,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35925,7 +36312,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35936,7 +36323,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35947,7 +36334,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35958,7 +36345,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35969,7 +36356,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35980,7 +36367,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35991,7 +36378,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36002,7 +36389,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "radiantlullaby.com": { + "domain": "radiantlullaby.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36013,7 +36411,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36024,7 +36422,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36035,7 +36433,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36046,7 +36444,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36057,7 +36455,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36068,7 +36466,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36079,7 +36477,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36090,7 +36488,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36101,7 +36499,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36112,7 +36510,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36123,7 +36521,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "reconditeprison.com": { + "domain": "reconditeprison.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36134,7 +36543,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36145,7 +36554,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36156,7 +36565,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36167,7 +36576,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36178,7 +36587,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36189,7 +36598,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "resplendentecho.com": { + "domain": "resplendentecho.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36200,7 +36620,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36211,7 +36631,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36222,7 +36642,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36233,7 +36653,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36244,7 +36664,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36255,7 +36675,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36266,7 +36686,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36277,7 +36697,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36288,7 +36708,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36299,7 +36719,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36310,7 +36730,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36321,7 +36741,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36332,7 +36752,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "scaredslip.com": { + "domain": "scaredslip.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36343,7 +36774,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36354,7 +36785,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36365,7 +36796,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36376,7 +36807,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36387,7 +36818,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36398,7 +36829,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36409,7 +36840,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36420,7 +36851,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36431,7 +36862,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36442,7 +36873,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36453,7 +36884,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36464,7 +36895,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36475,7 +36906,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36486,7 +36917,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36497,7 +36928,29 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "seraphicjubilee.com": { + "domain": "seraphicjubilee.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "serenepebble.com": { + "domain": "serenepebble.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36508,7 +36961,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36519,7 +36972,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36530,7 +36983,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36541,7 +36994,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36552,7 +37005,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36563,7 +37016,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36574,7 +37027,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36585,7 +37038,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36596,7 +37049,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36607,7 +37060,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36618,7 +37071,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36629,7 +37082,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36640,7 +37093,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36651,7 +37104,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36662,7 +37115,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36673,7 +37126,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36684,7 +37137,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36695,7 +37148,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36706,7 +37159,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36717,7 +37170,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36728,7 +37181,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36739,7 +37192,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36750,7 +37203,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36761,7 +37214,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36772,7 +37225,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36783,7 +37236,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "soggyzoo.com": { + "domain": "soggyzoo.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36794,7 +37258,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36805,7 +37269,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36816,7 +37280,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36827,7 +37291,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36838,7 +37302,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "soresidewalk.com": { + "domain": "soresidewalk.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36849,7 +37324,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36860,7 +37335,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36871,7 +37346,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36882,7 +37357,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36893,7 +37368,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36904,7 +37379,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36915,7 +37390,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36926,7 +37401,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36937,7 +37412,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36948,7 +37423,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36959,7 +37434,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36970,7 +37445,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36981,7 +37456,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36992,7 +37467,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37003,7 +37478,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37014,7 +37489,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37025,7 +37500,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37036,7 +37511,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37047,7 +37522,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37058,7 +37533,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37069,7 +37544,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37080,7 +37555,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37091,7 +37566,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37102,7 +37577,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37113,7 +37588,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37124,7 +37599,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37135,7 +37610,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37146,7 +37621,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37157,7 +37632,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37168,7 +37643,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37179,7 +37654,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37190,7 +37665,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "stripedbat.com": { + "domain": "stripedbat.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37201,7 +37687,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37212,7 +37698,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37223,7 +37709,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37234,7 +37720,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37245,7 +37731,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37256,7 +37742,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37267,7 +37753,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37278,7 +37764,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37289,7 +37775,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37300,7 +37786,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37311,7 +37797,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37322,7 +37808,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37333,7 +37819,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37344,7 +37830,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37355,7 +37841,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37366,7 +37852,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37377,7 +37863,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37388,7 +37874,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37399,7 +37885,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37410,7 +37896,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37421,7 +37907,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37432,7 +37918,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37443,7 +37929,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "thingstaste.com": { + "domain": "thingstaste.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37454,7 +37951,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37465,7 +37962,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37476,7 +37973,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37487,7 +37984,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37498,7 +37995,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37509,7 +38006,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37520,7 +38017,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "tranquilcan.com": { + "domain": "tranquilcan.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37531,7 +38039,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "tranquilplume.com": { + "domain": "tranquilplume.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37542,7 +38061,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37553,7 +38072,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37564,7 +38083,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37575,7 +38094,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37586,7 +38105,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37597,7 +38116,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37608,7 +38127,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37619,7 +38138,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37630,7 +38149,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37641,7 +38160,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37652,7 +38171,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37663,7 +38182,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37674,7 +38193,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37685,7 +38204,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37696,7 +38215,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37707,7 +38226,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37718,7 +38237,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37729,7 +38248,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37740,7 +38259,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37751,7 +38270,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37762,7 +38281,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37773,7 +38292,40 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "vanishmemory.com": { + "domain": "vanishmemory.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "velvetquasar.com": { + "domain": "velvetquasar.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "venomousvessel.com": { + "domain": "venomousvessel.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37784,7 +38336,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37795,7 +38347,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "verdantloom.com": { + "domain": "verdantloom.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37806,7 +38369,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "vibrantgale.com": { + "domain": "vibrantgale.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37817,7 +38391,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "vibranttalisman.com": { + "domain": "vibranttalisman.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37828,7 +38413,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "vividmeadow.com": { + "domain": "vividmeadow.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37839,7 +38435,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37850,7 +38446,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37861,7 +38457,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37872,7 +38468,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "whimsicalcanyon.com": { + "domain": "whimsicalcanyon.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37883,7 +38490,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37894,7 +38501,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "whisperingquasar.com": { + "domain": "whisperingquasar.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37905,7 +38523,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37916,7 +38534,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37927,7 +38545,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "wistfulwaste.com": { + "domain": "wistfulwaste.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37938,7 +38567,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "wretchedfloor.com": { + "domain": "wretchedfloor.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37949,7 +38589,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "zephyrlabyrinth.com": { + "domain": "zephyrlabyrinth.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37960,7 +38611,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37971,7 +38622,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0146, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -47997,11 +48648,14 @@ "aliveachiever.com", "alluringbucket.com", "aloofvest.com", + "ambientdusk.com", "ambiguousafternoon.com", "ambiguousdinosaurs.com", + "ambrosialsummit.com", "amethystzenith.com", "amuckafternoon.com", "amusedbucket.com", + "analyzecorona.com", "ancientact.com", "annoyedairport.com", "annoyingacoustics.com", @@ -48083,6 +48737,7 @@ "cautiouscredit.com", "cavecurtain.com", "ceciliavenus.com", + "celestialquasar.com", "celestialspectra.com", "chalkoil.com", "changeablecats.com", @@ -48116,6 +48771,9 @@ "confusedcart.com", "consciouscheese.com", "consciousdirt.com", + "coordinatedcoat.com", + "copycarpenter.com", + "cosmicsculptor.com", "courageousbaby.com", "coverapparatus.com", "cozyhillside.com", @@ -48152,6 +48810,8 @@ "deerbeginner.com", "defeatedbadge.com", "delicatecascade.com", + "deliciousducks.com", + "dependenttrip.com", "detailedkitten.com", "detectdiscovery.com", "devilishdinner.com", @@ -48170,18 +48830,23 @@ "dreamycanyon.com", "dustydime.com", "dustyhammer.com", + "effervescentvista.com", "elasticchange.com", "elderlybean.com", "eminentbubble.com", + "enchantingmystique.com", "encouragingthread.com", "endurablebulb.com", "energeticladybug.com", + "enigmaticcanyon.com", + "enigmaticvoyage.com", "enormousearth.com", "entertainskin.com", "enviousshape.com", "equablekettle.com", "ethereallagoon.com", "evanescentedge.com", + "evasivejar.com", "eventexistence.com", "exampleshake.com", "excitingtub.com", @@ -48225,6 +48890,7 @@ "franticroof.com", "freezingbuilding.com", "frequentflesh.com", + "friendlycrayon.com", "friendwool.com", "fronttoad.com", "fumblingform.com", @@ -48246,6 +48912,7 @@ "gloriousbeef.com", "gondolagnome.com", "gorgeousedge.com", + "gracefulmilk.com", "grainmass.com", "grandfatherguitar.com", "grayoranges.com", @@ -48258,6 +48925,8 @@ "guiltlessbasketball.com", "gulliblegrip.com", "gustygrandmother.com", + "halcyoncanyon.com", + "halcyonsculpture.com", "hallowedinvention.com", "haltingbadge.com", "haltingdivision.com", @@ -48299,6 +48968,9 @@ "internalsink.com", "j93557g.com", "jubilantcanyon.com", + "jubilantcascade.com", + "jubilantglimmer.com", + "jubilantwhisper.com", "kaputquill.com", "knitstamp.com", "knottyswing.com", @@ -48314,6 +48986,7 @@ "livelyreward.com", "livingsleet.com", "lizardslaugh.com", + "loadsurprise.com", "lonelyflavor.com", "longingtrees.com", "looseloaf.com", @@ -48321,8 +48994,10 @@ "losslace.com", "lovelydrum.com", "ludicrousarch.com", + "luminouscatalyst.com", "lumpylumber.com", "lunchroomlock.com", + "lustroushaven.com", "maddeningpowder.com", "maliciousmusic.com", "marketspiders.com", @@ -48352,12 +49027,19 @@ "mundanenail.com", "mushywaste.com", "muteknife.com", + "mysticalagoon.com", "naivestatement.com", "nappyattack.com", "neatshade.com", + "nebulacrescent.com", + "nebulajubilee.com", "nebulousamusement.com", + "nebulousgarden.com", + "nebulousquasar.com", + "nebulousripple.com", "needlessnorth.com", "nervoussummer.com", + "niftyhospital.com", "nightwound.com", "nondescriptcrowd.com", "nondescriptnote.com", @@ -48380,12 +49062,14 @@ "panickycurtain.com", "panickypancake.com", "panoramicplane.com", + "parallelbulb.com", "parchedsofa.com", "parentpicture.com", "partplanes.com", "passivepolo.com", "peacefullimit.com", "petiteumbrella.com", + "piquantvortex.com", "placidactivity.com", "placidperson.com", "planebasin.com", @@ -48397,6 +49081,7 @@ "poeticpackage.com", "pointdigestion.com", "pointlesspocket.com", + "pointlessprofit.com", "politeplanes.com", "politicalporter.com", "possibleboats.com", @@ -48424,6 +49109,7 @@ "quizzicalzephyr.com", "rabbitbreath.com", "rabbitrifle.com", + "radiantlullaby.com", "radiateprose.com", "railwaygiraffe.com", "railwayreason.com", @@ -48440,6 +49126,7 @@ "rebelswing.com", "receptivereaction.com", "recessrain.com", + "reconditeprison.com", "reconditerake.com", "reconditerespect.com", "reflectivestatement.com", @@ -48451,6 +49138,7 @@ "resonantbrush.com", "resonantrock.com", "respectrain.com", + "resplendentecho.com", "restrainstorm.com", "restructureinvention.com", "retrievemint.com", @@ -48474,6 +49162,7 @@ "savoryorange.com", "scarceshock.com", "scaredcomfort.com", + "scaredslip.com", "scaredsnake.com", "scaredsnakes.com", "scaredsong.com", @@ -48499,6 +49188,8 @@ "selectivesummer.com", "selfishsnake.com", "separatesort.com", + "seraphicjubilee.com", + "serenepebble.com", "serioussuit.com", "serpentshampoo.com", "settleshoes.com", @@ -48538,12 +49229,14 @@ "smoggysongs.com", "sneakwind.com", "soggysponge.com", + "soggyzoo.com", "solarislabyrinth.com", "somberscarecrow.com", "sombersticks.com", "songsterritory.com", "soothingglade.com", "sordidsmile.com", + "soresidewalk.com", "soretrain.com", "sortsail.com", "sortsummer.com", @@ -48590,6 +49283,7 @@ "stretchsister.com", "stretchsneeze.com", "stretchsquirrel.com", + "stripedbat.com", "strivesidewalk.com", "strivesquirrel.com", "strokesystem.com", @@ -48626,6 +49320,7 @@ "tendertest.com", "terriblethumb.com", "terrifictooth.com", + "thingstaste.com", "thinkitten.com", "thirdrespect.com", "thomastorch.com", @@ -48636,7 +49331,9 @@ "tiredthroat.com", "tiresomethunder.com", "tradetooth.com", + "tranquilcan.com", "tranquilcanyon.com", + "tranquilplume.com", "tremendousearthquake.com", "tremendousplastic.com", "tritebadge.com", @@ -48665,12 +49362,19 @@ "unwieldyimpulse.com", "unwieldyplastic.com", "uselesslumber.com", + "vanishmemory.com", + "velvetquasar.com", "vengefulgrass.com", + "venomousvessel.com", "venusgloria.com", "verdantanswer.com", + "verdantloom.com", "verseballs.com", + "vibrantgale.com", "vibranthaven.com", + "vibranttalisman.com", "virtualvincent.com", + "vividmeadow.com", "volatileprofit.com", "volatilevessel.com", "voraciousgrip.com", @@ -48679,18 +49383,23 @@ "warmquiver.com", "wearbasin.com", "wellgroomedhydrant.com", + "whimsicalcanyon.com", "whimsicalgrove.com", "whisperingcascade.com", + "whisperingquasar.com", "whisperingsummit.com", "whispermeeting.com", "wildcommittee.com", + "wistfulwaste.com", "workoperation.com", + "wretchedfloor.com", "wrongwound.com", + "zephyrlabyrinth.com", "zestycrime.com", "zipperxray.com", "zlp6s.pw" ], - "prevalence": 0.0151, + "prevalence": 0.0146, "displayName": "Admiral" } }, @@ -49436,11 +50145,14 @@ "aliveachiever.com": "Leven Labs, Inc. DBA Admiral", "alluringbucket.com": "Leven Labs, Inc. DBA Admiral", "aloofvest.com": "Leven Labs, Inc. DBA Admiral", + "ambientdusk.com": "Leven Labs, Inc. DBA Admiral", "ambiguousafternoon.com": "Leven Labs, Inc. DBA Admiral", "ambiguousdinosaurs.com": "Leven Labs, Inc. DBA Admiral", + "ambrosialsummit.com": "Leven Labs, Inc. DBA Admiral", "amethystzenith.com": "Leven Labs, Inc. DBA Admiral", "amuckafternoon.com": "Leven Labs, Inc. DBA Admiral", "amusedbucket.com": "Leven Labs, Inc. DBA Admiral", + "analyzecorona.com": "Leven Labs, Inc. DBA Admiral", "ancientact.com": "Leven Labs, Inc. DBA Admiral", "annoyedairport.com": "Leven Labs, Inc. DBA Admiral", "annoyingacoustics.com": "Leven Labs, Inc. DBA Admiral", @@ -49522,6 +50234,7 @@ "cautiouscredit.com": "Leven Labs, Inc. DBA Admiral", "cavecurtain.com": "Leven Labs, Inc. DBA Admiral", "ceciliavenus.com": "Leven Labs, Inc. DBA Admiral", + "celestialquasar.com": "Leven Labs, Inc. DBA Admiral", "celestialspectra.com": "Leven Labs, Inc. DBA Admiral", "chalkoil.com": "Leven Labs, Inc. DBA Admiral", "changeablecats.com": "Leven Labs, Inc. DBA Admiral", @@ -49555,6 +50268,9 @@ "confusedcart.com": "Leven Labs, Inc. DBA Admiral", "consciouscheese.com": "Leven Labs, Inc. DBA Admiral", "consciousdirt.com": "Leven Labs, Inc. DBA Admiral", + "coordinatedcoat.com": "Leven Labs, Inc. DBA Admiral", + "copycarpenter.com": "Leven Labs, Inc. DBA Admiral", + "cosmicsculptor.com": "Leven Labs, Inc. DBA Admiral", "courageousbaby.com": "Leven Labs, Inc. DBA Admiral", "coverapparatus.com": "Leven Labs, Inc. DBA Admiral", "cozyhillside.com": "Leven Labs, Inc. DBA Admiral", @@ -49591,6 +50307,8 @@ "deerbeginner.com": "Leven Labs, Inc. DBA Admiral", "defeatedbadge.com": "Leven Labs, Inc. DBA Admiral", "delicatecascade.com": "Leven Labs, Inc. DBA Admiral", + "deliciousducks.com": "Leven Labs, Inc. DBA Admiral", + "dependenttrip.com": "Leven Labs, Inc. DBA Admiral", "detailedkitten.com": "Leven Labs, Inc. DBA Admiral", "detectdiscovery.com": "Leven Labs, Inc. DBA Admiral", "devilishdinner.com": "Leven Labs, Inc. DBA Admiral", @@ -49609,18 +50327,23 @@ "dreamycanyon.com": "Leven Labs, Inc. DBA Admiral", "dustydime.com": "Leven Labs, Inc. DBA Admiral", "dustyhammer.com": "Leven Labs, Inc. DBA Admiral", + "effervescentvista.com": "Leven Labs, Inc. DBA Admiral", "elasticchange.com": "Leven Labs, Inc. DBA Admiral", "elderlybean.com": "Leven Labs, Inc. DBA Admiral", "eminentbubble.com": "Leven Labs, Inc. DBA Admiral", + "enchantingmystique.com": "Leven Labs, Inc. DBA Admiral", "encouragingthread.com": "Leven Labs, Inc. DBA Admiral", "endurablebulb.com": "Leven Labs, Inc. DBA Admiral", "energeticladybug.com": "Leven Labs, Inc. DBA Admiral", + "enigmaticcanyon.com": "Leven Labs, Inc. DBA Admiral", + "enigmaticvoyage.com": "Leven Labs, Inc. DBA Admiral", "enormousearth.com": "Leven Labs, Inc. DBA Admiral", "entertainskin.com": "Leven Labs, Inc. DBA Admiral", "enviousshape.com": "Leven Labs, Inc. DBA Admiral", "equablekettle.com": "Leven Labs, Inc. DBA Admiral", "ethereallagoon.com": "Leven Labs, Inc. DBA Admiral", "evanescentedge.com": "Leven Labs, Inc. DBA Admiral", + "evasivejar.com": "Leven Labs, Inc. DBA Admiral", "eventexistence.com": "Leven Labs, Inc. DBA Admiral", "exampleshake.com": "Leven Labs, Inc. DBA Admiral", "excitingtub.com": "Leven Labs, Inc. DBA Admiral", @@ -49664,6 +50387,7 @@ "franticroof.com": "Leven Labs, Inc. DBA Admiral", "freezingbuilding.com": "Leven Labs, Inc. DBA Admiral", "frequentflesh.com": "Leven Labs, Inc. DBA Admiral", + "friendlycrayon.com": "Leven Labs, Inc. DBA Admiral", "friendwool.com": "Leven Labs, Inc. DBA Admiral", "fronttoad.com": "Leven Labs, Inc. DBA Admiral", "fumblingform.com": "Leven Labs, Inc. DBA Admiral", @@ -49685,6 +50409,7 @@ "gloriousbeef.com": "Leven Labs, Inc. DBA Admiral", "gondolagnome.com": "Leven Labs, Inc. DBA Admiral", "gorgeousedge.com": "Leven Labs, Inc. DBA Admiral", + "gracefulmilk.com": "Leven Labs, Inc. DBA Admiral", "grainmass.com": "Leven Labs, Inc. DBA Admiral", "grandfatherguitar.com": "Leven Labs, Inc. DBA Admiral", "grayoranges.com": "Leven Labs, Inc. DBA Admiral", @@ -49697,6 +50422,8 @@ "guiltlessbasketball.com": "Leven Labs, Inc. DBA Admiral", "gulliblegrip.com": "Leven Labs, Inc. DBA Admiral", "gustygrandmother.com": "Leven Labs, Inc. DBA Admiral", + "halcyoncanyon.com": "Leven Labs, Inc. DBA Admiral", + "halcyonsculpture.com": "Leven Labs, Inc. DBA Admiral", "hallowedinvention.com": "Leven Labs, Inc. DBA Admiral", "haltingbadge.com": "Leven Labs, Inc. DBA Admiral", "haltingdivision.com": "Leven Labs, Inc. DBA Admiral", @@ -49738,6 +50465,9 @@ "internalsink.com": "Leven Labs, Inc. DBA Admiral", "j93557g.com": "Leven Labs, Inc. DBA Admiral", "jubilantcanyon.com": "Leven Labs, Inc. DBA Admiral", + "jubilantcascade.com": "Leven Labs, Inc. DBA Admiral", + "jubilantglimmer.com": "Leven Labs, Inc. DBA Admiral", + "jubilantwhisper.com": "Leven Labs, Inc. DBA Admiral", "kaputquill.com": "Leven Labs, Inc. DBA Admiral", "knitstamp.com": "Leven Labs, Inc. DBA Admiral", "knottyswing.com": "Leven Labs, Inc. DBA Admiral", @@ -49753,6 +50483,7 @@ "livelyreward.com": "Leven Labs, Inc. DBA Admiral", "livingsleet.com": "Leven Labs, Inc. DBA Admiral", "lizardslaugh.com": "Leven Labs, Inc. DBA Admiral", + "loadsurprise.com": "Leven Labs, Inc. DBA Admiral", "lonelyflavor.com": "Leven Labs, Inc. DBA Admiral", "longingtrees.com": "Leven Labs, Inc. DBA Admiral", "looseloaf.com": "Leven Labs, Inc. DBA Admiral", @@ -49760,8 +50491,10 @@ "losslace.com": "Leven Labs, Inc. DBA Admiral", "lovelydrum.com": "Leven Labs, Inc. DBA Admiral", "ludicrousarch.com": "Leven Labs, Inc. DBA Admiral", + "luminouscatalyst.com": "Leven Labs, Inc. DBA Admiral", "lumpylumber.com": "Leven Labs, Inc. DBA Admiral", "lunchroomlock.com": "Leven Labs, Inc. DBA Admiral", + "lustroushaven.com": "Leven Labs, Inc. DBA Admiral", "maddeningpowder.com": "Leven Labs, Inc. DBA Admiral", "maliciousmusic.com": "Leven Labs, Inc. DBA Admiral", "marketspiders.com": "Leven Labs, Inc. DBA Admiral", @@ -49791,12 +50524,19 @@ "mundanenail.com": "Leven Labs, Inc. DBA Admiral", "mushywaste.com": "Leven Labs, Inc. DBA Admiral", "muteknife.com": "Leven Labs, Inc. DBA Admiral", + "mysticalagoon.com": "Leven Labs, Inc. DBA Admiral", "naivestatement.com": "Leven Labs, Inc. DBA Admiral", "nappyattack.com": "Leven Labs, Inc. DBA Admiral", "neatshade.com": "Leven Labs, Inc. DBA Admiral", + "nebulacrescent.com": "Leven Labs, Inc. DBA Admiral", + "nebulajubilee.com": "Leven Labs, Inc. DBA Admiral", "nebulousamusement.com": "Leven Labs, Inc. DBA Admiral", + "nebulousgarden.com": "Leven Labs, Inc. DBA Admiral", + "nebulousquasar.com": "Leven Labs, Inc. DBA Admiral", + "nebulousripple.com": "Leven Labs, Inc. DBA Admiral", "needlessnorth.com": "Leven Labs, Inc. DBA Admiral", "nervoussummer.com": "Leven Labs, Inc. DBA Admiral", + "niftyhospital.com": "Leven Labs, Inc. DBA Admiral", "nightwound.com": "Leven Labs, Inc. DBA Admiral", "nondescriptcrowd.com": "Leven Labs, Inc. DBA Admiral", "nondescriptnote.com": "Leven Labs, Inc. DBA Admiral", @@ -49819,12 +50559,14 @@ "panickycurtain.com": "Leven Labs, Inc. DBA Admiral", "panickypancake.com": "Leven Labs, Inc. DBA Admiral", "panoramicplane.com": "Leven Labs, Inc. DBA Admiral", + "parallelbulb.com": "Leven Labs, Inc. DBA Admiral", "parchedsofa.com": "Leven Labs, Inc. DBA Admiral", "parentpicture.com": "Leven Labs, Inc. DBA Admiral", "partplanes.com": "Leven Labs, Inc. DBA Admiral", "passivepolo.com": "Leven Labs, Inc. DBA Admiral", "peacefullimit.com": "Leven Labs, Inc. DBA Admiral", "petiteumbrella.com": "Leven Labs, Inc. DBA Admiral", + "piquantvortex.com": "Leven Labs, Inc. DBA Admiral", "placidactivity.com": "Leven Labs, Inc. DBA Admiral", "placidperson.com": "Leven Labs, Inc. DBA Admiral", "planebasin.com": "Leven Labs, Inc. DBA Admiral", @@ -49836,6 +50578,7 @@ "poeticpackage.com": "Leven Labs, Inc. DBA Admiral", "pointdigestion.com": "Leven Labs, Inc. DBA Admiral", "pointlesspocket.com": "Leven Labs, Inc. DBA Admiral", + "pointlessprofit.com": "Leven Labs, Inc. DBA Admiral", "politeplanes.com": "Leven Labs, Inc. DBA Admiral", "politicalporter.com": "Leven Labs, Inc. DBA Admiral", "possibleboats.com": "Leven Labs, Inc. DBA Admiral", @@ -49863,6 +50606,7 @@ "quizzicalzephyr.com": "Leven Labs, Inc. DBA Admiral", "rabbitbreath.com": "Leven Labs, Inc. DBA Admiral", "rabbitrifle.com": "Leven Labs, Inc. DBA Admiral", + "radiantlullaby.com": "Leven Labs, Inc. DBA Admiral", "radiateprose.com": "Leven Labs, Inc. DBA Admiral", "railwaygiraffe.com": "Leven Labs, Inc. DBA Admiral", "railwayreason.com": "Leven Labs, Inc. DBA Admiral", @@ -49879,6 +50623,7 @@ "rebelswing.com": "Leven Labs, Inc. DBA Admiral", "receptivereaction.com": "Leven Labs, Inc. DBA Admiral", "recessrain.com": "Leven Labs, Inc. DBA Admiral", + "reconditeprison.com": "Leven Labs, Inc. DBA Admiral", "reconditerake.com": "Leven Labs, Inc. DBA Admiral", "reconditerespect.com": "Leven Labs, Inc. DBA Admiral", "reflectivestatement.com": "Leven Labs, Inc. DBA Admiral", @@ -49890,6 +50635,7 @@ "resonantbrush.com": "Leven Labs, Inc. DBA Admiral", "resonantrock.com": "Leven Labs, Inc. DBA Admiral", "respectrain.com": "Leven Labs, Inc. DBA Admiral", + "resplendentecho.com": "Leven Labs, Inc. DBA Admiral", "restrainstorm.com": "Leven Labs, Inc. DBA Admiral", "restructureinvention.com": "Leven Labs, Inc. DBA Admiral", "retrievemint.com": "Leven Labs, Inc. DBA Admiral", @@ -49913,6 +50659,7 @@ "savoryorange.com": "Leven Labs, Inc. DBA Admiral", "scarceshock.com": "Leven Labs, Inc. DBA Admiral", "scaredcomfort.com": "Leven Labs, Inc. DBA Admiral", + "scaredslip.com": "Leven Labs, Inc. DBA Admiral", "scaredsnake.com": "Leven Labs, Inc. DBA Admiral", "scaredsnakes.com": "Leven Labs, Inc. DBA Admiral", "scaredsong.com": "Leven Labs, Inc. DBA Admiral", @@ -49938,6 +50685,8 @@ "selectivesummer.com": "Leven Labs, Inc. DBA Admiral", "selfishsnake.com": "Leven Labs, Inc. DBA Admiral", "separatesort.com": "Leven Labs, Inc. DBA Admiral", + "seraphicjubilee.com": "Leven Labs, Inc. DBA Admiral", + "serenepebble.com": "Leven Labs, Inc. DBA Admiral", "serioussuit.com": "Leven Labs, Inc. DBA Admiral", "serpentshampoo.com": "Leven Labs, Inc. DBA Admiral", "settleshoes.com": "Leven Labs, Inc. DBA Admiral", @@ -49977,12 +50726,14 @@ "smoggysongs.com": "Leven Labs, Inc. DBA Admiral", "sneakwind.com": "Leven Labs, Inc. DBA Admiral", "soggysponge.com": "Leven Labs, Inc. DBA Admiral", + "soggyzoo.com": "Leven Labs, Inc. DBA Admiral", "solarislabyrinth.com": "Leven Labs, Inc. DBA Admiral", "somberscarecrow.com": "Leven Labs, Inc. DBA Admiral", "sombersticks.com": "Leven Labs, Inc. DBA Admiral", "songsterritory.com": "Leven Labs, Inc. DBA Admiral", "soothingglade.com": "Leven Labs, Inc. DBA Admiral", "sordidsmile.com": "Leven Labs, Inc. DBA Admiral", + "soresidewalk.com": "Leven Labs, Inc. DBA Admiral", "soretrain.com": "Leven Labs, Inc. DBA Admiral", "sortsail.com": "Leven Labs, Inc. DBA Admiral", "sortsummer.com": "Leven Labs, Inc. DBA Admiral", @@ -50029,6 +50780,7 @@ "stretchsister.com": "Leven Labs, Inc. DBA Admiral", "stretchsneeze.com": "Leven Labs, Inc. DBA Admiral", "stretchsquirrel.com": "Leven Labs, Inc. DBA Admiral", + "stripedbat.com": "Leven Labs, Inc. DBA Admiral", "strivesidewalk.com": "Leven Labs, Inc. DBA Admiral", "strivesquirrel.com": "Leven Labs, Inc. DBA Admiral", "strokesystem.com": "Leven Labs, Inc. DBA Admiral", @@ -50065,6 +50817,7 @@ "tendertest.com": "Leven Labs, Inc. DBA Admiral", "terriblethumb.com": "Leven Labs, Inc. DBA Admiral", "terrifictooth.com": "Leven Labs, Inc. DBA Admiral", + "thingstaste.com": "Leven Labs, Inc. DBA Admiral", "thinkitten.com": "Leven Labs, Inc. DBA Admiral", "thirdrespect.com": "Leven Labs, Inc. DBA Admiral", "thomastorch.com": "Leven Labs, Inc. DBA Admiral", @@ -50075,7 +50828,9 @@ "tiredthroat.com": "Leven Labs, Inc. DBA Admiral", "tiresomethunder.com": "Leven Labs, Inc. DBA Admiral", "tradetooth.com": "Leven Labs, Inc. DBA Admiral", + "tranquilcan.com": "Leven Labs, Inc. DBA Admiral", "tranquilcanyon.com": "Leven Labs, Inc. DBA Admiral", + "tranquilplume.com": "Leven Labs, Inc. DBA Admiral", "tremendousearthquake.com": "Leven Labs, Inc. DBA Admiral", "tremendousplastic.com": "Leven Labs, Inc. DBA Admiral", "tritebadge.com": "Leven Labs, Inc. DBA Admiral", @@ -50104,12 +50859,19 @@ "unwieldyimpulse.com": "Leven Labs, Inc. DBA Admiral", "unwieldyplastic.com": "Leven Labs, Inc. DBA Admiral", "uselesslumber.com": "Leven Labs, Inc. DBA Admiral", + "vanishmemory.com": "Leven Labs, Inc. DBA Admiral", + "velvetquasar.com": "Leven Labs, Inc. DBA Admiral", "vengefulgrass.com": "Leven Labs, Inc. DBA Admiral", + "venomousvessel.com": "Leven Labs, Inc. DBA Admiral", "venusgloria.com": "Leven Labs, Inc. DBA Admiral", "verdantanswer.com": "Leven Labs, Inc. DBA Admiral", + "verdantloom.com": "Leven Labs, Inc. DBA Admiral", "verseballs.com": "Leven Labs, Inc. DBA Admiral", + "vibrantgale.com": "Leven Labs, Inc. DBA Admiral", "vibranthaven.com": "Leven Labs, Inc. DBA Admiral", + "vibranttalisman.com": "Leven Labs, Inc. DBA Admiral", "virtualvincent.com": "Leven Labs, Inc. DBA Admiral", + "vividmeadow.com": "Leven Labs, Inc. DBA Admiral", "volatileprofit.com": "Leven Labs, Inc. DBA Admiral", "volatilevessel.com": "Leven Labs, Inc. DBA Admiral", "voraciousgrip.com": "Leven Labs, Inc. DBA Admiral", @@ -50118,13 +50880,18 @@ "warmquiver.com": "Leven Labs, Inc. DBA Admiral", "wearbasin.com": "Leven Labs, Inc. DBA Admiral", "wellgroomedhydrant.com": "Leven Labs, Inc. DBA Admiral", + "whimsicalcanyon.com": "Leven Labs, Inc. DBA Admiral", "whimsicalgrove.com": "Leven Labs, Inc. DBA Admiral", "whisperingcascade.com": "Leven Labs, Inc. DBA Admiral", + "whisperingquasar.com": "Leven Labs, Inc. DBA Admiral", "whisperingsummit.com": "Leven Labs, Inc. DBA Admiral", "whispermeeting.com": "Leven Labs, Inc. DBA Admiral", "wildcommittee.com": "Leven Labs, Inc. DBA Admiral", + "wistfulwaste.com": "Leven Labs, Inc. DBA Admiral", "workoperation.com": "Leven Labs, Inc. DBA Admiral", + "wretchedfloor.com": "Leven Labs, Inc. DBA Admiral", "wrongwound.com": "Leven Labs, Inc. DBA Admiral", + "zephyrlabyrinth.com": "Leven Labs, Inc. DBA Admiral", "zestycrime.com": "Leven Labs, Inc. DBA Admiral", "zipperxray.com": "Leven Labs, Inc. DBA Admiral", "zlp6s.pw": "Leven Labs, Inc. DBA Admiral", diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index 650dcf5f25..2f5d747ade 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -23,6 +23,7 @@ import Foundation import AppKit import Common import LoginItems +import NetworkProtectionProxy @MainActor final class DataBrokerProtectionDebugMenu: NSMenu { @@ -82,6 +83,11 @@ final class DataBrokerProtectionDebugMenu: NSMenu { NSMenuItem(title: "Restart", action: #selector(DataBrokerProtectionDebugMenu.backgroundAgentRestart)) .targetting(self) + + NSMenuItem.separator() + + NSMenuItem(title: "Show agent IP address", action: #selector(DataBrokerProtectionDebugMenu.showAgentIPAddress)) + .targetting(self) } NSMenuItem(title: "Operations") { @@ -138,6 +144,8 @@ final class DataBrokerProtectionDebugMenu: NSMenu { .targetting(self) NSMenuItem(title: "Run Personal Information Removal Debug Mode", action: #selector(DataBrokerProtectionDebugMenu.runCustomJSON)) .targetting(self) + NSMenuItem(title: "Reset All State and Delete All Data", action: #selector(DataBrokerProtectionDebugMenu.deleteAllDataAndStopAgent)) + .targetting(self) } } @@ -228,6 +236,14 @@ final class DataBrokerProtectionDebugMenu: NSMenu { LoginItemsManager().enableLoginItems([LoginItem.dbpBackgroundAgent], log: .dbp) } + @objc private func deleteAllDataAndStopAgent() { + Task { @MainActor in + guard case .alertFirstButtonReturn = await NSAlert.removeAllDBPStateAndDataAlert().runModal() else { return } + resetWaitlistState() + DataBrokerProtectionFeatureDisabler().disableAndDelete() + } + } + @objc private func showDatabaseBrowser() { let viewController = DataBrokerDatabaseBrowserViewController() let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 500, height: 400), @@ -243,6 +259,10 @@ final class DataBrokerProtectionDebugMenu: NSMenu { window.delegate = self } + @objc private func showAgentIPAddress() { + DataBrokerProtectionManager.shared.showAgentIPAddress() + } + @objc private func showForceOptOutWindow() { let viewController = DataBrokerForceOptOutViewController() let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 500, height: 400), diff --git a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift index f2c9bebd4d..75f8c3e855 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift @@ -41,8 +41,10 @@ public final class DataBrokerProtectionManager { return dataManager }() + private lazy var ipcClient = DataBrokerProtectionIPCClient(machServiceName: Bundle.main.dbpBackgroundAgentBundleId, pixelHandler: pixelHandler) + lazy var scheduler: DataBrokerProtectionLoginItemScheduler = { - let ipcClient = DataBrokerProtectionIPCClient(machServiceName: Bundle.main.dbpBackgroundAgentBundleId, pixelHandler: pixelHandler) + let ipcScheduler = DataBrokerProtectionIPCScheduler(ipcClient: ipcClient) return DataBrokerProtectionLoginItemScheduler(ipcScheduler: ipcScheduler, pixelHandler: pixelHandler) @@ -57,6 +59,12 @@ public final class DataBrokerProtectionManager { public func shouldAskForInviteCode() -> Bool { redeemUseCase.shouldAskForInviteCode() } + + // MARK: - Debugging Features + + public func showAgentIPAddress() { + ipcClient.openBrowser(domain: "https://www.whatismyip.com") + } } extension DataBrokerProtectionManager: DataBrokerProtectionDataManagerDelegate { diff --git a/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift b/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift index e1e00e38c7..cdbfa623a9 100644 --- a/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift +++ b/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import Foundation import LoginItems #if DBP diff --git a/DuckDuckGo/DataExport/BookmarksExporter.swift b/DuckDuckGo/DataExport/BookmarksExporter.swift index c089132d9c..eb910ed472 100644 --- a/DuckDuckGo/DataExport/BookmarksExporter.swift +++ b/DuckDuckGo/DataExport/BookmarksExporter.swift @@ -34,7 +34,7 @@ struct BookmarksExporter { for entity in entities { if let bookmark = entity as? Bookmark { content.append(Template.bookmark(level: level, - title: bookmark.title.escapedForHTML, + title: bookmark.title.escapedUnicodeHtmlString(), url: bookmark.url, isFavorite: bookmark.isFavorite)) } @@ -100,12 +100,6 @@ extension BookmarksExporter { fileprivate extension String { - var escapedForHTML: String { - self.replacingOccurrences(of: "&", with: "&") - .replacingOccurrences(of: "<", with: "<") - .replacingOccurrences(of: ">", with: ">") - } - static func indent(by level: Int) -> String { return String(repeating: "\t", count: level) } diff --git a/DuckDuckGo/DataImport/View/FileImportView.swift b/DuckDuckGo/DataImport/View/FileImportView.swift index faab1cf62b..0663fb5350 100644 --- a/DuckDuckGo/DataImport/View/FileImportView.swift +++ b/DuckDuckGo/DataImport/View/FileImportView.swift @@ -476,9 +476,9 @@ struct FileImportView: View { { switch dataType { case .bookmarks: - Text("Import Bookmarks") + Text("Import Bookmarks", comment: "Title of dialog with instruction for the user to import bookmarks from another browser") case .passwords: - Text("Import Passwords") + Text("Import Passwords", comment: "Title of dialog with instruction for the user to import passwords from another browser") } }().bold() diff --git a/DuckDuckGo/DataImport/View/ReportFeedbackView.swift b/DuckDuckGo/DataImport/View/ReportFeedbackView.swift index 577b779cc6..dc35dc0745 100644 --- a/DuckDuckGo/DataImport/View/ReportFeedbackView.swift +++ b/DuckDuckGo/DataImport/View/ReportFeedbackView.swift @@ -52,7 +52,7 @@ struct ReportFeedbackView: View { Text("The version of the browser you are trying to import from", comment: "Data import failure Report dialog description of a report field providing version of a browser user is trying to import data from") } InfoItemView(model.error.localizedDescription) { - Text("Error message & code", comment: "") + Text("Error message & code", comment: "Title of the section of a dialog (form where the user can report feedback) where the error message and the error code are shown") } } .padding(.bottom, 24) diff --git a/DuckDuckGo/DuckDuckGo.entitlements b/DuckDuckGo/DuckDuckGo.entitlements index 7b79b8b2fe..757dc88e2c 100644 --- a/DuckDuckGo/DuckDuckGo.entitlements +++ b/DuckDuckGo/DuckDuckGo.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider-systemextension + app-proxy-provider-systemextension com.apple.developer.system-extension.install diff --git a/DuckDuckGo/DuckDuckGoAppStore.entitlements b/DuckDuckGo/DuckDuckGoAppStore.entitlements index e419bc0920..97443cb452 100644 --- a/DuckDuckGo/DuckDuckGoAppStore.entitlements +++ b/DuckDuckGo/DuckDuckGoAppStore.entitlements @@ -19,6 +19,11 @@ com.apple.security.files.user-selected.read-write + com.apple.developer.networking.networkextension + + packet-tunnel-provider + app-proxy-provider + com.apple.security.network.client com.apple.security.personal-information.location diff --git a/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements b/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements index a2c7bd6bd5..13ea43d233 100644 --- a/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements +++ b/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements @@ -2,10 +2,6 @@ - com.apple.developer.networking.networkextension - - packet-tunnel-provider - com.apple.security.app-sandbox com.apple.security.application-groups diff --git a/DuckDuckGo/DuckDuckGoDebug.entitlements b/DuckDuckGo/DuckDuckGoDebug.entitlements index dcffb16791..dad1686cba 100644 --- a/DuckDuckGo/DuckDuckGoDebug.entitlements +++ b/DuckDuckGo/DuckDuckGoDebug.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.developer.system-extension.install diff --git a/DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements b/DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements deleted file mode 100644 index 069c866e05..0000000000 --- a/DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements +++ /dev/null @@ -1,30 +0,0 @@ - - - - - com.apple.developer.networking.networkextension - - packet-tunnel-provider - - com.apple.developer.system-extension.install - - com.apple.developer.usernotifications.time-sensitive - - com.apple.security.application-groups - - HKE973VLUW.com.duckduckgo.network-protection - $(NETP_APP_GROUP) - - com.apple.security.device.audio-input - - com.apple.security.device.camera - - com.apple.security.personal-information.location - - keychain-access-groups - - $(AppIdentifierPrefix)com.duckduckgo.macos.browser - $(AppIdentifierPrefix)com.duckduckgo.network-protection - - - diff --git a/DuckDuckGo/DuckDuckGo_NetP_Release.entitlements b/DuckDuckGo/DuckDuckGo_NetP_Release.entitlements deleted file mode 100644 index a2226d1f8d..0000000000 --- a/DuckDuckGo/DuckDuckGo_NetP_Release.entitlements +++ /dev/null @@ -1,38 +0,0 @@ - - - - - com.apple.developer.networking.networkextension - - packet-tunnel-provider-systemextension - - com.apple.developer.system-extension.install - - com.apple.developer.usernotifications.time-sensitive - - com.apple.security.application-groups - - $(TeamIdentifierPrefix)com.duckduckgo.macos.browser.network-protection - $(NETP_APP_GROUP) - - com.apple.security.device.audio-input - - com.apple.security.device.camera - - com.apple.security.personal-information.location - - keychain-access-groups - - $(AppIdentifierPrefix)com.duckduckgo.macos.browser - $(AppIdentifierPrefix)com.duckduckgo.network-protection - - com.apple.security.personal-information.location - - com.apple.developer.networking.networkextension - - packet-tunnel-provider-systemextension - - com.apple.developer.system-extension.install - - - diff --git a/DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift b/DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift new file mode 100644 index 0000000000..af9d04103e --- /dev/null +++ b/DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift @@ -0,0 +1,45 @@ +// +// ErrorPageHTMLTemplate.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import ContentScopeScripts +import WebKit + +struct ErrorPageHTMLTemplate { + + static var htmlTemplatePath: String { + guard let file = ContentScopeScripts.Bundle.path(forResource: "index", ofType: "html", inDirectory: "pages/errorpage") else { + assertionFailure("HTML template not found") + return "" + } + return file + } + + let error: WKError + let header: String + + func makeHTMLFromTemplate() -> String { + guard let html = try? String(contentsOfFile: Self.htmlTemplatePath) else { + assertionFailure("Should be able to load template") + return "" + } + return html.replacingOccurrences(of: "$ERROR_DESCRIPTION$", with: error.localizedDescription.escapedUnicodeHtmlString(), options: .literal) + .replacingOccurrences(of: "$HEADER$", with: header.escapedUnicodeHtmlString(), options: .literal) + } + +} diff --git a/DuckDuckGo/HomePage/View/RecentlyVisitedView.swift b/DuckDuckGo/HomePage/View/RecentlyVisitedView.swift index c57c95da96..c7e865ac7a 100644 --- a/DuckDuckGo/HomePage/View/RecentlyVisitedView.swift +++ b/DuckDuckGo/HomePage/View/RecentlyVisitedView.swift @@ -362,6 +362,10 @@ struct SiteIconAndConnector: View { @State var isHovering = false + private var favicon: FaviconView { + FaviconView(url: site.url, size: 22, letterPaddingModifier: 0.22) + } + var body: some View { VStack(spacing: 0) { if site.isRealDomain { @@ -383,7 +387,7 @@ struct SiteIconAndConnector: View { RoundedRectangle(cornerRadius: 6) .fill(isHovering ? mouseOverColor : backgroundColor) - FaviconView(url: site.url, size: 22) + favicon } .link { self.isHovering = $0 @@ -405,7 +409,7 @@ struct SiteIconAndConnector: View { RoundedRectangle(cornerRadius: 6) .fill(backgroundColor) - FaviconView(url: site.url, size: 22) + favicon } .frame(width: 32, height: 32) } @@ -440,9 +444,9 @@ struct SiteTrackerSummary: View { Group { Group { if #available(macOS 12, *) { - Text("**\(site.numberOfTrackersBlocked)** tracking attempts blocked") + Text("**\(site.numberOfTrackersBlocked)** tracking attempts blocked", comment: "The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@") } else { - Text("\(site.numberOfTrackersBlocked) tracking attempts blocked") + Text("\(site.numberOfTrackersBlocked) tracking attempts blocked", comment: "The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@") } } .visibility(site.blockedEntities.isEmpty ? .gone : .visible) diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index d53f2f6d1c..ddf6e62331 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -32,7 +32,7 @@ }, "**%lld** tracking attempts blocked" : { - + "comment" : "The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@" }, "%@ does not support storing passwords" : { "comment" : "Data Import disabled checkbox message about a browser (%@) not supporting storing passwords" @@ -41,7 +41,7 @@ }, "%lld tracking attempts blocked" : { - + "comment" : "The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@" }, "• Because data broker sites often have multi-step processes required to have information removed, and because they regularly update their databases with new personal information, this authorization includes ongoing action on your behalf solely to perform the service." : { @@ -230,7 +230,7 @@ } }, "Address" : { - + "comment" : "Title of the section of the Identities manager where the user can add/modify an address (street city etc,)" }, "Address:" : { "comment" : "Add Bookmark dialog bookmark url field heading" @@ -986,7 +986,7 @@ } }, "Birthday" : { - + "comment" : "Title of the section of the Identities manager where the user can add/modify a date of birth" }, "bitwarden.app.found" : { "comment" : "Setup of the integration with Bitwarden app", @@ -1001,6 +1001,7 @@ } }, "bitwarden.cant.access.container" : { + "comment" : "Requests user Full Disk access in order to access password manager Birwarden", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1012,6 +1013,7 @@ } }, "bitwarden.connect.communication-info" : { + "comment" : "Warns users that all communication between the DuckDuckGo browser and the password manager Bitwarden is encrypted and doesn't leave the user device", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1023,6 +1025,7 @@ } }, "bitwarden.connect.description" : { + "comment" : "Description for when the user wants to connect the browser to the password manager Bitwarned.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1034,6 +1037,7 @@ } }, "bitwarden.connect.history-info" : { + "comment" : "Warn users that the password Manager Bitwarden will have access to their browsing history", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1068,6 +1072,7 @@ } }, "bitwarden.connecting" : { + "comment" : "It appears in a dialog when the users are connecting to Bitwardern and shows the status of the action, in this case we are in the progress of connecting the browser to the Bitwarden password maanger.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1079,6 +1084,7 @@ } }, "bitwarden.error" : { + "comment" : "This message appears when the application is unable to find or connect to Bitwarden, indicating a connection issue.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1090,6 +1096,7 @@ } }, "bitwarden.handshake.not.approved" : { + "comment" : "It appears in a dialog when the users are connecting to Bitwardern and shows the status of the action. This message indicates that the handshake process was not approved in the Bitwarden app.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1149,6 +1156,7 @@ } }, "bitwarden.integration.not.approved" : { + "comment" : "While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates that the integration with DuckDuckGo has not been approved in the Bitwarden app.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1172,6 +1180,7 @@ } }, "bitwarden.missing.handshake" : { + "comment" : "While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates a missing handshake (a way for two devices or systems to say hello to each other and agree to communicate or exchange information).", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1194,6 +1203,7 @@ } }, "bitwarden.old.version" : { + "comment" : "Message that warns user they need to update their password manager Bitwarden app vesion", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1205,6 +1215,7 @@ } }, "bitwarden.preferences.complete-setup" : { + "comment" : "action option that prompts the user to complete the setup process in Bitwarden preferences", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1216,6 +1227,7 @@ } }, "bitwarden.preferences.open-bitwarden" : { + "comment" : "Button to open Bitwarden app", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1227,6 +1239,7 @@ } }, "bitwarden.preferences.run" : { + "comment" : "Warns user that the password manager Bitwarden app is not running", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1238,6 +1251,7 @@ } }, "bitwarden.preferences.unable-to-connect" : { + "comment" : "Dialog telling the user Bitwarden (a password manager) is not available", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1249,6 +1263,7 @@ } }, "bitwarden.preferences.unlock" : { + "comment" : "Asks the user to unlock the password manager Bitwarden", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1260,6 +1275,7 @@ } }, "bitwarden.waiting.for.handshake" : { + "comment" : "While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates the system is waiting for the handshake (a way for two devices or systems to say hello to each other and agree to communicate or exchange information).", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1283,6 +1299,7 @@ } }, "bitwarden.waiting.for.status.response" : { + "comment" : "It appears in a dialog when the users are connecting to Bitwardern and shows the status of the action, in this case that the application is currently waiting for a response from the Bitwarden service.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1768,6 +1785,7 @@ "comment" : "Main Menu Window item" }, "burner.homepage.description.1" : { + "comment" : "Descriptions of features Fire page. Provides information about browsing functionalities such as browsing without saving local history, signing in to a site with a different account, and troubleshooting websites.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1779,6 +1797,7 @@ } }, "burner.homepage.description.2" : { + "comment" : "Descriptions of features Fire page. Provides information about browsing functionalities such as browsing without saving local history, signing in to a site with a different account, and troubleshooting websites.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1790,6 +1809,7 @@ } }, "burner.homepage.description.3" : { + "comment" : "Descriptions of features Fire page. Provides information about browsing functionalities such as browsing without saving local history, signing in to a site with a different account, and troubleshooting websites.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1801,6 +1821,7 @@ } }, "burner.homepage.description.4" : { + "comment" : "This describes the functionality of one of out browser feature Fire Window, highlighting their isolation from other browser data and the automatic deletion of their data upon closure. Additionally, it emphasizes that fire windows offer the same level of tracking protection as other browsing windows.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -1974,7 +1995,7 @@ } }, "Contact Info" : { - + "comment" : "Title of the section of the Identities manager where the user can add/modify contact info (phone, email address)" }, "copy" : { "comment" : "Copy button", @@ -2040,7 +2061,7 @@ } }, "Country" : { - + "comment" : "Title of the section of the Identities manager where the user can add/modify a country (US,UK, Italy etc...)" }, "crash-report.description" : { "comment" : "Description of the dialog where the user can send a crash report", @@ -2190,6 +2211,7 @@ } }, "default.browser.prompt.button" : { + "comment" : "represents a prompt message asking the user to make DuckDuckGo their default browser.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -2201,6 +2223,7 @@ } }, "default.browser.prompt.message" : { + "comment" : "represents a prompt message asking the user to make DuckDuckGo their default browser.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -2808,7 +2831,7 @@ } }, "email.copied" : { - "comment" : "Private email address was copied to clipboard message", + "comment" : "Notification that the Private email address was copied to clipboard after the user generated a new address", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -2868,7 +2891,7 @@ } }, "email.optionsMenu.turnOn" : { - "comment" : "Enable email sub menu item", + "comment" : "Sub menu item to enable Email Protection", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -2883,19 +2906,7 @@ "comment" : "Main Menu View item" }, "Error message & code" : { - - }, - "error.unknown" : { - "comment" : "Error page subtitle", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "An unknown error has occurred" - } - } - } + "comment" : "Title of the section of a dialog (form where the user can report feedback) where the error message and the error code are shown" }, "error.unknown.try.again" : { "comment" : "Generic error message on a dialog for when the cause is not known.", @@ -3033,7 +3044,7 @@ } }, "feedback.bug.description" : { - "comment" : "Label in the feedback form", + "comment" : "Label in the feedback form that users can submit to say that a website is not working properly in DuckDuckGo", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3045,7 +3056,7 @@ } }, "feedback.disclaimer" : { - "comment" : "Disclaimer in breakage form", + "comment" : "Disclaimer in breakage form - a form that users can submit to say that a website is not working properly in DuckDuckGo", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3057,7 +3068,7 @@ } }, "feedback.feature.request.description" : { - "comment" : "Label in the feedback form", + "comment" : "Label in the feedback form for feature requests.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3123,7 +3134,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Close %1$d active %2$@ and clear all browsing history and cookies (%3$d %4$@)." + "value" : "Close active tabs (%1$d) and clear all browsing history and cookies (sites: %2$d)." } } } @@ -3363,7 +3374,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Close this tab and clear its browsing history and cookies (%1$d %2$@)." + "value" : "Close this tab and clear its browsing history and cookies (sites: %d)." } } } @@ -3756,6 +3767,7 @@ } }, "home.page.empty.state.item.message" : { + "comment" : "This string represents the message for an empty state item on the home page, encouraging the user to keep browsing to see how many trackers were blocked", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3767,6 +3779,7 @@ } }, "home.page.empty.state.item.title" : { + "comment" : "This string represents the title for an empty state item on the home page, indicating that recently visited sites will appear here", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3778,6 +3791,7 @@ } }, "home.page.no.trackers.blocked" : { + "comment" : "This string represents a message on the home page indicating that no trackers were blocked", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3789,6 +3803,7 @@ } }, "home.page.no.trackers.found" : { + "comment" : "This string represents a message on the home page indicating that no trackers were found", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3812,6 +3827,7 @@ } }, "home.page.protection.summary.info" : { + "comment" : "This string represents a message in the protection summary on the home page, indicating that there is no recent activity", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3823,6 +3839,7 @@ } }, "home.page.protection.summary.message" : { + "comment" : "The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -3840,10 +3857,10 @@ "comment" : "Menu item" }, "Import Bookmarks" : { - + "comment" : "Title of dialog with instruction for the user to import bookmarks from another browser" }, "Import Passwords" : { - "comment" : "my comment" + "comment" : "Title of dialog with instruction for the user to import passwords from another browser" }, "Import Results:" : { "comment" : "Data Import result summary headline" @@ -3891,7 +3908,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Importing %d bookmarks…" + "value" : "Importing bookmarks (%d)…" } } } @@ -4431,7 +4448,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Importing %d passwords…" + "value" : "Importing passwords (%d)…" } } } @@ -4701,7 +4718,7 @@ } }, "main-menu.edit.paste-and-match-style" : { - "comment" : "Main Menu Edit item", + "comment" : "Main Menu Edit item - Action that allows the user to paste copy into a target document and the target document's style will be retained (instead of the source style)", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -4917,7 +4934,7 @@ } }, "main-menu.file.open-location" : { - "comment" : "Main Menu File item", + "comment" : "Main Menu File item- Menu option that allows the user to connect to an address (type an address) on click the address bar of the browser is selected and the user can type.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -5075,14 +5092,26 @@ } } }, + "mute.tab" : { + "comment" : "Menu item. Mute tab", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Mute Tab" + } + } + } + }, "n.more.tabs" : { - "comment" : "suffix of string in Recently Closed menu", + "comment" : "String in Recently Closed menu item for recently closed browser window and number of tabs contained in the closed window", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : " (and %d more tabs)" + "value" : "Window with multiple tabs (%d)" } } } @@ -5475,7 +5504,7 @@ } }, "no.access.to.downloads.folder.header" : { - "comment" : "Header of the alert dialog informing user about failed download", + "comment" : "Header of the alert dialog warning the user they need to give the browser permission to access the Downloads folder", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -5690,18 +5719,6 @@ } } }, - "one.more.tab" : { - "comment" : "suffix of string in Recently Closed menu", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : " (and 1 more tab)" - } - } - } - }, "Only Show on New Tab" : { "comment" : "Preference for only showing the bookmarks bar on new tab" }, @@ -5766,7 +5783,7 @@ } }, "open.externally.failed" : { - "comment" : "’Link’ is link on a website", + "comment" : "’Link’ is link on a website, it couldn't be opened due to the required app not being found", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -5874,6 +5891,7 @@ } }, "open.settings" : { + "comment" : "This string represents a prompt or button label prompting the user to open system settings", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -5920,6 +5938,42 @@ } } }, + "page.crash.header" : { + "comment" : "Error page heading text shown when a Web Page process had crashed", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "This webpage has crashed." + } + } + } + }, + "page.crash.message" : { + "comment" : "Error page message text shown when a Web Page process had crashed", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Try reloading the page or come back later." + } + } + } + }, + "page.error.header" : { + "comment" : "Error page heading text", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DuckDuckGo can’t load this page." + } + } + } + }, "passsword.management" : { "comment" : "Used as title for password management user interface", "extractionState" : "extracted_with_value", @@ -6545,7 +6599,7 @@ } }, "permission.popup.title" : { - "comment" : "List of blocked popups Title", + "comment" : "Title of a popup that has a list of blocked popups", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -7100,6 +7154,7 @@ } }, "pm.lock-screen.duration" : { + "comment" : "Message about the duration for which autofill information remains unlocked on the lock screen.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -7855,6 +7910,7 @@ } }, "preferences.about.unsupported-device-info1" : { + "comment" : "This string represents a message informing the user that DuckDuckGo is no longer providing browser updates for their version of macOS", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -7866,61 +7922,13 @@ } }, "preferences.about.unsupported-device-info2" : { - "comment" : "Link to the about page", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Please update to macOS %@ or later to use the most recent version" - } - } - } - }, - "preferences.about.unsupported-device-info2-part1" : { - "comment" : "Second paragraph of unsupported device info - sentence part 1", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Please" - } - } - } - }, - "preferences.about.unsupported-device-info2-part2" : { - "comment" : "Second paragraph of unsupported device info - sentence part 2 (underlined)", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "update to macOS %@" - } - } - } - }, - "preferences.about.unsupported-device-info2-part3" : { - "comment" : "Second paragraph of unsupported device info - sentence part 3", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "or later to use the most recent version" - } - } - } - }, - "preferences.about.unsupported-device-info2-part4" : { - "comment" : "Second paragraph of unsupported device info - sentence part 4", + "comment" : "Copy in section that tells the user to update their macOS version since their current version is unsupported", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "of DuckDuckGo. You can also keep using your current version of the browser, but it will not receive further updates." + "value" : "Please update to macOS %@ or later to use the most recent version of DuckDuckGo. You can also keep using your current version of the browser, but it will not receive further updates." } } } @@ -8082,6 +8090,7 @@ } }, "preferences.default-browser.button.make-default" : { + "comment" : "represents a prompt message asking the user to make DuckDuckGo their default browser.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -8188,18 +8197,6 @@ } } }, - "preferences.subscription" : { - "comment" : "Show subscription preferences", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Privacy Pro" - } - } - } - }, "preferences.sync" : { "comment" : "Show sync preferences", "extractionState" : "extracted_with_value", @@ -8312,6 +8309,7 @@ "comment" : "Main Menu History item" }, "reopen.last.closed.tab" : { + "comment" : "This string represents an action to reopen the last closed tab in the browser", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -8323,6 +8321,7 @@ } }, "reopen.last.closed.window" : { + "comment" : "This string represents an action to reopen the last closed window in the browser", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -8358,6 +8357,7 @@ } }, "restart.bitwarden.info" : { + "comment" : "This string represents a message informing the user that Bitwarden is not responding and prompts them to restart the application to initiate communication again.", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -8692,54 +8692,6 @@ } } }, - "subscription.menu.item" : { - "comment" : "Title for Subscription item in the options menu", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Privacy Pro" - } - } - } - }, - "subscription.progress.view.completing.purchase" : { - "comment" : "Progress view title when completing the purchase", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Completing purchase..." - } - } - } - }, - "subscription.progress.view.purchasing.subscription" : { - "comment" : "Progress view title when starting the purchase", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Purchase in progress..." - } - } - } - }, - "subscription.progress.view.restoring.subscription" : { - "comment" : "Progress view title when restoring past subscription purchase", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Restoring subscription..." - } - } - } - }, "tab.bookmarks.title" : { "comment" : "Tab bookmarks title", "extractionState" : "extracted_with_value", @@ -8759,7 +8711,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Oops!" + "value" : "Failed to open page" } } } @@ -8948,7 +8900,7 @@ } }, "tooltip.clearHistory" : { - "comment" : "Tooltip for burn button", + "comment" : "Tooltip for burn button where %@ is the domain", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -8960,7 +8912,7 @@ } }, "tooltip.clearHistoryAndData" : { - "comment" : "Tooltip for burn button", + "comment" : "Tooltip for burn button where %@ is the domain", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -9160,6 +9112,18 @@ } } }, + "unmute.tab" : { + "comment" : "Menu item. Unmute tab", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Unmute Tab" + } + } + } + }, "unpin.tab" : { "comment" : "Menu item. Unpin as a verb", "extractionState" : "extracted_with_value", @@ -9173,6 +9137,7 @@ } }, "unsupported.device.info.alert.header" : { + "comment" : "his string represents the header for an alert informing the user that their version of macOS is no longer supported", "extractionState" : "extracted_with_value", "localizations" : { "en" : { diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 9b7c307eea..843a190735 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -65,7 +65,7 @@ final class MainViewController: NSViewController { tabBarViewController = TabBarViewController.create(tabCollectionViewModel: tabCollectionViewModel) navigationBarViewController = NavigationBarViewController.create(tabCollectionViewModel: tabCollectionViewModel, isBurner: isBurner) - browserTabViewController = BrowserTabViewController.create(tabCollectionViewModel: tabCollectionViewModel) + browserTabViewController = BrowserTabViewController(tabCollectionViewModel: tabCollectionViewModel, bookmarkManager: bookmarkManager) findInPageViewController = FindInPageViewController.create() fireViewController = FireViewController.create(tabCollectionViewModel: tabCollectionViewModel) bookmarksBarViewController = BookmarksBarViewController.create(tabCollectionViewModel: tabCollectionViewModel, bookmarkManager: bookmarkManager) @@ -209,6 +209,14 @@ final class MainViewController: NSViewController { tabBarViewController.hideTabPreview() } + func windowWillMiniaturize() { + tabBarViewController.hideTabPreview() + } + + func windowWillEnterFullScreen() { + tabBarViewController.hideTabPreview() + } + func toggleBookmarksBarVisibility() { updateBookmarksBarViewVisibility(visible: !(mainView.bookmarksBarHeightConstraint.constant > 0)) } diff --git a/DuckDuckGo/MainWindow/MainWindowController.swift b/DuckDuckGo/MainWindow/MainWindowController.swift index d37dfab18a..06b35517fe 100644 --- a/DuckDuckGo/MainWindow/MainWindowController.swift +++ b/DuckDuckGo/MainWindow/MainWindowController.swift @@ -203,6 +203,11 @@ extension MainWindowController: NSWindowDelegate { func windowWillEnterFullScreen(_ notification: Notification) { mainViewController.tabBarViewController.draggingSpace.isHidden = true + mainViewController.windowWillEnterFullScreen() + } + + func windowWillMiniaturize(_ notification: Notification) { + mainViewController.windowWillMiniaturize() } func windowDidEnterFullScreen(_ notification: Notification) { diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 584481ac9f..7cf629775f 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -96,6 +96,7 @@ import SubscriptionUI private var loggingMenu: NSMenu? let customConfigurationUrlMenuItem = NSMenuItem(title: "Last Update Time", action: nil) let configurationDateAndTimeMenuItem = NSMenuItem(title: "Configuration URL", action: nil) + let autofillDebugScriptMenuItem = NSMenuItem(title: "Autofill Debug Script", action: #selector(MainMenu.toggleAutofillScriptDebugSettingsAction)) // MARK: - Help @@ -385,6 +386,7 @@ import SubscriptionUI updateLoggingMenuItems() updateInternalUserItem() updateRemoteConfigurationInfo() + updateAutofillDebugScriptMenuItem() } // MARK: - Bookmarks @@ -614,6 +616,9 @@ import SubscriptionUI menu.addItem(menuItem) } + menu.addItem(autofillDebugScriptMenuItem + .targetting(self)) + menu.addItem(.separator()) let debugLoggingMenuItem = NSMenuItem(title: OSLog.isRunningInDebugEnvironment ? "Disable DEBUG level logging…" : "Enable DEBUG level logging…", action: #selector(debugLoggingMenuItemAction), target: self) menu.addItem(debugLoggingMenuItem) @@ -642,6 +647,10 @@ import SubscriptionUI } } + private func updateAutofillDebugScriptMenuItem() { + autofillDebugScriptMenuItem.state = AutofillPreferences().debugScriptEnabled ? .on : .off + } + private func updateRemoteConfigurationInfo() { var dateString: String if let date = ConfigurationManager.shared.lastConfigurationInstallDate { @@ -672,6 +681,12 @@ import SubscriptionUI OSLog.loggingCategories = [] } + @objc private func toggleAutofillScriptDebugSettingsAction(_ sender: NSMenuItem) { + AutofillPreferences().debugScriptEnabled = !AutofillPreferences().debugScriptEnabled + NotificationCenter.default.post(name: .autofillScriptDebugSettingsDidChange, object: nil) + updateAutofillDebugScriptMenuItem() + } + @objc private func debugLoggingMenuItemAction(_ sender: NSMenuItem) { #if APPSTORE if !OSLog.isRunningInDebugEnvironment { diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index 702fd0aa8b..b5846ee9ce 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -282,12 +282,18 @@ final class AddressBarButtonsViewController: NSViewController { guard view.window?.isPopUpWindow == false else { return } bookmarkButton.setAccessibilityIdentifier("Bookmarks Button") let hasEmptyAddressBar = textFieldValue?.isEmpty ?? true - var isUrlBookmarked = false - if let url = tabCollectionViewModel.selectedTabViewModel?.tab.content.url, - bookmarkManager.isUrlBookmarked(url: url) { - isUrlBookmarked = true + var showBookmarkButton: Bool { + guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel, + selectedTabViewModel.canBeBookmarked else { return false } + + var isUrlBookmarked = false + if let url = selectedTabViewModel.tab.content.url, + bookmarkManager.isUrlBookmarked(url: url) { + isUrlBookmarked = true + } + + return clearButton.isHidden && !hasEmptyAddressBar && (isMouseOverNavigationBar || bookmarkPopover?.isShown == true || isUrlBookmarked) } - let showBookmarkButton = clearButton.isHidden && !hasEmptyAddressBar && (isMouseOverNavigationBar || bookmarkPopover?.isShown == true || isUrlBookmarked) bookmarkButton.isHidden = !showBookmarkButton } @@ -612,15 +618,18 @@ final class AddressBarButtonsViewController: NSViewController { guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { updateBookmarkButtonImage() - updateBookmarkButtonVisibility() + updateButtons() return } - urlCancellable = selectedTabViewModel.tab.$content.receive(on: DispatchQueue.main).sink { [weak self] _ in - self?.stopAnimations() - self?.updateBookmarkButtonImage() - self?.updateBookmarkButtonVisibility() - } + urlCancellable = selectedTabViewModel.tab.$content + .combineLatest(selectedTabViewModel.tab.$error) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.stopAnimations() + self?.updateBookmarkButtonImage() + self?.updateButtons() + } } private func subscribeToPermissions() { @@ -690,8 +699,8 @@ final class AddressBarButtonsViewController: NSViewController { private func updatePermissionButtons() { permissionButtons.isHidden = isTextFieldEditorFirstResponder - || isAnyTrackerAnimationPlaying - || (tabCollectionViewModel.selectedTabViewModel?.errorViewState.isVisible ?? true) + || isAnyTrackerAnimationPlaying + || (tabCollectionViewModel.selectedTabViewModel?.isShowingErrorPage ?? true) defer { showOrHidePermissionPopoverIfNeeded() } @@ -750,6 +759,8 @@ final class AddressBarButtonsViewController: NSViewController { // Image button switch controllerMode { + case .browsing where selectedTabViewModel.isShowingErrorPage: + imageButton.image = Self.webImage case .browsing: imageButton.image = selectedTabViewModel.favicon case .editing(isUrl: true): @@ -774,12 +785,12 @@ final class AddressBarButtonsViewController: NSViewController { let isLocalUrl = selectedTabViewModel.tab.content.url?.isLocalURL ?? false // Privacy entry point button - privacyEntryPointButton.isHidden = isEditingMode || - isTextFieldEditorFirstResponder || - !isHypertextUrl || - selectedTabViewModel.errorViewState.isVisible || - isTextFieldValueText || - isLocalUrl + privacyEntryPointButton.isHidden = isEditingMode + || isTextFieldEditorFirstResponder + || !isHypertextUrl + || selectedTabViewModel.isShowingErrorPage + || isTextFieldValueText + || isLocalUrl imageButtonWrapper.isHidden = view.window?.isPopUpWindow == true || !privacyEntryPointButton.isHidden || isAnyTrackerAnimationPlaying diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index 24d7ebe5fc..e60a26b90f 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -257,12 +257,16 @@ final class AddressBarViewController: NSViewController { } .store(in: &tabViewModelCancellables) - selectedTabViewModel.$isLoading - .sink { [weak self] isLoading in + selectedTabViewModel.$isLoading.combineLatest(selectedTabViewModel.tab.$error) + .debounce(for: 0.1, scheduler: RunLoop.main) + .sink { [weak self] isLoading, error in guard let progressIndicator = self?.progressIndicator else { return } if isLoading, - selectedTabViewModel.tab.content.url?.isDuckDuckGoSearch == false { + let url = selectedTabViewModel.tab.content.url, + [.http, .https].contains(url.navigationalScheme), + url.isDuckDuckGoSearch == false, + error == nil { progressIndicator.show(progress: selectedTabViewModel.progress, startTime: selectedTabViewModel.loadingStartTime) diff --git a/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift b/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift index 55b81c2954..ebf24cfbfb 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift @@ -44,22 +44,21 @@ extension NavigationButtonMenuDelegate: NSMenuDelegate { let listItems = listItems // Don't show menu if there is just the current item - if listItems.items.count == 0 || (listItems.items.count == 1 && listItems.currentIndex == 0) { return 0 } + if listItems.count == 1 { return 0 } - return listItems.items.count + return listItems.count } func menu(_ menu: NSMenu, update item: NSMenuItem, at index: Int, shouldCancel: Bool) -> Bool { - let (listItems, currentIndex) = self.listItems guard let listItem = listItems[safe: index] else { os_log("%s: Index out of bounds", type: .error, className) return true } - let listItemViewModel = WKBackForwardListItemViewModel(backForwardListItem: listItem, - faviconManagement: FaviconManager.shared, - historyCoordinating: HistoryCoordinator.shared, - isCurrentItem: index == currentIndex) + let listItemViewModel = BackForwardListItemViewModel(backForwardListItem: listItem, + faviconManagement: FaviconManager.shared, + historyCoordinating: HistoryCoordinator.shared, + isCurrentItem: index == 0) item.title = listItemViewModel.title item.image = listItemViewModel.image @@ -74,67 +73,25 @@ extension NavigationButtonMenuDelegate: NSMenuDelegate { @MainActor @objc func menuItemAction(_ sender: NSMenuItem) { let index = sender.tag - let (listItems, currentIndex) = self.listItems guard let listItem = listItems[safe: index] else { os_log("%s: Index out of bounds", type: .error, className) return } - - guard currentIndex != index else { - // current item selected: do nothing - return - } - guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { - os_log("%s: Selected tab view model is nil", type: .error, className) - return - } - - switch listItem { - case .backForwardListItem(let wkListItem): - selectedTabViewModel.tab.go(to: wkListItem) - case .goBackToCloseItem(parentTab:): - tabCollectionViewModel.selectedTabViewModel?.tab.goBack() - case .error: - break - } + tabCollectionViewModel.selectedTabViewModel?.tab.go(to: listItem) } - private var listItems: (items: [BackForwardListItem], currentIndex: Int?) { + private var listItems: [BackForwardListItem] { guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { - os_log("%s: Selected tab view model is nil", type: .error, className) - return ([], nil) - } - - let backForwardList = selectedTabViewModel.tab.webView.backForwardList - let wkList = buttonType == .back ? backForwardList.backList.reversed() : backForwardList.forwardList - var list = wkList.map { BackForwardListItem.backForwardListItem($0) } - var currentIndex: Int? - - // Add closing with back button to the list - if list.count == 0, - let parentTab = selectedTabViewModel.tab.parentTab, - buttonType == .back { - list.insert(.goBackToCloseItem(parentTab: parentTab), at: 0) + assertionFailure("Selected tab view model is nil") + return [] } + guard let currentItem = selectedTabViewModel.tab.currentHistoryItem else { return [] } - // Add current item to the list - if let currentItem = selectedTabViewModel.tab.webView.backForwardList.currentItem { - list.insert(.backForwardListItem(currentItem), at: 0) - currentIndex = 0 - } - - // Add error to the list - if selectedTabViewModel.tab.error != nil { - if buttonType == .back { - list.insert(.error, at: 0) - currentIndex = 0 - } else { - list = [] - currentIndex = nil - } - } + let list = [currentItem] + (buttonType == .back + ? selectedTabViewModel.tab.backHistoryItems.reversed() + : selectedTabViewModel.tab.forwardHistoryItems) - return (list, currentIndex) + return list } } diff --git a/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItem.swift b/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItem.swift index af0c0289de..51fbfe4e81 100644 --- a/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItem.swift +++ b/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItem.swift @@ -16,22 +16,40 @@ // limitations under the License. // +import Navigation import WebKit -enum BackForwardListItem: Equatable { - case backForwardListItem(WKBackForwardListItem) - case goBackToCloseItem(parentTab: Tab) - case error +struct BackForwardListItem: Hashable { + + enum Kind: Hashable { + case url(URL) + case goBackToClose(URL?) + } + let kind: Kind + let title: String? + let identity: HistoryItemIdentity? var url: URL? { - switch self { - case .backForwardListItem(let item): - return item.url - case .goBackToCloseItem(parentTab: let tab): - return tab.content.url - case .error: - return nil + switch kind { + case .url(let url): return url + case .goBackToClose(let url): return url } } + init(kind: Kind, title: String?, identity: HistoryItemIdentity?) { + self.kind = kind + self.title = title + self.identity = identity + } + + init(_ wkItem: WKBackForwardListItem) { + self.init(kind: .url(wkItem.url), title: wkItem.tabTitle ?? wkItem.title, identity: wkItem.identity) + } + +} + +extension [BackForwardListItem] { + init(_ items: [WKBackForwardListItem]) { + self = items.map(BackForwardListItem.init) + } } diff --git a/DuckDuckGo/NavigationBar/ViewModel/WKBackForwardListItemViewModel.swift b/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItemViewModel.swift similarity index 65% rename from DuckDuckGo/NavigationBar/ViewModel/WKBackForwardListItemViewModel.swift rename to DuckDuckGo/NavigationBar/ViewModel/BackForwardListItemViewModel.swift index 3eae6df4ae..2f3358c167 100644 --- a/DuckDuckGo/NavigationBar/ViewModel/WKBackForwardListItemViewModel.swift +++ b/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItemViewModel.swift @@ -1,5 +1,5 @@ // -// WKBackForwardListItemViewModel.swift +// BackForwardListItemViewModel.swift // // Copyright © 2020 DuckDuckGo. All rights reserved. // @@ -17,9 +17,8 @@ // import Cocoa -import WebKit -final class WKBackForwardListItemViewModel { +final class BackForwardListItemViewModel { private let backForwardListItem: BackForwardListItem private let faviconManagement: FaviconManagement @@ -37,42 +36,33 @@ final class WKBackForwardListItemViewModel { } var title: String { - switch backForwardListItem { - case .backForwardListItem(let item): - if item.url == .newtab { + switch backForwardListItem.kind { + case .url(let url): + if url == .newtab { return UserText.tabHomeTitle } - var title = item.title + var title = backForwardListItem.title if title == nil || (title?.isEmpty ?? false) { - title = historyCoordinating.title(for: item.url) + title = historyCoordinating.title(for: url) } - return title ?? - item.url.host ?? - item.url.absoluteString + return (title ?? url.host ?? url.absoluteString).truncated(length: MainMenu.Constants.maxTitleLength) - case .goBackToCloseItem(parentTab: let tab): - if let title = tab.title, - !title.isEmpty { - return String(format: UserText.closeAndReturnToParentFormat, title) + case .goBackToClose(let url): + if let title = backForwardListItem.title ?? url?.absoluteString, !title.isEmpty { + return String(format: UserText.closeAndReturnToParentFormat, title.truncated(length: MainMenu.Constants.maxTitleLength)) } else { return UserText.closeAndReturnToParent } - case .error: - return UserText.tabErrorTitle } } @MainActor(unsafe) var image: NSImage? { - if case .error = backForwardListItem { - return nil - } - if backForwardListItem.url == .newtab { - return NSImage(named: "HomeFavicon") + return .homeFavicon } if backForwardListItem.url?.isDuckPlayer == true { @@ -85,23 +75,11 @@ final class WKBackForwardListItemViewModel { return image } - return NSImage(named: "DefaultFavicon") + return .globeMulticolor16 } var state: NSControl.StateValue { - if case .goBackToCloseItem = backForwardListItem { - return .off - } - - return isCurrentItem ? .on : .off - } - - var isGoBackToCloseItem: Bool { - if case .goBackToCloseItem = backForwardListItem { - return true - } - - return false + isCurrentItem ? .on : .off } } diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift new file mode 100644 index 0000000000..169f0ceb50 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift @@ -0,0 +1,65 @@ +// +// Bundle+VPN.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NetworkProtection + +extension Bundle { + + private enum VPNInfoKey: String { + case tunnelExtensionBundleID = "TUNNEL_EXTENSION_BUNDLE_ID" + case proxyExtensionBundleID = "PROXY_EXTENSION_BUNDLE_ID" + } + + static var tunnelExtensionBundleID: String { + string(for: .tunnelExtensionBundleID) + } + + static var proxyExtensionBundleID: String { + string(for: .proxyExtensionBundleID) + } + + private static func string(for key: VPNInfoKey) -> String { + guard let bundleID = Bundle.main.object(forInfoDictionaryKey: key.rawValue) as? String else { + fatalError("Info.plist is missing \(key)") + } + + return bundleID + } + +#if !NETWORK_EXTENSION + // for the Main or Launcher Agent app + static func mainAppBundle() -> Bundle { + return Bundle.main + } +#elseif NETP_SYSTEM_EXTENSION + // for the System Extension (Developer ID) + static func mainAppBundle() -> Bundle { + return Bundle(url: .mainAppBundleURL)! + } + // AppEx (App Store) can‘t access Main App Bundle +#endif + + static let keychainType: KeychainType = { +#if NETP_SYSTEM_EXTENSION + .system +#else + .dataProtection(.named(Bundle.main.appGroup(bundle: .netP))) +#endif + }() +} diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionBundle.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionBundle.swift deleted file mode 100644 index e14b7f1e84..0000000000 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionBundle.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// NetworkProtectionBundle.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import NetworkProtection - -enum NetworkProtectionBundle { - -#if !NETWORK_EXTENSION - // for the Main or Launcher Agent app - static func mainAppBundle() -> Bundle { - return Bundle.main - } -#elseif NETP_SYSTEM_EXTENSION - // for the System Extension (Developer ID) - static func mainAppBundle() -> Bundle { - return Bundle(url: .mainAppBundleURL)! - } - // AppEx (App Store) can‘t access Main App Bundle -#endif - - static func extensionBundle() -> Bundle { -#if NETWORK_EXTENSION // When this code is compiled for any network-extension - return Bundle.main -#elseif NETP_SYSTEM_EXTENSION // When this code is compiled for the app when configured to use the sysex - let extensionsDirectoryURL = URL(fileURLWithPath: "Contents/Library/SystemExtensions", relativeTo: Bundle.main.bundleURL) - return extensionBundle(at: extensionsDirectoryURL) -#else // When this code is compiled for the app when configured to use the appex - let extensionsDirectoryURL = URL(fileURLWithPath: "Contents/Plugins", relativeTo: Bundle.main.bundleURL) - return extensionBundle(at: extensionsDirectoryURL) -#endif - } - - static func extensionBundle(at url: URL) -> Bundle { - let extensionURLs: [URL] - do { - extensionURLs = try FileManager.default.contentsOfDirectory(at: url, - includingPropertiesForKeys: nil, - options: .skipsHiddenFiles) - } catch let error { - fatalError("🔵 Failed to get the contents of \(url.absoluteString): \(error.localizedDescription)") - } - - // This should be updated to work well with other extensions - guard let extensionURL = extensionURLs.first else { - fatalError("🔵 Failed to find any system extensions") - } - - guard let extensionBundle = Bundle(url: extensionURL) else { - fatalError("🔵 Failed to create a bundle with URL \(extensionURL.absoluteString)") - } - - return extensionBundle - } - - static let keychainType: KeychainType = { -#if NETP_SYSTEM_EXTENSION - .system -#else - .dataProtection(.named(Bundle.main.appGroup(bundle: .netP))) -#endif - }() -} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift index b0ead6b8be..3e02fb49ed 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift @@ -58,7 +58,7 @@ final class NetworkProtectionAppEvents { let loginItemsManager = LoginItemsManager() Task { @MainActor in - guard featureVisibility.isNetworkProtectionVisible() else { + if featureVisibility.shouldUninstallAutomatically() { featureVisibility.disableForAllUsers() return } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index e1696a7aba..f54236f387 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -22,6 +22,7 @@ import AppKit import Common import Foundation import NetworkProtection +import NetworkProtectionProxy import SwiftUI /// Controller for the Network Protection debug menu. @@ -29,6 +30,10 @@ import SwiftUI @MainActor final class NetworkProtectionDebugMenu: NSMenu { + private let transparentProxySettings = TransparentProxySettings(defaults: .netP) + + // MARK: - Menus + private let environmentMenu = NSMenu() private let preferredServerMenu: NSMenu @@ -39,7 +44,9 @@ final class NetworkProtectionDebugMenu: NSMenu { private let resetToDefaults = NSMenuItem(title: "Reset Settings to defaults", action: #selector(NetworkProtectionDebugMenu.resetSettings)) - private let exclusionsMenu = NSMenu() + private let excludedRoutesMenu = NSMenu() + private let excludeDDGBrowserTrafficFromVPN = NSMenuItem(title: "DDG Browser", action: #selector(toggleExcludeDDGBrowser)) + private let excludeDBPTrafficFromVPN = NSMenuItem(title: "DBP Background Agent", action: #selector(toggleExcludeDBPBackgroundAgent)) private let shouldEnforceRoutesMenuItem = NSMenuItem(title: "Kill Switch (enforceRoutes)", action: #selector(NetworkProtectionDebugMenu.toggleEnforceRoutesAction)) private let shouldIncludeAllNetworksMenuItem = NSMenuItem(title: "includeAllNetworks", action: #selector(NetworkProtectionDebugMenu.toggleIncludeAllNetworks)) @@ -89,7 +96,6 @@ final class NetworkProtectionDebugMenu: NSMenu { .targetting(self) shouldEnforceRoutesMenuItem .targetting(self) - NSMenuItem(title: "Excluded Routes").submenu(exclusionsMenu) NSMenuItem.separator() NSMenuItem(title: "Send Test Notification", action: #selector(NetworkProtectionDebugMenu.sendTestNotification)) @@ -104,6 +110,14 @@ final class NetworkProtectionDebugMenu: NSMenu { NSMenuItem(title: "Environment") .submenu(environmentMenu) + NSMenuItem(title: "Exclusions") { + NSMenuItem(title: "Excluded Apps") { + excludeDDGBrowserTrafficFromVPN.targetting(self) + excludeDBPTrafficFromVPN.targetting(self) + } + NSMenuItem(title: "Excluded Routes").submenu(excludedRoutesMenu) + } + NSMenuItem(title: "Preferred Server").submenu(preferredServerMenu) NSMenuItem(title: "Registration Key") { @@ -172,8 +186,8 @@ final class NetworkProtectionDebugMenu: NSMenu { populateNetworkProtectionServerListMenuItems() populateNetworkProtectionRegistrationKeyValidityMenuItems() - exclusionsMenu.delegate = self - exclusionsMenu.autoenablesItems = false + excludedRoutesMenu.delegate = self + excludedRoutesMenu.autoenablesItems = false populateExclusionsMenuItems() } @@ -391,7 +405,7 @@ final class NetworkProtectionDebugMenu: NSMenu { } private func populateExclusionsMenuItems() { - exclusionsMenu.removeAllItems() + excludedRoutesMenu.removeAllItems() for item in settings.excludedRoutes { let menuItem: NSMenuItem @@ -406,7 +420,7 @@ final class NetworkProtectionDebugMenu: NSMenu { target: self, representedObject: range.stringRepresentation) } - exclusionsMenu.addItem(menuItem) + excludedRoutesMenu.addItem(menuItem) } // Only allow testers to enter a custom code if they're on the waitlist, to simulate the correct path through the flow @@ -419,6 +433,7 @@ final class NetworkProtectionDebugMenu: NSMenu { override func update() { updateEnvironmentMenu() + updateExclusionsMenu() updatePreferredServerMenu() updateRekeyValidityMenu() updateNetworkProtectionMenuItemsState() @@ -588,6 +603,7 @@ final class NetworkProtectionDebugMenu: NSMenu { } // MARK: Environment + @objc func setSelectedEnvironment(_ menuItem: NSMenuItem) { let title = menuItem.title let selectedEnvironment: VPNSettings.SelectedEnvironment @@ -608,6 +624,24 @@ final class NetworkProtectionDebugMenu: NSMenu { settings.selectedServer = .automatic } } + + // MARK: - Exclusions + + private let dbpBackgroundAppIdentifier = Bundle.main.dbpBackgroundAgentBundleId + private let ddgBrowserAppIdentifier = Bundle.main.bundleIdentifier! + + private func updateExclusionsMenu() { + excludeDBPTrafficFromVPN.state = transparentProxySettings.isExcluding(dbpBackgroundAppIdentifier) ? .on : .off + excludeDDGBrowserTrafficFromVPN.state = transparentProxySettings.isExcluding(ddgBrowserAppIdentifier) ? .on : .off + } + + @objc private func toggleExcludeDBPBackgroundAgent() { + transparentProxySettings.toggleExclusion(for: dbpBackgroundAppIdentifier) + } + + @objc private func toggleExcludeDDGBrowser() { + transparentProxySettings.toggleExclusion(for: ddgBrowserAppIdentifier) + } } extension NetworkProtectionDebugMenu: NSMenuDelegate { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index f67223a545..0bd4975196 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -24,6 +24,7 @@ import SwiftUI import Common import NetworkExtension import NetworkProtection +import NetworkProtectionProxy import NetworkProtectionUI import Networking import PixelKit @@ -38,6 +39,8 @@ typealias NetworkProtectionConfigChangeHandler = () -> Void final class NetworkProtectionTunnelController: TunnelController, TunnelSessionProvider { + // MARK: - Settings + let settings: VPNSettings // MARK: - Combine Cancellables @@ -60,6 +63,8 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// private let controllerErrorStore = NetworkProtectionControllerErrorStore() + private let notificationCenter: NotificationCenter + // MARK: - VPN Tunnel & Configuration /// Auth token store @@ -95,6 +100,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// Loads the configuration matching our ``extensionID``. /// + @MainActor public var manager: NETunnelProviderManager? { get async { if let internalManager { @@ -139,13 +145,14 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr init(networkExtensionBundleID: String, networkExtensionController: NetworkExtensionController, settings: VPNSettings, - notificationCenter: NotificationCenter = .default, tokenStore: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), + notificationCenter: NotificationCenter = .default, logger: NetworkProtectionLogger = DefaultNetworkProtectionLogger()) { self.logger = logger self.networkExtensionBundleID = networkExtensionBundleID self.networkExtensionController = networkExtensionController + self.notificationCenter = notificationCenter self.settings = settings self.tokenStore = tokenStore @@ -254,7 +261,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr tunnelManager.protocolConfiguration = { let protocolConfiguration = tunnelManager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol() protocolConfiguration.serverAddress = "127.0.0.1" // Dummy address... the NetP service will take care of grabbing a real server - protocolConfiguration.providerBundleIdentifier = NetworkProtectionBundle.extensionBundle().bundleIdentifier + protocolConfiguration.providerBundleIdentifier = Bundle.tunnelExtensionBundleID protocolConfiguration.providerConfiguration = [ NetworkProtectionOptionKey.defaultPixelHeaders: APIRequest.Headers().httpHeaders, NetworkProtectionOptionKey.includedRoutes: includedRoutes().map(\.stringRepresentation) as NSArray @@ -304,6 +311,14 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr } } + // MARK: - Connection + + public var status: NEVPNStatus { + get async { + await connection?.status ?? .disconnected + } + } + // MARK: - Connection Status Querying /// Queries Network Protection to know if its VPN is connected. diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index 5770b78a2f..3a3a392736 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -224,7 +224,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { let tunnelHealthStore = NetworkProtectionTunnelHealthStore(notificationCenter: notificationCenter) let controllerErrorStore = NetworkProtectionTunnelErrorStore(notificationCenter: notificationCenter) let debugEvents = Self.networkProtectionDebugEvents(controllerErrorStore: controllerErrorStore) - let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: NetworkProtectionBundle.keychainType, + let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: Bundle.keychainType, serviceName: Self.tokenServiceName, errorEvents: debugEvents) let notificationsPresenter = NetworkProtectionNotificationsPresenterFactory().make(settings: settings) @@ -232,7 +232,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { super.init(notificationsPresenter: notificationsPresenter, tunnelHealthStore: tunnelHealthStore, controllerErrorStore: controllerErrorStore, - keychainType: NetworkProtectionBundle.keychainType, + keychainType: Bundle.keychainType, tokenStore: tokenStore, debugEvents: debugEvents, providerEvents: Self.packetTunnelProviderEvents, @@ -323,13 +323,6 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { case missingPixelHeaders } - override func prepareToConnect(using provider: NETunnelProviderProtocol?) { - super.prepareToConnect(using: provider) - - guard PixelKit.shared == nil, let options = provider?.providerConfiguration else { return } - try? loadDefaultPixelHeaders(from: options) - } - public override func loadVendorOptions(from provider: NETunnelProviderProtocol?) throws { try super.loadVendorOptions(from: provider) @@ -350,6 +343,15 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { setupPixels(defaultHeaders: defaultPixelHeaders) } + // MARK: - Overrideable Connection Events + + override func prepareToConnect(using provider: NETunnelProviderProtocol?) { + super.prepareToConnect(using: provider) + + guard PixelKit.shared == nil, let options = provider?.providerConfiguration else { return } + try? loadDefaultPixelHeaders(from: options) + } + // MARK: - Start/Stop Tunnel override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift new file mode 100644 index 0000000000..d300309ec6 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift @@ -0,0 +1,94 @@ +// +// MacTransparentProxyProvider.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Common +import Foundation +import Networking +import NetworkExtension +import NetworkProtectionProxy +import os.log // swiftlint:disable:this enforce_os_log_wrapper +import PixelKit + +final class MacTransparentProxyProvider: TransparentProxyProvider { + + static var vpnProxyLogger = Logger(subsystem: OSLog.subsystem, category: "VPN Proxy") + + private var cancellables = Set() + + @objc init() { + let loadSettingsFromStartupOptions: Bool = { +#if NETP_SYSTEM_EXTENSION + true +#else + false +#endif + }() + + let settings: TransparentProxySettings = { +#if NETP_SYSTEM_EXTENSION + /// Because our System Extension is running in the system context and doesn't have access + /// to shared user defaults, we just make it use the `.standard` defaults. + TransparentProxySettings(defaults: .standard) +#else + /// Because our App Extension is running in the user context and has access + /// to shared user defaults, we take advantage of this and use the `.netP` defaults. + TransparentProxySettings(defaults: .netP) +#endif + }() + + let configuration = TransparentProxyProvider.Configuration( + loadSettingsFromProviderConfiguration: loadSettingsFromStartupOptions) + + super.init(settings: settings, + configuration: configuration, + logger: Self.vpnProxyLogger) + + eventHandler = eventHandler(_:) + +#if !NETP_SYSTEM_EXTENSION + let dryRun: Bool +#if DEBUG + dryRun = true +#else + dryRun = false +#endif + + PixelKit.setUp(dryRun: dryRun, + appVersion: AppVersion.shared.versionNumber, + source: "vpnProxyExtension", + defaultHeaders: [:], + log: .networkProtectionPixel, + defaults: .netP) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in + + let url = URL.pixelUrl(forPixelNamed: pixelName) + let apiHeaders = APIRequest.Headers(additionalHeaders: headers) + let configuration = APIRequest.Configuration(url: url, method: .get, queryParameters: parameters, headers: apiHeaders) + let request = APIRequest(configuration: configuration) + + request.fetch { _, error in + onComplete(error == nil, error) + } + } +#endif + } + + private func eventHandler(_ event: TransparentProxyProvider.Event) { + PixelKit.fire(event) + } +} diff --git a/DuckDuckGo/NetworkProtectionAppExtension.entitlements b/DuckDuckGo/NetworkProtectionAppExtension.entitlements index 13dd983ca1..d37610bb07 100644 --- a/DuckDuckGo/NetworkProtectionAppExtension.entitlements +++ b/DuckDuckGo/NetworkProtectionAppExtension.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.security.app-sandbox diff --git a/DuckDuckGo/PasswordManager/Bitwarden/Model/BWManager.swift b/DuckDuckGo/PasswordManager/Bitwarden/Model/BWManager.swift index 7e1255168d..24be234ef2 100644 --- a/DuckDuckGo/PasswordManager/Bitwarden/Model/BWManager.swift +++ b/DuckDuckGo/PasswordManager/Bitwarden/Model/BWManager.swift @@ -232,7 +232,6 @@ final class BWManager: BWManagement, ObservableObject { switch error { case "cannot-decrypt": logOrAssertionFailure("BWManagement: Bitwarden error - cannot decrypt") - Pixel.fire(.debug(event: .bitwardenRespondedCannotDecrypt)) if Pixel.Event.Repetition(key: "bitwardenRespondedCannotDecryptUnique", update: false) != .repetitive { Pixel.fire(.debug(event: .bitwardenRespondedCannotDecryptUnique())) diff --git a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift index 2c714304df..ee5c9f0c91 100644 --- a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift +++ b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift @@ -46,6 +46,7 @@ final class PinnedTabsViewModel: ObservableObject { didSet { if let selectedItem = selectedItem { selectedItemIndex = items.firstIndex(of: selectedItem) + updateTabAudioState(tab: selectedItem) } else { selectedItemIndex = nil } @@ -57,6 +58,7 @@ final class PinnedTabsViewModel: ObservableObject { didSet { if let hoveredItem = hoveredItem { hoveredItemIndex = items.firstIndex(of: hoveredItem) + updateTabAudioState(tab: hoveredItem) } else { hoveredItemIndex = nil } @@ -72,6 +74,7 @@ final class PinnedTabsViewModel: ObservableObject { @Published private(set) var selectedItemIndex: Int? @Published private(set) var hoveredItemIndex: Int? @Published private(set) var dragMovesWindow: Bool = true + @Published private(set) var audioStateView: AudioStateView = .notSupported @Published private(set) var itemsWithoutSeparator: Set = [] @@ -111,6 +114,18 @@ final class PinnedTabsViewModel: ObservableObject { } itemsWithoutSeparator = items } + + private func updateTabAudioState(tab: Tab) { + let audioState = tab.audioState + switch audioState { + case .muted: + audioStateView = .muted + case .unmuted: + audioStateView = .unmuted + case .notSupported: + audioStateView = .notSupported + } + } } // MARK: - Context Menu @@ -124,6 +139,13 @@ extension PinnedTabsViewModel { case fireproof(Tab) case removeFireproofing(Tab) case close(Int) + case muteOrUnmute(Tab) + } + + enum AudioStateView { + case muted + case unmuted + case notSupported } func isFireproof(_ tab: Tab) -> Bool { @@ -168,4 +190,9 @@ extension PinnedTabsViewModel { func removeFireproofing(_ tab: Tab) { contextMenuActionSubject.send(.removeFireproofing(tab)) } + + func muteOrUmute(_ tab: Tab) { + contextMenuActionSubject.send(.muteOrUnmute(tab)) + updateTabAudioState(tab: tab) + } } diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift index 148b0d5d82..278fdfa7ca 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift @@ -21,8 +21,8 @@ import SwiftUIExtensions struct PinnedTabView: View { enum Const { - static let dimension: CGFloat = 32 - static let cornerRadius: CGFloat = 6 + static let dimension: CGFloat = 34 + static let cornerRadius: CGFloat = 10 } @ObservedObject var model: Tab @@ -96,7 +96,17 @@ struct PinnedTabView: View { fireproofAction Divider() - + switch collectionModel.audioStateView { + case .muted, .unmuted: + let audioStateText = collectionModel.audioStateView == .muted ? UserText.unmuteTab : UserText.muteTab + Button(audioStateText) { [weak collectionModel, weak model] in + guard let model = model else { return } + collectionModel?.muteOrUmute(model) + } + Divider() + case .notSupported: + EmptyView() + } Button(UserText.closeTab) { [weak collectionModel, weak model] in guard let model = model else { return } collectionModel?.close(model) @@ -163,6 +173,7 @@ struct PinnedTabInnerView: View { var foregroundColor: Color var drawSeparator: Bool = true + @Environment(\.colorScheme) var colorScheme @EnvironmentObject var model: Tab @Environment(\.controlActiveState) private var controlActiveState @@ -187,11 +198,32 @@ struct PinnedTabInnerView: View { .frame(width: PinnedTabView.Const.dimension) } + @ViewBuilder + var mutedTabIndicator: some View { + switch model.audioState { + case .muted: + ZStack { + Circle() + .stroke(Color.gray.opacity(0.5), lineWidth: 0.5) + .background(Circle().foregroundColor(Color("PinnedTabMuteStateCircleColor"))) + .frame(width: 16, height: 16) + Image("Audio-Mute") + .resizable() + .renderingMode(.template) + .frame(width: 12, height: 12) + }.offset(x: 8, y: -8) + default: EmptyView() + } + } + @ViewBuilder var favicon: some View { if let favicon = model.favicon { - Image(nsImage: favicon) - .resizable() + ZStack(alignment: .topTrailing) { + Image(nsImage: favicon) + .resizable() + mutedTabIndicator + } } else if let domain = model.content.url?.host, let eTLDplus1 = ContentBlocking.shared.tld.eTLDplus1(domain), let firstLetter = eTLDplus1.capitalized.first.flatMap(String.init) { ZStack { Rectangle() @@ -199,11 +231,15 @@ struct PinnedTabInnerView: View { Text(firstLetter) .font(.caption) .foregroundColor(.white) + mutedTabIndicator } .cornerRadius(4.0) } else { - Image(nsImage: #imageLiteral(resourceName: "Web")) - .resizable() + ZStack { + Image(nsImage: #imageLiteral(resourceName: "Web")) + .resizable() + mutedTabIndicator + } } } } diff --git a/DuckDuckGo/Preferences/Model/AutofillPreferences.swift b/DuckDuckGo/Preferences/Model/AutofillPreferences.swift index fcd0244637..58a55e86f1 100644 --- a/DuckDuckGo/Preferences/Model/AutofillPreferences.swift +++ b/DuckDuckGo/Preferences/Model/AutofillPreferences.swift @@ -26,6 +26,7 @@ protocol AutofillPreferencesPersistor { var askToSavePaymentMethods: Bool { get set } var autolockLocksFormFilling: Bool { get set } var passwordManager: PasswordManager { get set } + var debugScriptEnabled: Bool { get set } } enum PasswordManager: String, CaseIterable { @@ -67,6 +68,7 @@ enum AutofillAutoLockThreshold: String, CaseIterable { extension NSNotification.Name { static let autofillAutoLockSettingsDidChange = NSNotification.Name("autofillAutoLockSettingsDidChange") static let autofillUserSettingsDidChange = NSNotification.Name("autofillUserSettingsDidChange") + static let autofillScriptDebugSettingsDidChange = NSNotification.Name("autofillScriptDebugSettingsDidChange") } final class AutofillPreferences: AutofillPreferencesPersistor { @@ -132,6 +134,21 @@ final class AutofillPreferences: AutofillPreferencesPersistor { @UserDefaultsWrapper(key: .selectedPasswordManager, defaultValue: PasswordManager.duckduckgo.rawValue) private var selectedPasswordManager: String + @UserDefaultsWrapper(key: .autofillDebugScriptEnabled, defaultValue: false) + private var debugScriptEnabledWrapped: Bool + + var debugScriptEnabled: Bool { + get { + return debugScriptEnabledWrapped + } + + set { + if debugScriptEnabledWrapped != newValue { + debugScriptEnabledWrapped = newValue + } + } + } + private var statisticsStore: StatisticsStore { return injectedDependencyStore ?? defaultDependencyStore } diff --git a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift index 30b4febb5f..96315f1cc0 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift @@ -131,26 +131,11 @@ extension Preferences { let narrowContentView = Text(combinedText) let wideContentView: some View = VStack(alignment: .leading, spacing: 0) { - HStack(alignment: .center, spacing: 0) { - Text(UserText.aboutUnsupportedDeviceInfo2Part1 + " ") - Button(action: { - NSWorkspace.shared.open(Self.softwareUpdateURL) - }) { - Text(UserText.aboutUnsupportedDeviceInfo2Part2(version: versionString) + " ") - .foregroundColor(Color.blue) - .underline() - } - .buttonStyle(PlainButtonStyle()) - .onHover { hovering in - if hovering { - NSCursor.pointingHand.set() - } else { - NSCursor.arrow.set() - } - } - Text(UserText.aboutUnsupportedDeviceInfo2Part3) + if #available(macOS 12.0, *) { + Text(aboutUnsupportedDeviceInfo2Attributed) + } else { + aboutUnsupportedDeviceInfo2DeprecatedView() } - Text(UserText.aboutUnsupportedDeviceInfo2Part4) } return HStack(alignment: .top) { @@ -169,6 +154,39 @@ extension Preferences { .cornerRadius(8) .frame(width: width, height: height) } - } + @available(macOS 12, *) + private var aboutUnsupportedDeviceInfo2Attributed: AttributedString { + let baseString = UserText.aboutUnsupportedDeviceInfo2(version: versionString) + var instructions = AttributedString(baseString) + if let range = instructions.range(of: "macOS \(versionString)") { + instructions[range].link = Self.softwareUpdateURL + } + return instructions + } + + @ViewBuilder + private func aboutUnsupportedDeviceInfo2DeprecatedView() -> some View { + HStack(alignment: .center, spacing: 0) { + Text(verbatim: UserText.aboutUnsupportedDeviceInfo2Part1 + " ") + Button(action: { + NSWorkspace.shared.open(Self.softwareUpdateURL) + }) { + Text(verbatim: UserText.aboutUnsupportedDeviceInfo2Part2(version: versionString) + " ") + .foregroundColor(Color.blue) + .underline() + } + .buttonStyle(PlainButtonStyle()) + .onHover { hovering in + if hovering { + NSCursor.pointingHand.set() + } else { + NSCursor.arrow.set() + } + } + Text(verbatim: UserText.aboutUnsupportedDeviceInfo2Part3) + } + Text(verbatim: UserText.aboutUnsupportedDeviceInfo2Part4) + } + } } diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 2c0deacf52..2e298e833d 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -40,6 +40,18 @@ enum Preferences { @ObservedObject var model: PreferencesSidebarModel +#if SUBSCRIPTION + var subscriptionModel: PreferencesSubscriptionModel? +#endif + + init(model: PreferencesSidebarModel) { + self.model = model + +#if SUBSCRIPTION + self.subscriptionModel = makeSubscriptionViewModel() +#endif + } + var body: some View { HStack(spacing: 0) { Sidebar().environmentObject(model).frame(width: Const.sidebarWidth) @@ -67,7 +79,7 @@ enum Preferences { #if SUBSCRIPTION case .subscription: - makeSubscriptionView() + SubscriptionUI.PreferencesSubscriptionView(model: subscriptionModel!) #endif case .autofill: AutofillView(model: AutofillPreferencesModel()) @@ -98,18 +110,19 @@ enum Preferences { } #if SUBSCRIPTION - private func makeSubscriptionView() -> some View { + private func makeSubscriptionViewModel() -> PreferencesSubscriptionModel { let openURL: (URL) -> Void = { url in - WindowControllersManager.shared.showTab(with: .subscription(url)) + DispatchQueue.main.async { + WindowControllersManager.shared.showTab(with: .subscription(url)) + } } let sheetActionHandler = SubscriptionAccessActionHandlers(restorePurchases: { SubscriptionPagesUseSubscriptionFeature.startAppStoreRestoreFlow() }, openURLHandler: openURL, goToSyncPreferences: { self.model.selectPane(.sync) }) - let model = PreferencesSubscriptionModel(openURLHandler: openURL, - sheetActionHandler: sheetActionHandler) - return SubscriptionUI.PreferencesSubscriptionView(model: model) + return PreferencesSubscriptionModel(openURLHandler: openURL, + sheetActionHandler: sheetActionHandler) } #endif } diff --git a/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift b/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift index 19d72e71d3..79dfdb9fa0 100644 --- a/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift +++ b/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift @@ -100,18 +100,7 @@ private extension NSMenuItem { return nil } - if recentlyClosedWindow.tabs.count > 1 { - let moreCount = recentlyClosedWindow.tabs.count - 1 - let titleSuffix: String - if moreCount == 1 { - titleSuffix = UserText.recentlyClosedMenuItemSuffixOne - } else { - titleSuffix = String(format: UserText.recentlyClosedMenuItemSuffixMultiple, moreCount) - } - - item.title = item.title.truncated(length: MainMenu.Constants.maxTitleLength - titleSuffix.count) - item.title += titleSuffix - } + item.title = String(format: UserText.recentlyClosedWindowMenuItem, recentlyClosedWindow.tabs.count) item.representedObject = recentlyClosedWindow return item } diff --git a/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift b/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift index 39f7872e27..c61f85672a 100644 --- a/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift +++ b/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift @@ -107,7 +107,7 @@ extension UserText { static func pmLockScreenDuration(duration: String) -> String { let localized = NSLocalizedString("pm.lock-screen.duration", value: "Your autofill info will remain unlocked until your computer is idle for %@.", - comment: "") + comment: "Message about the duration for which autofill information remains unlocked on the lock screen.") return String(format: localized, duration) } diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementIdentityItemView.swift b/DuckDuckGo/SecureVault/View/PasswordManagementIdentityItemView.swift index c82e1b5126..639c11a82f 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementIdentityItemView.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementIdentityItemView.swift @@ -106,7 +106,7 @@ private struct IdentificationView: View { EditableIdentityField(textFieldValue: $model.lastName, title: UserText.pmLastName) if model.isInEditMode { - Text("Birthday") + Text("Birthday", comment: "Title of the section of the Identities manager where the user can add/modify a date of birth") .bold() .padding(.bottom, 5) @@ -219,7 +219,7 @@ private struct AddressView: View { !model.addressPostalCode.isEmpty || !model.addressCountryCode.isEmpty || model.isInEditMode { - Text("Address") + Text("Address", comment: "Title of the section of the Identities manager where the user can add/modify an address (street city etc,)") .bold() .foregroundColor(.gray) .padding(.bottom, 20) @@ -232,7 +232,7 @@ private struct AddressView: View { EditableIdentityField(textFieldValue: $model.addressPostalCode, title: UserText.pmAddressPostalCode) if model.isInEditMode { - Text("Country") + Text("Country", comment: "Title of the section of the Identities manager where the user can add/modify a country (US,UK, Italy etc...)") .bold() .padding(.bottom, 5) @@ -252,7 +252,7 @@ private struct AddressView: View { .padding(.bottom, 5) } else if !model.addressCountryCode.isEmpty { - Text("Country") + Text("Country", comment: "Title of the section of the Identities manager where the user can add/modify a country (US,UK, Italy etc...)") .bold() .padding(.bottom, 5) @@ -273,7 +273,7 @@ private struct ContactInfoView: View { VStack(alignment: .leading, spacing: 0) { if !model.homePhone.isEmpty || !model.mobilePhone.isEmpty || !model.emailAddress.isEmpty || model.isInEditMode { - Text("Contact Info") + Text("Contact Info", comment: "Title of the section of the Identities manager where the user can add/modify contact info (phone, email address)") .bold() .foregroundColor(.gray) .padding(.bottom, 20) diff --git a/DuckDuckGo/Sharing/SharingMenu.swift b/DuckDuckGo/Sharing/SharingMenu.swift index c3a6c1ec12..50c5808e4a 100644 --- a/DuckDuckGo/Sharing/SharingMenu.swift +++ b/DuckDuckGo/Sharing/SharingMenu.swift @@ -48,6 +48,7 @@ final class SharingMenu: NSMenu { private func sharingData() -> SharingData? { guard let tabViewModel = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel.selectedTabViewModel, tabViewModel.canReload, + !tabViewModel.isShowingErrorPage, let url = tabViewModel.tab.content.url else { return nil } let sharingData = DuckPlayer.shared.sharingData(for: tabViewModel.title, url: url) ?? (tabViewModel.title, url) diff --git a/DuckDuckGo/Statistics/PixelEvent.swift b/DuckDuckGo/Statistics/PixelEvent.swift index e15e0c4066..c4de970179 100644 --- a/DuckDuckGo/Statistics/PixelEvent.swift +++ b/DuckDuckGo/Statistics/PixelEvent.swift @@ -288,7 +288,6 @@ extension Pixel { case removedInvalidBookmarkManagedObjects case bitwardenNotResponding - case bitwardenRespondedCannotDecrypt case bitwardenRespondedCannotDecryptUnique(repetition: Repetition = .init(key: "bitwardenRespondedCannotDecryptUnique")) case bitwardenHandshakeFailed case bitwardenDecryptionOfSharedKeyFailed @@ -734,8 +733,6 @@ extension Pixel.Event.Debug { case .bitwardenNotResponding: return "bitwarden_not_responding" - case .bitwardenRespondedCannotDecrypt: - return "bitwarden_responded_cannot_decrypt" case .bitwardenRespondedCannotDecryptUnique: return "bitwarden_responded_cannot_decrypt_unique" case .bitwardenHandshakeFailed: diff --git a/DuckDuckGo/Statistics/PixelParameters.swift b/DuckDuckGo/Statistics/PixelParameters.swift index f5aa69b6c2..60ff916e27 100644 --- a/DuckDuckGo/Statistics/PixelParameters.swift +++ b/DuckDuckGo/Statistics/PixelParameters.swift @@ -224,7 +224,6 @@ extension Pixel.Event.Debug { .webKitDidTerminate, .removedInvalidBookmarkManagedObjects, .bitwardenNotResponding, - .bitwardenRespondedCannotDecrypt, .bitwardenRespondedCannotDecryptUnique, .bitwardenHandshakeFailed, .bitwardenDecryptionOfSharedKeyFailed, diff --git a/DuckDuckGo/Tab/Model/Tab+Navigation.swift b/DuckDuckGo/Tab/Model/Tab+Navigation.swift index daaae97f59..3df38461d6 100644 --- a/DuckDuckGo/Tab/Model/Tab+Navigation.swift +++ b/DuckDuckGo/Tab/Model/Tab+Navigation.swift @@ -42,6 +42,10 @@ extension Tab: NavigationResponder { navigationDelegate.setResponders( .weak(nullable: self.navigationHotkeyHandler), + // redirect to SERP for non-valid domains entered by user + // should be before `self` to avoid Tab presenting an error screen + .weak(nullable: self.searchForNonexistentDomains), + .weak(self), // Duck Player overlay navigations handling @@ -65,9 +69,6 @@ extension Tab: NavigationResponder { // add extra headers to SERP requests .struct(SerpHeadersNavigationResponder()), - // redirect to SERP for non-valid domains entered by user - .weak(nullable: self.searchForNonexistentDomains), - // ensure Content Blocking Rules are applied before navigation .weak(nullable: self.contentBlockingAndSurrogates), // update click-to-load state diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 2a9eadf65b..31164547e9 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -440,6 +440,7 @@ protocol NewWindowPolicyDecisionMaker { isTabPinned: { tabGetter().map { tab in pinnedTabsManager.isTabPinned(tab) } ?? false }, isTabBurner: burnerMode.isBurner, contentPublisher: _content.projectedValue.eraseToAnyPublisher(), + setContent: { tabGetter()?.setContent($0) }, titlePublisher: _title.projectedValue.eraseToAnyPublisher(), userScriptsPublisher: userScriptsPublisher, inheritedAttribution: parentTab?.adClickAttribution?.currentAttributionState, @@ -487,6 +488,7 @@ protocol NewWindowPolicyDecisionMaker { } #endif + self.audioState = webView.audioState() addDeallocationChecks(for: webView) } @@ -598,7 +600,6 @@ protocol NewWindowPolicyDecisionMaker { let webViewDidReceiveRedirectPublisher = PassthroughSubject() let webViewDidCommitNavigationPublisher = PassthroughSubject() let webViewDidFinishNavigationPublisher = PassthroughSubject() - let webViewDidFailNavigationPublisher = PassthroughSubject() // MARK: - Properties @@ -616,7 +617,6 @@ protocol NewWindowPolicyDecisionMaker { webView.stopAllMedia(shouldStopLoading: false) } handleFavicon(oldValue: oldValue) - invalidateInteractionStateData() if navigationDelegate.currentNavigation == nil { updateCanGoBackForward(withCurrentNavigation: nil) } @@ -674,6 +674,12 @@ protocol NewWindowPolicyDecisionMaker { @Published var title: String? private func updateTitle() { + if let error { + if error.code != .webContentProcessTerminated { + self.title = nil + } + return + } var title = webView.title?.trimmingWhitespace() if title?.isEmpty ?? true { title = webView.url?.host?.droppingWwwPrefix() @@ -682,14 +688,16 @@ protocol NewWindowPolicyDecisionMaker { if title != self.title { self.title = title } + + if let wkBackForwardListItem = webView.backForwardList.currentItem, + content.urlForWebView == wkBackForwardListItem.url { + wkBackForwardListItem.tabTitle = title + } } @PublishedAfter var error: WKError? { didSet { - if error == nil || error?.isFrameLoadInterrupted == true || error?.isNavigationCancelled == true { - return - } - webView.stopAllMediaPlayback() + updateTitle() } } let permissions: PermissionModel @@ -765,6 +773,23 @@ protocol NewWindowPolicyDecisionMaker { @Published private(set) var canGoBack: Bool = false @Published private(set) var canReload: Bool = false + @MainActor + var backHistoryItems: [BackForwardListItem] { + [BackForwardListItem](webView.backForwardList.backList) + + (canBeClosedWithBack ? [BackForwardListItem(kind: .goBackToClose(parentTab?.url), title: parentTab?.title, identity: nil)] : []) + } + @MainActor + var currentHistoryItem: BackForwardListItem? { + webView.backForwardList.currentItem.map(BackForwardListItem.init) + ?? (content.url ?? navigationDelegate.currentNavigation?.url).map { url in + BackForwardListItem(kind: .url(url), title: webView.title ?? title, identity: nil) + } + } + @MainActor + var forwardHistoryItems: [BackForwardListItem] { + [BackForwardListItem](webView.backForwardList.forwardList) + } + private func updateCanGoBackForward() { updateCanGoBackForward(withCurrentNavigation: navigationDelegate.currentNavigation) } @@ -783,8 +808,8 @@ protocol NewWindowPolicyDecisionMaker { return } - let canGoBack = webView.canGoBack || self.error != nil - let canGoForward = webView.canGoForward && self.error == nil + let canGoBack = webView.canGoBack + let canGoForward = webView.canGoForward let canReload = self.content.userEditableUrl != nil if canGoBack != self.canGoBack { @@ -808,10 +833,6 @@ protocol NewWindowPolicyDecisionMaker { return nil } - guard error == nil else { - return webView.navigator()?.reload(withExpectedNavigationType: .reload) - } - userInteractionDialog = nil return webView.navigator()?.goBack(withExpectedNavigationType: .backForward(distance: -1)) } @@ -820,14 +841,62 @@ protocol NewWindowPolicyDecisionMaker { @discardableResult func goForward() -> ExpectedNavigation? { guard canGoForward else { return nil } + + userInteractionDialog = nil return webView.navigator()?.goForward(withExpectedNavigationType: .backForward(distance: 1)) } - func go(to item: WKBackForwardListItem) { - webView.go(to: item) + @MainActor + @discardableResult + func go(to item: BackForwardListItem) -> ExpectedNavigation? { + userInteractionDialog = nil + + switch item.kind { + case .goBackToClose: + delegate?.closeTab(self) + return nil + + case .url: break + } + + var backForwardNavigation: (distance: Int, item: WKBackForwardListItem)? { + guard let identity = item.identity else { return nil } + + let backForwardList = webView.backForwardList + if let backItem = backForwardList.backItem, backItem.identity == identity { + return (-1, backItem) + } else if let forwardItem = backForwardList.forwardItem, forwardItem.identity == identity { + return (1, forwardItem) + } else if backForwardList.currentItem?.identity == identity { + return nil + } + + let forwardList = backForwardList.forwardList + if let forwardIndex = forwardList.firstIndex(where: { $0.identity == identity }) { + return (forwardIndex + 1, forwardList[forwardIndex]) // going forward, adding 1 to zero based index + } + + let backList = backForwardList.backList + if let backIndex = backList.lastIndex(where: { $0.identity == identity }) { + return (-(backList.count - backIndex), backList[backIndex]) // item is in _reversed_ backList + } + + return nil + + } + + guard let backForwardNavigation else { + os_log(.error, "item `\(item.title ?? "") – \(item.url?.absoluteString ?? "")` is not in the backForwardList") + return nil + } + + return webView.navigator()?.go(to: backForwardNavigation.item, + withExpectedNavigationType: .backForward(distance: backForwardNavigation.distance)) } func openHomePage() { + userInteractionDialog = nil + if startupPreferences.launchToCustomHomePage, let customURL = URL(string: startupPreferences.formattedCustomHomePageURL) { webView.load(URLRequest(url: customURL)) @@ -837,27 +906,46 @@ protocol NewWindowPolicyDecisionMaker { } func startOnboarding() { - webView.load(URLRequest(url: .welcome)) + userInteractionDialog = nil + + setContent(.onboarding) } - func reload() { + @MainActor(unsafe) + @discardableResult + func reload() -> ExpectedNavigation? { userInteractionDialog = nil // In the case of an error only reload web URLs to prevent uxss attacks via redirecting to javascript:// - if let error = error, let failingUrl = error.failingUrl, failingUrl.isHttp || failingUrl.isHttps { - webView.load(URLRequest(url: failingUrl, cachePolicy: .reloadIgnoringLocalCacheData)) - return + if let error = error, + let failingUrl = error.failingUrl ?? content.urlForWebView, + failingUrl.isHttp || failingUrl.isHttps, + // navigate in-place to preserve back-forward history + // launch navigation using javascript: URL navigation to prevent WebView from + // interpreting the action as user-initiated link navigation causing a new tab opening when Cmd is pressed + let redirectUrl = URL(string: "javascript:location.replace('\(failingUrl.absoluteString.escapedJavaScriptString())')") { + + webView.load(URLRequest(url: redirectUrl)) + return nil } if webView.url == nil, content.isUrl { self.content = content.forceReload() // load from cache or interactionStateData when called by lazy loader - reloadIfNeeded(shouldLoadInBackground: true) + return reloadIfNeeded(shouldLoadInBackground: true) } else { - webView.reload() + return webView.navigator(distributedNavigationDelegate: navigationDelegate).reload(withExpectedNavigationType: .reload) } } + @Published private(set) var audioState: WKWebView.AudioState = .notSupported + + func muteUnmuteTab() { + webView.muteOrUnmute() + + audioState = webView.audioState() + } + @MainActor(unsafe) @discardableResult private func reloadIfNeeded(shouldLoadInBackground: Bool = false) -> ExpectedNavigation? { @@ -875,7 +963,11 @@ protocol NewWindowPolicyDecisionMaker { let forceReload = (url.absoluteString == content.userEnteredValue) ? shouldLoadInBackground : (source == .reload) if forceReload || shouldReload(url, shouldLoadInBackground: shouldLoadInBackground) { + if webView.url == url, webView.backForwardList.currentItem?.url == url, !webView.isLoading { + return reload() + } if restoreInteractionStateDataIfNeeded() { return nil /* session restored */ } + invalidateInteractionStateData() if url.isFileURL { return webView.navigator(distributedNavigationDelegate: navigationDelegate) @@ -886,7 +978,6 @@ protocol NewWindowPolicyDecisionMaker { if #available(macOS 12.0, *), content.isUserEnteredUrl { request.attribution = .user } - invalidateInteractionStateData() return webView.navigator(distributedNavigationDelegate: navigationDelegate) .load(request, withExpectedNavigationType: source.navigationType) @@ -900,7 +991,7 @@ protocol NewWindowPolicyDecisionMaker { guard url.isValid, webView.superview != nil || shouldLoadInBackground, // don‘t reload when already loaded - webView.url != url else { return false } + webView.url != url || error != nil else { return false } // if content not loaded inspect error switch error { @@ -984,9 +1075,13 @@ protocol NewWindowPolicyDecisionMaker { } }.store(in: &webViewCancellables) - webView.observe(\.url) { [weak self] _, _ in - self?.handleUrlDidChange() - }.store(in: &webViewCancellables) + webView.publisher(for: \.url) + .dropFirst() + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.handleUrlDidChange() + }.store(in: &webViewCancellables) + webView.observe(\.title) { [weak self] _, _ in self?.updateTitle() }.store(in: &webViewCancellables) @@ -1042,7 +1137,7 @@ protocol NewWindowPolicyDecisionMaker { @MainActor(unsafe) private func handleFavicon(oldValue: TabContent? = nil) { - guard content.isUrl, let url = content.urlForWebView else { + guard content.isUrl, let url = content.urlForWebView, error == nil else { favicon = nil return } @@ -1095,6 +1190,7 @@ extension Tab: FaviconUserScriptDelegate { func faviconUserScript(_ faviconUserScript: FaviconUserScript, didFindFaviconLinks faviconLinks: [FaviconUserScript.FaviconLink], for documentUrl: URL) { + guard documentUrl != .error else { return } faviconManagement.handleFaviconLinks(faviconLinks, documentUrl: documentUrl) { favicon in guard documentUrl == self.content.url, let favicon = favicon else { return @@ -1198,8 +1294,11 @@ extension Tab/*: NavigationResponder*/ { // to be moved to Tab+Navigation.swift userInteractionDialog = nil // Unnecessary assignment triggers publishing - if error != nil { error = nil } if lastWebError != nil { lastWebError = nil } + if error != nil, + navigation.navigationAction.navigationType != .alternateHtmlLoad { // error page navigation + error = nil + } invalidateInteractionStateData() } @@ -1219,12 +1318,20 @@ extension Tab/*: NavigationResponder*/ { // to be moved to Tab+Navigation.swift @MainActor func navigation(_ navigation: Navigation, didFailWith error: WKError) { - if navigation.isCurrent { + invalidateInteractionStateData() + + let url = error.failingUrl ?? navigation.url + if navigation.isCurrent, + !error.isFrameLoadInterrupted, !error.isNavigationCancelled, + // don‘t show an error page if the error was already handled + // (by SearchNonexistentDomainNavigationResponder) or another navigation was triggered by `setContent` + self.content.urlForWebView == url { + self.error = error + // when already displaying the error page and reload navigation fails again: don‘t navigate, just update page HTML + let shouldPerformAlternateNavigation = navigation.url != webView.url || navigation.navigationAction.targetFrame?.url != .error + loadErrorHTML(error, header: UserText.errorPageHeader, forUnreachableURL: url, alternate: shouldPerformAlternateNavigation) } - - invalidateInteractionStateData() - webViewDidFailNavigationPublisher.send() } @MainActor @@ -1232,8 +1339,31 @@ extension Tab/*: NavigationResponder*/ { // to be moved to Tab+Navigation.swift lastWebError = error } + @MainActor func webContentProcessDidTerminate(with reason: WKProcessTerminationReason?) { - Pixel.fire(.debug(event: .webKitDidTerminate, error: NSError(domain: "WKProcessTerminated", code: reason?.rawValue ?? -1))) + let error = WKError(.webContentProcessTerminated, userInfo: [ + WKProcessTerminationReason.userInfoKey: reason?.rawValue ?? -1, + NSLocalizedDescriptionKey: UserText.webProcessCrashPageMessage, + ]) + + if case.url(let url, _, _) = content { + self.error = error + + loadErrorHTML(error, header: UserText.webProcessCrashPageHeader, forUnreachableURL: url, alternate: true) + } + + Pixel.fire(.debug(event: .webKitDidTerminate, error: error)) + } + + @MainActor + private func loadErrorHTML(_ error: WKError, header: String, forUnreachableURL url: URL, alternate: Bool) { + let html = ErrorPageHTMLTemplate(error: error, header: header).makeHTMLFromTemplate() + if alternate { + webView.loadAlternateHTML(html, baseURL: .error, forUnreachableURL: url) + } else { + // this should be updated using an error page update script call when (if) we have a dynamic error page content implemented + webView.setDocumentHtml(html) + } } } diff --git a/DuckDuckGo/Tab/Model/UserContentUpdating.swift b/DuckDuckGo/Tab/Model/UserContentUpdating.swift index 1a7eb1d79f..ba9f8ba7b6 100644 --- a/DuckDuckGo/Tab/Model/UserContentUpdating.swift +++ b/DuckDuckGo/Tab/Model/UserContentUpdating.swift @@ -81,6 +81,7 @@ final class UserContentUpdating { .combineLatest(privacySecurityPreferences.$gpcEnabled) .map { $0.0 } // drop gpcEnabled value: $0.1 .combineLatest(onNotificationWithInitial(.autofillUserSettingsDidChange), combine) + .combineLatest(onNotificationWithInitial(.autofillScriptDebugSettingsDidChange), combine) // DefaultScriptSourceProvider instance should be created once per rules/config change and fed into UserScripts initialization .map(makeValue) .assign(to: \.bufferedValue, onWeaklyHeld: self) // buffer latest update value diff --git a/DuckDuckGo/Tab/Navigation/SearchNonexistentDomainNavigationResponder.swift b/DuckDuckGo/Tab/Navigation/SearchNonexistentDomainNavigationResponder.swift index b157283f5d..713f737e12 100644 --- a/DuckDuckGo/Tab/Navigation/SearchNonexistentDomainNavigationResponder.swift +++ b/DuckDuckGo/Tab/Navigation/SearchNonexistentDomainNavigationResponder.swift @@ -24,11 +24,13 @@ import Navigation final class SearchNonexistentDomainNavigationResponder { private let tld: TLD + private let setContent: (Tab.TabContent) -> Void private var lastUserEnteredValue: String? private var cancellable: AnyCancellable? - init(tld: TLD, contentPublisher: some Publisher) { + init(tld: TLD, contentPublisher: some Publisher, setContent: @escaping (Tab.TabContent) -> Void) { self.tld = tld + self.setContent = setContent cancellable = contentPublisher.sink { [weak self] tabContent in if case .url(_, credential: .none, source: .userEntered(let userEnteredValue)) = tabContent { @@ -65,7 +67,7 @@ extension SearchNonexistentDomainNavigationResponder: NavigationResponder { // redirect to SERP for non-valid domains entered by user // https://app.asana.com/0/1177771139624306/1204041033469842/f - navigation.navigationAction.targetFrame?.webView?.load(URLRequest(url: url)) + setContent(.url(url, source: .userEntered(lastUserEnteredValue))) } } diff --git a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift index 75d2050c92..dd6f107694 100644 --- a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift +++ b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift @@ -77,6 +77,7 @@ typealias TabExtensionsBuilderArguments = ( isTabPinned: () -> Bool, isTabBurner: Bool, contentPublisher: AnyPublisher, + setContent: (Tab.TabContent) -> Void, titlePublisher: AnyPublisher, userScriptsPublisher: AnyPublisher, inheritedAttribution: AdClickAttributionLogic.State?, @@ -161,7 +162,7 @@ extension TabExtensionsBuilder { isBurner: args.isTabBurner) } add { - SearchNonexistentDomainNavigationResponder(tld: dependencies.privacyFeatures.contentBlocking.tld, contentPublisher: args.contentPublisher) + SearchNonexistentDomainNavigationResponder(tld: dependencies.privacyFeatures.contentBlocking.tld, contentPublisher: args.contentPublisher, setContent: args.setContent) } add { HistoryTabExtension(isBurner: args.isTabBurner, diff --git a/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift b/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift index d690530c46..20415c11e6 100644 --- a/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift +++ b/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift @@ -16,8 +16,9 @@ // limitations under the License. // -import Foundation import Combine +import Foundation +import Navigation protocol LazyLoadable: AnyObject, Identifiable { @@ -28,7 +29,8 @@ protocol LazyLoadable: AnyObject, Identifiable { var isLazyLoadingInProgress: Bool { get set } var loadingFinishedPublisher: AnyPublisher { get } - func reload() + @discardableResult + func reload() -> ExpectedNavigation? func isNewer(than other: Self) -> Bool } @@ -38,7 +40,7 @@ extension Tab: LazyLoadable { var url: URL? { content.url } var loadingFinishedPublisher: AnyPublisher { - Publishers.Merge(webViewDidFinishNavigationPublisher, webViewDidFailNavigationPublisher) + webViewDidFinishNavigationPublisher .prefix(1) .map { self } .eraseToAnyPublisher() diff --git a/DuckDuckGo/Tab/View/Base.lproj/BrowserTab.storyboard b/DuckDuckGo/Tab/View/Base.lproj/BrowserTab.storyboard deleted file mode 100644 index b21cbc5cda..0000000000 --- a/DuckDuckGo/Tab/View/Base.lproj/BrowserTab.storyboard +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 3854e0068e..69a680f172 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -24,11 +24,11 @@ import SwiftUI import BrowserServicesKit final class BrowserTabViewController: NSViewController { - @IBOutlet var errorView: NSView! - @IBOutlet var homePageView: NSView! - @IBOutlet var errorMessageLabel: NSTextField! - @IBOutlet var hoverLabel: NSTextField! - @IBOutlet var hoverLabelContainer: NSView! + + private lazy var homePageView = NSView() + private lazy var hoverLabel = NSTextField(string: URL.duckDuckGo.absoluteString) + private lazy var hoverLabelContainer = ColorView(frame: .zero, backgroundColor: .browserTabBackground, borderWidth: 0) + private weak var webView: WebView? private weak var webViewContainer: NSView? private weak var webViewSnapshot: NSView? @@ -36,11 +36,11 @@ final class BrowserTabViewController: NSViewController { var tabViewModel: TabViewModel? private let tabCollectionViewModel: TabCollectionViewModel + private let bookmarkManager: BookmarkManager private var tabContentCancellable: AnyCancellable? private var userDialogsCancellable: AnyCancellable? private var activeUserDialogCancellable: Cancellable? - private var errorViewStateCancellable: AnyCancellable? private var hoverLinkCancellable: AnyCancellable? private var pinnedTabsDelegatesCancellable: AnyCancellable? private var keyWindowSelectedTabCancellable: AnyCancellable? @@ -53,32 +53,62 @@ final class BrowserTabViewController: NSViewController { private var transientTabContentViewController: NSViewController? - static func create(tabCollectionViewModel: TabCollectionViewModel) -> BrowserTabViewController { - NSStoryboard(name: "BrowserTab", bundle: nil).instantiateInitialController { coder in - self.init(coder: coder, tabCollectionViewModel: tabCollectionViewModel) - }! - } - required init?(coder: NSCoder) { fatalError("BrowserTabViewController: Bad initializer") } - init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel) { + init(tabCollectionViewModel: TabCollectionViewModel, bookmarkManager: BookmarkManager = LocalBookmarkManager.shared) { self.tabCollectionViewModel = tabCollectionViewModel + self.bookmarkManager = bookmarkManager - super.init(coder: coder) + super.init(nibName: nil, bundle: nil) } - override func viewDidLoad() { - super.viewDidLoad() + override func loadView() { + view = BrowserTabView(frame: .zero, backgroundColor: .browserTabBackground) - let homePageViewController = HomePageViewController(tabCollectionViewModel: tabCollectionViewModel, bookmarkManager: LocalBookmarkManager.shared) + homePageView.translatesAutoresizingMaskIntoConstraints = false + view.addAndLayout(homePageView) + + hoverLabelContainer.cornerRadius = 4 + view.addSubview(hoverLabelContainer) + + hoverLabel.focusRingType = .none + hoverLabel.translatesAutoresizingMaskIntoConstraints = false + hoverLabel.font = .systemFont(ofSize: 13) + hoverLabel.drawsBackground = false + hoverLabel.isEditable = false + hoverLabel.isBordered = false + hoverLabel.lineBreakMode = .byClipping + hoverLabel.textColor = .labelColor + hoverLabelContainer.addSubview(hoverLabel) + + setupLayout() + + let homePageViewController = HomePageViewController(tabCollectionViewModel: tabCollectionViewModel, bookmarkManager: bookmarkManager) self.addAndLayoutChild(homePageViewController, into: homePageView) + } + + private func setupLayout() { + hoverLabelContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: -2).isActive = true + view.bottomAnchor.constraint(equalTo: hoverLabelContainer.bottomAnchor, constant: -4).isActive = true + + hoverLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) + hoverLabel.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + hoverLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) + hoverLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + hoverLabelContainer.bottomAnchor.constraint(equalTo: hoverLabel.bottomAnchor, constant: 10).isActive = true + hoverLabel.leadingAnchor.constraint(equalTo: hoverLabelContainer.leadingAnchor, constant: 12).isActive = true + hoverLabelContainer.trailingAnchor.constraint(equalTo: hoverLabel.trailingAnchor, constant: 8).isActive = true + hoverLabel.topAnchor.constraint(equalTo: hoverLabelContainer.topAnchor, constant: 6).isActive = true + } + + override func viewDidLoad() { + super.viewDidLoad() hoverLabelContainer.alphaValue = 0 subscribeToTabs() subscribeToSelectedTabViewModel() - subscribeToErrorViewState() view.registerForDraggedTypes([.URL, .fileURL]) } @@ -217,7 +247,6 @@ final class BrowserTabViewController: NSViewController { generateNativePreviewIfNeeded() self.tabViewModel = selectedTabViewModel self.showTabContent(of: selectedTabViewModel) - self.subscribeToErrorViewState() self.subscribeToTabContent(of: selectedTabViewModel) self.subscribeToHoveredLink(of: selectedTabViewModel) self.subscribeToUserDialogs(of: selectedTabViewModel) @@ -294,10 +323,8 @@ final class BrowserTabViewController: NSViewController { private func addWebViewToViewHierarchy(_ webView: WebView, tab: Tab) { let container = WebViewContainerView(tab: tab, webView: webView, frame: view.bounds) self.webViewContainer = container - view.addSubview(container) - // Make sure link preview (tooltip shown in the bottom-left) is on top - view.addSubview(hoverLabelContainer) + view.addSubview(container, positioned: .below, relativeTo: hoverLabelContainer) } private func changeWebView(tabViewModel: TabViewModel?) { @@ -355,11 +382,10 @@ final class BrowserTabViewController: NSViewController { // For URL tabs, we only want to show tab content (webView) when webView starts // navigation or when another navigation-related event happens. // We take the first such event and move forward. - return Publishers.Merge5( + return Publishers.Merge4( tabViewModel.tab.webViewDidStartNavigationPublisher, tabViewModel.tab.webViewDidReceiveRedirectPublisher, tabViewModel.tab.webViewDidCommitNavigationPublisher, - tabViewModel.tab.webViewDidFailNavigationPublisher, tabViewModel.tab.webViewDidReceiveUserInteractiveChallengePublisher ) .prefix(1) @@ -387,19 +413,15 @@ final class BrowserTabViewController: NSViewController { } } - private func subscribeToErrorViewState() { - errorViewStateCancellable = tabViewModel?.$errorViewState.receive(on: DispatchQueue.main).sink { [weak self] _ in - self?.displayErrorView( - self?.tabViewModel?.errorViewState.isVisible ?? false, - message: self?.tabViewModel?.errorViewState.message ?? UserText.unknownErrorMessage - ) - } - } - func subscribeToHoveredLink(of tabViewModel: TabViewModel?) { hoverLinkCancellable = tabViewModel?.tab.hoveredLinkPublisher.sink { [weak self] in self?.scheduleHoverLabelUpdatesForUrl($0) } +#if DEBUG + if case .xcPreviews = NSApp.runType { + self.scheduleHoverLabelUpdatesForUrl(.duckDuckGo) + } +#endif } func makeWebViewFirstResponder() { @@ -427,13 +449,6 @@ final class BrowserTabViewController: NSViewController { } } - private func displayErrorView(_ shown: Bool, message: String) { - errorMessageLabel.stringValue = message - errorView.isHidden = !shown - webView?.isHidden = shown - homePageView.isHidden = shown - } - func openNewTab(with content: Tab.TabContent) { guard tabCollectionViewModel.selectDisplayableTabIfPresent(content) == false else { return @@ -543,7 +558,7 @@ final class BrowserTabViewController: NSViewController { } func generateNativePreviewIfNeeded() { - guard let tabViewModel = tabViewModel, !tabViewModel.tab.content.isUrl, !tabViewModel.errorViewState.isVisible else { + guard let tabViewModel = tabViewModel, !tabViewModel.tab.content.isUrl, !tabViewModel.isShowingErrorPage else { return } @@ -1105,3 +1120,8 @@ fileprivate extension NSView { } } + +@available(macOS 14.0, *) +#Preview { + BrowserTabViewController(tabCollectionViewModel: TabCollectionViewModel(tabCollection: TabCollection(tabs: [.init(content: .url(.duckDuckGo, source: .ui))]))) +} diff --git a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift index 972adad69a..91803f30ac 100644 --- a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift @@ -48,15 +48,8 @@ final class TabViewModel { } @Published var progress: Double = 0.0 - struct ErrorViewState { - var isVisible: Bool = false - var message: String? - } - @Published var errorViewState = ErrorViewState() { - didSet { - updateTitle() - updateFavicon() - } + var isShowingErrorPage: Bool { + tab.error != nil } @Published var autofillDataToSave: AutofillData? @@ -76,11 +69,11 @@ final class TabViewModel { @Published private(set) var permissionAuthorizationQuery: PermissionAuthorizationQuery? var canPrint: Bool { - self.canReload && tab.webView.canPrint + !isShowingErrorPage && canReload && tab.webView.canPrint } var canSaveContent: Bool { - self.canReload && !tab.webView.isInFullScreenMode + !isShowingErrorPage && canReload && !tab.webView.isInFullScreenMode } init(tab: Tab, appearancePreferences: AppearancePreferences = .shared) { @@ -169,18 +162,10 @@ final class TabViewModel { } private func subscribeToTabError() { - tab.$error - .map { error -> ErrorViewState in - - if let error = error, !error.isFrameLoadInterrupted, !error.isNavigationCancelled { - // don‘t show error for interrupted load like downloads and for cancelled loads - return .init(isVisible: true, message: error.localizedDescription) - } else { - return .init(isVisible: false, message: nil) - } - } - .assign(to: \.errorViewState, onWeaklyHeld: self) - .store(in: &cancellables) + tab.$error.sink { [weak self] _ in + self?.updateTitle() + self?.updateFavicon() + }.store(in: &cancellables) } private func subscribeToPermissions() { @@ -209,7 +194,7 @@ final class TabViewModel { } private func updateCanBeBookmarked() { - canBeBookmarked = tab.content.url ?? .blankPage != .blankPage + canBeBookmarked = !isShowingErrorPage && (tab.content.url ?? .blankPage) != .blankPage } private var tabURL: URL? { @@ -221,14 +206,6 @@ final class TabViewModel { } func updateAddressBarStrings() { - guard !errorViewState.isVisible else { - let failingUrl = tab.error?.failingUrl - let failingUrlHost = failingUrl?.host?.droppingWwwPrefix() ?? "" - addressBarString = failingUrl?.absoluteString ?? "" - passiveAddressBarString = appearancePreferences.showFullURL ? addressBarString : failingUrlHost - return - } - guard tab.content.isUrl, let url = tabURL else { addressBarString = "" passiveAddressBarString = "" @@ -273,13 +250,12 @@ final class TabViewModel { } } - private func updateTitle() { - guard !errorViewState.isVisible else { - title = UserText.tabErrorTitle - return - } - + private func updateTitle() { // swiftlint:disable:this cyclomatic_complexity + let title: String switch tab.content { + // keep an old tab title for web page terminated page, display "Failed to open page" for loading errors + case _ where isShowingErrorPage && (tab.error?.code != .webContentProcessTerminated || tab.title == nil): + title = UserText.tabErrorTitle case .dataBrokerProtection: title = UserText.tabDataBrokerProtectionTitle case .settings: @@ -295,20 +271,22 @@ final class TabViewModel { case .onboarding: title = UserText.tabOnboardingTitle case .url, .none, .subscription: - if let title = tab.title?.trimmingWhitespace(), - !title.isEmpty { - self.title = title + if let tabTitle = tab.title?.trimmingWhitespace(), !tabTitle.isEmpty { + title = tabTitle } else if let host = tab.url?.host?.droppingWwwPrefix() { - self.title = host + title = host } else { - self.title = addressBarString + title = addressBarString } } + if self.title != title { + self.title = title + } } private func updateFavicon() { - guard !errorViewState.isVisible else { - favicon = nil + guard !isShowingErrorPage else { + favicon = .alertCircleColor16 return } diff --git a/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard b/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard index 2bf37ba9b2..ff559dfdfe 100644 --- a/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard +++ b/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard @@ -1,7 +1,7 @@ - + - + @@ -62,7 +62,7 @@ - + - + @@ -116,7 +125,7 @@ - + @@ -124,7 +133,9 @@ + + @@ -134,11 +145,12 @@ - - + + + @@ -147,7 +159,7 @@ - + @@ -158,17 +170,20 @@ + + + diff --git a/DuckDuckGo/TabPreview/TabPreview.storyboard b/DuckDuckGo/TabPreview/TabPreview.storyboard deleted file mode 100644 index f8ffbfe7b0..0000000000 --- a/DuckDuckGo/TabPreview/TabPreview.storyboard +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/DuckDuckGo/TabPreview/TabPreviewViewController.swift b/DuckDuckGo/TabPreview/TabPreviewViewController.swift index 264fee1153..e805efac9c 100644 --- a/DuckDuckGo/TabPreview/TabPreviewViewController.swift +++ b/DuckDuckGo/TabPreview/TabPreviewViewController.swift @@ -18,34 +18,119 @@ import Cocoa -final class TabPreviewViewController: NSViewController { - - @IBOutlet weak var titleTextField: NSTextField! - @IBOutlet weak var urlTextField: NSTextField! - @IBOutlet weak var snapshotImageView: NSImageView! - @IBOutlet weak var snapshotImageViewHeightConstraint: NSLayoutConstraint! +protocol Previewable { + var shouldShowPreview: Bool { get } + var title: String { get } + var tabContent: Tab.TabContent { get } + var snapshot: NSImage? { get } } -extension TabPreviewViewController { +final class TabPreviewViewController: NSViewController { enum TextFieldMaskGradientSize: CGFloat { case width = 6 case trailingSpace = 12 } - override func viewDidLoad() { - super.viewDidLoad() + private lazy var viewColorView = ColorView(frame: .zero, backgroundColor: .controlBackgroundColor) + private lazy var titleTextField = NSTextField() + private lazy var urlTextField = NSTextField() + private lazy var box = NSBox() + private lazy var snapshotImageView = NSImageView() + + private var snapshotImageViewHeightConstraint: NSLayoutConstraint! + + init() { + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("\(Self.self): Bad initializer") + } + + override func loadView() { + view = NSView() + + view.addSubview(viewColorView) + view.addSubview(titleTextField) + view.addSubview(urlTextField) + view.addSubview(box) + view.addSubview(snapshotImageView) + + snapshotImageView.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + snapshotImageView.setContentHuggingPriority(.init(rawValue: 251), for: .vertical) + snapshotImageView.translatesAutoresizingMaskIntoConstraints = false + snapshotImageView.imageScaling = .scaleProportionallyDown + box.boxType = .separator + box.setContentHuggingPriority(.defaultHigh, for: .vertical) + box.translatesAutoresizingMaskIntoConstraints = false + + urlTextField.isEditable = false + urlTextField.isBordered = false + urlTextField.setContentHuggingPriority(.defaultHigh, for: .vertical) + urlTextField.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + urlTextField.translatesAutoresizingMaskIntoConstraints = false + urlTextField.backgroundColor = .textBackgroundColor + urlTextField.font = .systemFont(ofSize: 13) + urlTextField.lineBreakMode = .byTruncatingTail + urlTextField.textColor = .tabPreviewSecondaryTint + + titleTextField.isEditable = false + titleTextField.isBordered = false + titleTextField.setContentHuggingPriority(.defaultHigh, for: .vertical) + titleTextField.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + titleTextField.translatesAutoresizingMaskIntoConstraints = false + titleTextField.backgroundColor = .textBackgroundColor + titleTextField.font = .systemFont(ofSize: 13, weight: .medium) + titleTextField.textColor = .tabPreviewTint titleTextField.maximumNumberOfLines = 3 titleTextField.cell?.truncatesLastVisibleLine = true + + viewColorView.translatesAutoresizingMaskIntoConstraints = false + + setupLayout() + } + + private func setupLayout() { + + viewColorView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + titleTextField.topAnchor.constraint(equalTo: viewColorView.topAnchor, constant: 10).isActive = true + box.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + view.trailingAnchor.constraint(equalTo: snapshotImageView.trailingAnchor).isActive = true + view.trailingAnchor.constraint(equalTo: viewColorView.trailingAnchor).isActive = true + urlTextField.topAnchor.constraint(equalTo: titleTextField.bottomAnchor, constant: 4).isActive = true + viewColorView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + view.bottomAnchor.constraint(equalTo: snapshotImageView.bottomAnchor).isActive = true + titleTextField.topAnchor.constraint(equalTo: view.topAnchor, constant: 10).isActive = true + snapshotImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + urlTextField.bottomAnchor.constraint(equalTo: viewColorView.bottomAnchor, constant: -12).isActive = true + titleTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16).isActive = true + view.trailingAnchor.constraint(equalTo: box.trailingAnchor).isActive = true + urlTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16).isActive = true + view.trailingAnchor.constraint(equalTo: urlTextField.trailingAnchor, constant: 8).isActive = true + view.trailingAnchor.constraint(equalTo: titleTextField.trailingAnchor, constant: 8).isActive = true + box.bottomAnchor.constraint(equalTo: viewColorView.bottomAnchor).isActive = true + snapshotImageView.topAnchor.constraint(equalTo: viewColorView.bottomAnchor).isActive = true + + box.heightAnchor.constraint(equalToConstant: 1).isActive = true + + titleTextField.widthAnchor.constraint(equalToConstant: 256).isActive = true + + viewColorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 57).isActive = true + + snapshotImageViewHeightConstraint = snapshotImageView.heightAnchor.constraint(equalToConstant: 0) + snapshotImageViewHeightConstraint.isActive = true } - func display(tabViewModel: TabViewModel, isSelected: Bool) { + func display(tabViewModel: Previewable, isSelected: Bool) { + _=view // load view if needed + titleTextField.stringValue = tabViewModel.title titleTextField.lineBreakMode = isSelected ? .byWordWrapping : .byTruncatingTail - switch tabViewModel.tab.content { + switch tabViewModel.tabContent { case .url(let url, credential: _, source: _): urlTextField.stringValue = url.toString(decodePunycode: true, dropScheme: true, @@ -57,9 +142,9 @@ extension TabPreviewViewController { urlTextField.stringValue = "" } - if !isSelected, !tabViewModel.errorViewState.isVisible, let snapshot = tabViewModel.tab.tabSnapshot { + if !isSelected, tabViewModel.shouldShowPreview, let snapshot = tabViewModel.snapshot { snapshotImageView.image = snapshot - snapshotImageViewHeightConstraint.constant = getHeight(for: tabViewModel.tab.tabSnapshot) + snapshotImageViewHeightConstraint.constant = getHeight(for: snapshot) } else { snapshotImageView.image = nil snapshotImageViewHeightConstraint.constant = 0 @@ -76,3 +161,70 @@ extension TabPreviewViewController { } } + +extension TabViewModel: Previewable { + + var shouldShowPreview: Bool { + !isShowingErrorPage + } + + var snapshot: NSImage? { + tab.tabSnapshot + } + + var tabContent: Tab.TabContent { + tab.content + } + +} + +#if DEBUG +extension TabPreviewViewController { + func displayMockPreview(of size: NSSize, withTitle title: String, content: Tab.TabContent, previewable: Bool, isSelected: Bool) { + + struct PreviewableMock: Previewable { + let size: NSSize + let title: String + var tabContent: Tab.TabContent + let shouldShowPreview: Bool + + var snapshot: NSImage? { + let image = NSImage(size: size) + image.lockFocus() + NSColor(deviceRed: 0.95, green: 0.98, blue: 0.99, alpha: 1).setFill() + NSRect(origin: .zero, size: image.size).fill() + image.unlockFocus() + return image + } + } + + self.display(tabViewModel: PreviewableMock(size: size, title: title, tabContent: content, shouldShowPreview: previewable), isSelected: isSelected) + } +} + +import Combine +private let previewSize = NSSize(width: 280, height: 220) + +@available(macOS 14.0, *) +#Preview(traits: .fixedLayout(width: previewSize.width, height: previewSize.height)) { { + + let vc = TabPreviewViewController() + vc.displayMockPreview(of: NSSize(width: 1280, height: 560), + withTitle: "Some reasonably long tab preview title that won‘t fit in one line", + content: .url(.makeSearchUrl(from: "SERP query string to search for some ducks")!, source: .ui), + previewable: true, + isSelected: true) + + var c: AnyCancellable! + c = vc.publisher(for: \.view.window).sink { window in + window?.titlebarAppearsTransparent = true + window?.titleVisibility = .hidden + window?.styleMask = [] + window?.setFrame(NSRect(origin: .zero, size: vc.view.bounds.size), display: true) + withExtendedLifetime(c) {} + } + + return vc + +}() } +#endif diff --git a/DuckDuckGo/TabPreview/TabPreviewWindowController.swift b/DuckDuckGo/TabPreview/TabPreviewWindowController.swift index 6e4e5f4456..f9fca79fcd 100644 --- a/DuckDuckGo/TabPreview/TabPreviewWindowController.swift +++ b/DuckDuckGo/TabPreview/TabPreviewWindowController.swift @@ -34,20 +34,39 @@ final class TabPreviewWindowController: NSWindowController { // swiftlint:disable force_cast var tabPreviewViewController: TabPreviewViewController { - contentViewController as! TabPreviewViewController + return self.window!.contentViewController as! TabPreviewViewController } // swiftlint:enable force_cast - override func windowDidLoad() { - super.windowDidLoad() + init() { + super.init(window: Self.loadWindow()) - window?.animationBehavior = .utilityWindow NotificationCenter.default.addObserver(self, selector: #selector(suggestionWindowOpenNotification(_:)), name: .suggestionWindowOpen, object: nil) } + required init?(coder: NSCoder) { + fatalError("\(Self.self): Bad initializer") + } + + private static func loadWindow() -> NSWindow { + let tabPreviewViewController = TabPreviewViewController() + + let window = NSWindow(contentRect: CGRect(x: 294, y: 313, width: 280, height: 58), styleMask: [.titled, .fullSizeContentView], backing: .buffered, defer: true) + window.contentViewController = tabPreviewViewController + + window.allowsToolTipsWhenApplicationIsInactive = false + window.autorecalculatesKeyViewLoop = false + window.isReleasedWhenClosed = false + window.titleVisibility = .hidden + window.titlebarAppearsTransparent = true + window.animationBehavior = .utilityWindow + + return window + } + deinit { NotificationCenter.default.removeObserver(self) } diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index 729e2e7114..35dd79662b 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -27,6 +27,7 @@ import NetworkProtectionUI protocol NetworkProtectionFeatureVisibility { func isNetworkProtectionVisible() -> Bool + func shouldUninstallAutomatically() -> Bool func disableForAllUsers() func disableForWaitlistUsers() } @@ -65,30 +66,17 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { /// /// Once the waitlist beta has ended, we can trigger a remote change that removes the user's auth token and turn off the waitlist flag, hiding Network Protection from the user. func isNetworkProtectionVisible() -> Bool { - #if APPSTORE - return isEasterEggUser || (isUserLocaleAllowed && waitlistIsOngoing) - #else return isEasterEggUser || waitlistIsOngoing - #endif } - var isUserLocaleAllowed: Bool { - var regionCode: String? - if #available(macOS 13, *) { - regionCode = Locale.current.region?.identifier - } else { - regionCode = Locale.current.regionCode - } - - if isInternalUser { - regionCode = "US" - } + /// Returns whether Network Protection should be uninstalled automatically. + /// This is only true when the user is not an Easter Egg user, the waitlist test has ended, and the user is onboarded. + func shouldUninstallAutomatically() -> Bool { + let waitlistAccessEnded = isWaitlistUser && !waitlistIsOngoing + let isNotEasterEggUser = !isEasterEggUser + let isOnboarded = UserDefaults.netP.networkProtectionOnboardingStatus != .default - #if DEBUG // Always assume US for debug builds - regionCode = "US" - #endif - - return (regionCode ?? "US") == "US" + return isNotEasterEggUser && waitlistAccessEnded && isOnboarded } /// Whether the user is fully onboarded @@ -152,10 +140,6 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { } } - private var isInternalUser: Bool { - NSApp.delegateTyped.internalUserDecider.isInternalUser - } - func disableForAllUsers() { Task { await featureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: false) diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift index a57f240f43..78d79bbf5b 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift @@ -91,61 +91,61 @@ private extension Text { struct NetworkProtectionTermsAndConditionsContentView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { - Text(UserText.networkProtectionPrivacyPolicyTitle) + Text(verbatim: UserText.networkProtectionPrivacyPolicyTitle) .font(.system(size: 15, weight: .bold)) .multilineTextAlignment(.leading) Group { - Text(UserText.networkProtectionPrivacyPolicySection1Title).titleStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection1Title).titleStyle() if #available(macOS 12.0, *) { - Text(LocalizedStringKey(UserText.networkProtectionPrivacyPolicySection1ListMarkdown)).bodyStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection1ListMarkdown).bodyStyle() } else { - Text(UserText.networkProtectionPrivacyPolicySection1ListNonMarkdown).bodyStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection1ListNonMarkdown).bodyStyle() } - Text(UserText.networkProtectionPrivacyPolicySection2Title).titleStyle() - Text(UserText.networkProtectionPrivacyPolicySection2List).bodyStyle() - Text(UserText.networkProtectionPrivacyPolicySection3Title).titleStyle() - Text(UserText.networkProtectionPrivacyPolicySection3List).bodyStyle() - Text(UserText.networkProtectionPrivacyPolicySection4Title).titleStyle() - Text(UserText.networkProtectionPrivacyPolicySection4List).bodyStyle() - Text(UserText.networkProtectionPrivacyPolicySection5Title).titleStyle() - Text(UserText.networkProtectionPrivacyPolicySection5List).bodyStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection2Title).titleStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection2List).bodyStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection3Title).titleStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection3List).bodyStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection4Title).titleStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection4List).bodyStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection5Title).titleStyle() + Text(verbatim: UserText.networkProtectionPrivacyPolicySection5List).bodyStyle() } - Text(UserText.networkProtectionTermsOfServiceTitle) + Text(verbatim: UserText.networkProtectionTermsOfServiceTitle) .font(.system(size: 15, weight: .bold)) .multilineTextAlignment(.leading) .padding(.top, 28) .padding(.bottom, 14) Group { - Text(UserText.networkProtectionTermsOfServiceSection1Title).titleStyle(topPadding: 0) - Text(UserText.networkProtectionTermsOfServiceSection1List).bodyStyle() - Text(UserText.networkProtectionTermsOfServiceSection2Title).titleStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection1Title).titleStyle(topPadding: 0) + Text(verbatim: UserText.networkProtectionTermsOfServiceSection1List).bodyStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection2Title).titleStyle() if #available(macOS 12.0, *) { - Text(LocalizedStringKey(UserText.networkProtectionTermsOfServiceSection2ListMarkdown)).bodyStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection2ListMarkdown).bodyStyle() } else { Text(UserText.networkProtectionTermsOfServiceSection2ListNonMarkdown).bodyStyle() } - Text(UserText.networkProtectionTermsOfServiceSection3Title).titleStyle() - Text(UserText.networkProtectionTermsOfServiceSection3List).bodyStyle() - Text(UserText.networkProtectionTermsOfServiceSection4Title).titleStyle() - Text(UserText.networkProtectionTermsOfServiceSection4List).bodyStyle() - Text(UserText.networkProtectionTermsOfServiceSection5Title).titleStyle() - Text(UserText.networkProtectionTermsOfServiceSection5List).bodyStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection3Title).titleStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection3List).bodyStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection4Title).titleStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection4List).bodyStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection5Title).titleStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection5List).bodyStyle() } Group { - Text(UserText.networkProtectionTermsOfServiceSection6Title).titleStyle() - Text(UserText.networkProtectionTermsOfServiceSection6List).bodyStyle() - Text(UserText.networkProtectionTermsOfServiceSection7Title).titleStyle() - Text(UserText.networkProtectionTermsOfServiceSection7List).bodyStyle() - Text(UserText.networkProtectionTermsOfServiceSection8Title).titleStyle() - Text(UserText.networkProtectionTermsOfServiceSection8List).bodyStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection6Title).titleStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection6List).bodyStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection7Title).titleStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection7List).bodyStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection8Title).titleStyle() + Text(verbatim: UserText.networkProtectionTermsOfServiceSection8List).bodyStyle() } } .padding(.all, 20) @@ -169,19 +169,19 @@ struct DataBrokerProtectionTermsAndConditionsContentView: View { var body: some View { VStack(alignment: .leading, spacing: 5) { - Text(UserText.dataBrokerProtectionPrivacyPolicyTitle) + Text(verbatim: UserText.dataBrokerProtectionPrivacyPolicyTitle) .font(.system(size: 15, weight: .bold)) .multilineTextAlignment(.leading) - Text("\nWe don’t save your personal information for this service to function.") + Text(verbatim: "\nWe don’t save your personal information for this service to function.") .fontWeight(.bold) .padding(.bottom, sectionBottomPadding) Group { - Text("• This Privacy Policy is for our waitlist beta service.") + Text(verbatim: "• This Privacy Policy is for our waitlist beta service.") HStack(spacing: 0) { - Text("• Our main ") - Text("Privacy Policy ") + Text(verbatim: "• Our main ") + Text(verbatim: "Privacy Policy ") .foregroundColor(Color.blue) .underline(color: .blue) .onTapGesture { @@ -189,94 +189,94 @@ struct DataBrokerProtectionTermsAndConditionsContentView: View { WindowsManager.openNewWindow(with: url, source: .ui, isBurner: false) } } - Text("also applies here.") + Text(verbatim: "also applies here.") } - Text("• This beta product may collect more diagnostic data than our typical products. Examples of such data include: alerts of low memory, application restarts, and user engagement with product features.") + Text(verbatim: "• This beta product may collect more diagnostic data than our typical products. Examples of such data include: alerts of low memory, application restarts, and user engagement with product features.") } .padding(.leading, groupLeadingPadding) - Text("\nYour personal information is stored locally on your device.") + Text(verbatim: "\nYour personal information is stored locally on your device.") .fontWeight(.bold) .padding(.bottom, sectionBottomPadding) Group { - Text("• The information you provide when you sign-up to use this service, for example your name, age, address, and phone number is stored on your device.") - Text("• We then scan data brokers from your device to check if any sites contain your personal information.") - Text("• We may find additional information on data broker sites through this scanning process, like alternative names or phone numbers, or the names of your relatives. This information is also stored locally on your device.") + Text(verbatim: "• The information you provide when you sign-up to use this service, for example your name, age, address, and phone number is stored on your device.") + Text(verbatim: "• We then scan data brokers from your device to check if any sites contain your personal information.") + Text(verbatim: "• We may find additional information on data broker sites through this scanning process, like alternative names or phone numbers, or the names of your relatives. This information is also stored locally on your device.") } .padding(.leading, groupLeadingPadding) - Text("\nWe submit removal requests to data broker sites on your behalf.") + Text(verbatim: "\nWe submit removal requests to data broker sites on your behalf.") .fontWeight(.bold) .padding(.bottom, sectionBottomPadding) Group { - Text("• We submit removal requests to the data broker sites directly from your device, unlike other services where the removal process is initiated on remote servers.") - Text("• The only personal information we may receive is a confirmation email from data broker sites which is deleted within 72 hours.") - Text("• We regularly re-scan data broker sites to check on the removal status of your information. If it has reappeared, we resubmit the removal request.") + Text(verbatim: "• We submit removal requests to the data broker sites directly from your device, unlike other services where the removal process is initiated on remote servers.") + Text(verbatim: "• The only personal information we may receive is a confirmation email from data broker sites which is deleted within 72 hours.") + Text(verbatim: "• We regularly re-scan data broker sites to check on the removal status of your information. If it has reappeared, we resubmit the removal request.") } .padding(.leading, groupLeadingPadding) - Text("\nTerms of Service") + Text(verbatim: "\nTerms of Service") .fontWeight(.bold) - Text("You must be eligible to use this service.") + Text(verbatim: "You must be eligible to use this service.") .fontWeight(.bold) .padding(.bottom, sectionBottomPadding) Group { - Text("• To use this service, you must be 18 or older.") + Text(verbatim: "• To use this service, you must be 18 or older.") } .padding(.leading, groupLeadingPadding) - Text("\nThe service is for limited and personal use only.") + Text(verbatim: "\nThe service is for limited and personal use only.") .fontWeight(.bold) .padding(.bottom, sectionBottomPadding) Group { - Text("• The service is available for your personal use only. You represent and warrant that you will only initiate removal of your own personal information.") - Text("• This service is available on one device only.") + Text(verbatim: "• The service is available for your personal use only. You represent and warrant that you will only initiate removal of your own personal information.") + Text(verbatim: "• This service is available on one device only.") } .padding(.leading, groupLeadingPadding) - Text("\nYou give DuckDuckGo authority to act on your Here's an updated version with the remaining content:") + Text(verbatim: "\nYou give DuckDuckGo authority to act on your Here's an updated version with the remaining content:") .fontWeight(.bold) .padding(.bottom, sectionBottomPadding) Group { - Text("• You hereby authorize DuckDuckGo to act on your behalf to request removal of your personal information from data broker sites.") - Text("• Because data broker sites often have multi-step processes required to have information removed, and because they regularly update their databases with new personal information, this authorization includes ongoing action on your behalf solely to perform the service.") + Text(verbatim: "• You hereby authorize DuckDuckGo to act on your behalf to request removal of your personal information from data broker sites.") + Text(verbatim: "• Because data broker sites often have multi-step processes required to have information removed, and because they regularly update their databases with new personal information, this authorization includes ongoing action on your behalf solely to perform the service.") } .padding(.leading, groupLeadingPadding) - Text("\nThe service cannot remove all of your information from the Internet.") + Text(verbatim: "\nThe service cannot remove all of your information from the Internet.") .fontWeight(.bold) .padding(.bottom, sectionBottomPadding) Group { - Text("• This service requests removal from a limited number of data broker sites only. You understand that we cannot guarantee that the third-party sites will honor the requests, or that your personal information will not reappear in the future.") - Text("• You understand that we will only be able to request the removal of information based upon the information you provide to us.") + Text(verbatim: "• This service requests removal from a limited number of data broker sites only. You understand that we cannot guarantee that the third-party sites will honor the requests, or that your personal information will not reappear in the future.") + Text(verbatim: "• You understand that we will only be able to request the removal of information based upon the information you provide to us.") } .padding(.leading, groupLeadingPadding) - Text("\nWe provide this beta service as-is, and without warranty.") + Text(verbatim: "\nWe provide this beta service as-is, and without warranty.") .fontWeight(.bold) .padding(.bottom, sectionBottomPadding) Group { - Text("• This service is provided as-is and without warranties or guarantees of any kind.") - Text("• To the extent possible under applicable law, DuckDuckGo will not be liable for any damage or loss arising from your use of the service. In any event, the total aggregate liability of DuckDuckGo shall not exceed $25 or the equivalent in your local currency.") - Text("• We may in the future transfer responsibility for the service to a subsidiary of DuckDuckGo. If that happens, you agree that references to “DuckDuckGo” will refer to our subsidiary, which will then become responsible for providing the service and for any liabilities relating to it.") + Text(verbatim: "• This service is provided as-is and without warranties or guarantees of any kind.") + Text(verbatim: "• To the extent possible under applicable law, DuckDuckGo will not be liable for any damage or loss arising from your use of the service. In any event, the total aggregate liability of DuckDuckGo shall not exceed $25 or the equivalent in your local currency.") + Text(verbatim: "• We may in the future transfer responsibility for the service to a subsidiary of DuckDuckGo. If that happens, you agree that references to “DuckDuckGo” will refer to our subsidiary, which will then become responsible for providing the service and for any liabilities relating to it.") } .padding(.leading, groupLeadingPadding) - Text("\nWe may terminate access at any time.") + Text(verbatim: "\nWe may terminate access at any time.") .fontWeight(.bold) .padding(.bottom, sectionBottomPadding) Group { - Text("• This service is in beta, and your access to it is temporary.") - Text("• We reserve the right to terminate access at any time in our sole discretion, including for violation of these terms or our DuckDuckGo Terms of Service, which are incorporated by reference.") + Text(verbatim: "• This service is in beta, and your access to it is temporary.") + Text(verbatim: "• We reserve the right to terminate access at any time in our sole discretion, including for violation of these terms or our DuckDuckGo Terms of Service, which are incorporated by reference.") } .padding(.leading, groupLeadingPadding) diff --git a/DuckDuckGo/Waitlist/Waitlist.swift b/DuckDuckGo/Waitlist/Waitlist.swift index 6400f655a0..31331e3b63 100644 --- a/DuckDuckGo/Waitlist/Waitlist.swift +++ b/DuckDuckGo/Waitlist/Waitlist.swift @@ -357,7 +357,9 @@ struct DataBrokerProtectionWaitlist: Waitlist { UserDefaults().setValue(true, forKey: UserDefaultsWrapper.Key.shouldShowDBPWaitlistInvitedCardUI.rawValue) sendInviteCodeAvailableNotification { - DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistNotificationShown, frequency: .dailyAndCount) + DispatchQueue.main.async { + DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistNotificationShown, frequency: .dailyAndCount) + } } } } diff --git a/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift b/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift index fe35c9f578..85565b322d 100644 --- a/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift +++ b/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift @@ -21,6 +21,27 @@ import WebKit final class DuckURLSchemeHandler: NSObject, WKURLSchemeHandler { + static let emptyHtml = """ + + + + + + + """ + func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { guard let requestURL = webView.url ?? urlSchemeTask.request.url else { assertionFailure("No URL for Private Player scheme handler") @@ -29,7 +50,7 @@ final class DuckURLSchemeHandler: NSObject, WKURLSchemeHandler { guard requestURL.isDuckPlayer else { // return empty page for native UI pages navigations (like the Home page or Settings) if the request is not for the Duck Player - let data = "".utf8data + let data = Self.emptyHtml.utf8data let response = URLResponse(url: requestURL, mimeType: "text/html", diff --git a/DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift b/DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift new file mode 100644 index 0000000000..85891c6604 --- /dev/null +++ b/DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift @@ -0,0 +1,64 @@ +// +// BrowserWindowManager.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import Foundation +import WebKit + +/// A class that offers functionality to quickly show an interactive browser window. +/// +/// This class is meant to aid with debugging and should not be included in release builds. +/// . +final class BrowserWindowManager: NSObject { + private var interactiveBrowserWindow: NSWindow? + + @MainActor + func show(domain: String) { + if let interactiveBrowserWindow, interactiveBrowserWindow.isVisible { + return + } + + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], + backing: .buffered, defer: false) + window.center() + window.title = "Web Browser" + window.delegate = self + interactiveBrowserWindow = window + + // Create the WKWebView. + let webView = WKWebView(frame: window.contentView!.bounds) + webView.autoresizingMask = [.width, .height] + window.contentView!.addSubview(webView) + + // Load a URL. + let url = URL(string: domain)! + let request = URLRequest(url: url) + webView.load(request) + + // Show the window. + window.makeKeyAndOrderFront(nil) + } +} + +extension BrowserWindowManager: NSWindowDelegate { + func windowWillClose(_ notification: Notification) { + interactiveBrowserWindow = nil + } +} diff --git a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift index 1d7c0403fb..c452200f7b 100644 --- a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift +++ b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift @@ -17,10 +17,10 @@ // import Combine -import Foundation +import Common import DataBrokerProtection +import Foundation import PixelKit -import Common /// Manages the IPC service for the Agent app /// @@ -28,6 +28,7 @@ import Common /// demand interaction with. /// final class IPCServiceManager { + private var browserWindowManager: BrowserWindowManager private let ipcServer: DataBrokerProtectionIPCServer private let scheduler: DataBrokerProtectionScheduler private let pixelHandler: EventMapping @@ -41,6 +42,8 @@ final class IPCServiceManager { self.scheduler = scheduler self.pixelHandler = pixelHandler + browserWindowManager = BrowserWindowManager() + ipcServer.serverDelegate = self ipcServer.activate() } @@ -102,4 +105,10 @@ extension IPCServiceManager: IPCServerInterface { pixelHandler.fire(.ipcServerRunAllOperations) scheduler.runAllOperations(showWebView: showWebView) } + + func openBrowser(domain: String) { + Task { @MainActor in + browserWindowManager.show(domain: domain) + } + } } diff --git a/DuckDuckGoVPN/DuckDuckGoVPN.entitlements b/DuckDuckGoVPN/DuckDuckGoVPN.entitlements index 653311b9ec..2797c3f947 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPN.entitlements +++ b/DuckDuckGoVPN/DuckDuckGoVPN.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider-systemextension + app-proxy-provider-systemextension com.apple.developer.system-extension.install diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index add4af8a6d..f14bbac165 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -23,7 +23,7 @@ import LoginItems import Networking import NetworkExtension import NetworkProtection -import NetworkProtectionIPC +import NetworkProtectionProxy import NetworkProtectionUI import ServiceManagement import PixelKit @@ -60,18 +60,82 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { private var cancellables = Set() - var networkExtensionBundleID: String { - Bundle.main.networkExtensionBundleID + var proxyExtensionBundleID: String { + Bundle.proxyExtensionBundleID } -#if NETWORK_PROTECTION - private lazy var networkExtensionController = NetworkExtensionController(extensionBundleID: networkExtensionBundleID) + var tunnelExtensionBundleID: String { + Bundle.tunnelExtensionBundleID + } + + private lazy var networkExtensionController = NetworkExtensionController(extensionBundleID: tunnelExtensionBundleID) + + private var storeProxySettingsInProviderConfiguration: Bool { +#if NETP_SYSTEM_EXTENSION + true +#else + false #endif + } private lazy var tunnelSettings = VPNSettings(defaults: .netP) + private lazy var proxySettings = TransparentProxySettings(defaults: .netP) + + @MainActor + private lazy var vpnProxyLauncher = VPNProxyLauncher( + tunnelController: tunnelController, + proxyController: proxyController) + + @MainActor + private lazy var proxyController: TransparentProxyController = { + let controller = TransparentProxyController( + extensionID: proxyExtensionBundleID, + storeSettingsInProviderConfiguration: storeProxySettingsInProviderConfiguration, + settings: proxySettings) { [weak self] manager in + guard let self else { return } + + manager.localizedDescription = "DuckDuckGo VPN Proxy" + + if !manager.isEnabled { + manager.isEnabled = true + } + + manager.protocolConfiguration = { + let protocolConfiguration = manager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol() + protocolConfiguration.serverAddress = "127.0.0.1" // Dummy address... the NetP service will take care of grabbing a real server + protocolConfiguration.providerBundleIdentifier = self.proxyExtensionBundleID + + // always-on + protocolConfiguration.disconnectOnSleep = false + + // kill switch + // protocolConfiguration.enforceRoutes = false + + // this setting breaks Connection Tester + // protocolConfiguration.includeAllNetworks = settings.includeAllNetworks + + // This is intentionally not used but left here for documentation purposes. + // The reason for this is that we want to have full control of the routes that + // are excluded, so instead of using this setting we're just configuring the + // excluded routes through our VPNSettings class, which our extension reads directly. + // protocolConfiguration.excludeLocalNetworks = settings.excludeLocalNetworks + return protocolConfiguration + }() + } + + controller.eventHandler = handleControllerEvent(_:) + + return controller + }() + + private func handleControllerEvent(_ event: TransparentProxyController.Event) { + PixelKit.fire(event) + } + + @MainActor private lazy var tunnelController = NetworkProtectionTunnelController( - networkExtensionBundleID: networkExtensionBundleID, + networkExtensionBundleID: tunnelExtensionBundleID, networkExtensionController: networkExtensionController, settings: tunnelSettings) @@ -79,6 +143,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { /// /// This is used by our main app to control the tunnel through the VPN login item. /// + @MainActor private lazy var tunnelControllerIPCService: TunnelControllerIPCService = { let ipcServer = TunnelControllerIPCService( tunnelController: tunnelController, @@ -88,17 +153,19 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { return ipcServer }() + @MainActor + private lazy var statusObserver = ConnectionStatusObserverThroughSession( + tunnelSessionProvider: tunnelController, + platformNotificationCenter: NSWorkspace.shared.notificationCenter, + platformDidWakeNotification: NSWorkspace.didWakeNotification) + + @MainActor private lazy var statusReporter: NetworkProtectionStatusReporter = { let errorObserver = ConnectionErrorObserverThroughSession( tunnelSessionProvider: tunnelController, platformNotificationCenter: NSWorkspace.shared.notificationCenter, platformDidWakeNotification: NSWorkspace.didWakeNotification) - let statusObserver = ConnectionStatusObserverThroughSession( - tunnelSessionProvider: tunnelController, - platformNotificationCenter: NSWorkspace.shared.notificationCenter, - platformDidWakeNotification: NSWorkspace.didWakeNotification) - let serverInfoObserver = ConnectionServerInfoObserverThroughSession( tunnelSessionProvider: tunnelController, platformNotificationCenter: NSWorkspace.shared.notificationCenter, @@ -113,6 +180,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { ) }() + @MainActor private lazy var vpnAppEventsHandler = { VPNAppEventsHandler(tunnelController: tunnelController) }() @@ -175,8 +243,9 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { bouncer.requireAuthTokenOrKillApp() - // Initialize the IPC server + // Initialize lazy properties _ = tunnelControllerIPCService + _ = vpnProxyLauncher let dryRun: Bool diff --git a/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements b/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements index a6ed34f64f..f531d0bc0c 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements +++ b/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.developer.system-extension.install diff --git a/DuckDuckGoVPN/Info-AppStore.plist b/DuckDuckGoVPN/Info-AppStore.plist index 7627fdd9c9..a1b2b02a02 100644 --- a/DuckDuckGoVPN/Info-AppStore.plist +++ b/DuckDuckGoVPN/Info-AppStore.plist @@ -6,8 +6,10 @@ $(DISTRIBUTED_NOTIFICATIONS_PREFIX) NETP_APP_GROUP $(NETP_APP_GROUP) - SYSEX_BUNDLE_ID - $(SYSEX_BUNDLE_ID) + PROXY_EXTENSION_BUNDLE_ID + $(PROXY_EXTENSION_BUNDLE_ID) + TUNNEL_EXTENSION_BUNDLE_ID + $(TUNNEL_EXTENSION_BUNDLE_ID) LSApplicationCategoryType public.app-category.productivity CFBundleShortVersionString diff --git a/DuckDuckGoVPN/Info.plist b/DuckDuckGoVPN/Info.plist index 7627fdd9c9..a1b2b02a02 100644 --- a/DuckDuckGoVPN/Info.plist +++ b/DuckDuckGoVPN/Info.plist @@ -6,8 +6,10 @@ $(DISTRIBUTED_NOTIFICATIONS_PREFIX) NETP_APP_GROUP $(NETP_APP_GROUP) - SYSEX_BUNDLE_ID - $(SYSEX_BUNDLE_ID) + PROXY_EXTENSION_BUNDLE_ID + $(PROXY_EXTENSION_BUNDLE_ID) + TUNNEL_EXTENSION_BUNDLE_ID + $(TUNNEL_EXTENSION_BUNDLE_ID) LSApplicationCategoryType public.app-category.productivity CFBundleShortVersionString diff --git a/DuckDuckGoVPN/VPNProxyLauncher.swift b/DuckDuckGoVPN/VPNProxyLauncher.swift new file mode 100644 index 0000000000..c99d187cf2 --- /dev/null +++ b/DuckDuckGoVPN/VPNProxyLauncher.swift @@ -0,0 +1,149 @@ +// +// VPNProxyLauncher.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation +import NetworkProtectionProxy +import NetworkExtension + +/// Starts and stops the VPN proxy component. +/// +/// This class looks at the tunnel and the proxy components and their status and settings, and decides based on +/// a number of conditions whether to start the proxy, stop it, or just leave it be. +/// +@MainActor +final class VPNProxyLauncher { + private let tunnelController: NetworkProtectionTunnelController + private let proxyController: TransparentProxyController + private let notificationCenter: NotificationCenter + private var cancellables = Set() + + init(tunnelController: NetworkProtectionTunnelController, + proxyController: TransparentProxyController, + notificationCenter: NotificationCenter = .default) { + + self.notificationCenter = notificationCenter + self.proxyController = proxyController + self.tunnelController = tunnelController + + subscribeToStatusChanges() + subscribeToProxySettingChanges() + } + + // MARK: - Status Changes + + private func subscribeToStatusChanges() { + notificationCenter.publisher(for: .NEVPNStatusDidChange) + .receive(on: DispatchQueue.main) + .removeDuplicates() + .sink(receiveValue: statusChanged(notification:)) + .store(in: &cancellables) + } + + private func statusChanged(notification: Notification) { + Task { @MainActor in + let isProxyConnectionStatusChange = await proxyController.connection == notification.object as? NEVPNConnection + + try await startOrStopProxyIfNeeded(isProxyConnectionStatusChange: isProxyConnectionStatusChange) + } + } + + // MARK: - Proxy Settings Changes + + private func subscribeToProxySettingChanges() { + proxyController.settings.changePublisher + .sink(receiveValue: proxySettingChanged(_:)) + .store(in: &cancellables) + } + + private func proxySettingChanged(_ change: TransparentProxySettings.Change) { + Task { @MainActor in + try await startOrStopProxyIfNeeded() + } + } + + // MARK: - Auto starting & stopping the proxy component + + private var isControllingProxy = false + + private func startOrStopProxyIfNeeded(isProxyConnectionStatusChange: Bool = false) async throws { + if await shouldStartProxy { + guard !isControllingProxy else { + return + } + + isControllingProxy = true + + // When we're auto-starting the proxy because its own status changed to + // disconnected, we want to give it a pause because if it fails to connect again + // we risk the proxy entering a frenetic connect / disconnect loop + if isProxyConnectionStatusChange { + // If the proxy connection was stopped, let's wait a bit before trying to enable it again + try await Task.sleep(interval: .seconds(10)) + + // And we want to check again if the proxy still needs to start after waiting + guard await shouldStartProxy else { + return + } + } + + do { + try await proxyController.start() + isControllingProxy = false + } catch { + isControllingProxy = false + throw error + } + } else if await shouldStopProxy { + guard !isControllingProxy else { + return + } + + isControllingProxy = true + await proxyController.stop() + isControllingProxy = false + } + } + + private var shouldStartProxy: Bool { + get async { + let proxyIsDisconnected = await proxyController.status == .disconnected + let tunnelIsConnected = await tunnelController.status == .connected + + // Starting the proxy only when it's required for active features + // is a product decision. It may change once we decide the proxy + // is stable enough to be running at all times. + return proxyIsDisconnected + && tunnelIsConnected + && proxyController.isRequiredForActiveFeatures + } + } + + private var shouldStopProxy: Bool { + get async { + let proxyIsConnected = await proxyController.status == .connected + let tunnelIsDisconnected = await tunnelController.status == .disconnected + + // Stopping the proxy when it's not required for active features + // is a product decision. It may change once we decide the proxy + // is stable enough to be running at all times. + return proxyIsConnected + && (tunnelIsDisconnected || !proxyController.isRequiredForActiveFeatures) + } + } +} diff --git a/IntegrationTests/Common/IntegrationTestsBridging.h b/IntegrationTests/Common/IntegrationTestsBridging.h new file mode 100644 index 0000000000..b2f140218b --- /dev/null +++ b/IntegrationTests/Common/IntegrationTestsBridging.h @@ -0,0 +1,21 @@ +// +// IntegrationTestsBridging.h +// +// Copyright © 2021 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "Bridging.h" + +#import "WKURLSchemeTask+Private.h" diff --git a/IntegrationTests/Tab/ErrorPageTests.swift b/IntegrationTests/Tab/ErrorPageTests.swift new file mode 100644 index 0000000000..3b8e4134ad --- /dev/null +++ b/IntegrationTests/Tab/ErrorPageTests.swift @@ -0,0 +1,930 @@ +// +// ErrorPageTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Common +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +@available(macOS 12.0, *) +@MainActor +class ErrorPageTests: XCTestCase { + + var window: NSWindow! + + var mainViewController: MainViewController { + (window.contentViewController as! MainViewController) + } + + var tabViewModel: TabViewModel { + mainViewController.browserTabViewController.tabViewModel! + } + + var webViewConfiguration: WKWebViewConfiguration! + var schemeHandler: TestSchemeHandler! + + static let pageTitle = "test page" + static let testHtml = "\(pageTitle)test" + static let alternativeTitle = "alternative page" + static let alternativeHtml = "\(alternativeTitle)alternative body" + + static let sessionStateData = Data.sessionRestorationMagic + """ + + + + + IsAppInitiated + + SessionHistory + + SessionHistoryVersion + 1 + SessionHistoryEntries + + + SessionHistoryEntryOriginalURL + \(URL.newtab.absoluteString) + SessionHistoryEntryTitle + + SessionHistoryEntryShouldOpenExternalURLsPolicyKey + 1 + SessionHistoryEntryData + AAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAEGPVpVAQBgAAAAAAAAAAAP////8AAAAAD2PVpVAQBgD/////AAAAAAAAAAAAAIA/AAAAAP////8= + SessionHistoryEntryURL + \(URL.newtab.absoluteString) + + + SessionHistoryEntryOriginalURL + \(URL.test.absoluteString) + SessionHistoryEntryTitle + test page + SessionHistoryEntryShouldOpenExternalURLsPolicyKey + 1 + SessionHistoryEntryData + AAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAwvZLp1AQBgAAAAAAAAAAAP////8AAAAAwfZLp1AQBgD/////AAAAAAAAAAAAAIA/AAAAAP////8= + SessionHistoryEntryURL + \(URL.test.absoluteString) + + + SessionHistoryEntryOriginalURL + \(URL.alternative.absoluteString) + SessionHistoryEntryTitle + alternative page + SessionHistoryEntryShouldOpenExternalURLsPolicyKey + 1 + SessionHistoryEntryData + AAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAeWCYp1AQBgAAAAAAAAAAAP////8AAAAAeGCYp1AQBgD/////AAAAAAAAAAAAAAAAAAAAAP////8= + SessionHistoryEntryURL + \(URL.alternative.absoluteString) + + + SessionHistoryCurrentIndex + 1 + + RenderTreeSize + 4 + + + """.utf8data + + @MainActor + override func setUp() async throws { + schemeHandler = TestSchemeHandler() + WKWebView.customHandlerSchemes = [.http, .https] + + webViewConfiguration = WKWebViewConfiguration() + // ! uncomment this to view navigation logs + // OSLog.loggingCategories.insert(OSLog.AppCategories.navigation.rawValue) + + // tests return debugDescription instead of localizedDescription + NSError.disableSwizzledDescription = true + + // mock WebView https protocol handling + webViewConfiguration.setURLSchemeHandler(schemeHandler, forURLScheme: URL.NavigationalScheme.https.rawValue) + } + + @MainActor + override func tearDown() async throws { + window?.close() + window = nil + + webViewConfiguration = nil + schemeHandler = nil + WKWebView.customHandlerSchemes = [] + + NSError.disableSwizzledDescription = false + } + + func testWhenPageFailsToLoad_errorPageShown() async throws { + // open Tab with newtab page + let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // navigate to test url, fail with error + schemeHandler.middleware = [{ _ in + .failure(NSError.hostNotFound) + }] + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + let error = try await eNavigationFailed.value + _=try await eNavigationFinished.value + + XCTAssertEqual(error.errorCode, NSError.hostNotFound.code) + XCTAssertEqual(error.localizedDescription, NSError.hostNotFound.localizedDescription) + let headerText: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-header')[0].innerText") + let errorDescr: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-description')[0].innerText") + + XCTAssertNil(tab.title) + XCTAssertEqual(tabViewModel.title, UserText.tabErrorTitle) + XCTAssertEqual(headerText?.trimmingWhitespace(), UserText.errorPageHeader) + XCTAssertEqual(errorDescr?.trimmingWhitespace(), NSError.hostNotFound.localizedDescription) + XCTAssertTrue(tab.canGoBack) + XCTAssertFalse(tab.canGoForward) + XCTAssertTrue(tab.canReload) + XCTAssertFalse(viewModel.tabViewModel(at: 0)!.canSaveContent) + XCTAssertEqual(tab.backHistoryItems.count, 1) + XCTAssertEqual(tab.backHistoryItems.first?.url, .newtab) + XCTAssertNil(tab.currentHistoryItem?.title) + XCTAssertEqual(tab.currentHistoryItem?.url, .test) + XCTAssertEqual(tab.content.userEditableUrl, .test) + } + + func testWhenTabWithNoConnectionErrorActivated_reloadTriggered() async throws { + // open 2 Tabs with newtab page + let tab1 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let tab2 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let tabsViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab1, tab2])) + window = WindowsManager.openNewWindow(with: tabsViewModel)! + + // wait until Home page loads + let eNewtabPageLoaded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // navigate to a failing url + schemeHandler.middleware = [{ _ in + .failure(NSError.noConnection) + }] + tab1.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + // wait for error page to open + let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() + + _=try await eNavigationFailed.value + + // switch to tab 2 + tabsViewModel.select(at: .unpinned(1)) + + // next load should be ok + let eServerQueried = expectation(description: "server request sent") + schemeHandler.middleware = [{ _ in + eServerQueried.fulfill() + return .ok(.html(Self.testHtml)) + }] + // coming back to the failing tab 1 should trigger its reload + let eNavigationSucceeded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tabsViewModel.select(at: .unpinned(0)) + + _=try await eNavigationSucceeded.value + await fulfillment(of: [eServerQueried], timeout: 1) + XCTAssertEqual(tab1.content.url, .test) + XCTAssertNil(tab1.error) + } + + func testWhenTabWithConnectionLostErrorActivatedAndReloadFailsAgain_errorPageIsShownOnce() async throws { + // open 2 Tabs with newtab page + // navigate to a failing url right away + schemeHandler.middleware = [{ _ in + .failure(NSError.connectionLost) + }] + let tab1 = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration) + let tab2 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let tabsViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab1, tab2])) + window = WindowsManager.openNewWindow(with: tabsViewModel)! + + // wait for error page to open + let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + _=try await eNavigationFailed.value + _=try await eNavigationFinished.value + + // switch to tab 2 + tabsViewModel.select(at: .unpinned(1)) + + // coming back to the failing tab 1 should trigger its reload but it will fail again + let eServerQueried = expectation(description: "server request sent") + schemeHandler.middleware = [{ _ in + eServerQueried.fulfill() + return .failure(NSError.noConnection) + }] + let eNavigationFailed2 = tab1.$error.compactMap { $0 }.filter { + $0.errorCode == NSError.noConnection.code + }.timeout(5).first().promise() + + tabsViewModel.select(at: .unpinned(0)) + + await fulfillment(of: [eServerQueried], timeout: 1) + let error = try await eNavigationFailed2.value + + let c = tab1.$isLoading.dropFirst().sink { isLoading in + XCTFail("Failing tab shouldn‘t reload again (isLoading: \(isLoading))") + } + + XCTAssertEqual(error.errorCode, NSError.noConnection.code) + XCTAssertEqual(error.localizedDescription, NSError.noConnection.localizedDescription) + let headerText: String? = try await tab1.webView.evaluateJavaScript("document.getElementsByClassName('error-header')[0].innerText") + let errorDescr: String? = try await tab1.webView.evaluateJavaScript("document.getElementsByClassName('error-description')[0].innerText") + XCTAssertNil(tab1.title) + XCTAssertEqual(tabsViewModel.tabViewModel(at: 0)?.title, UserText.tabErrorTitle) + XCTAssertEqual(headerText?.trimmingWhitespace(), UserText.errorPageHeader) + XCTAssertEqual(errorDescr?.trimmingWhitespace(), NSError.noConnection.localizedDescription) + + try await Task.sleep(interval: 0.4) // sleep a little to confirm no more navigations are performed + withExtendedLifetime(c) {} + } + + func testWhenTabWithOtherErrorActivated_reloadNotTriggered() async throws { + // open 2 Tabs with newtab page + // navigate to a failing url right away + schemeHandler.middleware = [{ _ in + .failure(NSError.hostNotFound) + }] + let tab1 = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration) + let tab2 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let tabsViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab1, tab2])) + window = WindowsManager.openNewWindow(with: tabsViewModel)! + + // wait for error page to open + let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise() + let errorNavigationFinished = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + _=try await eNavigationFailed.value + _=try await errorNavigationFinished.value + + // switch to tab 2 + tabsViewModel.select(at: .unpinned(1)) + + // coming back to the failing tab 1 should not trigger reload + let c = tab1.$isLoading.filter { $0 == true }.sink { isLoading in + XCTFail("Failing tab shouldn‘t reload again (isLoading: \(isLoading))") + } + tabsViewModel.select(at: .unpinned(0)) + + try await Task.sleep(interval: 0.4) // sleep a little to confirm no more navigations are performed + withExtendedLifetime(c) {} + } + + func testWhenGoingBackToFailingPage_reloadIsTriggered() async throws { + // open Tab with newtab page + // navigate to a failing url right away + schemeHandler.middleware = [{ _ in + .failure(NSError.hostNotFound) + }] + let tab = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration) + window = WindowsManager.openNewWindow(with: tab)! + + // wait for navigation to fail + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let errorNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + _=try await eNavigationFailed.value + _=try await errorNavigationFinished.value + + let ePageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + // navigate to test url: success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.alternativeHtml)) + }] + tab.setContent(.url(.alternative, source: .userEntered(URL.test.absoluteString))) + + try await ePageLoaded.value + + // navigate back to failing page: success + let eBackPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + let eRequestSent = expectation(description: "request sent") + schemeHandler.middleware = [{ _ in + eRequestSent.fulfill() + return .ok(.html(Self.testHtml)) + }] + tab.goBack() + try await eBackPageLoaded.value + await fulfillment(of: [eRequestSent]) + + let titleText: String? = try await tab.webView.evaluateJavaScript("document.getElementsByTagName('title')[0].innerText") + XCTAssertEqual(titleText, Self.pageTitle) + XCTAssertEqual(tab.title, titleText) + XCTAssertEqual(tab.currentHistoryItem?.url, .test) + XCTAssertEqual(tab.currentHistoryItem?.title, titleText) + + XCTAssertEqual(tab.backHistoryItems.count, 0) + XCTAssertFalse(tab.canGoBack) + + XCTAssertEqual(tab.forwardHistoryItems.count, 1) + XCTAssertEqual(tab.forwardHistoryItems.first?.url, .alternative) + XCTAssertEqual(tab.forwardHistoryItems.first?.title, Self.alternativeTitle) + XCTAssertTrue(tab.canGoForward) + + XCTAssertTrue(tab.canReload) + } + + func testWhenGoingBackToFailingPageAndItFailsAgain_errorPageIsUpdated() async throws { + // open Tab with newtab page + // navigate to a failing url right away + schemeHandler.middleware = [{ _ in + .failure(NSError.hostNotFound) + }] + let tab = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration) + window = WindowsManager.openNewWindow(with: tab)! + + // wait for navigation to fail + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let errorNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + _=try await eNavigationFailed.value + _=try await errorNavigationFinished.value + + let ePageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + // navigate to test url: success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.alternativeHtml)) + }] + tab.setContent(.url(.alternative, source: .userEntered(URL.test.absoluteString))) + + try await ePageLoaded.value + + // navigate back to failing page: failure + let eBackPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + let eNavigationFailed2 = tab.$error.compactMap { $0 }.filter { + $0.errorCode == NSError.noConnection.code + }.timeout(5).first().promise() + + schemeHandler.middleware = [{ _ in + .failure(NSError.noConnection) + }] + tab.goBack() + _=try await eNavigationFailed2.value + _=try await eBackPageLoaded.value + + let headerText: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-header')[0].innerText") + let errorDescr: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-description')[0].innerText") + XCTAssertNil(tab.title) + XCTAssertEqual(tabViewModel.title, UserText.tabErrorTitle) + XCTAssertEqual(headerText?.trimmingWhitespace(), UserText.errorPageHeader) + XCTAssertEqual(errorDescr?.trimmingWhitespace(), NSError.noConnection.localizedDescription) + + XCTAssertEqual(tab.currentHistoryItem?.url, .test) + XCTAssertNil(tab.currentHistoryItem?.title) + + XCTAssertEqual(tab.backHistoryItems.count, 0) + XCTAssertFalse(tab.canGoBack) + + XCTAssertEqual(tab.forwardHistoryItems.count, 1) + XCTAssertEqual(tab.forwardHistoryItems.first?.url, .alternative) + XCTAssertEqual(tab.forwardHistoryItems.first?.title, Self.alternativeTitle) + XCTAssertTrue(tab.canGoForward) + + XCTAssertTrue(tab.canReload) + } + + func testWhenPageLoadedAndFailsOnRefreshAndOnConsequentRefresh_errorPageIsUpdatedKeepingForwardHistory() async throws { + // open Tab with newtab page + let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // navigate to test url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.testHtml)) + }] + let eNavigationFinished2 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + _=try await eNavigationFinished2.value + + // navigate to another url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.alternativeHtml)) + }] + let eNavigationFinished3 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.alternative, source: .userEntered(URL.alternative.absoluteString))) + _=try await eNavigationFinished3.value + + // navigate back + let eBackNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.goBack() + _=try await eBackNavigationFinished.value + + // refresh: fail + schemeHandler.middleware = [{ _ in + .failure(NSError.noConnection) + }] + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished4 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tab.reload() + _=try await eNavigationFailed.value + _=try await eNavigationFinished4.value + + // refresh again: fail + let eServerQueried = expectation(description: "server request sent") + schemeHandler.middleware = [{ _ in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + eServerQueried.fulfill() + } + return .failure(NSError.connectionLost) + }] + let eNavigationFailed2 = tab.$error.compactMap { $0 }.timeout(5).first().promise() + + tab.reload() + _=try await eNavigationFailed2.value + await fulfillment(of: [eServerQueried]) + + let headerText: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-header')[0].innerText") + let errorDescr: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-description')[0].innerText") + XCTAssertNil(tab.title) + XCTAssertEqual(tabViewModel.title, UserText.tabErrorTitle) + XCTAssertEqual(headerText?.trimmingWhitespace(), UserText.errorPageHeader) + XCTAssertEqual(errorDescr?.trimmingWhitespace(), NSError.connectionLost.localizedDescription) + + XCTAssertEqual(tab.currentHistoryItem?.url, .test) + XCTAssertEqual(tab.currentHistoryItem?.title, URL.test.host) + + XCTAssertEqual(tab.backHistoryItems.count, 1) + XCTAssertEqual(tab.backHistoryItems.first?.url, .newtab, "url") + XCTAssertTrue(tab.canGoBack) + + XCTAssertEqual(tab.forwardHistoryItems.count, 1) + XCTAssertEqual(tab.forwardHistoryItems.first?.url, .alternative, "url") + XCTAssertEqual(tab.forwardHistoryItems.first?.title, Self.alternativeTitle, "title") + XCTAssertTrue(tab.canGoForward) + + XCTAssertTrue(tab.canReload) + } + + func testWhenPageLoadedAndFailsOnRefreshAndSucceedsOnConsequentRefresh_forwardHistoryIsPreserved() async throws { + // open Tab with newtab page + let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // navigate to test url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.testHtml)) + }] + let eNavigationFinished2 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + _=try await eNavigationFinished2.value + + // navigate to another url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.alternativeHtml)) + }] + let eNavigationFinished3 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.alternative, source: .userEntered(URL.alternative.absoluteString))) + _=try await eNavigationFinished3.value + + // navigate back + let eBackNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.goBack() + _=try await eBackNavigationFinished.value + + // refresh: fail + schemeHandler.middleware = [{ _ in + .failure(NSError.noConnection) + }] + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished4 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tab.reload() + _=try await eNavigationFailed.value + _=try await eNavigationFinished4.value + + // refresh again: success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.testHtml)) + }] + let eNavigationFinished5 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tab.reload() + _=try await eNavigationFinished5.value + + let titleText: String? = try await tab.webView.evaluateJavaScript("document.getElementsByTagName('title')[0].innerText") + XCTAssertEqual(tab.title, Self.pageTitle) + XCTAssertEqual(titleText?.trimmingWhitespace(), tab.title) + + XCTAssertEqual(tab.currentHistoryItem?.url, .test) + XCTAssertEqual(tab.currentHistoryItem?.title, Self.pageTitle) + + XCTAssertEqual(tab.backHistoryItems.count, 1) + XCTAssertEqual(tab.backHistoryItems.first?.url, .newtab) + XCTAssertTrue(tab.canGoBack) + + XCTAssertEqual(tab.forwardHistoryItems.count, 1) + XCTAssertEqual(tab.forwardHistoryItems.first?.url, .alternative) + XCTAssertEqual(tab.forwardHistoryItems.first?.title, Self.alternativeTitle) + XCTAssertTrue(tab.canGoForward) + + XCTAssertTrue(tab.canReload) + } + + func testWhenReloadingBySubmittingSameURL_errorPageRemainsSame() async throws { + // open Tab with newtab page + let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // navigate to test url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.testHtml)) + }] + let eNavigationFinished2 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + _=try await eNavigationFinished2.value + + // navigate to another url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.alternativeHtml)) + }] + let eNavigationFinished3 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.alternative, source: .userEntered(URL.alternative.absoluteString))) + _=try await eNavigationFinished3.value + + // navigate back + let eBackNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.goBack() + _=try await eBackNavigationFinished.value + + // refresh: fail + schemeHandler.middleware = [{ _ in + .failure(NSError.noConnection) + }] + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished4 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + _=try await eNavigationFailed.value + _=try await eNavigationFinished4.value + + // refresh again: fail + let eServerQueried = expectation(description: "server request sent") + schemeHandler.middleware = [{ _ in + eServerQueried.fulfill() + return .failure(NSError.connectionLost) + }] + let eNavigationFailed2 = tab.$error.compactMap { $0 }.timeout(5).first().promise() + + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + _=try await eNavigationFailed2.value + await fulfillment(of: [eServerQueried]) + + let headerText: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-header')[0].innerText") + let errorDescr: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-description')[0].innerText") + XCTAssertNil(tab.title) + XCTAssertEqual(tabViewModel.title, UserText.tabErrorTitle) + XCTAssertEqual(headerText?.trimmingWhitespace(), UserText.errorPageHeader) + XCTAssertEqual(errorDescr?.trimmingWhitespace(), NSError.connectionLost.localizedDescription) + + XCTAssertEqual(tab.currentHistoryItem?.url, .test) + XCTAssertEqual(tab.currentHistoryItem?.title, URL.test.host) + + XCTAssertEqual(tab.backHistoryItems.count, 1) + XCTAssertEqual(tab.backHistoryItems.first?.url, .newtab, "url") + XCTAssertNil(tab.backHistoryItems.first?.title, "title") + XCTAssertTrue(tab.canGoBack) + + XCTAssertEqual(tab.forwardHistoryItems.count, 1) + XCTAssertEqual(tab.forwardHistoryItems.first?.url, .alternative, "url") + XCTAssertEqual(tab.forwardHistoryItems.first?.title, Self.alternativeTitle, "title") + XCTAssertTrue(tab.canGoForward) + + XCTAssertTrue(tab.canReload) + } + + func testWhenGoingToAnotherUrlFails_newBackForwardHistoryItemIsAdded() async throws { + // open Tab with newtab page + let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // navigate to test url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.testHtml)) + }] + let eNavigationFinished2 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + _=try await eNavigationFinished2.value + + // navigate to another url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.alternativeHtml)) + }] + let eNavigationFinished3 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.alternative, source: .userEntered(URL.alternative.absoluteString))) + _=try await eNavigationFinished3.value + + // navigate back + let eBackNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.goBack() + _=try await eBackNavigationFinished.value + + // refresh: fail + schemeHandler.middleware = [{ _ in + .failure(NSError.noConnection) + }] + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished4 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + _=try await eNavigationFailed.value + _=try await eNavigationFinished4.value + + // go to another url: fail + let eServerQueried = expectation(description: "server request sent") + schemeHandler.middleware = [{ _ in + eServerQueried.fulfill() + return .failure(NSError.connectionLost) + }] + let eNavigationFailed2 = tab.$error.compactMap { $0 }.timeout(5).first().promise() + + tab.setContent(.url(.alternative, source: .userEntered(URL.alternative.absoluteString))) + _=try await eNavigationFailed2.value + await fulfillment(of: [eServerQueried]) + + let headerText: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-header')[0].innerText") + let errorDescr: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-description')[0].innerText") + XCTAssertNil(tab.title) + XCTAssertEqual(tabViewModel.title, UserText.tabErrorTitle) + XCTAssertEqual(headerText?.trimmingWhitespace(), UserText.errorPageHeader) + XCTAssertEqual(errorDescr?.trimmingWhitespace(), NSError.connectionLost.localizedDescription) + + XCTAssertEqual(tab.currentHistoryItem?.url, .alternative) + XCTAssertNil(tab.currentHistoryItem?.title) + + XCTAssertEqual(tab.backHistoryItems.count, 2) + XCTAssertEqual(tab.backHistoryItems[safe: 0]?.url, .newtab) + XCTAssertNil(tab.backHistoryItems[safe: 0]?.title) + XCTAssertEqual(tab.backHistoryItems[safe: 1]?.url, .test) + XCTAssertEqual(tab.backHistoryItems[safe: 1]?.title, Self.pageTitle) + XCTAssertTrue(tab.canGoBack) + + XCTAssertEqual(tab.forwardHistoryItems.count, 0) + XCTAssertFalse(tab.canGoForward) + + XCTAssertTrue(tab.canReload) + } + + func testWhenGoingToAnotherUrlSucceeds_newBackForwardHistoryItemIsAdded() async throws { + // open Tab with newtab page + let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // navigate to test url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.testHtml)) + }] + let eNavigationFinished2 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + _=try await eNavigationFinished2.value + + // navigate to another url, success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.alternativeHtml)) + }] + let eNavigationFinished3 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.setContent(.url(.alternative, source: .userEntered(URL.alternative.absoluteString))) + _=try await eNavigationFinished3.value + + // navigate back + let eBackNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + tab.goBack() + _=try await eBackNavigationFinished.value + + // refresh: fail + schemeHandler.middleware = [{ _ in + .failure(NSError.noConnection) + }] + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished4 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString))) + _=try await eNavigationFailed.value + _=try await eNavigationFinished4.value + + // go to another url: success + schemeHandler.middleware = [{ _ in + .ok(.html(Self.alternativeHtml)) + }] + let eNavigationFinished5 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tab.setContent(.url(.alternative, source: .userEntered(URL.alternative.absoluteString))) + _=try await eNavigationFinished5.value + + let titleText: String? = try await tab.webView.evaluateJavaScript("document.getElementsByTagName('title')[0].innerText") + XCTAssertEqual(tab.title, Self.alternativeTitle) + XCTAssertEqual(titleText?.trimmingWhitespace(), tab.title) + + XCTAssertEqual(tab.currentHistoryItem?.url, .alternative) + XCTAssertEqual(tab.currentHistoryItem?.title, Self.alternativeTitle) + + XCTAssertEqual(tab.backHistoryItems.count, 2) + XCTAssertEqual(tab.backHistoryItems[safe: 0]?.url, .newtab) + XCTAssertNil(tab.backHistoryItems[safe: 0]?.title) + XCTAssertEqual(tab.backHistoryItems[safe: 1]?.url, .test) + XCTAssertEqual(tab.backHistoryItems[safe: 1]?.title, Self.pageTitle) + XCTAssertTrue(tab.canGoBack) + + XCTAssertEqual(tab.forwardHistoryItems.count, 0) + XCTAssertFalse(tab.canGoForward) + + XCTAssertTrue(tab.canReload) + } + + func testWhenLoadingFailsAfterSessionRestoration_navigationHistoryIsPreserved() async throws { + schemeHandler.middleware = [{ _ in + .failure(NSError.noConnection) + }] + + let tab = Tab(content: .url(.test, source: .pendingStateRestoration), webViewConfiguration: webViewConfiguration, interactionStateData: Self.sessionStateData) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + XCTAssertTrue(tab.canReload) + + schemeHandler.middleware = [{ _ in + .ok(.html(Self.testHtml)) + }] + // open new tab + viewModel.appendNewTab() + + // select the failing tab triggering its reload + let eReloadFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + viewModel.select(at: .unpinned(0)) + _=try await eReloadFinished.value + + XCTAssertEqual(tab.currentHistoryItem?.url, .test) + XCTAssertEqual(tab.currentHistoryItem?.title, Self.pageTitle) + + XCTAssertEqual(tab.backHistoryItems.count, 1) + XCTAssertEqual(tab.backHistoryItems.first?.url, .newtab) + XCTAssertEqual(tab.backHistoryItems.first?.title ?? "", "") + XCTAssertTrue(tab.canGoBack) + + XCTAssertEqual(tab.forwardHistoryItems.count, 1) + XCTAssertEqual(tab.forwardHistoryItems.first?.url, .alternative) + XCTAssertEqual(tab.forwardHistoryItems.first?.title, Self.alternativeTitle) + XCTAssertTrue(tab.canGoForward) + + XCTAssertTrue(tab.canReload) + } + + func testPinnedTabDoesNotNavigateAway() async throws { + schemeHandler.middleware = [{ _ in + .ok(.html(Self.testHtml)) + }] + + let tab = Tab(content: .url(.alternative, source: .ui), webViewConfiguration: webViewConfiguration) + let manager = PinnedTabsManager() + manager.pin(tab) + + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: []), pinnedTabsManager: manager) + window = WindowsManager.openNewWindow(with: viewModel)! + viewModel.select(at: .pinned(0)) + + // wait for tab to load + let eNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + _=try await eNavigationFinished.value + + // refresh: fail + schemeHandler.middleware = [{ _ in + .failure(NSError.noConnection) + }] + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished2 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tab.reload() + _=try await eNavigationFailed.value + _=try await eNavigationFinished2.value + + XCTAssertNotNil(tab.error) + + schemeHandler.middleware = [{ _ in + .ok(.html(Self.testHtml)) + }] + let eNavigationFinished5 = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + tab.reload() + _=try await eNavigationFinished5.value + + XCTAssertNil(tab.error) + XCTAssertEqual(viewModel.tabs.count, 1) + } + + func testWhenPageFailsToLoadAfterRedirect_errorPageShown() async throws { + // open Tab with newtab page + let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + window = WindowsManager.openNewWindow(with: viewModel)! + let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + try await eNewtabPageLoaded.value + + // navigate to alt url, redirect to test url, fail with error + schemeHandler.middleware = [{ request in + .init { task in + let response = URLResponse(url: request.url!, mimeType: nil, expectedContentLength: 0, textEncodingName: nil) + let newRequest = URLRequest(url: .test) + task._didPerformRedirection(response, newRequest: newRequest) + + task.didFailWithError(NSError.hostNotFound) + } + }] + tab.setContent(.url(.test, source: .userEntered(URL.alternative.absoluteString))) + + let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise() + let eNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + let error = try await eNavigationFailed.value + _=try await eNavigationFinished.value + + XCTAssertEqual(error.errorCode, NSError.hostNotFound.code) + XCTAssertEqual(error.localizedDescription, NSError.hostNotFound.localizedDescription) + let headerText: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-header')[0].innerText") + let errorDescr: String? = try await tab.webView.evaluateJavaScript("document.getElementsByClassName('error-description')[0].innerText") + + XCTAssertNil(tab.title) + XCTAssertEqual(tabViewModel.title, UserText.tabErrorTitle) + XCTAssertEqual(headerText?.trimmingWhitespace(), UserText.errorPageHeader) + XCTAssertEqual(errorDescr?.trimmingWhitespace(), NSError.hostNotFound.localizedDescription) + XCTAssertTrue(tab.canGoBack) + XCTAssertFalse(tab.canGoForward) + XCTAssertTrue(tab.canReload) + XCTAssertFalse(viewModel.tabViewModel(at: 0)!.canSaveContent) + XCTAssertEqual(tab.backHistoryItems.count, 1) + XCTAssertEqual(tab.backHistoryItems.first?.url, .newtab) + XCTAssertNil(tab.currentHistoryItem?.title) + XCTAssertEqual(tab.currentHistoryItem?.url, .test) + XCTAssertEqual(tab.content.userEditableUrl, .test) + } + +} + +private extension URL { + static let test = URL(string: "https://test.com/")! + static let alternative = URL(string: "https://alternative.com/")! +} + +private extension NSError { + + static let hostNotFound: NSError = { + let errorCode = -1003 + let errorDescription = "hostname not found" + let wkError = NSError(domain: NSURLErrorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription]) + return wkError + }() + + static let noConnection: NSError = { + let errorDescription = "no internet connection" + return URLError(.notConnectedToInternet, userInfo: [NSLocalizedDescriptionKey: errorDescription]) as NSError + }() + + static let connectionLost: NSError = { + let errorDescription = "connection lost" + return URLError(.networkConnectionLost, userInfo: [NSLocalizedDescriptionKey: errorDescription]) as NSError + }() + +} + +extension Data { + + static let sessionRestorationMagic = Data([0x00, 0x00, 0x00, 0x02]) + +} diff --git a/IntegrationTests/TabExtensions/SearchNonexistentDomainTests.swift b/IntegrationTests/Tab/SearchNonexistentDomainTests.swift similarity index 77% rename from IntegrationTests/TabExtensions/SearchNonexistentDomainTests.swift rename to IntegrationTests/Tab/SearchNonexistentDomainTests.swift index 778ee77770..346231ede7 100644 --- a/IntegrationTests/TabExtensions/SearchNonexistentDomainTests.swift +++ b/IntegrationTests/Tab/SearchNonexistentDomainTests.swift @@ -92,28 +92,27 @@ final class SearchNonexistentDomainTests: XCTestCase { let tab = Tab(content: .none, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) window = WindowsManager.openNewWindow(with: tab)! - let eRedirected = Future { promise in - self.schemeHandler.middleware = [{ request in - if request.url!.isDuckDuckGoSearch { - promise(.success(request.url!)) - return .ok(.html("")) - } else { - return .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) - } - }] - }.timeout(3).first().promise() - let url = urls.invalidTLD let enteredString = url.absoluteString.dropping(prefix: url.navigationalScheme!.separated()) + let eRedirected = expectation(description: "Redirected to SERP") + self.schemeHandler.middleware = [{ request in + if request.url!.isDuckDuckGoSearch { + XCTAssertEqual(request.url, URL.makeSearchUrl(from: enteredString)) + eRedirected.fulfill() + return .ok(.html("")) + } else { + return .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) + } + }] + addressBar.makeMeFirstResponder() addressBar.stringValue = enteredString NSApp.swizzled_currentEvent = NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [], timestamp: Date().timeIntervalSinceReferenceDate, windowNumber: 0, context: nil, characters: "\n", charactersIgnoringModifiers: "", isARepeat: false, keyCode: UInt16(kVK_Return))! _=addressBar.control(addressBar, textView: addressBar.currentEditor() as! NSTextView, doCommandBy: #selector(NSResponder.insertNewline)) - let redirectUrl = try await eRedirected.value - XCTAssertEqual(redirectUrl, URL.makeSearchUrl(from: enteredString)) + await fulfillment(of: [eRedirected], timeout: 3) } @MainActor @@ -121,14 +120,20 @@ final class SearchNonexistentDomainTests: XCTestCase { let tab = Tab(content: .none, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) window = WindowsManager.openNewWindow(with: tab)! - self.schemeHandler.middleware = [{ _ in - .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) + self.schemeHandler.middleware = [{ request in + XCTAssertFalse(request.url!.isDuckDuckGoSearch) + return .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) }] let eNavigationFailed = tab.$error .compactMap { $0 } .timeout(3) .first() .promise() + // error page navigation + let eNavigationDidFinish = tab.webViewDidFinishNavigationPublisher + .timeout(3) + .first() + .promise() let url = urls.validTLD let enteredString = url.absoluteString.dropping(prefix: url.navigationalScheme!.separated()) @@ -140,6 +145,7 @@ final class SearchNonexistentDomainTests: XCTestCase { _=addressBar.control(addressBar, textView: addressBar.currentEditor() as! NSTextView, doCommandBy: #selector(NSResponder.insertNewline)) let error = try await eNavigationFailed.value + _=try await eNavigationDidFinish.value XCTAssertEqual(error.errorCode, NSURLErrorCannotFindHost) } @@ -148,14 +154,20 @@ final class SearchNonexistentDomainTests: XCTestCase { let tab = Tab(content: .none, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) window = WindowsManager.openNewWindow(with: tab)! - self.schemeHandler.middleware = [{ _ in - .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) + self.schemeHandler.middleware = [{ request in + XCTAssertFalse(request.url!.isDuckDuckGoSearch) + return .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) }] let eNavigationFailed = tab.$error .compactMap { $0 } .timeout(3) .first() .promise() + // error page navigation + let eNavigationDidFinish = tab.webViewDidFinishNavigationPublisher + .timeout(3) + .first() + .promise() let url = urls.invalidTLD let enteredString = url.absoluteString @@ -167,6 +179,7 @@ final class SearchNonexistentDomainTests: XCTestCase { _=addressBar.control(addressBar, textView: addressBar.currentEditor() as! NSTextView, doCommandBy: #selector(NSResponder.insertNewline)) let error = try await eNavigationFailed.value + _=try await eNavigationDidFinish.value XCTAssertEqual(error.errorCode, NSURLErrorCannotFindHost) } @@ -175,11 +188,17 @@ final class SearchNonexistentDomainTests: XCTestCase { let tab = Tab(content: .none, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) window = WindowsManager.openNewWindow(with: tab)! - self.schemeHandler.middleware = [{ _ in - .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) + self.schemeHandler.middleware = [{ request in + XCTAssertFalse(request.url!.isDuckDuckGoSearch) + return .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) }] let eNavigationFailed = tab.$error .compactMap { $0 } + .timeout(3) + .first() + .promise() + // error page navigation + let eNavigationDidFinish = tab.webViewDidFinishNavigationPublisher .timeout(10) .first() .promise() @@ -189,6 +208,7 @@ final class SearchNonexistentDomainTests: XCTestCase { tab.setUrl(url, source: .link) let error = try await eNavigationFailed.value + _=try await eNavigationDidFinish.value XCTAssertEqual(error.errorCode, NSURLErrorCannotFindHost) } @@ -197,20 +217,21 @@ final class SearchNonexistentDomainTests: XCTestCase { let tab = Tab(content: .none, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) window = WindowsManager.openNewWindow(with: tab)! - let eRedirected = Future { promise in - self.schemeHandler.middleware = [{ request in - if request.url!.isDuckDuckGoSearch { - promise(.success(request.url!)) - return .ok(.html("")) - } else { - return .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) - } - }] - }.timeout(3).first().promise() - let url = urls.invalidTLD let enteredString = url.absoluteString.dropping(prefix: url.navigationalScheme!.separated()) + let eRedirected = expectation(description: "Redirected to SERP") + self.schemeHandler.middleware = [{ request in + if request.url!.isDuckDuckGoSearch { + XCTAssertEqual(request.url, URL.makeSearchUrl(from: enteredString)) + eRedirected.fulfill() + + return .ok(.html("")) + } else { + return .failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)) + } + }] + addressBar.makeMeFirstResponder() addressBar.stringValue = enteredString @@ -223,8 +244,7 @@ final class SearchNonexistentDomainTests: XCTestCase { addressBar.suggestionViewControllerDidConfirmSelection(addressBar.suggestionViewController) - let redirectUrl = try await eRedirected.value - XCTAssertEqual(redirectUrl, URL.makeSearchUrl(from: enteredString)) + await fulfillment(of: [eRedirected], timeout: 3) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift index 857f174360..52eabbae92 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift @@ -290,4 +290,26 @@ extension InMemoryDataCache: DBPUICommunicationDelegate { return mapper.maintenanceScanState(brokerProfileQueryData) } + + func getDataBrokers() async -> [DBPUIDataBroker] { + brokerProfileQueryData + // 1. We get all brokers (in this list brokers are repeated) + .map { $0.dataBroker } + // 2. We map the brokers to the UI model + .flatMap { dataBroker -> [DBPUIDataBroker] in + var result: [DBPUIDataBroker] = [] + result.append(DBPUIDataBroker(name: dataBroker.name, url: dataBroker.url)) + + for mirrorSite in dataBroker.mirrorSites { + result.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url)) + } + return result + } + // 3. We delete duplicates + .reduce(into: [DBPUIDataBroker]()) { (result, dataBroker) in + if !result.contains(where: { $0.url == dataBroker.url }) { + result.append(dataBroker) + } + } + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserView.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserView.swift index 18b5ffa6b6..fd09e77417 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserView.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserView.swift @@ -49,6 +49,7 @@ struct DatabaseView: View { @State private var isPopoverVisible = false @State private var selectedData: String = "" let data: [DataBrokerDatabaseBrowserData.Row] + let rowHeight: CGFloat = 40.0 var body: some View { if data.count > 0 { @@ -62,6 +63,11 @@ struct DatabaseView: View { } } + private func spacerHeight(_ geometry: GeometryProxy) -> CGFloat { + let result = geometry.size.height - CGFloat(data.count) * rowHeight + return max(0, result) + } + private func dataView() -> some View { GeometryReader { geometry in ScrollView([.horizontal, .vertical]) { @@ -86,7 +92,8 @@ struct DatabaseView: View { ForEach(row.data.keys.sorted(), id: \.self) { key in VStack { Text("\(row.data[key]?.description ?? "")") - .frame(maxWidth: 200, maxHeight: 50) + .frame(maxWidth: 200) + .frame(height: rowHeight) .frame(minWidth: 60) .onTapGesture { selectedData = row.data[key]?.description ?? "" @@ -100,7 +107,8 @@ struct DatabaseView: View { } } } - Spacer(minLength: geometry.size.height) + Spacer() + .frame(height: spacerHeight(geometry)) } .frame(minWidth: geometry.size.width, minHeight: 0, alignment: .topLeading) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index 4a6f58ac28..2aa3953437 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -17,9 +17,9 @@ // import Combine +import Common import Foundation import XPCHelper -import Common /// This protocol describes the server-side IPC interface for controlling the tunnel /// @@ -150,6 +150,17 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { // If you add a completion block, please remember to call it here too! }) } + + public func openBrowser(domain: String) { + self.pixelHandler.fire(.ipcServerRunAllOperations) + xpc.execute(call: { server in + server.openBrowser(domain: domain) + }, xpcReplyErrorHandler: { error in + os_log("Error \(error.localizedDescription)") + // Intentional no-op as there's no completion block + // If you add a completion block, please remember to call it here too! + }) + } } // MARK: - Incoming communication from the server diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift index fde0274a5f..a2bc3d0e56 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift @@ -42,6 +42,12 @@ public protocol IPCServerInterface: AnyObject { func scanAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runQueuedOperations(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runAllOperations(showWebView: Bool) + + // MARK: - Debugging Features + + /// Opens a browser window with the specified domain + /// + func openBrowser(domain: String) } /// This protocol describes the server-side XPC interface. @@ -71,6 +77,12 @@ protocol XPCServerInterface { func scanAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runQueuedOperations(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runAllOperations(showWebView: Bool) + + // MARK: - Debugging Features + + /// Opens a browser window with the specified domain + /// + func openBrowser(domain: String) } public final class DataBrokerProtectionIPCServer { @@ -146,4 +158,8 @@ extension DataBrokerProtectionIPCServer: XPCServerInterface { func runAllOperations(showWebView: Bool) { serverDelegate?.runAllOperations(showWebView: showWebView) } + + func openBrowser(domain: String) { + serverDelegate?.openBrowser(domain: domain) + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift index 353e3912df..5f242b34a2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift @@ -103,12 +103,24 @@ struct DBPUIAddressAtIndex: Codable { /// Message Object representing a data broker struct DBPUIDataBroker: Codable, Hashable { let name: String + let url: String + let date: Double? + + init(name: String, url: String, date: Double? = nil) { + self.name = name + self.url = url + self.date = date + } func hash(into hasher: inout Hasher) { hasher.combine(name) } } +struct DBPUIDataBrokerList: DBPUISendableMessage { + let dataBrokers: [DBPUIDataBroker] +} + /// Message Object representing a requested change to the user profile's brith year struct DBPUIBirthYear: Codable { let year: Int @@ -123,6 +135,7 @@ struct DBPUIDataBrokerProfileMatch: Codable { let addresses: [DBPUIUserProfileAddress] let alternativeNames: [String] let relatives: [String] + let date: Double? // Used in some methods to set the removedDate or found date } /// Protocol to represent a message that can be passed from the host to the UI @@ -139,6 +152,10 @@ struct DBPUIScanAndOptOutMaintenanceState: DBPUISendableMessage { struct DBPUIOptOutMatch: DBPUISendableMessage { let dataBroker: DBPUIDataBroker let matches: Int + let name: String + let alternativeNames: [String] + let addresses: [DBPUIUserProfileAddress] + let date: Double } /// Data representing the initial scan progress diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift index fb96638206..e1a17f590b 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift @@ -32,13 +32,46 @@ extension Int { struct MirrorSite: Codable, Sendable { let name: String + let url: String let addedAt: Date let removedAt: Date? + + enum CodingKeys: CodingKey { + case name + case url + case addedAt + case removedAt + } + + init(name: String, url: String, addedAt: Date, removedAt: Date? = nil) { + self.name = name + self.url = url + self.addedAt = addedAt + self.removedAt = removedAt + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + name = try container.decode(String.self, forKey: .name) + + // The older versions of the JSON file did not have a URL property. + // When decoding those cases, we fallback to its name, since the name was the URL. + do { + url = try container.decode(String.self, forKey: .url) + } catch { + url = name + } + + addedAt = try container.decode(Date.self, forKey: .addedAt) + removedAt = try? container.decode(Date.self, forKey: .removedAt) + + } } struct DataBroker: Codable, Sendable { let id: Int64? let name: String + let url: String let steps: [Step] let version: String let schedulingConfig: DataBrokerScheduleConfig @@ -51,6 +84,7 @@ struct DataBroker: Codable, Sendable { enum CodingKeys: CodingKey { case name + case url case steps case version case schedulingConfig @@ -60,6 +94,7 @@ struct DataBroker: Codable, Sendable { init(id: Int64? = nil, name: String, + url: String, steps: [Step], version: String, schedulingConfig: DataBrokerScheduleConfig, @@ -68,6 +103,13 @@ struct DataBroker: Codable, Sendable { ) { self.id = id self.name = name + + if url.isEmpty { + self.url = name + } else { + self.url = url + } + self.steps = steps self.version = version self.schedulingConfig = schedulingConfig @@ -78,6 +120,15 @@ struct DataBroker: Codable, Sendable { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decode(String.self, forKey: .name) + + // The older versions of the JSON file did not have a URL property. + // When decoding those cases, we fallback to its name, since the name was the URL. + do { + url = try container.decode(String.self, forKey: .url) + } catch { + url = name + } + version = try container.decode(String.self, forKey: .version) steps = try container.decode([Step].self, forKey: .steps) schedulingConfig = try container.decode(DataBrokerScheduleConfig.self, forKey: .schedulingConfig) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift index 29757bc891..1fd0366c8e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift @@ -77,6 +77,7 @@ struct ExtractedProfile: Codable, Sendable { var email: String? var removedDate: Date? let fullName: String? + let identifier: String? enum CodingKeys: CodingKey { case id @@ -92,6 +93,7 @@ struct ExtractedProfile: Codable, Sendable { case email case removedDate case fullName + case identifier } init(id: Int64? = nil, @@ -105,7 +107,8 @@ struct ExtractedProfile: Codable, Sendable { reportId: String? = nil, age: String? = nil, email: String? = nil, - removedDate: Date? = nil) { + removedDate: Date? = nil, + identifier: String? = nil) { self.id = id self.name = name self.alternativeNames = alternativeNames @@ -119,6 +122,29 @@ struct ExtractedProfile: Codable, Sendable { self.email = email self.removedDate = removedDate self.fullName = name + self.identifier = identifier + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decodeIfPresent(Int64.self, forKey: .id) + name = try container.decodeIfPresent(String.self, forKey: .name) + alternativeNames = try container.decodeIfPresent([String].self, forKey: .alternativeNames) + addressFull = try container.decodeIfPresent(String.self, forKey: .addressFull) + addresses = try container.decodeIfPresent([AddressCityState].self, forKey: .addresses) + phoneNumbers = try container.decodeIfPresent([String].self, forKey: .phoneNumbers) + relatives = try container.decodeIfPresent([String].self, forKey: .relatives) + profileUrl = try container.decode(String.self, forKey: .profileUrl) + reportId = try container.decodeIfPresent(String.self, forKey: .reportId) + age = try container.decodeIfPresent(String.self, forKey: .age) + email = try container.decodeIfPresent(String.self, forKey: .email) + removedDate = try container.decodeIfPresent(Date.self, forKey: .removedDate) + fullName = try container.decodeIfPresent(String.self, forKey: .fullName) + if let identifier = try container.decodeIfPresent(String.self, forKey: .identifier) { + self.identifier = identifier + } else { + self.identifier = profileUrl + } } func merge(with profile: ProfileQuery) -> ExtractedProfile { @@ -134,7 +160,8 @@ struct ExtractedProfile: Codable, Sendable { reportId: self.reportId, age: self.age ?? String(profile.age), email: self.email, - removedDate: self.removedDate + removedDate: self.removedDate, + identifier: self.identifier ) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift index 5777596838..37cd385e4e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift @@ -99,7 +99,7 @@ extension DataBrokerOperation { if action.needsEmail { do { stageCalculator?.setStage(.emailGenerate) - extractedProfile?.email = try await emailService.getEmail(dataBrokerName: query.dataBroker.name) + extractedProfile?.email = try await emailService.getEmail(dataBrokerURL: query.dataBroker.url) stageCalculator?.fireOptOutEmailGenerate() } catch { await onError(error: DataBrokerProtectionError.emailError(error as? EmailError)) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index 1b043a6d41..73f08604f6 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -135,10 +135,10 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { // We check if the profile exists in the database. let extractedProfilesForBroker = database.fetchExtractedProfiles(for: brokerId) - let doesProfileExistsInDatabase = extractedProfilesForBroker.contains { $0.profileUrl == extractedProfile.profileUrl } + let doesProfileExistsInDatabase = extractedProfilesForBroker.contains { $0.identifier == extractedProfile.identifier } // If the profile exists we do not create a new opt-out operation - if doesProfileExistsInDatabase, let alreadyInDatabaseProfile = extractedProfilesForBroker.first(where: { $0.profileUrl == extractedProfile.profileUrl }), let id = alreadyInDatabaseProfile.id { + if doesProfileExistsInDatabase, let alreadyInDatabaseProfile = extractedProfilesForBroker.first(where: { $0.identifier == extractedProfile.identifier }), let id = alreadyInDatabaseProfile.id { // If it was removed in the past but was found again when scanning, it means it appearead again, so we reset the remove date. if alreadyInDatabaseProfile.removedDate != nil { database.updateRemovedDate(nil, on: id) @@ -176,7 +176,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { // Check for removed profiles let removedProfiles = brokerProfileQueryData.extractedProfiles.filter { savedProfile in !extractedProfiles.contains { recentlyFoundProfile in - recentlyFoundProfile.profileUrl == savedProfile.profileUrl + recentlyFoundProfile.identifier == savedProfile.identifier } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift index 28c2128a00..f3203334e2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift @@ -152,7 +152,7 @@ public struct DataBrokerProtectionBrokerUpdater { // 2. If does exist, we check the number version, if the version number is new, we update it // 3. If it does not exist, we add it, and we create the scan operations related to it private func update(_ broker: DataBroker) throws { - guard let savedBroker = try vault.fetchBroker(with: broker.name) else { + guard let savedBroker = try vault.fetchBroker(with: broker.url) else { // The broker does not exist in the current storage. We need to add it. try add(broker) return diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index ad3dfda484..9f12bbab0c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -279,7 +279,6 @@ public enum DataBrokerProtectionPixels { } extension DataBrokerProtectionPixels: PixelKitEvent { - public var name: String { switch self { case .parentChildMatches: return "m_mac_dbp_macos_parent-child-broker-matches" diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json index 60823c78ac..d7daac5036 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json @@ -1,8 +1,9 @@ { - "name": "advancedbackgroundchecks.com", - "version": "0.1.0", + "name": "AdvancedBackgroundChecks", + "url": "advancedbackgroundchecks.com", + "version": "0.1.5", "parent": "peoplefinders.com", - "addedDatetime": 1678078800000, + "addedDatetime": 1678060800000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "ef8031e6-5e61-4183-b57e-7df156c7129a", + "id": "7967f064-e3c5-442d-8380-99cf752fb8df", "url": "https://www.advancedbackgroundchecks.com/names/${firstName}-${lastName}_${city}-${state}_age_${age}" }, { "actionType": "extract", - "id": "f3ed744c-6cfc-4a99-b46e-6095587eadfc", + "id": "6f6bb616-a4cb-4231-9abb-522722208f95", "selector": ".card-block", "profile": { "name": { @@ -24,7 +25,8 @@ }, "alternativeNamesList": { "selector": "(.//p[@class='card-text max-lines-1'])[1]", - "afterText": "AKA:" + "afterText": "AKA:", + "separator": "," }, "age": { "selector": ".card-title", @@ -39,7 +41,8 @@ }, "relativesList": { "selector": "(.//p[@class='card-text max-lines-1'])[2]", - "afterText": "Related to:" + "afterText": "Related to:", + "separator": "," }, "profileUrl": { "selector": ".link-to-details" @@ -59,4 +62,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} \ No newline at end of file +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/backgroundcheck.run.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/backgroundcheck.run.json index 5b3800cfcf..552923ca1f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/backgroundcheck.run.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/backgroundcheck.run.json @@ -1,6 +1,7 @@ { "name": "backgroundcheck.run", - "version": "0.1.1", + "url": "backgroundcheck.run", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1677736800000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "aa12b430-8e5d-4c64-bb77-2961f19a1bc8", + "id": "5f90e39f-cb94-4b8d-94ed-48ba0060dc08", "url": "https://backgroundcheck.run/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}" }, { "actionType": "extract", - "id": "75fd2e16-d84a-4bbe-9cf1-79c6d1cc4dec", + "id": "3225fa15-4e00-4e6a-bfc7-a85dfb504c86", "selector": ".b-pfl-list", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json index 130c996369..7050bab137 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json @@ -1,8 +1,9 @@ { - "name": "centeda.com", - "version": "0.1.1", + "name": "Centeda", + "url": "centeda.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677715200000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "25990359-3d58-45de-bdfd-d524b1946e57", + "id": "af9c9f03-e778-4c29-85fc-e5cbbfec563c", "url": "https://centeda.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,8 +25,8 @@ }, { "actionType": "extract", - "id": "7108af78-dbbf-47ec-8bb9-e44be505993e", - "selector": ".search-item", + "id": "79fa2a1c-65b4-417a-a8ac-2ca6d729ffc1", + "selector": ".search-result > a", "profile": { "name": { "selector": ".title", @@ -46,7 +47,7 @@ "selector": ".//div[@class='col-sm-24 col-md-8 related-to']//li" }, "profileUrl": { - "selector": ".get-report-btn" + "selector": "a" } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clubset.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clubset.com.json index f871133c15..8b6801fc48 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clubset.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clubset.com.json @@ -1,6 +1,7 @@ { - "name": "clubset.com", - "version": "0.1.1", + "name": "Clubset", + "url": "clubset.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1702965600000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "917f5d40-2011-4fe5-9ef6-136d6bfaea35", + "id": "5c559c67-c13c-4055-a318-6ba35d62a2cf", "url": "https://clubset.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state|upcase}&city=${city|capitalize}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "06e37215-ef34-4971-bf86-e5a03dfe46e8", + "id": "866bdfc5-069e-4734-9ce0-a19976fa796b", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clustrmaps.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clustrmaps.com.json index 0aca895c02..4c2bd20999 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clustrmaps.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clustrmaps.com.json @@ -1,8 +1,9 @@ { - "name": "clustrmaps.com", - "version": "0.1.1", + "name": "ClustrMaps", + "url": "clustrmaps.com", + "version": "0.1.4", "parent": "neighbor.report", - "addedDatetime": 1692590400000, + "addedDatetime": 1692594000000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "a39655de-5c23-477d-9887-1d34966a1069", + "id": "e6929e37-4764-450a-be2a-73479f11842a", "url": "https://clustrmaps.com/persons/${firstName}-${lastName}/${state|stateFull|capitalize}/${city|hyphenated}" }, { "actionType": "extract", - "id": "4e3a628e-3634-4a2b-b632-4fbb8ce0b52b", + "id": "06f39df7-89c2-40da-b288-cdf3ed0e4bfd", "selector": ".//div[@itemprop='Person']", "profile": { "name": { @@ -55,4 +56,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json index d68f6b9f4c..3df8d7f195 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json @@ -1,6 +1,7 @@ { - "name": "councilon.com", - "version": "0.1.1", + "name": "Councilon", + "url": "councilon.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1702965600000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "295418e5-e1da-43b4-af50-75576ca4f843", + "id": "a5052dda-d4e7-4d3f-97bc-ef9f0aa9ae5f", "url": "https://councilon.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "eead1b72-7d6b-4cdc-988d-5ea66eb398f1", + "id": "55a50a37-9b1b-40fa-8533-af1273a26258", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/curadvisor.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/curadvisor.com.json index 33f27d0c79..3b59ed585e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/curadvisor.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/curadvisor.com.json @@ -1,8 +1,9 @@ { - "name": "curadvisor.com", - "version": "0.1.1", + "name": "CurAdvisor", + "url": "curadvisor.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677736800000, + "addedDatetime": 1703052000000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "bdb69f52-8ece-4d65-9b78-543fef0e90ae", + "id": "ab5503c7-bd11-4320-b38e-c637b239182e", "url": "https://curadvisor.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "3b9bc992-ecc0-4dc5-b716-fcea021cbcdb", + "id": "d273c1cf-2635-40d7-b26f-6f34467282cf", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/cyberbackgroundchecks.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/cyberbackgroundchecks.com.json index e27b744790..f930834db9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/cyberbackgroundchecks.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/cyberbackgroundchecks.com.json @@ -1,6 +1,7 @@ { - "name": "cyberbackgroundchecks.com", - "version": "0.1.1", + "name": "Cyber Background Checks", + "url": "cyberbackgroundchecks.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1705644000000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "4b7037f3-9c6a-42b5-929e-621256e0a044", + "id": "d8c84470-d8b3-4c46-a645-01cc6b139b3b", "url": "https://www.cyberbackgroundchecks.com/people/${firstName}-${lastName}/${state}/${city}" }, { "actionType": "extract", - "id": "f36a73d7-9efb-452e-8c60-6d9df2964bcf", + "id": "b4c12cf2-0fd6-4209-8816-3bf2cce23cde", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/dataveria.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/dataveria.com.json index 3dfe8e5431..9949e04675 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/dataveria.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/dataveria.com.json @@ -1,8 +1,9 @@ { - "name": "dataveria.com", - "version": "0.1.1", + "name": "Dataveria", + "url": "dataveria.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "fc449310-7b7b-45d4-bcf9-0c5d51c246f8", + "id": "a8f3a259-2d39-4ae3-ac13-65aa63a53331", "url": "https://dataveria.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "0481dc49-43e8-4af0-b697-680fb57ec24b", + "id": "e810cc23-2d2a-4e6e-b06f-dfc8a2e1e85d", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/fastbackgroundcheck.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/fastbackgroundcheck.com.json index 4462e0c86d..9c1129a333 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/fastbackgroundcheck.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/fastbackgroundcheck.com.json @@ -1,8 +1,9 @@ { - "name": "fastbackgroundcheck.com", - "version": "0.1.1", + "name": "FastBackgroundCheck.com", + "url": "fastbackgroundcheck.com", + "version": "0.1.4", "parent": "peoplefinders.com", - "addedDatetime": 1678082400000, + "addedDatetime": 1706248800000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "2a3a5979-9de0-44b2-ae03-f25422f0c2aa", + "id": "997adf8d-023c-409e-9206-57871cd25f0a", "url": "https://www.fastbackgroundcheck.com/people/${firstName}-${lastName}/${city}-${state}" }, { "actionType": "extract", - "id": "4818ff1c-d419-44c2-8168-501b456c6c6a", + "id": "2f531e34-2ac0-4743-a760-065187d6c951", "selector": ".person-container", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json index 2c215abdf6..9d114d48b3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json @@ -1,6 +1,7 @@ { - "name": "freepeopledirectory.com", - "version": "0.1.1", + "name": "FreePeopleDirectory", + "url": "freepeopledirectory.com", + "version": "0.1.4", "parent": "spokeo.com", "addedDatetime": 1674540000000, "steps": [ @@ -10,17 +11,21 @@ "actions": [ { "actionType": "navigate", - "id": "815a1cd3-2577-4f43-a163-0cf4d22e66a4", + "id": "b8b912b0-201d-4cd1-8237-235c34fe0fea", "url": "https://www.freepeopledirectory.com/name/${firstName}-${lastName}/${state|upcase}/${city}" }, { "actionType": "extract", - "id": "10738ba0-bc6b-42ba-a37c-487ff3927dd5", + "id": "50e30922-ef1d-4820-abbd-f536378472d4", "selector": ".whole-card", "profile": { "name": { "selector": ".card-title" }, + "alternativeNamesList": { + "selector": ".//h3/span[contains(text(),'AKA:')]/following-sibling::span", + "afterText": "No other aliases." + }, "addressCityState": { "selector": ".city" }, @@ -49,4 +54,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} \ No newline at end of file +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/inforver.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/inforver.com.json index 961bb83ae3..2c035a980c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/inforver.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/inforver.com.json @@ -1,8 +1,9 @@ { - "name": "inforver.com", - "version": "0.1.1", + "name": "Inforver", + "url": "inforver.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "a56ab792-fc1b-4e60-b0b9-0bd4f580476f", + "id": "85fac850-36ad-4d9c-ad7c-c1250c7b5585", "url": "https://inforver.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "591ba784-106c-421b-b188-a376f1f9cb01", + "id": "e5e9c1b0-4af4-4fb6-bd2d-7d026ffd95e7", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json index 31b5dc20a8..5f7e750909 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json @@ -1,6 +1,7 @@ { - "name": "kwold.com", - "version": "0.1.1", + "name": "Kwold", + "url": "kwold.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1702965600000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "47152fc1-79d5-4bcc-b930-6b5cdc66e972", + "id": "936eee30-d31e-48fb-8cc4-9391869934b9", "url": "https://kwold.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "50507ab4-2e75-4f1d-af23-9725b9955bc3", + "id": "870ee174-275a-4ea8-b2d7-a222418e5de9", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json index d640212852..92a0d2af57 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json @@ -1,7 +1,8 @@ { - "name": "neighbor.report", + "name": "Neighbor Report", + "url": "neighbor.report", "version": "0.1.4", - "addedDatetime": 1703559600000, + "addedDatetime": 1703570400000, "steps": [ { "stepType": "scan", @@ -9,12 +10,12 @@ "actions": [ { "actionType": "navigate", - "id": "a554d7d2-f348-487a-97de-8d4f0d1d35c0", + "id": "bbaf8a18-fef8-42a6-9682-747b8ff485b2", "url": "https://neighbor.report/${firstName}-${lastName}/${state|stateFull|hyphenated}/${city|hyphenated}" }, { "actionType": "extract", - "id": "17f80250-1e3c-4e55-8e50-68fe98a6ce23", + "id": "0dac4a6d-1291-47c3-97b8-56200f751ac8", "selector": ".lstd", "profile": { "name": { @@ -50,12 +51,12 @@ "actions": [ { "actionType": "navigate", - "id": "59cc488d-e317-4fb4-8aaa-a20cb71f7480", + "id": "b1f7f4ab-51b0-4885-ba73-97be0822d0ba", "url": "https://neighbor.report/remove" }, { "actionType": "fillForm", - "id": "3a4c1775-941a-4f48-873d-c780f5ea25a0", + "id": "743afa6c-7dea-4115-934b-bea369307acd", "selector": ".form-horizontal", "elements": [ { @@ -74,17 +75,17 @@ }, { "actionType": "getCaptchaInfo", - "id": "f470e245-d5ee-4908-bd6c-16e604a1a29b", + "id": "24ce0da0-7cc3-47e7-bf8e-6f5fe98b7a91", "selector": ".recaptcha-div" }, { "actionType": "solveCaptcha", - "id": "7f0a8fc6-32a3-4f4a-b61d-267f9666de91", + "id": "b720de9a-f519-466f-980d-d9c52d8870a2", "selector": ".recaptcha-div" }, { "actionType": "click", - "id": "7fbc5a97-bc57-41bc-a556-0fdfd8a0845d", + "id": "46690938-f112-4091-bd07-b5641e38151f", "elements": [ { "type": "button", @@ -94,7 +95,7 @@ }, { "actionType": "click", - "id": "d1513f65-a746-4597-9ed2-4cd5e40dead3", + "id": "07cfed17-9d75-471a-b6a0-0522add35ffa", "elements": [ { "type": "button", @@ -108,7 +109,7 @@ }, { "actionType": "expectation", - "id": "8acd9c96-443d-4593-a3a7-9efc9fd5070a", + "id": "ebd61347-60e1-4c19-bc41-dd1ce36d3138", "expectations": [ { "type": "text", @@ -125,4 +126,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/newenglandfacts.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/newenglandfacts.com.json index ddb83134f9..54f8d23ac8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/newenglandfacts.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/newenglandfacts.com.json @@ -1,6 +1,7 @@ { - "name": "newenglandfacts.com", - "version": "0.1.1", + "name": "New England Facts", + "url": "newenglandfacts.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "8bd7953c-ee22-49be-8937-a1798046a0c1", + "id": "05725a5a-ec3f-49c8-875b-ab9787b9385f", "url": "https://newenglandfacts.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "4012d312-2f7f-4cc1-bf7a-b7655f550c1a", + "id": "7f41b78a-bb65-4bb2-a6ca-1a6ab55890ce", "selector": ".b-pfl-list", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/officialusa.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/officialusa.com.json index a7b4efd714..9cb63483be 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/officialusa.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/officialusa.com.json @@ -1,8 +1,9 @@ { - "name": "officialusa.com", - "version": "0.1.0", + "name": "OfficialUSA", + "url": "officialusa.com", + "version": "0.1.4", "parent": "neighbor.report", - "addedDatetime": 1692590400000, + "addedDatetime": 1692594000000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "dad25b4c-743b-4bca-a395-05f1e76ef5c9", + "id": "b430e29e-89f0-4994-96b2-08d0cbdc388c", "url": "https://officialusa.com/names/${firstName}-${lastName}/" }, { "actionType": "extract", - "id": "b867d570-6124-40d9-9076-7ee0fa5b4d68", + "id": "d989f3b7-9b8a-44a6-a51e-70762255f3fc", "selector": ".person", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-background-check.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-background-check.com.json index 7b1d26eb38..b8550c93e4 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-background-check.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-background-check.com.json @@ -1,6 +1,7 @@ { - "name": "people-background-check.com", - "version": "0.1.1", + "name": "People Background Check", + "url": "people-background-check.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1702965600000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "18e35c3b-b837-40e9-b353-20230d36bc4d", + "id": "6fee90c5-5f7e-4fd0-badf-069e2b94a65d", "url": "https://people-background-check.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}" }, { "actionType": "extract", - "id": "7a23b927-acfc-4d29-b4b6-3f204687619c", + "id": "ee03ba42-e9a5-4489-a7d6-d50bf21238aa", "selector": ".b-pfl-list", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json index 7e690167c8..34bc5b8770 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json @@ -1,7 +1,8 @@ { - "name": "peoplefinders.com", - "version": "0.1.0", - "addedDatetime": 1677128400000, + "name": "PeopleFinders", + "url": "peoplefinders.com", + "version": "0.1.4", + "addedDatetime": 1677132000000, "steps": [ { "stepType": "scan", @@ -9,12 +10,12 @@ "actions": [ { "actionType": "navigate", - "id": "aafba5bd-a157-4e35-b653-0797a732d94c", + "id": "71c7cb2f-14fe-43b8-9623-452b8bd10d4e", "url": "https://www.peoplefinders.com/people/${firstName}-${lastName}/${state}/${city}?landing=all&age=${age}" }, { "actionType": "extract", - "id": "b8f10f20-3363-4781-a03b-c4958b6269c7", + "id": "5c5af912-091f-4f48-922f-ba554951ddd9", "selector": ".record", "profile": { "name": { @@ -48,12 +49,12 @@ "actions": [ { "actionType": "navigate", - "id": "f5fbd4f5-23f7-45ed-a9ce-3e9b0a5a7a0a", + "id": "4b065fde-35c7-43d7-aed6-3abcdac94f08", "url": "https://www.peoplefinders.com/opt-out" }, { "actionType": "click", - "id": "7b33cd1b-3948-4454-8434-e703cc235123", + "id": "b5c0929e-e362-4570-815b-0433ef97fddf", "elements": [ { "type": "button", @@ -63,7 +64,7 @@ }, { "actionType": "fillForm", - "id": "32056b7a-dc80-4d5d-b9cc-dccd32cb56be", + "id": "2fb91804-e5ea-414e-9354-fba98f3c00e1", "selector": ".opt-out-form", "elements": [ { @@ -78,17 +79,17 @@ }, { "actionType": "getCaptchaInfo", - "id": "5d5068aa-5c16-4fdc-8f3b-e412ad4eabed", + "id": "4b9706ef-dd9b-47d6-b337-12f66a5f9138", "selector": ".g-recaptcha" }, { "actionType": "solveCaptcha", - "id": "3443e060-8aee-4bd0-ab2c-ea03f8b8f93c", + "id": "770019d3-fa88-400a-8480-7cc31d6b3382", "selector": ".g-recaptcha" }, { "actionType": "click", - "id": "cb9ef5b0-0155-42f1-a766-145b3c14586b", + "id": "a7285f44-6c99-44b1-8199-eb6c383fe12b", "elements": [ { "type": "button", @@ -98,22 +99,22 @@ }, { "actionType": "emailConfirmation", - "id": "5cc7cfa5-e8ab-4dc1-b58b-973af3d3f364", + "id": "05cc08ea-fb80-40fb-8cce-3ca674eea03b", "pollingTime": 30 }, { "actionType": "getCaptchaInfo", - "id": "3a44c15d-1dd0-4e92-beba-bf3d8544c6e9", + "id": "8cb4256a-b162-407f-8434-5536c7560c98", "selector": ".g-recaptcha" }, { "actionType": "solveCaptcha", - "id": "d2566371-8b02-4414-9a24-1f9d2761eb1d", + "id": "38c64eec-6bd9-4751-a7cf-8cbe9901b0f6", "selector": ".g-recaptcha" }, { "actionType": "click", - "id": "259d8895-ac58-46b0-a209-7f209171e13c", + "id": "d1d25423-912b-4828-825b-eb83809ada08", "elements": [ { "type": "button", @@ -123,7 +124,7 @@ }, { "actionType": "expectation", - "id": "ed02f55b-67b3-4efc-a3cc-ce6b6c7ceeed", + "id": "fdb755da-8970-426f-b09e-12165c2169dd", "expectations": [ { "type": "url", @@ -139,4 +140,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplesearchnow.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplesearchnow.com.json index 6e477c6e13..6189f3d311 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplesearchnow.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplesearchnow.com.json @@ -1,6 +1,7 @@ { - "name": "peoplesearchnow.com", - "version": "0.1.1", + "name": "People Search Now", + "url": "peoplesearchnow.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1705989600000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "b6994b26-9904-407b-9bcf-0fd6f809771d", + "id": "db9e093d-68c2-45e1-a529-29a2dc67dfab", "url": "https://peoplesearchnow.com/person/${firstName}-${lastName}_${city}_${state}/" }, { "actionType": "extract", - "id": "4e7f0e9a-1d24-47c0-886f-a08d88074878", + "id": "78912133-761b-4971-9780-4e16c8dd43b2", "selector": ".result-search-block", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/pub360.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/pub360.com.json index 503392f378..815fdcb8fc 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/pub360.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/pub360.com.json @@ -1,8 +1,9 @@ { - "name": "pub360.com", - "version": "0.1.1", + "name": "Pub360", + "url": "pub360.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "72fc91c4-e8bc-4656-8260-cd3bb15e2001", + "id": "8e2a1251-2685-476a-b4c1-53d138331abe", "url": "https://pub360.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "2cb4778d-e3d6-4432-8421-84438c280e19", + "id": "9ce62e6f-b103-45f6-9f92-56785eb22320", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/publicreports.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/publicreports.com.json index 991d4d2b2c..5ea3d241e4 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/publicreports.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/publicreports.com.json @@ -1,6 +1,7 @@ { - "name": "publicreports.com", - "version": "0.1.1", + "name": "PublicReports", + "url": "publicreports.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "ae0104a2-a75c-4d97-bada-dda4f21dd446", + "id": "b995b1bf-6610-4085-9d07-d38857807535", "url": "https://publicreports.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "388c55e3-fa12-4376-9a02-01190b8a30fd", + "id": "7fb121fb-e2a0-4fa2-9b97-51130104971c", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/quickpeopletrace.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/quickpeopletrace.com.json index 7409fd240b..e8b18f9ec8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/quickpeopletrace.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/quickpeopletrace.com.json @@ -1,6 +1,7 @@ { - "name": "quickpeopletrace.com", - "version": "0.1.1", + "name": "Quick People Trace", + "url": "quickpeopletrace.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1674540000000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "45443ab2-7563-4c7d-8bf2-b1b550f4b825", + "id": "2db8c120-a8c3-4aa0-a9ce-b075ca85fc68", "url": "https://www.quickpeopletrace.com/search/?addresssearch=1&tabid=1&teaser-firstname=${firstName}&teaser-middlename=&teaser-lastname=${lastName}&teaser-city=${city}&teaser-state=${state|upcase}&teaser-submitted=Search" }, { "actionType": "extract", - "id": "08607047-96e8-4fbb-9af9-bf7b8e163b20", + "id": "bd48b737-89c4-408a-a28c-2dfa828aebd8", "selector": "//table/tbody/tr[position() > 1]", "profile": { "name": { @@ -24,9 +25,6 @@ "age": { "selector": ".//td[3]" }, - "addressCityState": { - "selector": ".//td[4]/strong" - }, "addressCityStateList": { "selector": ".//td[4]" }, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/searchpeoplefree.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/searchpeoplefree.com.json index 535ec0f63d..4a68c912e3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/searchpeoplefree.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/searchpeoplefree.com.json @@ -1,6 +1,7 @@ { - "name": "searchpeoplefree.com", - "version": "0.1.1", + "name": "Search People FREE", + "url": "searchpeoplefree.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "2b537ff2-8967-465c-ad5c-c4c2d31f60e1", + "id": "f5bad072-6f55-4357-b23b-1df4c9584e67", "url": "https://searchpeoplefree.com/find/${firstName}-${lastName}/${state}/${city}" }, { "actionType": "extract", - "id": "70728718-fe02-43f6-b86f-6d6c6bbbf009", + "id": "749fb8fe-9994-41e2-a0ea-ae6334c5aee0", "selector": "//li[@class='toc l-i mb-5']", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/smartbackgroundchecks.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/smartbackgroundchecks.com.json index 31424db72d..23f588c796 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/smartbackgroundchecks.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/smartbackgroundchecks.com.json @@ -1,6 +1,7 @@ { - "name": "smartbackgroundchecks.com", - "version": "0.1.1", + "name": "SmartBackgroundChecks", + "url": "smartbackgroundchecks.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1678082400000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "97b307c8-e3e4-4090-a6ab-c5eeb599d248", + "id": "1c6bdc6e-12dd-47db-b5b0-13055c1f3d5d", "url": "https://www.smartbackgroundchecks.com/people/${firstName}-${lastName}/${city}/${state}" }, { "actionType": "extract", - "id": "ca20a933-b703-427e-8cbf-e2f25cd763a6", + "id": "ac554b4f-e4a0-44c5-81a6-c04e46e4ce3b", "selector": ".card-block", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/spokeo.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/spokeo.com.json index 2618099829..3c0f0008f8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/spokeo.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/spokeo.com.json @@ -1,12 +1,33 @@ { - "name": "spokeo.com", - "version": "0.1.3", + "name": "Spokeo", + "url": "spokeo.com", + "version": "0.1.4", "addedDatetime": 1692594000000, "mirrorSites": [ - { "name": "callersmart.com", "addedAt": 1705599286529, "removedAt": null }, - { "name": "selfie.network", "addedAt": 1705599286529, "removedAt": null }, - { "name": "selfie.systems", "addedAt": 1705599286529, "removedAt": null }, - { "name": "peoplewin.com", "addedAt": 1705599286529, "removedAt": null } + { + "name": "CallerSmart", + "url": "callersmart.com", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "Selfie Network", + "url": "selfie.network", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "Selfie Systems", + "url": "selfie.systems", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "PeopleWin", + "url": "peoplewin.com", + "addedAt": 1705599286529, + "removedAt": null + } ], "steps": [ { @@ -15,12 +36,12 @@ "actions": [ { "actionType": "navigate", - "id": "d3174bd8-3253-45e3-88f0-1366882a2df7", + "id": "9b617d27-b330-46fc-bdb0-6239c0873897", "url": "https://www.spokeo.com/${firstName}-${lastName}/${state|stateFull}/${city}" }, { "actionType": "extract", - "id": "e47f5f27-dfbf-4f2c-8d7a-43f581abdaa2", + "id": "4f7124c2-bd8c-4649-84f2-04f0962225b5", "selector": ".single-column-list-item", "profile": { "name": { @@ -52,12 +73,12 @@ "actions": [ { "actionType": "navigate", - "id": "dba8f444-a433-4ad2-9819-3c555bfedd9c", + "id": "df75e4fb-f14b-4b65-afe2-82e03b71c6a9", "url": "https://www.spokeo.com/optout" }, { "actionType": "fillForm", - "id": "b1145fca-3e35-4ee9-86f2-e35c393846d3", + "id": "42cbfc2b-d96b-4bd6-8d16-0542a672d869", "selector": ".optout_container", "elements": [ { @@ -72,17 +93,17 @@ }, { "actionType": "getCaptchaInfo", - "id": "8f9608b4-bbf7-4540-8b22-5c381225cd02", + "id": "e1581b9e-7460-4bbd-a010-634c2db12ca1", "selector": "#g-recaptcha" }, { "actionType": "solveCaptcha", - "id": "a5a884b8-12f6-4029-aa80-244f1a163f67", + "id": "01ca39d9-e842-41cf-b0f9-a7d517bc0dd6", "selector": "#g-recaptcha" }, { "actionType": "click", - "id": "a7d4fdd4-30b8-46f7-8700-67c633da1f91", + "id": "7556edd5-570b-4c4a-acc7-f1066138d513", "elements": [ { "type": "button", @@ -92,7 +113,7 @@ }, { "actionType": "expectation", - "id": "d4a804a3-de62-4f66-a56e-e9d7e65fb8bb", + "id": "f7b5125e-0dda-4a14-8943-8c20c09125bc", "expectations": [ { "type": "text", @@ -103,12 +124,12 @@ }, { "actionType": "emailConfirmation", - "id": "5138062c-99d3-4523-b222-8123b13bc524", + "id": "dbd875b6-bdc7-48ca-962b-885941e6284a", "pollingTime": 30 }, { "actionType": "expectation", - "id": "cc14f3ea-35f8-4d31-a280-dc97526de12a", + "id": "b2f1c371-d779-4b3b-8516-0d13169cf873", "expectations": [ { "type": "text", @@ -125,4 +146,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/truepeoplesearch.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/truepeoplesearch.com.json index 84d468f943..a226c959a0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/truepeoplesearch.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/truepeoplesearch.com.json @@ -1,6 +1,7 @@ { - "name": "truepeoplesearch.com", - "version": "0.1.1", + "name": "TruePeopleSearch", + "url": "truepeoplesearch.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1703138400000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "4b7d8751-d3cd-4e4b-b2a2-66219eb6a8e8", + "id": "12eb70c1-53d5-4881-9dce-74ed4fada583", "url": "https://www.truepeoplesearch.com/results?name=${firstName}%20${lastName}&citystatezip=${city|capitalize},${state|upcase}" }, { "actionType": "extract", - "id": "cdb5940a-8505-4b28-9699-d98235e1fff1", + "id": "881e0e21-c375-4083-a9be-86f82063849b", "selector": ".card-summary", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json index 71c8e711ed..b8fba84277 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json @@ -1,8 +1,9 @@ { - "name": "usa-people-search.com", - "version": "0.1.1", + "name": "USA People Search", + "url": "usa-people-search.com", + "version": "0.1.4", "parent": "peoplefinders.com", - "addedDatetime": 1678078800000, + "addedDatetime": 1678082400000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "2c4b31a3-661b-4f30-a4d3-b5a4a13c95db", + "id": "67e80e69-f542-4714-8705-c43af630ac72", "url": "https://usa-people-search.com/name/${firstName|downcase}-${lastName|downcase}/${city|downcase}-${state|stateFull|downcase}?age=${age}" }, { "actionType": "extract", - "id": "20a4d510-56b6-46a8-92ce-be16ed3ce049", + "id": "c0a82b15-7564-4e12-8c4e-084174242623", "selector": ".card-block", "profile": { "name": { @@ -62,4 +63,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usatrace.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usatrace.com.json index 412a14d7d9..4645c85dd0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usatrace.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usatrace.com.json @@ -1,6 +1,7 @@ { - "name": "usatrace.com", - "version": "0.1.1", + "name": "USA Trace", + "url": "usatrace.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1674540000000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "5071e480-88ae-49b5-91b6-1daf26c55acf", + "id": "17217b04-28ae-4262-aa33-ee3695bb6bd6", "url": "https://www.usatrace.com/people-search/${firstName}-${lastName}/${city}-${state|upcase}" }, { "actionType": "extract", - "id": "3237fc09-247c-4942-9920-9bbb937f6ac2", + "id": "426d8e8a-2f32-46f3-9d1d-e7f6e2fddadb", "selector": "//table/tbody/tr[position() > 1]", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usphonebook.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usphonebook.com.json index 0770b2f474..5aff073d4a 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usphonebook.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usphonebook.com.json @@ -1,8 +1,9 @@ { - "name": "usphonebook.com", - "version": "0.1.1", + "name": "USPhoneBook", + "url": "usphonebook.com", + "version": "0.1.4", "parent": "peoplefinders.com", - "addedDatetime": 1678078800000, + "addedDatetime": 1678082400000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "f214150b-4f02-46e1-b7ea-81f6bb1bf097", + "id": "6ee93554-95da-4a36-a7f7-c059d8f53ca3", "url": "https://www.usphonebook.com/${firstName}-${lastName}/${state|stateFull}/${city}" }, { "actionType": "extract", - "id": "af98bb63-b885-4f47-bb47-5f9ec5b491a4", + "id": "fffae12f-4ca1-4a8f-81b9-00adf0487129", "selector": ".ls_contacts-people-finder-wrapper", "profile": { "name": { @@ -56,4 +57,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/verecor.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/verecor.com.json index f493fbd347..5aff5bd46e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/verecor.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/verecor.com.json @@ -1,7 +1,8 @@ { - "name": "verecor.com", - "version": "0.1.2", - "addedDatetime": 1677128400000, + "name": "Verecor", + "url": "verecor.com", + "version": "0.1.4", + "addedDatetime": 1677132000000, "steps": [ { "stepType": "scan", @@ -9,7 +10,7 @@ "actions": [ { "actionType": "navigate", - "id": "6f53d146-af6a-4bce-970d-f1dcbc496037", + "id": "37fc63a6-e434-4ba0-9e9e-d80898e4dfa4", "url": "https://verecor.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -23,7 +24,7 @@ }, { "actionType": "extract", - "id": "e8c09200-030c-492a-8e54-22bc6bdb6829", + "id": "a955924c-7959-48c8-9511-3f843baed729", "selector": ".search-item", "profile": { "name": { @@ -58,12 +59,12 @@ "actions": [ { "actionType": "navigate", - "id": "f4bbe480-a6ff-40a5-aa25-8ff9ac40c9bf", + "id": "85cd9682-94d8-46ac-9999-e03dfa9f8d4e", "url": "https://verecor.com/ng/control/privacy" }, { "actionType": "fillForm", - "id": "1e6302e1-daf9-49d6-951f-506ea5e266a0", + "id": "ed45c76b-e537-4072-9f46-9515c6e215be", "selector": ".ahm", "elements": [ { @@ -82,17 +83,17 @@ }, { "actionType": "getCaptchaInfo", - "id": "6a3dc470-3bf7-4b8b-bb44-f77ef1a2c540", + "id": "0e1474f0-24fe-4f6a-8d2e-2dfd91cf574b", "selector": ".g-recaptcha" }, { "actionType": "solveCaptcha", - "id": "83157244-c5bf-44a9-979c-679e1404d67d", + "id": "52a858f5-7dc5-40aa-aaa7-7090e06ea55e", "selector": ".g-recaptcha" }, { "actionType": "click", - "id": "15c50d7f-0e72-4509-be2b-40cde34b48e6", + "id": "759e0dd2-3a93-42a8-9a83-5e3408f5566b", "elements": [ { "type": "button", @@ -102,7 +103,7 @@ }, { "actionType": "expectation", - "id": "2ed336a2-a7a9-4cbd-933c-cd463df4f553", + "id": "089924be-5ea3-48a9-a325-8976d262f39b", "expectations": [ { "type": "text", @@ -113,12 +114,12 @@ }, { "actionType": "emailConfirmation", - "id": "88c09081-e848-4e75-a7b9-3ee28e95a459", + "id": "8094718e-412a-418f-b74d-cd4fc5e42c56", "pollingTime": 30 }, { "actionType": "expectation", - "id": "dd03cf9f-8227-4881-86bf-09ce158bf151", + "id": "af8fb89b-88d2-4901-b90c-eaac3c7566db", "expectations": [ { "type": "text", @@ -135,4 +136,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/vericora.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/vericora.com.json index 814e908fae..c7a3bad5c2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/vericora.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/vericora.com.json @@ -1,8 +1,9 @@ { - "name": "vericora.com", - "version": "0.1.1", + "name": "Vericora", + "url": "vericora.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "9488e141-d109-4cbf-bc65-1b9036728ff4", + "id": "69175f1a-0024-4efd-ab3e-67bcf915a770", "url": "https://vericora.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "baaecb74-8d63-496c-a3e0-a8acbdee2c99", + "id": "bd941009-4462-4d59-ba44-46250f580531", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veriforia.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veriforia.com.json index 121bc68d3e..5f4f307f92 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veriforia.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veriforia.com.json @@ -1,8 +1,9 @@ { - "name": "veriforia.com", - "version": "0.1.1", + "name": "Veriforia", + "url": "veriforia.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "ffb30e97-b03f-4157-a511-09ad8ffb8b54", + "id": "17442975-944c-4b01-8518-7f1dff171ad2", "url": "https://veriforia.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "1d8d9c20-9897-4386-8bc1-bd591abe7c81", + "id": "32e963e1-4959-4e5e-981b-550f1bf36f9a", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veripages.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veripages.com.json index 43d95caf8a..61becad701 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veripages.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veripages.com.json @@ -1,8 +1,9 @@ { - "name": "veripages.com", - "version": "0.1.2", + "name": "Veripages", + "url": "veripages.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1691982000000, + "addedDatetime": 1691989200000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "5bf98772-1804-4939-a06b-dbf9cd31f198", + "id": "2346b569-1c46-4ef9-8ea0-fa18bea967fa", "url": "https://veripages.com/inner/profile/search?fname=${firstName}&lname=${lastName}&fage=${age|ageRange}&state=${state}&city=${city}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "f3d53642-9a58-4275-b97f-5547e3ef8e55", + "id": "c4281ca8-d4d0-4091-b6c2-3094801e99c0", "selector": ".search-item", "profile": { "name": { @@ -66,4 +67,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/virtory.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/virtory.com.json index d12865681f..3d94019338 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/virtory.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/virtory.com.json @@ -1,6 +1,7 @@ { - "name": "virtory.com", - "version": "0.1.1", + "name": "Virtory", + "url": "virtory.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "99465bd6-ce87-4fc6-96a2-eea8137e4a30", + "id": "0568e4f5-73c2-4b1a-9eb6-ac3571b1a01e", "url": "https://virtory.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "8d66ac98-f788-4fbd-acec-56034682b4b1", + "id": "df2216f3-0890-4d13-b2aa-233084167720", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/wellnut.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/wellnut.com.json index b4fd3669ec..12c43b7fa4 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/wellnut.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/wellnut.com.json @@ -1,6 +1,7 @@ { - "name": "wellnut.com", - "version": "0.1.1", + "name": "Wellnut", + "url": "wellnut.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "b9db3c1e-ece6-45d1-94ec-1143da9607aa", + "id": "a38752f3-ae69-45c3-ba3f-3a73e549e644", "url": "https://wellnut.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "e4b7c983-c96e-4ce8-8703-3cb319454db7", + "id": "b7747e92-5fe5-46f7-b083-5df6fbdc2b84", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index 0e6b930d71..c61178c0f9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -106,7 +106,6 @@ final class DataBrokerProtectionProcessor { completion: @escaping () -> Void) { // Before running new operations we check if there is any updates to the broker files. - // This runs only once per 24 hours. if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) { let brokerUpdater = DataBrokerProtectionBrokerUpdater(vault: vault) brokerUpdater.checkForUpdatesInBrokerJSONFiles() diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift index 75b80b195a..038c72c2d8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift @@ -30,7 +30,7 @@ public enum EmailError: Error, Equatable, Codable { } protocol EmailServiceProtocol { - func getEmail(dataBrokerName: String?) async throws -> String + func getEmail(dataBrokerURL: String?) async throws -> String func getConfirmationLink(from email: String, numberOfRetries: Int, pollingIntervalInSeconds: Int, @@ -51,10 +51,10 @@ struct EmailService: EmailServiceProtocol { self.redeemUseCase = redeemUseCase } - func getEmail(dataBrokerName: String? = nil) async throws -> String { + func getEmail(dataBrokerURL: String? = nil) async throws -> String { var urlString = Constants.baseUrl + "/generate" - if let dataBrokerValue = dataBrokerName { + if let dataBrokerValue = dataBrokerURL { urlString += "?dataBroker=\(dataBrokerValue)" } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift index 384157de75..a70ad31818 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift @@ -34,7 +34,7 @@ protocol DataBrokerProtectionDatabaseProvider: SecureStorageDatabaseProvider { func save(_ broker: BrokerDB) throws -> Int64 func update(_ broker: BrokerDB) throws func fetchBroker(with id: Int64) throws -> BrokerDB? - func fetchBroker(with name: String) throws -> BrokerDB? + func fetchBroker(with url: String) throws -> BrokerDB? func fetchAllBrokers() throws -> [BrokerDB] func save(_ profileQuery: ProfileQueryDB) throws -> Int64 @@ -85,6 +85,8 @@ final class DefaultDataBrokerProtectionDatabaseProvider: GRDBSecureStorageDataba public init(file: URL = DefaultDataBrokerProtectionDatabaseProvider.defaultDatabaseURL(), key: Data) throws { try super.init(file: file, key: key, writerType: .pool) { migrator in migrator.registerMigration("v1", migrate: Self.migrateV1(database:)) + migrator.registerMigration("v2", migrate: Self.migrateV2(database:)) + } } @@ -259,6 +261,16 @@ final class DefaultDataBrokerProtectionDatabaseProvider: GRDBSecureStorageDataba $0.column(OptOutAttemptDB.Columns.startDate.name, .date).notNull() } } + + static func migrateV2(database: Database) throws { + try database.alter(table: BrokerDB.databaseTableName) { + $0.add(column: BrokerDB.Columns.url.name, .text) + } + try database.execute(sql: """ + UPDATE \(BrokerDB.databaseTableName) SET \(BrokerDB.Columns.url.name) = \(BrokerDB.Columns.name.name) + """) + } + // swiftlint:enable function_body_length func updateProfile(profile: DataBrokerProtectionProfile, mapperToDB: MapperToDB) throws -> Int64 { @@ -320,7 +332,8 @@ final class DefaultDataBrokerProtectionDatabaseProvider: GRDBSecureStorageDataba } func deleteProfileData() throws { - try db.write { db in + try db.writeWithoutTransaction { db in + try db.execute(sql: "PRAGMA foreign_keys = OFF;") try OptOutDB .deleteAll(db) try ScanDB @@ -331,6 +344,11 @@ final class DefaultDataBrokerProtectionDatabaseProvider: GRDBSecureStorageDataba .deleteAll(db) try PhoneDB .deleteAll(db) + try ProfileDB + .deleteAll(db) + try BrokerDB + .deleteAll(db) + try db.execute(sql: "PRAGMA foreign_keys = ON;") } } @@ -353,10 +371,10 @@ final class DefaultDataBrokerProtectionDatabaseProvider: GRDBSecureStorageDataba } } - func fetchBroker(with name: String) throws -> BrokerDB? { + func fetchBroker(with url: String) throws -> BrokerDB? { try db.read { db in return try BrokerDB - .filter(Column(BrokerDB.Columns.name.name) == name) + .filter(Column(BrokerDB.Columns.url.name) == url) .fetchOne(db) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift index 267b013ed8..81cf02f70d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift @@ -58,7 +58,7 @@ struct MapperToDB { func mapToDB(_ broker: DataBroker, id: Int64? = nil) throws -> BrokerDB { let encodedBroker = try jsonEncoder.encode(broker) - return .init(id: id, name: broker.name, json: encodedBroker, version: broker.version) + return .init(id: id, name: broker.name, json: encodedBroker, version: broker.version, url: broker.url) } func mapToDB(_ profileQuery: ProfileQuery, relatedTo profileId: Int64) throws -> ProfileQueryDB { @@ -171,6 +171,7 @@ struct MapperToModel { return DataBroker( id: brokerDB.id, name: decodedBroker.name, + url: decodedBroker.url, steps: decodedBroker.steps, version: decodedBroker.version, schedulingConfig: decodedBroker.schedulingConfig, @@ -232,7 +233,8 @@ struct MapperToModel { reportId: extractedProfile.reportId, age: extractedProfile.age, email: extractedProfile.email, - removedDate: extractedProfileDB.removedDate) + removedDate: extractedProfileDB.removedDate, + identifier: extractedProfile.identifier) } func mapToModel(_ scanEvent: ScanHistoryEventDB) throws -> HistoryEvent { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/SchedulerSchema.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/SchedulerSchema.swift index 348d99180d..a772e4d543 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/SchedulerSchema.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/SchedulerSchema.swift @@ -94,6 +94,7 @@ struct BrokerDB: Codable { let name: String let json: Data let version: String + let url: String } extension BrokerDB: PersistableRecord, FetchableRecord { @@ -104,6 +105,7 @@ extension BrokerDB: PersistableRecord, FetchableRecord { case name case json case version + case url } init(row: Row) throws { @@ -111,6 +113,7 @@ extension BrokerDB: PersistableRecord, FetchableRecord { name = row[Columns.name] json = row[Columns.json] version = row[Columns.version] + url = row[Columns.url] } func encode(to container: inout PersistenceContainer) throws { @@ -118,6 +121,7 @@ extension BrokerDB: PersistableRecord, FetchableRecord { container[Columns.name] = name container[Columns.json] = json container[Columns.version] = version + container[Columns.url] = url } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift index 63b1b4a47e..9f77b8a675 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift @@ -36,6 +36,7 @@ protocol DBPUICommunicationDelegate: AnyObject { func startScanAndOptOut() -> Bool func getInitialScanState() async -> DBPUIInitialScanState func getMaintananceScanState() async -> DBPUIScanAndOptOutMaintenanceState + func getDataBrokers() async -> [DBPUIDataBroker] } enum DBPUIReceivedMethodName: String { @@ -53,6 +54,7 @@ enum DBPUIReceivedMethodName: String { case startScanAndOptOut case initialScanStatus case maintenanceScanStatus + case getDataBrokers } enum DBPUISendableMethodName: String { @@ -69,7 +71,7 @@ struct DBPUICommunicationLayer: Subfeature { weak var delegate: DBPUICommunicationDelegate? private enum Constants { - static let version = 1 + static let version = 2 } internal init(webURLSettings: DataBrokerProtectionWebUIURLSettingsRepresentable) { @@ -101,6 +103,7 @@ struct DBPUICommunicationLayer: Subfeature { case .startScanAndOptOut: return startScanAndOptOut case .initialScanStatus: return initialScanStatus case .maintenanceScanStatus: return maintenanceScanStatus + case .getDataBrokers: return getDataBrokers } } @@ -264,6 +267,11 @@ struct DBPUICommunicationLayer: Subfeature { return maintenanceScanStatus } + func getDataBrokers(params: Any, origin: WKScriptMessage) async throws -> Encodable? { + let dataBrokers = await delegate?.getDataBrokers() ?? [DBPUIDataBroker]() + return DBPUIDataBrokerList(dataBrokers: dataBrokers) + } + func sendMessageToUI(method: DBPUISendableMethodName, params: DBPUISendableMessage, into webView: WKWebView) { broker?.push(method: method.rawValue, params: params, for: self, into: webView) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift index accf119bb7..ce258c3565 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift @@ -26,6 +26,7 @@ final public class DataBrokerProtectionViewController: NSViewController { private let dataManager: DataBrokerProtectionDataManaging private let scheduler: DataBrokerProtectionScheduler private var webView: WKWebView? + private var loader: NSProgressIndicator! private let webUISettings: DataBrokerProtectionWebUIURLSettingsRepresentable private let webUIViewModel: DBPUIViewModel @@ -63,9 +64,10 @@ final public class DataBrokerProtectionViewController: NSViewController { public override func viewDidLoad() { super.viewDidLoad() + addLoadingIndicator() reloadObserver = NotificationCenter.default.addObserver(forName: DataBrokerProtectionNotifications.shouldReloadUI, - object: nil, - queue: .main) { [weak self] _ in + object: nil, + queue: .main) { [weak self] _ in self?.webView?.reload() } } @@ -75,16 +77,39 @@ final public class DataBrokerProtectionViewController: NSViewController { webView = WKWebView(frame: CGRect(x: 0, y: 0, width: 1024, height: 768), configuration: configuration) webView?.uiDelegate = self + webView?.navigationDelegate = self view = webView! if let url = URL(string: webUISettings.selectedURL) { webView?.load(url) } else { + removeLoadingIndicator() assertionFailure("Selected URL is not valid \(webUISettings.selectedURL)") } } + private func addLoadingIndicator() { + loader = NSProgressIndicator() + loader.wantsLayer = true + loader.style = .spinning + loader.controlSize = .regular + loader.sizeToFit() + loader.translatesAutoresizingMaskIntoConstraints = false + loader.controlSize = .large + view.addSubview(loader) + + NSLayoutConstraint.activate([ + loader.centerXAnchor.constraint(equalTo: view.centerXAnchor), + loader.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + } + + private func removeLoadingIndicator() { + loader.stopAnimation(nil) + loader.removeFromSuperview() + } + deinit { if let reloadObserver { NotificationCenter.default.removeObserver(reloadObserver) @@ -98,3 +123,14 @@ extension DataBrokerProtectionViewController: WKUIDelegate { return nil } } + +extension DataBrokerProtectionViewController: WKNavigationDelegate { + + public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + loader.startAnimation(nil) + } + + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + removeLoadingIndicator() + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift index 836bd52a30..454f1c75a9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift @@ -26,22 +26,24 @@ struct MapperToUI { name: extractedProfile.fullName ?? "No name", addresses: extractedProfile.addresses?.map(mapToUI) ?? [], alternativeNames: extractedProfile.alternativeNames ?? [String](), - relatives: extractedProfile.relatives ?? [String]() + relatives: extractedProfile.relatives ?? [String](), + date: extractedProfile.removedDate?.timeIntervalSince1970 ) } - func mapToUI(_ dataBrokerName: String, extractedProfile: ExtractedProfile) -> DBPUIDataBrokerProfileMatch { + func mapToUI(_ dataBrokerName: String, databrokerURL: String, extractedProfile: ExtractedProfile) -> DBPUIDataBrokerProfileMatch { DBPUIDataBrokerProfileMatch( - dataBroker: DBPUIDataBroker(name: dataBrokerName), + dataBroker: DBPUIDataBroker(name: dataBrokerName, url: databrokerURL), name: extractedProfile.fullName ?? "No name", addresses: extractedProfile.addresses?.map(mapToUI) ?? [], alternativeNames: extractedProfile.alternativeNames ?? [String](), - relatives: extractedProfile.relatives ?? [String]() + relatives: extractedProfile.relatives ?? [String](), + date: extractedProfile.removedDate?.timeIntervalSince1970 ) } func mapToUI(_ dataBroker: DataBroker) -> DBPUIDataBroker { - DBPUIDataBroker(name: dataBroker.name) + DBPUIDataBroker(name: dataBroker.name, url: dataBroker.url) } func mapToUI(_ address: AddressCityState) -> DBPUIUserProfileAddress { @@ -75,7 +77,7 @@ struct MapperToUI { if !$0.dataBroker.mirrorSites.isEmpty { let mirrorSitesMatches = $0.dataBroker.mirrorSites.compactMap { mirrorSite in if mirrorSite.shouldWeIncludeMirrorSite() { - return mapToUI(mirrorSite.name, extractedProfile: extractedProfile) + return mapToUI(mirrorSite.name, databrokerURL: mirrorSite.url, extractedProfile: extractedProfile) } return nil @@ -110,7 +112,7 @@ struct MapperToUI { if let closestMatchesFoundEvent = scanOperation.closestMatchesFoundEvent() { for mirrorSite in dataBroker.mirrorSites where mirrorSite.shouldWeIncludeMirrorSite(for: closestMatchesFoundEvent.date) { - let mirrorSiteMatch = mapToUI(mirrorSite.name, extractedProfile: extractedProfile) + let mirrorSiteMatch = mapToUI(mirrorSite.name, databrokerURL: mirrorSite.url, extractedProfile: extractedProfile) if let extractedProfileRemovedDate = extractedProfile.removedDate, mirrorSite.shouldWeIncludeMirrorSite(for: extractedProfileRemovedDate) { @@ -124,11 +126,21 @@ struct MapperToUI { } let completedOptOutsDictionary = Dictionary(grouping: removedProfiles, by: { $0.dataBroker }) - let completedOptOuts = completedOptOutsDictionary.map { (key: DBPUIDataBroker, value: [DBPUIDataBrokerProfileMatch]) in - DBPUIOptOutMatch(dataBroker: key, matches: value.count) - } - let lastScans = getLastScanInformation(brokerProfileQueryData: brokerProfileQueryData) - let nextScans = getNextScansInformation(brokerProfileQueryData: brokerProfileQueryData) + let completedOptOuts: [DBPUIOptOutMatch] = completedOptOutsDictionary.compactMap { (key: DBPUIDataBroker, value: [DBPUIDataBrokerProfileMatch]) in + value.compactMap { match in + guard let removedDate = match.date else { return nil } + return DBPUIOptOutMatch(dataBroker: key, + matches: value.count, + name: match.name, + alternativeNames: match.alternativeNames, + addresses: match.addresses, + date: removedDate) + } + }.flatMap { $0 } + + let nearestScanByBrokerURL = nearestRunDates(for: brokerProfileQueryData) + let lastScans = getLastScanInformation(brokerProfileQueryData: brokerProfileQueryData, nearestScanOperationByBroker: nearestScanByBrokerURL) + let nextScans = getNextScansInformation(brokerProfileQueryData: brokerProfileQueryData, nearestScanOperationByBroker: nearestScanByBrokerURL) return DBPUIScanAndOptOutMaintenanceState( inProgressOptOuts: inProgressOptOuts, @@ -140,7 +152,8 @@ struct MapperToUI { private func getLastScanInformation(brokerProfileQueryData: [BrokerProfileQueryData], currentDate: Date = Date(), - format: String = "dd/MM/yyyy") -> DBUIScanDate { + format: String = "dd/MM/yyyy", + nearestScanOperationByBroker: [String: Date]) -> DBUIScanDate { let scansGroupedByLastRunDate = Dictionary(grouping: brokerProfileQueryData, by: { $0.scanOperationData.lastRunDate?.toFormat(format) }) let closestScansBeforeToday = scansGroupedByLastRunDate .filter { $0.key != nil && $0.key!.toDate(using: format) < currentDate } @@ -148,12 +161,13 @@ struct MapperToUI { .flatMap { [$0.key?.toDate(using: format): $0.value] } .last - return scanDate(element: closestScansBeforeToday) + return scanDate(element: closestScansBeforeToday, nearestScanOperationByBroker: nearestScanOperationByBroker) } private func getNextScansInformation(brokerProfileQueryData: [BrokerProfileQueryData], currentDate: Date = Date(), - format: String = "dd/MM/yyyy") -> DBUIScanDate { + format: String = "dd/MM/yyyy", + nearestScanOperationByBroker: [String: Date]) -> DBUIScanDate { let scansGroupedByPreferredRunDate = Dictionary(grouping: brokerProfileQueryData, by: { $0.scanOperationData.preferredRunDate?.toFormat(format) }) let closestScansAfterToday = scansGroupedByPreferredRunDate .filter { $0.key != nil && $0.key!.toDate(using: format) > currentDate } @@ -161,22 +175,50 @@ struct MapperToUI { .flatMap { [$0.key?.toDate(using: format): $0.value] } .first - return scanDate(element: closestScansAfterToday) + return scanDate(element: closestScansAfterToday, nearestScanOperationByBroker: nearestScanOperationByBroker) + } + + // A dictionary containing the closest scan by broker + private func nearestRunDates(for brokerData: [BrokerProfileQueryData]) -> [String: Date] { + let today = Date() + let nearestDates = brokerData.reduce(into: [String: Date]()) { result, data in + let url = data.dataBroker.url + if let operationDate = data.scanOperationData.preferredRunDate { + if operationDate > today { + if let existingDate = result[url] { + if operationDate < existingDate { + result[url] = operationDate + } + } else { + result[url] = operationDate + } + } + } + } + return nearestDates } - private func scanDate(element: Dictionary.Element?) -> DBUIScanDate { + private func scanDate(element: Dictionary.Element?, + nearestScanOperationByBroker: [String: Date]) -> DBUIScanDate { if let element = element, let date = element.key { return DBUIScanDate( date: date.timeIntervalSince1970, dataBrokers: element.value.flatMap { - var brokers = [DBPUIDataBroker(name: $0.dataBroker.name)] + let brokerOperationDate = nearestScanOperationByBroker[$0.dataBroker.url] + var brokers = [DBPUIDataBroker(name: $0.dataBroker.name, url: $0.dataBroker.url, date: brokerOperationDate?.timeIntervalSince1970 ?? nil)] for mirrorSite in $0.dataBroker.mirrorSites where mirrorSite.shouldWeIncludeMirrorSite(for: date) { - brokers.append(DBPUIDataBroker(name: mirrorSite.name)) + brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: brokerOperationDate?.timeIntervalSince1970 ?? nil)) } return brokers } + .reduce(into: []) { result, dataBroker in // Remove dupes + guard !result.contains(where: { $0.url == dataBroker.url }) else { + return + } + result.append(dataBroker) + } ) } else { return DBUIScanDate(date: 0, dataBrokers: [DBPUIDataBroker]()) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift index 2a6108f826..a604a0a4d7 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift @@ -20,11 +20,174 @@ import XCTest @testable import DataBrokerProtection final class BrokerJSONCodableTests: XCTestCase { - let verecorJSONString = """ + let verecorWithURLJSONString = """ + { + "name": "Verecor", + "url": "verecor.com", + "version": "0.1.0", + "addedDatetime": 1677128400000, + "mirrorSites": [ + { + "name": "Potato", + "url": "potato.com", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "Tomato", + "url": "tomato.com", + "addedAt": 1705599286529, + "removedAt": null + } + ], + "steps": [ + { + "stepType": "scan", + "scanType": "templatedUrl", + "actions": [ + { + "actionType": "navigate", + "id": "84aa05bc-1ca0-4f16-ae74-dfb352ce0eee", + "url": "https://verecor.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${ageRange}", + "ageRange": [ + "18-30", + "31-40", + "41-50", + "51-60", + "61-70", + "71-80", + "81+" + ] + }, + { + "actionType": "extract", + "id": "92252eb5-ccaf-4b00-a3fe-019110ce0534", + "selector": ".search-item", + "profile": { + "name": { + "selector": "h4" + }, + "alternativeNamesList": { + "selector": ".//div[@class='col-sm-24 col-md-16 name']//li", + "findElements": true + }, + "age": { + "selector": ".age" + }, + "addressCityStateList": { + "selector": ".//div[@class='col-sm-24 col-md-8 location']//li", + "findElements": true + }, + "profileUrl": { + "selector": "a" + } + } + } + ] + }, + { + "stepType": "optOut", + "optOutType": "formOptOut", + "actions": [ + { + "actionType": "navigate", + "id": "49f9aa73-4f97-47c0-b8bf-1729e9c169c0", + "url": "https://verecor.com/ng/control/privacy" + }, + { + "actionType": "fillForm", + "id": "55b1d0bb-d303-4b6f-bf9e-3fd96746f27e", + "selector": ".ahm", + "elements": [ + { + "type": "fullName", + "selector": "#user_name" + }, + { + "type": "email", + "selector": "#user_email" + }, + { + "type": "profileUrl", + "selector": "#url" + } + ] + }, + { + "actionType": "getCaptchaInfo", + "id": "9efb1153-8f52-41e4-a8fb-3077a97a586d", + "selector": ".g-recaptcha" + }, + { + "actionType": "solveCaptcha", + "id": "ed49e4c3-0cfa-4f1e-b3d1-06ad7b8b9ba4", + "selector": ".g-recaptcha" + }, + { + "actionType": "click", + "id": "6b986aa4-3d1b-44d5-8b2b-5463ee8916c9", + "elements": [ + { + "type": "button", + "selector": ".btn-sbmt" + } + ] + }, + { + "actionType": "expectation", + "id": "d4c64d9b-1004-487e-ab06-ae74869bc9a7", + "expectations": [ + { + "type": "text", + "selector": "body", + "expect": "Your removal request has been received" + } + ] + }, + { + "actionType": "emailConfirmation", + "id": "3b4c611a-61ab-4792-810e-d5b3633ea203", + "pollingTime": 30 + }, + { + "actionType": "expectation", + "id": "afe805a0-d422-473c-b47f-995a8672d476", + "expectations": [ + { + "type": "text", + "selector": "body", + "expect": "Your information control request has been confirmed." + } + ] + } + ] + } + ], + "schedulingConfig": { + "retryError": 48, + "confirmOptOutScan": 72, + "maintenanceScan": 240 + } + } + + """ + let verecorNoURLJSONString = """ { "name": "verecor.com", "version": "0.1.0", "addedDatetime": 1677128400000, + "mirrorSites": [ + { + "name": "tomato.com", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "potato.com", + "addedAt": 1705599286529, + "removedAt": null + } + ], "steps": [ { "stepType": "scan", @@ -157,9 +320,27 @@ final class BrokerJSONCodableTests: XCTestCase { """ - func testVerecorJSON_isCorrectlyParsed() { + func testVerecorJSONNoURL_isCorrectlyParsed() { do { - _ = try JSONDecoder().decode(DataBroker.self, from: verecorJSONString.data(using: .utf8)!) + let broker = try JSONDecoder().decode(DataBroker.self, from: verecorNoURLJSONString.data(using: .utf8)!) + XCTAssertEqual(broker.url, broker.name) + for mirror in broker.mirrorSites { + XCTAssertEqual(mirror.url, mirror.name) + } + } catch { + XCTFail("JSON string should be parsed correctly.") + } + } + + func testVerecorJSONWithURL_isCorrectlyParsed() { + do { + let broker = try JSONDecoder().decode(DataBroker.self, from: verecorWithURLJSONString.data(using: .utf8)!) + XCTAssertEqual(broker.url, "verecor.com") + XCTAssertEqual(broker.name, "Verecor") + + for mirror in broker.mirrorSites { + XCTAssertNotEqual(mirror.url, mirror.name) + } } catch { XCTFail("JSON string should be parsed correctly.") } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift index 3a65f7ae9a..5fa7f4b069 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift @@ -44,7 +44,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -92,7 +92,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -143,7 +143,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -745,7 +745,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -770,7 +770,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let currentPreferredRunDate = Date() let expectedPreferredRunDate = Date().addingTimeInterval(config.confirmOptOutScan.hoursToSeconds) - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -846,6 +846,7 @@ extension DataBroker { DataBroker( id: 1, name: "Test broker", + url: "testbroker.com", steps: [ Step(type: .scan, actions: [Action]()), Step(type: .optOut, actions: [Action]()) @@ -863,6 +864,7 @@ extension DataBroker { DataBroker( id: 1, name: "Test broker", + url: "testbroker.com", steps: [ Step(type: .scan, actions: [Action]()), Step(type: .optOut, actions: [Action](), optOutType: .parentSiteOptOut) @@ -879,6 +881,7 @@ extension DataBroker { static var mockWithoutId: DataBroker { DataBroker( name: "Test broker", + url: "testbroker.com", steps: [Step](), version: "1.0", schedulingConfig: DataBrokerScheduleConfig( diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift index dcbc31a911..2036ac9a1d 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift @@ -111,7 +111,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { if let vault = self.vault { let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil - resources.brokersList = [.init(id: 1, name: "Broker", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] + resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] vault.shouldReturnOldVersionBroker = true sut.checkForUpdatesInBrokerJSONFiles() @@ -129,7 +129,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { if let vault = self.vault { let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil - resources.brokersList = [.init(id: 1, name: "Broker", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] + resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] vault.shouldReturnNewVersionBroker = true sut.checkForUpdatesInBrokerJSONFiles() @@ -146,7 +146,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { if let vault = self.vault { let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil - resources.brokersList = [.init(id: 1, name: "Broker", steps: [Step](), version: "1.0.0", schedulingConfig: .mock)] + resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock)] vault.profileQueries = [.mock] sut.checkForUpdatesInBrokerJSONFiles() diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift index cc23589ec8..c82680d1e9 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift @@ -41,7 +41,7 @@ final class EmailServiceTests: XCTestCase { let sut = EmailService(urlSession: mockURLSession, redeemUseCase: MockRedeemUseCase()) do { - _ = try await sut.getEmail(dataBrokerName: "fakeBroker") + _ = try await sut.getEmail(dataBrokerURL: "fakeBroker") XCTFail("Expected an error to be thrown") } catch { if let error = error as? EmailError, @@ -62,7 +62,7 @@ final class EmailServiceTests: XCTestCase { let sut = EmailService(urlSession: mockURLSession, redeemUseCase: MockRedeemUseCase()) do { - _ = try await sut.getEmail(dataBrokerName: "fakeBroker") + _ = try await sut.getEmail(dataBrokerURL: "fakeBroker") XCTFail("Expected an error to be thrown") } catch { if let error = error as? EmailError, case .cantFindEmail = error { @@ -81,7 +81,7 @@ final class EmailServiceTests: XCTestCase { let sut = EmailService(urlSession: mockURLSession, redeemUseCase: MockRedeemUseCase()) do { - let email = try await sut.getEmail(dataBrokerName: "fakeBroker") + let email = try await sut.getEmail(dataBrokerURL: "fakeBroker") XCTAssertEqual("test@ddg.com", email) } catch { XCTFail("Unexpected. It should not throw") diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift index 4d1ff6b8f8..c59ee80486 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift @@ -140,9 +140,9 @@ final class MapperToUITests: XCTestCase { func testLastScans_areMappedCorrectly() { let brokerProfileQueryData: [BrokerProfileQueryData] = [ - .mock(dataBrokerName: "Broker #1", lastRunDate: Date().yesterday), - .mock(dataBrokerName: "Broker #2", lastRunDate: Date().yesterday), - .mock(dataBrokerName: "Broker #3") + .mock(dataBrokerName: "Broker #1", url: "broker1.com", lastRunDate: Date().yesterday), + .mock(dataBrokerName: "Broker #2", url: "broker2.com", lastRunDate: Date().yesterday), + .mock(dataBrokerName: "Broker #3", url: "broker3.com") ] let result = sut.maintenanceScanState(brokerProfileQueryData) @@ -153,9 +153,9 @@ final class MapperToUITests: XCTestCase { func testNextScans_areMappedCorrectly() { let brokerProfileQueryData: [BrokerProfileQueryData] = [ - .mock(dataBrokerName: "Broker #1", preferredRunDate: Date().tomorrow), - .mock(dataBrokerName: "Broker #2", preferredRunDate: Date().tomorrow), - .mock(dataBrokerName: "Broker #3") + .mock(dataBrokerName: "Broker #1", url: "broker1.com", preferredRunDate: Date().tomorrow), + .mock(dataBrokerName: "Broker #2", url: "broker2.com", preferredRunDate: Date().tomorrow), + .mock(dataBrokerName: "Broker #3", url: "broker3.com") ] let result = sut.maintenanceScanState(brokerProfileQueryData) @@ -165,7 +165,7 @@ final class MapperToUITests: XCTestCase { } func testWhenMirrorSiteIsNotInRemovedPeriod_thenItShouldBeAddedToTotalScans() { - let brokerProfileQueryWithMirrorSite: BrokerProfileQueryData = .mock(dataBrokerName: "Broker #1", mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: nil)]) + let brokerProfileQueryWithMirrorSite: BrokerProfileQueryData = .mock(dataBrokerName: "Broker #1", mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: nil)]) let brokerProfileQueryData: [BrokerProfileQueryData] = [ brokerProfileQueryWithMirrorSite, brokerProfileQueryWithMirrorSite, @@ -178,7 +178,7 @@ final class MapperToUITests: XCTestCase { } func testWhenMirrorSiteIsInRemovedPeriod_thenItShouldNotBeAddedToTotalScans() { - let brokerWithMirrorSiteThatWasRemoved = BrokerProfileQueryData.mock(dataBrokerName: "Broker #1", mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: Date().yesterday)]) + let brokerWithMirrorSiteThatWasRemoved = BrokerProfileQueryData.mock(dataBrokerName: "Broker #1", mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: Date().yesterday)]) let brokerProfileQueryData: [BrokerProfileQueryData] = [.mock(dataBrokerName: "Broker #1"), brokerWithMirrorSiteThatWasRemoved, .mock(dataBrokerName: "Broker #2")] let result = sut.initialScanState(brokerProfileQueryData) @@ -190,7 +190,7 @@ final class MapperToUITests: XCTestCase { let brokerWithMirrorSiteNotRemovedAndWithScan = BrokerProfileQueryData.mock( dataBrokerName: "Broker #1", lastRunDate: Date(), - mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: nil)] + mirrorSites: [.init(name: "mirror", url: "mirror.com", addedAt: Date(), removedAt: nil)] ) let brokerProfileQueryData: [BrokerProfileQueryData] = [ brokerWithMirrorSiteNotRemovedAndWithScan, @@ -207,7 +207,7 @@ final class MapperToUITests: XCTestCase { let brokerWithMirrorSiteRemovedAndWithScan = BrokerProfileQueryData.mock( dataBrokerName: "Broker #2", lastRunDate: Date(), - mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: Date().yesterday)] + mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: Date().yesterday)] ) let brokerProfileQueryData: [BrokerProfileQueryData] = [ .mock(dataBrokerName: "Broker #1"), @@ -223,7 +223,7 @@ final class MapperToUITests: XCTestCase { func testWhenMirrorSiteIsNotInRemovedPeriod_thenMatchIsAdded() { let brokerWithMirrorSiteNotRemovedAndWithMatch = BrokerProfileQueryData.mock( extractedProfile: .mockWithoutRemovedDate, - mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: nil)] + mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: nil)] ) let brokerProfileQueryData: [BrokerProfileQueryData] = [.mock(), .mock(), brokerWithMirrorSiteNotRemovedAndWithMatch] @@ -235,7 +235,7 @@ final class MapperToUITests: XCTestCase { func testWhenMirrorSiteIsInRemovedPeriod_thenMatchIsNotAdded() { let brokerWithMirrorSiteRemovedAndWithMatch = BrokerProfileQueryData.mock( extractedProfile: .mockWithoutRemovedDate, - mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: Date().yesterday)] + mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: Date().yesterday)] ) let brokerProfileQueryData: [BrokerProfileQueryData] = [.mock(), .mock(), brokerWithMirrorSiteRemovedAndWithMatch] @@ -246,8 +246,8 @@ final class MapperToUITests: XCTestCase { func testMirrorSites_areCorrectlyMappedToInProgressOptOuts() { let scanHistoryEventsWithMatchesFound: [HistoryEvent] = [.init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 1), date: Date())] - let mirrorSiteNotRemoved = MirrorSite(name: "mirror #1", addedAt: Date.distantPast, removedAt: nil) - let mirrorSiteRemoved = MirrorSite(name: "mirror #2", addedAt: Date.distantPast, removedAt: Date().yesterday) // Should not be added + let mirrorSiteNotRemoved = MirrorSite(name: "mirror #1", url: "mirror1.com", addedAt: Date.distantPast, removedAt: nil) + let mirrorSiteRemoved = MirrorSite(name: "mirror #2", url: "mirror2.com", addedAt: Date.distantPast, removedAt: Date().yesterday) // Should not be added let brokerProfileQueryData: [BrokerProfileQueryData] = [ .mock(extractedProfile: .mockWithoutRemovedDate, scanHistoryEvents: scanHistoryEventsWithMatchesFound, @@ -261,10 +261,10 @@ final class MapperToUITests: XCTestCase { func testWhenMirrorSiteRemovedIsInRangeToPastRemovedProfile_thenIsAddedToCompletedOptOuts() { let scanHistoryEventsWithMatchesFound: [HistoryEvent] = [.init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 1), date: Date().yesterday!)] - let mirrorSiteRemoved = MirrorSite(name: "mirror #1", addedAt: Date.distantPast, removedAt: Date()) // Should be added + let mirrorSiteRemoved = MirrorSite(name: "mirror #1", url: "mirror1.com", addedAt: Date.distantPast, removedAt: Date()) // Should be added // The next two mirror sites should not be added. New mirror sites should not count for old opt-outs - let newMirrorSiteOne = MirrorSite(name: "mirror #2", addedAt: Date(), removedAt: nil) - let newMirrorSiteTwo = MirrorSite(name: "mirror #3", addedAt: Date(), removedAt: nil) + let newMirrorSiteOne = MirrorSite(name: "mirror #2", url: "mirror2.com", addedAt: Date(), removedAt: nil) + let newMirrorSiteTwo = MirrorSite(name: "mirror #3", url: "mirror3.com", addedAt: Date(), removedAt: nil) let brokerProfileQuery = BrokerProfileQueryData.mock(extractedProfile: .mockWithRemoveDate(Date().yesterday!), scanHistoryEvents: scanHistoryEventsWithMatchesFound, mirrorSites: [mirrorSiteRemoved, newMirrorSiteOne, newMirrorSiteTwo]) @@ -276,12 +276,12 @@ final class MapperToUITests: XCTestCase { } func testLastScansWithMirrorSites_areMappedCorrectly() { - let includedMirrorSite = MirrorSite(name: "mirror #1", addedAt: Date.distantPast, removedAt: nil) - let notIncludedMirrorSite = MirrorSite(name: "mirror #2", addedAt: Date(), removedAt: nil) + let includedMirrorSite = MirrorSite(name: "mirror #1", url: "mirror1.com", addedAt: Date.distantPast, removedAt: nil) + let notIncludedMirrorSite = MirrorSite(name: "mirror #2", url: "mirror2.com", addedAt: Date(), removedAt: nil) let brokerProfileQueryData: [BrokerProfileQueryData] = [ - .mock(dataBrokerName: "Broker #1", lastRunDate: Date().yesterday, mirrorSites: [includedMirrorSite, notIncludedMirrorSite]), - .mock(dataBrokerName: "Broker #2", lastRunDate: Date().yesterday), - .mock(dataBrokerName: "Broker #3") + .mock(dataBrokerName: "Broker #1", url: "broker1.com", lastRunDate: Date().yesterday, mirrorSites: [includedMirrorSite, notIncludedMirrorSite]), + .mock(dataBrokerName: "Broker #2", url: "broker2.com", lastRunDate: Date().yesterday), + .mock(dataBrokerName: "Broker #3", url: "broker3.com") ] let result = sut.maintenanceScanState(brokerProfileQueryData) @@ -291,12 +291,12 @@ final class MapperToUITests: XCTestCase { } func testNextScansWithMirrorSites_areMappedCorrectly() { - let includedMirrorSite = MirrorSite(name: "mirror #1", addedAt: Date.distantPast, removedAt: nil) - let notIncludedMirrorSite = MirrorSite(name: "mirror #2", addedAt: Date.distantPast, removedAt: Date()) + let includedMirrorSite = MirrorSite(name: "mirror #1", url: "mirror1.com", addedAt: Date.distantPast, removedAt: nil) + let notIncludedMirrorSite = MirrorSite(name: "mirror #2", url: "mirror2.com", addedAt: Date.distantPast, removedAt: Date()) let brokerProfileQueryData: [BrokerProfileQueryData] = [ - .mock(dataBrokerName: "Broker #1", preferredRunDate: Date().tomorrow, mirrorSites: [includedMirrorSite, notIncludedMirrorSite]), - .mock(dataBrokerName: "Broker #2", preferredRunDate: Date().tomorrow), - .mock(dataBrokerName: "Broker #3") + .mock(dataBrokerName: "Broker #1", url: "broker1.com", preferredRunDate: Date().tomorrow, mirrorSites: [includedMirrorSite, notIncludedMirrorSite]), + .mock(dataBrokerName: "Broker #2", url: "broker2.com", preferredRunDate: Date().tomorrow), + .mock(dataBrokerName: "Broker #3", url: "broker3.com") ] let result = sut.maintenanceScanState(brokerProfileQueryData) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift index 8585792744..12b7120077 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift @@ -152,6 +152,7 @@ extension BrokerProfileQueryData { BrokerProfileQueryData( dataBroker: DataBroker( name: "parent", + url: "parent.com", steps: [Step](), version: "1.0.0", schedulingConfig: DataBrokerScheduleConfig.mock @@ -165,6 +166,7 @@ extension BrokerProfileQueryData { BrokerProfileQueryData( dataBroker: DataBroker( name: "child", + url: "child.com", steps: [Step](), version: "1.0.0", schedulingConfig: DataBrokerScheduleConfig.mock, diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index d053404255..95ea0d1493 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -27,6 +27,7 @@ import GRDB extension BrokerProfileQueryData { static func mock(with steps: [Step] = [Step](), dataBrokerName: String = "test", + url: String = "test.com", lastRunDate: Date? = nil, preferredRunDate: Date? = nil, extractedProfile: ExtractedProfile? = nil, @@ -36,6 +37,7 @@ extension BrokerProfileQueryData { BrokerProfileQueryData( dataBroker: DataBroker( name: dataBrokerName, + url: url, steps: steps, version: "1.0.0", schedulingConfig: DataBrokerScheduleConfig.mock, @@ -232,7 +234,7 @@ final class EmailServiceMock: EmailServiceProtocol { var shouldThrow: Bool = false - func getEmail(dataBrokerName: String?) async throws -> String { + func getEmail(dataBrokerURL: String?) async throws -> String { if shouldThrow { throw DataBrokerProtectionError.emailError(nil) } @@ -491,9 +493,9 @@ final class DataBrokerProtectionSecureVaultMock: DataBrokerProtectionSecureVault func fetchBroker(with name: String) throws -> DataBroker? { if shouldReturnOldVersionBroker { - return .init(id: 1, name: "Broker", steps: [Step](), version: "1.0.0", schedulingConfig: .mock) + return .init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock) } else if shouldReturnNewVersionBroker { - return .init(id: 1, name: "Broker", steps: [Step](), version: "1.0.1", schedulingConfig: .mock) + return .init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock) } return nil diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift index b9472a9ab9..369f80ee32 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift @@ -35,6 +35,7 @@ final class OperationPreferredDateUpdaterTests: XCTestCase { let childBroker = DataBroker( id: 1, name: "Child broker", + url: "childbroker.com", steps: [Step](), version: "1.0", schedulingConfig: DataBrokerScheduleConfig( diff --git a/LocalPackages/NetworkProtectionMac/Package.resolved b/LocalPackages/NetworkProtectionMac/Package.resolved new file mode 100644 index 0000000000..08c5add0f4 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Package.resolved @@ -0,0 +1,104 @@ +{ + "pins" : [ + { + "identity" : "bloom_cpp", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/bloom_cpp.git", + "state" : { + "revision" : "8076199456290b61b4544bf2f4caf296759906a0", + "version" : "3.0.0" + } + }, + { + "identity" : "browserserviceskit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/BrowserServicesKit", + "state" : { + "revision" : "1f7932fe67a0d8b1ae97e62cb333639353d4772f", + "version" : "101.2.2" + } + }, + { + "identity" : "content-scope-scripts", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/content-scope-scripts", + "state" : { + "revision" : "0b68b0d404d8d4f32296cd84fa160b18b0aeaf44", + "version" : "4.59.1" + } + }, + { + "identity" : "duckduckgo-autofill", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", + "state" : { + "revision" : "b972bc0ab6ee1d57a0a18a197dcc31e40ae6ac57", + "version" : "10.0.3" + } + }, + { + "identity" : "grdb.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/GRDB.swift.git", + "state" : { + "revision" : "9f049d7b97b1e68ffd86744b500660d34a9e79b8", + "version" : "2.3.0" + } + }, + { + "identity" : "privacy-dashboard", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/privacy-dashboard", + "state" : { + "revision" : "38336a574e13090764ba09a6b877d15ee514e371", + "version" : "3.1.1" + } + }, + { + "identity" : "punycodeswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gumob/PunycodeSwift.git", + "state" : { + "revision" : "4356ec54e073741449640d3d50a1fd24fd1e1b8b", + "version" : "2.1.0" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", + "version" : "1.3.0" + } + }, + { + "identity" : "sync_crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/sync_crypto", + "state" : { + "revision" : "2ab6ab6f0f96b259c14c2de3fc948935fc16ac78", + "version" : "0.2.0" + } + }, + { + "identity" : "trackerradarkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/TrackerRadarKit", + "state" : { + "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", + "version" : "1.2.2" + } + }, + { + "identity" : "wireguard-apple", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/wireguard-apple", + "state" : { + "revision" : "2d8172c11478ab11b0f5ad49bdb4f93f4b3d5e0d", + "version" : "1.1.1" + } + } + ], + "version" : 2 +} diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 18d2ba42c6..5d017910e3 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -27,6 +27,7 @@ let package = Package( ], products: [ .library(name: "NetworkProtectionIPC", targets: ["NetworkProtectionIPC"]), + .library(name: "NetworkProtectionProxy", targets: ["NetworkProtectionProxy"]), .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ @@ -50,6 +51,19 @@ let package = Package( plugins: [.plugin(name: "SwiftLintPlugin", package: "BrowserServicesKit")] ), + // MARK: - NetworkProtectionProxy + + .target( + name: "NetworkProtectionProxy", + dependencies: [ + .product(name: "NetworkProtection", package: "BrowserServicesKit") + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin", package: "BrowserServicesKit")] + ), + // MARK: - NetworkProtectionUI .target( diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift new file mode 100644 index 0000000000..882eb19734 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift @@ -0,0 +1,242 @@ +// +// TCPFlowManager.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NetworkExtension +import OSLog // swiftlint:disable:this enforce_os_log_wrapper + +/// A private global actor to handle UDP flows management +/// +@globalActor +struct TCPFlowActor { + actor ActorType { } + + static let shared: ActorType = ActorType() +} + +@TCPFlowActor +enum RemoteConnectionError: Error { + case complete + case cancelled + case couldNotEstablishConnection(_ error: Error) + case unhandledError(_ error: Error) +} + +final class TCPFlowManager { + private let flow: NEAppProxyTCPFlow + private var connectionTask: Task? + private var connection: NWConnection? + + init(flow: NEAppProxyTCPFlow) { + self.flow = flow + } + + deinit { + // Just making extra sure we don't have any unexpected retain cycle + connection?.stateUpdateHandler = nil + connection?.cancel() + } + + func start(interface: NWInterface) async throws { + guard let remoteEndpoint = flow.remoteEndpoint as? NWHostEndpoint else { + return + } + + try await connectAndStartRunLoop(remoteEndpoint: remoteEndpoint, interface: interface) + } + + private func connectAndStartRunLoop(remoteEndpoint: NWHostEndpoint, interface: NWInterface) async throws { + let remoteConnection = try await connect(to: remoteEndpoint, interface: interface) + try await flow.open(withLocalEndpoint: nil) + + do { + try await startDataCopyLoop(for: remoteConnection) + + remoteConnection.cancel() + flow.closeReadWithError(nil) + flow.closeWriteWithError(nil) + } catch { + remoteConnection.cancel() + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + } + } + + func connect(to remoteEndpoint: NWHostEndpoint, interface: NWInterface) async throws -> NWConnection { + try await withCheckedThrowingContinuation { continuation in + connect(to: remoteEndpoint, interface: interface) { result in + switch result { + case .success(let connection): + continuation.resume(returning: connection) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + func connect(to remoteEndpoint: NWHostEndpoint, interface: NWInterface, completion: @escaping @TCPFlowActor (Result) -> Void) { + let host = Network.NWEndpoint.Host(remoteEndpoint.hostname) + let port = Network.NWEndpoint.Port(remoteEndpoint.port)! + + let parameters = NWParameters.tcp + parameters.preferNoProxies = true + parameters.requiredInterface = interface + parameters.prohibitedInterfaceTypes = [.other] + + let connection = NWConnection(host: host, port: port, using: parameters) + self.connection = connection + + connection.stateUpdateHandler = { (state: NWConnection.State) in + Task { @TCPFlowActor in + switch state { + case .ready: + connection.stateUpdateHandler = nil + completion(.success(connection)) + case .cancelled: + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.cancelled)) + case .failed(let error), .waiting(let error): + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.couldNotEstablishConnection(error))) + default: + break + } + } + } + + connection.start(queue: .global()) + } + + private func startDataCopyLoop(for remoteConnection: NWConnection) async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { [weak self] in + while true { + guard let self else { + throw RemoteConnectionError.cancelled + } + + try Task.checkCancellation() + try await self.copyOutboundTraffic(to: remoteConnection) + } + } + + group.addTask { [weak self] in + while true { + guard let self else { + throw RemoteConnectionError.cancelled + } + + try Task.checkCancellation() + try await self.copyInboundTraffic(from: remoteConnection) + } + } + + while !group.isEmpty { + do { + try await group.next() + + } catch { + group.cancelAll() + throw error + } + } + } + } + + @MainActor + func closeFlow(remoteConnection: NWConnection, error: Error?) { + remoteConnection.forceCancel() + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + } + + static let maxReceiveSize: Int = Int(Measurement(value: 2, unit: UnitInformationStorage.megabytes).converted(to: .bytes).value) + + func copyInboundTraffic(from remoteConnection: NWConnection) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + Task { @TCPFlowActor in + remoteConnection.receive(minimumIncompleteLength: 1, + maximumLength: Self.maxReceiveSize) { [weak flow] (data, _, isComplete, error) in + guard let flow else { + continuation.resume(throwing: RemoteConnectionError.cancelled) + return + } + + switch (data, isComplete, error) { + case (.some(let data), _, _) where !data.isEmpty: + flow.write(data) { writeError in + if let writeError { + continuation.resume(throwing: writeError) + remoteConnection.cancel() + } else { + continuation.resume() + } + } + case (_, isComplete, _) where isComplete == true: + continuation.resume(throwing: RemoteConnectionError.complete) + remoteConnection.cancel() + case (_, _, .some(let error)): + continuation.resume(throwing: RemoteConnectionError.unhandledError(error)) + remoteConnection.cancel() + default: + continuation.resume(throwing: RemoteConnectionError.complete) + remoteConnection.cancel() + } + } + } + } + } + + func copyOutboundTraffic(to remoteConnection: NWConnection) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + Task { @TCPFlowActor in + flow.readData { data, error in + switch (data, error) { + case (.some(let data), _) where !data.isEmpty: + remoteConnection.send(content: data, completion: .contentProcessed({ error in + if let error { + continuation.resume(throwing: error) + remoteConnection.cancel() + return + } + + continuation.resume() + })) + case (_, .some(let error)): + continuation.resume(throwing: error) + remoteConnection.cancel() + default: + continuation.resume(throwing: RemoteConnectionError.complete) + remoteConnection.cancel() + } + } + } + } + } +} + +extension TCPFlowManager: Hashable { + static func == (lhs: TCPFlowManager, rhs: TCPFlowManager) -> Bool { + lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine(flow) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/UDPFlowManager.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/UDPFlowManager.swift new file mode 100644 index 0000000000..000f37d20e --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/UDPFlowManager.swift @@ -0,0 +1,329 @@ +// +// UDPFlowManager.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NetworkExtension +import OSLog // swiftlint:disable:this enforce_os_log_wrapper + +/// A private global actor to handle UDP flows management +/// +@globalActor +struct UDPFlowActor { + actor ActorType { } + + static let shared: ActorType = ActorType() +} + +/// Class to handle UDP connections +/// +/// This is necessary because as described in the reference comment for this implementation (see ``UDPFlowManager``'s documentation) +/// it's noted that a single UDP flow can have to manage multiple connections. +/// +@UDPFlowActor +final class UDPConnectionManager { + let endpoint: NWEndpoint + private let connection: NWConnection + private let onReceive: (_ endpoint: NWEndpoint, _ result: Result) async -> Void + + init(endpoint: NWHostEndpoint, interface: NWInterface?, onReceive: @UDPFlowActor @escaping (_ endpoint: NWEndpoint, _ result: Result) async -> Void) { + let host = Network.NWEndpoint.Host(endpoint.hostname) + let port = Network.NWEndpoint.Port(endpoint.port)! + + let parameters = NWParameters.udp + parameters.preferNoProxies = true + parameters.requiredInterface = interface + parameters.prohibitedInterfaceTypes = [.other] + + let connection = NWConnection(host: host, port: port, using: parameters) + + self.connection = connection + self.endpoint = endpoint + self.onReceive = onReceive + } + + deinit { + // Just making extra sure we don't retain anything we don't need to + connection.stateUpdateHandler = nil + connection.cancel() + } + + // MARK: - General Operation + + /// Starts the operation of this connection manager + /// + /// Can be called multiple times safely. + /// + private func start() async throws { + guard connection.state == .setup else { + return + } + + try await connect() + + Task { + while true { + do { + let datagram = try await receive() + await onReceive(endpoint, .success(datagram)) + } catch { + connection.cancel() + await onReceive(endpoint, .failure(error)) + break + } + } + } + } + + // MARK: - Connection Management + + private func connect() async throws { + try await withCheckedThrowingContinuation { continuation in + connect { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + private func connect(completion: @escaping (Result) -> Void) { + connection.stateUpdateHandler = { [connection] (state: NWConnection.State) in + switch state { + case .ready: + connection.stateUpdateHandler = nil + completion(.success(())) + case .cancelled: + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.cancelled)) + case .failed(let error), .waiting(let error): + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.couldNotEstablishConnection(error))) + default: + break + } + } + + connection.start(queue: .global()) + } + + // MARK: - Receiving from remote + + private func receive() async throws -> Data { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + connection.receiveMessage { [weak self] data, _, isComplete, error in + + guard self != nil else { + continuation.resume(throwing: RemoteConnectionError.cancelled) + return + } + + switch (data, isComplete, error) { + case (let data?, _, _): + continuation.resume(returning: data) + case (_, true, _): + continuation.resume(throwing: RemoteConnectionError.cancelled) + case (_, _, let error?): + continuation.resume(throwing: RemoteConnectionError.unhandledError(error)) + default: + continuation.resume(throwing: RemoteConnectionError.cancelled) + } + } + } + } + + // MARK: - Writing datagrams + + func write(datagram: Data) async throws { + try await start() + + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + connection.send(content: datagram, completion: .contentProcessed({ error in + if let error { + continuation.resume(throwing: error) + return + } + + continuation.resume() + })) + } + } +} + +extension UDPConnectionManager: Hashable, Equatable { + // MARK: - Equatable + + static func == (lhs: UDPConnectionManager, rhs: UDPConnectionManager) -> Bool { + lhs.endpoint == rhs.endpoint + } + + // MARK: - Hashable + + func hash(into hasher: inout Hasher) { + hasher.combine(endpoint) + } +} + +/// UDP flow manager class +/// +/// There is documentation explaining how to handle TCP flows here: +/// https://developer.apple.com/documentation/networkextension/app_proxy_provider/handling_flow_copying?changes=_8 +/// +/// Unfortunately there isn't good official documentation showcasing how to implement UDP flow management. +/// The best we could fine are two comments by an Apple engineer that shine some light on how that implementation should be like: +/// https://developer.apple.com/forums/thread/678464?answerId=671531022#671531022 +/// https://developer.apple.com/forums/thread/678464?answerId=671892022#671892022 +/// +/// This class is the result of implementing the description found in that comment. +/// +@UDPFlowActor +final class UDPFlowManager { + private let flow: NEAppProxyUDPFlow + private var interface: NWInterface? + + private var connectionManagers = [NWEndpoint: UDPConnectionManager]() + + init(flow: NEAppProxyUDPFlow) { + self.flow = flow + } + + func start(interface: NWInterface) async throws { + self.interface = interface + try await connectAndStartRunLoop() + } + + private func connectAndStartRunLoop() async throws { + do { + try await flow.open(withLocalEndpoint: nil) + try await startDataCopyLoop() + + flow.closeReadWithError(nil) + flow.closeWriteWithError(nil) + } catch { + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + } + } + + private func startDataCopyLoop() async throws { + while true { + try await copyOutoundTraffic() + } + } + + func copyInboundTraffic(endpoint: NWEndpoint, result: Result) async { + switch result { + case .success(let data): + do { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + flow.writeDatagrams([data], sentBy: [endpoint]) { error in + if let error { + continuation.resume(throwing: error) + return + } + + continuation.resume() + } + } + } catch { + // Any failure means we close the connection + connectionManagers.removeValue(forKey: endpoint) + } + case .failure: + // Any failure means we close the connection + connectionManagers.removeValue(forKey: endpoint) + } + } + + func copyOutoundTraffic() async throws { + let (datagrams, endpoints) = try await read() + + // Ref: https://developer.apple.com/documentation/networkextension/neappproxyudpflow/1406576-readdatagrams + if datagrams.isEmpty || endpoints.isEmpty { + throw NEAppProxyFlowError(.aborted) + } + + for (datagram, endpoint) in zip(datagrams, endpoints) { + guard let endpoint = endpoint as? NWHostEndpoint else { + // Not sure what to do about this... + continue + } + + let manager = connectionManagers[endpoint] ?? { + let manager = UDPConnectionManager(endpoint: endpoint, interface: interface, onReceive: copyInboundTraffic) + connectionManagers[endpoint] = manager + return manager + }() + + do { + try await manager.write(datagram: datagram) + } catch { + // Any failure means we close the connection + connectionManagers.removeValue(forKey: endpoint) + } + } + } + + /// Reads datagrams from the flow. + /// + /// Apple's documentation is very bad here, but it seems each datagram is corresponded with an endpoint at the same position in the array + /// as mentioned here: https://developer.apple.com/forums/thread/75893 + /// + private func read() async throws -> (datagrams: [Data], endpoints: [NWEndpoint]) { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<([Data], [NWEndpoint]), Error>) in + flow.readDatagrams { datagrams, endpoints, error in + if let error { + continuation.resume(throwing: error) + return + } + + guard let datagrams, let endpoints else { + continuation.resume(throwing: NEAppProxyFlowError(.aborted)) + return + } + + continuation.resume(returning: (datagrams, endpoints)) + } + } + } + + private func send(datagram: Data, through remoteConnection: NWConnection) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + remoteConnection.send(content: datagram, completion: .contentProcessed({ error in + if let error { + continuation.resume(throwing: error) + return + } + + continuation.resume() + })) + } + } +} + +extension UDPFlowManager: Hashable { + static func == (lhs: UDPFlowManager, rhs: UDPFlowManager) -> Bool { + lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine(flow) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyAppMessageHandler.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyAppMessageHandler.swift new file mode 100644 index 0000000000..12339a673d --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyAppMessageHandler.swift @@ -0,0 +1,82 @@ +// +// TransparentProxyAppMessageHandler.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import OSLog // swiftlint:disable:this enforce_os_log_wrapper + +/// Handles app messages +/// +final class TransparentProxyAppMessageHandler { + + private let settings: TransparentProxySettings + + init(settings: TransparentProxySettings) { + self.settings = settings + } + + func handle(_ data: Data) async -> Data? { + do { + let message = try JSONDecoder().decode(TransparentProxyMessage.self, from: data) + return await handle(message) + } catch { + return nil + } + } + + /// Handles a message. + /// + /// This method will wrap the message into a request with a completion handler, and will process it. + /// The reason why this method wraps the message in a request is to ensure that the response + /// type stays syncrhonized between app and provider. + /// + private func handle(_ message: TransparentProxyMessage) async -> Data? { + await withCheckedContinuation { continuation in + var request: TransparentProxyRequest + + switch message { + case .changeSetting(let change): + request = .changeSetting(change, responseHandler: { + continuation.resume(returning: nil) + }) + } + + handle(request) + } + } + + /// Handles a request and calls the response handler when done. + /// + private func handle(_ request: TransparentProxyRequest) { + switch request { + case .changeSetting(let change, let responseHandler): + handle(change) + responseHandler() + } + } + + /// Handles a settings change. + /// + private func handle(_ settingChange: TransparentProxySettings.Change) { + switch settingChange { + case .appRoutingRules(let routingRules): + settings.appRoutingRules = routingRules + case .excludedDomains(let excludedDomains): + settings.excludedDomains = excludedDomains + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyRequest.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyRequest.swift new file mode 100644 index 0000000000..0881dba3b1 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyRequest.swift @@ -0,0 +1,67 @@ +// +// TransparentProxyRequest.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NetworkExtension + +public enum TransparentProxyMessage: Codable { + case changeSetting(_ change: TransparentProxySettings.Change) +} + +/// A request for the TransparentProxyProvider. +/// +/// This enum associates a request with a response handler making XPC communication simpler. +/// Once the request completes, `responseHandler` will be called with the result. +/// +public enum TransparentProxyRequest { + case changeSetting(_ settingChange: TransparentProxySettings.Change, responseHandler: () -> Void) + + var message: TransparentProxyMessage { + switch self { + case .changeSetting(let change, _): + return .changeSetting(change) + } + } + + func handleResponse(data: Data?) { + switch self { + case .changeSetting(_, let handleResponse): + handleResponse() + } + } +} + +/// Respresents a transparent proxy session. +/// +/// Offers basic IPC communication support for the app that owns the proxy. This mechanism +/// is implemented through `NETunnelProviderSession` which means only the app that +/// owns the proxy can use this class. +/// +public class TransparentProxySession { + + private let session: NETunnelProviderSession + + init(_ session: NETunnelProviderSession) { + self.session = session + } + + func send(_ request: TransparentProxyRequest) throws { + let payload = try JSONEncoder().encode(request.message) + try session.sendProviderMessage(payload, responseHandler: request.handleResponse(data:)) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift new file mode 100644 index 0000000000..b4c9b8cebb --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift @@ -0,0 +1,89 @@ +// +// TransparentProxyControllerPixel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import PixelKit + +extension TransparentProxyController.StartError: PixelKitEventErrorDetails { + public var underlyingError: Error? { + switch self { + case .failedToLoadConfiguration(let underlyingError), + .failedToSaveConfiguration(let underlyingError), + .failedToStartProvider(let underlyingError): + return underlyingError + default: + return nil + } + } +} + +extension TransparentProxyController { + + public enum Event: PixelKitEventV2 { + case startInitiated + case startSuccess + case startFailure(_ error: Error) + + // MARK: - PixelKit.Event + + public var name: String { + namePrefix + "_" + nameSuffix + } + + public var parameters: [String: String]? { + switch self { + case .startInitiated: + return nil + case .startSuccess: + return nil + case .startFailure: + return nil + } + } + + // MARK: - PixelKit Support + + private static let pixelNamePrefix = "vpn_proxy_controller" + + private var namePrefix: String { + Self.pixelNamePrefix + } + + private var nameSuffix: String { + switch self { + case .startInitiated: + return "start_initiated" + case .startFailure: + return "start_failure" + case .startSuccess: + return "start_success" + } + } + + public var error: Error? { + switch self { + case .startInitiated: + return nil + case .startFailure(let error): + return error + case .startSuccess: + return nil + } + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift new file mode 100644 index 0000000000..aff7421bea --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift @@ -0,0 +1,93 @@ +// +// TransparentProxyProviderPixel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import PixelKit + +extension TransparentProxyProvider.StartError: ErrorWithPixelParameters { + public var errorParameters: [String: String] { + switch self { + case .failedToUpdateNetworkSettings(let underlyingError): + return [ + PixelKit.Parameters.underlyingErrorCode: "\((underlyingError as NSError).code)", + PixelKit.Parameters.underlyingErrorDesc: (underlyingError as NSError).domain, + ] + default: + return [:] + } + } +} + +extension TransparentProxyProvider { + + public enum Event: PixelKitEventV2 { + case failedToUpdateNetworkSettings(_ error: Error) + case startInitiated + case startSuccess + case startFailure(_ error: Error) + + private static let pixelNamePrefix = "vpn_proxy_provider" + + private var namePrefix: String { + Self.pixelNamePrefix + } + + private var namePostfix: String { + switch self { + case .failedToUpdateNetworkSettings: + return "failed_to_update_network_settings" + case .startFailure: + return "start_failure" + case .startInitiated: + return "start_initiated" + case .startSuccess: + return "start_success" + } + } + + public var name: String { + namePrefix + "_" + namePostfix + } + + public var parameters: [String: String]? { + switch self { + case.failedToUpdateNetworkSettings: + return nil + case .startFailure: + return nil + case .startInitiated: + return nil + case .startSuccess: + return nil + } + } + + public var error: Error? { + switch self { + case .failedToUpdateNetworkSettings(let error): + return error + case .startInitiated: + return nil + case .startFailure(let error): + return error + case .startSuccess: + return nil + } + } + } +} diff --git a/DuckDuckGoVPN/Bundle+Configuration.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNAppRoutingRules.swift similarity index 56% rename from DuckDuckGoVPN/Bundle+Configuration.swift rename to LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNAppRoutingRules.swift index 936c44a4a8..15d4a4e1a1 100644 --- a/DuckDuckGoVPN/Bundle+Configuration.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNAppRoutingRules.swift @@ -1,7 +1,7 @@ // -// Bundle+Configuration.swift +// VPNAppRoutingRules.swift // -// Copyright © 2023 DuckDuckGo. All rights reserved. +// Copyright © 2024 DuckDuckGo. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,14 +18,4 @@ import Foundation -extension Bundle { - private static let networkExtensionBundleIDKey = "SYSEX_BUNDLE_ID" - - var networkExtensionBundleID: String { - guard let bundleID = object(forInfoDictionaryKey: Self.networkExtensionBundleIDKey) as? String else { - fatalError("Info.plist is missing \(Self.networkExtensionBundleIDKey)") - } - - return bundleID - } -} +public typealias VPNAppRoutingRules = [String: VPNRoutingRule] diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/SystemExtensionAndNotificationTargets/NetworkProtectionExtensionMachService.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNRoutingRule.swift similarity index 59% rename from DuckDuckGo/NetworkProtection/NetworkExtensionTargets/SystemExtensionAndNotificationTargets/NetworkProtectionExtensionMachService.swift rename to LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNRoutingRule.swift index e90627cbc2..b96a0773cf 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/SystemExtensionAndNotificationTargets/NetworkProtectionExtensionMachService.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNRoutingRule.swift @@ -1,7 +1,7 @@ // -// NetworkProtectionExtensionMachService.swift +// VPNRoutingRule.swift // -// Copyright © 2023 DuckDuckGo. All rights reserved. +// Copyright © 2024 DuckDuckGo. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,14 +18,12 @@ import Foundation -/// Helper methods associated with mach services. +/// Routing rules /// -final class NetworkProtectionExtensionMachService { - - /// Retrieves the mach service name from a network extension bundle. - /// - static func serviceName() -> String { - NetworkProtectionBundle.extensionBundle().machServiceName - } - +/// Note that there's no need for an `ignore` case because that's achieved by not having a rule +/// in the first place. +/// +public enum VPNRoutingRule: Codable, Equatable { + case block + case exclude } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/TransparentProxySettings.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/TransparentProxySettings.swift new file mode 100644 index 0000000000..db010ec2b5 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/TransparentProxySettings.swift @@ -0,0 +1,134 @@ +// +// TransparentProxySettings.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation + +public final class TransparentProxySettings { + public enum Change: Codable { + case appRoutingRules(_ routingRules: VPNAppRoutingRules) + case excludedDomains(_ excludedDomains: [String]) + } + + let defaults: UserDefaults + + private(set) public lazy var changePublisher: AnyPublisher = { + Publishers.MergeMany( + defaults.vpnProxyAppRoutingRulesPublisher + .dropFirst() + .removeDuplicates() + .map { routingRules in + Change.appRoutingRules(routingRules) + }.eraseToAnyPublisher(), + defaults.vpnProxyExcludedDomainsPublisher + .dropFirst() + .removeDuplicates() + .map { excludedDomains in + Change.excludedDomains(excludedDomains) + }.eraseToAnyPublisher() + ).eraseToAnyPublisher() + }() + + public init(defaults: UserDefaults) { + self.defaults = defaults + } + + // MARK: - Settings + + public var appRoutingRules: VPNAppRoutingRules { + get { + defaults.vpnProxyAppRoutingRules + } + + set { + defaults.vpnProxyAppRoutingRules = newValue + } + } + + public var excludedDomains: [String] { + get { + defaults.vpnProxyExcludedDomains + } + + set { + defaults.vpnProxyExcludedDomains = newValue + } + } + + // MARK: - Reset to factory defaults + + public func resetAll() { + defaults.resetVPNProxyAppRoutingRules() + defaults.resetVPNProxyExcludedDomains() + } + + // MARK: - App routing rules logic + + public func isBlocking(_ appIdentifier: String) -> Bool { + appRoutingRules[appIdentifier] == .block + } + + public func isExcluding(_ appIdentifier: String) -> Bool { + appRoutingRules[appIdentifier] == .exclude + } + + public func toggleBlocking(for appIdentifier: String) { + if isBlocking(appIdentifier) { + appRoutingRules.removeValue(forKey: appIdentifier) + } else { + appRoutingRules[appIdentifier] = .block + } + } + + public func toggleExclusion(for appIdentifier: String) { + if isExcluding(appIdentifier) { + appRoutingRules.removeValue(forKey: appIdentifier) + } else { + appRoutingRules[appIdentifier] = .exclude + } + } + + // MARK: - Snapshot support + + public func snapshot() -> TransparentProxySettingsSnapshot { + .init(appRoutingRules: appRoutingRules, excludedDomains: excludedDomains) + } + + public func apply(_ snapshot: TransparentProxySettingsSnapshot) { + appRoutingRules = snapshot.appRoutingRules + excludedDomains = snapshot.excludedDomains + } +} + +extension TransparentProxySettings: CustomStringConvertible { + public var description: String { + """ + TransparentProxySettings {\n + appRoutingRules: \(appRoutingRules)\n + excludedDomains: \(excludedDomains)\n + } + """ + } +} + +public struct TransparentProxySettingsSnapshot: Codable { + public static let key = "com.duckduckgo.TransparentProxySettingsSnapshot" + + public let appRoutingRules: VPNAppRoutingRules + public let excludedDomains: [String] +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedApps.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedApps.swift new file mode 100644 index 0000000000..1090ed1626 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedApps.swift @@ -0,0 +1,79 @@ +// +// UserDefaults+excludedApps.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation + +extension UserDefaults { + private var vpnProxyAppRoutingRulesDataKey: String { + "vpnProxyAppRoutingRulesData" + } + + @objc + dynamic var vpnProxyAppRoutingRulesData: Data? { + get { + object(forKey: vpnProxyAppRoutingRulesDataKey) as? Data + } + + set { + guard let newValue, + newValue.count > 0 else { + + removeObject(forKey: vpnProxyAppRoutingRulesDataKey) + return + } + + set(newValue, forKey: vpnProxyAppRoutingRulesDataKey) + } + } + + var vpnProxyAppRoutingRules: VPNAppRoutingRules { + get { + guard let data = vpnProxyAppRoutingRulesData, + let routingRules = try? JSONDecoder().decode(VPNAppRoutingRules.self, from: data) else { + return [:] + } + + return routingRules + } + + set { + if newValue.isEmpty { + vpnProxyAppRoutingRulesData = nil + return + } + + guard let data = try? JSONEncoder().encode(newValue) else { + vpnProxyAppRoutingRulesData = nil + return + } + + vpnProxyAppRoutingRulesData = data + } + } + + var vpnProxyAppRoutingRulesPublisher: AnyPublisher { + publisher(for: \.vpnProxyAppRoutingRulesData).map { [weak self] _ in + self?.vpnProxyAppRoutingRules ?? [:] + }.eraseToAnyPublisher() + } + + func resetVPNProxyAppRoutingRules() { + removeObject(forKey: vpnProxyAppRoutingRulesDataKey) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedDomains.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedDomains.swift new file mode 100644 index 0000000000..7500178da7 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedDomains.swift @@ -0,0 +1,51 @@ +// +// UserDefaults+excludedDomains.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation + +extension UserDefaults { + private var vpnProxyExcludedDomainsKey: String { + "vpnProxyExcludedDomains" + } + + @objc + dynamic var vpnProxyExcludedDomains: [String] { + get { + object(forKey: vpnProxyExcludedDomainsKey) as? [String] ?? [] + } + + set { + guard newValue.count > 0 else { + + removeObject(forKey: vpnProxyExcludedDomainsKey) + return + } + + set(newValue, forKey: vpnProxyExcludedDomainsKey) + } + } + + var vpnProxyExcludedDomainsPublisher: AnyPublisher<[String], Never> { + publisher(for: \.vpnProxyExcludedDomains).eraseToAnyPublisher() + } + + func resetVPNProxyExcludedDomains() { + removeObject(forKey: vpnProxyExcludedDomainsKey) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyController.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyController.swift new file mode 100644 index 0000000000..fdc7fb3177 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyController.swift @@ -0,0 +1,293 @@ +// +// TransparentProxyController.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation +import NetworkExtension +import NetworkProtection +import OSLog // swiftlint:disable:this enforce_os_log_wrapper +import PixelKit +import SystemExtensions + +/// Controller for ``TransparentProxyProvider`` +/// +@MainActor +public final class TransparentProxyController { + + public enum StartError: Error { + case attemptToStartWithoutBackingActiveFeatures + case couldNotRetrieveProtocolConfiguration + case couldNotEncodeSettingsSnapshot + case failedToLoadConfiguration(_ error: Error) + case failedToSaveConfiguration(_ error: Error) + case failedToStartProvider(_ error: Error) + } + + public typealias EventCallback = (Event) -> Void + public typealias ManagerSetupCallback = (_ manager: NETransparentProxyManager) async -> Void + + /// Dry mode means this won't really do anything to start or stop the proxy. + /// + /// This is useful for testing. + /// + private let dryMode: Bool + + /// The bundleID of the extension that contains the ``TransparentProxyProvider``. + /// + private let extensionID: String + + /// The event handler + /// + public var eventHandler: EventCallback? + + /// Callback to set up a ``NETransparentProxyManager``. + /// + public let setup: ManagerSetupCallback + + private var internalManager: NETransparentProxyManager? + + /// Whether the proxy settings should be stored in the provider configuration. + /// + /// We recommend setting this to true if the provider is running in a System Extension and can't access + /// shared `TransparentProxySettings`. If the provider is in an App Extension you should instead + /// use a shared `TransparentProxySettings` and set this to false. + /// + private let storeSettingsInProviderConfiguration: Bool + public let settings: TransparentProxySettings + private let notificationCenter: NotificationCenter + private var cancellables = Set() + + // MARK: - Initializers + + /// Default initializer. + /// + /// - Parameters: + /// - extensionID: the bundleID for the extension that contains the ``TransparentProxyProvider``. + /// This class DOES NOT take any responsibility in installing the system extension. It only uses + /// the extensionID to identify the appropriate manager configuration to load / save. + /// - storeSettingsInProviderConfiguration: whether the provider configuration will be used for storing + /// the proxy settings. Should be `true` when using a System Extension and `false` when using + /// an App Extension. + /// - settings: the settings to use for this proxy. + /// - dryMode: whether this class is initialized in dry mode. + /// - setup: a callback that will be called whenever a ``NETransparentProxyManager`` needs + /// to be setup. + /// + public init(extensionID: String, + storeSettingsInProviderConfiguration: Bool, + settings: TransparentProxySettings, + notificationCenter: NotificationCenter = .default, + dryMode: Bool = false, + setup: @escaping ManagerSetupCallback) { + + self.dryMode = dryMode + self.extensionID = extensionID + self.notificationCenter = notificationCenter + self.settings = settings + self.setup = setup + self.storeSettingsInProviderConfiguration = storeSettingsInProviderConfiguration + + subscribeToProviderConfigurationChanges() + subscribeToSettingsChanges() + } + + // MARK: - Relay Settings Changes + + private func subscribeToProviderConfigurationChanges() { + notificationCenter.publisher(for: .NEVPNConfigurationChange) + .receive(on: DispatchQueue.main) + .sink { _ in + self.reloadProviderConfiguration() + } + .store(in: &cancellables) + } + + private func reloadProviderConfiguration() { + Task { @MainActor in + try? await self.manager?.loadFromPreferences() + } + } + + private func subscribeToSettingsChanges() { + settings.changePublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: relay(_:)) + .store(in: &cancellables) + } + + private func relay(_ change: TransparentProxySettings.Change) { + Task { @MainActor in + guard let session = await session else { + return + } + + switch session.status { + case .connected, .connecting, .reasserting: + break + default: + return + } + + try TransparentProxySession(session).send(.changeSetting(change, responseHandler: { + // no-op + })) + } + } + + // MARK: - Setting up NETransparentProxyManager + + /// Loads a saved manager + /// + /// This is a bit of a hack that will be run just once for the instance. The reason we want this to run only once is that + /// `NETransparentProxyManager.loadAllFromPreferences()` has a bug where it triggers status change + /// notifications. If the code trying to retrieve the manager is the result of a notification, we may soon find outselves + /// in an infinite loop. + /// + private var triedLoadingManager = false + + /// Loads the configuration matching our ``extensionID``. + /// + public var manager: NETransparentProxyManager? { + get async { + if let internalManager { + return internalManager + } + + if !triedLoadingManager { + triedLoadingManager = true + + let manager = try? await NETransparentProxyManager.loadAllFromPreferences().first { manager in + (manager.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier == extensionID + } + self.internalManager = manager + } + + return internalManager + } + } + + /// Loads an existing configuration or creates a new one, if one doesn't exist. + /// + /// - Returns a properly configured `NETransparentProxyManager`. + /// + public func loadOrCreateConfiguration() async throws -> NETransparentProxyManager { + let manager = await manager ?? { + let manager = NETransparentProxyManager() + internalManager = manager + return manager + }() + + await setup(manager) + try setupAdditionalProviderConfiguration(manager) + + try await manager.saveToPreferences() + try await manager.loadFromPreferences() + + return manager + } + + private func setupAdditionalProviderConfiguration(_ manager: NETransparentProxyManager) throws { + guard storeSettingsInProviderConfiguration else { + return + } + + guard let providerProtocol = manager.protocolConfiguration as? NETunnelProviderProtocol else { + throw StartError.couldNotRetrieveProtocolConfiguration + } + + var providerConfiguration = providerProtocol.providerConfiguration ?? [String: Any]() + + guard let encodedSettings = try? JSONEncoder().encode(settings.snapshot()), + let encodedSettingsString = String(data: encodedSettings, encoding: .utf8) else { + + throw StartError.couldNotEncodeSettingsSnapshot + } + + providerConfiguration[TransparentProxySettingsSnapshot.key] = encodedSettingsString as NSString + providerProtocol.providerConfiguration = providerConfiguration + + } + + // MARK: - Connection & Session + + public var connection: NEVPNConnection? { + get async { + await manager?.connection + } + } + + public var session: NETunnelProviderSession? { + get async { + guard let manager = await manager, + let session = manager.connection as? NETunnelProviderSession else { + + // The active connection is not running, so there's no session, this is acceptable + return nil + } + + return session + } + } + + // MARK: - Connection + + public var status: NEVPNStatus { + get async { + await connection?.status ?? .disconnected + } + } + + // MARK: - Start & stop the proxy + + public var isRequiredForActiveFeatures: Bool { + settings.appRoutingRules.count > 0 || settings.excludedDomains.count > 0 + } + + public func start() async throws { + eventHandler?(.startInitiated) + + guard isRequiredForActiveFeatures else { + let error = StartError.attemptToStartWithoutBackingActiveFeatures + eventHandler?(.startFailure(error)) + throw error + } + + let manager: NETransparentProxyManager + + do { + manager = try await loadOrCreateConfiguration() + } catch { + eventHandler?(.startFailure(error)) + throw error + } + + do { + try manager.connection.startVPNTunnel(options: [:]) + } catch { + let error = StartError.failedToStartProvider(error) + eventHandler?(.startFailure(error)) + throw error + } + + eventHandler?(.startSuccess) + } + + public func stop() async { + await connection?.stopVPNTunnel() + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProvider.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProvider.swift new file mode 100644 index 0000000000..33b75fd73b --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProvider.swift @@ -0,0 +1,389 @@ +// +// TransparentProxyProvider.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NetworkExtension +import NetworkProtection +import os.log // swiftlint:disable:this enforce_os_log_wrapper +import SystemConfiguration + +open class TransparentProxyProvider: NETransparentProxyProvider { + + public enum StartError: Error { + case missingProviderConfiguration + case failedToUpdateNetworkSettings(underlyingError: Error) + } + + public typealias EventCallback = (Event) -> Void + public typealias LoadOptionsCallback = (_ options: [String: Any]?) throws -> Void + + static let dnsPort = 53 + + @TCPFlowActor + var tcpFlowManagers = Set() + + @UDPFlowActor + var udpFlowManagers = Set() + + private let monitor = nw_path_monitor_create() + var directInterface: nw_interface_t? + + private let bMonitor = NWPathMonitor() + var interface: NWInterface? + + public let configuration: Configuration + public let settings: TransparentProxySettings + + @MainActor + public var isRunning = false + + public var eventHandler: EventCallback? + private let logger: Logger + + private lazy var appMessageHandler = TransparentProxyAppMessageHandler(settings: settings) + + // MARK: - Init + + public init(settings: TransparentProxySettings, + configuration: Configuration, + logger: Logger) { + + self.configuration = configuration + self.logger = logger + self.settings = settings + + logger.debug("[+] \(String(describing: Self.self), privacy: .public)") + } + + deinit { + logger.debug("[-] \(String(describing: Self.self), privacy: .public)") + } + + private func loadProviderConfiguration() throws { + guard configuration.loadSettingsFromProviderConfiguration else { + return + } + + guard let providerConfiguration = (protocolConfiguration as? NETunnelProviderProtocol)?.providerConfiguration, + let encodedSettingsString = providerConfiguration[TransparentProxySettingsSnapshot.key] as? String, + let encodedSettings = encodedSettingsString.data(using: .utf8) else { + + throw StartError.missingProviderConfiguration + } + + let snapshot = try JSONDecoder().decode(TransparentProxySettingsSnapshot.self, from: encodedSettings) + settings.apply(snapshot) + } + + @MainActor + public func updateNetworkSettings() async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + Task { @MainActor in + let networkSettings = makeNetworkSettings() + logger.log("Updating network settings: \(String(describing: networkSettings), privacy: .public)") + + setTunnelNetworkSettings(networkSettings) { [eventHandler, logger] error in + if let error { + logger.error("Failed to update network settings: \(String(describing: error), privacy: .public)") + eventHandler?(.failedToUpdateNetworkSettings(error)) + continuation.resume(throwing: error) + return + } + + logger.log("Successfully Updated network settings: \(String(describing: error), privacy: .public))") + continuation.resume() + } + } + } + } + + private func makeNetworkSettings() -> NETransparentProxyNetworkSettings { + let networkSettings = NETransparentProxyNetworkSettings(tunnelRemoteAddress: "127.0.0.1") + + networkSettings.includedNetworkRules = [ + NENetworkRule(remoteNetwork: NWHostEndpoint(hostname: "127.0.0.1", port: ""), remotePrefix: 0, localNetwork: nil, localPrefix: 0, protocol: .any, direction: .outbound) + ] + + return networkSettings + } + + override public func startProxy(options: [String: Any]?, + completionHandler: @escaping (Error?) -> Void) { + + eventHandler?(.startInitiated) + + logger.log( + """ + Starting proxy\n + > configuration: \(String(describing: self.configuration), privacy: .public)\n + > settings: \(String(describing: self.settings), privacy: .public)\n + > options: \(String(describing: options), privacy: .public) + """) + + do { + try loadProviderConfiguration() + } catch { + logger.error("Failed to load provider configuration, bailing out") + eventHandler?(.startFailure(error)) + completionHandler(error) + return + } + + Task { @MainActor in + do { + startMonitoringNetworkInterfaces() + + try await updateNetworkSettings() + logger.log("Proxy started successfully") + isRunning = true + eventHandler?(.startSuccess) + completionHandler(nil) + } catch { + let error = StartError.failedToUpdateNetworkSettings(underlyingError: error) + logger.error("Proxy failed to start \(String(reflecting: error), privacy: .public)") + eventHandler?(.startFailure(error)) + completionHandler(error) + } + } + } + + override public func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + logger.log("Stopping proxy with reason: \(String(reflecting: reason), privacy: .public)") + + Task { @MainActor in + stopMonitoringNetworkInterfaces() + isRunning = false + completionHandler() + } + } + + override public func sleep(completionHandler: @escaping () -> Void) { + Task { @MainActor in + stopMonitoringNetworkInterfaces() + logger.log("The proxy is now sleeping") + completionHandler() + } + } + + override public func wake() { + Task { @MainActor in + logger.log("The proxy is now awake") + startMonitoringNetworkInterfaces() + } + } + + override public func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool { + guard let flow = flow as? NEAppProxyTCPFlow else { + logger.info("Expected a TCP flow, but got something else. We're ignoring it.") + return false + } + + guard let remoteEndpoint = flow.remoteEndpoint as? NWHostEndpoint, + !isDnsServer(remoteEndpoint) else { + return false + } + + let printableRemote = flow.remoteHostname ?? (flow.remoteEndpoint as? NWHostEndpoint)?.hostname ?? "unknown" + + logger.debug( + """ + [TCP] New flow: \(String(describing: flow), privacy: .public) + - remote: \(printableRemote, privacy: .public) + - flowID: \(String(describing: flow.metaData.filterFlowIdentifier?.uuidString), privacy: .public) + - appID: \(String(describing: flow.metaData.sourceAppSigningIdentifier), privacy: .public) + """) + + guard let interface else { + logger.error("[TCP: \(String(describing: flow), privacy: .public)] Expected an interface to exclude traffic through") + return false + } + + switch path(for: flow) { + case .block(let reason): + switch reason { + case .appRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Blocking traffic due to app rule") + case .domainRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Blocking traffic due to domain rule") + } + case .excludeFromVPN(let reason): + switch reason { + case .appRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Excluding traffic due to app rule") + case .domainRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Excluding traffic due to domain rule") + } + case .routeThroughVPN: + return false + } + + flow.networkInterface = directInterface + + Task { @TCPFlowActor in + let flowManager = TCPFlowManager(flow: flow) + tcpFlowManagers.insert(flowManager) + + try? await flowManager.start(interface: interface) + tcpFlowManagers.remove(flowManager) + } + + return true + } + + override public func handleNewUDPFlow(_ flow: NEAppProxyUDPFlow, initialRemoteEndpoint remoteEndpoint: NWEndpoint) -> Bool { + + guard let remoteEndpoint = remoteEndpoint as? NWHostEndpoint, + !isDnsServer(remoteEndpoint) else { + return false + } + + let printableRemote = remoteEndpoint.hostname + + logger.log( + """ + [UDP] New flow: \(String(describing: flow), privacy: .public) + - remote: \(printableRemote, privacy: .public) + - flowID: \(String(describing: flow.metaData.filterFlowIdentifier?.uuidString), privacy: .public) + - appID: \(String(describing: flow.metaData.sourceAppSigningIdentifier), privacy: .public) + """) + + guard let interface else { + logger.error("[UDP: \(String(describing: flow), privacy: .public)] Expected an interface to exclude traffic through") + return false + } + + switch path(for: flow) { + case .block(let reason): + switch reason { + case .appRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Blocking traffic due to app rule") + case .domainRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Blocking traffic due to domain rule") + } + case .excludeFromVPN(let reason): + switch reason { + case .appRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Excluding traffic due to app rule") + case .domainRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Excluding traffic due to domain rule") + } + case .routeThroughVPN: + return false + } + + flow.networkInterface = directInterface + + Task { @UDPFlowActor in + let flowManager = UDPFlowManager(flow: flow) + udpFlowManagers.insert(flowManager) + + try? await flowManager.start(interface: interface) + udpFlowManagers.remove(flowManager) + } + + return true + } + + // MARK: - Path Monitors + + @MainActor + private func startMonitoringNetworkInterfaces() { + bMonitor.pathUpdateHandler = { [weak self, logger] path in + logger.log("Available interfaces updated: \(String(reflecting: path.availableInterfaces), privacy: .public)") + + self?.interface = path.availableInterfaces.first { interface in + interface.type != .other + } + } + bMonitor.start(queue: .main) + + nw_path_monitor_set_queue(monitor, .main) + nw_path_monitor_set_update_handler(monitor) { [weak self, logger] path in + guard let self else { return } + + let interfaces = SCNetworkInterfaceCopyAll() + logger.log("Available interfaces updated: \(String(reflecting: interfaces), privacy: .public)") + + nw_path_enumerate_interfaces(path) { interface in + guard nw_interface_get_type(interface) != nw_interface_type_other else { + return true + } + + self.directInterface = interface + return false + } + } + + nw_path_monitor_start(monitor) + } + + @MainActor + private func stopMonitoringNetworkInterfaces() { + bMonitor.cancel() + nw_path_monitor_cancel(monitor) + } + + // MARK: - Ignoring DNS flows + + private func isDnsServer(_ endpoint: NWHostEndpoint) -> Bool { + Int(endpoint.port) == Self.dnsPort + } + + // MARK: - VPN exclusions logic + + private enum FlowPath { + case block(dueTo: Reason) + case excludeFromVPN(dueTo: Reason) + case routeThroughVPN + + enum Reason { + case appRule + case domainRule + } + } + + private func path(for flow: NEAppProxyFlow) -> FlowPath { + let appIdentifier = flow.metaData.sourceAppSigningIdentifier + + switch settings.appRoutingRules[appIdentifier] { + case .none: + if let hostname = flow.remoteHostname, + isExcludedDomain(hostname) { + return .excludeFromVPN(dueTo: .domainRule) + } + + return .routeThroughVPN + case .block: + return .block(dueTo: .appRule) + case .exclude: + return .excludeFromVPN(dueTo: .domainRule) + } + } + + private func isExcludedDomain(_ hostname: String) -> Bool { + settings.excludedDomains.contains { excludedDomain in + hostname.hasSuffix(excludedDomain) + } + } + + // MARK: - Communication with App + + override public func handleAppMessage(_ messageData: Data) async -> Data? { + await appMessageHandler.handle(messageData) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProviderConfiguration.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProviderConfiguration.swift new file mode 100644 index 0000000000..3e841faeca --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProviderConfiguration.swift @@ -0,0 +1,40 @@ +// +// TransparentProxyProviderConfiguration.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +extension TransparentProxyProvider { + /// Configuration to define behaviour for the provider based on the parent process' + /// business domain. + /// + /// This should not be passed in the startup options dictionary. + /// + public struct Configuration { + /// Whether the proxy settings should be loaded from the provider configuration in the startup options. + /// + /// We recommend setting this to true if the provider is running in a System Extension and can't access + /// shared `TransparentProxySettings`. If the provider is in an App Extension you should instead + /// use a shared `TransparentProxySettings` and set this to false. + /// + let loadSettingsFromProviderConfiguration: Bool + + public init(loadSettingsFromProviderConfiguration: Bool) { + self.loadSettingsFromProviderConfiguration = loadSettingsFromProviderConfiguration + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift index 66f9bb15ea..2575803866 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift @@ -54,9 +54,11 @@ public struct NetworkProtectionStatusView: View { PromptActionView(model: promptActionViewModel) .padding(.horizontal, 5) .padding(.top, 5) + .transition(.slide) } else { if let healthWarning = model.issueDescription { connectionHealthWarningView(message: healthWarning) + .transition(.slide) } } @@ -67,12 +69,14 @@ public struct NetworkProtectionStatusView: View { if model.showDebugInformation { DebugInformationView(model: DebugInformationViewModel()) + .transition(.slide) } bottomMenuView() } .padding(5) .frame(maxWidth: 350, alignment: .top) + .transition(.slide) } // MARK: - Composite Views diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index 2a86e999d0..754ca81034 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -143,9 +143,9 @@ extension NetworkProtectionStatusView { onboardingStatusPublisher .receive(on: DispatchQueue.main) .sink { [weak self] status in - self?.onboardingStatus = status - } - .store(in: &cancellables) + self?.onboardingStatus = status + } + .store(in: &cancellables) } func refreshLoginItemStatus() { @@ -184,14 +184,14 @@ extension NetworkProtectionStatusView { .subscribe(on: Self.tunnelErrorDispatchQueue) .sink { [weak self] errorMessage in - guard let self else { - return - } + guard let self else { + return + } - Task { @MainActor in - self.lastTunnelErrorMessage = errorMessage - } - }.store(in: &cancellables) + Task { @MainActor in + self.lastTunnelErrorMessage = errorMessage + } + }.store(in: &cancellables) } private func subscribeToControllerErrorMessages() { @@ -199,14 +199,14 @@ extension NetworkProtectionStatusView { .subscribe(on: Self.controllerErrorDispatchQueue) .sink { [weak self] errorMessage in - guard let self else { - return - } + guard let self else { + return + } - Task { @MainActor in - self.lastControllerErrorMessage = errorMessage - } - }.store(in: &cancellables) + Task { @MainActor in + self.lastControllerErrorMessage = errorMessage + } + }.store(in: &cancellables) } private func subscribeToDebugInformationChanges() { diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift new file mode 100644 index 0000000000..bd21fe50d1 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift @@ -0,0 +1,120 @@ +// +// TransparentProxyControllerPixelTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import NetworkProtectionProxy +import PixelKit +import PixelKitTestingUtilities +import XCTest + +extension TransparentProxyController.Event: Hashable { + public static func == (lhs: NetworkProtectionProxy.TransparentProxyController.Event, rhs: NetworkProtectionProxy.TransparentProxyController.Event) -> Bool { + + lhs.name == rhs.name && lhs.parameters == rhs.parameters + } + + public func hash(into hasher: inout Hasher) { + name.hash(into: &hasher) + parameters.hash(into: &hasher) + } +} + +extension TransparentProxyController.StartError: Hashable { + public static func == (lhs: NetworkProtectionProxy.TransparentProxyController.StartError, rhs: NetworkProtectionProxy.TransparentProxyController.StartError) -> Bool { + + let lhs = lhs as NSError + let rhs = rhs as NSError + + return lhs.code == rhs.code && lhs.domain == rhs.domain + } + + public func hash(into hasher: inout Hasher) { + (self as NSError).hash(into: &hasher) + (underlyingError as? NSError)?.hash(into: &hasher) + } +} + +final class TransparentProxyControllerPixelTests: XCTestCase { + + static let startFailureFullPixelName = "m_mac_vpn_proxy_controller_start_failure" + static let startInitiatedFullPixelName = "m_mac_vpn_proxy_controller_start_initiated" + static let startSuccessFullPixelName = "m_mac_vpn_proxy_controller_start_success" + + enum TestError: PixelKitEventErrorDetails { + case testError + + static let underlyingError = NSError(domain: "test", code: 1) + + var underlyingError: Error? { + Self.underlyingError + } + } + + // MARK: - Test Firing Pixels + + func testFiringPixelsWithoutParameters() { + let tests: [TransparentProxyController.Event: PixelFireExpectations] = [ + .startInitiated: PixelFireExpectations(pixelName: Self.startInitiatedFullPixelName), + .startSuccess: PixelFireExpectations(pixelName: Self.startSuccessFullPixelName) + ] + + for (event, expectations) in tests { + verifyThat(event, + meets: expectations, + file: #filePath, + line: #line) + } + } + + func testFiringStartFailures() { + // Just a convenience method to return the right expectation for each error + func expectaton(forError error: TransparentProxyController.StartError) -> PixelFireExpectations { + switch error { + case .attemptToStartWithoutBackingActiveFeatures, + .couldNotEncodeSettingsSnapshot, + .couldNotRetrieveProtocolConfiguration: + return PixelFireExpectations( + pixelName: Self.startFailureFullPixelName, + error: error) + case .failedToLoadConfiguration(let underlyingError), + .failedToSaveConfiguration(let underlyingError), + .failedToStartProvider(let underlyingError): + return PixelFireExpectations( + pixelName: Self.startFailureFullPixelName, + error: error, + underlyingError: underlyingError) + } + } + + let errors: [TransparentProxyController.StartError] = [ + .attemptToStartWithoutBackingActiveFeatures, + .couldNotEncodeSettingsSnapshot, + .couldNotRetrieveProtocolConfiguration, + .failedToLoadConfiguration(TestError.underlyingError), + .failedToSaveConfiguration(TestError.underlyingError), + .failedToStartProvider(TestError.underlyingError) + ] + + for error in errors { + verifyThat(TransparentProxyController.Event.startFailure(error), + meets: expectaton(forError: error), + file: #filePath, + line: #line) + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyProviderPixelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyProviderPixelTests.swift new file mode 100644 index 0000000000..faf31729a4 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyProviderPixelTests.swift @@ -0,0 +1,66 @@ +// +// TransparentProxyProviderPixelTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import NetworkProtectionProxy +import PixelKit +import PixelKitTestingUtilities +import XCTest + +extension TransparentProxyProvider.Event: Hashable { + public static func == (lhs: NetworkProtectionProxy.TransparentProxyProvider.Event, rhs: NetworkProtectionProxy.TransparentProxyProvider.Event) -> Bool { + + lhs.name == rhs.name && lhs.parameters == rhs.parameters + } + + public func hash(into hasher: inout Hasher) { + name.hash(into: &hasher) + parameters.hash(into: &hasher) + } +} + +final class TransparentProxyProviderPixelTests: XCTestCase { + + static let startFailureFullPixelName = "m_mac_vpn_proxy_provider_start_failure" + static let startInitiatedFullPixelName = "m_mac_vpn_proxy_provider_start_initiated" + static let startSuccessFullPixelName = "m_mac_vpn_proxy_provider_start_success" + + enum TestError: Error { + case testError + } + + // MARK: - Test Firing Pixels + + func testFiringPixels() { + let tests: [TransparentProxyProvider.Event: PixelFireExpectations] = [ + .startInitiated: PixelFireExpectations(pixelName: Self.startInitiatedFullPixelName), + .startFailure(TestError.testError): + PixelFireExpectations( + pixelName: Self.startFailureFullPixelName, + error: TestError.testError), + .startSuccess: PixelFireExpectations(pixelName: Self.startSuccessFullPixelName) + ] + + for (event, expectations) in tests { + verifyThat(event, + meets: expectations, + file: #filePath, + line: #line) + } + } +} diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift index ffd9d88796..7a25f133ff 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift @@ -94,11 +94,13 @@ public extension Error { let nsError = self as NSError params[PixelKit.Parameters.errorCode] = "\(nsError.code)" - params[PixelKit.Parameters.errorDesc] = nsError.domain + params[PixelKit.Parameters.errorDomain] = nsError.domain + params[PixelKit.Parameters.errorDesc] = nsError.localizedDescription if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError { params[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" - params[PixelKit.Parameters.underlyingErrorDesc] = underlyingError.domain + params[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain + params[PixelKit.Parameters.underlyingErrorDesc] = underlyingError.localizedDescription } if let sqlErrorCode = nsError.userInfo["SQLiteResultCode"] as? NSNumber { diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift index b13aea7b17..9c616d0060 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift @@ -275,11 +275,23 @@ public final class PixelKit { newParams = nil } + let newError: Error? + + if let event = event as? PixelKitEventV2 { + // For v2 events we only consider the error specified in the event + // and purposedly ignore the parameter in this call. + // This is to encourage moving the error over to the protocol error + // instead of still relying on the parameter of this call. + newError = event.error + } else { + newError = error + } + fire(pixelNamed: pixelName, frequency: frequency, withHeaders: headers, withAdditionalParameters: newParams, - withError: error, + withError: newError, allowedQueryReservedCharacters: allowedQueryReservedCharacters, includeAppVersionParameter: includeAppVersionParameter, onComplete: onComplete) @@ -365,8 +377,16 @@ extension Dictionary where Key == String, Value == String { self[PixelKit.Parameters.errorCode] = "\(nsError.code)" self[PixelKit.Parameters.errorDomain] = nsError.domain + self[PixelKit.Parameters.errorDesc] = nsError.localizedDescription + + if let error = error as? PixelKitEventErrorDetails, + let underlyingError = error.underlyingError { - if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError { + let underlyingNSError = underlyingError as NSError + self[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingNSError.code)" + self[PixelKit.Parameters.underlyingErrorDomain] = underlyingNSError.domain + self[PixelKit.Parameters.underlyingErrorDesc] = underlyingNSError.localizedDescription + } else if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError { self[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" self[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain } else if let sqlErrorCode = nsError.userInfo["NSSQLiteErrorDomain"] as? NSNumber { diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift index 83965ba999..bc87070df3 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift @@ -34,7 +34,7 @@ public final class DebugEvent: PixelKitEvent { } public let eventType: EventType - private let error: Error? + public let error: Error? public init(eventType: EventType, error: Error? = nil) { self.eventType = eventType diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift new file mode 100644 index 0000000000..7048519e32 --- /dev/null +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift @@ -0,0 +1,70 @@ +// +// PixelKitEventV2.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public protocol PixelKitEventErrorDetails: Error { + var underlyingError: Error? { get } +} + +extension PixelKitEventErrorDetails { + var underlyingErrorParameters: [String: String] { + guard let nsError = underlyingError as? NSError else { + return [:] + } + + return [ + PixelKit.Parameters.underlyingErrorCode: "\(nsError.code)", + PixelKit.Parameters.underlyingErrorDomain: nsError.domain, + PixelKit.Parameters.underlyingErrorDesc: nsError.localizedDescription + ] + } +} + +/// New version of this protocol that allows us to maintain backwards-compatibility with PixelKitEvent +/// +/// This new implementation seeks to unify the handling of standard pixel parameters inside PixelKit. +/// The starting example of how this can be useful is error parameter handling - this protocol allows +/// the implementer to speciy an error without having to know about the parametrization of the error. +/// +/// The reason this wasn't done directly in `PixelKitEvent` is to reduce the risk of breaking existing +/// pixels, and to allow us to migrate towards this incrementally. +/// +public protocol PixelKitEventV2: PixelKitEvent { + var error: Error? { get } +} + +extension PixelKitEventV2 { + var pixelParameters: [String: String] { + guard let error else { + return [:] + } + + let nsError = error as NSError + var parameters = [ + PixelKit.Parameters.errorCode: "\(nsError.code)", + PixelKit.Parameters.errorDomain: nsError.domain, + ] + + if let error = error as? PixelKitEventErrorDetails { + parameters.merge(error.underlyingErrorParameters, uniquingKeysWith: { $1 }) + } + + return parameters + } +} diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift new file mode 100644 index 0000000000..067eee091e --- /dev/null +++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift @@ -0,0 +1,36 @@ +// +// PixelFireExpectations.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Structure containing information about a pixel fire event. +/// +/// This is useful for test validation for libraries that rely on PixelKit, to make sure the pixels contain +/// all of the fields they are supposed to contain.. +/// +public struct PixelFireExpectations { + let pixelName: String + var error: Error? + var underlyingError: Error? + + public init(pixelName: String, error: Error? = nil, underlyingError: Error? = nil) { + self.pixelName = pixelName + self.error = error + self.underlyingError = underlyingError + } +} diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift new file mode 100644 index 0000000000..5088ba1371 --- /dev/null +++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift @@ -0,0 +1,148 @@ +// +// XCTestCase+PixelKit.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import PixelKit +import XCTest + +public extension XCTestCase { + + // MARK: - Parameters + + /// List of standard pixel parameters. + /// + /// This is useful to support filtering these parameters out if needed. + /// + private static var standardPixelParameters = [ + PixelKit.Parameters.appVersion, + PixelKit.Parameters.pixelSource, + PixelKit.Parameters.test + ] + + /// List of errror pixel parameters + /// + private static var errorPixelParameters = [ + PixelKit.Parameters.errorCode, + PixelKit.Parameters.errorDomain, + PixelKit.Parameters.errorDesc + ] + + /// List of underlying error pixel parameters + /// + private static var underlyingErrorPixelParameters = [ + PixelKit.Parameters.underlyingErrorCode, + PixelKit.Parameters.underlyingErrorDomain, + PixelKit.Parameters.underlyingErrorDesc + ] + + /// Filter out the standard parameters. + /// + private static func filterStandardPixelParameters(from parameters: [String: String]) -> [String: String] { + parameters.filter { element in + !standardPixelParameters.contains(element.key) + } + } + + static var pixelPlatformPrefix: String { +#if os(macOS) + return "m_mac_" +#else + // Intentionally left blank for now because PixelKit currently doesn't support + // other platforms, but if we decide to implement another platform this'll fail + // and indicate that we need a value here. +#endif + } + + func expectedParameters(for event: PixelKitEventV2) -> [String: String] { + var expectedParameters = [String: String]() + + if let error = event.error { + let nsError = error as NSError + expectedParameters[PixelKit.Parameters.errorCode] = "\(nsError.code)" + expectedParameters[PixelKit.Parameters.errorDomain] = nsError.domain + expectedParameters[PixelKit.Parameters.errorDesc] = nsError.localizedDescription + + if let underlyingError = (error as? PixelKitEventErrorDetails)?.underlyingError { + let underlyingNSError = underlyingError as NSError + expectedParameters[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingNSError.code)" + expectedParameters[PixelKit.Parameters.underlyingErrorDomain] = underlyingNSError.domain + expectedParameters[PixelKit.Parameters.underlyingErrorDesc] = underlyingNSError.localizedDescription + } + } + + return expectedParameters + } + + // MARK: - Misc Convenience + + private var userDefaults: UserDefaults { + UserDefaults(suiteName: "testing_\(UUID().uuidString)")! + } + + // MARK: - Pixel Firing Expectations + + /// Provides some snapshot of a fired pixel so that external libraries can validate all the expected info is included. + /// + /// This method also checks that there is internal consistency in the expected fields. + /// + func verifyThat(_ event: PixelKitEventV2, meets expectations: PixelFireExpectations, file: StaticString, line: UInt) { + + let expectedPixelName = Self.pixelPlatformPrefix + event.name + let expectedParameters = expectedParameters(for: event) + let callbackExecutedExpectation = expectation(description: "The PixelKit callback has been executed") + + PixelKit.setUp(dryRun: false, + appVersion: "1.0.5", + source: "test-app", + defaultHeaders: [:], + log: .disabled, + defaults: userDefaults) { firedPixelName, _, firedParameters, _, _, completion in + callbackExecutedExpectation.fulfill() + + let firedParameters = Self.filterStandardPixelParameters(from: firedParameters) + + // Internal validations + + XCTAssertEqual(firedPixelName, expectedPixelName, file: file, line: line) + XCTAssertEqual(firedParameters, expectedParameters, file: file, line: line) + + // Expectations + + XCTAssertEqual(firedPixelName, expectations.pixelName) + + if let error = expectations.error { + let nsError = error as NSError + XCTAssertEqual(firedParameters[PixelKit.Parameters.errorCode], String(nsError.code), file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.errorDomain], nsError.domain, file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.errorDesc], nsError.localizedDescription, file: file, line: line) + } + + if let underlyingError = expectations.underlyingError { + let nsError = underlyingError as NSError + XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorCode], String(nsError.code), file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorDomain], nsError.domain, file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorDesc], nsError.localizedDescription, file: file, line: line) + } + + completion(true, nil) + } + + PixelKit.fire(event) + waitForExpectations(timeout: 0.1) + } +} diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift index 7c4dbf2ed8..fcba4739e3 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift @@ -103,11 +103,13 @@ public final class SubscriptionDebugMenu: NSMenuItem { Task { var results: [String] = [] - for entitlementName in ["fake", "dummy1", "dummy2", "dummy3"] { - let result = await AccountManager().hasEntitlement(for: entitlementName) - let resultSummary = "Entitlement check for \(entitlementName): \(result)" - results.append(resultSummary) - print(resultSummary) + let entitlements: [AccountManager.Entitlement] = [.networkProtection, .dataBrokerProtection, .identityTheftRestoration] + for entitlement in entitlements { + if case let .success(result) = await AccountManager().hasEntitlement(for: entitlement) { + let resultSummary = "Entitlement check for \(entitlement.rawValue): \(result)" + results.append(resultSummary) + print(resultSummary) + } } showAlert(title: "Check Entitlements", message: results.joined(separator: "\n")) diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift index 2f670acfe3..22ab992e79 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift @@ -22,7 +22,7 @@ import Subscription public final class PreferencesSubscriptionModel: ObservableObject { @Published var isUserAuthenticated: Bool = false - @Published var hasEntitlements: Bool = false + @Published var cachedEntitlements: [AccountManager.Entitlement] = [] @Published var subscriptionDetails: String? private var subscriptionPlatform: SubscriptionService.GetSubscriptionDetailsResponse.Platform? @@ -33,6 +33,8 @@ public final class PreferencesSubscriptionModel: ObservableObject { private let openURLHandler: (URL) -> Void private let sheetActionHandler: SubscriptionAccessActionHandlers + private var fetchSubscriptionDetailsTask: Task<(), Never>? + private var signInObserver: Any? private var signOutObserver: Any? @@ -42,7 +44,6 @@ public final class PreferencesSubscriptionModel: ObservableObject { self.sheetActionHandler = sheetActionHandler self.isUserAuthenticated = accountManager.isUserAuthenticated - self.hasEntitlements = self.isUserAuthenticated if let cachedDate = SubscriptionService.cachedSubscriptionDetailsResponse?.expiresOrRenewsAt { updateDescription(for: cachedDate) @@ -170,12 +171,21 @@ public final class PreferencesSubscriptionModel: ObservableObject { @MainActor func fetchAndUpdateSubscriptionDetails() { - Task { - guard let token = accountManager.accessToken else { return } + guard fetchSubscriptionDetailsTask == nil else { return } + + fetchSubscriptionDetailsTask = Task { [weak self] in + defer { + self?.fetchSubscriptionDetailsTask = nil + } + + guard let token = self?.accountManager.accessToken else { return } if let cachedDate = SubscriptionService.cachedSubscriptionDetailsResponse?.expiresOrRenewsAt { - updateDescription(for: cachedDate) - self.hasEntitlements = cachedDate.timeIntervalSinceNow > 0 + self?.updateDescription(for: cachedDate) + + if cachedDate.timeIntervalSinceNow < 0 { + self?.cachedEntitlements = [] + } } if case .success(let response) = await SubscriptionService.getSubscriptionDetails(token: token) { @@ -184,12 +194,14 @@ public final class PreferencesSubscriptionModel: ObservableObject { return } - updateDescription(for: response.expiresOrRenewsAt) + self?.updateDescription(for: response.expiresOrRenewsAt) - subscriptionPlatform = response.platform + self?.subscriptionPlatform = response.platform } - self.hasEntitlements = await AccountManager().hasEntitlement(for: "dummy1") + if case let .success(entitlements) = await AccountManager().fetchEntitlements() { + self?.cachedEntitlements = entitlements + } } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift index d20be23069..5ef3942106 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift @@ -139,7 +139,7 @@ public struct PreferencesSubscriptionView: View { description: UserText.vpnServiceDescription, buttonName: model.isUserAuthenticated ? "Manage" : nil, buttonAction: { model.openVPN() }, - enabled: !model.isUserAuthenticated || model.hasEntitlements) + enabled: !model.isUserAuthenticated || model.cachedEntitlements.contains(.networkProtection)) Divider() .foregroundColor(Color.secondary) @@ -149,7 +149,7 @@ public struct PreferencesSubscriptionView: View { description: UserText.personalInformationRemovalServiceDescription, buttonName: model.isUserAuthenticated ? "View" : nil, buttonAction: { model.openPersonalInformationRemoval() }, - enabled: !model.isUserAuthenticated || model.hasEntitlements) + enabled: !model.isUserAuthenticated || model.cachedEntitlements.contains(.dataBrokerProtection)) Divider() .foregroundColor(Color.secondary) @@ -159,7 +159,7 @@ public struct PreferencesSubscriptionView: View { description: UserText.identityTheftRestorationServiceDescription, buttonName: model.isUserAuthenticated ? "View" : nil, buttonAction: { model.openIdentityTheftRestoration() }, - enabled: !model.isUserAuthenticated || model.hasEntitlements) + enabled: !model.isUserAuthenticated || model.cachedEntitlements.contains(.identityTheftRestoration)) } .padding(10) .roundedBorder() diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/ButtonStyles.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/ButtonStyles.swift index f0dd3f4171..2c2aad1b82 100644 --- a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/ButtonStyles.swift +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/ButtonStyles.swift @@ -55,8 +55,9 @@ public struct DefaultActionButtonStyle: ButtonStyle { let labelColor = enabled ? Color.white : Color.primary.opacity(0.3) configuration.label - .lineLimit(1) .font(.system(size: 13)) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) .frame(minWidth: 44) // OK buttons will match the width of "Cancel" at least in English .padding(.top, 2.5) .padding(.bottom, 3) diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/LetterIconView.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/LetterIconView.swift index 84cdc2a6ed..f0e70db952 100644 --- a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/LetterIconView.swift +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/LetterIconView.swift @@ -24,7 +24,8 @@ public struct LetterIconView: View { public var size: CGFloat public var prefferedFirstCharacters: String? public var characterCount: Int - private var padding: CGFloat = 0.33 + private var paddingModifier: CGFloat + private var font: Font private static let wwwPreffix = "www." private var characters: String { @@ -35,11 +36,20 @@ public struct LetterIconView: View { return String(title.replacingOccurrences(of: Self.wwwPreffix, with: "").prefix(characterCount)) } - public init(title: String, size: CGFloat = 32, prefferedFirstCharacters: String? = nil, characterCount: Int = 2) { + /// Initializes a `LetterIconView` + /// Note: The `paddingModifier`parameter is used to calculate the inner frame width/height using `size - (size * paddingModifier)` + public init(title: String, + size: CGFloat = 32, + prefferedFirstCharacters: String? = nil, + characterCount: Int = 2, + font: Font = .title, + paddingModifier: CGFloat = 0.33) { self.title = title self.size = size self.prefferedFirstCharacters = prefferedFirstCharacters self.characterCount = characterCount + self.font = font + self.paddingModifier = paddingModifier } public var body: some View { @@ -47,13 +57,11 @@ public struct LetterIconView: View { RoundedRectangle(cornerRadius: size * 0.125) .foregroundColor(Color.forString(title)) .frame(width: size, height: size) - Text(characters.capitalized(with: .current)) - .frame(width: size - (size * padding), height: size - (size * padding)) + .frame(width: size - (size * paddingModifier), height: size - (size * paddingModifier)) .foregroundColor(.white) .minimumScaleFactor(0.01) - .font(.system(size: size, weight: .semibold)) + .font(font) } - .padding(.leading, 8) } } diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 8235911de7..da7db34f24 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -23,6 +23,9 @@ let package = Package( .product(name: "PreferencesViews", package: "SwiftUIExtensions"), .product(name: "SwiftUIExtensions", package: "SwiftUIExtensions") ], + resources: [ + .process("Resources") + ], swiftSettings: [ .define("DEBUG", .when(configuration: .debug)) ], diff --git a/LocalPackages/SyncUI/Sources/SyncUI/internal/Localizable.xcstrings b/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings similarity index 99% rename from LocalPackages/SyncUI/Sources/SyncUI/internal/Localizable.xcstrings rename to LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings index abcf797258..340db4147a 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/internal/Localizable.xcstrings +++ b/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings @@ -209,7 +209,7 @@ } }, "paste-from-clipboard" : { - "comment" : "Paste button", + "comment" : "Paste from Clipboard button", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -245,7 +245,7 @@ } }, "preferences.begin-sync.card-footer" : { - "comment" : "Footer / captoin on the Begin Syncing card in sync settings", + "comment" : "Footer / caption on the Begin Syncing card in sync settings", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -353,7 +353,7 @@ } }, "preferences.preparing-to-sync.dialog-title" : { - "comment" : "Peparing to sync dialog title during sync set up", + "comment" : "Preparing to sync dialog title during sync set up", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -389,7 +389,7 @@ } }, "preferences.recover-synced-data.dialog-subtitle" : { - "comment" : "Recover synced data during Sync revoery process dialog subtitle", + "comment" : "Recover synced data during Sync recovery process dialog subtitle", "extractionState" : "extracted_with_value", "localizations" : { "en" : { diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/SyncWithAnotherDeviceView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/SyncWithAnotherDeviceView.swift index 7c24341d57..61ce3d1bb8 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/SyncWithAnotherDeviceView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/Dialogs/SyncWithAnotherDeviceView.swift @@ -56,7 +56,8 @@ struct SyncWithAnotherDeviceView: View { Spacer(minLength: 0) } .padding(.top, 16) - .frame(width: 380, height: 332) + .frame(height: 332) + .frame(minWidth: 380) .roundedBorder() } @@ -83,7 +84,8 @@ struct SyncWithAnotherDeviceView: View { pickerOptionView(imageName: "QR-Icon", title: UserText.syncWithAnotherDeviceShowCodeButton, tag: 0) pickerOptionView(imageName: "Keyboard-16D", title: UserText.syncWithAnotherDeviceEnterCodeButton, tag: 1) } - .frame(width: 348, height: 32) + .frame(height: 32) + .frame(minWidth: 348) .roundedBorder() } @@ -96,7 +98,9 @@ struct SyncWithAnotherDeviceView: View { Image(imageName) Text(title) } - .frame(width: 172, height: 28) + .frame(height: 28) + .frame(minWidth: 172) + .padding(.horizontal, 8) .background( ZStack { RoundedRectangle(cornerRadius: 8) diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncSetupView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncSetupView.swift index e1d77dcb9f..6743f311ce 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncSetupView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncSetupView.swift @@ -28,7 +28,6 @@ struct SyncSetupView: View where ViewModel: ManagementViewModel { syncUnavailableView() syncWithAnotherDeviceView() SyncUIViews.TextDetailSecondary(text: UserText.beginSyncFooter) - .frame(height: 28) .padding(.bottom, 24) .padding(.horizontal, 110) .font(.system(size: 11)) @@ -109,10 +108,10 @@ private struct SyncWithAnotherDeviceButtonStyle: ButtonStyle { configuration.label .lineLimit(1) .font(.body.bold()) - .frame(width: 220, height: 32) + .frame(height: 32) + .padding(.horizontal, 24) .background(enabled ? enabledBackgroundColor : disabledBackgroundColor) .foregroundColor(labelColor) .cornerRadius(8) - } } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/internal/SyncUIViews.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/internal/SyncUIViews.swift index 10e33ed47f..b36dd20085 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/internal/SyncUIViews.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/internal/SyncUIViews.swift @@ -27,6 +27,8 @@ enum SyncUIViews { Text(text) .bold() .font(.system(size: 17)) + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.center) } } @@ -47,7 +49,7 @@ enum SyncUIViews { var body: some View { Text(text) - .fixedSize(horizontal: false, vertical: /*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) + .fixedSize(horizontal: false, vertical: true) .multilineTextAlignment(.center) } } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift b/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift index ca0babbe0f..3d9356af3e 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift @@ -21,150 +21,151 @@ import Foundation enum UserText { // Generic Buttons - static let ok = NSLocalizedString("ok", value: "OK", comment: "OK button") - static let notNow = NSLocalizedString("notnow", value: "Not Now", comment: "Not Now button") - static let cancel = NSLocalizedString("cancel", value: "Cancel", comment: "Cancel button") - static let submit = NSLocalizedString("submit", value: "Submit", comment: "Submit button") - static let next = NSLocalizedString("next", value: "Next", comment: "Next button") - static let copy = NSLocalizedString("copy", value: "Copy", comment: "Copy button") - static let share = NSLocalizedString("share", value: "Share", comment: "Share button") - static let paste = NSLocalizedString("paste", value: "Paste", comment: "Paste button") - static let pasteFromClipboard = NSLocalizedString("paste-from-clipboard", value: "Paste from Clipboard", comment: "Paste button") - static let done = NSLocalizedString("done", value: "Done", comment: "Done button") + static let ok = NSLocalizedString("ok", bundle: Bundle.module, value: "OK", comment: "OK button") + static let notNow = NSLocalizedString("notnow", bundle: Bundle.module, value: "Not Now", comment: "Not Now button") + static let cancel = NSLocalizedString("cancel", bundle: Bundle.module, value: "Cancel", comment: "Cancel button") + static let submit = NSLocalizedString("submit", bundle: Bundle.module, value: "Submit", comment: "Submit button") + static let next = NSLocalizedString("next", bundle: Bundle.module, value: "Next", comment: "Next button") + static let copy = NSLocalizedString("copy", bundle: Bundle.module, value: "Copy", comment: "Copy button") + static let share = NSLocalizedString("share", bundle: Bundle.module, value: "Share", comment: "Share button") + static let paste = NSLocalizedString("paste", bundle: Bundle.module, value: "Paste", comment: "Paste button") + static let pasteFromClipboard = NSLocalizedString("paste-from-clipboard", bundle: Bundle.module, value: "Paste from Clipboard", comment: "Paste from Clipboard button") + static let done = NSLocalizedString("done", bundle: Bundle.module, value: "Done", comment: "Done button") // Sync Set Up View // Begin Sync card - static let beginSyncTitle = NSLocalizedString("preferences.begin-sync.card-title", value: "Begin Syncing", comment: "Begin Syncing card title in sync settings") - static let beginSyncDescription = NSLocalizedString("preferences.begin-sync.card-description", value: "Securely sync bookmarks and passwords between your devices.", comment: "Begin Syncing card description in sync settings") - static let beginSyncButton = NSLocalizedString("preferences.begin-sync.card-button", value: "Sync With Another Device", comment: "Button text on the Begin Syncing card in sync settings") - static let beginSyncFooter = NSLocalizedString("preferences.begin-sync.card-footer", value: "Your data is end-to-end encrypted, and DuckDuckGo does not have access to the encryption key.", comment: "Footer / captoin on the Begin Syncing card in sync settings") + static let beginSyncTitle = NSLocalizedString("preferences.begin-sync.card-title", bundle: Bundle.module, value: "Begin Syncing", comment: "Begin Syncing card title in sync settings") + static let beginSyncDescription = NSLocalizedString("preferences.begin-sync.card-description", bundle: Bundle.module, value: "Securely sync bookmarks and passwords between your devices.", comment: "Begin Syncing card description in sync settings") + static let beginSyncButton = NSLocalizedString("preferences.begin-sync.card-button", bundle: Bundle.module, value: "Sync With Another Device", comment: "Button text on the Begin Syncing card in sync settings") + static let beginSyncFooter = NSLocalizedString("preferences.begin-sync.card-footer", bundle: Bundle.module, value: "Your data is end-to-end encrypted, and DuckDuckGo does not have access to the encryption key.", comment: "Footer / caption on the Begin Syncing card in sync settings") + // Options - static let otherOptionsSectionTitle = NSLocalizedString("preferences.other-options.section-title", value: "Other Options", comment: "Sync settings. Other Options section title") - static let syncThisDeviceLink = NSLocalizedString("preferences.sync-this-device.link-title", value: "Sync and Back Up This Device", comment: "Sync settings. Title of a link to start setting up sync and backup the device") - static let recoverDataLink = NSLocalizedString("preferences.recover-data.link-title", value: "Recover Synced Data", comment: "Sync settings. Link to recover synced data.") + static let otherOptionsSectionTitle = NSLocalizedString("preferences.other-options.section-title", bundle: Bundle.module, value: "Other Options", comment: "Sync settings. Other Options section title") + static let syncThisDeviceLink = NSLocalizedString("preferences.sync-this-device.link-title", bundle: Bundle.module, value: "Sync and Back Up This Device", comment: "Sync settings. Title of a link to start setting up sync and backup the device") + static let recoverDataLink = NSLocalizedString("preferences.recover-data.link-title", bundle: Bundle.module, value: "Recover Synced Data", comment: "Sync settings. Link to recover synced data.") // Preparing to sync dialog - static let preparingToSyncDialogTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-title", value: "Preparing To Sync", comment: "Peparing to sync dialog title during sync set up") - static let preparingToSyncDialogSubTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-subtitle", value: "We're setting up the connection to synchronize your bookmarks and saved logins with the other device.", comment: "Preparing to sync dialog subtitle during sync set up") - static let preparingToSyncDialogAction = NSLocalizedString("preferences.preparing-to-sync.dialog-action", value: "Connecting…", comment: "Sync preparing to sync dialog action") + static let preparingToSyncDialogTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-title", bundle: Bundle.module, value: "Preparing To Sync", comment: "Preparing to sync dialog title during sync set up") + static let preparingToSyncDialogSubTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-subtitle", bundle: Bundle.module, value: "We're setting up the connection to synchronize your bookmarks and saved logins with the other device.", comment: "Preparing to sync dialog subtitle during sync set up") + static let preparingToSyncDialogAction = NSLocalizedString("preferences.preparing-to-sync.dialog-action", bundle: Bundle.module, value: "Connecting…", comment: "Sync preparing to sync dialog action") // Enter recovery code dialog - static let enterRecoveryCodeDialogTitle = NSLocalizedString("preferences.enter-recovery-code.dialog-title", value: "Enter Code", comment: "Sync enter recovery code dialog title") - static let enterRecoveryCodeDialogSubtitle = NSLocalizedString("preferences.enter-recovery-code.dialog-subtitle", value: "Enter the code on your Recovery PDF, or another synced device, to recover your synced data.", comment: "Sync enter recovery code dialog subtitle") - static let enterRecoveryCodeDialogAction1 = NSLocalizedString("preferences.enter-recovery-code.dialog-action1", value: "Paste Code Here", comment: "Sync enter recovery code dialog first possible action") - static let enterRecoveryCodeDialogAction2 = NSLocalizedString("preferences.enter-recovery-code.dialog-action2", value: "or scan QR code with a device that is still connected", comment: "Sync enter recovery code dialog second possible action") + static let enterRecoveryCodeDialogTitle = NSLocalizedString("preferences.enter-recovery-code.dialog-title", bundle: Bundle.module, value: "Enter Code", comment: "Sync enter recovery code dialog title") + static let enterRecoveryCodeDialogSubtitle = NSLocalizedString("preferences.enter-recovery-code.dialog-subtitle", bundle: Bundle.module, value: "Enter the code on your Recovery PDF, or another synced device, to recover your synced data.", comment: "Sync enter recovery code dialog subtitle") + static let enterRecoveryCodeDialogAction1 = NSLocalizedString("preferences.enter-recovery-code.dialog-action1", bundle: Bundle.module, value: "Paste Code Here", comment: "Sync enter recovery code dialog first possible action") + static let enterRecoveryCodeDialogAction2 = NSLocalizedString("preferences.enter-recovery-code.dialog-action2", bundle: Bundle.module, value: "or scan QR code with a device that is still connected", comment: "Sync enter recovery code dialog second possible action") // Recover synced data dialog - static let reciverSyncedDataDialogTitle = NSLocalizedString("preferences.recover-synced-data.dialog-title", value: "Recover Synced Data", comment: "Sync recover synced data dialog title") - static let reciverSyncedDataDialogSubitle = NSLocalizedString("preferences.recover-synced-data.dialog-subtitle", value: "To restore your synced data, you'll need the Recovery Code you saved when you first set up Sync. This code may have been saved as a PDF on the device you originally used to set up Sync.", comment: "Recover synced data during Sync revoery process dialog subtitle") - static let reciverSyncedDataDialogButton = NSLocalizedString("preferences.recover-synced-data.dialog-button", value: "Get Started", comment: "Sync recover synced data dialog button") + static let reciverSyncedDataDialogTitle = NSLocalizedString("preferences.recover-synced-data.dialog-title", bundle: Bundle.module, value: "Recover Synced Data", comment: "Sync recover synced data dialog title") + static let reciverSyncedDataDialogSubitle = NSLocalizedString("preferences.recover-synced-data.dialog-subtitle", bundle: Bundle.module, value: "To restore your synced data, you'll need the Recovery Code you saved when you first set up Sync. This code may have been saved as a PDF on the device you originally used to set up Sync.", comment: "Recover synced data during Sync recovery process dialog subtitle") + static let reciverSyncedDataDialogButton = NSLocalizedString("preferences.recover-synced-data.dialog-button", bundle: Bundle.module, value: "Get Started", comment: "Sync recover synced data dialog button") // Sync Title - static let sync = NSLocalizedString("preferences.sync", value: "Sync & Backup", comment: "Show sync preferences") - static let syncRollOutBannerDescription = NSLocalizedString("preferences.sync.rollout-banner.description", value: "Sync & Backup is rolling out gradually and may not be available yet within DuckDuckGo on your other devices.", comment: "Description of rollout banner") + static let sync = NSLocalizedString("preferences.sync", bundle: Bundle.module, value: "Sync & Backup", comment: "Show sync preferences") + static let syncRollOutBannerDescription = NSLocalizedString("preferences.sync.rollout-banner.description", bundle: Bundle.module, value: "Sync & Backup is rolling out gradually and may not be available yet within DuckDuckGo on your other devices.", comment: "Description of rollout banner") - static let turnOff = NSLocalizedString("preferences.sync.turn-off", value: "Turn Off", comment: "Turn off sync confirmation dialog button title") - static let turnOffSync = NSLocalizedString("preferences.sync.turn-off.ellipsis", value: "Turn Off Sync…", comment: "Disable sync button caption") + static let turnOff = NSLocalizedString("preferences.sync.turn-off", bundle: Bundle.module, value: "Turn Off", comment: "Turn off sync confirmation dialog button title") + static let turnOffSync = NSLocalizedString("preferences.sync.turn-off.ellipsis", bundle: Bundle.module, value: "Turn Off Sync…", comment: "Disable sync button caption") // Sync Enabled View // Turn off sync dialog - static let turnOffSyncConfirmTitle = NSLocalizedString("preferences.sync.turn-off.confirm.title", value: "Turn off sync?", comment: "Turn off sync confirmation dialog title") - static let turnOffSyncConfirmMessage = NSLocalizedString("preferences.sync.turn-off.confirm.message", value: "This device will no longer be able to access your synced data.", comment: "Turn off sync confirmation dialog message") + static let turnOffSyncConfirmTitle = NSLocalizedString("preferences.sync.turn-off.confirm.title", bundle: Bundle.module, value: "Turn off sync?", comment: "Turn off sync confirmation dialog title") + static let turnOffSyncConfirmMessage = NSLocalizedString("preferences.sync.turn-off.confirm.message", bundle: Bundle.module, value: "This device will no longer be able to access your synced data.", comment: "Turn off sync confirmation dialog message") // Delete server data - static let turnOffAndDeleteServerData = NSLocalizedString("preferences.sync.turn-off-and-delete-data", value: "Turn Off and Delete Server Data…", comment: "Disable and delete data sync button caption") + static let turnOffAndDeleteServerData = NSLocalizedString("preferences.sync.turn-off-and-delete-data", bundle: Bundle.module, value: "Turn Off and Delete Server Data…", comment: "Disable and delete data sync button caption") // sync connected - static let syncConnected = NSLocalizedString("preferences.sync.connected", value: "Sync Enabled", comment: "Sync state is enabled") + static let syncConnected = NSLocalizedString("preferences.sync.connected", bundle: Bundle.module, value: "Sync Enabled", comment: "Sync state is enabled") // synced devices - static let syncedDevices = NSLocalizedString("preferences.sync.synced-devices", value: "Synced Devices", comment: "Settings section title") - static let thisDevice = NSLocalizedString("preferences.sync.this-device", value: "This Device", comment: "Indicator of a current user's device on the list") - static let currentDeviceDetails = NSLocalizedString("preferences.sync.current-device-details", value: "Details...", comment: "Sync Settings device details button") - static let removeDeviceButton = NSLocalizedString("preferences.sync.remove-device", value: "Remove...", comment: "Button to remove a device") + static let syncedDevices = NSLocalizedString("preferences.sync.synced-devices", bundle: Bundle.module, value: "Synced Devices", comment: "Settings section title") + static let thisDevice = NSLocalizedString("preferences.sync.this-device", bundle: Bundle.module, value: "This Device", comment: "Indicator of a current user's device on the list") + static let currentDeviceDetails = NSLocalizedString("preferences.sync.current-device-details", bundle: Bundle.module, value: "Details...", comment: "Sync Settings device details button") + static let removeDeviceButton = NSLocalizedString("preferences.sync.remove-device", bundle: Bundle.module, value: "Remove...", comment: "Button to remove a device") // Remove device dialog - static let removeDeviceConfirmTitle = NSLocalizedString("preferences.sync.remove-device-title", value: "Remove device?", comment: "Title on remove a device confirmation") - static let removeDeviceConfirmButton = NSLocalizedString("preferences.sync.remove-device-button", value: "Remove Device", comment: "Button text on remove a device confirmation button") + static let removeDeviceConfirmTitle = NSLocalizedString("preferences.sync.remove-device-title", bundle: Bundle.module, value: "Remove device?", comment: "Title on remove a device confirmation") + static let removeDeviceConfirmButton = NSLocalizedString("preferences.sync.remove-device-button", bundle: Bundle.module, value: "Remove Device", comment: "Button text on remove a device confirmation button") static func removeDeviceConfirmMessage(_ deviceName: String) -> String { let localized = NSLocalizedString("preferences.sync.remove-device-message", - value: "\"%@\" will no longer be able to access your synced data.", + bundle: Bundle.module, value: "\"%@\" will no longer be able to access your synced data.", comment: "Message to confirm the device will no longer be able to access the synced data - devoce name item inserted") return String(format: localized, deviceName) } - static let recovery = NSLocalizedString("prefrences.sync.recovery", value: "Recovery", comment: "Sync settings section title") - static let recoveryInstructions = NSLocalizedString("prefrences.sync.recovery-instructions", value: "If you lose your device, you will need this recovery code to restore your synced data.", comment: "Instructions on how to restore synced data") + static let recovery = NSLocalizedString("prefrences.sync.recovery", bundle: Bundle.module, value: "Recovery", comment: "Sync settings section title") + static let recoveryInstructions = NSLocalizedString("prefrences.sync.recovery-instructions", bundle: Bundle.module, value: "If you lose your device, you will need this recovery code to restore your synced data.", comment: "Instructions on how to restore synced data") // Sync with another device dialog - static let syncWithAnotherDeviceTitle = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-title", value: "Sync With Another Device", comment: "Sync with another device dialog title") + static let syncWithAnotherDeviceTitle = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-title", bundle: Bundle.module, value: "Sync With Another Device", comment: "Sync with another device dialog title") static func syncWithAnotherDeviceSubtitle(syncMenuPath: String) -> String { - let localized = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-subtitle1", value: "Go to %@ in the DuckDuckGo Browser on another device and select Sync With Another Device.", comment: "Sync with another device dialog subtitle - Instruction with sync menu path item inserted") + let localized = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-subtitle1", bundle: Bundle.module, value: "Go to %@ in the DuckDuckGo Browser on another device and select Sync With Another Device.", comment: "Sync with another device dialog subtitle - Instruction with sync menu path item inserted") return String(format: localized, syncMenuPath) } - static let syncMenuPath = NSLocalizedString("sync.menu.path", value: "Settings › Sync & Backup", comment: "Sync Menu Path") - static let syncWithAnotherDeviceShowCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-button", value: "Show Code", comment: "Text on show code button on Sync with another device dialog") - static let syncWithAnotherDeviceEnterCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-button", value: "Enter Code", comment: "Text on enter code button on Sync with another device dialog") - static let syncWithAnotherDeviceShowQRCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.show-qr-code-explanation", value: "Scan this QR code to connect.", comment: "Sync with another device dialog show qr code explanation") - static let syncWithAnotherDeviceEnterCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-explanation", value: "Paste the code here to sync.", comment: "Sync with another device dialog enter code explanation") - static let syncWithAnotherDeviceShowCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-explanation", value: "Share this code to connect with a desktop machine.", comment: "Sync with another device dialog show code explanation") - static let syncWithAnotherDeviceViewQRCode = NSLocalizedString("preferences.sync.sync-with-another-device.view-qr-code-link", value: "View QR Code", comment: "Sync with another device dialog view qr code link") - static let syncWithAnotherDeviceViewTextCode = NSLocalizedString("preferences.sync.sync-with-another-device.view-text-code-link", value: "View Text Code", comment: "Sync with another device dialog view text code link") + static let syncMenuPath = NSLocalizedString("sync.menu.path", bundle: Bundle.module, value: "Settings › Sync & Backup", comment: "Sync Menu Path") + static let syncWithAnotherDeviceShowCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-button", bundle: Bundle.module, value: "Show Code", comment: "Text on show code button on Sync with another device dialog") + static let syncWithAnotherDeviceEnterCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-button", bundle: Bundle.module, value: "Enter Code", comment: "Text on enter code button on Sync with another device dialog") + static let syncWithAnotherDeviceShowQRCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.show-qr-code-explanation", bundle: Bundle.module, value: "Scan this QR code to connect.", comment: "Sync with another device dialog show qr code explanation") + static let syncWithAnotherDeviceEnterCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-explanation", bundle: Bundle.module, value: "Paste the code here to sync.", comment: "Sync with another device dialog enter code explanation") + static let syncWithAnotherDeviceShowCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-explanation", bundle: Bundle.module, value: "Share this code to connect with a desktop machine.", comment: "Sync with another device dialog show code explanation") + static let syncWithAnotherDeviceViewQRCode = NSLocalizedString("preferences.sync.sync-with-another-device.view-qr-code-link", bundle: Bundle.module, value: "View QR Code", comment: "Sync with another device dialog view qr code link") + static let syncWithAnotherDeviceViewTextCode = NSLocalizedString("preferences.sync.sync-with-another-device.view-text-code-link", bundle: Bundle.module, value: "View Text Code", comment: "Sync with another device dialog view text code link") // Save recovery PDF dialog - static let saveRecoveryPDF = NSLocalizedString("prefrences.sync.save-recovery-pdf", value: "Save Your Recovery Code", comment: "Caption for a button to save Sync recovery PDF") - static let recoveryPDFExplanation = NSLocalizedString("prefrences.sync.recovery-pdf-explanation", value: "If you lose access to your devices, you will need this code to recover your synced data. You can save this code to your device as a PDF.", comment: "Sync recovery PDF explanation") - static let recoveryPDFCopyCodeButton = NSLocalizedString("prefrences.sync.recovery-pdf-copy-code-button", value: "Copy Code", comment: "Sync recovery PDF copy code button") - static let recoveryPDFSavePDFButton = NSLocalizedString("prefrences.sync.recovery-pdf-save-pdf-button", value: "Save PDF", comment: "Sync recovery PDF save pdf button") - static let recoveryPDFWarning = NSLocalizedString("prefrences.sync.recovery-pdf-warning", value: "Anyone with access to this code can access your synced data, so please keep it in a safe place.", comment: "Sync recovery PDF warning") + static let saveRecoveryPDF = NSLocalizedString("prefrences.sync.save-recovery-pdf", bundle: Bundle.module, value: "Save Your Recovery Code", comment: "Caption for a button to save Sync recovery PDF") + static let recoveryPDFExplanation = NSLocalizedString("prefrences.sync.recovery-pdf-explanation", bundle: Bundle.module, value: "If you lose access to your devices, you will need this code to recover your synced data. You can save this code to your device as a PDF.", comment: "Sync recovery PDF explanation") + static let recoveryPDFCopyCodeButton = NSLocalizedString("prefrences.sync.recovery-pdf-copy-code-button", bundle: Bundle.module, value: "Copy Code", comment: "Sync recovery PDF copy code button") + static let recoveryPDFSavePDFButton = NSLocalizedString("prefrences.sync.recovery-pdf-save-pdf-button", bundle: Bundle.module, value: "Save PDF", comment: "Sync recovery PDF save pdf button") + static let recoveryPDFWarning = NSLocalizedString("prefrences.sync.recovery-pdf-warning", bundle: Bundle.module, value: "Anyone with access to this code can access your synced data, so please keep it in a safe place.", comment: "Sync recovery PDF warning") // Sync with server dialog - static let syncWithServerTitle = NSLocalizedString("preferences.sync.sync-with-server-title", value: "Sync and Back Up This Device", comment: "Sync with server dialog title") - static let syncWithServerSubtitle1 = NSLocalizedString("preferences.sync.sync-with-server-subtitle1", value: "This creates an encrypted backup of your bookmarks and passwords on DuckDuckGo’s secure server, which can be synced with your other devices.", comment: "Sync with server dialog first subtitle") - static let syncWithServerSubtitle2 = NSLocalizedString("preferences.sync.sync-with-server-subtitle2", value: "The encryption key is only stored on your device, DuckDuckGo cannot access it.", comment: "Sync with server dialog second subtitle") - static let syncWithServerButton = NSLocalizedString("preferences.sync.sync-with-server-button", value: "Turn On Sync & Backup", comment: "Sync with server dialog button") + static let syncWithServerTitle = NSLocalizedString("preferences.sync.sync-with-server-title", bundle: Bundle.module, value: "Sync and Back Up This Device", comment: "Sync with server dialog title") + static let syncWithServerSubtitle1 = NSLocalizedString("preferences.sync.sync-with-server-subtitle1", bundle: Bundle.module, value: "This creates an encrypted backup of your bookmarks and passwords on DuckDuckGo’s secure server, which can be synced with your other devices.", comment: "Sync with server dialog first subtitle") + static let syncWithServerSubtitle2 = NSLocalizedString("preferences.sync.sync-with-server-subtitle2", bundle: Bundle.module, value: "The encryption key is only stored on your device, DuckDuckGo cannot access it.", comment: "Sync with server dialog second subtitle") + static let syncWithServerButton = NSLocalizedString("preferences.sync.sync-with-server-button", bundle: Bundle.module, value: "Turn On Sync & Backup", comment: "Sync with server dialog button") // Device synced dialog - static let deviceSynced = NSLocalizedString("prefrences.sync.device-synced", value: "Your data is synced!", comment: "Sync setup confirmation dialog title") + static let deviceSynced = NSLocalizedString("prefrences.sync.device-synced", bundle: Bundle.module, value: "Your data is synced!", comment: "Sync setup confirmation dialog title") // Device details - static let deviceDetailsTitle = NSLocalizedString("prefrences.sync.device-details.title", value: "Device Details", comment: "The title of the device details dialog") - static let deviceDetailsLabel = NSLocalizedString("prefrences.sync.device-details.label", value: "Name", comment: "The text entry label to name the device") - static let deviceDetailsPrompt = NSLocalizedString("prefrences.sync.device-details.prompt", value: "Device name", comment: "The text entry prompt to name the device") + static let deviceDetailsTitle = NSLocalizedString("prefrences.sync.device-details.title", bundle: Bundle.module, value: "Device Details", comment: "The title of the device details dialog") + static let deviceDetailsLabel = NSLocalizedString("prefrences.sync.device-details.label", bundle: Bundle.module, value: "Name", comment: "The text entry label to name the device") + static let deviceDetailsPrompt = NSLocalizedString("prefrences.sync.device-details.prompt", bundle: Bundle.module, value: "Device name", comment: "The text entry prompt to name the device") // Delete Account Dialog - static let deleteAccountTitle = NSLocalizedString("prefrences.sync.delete-account.title", value: "Delete server data?", comment: "Title for delete account confirmation pop up") - static let deleteAccountMessage = NSLocalizedString("prefrences.sync.delete-account.message", value: "These devices will be disconnected and your synced data will be deleted from the server.", comment: "Message for delete account confirmation pop up") - static let deleteAccountButton = NSLocalizedString("prefrences.sync.delete-account.button", value: "Delete Data", comment: "Label for delete account button") + static let deleteAccountTitle = NSLocalizedString("prefrences.sync.delete-account.title", bundle: Bundle.module, value: "Delete server data?", comment: "Title for delete account confirmation pop up") + static let deleteAccountMessage = NSLocalizedString("prefrences.sync.delete-account.message", bundle: Bundle.module, value: "These devices will be disconnected and your synced data will be deleted from the server.", comment: "Message for delete account confirmation pop up") + static let deleteAccountButton = NSLocalizedString("prefrences.sync.delete-account.button", bundle: Bundle.module, value: "Delete Data", comment: "Label for delete account button") // Sync enabled options - static let optionsSectionTitle = NSLocalizedString("prefrences.sync.options-section-title", value: "Options", comment: "Title for options settings") - static let shareFavoritesOptionTitle = NSLocalizedString("prefrences.sync.share-favorite-option-title", value: "Unify Favorites Across Devices", comment: "Title for share favorite option") - static let shareFavoritesOptionCaption = NSLocalizedString("prefrences.sync.share-favorite-option-caption", value: "Use the same favorite bookmarks on all your devices. Leave off to keep mobile and desktop favorites separate.", comment: "Caption for share favorite option") - static let fetchFaviconsOptionTitle = NSLocalizedString("prefrences.sync.fetch-favicons-option-title", value: "Auto-Download Icons", comment: "Title for fetch favicons option") - static let fetchFaviconsOptionCaption = NSLocalizedString("prefrences.sync.fetch-favicons-option-caption", value: "Automatically download icons for synced bookmarks. Icon downloads are exposed to your network.", comment: "Caption for fetch favicons option") + static let optionsSectionTitle = NSLocalizedString("prefrences.sync.options-section-title", bundle: Bundle.module, value: "Options", comment: "Title for options settings") + static let shareFavoritesOptionTitle = NSLocalizedString("prefrences.sync.share-favorite-option-title", bundle: Bundle.module, value: "Unify Favorites Across Devices", comment: "Title for share favorite option") + static let shareFavoritesOptionCaption = NSLocalizedString("prefrences.sync.share-favorite-option-caption", bundle: Bundle.module, value: "Use the same favorite bookmarks on all your devices. Leave off to keep mobile and desktop favorites separate.", comment: "Caption for share favorite option") + static let fetchFaviconsOptionTitle = NSLocalizedString("prefrences.sync.fetch-favicons-option-title", bundle: Bundle.module, value: "Auto-Download Icons", comment: "Title for fetch favicons option") + static let fetchFaviconsOptionCaption = NSLocalizedString("prefrences.sync.fetch-favicons-option-caption", bundle: Bundle.module, value: "Automatically download icons for synced bookmarks. Icon downloads are exposed to your network.", comment: "Caption for fetch favicons option") // sync enabled errors - static let syncLimitExceededTitle = NSLocalizedString("prefrences.sync.limit-exceeded-title", value: "Sync Paused", comment: "Title for sync limits exceeded warning") - static let bookmarksLimitExceededDescription = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-description", value: "Bookmark limit exceeded. Delete some to resume syncing.", comment: "Description for sync bookmarks limits exceeded warning") - static let credentialsLimitExceededDescription = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-description", value: "Logins limit exceeded. Delete some to resume syncing.", comment: "Description for sync credentials limits exceeded warning") - static let bookmarksLimitExceededAction = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-action", value: "Manage Bookmarks", comment: "Button title for sync bookmarks limits exceeded warning to go to manage bookmarks") - static let credentialsLimitExceededAction = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-action", value: "Manage passwords…", comment: "Button title for sync credentials limits exceeded warning to go to manage passwords") - static let syncErrorAlertTitle = NSLocalizedString("alert.sync-error", value: "Sync & Backup Error", comment: "Title for sync error alert") - static let unableToSyncToServerDescription = NSLocalizedString("alert.unable-to-sync-to-server-description", value: "Unable to connect to the server.", comment: "Description for unable to sync to server error") - static let unableToSyncWithAnotherDeviceDescription = NSLocalizedString("alert.unable-to-sync-with-another-device-description", value: "Unable to Sync with another device.", comment: "Description for unable to sync with another device error") - static let unableToMergeTwoAccountsDescription = NSLocalizedString("alert.unable-to-merge-two-accounts-description", value: "To pair these devices, turn off Sync & Backup on one device then tap \"Sync With Another Device\" on the other device.", comment: "Description for unable to merge two accounts error") - static let unableToUpdateDeviceNameDescription = NSLocalizedString("alert.unable-to-update-device-name-description", value: "Unable to update the device name.", comment: "Description for unable to update device name error") - static let unableToTurnSyncOffDescription = NSLocalizedString("alert.unable-to-turn-sync-off-description", value: "Unable to turn Sync & Backup off.", comment: "Description for unable to turn sync off error") - static let unableToDeleteDataDescription = NSLocalizedString("alert.unable-to-delete-data-description", value: "Unable to delete data on the server.", comment: "Description for unable to delete data error") - static let unableToRemoveDeviceDescription = NSLocalizedString("alert.unable-to-remove-device-description", value: "Unable to remove this device from Sync & Backup.", comment: "Description for unable to remove device error") - static let invalidCodeDescription = NSLocalizedString("alert.invalid-code-description", value: "Sorry, this code is invalid. Please make sure it was entered correctly.", comment: "Description for invalid code error") - static let unableCreateRecoveryPdfDescription = NSLocalizedString("alert.unable-to-create-recovery-pdf-description", value: "Unable to create the recovery PDF.", comment: "Description for unable to create recovery pdf error") - - static let fetchFaviconsOnboardingTitle = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-title", value: "Download Missing Icons?", comment: "Title for fetch favicons onboarding dialog") - static let fetchFaviconsOnboardingMessage = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-message", value: "Do you want this device to automatically download icons for any new bookmarks synced from your other devices? This will expose the download to your network any time a bookmark is synced.", comment: "Text for fetch favicons onboarding dialog") - static let keepFaviconsUpdated = NSLocalizedString("prefrences.sync.keep-favicons-updated", value: "Keep Bookmarks Icons Updated", comment: "Title of the confirmation button for favicons fetching") + static let syncLimitExceededTitle = NSLocalizedString("prefrences.sync.limit-exceeded-title", bundle: Bundle.module, value: "Sync Paused", comment: "Title for sync limits exceeded warning") + static let bookmarksLimitExceededDescription = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-description", bundle: Bundle.module, value: "Bookmark limit exceeded. Delete some to resume syncing.", comment: "Description for sync bookmarks limits exceeded warning") + static let credentialsLimitExceededDescription = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-description", bundle: Bundle.module, value: "Logins limit exceeded. Delete some to resume syncing.", comment: "Description for sync credentials limits exceeded warning") + static let bookmarksLimitExceededAction = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-action", bundle: Bundle.module, value: "Manage Bookmarks", comment: "Button title for sync bookmarks limits exceeded warning to go to manage bookmarks") + static let credentialsLimitExceededAction = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-action", bundle: Bundle.module, value: "Manage passwords…", comment: "Button title for sync credentials limits exceeded warning to go to manage passwords") + static let syncErrorAlertTitle = NSLocalizedString("alert.sync-error", bundle: Bundle.module, value: "Sync & Backup Error", comment: "Title for sync error alert") + static let unableToSyncToServerDescription = NSLocalizedString("alert.unable-to-sync-to-server-description", bundle: Bundle.module, value: "Unable to connect to the server.", comment: "Description for unable to sync to server error") + static let unableToSyncWithAnotherDeviceDescription = NSLocalizedString("alert.unable-to-sync-with-another-device-description", bundle: Bundle.module, value: "Unable to Sync with another device.", comment: "Description for unable to sync with another device error") + static let unableToMergeTwoAccountsDescription = NSLocalizedString("alert.unable-to-merge-two-accounts-description", bundle: Bundle.module, value: "To pair these devices, turn off Sync & Backup on one device then tap \"Sync With Another Device\" on the other device.", comment: "Description for unable to merge two accounts error") + static let unableToUpdateDeviceNameDescription = NSLocalizedString("alert.unable-to-update-device-name-description", bundle: Bundle.module, value: "Unable to update the device name.", comment: "Description for unable to update device name error") + static let unableToTurnSyncOffDescription = NSLocalizedString("alert.unable-to-turn-sync-off-description", bundle: Bundle.module, value: "Unable to turn Sync & Backup off.", comment: "Description for unable to turn sync off error") + static let unableToDeleteDataDescription = NSLocalizedString("alert.unable-to-delete-data-description", bundle: Bundle.module, value: "Unable to delete data on the server.", comment: "Description for unable to delete data error") + static let unableToRemoveDeviceDescription = NSLocalizedString("alert.unable-to-remove-device-description", bundle: Bundle.module, value: "Unable to remove this device from Sync & Backup.", comment: "Description for unable to remove device error") + static let invalidCodeDescription = NSLocalizedString("alert.invalid-code-description", bundle: Bundle.module, value: "Sorry, this code is invalid. Please make sure it was entered correctly.", comment: "Description for invalid code error") + static let unableCreateRecoveryPdfDescription = NSLocalizedString("alert.unable-to-create-recovery-pdf-description", bundle: Bundle.module, value: "Unable to create the recovery PDF.", comment: "Description for unable to create recovery pdf error") + + static let fetchFaviconsOnboardingTitle = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-title", bundle: Bundle.module, value: "Download Missing Icons?", comment: "Title for fetch favicons onboarding dialog") + static let fetchFaviconsOnboardingMessage = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-message", bundle: Bundle.module, value: "Do you want this device to automatically download icons for any new bookmarks synced from your other devices? This will expose the download to your network any time a bookmark is synced.", comment: "Text for fetch favicons onboarding dialog") + static let keepFaviconsUpdated = NSLocalizedString("prefrences.sync.keep-favicons-updated", bundle: Bundle.module, value: "Keep Bookmarks Icons Updated", comment: "Title of the confirmation button for favicons fetching") // Sync Feature Flags - static let syncUnavailableTitle = NSLocalizedString("sync.warning.sync-unavailable", value: "Sync & Backup is Unavailable", comment: "Title of the warning message that sync and backup are unavailable") - static let syncPausedTitle = NSLocalizedString("sync.warning.sync-paused", value: "Sync & Backup is Paused", comment: "Title of the warning message that Sync & Backup is Paused") - static let syncUnavailableMessage = NSLocalizedString("sync.warning.sync-unavailable-message", value: "Sorry, but Sync & Backup is currently unavailable. Please try again later.", comment: "Data syncing unavailable warning message") - static let syncUnavailableMessageUpgradeRequired = NSLocalizedString("sync.warning.data-syncing-disabled-upgrade-required", value: "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue.", comment: "Data syncing unavailable warning message") + static let syncUnavailableTitle = NSLocalizedString("sync.warning.sync-unavailable", bundle: Bundle.module, value: "Sync & Backup is Unavailable", comment: "Title of the warning message that sync and backup are unavailable") + static let syncPausedTitle = NSLocalizedString("sync.warning.sync-paused", bundle: Bundle.module, value: "Sync & Backup is Paused", comment: "Title of the warning message that Sync & Backup is Paused") + static let syncUnavailableMessage = NSLocalizedString("sync.warning.sync-unavailable-message", bundle: Bundle.module, value: "Sorry, but Sync & Backup is currently unavailable. Please try again later.", comment: "Data syncing unavailable warning message") + static let syncUnavailableMessageUpgradeRequired = NSLocalizedString("sync.warning.data-syncing-disabled-upgrade-required", bundle: Bundle.module, value: "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue.", comment: "Data syncing unavailable warning message") } diff --git a/NetworkProtectionSystemExtension/Info.plist b/NetworkProtectionSystemExtension/Info.plist index c35c8be1ca..43844d5702 100644 --- a/NetworkProtectionSystemExtension/Info.plist +++ b/NetworkProtectionSystemExtension/Info.plist @@ -14,6 +14,8 @@ com.apple.networkextension.packet-tunnel $(PRODUCT_MODULE_NAME).MacPacketTunnelProvider + com.apple.networkextension.app-proxy + $(PRODUCT_MODULE_NAME).MacTransparentProxyProvider MAIN_BUNDLE_IDENTIFIER $(MAIN_BUNDLE_IDENTIFIER) diff --git a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements index a049fa6886..4252e67c8e 100644 --- a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements +++ b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.security.app-sandbox diff --git a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements index f7d87546d2..23068f001f 100644 --- a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements +++ b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider-systemextension + app-proxy-provider-systemextension com.apple.security.app-sandbox diff --git a/UnitTests/Common/Extensions/StringExtensionTests.swift b/UnitTests/Common/Extensions/StringExtensionTests.swift new file mode 100644 index 0000000000..76a5b0fd58 --- /dev/null +++ b/UnitTests/Common/Extensions/StringExtensionTests.swift @@ -0,0 +1,44 @@ +// +// StringExtensionTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +class StringExtensionTests: XCTestCase { + + func testHtmlEscapedString() { + NSError.disableSwizzledDescription = true + defer { NSError.disableSwizzledDescription = false } + + XCTAssertEqual("\"DuckDuckGo\"®".escapedUnicodeHtmlString(), ""DuckDuckGo"®") + XCTAssertEqual("i don‘t want to 'sleep'™".escapedUnicodeHtmlString(), "i don‘t want to 'sleep'™") + XCTAssertEqual("{ $embraced [&text]}".escapedUnicodeHtmlString(), "{ $embraced [&text]}") + XCTAssertEqual("X ^ 2 + y / 2 = 4 < 6%".escapedUnicodeHtmlString(), "X ^ 2 + y / 2 = 4 < 6%") + XCTAssertEqual("".escapedUnicodeHtmlString(), "<some&tag>") + XCTAssertEqual("© “text” with «emojis» 🩷🦆".escapedUnicodeHtmlString(), "© “text” with «emojis» 🩷🦆") + XCTAssertEqual("`my.mail@duck.com`".escapedUnicodeHtmlString(), "amy.mail@duck.coma") + XCTAssertEqual("floop!burp".escapedUnicodeHtmlString(), + "<hey beep=\"#test\" boop='#' fool=1 >floop!<b>burp</b></hey>") + + XCTAssertEqual(URLError(URLError.Code.cannotConnectToHost, userInfo: [NSLocalizedDescriptionKey: "Could not connect to the server."]).localizedDescription.escapedUnicodeHtmlString(), "Could not connect to the server.") + XCTAssertEqual(URLError(URLError.Code.cannotConnectToHost).localizedDescription.escapedUnicodeHtmlString(), "The operation couldn’t be completed. (NSURLErrorDomain error -1004.)") + XCTAssertEqual(URLError(URLError.Code.cannotFindHost).localizedDescription.escapedUnicodeHtmlString(), "The operation couldn’t be completed. (NSURLErrorDomain error -1003.)") + } + +} diff --git a/UnitTests/Common/NSErrorAdditionalInfo.swift b/UnitTests/Common/NSErrorAdditionalInfo.swift index 7b8e6e0a31..6f861ae629 100644 --- a/UnitTests/Common/NSErrorAdditionalInfo.swift +++ b/UnitTests/Common/NSErrorAdditionalInfo.swift @@ -27,8 +27,17 @@ extension NSError { method_exchangeImplementations(originalLocalizedDescription, swizzledLocalizedDescription) }() - @objc func swizzledLocalizedDescription() -> String { - self.debugDescription + // use `NSError.disableSwizzledDescription = true` to return an original localizedDescription, don‘t forget to set it back in tearDown + @objc dynamic func swizzledLocalizedDescription() -> String { + if Self.disableSwizzledDescription { + self.swizzledLocalizedDescription() // return original + } else { + self.debugDescription + " – NSErrorAdditionalInfo.swift" + } } + private static let disableSwizzledDescriptionKey = UnsafeRawPointer(bitPattern: "disableSwizzledDescriptionKey".hashValue)! + + static var disableSwizzledDescription: Bool = false + } diff --git a/UnitTests/Common/TestsBridging.h b/UnitTests/Common/TestsBridging.h index 2d5e4175c3..b2184c97d8 100644 --- a/UnitTests/Common/TestsBridging.h +++ b/UnitTests/Common/TestsBridging.h @@ -19,3 +19,4 @@ #import "Bridging.h" #import "DownloadsWebViewMock.h" +#import "WKURLSchemeTask+Private.h" diff --git a/UnitTests/Common/WKURLSchemeTask+Private.h b/UnitTests/Common/WKURLSchemeTask+Private.h new file mode 100644 index 0000000000..2264f27d76 --- /dev/null +++ b/UnitTests/Common/WKURLSchemeTask+Private.h @@ -0,0 +1,30 @@ +// +// WKURLSchemeTask+Private.h +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol WKURLSchemeTaskPrivate + +- (void)_willPerformRedirection:(NSURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler; +- (void)_didPerformRedirection:(NSURLResponse *)response newRequest:(NSURLRequest *)request; + +@end + +NS_ASSUME_NONNULL_END diff --git a/UnitTests/Common/WKWebViewMockingExtension.swift b/UnitTests/Common/WKWebViewMockingExtension.swift index aa5e512c39..2824714d35 100644 --- a/UnitTests/Common/WKWebViewMockingExtension.swift +++ b/UnitTests/Common/WKWebViewMockingExtension.swift @@ -53,7 +53,7 @@ class TestSchemeHandler: NSObject, WKURLSchemeHandler { func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { for middleware in middleware { if let handler = middleware(urlSchemeTask.request) { - handler(urlSchemeTask) + handler(urlSchemeTask as! WKURLSchemeTaskPrivate) return } } @@ -103,12 +103,12 @@ struct WKURLSchemeTaskHandler { } } - let handler: (WKURLSchemeTask) -> Void - init(handler: @escaping (WKURLSchemeTask) -> Void) { + let handler: (WKURLSchemeTaskPrivate) -> Void + init(handler: @escaping (WKURLSchemeTaskPrivate) -> Void) { self.handler = handler } - func callAsFunction(_ task: WKURLSchemeTask) { + func callAsFunction(_ task: WKURLSchemeTaskPrivate) { handler(task) } diff --git a/UnitTests/DataImport/DataImportViewModelTests.swift b/UnitTests/DataImport/DataImportViewModelTests.swift index 147784dcb3..6d698185e4 100644 --- a/UnitTests/DataImport/DataImportViewModelTests.swift +++ b/UnitTests/DataImport/DataImportViewModelTests.swift @@ -36,6 +36,7 @@ import XCTest model = nil importTask = nil openPanelCallback = nil + NSError.disableSwizzledDescription = false } // MARK: - Tests @@ -1514,6 +1515,8 @@ import XCTest // MARK: - Feedback func testFeedbackSending() { + NSError.disableSwizzledDescription = true + let summary: [DataImportViewModel.DataTypeImportResult] = [ .init(.bookmarks, .success(.empty)), .init(.bookmarks, .failure(Failure(.passwords, .dataCorrupted))), diff --git a/UnitTests/HomePage/ContinueSetUpModelTests.swift b/UnitTests/HomePage/ContinueSetUpModelTests.swift index e37b1fff44..e9411db6e1 100644 --- a/UnitTests/HomePage/ContinueSetUpModelTests.swift +++ b/UnitTests/HomePage/ContinueSetUpModelTests.swift @@ -109,6 +109,8 @@ final class ContinueSetUpModelTests: XCTestCase { dataBrokerProtectionRemoteMessaging: MockDataBrokerProtectionRemoteMessaging(), dataBrokerProtectionUserDefaults: userDefaults ) +#else + let messaging = HomePageRemoteMessaging.defaultMessaging() #endif vm = HomePage.Models.ContinueSetUpModel( @@ -543,6 +545,8 @@ final class ContinueSetUpModelTests: XCTestCase { dataBrokerProtectionRemoteMessaging: MockDataBrokerProtectionRemoteMessaging(), dataBrokerProtectionUserDefaults: userDefaults ) +#else + return HomePageRemoteMessaging.defaultMessaging() #endif } @@ -587,6 +591,8 @@ extension HomePage.Models.ContinueSetUpModel { dataBrokerProtectionRemoteMessaging: MockDataBrokerProtectionRemoteMessaging(), dataBrokerProtectionUserDefaults: appGroupUserDefaults ) +#else + let messaging = HomePageRemoteMessaging.defaultMessaging() #endif return HomePage.Models.ContinueSetUpModel( diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index bee9716173..f64d87be9b 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -167,6 +167,10 @@ final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility self.visible = visible } + func shouldUninstallAutomatically() -> Bool { + return !visible + } + func isNetworkProtectionVisible() -> Bool { return visible } diff --git a/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift b/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift index 6e34010422..c9f063d54f 100644 --- a/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift +++ b/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift @@ -149,17 +149,19 @@ class PinnedTabsViewModelTests: XCTestCase { model.fireproof(tabA) model.removeFireproofing(tabB) model.close(tabA) + model.muteOrUmute(tabB) cancellable.cancel() - XCTAssertEqual(events.count, 6) + XCTAssertEqual(events.count, 7) guard case .bookmark(tabA) = events[0], case .unpin(1) = events[1], case .duplicate(0) = events[2], case .fireproof(tabA) = events[3], case .removeFireproofing(tabB) = events[4], - case .close(0) = events[5] + case .close(0) = events[5], + case .muteOrUnmute(tabB) = events[6] else { XCTFail("Incorrect context menu action") return diff --git a/UnitTests/Preferences/AutofillPreferencesModelTests.swift b/UnitTests/Preferences/AutofillPreferencesModelTests.swift index 184758d8c9..6c77002c64 100644 --- a/UnitTests/Preferences/AutofillPreferencesModelTests.swift +++ b/UnitTests/Preferences/AutofillPreferencesModelTests.swift @@ -28,6 +28,7 @@ final class AutofillPreferencesPersistorMock: AutofillPreferencesPersistor { var askToSavePaymentMethods: Bool = true var passwordManager: PasswordManager = .duckduckgo var autolockLocksFormFilling: Bool = false + var debugScriptEnabled: Bool = false } final class UserAuthenticatorMock: UserAuthenticating { diff --git a/UnitTests/Tab/Model/TabTests.swift b/UnitTests/Tab/Model/TabTests.swift index f366cc4ba7..faf05897e4 100644 --- a/UnitTests/Tab/Model/TabTests.swift +++ b/UnitTests/Tab/Model/TabTests.swift @@ -269,8 +269,8 @@ final class TabTests: XCTestCase { XCTAssertTrue(tab.canGoBack) XCTAssertFalse(tab.canGoForward) XCTAssertEqual(tab.webView.url, urls.url3) - XCTAssertEqual(tab.webView.backForwardList.backList.map(\.url), [urls.url]) - XCTAssertEqual(tab.webView.backForwardList.forwardList, []) + XCTAssertEqual(tab.backHistoryItems.map(\.url), [urls.url]) + XCTAssertEqual(tab.forwardHistoryItems, []) withExtendedLifetime((c1, c2)) {} } @@ -344,8 +344,8 @@ final class TabTests: XCTestCase { XCTAssertTrue(tab.canGoBack) XCTAssertFalse(tab.canGoForward) XCTAssertEqual(tab.webView.url, urls.url3) - XCTAssertEqual(tab.webView.backForwardList.backList.map(\.url), [urls.url, urls.url3]) - XCTAssertEqual(tab.webView.backForwardList.forwardList, []) + XCTAssertEqual(tab.backHistoryItems.map(\.url), [urls.url, urls.url3]) + XCTAssertEqual(tab.forwardHistoryItems, []) withExtendedLifetime((c1, c2)) {} } diff --git a/UnitTests/Tab/ViewModel/TabViewModelTests.swift b/UnitTests/Tab/ViewModel/TabViewModelTests.swift index 5f0029ad42..9d14bf585a 100644 --- a/UnitTests/Tab/ViewModel/TabViewModelTests.swift +++ b/UnitTests/Tab/ViewModel/TabViewModelTests.swift @@ -134,18 +134,23 @@ final class TabViewModelTests: XCTestCase { XCTAssertEqual(tabViewModel.title, "New Tab") } - func testWhenTabTitleIsNotNilThenTitleReflectsTabTitle() { + func testWhenTabTitleIsNotNilThenTitleReflectsTabTitle() async throws { let tabViewModel = TabViewModel.forTabWithURL(.duckDuckGo) let testTitle = "Test title" - tabViewModel.tab.title = testTitle let titleExpectation = expectation(description: "Title") - - tabViewModel.$title.debounce(for: 0.1, scheduler: RunLoop.main).sink { title in + tabViewModel.$title.dropFirst().sink { + if case .failure(let error) = $0 { + XCTFail("\(error)") + } + } receiveValue: { title in XCTAssertEqual(title, testTitle) titleExpectation.fulfill() } .store(in: &cancellables) - waitForExpectations(timeout: 1, handler: nil) + + tabViewModel.tab.title = testTitle + + await fulfillment(of: [titleExpectation], timeout: 0.5) } func testWhenTabTitleIsNilThenTitleIsAddressBarString() { @@ -153,7 +158,7 @@ final class TabViewModelTests: XCTestCase { let titleExpectation = expectation(description: "Title") - tabViewModel.$title.debounce(for: 0.1, scheduler: RunLoop.main).sink { title in + tabViewModel.$title.debounce(for: 0.01, scheduler: RunLoop.main).sink { title in XCTAssertEqual(title, URL.duckDuckGo.host!) titleExpectation.fulfill() } .store(in: &cancellables) diff --git a/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift b/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift index b5c840aa5d..806c139493 100644 --- a/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift +++ b/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift @@ -31,6 +31,10 @@ final class WKWebViewPrivateMethodsAvailabilityTests: XCTestCase { XCTAssertTrue(WKWebView.instancesRespond(to: WKWebView.Selector.fullScreenPlaceholderView)) } + func testWebViewRespondsTo_loadAlternateHTMLString() { + XCTAssertTrue(WKWebView.instancesRespond(to: WKWebView.Selector.loadAlternateHTMLString)) + } + func testWKBackForwardListRespondsTo_removeAllItems() { XCTAssertTrue(WKBackForwardList.instancesRespond(to: WKBackForwardList.removeAllItemsSelector)) } diff --git a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift index 9d576f6550..ab577650a2 100644 --- a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift +++ b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift @@ -22,6 +22,7 @@ import Foundation class MockTabViewItemDelegate: TabBarViewItemDelegate { var hasItemsToTheRight = false + var audioState: WKWebView.AudioState = .notSupported func tabBarViewItem(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem, isMouseOver: Bool) { @@ -75,8 +76,20 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { } + func tabBarViewItemAudioState(_ tabBarViewItem: TabBarViewItem) -> WKWebView.AudioState { + return audioState + } + + func tabBarViewItemMuteUnmuteSite(_ tabBarViewItem: TabBarViewItem) { + + } + func otherTabBarViewItemsState(for tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) -> DuckDuckGo_Privacy_Browser.OtherTabBarViewItemsState { OtherTabBarViewItemsState(hasItemsToTheLeft: false, hasItemsToTheRight: hasItemsToTheRight) } + func clear() { + self.audioState = .notSupported + } + } diff --git a/UnitTests/TabBar/View/TabBarViewItemTests.swift b/UnitTests/TabBar/View/TabBarViewItemTests.swift index d855470109..c4e8f4418b 100644 --- a/UnitTests/TabBar/View/TabBarViewItemTests.swift +++ b/UnitTests/TabBar/View/TabBarViewItemTests.swift @@ -33,6 +33,10 @@ final class TabBarViewItemTests: XCTestCase { tabBarViewItem.delegate = delegate } + override func tearDown() { + delegate.clear() + } + func testThatAllExpectedItemsAreShown() { tabBarViewItem.menuNeedsUpdate(menu) @@ -48,6 +52,24 @@ final class TabBarViewItemTests: XCTestCase { XCTAssertEqual(menu.item(at: 9)?.title, UserText.moveTabToNewWindow) } + func testThatMuteIsShownWhenCurrentAudioStateIsUnmuted() { + delegate.audioState = .unmuted + tabBarViewItem.menuNeedsUpdate(menu) + + XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) + XCTAssertEqual(menu.item(at: 6)?.title, UserText.muteTab) + XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + } + + func testThatUnmuteIsShownWhenCurrentAudioStateIsMuted() { + delegate.audioState = .muted + tabBarViewItem.menuNeedsUpdate(menu) + + XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) + XCTAssertEqual(menu.item(at: 6)?.title, UserText.unmuteTab) + XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + } + func testWhenOneTabCloseThenOtherTabsItemIsDisabled() { tabBarViewItem.menuNeedsUpdate(menu) diff --git a/UnitTests/TabBar/ViewModel/TabLazyLoaderTests.swift b/UnitTests/TabBar/ViewModel/TabLazyLoaderTests.swift index d4ea051d82..b14feb90fd 100644 --- a/UnitTests/TabBar/ViewModel/TabLazyLoaderTests.swift +++ b/UnitTests/TabBar/ViewModel/TabLazyLoaderTests.swift @@ -18,6 +18,7 @@ import XCTest import Combine +import Navigation @testable import DuckDuckGo_Privacy_Browser private final class TabMock: LazyLoadable { @@ -32,7 +33,8 @@ private final class TabMock: LazyLoadable { lazy var loadingFinishedPublisher: AnyPublisher = loadingFinishedSubject.eraseToAnyPublisher() func isNewer(than other: TabMock) -> Bool { isNewerClosure(other) } - func reload() { reloadClosure(self) } + @discardableResult + func reload() -> ExpectedNavigation? { reloadClosure(self); return nil } var isNewerClosure: (TabMock) -> Bool = { _ in true } var reloadClosure: (TabMock) -> Void = { _ in } diff --git a/VPNProxyExtension/Info.plist b/VPNProxyExtension/Info.plist new file mode 100644 index 0000000000..7f2489c298 --- /dev/null +++ b/VPNProxyExtension/Info.plist @@ -0,0 +1,17 @@ + + + + + DISTRIBUTED_NOTIFICATIONS_PREFIX + $(DISTRIBUTED_NOTIFICATIONS_PREFIX) + NETP_APP_GROUP + $(NETP_APP_GROUP) + NSExtension + + NSExtensionPointIdentifier + com.apple.networkextension.app-proxy + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).MacTransparentProxyProvider + + + diff --git a/VPNProxyExtension/VPNProxyExtension.entitlements b/VPNProxyExtension/VPNProxyExtension.entitlements new file mode 100644 index 0000000000..968c758f97 --- /dev/null +++ b/VPNProxyExtension/VPNProxyExtension.entitlements @@ -0,0 +1,25 @@ + + + + + com.apple.security.application-groups + + $(TeamIdentifierPrefix)com.duckduckgo.macos.browser.network-protection + $(NETP_APP_GROUP) + + com.apple.security.app-sandbox + + com.apple.security.network.server + + keychain-access-groups + + $(NETP_APP_GROUP) + + com.apple.developer.networking.networkextension + + app-proxy-provider + + com.apple.security.network.client + + + diff --git a/fastlane/Matchfile b/fastlane/Matchfile index 8dbddfccb8..2af9eed1fe 100644 --- a/fastlane/Matchfile +++ b/fastlane/Matchfile @@ -11,6 +11,8 @@ app_identifier [ "com.duckduckgo.mobile.ios.review", "com.duckduckgo.mobile.ios.vpn.agent.review", "com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension", + "com.duckduckgo.mobile.ios.vpn.agent.proxy", + "com.duckduckgo.mobile.ios.vpn.agent.review.proxy", "com.duckduckgo.mobile.ios.DBP.backgroundAgent.review", "com.duckduckgo.mobile.ios.DBP.backgroundAgent" diff --git a/scripts/appcast_manager/appcastManager.swift b/scripts/appcast_manager/appcastManager.swift index cf059f2cb0..c3906a3cae 100755 --- a/scripts/appcast_manager/appcastManager.swift +++ b/scripts/appcast_manager/appcastManager.swift @@ -11,9 +11,10 @@ signal(SIGINT) { _ in exit(1) } +let isCI = ProcessInfo.processInfo.environment["CI"] != nil let appcastURLString = "https://staticcdn.duckduckgo.com/macos-desktop-browser/appcast2.xml" let appcastURL = URL(string: appcastURLString)! -let tmpDir = NSString(string: "~/Developer").expandingTildeInPath +let tmpDir = isCI ? "." : NSString(string: "~/Developer").expandingTildeInPath let tmpDirURL = URL(fileURLWithPath: tmpDir, isDirectory: true) let specificDir = tmpDirURL.appendingPathComponent("sparkle-updates") let appcastFilePath = specificDir.appendingPathComponent("appcast2.xml") @@ -76,9 +77,9 @@ NAME appcastManager – automation of appcast file management SYNOPSIS - appcastManager --release-to-internal-channel --dmg --release-notes - appcastManager --release-to-public-channel --version [--release-notes ] - appcastManager --release-hotfix-to-public-channel --dmg --release-notes + appcastManager --release-to-internal-channel --dmg --release-notes [--key ] + appcastManager --release-to-public-channel --version [--release-notes ] [--key ] + appcastManager --release-hotfix-to-public-channel --dmg --release-notes [--key ] appcastManager --help DESCRIPTION @@ -112,10 +113,14 @@ case .releaseToInternalChannel, .releaseHotfixToPublicChannel: print("Missing required parameters") exit(1) } + let keyFile = readKeyFileArgument() print("➡️ Action: Add to internal channel") print("➡️ DMG Path: \(dmgPath)") print("➡️ Release Notes Path: \(releaseNotesPath)") + if isCI, let keyFile { + print("➡️ Key file: \(keyFile)") + } performCommonChecksAndOperations() @@ -132,9 +137,9 @@ case .releaseToInternalChannel, .releaseHotfixToPublicChannel: // Differentiate between the two actions if arguments.action == .releaseToInternalChannel { - runGenerateAppcast(with: versionNumber, channel: "internal-channel") + runGenerateAppcast(with: versionNumber, channel: "internal-channel", keyFile: keyFile) } else { - runGenerateAppcast(with: versionNumber) + runGenerateAppcast(with: versionNumber, keyFile: keyFile) } case .releaseToPublicChannel: @@ -142,11 +147,15 @@ case .releaseToPublicChannel: print("Missing required version parameter for action '--release-to-public-channel'") exit(1) } + let keyFile = readKeyFileArgument() let versionNumber = extractVersionNumber(from: versionIdentifier) - print("Action: Release to public channel") - print("Version: \(versionIdentifier)") + print("➡️ Action: Release to public channel") + print("➡️ Version: \(versionIdentifier)") + if isCI, let keyFile { + print("➡️ Key file: \(keyFile)") + } performCommonChecksAndOperations() @@ -171,11 +180,25 @@ case .releaseToPublicChannel: } print("⚠️ Version \(versionIdentifier) removed from the appcast.") - runGenerateAppcast(with: versionNumber, rolloutInterval: "43200") + runGenerateAppcast(with: versionNumber, rolloutInterval: "43200", keyFile: keyFile) } // MARK: - Common +func readKeyFileArgument() -> String? { + let keyFile: String? = arguments.parameters["--key"] + + if isCI { + print("Running in CI mode") + guard keyFile != nil else { + print("Missing required key parameter for CI") + exit(1) + } + } + + return keyFile +} + func extractVersionNumber(from versionIdentifier: String) -> String { let components = versionIdentifier.components(separatedBy: ".") guard components.count == 4 else { @@ -251,6 +274,10 @@ extension DateFormatter { // MARK: - Verification of the signing keys func verifySigningKeys() -> Bool { + if isCI { + print("Running in CI mode. Skipping verification of signing keys.") + return true + } let publicKeyOutput = shell("generate_keys", "-p").trimmingCharacters(in: .whitespacesAndNewlines) let desiredPublicKey = "ZaO/DNMzMPBldh40b5xVrpNBmqRkuGY0BNRCUng2qRo=" @@ -703,7 +730,7 @@ func writeAppcastContent(_ content: String, to filePath: URL) { // MARK: - Generating of New Appcast -func runGenerateAppcast(with versionNumber: String, channel: String? = nil, rolloutInterval: String? = nil) { +func runGenerateAppcast(with versionNumber: String, channel: String? = nil, rolloutInterval: String? = nil, keyFile: String? = nil) { // Check if backup file already exists and remove it if FileManager.default.fileExists(atPath: backupFileURL.path) { do { @@ -730,6 +757,9 @@ func runGenerateAppcast(with versionNumber: String, channel: String? = nil, roll commandComponents.append("--versions \(versionNumber)") commandComponents.append("--maximum-versions \(maximumVersions)") commandComponents.append("--maximum-deltas \(maximumDeltas)") + if let keyFile { + commandComponents.append("--ed-key-file \(keyFile)") + } if let channel = channel { commandComponents.append("--channel \(channel)") @@ -779,8 +809,10 @@ func runGenerateAppcast(with versionNumber: String, channel: String? = nil, roll moveFiles(from: specificDir.appendingPathComponent("old_updates"), to: specificDir) print("Old update files moved back to \(specificDir.path)") - // Open specific directory in Finder - shell("open", specificDir.path) + if !isCI { + // Open specific directory in Finder + shell("open", specificDir.path) + } } func moveFiles(from sourceDir: URL, to destinationDir: URL) { diff --git a/scripts/assets/AppStoreExportOptions.plist b/scripts/assets/AppStoreExportOptions.plist index b9395f914a..9ebf3d7885 100644 --- a/scripts/assets/AppStoreExportOptions.plist +++ b/scripts/assets/AppStoreExportOptions.plist @@ -14,12 +14,16 @@ match AppStore com.duckduckgo.mobile.ios.vpn.agent macos com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension match AppStore com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension macos + com.duckduckgo.mobile.ios.vpn.agent.proxy + match AppStore com.duckduckgo.mobile.ios.vpn.agent.proxy macos com.duckduckgo.mobile.ios.review match AppStore com.duckduckgo.mobile.ios.review macos com.duckduckgo.mobile.ios.vpn.agent.review match AppStore com.duckduckgo.mobile.ios.vpn.agent.review macos com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension match AppStore com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension macos + com.duckduckgo.mobile.ios.vpn.agent.review.proxy + match AppStore com.duckduckgo.mobile.ios.vpn.agent.review.proxy macos com.duckduckgo.mobile.ios.DBP.backgroundAgent match AppStore com.duckduckgo.mobile.ios.DBP.backgroundAgent macos com.duckduckgo.mobile.ios.DBP.backgroundAgent.review diff --git a/scripts/assets/appstore-release-mm-template.json b/scripts/assets/appstore-release-mm-template.json index 6ea1754832..fe6ccb2b22 100644 --- a/scripts/assets/appstore-release-mm-template.json +++ b/scripts/assets/appstore-release-mm-template.json @@ -1 +1,14 @@ -{"channel":"${MM_USER_HANDLE}","username":"GitHub Actions","text":"**macOS app has been successfully uploaded to ${DESTINATION}** :goose_honk_tada: | [:github: Workflow run summary](${WORKFLOW_URL})","icon_url":"https://duckduckgo.com/assets/logo_header.v108.svg"} +{ + "success": { + "channel": "${MM_USER_HANDLE}", + "username": "GitHub Actions", + "text": "**macOS app has been successfully uploaded to ${DESTINATION}** :goose_honk_tada: | [:github: Workflow run summary](${WORKFLOW_URL})", + "icon_url": "https://duckduckgo.com/assets/logo_header.v108.svg" + }, + "failure": { + "channel": "${MM_USER_HANDLE}", + "username": "GitHub Actions", + "text": ":rotating_light: **macOS app ${DESTINATION} workflow failed** | [:github: Workflow run summary](${WORKFLOW_URL})", + "icon_url": "https://duckduckgo.com/assets/logo_header.v108.svg" + } +} diff --git a/scripts/assets/release-mm-template.json b/scripts/assets/release-mm-template.json index 21278d51a1..c8f2e88d81 100644 --- a/scripts/assets/release-mm-template.json +++ b/scripts/assets/release-mm-template.json @@ -1 +1,14 @@ -{"channel":"${MM_USER_HANDLE}","username":"GitHub Actions","text":"**Notarized macOS app `${RELEASE_TYPE}` build is ready** :goose_honk_tada: | [:github: Workflow run summary](${WORKFLOW_URL})${ASANA_LINK}","icon_url":"https://duckduckgo.com/assets/logo_header.v108.svg"} +{ + "success": { + "channel": "${MM_USER_HANDLE}", + "username": "GitHub Actions", + "text": "**Notarized macOS app `${RELEASE_TYPE}` build is ready** :goose_honk_tada: | [:github: Workflow run summary](${WORKFLOW_URL})${ASANA_LINK}", + "icon_url": "https://duckduckgo.com/assets/logo_header.v108.svg" + }, + "failure": { + "channel": "${MM_USER_HANDLE}", + "username": "GitHub Actions", + "text": ":rotating_light: **Notarized macOS app `${RELEASE_TYPE}` build failed** | [:github: Workflow run summary](${WORKFLOW_URL})${ASANA_LINK}", + "icon_url": "https://duckduckgo.com/assets/logo_header.v108.svg" + } +} diff --git a/scripts/assets/variants-release-mm-template.json b/scripts/assets/variants-release-mm-template.json index e95f2681a6..bef743a5f9 100644 --- a/scripts/assets/variants-release-mm-template.json +++ b/scripts/assets/variants-release-mm-template.json @@ -1 +1,14 @@ -{"channel":"${MM_USER_HANDLE}","username":"GitHub Actions","text":"**macOS app variants have been published successfully** :goose_honk_tada: | [:github: Workflow run summary](${WORKFLOW_URL})","icon_url":"https://duckduckgo.com/assets/logo_header.v108.svg"} +{ + "success": { + "channel": "${MM_USER_HANDLE}", + "username": "GitHub Actions", + "text": "**macOS app variants have been published successfully** :goose_honk_tada: | [:github: Workflow run summary](${WORKFLOW_URL})", + "icon_url": "https://duckduckgo.com/assets/logo_header.v108.svg" + }, + "failure": { + "channel": "${MM_USER_HANDLE}", + "username": "GitHub Actions", + "text": ":rotating_light: **macOS app variants workflow failed** | [:github: Workflow run summary](${WORKFLOW_URL})", + "icon_url": "https://duckduckgo.com/assets/logo_header.v108.svg" + } +} diff --git a/scripts/extract_release_notes.sh b/scripts/extract_release_notes.sh new file mode 100755 index 0000000000..175cbf5ee0 --- /dev/null +++ b/scripts/extract_release_notes.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# +# This script extracts release notes from Asana release task description. +# +# Usage: +# cat release_task_description.txt | ./extract_release_notes.sh +# + +notes_start="release notes" +notes_end="this release includes:" +is_release_notes=0 +has_release_notes=0 + +while read -r line +do + if [[ $(tr '[:upper:]' '[:lower:]' <<< "$line") == "$notes_start" ]]; then + is_release_notes=1 + elif [[ $(tr '[:upper:]' '[:lower:]' <<< "$line") == "$notes_end" ]]; then + exit 0 + elif [[ $is_release_notes -eq 1 && -n "$line" ]]; then + has_release_notes=1 + echo "$line" + fi +done + +if [[ $has_release_notes -eq 0 ]]; then + exit 1 +fi + +exit 0 diff --git a/scripts/update_asana_for_release.sh b/scripts/update_asana_for_release.sh new file mode 100755 index 0000000000..6f70c994e7 --- /dev/null +++ b/scripts/update_asana_for_release.sh @@ -0,0 +1,181 @@ +#!/bin/bash +# +# This scripts updates Asana tasks related to the release: +# - Updates "This release includes:" section of the release task with the list +# of Asana tasks linked in git commit messages since the last official release tag. +# - Moves all tasks (including the release task itself) to the Validation section +# in macOS App Board project. +# - Tags all tasks with the release tag (creating the tag as needed). +# +# Note: this script is intended to be run in CI environment and should not +# be run locally as part of the release process. +# +# Usage: +# ./update_asana_for_release.sh +# + +set -e -o pipefail + +workspace_id="137249556945" +asana_api_url="https://app.asana.com/api/1.0" +task_url_regex='^https://app.asana.com/[0-9]/[0-9]*/([0-9]*)/f$' +cwd="$(dirname "${BASH_SOURCE[0]}")" + +find_task_urls_in_git_log() { + git fetch -q --tags + last_release_tag="$(gh api /repos/duckduckgo/macos-browser/releases/latest --jq .tag_name)" + + # 1. Fetch all commit messages since the last release tag + # 2. Extract Asana task URLs from the commit messages + # (Use -A 1 to handle cases where URL is on the next line after "Task/Issue URL:") + # 3. Print the last space-separated field ($NF) of each line + # 4. Filter only Asana URLs + # 5. Remove duplicates + git log "${last_release_tag}"..HEAD \ + | grep -A 1 'Task.*URL' \ + | awk '{ print $NF; }' \ + | grep app\.asana\.com \ + | uniq +} + +fetch_current_release_notes() { + local release_task_id="$1" + curl -fLSs "${asana_api_url}/tasks/${release_task_id}?opt_fields=notes" \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + | jq -r .data.notes \ + | "${cwd}"/extract_release_notes.sh +} + +get_task_id() { + local url="$1" + if [[ "$url" =~ ${task_url_regex} ]]; then + echo "${BASH_REMATCH[1]}" + fi +} + +construct_task_description() { + local escaped_release_note + printf '%s' "Note: This task's description is managed automatically.\n" + printf '%s' 'Only the Release notes section below should be modified manually.\n' + printf '%s' 'Please do not adjust formatting.

    Release notes

    ' + if [[ -n "${release_notes[*]}" ]]; then + printf '%s' '
      ' + for release_note in "${release_notes[@]}"; do + escaped_release_note="$(sed -e 's/&/\&/g' -e 's//\>/g' <<< "${release_note}")" + printf '%s' "
    • ${escaped_release_note}
    • " + done + printf '%s' '
    ' + fi + + printf '%s' '

    This release includes:

    ' + + if [[ -n "${task_ids[*]}" ]]; then + printf '%s' '' + fi + + printf '%s' '' +} + +update_task_description() { + local html_notes="$1" + local request_payload="{\"data\":{\"html_notes\":\"${html_notes}\"}}" + + curl -fLSs -X PUT "${asana_api_url}/tasks/${release_task_id}?opt_fields=permalink_url" \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + -d "$request_payload" | jq -r .data.permalink_url +} + +move_tasks_to_section() { + local section_id="$1" + shift + local task_ids=("$@") + + for task_id in "${task_ids[@]}"; do + curl -fLSs "${asana_api_url}/sections/${section_id}/addTask" \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + --output /dev/null \ + -d "{\"data\": {\"task\": \"${task_id}\"}}" + done +} + +find_or_create_asana_release_tag() { + local marketing_version="$1" + local tag_name="macos-app-release-${marketing_version}" + local tag_id + + tag_id="$(curl -fLSs "${asana_api_url}/tasks/${release_task_id}/tags?opt_fields=name" \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + | jq -r ".data[] | select(.name==\"${tag_name}\").gid")" + + if [[ -z "$tag_id" ]]; then + tag_id=$(curl -fLSs "${asana_api_url}/workspaces/${workspace_id}/tags?opt_fields=gid" \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + -d "{\"data\": {\"name\": \"${tag_name}\"}}" | jq -r .data.gid) + fi + + echo "$tag_id" +} + +tag_tasks() { + local tag_id="$1" + shift + local task_ids=("$@") + + for task_id in "${task_ids[@]}"; do + curl -fLSs "${asana_api_url}/tasks/${task_id}/addTag" \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + --output /dev/null \ + -d "{\"data\": {\"tag\": \"${tag_id}\"}}" + done +} + +main() { + local release_task_id="$1" + local marketing_version="$2" + local validation_section_id="$3" + + if [[ -z "$release_task_id" ]]; then + echo "Usage: $0 " + exit 1 + fi + + # 1. Fetch task URLs from git commit messages + task_ids=() + while read -r line; do + task_ids+=("$(get_task_id "$line")") + done <<< "$(find_task_urls_in_git_log)" + + # 2. Fetch current release notes from Asana release task. + release_notes=() + while read -r line; do + release_notes+=("$line") + done <<< "$(fetch_current_release_notes "${release_task_id}")" + + # 3. Construct new release task description + local html_notes + html_notes="$(construct_task_description)" + + # 4. Update release task description + update_task_description "$html_notes" + + # 5. Move all tasks (including release task itself) to the validation section + task_ids+=("${release_task_id}") + move_tasks_to_section "$validation_section_id" "${task_ids[@]}" + + # 6. Get the existing Asana tag for the release, or create a new one. + local tag_id + tag_id=$(find_or_create_asana_release_tag "$marketing_version") + + # 7. Tag all tasks with the release tag + tag_tasks "$tag_id" "${task_ids[@]}" +} + +main "$@" \ No newline at end of file diff --git a/scripts/upload_to_s3/upload_to_s3.sh b/scripts/upload_to_s3/upload_to_s3.sh index e8250295dd..ddce4555c6 100755 --- a/scripts/upload_to_s3/upload_to_s3.sh +++ b/scripts/upload_to_s3/upload_to_s3.sh @@ -2,13 +2,21 @@ # Constants S3_PATH="s3://ddg-staticcdn/macos-desktop-browser/" +CDN_PATH="https://staticcdn.duckduckgo.com/macos-desktop-browser/" # Defaults -DIRECTORY="$HOME/Developer/sparkle-updates" -PROFILE="ddg-macos" +if [[ -n "$CI" ]]; then + AWS="aws" + DIRECTORY="sparkle-updates" +else + AWS="aws --profile ddg-macos" + DIRECTORY="$HOME/Developer/sparkle-updates" +fi + DEBUG=0 OVERWRITE_DMG_VERSION="" RUN_COMMAND=0 +FORCE=0 # Print the usage function print_usage() { @@ -17,7 +25,7 @@ NAME upload_to_s3.sh – automation tool for uploading files to AWS S3 for macOS Desktop Browser SYNOPSIS - $0 --run [--directory directory_path] [--overwrite-duckduckgo-dmg version] [--debug] + $0 --run [--directory directory_path] [--overwrite-duckduckgo-dmg version] [--debug] [--force] $0 --help DESCRIPTION @@ -35,6 +43,9 @@ DESCRIPTION --debug In debug mode, no 'aws cp' commands will be executed; they will only be printed to stdout. + --force + Forces the upload process to continue without asking for confirmation. + --help Displays this help message. @@ -62,13 +73,11 @@ function check_aws_installed() { # Check if there‘s a valid token function check_and_login_aws_sso() { - SSO_ACCOUNT_PROFILE=$(aws sts get-caller-identity --query "Account" --profile $PROFILE) - - if [ ${#SSO_ACCOUNT_PROFILE} -eq 14 ]; then + if $AWS sts get-caller-identity --query "Account" >/dev/null 2>&1; then echo "Session is still valid" else echo "Session has expired" - aws sso login --profile $PROFILE + $AWS sso login fi } @@ -105,6 +114,7 @@ while [[ "$#" -gt 0 ]]; do --debug) DEBUG=1 ;; --help) print_usage; exit 0 ;; # Display the help and exit immediately. --run) RUN_COMMAND=1 ;; + --force) FORCE=1 ;; *) echo "Unknown parameter passed: $1"; print_usage; exit 1 ;; # Display the help and exit with error. esac shift @@ -115,8 +125,10 @@ if [[ $RUN_COMMAND -eq 0 ]]; then exit 0 fi -# Perform AWS login if needed -check_and_login_aws_sso +if [[ -z "$CI" ]]; then + # When not in CI, perform AWS login if needed + check_and_login_aws_sso +fi # Ensure appcast2.xml exists if [[ ! -f "$DIRECTORY/appcast2.xml" ]]; then @@ -138,13 +150,13 @@ for FILENAME in $FILES_TO_UPLOAD; do fi # Check if the file exists on S3 - AWS_CMD="aws --profile $PROFILE s3 ls ${S3_PATH}${FILENAME}" - echo "Checking S3 for ${S3_PATH}${FILENAME}..." - if ! aws --profile "$PROFILE" s3 ls "${S3_PATH}${FILENAME}" > /dev/null 2>&1; then - echo "$FILENAME not found on S3. Marking for upload." - MISSING_FILES+=("$FILENAME") + printf '%s' "Checking CDN for ${CDN_PATH}${FILENAME} ... " + if curl -fLSsI "${CDN_PATH}${FILENAME}" >/dev/null 2>&1; then + echo "✅" else - echo "$FILENAME exists on S3. Skipping." + echo "❌" + echo "🚢 Marking $FILENAME for upload." + MISSING_FILES+=("$FILENAME") fi done @@ -169,23 +181,41 @@ if [[ ${#MISSING_FILES[@]} -gt 0 ]] || [[ -n "$OVERWRITE_DMG_VERSION" ]]; then echo "The file duckduckgo-$OVERWRITE_DMG_VERSION.dmg will be used to overwrite duckduckgo.dmg on S3." fi - read -p "Do you wish to continue? (y/n) " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - exit 1 + if [[ $FORCE -eq 0 ]]; then + read -p "Do you wish to continue? (y/n) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi fi fi # Upload each missing file for FILE in "${MISSING_FILES[@]}"; do - AWS_CMD="aws --profile $PROFILE s3 cp \"${DIRECTORY}/${FILE}\" ${S3_PATH}${FILE} --acl public-read" + AWS_CMD="$AWS s3 cp \"${DIRECTORY}/${FILE}\" ${S3_PATH}${FILE} --acl public-read" execute_aws "$AWS_CMD" || exit 1 done # If the overwrite flag was set, overwrite the primary dmg if [[ -n "$OVERWRITE_DMG_VERSION" ]]; then - AWS_CMD="aws --profile $PROFILE s3 cp \"${DIRECTORY}/duckduckgo-$OVERWRITE_DMG_VERSION.dmg\" ${S3_PATH}duckduckgo.dmg --acl public-read" + AWS_CMD="$AWS s3 cp \"${DIRECTORY}/duckduckgo-$OVERWRITE_DMG_VERSION.dmg\" ${S3_PATH}duckduckgo.dmg --acl public-read" execute_aws "$AWS_CMD" || exit 1 fi +if [[ -n "$CI" ]]; then + # Store the list of uploaded files in a file + TMP_FILE="$(mktemp)" + for FILE in "${MISSING_FILES[@]}"; do + echo "$FILE" >> "$TMP_FILE" + done + if [[ -n "$OVERWRITE_DMG_VERSION" ]]; then + echo "duckduckgo.dmg" >> "$TMP_FILE" + fi + + FILES_LIST_FILE="${DIRECTORY}/uploaded_files_list.txt" + rm -f "$FILES_LIST_FILE" + sort -f < "$TMP_FILE" > "$FILES_LIST_FILE" + rm -f "$TMP_FILE" +fi + echo "Upload complete!"