From 696c1d59bed8ba19916cddcd11327f8329d02f7a Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Thu, 26 May 2022 19:49:32 +0200 Subject: [PATCH 01/20] Generate tutorial addons --- .github/workflows/linters.yaml | 8 +- .github/workflows/macos-build.yaml | 6 + .gitignore | 3 + CMakeLists.txt | 3 + addons/CMakeLists.txt | 44 +++ .../guide_01_how_to_vpn/image.svg | 0 addons/guide_01_how_to_vpn/manifest.json | 50 +++ .../image.svg | 0 .../manifest.json | 37 ++ .../image.svg | 0 .../manifest.json | 43 +++ .../image.svg | 0 .../manifest.json | 43 +++ addons/language/manifest.json | 3 +- addons/tic-tac-toe/manifest.json | 1 + .../tutorial_01_get_started/image.svg | 0 addons/tutorial_01_get_started/manifest.json | 69 ++++ .../tutorial_02_connect_on_startup/image.svg | 0 .../manifest.json | 48 +++ scripts/README.md | 5 + scripts/addon/build.py | 361 ++++++++++++++++++ scripts/addon/generate_all.py | 43 +++ scripts/android/package.sh | 3 + scripts/ci/check_jsonschema.py | 11 +- scripts/ci/jsonSchemas/addon.json | 35 ++ scripts/linux/ppa_script.sh | 3 + scripts/macos/apple_compile.sh | 3 + scripts/utils/generate_strings.py | 178 --------- scripts/utils/import_languages.py | 2 - scripts/wasm/compile.sh | 13 + scripts/windows/compile.bat | 3 + src/addon.h | 2 + src/addonmanager.cpp | 112 +++++- src/addonmanager.h | 7 +- src/cmake/linux.cmake | 4 +- src/cmake/sources.cmake | 2 - src/featureslist.h | 4 +- src/inspector/inspectorhandler.cpp | 6 - src/models/guide.cpp | 49 +-- src/models/guide.h | 3 +- src/models/guideblock.cpp | 40 +- src/models/guideblock.h | 2 +- src/models/guidemodel.cpp | 23 +- src/models/guidemodel.h | 5 +- src/models/tutorial.cpp | 54 +-- src/models/tutorial.h | 2 + src/models/tutorialmodel.cpp | 26 +- src/models/tutorialmodel.h | 5 +- src/models/tutorialstep.cpp | 15 +- src/qmake/platforms/android.pri | 5 + src/qmake/platforms/ios.pri | 4 + src/qmake/platforms/linux.pri | 7 + src/qmake/platforms/macos.pri | 5 + src/qmake/platforms/wasm.pri | 1 + src/qmake/platforms/windows.pri | 5 + src/qmake/sources.pri | 2 - src/ui/developerMenu/ViewTutorials.qml | 4 +- src/ui/guides/.keepme | 0 src/ui/guides/01_how_to_vpn.json | 44 --- .../02_is_my_vpn_working_correctly.json | 31 -- .../03_adding_and_removing_devices.json | 36 -- .../04_connecting_external_devices.json | 37 -- src/ui/main.qml | 6 +- src/ui/settings/ViewGuide.qml | 10 +- src/ui/settings/ViewTipsAndTricks.qml | 10 +- src/ui/tutorials/.keepme | 0 src/ui/tutorials/01_get_started.json | 63 --- src/ui/tutorials/02_connect_on_startup.json | 42 -- src/ui/views/ViewAddon.qml | 9 - .../scripts/build/android_build_debug.sh | 2 + .../scripts/build/android_build_release.sh | 3 + taskcluster/scripts/build/wasm.sh | 2 + taskcluster/scripts/build/windows.ps1 | 1 + tests/unit/CMakeLists.txt | 4 + tests/unit/testguide.cpp | 105 ++--- tests/unit/testguide.h | 2 - tests/unit/testtutorial.cpp | 113 ++---- tests/unit/unit.pro | 4 + translations/CMakeLists.txt | 2 - translations/translations.pri | 4 - windows/installer/CMakeLists.txt | 5 +- windows/installer/MozillaVPN.wxs | 9 +- windows/installer/MozillaVPN_cmake.wxs | 5 +- windows/installer/MozillaVPN_prod.wxs | 5 +- windows/installer/build.cmd | 5 +- windows/installer/build_prod.cmd | 5 +- 86 files changed, 1155 insertions(+), 821 deletions(-) create mode 100644 addons/CMakeLists.txt rename src/ui/guides/01_how_to_vpn.svg => addons/guide_01_how_to_vpn/image.svg (100%) create mode 100644 addons/guide_01_how_to_vpn/manifest.json rename src/ui/guides/02_is_my_vpn_working_correctly.svg => addons/guide_02_is_my_vpn_working_correctly/image.svg (100%) create mode 100644 addons/guide_02_is_my_vpn_working_correctly/manifest.json rename src/ui/guides/03_adding_and_removing_devices.svg => addons/guide_03_adding_and_removing_devices/image.svg (100%) create mode 100644 addons/guide_03_adding_and_removing_devices/manifest.json rename src/ui/guides/04_connecting_external_devices.svg => addons/guide_04_connecting_external_devices/image.svg (100%) create mode 100644 addons/guide_04_connecting_external_devices/manifest.json rename src/ui/tutorials/01_get_started.svg => addons/tutorial_01_get_started/image.svg (100%) create mode 100644 addons/tutorial_01_get_started/manifest.json rename src/ui/tutorials/02_connect_on_startup.svg => addons/tutorial_02_connect_on_startup/image.svg (100%) create mode 100644 addons/tutorial_02_connect_on_startup/manifest.json create mode 100755 scripts/addon/build.py create mode 100755 scripts/addon/generate_all.py create mode 100644 scripts/ci/jsonSchemas/addon.json delete mode 100644 src/ui/guides/.keepme delete mode 100644 src/ui/guides/01_how_to_vpn.json delete mode 100644 src/ui/guides/02_is_my_vpn_working_correctly.json delete mode 100644 src/ui/guides/03_adding_and_removing_devices.json delete mode 100644 src/ui/guides/04_connecting_external_devices.json delete mode 100644 src/ui/tutorials/.keepme delete mode 100644 src/ui/tutorials/01_get_started.json delete mode 100644 src/ui/tutorials/02_connect_on_startup.json diff --git a/.github/workflows/linters.yaml b/.github/workflows/linters.yaml index c6a72ac949..9f98fb818e 100644 --- a/.github/workflows/linters.yaml +++ b/.github/workflows/linters.yaml @@ -61,13 +61,9 @@ jobs: export PATH=/opt/$QTVERSION/gcc_64/bin:$PATH python scripts/ci/check_qrc.py - - name: Check for guide JSON syntax + - name: Check for addons JSON syntax run: | - python scripts/ci/check_jsonschema.py guide.json src/ui/guides - - - name: Check for tutorial JSON syntax - run: | - python scripts/ci/check_jsonschema.py tutorial.json src/ui/tutorials + python scripts/ci/check_jsonschema.py addon.json addons/*/manifest.json - name: Check for issues with clang-format uses: DoozyX/clang-format-lint-action@v0.11 diff --git a/.github/workflows/macos-build.yaml b/.github/workflows/macos-build.yaml index 637a251a58..1dfdf7a7a0 100644 --- a/.github/workflows/macos-build.yaml +++ b/.github/workflows/macos-build.yaml @@ -47,6 +47,12 @@ jobs: pip3 install -r requirements.txt python3 scripts/utils/generate_glean.py + - name: Generating addons + shell: bash + run: | + export PATH=/opt/qt6/bin:$PATH + python3 scripts/addon/generate_all.py + - name: Importing translation files shell: bash run: | diff --git a/.gitignore b/.gitignore index 237a4cbda6..a60b2a6748 100644 --- a/.gitignore +++ b/.gitignore @@ -164,6 +164,9 @@ translations/*/locversion.plist translations/generated/ macos/pkg/Resources/*.lproj +# Addons +addons/generated/ + # Adjust SDK Files android/src/com/adjust diff --git a/CMakeLists.txt b/CMakeLists.txt index c3fe8eb5c6..2d63229272 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,6 +81,9 @@ if(NOT CMAKE_CROSSCOMPILING) add_subdirectory(extension) endif() +# Addons +add_subdirectory(addons) + # Extra platform stuff if(WIN32) add_subdirectory(windows/installer) diff --git a/addons/CMakeLists.txt b/addons/CMakeLists.txt new file mode 100644 index 0000000000..fe91c15c28 --- /dev/null +++ b/addons/CMakeLists.txt @@ -0,0 +1,44 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +add_library(addons STATIC) + +find_package(Qt6 REQUIRED COMPONENTS Core Qml) +target_link_libraries(addons PRIVATE Qt6::Core Qt6::Qml) + +get_filename_component(MVPN_SCRIPT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../scripts ABSOLUTE) +get_filename_component(GENERATED_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated ABSOLUTE) +file(MAKE_DIRECTORY ${GENERATED_DIR}) +target_include_directories(addons PUBLIC ${GENERATED_DIR}) + +file(GLOB_RECURSE + manifests + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + "${CMAKE_CURRENT_SOURCE_DIR}/*/manifest.json" +) + +foreach(manifest ${manifests}) + string(REGEX REPLACE "/manifest.json$" ".rcc" addon_name ${manifest}) + if (${manifest} MATCHES "tutorial_*" OR ${manifest} MATCHES "guide_*") + add_custom_command( + OUTPUT ${GENERATED_DIR}/${addon_name} + DEPENDS ${manifest} + COMMAND python3 ${MVPN_SCRIPT_DIR}/addon/build.py + ${CMAKE_CURRENT_SOURCE_DIR}/${manifest} ${GENERATED_DIR} + ) + list(APPEND list_addons "${GENERATED_DIR}/${addon_name}") + endif() +endforeach() + +add_custom_target(generate_all_addons + ALL + DEPENDS ${list_addons} +) + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + include(GNUInstallDirs) + install(FILES ${list_addons} DESTINATION ${CMAKE_INSTALL_DATADIR}/mozillavpn/addons) +elseif(WIN32) + install(FILES ${list_addons} DESTINATION addons) +endif() diff --git a/src/ui/guides/01_how_to_vpn.svg b/addons/guide_01_how_to_vpn/image.svg similarity index 100% rename from src/ui/guides/01_how_to_vpn.svg rename to addons/guide_01_how_to_vpn/image.svg diff --git a/addons/guide_01_how_to_vpn/manifest.json b/addons/guide_01_how_to_vpn/manifest.json new file mode 100644 index 0000000000..a6d3ebe5b5 --- /dev/null +++ b/addons/guide_01_how_to_vpn/manifest.json @@ -0,0 +1,50 @@ +{ + "version": "0.1", + "id": "guide_01_how_to_vpn", + "name": "Guide: how to vpn", + "type": "guide", + "guide": { + "id": "how_to_vpn", + "title": "The top uses and benefits of using Mozilla VPN", + "subtitle": "Mozilla VPN secures the connection between your device and the web. Here’s some of the most useful features your VPN offers:", + "image": "qrc:/addons/guide_01_how_to_vpn/image.svg", + "blocks": [ + { "id": "c_1", + "type": "title", + "content": "Safeguard your personal data and identity" }, + { "id": "c_2", + "type": "ulist", + "content": [ + { "id": "l_1", + "content": "Surf the web and use your favorite apps freely without being tracked" }, + { "id": "l_2", + "content": "Keep confidential materials safe via our premium data encryption" } + ] + }, + { "id": "c_3", + "type": "title", + "content": "Defense on the go" }, + { "id": "c_4", + "type": "ulist", + "content": [ + { "id": "l_1", + "content": "Blocks unknown entities from seeing your private data on risky public Wi-Fi and hotspots" }, + { "id": "l_2", + "content": "Protection wherever you are, from restaurants to vacation rentals" } + ] + }, + { "id": "c_5", + "type": "title", + "content": "Connect to 30+ countries" }, + { "id": "c_6", + "type": "ulist", + "content": [ + { "id": "l_1", + "content": "Browse and access content from all over the world" }, + { "id": "l_2", + "content": "Shop on a regional server to get localized deals like cheaper airfare" } + ] + } + ] + } +} diff --git a/src/ui/guides/02_is_my_vpn_working_correctly.svg b/addons/guide_02_is_my_vpn_working_correctly/image.svg similarity index 100% rename from src/ui/guides/02_is_my_vpn_working_correctly.svg rename to addons/guide_02_is_my_vpn_working_correctly/image.svg diff --git a/addons/guide_02_is_my_vpn_working_correctly/manifest.json b/addons/guide_02_is_my_vpn_working_correctly/manifest.json new file mode 100644 index 0000000000..4cf4850e32 --- /dev/null +++ b/addons/guide_02_is_my_vpn_working_correctly/manifest.json @@ -0,0 +1,37 @@ +{ + "version": "0.1", + "id": "guide_02_is_my_vpn_working_correctly", + "name": "Guide: is my VPN working correctly", + "type": "guide", + "guide": { + "id": "is_my_vpn_working_correctly", + "title": "Is my VPN connected to the right server location?", + "subtitle": "Test your geolocation on one of our 400+ worldwide servers in two super simple ways:", + "image": "qrc:/addons/guide_02_is_my_vpn_working_correctly/image.svg", + "blocks": [ + { "id": "c_1", + "type": "title", + "content": "Search in your browser" }, + { "id": "c_2", + "type": "text", + "content": "Enter a search term like “local weather” in your browser and see a real-time weather update for your current VPN location." }, + { "id": "c_3", + "type": "title", + "content": "Sign in to your favorite app or streaming site" }, + { "id": "c_4", + "type": "text", + "content": "Traveling abroad and need to access content from back home?" }, + { "id": "c_5", + "type": "olist", + "content": [ + { + "id": "l_1", + "content": "Connect to the country server of your choice" }, + { + "id": "l_2", + "content": "Sign on to your favorite app or streaming site" } + ] + } + ] + } +} diff --git a/src/ui/guides/03_adding_and_removing_devices.svg b/addons/guide_03_adding_and_removing_devices/image.svg similarity index 100% rename from src/ui/guides/03_adding_and_removing_devices.svg rename to addons/guide_03_adding_and_removing_devices/image.svg diff --git a/addons/guide_03_adding_and_removing_devices/manifest.json b/addons/guide_03_adding_and_removing_devices/manifest.json new file mode 100644 index 0000000000..2e34207189 --- /dev/null +++ b/addons/guide_03_adding_and_removing_devices/manifest.json @@ -0,0 +1,43 @@ +{ + "version": "0.1", + "id": "guide_03_adding_and_removing_devices", + "name": "Guide: adding and removing devices", + "type": "guide", + "guide": { + "id": "adding_and_removing_devices", + "title": "How do I add and remove devices?", + "subtitle": "Follow this quick and simple 3 step guide to add and remove all of your devices easily.", + "image": "image.svg", + "image": "qrc:/addons/guide_03_adding_and_removing_devices/image.svg", + "blocks": [ + { "id": "c_1", + "type": "title", + "content": "How to add a device" }, + { "id": "c_2", + "type": "olist", + "content": [ + { "id": "l_1", + "content": "Open the Mozilla VPN app on your preferred device" }, + { "id": "l_2", + "content": "Sign in to your account" }, + { "id": "l_3", + "content": "Voila! Your device has been automatically added to your VPN list of connected devices" } + ] + }, + { "id": "c_3", + "type": "title", + "content": "How to remove a device" }, + { "id": "c_4", + "type": "olist", + "content": [ + { "id": "l_1", + "content": "Open Mozilla VPN in your chosen desktop or mobile device" }, + { "id": "l_2", + "content": "Tap on “My Devices” in your VPN homescreen" }, + { "id": "l_3", + "content": "Select the “Trash” icon next to the device you want to remove from your list of connected devices" } + ] + } + ] + } +} diff --git a/src/ui/guides/04_connecting_external_devices.svg b/addons/guide_04_connecting_external_devices/image.svg similarity index 100% rename from src/ui/guides/04_connecting_external_devices.svg rename to addons/guide_04_connecting_external_devices/image.svg diff --git a/addons/guide_04_connecting_external_devices/manifest.json b/addons/guide_04_connecting_external_devices/manifest.json new file mode 100644 index 0000000000..30fbfdaf46 --- /dev/null +++ b/addons/guide_04_connecting_external_devices/manifest.json @@ -0,0 +1,43 @@ +{ + "version": "0.1", + "id": "guide_04_connecting_external_devices", + "name": "Guide: connecting external devices", + "type": "guide", + "guide": { + "id": "connecting_external_devices", + "title": "Connecting to other devices while using VPN", + "subtitle": "Enabling local network access allows you to connect to other devices without sacrificing your VPN protection.", + "image": "qrc:/addons/guide_04_connecting_external_devices/image.svg", + "blocks": [ + { "id": "c_1", + "type": "title", + "content": "How do I connect to my printer or fax?" }, + { "id": "c_2", + "type": "olist", + "content": [ + { "id": "l_1", + "content": "Enable “Local network access” in your network settings" }, + { "id": "l_2", + "content": "Connect the printer or fax cable to your device’s port (wired) or via wireless" }, + { "id": "l_3", + "content": "Print or fax your documents as normal Desktop computers / laptops / mobile phones" } + ] + }, + { "id": "c_3", + "type": "title", + "content": "What other types of devices can I connect to?" }, + { "id": "c_4", + "type": "text", + "content": "With “Local network access” turned on, you can also connect to devices like:" }, + { "id": "c_5", + "type": "ulist", + "content": [ + { "id": "l_1", + "content": "Other computers and mobile phones" }, + { "id": "l_2", + "content": "Streaming sticks (Chromecast / Roku / Apple AirPlay)" } + ] + } + ] + } +} diff --git a/addons/language/manifest.json b/addons/language/manifest.json index a87d8e807e..b5bbc76a7b 100644 --- a/addons/language/manifest.json +++ b/addons/language/manifest.json @@ -1,5 +1,6 @@ { + "id": "language-addon", "version": "0.1", - "name": "Tic-Tac-Toe", + "name": "language package", "type": "i18n" } diff --git a/addons/tic-tac-toe/manifest.json b/addons/tic-tac-toe/manifest.json index a161dca706..d89244e57f 100644 --- a/addons/tic-tac-toe/manifest.json +++ b/addons/tic-tac-toe/manifest.json @@ -1,4 +1,5 @@ { + "id": "tic-tac-toe", "version": "0.1", "name": "Tic-Tac-Toe", "qml": "tic-tac-toe.qml", diff --git a/src/ui/tutorials/01_get_started.svg b/addons/tutorial_01_get_started/image.svg similarity index 100% rename from src/ui/tutorials/01_get_started.svg rename to addons/tutorial_01_get_started/image.svg diff --git a/addons/tutorial_01_get_started/manifest.json b/addons/tutorial_01_get_started/manifest.json new file mode 100644 index 0000000000..71c1701f33 --- /dev/null +++ b/addons/tutorial_01_get_started/manifest.json @@ -0,0 +1,69 @@ +{ + "version": "0.1", + "id": "tutorial_01_get_started", + "name": "Tutorial: get started", + "type": "tutorial", + "tutorial": { + "id": "01_get_started", + "highlighted": true, + "image": "qrc:/addons/tutorial_01_get_started/image.svg", + "title": "Getting started with VPN", + "subtitle": "Follow this walkthrough to learn how to get started with using your VPN.", + "completion_message": "You’ve successfully changed your location, turned the VPN on and off. Would you like to learn more tips and tricks?", + "steps": [ + { + "id": "mainScreen", + "element": "serverListButton", + "tooltip": "Select your location", + "before": [{ + "op": "vpn_location_set", + "exitCountryCode": "at", + "exitCity": "Vienna", + "entryCountryCode": "", + "entryCity": "" + }], + "next": { + "op": "signal", + "qml_emitter": "serverListButton", + "signal": "visibleChanged" + } + }, + { + "id": "countryAU", + "element": "serverCountryList/serverCountry-au", + "tooltip": "Select a different country", + "before": [{ + "op": "property_set", + "element": "serverCountryView", + "property": "contentY", + "value": 0 + }], + "next": { + "op": "signal", + "qml_emitter": "serverCountryList/serverCountry-au", + "signal": "cityListVisibleChanged" + } + }, + { + "id": "cityAU", + "element": "serverCountryList/serverCountry-au/serverCityList/serverCity-Melbourne", + "tooltip": "Select a different server location", + "next": { + "op": "signal", + "qml_emitter": "serverCountryList/serverCountry-au/serverCityList/serverCity-Melbourne", + "signal": "visibleChanged" + } + }, + { + "id": "toggle", + "element": "controllerToggle", + "tooltip": "Toggle this switch to activate or deactivate the VPN", + "next": { + "op": "signal", + "vpn_emitter": "controller", + "signal": "stateChanged" + } + } + ] + } +} diff --git a/src/ui/tutorials/02_connect_on_startup.svg b/addons/tutorial_02_connect_on_startup/image.svg similarity index 100% rename from src/ui/tutorials/02_connect_on_startup.svg rename to addons/tutorial_02_connect_on_startup/image.svg diff --git a/addons/tutorial_02_connect_on_startup/manifest.json b/addons/tutorial_02_connect_on_startup/manifest.json new file mode 100644 index 0000000000..aa2730deef --- /dev/null +++ b/addons/tutorial_02_connect_on_startup/manifest.json @@ -0,0 +1,48 @@ +{ + "version": "0.1", + "id": "tutorial_02_connect_on_startup", + "name": "Tutorial: Connect on startup", + "type": "tutorial", + "tutorial": { + "id": "02_connect_on_startup", + "image": "qrc:/addons/tutorial_02_connect_on_startup/image.svg", + "title": "Connect VPN on startup", + "subtitle": "Follow this walkthrough to learn how to activate your VPN when you start your device.", + "completion_message": "You’ve successfully learned how to set “Connect VPN on startup”. Would you like to learn more tips and tricks?", + "conditions": { + "enabledFeatures": ["startOnBoot"] + }, + "steps": [ + { + "id": "s1", + "element": "settingsButton", + "tooltip": "Select your account settings", + "next": { + "op": "signal", + "qml_emitter": "settingsButton", + "signal": "visibleChanged" + } + }, + { + "id": "s2", + "element": "settingsPreferences", + "tooltip": "Select your system preferences", + "next": { + "op": "signal", + "qml_emitter": "settingsPreferences", + "signal": "visibleChanged" + } + }, + { + "id": "s3", + "element": "settingStartAtBoot", + "tooltip": "Select or deselect this checkbox", + "next": { + "op": "signal", + "vpn_emitter": "settingsHolder", + "signal": "startAtBootChanged" + } + } + ] + } +} diff --git a/scripts/README.md b/scripts/README.md index b84e5a3989..f37d282f32 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -68,6 +68,11 @@ TODO: - ./apply-format - apply clang-format rules - ./git-pre-commit-format - configure the pre-commit git hook for clang-formatting +# Addons + +- ./addon/build.py - generate a single addon +- ./addon/generate_all.py generate all the addons + # Others - ./tooltool.py - utility used in taskcluster diff --git a/scripts/addon/build.py b/scripts/addon/build.py new file mode 100755 index 0000000000..01eb1cb4ea --- /dev/null +++ b/scripts/addon/build.py @@ -0,0 +1,361 @@ +#! /usr/bin/env python3 +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import argparse +import json +import os +from pathlib import Path +import xml.etree.ElementTree as ET +import tempfile +import shutil +import sys + +comment_types = { + "text": f"Standard text in a guide block", + "title": f"Title in a guide block", + "ulist": f"Bullet unordered list item in a guide block", + "olist": f"Bullet ordered list item in a guide block", +} + + +def retrieve_strings_tutorial(manifest, filename): + tutorial_strings = {} + + tutorial_json = manifest["tutorial"] + if not "id" in tutorial_json: + exit(f"Tutorial {filename} does not have an id") + if not "title" in tutorial_json: + exit(f"Tutorial {filename} does not have a title") + if not "subtitle" in tutorial_json: + exit(f"Tutorial {filename} does not have a subtitle") + if not "completion_message" in tutorial_json: + exit(f"Tutorial {filename} does not have a completion message") + if not "steps" in tutorial_json: + exit(f"Tutorial {filename} does not have steps") + + tutorial_id = tutorial_json["id"] + title_id = f"tutorial.{tutorial_id}.title" + tutorial_strings[title_id] = { + "value": tutorial_json["title"], + "comments": tutorial_json.get("title_comment", "Title for a tutorial view"), + } + + subtitle_id = f"tutorial.{tutorial_id}.subtitle" + tutorial_strings[subtitle_id] = { + "value": tutorial_json["subtitle"], + "comments": tutorial_json.get( + "subtitle_comment", "Subtitle for a tutorial view" + ), + } + + completion_id = f"tutorial.{tutorial_id}.completion_message" + tutorial_strings[completion_id] = { + "value": tutorial_json["completion_message"], + "comments": tutorial_json.get( + "completion_message_comment", "Completion message for a tutorial view" + ), + } + + for step in tutorial_json["steps"]: + if not "id" in step: + exit(f"Tutorial {filename} does not have an id for one of the steps") + if not "tooltip" in step: + exit( + f"Tutorial {filename} does not have a tooltip for step id {step['id']}" + ) + + step_id = f"tutorial.{tutorial_id}.step.{step['id']}" + if step_id in tutorial_strings: + exit(f"Duplicate step id {step_id} when parsing {filename}") + + tutorial_strings[step_id] = { + "value": step["tooltip"], + "comments": step.get("comment", "A tutorial step tooltip"), + } + + return tutorial_strings + + +def retrieve_strings_guide(manifest, filename): + guide_strings = {} + + guide_json = manifest["guide"] + if not "id" in guide_json: + exit(f"Guide {filename} does not have an id") + if not "title" in guide_json: + exit(f"Guide {filename} does not have a title") + if not "subtitle" in guide_json: + exit(f"Guide {filename} does not have a subtitle") + if not "blocks" in guide_json: + exit(f"Guide {filename} does not have a blocks") + + guide_id = guide_json["id"] + title_id = f"guide.{guide_id}.title" + guide_strings[title_id] = { + "value": guide_json["title"], + "comments": guide_json.get("title_comment", "Title for a guide view"), + } + subtitle_id = f"guide.{guide_id}.subtitle" + guide_strings[subtitle_id] = { + "value": guide_json["subtitle"], + "comments": guide_json.get("subtitle_comment", "Subtitle for a guide view"), + } + + for block in guide_json["blocks"]: + if not "id" in block: + exit(f"Guide {filename} does not have an id for one of the blocks") + if not "type" in block: + exit(f"Guide {filename} does not have a type for block id {block['id']}") + if not "content" in block: + exit(f"Guide {filename} does not have a content for block id {block['id']}") + + block_id = block["id"] + block_string_id = f"guide.{guide_id}.block.{block_id}" + block_default_comment = comment_types.get(block["type"], "") + if block_string_id in guide_strings: + exit(f"Duplicate block enum {block_string_id} when parsing {filename}") + + if not isinstance(block["content"], list): + guide_strings[block_string_id] = { + "value": block["content"], + "comments": block.get("comment", block_default_comment), + } + continue + + for subblock in block["content"]: + if not "id" in subblock: + exit( + f"Guide {filename} does not have an id for one of the subblocks of block {block_id}" + ) + if not "content" in subblock: + exit( + f"Guide file {filename} does not have a content for subblock id {subblock['id']}" + ) + + subblock_id = subblock["id"] + subblock_string_id = f"guide.{guide_id}.block.{block_id}.{subblock_id}" + if subblock_string_id in guide_strings: + exit( + f"Duplicate sub-block enum {subblock_string_id} when parsing {filename}" + ) + + guide_strings[subblock_string_id] = { + "value": subblock["content"], + "comments": subblock.get("comment", block_default_comment), + } + + return guide_strings + + +def write_en_language(filename, strings): + ts = ET.Element("TS") + ts.set("version", "2.1") + ts.set("language", "en") + + context = ET.SubElement(ts, "context") + name = ET.SubElement(context, "name") + + for key, value in strings.items(): + message = ET.SubElement(context, "message") + message.set("id", key) + + source = ET.SubElement(message, "source") + source.text = value["value"] + + translation = ET.SubElement(message, "translation") + translation.set("type", "unfinished") + + if len(value["comments"]) > 0: + extracomment = ET.SubElement(message, "extracomment") + extracomment.text = value["comments"] + + with open(filename, "w", encoding="utf-8") as f: + f.write(ET.tostring(ts, encoding="unicode")) + + +def copy_files(path, dest_path): + for file in os.listdir(path): + if file.startswith("."): + continue + + for ext in ["qrc", "ts", "qm", "rcc"]: + if file.endswith(f".{ext}"): + exit(f"Unexpected {ext} file found: {os.path.join(path, file)}") + + file_path = os.path.join(path, file) + if os.path.isfile(file_path): + shutil.copyfile(file_path, os.path.join(dest_path, file)) + continue + + if os.path.isdir(file_path): + dir_path = os.path.join(dest_path, file) + os.mkdir(dir_path) + copy_files(file_path, dir_path) + + +def get_file_list(path, prefix): + file_list = [] + for file in os.listdir(path): + file_path = os.path.join(path, file) + if ( + os.path.isfile(file_path) + and not file_path.endswith(".ts") + and not file_path.endswith(".qrc") + and not file_path.endswith(".rcc") + ): + file_list.append(f"{prefix}{file}") + continue + + if os.path.isdir(file_path): + file_list += get_file_list(file_path, f"{prefix}{file}/") + + return file_list + + +parser = argparse.ArgumentParser(description="Generate an addon package") +parser.add_argument( + "source", + metavar="manifest", + type=str, + action="store", + help="The addon manifest", +) +parser.add_argument( + "dest", + metavar="dest", + type=str, + action="store", + help="The destination folder", +) +parser.add_argument( + "-q", + "--qt_path", + default=None, + dest="qtpath", + help="The QT binary path. If not set, we try to guess.", +) +args = parser.parse_args() + + +def qtquery(qmake, propname): + try: + qtquery = os.popen(f"{qmake} -query {propname}") + qtpath = qtquery.read().strip() + if len(qtpath) > 0: + return qtpath + finally: + pass + return None + + +qtbinpath = args.qtpath +if qtbinpath is None: + qtbinpath = qtquery("qmake", "QT_INSTALL_BINS") +if qtbinpath is None: + qtbinpath = qtquery("qmake6", "QT_INSTALL_BINS") +if qtbinpath is None: + print("Unable to locate qmake tool.") + sys.exit(1) + +if not os.path.isdir(qtbinpath): + print(f"QT path is not a diretory: {qtbinpath}") + sys.exit(1) + +lconvert = os.path.join(qtbinpath, "lconvert") +lrelease = os.path.join(qtbinpath, "lrelease") + +rcc_bin = "rcc" +if os.name == "nt": + rcc_bin = "rcc.exe" + +rcc = os.path.join(qtbinpath, rcc_bin) +if not os.path.isfile(rcc): + qtlibexecpath = qtquery(os.path.join(qtbinpath, "qmake"), "QT_INSTALL_LIBEXECS") + if qtlibexecpath is None: + qtlibexecpath = qtquery( + os.path.join(qtbinpath, "qmake6"), "QT_INSTALL_LIBEXECS" + ) + if qtlibexecpath is None: + print("Unable to locate qmake libexec path.") + sys.exit(1) + rcc = os.path.join(qtlibexecpath, rcc_bin) + if not os.path.isfile(rcc): + print("Unable to locate rcc path.") + sys.exit(1) + +if not os.path.isfile(args.source): + exit(f"`{args.source}` is not a file") + +if not os.path.isdir(args.dest): + exit(f"`{args.dest}` is not a directory") + +script_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + +jsonSchema = os.path.join(script_path, "ci", "jsonSchemas", "addon.json") +if not os.path.isfile(jsonSchema): + exit(f"The JSONSchema {jsonSchema} does not exist") + +with open(args.source, "r", encoding="utf-8") as file: + manifest = json.load(file) + + print(f"Copying files in a temporary folder...") + tmp_path = tempfile.mkdtemp() + copy_files(os.path.dirname(args.source), tmp_path) + + print(f"Retrieving strings...") + strings = {} + if manifest["type"] == "tutorial": + strings = retrieve_strings_tutorial(manifest, args.source) + elif manifest["type"] == "guide": + strings = retrieve_strings_guide(manifest, args.source) + else: + exit(f"Unupported manifest type `{manifest['type']}`") + + print(f"Create localization file...") + os.mkdir(os.path.join(tmp_path, "i18n")) + template_ts_file = os.path.join(args.dest, f"{manifest['id']}.ts") + write_en_language(template_ts_file, strings) + + # This will be probably replaced by the en locale if it exists + en_ts_file = os.path.join(tmp_path, "i18n", "locale_en.ts") + shutil.copyfile(template_ts_file, en_ts_file) + os.system(f"{lrelease} -idbased {en_ts_file}") + + # Fallback + ts_file = os.path.join(tmp_path, "i18n", "locale.ts") + shutil.copyfile(template_ts_file, ts_file) + os.system(f"{lrelease} -idbased {ts_file}") + + i18n_path = os.path.join(os.path.dirname(script_path), "i18n") + for locale in os.listdir(i18n_path): + if not os.path.isdir(os.path.join(i18n_path, locale)) or locale.startswith("."): + continue + + xliff_path = os.path.join( + i18n_path, locale, "addons", manifest["id"], "locale.xliff" + ) + + if os.path.isfile(xliff_path): + locale_file = os.path.join(tmp_path, "i18n", f"locale_{locale}.ts") + os.system(f"{lconvert} -if xlf -i {xliff_path} -o {locale_file}") + os.system(f"{lrelease} -idbased {locale_file}") + + print(f"Generate the RC file...") + files = get_file_list(tmp_path, "") + + qrc_file = os.path.join(tmp_path, f"{manifest['id']}.qrc") + with open(qrc_file, "w", encoding="utf-8") as f: + rcc_elm = ET.Element("RCC") + qresource = ET.SubElement(rcc_elm, "qresource") + qresource.set("prefix", f"/{manifest['id']}") + for file in files: + elm = ET.SubElement(qresource, "file") + elm.text = file + f.write(ET.tostring(rcc_elm, encoding="unicode")) + + print(f"Creating the final addon...") + rcc_file = os.path.join(args.dest, f"{manifest['id']}.rcc") + os.system(f"{rcc} {qrc_file} -o {rcc_file} -binary") + print(f"Done: {rcc_file}") diff --git a/scripts/addon/generate_all.py b/scripts/addon/generate_all.py new file mode 100755 index 0000000000..1ef4ba3f64 --- /dev/null +++ b/scripts/addon/generate_all.py @@ -0,0 +1,43 @@ +#! /usr/bin/env python3 +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import argparse +import os +import subprocess +import sys + +parser = argparse.ArgumentParser(description="Generate an addon package") +parser.add_argument( + "-q", + "--qt_path", + default=None, + dest="qtpath", + help="The QT binary path. If not set, we try to guess.", +) +args = parser.parse_args() + +build_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "build.py") +addons_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))), + "addons", +) + +generated_path = os.path.join(addons_path, "generated") +if not os.path.isdir(generated_path): + os.mkdir(generated_path) +generated_path = os.path.join(generated_path, "addons") +if not os.path.isdir(generated_path): + os.mkdir(generated_path) + +for file in os.listdir(addons_path): + if not file.startswith("tutorial_") and not file.startswith("guide_"): + continue + addon_path = os.path.join(addons_path, file, "manifest.json") + + build_args = [sys.executable, build_path, addon_path, generated_path] + if args.qtpath: + build_args.append("-q") + build_args.append(args.qtpath) + subprocess.call(build_args) diff --git a/scripts/android/package.sh b/scripts/android/package.sh index b51dbe769f..4dbd6f36fb 100755 --- a/scripts/android/package.sh +++ b/scripts/android/package.sh @@ -122,6 +122,9 @@ python3 scripts/utils/import_languages.py || die "Failed to import languages" print Y "Generating glean samples..." python3 scripts/utils/generate_glean.py -j "android/src/" || die "Failed to generate glean samples" +print Y "Generate all the addons..." +python3 ./scripts/addon/generate_all.py || die "Failed to generate addons" + print Y "Copy and patch Adjust SDK..." rm -rf "android/src/com/adjust" || die "Failed to remove the adjust folder" cp -a "3rdparty/adjust-android-sdk/Adjust/sdk-core/src/main/java/com/." "android/src/com/" || die "Failed to copy the adjust codebase" diff --git a/scripts/ci/check_jsonschema.py b/scripts/ci/check_jsonschema.py index 1e71220d0a..1e11f8744c 100755 --- a/scripts/ci/check_jsonschema.py +++ b/scripts/ci/check_jsonschema.py @@ -12,7 +12,7 @@ parser = argparse.ArgumentParser() parser.add_argument('jsonSchema', metavar='JSONSchema', type=str, nargs=1, help='the JSON Schema file to be used. The file must be stored in `scripts/ci/jsonSchemas`') -parser.add_argument('path', metavar='path', type=str, nargs=1, +parser.add_argument('path', metavar='path', type=str, nargs='+', help='the path containing the JSON files to be validated.') args = parser.parse_args() @@ -26,14 +26,9 @@ def validateFile(file, schema, resolver): if not os.path.isfile(jsonSchema): exit(f"The JSONSchema {jsonSchema} does not exist") -if not os.path.isdir(args.path[0]): - exit(f"`{args.path[0]}` is not a directory") - with open(jsonSchema, "r", encoding="utf-8") as schema: schema = json.load(schema) resolver = RefResolver(Path(jsonSchema).as_uri(), "") - for root,d_names,f_names in os.walk(args.path[0]): - for f in f_names: - if f.endswith(".json"): - validateFile(os.path.join(args.path[0], f), schema, resolver) + for path in args.path: + validateFile(path, schema, resolver) diff --git a/scripts/ci/jsonSchemas/addon.json b/scripts/ci/jsonSchemas/addon.json new file mode 100644 index 0000000000..5b3223b050 --- /dev/null +++ b/scripts/ci/jsonSchemas/addon.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "description": "The id of this addon" + }, + "name": { + "type": "string", + "description": "The name of this addon" + }, + "version": { + "type": "string", + "description": "The addon version framework" + }, + "type": { + "type": "string", + "description": "The type of the addon: tutorial, guide, i18n" + }, + "tutorial": { + "$ref": "tutorial.json#" + }, + "guide": { + "$ref": "guide.json#" + }, + "qml": { + "type": "string", + "description": "The QML entry point" + } + }, + "required": [ "id", "name", "version", "type" ] +} + diff --git a/scripts/linux/ppa_script.sh b/scripts/linux/ppa_script.sh index 9878664bea..b64d4bd9f4 100755 --- a/scripts/linux/ppa_script.sh +++ b/scripts/linux/ppa_script.sh @@ -119,6 +119,9 @@ else print Y "Generating glean samples..." python3 scripts/utils/generate_glean.py || die "Failed to generate glean samples" + print Y "Generating addons..." + python3 scripts/addon/generate_all.py || die "Failed to generate addons" + printn Y "Removing the debian template folder... " rm -rf linux/debian || die "Failed" print G "done." diff --git a/scripts/macos/apple_compile.sh b/scripts/macos/apple_compile.sh index 0a6894f98e..e4728e9985 100755 --- a/scripts/macos/apple_compile.sh +++ b/scripts/macos/apple_compile.sh @@ -109,6 +109,9 @@ python3 scripts/utils/import_languages.py $([[ $QTBINPATH ]] && echo "-q $QTBINP print Y "Generating glean samples..." python3 scripts/utils/generate_glean.py || die "Failed to generate glean samples" +print Y "Generating addons..." +python3 scripts/addon/generate_all.py $([[ $QTBINPATH ]] && echo "-q $QTBINPATH") || die "Failed to generate addons" + printn Y "Extract the project version... " SHORTVERSION=$(cat version.pri | grep VERSION | grep defined | cut -d= -f2 | tr -d \ ) FULLVERSION=$(echo $SHORTVERSION | cut -d. -f1).$(date +"%Y%m%d%H%M") diff --git a/scripts/utils/generate_strings.py b/scripts/utils/generate_strings.py index 936451da12..eb8e076bf5 100755 --- a/scripts/utils/generate_strings.py +++ b/scripts/utils/generate_strings.py @@ -9,14 +9,6 @@ import json import argparse -comment_types = { - "text": f"Standard text in a guide block", - "title": f"Title in a guide block", - "ulist": f"Bullet unordered list item in a guide block", - "olist": f"Bullet ordered list item in a guide block", -} - - def stop(string_id): exit( f"Each key must be a string or a list with 1 or more items. Fix string ID `{string_id}`" @@ -121,168 +113,6 @@ def parseTranslationStrings(yamlfile): return yaml_strings -## Parse the strings from a guide block and append them to the output dictionary. -def parseGuideBlock(block, guide_id, output, filename): - if not "id" in block: - exit(f"Guide {filename} does not have an id for one of the blocks") - if not "type" in block: - exit(f"Guide {filename} does not have a type for block id {block['id']}") - if not "content" in block: - exit(f"Guide {filename} does not have a content for block id {block['id']}") - - block_id = block['id'] - block_enum = pascalize(f"guide_{guide_id}_block_{block_id}") - block_default_comment = comment_types.get(block['type'], "") - if block_enum in output: - exit(f"Duplicate block enum {block_enum} when parsing {filename}") - - if not isinstance(block["content"], list): - output[block_enum] = { - "string_id": f"guide.{guide_id}.block.{block_id}", - "value": [block["content"]], - "comments": [block.get("comment", block_default_comment)], - } - return - - for subblock in block["content"]: - if not "id" in subblock: - exit(f"Guide {filename} does not have an id for one of the subblocks of block {block_id}") - if not "content" in subblock: - exit(f"Guide file {filename} does not have a content for subblock id {subblock['id']}") - - subblock_id = subblock['id'] - subblock_enum = pascalize(f"guide_{guide_id}_block_{block_id}_{subblock_id}") - if subblock_enum in output: - exit(f"Duplicate sub-block enum {subblock_enum} when parsing {filename}") - - output[subblock_enum] = { - "string_id": f"guide.{guide_id}.block.{block_id}.{subblock_id}", - "value": [subblock["content"]], - "comments": [subblock.get("comment", block_default_comment)], - } - -## Parse the strings from a JSON guide and return them as a dictionary. -def parseGuideJson(guide_json, filename): - guide_strings = {} - - if not "id" in guide_json: - exit(f"Guide {filename} does not have an id") - if not "title" in guide_json: - exit(f"Guide {filename} does not have a title") - if not "subtitle" in guide_json: - exit(f"Guide {filename} does not have a subtitle") - if not "blocks" in guide_json: - exit(f"Guide {filename} does not have a blocks") - - guide_id = guide_json['id'] - title_enum = pascalize(f"guide_{guide_id}_title") - guide_strings[title_enum] = { - "string_id": f"guide.{guide_id}.title", - "value": [guide_json["title"]], - "comments": [guide_json.get("title_comment", "Title for a guide view")], - } - subtitle_enum = pascalize(f"guide_{guide_id}_subtitle") - guide_strings[subtitle_enum] = { - "string_id": f"guide.{guide_id}.subtitle", - "value": [guide_json["subtitle"]], - "comments": [guide_json.get("subtitle_comment", "Subtitle for a guide view")], - } - - for block in guide_json["blocks"]: - parseGuideBlock(block, guide_id, guide_strings, filename) - - return guide_strings - -## Parse a directory of JSON guides, returning their combined strings as a dictionary -def parseGuideStrings(guidepath): - guide_strings = {} - for filename in os.listdir(guidepath): - if not filename.endswith(".json"): - continue - - with open(os.path.join(guidepath, filename), "r", encoding="utf-8") as fp: - guide_json = json.load(fp) - - for key, value in parseGuideJson(guide_json, filename).items(): - if key in guide_strings: - exit(f"Duplicate enum {key} when parsing {filename}") - guide_strings[key] = value - - return guide_strings - -## Parse the strings from a JSON tutorial and return them as a dictionary. -def parseTutorialJson(tutorial_json, filename): - tutorial_strings = {} - - if not "id" in tutorial_json: - exit(f"Tutorial {filename} does not have an id") - if not "title" in tutorial_json: - exit(f"Tutorial {filename} does not have a title") - if not "subtitle" in tutorial_json: - exit(f"Tutorial {filename} does not have a subtitle") - if not "completion_message" in tutorial_json: - exit(f"Tutorial {filename} does not have a completion message") - if not "steps" in tutorial_json: - exit(f"Tutorial {filename} does not have steps") - - tutorial_id = tutorial_json['id'] - title_enum = pascalize(f"tutorial_{tutorial_id}_title") - tutorial_strings[title_enum] = { - "string_id": f"tutorial.{tutorial_id}.title", - "value": [tutorial_json["title"]], - "comments": [tutorial_json.get("title_comment", "Title for a tutorial view")], - } - - subtitle_enum = pascalize(f"tutorial_{tutorial_id}_subtitle") - tutorial_strings[subtitle_enum] = { - "string_id": f"tutorial.{tutorial_id}.subtitle", - "value": [tutorial_json["subtitle"]], - "comments": [tutorial_json.get("subtitle_comment", "Subtitle for a tutorial view")], - } - - completion_enum = pascalize(f"tutorial_{tutorial_id}_completion_message") - tutorial_strings[completion_enum] = { - "string_id": f"tutorial.{tutorial_id}.completion_message", - "value": [tutorial_json["completion_message"]], - "comments": [tutorial_json.get("completion_message_comment", "Completion message for a tutorial view")], - } - - for step in tutorial_json["steps"]: - if not "id" in step: - exit(f"Tutorial {filename} does not have an id for one of the steps") - if not "tooltip" in step: - exit(f"Tutorial {filename} does not have a tooltip for step id {step['id']}") - - step_id = step['id'] - step_enum = pascalize(f"tutorial_{tutorial_id}_step_{step_id}") - if step_enum in tutorial_strings: - exit(f"Duplicate step enum {step_enum} when parsing {filename}") - - tutorial_strings[step_enum] = { - "string_id": f"tutorial.{tutorial_id}.step.{step_id}", - "value": [step["tooltip"]], - "comments": [step.get("comment", "A tutorial step tooltip")], - } - - return tutorial_strings - -## Parse a directory of JSON tutorials, returning their combined strings as a dictionary -def parseTutorialStrings(tutorialpath): - tutorial_strings = {} - for filename in os.listdir(tutorialpath): - if not filename.endswith(".json"): - continue - - with open(os.path.join(tutorialpath, filename), "r", encoding="utf-8") as fp: - tutorial_json = json.load(fp) - - for key, value in parseTutorialJson(tutorial_json, filename).items(): - if key in tutorial_strings: - exit(f"Duplicate enum {key} when parsing {filename}") - tutorial_strings[key] = value - - return tutorial_strings - ## Render a dictionary of strings into the l18nstrings module. def generateStrings(strings, outdir): os.makedirs(outdir, exist_ok=True) @@ -388,10 +218,6 @@ def serialize(string): help='YAML strings file to process') parser.add_argument('-o', '--output', metavar='DIR', type=str, action='store', help='Output directory for generated files') - parser.add_argument('-g', '--guides', metavar='DIR', type=str, action='store', - help='Parse JSON guides from DIR') - parser.add_argument('-t', '--tutorials', metavar='DIR', type=str, action='store', - help='Parse JSON tutorials from DIR') args = parser.parse_args() ## If no source was provided, find it relative to this script file. @@ -405,10 +231,6 @@ def serialize(string): ## Parse the inputs for their sweet juicy strings. strings = parseTranslationStrings(args.source) - if args.guides: - strings.update(parseGuideStrings(args.guides)) - if args.tutorials: - strings.update(parseTutorialStrings(args.tutorials)) ## Render the strings into generated content. generateStrings(strings, args.output) diff --git a/scripts/utils/import_languages.py b/scripts/utils/import_languages.py index 695d9fcfa8..4a6b2e5b7c 100755 --- a/scripts/utils/import_languages.py +++ b/scripts/utils/import_languages.py @@ -171,8 +171,6 @@ def qtquery(qmake, propname): title("Generate the Js/C++ string definitions...") try: subprocess.call([sys.executable, os.path.join('scripts', 'utils', 'generate_strings.py'), - '-g', os.path.join('src', 'ui', 'guides'), - '-t', os.path.join('src', 'ui', 'tutorials'), '-o', os.path.join('translations', 'generated'), os.path.join('translations', 'strings.yaml')]) except Exception as e: diff --git a/scripts/wasm/compile.sh b/scripts/wasm/compile.sh index 0af95ec25c..a1bd0a104f 100755 --- a/scripts/wasm/compile.sh +++ b/scripts/wasm/compile.sh @@ -57,6 +57,19 @@ python3 scripts/utils/import_languages.py || die "Failed to import languages" print Y "Generating glean samples..." python3 scripts/utils/generate_glean.py || die "Failed to generate glean samples" +print Y "Generating addons..." +python3 ./scripts/addon/generate_all.py || die "Failed to generate addons" + +print Y "Merge addons..." +( + cd addons/generated/addons + echo '' + for i in *; do + echo "$i" + done + echo '' +) > addons/generated/addons/addons.qrc + printn Y "Mode: " MODE= if [ "$DEBUG" = 1 ]; then diff --git a/scripts/windows/compile.bat b/scripts/windows/compile.bat index a9db080311..2c24304c51 100644 --- a/scripts/windows/compile.bat +++ b/scripts/windows/compile.bat @@ -76,6 +76,9 @@ python3 scripts\utils\import_languages.py ECHO Generating glean samples... python3 scripts\utils\generate_glean.py +ECHO Generating addons... +python3 scripts\addon\generate_all.py + ECHO Creating the project with flags: %FLAGS% if %DEBUG_BUILD% == T ( diff --git a/src/addon.h b/src/addon.h index b67538e00e..173dcfb4c6 100644 --- a/src/addon.h +++ b/src/addon.h @@ -19,7 +19,9 @@ class Addon final : public QObject { public: enum AddonType { AddonTypeDemo, + AddonTypeGuide, AddonTypeI18n, + AddonTypeTutorial, }; Addon(QObject* parent, AddonType addonType, const QString& fileName, diff --git a/src/addonmanager.cpp b/src/addonmanager.cpp index 67a8d74b4c..3d50a9eee0 100644 --- a/src/addonmanager.cpp +++ b/src/addonmanager.cpp @@ -3,17 +3,22 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "addonmanager.h" +#include "constants.h" #include "leakdetector.h" #include "localizer.h" #include "logger.h" #include "models/feature.h" +#include "models/guidemodel.h" +#include "models/tutorialmodel.h" #include +#include #include #include #include #include #include +#include namespace { @@ -27,6 +32,7 @@ AddonManager* s_instance = nullptr; AddonManager* AddonManager::instance() { if (!s_instance) { s_instance = new AddonManager(qApp); + s_instance->loadAll(); } return s_instance; } @@ -37,7 +43,56 @@ AddonManager::AddonManager(QObject* parent) : QObject(parent) { AddonManager::~AddonManager() { MVPN_COUNT_DTOR(AddonManager); } +void AddonManager::loadAll() { + if (!Feature::get(Feature::Feature_addon)->isSupported()) { + logger.warning() << "Addons disabled by feature flag"; + return; + } + + QString addonPath; +#if defined(ADDONS_PATH) + addonPath = ADDONS_PATH; +#elif defined(MVPN_WINDOWS) + addonPath = + QString("%1/addons").arg(QCoreApplication::applicationDirPath()); // TODO +#elif defined(MVPN_MACOS) + addonPath = QString("%1/../Contents/Release/addons") + .arg(QCoreApplication::applicationDirPath()); +#elif defined(MVPN_IOS) + addonPath = QString("%1/addons").arg(QCoreApplication::applicationDirPath()); +#elif defined(MVPN_ANDROID) + addonPath = QString("assets:/addons"); +#elif defined(MVPN_WASM) + addonPath = QString(":/addons"); +#endif + + logger.debug() << "Loading addon from" << addonPath; + + if (!addonPath.isEmpty()) { + QDir addonDir(addonPath); + addonDir.setSorting(QDir::Name); + loadAll(addonDir); + } + + if (!Constants::inProduction()) { + QDir homePath( + QStandardPaths::writableLocation(QStandardPaths::HomeLocation)); + homePath.cd(".mozillavpn_addons"); + homePath.setSorting(QDir::Name); + loadAll(homePath); + } +} + +void AddonManager::loadAll(const QDir& path) { + for (const QString& file : + path.entryList(QStringList{"*.rcc"}, QDir::Files)) { + load(path.filePath(file)); + } +} + bool AddonManager::load(const QString& fileName) { + logger.debug() << "Load addon" << fileName; + if (!Feature::get(Feature::Feature_addon)->isSupported()) { logger.warning() << "Addons disabled by feature flag"; return false; @@ -82,6 +137,18 @@ bool AddonManager::load(const QString& fileName) { return false; } + QString id = obj["id"].toString(); + if (id.isEmpty()) { + logger.warning() << "No id in the manifest" << addonId; + return false; + } + + if (id != addonId) { + logger.warning() << "The ID does not match with the addon one" << addonId + << id; + return false; + } + QString name = obj["name"].toString(); if (name.isEmpty()) { logger.warning() << "No name in the manifest" << addonId; @@ -113,34 +180,37 @@ bool AddonManager::load(const QString& fileName) { } } else if (type == "i18n") { addonType = Addon::AddonTypeI18n; + } else if (type == "tutorial") { + addonType = Addon::AddonTypeTutorial; + } else if (type == "guide") { + addonType = Addon::AddonTypeGuide; } else { logger.warning() << "Unsupported type" << type << addonId; return false; } + if (addonType == Addon::AddonTypeTutorial) { + if (!TutorialModel::instance()->createFromJson( + obj["tutorial"].toObject())) { + logger.warning() << "Unable to add the tutorial"; + return false; + } + } + + if (addonType == Addon::AddonTypeGuide) { + if (!GuideModel::instance()->createFromJson(obj["guide"].toObject())) { + logger.warning() << "Unable to add the guide"; + return false; + } + } + guard.dismiss(); Addon* addon = new Addon(this, addonType, fileName, addonId, name, qmlFileName); m_addons.insert(addonId, addon); - return true; -} - -void AddonManager::unload(const QString& addonId) { - if (!m_addons.contains(addonId)) { - logger.warning() << "No addon with id" << addonId; - return; - } - Addon* addon = m_addons[addonId]; - Q_ASSERT(addon); - - QResource::unregisterResource(addon->fileName(), "/addons"); - - emit unloadAddon(addonId); - m_addons.remove(addonId); - - addon->deleteLater(); + return true; } void AddonManager::run(const QString& addonId) { @@ -165,6 +235,14 @@ void AddonManager::run(const QString& addonId) { case Addon::AddonTypeI18n: emit Localizer::instance()->codeChanged(); break; + + case Addon::AddonTypeTutorial: + // Nothing todo for these types + break; + + case Addon::AddonTypeGuide: + // Nothing todo for these types + break; } } diff --git a/src/addonmanager.h b/src/addonmanager.h index b304bfbb36..a4e9eb4f0a 100644 --- a/src/addonmanager.h +++ b/src/addonmanager.h @@ -10,6 +10,8 @@ #include #include +class QDir; + class AddonManager final : public QObject { Q_OBJECT Q_DISABLE_COPY_MOVE(AddonManager) @@ -20,7 +22,6 @@ class AddonManager final : public QObject { ~AddonManager(); bool load(const QString& addonFileName); - void unload(const QString& addonName); void run(const QString& addonName); void retranslate(); @@ -28,9 +29,11 @@ class AddonManager final : public QObject { private: explicit AddonManager(QObject* parent); + void loadAll(); + void loadAll(const QDir& path); + signals: void runAddon(Addon* addon); - void unloadAddon(const QString& addonId); private: QHash m_addons; diff --git a/src/cmake/linux.cmake b/src/cmake/linux.cmake index 2af9c4d5e3..860e43c972 100644 --- a/src/cmake/linux.cmake +++ b/src/cmake/linux.cmake @@ -96,7 +96,7 @@ install(FILES ../linux/extra/icons/32x32/mozillavpn.png install(FILES ../linux/extra/icons/48x48/mozillavpn.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/48x48/apps) -add_definitions(-DMVPN_ICON_PATH=\"${CMAKE_INSTALL_DATADIR}/icons/hicolor/64x64/apps/mozillavpn.png\") +add_definitions(-DMVPN_ICON_PATH=\"${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/icons/hicolor/64x64/apps/mozillavpn.png\") install(FILES ../linux/extra/icons/64x64/mozillavpn.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/64x64/apps) @@ -112,6 +112,8 @@ install(FILES platforms/linux/daemon/org.mozilla.vpn.conf install(FILES platforms/linux/daemon/org.mozilla.vpn.dbus.service DESTINATION ${CMAKE_INSTALL_DATADIR}/dbus-1/system-services) +add_definitions(-DADDONS_PATH=\"${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/mozillavpn/addons\") + ## This is only really needed when building from source. Otherwise, we ## expect the Distro's packaging magic to sort this out. pkg_check_modules(SYSTEMD systemd) diff --git a/src/cmake/sources.cmake b/src/cmake/sources.cmake index cd9819c4cd..6806db6992 100644 --- a/src/cmake/sources.cmake +++ b/src/cmake/sources.cmake @@ -291,10 +291,8 @@ target_sources(mozillavpn PRIVATE # VPN Client UI resources target_sources(mozillavpn PRIVATE - ui/guides.qrc ui/license.qrc ui/resources.qrc - ui/tutorials.qrc ui/ui.qrc resources/certs/certs.qrc ) diff --git a/src/featureslist.h b/src/featureslist.h index 12eea99ded..9ebea308ca 100644 --- a/src/featureslist.h +++ b/src/featureslist.h @@ -30,9 +30,9 @@ FEATURE_SIMPLE(addon, // Feature ID "Addon support", // Feature name "2.9.0", // released true, // Can be flipped on - false, // Can be flipped off + true, // Can be flipped off QStringList(), // feature dependencies - FeatureCallback_false) + FeatureCallback_true) FEATURE_SIMPLE(appReview, // Feature ID "App Review", // Feature name diff --git a/src/inspector/inspectorhandler.cpp b/src/inspector/inspectorhandler.cpp index 4457cc9067..edb0bd3a96 100644 --- a/src/inspector/inspectorhandler.cpp +++ b/src/inspector/inspectorhandler.cpp @@ -897,12 +897,6 @@ static QList s_commands{ return obj; }}, - InspectorCommand{"unload_addon", "Load an addon", 1, - [](InspectorHandler*, const QList& arguments) { - AddonManager::instance()->unload(arguments[1]); - return QJsonObject(); - }}, - InspectorCommand{"run_addon", "Load an addon", 1, [](InspectorHandler*, const QList& arguments) { AddonManager::instance()->run(arguments[1]); diff --git a/src/models/guide.cpp b/src/models/guide.cpp index 87b37ae666..28e0539f05 100644 --- a/src/models/guide.cpp +++ b/src/models/guide.cpp @@ -4,7 +4,6 @@ #include "guide.h" #include "guideblock.h" -#include "l18nstrings.h" #include "leakdetector.h" #include "logger.h" #include "models/feature.h" @@ -136,8 +135,11 @@ Guide* Guide::create(QObject* parent, const QString& fileName) { return nullptr; } - QJsonObject obj = json.object(); + return create(parent, json.object()); +} +// static +Guide* Guide::create(QObject* parent, const QJsonObject& obj) { QJsonObject conditions = obj["conditions"].toObject(); if (!evaluateConditions(conditions)) { logger.info() << "Exclude the guide because conditions do not match"; @@ -146,52 +148,37 @@ Guide* Guide::create(QObject* parent, const QString& fileName) { QString guideId = obj["id"].toString(); if (guideId.isEmpty()) { - logger.warning() << "Empty ID for guide file" << fileName; + logger.warning() << "Empty ID for guide"; return nullptr; } - L18nStrings* l18nStrings = L18nStrings::instance(); - Q_ASSERT(l18nStrings); - Guide* guide = new Guide(parent); auto guard = qScopeGuard([&] { guide->deleteLater(); }); - guide->m_titleId = pascalize(QString("guide_%1_title").arg(guideId)); - if (!l18nStrings->contains(guide->m_titleId)) { - logger.warning() << "No string ID found for the title of guide file" - << fileName << "ID:" << guide->m_titleId; - return nullptr; - } - - guide->m_subtitleId = pascalize(QString("guide_%1_subtitle").arg(guideId)); - if (!l18nStrings->contains(guide->m_subtitleId)) { - logger.warning() << "No string ID found for the subtitle of guide file" - << fileName << "ID:" << guide->m_subtitleId; - return nullptr; - } + guide->m_titleId = QString("guide.%1.title").arg(guideId); + guide->m_subtitleId = QString("guide.%1.subtitle").arg(guideId); guide->m_image = obj["image"].toString(); if (guide->m_image.isEmpty()) { - logger.warning() << "Empty image for guide file" << fileName; + logger.warning() << "Empty image for guide"; return nullptr; } QJsonValue blocksArray = obj["blocks"]; if (!blocksArray.isArray()) { - logger.warning() << "No blocks for guide file" << fileName; + logger.warning() << "No blocks for guide"; return nullptr; } for (QJsonValue blockValue : blocksArray.toArray()) { if (!blockValue.isObject()) { - logger.warning() << "Expected JSON objects as blocks for guide file" - << fileName; + logger.warning() << "Expected JSON objects as blocks for guide"; return nullptr; } QJsonObject blockObj = blockValue.toObject(); - GuideBlock* block = GuideBlock::create(guide, guideId, fileName, blockObj); + GuideBlock* block = GuideBlock::create(guide, guideId, blockObj); if (!block) { return nullptr; } @@ -203,20 +190,6 @@ Guide* Guide::create(QObject* parent, const QString& fileName) { return guide; } -// static -QString Guide::pascalize(const QString& input) { - QString output; - - for (QString chunk : input.split("_")) { - if (chunk.isEmpty()) continue; - - chunk[0] = chunk[0].toUpper(); - output.append(chunk); - } - - return output; -} - // static bool Guide::evaluateConditions(const QJsonObject& conditions) { if (!evaluateConditionsEnabledFeatures( diff --git a/src/models/guide.h b/src/models/guide.h index 0662e4e71e..35539d2937 100644 --- a/src/models/guide.h +++ b/src/models/guide.h @@ -20,8 +20,7 @@ class Guide final : public QObject { public: static Guide* create(QObject* parent, const QString& fileName); - - static QString pascalize(const QString& input); + static Guide* create(QObject* parent, const QJsonObject& obj); static bool evaluateConditions(const QJsonObject& conditions); diff --git a/src/models/guideblock.cpp b/src/models/guideblock.cpp index a054120c1b..2c326363c7 100644 --- a/src/models/guideblock.cpp +++ b/src/models/guideblock.cpp @@ -4,7 +4,6 @@ #include "guideblock.h" #include "guide.h" -#include "l18nstrings.h" #include "leakdetector.h" #include "logger.h" @@ -23,23 +22,18 @@ GuideBlock::~GuideBlock() { MVPN_COUNT_DTOR(GuideBlock); } // static GuideBlock* GuideBlock::create(QObject* parent, const QString& guideId, - const QString& fileName, const QJsonObject& blockObj) { Q_ASSERT(parent); - L18nStrings* l18nStrings = L18nStrings::instance(); - Q_ASSERT(l18nStrings); - GuideBlock* block = new GuideBlock(parent); QString blockId = blockObj["id"].toString(); if (blockId.isEmpty()) { - logger.error() << "Empty block ID for guide file" << fileName; + logger.error() << "Empty block ID for guide"; return nullptr; } - block->m_id = - Guide::pascalize(QString("guide_%1_block_%2").arg(guideId).arg(blockId)); + block->m_id = QString("guide.%1.block.%2").arg(guideId).arg(blockId); QString type = blockObj["type"].toString(); if (type == "title") { @@ -51,7 +45,7 @@ GuideBlock* GuideBlock::create(QObject* parent, const QString& guideId, } else if (type == "ulist") { block->m_type = GuideModel::GuideBlockTypeUnorderedList; } else { - logger.error() << "Invalid type for block for guide file" << fileName; + logger.error() << "Invalid type for block for guide"; return nullptr; } @@ -59,44 +53,30 @@ GuideBlock* GuideBlock::create(QObject* parent, const QString& guideId, block->m_type == GuideModel::GuideBlockTypeUnorderedList) { QJsonValue subBlockArray = blockObj["content"]; if (!subBlockArray.isArray()) { - logger.error() << "No content for block type list in guide file" - << fileName; + logger.error() << "No content for block type list in guide"; return nullptr; } for (QJsonValue subBlockValue : subBlockArray.toArray()) { if (!subBlockValue.isObject()) { logger.error() - << "Expected JSON object for block content list in guide file" - << fileName; + << "Expected JSON object for block content list in guide"; return nullptr; } QJsonObject subBlockObj = subBlockValue.toObject(); QString subBlockId = subBlockObj["id"].toString(); if (subBlockId.isEmpty()) { - logger.error() << "Empty sub block ID for guide file" << fileName; - return nullptr; - } - - subBlockId = Guide::pascalize(QString("guide_%1_block_%2_%3") - .arg(guideId) - .arg(blockId) - .arg(subBlockId)); - if (!l18nStrings->contains(subBlockId)) { - logger.error() << "No string ID found for the block of guide file" - << fileName << "ID:" << subBlockId; + logger.error() << "Empty sub block ID for guide"; return nullptr; } + subBlockId = QString("guide.%1.block.%2.%3") + .arg(guideId) + .arg(blockId) + .arg(subBlockId); block->m_subBlockIds.append(subBlockId); } - } else { - if (!l18nStrings->contains(block->m_id)) { - logger.error() << "No string ID found for the block of guide file" - << fileName << "ID:" << block->m_id; - return nullptr; - } } return block; diff --git a/src/models/guideblock.h b/src/models/guideblock.h index e9857adbad..e363681c3d 100644 --- a/src/models/guideblock.h +++ b/src/models/guideblock.h @@ -20,7 +20,7 @@ class GuideBlock final : public QObject { Q_PROPERTY(QStringList subBlockIds MEMBER m_subBlockIds CONSTANT) static GuideBlock* create(QObject* parent, const QString& guideId, - const QString& fileName, const QJsonObject& json); + const QJsonObject& json); ~GuideBlock(); private: diff --git a/src/models/guidemodel.cpp b/src/models/guidemodel.cpp index 1f037d1643..954164025a 100644 --- a/src/models/guidemodel.cpp +++ b/src/models/guidemodel.cpp @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "guidemodel.h" +#include "addonmanager.h" #include "guide.h" #include "leakdetector.h" #include "logger.h" @@ -19,6 +20,9 @@ Logger logger(LOG_MAIN, "GuideModel"); GuideModel* GuideModel::instance() { if (!s_instance) { s_instance = new GuideModel(qApp); + + // We need tutorials from the addon manager. + AddonManager::instance(); } return s_instance; @@ -26,7 +30,6 @@ GuideModel* GuideModel::instance() { GuideModel::GuideModel(QObject* parent) : QAbstractListModel(parent) { MVPN_COUNT_CTOR(GuideModel); - initialize(); } GuideModel::~GuideModel() { MVPN_COUNT_DTOR(GuideModel); } @@ -40,18 +43,14 @@ QStringList GuideModel::guideTitleIds() const { return guides; } -void GuideModel::initialize() { - QDir dir(":/guides"); - QStringList files = dir.entryList(); - files.sort(); - for (const QString& file : files) { - if (file.endsWith(".json")) { - Guide* guide = Guide::create(this, dir.filePath(file)); - if (guide) { - m_guides.append(guide); - } - } +bool GuideModel::createFromJson(const QJsonObject& obj) { + Guide* guide = Guide::create(this, obj); + if (guide) { + m_guides.append(guide); + return true; } + + return false; } QHash GuideModel::roleNames() const { diff --git a/src/models/guidemodel.h b/src/models/guidemodel.h index c7cd88ec98..95b1799cd0 100644 --- a/src/models/guidemodel.h +++ b/src/models/guidemodel.h @@ -9,6 +9,7 @@ #include class Guide; +class QJsonObject; class GuideModel final : public QAbstractListModel { Q_OBJECT @@ -33,6 +34,8 @@ class GuideModel final : public QAbstractListModel { QStringList guideTitleIds() const; + bool createFromJson(const QJsonObject& obj); + // QAbstractListModel methods QHash roleNames() const override; @@ -44,8 +47,6 @@ class GuideModel final : public QAbstractListModel { private: explicit GuideModel(QObject* parent); - void initialize(); - QList m_guides; }; diff --git a/src/models/tutorial.cpp b/src/models/tutorial.cpp index 49618ed96a..c1d011b121 100644 --- a/src/models/tutorial.cpp +++ b/src/models/tutorial.cpp @@ -4,7 +4,6 @@ #include "tutorial.h" #include "guide.h" -#include "l18nstrings.h" #include "leakdetector.h" #include "logger.h" #include "qmlengineholder.h" @@ -30,6 +29,8 @@ Tutorial::~Tutorial() { MVPN_COUNT_DTOR(Tutorial); } // static Tutorial* Tutorial::create(QObject* parent, const QString& fileName) { + logger.debug() << "Tutorial create" << fileName; + QFile file(fileName); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { logger.warning() << "Unable to read the tutorial file" << fileName; @@ -43,8 +44,11 @@ Tutorial* Tutorial::create(QObject* parent, const QString& fileName) { return nullptr; } - QJsonObject obj = json.object(); + return create(parent, json.object()); +} +// static +Tutorial* Tutorial::create(QObject* parent, const QJsonObject& obj) { QJsonObject conditions = obj["conditions"].toObject(); if (!Guide::evaluateConditions(conditions)) { logger.info() << "Exclude the tutorial because conditions do not match"; @@ -53,66 +57,41 @@ Tutorial* Tutorial::create(QObject* parent, const QString& fileName) { QString tutorialId = obj["id"].toString(); if (tutorialId.isEmpty()) { - logger.warning() << "Empty ID for tutorial file" << fileName; + logger.warning() << "Empty ID for tutorial"; return nullptr; } - L18nStrings* l18nStrings = L18nStrings::instance(); - Q_ASSERT(l18nStrings); - Tutorial* tutorial = new Tutorial(parent); auto guard = qScopeGuard([&] { tutorial->deleteLater(); }); tutorial->m_highlighted = obj["highlighted"].toBool(); - tutorial->m_titleId = - Guide::pascalize(QString("tutorial_%1_title").arg(tutorialId)); - if (!l18nStrings->contains(tutorial->m_titleId)) { - logger.warning() << "No string ID found for the title of tutorial file" - << fileName << "ID:" << tutorial->m_titleId; - return nullptr; - } - - tutorial->m_subtitleId = - Guide::pascalize(QString("tutorial_%1_subtitle").arg(tutorialId)); - if (!l18nStrings->contains(tutorial->m_subtitleId)) { - logger.warning() << "No string ID found for the subtitle of tutorial file" - << fileName << "ID:" << tutorial->m_subtitleId; - return nullptr; - } - - tutorial->m_completionMessageId = Guide::pascalize( - QString("tutorial_%1_completion_message").arg(tutorialId)); - if (!l18nStrings->contains(tutorial->m_completionMessageId)) { - logger.warning() - << "No string ID found for the completion message of tutorial file" - << fileName << "ID:" << tutorial->m_completionMessageId; - return nullptr; - } + tutorial->m_titleId = QString("tutorial.%1.title").arg(tutorialId); + tutorial->m_subtitleId = QString("tutorial.%1.subtitle").arg(tutorialId); + tutorial->m_completionMessageId = + QString("tutorial.%1.completion_message").arg(tutorialId); tutorial->m_image = obj["image"].toString(); if (tutorial->m_image.isEmpty()) { - logger.warning() << "Empty image for tutorial file" << fileName; + logger.warning() << "Empty image for tutorial"; return nullptr; } QJsonValue stepsArray = obj["steps"]; if (!stepsArray.isArray()) { - logger.warning() << "No steps for tutorial file" << fileName; + logger.warning() << "No steps for tutorial"; return nullptr; } for (QJsonValue stepValue : stepsArray.toArray()) { if (!stepValue.isObject()) { - logger.warning() << "Expected JSON objects as steps for tutorial file" - << fileName; + logger.warning() << "Expected JSON objects as steps for tutorial"; return nullptr; } TutorialStep* ts = TutorialStep::create(tutorial, tutorialId, stepValue); if (!ts) { - logger.warning() << "Unable to create a tutorial step for tutorial file" - << fileName; + logger.warning() << "Unable to create a tutorial step for tutorial"; return nullptr; } @@ -159,8 +138,7 @@ bool Tutorial::maybeStop(bool completed) { TutorialModel* tutorialModel = TutorialModel::instance(); Q_ASSERT(tutorialModel); - tutorialModel->requireTutorialCompleted( - this, L18nStrings::instance()->value(m_completionMessageId).toString()); + tutorialModel->requireTutorialCompleted(this, m_completionMessageId); } TutorialModel::instance()->stop(); diff --git a/src/models/tutorial.h b/src/models/tutorial.h index 8097414e38..b6bc5464dc 100644 --- a/src/models/tutorial.h +++ b/src/models/tutorial.h @@ -7,6 +7,7 @@ #include "itempicker.h" +class QJsonObject; class TutorialStep; class Tutorial final : public ItemPicker { @@ -21,6 +22,7 @@ class Tutorial final : public ItemPicker { ~Tutorial(); static Tutorial* create(QObject* parent, const QString& fileName); + static Tutorial* create(QObject* parent, const QJsonObject& obj); void play(const QStringList& allowedItems); void stop(); diff --git a/src/models/tutorialmodel.cpp b/src/models/tutorialmodel.cpp index 1d3a91ee4a..7b2b598526 100644 --- a/src/models/tutorialmodel.cpp +++ b/src/models/tutorialmodel.cpp @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "tutorialmodel.h" +#include "addonmanager.h" #include "tutorial.h" #include "leakdetector.h" #include "logger.h" @@ -19,30 +20,31 @@ Logger logger(LOG_MAIN, "TutorialModel"); TutorialModel* TutorialModel::instance() { if (!s_instance) { s_instance = new TutorialModel(qApp); + + // We need tutorials from the addon manager. + AddonManager::instance(); } return s_instance; } TutorialModel::TutorialModel(QObject* parent) : QAbstractListModel(parent) { + logger.debug() << "create"; MVPN_COUNT_CTOR(TutorialModel); - initialize(); } TutorialModel::~TutorialModel() { MVPN_COUNT_DTOR(TutorialModel); } -void TutorialModel::initialize() { - QDir dir(":/tutorials"); - QStringList files = dir.entryList(); - files.sort(); - for (const QString& file : files) { - if (file.endsWith(".json")) { - Tutorial* tutorial = Tutorial::create(this, dir.filePath(file)); - if (tutorial) { - m_tutorials.append(tutorial); - } - } +bool TutorialModel::createFromJson(const QJsonObject& obj) { + logger.debug() << "Creation from json"; + + Tutorial* tutorial = Tutorial::create(this, obj); + if (tutorial) { + m_tutorials.append(tutorial); + return true; } + + return false; } QHash TutorialModel::roleNames() const { diff --git a/src/models/tutorialmodel.h b/src/models/tutorialmodel.h index 57216fed83..ac3b1fcb29 100644 --- a/src/models/tutorialmodel.h +++ b/src/models/tutorialmodel.h @@ -10,6 +10,7 @@ #include #include +class QJsonObject; class Tutorial; class TutorialModel final : public QAbstractListModel { @@ -42,6 +43,8 @@ class TutorialModel final : public QAbstractListModel { Tutorial* highlightedTutorial() const; + bool createFromJson(const QJsonObject& obj); + // QAbstractListModel methods QHash roleNames() const override; @@ -60,8 +63,6 @@ class TutorialModel final : public QAbstractListModel { private: explicit TutorialModel(QObject* parent); - void initialize(); - QList m_tutorials; Tutorial* m_currentTutorial = nullptr; diff --git a/src/models/tutorialstep.cpp b/src/models/tutorialstep.cpp index fed7653916..fc2268ffae 100644 --- a/src/models/tutorialstep.cpp +++ b/src/models/tutorialstep.cpp @@ -5,7 +5,6 @@ #include "tutorialstep.h" #include "guide.h" #include "inspector/inspectorutils.h" -#include "l18nstrings.h" #include "leakdetector.h" #include "logger.h" #include "tutorial.h" @@ -27,9 +26,6 @@ constexpr int TIMEOUT_ITEM_TIMER_MSEC = 300; // static TutorialStep* TutorialStep::create(Tutorial* parent, const QString& tutorialId, const QJsonValue& json) { - L18nStrings* l18nStrings = L18nStrings::instance(); - Q_ASSERT(l18nStrings); - QJsonObject obj = json.toObject(); QString stepId = obj["id"].toString(); @@ -38,12 +34,7 @@ TutorialStep* TutorialStep::create(Tutorial* parent, const QString& tutorialId, return nullptr; } - stepId = Guide::pascalize( - QString("tutorial_%1_step_%2").arg(tutorialId).arg(stepId)); - if (!l18nStrings->contains(stepId)) { - logger.warning() << "No string ID found for the tutorial step" << stepId; - return nullptr; - } + stepId = QString("tutorial.%1.step.%2").arg(tutorialId).arg(stepId); QString element = obj["element"].toString(); if (element.isEmpty()) { @@ -156,8 +147,8 @@ void TutorialStep::startInternal() { tutorialModel->requireTooltipShown(m_parent, true); tutorialModel->requireTooltipNeeded( - m_parent, L18nStrings::instance()->value(m_stringId).toString(), - QRectF(x, y, item->width(), item->height()), m_element); + m_parent, m_stringId, QRectF(x, y, item->width(), item->height()), + m_element); connect(m_next, &TutorialStepNext::completed, this, &TutorialStep::completed); m_next->start(); diff --git a/src/qmake/platforms/android.pri b/src/qmake/platforms/android.pri index 3d94e24966..7e2939b9eb 100644 --- a/src/qmake/platforms/android.pri +++ b/src/qmake/platforms/android.pri @@ -118,3 +118,8 @@ DISTFILES += \ ../android/res/values/libs.xml ANDROID_PACKAGE_SOURCE_DIR = $$PWD/../../../android + +addons.files = $$PWD/../../../addons/generated/addons +addons.path = /assets +addons.CONFIG = no_check_exist executable +INSTALLS += addons diff --git a/src/qmake/platforms/ios.pri b/src/qmake/platforms/ios.pri index ece39a2c36..b588c32899 100644 --- a/src/qmake/platforms/ios.pri +++ b/src/qmake/platforms/ios.pri @@ -154,3 +154,7 @@ QMAKE_MAC_XCODE_SETTINGS += GROUP_ID_IOS DEVELOPMENT_TEAM.name = "DEVELOPMENT_TEAM" DEVELOPMENT_TEAM.value = "$$MVPN_DEVELOPMENT_TEAM" QMAKE_MAC_XCODE_SETTINGS += DEVELOPMENT_TEAM + +addons.files = $$PWD/../../../addons/generated/addons +addons.CONFIG = no_check_exist executable +QMAKE_BUNDLE_DATA += addons diff --git a/src/qmake/platforms/linux.pri b/src/qmake/platforms/linux.pri index 8303c523d4..65e2c157f5 100644 --- a/src/qmake/platforms/linux.pri +++ b/src/qmake/platforms/linux.pri @@ -172,3 +172,10 @@ INSTALLS += browserBridge CONFIG += link_pkgconfig PKGCONFIG += polkit-gobject-1 + +DEFINES += ADDONS_PATH=\\\"$${USRPATH}/share/mozillavpn/addons\\\" + +addons.files = $$PWD/../../../addons/generated/addons +addons.path = $${USRPATH}/share/mozillavpn +addons.CONFIG = no_check_exist executable +INSTALLS += addons diff --git a/src/qmake/platforms/macos.pri b/src/qmake/platforms/macos.pri index 196f6ec3b1..95a2b82799 100644 --- a/src/qmake/platforms/macos.pri +++ b/src/qmake/platforms/macos.pri @@ -98,6 +98,11 @@ extension_manifest.files = $$PWD/../../../extension/manifests/macos/mozillavpn.j extension_manifest.path = 'Contents/Resources/utils' QMAKE_BUNDLE_DATA += extension_manifest +addons.files = $$PWD/../../../addons/generated/addons +addons.path = 'Contents/Resources' +addons.CONFIG = no_check_exist executable +QMAKE_BUNDLE_DATA += addons + wireguardGo.input = WIREGUARDGO wireguardGo.output = ${QMAKE_FILE_IN}/wireguard-go wireguardGo.commands = @echo Compiling Wireguard GO ${QMAKE_FILE_IN} && \ diff --git a/src/qmake/platforms/wasm.pri b/src/qmake/platforms/wasm.pri index 99f6557c5f..cfe1e344b0 100644 --- a/src/qmake/platforms/wasm.pri +++ b/src/qmake/platforms/wasm.pri @@ -44,3 +44,4 @@ HEADERS += \ SOURCES -= networkrequest.cpp RESOURCES += platforms/wasm/networkrequests.qrc +RESOURCES += ../addons/generated/addons/addons.qrc diff --git a/src/qmake/platforms/windows.pri b/src/qmake/platforms/windows.pri index a3c2e91fa7..9bae07e286 100644 --- a/src/qmake/platforms/windows.pri +++ b/src/qmake/platforms/windows.pri @@ -155,3 +155,8 @@ mozillavpnnp.files = $$PWD/../../../mozillavpnnp.exe mozillavpnnp.path = $$PWD/../../../unsigned/ mozillavpnnp.CONFIG = no_check_exist executable INSTALLS += mozillavpnnp + +addons.files = $$PWD/../../../addons/generated/addons +addons.path = $$PWD/../../../unsigned +addons.CONFIG = no_check_exist executable +INSTALLS += addons diff --git a/src/qmake/sources.pri b/src/qmake/sources.pri index d3174325a9..9881e95cd5 100644 --- a/src/qmake/sources.pri +++ b/src/qmake/sources.pri @@ -295,5 +295,3 @@ RESOURCES += ui/resources.qrc RESOURCES += ui/license.qrc RESOURCES += ui/ui.qrc RESOURCES += resources/certs/certs.qrc -RESOURCES += ui/guides.qrc -RESOURCES += ui/tutorials.qrc diff --git a/src/ui/developerMenu/ViewTutorials.qml b/src/ui/developerMenu/ViewTutorials.qml index bc3203b37b..c8ae0fdbe4 100644 --- a/src/ui/developerMenu/ViewTutorials.qml +++ b/src/ui/developerMenu/ViewTutorials.qml @@ -42,8 +42,8 @@ Item { // I'm too lazy to create a proper view. function showTutorialContent(tutorial) { const list = []; - list.push("Translate title: " + VPNl18n[tutorial.titleId]); - list.push("Translate subtitle: " + VPNl18n[tutorial.subtitleId]); + list.push("Translate title: " + qsTrId(tutorial.titleId)); + list.push("Translate subtitle: " + qsTrId(tutorial.subtitleId)); list.push("Image: " + tutorial.image); list.push("Highlighted: " + tutorial.highlighted + "(" + (tutorial === VPNTutorial.highlightedTutorial ? "true" : "false") + ")"); diff --git a/src/ui/guides/.keepme b/src/ui/guides/.keepme deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/ui/guides/01_how_to_vpn.json b/src/ui/guides/01_how_to_vpn.json deleted file mode 100644 index 58be2d5fe8..0000000000 --- a/src/ui/guides/01_how_to_vpn.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "id": "how_to_vpn", - "title": "The top uses and benefits of using Mozilla VPN", - "subtitle": "Mozilla VPN secures the connection between your device and the web. Here’s some of the most useful features your VPN offers:", - "image": "qrc:/guides/01_how_to_vpn.svg", - "blocks": [ - { "id": "c_1", - "type": "title", - "content": "Safeguard your personal data and identity" }, - { "id": "c_2", - "type": "ulist", - "content": [ - { "id": "l_1", - "content": "Surf the web and use your favorite apps freely without being tracked" }, - { "id": "l_2", - "content": "Keep confidential materials safe via our premium data encryption" } - ] - }, - { "id": "c_3", - "type": "title", - "content": "Defense on the go" }, - { "id": "c_4", - "type": "ulist", - "content": [ - { "id": "l_1", - "content": "Blocks unknown entities from seeing your private data on risky public Wi-Fi and hotspots" }, - { "id": "l_2", - "content": "Protection wherever you are, from restaurants to vacation rentals" } - ] - }, - { "id": "c_5", - "type": "title", - "content": "Connect to 30+ countries" }, - { "id": "c_6", - "type": "ulist", - "content": [ - { "id": "l_1", - "content": "Browse and access content from all over the world" }, - { "id": "l_2", - "content": "Shop on a regional server to get localized deals like cheaper airfare" } - ] - } - ] -} diff --git a/src/ui/guides/02_is_my_vpn_working_correctly.json b/src/ui/guides/02_is_my_vpn_working_correctly.json deleted file mode 100644 index 1da459ba4b..0000000000 --- a/src/ui/guides/02_is_my_vpn_working_correctly.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "id": "is_my_vpn_working_correctly", - "title": "Is my VPN connected to the right server location?", - "subtitle": "Test your geolocation on one of our 400+ worldwide servers in two super simple ways:", - "image": "qrc:/guides/02_is_my_vpn_working_correctly.svg", - "blocks": [ - { "id": "c_1", - "type": "title", - "content": "Search in your browser" }, - { "id": "c_2", - "type": "text", - "content": "Enter a search term like “local weather” in your browser and see a real-time weather update for your current VPN location." }, - { "id": "c_3", - "type": "title", - "content": "Sign in to your favorite app or streaming site" }, - { "id": "c_4", - "type": "text", - "content": "Traveling abroad and need to access content from back home?" }, - { "id": "c_5", - "type": "olist", - "content": [ - { - "id": "l_1", - "content": "Connect to the country server of your choice" }, - { - "id": "l_2", - "content": "Sign on to your favorite app or streaming site" } - ] - } - ] -} diff --git a/src/ui/guides/03_adding_and_removing_devices.json b/src/ui/guides/03_adding_and_removing_devices.json deleted file mode 100644 index 28e3e3a905..0000000000 --- a/src/ui/guides/03_adding_and_removing_devices.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "id": "adding_and_removing_devices", - "title": "How do I add and remove devices?", - "subtitle": "Follow this quick and simple 3 step guide to add and remove all of your devices easily.", - "image": "qrc:/guides/03_adding_and_removing_devices.svg", - "blocks": [ - { "id": "c_1", - "type": "title", - "content": "How to add a device" }, - { "id": "c_2", - "type": "olist", - "content": [ - { "id": "l_1", - "content": "Open the Mozilla VPN app on your preferred device" }, - { "id": "l_2", - "content": "Sign in to your account" }, - { "id": "l_3", - "content": "Voila! Your device has been automatically added to your VPN list of connected devices" } - ] - }, - { "id": "c_3", - "type": "title", - "content": "How to remove a device" }, - { "id": "c_4", - "type": "olist", - "content": [ - { "id": "l_1", - "content": "Open Mozilla VPN in your chosen desktop or mobile device" }, - { "id": "l_2", - "content": "Tap on “My Devices” in your VPN homescreen" }, - { "id": "l_3", - "content": "Select the “Trash” icon next to the device you want to remove from your list of connected devices" } - ] - } - ] -} diff --git a/src/ui/guides/04_connecting_external_devices.json b/src/ui/guides/04_connecting_external_devices.json deleted file mode 100644 index 772fd42c55..0000000000 --- a/src/ui/guides/04_connecting_external_devices.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "id": "connecting_external_devices", - "title": "Connecting to other devices while using VPN", - "subtitle": "Enabling local network access allows you to connect to other devices without sacrificing your VPN protection.", - "image": "qrc:/guides/04_connecting_external_devices.svg", - "blocks": [ - { "id": "c_1", - "type": "title", - "content": "How do I connect to my printer or fax?" }, - { "id": "c_2", - "type": "olist", - "content": [ - { "id": "l_1", - "content": "Enable “Local network access” in your network settings" }, - { "id": "l_2", - "content": "Connect the printer or fax cable to your device’s port (wired) or via wireless" }, - { "id": "l_3", - "content": "Print or fax your documents as normal Desktop computers / laptops / mobile phones" } - ] - }, - { "id": "c_3", - "type": "title", - "content": "What other types of devices can I connect to?" }, - { "id": "c_4", - "type": "text", - "content": "With “Local network access” turned on, you can also connect to devices like:" }, - { "id": "c_5", - "type": "ulist", - "content": [ - { "id": "l_1", - "content": "Other computers and mobile phones" }, - { "id": "l_2", - "content": "Streaming sticks (Chromecast / Roku / Apple AirPlay)" } - ] - } - ] -} diff --git a/src/ui/main.qml b/src/ui/main.qml index 190a7afca2..5bcd6e3bdd 100644 --- a/src/ui/main.qml +++ b/src/ui/main.qml @@ -504,19 +504,19 @@ Window { Connections { target: VPNTutorial - function onTooltipNeeded(text, rect, objectName) { + function onTooltipNeeded(textId, rect, objectName) { console.log("OBJECT NAME:" + objectName); if (tooltip.height + rect.y + rect.height <= window.height - VPNTheme.theme.windowMargin) { tooltip.y = rect.y + rect.height; } else { tooltip.y = rect.y - tooltip.height; } - tooltip.text = text + tooltip.text = qsTrId(textId); tooltip.open(); } function onTutorialCompleted(text) { - console.log("TODO", text); + console.log("TODO", qsTrId(text)); } } diff --git a/src/ui/settings/ViewGuide.qml b/src/ui/settings/ViewGuide.qml index 8dd4dc7d15..42f0b13174 100644 --- a/src/ui/settings/ViewGuide.qml +++ b/src/ui/settings/ViewGuide.qml @@ -127,7 +127,7 @@ Item { Layout.topMargin: VPNTheme.theme.vSpacingSmall Layout.fillWidth: true - text: VPNl18n[guide.titleId] + text: qsTrId(guide.titleId) lineHeightMode: Text.FixedHeight lineHeight: VPNTheme.theme.vSpacing wrapMode: Text.Wrap @@ -138,7 +138,7 @@ Item { Layout.topMargin: VPNTheme.theme.listSpacing Layout.fillWidth: true - text: VPNl18n[guide.subtitleId] + text: qsTrId(guide.subtitleId) font.pixelSize: VPNTheme.theme.fontSizeSmall color: VPNTheme.theme.fontColor horizontalAlignment: Text.AlignLeft @@ -162,7 +162,7 @@ Item { VPNBoldInterLabel { property var guideBlock - text: VPNl18n[guideBlock.id] + text: qstrId(guideBlock.id) font.pixelSize: VPNTheme.theme.fontSize lineHeight: VPNTheme.theme.labelLineHeight verticalAlignment: Text.AlignVCenter @@ -175,7 +175,7 @@ Item { VPNInterLabel { property var guideBlock - text: VPNl18n[guideBlock.id] + text: qsTrId(guideBlock.id) font.pixelSize: VPNTheme.theme.fontSizeSmall color: VPNTheme.theme.fontColor horizontalAlignment: Text.AlignLeft @@ -188,7 +188,7 @@ Item { VPNInterLabel { property var guideBlock property string listType - property var tagsList: guideBlock.subBlockIds.map(subBlockId => `
  • ${VPNl18n[subBlockId]}
  • `) + property var tagsList: guideBlock.subBlockIds.map(subBlockId => `
  • ${qsTrId(subBlockId)}
  • `) text: `<${listType} style='margin-left: -24px;-qt-list-indent:1;'>%1`.arg(tagsList.join("")) textFormat: Text.RichText diff --git a/src/ui/settings/ViewTipsAndTricks.qml b/src/ui/settings/ViewTipsAndTricks.qml index 5dd81a7fba..a2968ab4e7 100644 --- a/src/ui/settings/ViewTipsAndTricks.qml +++ b/src/ui/settings/ViewTipsAndTricks.qml @@ -57,8 +57,8 @@ VPNFlickable { height: parent.height imageSrc: highlightedTutorial.image - title: VPNl18n[highlightedTutorial.titleId] - description: VPNl18n[highlightedTutorial.subtitleId] + title: qsTrId(highlightedTutorial.titleId) + description: qsTrId(highlightedTutorial.subtitleId) } } @@ -124,7 +124,7 @@ VPNFlickable { width: vpnFlickable.width < VPNTheme.theme.tabletMinimumWidth ? (parent.width - parent.spacing) / 2 : (parent.width - (parent.spacing * 2)) / 3 imageSrc: guide.image - title: VPNl18n[guide.titleId] + title: qsTrId(guide.titleId) onClicked: mainStackView.push("qrc:/ui/settings/ViewGuide.qml", {"guide": guide, "imageBgColor": imageBgColor}) } @@ -151,8 +151,8 @@ VPNFlickable { height: VPNTheme.theme.tutorialCardHeight imageSrc: tutorial.image - title: VPNl18n[tutorial.titleId] - description: VPNl18n[tutorial.subtitleId] + title: qsTrId(tutorial.titleId) + description: qsTrId(tutorial.subtitleId) } } } diff --git a/src/ui/tutorials/.keepme b/src/ui/tutorials/.keepme deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/ui/tutorials/01_get_started.json b/src/ui/tutorials/01_get_started.json deleted file mode 100644 index 2e01e485fe..0000000000 --- a/src/ui/tutorials/01_get_started.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "id": "01_get_started", - "highlighted": true, - "image": "qrc:/tutorials/01_get_started.svg", - "title": "Getting started with VPN", - "subtitle": "Follow this walkthrough to learn how to get started with using your VPN.", - "completion_message": "You’ve successfully changed your location, turned the VPN on and off. Would you like to learn more tips and tricks?", - "steps": [ - { - "id": "mainScreen", - "element": "serverListButton", - "tooltip": "Select your location", - "before": [{ - "op": "vpn_location_set", - "exitCountryCode": "at", - "exitCity": "Vienna", - "entryCountryCode": "", - "entryCity": "" - }], - "next": { - "op": "signal", - "qml_emitter": "serverListButton", - "signal": "visibleChanged" - } - }, - { - "id": "countryAU", - "element": "serverCountryList/serverCountry-au", - "tooltip": "Select a different country", - "before": [{ - "op": "property_set", - "element": "serverCountryView", - "property": "contentY", - "value": 0 - }], - "next": { - "op": "signal", - "qml_emitter": "serverCountryList/serverCountry-au", - "signal": "cityListVisibleChanged" - } - }, - { - "id": "cityAU", - "element": "serverCountryList/serverCountry-au/serverCityList/serverCity-Melbourne", - "tooltip": "Select a different server location", - "next": { - "op": "signal", - "qml_emitter": "serverCountryList/serverCountry-au/serverCityList/serverCity-Melbourne", - "signal": "visibleChanged" - } - }, - { - "id": "toggle", - "element": "controllerToggle", - "tooltip": "Toggle this switch to activate or deactivate the VPN", - "next": { - "op": "signal", - "vpn_emitter": "controller", - "signal": "stateChanged" - } - } - ] -} diff --git a/src/ui/tutorials/02_connect_on_startup.json b/src/ui/tutorials/02_connect_on_startup.json deleted file mode 100644 index c77d9d3816..0000000000 --- a/src/ui/tutorials/02_connect_on_startup.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "id": "02_connect_on_startup", - "image": "qrc:/tutorials/02_connect_on_startup.svg", - "title": "Connect VPN on startup", - "subtitle": "Follow this walkthrough to learn how to activate your VPN when you start your device.", - "completion_message": "You’ve successfully learned how to set “Connect VPN on startup”. Would you like to learn more tips and tricks?", - "conditions": { - "enabledFeatures": ["startOnBoot"] - }, - "steps": [ - { - "id": "s1", - "element": "settingsButton", - "tooltip": "Select your account settings", - "next": { - "op": "signal", - "qml_emitter": "settingsButton", - "signal": "visibleChanged" - } - }, - { - "id": "s2", - "element": "settingsPreferences", - "tooltip": "Select your system preferences", - "next": { - "op": "signal", - "qml_emitter": "settingsPreferences", - "signal": "visibleChanged" - } - }, - { - "id": "s3", - "element": "settingStartAtBoot", - "tooltip": "Select or deselect this checkbox", - "next": { - "op": "signal", - "vpn_emitter": "settingsHolder", - "signal": "startAtBootChanged" - } - } - ] -} diff --git a/src/ui/views/ViewAddon.qml b/src/ui/views/ViewAddon.qml index 50071b1c75..a308676a4d 100644 --- a/src/ui/views/ViewAddon.qml +++ b/src/ui/views/ViewAddon.qml @@ -37,13 +37,4 @@ Item { source: "qrc" + parent.addon.qml } - - Connections { - target: VPNAddonManager - function onUnloadAddon(addonId) { - if (addonId === addon.id) { - return mainStackView.pop(addonWrapper, StackView.Immediate); - } - } - } } diff --git a/taskcluster/scripts/build/android_build_debug.sh b/taskcluster/scripts/build/android_build_debug.sh index 7a8e22da2b..dfaddb3d07 100755 --- a/taskcluster/scripts/build/android_build_debug.sh +++ b/taskcluster/scripts/build/android_build_debug.sh @@ -11,6 +11,8 @@ git submodule update ./scripts/utils/generate_glean.py # translations ./scripts/utils/import_languages.py +# addons +./scripts/addon/generate_all.py # $1 should be the qmake arch. # Note this is different from what aqt expects as arch: diff --git a/taskcluster/scripts/build/android_build_release.sh b/taskcluster/scripts/build/android_build_release.sh index d295e2a7c0..b12001d329 100755 --- a/taskcluster/scripts/build/android_build_release.sh +++ b/taskcluster/scripts/build/android_build_release.sh @@ -12,6 +12,9 @@ git submodule update # translations echo "Importing translations" ./scripts/utils/import_languages.py +# addons +echo "Generating addons..." +./scripts/addon/generate_all.py # Get Secrets for building echo "Fetching Tokens!" diff --git a/taskcluster/scripts/build/wasm.sh b/taskcluster/scripts/build/wasm.sh index 98deba2cdc..821be2ac75 100755 --- a/taskcluster/scripts/build/wasm.sh +++ b/taskcluster/scripts/build/wasm.sh @@ -18,6 +18,8 @@ pip3 install -r requirements.txt python3 ./scripts/utils/generate_glean.py # translations python3 ./scripts/utils/import_languages.py +# addons +python3 ./scripts/addon/generate_all.py # Add the Wasm qmake after import languages into the path, # Otherwise import_languages.py will search for lupdate diff --git a/taskcluster/scripts/build/windows.ps1 b/taskcluster/scripts/build/windows.ps1 index d148902725..2ba7ba403d 100644 --- a/taskcluster/scripts/build/windows.ps1 +++ b/taskcluster/scripts/build/windows.ps1 @@ -33,6 +33,7 @@ Copy-Item -Path $env:VCToolsRedistDir\\MergeModules\\Microsoft_VC143_CRT_x86.msm # We need to pre-generate those resources here. python3 ./scripts/utils/generate_glean.py python3 ./scripts/utils/import_languages.py +python3 ./scripts/addon/generate_all.py ./scripts/windows/compile.bat --nmake nmake install diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 59be7c038f..66c85d940b 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -36,6 +36,10 @@ target_link_libraries(unit_tests PRIVATE glean lottie nebula translations) # VPN Client source files target_sources(unit_tests PRIVATE + ${MVPN_SOURCE_DIR}/addon.cpp + ${MVPN_SOURCE_DIR}/addon.h + ${MVPN_SOURCE_DIR}/addonmanager.cpp + ${MVPN_SOURCE_DIR}/addonmanager.h ${MVPN_SOURCE_DIR}/adjust/adjustfiltering.cpp ${MVPN_SOURCE_DIR}/adjust/adjustfiltering.h ${MVPN_SOURCE_DIR}/adjust/adjustproxypackagehandler.cpp diff --git a/tests/unit/testguide.cpp b/tests/unit/testguide.cpp index eded6a9509..a135e72d9a 100644 --- a/tests/unit/testguide.cpp +++ b/tests/unit/testguide.cpp @@ -6,114 +6,88 @@ #include "../../src/models/guide.h" #include "../../src/models/guidemodel.h" #include "../../src/settingsholder.h" -#include "../../translations/generated/l18nstrings.h" #include "helper.h" -void TestGuide::pascalize() { - QCOMPARE(Guide::pascalize(""), ""); - QCOMPARE(Guide::pascalize("a"), "A"); - QCOMPARE(Guide::pascalize("ab"), "Ab"); - QCOMPARE(Guide::pascalize("ab_c"), "AbC"); - QCOMPARE(Guide::pascalize("ab_cd"), "AbCd"); -} - void TestGuide::create_data() { - QTest::addColumn("l18n"); + QTest::addColumn("id"); QTest::addColumn("content"); QTest::addColumn("created"); - QTest::addRow("empty") << QStringList() << QByteArray("") << false; - QTest::addRow("non-object") << QStringList() << QByteArray("[]") << false; - QTest::addRow("object-without-id") - << QStringList() << QByteArray("{}") << false; + QTest::addRow("empty") << "" << QByteArray("") << false; + QTest::addRow("non-object") << "" << QByteArray("[]") << false; + QTest::addRow("object-without-id") << "" << QByteArray("{}") << false; QJsonObject obj; obj["id"] = "foo"; - QTest::addRow("invalid-id") - << QStringList() << QJsonDocument(obj).toJson() << false; - QTest::addRow("no-image") << QStringList{"GuideFooTitle", "GuideFooSubtitle"} - << QJsonDocument(obj).toJson() << false; + QTest::addRow("no-image") << "foo" << QJsonDocument(obj).toJson() << false; obj["image"] = "foo.png"; - QTest::addRow("no-blocks") << QStringList{"GuideFooTitle", "GuideFooSubtitle"} - << QJsonDocument(obj).toJson() << false; + QTest::addRow("no-blocks") << "foo" << QJsonDocument(obj).toJson() << false; QJsonArray blocks; obj["blocks"] = blocks; - QTest::addRow("with-blocks") - << QStringList{"GuideFooTitle", "GuideFooSubtitle"} - << QJsonDocument(obj).toJson() << true; + QTest::addRow("with-blocks") << "foo" << QJsonDocument(obj).toJson() << true; blocks.append(""); obj["blocks"] = blocks; QTest::addRow("with-invalid-block") - << QStringList{"GuideFooTitle", "GuideFooSubtitle"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; QJsonObject block; blocks.replace(0, block); obj["blocks"] = blocks; QTest::addRow("with-block-without-id") - << QStringList{"GuideFooTitle", "GuideFooSubtitle"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; block["id"] = "A"; blocks.replace(0, block); obj["blocks"] = blocks; QTest::addRow("with-block-without-type") - << QStringList{"GuideFooTitle", "GuideFooSubtitle"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; block["type"] = "wow"; blocks.replace(0, block); obj["blocks"] = blocks; QTest::addRow("with-block-with-invalid-type") - << QStringList{"GuideFooTitle", "GuideFooSubtitle"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; block["type"] = "title"; blocks.replace(0, block); obj["blocks"] = blocks; QTest::addRow("with-block-type-title") - << QStringList{"GuideFooTitle", "GuideFooSubtitle", "GuideFooBlockA"} - << QJsonDocument(obj).toJson() << true; + << "foo" << QJsonDocument(obj).toJson() << true; block["type"] = "text"; blocks.replace(0, block); obj["blocks"] = blocks; QTest::addRow("with-block-type-text") - << QStringList{"GuideFooTitle", "GuideFooSubtitle", "GuideFooBlockA"} - << QJsonDocument(obj).toJson() << true; + << "foo" << QJsonDocument(obj).toJson() << true; block["type"] = "olist"; blocks.replace(0, block); obj["blocks"] = blocks; QTest::addRow("with-block-type-olist-without-content") - << QStringList{"GuideFooTitle", "GuideFooSubtitle", "GuideFooBlockA"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; block["content"] = "foo"; blocks.replace(0, block); obj["blocks"] = blocks; QTest::addRow("with-block-type-olist-with-invalid-content") - << QStringList{"GuideFooTitle", "GuideFooSubtitle", "GuideFooBlockA"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; QJsonArray content; block["content"] = content; blocks.replace(0, block); obj["blocks"] = blocks; QTest::addRow("with-block-type-olist-with-empty-content") - << QStringList{"GuideFooTitle", "GuideFooSubtitle", "GuideFooBlockA"} - << QJsonDocument(obj).toJson() << true; + << "foo" << QJsonDocument(obj).toJson() << true; content.append("foo"); block["content"] = content; blocks.replace(0, block); obj["blocks"] = blocks; QTest::addRow("with-block-type-olist-with-invalid-content") - << QStringList{"GuideFooTitle", "GuideFooSubtitle", "GuideFooBlockA"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; QJsonObject subBlock; content.replace(0, subBlock); @@ -121,8 +95,7 @@ void TestGuide::create_data() { blocks.replace(0, block); obj["blocks"] = blocks; QTest::addRow("with-block-type-olist-without-id-subblock") - << QStringList{"GuideFooTitle", "GuideFooSubtitle", "GuideFooBlockA"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; subBlock["id"] = "sub"; content.replace(0, subBlock); @@ -130,36 +103,24 @@ void TestGuide::create_data() { blocks.replace(0, block); obj["blocks"] = blocks; QTest::addRow("with-block-type-olist-with-subblock") - << QStringList{"GuideFooTitle", "GuideFooSubtitle", "GuideFooBlockA", - "GuideFooBlockASub"} - << QJsonDocument(obj).toJson() << true; + << "foo" << QJsonDocument(obj).toJson() << true; obj["conditions"] = QJsonObject(); QTest::addRow("with-block-type-olist-with-subblock and conditions") - << QStringList{"GuideFooTitle", "GuideFooSubtitle", "GuideFooBlockA", - "GuideFooBlockASub"} - << QJsonDocument(obj).toJson() << true; + << "foo" << QJsonDocument(obj).toJson() << true; block["type"] = "ulist"; blocks.replace(0, block); obj["conditions"] = QJsonObject(); QTest::addRow("with-block-type-ulist-with-subblock and conditions") - << QStringList{"GuideFooTitle", "GuideFooSubtitle", "GuideFooBlockA", - "GuideFooBlockASub"} - << QJsonDocument(obj).toJson() << true; + << "foo" << QJsonDocument(obj).toJson() << true; } void TestGuide::create() { - QFETCH(QStringList, l18n); + QFETCH(QString, id); QFETCH(QByteArray, content); QFETCH(bool, created); - L18nStrings* l18nStrings = L18nStrings::instance(); - QVERIFY(!!l18nStrings); - for (const QString& s : l18n) { - l18nStrings->insert(s, "WOW!"); - } - QTemporaryFile file; QVERIFY(file.open()); QCOMPARE(file.write(content.data(), content.length()), content.length()); @@ -173,9 +134,9 @@ void TestGuide::create() { } QString guideTitleId = guide->property("titleId").toString(); - QVERIFY(l18nStrings->contains(guideTitleId)); + QCOMPARE(guideTitleId, QString("guide.%1.title").arg(id)); QString guideSubTitleId = guide->property("subtitleId").toString(); - QVERIFY(l18nStrings->contains(guideSubTitleId)); + QCOMPARE(guideSubTitleId, QString("guide.%1.subtitle").arg(id)); QCOMPARE(guide->property("image").toString(), "foo.png"); @@ -189,15 +150,7 @@ void TestGuide::createNotExisting() { } void TestGuide::model() { - L18nStrings* l18nStrings = L18nStrings::instance(); - QVERIFY(!!l18nStrings); - for (const QString& s : QStringList{ - "GuideDemoTitle", "GuideDemoSubtitle", "GuideDemoBlockC1", - "GuideDemoBlockC2", "GuideDemoBlockC3L1", "GuideDemoBlockC3L2", - "GuideDemoBlockC3L3", "GuideDemoBlockC4L1", "GuideDemoBlockC4L2", - "GuideDemoBlockC4L3"}) { - l18nStrings->insert(s, "WOW!"); - } + SettingsHolder settingsHolder; GuideModel* mg = GuideModel::instance(); QVERIFY(!!mg); @@ -206,6 +159,14 @@ void TestGuide::model() { QCOMPARE(rn.count(), 1); QCOMPARE(rn[GuideModel::GuideRole], "guide"); + QCOMPARE(mg->rowCount(QModelIndex()), 0); + + QFile guideFile(":/guides/01_demo.json"); + QVERIFY(guideFile.open(QIODevice::ReadOnly | QIODevice::Text)); + QJsonDocument json = QJsonDocument::fromJson(guideFile.readAll()); + QVERIFY(json.isObject()); + mg->createFromJson(json.object()); + QCOMPARE(mg->rowCount(QModelIndex()), 1); QCOMPARE(mg->data(QModelIndex(), GuideModel::GuideRole), QVariant()); diff --git a/tests/unit/testguide.h b/tests/unit/testguide.h index 6dd94ed9c1..d08edb08bc 100644 --- a/tests/unit/testguide.h +++ b/tests/unit/testguide.h @@ -8,8 +8,6 @@ class TestGuide final : public TestHelper { Q_OBJECT private slots: - void pascalize(); - void create_data(); void create(); void createNotExisting(); diff --git a/tests/unit/testtutorial.cpp b/tests/unit/testtutorial.cpp index a8ecec15c6..7618427b06 100644 --- a/tests/unit/testtutorial.cpp +++ b/tests/unit/testtutorial.cpp @@ -7,15 +7,9 @@ #include "../../src/models/tutorialmodel.h" #include "../../src/qmlengineholder.h" #include "../../src/settingsholder.h" -#include "../../translations/generated/l18nstrings.h" void TestTutorial::model() { - L18nStrings* l18nStrings = L18nStrings::instance(); - l18nStrings->insert("TutorialDemoTitle", "title"); - l18nStrings->insert("TutorialDemoSubtitle", "subtitle"); - l18nStrings->insert("TutorialDemoCompletionMessage", "completion_message"); - l18nStrings->insert("TutorialDemoStepS1", "wow1"); - l18nStrings->insert("TutorialDemoStepS2", "wow2"); + SettingsHolder settingsHolder; TutorialModel* mg = TutorialModel::instance(); QVERIFY(!!mg); @@ -24,6 +18,14 @@ void TestTutorial::model() { QCOMPARE(rn.count(), 1); QCOMPARE(rn[TutorialModel::TutorialRole], "tutorial"); + QCOMPARE(mg->rowCount(QModelIndex()), 0); + + QFile tutorialFile(":/tutorials/01_demo.json"); + QVERIFY(tutorialFile.open(QIODevice::ReadOnly | QIODevice::Text)); + QJsonDocument json = QJsonDocument::fromJson(tutorialFile.readAll()); + QVERIFY(json.isObject()); + mg->createFromJson(json.object()); + QCOMPARE(mg->rowCount(QModelIndex()), 1); QCOMPARE(mg->data(QModelIndex(), TutorialModel::TutorialRole), QVariant()); @@ -33,75 +35,54 @@ void TestTutorial::model() { } void TestTutorial::create_data() { - QTest::addColumn("l18n"); + QTest::addColumn("id"); QTest::addColumn("content"); QTest::addColumn("created"); - QTest::addRow("empty") << QStringList() << QByteArray("") << false; - QTest::addRow("non-object") << QStringList() << QByteArray("[]") << false; - QTest::addRow("object-without-id") - << QStringList() << QByteArray("{}") << false; + QTest::addRow("empty") << "" << QByteArray("") << false; + QTest::addRow("non-object") << "" << QByteArray("[]") << false; + QTest::addRow("object-without-id") << "" << QByteArray("{}") << false; QJsonObject obj; obj["id"] = "foo"; - QTest::addRow("invalid-id") - << QStringList() << QJsonDocument(obj).toJson() << false; - QTest::addRow("no-image") - << QStringList{"TutorialFooTitle", "TutorialFooSubtitle", - "TutorialFooCompletionMessage"} - << QJsonDocument(obj).toJson() << false; + QTest::addRow("invalid-id") << "foo" << QJsonDocument(obj).toJson() << false; + QTest::addRow("no-image") << "foo" << QJsonDocument(obj).toJson() << false; obj["image"] = "foo.png"; - QTest::addRow("no-steps") - << QStringList{"TutorialFooTitle", "TutorialFooSubtitle", - "TutorialFooCompletionMessage"} - << QJsonDocument(obj).toJson() << false; + QTest::addRow("no-steps") << "foo" << QJsonDocument(obj).toJson() << false; QJsonArray steps; obj["steps"] = steps; - QTest::addRow("with-steps") - << QStringList{"TutorialFooTitle", "TutorialFooSubtitle", - "TutorialFooCompletionMessage"} - << QJsonDocument(obj).toJson() << false; + QTest::addRow("with-steps") << "foo" << QJsonDocument(obj).toJson() << false; steps.append(""); obj["steps"] = steps; QTest::addRow("with-invalid-step") - << QStringList{"TutorialFooTitle", "TutorialFooSubtitle", - "TutorialFooCompletionMessage"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; QJsonObject step; steps.replace(0, step); obj["steps"] = steps; QTest::addRow("with-step-without-id") - << QStringList{"TutorialFooTitle", "TutorialFooSubtitle", - "TutorialFooCompletionMessage"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; step["id"] = "s1"; steps.replace(0, step); obj["steps"] = steps; QTest::addRow("with-step-without-element") - << QStringList{"TutorialFooTitle", "TutorialFooSubtitle", - "TutorialFooCompletionMessage"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; step["element"] = "wow"; steps.replace(0, step); obj["steps"] = steps; QTest::addRow("with-step-without-next") - << QStringList{"TutorialFooTitle", "TutorialFooSubtitle", - "TutorialFooCompletionMessage", "TutorialFooStepS1"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; step["next"] = "wow"; steps.replace(0, step); obj["steps"] = steps; QTest::addRow("with-step-with-invalid-next") - << QStringList{"TutorialFooTitle", "TutorialFooSubtitle", - "TutorialFooCompletionMessage", "TutorialFooStepS1"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; QJsonObject nextObj; @@ -109,92 +90,69 @@ void TestTutorial::create_data() { steps.replace(0, step); obj["steps"] = steps; QTest::addRow("with-step-with-invalid-next-1") - << QStringList{"TutorialFooTitle", "TutorialFooSubtitle", - "TutorialFooCompletionMessage", "TutorialFooStepS1"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; nextObj["op"] = "wow"; step["next"] = nextObj; steps.replace(0, step); obj["steps"] = steps; QTest::addRow("with-step-with-invalid-next-2") - << QStringList{"TutorialFooTitle", "TutorialFooSubtitle", - "TutorialFooCompletionMessage", "TutorialFooStepS1"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; nextObj["op"] = "signal"; step["next"] = nextObj; steps.replace(0, step); obj["steps"] = steps; QTest::addRow("with-step-with-invalid-next-3") - << QStringList{"TutorialFooTitle", "TutorialFooSubtitle", - "TutorialFooCompletionMessage", "TutorialFooStepS1"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; nextObj["signal"] = "a"; step["next"] = nextObj; steps.replace(0, step); obj["steps"] = steps; QTest::addRow("with-step-with-invalid-next-4") - << QStringList{"TutorialFooTitle", "TutorialFooSubtitle", - "TutorialFooCompletionMessage", "TutorialFooStepS1"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; nextObj["qml_emitter"] = "a"; step["next"] = nextObj; steps.replace(0, step); obj["steps"] = steps; QTest::addRow("with-step-with-invalid-next-5") - << QStringList{"TutorialFooTitle", "TutorialFooSubtitle", - "TutorialFooCompletionMessage", "TutorialFooStepS1"} - << QJsonDocument(obj).toJson() << true; + << "foo" << QJsonDocument(obj).toJson() << true; nextObj["vpn_emitter"] = "a"; step["next"] = nextObj; steps.replace(0, step); obj["steps"] = steps; QTest::addRow("with-step-with-invalid-next-6") - << QStringList{"TutorialFooTitle", "TutorialFooSubtitle", - "TutorialFooCompletionMessage", "TutorialFooStepS1"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; nextObj.remove("qml_emitter"); step["next"] = nextObj; steps.replace(0, step); obj["steps"] = steps; QTest::addRow("with-step-with-invalid-next-7") - << QStringList{"TutorialFooTitle", "TutorialFooSubtitle", - "TutorialFooCompletionMessage", "TutorialFooStepS1"} - << QJsonDocument(obj).toJson() << false; + << "foo" << QJsonDocument(obj).toJson() << false; nextObj["vpn_emitter"] = "settingsHolder"; step["next"] = nextObj; steps.replace(0, step); obj["steps"] = steps; QTest::addRow("with-step-with-invalid-next-8") - << QStringList{"TutorialFooTitle", "TutorialFooSubtitle", - "TutorialFooCompletionMessage", "TutorialFooStepS1"} - << QJsonDocument(obj).toJson() << true; + << "foo" << QJsonDocument(obj).toJson() << true; obj["conditions"] = QJsonObject(); QTest::addRow("with-step-element and conditions") - << QStringList{"GuideFooTitle", "GuideFooBlockA", "GuideFooBlockASub"} - << QJsonDocument(obj).toJson() << true; + << "foo" << QJsonDocument(obj).toJson() << true; } void TestTutorial::create() { - QFETCH(QStringList, l18n); + QFETCH(QString, id); QFETCH(QByteArray, content); QFETCH(bool, created); SettingsHolder settingsHolder; - L18nStrings* l18nStrings = L18nStrings::instance(); - QVERIFY(!!l18nStrings); - for (const QString& s : l18n) { - l18nStrings->insert(s, "WOW!"); - } - QTemporaryFile file; QVERIFY(file.open()); QCOMPARE(file.write(content.data(), content.length()), content.length()); @@ -212,14 +170,15 @@ void TestTutorial::create() { QVERIFY(!tm->isPlaying()); QString tutorialTitleId = tutorial->property("titleId").toString(); - QVERIFY(l18nStrings->contains(tutorialTitleId)); + QCOMPARE(tutorialTitleId, QString("tutorial.%1.title").arg(id)); QString tutorialSubtitleId = tutorial->property("subtitleId").toString(); - QVERIFY(l18nStrings->contains(tutorialSubtitleId)); + QCOMPARE(tutorialSubtitleId, QString("tutorial.%1.subtitle").arg(id)); QString tutorialCompletionMessageId = tutorial->property("completionMessageId").toString(); - QVERIFY(l18nStrings->contains(tutorialCompletionMessageId)); + QCOMPARE(tutorialCompletionMessageId, + QString("tutorial.%1.completion_message").arg(id)); QCOMPARE(tutorial->property("image").toString(), "foo.png"); diff --git a/tests/unit/unit.pro b/tests/unit/unit.pro index d54d8b7484..cd754edf9b 100644 --- a/tests/unit/unit.pro +++ b/tests/unit/unit.pro @@ -42,6 +42,8 @@ include($$PWD/../../translations/translations.pri) RESOURCES ~= 's/.*servers.qrc//g' HEADERS += \ + ../../src/addon.h \ + ../../src/addonmanager.h \ ../../src/adjust/adjustfiltering.h \ ../../src/adjust/adjustproxypackagehandler.h \ ../../src/captiveportal/captiveportal.h \ @@ -149,6 +151,8 @@ HEADERS += \ testwebsockethandler.h SOURCES += \ + ../../src/addon.cpp \ + ../../src/addonmanager.cpp \ ../../src/adjust/adjustfiltering.cpp \ ../../src/adjust/adjustproxypackagehandler.cpp \ ../../src/captiveportal/captiveportal.cpp \ diff --git a/translations/CMakeLists.txt b/translations/CMakeLists.txt index fb349ebf72..61f8a82e60 100644 --- a/translations/CMakeLists.txt +++ b/translations/CMakeLists.txt @@ -25,8 +25,6 @@ add_custom_command( OUTPUT ${GENERATED_DIR}/l18nstrings_p.cpp ${GENERATED_DIR}/l18nstrings.h MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/strings.yaml COMMAND python3 ${MVPN_SCRIPT_DIR}/utils/generate_strings.py -o ${GENERATED_DIR} - -g ${CMAKE_SOURCE_DIR}/src/ui/guides - -t ${CMAKE_SOURCE_DIR}/src/ui/tutorials ${CMAKE_CURRENT_SOURCE_DIR}/strings.yaml ) diff --git a/translations/translations.pri b/translations/translations.pri index 55efcc9644..f8e28a3e27 100644 --- a/translations/translations.pri +++ b/translations/translations.pri @@ -23,14 +23,10 @@ STRING_SOURCES = $$PWD/strings.yaml HEADERS += $$PWD/generated/l18nstrings.h ## Perform string generation -## FIXME: This also depends vaguely on the contents of the guide and tutorials. -## TODO: Make translations for the guides and tutorials loaded dynamically. stringgen.input = STRING_SOURCES stringgen.output = $$PWD/generated/l18nstrings.h stringgen.commands = @echo Generating strings from ${QMAKE_FILE_IN} \ && python3 $$PWD/../scripts/utils/generate_strings.py \ - -g ${QMAKE_FILE_IN_PATH}/../src/ui/guides \ - -t ${QMAKE_FILE_IN_PATH}/../src/ui/tutorials \ -o ${QMAKE_FILE_OUT_PATH} ${QMAKE_FILE_IN} stringgen.depends += ${QMAKE_FILE_IN} stringgen.variable_out = HEADERS diff --git a/windows/installer/CMakeLists.txt b/windows/installer/CMakeLists.txt index 11b536b88d..6993f02333 100644 --- a/windows/installer/CMakeLists.txt +++ b/windows/installer/CMakeLists.txt @@ -20,8 +20,9 @@ add_custom_target(msi WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/staging COMMAND ${CMAKE_COMMAND} -E echo "Building MSI installer for $" COMMAND ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR} --prefix ${CMAKE_CURRENT_BINARY_DIR}/staging --config $ + COMMAND ${WIX_BINARY_DIR}/heat dir addons -o ${CMAKE_CURRENT_BINARY_DIR}/addons.wxs -scon -sfrag -srd -sreg -gg -cg addons -dr MozillaVPNAddonFolder COMMAND ${WIX_BINARY_DIR}/candle ${WIX_CANDLE_FLAGS} -dPlatform=${WIX_PLATFORM} - -out ${CMAKE_CURRENT_BINARY_DIR}/MozillaVPN.wixobj -arch x64 ${CMAKE_CURRENT_SOURCE_DIR}/MozillaVPN_cmake.wxs - COMMAND ${WIX_BINARY_DIR}/light ${WIX_LIGHT_FLAGS} -out ${CMAKE_CURRENT_BINARY_DIR}/MozillaVPN.msi ${CMAKE_CURRENT_BINARY_DIR}/MozillaVPN.wixobj + -arch x64 ${CMAKE_CURRENT_SOURCE_DIR}/MozillaVPN_cmake.wxs ${CMAKE_CURRENT_BINARY_DIR}/addons.wxs + COMMAND ${WIX_BINARY_DIR}/light ${WIX_LIGHT_FLAGS} -b addons -out ${CMAKE_CURRENT_BINARY_DIR}/MozillaVPN.msi MozillaVPN_cmake.wixobj addons.wixobj ) set_directory_properties(PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES ${CMAKE_CURRENT_BINARY_DIR}/staging) diff --git a/windows/installer/MozillaVPN.wxs b/windows/installer/MozillaVPN.wxs index f385025dcd..0955d754b2 100644 --- a/windows/installer/MozillaVPN.wxs +++ b/windows/installer/MozillaVPN.wxs @@ -62,7 +62,9 @@ - + + + @@ -76,8 +78,8 @@ - - + + @@ -144,6 +146,7 @@ --> + diff --git a/windows/installer/MozillaVPN_cmake.wxs b/windows/installer/MozillaVPN_cmake.wxs index 645c7811f6..5e06b6dad7 100644 --- a/windows/installer/MozillaVPN_cmake.wxs +++ b/windows/installer/MozillaVPN_cmake.wxs @@ -58,7 +58,9 @@ - + + + @@ -135,6 +137,7 @@ --> + diff --git a/windows/installer/MozillaVPN_prod.wxs b/windows/installer/MozillaVPN_prod.wxs index 17db8c66e6..6d0b1b65ba 100644 --- a/windows/installer/MozillaVPN_prod.wxs +++ b/windows/installer/MozillaVPN_prod.wxs @@ -63,7 +63,9 @@ - + + + @@ -143,6 +145,7 @@ --> + diff --git a/windows/installer/build.cmd b/windows/installer/build.cmd index 285ba54ac0..fed2c28a45 100644 --- a/windows/installer/build.cmd +++ b/windows/installer/build.cmd @@ -43,9 +43,10 @@ if exist .deps\prepared goto :build :msi if not exist "%~1" mkdir "%~1" echo [+] Compiling %1 - "%WIX%bin\candle" %WIX_CANDLE_FLAGS% -dPlatform=%1 -out "%~1\MozillaVPN.wixobj" -arch %1 MozillaVPN.wxs || exit /b %errorlevel% + "%WIX%bin\heat" dir ..\..\addons\generated\addons -o addon.wxs -scon -sfrag -srd -sreg -gg -cg addons -dr MozillaVPNAddonFolder || exit /b %errorlevel% + "%WIX%bin\candle" %WIX_CANDLE_FLAGS% -dPlatform=%1 -arch %1 MozillaVPN.wxs addon.wxs || exit /b %errorlevel% echo [+] Linking %1 - "%WIX%bin\light" %WIX_LIGHT_FLAGS% -out "%~1/MozillaVPN.msi" "%~1\MozillaVPN.wixobj" || exit /b %errorlevel% + "%WIX%bin\light" %WIX_LIGHT_FLAGS% -out "%~1/MozillaVPN.msi" -b ..\..\addons\generated\addons MozillaVPN.wixobj addon.wixobj || exit /b %errorlevel% goto :eof :error diff --git a/windows/installer/build_prod.cmd b/windows/installer/build_prod.cmd index d0443d4222..906b75b96b 100644 --- a/windows/installer/build_prod.cmd +++ b/windows/installer/build_prod.cmd @@ -43,9 +43,10 @@ if exist .deps\prepared goto :build :msi if not exist "%~1" mkdir "%~1" echo [+] Compiling %1 - "%WIX%bin\candle" %WIX_CANDLE_FLAGS% -dPlatform=%1 -out "%~1\MozillaVPN.wixobj" -arch %1 MozillaVPN_prod.wxs || exit /b %errorlevel% + "%WIX%bin\heat" dir ..\..\unsigned\addons -o addon.wxs -scon -sfrag -srd -sreg -gg -cg addons -dr MozillaVPNAddonFolder || exit /b %errorlevel% + "%WIX%bin\candle" %WIX_CANDLE_FLAGS% -dPlatform=%1 -arch %1 MozillaVPN.wxs addon.wxs || exit /b %errorlevel% echo [+] Linking %1 - "%WIX%bin\light" %WIX_LIGHT_FLAGS% -out "%~1/MozillaVPN.msi" "%~1\MozillaVPN.wixobj" || exit /b %errorlevel% + "%WIX%bin\light" %WIX_LIGHT_FLAGS% -out "%~1/MozillaVPN.msi" -b ..\..\unsigned\addons MozillaVPN.wixobj addon.wixobj || exit /b %errorlevel% goto :eof :error From cadccacf756ed50c0b33c702ea7a85f61f385171 Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Sun, 5 Jun 2022 15:58:25 +0200 Subject: [PATCH 02/20] Comment applied --- scripts/addon/build.py | 26 ++++++++++++-------------- scripts/addon/generate_all.py | 8 ++++---- scripts/utils/generate_strings.py | 14 ++++++-------- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/scripts/addon/build.py b/scripts/addon/build.py index 01eb1cb4ea..2eb4fbc25d 100755 --- a/scripts/addon/build.py +++ b/scripts/addon/build.py @@ -6,17 +6,16 @@ import argparse import json import os -from pathlib import Path import xml.etree.ElementTree as ET import tempfile import shutil import sys comment_types = { - "text": f"Standard text in a guide block", - "title": f"Title in a guide block", - "ulist": f"Bullet unordered list item in a guide block", - "olist": f"Bullet ordered list item in a guide block", + "text": "Standard text in a guide block", + "title": "Title in a guide block", + "ulist": "Bullet unordered list item in a guide block", + "olist": "Bullet ordered list item in a guide block", } @@ -24,15 +23,15 @@ def retrieve_strings_tutorial(manifest, filename): tutorial_strings = {} tutorial_json = manifest["tutorial"] - if not "id" in tutorial_json: + if "id" not in tutorial_json: exit(f"Tutorial {filename} does not have an id") - if not "title" in tutorial_json: + if "title" not in tutorial_json: exit(f"Tutorial {filename} does not have a title") - if not "subtitle" in tutorial_json: + if "subtitle" not in tutorial_json: exit(f"Tutorial {filename} does not have a subtitle") - if not "completion_message" in tutorial_json: + if "completion_message" not in tutorial_json: exit(f"Tutorial {filename} does not have a completion message") - if not "steps" in tutorial_json: + if "steps" not in tutorial_json: exit(f"Tutorial {filename} does not have steps") tutorial_id = tutorial_json["id"] @@ -180,12 +179,11 @@ def copy_files(path, dest_path): if file.startswith("."): continue - for ext in ["qrc", "ts", "qm", "rcc"]: - if file.endswith(f".{ext}"): - exit(f"Unexpected {ext} file found: {os.path.join(path, file)}") - file_path = os.path.join(path, file) if os.path.isfile(file_path): + if file_path.endswith((".ts", ".qrc", ".rcc")): + exit(f"Unexpected {ext} file found: {os.path.join(path, file)}") + shutil.copyfile(file_path, os.path.join(dest_path, file)) continue diff --git a/scripts/addon/generate_all.py b/scripts/addon/generate_all.py index 1ef4ba3f64..e88dc05b49 100755 --- a/scripts/addon/generate_all.py +++ b/scripts/addon/generate_all.py @@ -36,8 +36,8 @@ continue addon_path = os.path.join(addons_path, file, "manifest.json") - build_args = [sys.executable, build_path, addon_path, generated_path] + build_cmd = [sys.executable, build_path, addon_path, generated_path] if args.qtpath: - build_args.append("-q") - build_args.append(args.qtpath) - subprocess.call(build_args) + build_cmd.append("-q") + build_cmd.append(args.qtpath) + subprocess.call(build_cmd) diff --git a/scripts/utils/generate_strings.py b/scripts/utils/generate_strings.py index eb8e076bf5..242734b3d9 100755 --- a/scripts/utils/generate_strings.py +++ b/scripts/utils/generate_strings.py @@ -3,10 +3,8 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -import re import os import yaml -import json import argparse def stop(string_id): @@ -113,7 +111,7 @@ def parseTranslationStrings(yamlfile): return yaml_strings -## Render a dictionary of strings into the l18nstrings module. +# Render a dictionary of strings into the l18nstrings module. def generateStrings(strings, outdir): os.makedirs(outdir, exist_ok=True) with open(os.path.join(outdir, "l18nstrings.h"), "w", encoding="utf-8") as output: @@ -211,7 +209,7 @@ def serialize(string): if __name__ == "__main__": - ## Parse arguments to locate the input and output files. + # Parse arguments to locate the input and output files. parser = argparse.ArgumentParser( description='Generate internationaliation strings database from a YAML source') parser.add_argument('source', metavar='SOURCE', type=str, action='store', nargs='?', @@ -220,17 +218,17 @@ def serialize(string): help='Output directory for generated files') args = parser.parse_args() - ## If no source was provided, find it relative to this script file. + # If no source was provided, find it relative to this script file. if args.source is None: rootpath = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) args.source = os.path.join('translations', 'strings.yaml') - ## If no output directory was provided, use the current directory. + # If no output directory was provided, use the current directory. if args.output is None: args.output = os.getcwd() - ## Parse the inputs for their sweet juicy strings. + # Parse the inputs for their sweet juicy strings. strings = parseTranslationStrings(args.source) - ## Render the strings into generated content. + # Render the strings into generated content. generateStrings(strings, args.output) From f0a144a133d8949e38762f771966466dc967117d Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Sun, 5 Jun 2022 16:26:30 +0200 Subject: [PATCH 03/20] Removed -g and -t params from generate_ts.sh --- scripts/utils/generate_ts.sh | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/scripts/utils/generate_ts.sh b/scripts/utils/generate_ts.sh index 1fefbd1f33..dfb89fe2cc 100755 --- a/scripts/utils/generate_ts.sh +++ b/scripts/utils/generate_ts.sh @@ -22,7 +22,7 @@ cp scripts/utils/generate_strings.py cache || die print G "done." printn Y "Generating strings... " -python cache/generate_strings.py -o translations/generated -g src/ui/guides -t src/ui/tutorials +python cache/generate_strings.py -o translations/generated print G "done." printn Y "Generating a dummy PRO file... " @@ -47,10 +47,7 @@ for branch in $(git branch -r | grep origin/releases); do printn Y "Importing strings from $branch..." git checkout $branch &>/dev/null || die - PARAMS= - [ -d src/ui/guides ] && PARAMS="$PARAMS -g src/ui/guides" - [ -d src/ui/tutorials ] && PARAMS="$PARAMS -t src/ui/tutorials" - python cache/generate_strings.py -o translations/generated $PARAMS || die + python cache/generate_strings.py -o translations/generated || die lupdate translations/generated/dummy.pro -ts branch.ts || die lconvert -i translations.ts branch.ts -o tmp.ts || die mv tmp.ts translations.ts || die From c978b7d5a4b4ea004f8834b7d5211a863300d10a Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Sun, 5 Jun 2022 17:47:43 +0200 Subject: [PATCH 04/20] No 'name' var assigned --- scripts/addon/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/addon/build.py b/scripts/addon/build.py index 2eb4fbc25d..d2cbae638c 100755 --- a/scripts/addon/build.py +++ b/scripts/addon/build.py @@ -154,7 +154,7 @@ def write_en_language(filename, strings): ts.set("language", "en") context = ET.SubElement(ts, "context") - name = ET.SubElement(context, "name") + ET.SubElement(context, "name") for key, value in strings.items(): message = ET.SubElement(context, "message") From 5c779ba41705b8693137338acc7f618999956b05 Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Mon, 6 Jun 2022 12:31:48 +0200 Subject: [PATCH 05/20] Load addons from manifest files --- src/addon.cpp | 22 +++++++--- src/addon.h | 10 ++--- src/addonmanager.cpp | 66 +++++++++++++++++++----------- src/addonmanager.h | 6 ++- src/inspector/inspectorhandler.cpp | 18 +++++++- src/ui/views/ViewAddon.qml | 2 +- 6 files changed, 86 insertions(+), 38 deletions(-) diff --git a/src/addon.cpp b/src/addon.cpp index 432fc11b45..e74f670d9a 100644 --- a/src/addon.cpp +++ b/src/addon.cpp @@ -8,16 +8,19 @@ #include "settingsholder.h" #include +#include +#include namespace { Logger logger(LOG_MAIN, "Addon"); } -Addon::Addon(QObject* parent, AddonType addonType, const QString& fileName, - const QString& id, const QString& name, const QString& qml) +Addon::Addon(QObject* parent, AddonType addonType, + const QString& manifestFileName, const QString& id, + const QString& name, const QString& qml) : QObject(parent), m_addonType(addonType), - m_fileName(fileName), + m_manifestFileName(manifestFileName), m_id(id), m_name(name), m_qml(qml) { @@ -43,8 +46,17 @@ void Addon::retranslate() { locale = QLocale(QLocale::system().bcp47Name()); } - if (!m_translator.load(locale, "locale", "_", - QString(":/addons/%1/i18n").arg(m_id))) { + if (!m_translator.load( + locale, "locale", "_", + QFileInfo(m_manifestFileName).dir().filePath("i18n"))) { logger.error() << "Loading the locale failed. - code:" << code; } } + +QString Addon::qml() const { + if (m_qml.at(0) == ':') { + return QString("qrc%1").arg(m_qml); + } + + return QString("file:%1").arg(m_qml); +} diff --git a/src/addon.h b/src/addon.h index 173dcfb4c6..30e0fada19 100644 --- a/src/addon.h +++ b/src/addon.h @@ -14,7 +14,7 @@ class Addon final : public QObject { Q_PROPERTY(QString id MEMBER m_id CONSTANT) Q_PROPERTY(QString name MEMBER m_name CONSTANT) - Q_PROPERTY(QString qml MEMBER m_qml CONSTANT) + Q_PROPERTY(QString qml READ qml CONSTANT) public: enum AddonType { @@ -24,19 +24,19 @@ class Addon final : public QObject { AddonTypeTutorial, }; - Addon(QObject* parent, AddonType addonType, const QString& fileName, + Addon(QObject* parent, AddonType addonType, const QString& manifestFileName, const QString& id, const QString& name, const QString& qml); ~Addon(); - const QString& fileName() const { return m_fileName; } - AddonType type() const { return m_addonType; } + QString qml() const; + void retranslate(); private: const AddonType m_addonType; - const QString m_fileName; + const QString m_manifestFileName; const QString m_id; const QString m_name; const QString m_qml; diff --git a/src/addonmanager.cpp b/src/addonmanager.cpp index 3d50a9eee0..07ebef9e14 100644 --- a/src/addonmanager.cpp +++ b/src/addonmanager.cpp @@ -109,18 +109,26 @@ bool AddonManager::load(const QString& fileName) { return false; } - auto guard = - qScopeGuard([&] { QResource::unregisterResource(fileName, "/addons"); }); + if (!loadManifest(QString(":/addons/%1/manifest.json").arg(addonId))) { + QResource::unregisterResource(fileName, "/addons"); + return false; + } + + return true; +} - QFile file(QString(":/addons/%1/manifest.json").arg(addonId)); +bool AddonManager::loadManifest(const QString& manifestFileName) { + QFile file(manifestFileName); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - logger.warning() << "Unable to read the addon manifest of" << addonId; + logger.warning() << "Unable to read the addon manifest of" + << manifestFileName; return false; } QJsonDocument json = QJsonDocument::fromJson(file.readAll()); if (!json.isObject()) { - logger.warning() << "The manifest must be a JSON document" << addonId; + logger.warning() << "The manifest must be a JSON document" + << manifestFileName; return false; } @@ -128,36 +136,30 @@ bool AddonManager::load(const QString& fileName) { QString version = obj["version"].toString(); if (version.isEmpty()) { - logger.warning() << "No version in the manifest" << addonId; + logger.warning() << "No version in the manifest" << manifestFileName; return false; } if (version != "0.1") { - logger.warning() << "Unsupported version" << version << addonId; + logger.warning() << "Unsupported version" << version << manifestFileName; return false; } QString id = obj["id"].toString(); if (id.isEmpty()) { - logger.warning() << "No id in the manifest" << addonId; - return false; - } - - if (id != addonId) { - logger.warning() << "The ID does not match with the addon one" << addonId - << id; + logger.warning() << "No id in the manifest" << manifestFileName; return false; } QString name = obj["name"].toString(); if (name.isEmpty()) { - logger.warning() << "No name in the manifest" << addonId; + logger.warning() << "No name in the manifest" << manifestFileName; return false; } QString type = obj["type"].toString(); if (type.isEmpty()) { - logger.warning() << "No type in the manifest" << addonId; + logger.warning() << "No type in the manifest" << manifestFileName; return false; } @@ -168,14 +170,14 @@ bool AddonManager::load(const QString& fileName) { addonType = Addon::AddonTypeDemo; QString qml = obj["qml"].toString(); if (qml.isEmpty()) { - logger.warning() << "No qml in the manifest" << addonId; + logger.warning() << "No qml in the manifest" << manifestFileName; return false; } - qmlFileName = QString(":/addons/%1/%2").arg(addonId).arg(qml); + qmlFileName = QFileInfo(manifestFileName).dir().filePath(qml); if (!QFile::exists(qmlFileName)) { logger.warning() << "Unable to load the qml entry" << qmlFileName << qml - << addonId; + << manifestFileName; return false; } } else if (type == "i18n") { @@ -185,7 +187,7 @@ bool AddonManager::load(const QString& fileName) { } else if (type == "guide") { addonType = Addon::AddonTypeGuide; } else { - logger.warning() << "Unsupported type" << type << addonId; + logger.warning() << "Unsupported type" << type << manifestFileName; return false; } @@ -204,15 +206,31 @@ bool AddonManager::load(const QString& fileName) { } } - guard.dismiss(); - Addon* addon = - new Addon(this, addonType, fileName, addonId, name, qmlFileName); - m_addons.insert(addonId, addon); + new Addon(this, addonType, manifestFileName, id, name, qmlFileName); + m_addons.insert(id, addon); return true; } +void AddonManager::unload(const QString& addonId) { + if (!Feature::get(Feature::Feature_addon)->isSupported()) { + logger.warning() << "Addons disabled by feature flag"; + return; + } + + if (!m_addons.contains(addonId)) { + logger.warning() << "No addon with id" << addonId; + return; + } + + Addon* addon = m_addons[addonId]; + Q_ASSERT(addon); + + m_addons.remove(addonId); + addon->deleteLater(); +} + void AddonManager::run(const QString& addonId) { if (!Feature::get(Feature::Feature_addon)->isSupported()) { logger.warning() << "Addons disabled by feature flag"; diff --git a/src/addonmanager.h b/src/addonmanager.h index a4e9eb4f0a..397d9c331a 100644 --- a/src/addonmanager.h +++ b/src/addonmanager.h @@ -22,7 +22,11 @@ class AddonManager final : public QObject { ~AddonManager(); bool load(const QString& addonFileName); - void run(const QString& addonName); + bool loadManifest(const QString& addonManifestFileName); + + void unload(const QString& addonId); + + void run(const QString& addonId); void retranslate(); diff --git a/src/inspector/inspectorhandler.cpp b/src/inspector/inspectorhandler.cpp index edb0bd3a96..5b33c34bef 100644 --- a/src/inspector/inspectorhandler.cpp +++ b/src/inspector/inspectorhandler.cpp @@ -889,7 +889,7 @@ static QList s_commands{ return QJsonObject(); }}, - InspectorCommand{"load_addon", "Load an addon", 1, + InspectorCommand{"load_addon", "Load an add-on", 1, [](InspectorHandler*, const QList& arguments) { QJsonObject obj; obj["value"] = @@ -897,7 +897,21 @@ static QList s_commands{ return obj; }}, - InspectorCommand{"run_addon", "Load an addon", 1, + InspectorCommand{"load_addon_manifest", "Load an add-on", 1, + [](InspectorHandler*, const QList& arguments) { + QJsonObject obj; + obj["value"] = + AddonManager::instance()->loadManifest(arguments[1]); + return obj; + }}, + + InspectorCommand{"unload_addon", "Unload an add-on", 1, + [](InspectorHandler*, const QList& arguments) { + AddonManager::instance()->unload(arguments[1]); + return QJsonObject(); + }}, + + InspectorCommand{"run_addon", "Run an add-on", 1, [](InspectorHandler*, const QList& arguments) { AddonManager::instance()->run(arguments[1]); return QJsonObject(); diff --git a/src/ui/views/ViewAddon.qml b/src/ui/views/ViewAddon.qml index a308676a4d..df84a1a0f7 100644 --- a/src/ui/views/ViewAddon.qml +++ b/src/ui/views/ViewAddon.qml @@ -35,6 +35,6 @@ Item { anchors.topMargin: VPNTheme.theme.windowMargin anchors.top: menu.bottom - source: "qrc" + parent.addon.qml + source: parent.addon.qml } } From d84c3d945cf42e06a88d2401a8a407abb9f9f062 Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Mon, 6 Jun 2022 14:04:47 +0200 Subject: [PATCH 06/20] Split addons by types --- src/addon.cpp | 62 ----------------- src/addonmanager.cpp | 117 ++------------------------------ src/addonmanager.h | 3 +- src/addons/addon.cpp | 125 +++++++++++++++++++++++++++++++++++ src/{ => addons}/addon.h | 28 +++----- src/addons/addondemo.cpp | 55 +++++++++++++++ src/addons/addondemo.h | 37 +++++++++++ src/addons/addonguide.cpp | 38 +++++++++++ src/addons/addonguide.h | 30 +++++++++ src/addons/addoni18n.cpp | 17 +++++ src/addons/addoni18n.h | 23 +++++++ src/addons/addontutorial.cpp | 38 +++++++++++ src/addons/addontutorial.h | 30 +++++++++ src/cmake/sources.cmake | 12 +++- src/qmake/sources.pri | 12 +++- tests/unit/CMakeLists.txt | 12 +++- tests/unit/unit.pro | 12 +++- 17 files changed, 451 insertions(+), 200 deletions(-) delete mode 100644 src/addon.cpp create mode 100644 src/addons/addon.cpp rename src/{ => addons}/addon.h (52%) create mode 100644 src/addons/addondemo.cpp create mode 100644 src/addons/addondemo.h create mode 100644 src/addons/addonguide.cpp create mode 100644 src/addons/addonguide.h create mode 100644 src/addons/addoni18n.cpp create mode 100644 src/addons/addoni18n.h create mode 100644 src/addons/addontutorial.cpp create mode 100644 src/addons/addontutorial.h diff --git a/src/addon.cpp b/src/addon.cpp deleted file mode 100644 index e74f670d9a..0000000000 --- a/src/addon.cpp +++ /dev/null @@ -1,62 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "addon.h" -#include "leakdetector.h" -#include "logger.h" -#include "settingsholder.h" - -#include -#include -#include - -namespace { -Logger logger(LOG_MAIN, "Addon"); -} - -Addon::Addon(QObject* parent, AddonType addonType, - const QString& manifestFileName, const QString& id, - const QString& name, const QString& qml) - : QObject(parent), - m_addonType(addonType), - m_manifestFileName(manifestFileName), - m_id(id), - m_name(name), - m_qml(qml) { - MVPN_COUNT_CTOR(Addon); - - QCoreApplication::installTranslator(&m_translator); - retranslate(); -} - -Addon::~Addon() { - MVPN_COUNT_DTOR(Addon); - QCoreApplication::removeTranslator(&m_translator); -} - -void Addon::retranslate() { - SettingsHolder* settingsHolder = SettingsHolder::instance(); - Q_ASSERT(settingsHolder); - - QString code = settingsHolder->languageCode(); - - QLocale locale = QLocale(code); - if (code.isEmpty()) { - locale = QLocale(QLocale::system().bcp47Name()); - } - - if (!m_translator.load( - locale, "locale", "_", - QFileInfo(m_manifestFileName).dir().filePath("i18n"))) { - logger.error() << "Loading the locale failed. - code:" << code; - } -} - -QString Addon::qml() const { - if (m_qml.at(0) == ':') { - return QString("qrc%1").arg(m_qml); - } - - return QString("file:%1").arg(m_qml); -} diff --git a/src/addonmanager.cpp b/src/addonmanager.cpp index 07ebef9e14..5819e9a8e0 100644 --- a/src/addonmanager.cpp +++ b/src/addonmanager.cpp @@ -3,21 +3,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "addonmanager.h" +#include "addons/addon.h" #include "constants.h" #include "leakdetector.h" -#include "localizer.h" #include "logger.h" #include "models/feature.h" -#include "models/guidemodel.h" -#include "models/tutorialmodel.h" #include #include #include -#include -#include #include -#include #include namespace { @@ -118,98 +113,14 @@ bool AddonManager::load(const QString& fileName) { } bool AddonManager::loadManifest(const QString& manifestFileName) { - QFile file(manifestFileName); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - logger.warning() << "Unable to read the addon manifest of" + Addon* addon = Addon::create(this, manifestFileName); + if (!addon) { + logger.warning() << "Unable to create an addon from manifest" << manifestFileName; return false; } - QJsonDocument json = QJsonDocument::fromJson(file.readAll()); - if (!json.isObject()) { - logger.warning() << "The manifest must be a JSON document" - << manifestFileName; - return false; - } - - QJsonObject obj = json.object(); - - QString version = obj["version"].toString(); - if (version.isEmpty()) { - logger.warning() << "No version in the manifest" << manifestFileName; - return false; - } - - if (version != "0.1") { - logger.warning() << "Unsupported version" << version << manifestFileName; - return false; - } - - QString id = obj["id"].toString(); - if (id.isEmpty()) { - logger.warning() << "No id in the manifest" << manifestFileName; - return false; - } - - QString name = obj["name"].toString(); - if (name.isEmpty()) { - logger.warning() << "No name in the manifest" << manifestFileName; - return false; - } - - QString type = obj["type"].toString(); - if (type.isEmpty()) { - logger.warning() << "No type in the manifest" << manifestFileName; - return false; - } - - Addon::AddonType addonType; - QString qmlFileName; - - if (type == "demo") { - addonType = Addon::AddonTypeDemo; - QString qml = obj["qml"].toString(); - if (qml.isEmpty()) { - logger.warning() << "No qml in the manifest" << manifestFileName; - return false; - } - - qmlFileName = QFileInfo(manifestFileName).dir().filePath(qml); - if (!QFile::exists(qmlFileName)) { - logger.warning() << "Unable to load the qml entry" << qmlFileName << qml - << manifestFileName; - return false; - } - } else if (type == "i18n") { - addonType = Addon::AddonTypeI18n; - } else if (type == "tutorial") { - addonType = Addon::AddonTypeTutorial; - } else if (type == "guide") { - addonType = Addon::AddonTypeGuide; - } else { - logger.warning() << "Unsupported type" << type << manifestFileName; - return false; - } - - if (addonType == Addon::AddonTypeTutorial) { - if (!TutorialModel::instance()->createFromJson( - obj["tutorial"].toObject())) { - logger.warning() << "Unable to add the tutorial"; - return false; - } - } - - if (addonType == Addon::AddonTypeGuide) { - if (!GuideModel::instance()->createFromJson(obj["guide"].toObject())) { - logger.warning() << "Unable to add the guide"; - return false; - } - } - - Addon* addon = - new Addon(this, addonType, manifestFileName, id, name, qmlFileName); - m_addons.insert(id, addon); - + m_addons.insert(addon->id(), addon); return true; } @@ -245,23 +156,7 @@ void AddonManager::run(const QString& addonId) { Addon* addon = m_addons[addonId]; Q_ASSERT(addon); - switch (addon->type()) { - case Addon::AddonTypeDemo: - emit runAddon(addon); - break; - - case Addon::AddonTypeI18n: - emit Localizer::instance()->codeChanged(); - break; - - case Addon::AddonTypeTutorial: - // Nothing todo for these types - break; - - case Addon::AddonTypeGuide: - // Nothing todo for these types - break; - } + addon->run(); } void AddonManager::retranslate() { diff --git a/src/addonmanager.h b/src/addonmanager.h index 397d9c331a..b983722e26 100644 --- a/src/addonmanager.h +++ b/src/addonmanager.h @@ -5,11 +5,10 @@ #ifndef ADDONMANAGER_H #define ADDONMANAGER_H -#include "addon.h" - #include #include +class Addon; class QDir; class AddonManager final : public QObject { diff --git a/src/addons/addon.cpp b/src/addons/addon.cpp new file mode 100644 index 0000000000..6f98b7c698 --- /dev/null +++ b/src/addons/addon.cpp @@ -0,0 +1,125 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "addon.h" +#include "addondemo.h" +#include "addonguide.h" +#include "addoni18n.h" +#include "addontutorial.h" +#include "leakdetector.h" +#include "logger.h" +#include "models/guidemodel.h" +#include "settingsholder.h" + +#include +#include +#include +#include +#include + +namespace { +Logger logger(LOG_MAIN, "Addon"); +} + +// static +Addon* Addon::create(QObject* parent, const QString& manifestFileName) { + QFile file(manifestFileName); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + logger.warning() << "Unable to read the addon manifest of" + << manifestFileName; + return nullptr; + } + + QJsonDocument json = QJsonDocument::fromJson(file.readAll()); + if (!json.isObject()) { + logger.warning() << "The manifest must be a JSON document" + << manifestFileName; + return nullptr; + } + + QJsonObject obj = json.object(); + + QString version = obj["version"].toString(); + if (version.isEmpty()) { + logger.warning() << "No version in the manifest" << manifestFileName; + return nullptr; + } + + if (version != "0.1") { + logger.warning() << "Unsupported version" << version << manifestFileName; + return nullptr; + } + + QString id = obj["id"].toString(); + if (id.isEmpty()) { + logger.warning() << "No id in the manifest" << manifestFileName; + return nullptr; + } + + QString name = obj["name"].toString(); + if (name.isEmpty()) { + logger.warning() << "No name in the manifest" << manifestFileName; + return nullptr; + } + + QString type = obj["type"].toString(); + if (type.isEmpty()) { + logger.warning() << "No type in the manifest" << manifestFileName; + return nullptr; + } + + if (type == "demo") { + return AddonDemo::create(parent, manifestFileName, id, name, obj); + } + + if (type == "i18n") { + return new AddonI18n(parent, manifestFileName, id, name); + } + + if (type == "tutorial") { + return AddonTutorial::create(parent, manifestFileName, id, name, obj); + } + + if (type == "guide") { + return AddonGuide::create(parent, manifestFileName, id, name, obj); + } + + logger.warning() << "Unsupported type" << type << manifestFileName; + return nullptr; +} + +Addon::Addon(QObject* parent, const QString& manifestFileName, + const QString& id, const QString& name) + : QObject(parent), + m_manifestFileName(manifestFileName), + m_id(id), + m_name(name) { + MVPN_COUNT_CTOR(Addon); + + QCoreApplication::installTranslator(&m_translator); + retranslate(); +} + +Addon::~Addon() { + MVPN_COUNT_DTOR(Addon); + QCoreApplication::removeTranslator(&m_translator); +} + +void Addon::retranslate() { + SettingsHolder* settingsHolder = SettingsHolder::instance(); + Q_ASSERT(settingsHolder); + + QString code = settingsHolder->languageCode(); + + QLocale locale = QLocale(code); + if (code.isEmpty()) { + locale = QLocale(QLocale::system().bcp47Name()); + } + + if (!m_translator.load( + locale, "locale", "_", + QFileInfo(m_manifestFileName).dir().filePath("i18n"))) { + logger.error() << "Loading the locale failed. - code:" << code; + } +} diff --git a/src/addon.h b/src/addons/addon.h similarity index 52% rename from src/addon.h rename to src/addons/addon.h index 30e0fada19..58f65cb484 100644 --- a/src/addon.h +++ b/src/addons/addon.h @@ -8,38 +8,32 @@ #include #include -class Addon final : public QObject { +class Addon : public QObject { Q_OBJECT Q_DISABLE_COPY_MOVE(Addon) - Q_PROPERTY(QString id MEMBER m_id CONSTANT) + Q_PROPERTY(QString id READ id CONSTANT) Q_PROPERTY(QString name MEMBER m_name CONSTANT) - Q_PROPERTY(QString qml READ qml CONSTANT) public: - enum AddonType { - AddonTypeDemo, - AddonTypeGuide, - AddonTypeI18n, - AddonTypeTutorial, - }; - - Addon(QObject* parent, AddonType addonType, const QString& manifestFileName, - const QString& id, const QString& name, const QString& qml); - ~Addon(); + static Addon* create(QObject* parent, const QString& manifestFileName); - AddonType type() const { return m_addonType; } + ~Addon(); - QString qml() const; + const QString& id() const { return m_id; } void retranslate(); + virtual void run() = 0; + + protected: + Addon(QObject* parent, const QString& manifestFileName, const QString& id, + const QString& name); + private: - const AddonType m_addonType; const QString m_manifestFileName; const QString m_id; const QString m_name; - const QString m_qml; QTranslator m_translator; }; diff --git a/src/addons/addondemo.cpp b/src/addons/addondemo.cpp new file mode 100644 index 0000000000..a30d2ae6e6 --- /dev/null +++ b/src/addons/addondemo.cpp @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "addondemo.h" +#include "addonmanager.h" +#include "leakdetector.h" +#include "logger.h" + +#include +#include +#include + +namespace { +Logger logger(LOG_MAIN, "AddonDemo"); +} + +// static +Addon* AddonDemo::create(QObject* parent, const QString& manifestFileName, + const QString& id, const QString& name, + const QJsonObject& obj) { + QString qml = obj["qml"].toString(); + if (qml.isEmpty()) { + logger.warning() << "No qml in the manifest" << manifestFileName; + return nullptr; + } + + QString qmlFileName = QFileInfo(manifestFileName).dir().filePath(qml); + if (!QFile::exists(qmlFileName)) { + logger.warning() << "Unable to load the qml entry" << qmlFileName << qml + << manifestFileName; + return nullptr; + } + + return new AddonDemo(parent, manifestFileName, id, name, qmlFileName); +} + +AddonDemo::AddonDemo(QObject* parent, const QString& manifestFileName, + const QString& id, const QString& name, + const QString& qmlFileName) + : Addon(parent, manifestFileName, id, name), m_qmlFileName(qmlFileName) { + MVPN_COUNT_CTOR(AddonDemo); +} + +AddonDemo::~AddonDemo() { MVPN_COUNT_DTOR(AddonDemo); } + +QString AddonDemo::qml() const { + if (m_qmlFileName.at(0) == ':') { + return QString("qrc%1").arg(m_qmlFileName); + } + + return QString("file:%1").arg(m_qmlFileName); +} + +void AddonDemo::run() { AddonManager::instance()->runAddon(this); } diff --git a/src/addons/addondemo.h b/src/addons/addondemo.h new file mode 100644 index 0000000000..fdd6b1211a --- /dev/null +++ b/src/addons/addondemo.h @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef ADDONDEMO_H +#define ADDONDEMO_H + +#include "addon.h" + +class QJsonObject; + +class AddonDemo final : public Addon { + Q_OBJECT + Q_DISABLE_COPY_MOVE(AddonDemo) + + Q_PROPERTY(QString qml READ qml CONSTANT) + + public: + static Addon* create(QObject* parent, const QString& manifestFileName, + const QString& id, const QString& name, + const QJsonObject& obj); + + ~AddonDemo(); + + QString qml() const; + + void run() override; + + private: + AddonDemo(QObject* parent, const QString& manifestFileName, const QString& id, + const QString& name, const QString& qmlFileName); + + private: + QString m_qmlFileName; +}; + +#endif // ADDONDEMO_H diff --git a/src/addons/addonguide.cpp b/src/addons/addonguide.cpp new file mode 100644 index 0000000000..73ba5079a2 --- /dev/null +++ b/src/addons/addonguide.cpp @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "addonguide.h" +#include "leakdetector.h" +#include "logger.h" +#include "models/guidemodel.h" + +#include + +namespace { +Logger logger(LOG_MAIN, "AddonGuide"); +} + +// static +Addon* AddonGuide::create(QObject* parent, const QString& manifestFileName, + const QString& id, const QString& name, + const QJsonObject& obj) { + if (!GuideModel::instance()->createFromJson(obj["guide"].toObject())) { + logger.warning() << "Unable to add the guide"; + return nullptr; + } + + return new AddonGuide(parent, manifestFileName, id, name); +} + +AddonGuide::AddonGuide(QObject* parent, const QString& manifestFileName, + const QString& id, const QString& name) + : Addon(parent, manifestFileName, id, name) { + MVPN_COUNT_CTOR(AddonGuide); +} + +AddonGuide::~AddonGuide() { MVPN_COUNT_DTOR(AddonGuide); } + +void AddonGuide::run() { + // Nothing to do here. +} diff --git a/src/addons/addonguide.h b/src/addons/addonguide.h new file mode 100644 index 0000000000..69553e3150 --- /dev/null +++ b/src/addons/addonguide.h @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef ADDONGUIDE_H +#define ADDONGUIDE_H + +#include "addon.h" + +class QJsonObject; + +class AddonGuide final : public Addon { + Q_OBJECT + Q_DISABLE_COPY_MOVE(AddonGuide) + + public: + static Addon* create(QObject* parent, const QString& manifestFileName, + const QString& id, const QString& name, + const QJsonObject& obj); + + ~AddonGuide(); + + void run() override; + + private: + AddonGuide(QObject* parent, const QString& manifestFileName, + const QString& id, const QString& name); +}; + +#endif // ADDONGUIDE_H diff --git a/src/addons/addoni18n.cpp b/src/addons/addoni18n.cpp new file mode 100644 index 0000000000..cf38670194 --- /dev/null +++ b/src/addons/addoni18n.cpp @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "addoni18n.h" +#include "leakdetector.h" +#include "localizer.h" + +AddonI18n::AddonI18n(QObject* parent, const QString& manifestFileName, + const QString& id, const QString& name) + : Addon(parent, manifestFileName, id, name) { + MVPN_COUNT_CTOR(AddonI18n); +} + +AddonI18n::~AddonI18n() { MVPN_COUNT_DTOR(AddonI18n); } + +void AddonI18n::run() { emit Localizer::instance()->codeChanged(); } diff --git a/src/addons/addoni18n.h b/src/addons/addoni18n.h new file mode 100644 index 0000000000..70ecf1e1ec --- /dev/null +++ b/src/addons/addoni18n.h @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef ADDONI18N_H +#define ADDONI18N_H + +#include "addon.h" + +class AddonI18n final : public Addon { + Q_OBJECT + Q_DISABLE_COPY_MOVE(AddonI18n) + + public: + AddonI18n(QObject* parent, const QString& manifestFileName, const QString& id, + const QString& name); + + ~AddonI18n(); + + void run() override; +}; + +#endif // ADDONI18N_H diff --git a/src/addons/addontutorial.cpp b/src/addons/addontutorial.cpp new file mode 100644 index 0000000000..2590987f3b --- /dev/null +++ b/src/addons/addontutorial.cpp @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "addontutorial.h" +#include "leakdetector.h" +#include "logger.h" +#include "models/tutorialmodel.h" + +#include + +namespace { +Logger logger(LOG_MAIN, "AddonTutorial"); +} + +// static +Addon* AddonTutorial::create(QObject* parent, const QString& manifestFileName, + const QString& id, const QString& name, + const QJsonObject& obj) { + if (!TutorialModel::instance()->createFromJson(obj["tutorial"].toObject())) { + logger.warning() << "Unable to add the tutorial"; + return nullptr; + } + + return new AddonTutorial(parent, manifestFileName, id, name); +} + +AddonTutorial::AddonTutorial(QObject* parent, const QString& manifestFileName, + const QString& id, const QString& name) + : Addon(parent, manifestFileName, id, name) { + MVPN_COUNT_CTOR(AddonTutorial); +} + +AddonTutorial::~AddonTutorial() { MVPN_COUNT_DTOR(AddonTutorial); } + +void AddonTutorial::run() { + // Nothing to do here. +} diff --git a/src/addons/addontutorial.h b/src/addons/addontutorial.h new file mode 100644 index 0000000000..b3da435369 --- /dev/null +++ b/src/addons/addontutorial.h @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef ADDONTUTORIAL_H +#define ADDONTUTORIAL_H + +#include "addon.h" + +class QJsonObject; + +class AddonTutorial final : public Addon { + Q_OBJECT + Q_DISABLE_COPY_MOVE(AddonTutorial) + + public: + static Addon* create(QObject* parent, const QString& manifestFileName, + const QString& id, const QString& name, + const QJsonObject& obj); + + ~AddonTutorial(); + + void run() override; + + private: + AddonTutorial(QObject* parent, const QString& manifestFileName, + const QString& id, const QString& name); +}; + +#endif // ADDONTUTORIAL_H diff --git a/src/cmake/sources.cmake b/src/cmake/sources.cmake index 6806db6992..5d84127c06 100644 --- a/src/cmake/sources.cmake +++ b/src/cmake/sources.cmake @@ -8,10 +8,18 @@ target_sources(mozillavpn PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/version.h) # VPN Client source files target_sources(mozillavpn PRIVATE - addon.cpp - addon.h addonmanager.cpp addonmanager.h + addons/addon.cpp + addons/addon.h + addons/addondemo.cpp + addons/addondemo.h + addons/addonguide.cpp + addons/addonguide.h + addons/addoni18n.cpp + addons/addoni18n.h + addons/addontutorial.cpp + addons/addontutorial.h appimageprovider.h applistprovider.h apppermission.cpp diff --git a/src/qmake/sources.pri b/src/qmake/sources.pri index 9881e95cd5..bb302fab22 100644 --- a/src/qmake/sources.pri +++ b/src/qmake/sources.pri @@ -3,8 +3,12 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. SOURCES += \ - addon.cpp \ addonmanager.cpp \ + addons/addon.cpp \ + addons/addondemo.cpp \ + addons/addonguide.cpp \ + addons/addoni18n.cpp \ + addons/addontutorial.cpp \ apppermission.cpp \ authenticationlistener.cpp \ authenticationinapp/authenticationinapp.cpp \ @@ -144,8 +148,12 @@ SOURCES += \ websockethandler.cpp HEADERS += \ - addon.h \ addonmanager.h \ + addons/addon.h \ + addons/addondemo.h \ + addons/addonguide.h \ + addons/addoni18n.h \ + addons/addontutorial.h \ appimageprovider.h \ apppermission.h \ applistprovider.h \ diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 66c85d940b..78e1c04923 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -36,10 +36,18 @@ target_link_libraries(unit_tests PRIVATE glean lottie nebula translations) # VPN Client source files target_sources(unit_tests PRIVATE - ${MVPN_SOURCE_DIR}/addon.cpp - ${MVPN_SOURCE_DIR}/addon.h ${MVPN_SOURCE_DIR}/addonmanager.cpp ${MVPN_SOURCE_DIR}/addonmanager.h + ${MVPN_SOURCE_DIR}/addons/addon.cpp + ${MVPN_SOURCE_DIR}/addons/addon.h + ${MVPN_SOURCE_DIR}/addons/addondemo.cpp + ${MVPN_SOURCE_DIR}/addons/addondemo.h + ${MVPN_SOURCE_DIR}/addons/addonguide.cpp + ${MVPN_SOURCE_DIR}/addons/addonguide.h + ${MVPN_SOURCE_DIR}/addons/addoni18n.cpp + ${MVPN_SOURCE_DIR}/addons/addoni18n.h + ${MVPN_SOURCE_DIR}/addons/addontutorial.cpp + ${MVPN_SOURCE_DIR}/addons/addontutorial.h ${MVPN_SOURCE_DIR}/adjust/adjustfiltering.cpp ${MVPN_SOURCE_DIR}/adjust/adjustfiltering.h ${MVPN_SOURCE_DIR}/adjust/adjustproxypackagehandler.cpp diff --git a/tests/unit/unit.pro b/tests/unit/unit.pro index cd754edf9b..5834ac2f1c 100644 --- a/tests/unit/unit.pro +++ b/tests/unit/unit.pro @@ -42,8 +42,12 @@ include($$PWD/../../translations/translations.pri) RESOURCES ~= 's/.*servers.qrc//g' HEADERS += \ - ../../src/addon.h \ ../../src/addonmanager.h \ + ../../src/addons/addon.h \ + ../../src/addons/addondemo.h \ + ../../src/addons/addonguide.h \ + ../../src/addons/addoni18n.h \ + ../../src/addons/addontutorial.h \ ../../src/adjust/adjustfiltering.h \ ../../src/adjust/adjustproxypackagehandler.h \ ../../src/captiveportal/captiveportal.h \ @@ -151,8 +155,12 @@ HEADERS += \ testwebsockethandler.h SOURCES += \ - ../../src/addon.cpp \ ../../src/addonmanager.cpp \ + ../../src/addons/addon.cpp \ + ../../src/addons/addondemo.cpp \ + ../../src/addons/addonguide.cpp \ + ../../src/addons/addoni18n.cpp \ + ../../src/addons/addontutorial.cpp \ ../../src/adjust/adjustfiltering.cpp \ ../../src/adjust/adjustproxypackagehandler.cpp \ ../../src/captiveportal/captiveportal.cpp \ From 5cfa664711aa15fe43a241e2eeb1f06794193c3a Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Mon, 6 Jun 2022 15:01:33 +0200 Subject: [PATCH 07/20] Load and unload guides and tutorials --- src/addonmanager.cpp | 18 --------------- src/addonmanager.h | 5 ++--- src/addons/addon.h | 2 -- src/addons/addondemo.cpp | 7 +++--- src/addons/addondemo.h | 2 -- src/addons/addonguide.cpp | 8 +++---- src/addons/addonguide.h | 2 -- src/addons/addoni18n.cpp | 4 ++-- src/addons/addoni18n.h | 2 -- src/addons/addontutorial.cpp | 9 ++++---- src/addons/addontutorial.h | 2 -- src/inspector/inspectorhandler.cpp | 6 ----- src/models/guidemodel.cpp | 24 +++++++++++++++----- src/models/guidemodel.h | 10 +++++++-- src/models/tutorialmodel.cpp | 36 +++++++++++++++++++++++++----- src/models/tutorialmodel.h | 16 ++++++++++--- 16 files changed, 87 insertions(+), 66 deletions(-) diff --git a/src/addonmanager.cpp b/src/addonmanager.cpp index 5819e9a8e0..abdebc0b9a 100644 --- a/src/addonmanager.cpp +++ b/src/addonmanager.cpp @@ -3,7 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "addonmanager.h" -#include "addons/addon.h" #include "constants.h" #include "leakdetector.h" #include "logger.h" @@ -142,23 +141,6 @@ void AddonManager::unload(const QString& addonId) { addon->deleteLater(); } -void AddonManager::run(const QString& addonId) { - if (!Feature::get(Feature::Feature_addon)->isSupported()) { - logger.warning() << "Addons disabled by feature flag"; - return; - } - - if (!m_addons.contains(addonId)) { - logger.warning() << "No addon with id" << addonId; - return; - } - - Addon* addon = m_addons[addonId]; - Q_ASSERT(addon); - - addon->run(); -} - void AddonManager::retranslate() { foreach (Addon* addon, m_addons) { // This comment is here to make the linter happy. diff --git a/src/addonmanager.h b/src/addonmanager.h index b983722e26..889670238f 100644 --- a/src/addonmanager.h +++ b/src/addonmanager.h @@ -5,10 +5,11 @@ #ifndef ADDONMANAGER_H #define ADDONMANAGER_H +#include "addons/addon.h" // required for the signal + #include #include -class Addon; class QDir; class AddonManager final : public QObject { @@ -25,8 +26,6 @@ class AddonManager final : public QObject { void unload(const QString& addonId); - void run(const QString& addonId); - void retranslate(); private: diff --git a/src/addons/addon.h b/src/addons/addon.h index 58f65cb484..876a5f4f16 100644 --- a/src/addons/addon.h +++ b/src/addons/addon.h @@ -24,8 +24,6 @@ class Addon : public QObject { void retranslate(); - virtual void run() = 0; - protected: Addon(QObject* parent, const QString& manifestFileName, const QString& id, const QString& name); diff --git a/src/addons/addondemo.cpp b/src/addons/addondemo.cpp index a30d2ae6e6..69fd8cdc8a 100644 --- a/src/addons/addondemo.cpp +++ b/src/addons/addondemo.cpp @@ -32,7 +32,10 @@ Addon* AddonDemo::create(QObject* parent, const QString& manifestFileName, return nullptr; } - return new AddonDemo(parent, manifestFileName, id, name, qmlFileName); + Addon* addon = new AddonDemo(parent, manifestFileName, id, name, qmlFileName); + emit AddonManager::instance()->runAddon(addon); + + return addon; } AddonDemo::AddonDemo(QObject* parent, const QString& manifestFileName, @@ -51,5 +54,3 @@ QString AddonDemo::qml() const { return QString("file:%1").arg(m_qmlFileName); } - -void AddonDemo::run() { AddonManager::instance()->runAddon(this); } diff --git a/src/addons/addondemo.h b/src/addons/addondemo.h index fdd6b1211a..3b10497d7d 100644 --- a/src/addons/addondemo.h +++ b/src/addons/addondemo.h @@ -24,8 +24,6 @@ class AddonDemo final : public Addon { QString qml() const; - void run() override; - private: AddonDemo(QObject* parent, const QString& manifestFileName, const QString& id, const QString& name, const QString& qmlFileName); diff --git a/src/addons/addonguide.cpp b/src/addons/addonguide.cpp index 73ba5079a2..6b54794780 100644 --- a/src/addons/addonguide.cpp +++ b/src/addons/addonguide.cpp @@ -17,7 +17,7 @@ Logger logger(LOG_MAIN, "AddonGuide"); Addon* AddonGuide::create(QObject* parent, const QString& manifestFileName, const QString& id, const QString& name, const QJsonObject& obj) { - if (!GuideModel::instance()->createFromJson(obj["guide"].toObject())) { + if (!GuideModel::instance()->createFromJson(id, obj["guide"].toObject())) { logger.warning() << "Unable to add the guide"; return nullptr; } @@ -31,8 +31,8 @@ AddonGuide::AddonGuide(QObject* parent, const QString& manifestFileName, MVPN_COUNT_CTOR(AddonGuide); } -AddonGuide::~AddonGuide() { MVPN_COUNT_DTOR(AddonGuide); } +AddonGuide::~AddonGuide() { + MVPN_COUNT_DTOR(AddonGuide); -void AddonGuide::run() { - // Nothing to do here. + GuideModel::instance()->remove(id()); } diff --git a/src/addons/addonguide.h b/src/addons/addonguide.h index 69553e3150..74ee6559b9 100644 --- a/src/addons/addonguide.h +++ b/src/addons/addonguide.h @@ -20,8 +20,6 @@ class AddonGuide final : public Addon { ~AddonGuide(); - void run() override; - private: AddonGuide(QObject* parent, const QString& manifestFileName, const QString& id, const QString& name); diff --git a/src/addons/addoni18n.cpp b/src/addons/addoni18n.cpp index cf38670194..c529da2e1f 100644 --- a/src/addons/addoni18n.cpp +++ b/src/addons/addoni18n.cpp @@ -10,8 +10,8 @@ AddonI18n::AddonI18n(QObject* parent, const QString& manifestFileName, const QString& id, const QString& name) : Addon(parent, manifestFileName, id, name) { MVPN_COUNT_CTOR(AddonI18n); + + emit Localizer::instance()->codeChanged(); } AddonI18n::~AddonI18n() { MVPN_COUNT_DTOR(AddonI18n); } - -void AddonI18n::run() { emit Localizer::instance()->codeChanged(); } diff --git a/src/addons/addoni18n.h b/src/addons/addoni18n.h index 70ecf1e1ec..7e7e779ec6 100644 --- a/src/addons/addoni18n.h +++ b/src/addons/addoni18n.h @@ -16,8 +16,6 @@ class AddonI18n final : public Addon { const QString& name); ~AddonI18n(); - - void run() override; }; #endif // ADDONI18N_H diff --git a/src/addons/addontutorial.cpp b/src/addons/addontutorial.cpp index 2590987f3b..abdcc5bfc7 100644 --- a/src/addons/addontutorial.cpp +++ b/src/addons/addontutorial.cpp @@ -17,7 +17,8 @@ Logger logger(LOG_MAIN, "AddonTutorial"); Addon* AddonTutorial::create(QObject* parent, const QString& manifestFileName, const QString& id, const QString& name, const QJsonObject& obj) { - if (!TutorialModel::instance()->createFromJson(obj["tutorial"].toObject())) { + if (!TutorialModel::instance()->createFromJson(id, + obj["tutorial"].toObject())) { logger.warning() << "Unable to add the tutorial"; return nullptr; } @@ -31,8 +32,8 @@ AddonTutorial::AddonTutorial(QObject* parent, const QString& manifestFileName, MVPN_COUNT_CTOR(AddonTutorial); } -AddonTutorial::~AddonTutorial() { MVPN_COUNT_DTOR(AddonTutorial); } +AddonTutorial::~AddonTutorial() { + MVPN_COUNT_DTOR(AddonTutorial); -void AddonTutorial::run() { - // Nothing to do here. + TutorialModel::instance()->remove(id()); } diff --git a/src/addons/addontutorial.h b/src/addons/addontutorial.h index b3da435369..1b45f529e8 100644 --- a/src/addons/addontutorial.h +++ b/src/addons/addontutorial.h @@ -20,8 +20,6 @@ class AddonTutorial final : public Addon { ~AddonTutorial(); - void run() override; - private: AddonTutorial(QObject* parent, const QString& manifestFileName, const QString& id, const QString& name); diff --git a/src/inspector/inspectorhandler.cpp b/src/inspector/inspectorhandler.cpp index 5b33c34bef..44ea8d773d 100644 --- a/src/inspector/inspectorhandler.cpp +++ b/src/inspector/inspectorhandler.cpp @@ -910,12 +910,6 @@ static QList s_commands{ AddonManager::instance()->unload(arguments[1]); return QJsonObject(); }}, - - InspectorCommand{"run_addon", "Run an add-on", 1, - [](InspectorHandler*, const QList& arguments) { - AddonManager::instance()->run(arguments[1]); - return QJsonObject(); - }}, }; // static diff --git a/src/models/guidemodel.cpp b/src/models/guidemodel.cpp index 954164025a..6eb810e0d8 100644 --- a/src/models/guidemodel.cpp +++ b/src/models/guidemodel.cpp @@ -36,23 +36,37 @@ GuideModel::~GuideModel() { MVPN_COUNT_DTOR(GuideModel); } QStringList GuideModel::guideTitleIds() const { QStringList guides; - for (const Guide* guide : m_guides) { - guides.append(guide->titleId()); + for (const GuideData& guideData : m_guides) { + guides.append(guideData.m_guide->titleId()); } return guides; } -bool GuideModel::createFromJson(const QJsonObject& obj) { +bool GuideModel::createFromJson(const QString& addonId, + const QJsonObject& obj) { Guide* guide = Guide::create(this, obj); if (guide) { - m_guides.append(guide); + beginResetModel(); + m_guides.append({addonId, guide}); + endResetModel(); return true; } return false; } +void GuideModel::remove(const QString& addonId) { + for (auto i = m_guides.begin(); i != m_guides.end(); ++i) { + if (i->m_addonId == addonId) { + beginResetModel(); + m_guides.erase(i); + endResetModel(); + break; + } + } +} + QHash GuideModel::roleNames() const { QHash roles; roles[GuideRole] = "guide"; @@ -68,7 +82,7 @@ QVariant GuideModel::data(const QModelIndex& index, int role) const { switch (role) { case GuideRole: - return QVariant::fromValue(m_guides.at(index.row())); + return QVariant::fromValue(m_guides.at(index.row()).m_guide); default: return QVariant(); diff --git a/src/models/guidemodel.h b/src/models/guidemodel.h index 95b1799cd0..a88e314dcc 100644 --- a/src/models/guidemodel.h +++ b/src/models/guidemodel.h @@ -34,7 +34,9 @@ class GuideModel final : public QAbstractListModel { QStringList guideTitleIds() const; - bool createFromJson(const QJsonObject& obj); + bool createFromJson(const QString& addonId, const QJsonObject& obj); + + void remove(const QString& addonId); // QAbstractListModel methods @@ -47,7 +49,11 @@ class GuideModel final : public QAbstractListModel { private: explicit GuideModel(QObject* parent); - QList m_guides; + struct GuideData { + QString m_addonId; + Guide* m_guide; + }; + QList m_guides; }; #endif // GUIDEMODEL_H diff --git a/src/models/tutorialmodel.cpp b/src/models/tutorialmodel.cpp index 7b2b598526..a90a0a7522 100644 --- a/src/models/tutorialmodel.cpp +++ b/src/models/tutorialmodel.cpp @@ -35,18 +35,42 @@ TutorialModel::TutorialModel(QObject* parent) : QAbstractListModel(parent) { TutorialModel::~TutorialModel() { MVPN_COUNT_DTOR(TutorialModel); } -bool TutorialModel::createFromJson(const QJsonObject& obj) { +bool TutorialModel::createFromJson(const QString& addonId, + const QJsonObject& obj) { logger.debug() << "Creation from json"; Tutorial* tutorial = Tutorial::create(this, obj); if (tutorial) { - m_tutorials.append(tutorial); + beginResetModel(); + m_tutorials.append({addonId, tutorial}); + endResetModel(); + + if (tutorial->highlighted()) { + emit highlightedTutorialChanged(); + } return true; } return false; } +void TutorialModel::remove(const QString& addonId) { + for (auto i = m_tutorials.begin(); i != m_tutorials.end(); ++i) { + if (i->m_addonId == addonId) { + bool wasHighlighted = i->m_tutorial->highlighted(); + + beginResetModel(); + m_tutorials.erase(i); + endResetModel(); + + if (wasHighlighted) { + emit highlightedTutorialChanged(); + } + break; + } + } +} + QHash TutorialModel::roleNames() const { QHash roles; roles[TutorialRole] = "tutorial"; @@ -64,7 +88,7 @@ QVariant TutorialModel::data(const QModelIndex& index, int role) const { switch (role) { case TutorialRole: - return QVariant::fromValue(m_tutorials.at(index.row())); + return QVariant::fromValue(m_tutorials.at(index.row()).m_tutorial); default: return QVariant(); @@ -122,9 +146,9 @@ void TutorialModel::requireTooltipShown(Tutorial* tutorial, bool shown) { } Tutorial* TutorialModel::highlightedTutorial() const { - for (Tutorial* tutorial : m_tutorials) { - if (tutorial->highlighted()) { - return tutorial; + for (const TutorialData& tutorialData : m_tutorials) { + if (tutorialData.m_tutorial->highlighted()) { + return tutorialData.m_tutorial; } } return nullptr; diff --git a/src/models/tutorialmodel.h b/src/models/tutorialmodel.h index ac3b1fcb29..965498c7cf 100644 --- a/src/models/tutorialmodel.h +++ b/src/models/tutorialmodel.h @@ -18,7 +18,8 @@ class TutorialModel final : public QAbstractListModel { Q_DISABLE_COPY_MOVE(TutorialModel) Q_PROPERTY(bool tooltipShown MEMBER m_tooltipShown NOTIFY tooltipShownChanged) Q_PROPERTY(bool playing READ isPlaying NOTIFY playingChanged) - Q_PROPERTY(Tutorial* highlightedTutorial READ highlightedTutorial CONSTANT) + Q_PROPERTY(Tutorial* highlightedTutorial READ highlightedTutorial NOTIFY + highlightedTutorialChanged) public: enum ModelRoles { @@ -43,7 +44,8 @@ class TutorialModel final : public QAbstractListModel { Tutorial* highlightedTutorial() const; - bool createFromJson(const QJsonObject& obj); + bool createFromJson(const QString& addonId, const QJsonObject& obj); + void remove(const QString& addonId); // QAbstractListModel methods @@ -60,10 +62,18 @@ class TutorialModel final : public QAbstractListModel { void tooltipShownChanged(); void tutorialCompleted(const QString& completionMessageText); + signals: + void highlightedTutorialChanged(); + private: explicit TutorialModel(QObject* parent); - QList m_tutorials; + struct TutorialData { + QString m_addonId; + Tutorial* m_tutorial; + }; + + QList m_tutorials; Tutorial* m_currentTutorial = nullptr; QStringList m_allowedItems; From 1b77b1ec579d6f856ac16cb62fbdaaa1e4c7f399 Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Mon, 6 Jun 2022 16:16:55 +0200 Subject: [PATCH 08/20] Update scripts/utils/generate_strings.py Co-authored-by: Francesco Lodolo --- scripts/utils/generate_strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/utils/generate_strings.py b/scripts/utils/generate_strings.py index 242734b3d9..d9f95ffcc0 100755 --- a/scripts/utils/generate_strings.py +++ b/scripts/utils/generate_strings.py @@ -221,7 +221,7 @@ def serialize(string): # If no source was provided, find it relative to this script file. if args.source is None: rootpath = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) - args.source = os.path.join('translations', 'strings.yaml') + args.source = os.path.join(rootpath, 'translations', 'strings.yaml') # If no output directory was provided, use the current directory. if args.output is None: From 8944fa672fadf12a3ff17b3509973d05ed967ed9 Mon Sep 17 00:00:00 2001 From: Francesco Lodolo Date: Mon, 6 Jun 2022 16:36:20 +0200 Subject: [PATCH 09/20] Fixes for PR3638 (#3680) --- .github/workflows/translations.yaml | 8 +++-- .gitignore | 9 ++++-- scripts/addon/build.py | 41 +++++++++++++------------ scripts/utils/generate_strings.py | 46 +++++++++++++++++++++-------- scripts/utils/generate_ts.sh | 29 ++++++++++++++++-- 5 files changed, 93 insertions(+), 40 deletions(-) diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 640c220c01..282c77010b 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -43,7 +43,9 @@ jobs: ./scripts/utils/generate_ts.sh - name: Uploading - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v3 with: - name: Translation file - path: translations.ts + name: Translation files + path: | + translations.ts + addon_ts/*.ts diff --git a/.gitignore b/.gitignore index a60b2a6748..34a2470d2a 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,9 @@ linux/netfilter/vendor/ __pycache__ lottie.mjs +# Translation pipeline +addon_ts/ + # CMake build artifacts cmake_install.cmake CMakeCache.txt @@ -47,7 +50,7 @@ src/mozillavpn_autogen/ node_modules #Github Codespaces -.venv +.venv asset_catalog_compiler.Info.plist @@ -60,7 +63,7 @@ android/local.properties android/**/*.cxx android/.cxx/* !android/res/debug/**/* -!android/*/**/Makefile +!android/*/**/Makefile xcode.xconfig .gradle/ .gradle_cache/ @@ -130,7 +133,7 @@ windows/split-tunnel/*.sys windows/split-tunnel/*.dll windows/split-tunnel/.status -#Rust +# Rust .cargo_home/ diff --git a/scripts/addon/build.py b/scripts/addon/build.py index d2cbae638c..591e8ebed8 100755 --- a/scripts/addon/build.py +++ b/scripts/addon/build.py @@ -6,7 +6,7 @@ import argparse import json import os -import xml.etree.ElementTree as ET +from lxml import etree as ET import tempfile import shutil import sys @@ -58,9 +58,9 @@ def retrieve_strings_tutorial(manifest, filename): } for step in tutorial_json["steps"]: - if not "id" in step: + if "id" not in step: exit(f"Tutorial {filename} does not have an id for one of the steps") - if not "tooltip" in step: + if "tooltip" not in step: exit( f"Tutorial {filename} does not have a tooltip for step id {step['id']}" ) @@ -81,13 +81,13 @@ def retrieve_strings_guide(manifest, filename): guide_strings = {} guide_json = manifest["guide"] - if not "id" in guide_json: + if "id" not in guide_json: exit(f"Guide {filename} does not have an id") - if not "title" in guide_json: + if "title" not in guide_json: exit(f"Guide {filename} does not have a title") - if not "subtitle" in guide_json: + if "subtitle" not in guide_json: exit(f"Guide {filename} does not have a subtitle") - if not "blocks" in guide_json: + if "blocks" not in guide_json: exit(f"Guide {filename} does not have a blocks") guide_id = guide_json["id"] @@ -103,11 +103,11 @@ def retrieve_strings_guide(manifest, filename): } for block in guide_json["blocks"]: - if not "id" in block: + if "id" not in block: exit(f"Guide {filename} does not have an id for one of the blocks") - if not "type" in block: + if "type" not in block: exit(f"Guide {filename} does not have a type for block id {block['id']}") - if not "content" in block: + if "content" not in block: exit(f"Guide {filename} does not have a content for block id {block['id']}") block_id = block["id"] @@ -124,11 +124,11 @@ def retrieve_strings_guide(manifest, filename): continue for subblock in block["content"]: - if not "id" in subblock: + if "id" not in subblock: exit( f"Guide {filename} does not have an id for one of the subblocks of block {block_id}" ) - if not "content" in subblock: + if "content" not in subblock: exit( f"Guide file {filename} does not have a content for subblock id {subblock['id']}" ) @@ -160,6 +160,9 @@ def write_en_language(filename, strings): message = ET.SubElement(context, "message") message.set("id", key) + location = ET.SubElement(message, "location") + location.set("filename", "addon.qml") + source = ET.SubElement(message, "source") source.text = value["value"] @@ -171,7 +174,7 @@ def write_en_language(filename, strings): extracomment.text = value["comments"] with open(filename, "w", encoding="utf-8") as f: - f.write(ET.tostring(ts, encoding="unicode")) + f.write(ET.tostring(ts, encoding="unicode", pretty_print=True)) def copy_files(path, dest_path): @@ -182,7 +185,7 @@ def copy_files(path, dest_path): file_path = os.path.join(path, file) if os.path.isfile(file_path): if file_path.endswith((".ts", ".qrc", ".rcc")): - exit(f"Unexpected {ext} file found: {os.path.join(path, file)}") + exit(f"Unexpected extension file found: {os.path.join(path, file)}") shutil.copyfile(file_path, os.path.join(dest_path, file)) continue @@ -298,11 +301,11 @@ def qtquery(qmake, propname): with open(args.source, "r", encoding="utf-8") as file: manifest = json.load(file) - print(f"Copying files in a temporary folder...") + print("Copying files in a temporary folder...") tmp_path = tempfile.mkdtemp() copy_files(os.path.dirname(args.source), tmp_path) - print(f"Retrieving strings...") + print("Retrieving strings...") strings = {} if manifest["type"] == "tutorial": strings = retrieve_strings_tutorial(manifest, args.source) @@ -311,7 +314,7 @@ def qtquery(qmake, propname): else: exit(f"Unupported manifest type `{manifest['type']}`") - print(f"Create localization file...") + print("Create localization file...") os.mkdir(os.path.join(tmp_path, "i18n")) template_ts_file = os.path.join(args.dest, f"{manifest['id']}.ts") write_en_language(template_ts_file, strings) @@ -340,7 +343,7 @@ def qtquery(qmake, propname): os.system(f"{lconvert} -if xlf -i {xliff_path} -o {locale_file}") os.system(f"{lrelease} -idbased {locale_file}") - print(f"Generate the RC file...") + print("Generate the RC file...") files = get_file_list(tmp_path, "") qrc_file = os.path.join(tmp_path, f"{manifest['id']}.qrc") @@ -353,7 +356,7 @@ def qtquery(qmake, propname): elm.text = file f.write(ET.tostring(rcc_elm, encoding="unicode")) - print(f"Creating the final addon...") + print("Creating the final addon...") rcc_file = os.path.join(args.dest, f"{manifest['id']}.rcc") os.system(f"{rcc} {qrc_file} -o {rcc_file} -binary") print(f"Done: {rcc_file}") diff --git a/scripts/utils/generate_strings.py b/scripts/utils/generate_strings.py index d9f95ffcc0..ffde41b469 100755 --- a/scripts/utils/generate_strings.py +++ b/scripts/utils/generate_strings.py @@ -7,6 +7,7 @@ import yaml import argparse + def stop(string_id): exit( f"Each key must be a string or a list with 1 or more items. Fix string ID `{string_id}`" @@ -34,6 +35,7 @@ def construct_mapping(self, node, deep=False): mapping.append(key) return super().construct_mapping(node, deep) + def parseTranslationStrings(yamlfile): if not os.path.isfile(yamlfile): exit(f"Unable to find {yamlfile}") @@ -108,9 +110,10 @@ def parseTranslationStrings(yamlfile): "value": value, "comments": comments, } - + return yaml_strings + # Render a dictionary of strings into the l18nstrings module. def generateStrings(strings, outdir): os.makedirs(outdir, exist_ok=True) @@ -162,7 +165,9 @@ class L18nStrings final : public QQmlPropertyMap { """ ) - with open(os.path.join(outdir, "l18nstrings_p.cpp"), "w", encoding="utf-8") as output: + with open( + os.path.join(outdir, "l18nstrings_p.cpp"), "w", encoding="utf-8" + ) as output: output.write( """/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -204,31 +209,46 @@ def serialize(string): # Generate the retranslate() method. output.write("void L18nStrings::retranslate() {\n") for key in strings: - output.write(f" insert(\"{key}\", qtTrId(_ids[{key}]));\n") + output.write(f' insert("{key}", qtTrId(_ids[{key}]));\n') output.write("}") if __name__ == "__main__": # Parse arguments to locate the input and output files. parser = argparse.ArgumentParser( - description='Generate internationaliation strings database from a YAML source') - parser.add_argument('source', metavar='SOURCE', type=str, action='store', nargs='?', - help='YAML strings file to process') - parser.add_argument('-o', '--output', metavar='DIR', type=str, action='store', - help='Output directory for generated files') + description="Generate internationaliation strings database from a YAML source" + ) + parser.add_argument( + "source", + metavar="SOURCE", + type=str, + action="store", + nargs="?", + help="YAML strings file to process", + ) + parser.add_argument( + "-o", + "--output", + metavar="DIR", + type=str, + action="store", + help="Output directory for generated files", + ) args = parser.parse_args() # If no source was provided, find it relative to this script file. if args.source is None: - rootpath = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) - args.source = os.path.join(rootpath, 'translations', 'strings.yaml') - + rootpath = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir, os.pardir) + ) + args.source = os.path.join(rootpath, "translations", "strings.yaml") + # If no output directory was provided, use the current directory. if args.output is None: args.output = os.getcwd() - + # Parse the inputs for their sweet juicy strings. strings = parseTranslationStrings(args.source) - + # Render the strings into generated content. generateStrings(strings, args.output) diff --git a/scripts/utils/generate_ts.sh b/scripts/utils/generate_ts.sh index dfb89fe2cc..ae4e22fe58 100755 --- a/scripts/utils/generate_ts.sh +++ b/scripts/utils/generate_ts.sh @@ -43,15 +43,40 @@ print G "done" print Y "Generating the main translation file... " lupdate translations/generated/dummy.pro -ts translations.ts || die -for branch in $(git branch -r | grep origin/releases); do - printn Y "Importing strings from $branch..." +printn Y "Generating strings for addons... " +python scripts/addon/generate_all.py +mkdir -p addon_ts || die +cp addons/generated/addons/*.ts addon_ts +print G "done." +for branch in $(git branch -r | grep origin/releases); do git checkout $branch &>/dev/null || die + + printn Y "Importing main strings from $branch..." python cache/generate_strings.py -o translations/generated || die lupdate translations/generated/dummy.pro -ts branch.ts || die lconvert -i translations.ts branch.ts -o tmp.ts || die mv tmp.ts translations.ts || die rm branch.ts || die + + if [ -f "scripts/addon/generate_all.py" ]; then + printn Y "Importing addon strings from $branch..." + python scripts/addon/generate_all.py + ts_files="addons/generated/addons/*.ts" + for f in $ts_files + do + ts_name=$(basename "$f") + if [ -f "addon_ts/${ts_name}"]; then + printn Y "File ${ts_name} exists, updating with branch strings..." + lconvert -i "cache_rs/${ts_name}" "addons/generated/addons/${ts_name}" -o tmp.ts || die + mv tmp.ts "addon_ts/${ts_name}" + else + printn Y "File ${ts_name} does not exist, copying over..." + cp "addons/generated/addons/${ts_name}" addon_ts/ + fi + done + rm addons/generated/addons/*.ts || die + fi done printn Y "Remove cache... " From 8b013c8f1ac21a49e3830cfa4d8130f092700d8b Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Mon, 6 Jun 2022 21:06:58 +0200 Subject: [PATCH 10/20] Fix the unit-tests --- tests/unit/testguide.cpp | 2 +- tests/unit/testtutorial.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/testguide.cpp b/tests/unit/testguide.cpp index a135e72d9a..f254b68ab1 100644 --- a/tests/unit/testguide.cpp +++ b/tests/unit/testguide.cpp @@ -165,7 +165,7 @@ void TestGuide::model() { QVERIFY(guideFile.open(QIODevice::ReadOnly | QIODevice::Text)); QJsonDocument json = QJsonDocument::fromJson(guideFile.readAll()); QVERIFY(json.isObject()); - mg->createFromJson(json.object()); + mg->createFromJson("test", json.object()); QCOMPARE(mg->rowCount(QModelIndex()), 1); QCOMPARE(mg->data(QModelIndex(), GuideModel::GuideRole), QVariant()); diff --git a/tests/unit/testtutorial.cpp b/tests/unit/testtutorial.cpp index 7618427b06..e15861c069 100644 --- a/tests/unit/testtutorial.cpp +++ b/tests/unit/testtutorial.cpp @@ -24,7 +24,7 @@ void TestTutorial::model() { QVERIFY(tutorialFile.open(QIODevice::ReadOnly | QIODevice::Text)); QJsonDocument json = QJsonDocument::fromJson(tutorialFile.readAll()); QVERIFY(json.isObject()); - mg->createFromJson(json.object()); + mg->createFromJson("test", json.object()); QCOMPARE(mg->rowCount(QModelIndex()), 1); QCOMPARE(mg->data(QModelIndex(), TutorialModel::TutorialRole), QVariant()); From 075ab295e61c44dceff669a95d6f8d77842b8c49 Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Mon, 6 Jun 2022 22:49:28 +0200 Subject: [PATCH 11/20] Add python3-lxml as dep for linux packages --- linux/debian/control.beineri | 1 + linux/debian/control.focal | 1 + linux/debian/control.qt5 | 1 + linux/debian/control.qt6 | 1 + 4 files changed, 4 insertions(+) diff --git a/linux/debian/control.beineri b/linux/debian/control.beineri index 32fbf4ebc3..16d8d38336 100644 --- a/linux/debian/control.beineri +++ b/linux/debian/control.beineri @@ -9,6 +9,7 @@ Build-Depends: debhelper (>= 9.20160709), golang (>=2:1.13~) | golang-1.13, cargo, python3-yaml, + python3-lxml, qt515base (>=5.15.2-1basyskom4), qt515declarative (>= 5.15.2-1basyskom1), qt515graphicaleffects (>= 5.15.2-1basyskom1), diff --git a/linux/debian/control.focal b/linux/debian/control.focal index 99dd7b129f..379ac7ae8f 100644 --- a/linux/debian/control.focal +++ b/linux/debian/control.focal @@ -9,6 +9,7 @@ Build-Depends: debhelper (>= 9.20160709), golang (>=1.13), cargo, python3-yaml, + python3-lxml, qt515base (>=5.15.2-1basyskom4), qt515declarative (>= 5.15.2-1basyskom1), qt515graphicaleffects (>= 5.15.2-1basyskom1), diff --git a/linux/debian/control.qt5 b/linux/debian/control.qt5 index eb4e4e7f54..13c8a13877 100644 --- a/linux/debian/control.qt5 +++ b/linux/debian/control.qt5 @@ -9,6 +9,7 @@ Build-Depends: debhelper (>= 9.20160709), golang (>=2:1.13~) | golang-1.13, cargo, python3-yaml, + python3-lxml, libqt5networkauth5-dev (>=5.15.2), libqt5websockets5-dev (>=5.15.2), qtbase5-dev (>=5.15.2), diff --git a/linux/debian/control.qt6 b/linux/debian/control.qt6 index 1eee88d582..da838a0950 100644 --- a/linux/debian/control.qt6 +++ b/linux/debian/control.qt6 @@ -10,6 +10,7 @@ Build-Depends: debhelper (>= 9.20160709), golang (>=2:1.13~) | golang-1.13, cargo, python3-yaml, + python3-lxml, libgl-dev, libopengl-dev (>= 1.3.0~) | libglvnd-dev (<< 1.3.0~), libqt6core5compat6-dev (>=6.2.0~), From 1d20a5287da52a7e497a82c76764c7554ec2a7b6 Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Tue, 7 Jun 2022 07:53:47 +0200 Subject: [PATCH 12/20] Fix a missing path --- scripts/utils/generate_ts.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/utils/generate_ts.sh b/scripts/utils/generate_ts.sh index ae4e22fe58..508ecae71d 100755 --- a/scripts/utils/generate_ts.sh +++ b/scripts/utils/generate_ts.sh @@ -26,6 +26,7 @@ python cache/generate_strings.py -o translations/generated print G "done." printn Y "Generating a dummy PRO file... " +mkdir -p translations/generated || die cat > translations/generated/dummy.pro << EOF HEADERS += l18nstrings.h SOURCES += l18nstrings_p.cpp From 20c5183b95264f7380da29d1f57a8614e376dd1a Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Tue, 7 Jun 2022 13:19:58 +0200 Subject: [PATCH 13/20] Fix the path for macos addons --- src/addonmanager.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/addonmanager.cpp b/src/addonmanager.cpp index abdebc0b9a..30cb76d0dc 100644 --- a/src/addonmanager.cpp +++ b/src/addonmanager.cpp @@ -50,7 +50,7 @@ void AddonManager::loadAll() { addonPath = QString("%1/addons").arg(QCoreApplication::applicationDirPath()); // TODO #elif defined(MVPN_MACOS) - addonPath = QString("%1/../Contents/Release/addons") + addonPath = QString("%1/../Resources/addons") .arg(QCoreApplication::applicationDirPath()); #elif defined(MVPN_IOS) addonPath = QString("%1/addons").arg(QCoreApplication::applicationDirPath()); @@ -78,8 +78,8 @@ void AddonManager::loadAll() { } void AddonManager::loadAll(const QDir& path) { - for (const QString& file : - path.entryList(QStringList{"*.rcc"}, QDir::Files)) { + qDebug() << path; + for (const QString& file : path.entryList(QStringList{"*"}, QDir::Files)) { load(path.filePath(file)); } } From 7aa368351210e518ef6304e52644c30c8f6229b8 Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Thu, 9 Jun 2022 14:46:36 +0200 Subject: [PATCH 14/20] Fix the strin generation script --- scripts/utils/generate_ts.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/utils/generate_ts.sh b/scripts/utils/generate_ts.sh index 508ecae71d..9691bb6222 100755 --- a/scripts/utils/generate_ts.sh +++ b/scripts/utils/generate_ts.sh @@ -54,7 +54,7 @@ for branch in $(git branch -r | grep origin/releases); do git checkout $branch &>/dev/null || die printn Y "Importing main strings from $branch..." - python cache/generate_strings.py -o translations/generated || die + python cache/generate_strings.py -o translations/generated translations/strings.yaml || die lupdate translations/generated/dummy.pro -ts branch.ts || die lconvert -i translations.ts branch.ts -o tmp.ts || die mv tmp.ts translations.ts || die From cd2c3f3cb9bd155a053c9e54bc90d155c295915a Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Thu, 9 Jun 2022 15:44:58 +0200 Subject: [PATCH 15/20] Remove the addon folder --- .github/workflows/macos-build.yaml | 6 --- CMakeLists.txt | 3 -- addons/CMakeLists.txt | 44 ------------------- scripts/android/package.sh | 3 -- scripts/linux/ppa_script.sh | 3 -- scripts/macos/apple_compile.sh | 3 -- scripts/wasm/compile.sh | 13 ------ scripts/windows/compile.bat | 3 -- src/cmake/linux.cmake | 2 - src/qmake/platforms/android.pri | 5 --- src/qmake/platforms/ios.pri | 4 -- src/qmake/platforms/linux.pri | 7 --- src/qmake/platforms/macos.pri | 5 --- src/qmake/platforms/wasm.pri | 1 - src/qmake/platforms/windows.pri | 5 --- .../scripts/build/android_build_debug.sh | 2 - .../scripts/build/android_build_release.sh | 3 -- taskcluster/scripts/build/wasm.sh | 2 - taskcluster/scripts/build/windows.ps1 | 1 - windows/installer/CMakeLists.txt | 5 +-- windows/installer/MozillaVPN.wxs | 5 +-- windows/installer/MozillaVPN_cmake.wxs | 5 +-- windows/installer/MozillaVPN_prod.wxs | 5 +-- windows/installer/build.cmd | 5 +-- windows/installer/build_prod.cmd | 5 +-- 25 files changed, 9 insertions(+), 136 deletions(-) delete mode 100644 addons/CMakeLists.txt diff --git a/.github/workflows/macos-build.yaml b/.github/workflows/macos-build.yaml index 1dfdf7a7a0..637a251a58 100644 --- a/.github/workflows/macos-build.yaml +++ b/.github/workflows/macos-build.yaml @@ -47,12 +47,6 @@ jobs: pip3 install -r requirements.txt python3 scripts/utils/generate_glean.py - - name: Generating addons - shell: bash - run: | - export PATH=/opt/qt6/bin:$PATH - python3 scripts/addon/generate_all.py - - name: Importing translation files shell: bash run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d63229272..c3fe8eb5c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,9 +81,6 @@ if(NOT CMAKE_CROSSCOMPILING) add_subdirectory(extension) endif() -# Addons -add_subdirectory(addons) - # Extra platform stuff if(WIN32) add_subdirectory(windows/installer) diff --git a/addons/CMakeLists.txt b/addons/CMakeLists.txt deleted file mode 100644 index fe91c15c28..0000000000 --- a/addons/CMakeLists.txt +++ /dev/null @@ -1,44 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -add_library(addons STATIC) - -find_package(Qt6 REQUIRED COMPONENTS Core Qml) -target_link_libraries(addons PRIVATE Qt6::Core Qt6::Qml) - -get_filename_component(MVPN_SCRIPT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../scripts ABSOLUTE) -get_filename_component(GENERATED_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated ABSOLUTE) -file(MAKE_DIRECTORY ${GENERATED_DIR}) -target_include_directories(addons PUBLIC ${GENERATED_DIR}) - -file(GLOB_RECURSE - manifests - RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} - "${CMAKE_CURRENT_SOURCE_DIR}/*/manifest.json" -) - -foreach(manifest ${manifests}) - string(REGEX REPLACE "/manifest.json$" ".rcc" addon_name ${manifest}) - if (${manifest} MATCHES "tutorial_*" OR ${manifest} MATCHES "guide_*") - add_custom_command( - OUTPUT ${GENERATED_DIR}/${addon_name} - DEPENDS ${manifest} - COMMAND python3 ${MVPN_SCRIPT_DIR}/addon/build.py - ${CMAKE_CURRENT_SOURCE_DIR}/${manifest} ${GENERATED_DIR} - ) - list(APPEND list_addons "${GENERATED_DIR}/${addon_name}") - endif() -endforeach() - -add_custom_target(generate_all_addons - ALL - DEPENDS ${list_addons} -) - -if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") - include(GNUInstallDirs) - install(FILES ${list_addons} DESTINATION ${CMAKE_INSTALL_DATADIR}/mozillavpn/addons) -elseif(WIN32) - install(FILES ${list_addons} DESTINATION addons) -endif() diff --git a/scripts/android/package.sh b/scripts/android/package.sh index 4dbd6f36fb..b51dbe769f 100755 --- a/scripts/android/package.sh +++ b/scripts/android/package.sh @@ -122,9 +122,6 @@ python3 scripts/utils/import_languages.py || die "Failed to import languages" print Y "Generating glean samples..." python3 scripts/utils/generate_glean.py -j "android/src/" || die "Failed to generate glean samples" -print Y "Generate all the addons..." -python3 ./scripts/addon/generate_all.py || die "Failed to generate addons" - print Y "Copy and patch Adjust SDK..." rm -rf "android/src/com/adjust" || die "Failed to remove the adjust folder" cp -a "3rdparty/adjust-android-sdk/Adjust/sdk-core/src/main/java/com/." "android/src/com/" || die "Failed to copy the adjust codebase" diff --git a/scripts/linux/ppa_script.sh b/scripts/linux/ppa_script.sh index b64d4bd9f4..9878664bea 100755 --- a/scripts/linux/ppa_script.sh +++ b/scripts/linux/ppa_script.sh @@ -119,9 +119,6 @@ else print Y "Generating glean samples..." python3 scripts/utils/generate_glean.py || die "Failed to generate glean samples" - print Y "Generating addons..." - python3 scripts/addon/generate_all.py || die "Failed to generate addons" - printn Y "Removing the debian template folder... " rm -rf linux/debian || die "Failed" print G "done." diff --git a/scripts/macos/apple_compile.sh b/scripts/macos/apple_compile.sh index e4728e9985..0a6894f98e 100755 --- a/scripts/macos/apple_compile.sh +++ b/scripts/macos/apple_compile.sh @@ -109,9 +109,6 @@ python3 scripts/utils/import_languages.py $([[ $QTBINPATH ]] && echo "-q $QTBINP print Y "Generating glean samples..." python3 scripts/utils/generate_glean.py || die "Failed to generate glean samples" -print Y "Generating addons..." -python3 scripts/addon/generate_all.py $([[ $QTBINPATH ]] && echo "-q $QTBINPATH") || die "Failed to generate addons" - printn Y "Extract the project version... " SHORTVERSION=$(cat version.pri | grep VERSION | grep defined | cut -d= -f2 | tr -d \ ) FULLVERSION=$(echo $SHORTVERSION | cut -d. -f1).$(date +"%Y%m%d%H%M") diff --git a/scripts/wasm/compile.sh b/scripts/wasm/compile.sh index a1bd0a104f..0af95ec25c 100755 --- a/scripts/wasm/compile.sh +++ b/scripts/wasm/compile.sh @@ -57,19 +57,6 @@ python3 scripts/utils/import_languages.py || die "Failed to import languages" print Y "Generating glean samples..." python3 scripts/utils/generate_glean.py || die "Failed to generate glean samples" -print Y "Generating addons..." -python3 ./scripts/addon/generate_all.py || die "Failed to generate addons" - -print Y "Merge addons..." -( - cd addons/generated/addons - echo '' - for i in *; do - echo "$i" - done - echo '' -) > addons/generated/addons/addons.qrc - printn Y "Mode: " MODE= if [ "$DEBUG" = 1 ]; then diff --git a/scripts/windows/compile.bat b/scripts/windows/compile.bat index 2c24304c51..a9db080311 100644 --- a/scripts/windows/compile.bat +++ b/scripts/windows/compile.bat @@ -76,9 +76,6 @@ python3 scripts\utils\import_languages.py ECHO Generating glean samples... python3 scripts\utils\generate_glean.py -ECHO Generating addons... -python3 scripts\addon\generate_all.py - ECHO Creating the project with flags: %FLAGS% if %DEBUG_BUILD% == T ( diff --git a/src/cmake/linux.cmake b/src/cmake/linux.cmake index 860e43c972..0e44005fb9 100644 --- a/src/cmake/linux.cmake +++ b/src/cmake/linux.cmake @@ -112,8 +112,6 @@ install(FILES platforms/linux/daemon/org.mozilla.vpn.conf install(FILES platforms/linux/daemon/org.mozilla.vpn.dbus.service DESTINATION ${CMAKE_INSTALL_DATADIR}/dbus-1/system-services) -add_definitions(-DADDONS_PATH=\"${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/mozillavpn/addons\") - ## This is only really needed when building from source. Otherwise, we ## expect the Distro's packaging magic to sort this out. pkg_check_modules(SYSTEMD systemd) diff --git a/src/qmake/platforms/android.pri b/src/qmake/platforms/android.pri index 7e2939b9eb..3d94e24966 100644 --- a/src/qmake/platforms/android.pri +++ b/src/qmake/platforms/android.pri @@ -118,8 +118,3 @@ DISTFILES += \ ../android/res/values/libs.xml ANDROID_PACKAGE_SOURCE_DIR = $$PWD/../../../android - -addons.files = $$PWD/../../../addons/generated/addons -addons.path = /assets -addons.CONFIG = no_check_exist executable -INSTALLS += addons diff --git a/src/qmake/platforms/ios.pri b/src/qmake/platforms/ios.pri index b588c32899..ece39a2c36 100644 --- a/src/qmake/platforms/ios.pri +++ b/src/qmake/platforms/ios.pri @@ -154,7 +154,3 @@ QMAKE_MAC_XCODE_SETTINGS += GROUP_ID_IOS DEVELOPMENT_TEAM.name = "DEVELOPMENT_TEAM" DEVELOPMENT_TEAM.value = "$$MVPN_DEVELOPMENT_TEAM" QMAKE_MAC_XCODE_SETTINGS += DEVELOPMENT_TEAM - -addons.files = $$PWD/../../../addons/generated/addons -addons.CONFIG = no_check_exist executable -QMAKE_BUNDLE_DATA += addons diff --git a/src/qmake/platforms/linux.pri b/src/qmake/platforms/linux.pri index 65e2c157f5..8303c523d4 100644 --- a/src/qmake/platforms/linux.pri +++ b/src/qmake/platforms/linux.pri @@ -172,10 +172,3 @@ INSTALLS += browserBridge CONFIG += link_pkgconfig PKGCONFIG += polkit-gobject-1 - -DEFINES += ADDONS_PATH=\\\"$${USRPATH}/share/mozillavpn/addons\\\" - -addons.files = $$PWD/../../../addons/generated/addons -addons.path = $${USRPATH}/share/mozillavpn -addons.CONFIG = no_check_exist executable -INSTALLS += addons diff --git a/src/qmake/platforms/macos.pri b/src/qmake/platforms/macos.pri index 95a2b82799..196f6ec3b1 100644 --- a/src/qmake/platforms/macos.pri +++ b/src/qmake/platforms/macos.pri @@ -98,11 +98,6 @@ extension_manifest.files = $$PWD/../../../extension/manifests/macos/mozillavpn.j extension_manifest.path = 'Contents/Resources/utils' QMAKE_BUNDLE_DATA += extension_manifest -addons.files = $$PWD/../../../addons/generated/addons -addons.path = 'Contents/Resources' -addons.CONFIG = no_check_exist executable -QMAKE_BUNDLE_DATA += addons - wireguardGo.input = WIREGUARDGO wireguardGo.output = ${QMAKE_FILE_IN}/wireguard-go wireguardGo.commands = @echo Compiling Wireguard GO ${QMAKE_FILE_IN} && \ diff --git a/src/qmake/platforms/wasm.pri b/src/qmake/platforms/wasm.pri index cfe1e344b0..99f6557c5f 100644 --- a/src/qmake/platforms/wasm.pri +++ b/src/qmake/platforms/wasm.pri @@ -44,4 +44,3 @@ HEADERS += \ SOURCES -= networkrequest.cpp RESOURCES += platforms/wasm/networkrequests.qrc -RESOURCES += ../addons/generated/addons/addons.qrc diff --git a/src/qmake/platforms/windows.pri b/src/qmake/platforms/windows.pri index 9bae07e286..a3c2e91fa7 100644 --- a/src/qmake/platforms/windows.pri +++ b/src/qmake/platforms/windows.pri @@ -155,8 +155,3 @@ mozillavpnnp.files = $$PWD/../../../mozillavpnnp.exe mozillavpnnp.path = $$PWD/../../../unsigned/ mozillavpnnp.CONFIG = no_check_exist executable INSTALLS += mozillavpnnp - -addons.files = $$PWD/../../../addons/generated/addons -addons.path = $$PWD/../../../unsigned -addons.CONFIG = no_check_exist executable -INSTALLS += addons diff --git a/taskcluster/scripts/build/android_build_debug.sh b/taskcluster/scripts/build/android_build_debug.sh index dfaddb3d07..7a8e22da2b 100755 --- a/taskcluster/scripts/build/android_build_debug.sh +++ b/taskcluster/scripts/build/android_build_debug.sh @@ -11,8 +11,6 @@ git submodule update ./scripts/utils/generate_glean.py # translations ./scripts/utils/import_languages.py -# addons -./scripts/addon/generate_all.py # $1 should be the qmake arch. # Note this is different from what aqt expects as arch: diff --git a/taskcluster/scripts/build/android_build_release.sh b/taskcluster/scripts/build/android_build_release.sh index b12001d329..d295e2a7c0 100755 --- a/taskcluster/scripts/build/android_build_release.sh +++ b/taskcluster/scripts/build/android_build_release.sh @@ -12,9 +12,6 @@ git submodule update # translations echo "Importing translations" ./scripts/utils/import_languages.py -# addons -echo "Generating addons..." -./scripts/addon/generate_all.py # Get Secrets for building echo "Fetching Tokens!" diff --git a/taskcluster/scripts/build/wasm.sh b/taskcluster/scripts/build/wasm.sh index 821be2ac75..98deba2cdc 100755 --- a/taskcluster/scripts/build/wasm.sh +++ b/taskcluster/scripts/build/wasm.sh @@ -18,8 +18,6 @@ pip3 install -r requirements.txt python3 ./scripts/utils/generate_glean.py # translations python3 ./scripts/utils/import_languages.py -# addons -python3 ./scripts/addon/generate_all.py # Add the Wasm qmake after import languages into the path, # Otherwise import_languages.py will search for lupdate diff --git a/taskcluster/scripts/build/windows.ps1 b/taskcluster/scripts/build/windows.ps1 index 2ba7ba403d..d148902725 100644 --- a/taskcluster/scripts/build/windows.ps1 +++ b/taskcluster/scripts/build/windows.ps1 @@ -33,7 +33,6 @@ Copy-Item -Path $env:VCToolsRedistDir\\MergeModules\\Microsoft_VC143_CRT_x86.msm # We need to pre-generate those resources here. python3 ./scripts/utils/generate_glean.py python3 ./scripts/utils/import_languages.py -python3 ./scripts/addon/generate_all.py ./scripts/windows/compile.bat --nmake nmake install diff --git a/windows/installer/CMakeLists.txt b/windows/installer/CMakeLists.txt index 6993f02333..11b536b88d 100644 --- a/windows/installer/CMakeLists.txt +++ b/windows/installer/CMakeLists.txt @@ -20,9 +20,8 @@ add_custom_target(msi WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/staging COMMAND ${CMAKE_COMMAND} -E echo "Building MSI installer for $" COMMAND ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR} --prefix ${CMAKE_CURRENT_BINARY_DIR}/staging --config $ - COMMAND ${WIX_BINARY_DIR}/heat dir addons -o ${CMAKE_CURRENT_BINARY_DIR}/addons.wxs -scon -sfrag -srd -sreg -gg -cg addons -dr MozillaVPNAddonFolder COMMAND ${WIX_BINARY_DIR}/candle ${WIX_CANDLE_FLAGS} -dPlatform=${WIX_PLATFORM} - -arch x64 ${CMAKE_CURRENT_SOURCE_DIR}/MozillaVPN_cmake.wxs ${CMAKE_CURRENT_BINARY_DIR}/addons.wxs - COMMAND ${WIX_BINARY_DIR}/light ${WIX_LIGHT_FLAGS} -b addons -out ${CMAKE_CURRENT_BINARY_DIR}/MozillaVPN.msi MozillaVPN_cmake.wixobj addons.wixobj + -out ${CMAKE_CURRENT_BINARY_DIR}/MozillaVPN.wixobj -arch x64 ${CMAKE_CURRENT_SOURCE_DIR}/MozillaVPN_cmake.wxs + COMMAND ${WIX_BINARY_DIR}/light ${WIX_LIGHT_FLAGS} -out ${CMAKE_CURRENT_BINARY_DIR}/MozillaVPN.msi ${CMAKE_CURRENT_BINARY_DIR}/MozillaVPN.wixobj ) set_directory_properties(PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES ${CMAKE_CURRENT_BINARY_DIR}/staging) diff --git a/windows/installer/MozillaVPN.wxs b/windows/installer/MozillaVPN.wxs index 0955d754b2..5ae08fc588 100644 --- a/windows/installer/MozillaVPN.wxs +++ b/windows/installer/MozillaVPN.wxs @@ -62,9 +62,7 @@ - - - + @@ -146,7 +144,6 @@ --> - diff --git a/windows/installer/MozillaVPN_cmake.wxs b/windows/installer/MozillaVPN_cmake.wxs index 5e06b6dad7..645c7811f6 100644 --- a/windows/installer/MozillaVPN_cmake.wxs +++ b/windows/installer/MozillaVPN_cmake.wxs @@ -58,9 +58,7 @@ - - - + @@ -137,7 +135,6 @@ --> - diff --git a/windows/installer/MozillaVPN_prod.wxs b/windows/installer/MozillaVPN_prod.wxs index 6d0b1b65ba..17db8c66e6 100644 --- a/windows/installer/MozillaVPN_prod.wxs +++ b/windows/installer/MozillaVPN_prod.wxs @@ -63,9 +63,7 @@ - - - + @@ -145,7 +143,6 @@ --> - diff --git a/windows/installer/build.cmd b/windows/installer/build.cmd index fed2c28a45..e91f1e8e8a 100644 --- a/windows/installer/build.cmd +++ b/windows/installer/build.cmd @@ -43,10 +43,9 @@ if exist .deps\prepared goto :build :msi if not exist "%~1" mkdir "%~1" echo [+] Compiling %1 - "%WIX%bin\heat" dir ..\..\addons\generated\addons -o addon.wxs -scon -sfrag -srd -sreg -gg -cg addons -dr MozillaVPNAddonFolder || exit /b %errorlevel% - "%WIX%bin\candle" %WIX_CANDLE_FLAGS% -dPlatform=%1 -arch %1 MozillaVPN.wxs addon.wxs || exit /b %errorlevel% + "%WIX%bin\candle" %WIX_CANDLE_FLAGS% -dPlatform=%1 -out "%~1\MozillaVPN.wixobj" -arch %1 MozillaVPN.wxs || exit /b %errorlevel% echo [+] Linking %1 - "%WIX%bin\light" %WIX_LIGHT_FLAGS% -out "%~1/MozillaVPN.msi" -b ..\..\addons\generated\addons MozillaVPN.wixobj addon.wixobj || exit /b %errorlevel% + "%WIX%bin\light" %WIX_LIGHT_FLAGS% -out "%~1/MozillaVPN.msi" "%~1\MozillaVPN.wixobj" || exit /b %errorlevel% goto :eof :error diff --git a/windows/installer/build_prod.cmd b/windows/installer/build_prod.cmd index 906b75b96b..0e3ed3c1d2 100644 --- a/windows/installer/build_prod.cmd +++ b/windows/installer/build_prod.cmd @@ -43,10 +43,9 @@ if exist .deps\prepared goto :build :msi if not exist "%~1" mkdir "%~1" echo [+] Compiling %1 - "%WIX%bin\heat" dir ..\..\unsigned\addons -o addon.wxs -scon -sfrag -srd -sreg -gg -cg addons -dr MozillaVPNAddonFolder || exit /b %errorlevel% - "%WIX%bin\candle" %WIX_CANDLE_FLAGS% -dPlatform=%1 -arch %1 MozillaVPN.wxs addon.wxs || exit /b %errorlevel% + "%WIX%bin\candle" %WIX_CANDLE_FLAGS% -dPlatform=%1 -out "%~1\MozillaVPN.wixobj" -arch %1 MozillaVPN_prod.wxs || exit /b %errorlevel% echo [+] Linking %1 - "%WIX%bin\light" %WIX_LIGHT_FLAGS% -out "%~1/MozillaVPN.msi" -b ..\..\unsigned\addons MozillaVPN.wixobj addon.wixobj || exit /b %errorlevel% + "%WIX%bin\light" %WIX_LIGHT_FLAGS% -out "%~1/MozillaVPN.msi" "%~1\MozillaVPN.wixobj" || exit /b %errorlevel% goto :eof :error From 687579bf65f8033ef88a07104a63af4b476cd8b7 Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Fri, 10 Jun 2022 11:55:30 +0200 Subject: [PATCH 16/20] Addon index --- scripts/addon/generate_all.py | 19 ++ src/addonmanager.cpp | 315 +++++++++++++++++++----- src/addonmanager.h | 30 ++- src/cmake/sources.cmake | 4 + src/constants.h | 2 + src/inspector/inspectorhandler.cpp | 15 +- src/models/guidemodel.cpp | 4 - src/models/tutorialmodel.cpp | 4 - src/mozillavpn.cpp | 7 +- src/qmake/sources.pri | 4 + src/tasks/addon/taskaddon.cpp | 41 +++ src/tasks/addon/taskaddon.h | 26 ++ src/tasks/addonindex/taskaddonindex.cpp | 38 +++ src/tasks/addonindex/taskaddonindex.h | 22 ++ tests/unit/CMakeLists.txt | 2 + tests/unit/unit.pro | 2 + 16 files changed, 456 insertions(+), 79 deletions(-) create mode 100644 src/tasks/addon/taskaddon.cpp create mode 100644 src/tasks/addon/taskaddon.h create mode 100644 src/tasks/addonindex/taskaddonindex.cpp create mode 100644 src/tasks/addonindex/taskaddonindex.h diff --git a/scripts/addon/generate_all.py b/scripts/addon/generate_all.py index e88dc05b49..d7f3733b7f 100755 --- a/scripts/addon/generate_all.py +++ b/scripts/addon/generate_all.py @@ -4,6 +4,8 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. import argparse +import hashlib +import json import os import subprocess import sys @@ -31,6 +33,7 @@ if not os.path.isdir(generated_path): os.mkdir(generated_path) +addons = [] for file in os.listdir(addons_path): if not file.startswith("tutorial_") and not file.startswith("guide_"): continue @@ -41,3 +44,19 @@ build_cmd.append("-q") build_cmd.append(args.qtpath) subprocess.call(build_cmd) + + generated_addon_path = os.path.join(generated_path, file + ".rcc") + if not os.path.exists(generated_addon_path): + exit(f"Expected addon file {generated_addon_path}") + + with open(generated_addon_path,"rb") as f: + sha256 = hashlib.sha256(f.read()).hexdigest(); + addons.append({ 'id': file, 'sha256': sha256 }) + +index = { + 'version': '0.1', + 'addons': addons, +} + +with open(os.path.join(generated_path, "manifest.json"), "w") as f: + f.write(json.dumps(index, indent=2)) diff --git a/src/addonmanager.cpp b/src/addonmanager.cpp index 30cb76d0dc..b9bee3d4a7 100644 --- a/src/addonmanager.cpp +++ b/src/addonmanager.cpp @@ -7,13 +7,22 @@ #include "leakdetector.h" #include "logger.h" #include "models/feature.h" +#include "taskscheduler.h" +#include "tasks/addon/taskaddon.h" #include +#include #include +#include +#include +#include #include #include #include +constexpr const char* ADDON_FOLDER = "addons"; +constexpr const char* ADDON_INDEX_FILENAME = "manifest.json"; + namespace { Logger logger(LOG_MAIN, "AddonManager"); @@ -26,7 +35,7 @@ AddonManager* s_instance = nullptr; AddonManager* AddonManager::instance() { if (!s_instance) { s_instance = new AddonManager(qApp); - s_instance->loadAll(); + s_instance->initialize(); } return s_instance; } @@ -37,81 +46,125 @@ AddonManager::AddonManager(QObject* parent) : QObject(parent) { AddonManager::~AddonManager() { MVPN_COUNT_DTOR(AddonManager); } -void AddonManager::loadAll() { +void AddonManager::initialize() { if (!Feature::get(Feature::Feature_addon)->isSupported()) { logger.warning() << "Addons disabled by feature flag"; return; } - QString addonPath; -#if defined(ADDONS_PATH) - addonPath = ADDONS_PATH; -#elif defined(MVPN_WINDOWS) - addonPath = - QString("%1/addons").arg(QCoreApplication::applicationDirPath()); // TODO -#elif defined(MVPN_MACOS) - addonPath = QString("%1/../Resources/addons") - .arg(QCoreApplication::applicationDirPath()); -#elif defined(MVPN_IOS) - addonPath = QString("%1/addons").arg(QCoreApplication::applicationDirPath()); -#elif defined(MVPN_ANDROID) - addonPath = QString("assets:/addons"); -#elif defined(MVPN_WASM) - addonPath = QString(":/addons"); -#endif - - logger.debug() << "Loading addon from" << addonPath; - - if (!addonPath.isEmpty()) { - QDir addonDir(addonPath); - addonDir.setSorting(QDir::Name); - loadAll(addonDir); - } - - if (!Constants::inProduction()) { - QDir homePath( - QStandardPaths::writableLocation(QStandardPaths::HomeLocation)); - homePath.cd(".mozillavpn_addons"); - homePath.setSorting(QDir::Name); - loadAll(homePath); + // Initialization of the addon folder. + { + QDir addonDir( + QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); + if (!addonDir.exists(ADDON_FOLDER) && !addonDir.mkdir(ADDON_FOLDER)) { + logger.info() << "Unable to create the addon folder"; + return; + } + } + + if (!validateIndex(readIndex())) { + logger.debug() << "Unable to validate the index"; } } -void AddonManager::loadAll(const QDir& path) { - qDebug() << path; - for (const QString& file : path.entryList(QStringList{"*"}, QDir::Files)) { - load(path.filePath(file)); +void AddonManager::updateIndex(const QByteArray& index) { + QByteArray currentIndex = readIndex(); + + if (currentIndex == index) { + logger.debug() << "The index has not changed"; + return; } + + if (!validateIndex(index)) { + logger.debug() << "Unable to validate the index"; + return; + } + + writeIndex(index); } -bool AddonManager::load(const QString& fileName) { - logger.debug() << "Load addon" << fileName; +bool AddonManager::validateIndex(const QByteArray& index) { + // TODO: signature validation - if (!Feature::get(Feature::Feature_addon)->isSupported()) { - logger.warning() << "Addons disabled by feature flag"; + QJsonDocument doc = QJsonDocument::fromJson(index); + if (!doc.isObject()) { + logger.debug() << "The index must be an object"; return false; } - QString addonId = QFileInfo(fileName).baseName(); - if (m_addons.contains(addonId)) { - logger.warning() << "Addon" << addonId << "already loaded"; + QJsonObject obj = doc.object(); + if (obj["version"].toString() != "0.1") { + logger.debug() << "Invalid index file - version does not match"; return false; } - if (!QResource::registerResource(fileName, "/addons")) { - logger.warning() << "Unable to load resource from file" << fileName; - return false; + QList addons; + for (const QJsonValue item : obj["addons"].toArray()) { + QJsonObject addonObj = item.toObject(); + + QString sha256hex = addonObj["sha256"].toString(); + if (sha256hex.isEmpty()) { + logger.warning() << "Incomplete index - sha256"; + return false; + } + + if (sha256hex.length() != 64) { + logger.warning() << "Invalid sha256 hash"; + return false; + } + + QString addonId = addonObj["id"].toString(); + if (addonId.isEmpty()) { + logger.warning() << "Incomplete index - addonId"; + return false; + } + + addons.append( + {QByteArray::fromHex(sha256hex.toLocal8Bit()), addonId, nullptr}); } - if (!loadManifest(QString(":/addons/%1/manifest.json").arg(addonId))) { - QResource::unregisterResource(fileName, "/addons"); - return false; + // Remove unknown addons + QStringList addonsToBeRemoved; + for (QHash::const_iterator i(m_addons.constBegin()); + i != m_addons.constEnd(); ++i) { + bool found = false; + for (const AddonData& addonData : addons) { + if (addonData.m_addonId == i.key()) { + found = true; + break; + } + } + + if (found) continue; + addonsToBeRemoved.append(i.key()); + } + + for (const QString& addonId : addonsToBeRemoved) { + unload(addonId); + removeAddon(addonId); + } + + // Fetch new addons + for (const AddonData& addonData : addons) { + if (!m_addons.contains(addonData.m_addonId) && + validateAndLoad(addonData.m_addonId, addonData.m_sha256)) { + Q_ASSERT(m_addons.contains(addonData.m_addonId)); + Q_ASSERT(m_addons[addonData.m_addonId].m_sha256 == addonData.m_sha256); + continue; + } + + if (!m_addons.contains(addonData.m_addonId) || + m_addons[addonData.m_addonId].m_sha256 != addonData.m_sha256) { + TaskScheduler::scheduleTask( + new TaskAddon(addonData.m_addonId, addonData.m_sha256)); + } } return true; } -bool AddonManager::loadManifest(const QString& manifestFileName) { +bool AddonManager::loadManifest(const QString& manifestFileName, + const QByteArray& sha256) { Addon* addon = Addon::create(this, manifestFileName); if (!addon) { logger.warning() << "Unable to create an addon from manifest" @@ -119,7 +172,7 @@ bool AddonManager::loadManifest(const QString& manifestFileName) { return false; } - m_addons.insert(addon->id(), addon); + m_addons.insert(addon->id(), {sha256, addon->id(), addon}); return true; } @@ -134,7 +187,7 @@ void AddonManager::unload(const QString& addonId) { return; } - Addon* addon = m_addons[addonId]; + Addon* addon = m_addons[addonId].m_addon; Q_ASSERT(addon); m_addons.remove(addonId); @@ -142,8 +195,160 @@ void AddonManager::unload(const QString& addonId) { } void AddonManager::retranslate() { - foreach (Addon* addon, m_addons) { + foreach (const AddonData& addonData, m_addons) { // This comment is here to make the linter happy. - addon->retranslate(); + addonData.m_addon->retranslate(); + } +} + +// static +bool AddonManager::addonDir(QDir* dir) { + Q_ASSERT(dir); + + QDir addonDir( + QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); + if (!addonDir.exists(ADDON_FOLDER)) { + return false; + } + + if (!addonDir.cd(ADDON_FOLDER)) { + logger.warning() << "Unable to open the addons folder"; + return false; + } + + *dir = addonDir; + return true; +} + +// static +QByteArray AddonManager::readIndex() { + QDir dir; + if (!addonDir(&dir)) { + return ""; + } + + QFile indexFile(dir.filePath(ADDON_INDEX_FILENAME)); + if (!indexFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + logger.warning() << "Unable to open the addon index"; + return ""; + } + + return indexFile.readAll(); +} + +// static +void AddonManager::writeIndex(const QByteArray& index) { + QDir dir; + if (!addonDir(&dir)) { + return; + } + + QFile indexFile(dir.filePath(ADDON_INDEX_FILENAME)); + if (!indexFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + logger.warning() << "Unable to open the addon index"; + return; + } + + if (!indexFile.write(index)) { + logger.warning() << "Unable to write the addon file"; + } +} + +// static +void AddonManager::removeAddon(const QString& addonId) { + QDir dir; + if (!addonDir(&dir)) { + return; + } + + QString addonFileName(QString("%1.rcc").arg(addonId)); + if (!dir.exists(addonFileName)) { + logger.warning() << "Addon does not exist" << addonFileName; + return; + } + + if (!dir.remove(addonFileName)) { + logger.warning() << "Unable to remove the addon file name"; + } +} + +bool AddonManager::validateAndLoad(const QString& addonId, + const QByteArray& sha256, bool checkSha256) { + logger.debug() << "Load addon" << addonId; + + if (m_addons.contains(addonId)) { + logger.warning() << "Addon" << addonId << "already loaded"; + return false; + } + + // Hash validation + QDir dir; + if (!addonDir(&dir)) { + return false; + } + + QString addonFileName(dir.filePath(QString("%1.rcc").arg(addonId))); + if (checkSha256) { + QFile addonFile(addonFileName); + if (!addonFile.open(QIODevice::ReadOnly)) { + logger.warning() << "Unable to open the addon file" << addonFileName; + return false; + } + + if (QCryptographicHash::hash(addonFile.readAll(), + QCryptographicHash::Sha256) != sha256) { + logger.warning() << "Addon hash does not match" << addonFileName; + return false; + } + } + + if (!QResource::registerResource(addonFileName, "/addons")) { + logger.warning() << "Unable to load resource from file" << addonFileName; + return false; + } + + if (!loadManifest(QString(":/addons/%1/manifest.json").arg(addonId), + sha256)) { + QResource::unregisterResource(addonFileName, "/addons"); + return false; + } + + return true; +} + +void AddonManager::storeAndLoadAddon(const QByteArray& addonData, + const QString& addonId, + const QByteArray& sha256) { + // Maybe we have to replace an existing addon. Let's start removing it. + if (m_addons.contains(addonId)) { + unload(addonId); + removeAddon(addonId); + } + + if (QCryptographicHash::hash(addonData, QCryptographicHash::Sha256) != + sha256) { + logger.warning() << "Invalid addon hash"; + return; + } + + QDir dir; + if (!addonDir(&dir)) { + return; + } + + QString addonFileName(dir.filePath(QString("%1.rcc").arg(addonId))); + QFile addonFile(addonFileName); + if (!addonFile.open(QIODevice::WriteOnly)) { + logger.warning() << "Unable to open the addon file" << addonFileName; + return; + } + + if (!addonFile.write(addonData)) { + logger.warning() << "Unable to write the addon file"; + return; + } + + if (!validateAndLoad(addonId, sha256, false)) { + logger.warning() << "Unable to load the addon"; } } diff --git a/src/addonmanager.h b/src/addonmanager.h index 889670238f..c06e9c45a1 100644 --- a/src/addonmanager.h +++ b/src/addonmanager.h @@ -21,8 +21,13 @@ class AddonManager final : public QObject { ~AddonManager(); - bool load(const QString& addonFileName); - bool loadManifest(const QString& addonManifestFileName); + void updateIndex(const QByteArray& index); + + void storeAndLoadAddon(const QByteArray& addonData, const QString& addonId, + const QByteArray& sha256); + + bool loadManifest(const QString& addonManifestFileName, + const QByteArray& sha256); void unload(const QString& addonId); @@ -31,14 +36,29 @@ class AddonManager final : public QObject { private: explicit AddonManager(QObject* parent); - void loadAll(); - void loadAll(const QDir& path); + void initialize(); + + bool validateIndex(const QByteArray& index); + bool validateAndLoad(const QString& addonId, const QByteArray& sha256, + bool checkSha256 = true); + + static bool addonDir(QDir* dir); + static QByteArray readIndex(); + static void writeIndex(const QByteArray& index); + + static void removeAddon(const QString& addonId); signals: void runAddon(Addon* addon); private: - QHash m_addons; + struct AddonData { + QByteArray m_sha256; + QString m_addonId; + Addon* m_addon; + }; + + QHash m_addons; }; #endif // ADDONMANAGER_H diff --git a/src/cmake/sources.cmake b/src/cmake/sources.cmake index 5d84127c06..8dd6b7e5cf 100644 --- a/src/cmake/sources.cmake +++ b/src/cmake/sources.cmake @@ -249,6 +249,10 @@ target_sources(mozillavpn PRIVATE tasks/account/taskaccount.h tasks/adddevice/taskadddevice.cpp tasks/adddevice/taskadddevice.h + tasks/addon/taskaddon.cpp + tasks/addon/taskaddon.h + tasks/addonindex/taskaddonindex.cpp + tasks/addonindex/taskaddonindex.h tasks/authenticate/taskauthenticate.cpp tasks/authenticate/taskauthenticate.h tasks/captiveportallookup/taskcaptiveportallookup.cpp diff --git a/src/constants.h b/src/constants.h index ebdfd96bd0..e25a9c8f31 100644 --- a/src/constants.h +++ b/src/constants.h @@ -103,6 +103,8 @@ PRODBETAEXPR( const char*, balrogRootCertFingerprint, "97e8ba9cf12fb3de53cc42a4e6577ed64df493c247b414fea036818d3823560e", "3c01446abe9036cea9a09acaa3a520ac628f20a7ae32ce861cb2efb70fa0c745"); +PRODBETAEXPR(const char*, addonSourceUrl, "TODO", + "https://mozilla-mobile.github.io/mozilla-vpn-client/addons/") #undef PRODBETAEXPR diff --git a/src/inspector/inspectorhandler.cpp b/src/inspector/inspectorhandler.cpp index 44ea8d773d..b28d2e3169 100644 --- a/src/inspector/inspectorhandler.cpp +++ b/src/inspector/inspectorhandler.cpp @@ -889,19 +889,14 @@ static QList s_commands{ return QJsonObject(); }}, - InspectorCommand{"load_addon", "Load an add-on", 1, - [](InspectorHandler*, const QList& arguments) { - QJsonObject obj; - obj["value"] = - AddonManager::instance()->load(arguments[1]); - return obj; - }}, - InspectorCommand{"load_addon_manifest", "Load an add-on", 1, [](InspectorHandler*, const QList& arguments) { QJsonObject obj; - obj["value"] = - AddonManager::instance()->loadManifest(arguments[1]); + // This is a debugging method. We don't need to compute + // the hash of the addon because we will not be able to + // find it in the addon index. + obj["value"] = AddonManager::instance()->loadManifest( + arguments[1], "INVALID SHA256"); return obj; }}, diff --git a/src/models/guidemodel.cpp b/src/models/guidemodel.cpp index 6eb810e0d8..cb9c9e19ea 100644 --- a/src/models/guidemodel.cpp +++ b/src/models/guidemodel.cpp @@ -3,7 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "guidemodel.h" -#include "addonmanager.h" #include "guide.h" #include "leakdetector.h" #include "logger.h" @@ -20,9 +19,6 @@ Logger logger(LOG_MAIN, "GuideModel"); GuideModel* GuideModel::instance() { if (!s_instance) { s_instance = new GuideModel(qApp); - - // We need tutorials from the addon manager. - AddonManager::instance(); } return s_instance; diff --git a/src/models/tutorialmodel.cpp b/src/models/tutorialmodel.cpp index a90a0a7522..75fee50959 100644 --- a/src/models/tutorialmodel.cpp +++ b/src/models/tutorialmodel.cpp @@ -3,7 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "tutorialmodel.h" -#include "addonmanager.h" #include "tutorial.h" #include "leakdetector.h" #include "logger.h" @@ -20,9 +19,6 @@ Logger logger(LOG_MAIN, "TutorialModel"); TutorialModel* TutorialModel::instance() { if (!s_instance) { s_instance = new TutorialModel(qApp); - - // We need tutorials from the addon manager. - AddonManager::instance(); } return s_instance; diff --git a/src/mozillavpn.cpp b/src/mozillavpn.cpp index 0b8b95fdda..f3dbaafd87 100644 --- a/src/mozillavpn.cpp +++ b/src/mozillavpn.cpp @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozillavpn.h" +#include "addonmanager.h" #include "authenticationinapp/authenticationinapp.h" #include "constants.h" #include "dnshelper.h" @@ -18,11 +19,13 @@ #include "settingsholder.h" #include "tasks/account/taskaccount.h" #include "tasks/adddevice/taskadddevice.h" +#include "tasks/addonindex/taskaddonindex.h" #include "tasks/authenticate/taskauthenticate.h" #include "tasks/captiveportallookup/taskcaptiveportallookup.h" #include "tasks/controlleraction/taskcontrolleraction.h" #include "tasks/deleteaccount/taskdeleteaccount.h" #include "tasks/function/taskfunction.h" +#include "tasks/getfeaturelist/taskgetfeaturelist.h" #include "tasks/group/taskgroup.h" #include "tasks/heartbeat/taskheartbeat.h" #include "tasks/products/taskproducts.h" @@ -31,7 +34,6 @@ #include "tasks/surveydata/tasksurveydata.h" #include "tasks/sendfeedback/tasksendfeedback.h" #include "tasks/createsupportticket/taskcreatesupportticket.h" -#include "tasks/getfeaturelist/taskgetfeaturelist.h" #include "taskscheduler.h" #include "telemetry/gleansample.h" #include "update/versionapi.h" @@ -220,6 +222,9 @@ void MozillaVPN::initialize() { m_private->m_webSocketHandler.initialize(); } + AddonManager::instance(); + TaskScheduler::scheduleTask(new TaskAddonIndex()); + TaskScheduler::scheduleTask(new TaskGetFeatureList()); #ifdef MVPN_ADJUST diff --git a/src/qmake/sources.pri b/src/qmake/sources.pri index bb302fab22..4bc4cd0ad2 100644 --- a/src/qmake/sources.pri +++ b/src/qmake/sources.pri @@ -122,6 +122,8 @@ SOURCES += \ statusicon.cpp \ tasks/account/taskaccount.cpp \ tasks/adddevice/taskadddevice.cpp \ + tasks/addon/taskaddon.cpp \ + tasks/addonindex/taskaddonindex.cpp \ tasks/authenticate/taskauthenticate.cpp \ tasks/captiveportallookup/taskcaptiveportallookup.cpp \ tasks/deleteaccount/taskdeleteaccount.cpp \ @@ -268,6 +270,8 @@ HEADERS += \ task.h \ tasks/account/taskaccount.h \ tasks/adddevice/taskadddevice.h \ + tasks/addon/taskaddon.h \ + tasks/addonindex/taskaddonindex.h \ tasks/authenticate/taskauthenticate.h \ tasks/captiveportallookup/taskcaptiveportallookup.h \ tasks/deleteaccount/taskdeleteaccount.h \ diff --git a/src/tasks/addon/taskaddon.cpp b/src/tasks/addon/taskaddon.cpp new file mode 100644 index 0000000000..66d6ef9feb --- /dev/null +++ b/src/tasks/addon/taskaddon.cpp @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "taskaddon.h" +#include "addonmanager.h" +#include "constants.h" +#include "leakdetector.h" +#include "logger.h" +#include "networkrequest.h" + +namespace { +Logger logger(LOG_MAIN, "TaskAddon"); +} + +TaskAddon::TaskAddon(const QString& addonId, const QByteArray& sha256) + : Task("TaskAddon"), m_addonId(addonId), m_sha256(sha256) { + MVPN_COUNT_CTOR(TaskAddon); +} + +TaskAddon::~TaskAddon() { MVPN_COUNT_DTOR(TaskAddon); } + +void TaskAddon::run() { + NetworkRequest* request = NetworkRequest::createForGetUrl( + this, QString("%1%2.rcc").arg(Constants::addonSourceUrl()).arg(m_addonId), + 200); + + connect(request, &NetworkRequest::requestFailed, this, + [this](QNetworkReply::NetworkError error, const QByteArray&) { + logger.error() << "Get addon failed" << error; + emit completed(); + }); + + connect(request, &NetworkRequest::requestCompleted, this, + [this](const QByteArray& data) { + logger.debug() << "Get addon completed"; + AddonManager::instance()->storeAndLoadAddon(data, m_addonId, + m_sha256); + emit completed(); + }); +} diff --git a/src/tasks/addon/taskaddon.h b/src/tasks/addon/taskaddon.h new file mode 100644 index 0000000000..99cf375166 --- /dev/null +++ b/src/tasks/addon/taskaddon.h @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef TASKADDON_H +#define TASKADDON_H + +#include "task.h" + +#include + +class TaskAddon final : public Task { + Q_DISABLE_COPY_MOVE(TaskAddon) + + public: + TaskAddon(const QString& addonId, const QByteArray& sha256); + ~TaskAddon(); + + void run() override; + + private: + const QString m_addonId; + const QByteArray m_sha256; +}; + +#endif // TASKADDON_H diff --git a/src/tasks/addonindex/taskaddonindex.cpp b/src/tasks/addonindex/taskaddonindex.cpp new file mode 100644 index 0000000000..8804b51c43 --- /dev/null +++ b/src/tasks/addonindex/taskaddonindex.cpp @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "taskaddonindex.h" +#include "addonmanager.h" +#include "constants.h" +#include "leakdetector.h" +#include "logger.h" +#include "networkrequest.h" + +namespace { +Logger logger(LOG_MAIN, "TaskAddonIndex"); +} + +TaskAddonIndex::TaskAddonIndex() : Task("TaskAddonIndex") { + MVPN_COUNT_CTOR(TaskAddonIndex); +} + +TaskAddonIndex::~TaskAddonIndex() { MVPN_COUNT_DTOR(TaskAddonIndex); } + +void TaskAddonIndex::run() { + NetworkRequest* request = NetworkRequest::createForGetUrl( + this, QString("%1manifest.json").arg(Constants::addonSourceUrl()), 200); + + connect(request, &NetworkRequest::requestFailed, this, + [this](QNetworkReply::NetworkError error, const QByteArray&) { + logger.error() << "Get addon index failed" << error; + emit completed(); + }); + + connect(request, &NetworkRequest::requestCompleted, this, + [this](const QByteArray& data) { + logger.debug() << "Get addon index completed"; + AddonManager::instance()->updateIndex(data); + emit completed(); + }); +} diff --git a/src/tasks/addonindex/taskaddonindex.h b/src/tasks/addonindex/taskaddonindex.h new file mode 100644 index 0000000000..adcac5a447 --- /dev/null +++ b/src/tasks/addonindex/taskaddonindex.h @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef TASKADDONINDEX_H +#define TASKADDONINDEX_H + +#include "task.h" + +#include + +class TaskAddonIndex final : public Task { + Q_DISABLE_COPY_MOVE(TaskAddonIndex) + + public: + TaskAddonIndex(); + ~TaskAddonIndex(); + + void run() override; +}; + +#endif // TASKADDONINDEX_H diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 78e1c04923..80cdb0e29e 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -187,6 +187,8 @@ target_sources(unit_tests PRIVATE ${MVPN_SOURCE_DIR}/tasks/account/taskaccount.h ${MVPN_SOURCE_DIR}/tasks/adddevice/taskadddevice.cpp ${MVPN_SOURCE_DIR}/tasks/adddevice/taskadddevice.h + ${MVPN_SOURCE_DIR}/tasks/addon/taskaddon.cpp + ${MVPN_SOURCE_DIR}/tasks/addon/taskaddon.h ${MVPN_SOURCE_DIR}/tasks/ipfinder/taskipfinder.cpp ${MVPN_SOURCE_DIR}/tasks/ipfinder/taskipfinder.h ${MVPN_SOURCE_DIR}/tasks/function/taskfunction.cpp diff --git a/tests/unit/unit.pro b/tests/unit/unit.pro index 5834ac2f1c..2bf06680af 100644 --- a/tests/unit/unit.pro +++ b/tests/unit/unit.pro @@ -119,6 +119,7 @@ HEADERS += \ ../../src/task.h \ ../../src/tasks/account/taskaccount.h \ ../../src/tasks/adddevice/taskadddevice.h \ + ../../src/tasks/addon/taskaddon.h \ ../../src/tasks/ipfinder/taskipfinder.h \ ../../src/tasks/function/taskfunction.h \ ../../src/tasks/release/taskrelease.h \ @@ -229,6 +230,7 @@ SOURCES += \ ../../src/statusicon.cpp \ ../../src/tasks/account/taskaccount.cpp \ ../../src/tasks/adddevice/taskadddevice.cpp \ + ../../src/tasks/addon/taskaddon.cpp \ ../../src/tasks/ipfinder/taskipfinder.cpp \ ../../src/tasks/function/taskfunction.cpp \ ../../src/tasks/release/taskrelease.cpp \ From 328cf42014f43645068318b2497986423457bb9b Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Fri, 10 Jun 2022 13:01:22 +0200 Subject: [PATCH 17/20] Github action for addon generation --- .github/workflows/wasm.yaml | 45 ++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wasm.yaml b/.github/workflows/wasm.yaml index 76b01b33e3..313eeecf83 100644 --- a/.github/workflows/wasm.yaml +++ b/.github/workflows/wasm.yaml @@ -93,9 +93,52 @@ jobs: name: Inspector Build path: tools/inspector/dist + addons: + runs-on: ubuntu-20.04 + env: + QTVERSION: 6.2.4 + + steps: + - name: Clone repository + uses: actions/checkout@v2 + + - uses: actions/setup-node@v2 + with: + node-version: '14' + + - name: Checkout submodules + shell: bash + run: | + auth_header="$(git config --local --get http.https://github.com/.extraheader)" + git submodule sync --recursive + git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1 + + - name: Install Qt + shell: bash + run: | + python3 -m pip install aqtinstall + python3 -m aqt install-qt -O /opt linux desktop $QTVERSION + + - name: Install python dependencies + shell: bash + run: | + pip3 install -r requirements.txt + + - name: Generating addons + shell: bash + run: | + export PATH=/opt/$QTVERSION/gcc_64/bin:$PATH + python3 scripts/addon/generate_all.py + + - name: Uploading + uses: actions/upload-artifact@v1 + with: + name: Inspector Build + path: addons/generated/addons + ghPages: runs-on: ubuntu-20.04 - needs: [wasmQt6, inspector] + needs: [wasmQt6, inspector, addons] name: Publish Wasm on Github Pages env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 9a5cbd8e7b1cc45e3fa4547e24680c887f177145 Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Fri, 10 Jun 2022 13:29:28 +0200 Subject: [PATCH 18/20] Review comments applied --- addons/guide_01_how_to_vpn/manifest.json | 2 +- addons/guide_02_is_my_vpn_working_correctly/manifest.json | 2 +- addons/guide_03_adding_and_removing_devices/manifest.json | 2 +- addons/guide_04_connecting_external_devices/manifest.json | 2 +- addons/language/language.qrc | 2 +- addons/language/manifest.json | 2 +- addons/tic-tac-toe/manifest.json | 2 +- addons/tic-tac-toe/tic-tac-toe.qrc | 2 +- addons/tutorial_01_get_started/manifest.json | 2 +- addons/tutorial_02_connect_on_startup/manifest.json | 2 +- scripts/addon/build.py | 4 ++-- scripts/addon/generate_all.py | 2 +- scripts/ci/jsonSchemas/addon.json | 4 ++-- src/addonmanager.cpp | 7 ++++--- src/addons/addon.cpp | 7 ++++--- 15 files changed, 23 insertions(+), 21 deletions(-) diff --git a/addons/guide_01_how_to_vpn/manifest.json b/addons/guide_01_how_to_vpn/manifest.json index a6d3ebe5b5..a40ebe97b0 100644 --- a/addons/guide_01_how_to_vpn/manifest.json +++ b/addons/guide_01_how_to_vpn/manifest.json @@ -1,5 +1,5 @@ { - "version": "0.1", + "api_version": "0.1", "id": "guide_01_how_to_vpn", "name": "Guide: how to vpn", "type": "guide", diff --git a/addons/guide_02_is_my_vpn_working_correctly/manifest.json b/addons/guide_02_is_my_vpn_working_correctly/manifest.json index 4cf4850e32..cea99b168d 100644 --- a/addons/guide_02_is_my_vpn_working_correctly/manifest.json +++ b/addons/guide_02_is_my_vpn_working_correctly/manifest.json @@ -1,5 +1,5 @@ { - "version": "0.1", + "api_version": "0.1", "id": "guide_02_is_my_vpn_working_correctly", "name": "Guide: is my VPN working correctly", "type": "guide", diff --git a/addons/guide_03_adding_and_removing_devices/manifest.json b/addons/guide_03_adding_and_removing_devices/manifest.json index 2e34207189..f427e8808d 100644 --- a/addons/guide_03_adding_and_removing_devices/manifest.json +++ b/addons/guide_03_adding_and_removing_devices/manifest.json @@ -1,5 +1,5 @@ { - "version": "0.1", + "api_version": "0.1", "id": "guide_03_adding_and_removing_devices", "name": "Guide: adding and removing devices", "type": "guide", diff --git a/addons/guide_04_connecting_external_devices/manifest.json b/addons/guide_04_connecting_external_devices/manifest.json index 30fbfdaf46..a2155cc5ac 100644 --- a/addons/guide_04_connecting_external_devices/manifest.json +++ b/addons/guide_04_connecting_external_devices/manifest.json @@ -1,5 +1,5 @@ { - "version": "0.1", + "api_version": "0.1", "id": "guide_04_connecting_external_devices", "name": "Guide: connecting external devices", "type": "guide", diff --git a/addons/language/language.qrc b/addons/language/language.qrc index 73980ab9c5..cd3ac5d910 100644 --- a/addons/language/language.qrc +++ b/addons/language/language.qrc @@ -1,5 +1,5 @@ - + manifest.json i18n/locale_en.qm diff --git a/addons/language/manifest.json b/addons/language/manifest.json index b5bbc76a7b..3bd3f262f0 100644 --- a/addons/language/manifest.json +++ b/addons/language/manifest.json @@ -1,6 +1,6 @@ { "id": "language-addon", - "version": "0.1", + "api_version": "0.1", "name": "language package", "type": "i18n" } diff --git a/addons/tic-tac-toe/manifest.json b/addons/tic-tac-toe/manifest.json index d89244e57f..13a94631ce 100644 --- a/addons/tic-tac-toe/manifest.json +++ b/addons/tic-tac-toe/manifest.json @@ -1,6 +1,6 @@ { "id": "tic-tac-toe", - "version": "0.1", + "api_version": "0.1", "name": "Tic-Tac-Toe", "qml": "tic-tac-toe.qml", "type": "demo" diff --git a/addons/tic-tac-toe/tic-tac-toe.qrc b/addons/tic-tac-toe/tic-tac-toe.qrc index 42a63db899..a419da4df9 100644 --- a/addons/tic-tac-toe/tic-tac-toe.qrc +++ b/addons/tic-tac-toe/tic-tac-toe.qrc @@ -1,5 +1,5 @@ - + manifest.json script.mjs tic-tac-toe.qml diff --git a/addons/tutorial_01_get_started/manifest.json b/addons/tutorial_01_get_started/manifest.json index 71c1701f33..c287ae4bd7 100644 --- a/addons/tutorial_01_get_started/manifest.json +++ b/addons/tutorial_01_get_started/manifest.json @@ -1,5 +1,5 @@ { - "version": "0.1", + "api_version": "0.1", "id": "tutorial_01_get_started", "name": "Tutorial: get started", "type": "tutorial", diff --git a/addons/tutorial_02_connect_on_startup/manifest.json b/addons/tutorial_02_connect_on_startup/manifest.json index aa2730deef..e91d202608 100644 --- a/addons/tutorial_02_connect_on_startup/manifest.json +++ b/addons/tutorial_02_connect_on_startup/manifest.json @@ -1,5 +1,5 @@ { - "version": "0.1", + "api_version": "0.1", "id": "tutorial_02_connect_on_startup", "name": "Tutorial: Connect on startup", "type": "tutorial", diff --git a/scripts/addon/build.py b/scripts/addon/build.py index 591e8ebed8..c31fa19915 100755 --- a/scripts/addon/build.py +++ b/scripts/addon/build.py @@ -343,14 +343,14 @@ def qtquery(qmake, propname): os.system(f"{lconvert} -if xlf -i {xliff_path} -o {locale_file}") os.system(f"{lrelease} -idbased {locale_file}") - print("Generate the RC file...") + print("Generate the RCC file...") files = get_file_list(tmp_path, "") qrc_file = os.path.join(tmp_path, f"{manifest['id']}.qrc") with open(qrc_file, "w", encoding="utf-8") as f: rcc_elm = ET.Element("RCC") qresource = ET.SubElement(rcc_elm, "qresource") - qresource.set("prefix", f"/{manifest['id']}") + qresource.set("prefix", "/") for file in files: elm = ET.SubElement(qresource, "file") elm.text = file diff --git a/scripts/addon/generate_all.py b/scripts/addon/generate_all.py index d7f3733b7f..4f65f7ed09 100755 --- a/scripts/addon/generate_all.py +++ b/scripts/addon/generate_all.py @@ -54,7 +54,7 @@ addons.append({ 'id': file, 'sha256': sha256 }) index = { - 'version': '0.1', + 'api_version': '0.1', 'addons': addons, } diff --git a/scripts/ci/jsonSchemas/addon.json b/scripts/ci/jsonSchemas/addon.json index 5b3223b050..d142ecb541 100644 --- a/scripts/ci/jsonSchemas/addon.json +++ b/scripts/ci/jsonSchemas/addon.json @@ -11,7 +11,7 @@ "type": "string", "description": "The name of this addon" }, - "version": { + "api_version": { "type": "string", "description": "The addon version framework" }, @@ -30,6 +30,6 @@ "description": "The QML entry point" } }, - "required": [ "id", "name", "version", "type" ] + "required": [ "id", "name", "api_version", "type" ] } diff --git a/src/addonmanager.cpp b/src/addonmanager.cpp index b9bee3d4a7..a1d6905df3 100644 --- a/src/addonmanager.cpp +++ b/src/addonmanager.cpp @@ -93,8 +93,8 @@ bool AddonManager::validateIndex(const QByteArray& index) { } QJsonObject obj = doc.object(); - if (obj["version"].toString() != "0.1") { - logger.debug() << "Invalid index file - version does not match"; + if (obj["api_version"].toString() != "0.1") { + logger.debug() << "Invalid index file - api_version does not match"; return false; } @@ -302,7 +302,8 @@ bool AddonManager::validateAndLoad(const QString& addonId, } } - if (!QResource::registerResource(addonFileName, "/addons")) { + if (!QResource::registerResource(addonFileName, + QString("/addons/%1").arg(addonId))) { logger.warning() << "Unable to load resource from file" << addonFileName; return false; } diff --git a/src/addons/addon.cpp b/src/addons/addon.cpp index 6f98b7c698..0266a0afbf 100644 --- a/src/addons/addon.cpp +++ b/src/addons/addon.cpp @@ -40,14 +40,15 @@ Addon* Addon::create(QObject* parent, const QString& manifestFileName) { QJsonObject obj = json.object(); - QString version = obj["version"].toString(); + QString version = obj["api_version"].toString(); if (version.isEmpty()) { - logger.warning() << "No version in the manifest" << manifestFileName; + logger.warning() << "No API version in the manifest" << manifestFileName; return nullptr; } if (version != "0.1") { - logger.warning() << "Unsupported version" << version << manifestFileName; + logger.warning() << "Unsupported API version" << version + << manifestFileName; return nullptr; } From a2ebdcb083d1344c50d83b11720bf9d3cf11b655 Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Fri, 10 Jun 2022 15:02:16 +0200 Subject: [PATCH 19/20] metavar uppercase --- scripts/addon/build.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/addon/build.py b/scripts/addon/build.py index c31fa19915..fff13cd5bb 100755 --- a/scripts/addon/build.py +++ b/scripts/addon/build.py @@ -218,14 +218,14 @@ def get_file_list(path, prefix): parser = argparse.ArgumentParser(description="Generate an addon package") parser.add_argument( "source", - metavar="manifest", + metavar="MANIFEST", type=str, action="store", help="The addon manifest", ) parser.add_argument( "dest", - metavar="dest", + metavar="DEST", type=str, action="store", help="The destination folder", From a4d1e1b6a27a64a2d6916b43c97a51af0fdce424 Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Fri, 10 Jun 2022 20:14:54 +0200 Subject: [PATCH 20/20] Removed node for the wasm/addon github action --- .github/workflows/wasm.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/wasm.yaml b/.github/workflows/wasm.yaml index 313eeecf83..65b629d299 100644 --- a/.github/workflows/wasm.yaml +++ b/.github/workflows/wasm.yaml @@ -102,10 +102,6 @@ jobs: - name: Clone repository uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: '14' - - name: Checkout submodules shell: bash run: |