[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[tor-commits] [Git][tpo/applications/tor-browser][tor-browser-156.0a1-17.0-2] 12 commits: fixup! Tor Browser localization migration scripts.



Title: GitLab

Pier Angelo Vendrame pushed to branch tor-browser-156.0a1-17.0-2 at The Tor Project / Applications / Tor Browser

Commits:

  • 60b70f9f
    by Henry Wilkes at 2026-11-11T08:59:14+01:00
    fixup! Tor Browser localization migration scripts.
    
    TB 45433: Change the script to be more versatile for test runs.
    
    Instead of passing in a python module name for the recipe, you now pass
    in the file path to the recipe script.
    
    You can now change the reference directory with the
    --reference-directory argument, which is useful for testing scripts
    before the changes have reached the translation repository.
    
  • b7ea5196
    by Henry Wilkes at 2026-11-11T08:59:15+01:00
    fixup! Tor Browser localization migration scripts.
    
    TB 45433: Refactor the WeblateMetadata class.
    
    We are stricter about checking the return types from the weblate API,
    and making sure it doesn't make requests to unexpected URLs.
    
    We also limit the components to the browser components, and we slightly
    alter the `is_translated` public method.
    
  • 873fe4a3
    by Henry Wilkes at 2026-11-11T08:59:15+01:00
    fixup! Tor Browser localization migration scripts.
    
    TB 45433: Add type hints to the script.
    
  • 42d45b17
    by Henry Wilkes at 2026-11-11T08:59:15+01:00
    fixup! Tor Browser localization migration scripts.
    
    TB 45433: Update the migrate.py documentation.
    
  • 01dfa930
    by hackademix at 2026-11-11T08:59:52+01:00
    BB 45438: Use OneCRL dump from 153esr
    
  • ee6f9f1d
    by Dana Keeler at 2026-11-11T08:59:54+01:00
    Bug 2084311 - ensure OneCRL bundle is loaded at init a=RyanVM
    
    Original Revision: https://phabricator.services.mozilla.com/D335790
    
    Differential Revision: https://phabricator.services.mozilla.com/D335837
    
  • a3805da7
    by Elena at 2026-11-11T08:59:55+01:00
    fixup! TB 40933: Add tor-launcher functionality
    
    TB 45425: Use an actual XPCOM type for status updates.
    
  • e77d5e6c
    by Elena at 2026-11-11T08:59:55+01:00
    fixup! TB 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
    
    TB 45425: Use an actual XPCOM type for status updates.
    
  • 7ed1acfb
    by Elena at 2026-11-11T08:59:55+01:00
    fixup! TB 42247: Android helpers for the TorProvider
    
    TB 45425: Use an actual XPCOM type for status updates.
    
  • cceebf2f
    by Elena at 2026-11-11T08:59:55+01:00
    fixup! TB 3455: Add DomainIsolator, for isolating circuit by domain.
    
    TB 45425: Use an actual XPCOM type for provider topics.
    
  • 96898066
    by Pier Angelo Vendrame at 2026-11-11T08:59:56+01:00
    fixup! TB 41600: Add a tor circuit display panel.
    
    TB 45425: Use an explicit boolean for bridges in NodeData
    
  • bd6af55f
    by Dan Ballard at 2026-11-11T08:59:56+01:00
    fixup! [android] Override settings
    
    TB 45365: Disable IPProtect for all in nimbus
    

17 changed files:

Changes:

  • browser/components/torcircuit/content/torCircuitPanel.js
    ... ... @@ -547,9 +547,9 @@ var gTorCircuitPanel = {
    547 547
         // bridge, or otherwise a guard.
    
    548 548
         document.l10n.setAttributes(
    
    549 549
           this._panelElements.newCircuitDescription,
    
    550
    -      nodes[0].bridgeType === null
    
    551
    -        ? "tor-circuit-panel-new-button-description-guard"
    
    552
    -        : "tor-circuit-panel-new-button-description-bridge"
    
    550
    +      nodes[0].isBridge
    
    551
    +        ? "tor-circuit-panel-new-button-description-bridge"
    
    552
    +        : "tor-circuit-panel-new-button-description-guard"
    
    553 553
         );
    
    554 554
       },
    
    555 555
     
    
    ... ... @@ -565,7 +565,7 @@ var gTorCircuitPanel = {
    565 565
         const nameEl = nodeItem.querySelector(".tor-circuit-node-name");
    
    566 566
         let flagSrc = null;
    
    567 567
     
    
    568
    -    if (node.bridgeType === null) {
    
    568
    +    if (!node.isBridge) {
    
    569 569
           const regionCode = node.regionCode;
    
    570 570
           flagSrc = this._regionFlagSrc(regionCode);
    
    571 571
     
    

  • browser/components/torpreferences/content/torLogDialog.js
    ... ... @@ -87,7 +87,13 @@ const gTorLogDialog = {
    87 87
     
    
    88 88
       observe(subject, topic) {
    
    89 89
         if (topic === TorProviderTopics.TorLog) {
    
    90
    -      this.addLogEntry(subject.wrappedJSObject, false);
    
    90
    +      let entry;
    
    91
    +      try {
    
    92
    +        entry = subject.QueryInterface(Ci.torITorLogEntry);
    
    93
    +      } catch {
    
    94
    +        return;
    
    95
    +      }
    
    96
    +      this.addLogEntry(entry, false);
    
    91 97
         }
    
    92 98
       },
    
    93 99
     
    
    ... ... @@ -96,7 +102,7 @@ const gTorLogDialog = {
    96 102
         timeEl.textContent = logEntry.timestamp;
    
    97 103
         timeEl.classList.add("time");
    
    98 104
         const messageEl = document.createElement("td");
    
    99
    -    messageEl.textContent = `[${logEntry.type}] ${logEntry.msg}`;
    
    105
    +    messageEl.textContent = `[${logEntry.severity}] ${logEntry.message}`;
    
    100 106
         messageEl.classList.add("message");
    
    101 107
     
    
    102 108
         const row = document.createElement("tr");
    

  • mobile/android/fenix/app/nimbus.fml.yaml
    ... ... @@ -1082,14 +1082,13 @@ features:
    1082 1082
         defaults:
    
    1083 1083
           - channel: developer
    
    1084 1084
             value:
    
    1085
    -          enabled: true
    
    1086
    -          allowed-regions: ["XX"]
    
    1085
    +          enabled: false
    
    1087 1086
           - channel: nightly
    
    1088 1087
             value:
    
    1089
    -          enabled: true
    
    1088
    +          enabled: false
    
    1090 1089
           - channel: beta
    
    1091 1090
             value:
    
    1092
    -          enabled: true
    
    1091
    +          enabled: false
    
    1093 1092
     
    
    1094 1093
       native-share-sheet:
    
    1095 1094
         description: Feature to enable the Native Share Sheet for users on Firefox on Android.
    

  • security/manager/ssl/RemoteSecuritySettings.sys.mjs
    ... ... @@ -259,6 +259,28 @@ export var RemoteSecuritySettings = {
    259 259
           signerName: SECURITY_STATE_SIGNER,
    
    260 260
         });
    
    261 261
         this.OneCRLBlocklistClient.on("sync", updateCertBlocklist);
    
    262
    +    // Ensure the bundled list of revocations is loaded.
    
    263
    +    this.OneCRLBlocklistClient.get()
    
    264
    +      .then(records => {
    
    265
    +        return updateCertBlocklist({
    
    266
    +          data: {
    
    267
    +            current: records,
    
    268
    +            created: records,
    
    269
    +            updated: [],
    
    270
    +            deleted: [],
    
    271
    +          },
    
    272
    +        });
    
    273
    +      })
    
    274
    +      .catch(e => {
    
    275
    +        lazy.log.error(e);
    
    276
    +      })
    
    277
    +      .finally(() => {
    
    278
    +        // Only used by tests, to know when the bundled list is in cert_storage.
    
    279
    +        Services.obs.notifyObservers(
    
    280
    +          null,
    
    281
    +          "remote-security-settings:onecrl-bundle-loaded"
    
    282
    +        );
    
    283
    +      });
    
    262 284
     
    
    263 285
         this.IntermediatePreloadsClient = new IntermediatePreloads();
    
    264 286
     
    

  • security/manager/ssl/tests/unit/test_onecrl_loads_bundle.js
    1
    +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
    
    2
    +// Any copyright is dedicated to the Public Domain.
    
    3
    +// http://creativecommons.org/publicdomain/zero/1.0/
    
    4
    +
    
    5
    +"use strict";
    
    6
    +
    
    7
    +// Checks that OneCRL loads its remote settings bundle at startup by ensuring
    
    8
    +// that each of the listed eMudhra intermediate certificates is revoked.
    
    9
    +
    
    10
    +do_get_profile();
    
    11
    +
    
    12
    +const { TestUtils } = ChromeUtils.importESModule(
    
    13
    +  "resource://testing-common/TestUtils.sys.mjs"
    
    14
    +);
    
    15
    +const { RemoteSecuritySettings } = ChromeUtils.importESModule(
    
    16
    +  "resource://gre/modules/psm/RemoteSecuritySettings.sys.mjs"
    
    17
    +);
    
    18
    +
    
    19
    +// DER encoding of the issuer name (CN=emSign Root TLS CA - G3), base64.
    
    20
    +const ISSUER =
    
    21
    +  "MFYxCzAJBgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVkMSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMw==";
    
    22
    +
    
    23
    +// Serial numbers are the contents of the DER INTEGER, in hex.
    
    24
    +const SERIALS = {
    
    25
    +  "EM DV TLS CA - G3B": "00a4aee678f23362b4c0e460b772f214",
    
    26
    +  "EM OV TLS CA - G3B": "6cada367fb1c100088120ea202d568",
    
    27
    +  "EM EV TLS CA - G3B": "00c7c41efe92d825f05eae841a4a82c1",
    
    28
    +  "EM DV TLS CA - G3C": "588b2e3c2bb8cda13e1a1209254321",
    
    29
    +  "EM OV TLS CA - G3C": "296926c227a3a1526392b19482a504",
    
    30
    +  "EM EV TLS CA - G3C": "008009928a64107601af05fa2f19f109",
    
    31
    +};
    
    32
    +
    
    33
    +function base64ToBytes(base64) {
    
    34
    +  return Array.from(atob(base64), c => c.charCodeAt(0));
    
    35
    +}
    
    36
    +
    
    37
    +function hexToBytes(hex) {
    
    38
    +  return hex.match(/../g).map(byte => parseInt(byte, 16));
    
    39
    +}
    
    40
    +
    
    41
    +add_task(async function test_emsign_intermediates_are_revoked() {
    
    42
    +  let loaded = TestUtils.topicObserved(
    
    43
    +    "remote-security-settings:onecrl-bundle-loaded"
    
    44
    +  );
    
    45
    +  RemoteSecuritySettings.init();
    
    46
    +  await loaded;
    
    47
    +
    
    48
    +  let certStorage = Cc["@mozilla.org/security/certstorage;1"].getService(
    
    49
    +    Ci.nsICertStorage
    
    50
    +  );
    
    51
    +  let issuer = base64ToBytes(ISSUER);
    
    52
    +
    
    53
    +  for (let [name, serial] of Object.entries(SERIALS)) {
    
    54
    +    Assert.equal(
    
    55
    +      certStorage.getRevocationState(issuer, hexToBytes(serial), [], []),
    
    56
    +      Ci.nsICertStorage.STATE_ENFORCE,
    
    57
    +      `${name} should be revoked`
    
    58
    +    );
    
    59
    +  }
    
    60
    +});

  • security/manager/ssl/tests/unit/xpcshell.toml
    ... ... @@ -293,6 +293,8 @@ run-sequentially = ["true"] # hardcoded ports
    293 293
     ["test_ocsp_url.js"]
    
    294 294
     run-sequentially = ["true"] # hardcoded ports
    
    295 295
     
    
    296
    +["test_onecrl_loads_bundle.js"]
    
    297
    +
    
    296 298
     ["test_oskeystore.js"]
    
    297 299
     skip-if = [
    
    298 300
       "os == 'mac' && os_version == '15.30' && arch == 'aarch64'", # Bug 1729538
    

  • services/settings/dumps/security-state/onecrl.json
    1 1
     {
    
    2 2
       "data": [
    
    3
    +    {
    
    4
    +      "schema": 1793206881749,
    
    5
    +      "details": {
    
    6
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2086683",
    
    7
    +        "who": "",
    
    8
    +        "why": "",
    
    9
    +        "name": "",
    
    10
    +        "created": ""
    
    11
    +      },
    
    12
    +      "enabled": false,
    
    13
    +      "issuerName": "MIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKTmV3IEplcnNleTEUMBIGA1UEBxMLSmVyc2V5IENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEuMCwGA1UEAxMlVVNFUlRydXN0IEVDQyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eQ==",
    
    14
    +      "serialNumber": "RB/KBH9VgbnLpOJqZoTyow==",
    
    15
    +      "id": "7b77a8a9-4ff9-4190-a63d-480067cf8c4e",
    
    16
    +      "last_modified": 1793208925525
    
    17
    +    },
    
    18
    +    {
    
    19
    +      "schema": 1793206882065,
    
    20
    +      "details": {
    
    21
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2086683",
    
    22
    +        "who": "",
    
    23
    +        "why": "",
    
    24
    +        "name": "",
    
    25
    +        "created": ""
    
    26
    +      },
    
    27
    +      "enabled": false,
    
    28
    +      "issuerName": "MGkxCzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBDb3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHk=",
    
    29
    +      "serialNumber": "FtGXvblx89aS5WAmhklF4wKyzbU=",
    
    30
    +      "id": "3fdd4f20-8e47-4578-997c-3016911d10a8",
    
    31
    +      "last_modified": 1793208925522
    
    32
    +    },
    
    33
    +    {
    
    34
    +      "schema": 1793206881998,
    
    35
    +      "details": {
    
    36
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2086683",
    
    37
    +        "who": "",
    
    38
    +        "why": "",
    
    39
    +        "name": "",
    
    40
    +        "created": ""
    
    41
    +      },
    
    42
    +      "enabled": false,
    
    43
    +      "issuerName": "MGkxCzAJBgNVBAYTAktSMSkwJwYDVQQKDCBOQVZFUiBDbG91ZCBUcnVzdCBTZXJ2aWNlcyBDb3JwLjEvMC0GA1UEAwwmTkFWRVIgQ2xvdWQgVHJ1c3QgU2VydmljZXMgRUNDIFJvb3QgRzE=",
    
    44
    +      "serialNumber": "DfW5xUaZ1oc/RsU7E9mA4Keg9GY=",
    
    45
    +      "id": "2d970c9e-ca3e-400f-a025-05a74762c688",
    
    46
    +      "last_modified": 1793208925519
    
    47
    +    },
    
    48
    +    {
    
    49
    +      "schema": 1793206881874,
    
    50
    +      "details": {
    
    51
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2086683",
    
    52
    +        "who": "",
    
    53
    +        "why": "",
    
    54
    +        "name": "",
    
    55
    +        "created": ""
    
    56
    +      },
    
    57
    +      "enabled": false,
    
    58
    +      "issuerName": "MEQxCzAJBgNVBAYTAkRFMQ8wDQYDVQQKEwZBbWF6b24xJDAiBgNVBAMTG0FtYXpvbiBFQ0RTQSAyNTYgUm9vdCBFVSBNMQ==",
    
    59
    +      "serialNumber": "B8DNf48cn1uAEWyCz2ilx7MfJQ==",
    
    60
    +      "id": "83ccc9c6-2121-4f62-b284-20d9514d1927",
    
    61
    +      "last_modified": 1793208925517
    
    62
    +    },
    
    63
    +    {
    
    64
    +      "schema": 1793206881808,
    
    65
    +      "details": {
    
    66
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2086683",
    
    67
    +        "who": "",
    
    68
    +        "why": "",
    
    69
    +        "name": "",
    
    70
    +        "created": ""
    
    71
    +      },
    
    72
    +      "enabled": false,
    
    73
    +      "issuerName": "MD0xCzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEdsb2JhbCBHMiBSb290",
    
    74
    +      "serialNumber": "YCnHTexdqAT9NNVLh8z2QA==",
    
    75
    +      "id": "66ad7766-46e5-415b-8950-9d4143d68a47",
    
    76
    +      "last_modified": 1793208925514
    
    77
    +    },
    
    78
    +    {
    
    79
    +      "schema": 1793206881627,
    
    80
    +      "details": {
    
    81
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2086683",
    
    82
    +        "who": "",
    
    83
    +        "why": "",
    
    84
    +        "name": "",
    
    85
    +        "created": ""
    
    86
    +      },
    
    87
    +      "enabled": false,
    
    88
    +      "issuerName": "MGkxCzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBDb3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHk=",
    
    89
    +      "serialNumber": "FJ5/xhegeJsX13b9RcYLaXVPUwA=",
    
    90
    +      "id": "a0372f6d-c436-429a-a41e-c16003c01ff0",
    
    91
    +      "last_modified": 1793208925511
    
    92
    +    },
    
    93
    +    {
    
    94
    +      "schema": 1793206881570,
    
    95
    +      "details": {
    
    96
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2086683",
    
    97
    +        "who": "",
    
    98
    +        "why": "",
    
    99
    +        "name": "",
    
    100
    +        "created": ""
    
    101
    +      },
    
    102
    +      "enabled": false,
    
    103
    +      "issuerName": "MGkxCzAJBgNVBAYTAktSMSkwJwYDVQQKDCBOQVZFUiBDbG91ZCBUcnVzdCBTZXJ2aWNlcyBDb3JwLjEvMC0GA1UEAwwmTkFWRVIgQ2xvdWQgVHJ1c3QgU2VydmljZXMgRUNDIFJvb3QgRzE=",
    
    104
    +      "serialNumber": "DjfBmWc92Uon9Pg7pj1ojD29WkA=",
    
    105
    +      "id": "fb78f960-2dd1-438c-a1e5-91f22ffb9aed",
    
    106
    +      "last_modified": 1793208925508
    
    107
    +    },
    
    108
    +    {
    
    109
    +      "schema": 1793206881509,
    
    110
    +      "details": {
    
    111
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2086683",
    
    112
    +        "who": "",
    
    113
    +        "why": "",
    
    114
    +        "name": "",
    
    115
    +        "created": ""
    
    116
    +      },
    
    117
    +      "enabled": false,
    
    118
    +      "issuerName": "MEQxCzAJBgNVBAYTAkRFMQ8wDQYDVQQKEwZBbWF6b24xJDAiBgNVBAMTG0FtYXpvbiBFQ0RTQSAzODQgUm9vdCBFVSBNMQ==",
    
    119
    +      "serialNumber": "B8DNf48ffwl2Bnue0WyZT7Yw+w==",
    
    120
    +      "id": "307e624d-6540-455a-80a5-a3a85c905e06",
    
    121
    +      "last_modified": 1793208925505
    
    122
    +    },
    
    123
    +    {
    
    124
    +      "schema": 1792622042163,
    
    125
    +      "details": {
    
    126
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2086683",
    
    127
    +        "who": "",
    
    128
    +        "why": "",
    
    129
    +        "name": "",
    
    130
    +        "created": ""
    
    131
    +      },
    
    132
    +      "enabled": false,
    
    133
    +      "issuerName": "MGkxCzAJBgNVBAYTAktSMSkwJwYDVQQKDCBOQVZFUiBDbG91ZCBUcnVzdCBTZXJ2aWNlcyBDb3JwLjEvMC0GA1UEAwwmTkFWRVIgQ2xvdWQgVHJ1c3QgU2VydmljZXMgUlNBIFJvb3QgRzE=",
    
    134
    +      "serialNumber": "C0Se6VVjeCepnsw2POzjSpEeSk0=",
    
    135
    +      "id": "beef1c99-a5f4-4bd6-8f9c-aa06e7d524df",
    
    136
    +      "last_modified": 1793208925502
    
    137
    +    },
    
    138
    +    {
    
    139
    +      "schema": 1793206881933,
    
    140
    +      "details": {
    
    141
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2086683",
    
    142
    +        "who": "",
    
    143
    +        "why": "",
    
    144
    +        "name": "",
    
    145
    +        "created": ""
    
    146
    +      },
    
    147
    +      "enabled": false,
    
    148
    +      "issuerName": "MGkxCzAJBgNVBAYTAktSMSkwJwYDVQQKDCBOQVZFUiBDbG91ZCBUcnVzdCBTZXJ2aWNlcyBDb3JwLjEvMC0GA1UEAwwmTkFWRVIgQ2xvdWQgVHJ1c3QgU2VydmljZXMgUlNBIFJvb3QgRzE=",
    
    149
    +      "serialNumber": "CuTxpDlw70JyQ/lqZvnIPouNYiQ=",
    
    150
    +      "id": "9b28e176-0b39-42b1-a4d8-ebb9f85d7558",
    
    151
    +      "last_modified": 1793208925499
    
    152
    +    },
    
    153
    +    {
    
    154
    +      "schema": 1793206881689,
    
    155
    +      "details": {
    
    156
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2086683",
    
    157
    +        "who": "",
    
    158
    +        "why": "",
    
    159
    +        "name": "",
    
    160
    +        "created": ""
    
    161
    +      },
    
    162
    +      "enabled": false,
    
    163
    +      "issuerName": "MEMxCzAJBgNVBAYTAkRFMQ8wDQYDVQQKEwZBbWF6b24xIzAhBgNVBAMTGkFtYXpvbiBSU0EgMjA0OCBSb290IEVVIE0x",
    
    164
    +      "serialNumber": "B8DNf48VlBcZv+FGxXhhwHG4iw==",
    
    165
    +      "id": "c2f70e6e-5a20-421f-8063-61f0854d171b",
    
    166
    +      "last_modified": 1793208925496
    
    167
    +    },
    
    168
    +    {
    
    169
    +      "schema": 1792621513680,
    
    170
    +      "details": {
    
    171
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2083382",
    
    172
    +        "who": "",
    
    173
    +        "why": "",
    
    174
    +        "name": "",
    
    175
    +        "created": ""
    
    176
    +      },
    
    177
    +      "enabled": true,
    
    178
    +      "subject": "MD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJWMRswGQYDVQQDDBJFTSBEViBUTFMgQ0EgLSBHM0I=",
    
    179
    +      "pubKeyHash": "6+1kdcLC2A/LGZ8/6N1uazzmC3frFdTkLD3sDG9bUQU=",
    
    180
    +      "id": "5ed2b441-a5bf-4f8e-89af-d01fa292a9b8",
    
    181
    +      "last_modified": 1792622041776
    
    182
    +    },
    
    183
    +    {
    
    184
    +      "schema": 1792621487501,
    
    185
    +      "details": {
    
    186
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2083382",
    
    187
    +        "who": "",
    
    188
    +        "why": "",
    
    189
    +        "name": "",
    
    190
    +        "created": ""
    
    191
    +      },
    
    192
    +      "enabled": true,
    
    193
    +      "subject": "MEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVkaHJhIEluYzEbMBkGA1UEAwwSRU0gT1YgVExTIENBIC0gRzND",
    
    194
    +      "pubKeyHash": "Y+zv7kMdSVI7wRNsTnLQIGpHcIbK9gbNXPAlaGwQnPg=",
    
    195
    +      "id": "f67aff65-7a13-42ef-b818-74bb69053273",
    
    196
    +      "last_modified": 1792622041773
    
    197
    +    },
    
    198
    +    {
    
    199
    +      "schema": 1792621521702,
    
    200
    +      "details": {
    
    201
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2083382",
    
    202
    +        "who": "",
    
    203
    +        "why": "",
    
    204
    +        "name": "",
    
    205
    +        "created": ""
    
    206
    +      },
    
    207
    +      "enabled": true,
    
    208
    +      "subject": "MEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVkaHJhIEluYzEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzND",
    
    209
    +      "pubKeyHash": "Sd01PcK5GaN3BkURfQ5gwWEV59G8JZ7v5zSwDqQ0/l8=",
    
    210
    +      "id": "fb4be101-8280-4d4e-866e-fd0213bc3a02",
    
    211
    +      "last_modified": 1792622041769
    
    212
    +    },
    
    213
    +    {
    
    214
    +      "schema": 1792621504826,
    
    215
    +      "details": {
    
    216
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2083382",
    
    217
    +        "who": "",
    
    218
    +        "why": "",
    
    219
    +        "name": "",
    
    220
    +        "created": ""
    
    221
    +      },
    
    222
    +      "enabled": true,
    
    223
    +      "subject": "MEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVkaHJhIEluYzEbMBkGA1UEAwwSRU0gRVYgVExTIENBIC0gRzND",
    
    224
    +      "pubKeyHash": "XAZgorFkkQ2P9YnBjhRVLp4Owjfc7+LLc21BpIEfgaA=",
    
    225
    +      "id": "adaa1e8d-57cb-4cfb-b647-305aa20f1b04",
    
    226
    +      "last_modified": 1792622041766
    
    227
    +    },
    
    228
    +    {
    
    229
    +      "schema": 1792621495984,
    
    230
    +      "details": {
    
    231
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2083382",
    
    232
    +        "who": "",
    
    233
    +        "why": "",
    
    234
    +        "name": "",
    
    235
    +        "created": ""
    
    236
    +      },
    
    237
    +      "enabled": true,
    
    238
    +      "subject": "MD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJWMRswGQYDVQQDDBJFTSBPViBUTFMgQ0EgLSBHM0I=",
    
    239
    +      "pubKeyHash": "Q7/6GdbvZRRR9oQeAZkx6W9vLq7/KKtLbxzkV23icXs=",
    
    240
    +      "id": "3f07ab6b-0d52-41a3-9a7b-de945502a3b8",
    
    241
    +      "last_modified": 1792622041762
    
    242
    +    },
    
    243
    +    {
    
    244
    +      "schema": 1792274405648,
    
    245
    +      "details": {
    
    246
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2083382",
    
    247
    +        "who": "",
    
    248
    +        "why": "",
    
    249
    +        "name": "",
    
    250
    +        "created": ""
    
    251
    +      },
    
    252
    +      "enabled": true,
    
    253
    +      "subject": "MD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJWMRswGQYDVQQDDBJFTSBFViBUTFMgQ0EgLSBHM0I=",
    
    254
    +      "pubKeyHash": "4CVMf83CZeLw+KQo2rt1q0rgObSHLWL17lfwh31aikE=",
    
    255
    +      "id": "5856b9af-5c14-46e7-b660-04d1cc3f2819",
    
    256
    +      "last_modified": 1792622041759
    
    257
    +    },
    
    258
    +    {
    
    259
    +      "schema": 1792270008816,
    
    260
    +      "details": {
    
    261
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2083382",
    
    262
    +        "who": "",
    
    263
    +        "why": "",
    
    264
    +        "name": "",
    
    265
    +        "created": ""
    
    266
    +      },
    
    267
    +      "enabled": true,
    
    268
    +      "issuerName": "MFYxCzAJBgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVkMSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMw==",
    
    269
    +      "serialNumber": "WIsuPCu4zaE+GhIJJUMh",
    
    270
    +      "id": "dfebe454-5411-4f64-bd26-ff0346cad020",
    
    271
    +      "last_modified": 1792274405312
    
    272
    +    },
    
    273
    +    {
    
    274
    +      "schema": 1792269992087,
    
    275
    +      "details": {
    
    276
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2083382",
    
    277
    +        "who": "",
    
    278
    +        "why": "",
    
    279
    +        "name": "",
    
    280
    +        "created": ""
    
    281
    +      },
    
    282
    +      "enabled": true,
    
    283
    +      "issuerName": "MFYxCzAJBgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVkMSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMw==",
    
    284
    +      "serialNumber": "AIAJkopkEHYBrwX6LxnxCQ==",
    
    285
    +      "id": "eefb82bf-3d32-4b7e-9dff-19fd2c21f755",
    
    286
    +      "last_modified": 1792274405309
    
    287
    +    },
    
    288
    +    {
    
    289
    +      "schema": 1792269983656,
    
    290
    +      "details": {
    
    291
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2083382",
    
    292
    +        "who": "",
    
    293
    +        "why": "",
    
    294
    +        "name": "",
    
    295
    +        "created": ""
    
    296
    +      },
    
    297
    +      "enabled": true,
    
    298
    +      "issuerName": "MFYxCzAJBgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVkMSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMw==",
    
    299
    +      "serialNumber": "bK2jZ/scEACIEg6iAtVo",
    
    300
    +      "id": "a0ac0973-ae76-4860-8f9c-eff5fd57bc3b",
    
    301
    +      "last_modified": 1792274405304
    
    302
    +    },
    
    303
    +    {
    
    304
    +      "schema": 1792269975630,
    
    305
    +      "details": {
    
    306
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2083382",
    
    307
    +        "who": "",
    
    308
    +        "why": "",
    
    309
    +        "name": "",
    
    310
    +        "created": ""
    
    311
    +      },
    
    312
    +      "enabled": true,
    
    313
    +      "issuerName": "MFYxCzAJBgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVkMSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMw==",
    
    314
    +      "serialNumber": "KWkmwiejoVJjkrGUgqUE",
    
    315
    +      "id": "c64301a0-888f-469d-86fc-a03bcd6bc407",
    
    316
    +      "last_modified": 1792274405300
    
    317
    +    },
    
    318
    +    {
    
    319
    +      "schema": 1792195211286,
    
    320
    +      "details": {
    
    321
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2083382",
    
    322
    +        "who": "",
    
    323
    +        "why": "",
    
    324
    +        "name": "",
    
    325
    +        "created": ""
    
    326
    +      },
    
    327
    +      "enabled": true,
    
    328
    +      "issuerName": "MFYxCzAJBgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVkMSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMw==",
    
    329
    +      "serialNumber": "AMfEHv6S2CXwXq6EGkqCwQ==",
    
    330
    +      "id": "5fb952e1-b09f-4187-805c-b87a91dad9bb",
    
    331
    +      "last_modified": 1792274405297
    
    332
    +    },
    
    333
    +    {
    
    334
    +      "schema": 1792269999860,
    
    335
    +      "details": {
    
    336
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2083382",
    
    337
    +        "who": "",
    
    338
    +        "why": "",
    
    339
    +        "name": "",
    
    340
    +        "created": ""
    
    341
    +      },
    
    342
    +      "enabled": true,
    
    343
    +      "issuerName": "MFYxCzAJBgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVkMSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMw==",
    
    344
    +      "serialNumber": "AKSu5njyM2K0wORgt3LyFA==",
    
    345
    +      "id": "bd595f9a-0da9-4980-bcc2-ef1e1bf70c7b",
    
    346
    +      "last_modified": 1792274405294
    
    347
    +    },
    
    348
    +    {
    
    349
    +      "schema": 1791318533037,
    
    350
    +      "details": {
    
    351
    +        "bug": "2078851",
    
    352
    +        "who": "",
    
    353
    +        "why": "",
    
    354
    +        "name": "",
    
    355
    +        "created": ""
    
    356
    +      },
    
    357
    +      "enabled": true,
    
    358
    +      "issuerName": "MDsxCzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTQ==",
    
    359
    +      "serialNumber": "LO0aXgKAW7xd34o67KqYWg==",
    
    360
    +      "id": "78e1876e-8832-49ee-9bd9-b36e3d53b059",
    
    361
    +      "last_modified": 1791573428759
    
    362
    +    },
    
    363
    +    {
    
    364
    +      "schema": 1791233394581,
    
    365
    +      "details": {
    
    366
    +        "bug": "2078851",
    
    367
    +        "who": "",
    
    368
    +        "why": "",
    
    369
    +        "name": "",
    
    370
    +        "created": ""
    
    371
    +      },
    
    372
    +      "enabled": true,
    
    373
    +      "issuerName": "MDsxCzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTQ==",
    
    374
    +      "serialNumber": "NIFgxR9e28td34nKtFczkg==",
    
    375
    +      "id": "2690f159-72d9-4a13-a7e4-ee2d2b5b7e5b",
    
    376
    +      "last_modified": 1791573428756
    
    377
    +    },
    
    378
    +    {
    
    379
    +      "schema": 1790874064811,
    
    380
    +      "details": {
    
    381
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2077222",
    
    382
    +        "who": "",
    
    383
    +        "why": "",
    
    384
    +        "name": "",
    
    385
    +        "created": ""
    
    386
    +      },
    
    387
    +      "enabled": false,
    
    388
    +      "issuerName": "MGExCzAJBgNVBAYTAkNOMTAwLgYDVQQKDCdDaGluYSBGaW5hbmNpYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIDAeBgNVBAMMF0NGQ0EgR2xvYmFsIFJTQSBST09UIEcy",
    
    389
    +      "serialNumber": "ePkoIc73UVpF6g==",
    
    390
    +      "id": "68d677f4-b0d4-4632-aa81-a7409c34b8ee",
    
    391
    +      "last_modified": 1791233394201
    
    392
    +    },
    
    393
    +    {
    
    394
    +      "schema": 1790874064627,
    
    395
    +      "details": {
    
    396
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2077222",
    
    397
    +        "who": "",
    
    398
    +        "why": "",
    
    399
    +        "name": "",
    
    400
    +        "created": ""
    
    401
    +      },
    
    402
    +      "enabled": false,
    
    403
    +      "issuerName": "MFYxCzAJBgNVBAYTAkNOMTAwLgYDVQQKDCdDaGluYSBGaW5hbmNpYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxFTATBgNVBAMMDENGQ0EgRVYgUk9PVA==",
    
    404
    +      "serialNumber": "V1JfiijGiegkWPH+Fx5j7Q==",
    
    405
    +      "id": "d5bae32a-daac-403e-a2d0-ca2eb2231041",
    
    406
    +      "last_modified": 1791233394198
    
    407
    +    },
    
    408
    +    {
    
    409
    +      "schema": 1790874064530,
    
    410
    +      "details": {
    
    411
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2077222",
    
    412
    +        "who": "",
    
    413
    +        "why": "",
    
    414
    +        "name": "",
    
    415
    +        "created": ""
    
    416
    +      },
    
    417
    +      "enabled": false,
    
    418
    +      "issuerName": "MFoxCzAJBgNVBAYTAkpQMSYwJAYDVQQKEx1TRUNPTSBUcnVzdCBTeXN0ZW1zIENvLiwgTHRkLjEjMCEGA1UEAxMaU0VDT00gVExTIEVDQyBSb290IENBIDIwMjQ=",
    
    419
    +      "serialNumber": "T+az/YRcbK7igx8id+gKnw==",
    
    420
    +      "id": "cb98a089-ec7a-4e52-bc08-31eb43815728",
    
    421
    +      "last_modified": 1791233394194
    
    422
    +    },
    
    423
    +    {
    
    424
    +      "schema": 1790874064341,
    
    425
    +      "details": {
    
    426
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2077222",
    
    427
    +        "who": "",
    
    428
    +        "why": "",
    
    429
    +        "name": "",
    
    430
    +        "created": ""
    
    431
    +      },
    
    432
    +      "enabled": false,
    
    433
    +      "issuerName": "MF0xCzAJBgNVBAYTAkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTI=",
    
    434
    +      "serialNumber": "IrmxL00F+e0T",
    
    435
    +      "id": "a1383d22-e6fd-4f35-8837-15ddb27ef93e",
    
    436
    +      "last_modified": 1791233394190
    
    437
    +    },
    
    438
    +    {
    
    439
    +      "schema": 1790874064147,
    
    440
    +      "details": {
    
    441
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2077222",
    
    442
    +        "who": "",
    
    443
    +        "why": "",
    
    444
    +        "name": "",
    
    445
    +        "created": ""
    
    446
    +      },
    
    447
    +      "enabled": false,
    
    448
    +      "issuerName": "MF8xCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxNjA0BgNVBAMTLVNlY3RpZ28gUHVibGljIFNlcnZlciBBdXRoZW50aWNhdGlvbiBSb290IEU0Ng==",
    
    449
    +      "serialNumber": "AIEKq4YGXjOEvyglU57/P4g=",
    
    450
    +      "id": "637d67cd-89bf-4c25-90cd-ee7be62a591d",
    
    451
    +      "last_modified": 1791233394186
    
    452
    +    },
    
    453
    +    {
    
    454
    +      "schema": 1790874064043,
    
    455
    +      "details": {
    
    456
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2077222",
    
    457
    +        "who": "",
    
    458
    +        "why": "",
    
    459
    +        "name": "",
    
    460
    +        "created": ""
    
    461
    +      },
    
    462
    +      "enabled": false,
    
    463
    +      "issuerName": "MFoxCzAJBgNVBAYTAkpQMSYwJAYDVQQKEx1TRUNPTSBUcnVzdCBTeXN0ZW1zIENvLiwgTHRkLjEjMCEGA1UEAxMaU0VDT00gVExTIEVDQyBSb290IENBIDIwMjQ=",
    
    464
    +      "serialNumber": "T+az/DaNHzV/czSbV8b+SQ==",
    
    465
    +      "id": "489486f0-960f-4185-8afd-219198f6f558",
    
    466
    +      "last_modified": 1791233394183
    
    467
    +    },
    
    468
    +    {
    
    469
    +      "schema": 1790874063948,
    
    470
    +      "details": {
    
    471
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2077222",
    
    472
    +        "who": "",
    
    473
    +        "why": "",
    
    474
    +        "name": "",
    
    475
    +        "created": ""
    
    476
    +      },
    
    477
    +      "enabled": false,
    
    478
    +      "issuerName": "MDsxCzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTQ==",
    
    479
    +      "serialNumber": "Pnz9VM3l4W1RzEmOGx7NSA==",
    
    480
    +      "id": "320d71d5-36a8-4410-9ad4-2124add204ff",
    
    481
    +      "last_modified": 1791233394178
    
    482
    +    },
    
    483
    +    {
    
    484
    +      "schema": 1790874063852,
    
    485
    +      "details": {
    
    486
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2077222",
    
    487
    +        "who": "",
    
    488
    +        "why": "",
    
    489
    +        "name": "",
    
    490
    +        "created": ""
    
    491
    +      },
    
    492
    +      "enabled": false,
    
    493
    +      "issuerName": "MFoxCzAJBgNVBAYTAkpQMSYwJAYDVQQKEx1TRUNPTSBUcnVzdCBTeXN0ZW1zIENvLiwgTHRkLjEjMCEGA1UEAxMaU0VDT00gVExTIFJTQSBSb290IENBIDIwMjQ=",
    
    494
    +      "serialNumber": "R6WRK1Xe7A7RwUFO96H4fQ==",
    
    495
    +      "id": "7891cb25-37db-4dcb-9218-c976cc847eb7",
    
    496
    +      "last_modified": 1791233394175
    
    497
    +    },
    
    498
    +    {
    
    499
    +      "schema": 1790874063748,
    
    500
    +      "details": {
    
    501
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2077222",
    
    502
    +        "who": "",
    
    503
    +        "why": "",
    
    504
    +        "name": "",
    
    505
    +        "created": ""
    
    506
    +      },
    
    507
    +      "enabled": false,
    
    508
    +      "issuerName": "MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2",
    
    509
    +      "serialNumber": "AIRDWFKkZS7STOWUW1z2GLE=",
    
    510
    +      "id": "dd420d19-4bce-4fe6-911a-7025430247be",
    
    511
    +      "last_modified": 1791233394172
    
    512
    +    },
    
    513
    +    {
    
    514
    +      "schema": 1790874064721,
    
    515
    +      "details": {
    
    516
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2077222",
    
    517
    +        "who": "",
    
    518
    +        "why": "",
    
    519
    +        "name": "",
    
    520
    +        "created": ""
    
    521
    +      },
    
    522
    +      "enabled": false,
    
    523
    +      "issuerName": "MF8xCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxNjA0BgNVBAMTLVNlY3RpZ28gUHVibGljIFNlcnZlciBBdXRoZW50aWNhdGlvbiBSb290IFI0Ng==",
    
    524
    +      "serialNumber": "VS0Duo3zjBUCApvCbQzk+Q==",
    
    525
    +      "id": "2823a801-82b1-43f1-858b-f740b601ddb4",
    
    526
    +      "last_modified": 1791233394168
    
    527
    +    },
    
    528
    +    {
    
    529
    +      "schema": 1790874064246,
    
    530
    +      "details": {
    
    531
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2077222",
    
    532
    +        "who": "",
    
    533
    +        "why": "",
    
    534
    +        "name": "",
    
    535
    +        "created": ""
    
    536
    +      },
    
    537
    +      "enabled": false,
    
    538
    +      "issuerName": "MFYxCzAJBgNVBAYTAkNOMTAwLgYDVQQKDCdDaGluYSBGaW5hbmNpYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxFTATBgNVBAMMDENGQ0EgRVYgUk9PVA==",
    
    539
    +      "serialNumber": "fXSiQkZ5TZ3Exw==",
    
    540
    +      "id": "0adcd89e-b78e-41c7-b10e-4d4f1ab97cc8",
    
    541
    +      "last_modified": 1791233394165
    
    542
    +    },
    
    543
    +    {
    
    544
    +      "schema": 1790874064436,
    
    545
    +      "details": {
    
    546
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2077222",
    
    547
    +        "who": "",
    
    548
    +        "why": "",
    
    549
    +        "name": "",
    
    550
    +        "created": ""
    
    551
    +      },
    
    552
    +      "enabled": false,
    
    553
    +      "issuerName": "MEgxCzAJBgNVBAYTAkRFMRUwEwYDVQQKEwxELVRydXN0IEdtYkgxIjAgBgNVBAMTGUQtVFJVU1QgRVYgUm9vdCBDQSAyIDIwMjM=",
    
    554
    +      "serialNumber": "Uu0msDCRjfC0h03kRtj2Ig==",
    
    555
    +      "id": "665bdb86-cf5b-451f-bb69-c0518c84b960",
    
    556
    +      "last_modified": 1791233394162
    
    557
    +    },
    
    558
    +    {
    
    559
    +      "schema": 1790553607419,
    
    560
    +      "details": {
    
    561
    +        "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=2077222",
    
    562
    +        "who": "",
    
    563
    +        "why": "",
    
    564
    +        "name": "",
    
    565
    +        "created": ""
    
    566
    +      },
    
    567
    +      "enabled": false,
    
    568
    +      "issuerName": "MFoxCzAJBgNVBAYTAkpQMSYwJAYDVQQKEx1TRUNPTSBUcnVzdCBTeXN0ZW1zIENvLiwgTHRkLjEjMCEGA1UEAxMaU0VDT00gVExTIFJTQSBSb290IENBIDIwMjQ=",
    
    569
    +      "serialNumber": "R6WRKuXIqENrxkAQkNTByQ==",
    
    570
    +      "id": "94a3e6fb-153b-45e8-b50a-0f212970114d",
    
    571
    +      "last_modified": 1791233394158
    
    572
    +    },
    
    3 573
         {
    
    4 574
           "schema": 1781802209467,
    
    5 575
           "details": {
    
    ... ... @@ -28199,5 +28769,5 @@
    28199 28769
           "last_modified": 1480349158647
    
    28200 28770
         }
    
    28201 28771
       ],
    
    28202
    -  "timestamp": 1781825227560
    
    28772
    +  "timestamp": 1793208925525
    
    28203 28773
     }
    

  • toolkit/components/tor-launcher/TorBootstrapRequest.sys.mjs
    ... ... @@ -7,6 +7,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
    7 7
         "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs",
    
    8 8
       TorProviderTopics:
    
    9 9
         "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs",
    
    10
    +  TorBootstrapError:
    
    11
    +    "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs",
    
    10 12
     });
    
    11 13
     
    
    12 14
     const log = console.createInstance({
    
    ... ... @@ -35,12 +37,18 @@ export class TorBootstrapRequest {
    35 37
       #timeoutID = null;
    
    36 38
     
    
    37 39
       observe(subject, topic) {
    
    38
    -    const obj = subject?.wrappedJSObject;
    
    39 40
         switch (topic) {
    
    40 41
           case lazy.TorProviderTopics.BootstrapStatus: {
    
    41
    -        const progress = obj.PROGRESS;
    
    42
    +        let obj;
    
    43
    +        try {
    
    44
    +          obj = subject.QueryInterface(Ci.torITorBootstrapStatus);
    
    45
    +        } catch (e) {
    
    46
    +          log.warn("BootstrapStatus with an unexpected subject", e, subject);
    
    47
    +          break;
    
    48
    +        }
    
    49
    +        const progress = obj.progress;
    
    42 50
             if (this.onbootstrapstatus) {
    
    43
    -          const status = obj.TAG;
    
    51
    +          const status = obj.tag;
    
    44 52
               this.onbootstrapstatus(progress, status);
    
    45 53
             }
    
    46 54
             if (progress === 100) {
    
    ... ... @@ -55,8 +63,21 @@ export class TorBootstrapRequest {
    55 63
             break;
    
    56 64
           }
    
    57 65
           case lazy.TorProviderTopics.BootstrapError: {
    
    66
    +        let obj;
    
    67
    +        try {
    
    68
    +          obj = subject.QueryInterface(Ci.torITorBootstrapStatus);
    
    69
    +        } catch (e) {
    
    70
    +          log.warn("TorBootstrapError with an unexpected subject", e, subject);
    
    71
    +          break;
    
    72
    +        }
    
    58 73
             log.info("TorBootstrapRequest: observerd TorBootstrapError", obj);
    
    59
    -        this.#stop(obj);
    
    74
    +        this.#stop(
    
    75
    +          new lazy.TorBootstrapError({
    
    76
    +            phase: obj.tag,
    
    77
    +            summary: obj.summary,
    
    78
    +            reason: obj.reason,
    
    79
    +          })
    
    80
    +        );
    
    60 81
             break;
    
    61 82
           }
    
    62 83
         }
    

  • toolkit/components/tor-launcher/TorDomainIsolator.sys.mjs
    ... ... @@ -288,8 +288,34 @@ class TorDomainIsolatorImpl {
    288 288
             // TODO: What UX to use here? See tor-browser#41708
    
    289 289
           }
    
    290 290
         } else if (topic === lazy.TorProviderTopics.CircuitCredentialsMatched) {
    
    291
    -      const { username, password, circuits } = subject.wrappedJSObject;
    
    292
    -      this.#updateCircuits(username, password, circuits);
    
    291
    +      let streamInfo;
    
    292
    +      try {
    
    293
    +        streamInfo = subject?.QueryInterface(Ci.torITorStreamInfo);
    
    294
    +      } catch (e) {
    
    295
    +        logger.warn(
    
    296
    +          "CircuitCredentialsMatched with invalid stream info",
    
    297
    +          e,
    
    298
    +          subject
    
    299
    +        );
    
    300
    +      }
    
    301
    +      if (streamInfo) {
    
    302
    +        const circuits = streamInfo.circuits?.map(circuit =>
    
    303
    +          circuit.map(node => {
    
    304
    +            return {
    
    305
    +              fingerprint: node.fingerprint,
    
    306
    +              ipAddrs: node.ipAddrs,
    
    307
    +              isBridge: node.isBridge,
    
    308
    +              bridgeType: node.bridgeType,
    
    309
    +              regionCode: node.regionCode,
    
    310
    +            };
    
    311
    +          })
    
    312
    +        );
    
    313
    +        this.#updateCircuits(
    
    314
    +          streamInfo.socksUsername,
    
    315
    +          streamInfo.socksPassword,
    
    316
    +          circuits
    
    317
    +        );
    
    318
    +      }
    
    293 319
         }
    
    294 320
       }
    
    295 321
     
    

  • toolkit/components/tor-launcher/TorLauncherUtil.sys.mjs
    ... ... @@ -644,17 +644,22 @@ export const TorLauncherUtil = {
    644 644
        * Therefore, the messages should avoid containing any information that might
    
    645 645
        * identify information about the user.
    
    646 646
        *
    
    647
    -   * @param {string} type The log type (ERR, WARN, etc...)
    
    648
    -   * @param {string} msg The log message
    
    647
    +   * @param {string} severity The log severity (ERR, WARN, etc...)
    
    648
    +   * @param {string} message The log message
    
    649 649
        */
    
    650
    -  log(type, msg) {
    
    650
    +  log(severity, message) {
    
    651 651
         const timestamp =
    
    652 652
           new Date().toISOString().replace("T", " ").replace("Z", "") + " UTC";
    
    653 653
         Services.obs.notifyObservers(
    
    654
    -      { type, msg, timestamp },
    
    654
    +      {
    
    655
    +        severity,
    
    656
    +        message,
    
    657
    +        timestamp,
    
    658
    +        QueryInterface: ChromeUtils.generateQI([Ci.torITorLogEntry]),
    
    659
    +      },
    
    655 660
           lazy.TorProviderTopics.TorLog
    
    656 661
         );
    
    657
    -    if (type === "WARN" || type === "ERR") {
    
    662
    +    if (severity === "WARN" || severity === "ERR") {
    
    658 663
           // Notify so that Copy Log can be enabled.
    
    659 664
           Services.obs.notifyObservers(null, lazy.TorProviderTopics.HasWarnOrErr);
    
    660 665
         }
    

  • toolkit/components/tor-launcher/TorProvider.sys.mjs
    ... ... @@ -7,10 +7,7 @@ import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
    7 7
     import { TorLauncherUtil } from "moz-src:///toolkit/components/tor-launcher/TorLauncherUtil.sys.mjs";
    
    8 8
     import { TorParsers } from "moz-src:///toolkit/components/tor-launcher/TorParsers.sys.mjs";
    
    9 9
     import { TorProviderBase } from "moz-src:///toolkit/components/tor-launcher/TorProviderBase.sys.mjs";
    
    10
    -import {
    
    11
    -  TorBootstrapError,
    
    12
    -  TorProviderTopics,
    
    13
    -} from "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs";
    
    10
    +import { TorProviderTopics } from "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs";
    
    14 11
     
    
    15 12
     const lazy = {};
    
    16 13
     ChromeUtils.defineESModuleGetters(lazy, {
    
    ... ... @@ -57,12 +54,12 @@ const logger = console.createInstance({
    57 54
      * @typedef NodeData
    
    58 55
      * @property {NodeFingerprint} fingerprint The node fingerprint
    
    59 56
      * @property {string[]} ipAddrs The ip addresses associated with this node
    
    60
    - * @property {string?} bridgeType The bridge type for this node, or "" if the
    
    61
    - *   node is a bridge but the type is unknown, or null if this is not a bridge
    
    62
    - *   node
    
    63
    - * @property {string?} regionCode An upper case 2-letter ISO3166-1 code for the
    
    64
    - *   first ip address, or null if there is no region. This should also be a
    
    65
    - *   valid BCP47 Region subtag
    
    57
    + * @property {boolean} isBridge Whether the node is a bridge.
    
    58
    + * @property {string} bridgeType The bridge type for this node, or an empty
    
    59
    + *   string if the type is unknown or the node is not a bridge.
    
    60
    + * @property {string} regionCode An upper case 2-letter ISO3166-1 code for the
    
    61
    + *   first ip address. This should also be a valid BCP47 Region subtag.
    
    62
    + *   It will be an empty string if the node is not a bridge.
    
    66 63
      */
    
    67 64
     
    
    68 65
     const Preferences = Object.freeze({
    
    ... ... @@ -929,14 +926,16 @@ export class TorProvider extends TorProviderBase {
    929 926
           const node = {
    
    930 927
             fingerprint: id,
    
    931 928
             ipAddrs: [],
    
    932
    -        bridgeType: null,
    
    933
    -        regionCode: null,
    
    929
    +        isBridge: false,
    
    930
    +        bridgeType: "",
    
    931
    +        regionCode: "",
    
    934 932
           };
    
    935 933
     
    
    936 934
           const bridge = (await this.#controller.getBridges())?.find(
    
    937 935
             foundBridge => foundBridge.id?.toUpperCase() === id.toUpperCase()
    
    938 936
           );
    
    939 937
           if (bridge) {
    
    938
    +        node.isBridge = true;
    
    940 939
             node.bridgeType = bridge.transport ?? "";
    
    941 940
             // Attempt to get an IP address from bridge address string.
    
    942 941
             const ip = bridge.addr.match(/^\[?([^\]]+)\]?:\d+$/)?.[1];
    
    ... ... @@ -1006,7 +1005,10 @@ export class TorProvider extends TorProviderBase {
    1006 1005
        */
    
    1007 1006
       #processBootstrapStatus(statusObj, isNotification) {
    
    1008 1007
         // Notify observers
    
    1009
    -    Services.obs.notifyObservers(statusObj, TorProviderTopics.BootstrapStatus);
    
    1008
    +    Services.obs.notifyObservers(
    
    1009
    +      new TorBootstrapStatus(statusObj),
    
    1010
    +      TorProviderTopics.BootstrapStatus
    
    1011
    +    );
    
    1010 1012
     
    
    1011 1013
         if (statusObj.PROGRESS === 100) {
    
    1012 1014
           return;
    
    ... ... @@ -1044,11 +1046,7 @@ export class TorProvider extends TorProviderBase {
    1044 1046
           // anymore, since the first error eligible for notification will as a
    
    1045 1047
           // matter of fact cancel the bootstrap.
    
    1046 1048
           Services.obs.notifyObservers(
    
    1047
    -        new TorBootstrapError({
    
    1048
    -          phase: statusObj.TAG,
    
    1049
    -          reason: statusObj.REASON,
    
    1050
    -          summary: statusObj.SUMMARY,
    
    1051
    -        }),
    
    1049
    +        new TorBootstrapStatus(statusObj),
    
    1052 1050
             TorProviderTopics.BootstrapError
    
    1053 1051
           );
    
    1054 1052
         }
    
    ... ... @@ -1147,14 +1145,24 @@ export class TorProvider extends TorProviderBase {
    1147 1145
        * @param {StreamID} _streamId The ID of the stream that switched to the
    
    1148 1146
        * sentconnect status.
    
    1149 1147
        * @param {CircuitID} circuitId The ID of the circuit used by the stream
    
    1150
    -   * @param {string} username The SOCKS username
    
    1151
    -   * @param {string} password The SOCKS password
    
    1148
    +   * @param {string} socksUsername The SOCKS username
    
    1149
    +   * @param {string} socksPassword The SOCKS password
    
    1152 1150
        */
    
    1153
    -  async onStreamSentConnect(_streamId, circuitId, username, password) {
    
    1154
    -    if (!username || !password) {
    
    1151
    +  async onStreamSentConnect(
    
    1152
    +    _streamId,
    
    1153
    +    circuitId,
    
    1154
    +    socksUsername,
    
    1155
    +    socksPassword
    
    1156
    +  ) {
    
    1157
    +    if (!socksUsername || !socksPassword) {
    
    1155 1158
           return;
    
    1156 1159
         }
    
    1157
    -    logger.debug("Stream sentconnect event", username, password, circuitId);
    
    1160
    +    logger.debug(
    
    1161
    +      "Stream sentconnect event",
    
    1162
    +      socksUsername,
    
    1163
    +      socksPassword,
    
    1164
    +      circuitId
    
    1165
    +    );
    
    1158 1166
         if (!this.#circuits.has(circuitId)) {
    
    1159 1167
           // tor-browser#42132: When using onion-grater (e.g., in Tails), we might
    
    1160 1168
           // not receive the CIRC BUILT event, as it is impossible to know whether
    
    ... ... @@ -1194,13 +1202,33 @@ export class TorProvider extends TorProviderBase {
    1194 1202
         );
    
    1195 1203
         Services.obs.notifyObservers(
    
    1196 1204
           {
    
    1197
    -        wrappedJSObject: {
    
    1198
    -          username,
    
    1199
    -          password,
    
    1200
    -          circuits,
    
    1201
    -        },
    
    1205
    +        socksUsername,
    
    1206
    +        socksPassword,
    
    1207
    +        circuits,
    
    1208
    +        QueryInterface: ChromeUtils.generateQI([Ci.torITorStreamInfo]),
    
    1202 1209
           },
    
    1203 1210
           TorProviderTopics.CircuitCredentialsMatched
    
    1204 1211
         );
    
    1205 1212
       }
    
    1206 1213
     }
    
    1214
    +
    
    1215
    +/**
    
    1216
    + * A _javascript_ implementation of torITorBootstrapStatus.
    
    1217
    + */
    
    1218
    +class TorBootstrapStatus {
    
    1219
    +  /**
    
    1220
    +   * Construct a typed status object from a status object sent by TorController.
    
    1221
    +   *
    
    1222
    +   * @param {object} statusObj The status object parsed by the controller
    
    1223
    +   */
    
    1224
    +  constructor(statusObj) {
    
    1225
    +    this.progress = statusObj.PROGRESS;
    
    1226
    +    this.severity = statusObj.TYPE;
    
    1227
    +    this.tag = statusObj.TAG;
    
    1228
    +    this.summary = statusObj.SUMMARY;
    
    1229
    +    this.reason = statusObj.REASON ?? "";
    
    1230
    +    this.shouldWarn = statusObj.RECOMMENDATION !== "ignore";
    
    1231
    +  }
    
    1232
    +
    
    1233
    +  QueryInterface = ChromeUtils.generateQI([Ci.torITorBootstrapStatus]);
    
    1234
    +}

  • toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs
    ... ... @@ -81,9 +81,9 @@ export const TorProviders = Object.freeze({
    81 81
     
    
    82 82
     /**
    
    83 83
      * @typedef {object} LogEntry An object with a log message
    
    84
    - * @property {string} timestamp The local date-time stamp at which we received the message
    
    85
    - * @property {string} type The message level
    
    86
    - * @property {string} msg The message
    
    84
    + * @property {string} timestamp The UTC date-time stamp at which we received the message
    
    85
    + * @property {string} severity The message severity
    
    86
    + * @property {string} message The message
    
    87 87
      */
    
    88 88
     
    
    89 89
     /**
    
    ... ... @@ -182,7 +182,18 @@ export class TorProviderBuilder {
    182 182
           1000
    
    183 183
         );
    
    184 184
         this.#logObserver = subject => {
    
    185
    -      this.#addLogEntry(subject.wrappedJSObject);
    
    185
    +      let entry;
    
    186
    +      try {
    
    187
    +        entry = subject.QueryInterface(Ci.torITorLogEntry);
    
    188
    +      } catch (e) {
    
    189
    +        lazy.logger.warn("TorLog with invalid subject", e, subject);
    
    190
    +        return;
    
    191
    +      }
    
    192
    +      this.#addLogEntry({
    
    193
    +        severity: entry.severity,
    
    194
    +        message: entry.message,
    
    195
    +        timestamp: entry.timestamp,
    
    196
    +      });
    
    186 197
         };
    
    187 198
         Services.obs.addObserver(this.#logObserver, TorProviderTopics.TorLog);
    
    188 199
     
    

  • toolkit/components/tor-launcher/TorProviderMock.sys.mjs
    ... ... @@ -3,19 +3,19 @@ import { TorProviderBase } from "moz-src:///toolkit/components/tor-launcher/TorP
    3 3
     import { TorProviderTopics } from "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs";
    
    4 4
     
    
    5 5
     const kBootstrapSteps = [
    
    6
    -  { PROGRESS: 5, TAG: "starting", SUMMARY: "Starting" },
    
    7
    -  { PROGRESS: 14, TAG: "handshake", SUMMARY: "Handshaking with a relay" },
    
    6
    +  { progress: 5, tag: "starting", summary: "Starting" },
    
    7
    +  { progress: 14, tag: "handshake", summary: "Handshaking with a relay" },
    
    8 8
       {
    
    9
    -    PROGRESS: 45,
    
    10
    -    TAG: "requesting_descriptors",
    
    11
    -    SUMMARY: "Asking for relay descriptors",
    
    9
    +    progress: 45,
    
    10
    +    tag: "requesting_descriptors",
    
    11
    +    summary: "Asking for relay descriptors",
    
    12 12
       },
    
    13 13
       {
    
    14
    -    PROGRESS: 75,
    
    15
    -    TAG: "loading_descriptors",
    
    16
    -    SUMMARY: "Loading relay descriptors",
    
    14
    +    progress: 75,
    
    15
    +    tag: "loading_descriptors",
    
    16
    +    summary: "Loading relay descriptors",
    
    17 17
       },
    
    18
    -  { PROGRESS: 100, TAG: "done", SUMMARY: "Done" },
    
    18
    +  { progress: 100, tag: "done", summary: "Done" },
    
    19 19
     ];
    
    20 20
     
    
    21 21
     const kBootstrapStepDelayMs = 500;
    
    ... ... @@ -48,7 +48,15 @@ export class TorProviderMock extends TorProviderBase {
    48 48
           const id = setTimeout(
    
    49 49
             () => {
    
    50 50
               Services.obs.notifyObservers(
    
    51
    -            { ...step, TYPE: "NOTICE" },
    
    51
    +            {
    
    52
    +              ...step,
    
    53
    +              severity: "NOTICE",
    
    54
    +              reason: "",
    
    55
    +              shouldWarn: false,
    
    56
    +              QueryInterface: ChromeUtils.generateQI([
    
    57
    +                Ci.torITorBootstrapStatus,
    
    58
    +              ]),
    
    59
    +            },
    
    52 60
                 TorProviderTopics.BootstrapStatus
    
    53 61
               );
    
    54 62
             },
    

  • toolkit/components/tor-launcher/moz.build
    1
    +XPIDL_MODULE = "torlauncher"
    
    2
    +XPIDL_SOURCES += ["tor-launcher.idl"]
    
    3
    +
    
    1 4
     MOZ_SRC_FILES += [
    
    2 5
         "TorBootstrapRequest.sys.mjs",
    
    3 6
         "TorControlPort.sys.mjs",
    

  • toolkit/components/tor-launcher/tor-launcher.idl
    1
    +/* This Source Code Form is subject to the terms of the Mozilla Public
    
    2
    + * License, v. 2.0. If a copy of the MPL was not distributed with this
    
    3
    + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
    
    4
    +
    
    5
    +#include "nsISupports.idl"
    
    6
    +
    
    7
    +/**
    
    8
    + * The information about the bootstrap status.
    
    9
    + * The values are extracted from C-tor's STATUS_CLIENT events, except for
    
    10
    + * shouldWarn, which is derived from the RECOMMENDATION value.
    
    11
    + */
    
    12
    +[scriptable, uuid(64b3197d-0b1e-46a1-aa18-8cea11bacf97)]
    
    13
    +interface torITorBootstrapStatus : nsISupports {
    
    14
    +  readonly attribute octet progress;
    
    15
    +  readonly attribute ACString severity;
    
    16
    +  readonly attribute ACString tag;
    
    17
    +  readonly attribute ACString summary;
    
    18
    +  readonly attribute ACString reason;
    
    19
    +  readonly attribute boolean shouldWarn;
    
    20
    +};
    
    21
    +
    
    22
    +/**
    
    23
    + * A tor log message.
    
    24
    + * The severity might be also something not used by C-tor.
    
    25
    + */
    
    26
    +[scriptable, uuid(f6d18f7d-c29c-4bf3-bf70-7d6bed3b427e)]
    
    27
    +interface torITorLogEntry : nsISupports {
    
    28
    +  readonly attribute ACString severity;
    
    29
    +  readonly attribute ACString message;
    
    30
    +  readonly attribute ACString timestamp;
    
    31
    +};
    
    32
    +
    
    33
    +/**
    
    34
    + * A tor node information.
    
    35
    + * The bridgeType and regionCode should be used only when isBridge is true.
    
    36
    + */
    
    37
    +[scriptable, uuid(7547d8e5-a93e-4a0c-9876-a7a2cc44e80f)]
    
    38
    +interface torITorNodeInfo : nsISupports {
    
    39
    +  readonly attribute ACString fingerprint;
    
    40
    +  readonly attribute Array<ACString> ipAddrs;
    
    41
    +  readonly attribute boolean isBridge;
    
    42
    +  readonly attribute ACString bridgeType;
    
    43
    +  readonly attribute ACString regionCode;
    
    44
    +};
    
    45
    +
    
    46
    +/**
    
    47
    + * The information to associate a SOCKS isolation credential (the combination
    
    48
    + * of SOCKS username and password) to one or more circuits.
    
    49
    + */
    
    50
    +[scriptable, uuid(a85269dc-1aca-4aec-87a6-88ff035f0eaa)]
    
    51
    +interface torITorStreamInfo : nsISupports {
    
    52
    +  readonly attribute ACString socksUsername;
    
    53
    +  readonly attribute ACString socksPassword;
    
    54
    +  readonly attribute Array<Array<torITorNodeInfo> > circuits;
    
    55
    +};

  • toolkit/modules/TorAndroidIntegration.sys.mjs
    ... ... @@ -130,13 +130,19 @@ class TorAndroidIntegrationImpl {
    130 130
               {}
    
    131 131
             );
    
    132 132
             break;
    
    133
    -      case lazy.TorProviderTopics.TorLog:
    
    134
    -        lazy.EventDispatcher.instance.sendRequest(EmittedEvents.torLogs, {
    
    135
    -          logType: subj.wrappedJSObject.type ?? "",
    
    136
    -          message: subj.wrappedJSObject.msg ?? "",
    
    137
    -          timestamp: subj.wrappedJSObject.timestamp ?? "",
    
    138
    -        });
    
    133
    +      case lazy.TorProviderTopics.TorLog: {
    
    134
    +        try {
    
    135
    +          const entry = subj.QueryInterface(Ci.torITorLogEntry);
    
    136
    +          lazy.EventDispatcher.instance.sendRequest(EmittedEvents.torLogs, {
    
    137
    +            logType: entry.severity,
    
    138
    +            message: entry.message,
    
    139
    +            timestamp: entry.timestamp,
    
    140
    +          });
    
    141
    +        } catch (e) {
    
    142
    +          logger.warn("Failed to dispatch a log event", e, subj);
    
    143
    +        }
    
    139 144
             break;
    
    145
    +      }
    
    140 146
           case lazy.TorSettingsTopics.Ready:
    
    141 147
             lazy.EventDispatcher.instance.sendRequest(EmittedEvents.settingsReady, {
    
    142 148
               settings: lazy.TorSettings.getSettings(),
    

  • tools/torbrowser/l10n/migrate.py
    1 1
     """
    
    2
    -Migration script for running migration recipes.
    
    2
    +Migration script for running Tor Browser migration recipes.
    
    3 3
     
    
    4
    -Requires the mozilla fluent.migrate package (available with pip).
    
    4
    +Based on the Firefox migration script, adapted for Tor Browser. See
    
    5
    +https://firefox-source-docs.mozilla.org/l10n/migrations/ for more context and
    
    6
    +information on how to write the migration recipes.
    
    5 7
     
    
    6
    -NOTE: This will likely send queries to the weblate API as part of the run.
    
    7
    -Weblate may have a query limit of 100 requests per day for unauthorised users.
    
    8 8
     
    
    9
    +## Concepts
    
    10
    +
    
    11
    ++ String. Here, when we say "string", it refers to a piece of text that can be
    
    12
    +  translated. In practice, you can think of it as a being defined by a triple:
    
    13
    +  a file path relative to a locale's directory, an ID, and some content. For
    
    14
    +  example `("./tor-browser.ftl", "my-id", "my-id =\\n .label=Content")`.
    
    15
    +  When a string is translated the file path and ID stay the same, whilst the
    
    16
    +  translatable part of the content can vary.
    
    17
    ++ Reference files. These are the en-US source Fluent files that contain the
    
    18
    +  *manual* string changes made by a developer. For most migrations, there is
    
    19
    +  only one reference file.
    
    20
    ++ Migration recipe. A collection of instructions that take some string inputs
    
    21
    +  and transforms them into new strings. These are meant to mirror the manual
    
    22
    +  changes that were made in the reference files, in a way that can be applied
    
    23
    +  to any locale.
    
    24
    ++ Translation repository. This refers to a local copy of the `translation`
    
    25
    +  repository (https://gitlab.torproject.org/tpo/translation). This repository
    
    26
    +  contains a different branch for different collections of strings. For
    
    27
    +  Tor Browser we usually use `tor-browser`, `base-browser` and
    
    28
    +  `fenix-torbrowserstringsxml`. Each branch will contain an `en-US` directory,
    
    29
    +  which contains the "source" files for translators to translate. These source
    
    30
    +  files are taken from the `tor-browser` repository. The branches also contain
    
    31
    +  a directory for each locale that is to be translated, in a file structure that
    
    32
    +  reflects the source `en-US` directory. This is where the migration script will
    
    33
    +  write changes to the translated files.
    
    34
    +
    
    35
    +
    
    36
    +## Usage
    
    37
    +
    
    38
    +You will likely want to run this with `./mach python`, so that it uses the
    
    39
    +`fluent.migrate` and `fluent.syntax` packages provided with the `tor-browser`
    
    40
    +repository.
    
    41
    +
    
    42
    +For example, you might run this as
    
    9 43
     
    
    10
    -Based on the firefox migration script, adapted for tor browser. See
    
    11
    -https://firefox-source-docs.mozilla.org/l10n/migrations/ for more context and
    
    12
    -information on how to write the migration recipes.
    
    44
    +```
    
    45
    +    ./mach python migrate.py --translation-git /home/user/translation --locales 'es-ES sr' ./migrations/bug-xxxx-my-migration-recipe.py
    
    46
    +```
    
    47
    +
    
    48
    +Here `/home/user/translation` is a local translation repository with one of its
    
    49
    +branches checked out (see below). This will run the migration recipe given in
    
    50
    +`./migrations/bug-xxxx-my-migration-recipe.py` for the `es-ES` and `sr`
    
    51
    +sub-directories within the `translation/` directory.
    
    52
    +
    
    53
    +### Changing the reference location
    
    54
    +
    
    55
    +You can optionally run the script with `--reference_directory` to read the
    
    56
    +reference en-US files from somewhere other than the `translation` directory.
    
    57
    +This can be useful  when your manual changes have yet to reach the translation
    
    58
    +repository, so you want to point the script to read the reference from the
    
    59
    +`tor-browser` working directory instead. E.g.
    
    60
    +
    
    61
    +```
    
    62
    +    --reference-directory /home/user/tor-browser/toolkit/locales/en-US/toolkit/global/
    
    63
    +```
    
    64
    +
    
    65
    +
    
    66
    +## Recipes
    
    67
    +
    
    68
    +Migration recipes are meant to mirror some manual change that was made to the
    
    69
    +en-US source strings, in a way that can be reproduced in another locale.
    
    13 70
     
    
    71
    +### Example 1
    
    14 72
     
    
    15
    -Usage with an example:
    
    73
    +For this example, a developer wants to remove the string content from
    
    74
    +`old-strings.properties`:
    
    16 75
     
    
    17
    -We have some en-US file "updated.ftl" in the tor-browser repository that we have
    
    18
    -added new strings to. Some of these strings are copies or concatenations of some
    
    19
    -already existing strings in a `.dtd`, `.properties` or `.ftl` file (including
    
    20
    -strings within "updated.ftl").
    
    76
    +```
    
    77
    +my.old.string=Some string content
    
    78
    +my.old.title=This is the tooltip
    
    79
    +```
    
    80
    +
    
    81
    +And add them to `tor-browser.ftl` instead, as:
    
    21 82
     
    
    22
    -So we also write a migration recipe in
    
    23
    -`./l10n_migrations/bug-xxxx-my-migration-recipe.py`
    
    24
    -which includes a `migrate(ctx)` method. This method takes the old strings and
    
    25
    -create new Messages or Terms for "updated.ftl" using `ctx.add_transforms`. These
    
    26
    -should match what was already done manually in "updated.ftl".
    
    83
    +```
    
    84
    +my-new-string = Some string content
    
    85
    +    .title = This is the tooltip
    
    86
    +```
    
    87
    +
    
    88
    +For the en-US source strings, the developer simply deletes the two lines in the
    
    89
    +`old-strings.properties` file (or the whole file, if these are the last two).
    
    90
    +And they manually write in the content of `my-new-string` to `tor-browser.ftl`.
    
    91
    +These manual changes will then eventually reach the `translation` repository to
    
    92
    +be translated.
    
    93
    +
    
    94
    +But to save translators making the same manual changes, or re-translating, the
    
    95
    +developer can write a migration recipe that mirror's their manual changes. In
    
    96
    +this case, this migration recipe would contain:
    
    97
    +
    
    98
    +```
    
    99
    +ctx.add_transforms(
    
    100
    +    "tor-browser.ftl", # The path to write to.
    
    101
    +    "tor-browser.ftl", # The path to the reference file.
    
    102
    +    transforms_from(
    
    103
    +        \"\"\"
    
    104
    +my-new-string = { COPY(path, "my.old.string") }
    
    105
    +    .title = { COPY(path, "my.old.title") }
    
    106
    +\"\"\",
    
    107
    +        path="old-strings.properties", # The path to read from.
    
    108
    +    ),
    
    109
    +)
    
    110
    +```
    
    111
    +
    
    112
    +Note, the first argument of `add_transforms` is the target path, which is
    
    113
    +`tor-browser.ftl` in this case. This means that the migration script will try to
    
    114
    +write the resulting `my-new-string` to the locale's `./tor-browser.ftl` file.
    
    115
    +For example, to `translation/es-ES/tor-browser.ftl`.
    
    116
    +
    
    117
    +The second argument refers to the *reference* path. In almost all cases the
    
    118
    +reference file will have the same name and path as the target path, but it has a
    
    119
    +different role. Here the path is relative to the reference directory, which
    
    120
    +contains the en-US source files. In this case, by default this means that
    
    121
    +`translation/en-US/tor-browser.ftl` will act as the reference, but the exact
    
    122
    +path could be changed with the `--reference-directory` argument. The existing
    
    123
    +`my-new-string` will be read from this file to confirm that the migration
    
    124
    +recipe produces the correct result.
    
    125
    +
    
    126
    +The file `old-strings.properties` is used as an input. This means that the
    
    127
    +existing strings in the locale's `old-strings.properties` file will be read to
    
    128
    +construct the new string `my-new-string`. For example, it will be read from
    
    129
    +`translation/es-ES/old-strings.properties`.
    
    130
    +
    
    131
    +The `COPY` acts like a Fluent function, which asks the transformation to copy
    
    132
    +the content from the translated string directly.
    
    133
    +
    
    134
    +### Example 2
    
    135
    +
    
    136
    +For this example, a developer wants to swap a string in `tor-browser.ftl` from:
    
    137
    +
    
    138
    +```
    
    139
    +my-old-string2 = Some string content
    
    140
    +    .title = This is the tooltip
    
    141
    +```
    
    142
    +
    
    143
    +to
    
    144
    +
    
    145
    +```
    
    146
    +my-new-string2 =
    
    147
    +    .label = Some string content
    
    148
    +    .title = This is the tooltip
    
    149
    +```
    
    27 150
     
    
    28
    -Then we copy the en-US file to our local translation repository, say at
    
    29
    -`/home/user/translation/en-US/updated.ftl`. The en-US file is needed as a
    
    30
    -reference during migration to know where in the new file the new Messages or
    
    31
    -Terms should be placed. Moreover, the migration can be verified against the
    
    32
    -en-US file to ensure it is working as intended.
    
    151
    +The developer removes `my-old-string2` from the file and adds `my-new-string2`
    
    152
    +instead. This manual change will then eventually reach the `translation`
    
    153
    +repository for `my-new-string2` to be translated.
    
    33 154
     
    
    34
    -Then we run the migration for individual locales:
    
    155
    +The developer also writes a migration recipe to mirror this change:
    
    35 156
     
    
    36 157
     ```
    
    37
    -    python migrate_l10n.py --translation-git /home/user/translation --locales 'es-ES sr' l10n_migrations.bug-xxxx-my-migration-recipe
    
    158
    +ctx.add_transforms(
    
    159
    +    "tor-browser.ftl",
    
    160
    +    "tor-browser.ftl",
    
    161
    +    transforms_from(
    
    162
    +        \"\"\"
    
    163
    +my-new-string2 =
    
    164
    +    .label = { COPY_PATTERN(path, "my-old-string2") }
    
    165
    +    .title = { COPY_PATTERN(path, "my-old-string2.title") }
    
    166
    +\"\"\",
    
    167
    +        path="tor-browser.ftl",
    
    168
    +    ),
    
    169
    +)
    
    38 170
     ```
    
    39 171
     
    
    40
    -This will first run a mock migration using the en-US localization files. This is
    
    41
    -to help ensure that the migration will work as intended. I.e. if we ran the
    
    42
    -migration recipe for en-US, using it as both the source of old strings and as a
    
    43
    -reference, then each transformation using these old strings should produce a
    
    44
    -Message or Term that is identical to the one in the reference file.
    
    172
    +Note that in this case the locale's `tor-browser.ftl` file is being used as both
    
    173
    +the input and the output.
    
    174
    +
    
    175
    +Note that for a Fluent-to-Fluent migration we use `COPY_PATTERN` rather than
    
    176
    +`COPY`. The second argument provides an ID that either point's to the
    
    177
    +Fluent Messages' value (roughly, the text content) by giving the Message's ID,
    
    178
    +or one of its attributes by adding a corresponding suffix to the ID.
    
    179
    +
    
    180
    +### Other examples
    
    181
    +
    
    182
    +Migration recipes can support a lot more, including more advanced
    
    183
    +transformations like removing or replacing HTML tags from content. There are
    
    184
    +some existing recipes in the `./migrations` directory that can be used as a
    
    185
    +reference.
    
    186
    +
    
    187
    +
    
    188
    +## translation directory
    
    189
    +
    
    190
    +In most cases, you will just need to check out a `translation` branch that
    
    191
    +contains the translated strings used by the migration recipe, as well as the
    
    192
    +Fluent file that will be written to. Usually, this will be a branch based on
    
    193
    +the origin/upstream `tor-browser` or `base-browser` branches.
    
    194
    +
    
    195
    +However, if the migration recipe requires source files that are in a different
    
    196
    +branch to the Fluent file that is being written to, then you will need to
    
    197
    +manually copy those files into the working directory before running the script.
    
    198
    +
    
    199
    +
    
    200
    +## Script procedure
    
    45 201
     
    
    46
    -Then this will run `migrate(ctx)` for the es-ES and sr locales. Some of the
    
    47
    -transformations will not be completed for a locale if that locale does not
    
    48
    -contain a translated version of the old strings the transformation requires
    
    49
    -(because there is no string to copy).
    
    202
    +### Test en-US migration
    
    50 203
     
    
    51
    -If one of the old strings is from a `.dtd` or `.properties` file, this will
    
    52
    -check whether they are marked as translated on weblate using its API. This
    
    53
    -prevents copying "placeholder" en-US values into the locale's fluent files.
    
    204
    +The script will first run a test migration using the en-US strings. It will run
    
    205
    +the migration using the files from `translation/en-US/` as the input. In the
    
    206
    +previous example 1, this will read `my.old.string` and `my.old.title` from
    
    207
    +`translation/en-US/old-strings.properties`. And in example 2, this will read
    
    208
    +`my-old-string2` from `translation/en-US/tor-browser.ftl`.
    
    54 209
     
    
    55
    -Any warnings or errors will be logged to stderr in the console.
    
    210
    +If any of the input strings are missing, this indicates that there is likely a
    
    211
    +mistake in the recipe or the translation directory and the script will be
    
    212
    +aborted without running the migration for the other locales.
    
    213
    +
    
    214
    +Otherwise, it then compares the result of this migration with the strings found
    
    215
    +in the reference files. In the previous example 1, this will read the entry for
    
    216
    +`my-new-string` from `<reference-dir>/tor-browser.ftl`, where `<reference-dir>`
    
    217
    +is `translation/en-US/` unless `--reference-directory` is given to use a
    
    218
    +different directory. If the reference entry for `my-new-string` differs from the
    
    219
    +entry that the recipe automatically produced, then this indicates that there is
    
    220
    +a mistake in the recipe and the script will be aborted.
    
    221
    +
    
    222
    +### Locale migrations
    
    223
    +
    
    224
    +If the test migration passed, then the recipe will be re-run for each of the
    
    225
    +specified locales. If the locale is missing some strings or files that a single
    
    226
    +transformation requires, then that transformation will be skipped and the
    
    227
    +corresponding entry won't be written to the Fluent file. Otherwise the new
    
    228
    +entries will be appended to the existing Fluent files.
    
    229
    +
    
    230
    +#### Non-Fluent strings and Weblate queries
    
    231
    +
    
    232
    +Some non-Fluent formats, like DTD and properties, will fill untranslated strings
    
    233
    +with the source en-US content. And we don't want these untranslated strings to
    
    234
    +be used for migrations. There is no metadata within the files to determine this,
    
    235
    +and simply being the same as the en-US string is not a reliable indicator.
    
    236
    +
    
    237
    +So for these strings the script will additionally make a remote query to the
    
    238
    +Weblate API to fetch the metadata that indicates whether the string is
    
    239
    +considered translated or not. If a transformation requires a string that is
    
    240
    +marked as untranslated, it will be skipped.
    
    241
    +
    
    242
    +NOTE: The Weblate metadata is not always reliable, so you should still manually
    
    243
    +review the changes.
    
    244
    +
    
    245
    +NOTE: When testing before, the Weblate API was limited to 100 requests per day
    
    246
    +per IP address for unauthorised users. If this becomes an issue you may want to
    
    247
    +run the migrations in limited batches of locales with new IP addresses. Or, you
    
    248
    +can create a Weblate account and change this script to pass an authorisation
    
    249
    +token with the request.
    
    56 250
     """
    
    57 251
     
    
    58 252
     import argparse
    
    ... ... @@ -63,142 +257,299 @@ import logging
    63 257
     import os
    
    64 258
     import sys
    
    65 259
     import urllib.request
    
    260
    +from collections.abc import Iterator
    
    261
    +from typing import Any, NamedTuple, Protocol, cast
    
    66 262
     
    
    67 263
     from fluent.migrate.context import MigrationContext
    
    68 264
     from fluent.migrate.errors import EmptyLocalizationError, UnreadableReferenceError
    
    69 265
     from fluent.syntax import ast, parse, serialize
    
    70 266
     
    
    71 267
     
    
    72
    -def in_red(string):
    
    268
    +def in_red(string: str) -> str:
    
    73 269
         """
    
    74 270
         Wrap a string so that it is shown as red in the console.
    
    271
    +    :param string: The string to wrap.
    
    272
    +    :returns: The wrapped string.
    
    75 273
         """
    
    76 274
         return f"\x1b[1;91m{string}\x1b[0m"
    
    77 275
     
    
    78 276
     
    
    277
    +def assert_keys(
    
    278
    +    check_dict: dict[Any, Any], expected_keys: tuple[str, ...], url: str
    
    279
    +) -> None:
    
    280
    +    """
    
    281
    +    Verify that the dictionary contains *at least* the expected keys,
    
    282
    +    otherwise this throws.
    
    283
    +    :param check_dict: The dictionary to check.
    
    284
    +    :param url: The url where the dictionary was fetched from.
    
    285
    +    """
    
    286
    +    for key in expected_keys:
    
    287
    +        if key not in check_dict:
    
    288
    +            raise ValueError(f"Missing '{key}' in response for {url}")
    
    289
    +
    
    290
    +
    
    291
    +class WeblateUnit:
    
    292
    +    """
    
    293
    +    Represents a Weblate "unit": roughly corresponds to a translated string.
    
    294
    +
    
    295
    +    See https://docs.weblate.org/en/latest/api.html#units.
    
    296
    +    """
    
    297
    +
    
    298
    +    def __init__(self, data: dict[Any, Any], url: str) -> None:
    
    299
    +        """
    
    300
    +        :param data: The Weblate data response.
    
    301
    +        :param url: The URL that the data came from.
    
    302
    +        """
    
    303
    +        assert_keys(data, ("context", "translated"), url)
    
    304
    +        string_id = data["context"]
    
    305
    +        translated = data["translated"]
    
    306
    +        if not isinstance(string_id, str):
    
    307
    +            raise ValueError(f"Unexpected 'context' type in response for {url}")
    
    308
    +        if not isinstance(translated, bool):
    
    309
    +            raise ValueError(f"Unexpected 'translated' type in response for {url}")
    
    310
    +
    
    311
    +        self.string_id = string_id
    
    312
    +        self.translated = translated
    
    313
    +
    
    314
    +
    
    315
    +class WeblateTranslation:
    
    316
    +    """
    
    317
    +    Represents a Weblate "translation": roughly corresponds to the translation
    
    318
    +    of a file for a given locale/language.
    
    319
    +
    
    320
    +    See https://docs.weblate.org/en/latest/api.html#translations.
    
    321
    +    """
    
    322
    +
    
    323
    +    def __init__(self, data: dict[Any, Any], url: str) -> None:
    
    324
    +        """
    
    325
    +        :param data: The Weblate data response.
    
    326
    +        :param url: The URL that the data came from.
    
    327
    +        """
    
    328
    +        assert_keys(data, ("filename", "units_list_url"), url)
    
    329
    +        locale_path = data["filename"]
    
    330
    +        units_url = data["units_list_url"]
    
    331
    +        if not isinstance(locale_path, str):
    
    332
    +            raise ValueError(f"Unexpected 'filename' type in response for {url}")
    
    333
    +        if not isinstance(units_url, str):
    
    334
    +            raise ValueError(f"Unexpected 'units_list_url' type in response for {url}")
    
    335
    +
    
    336
    +        self.locale_path = locale_path
    
    337
    +        self.units_url = units_url
    
    338
    +        # The units, indexed by their string ID. Lazy-loaded and `None` whilst
    
    339
    +        # unfilled.
    
    340
    +        self.units: None | dict[str, WeblateUnit] = None
    
    341
    +
    
    342
    +
    
    343
    +class WeblateComponent:
    
    344
    +    """
    
    345
    +    Represents a Weblate "component": roughly corresponds to a source (en-US)
    
    346
    +    file that can be translated.
    
    347
    +
    
    348
    +    See https://docs.weblate.org/en/latest/api.html#components.
    
    349
    +    And https://docs.weblate.org/en/latest/user/basic.html.
    
    350
    +    """
    
    351
    +
    
    352
    +    def __init__(self, data: dict[Any, Any], url: str) -> None:
    
    353
    +        """
    
    354
    +        :param data: The Weblate data response.
    
    355
    +        :param url: The URL that the data came from.
    
    356
    +        """
    
    357
    +        assert_keys(data, ("template", "translations_url", "branch"), url)
    
    358
    +        template_path = data["template"]
    
    359
    +        translations_url = data["translations_url"]
    
    360
    +        branch = data["branch"]
    
    361
    +        if not isinstance(template_path, str):
    
    362
    +            raise ValueError(f"Unexpected 'template' type in response for {url}")
    
    363
    +        if not isinstance(translations_url, str):
    
    364
    +            raise ValueError(
    
    365
    +                f"Unexpected 'translations_url' type in response for {url}"
    
    366
    +            )
    
    367
    +        if not isinstance(branch, str):
    
    368
    +            raise ValueError(f"Unexpected 'branch' type in response for {url}")
    
    369
    +
    
    370
    +        self.branch = branch
    
    371
    +        # The path to the source (en-US) file.
    
    372
    +        self.template_path = template_path
    
    373
    +        # The URL to fetch the component's translations from.
    
    374
    +        self.translations_url = translations_url
    
    375
    +        # The translations data, indexed by the locale file paths. Lazy-loaded
    
    376
    +        # and `None` whilst unfilled.
    
    377
    +        self.translations: None | dict[str, WeblateTranslation] = None
    
    378
    +
    
    379
    +
    
    79 380
     class WeblateMetadata:
    
    80 381
         """
    
    81
    -    Manages and fetches string metadata from weblate.
    
    382
    +    Manages and fetches string metadata from Weblate.
    
    82 383
         """
    
    83 384
     
    
    84
    -    def __init__(self):
    
    385
    +    # The translation branches that we expect to be used for Weblate.
    
    386
    +    _BROWSER_BRANCHES = ["tor-browser", "base-browser", "fenix-torbrowserstringsxml"]
    
    387
    +    _WEBLATE_API_URL = "https://hosted.weblate.org/api/"
    
    388
    +    _WEBLATE_API_TOR_COMPONENTS_URL = _WEBLATE_API_URL + "projects/tor/components/"
    
    389
    +
    
    390
    +    def __init__(self) -> None:
    
    391
    +        """
    
    392
    +        Create a new instance for fetching Weblate metadata.
    
    393
    +        """
    
    85 394
             self.logger = logging.getLogger("weblate-metadata")
    
    86 395
             self.logger.setLevel(logging.INFO)
    
    87 396
     
    
    88
    -        # The _components property will be lazy-loaded with weblate data.
    
    89
    -        #
    
    90
    -        # On weblate, for monolingual formats, the component will have a
    
    91
    -        # "template" file, which matches the en-US file path in the
    
    92
    -        # "translation" git repo. So we can find a component through a matching
    
    93
    -        # template.
    
    94
    -        #
    
    95
    -        # Each translation in that component will have a "filename" that will
    
    96
    -        # match the locale's file path in the "translation" git repo. So we can
    
    97
    -        # find a translation through a matching filename.
    
    98
    -        #
    
    99
    -        # Each translation can be queried for its units, including metadata.
    
    100
    -        # Each unit's "context" will match the string's id in the corresponding
    
    101
    -        # file.
    
    102
    -        #
    
    103
    -        # Expect the final structure to be:
    
    104
    -        # {
    
    105
    -        #   template: {
    
    106
    -        #     "translations-url": str,     # Used for API translations query.
    
    107
    -        #     "translations": {
    
    108
    -        #        filename: {
    
    109
    -        #          "units-url": str,       # Used for API units query.
    
    110
    -        #          "units": {
    
    111
    -        #            context: {
    
    112
    -        #              "translated": bool,
    
    113
    -        #            },
    
    114
    -        #          },
    
    115
    -        #        },
    
    116
    -        #     },
    
    117
    -        #   },
    
    118
    -        # }
    
    119
    -        self._components = None
    
    120
    -
    
    121
    -    def _get_weblate_response(self, url):
    
    397
    +        # The _components property will be lazy-loaded with Weblate data.
    
    398
    +        self._components: dict[str, WeblateComponent] | None = None
    
    399
    +
    
    400
    +    def _get_weblate_response(self, url: str) -> Any:
    
    401
    +        """
    
    402
    +        Make a single query to Weblate.
    
    403
    +        :param url: The URL to fetch.
    
    404
    +        :returns: The JSON response.
    
    405
    +        """
    
    406
    +        if not url.startswith(self._WEBLATE_API_URL):
    
    407
    +            raise ValueError(f"Unexpected URL request: {url}")
    
    408
    +
    
    122 409
             weblate_request = urllib.request.Request(
    
    123 410
                 url,
    
    124 411
                 # NOTE: can set the request header:
    
    125 412
                 # "Authorization": "Token WEBLATE_TOKEN"
    
    126
    -            # to get around rate-limiting
    
    413
    +            # to get around rate-limiting.
    
    414
    +            # NOTE: We empty the User-Agent to override the default
    
    415
    +            # value of "Python-urllib/{major}.{minor}".
    
    127 416
                 headers={"User-Agent": ""},
    
    128 417
             )
    
    129 418
     
    
    130
    -        with urllib.request.urlopen(weblate_request, timeout=20) as response:
    
    419
    +        with urllib.request.urlopen(weblate_request) as response:
    
    131 420
                 return json.load(response)
    
    132 421
     
    
    133
    -    def _get_from_weblate(self, url):
    
    134
    -        ret = []
    
    422
    +    def _get_from_weblate(self, url: str) -> list[dict[Any, Any]]:
    
    423
    +        """
    
    424
    +        Fetch a list of results from Weblate.
    
    425
    +        :param url: The initial URL to fetch a response from.
    
    426
    +        :returns: The returned list of results.
    
    427
    +        """
    
    428
    +        ret: list[dict[Any, Any]] = []
    
    135 429
             while url:
    
    136 430
                 response = self._get_weblate_response(url)
    
    431
    +            if not isinstance(response, dict):
    
    432
    +                raise ValueError(f"Unexpected response type for {url}")
    
    433
    +            assert_keys(response, ("results", "next"), url)
    
    434
    +
    
    435
    +            res_val = response["results"]
    
    436
    +            if not isinstance(res_val, list):
    
    437
    +                raise ValueError(f"Unexpected 'results' type for {url}")
    
    438
    +            for res_item in res_val:
    
    439
    +                if not isinstance(res_item, dict):
    
    440
    +                    raise ValueError(f"Unexpected 'results' item type for {url}")
    
    441
    +                ret.append(res_item)
    
    442
    +
    
    137 443
                 # Continue to fetch the next page, if it is present.
    
    138
    -            # Expect the "next" url to be empty
    
    444
    +            # Expect the "next" url to be null
    
    139 445
                 # or the same url with "?page=2", etc.
    
    140
    -            url = response["next"]
    
    141
    -            ret.extend(response["results"])
    
    446
    +            next_val = response["next"]
    
    447
    +            if next_val is not None and not isinstance(next_val, str):
    
    448
    +                raise ValueError(f"Unexpected 'next' type in response for {url}")
    
    449
    +            url = "" if next_val is None else next_val
    
    142 450
             return ret
    
    143 451
     
    
    144
    -    def _get_components(self):
    
    452
    +    def _get_components(self) -> dict[str, WeblateComponent]:
    
    453
    +        """
    
    454
    +        Get all the Tor Browser Weblate components.
    
    455
    +
    
    456
    +        We identify each Weblate component by its template path: the en-US
    
    457
    +        file path in the translation repository.
    
    458
    +
    
    459
    +        NOTE: We assume that the template path uniquely identifies the
    
    460
    +        component, even though this is not guaranteed in general by Weblate.
    
    461
    +
    
    462
    +        :returns: A dictionary of components, indexed by their en-US template
    
    463
    +          path.
    
    464
    +        """
    
    465
    +        url = self._WEBLATE_API_TOR_COMPONENTS_URL
    
    145 466
             if self._components is None:
    
    146 467
                 self._components = {
    
    147
    -                comp["template"]: {
    
    148
    -                    "translations": None,
    
    149
    -                    "translations-url": comp["translations_url"],
    
    150
    -                }
    
    151
    -                for comp in self._get_from_weblate(
    
    152
    -                    "https://hosted.weblate.org/api/projects/tor/components/"
    
    468
    +                comp.template_path: comp
    
    469
    +                for comp in (
    
    470
    +                    WeblateComponent(data, url) for data in self._get_from_weblate(url)
    
    153 471
                     )
    
    154
    -                if comp["template"]
    
    472
    +                if comp.template_path and comp.branch in self._BROWSER_BRANCHES
    
    155 473
                 }
    
    156 474
             return self._components
    
    157 475
     
    
    158
    -    def _get_translations(self, template):
    
    159
    -        component = self._get_components().get(template, None)
    
    476
    +    def _get_translations(
    
    477
    +        self, template_path: str
    
    478
    +    ) -> dict[str, WeblateTranslation] | None:
    
    479
    +        """
    
    480
    +        Get the Weblate translations for a given Weblate component.
    
    481
    +
    
    482
    +        We identify each translation by its file path: the locale-specific path
    
    483
    +        that the translated file is written to.
    
    484
    +
    
    485
    +        :param template_path: The component's en-US template path.
    
    486
    +        :returns: A dictionary of translations, indexed by their locale file
    
    487
    +          paths. Or `None` if no components were found.
    
    488
    +        """
    
    489
    +        component = self._get_components().get(template_path, None)
    
    160 490
             if not component:
    
    161
    -            self.logger.warning(f"No component in weblate for {template}.")
    
    491
    +            self.logger.warning(f"No component in Weblate for {template_path}.")
    
    162 492
                 return None
    
    163
    -        if component["translations"] is None:
    
    164
    -            component["translations"] = {
    
    165
    -                trans["filename"]: {
    
    166
    -                    "units": None,
    
    167
    -                    "units-url": trans["units_list_url"],
    
    168
    -                }
    
    169
    -                for trans in self._get_from_weblate(component["translations-url"])
    
    493
    +        if component.translations is None:
    
    494
    +            url = component.translations_url
    
    495
    +            component.translations = {
    
    496
    +                trans.locale_path: trans
    
    497
    +                for trans in (
    
    498
    +                    WeblateTranslation(data, url)
    
    499
    +                    for data in self._get_from_weblate(url)
    
    500
    +                )
    
    170 501
                 }
    
    171
    -        return component["translations"]
    
    502
    +        return component.translations
    
    503
    +
    
    504
    +    def _get_units(
    
    505
    +        self, template_path: str, locale_path: str
    
    506
    +    ) -> dict[str, WeblateUnit] | None:
    
    507
    +        """
    
    508
    +        Get the Weblate units for a given Weblate translation.
    
    509
    +
    
    510
    +        We identify each unit by its string ID.
    
    172 511
     
    
    173
    -    def _get_units(self, template, file):
    
    174
    -        translation_dict = self._get_translations(template)
    
    512
    +        :param template_path: The containing component's en-US template path.
    
    513
    +        :param locale_path: The translation's locale file path.
    
    514
    +        :returns: A dictionary of units, indexed by their string ID. Or `None`
    
    515
    +          if no translations could be found.
    
    516
    +        """
    
    517
    +        translation_dict = self._get_translations(template_path)
    
    175 518
             if translation_dict is None:
    
    176 519
                 return None
    
    177
    -        translation = translation_dict.get(file, None)
    
    520
    +        translation = translation_dict.get(locale_path, None)
    
    178 521
             if translation is None:
    
    179
    -            self.logger.warning(f"No translation in weblate for {file}.")
    
    522
    +            self.logger.warning(f"No translation in Weblate for {locale_path}.")
    
    180 523
                 return None
    
    181
    -        if translation["units"] is None:
    
    182
    -            translation["units"] = {
    
    183
    -                unit["context"]: {
    
    184
    -                    "translated": unit["translated"],
    
    185
    -                }
    
    186
    -                for unit in self._get_from_weblate(translation["units-url"])
    
    524
    +        if translation.units is None:
    
    525
    +            url = translation.units_url
    
    526
    +            translation.units = {
    
    527
    +                unit.string_id: unit
    
    528
    +                for unit in (
    
    529
    +                    WeblateUnit(data, url) for data in self._get_from_weblate(url)
    
    530
    +                )
    
    187 531
                 }
    
    188
    -        return translation["units"]
    
    532
    +        return translation.units
    
    189 533
     
    
    190
    -    def is_translated(self, template_path, locale_path, string_id):
    
    534
    +    def is_translated(self, file_path: str, locale: str, string_id: str) -> bool:
    
    191 535
             """
    
    192
    -        Whether the given string is marked as translated on weblate.
    
    536
    +        Test whether the given string is marked as translated on Weblate.
    
    537
    +
    
    538
    +        :param file_path: The file to check the translation of, relative to the
    
    539
    +          locale's directory.
    
    540
    +        :param locale: The locale to check for.
    
    541
    +        :param string_id: The ID of the string to check.
    
    193 542
             """
    
    543
    +        template_path = os.path.join("en-US", file_path)
    
    544
    +        locale_path = os.path.join(locale, file_path)
    
    194 545
             unit_dict = self._get_units(template_path, locale_path)
    
    195 546
             if unit_dict is None:
    
    196 547
                 return False
    
    197 548
             unit = unit_dict.get(string_id, None)
    
    198 549
             if unit is None:
    
    199
    -            self.logger.warning(f"No unit in weblate for {locale_path}:{string_id}.")
    
    550
    +            self.logger.warning(f"No unit in Weblate for {locale_path}:{string_id}.")
    
    200 551
                 return False
    
    201
    -        return unit["translated"]
    
    552
    +        return unit.translated
    
    202 553
     
    
    203 554
     
    
    204 555
     class TorBrowserMigrationContext(MigrationContext):
    
    ... ... @@ -206,12 +557,18 @@ class TorBrowserMigrationContext(MigrationContext):
    206 557
         Extension which adds some extra methods to use for tor-browser.
    
    207 558
         """
    
    208 559
     
    
    209
    -    def __init__(self, locale, reference_dir, localization_dir):
    
    560
    +    def __init__(self, locale: str, reference_dir: str, localization_dir: str) -> None:
    
    210 561
             super().__init__(locale, reference_dir, localization_dir)
    
    211 562
     
    
    212
    -    def _fluent_keys(self, resource):
    
    563
    +    def _fluent_keys(self, resource: ast.Resource) -> Iterator[str]:
    
    564
    +        """
    
    565
    +        Generate all the keys within a Fluent resource, where a key is an
    
    566
    +        identifier for a Term, a Message, a Term attribute, or a Message
    
    567
    +        attribute.
    
    568
    +        :yield: The Fluent keys.
    
    569
    +        """
    
    213 570
             # ast.Resource, want to extract all Message and Term identifiers, as
    
    214
    -        # well as their
    
    571
    +        # well as their attributes.
    
    215 572
             for entry in resource.body:
    
    216 573
                 if not isinstance(entry, (ast.Term, ast.Message)):
    
    217 574
                     continue
    
    ... ... @@ -223,12 +580,13 @@ class TorBrowserMigrationContext(MigrationContext):
    223 580
                 for attr in entry.attributes:
    
    224 581
                     yield f"{key}.{attr.id.name}"
    
    225 582
     
    
    226
    -    def tb_get_available_strings(self):
    
    583
    +    def tb_get_available_strings(self) -> set[tuple[str, str]]:
    
    227 584
             """
    
    228
    -        Return all the (path, string_id) pairs for all loaded localization
    
    585
    +        Get all the (path, string_id) pairs for all loaded localization
    
    229 586
             resources.
    
    587
    +        :returns: The string pairs.
    
    230 588
             """
    
    231
    -        all_strings = set()
    
    589
    +        all_strings: set[tuple[str, str]] = set()
    
    232 590
             # ctx.localization_resources is a dict containing all the loaded
    
    233 591
             # localization_resources that have been added during add_transforms.
    
    234 592
             # { localization_path: resource }
    
    ... ... @@ -237,14 +595,13 @@ class TorBrowserMigrationContext(MigrationContext):
    237 595
                     all_strings.update((path, key) for key in self._fluent_keys(resource))
    
    238 596
                 else:
    
    239 597
                     # dictionary of { identifies: value } in legacy resource.
    
    240
    -                # For tor-browser,
    
    241
    -                # Only include references that differ from the en-US strings.
    
    242 598
                     all_strings.update((path, key) for key in resource.keys())
    
    243 599
             return all_strings
    
    244 600
     
    
    245
    -    def tb_get_missing_resources(self):
    
    601
    +    def tb_get_missing_resources(self) -> set[str]:
    
    246 602
             """
    
    247
    -        Return the missing localization resources.
    
    603
    +        Get the missing localization resources.
    
    604
    +        :returns: The set of resource paths.
    
    248 605
             """
    
    249 606
             return set(
    
    250 607
                 path
    
    ... ... @@ -253,25 +610,51 @@ class TorBrowserMigrationContext(MigrationContext):
    253 610
                 if path not in self.localization_resources
    
    254 611
             )
    
    255 612
     
    
    256
    -    def tb_get_transform(self, target_path, transform_id):
    
    613
    +    def tb_get_transform(
    
    614
    +        self, target_path: str, transform_id: str
    
    615
    +    ) -> ast.Message | ast.Term | None:
    
    257 616
             """
    
    258
    -        Find the transformation node with the given id for the given path.
    
    617
    +        Find a transformation node.
    
    618
    +        :param target_path: The target path of the transformation.
    
    619
    +        :param transform_id: The ID of the transformation, AKA the string ID.
    
    620
    +        :returns: The matching transformation node, or `None` if there is no
    
    621
    +          match.
    
    259 622
             """
    
    260 623
             for node in self.transforms[target_path]:
    
    261 624
                 if node.id.name == transform_id:
    
    262 625
                     return node
    
    263 626
             return None
    
    264 627
     
    
    265
    -    def tb_get_reference_entry(self, target_path, entry_id):
    
    628
    +    def tb_get_reference_entry(
    
    629
    +        self, target_path: str, transform_id: str
    
    630
    +    ) -> ast.Message | ast.Term | None:
    
    266 631
             """
    
    267
    -        Find the reference node that would be used for the given id and path.
    
    632
    +        Find a reference Message or Term for a given transformation.
    
    633
    +        :param target_path: The target path of the transformation.
    
    634
    +        :param transform_id: The ID of the transformation, AKA the string ID.
    
    635
    +        :returns: The matching reference entry for the transformation, or `None`
    
    636
    +          if there is no match.
    
    268 637
             """
    
    269 638
             for entry in self.reference_resources[target_path].body:
    
    270
    -            if isinstance(entry, (ast.Term, ast.Message)) and entry.id.name == entry_id:
    
    639
    +            if (
    
    640
    +                isinstance(entry, (ast.Term, ast.Message))
    
    641
    +                and entry.id.name == transform_id
    
    642
    +            ):
    
    271 643
                     return entry.clone()
    
    272 644
             return None
    
    273 645
     
    
    274 646
     
    
    647
    +class RecipeModule(Protocol):
    
    648
    +    def migrate(self, context: MigrationContext) -> None: ...
    
    649
    +
    
    650
    +
    
    651
    +class FluentSyntaxError(NamedTuple):
    
    652
    +    path: str
    
    653
    +    message: str
    
    654
    +    line: int
    
    655
    +    sample: str
    
    656
    +
    
    657
    +
    
    275 658
     class TorBrowserMigrator:
    
    276 659
         """
    
    277 660
         Performs a tor-browser migration.
    
    ... ... @@ -279,26 +662,38 @@ class TorBrowserMigrator:
    279 662
     
    
    280 663
         def __init__(
    
    281 664
             self,
    
    282
    -        en_US_dir,
    
    283
    -        locale_dirs,
    
    284
    -        migrate_module,
    
    285
    -        weblate_metadata,
    
    286
    -    ):
    
    665
    +        en_US_ref_dir: str,
    
    666
    +        en_US_target_dir: str,
    
    667
    +        locale_dirs: dict[str, str],
    
    668
    +        recipe_module: RecipeModule,
    
    669
    +        weblate_metadata: WeblateMetadata,
    
    670
    +    ) -> None:
    
    671
    +        """
    
    672
    +        :param en_US_ref_dir: The path to the en-US directory where reference
    
    673
    +          Fluent entries for the recipe can be found.
    
    674
    +        :param en_US_target_dir: The path to the en-US directory where existing
    
    675
    +          strings used by the recipe can be found.
    
    676
    +        :param locale_dirs: The collection of locales to run the migrations for.
    
    677
    +        :param recipe_module: The python module that contains the recipe
    
    678
    +          instructions.
    
    679
    +        :param weblate_metadata: The object to fetch Weblate metadata from.
    
    680
    +        """
    
    287 681
             self.logger = logging.getLogger("tor-browser-migrator")
    
    288 682
             self.logger.setLevel(logging.INFO)
    
    289
    -        self.en_US_dir = en_US_dir
    
    683
    +        self.en_US_ref_dir = en_US_ref_dir
    
    684
    +        self.en_US_target_dir = en_US_target_dir
    
    290 685
             self.locale_dirs = locale_dirs
    
    291
    -        self.migrate_module = migrate_module
    
    686
    +        self.recipe_module = recipe_module
    
    292 687
             self.weblate_metadata = weblate_metadata
    
    293 688
     
    
    294
    -    def run(self):
    
    689
    +    def run(self) -> None:
    
    295 690
             """
    
    296 691
             Run the migration.
    
    297 692
             """
    
    298 693
             if not self._check_en_US_resources():
    
    299 694
                 sys.exit(1)
    
    300 695
     
    
    301
    -        fluent_errors = []
    
    696
    +        fluent_errors: list[FluentSyntaxError] = []
    
    302 697
             for locale, locale_dir in self.locale_dirs.items():
    
    303 698
                 if locale == "en-US":
    
    304 699
                     print("", file=sys.stderr)
    
    ... ... @@ -316,12 +711,18 @@ class TorBrowserMigrator:
    316 711
                     "Migration does not need to be run again, but the following "
    
    317 712
                     "syntax errors should be fixed manually.\n"
    
    318 713
                     + "\n".join(
    
    319
    -                    f"{in_red(full_path)}: line {line}: {message}: [[{sample}]]"
    
    320
    -                    for full_path, message, line, sample in fluent_errors
    
    714
    +                    f"{in_red(err.path)}: line {err.line}: {err.message}: [[{err.sample}]]"
    
    715
    +                    for err in fluent_errors
    
    321 716
                     )
    
    322 717
                 )
    
    323 718
     
    
    324
    -    def _run_locale(self, locale, locale_dir):
    
    719
    +    def _run_locale(self, locale: str, locale_dir: str) -> list[FluentSyntaxError]:
    
    720
    +        """
    
    721
    +        Run a migration for a given locale.
    
    722
    +        :param locale: The name of the locale.
    
    723
    +        :param locale_dir: The path to the locale's directory.
    
    724
    +        :returns: A list of Fluent errors in the written files.
    
    725
    +        """
    
    325 726
             print("\n\n", file=sys.stderr)
    
    326 727
             self.logger.info(f"Migrating '{in_red(locale)}' locale\n")
    
    327 728
     
    
    ... ... @@ -358,7 +759,7 @@ class TorBrowserMigrator:
    358 759
     
    
    359 760
             available_strings = ctx.tb_get_available_strings()
    
    360 761
             wrote_file = False
    
    361
    -        errors = []
    
    762
    +        errors: list[FluentSyntaxError] = []
    
    362 763
     
    
    363 764
             for target_path, reference in ctx.reference_resources.items():
    
    364 765
                 translated_ids = [
    
    ... ... @@ -422,13 +823,13 @@ class TorBrowserMigrator:
    422 823
                             break
    
    423 824
                         # Strings in legacy formats might have an entry in the file
    
    424 825
                         # that is just a copy of the en-US strings.
    
    425
    -                    # For these we want to check the weblate metadata to ensure
    
    826
    +                    # For these we want to check the Weblate metadata to ensure
    
    426 827
                         # it is a translated string.
    
    427 828
                         if not path.endswith(
    
    428 829
                             ".ftl"
    
    429 830
                         ) and not self.weblate_metadata.is_translated(
    
    430
    -                        os.path.join("en-US", path),
    
    431
    -                        os.path.join(locale, path),
    
    831
    +                        path,
    
    832
    +                        locale,
    
    432 833
                             string_id,
    
    433 834
                         ):
    
    434 835
                             can_transform = False
    
    ... ... @@ -436,7 +837,7 @@ class TorBrowserMigrator:
    436 837
                                 f"Skipping transform {target_path}:{transform_id} "
    
    437 838
                                 f"for '{locale}' locale because the string "
    
    438 839
                                 f"{path}:{string_id} has not been translated on "
    
    439
    -                            "weblate."
    
    840
    +                            "Weblate."
    
    440 841
                             )
    
    441 842
                             break
    
    442 843
                     if not can_transform:
    
    ... ... @@ -463,7 +864,9 @@ class TorBrowserMigrator:
    463 864
                 wrote_file = True
    
    464 865
                 # Collect any fluent parsing errors from the newly written file.
    
    465 866
                 errors.extend(
    
    466
    -                (full_path, message, line, sample)
    
    867
    +                FluentSyntaxError(
    
    868
    +                    path=full_path, message=message, line=line, sample=sample
    
    869
    +                )
    
    467 870
                     for message, line, sample in self._fluent_errors(full_content)
    
    468 871
                 )
    
    469 872
     
    
    ... ... @@ -471,27 +874,42 @@ class TorBrowserMigrator:
    471 874
                 self.logger.info(f"No files written for '{locale}' locale.")
    
    472 875
             return errors
    
    473 876
     
    
    474
    -    def _fluent_errors(self, fluent):
    
    877
    +    def _fluent_errors(self, fluent_content: str) -> Iterator[tuple[str, int, str]]:
    
    475 878
             """
    
    476
    -        Verify that the given fluent string can be parsed correctly.
    
    879
    +        Parse the given content as a Fluent file and generate all the syntax
    
    880
    +        errors.
    
    881
    +        :param fluent_content: The file content to parse.
    
    882
    +        :yield: Each encountered Fluent syntax error as a triple of the message,
    
    883
    +        the line number, and a sample.
    
    477 884
             """
    
    478
    -        resource = parse(fluent)
    
    885
    +        resource = parse(fluent_content)
    
    479 886
             for entry in resource.body:
    
    480 887
                 if not isinstance(entry, ast.Junk):
    
    481 888
                     continue
    
    482 889
                 for annotation in entry.annotations:
    
    483
    -                line = fluent[0 : annotation.span.start].count("\n") + 1
    
    484
    -                sample_start = max(annotation.span.start - 15, 0)
    
    485
    -                sample = "…" + fluent[sample_start : sample_start + 30] + "…"
    
    486
    -                yield annotation.message, line, sample
    
    487
    -
    
    488
    -    def _get_migration_context(self, locale, locale_dir):
    
    489
    -        prev_missing_resources = set()
    
    890
    +                start = 0
    
    891
    +                if annotation.span:
    
    892
    +                    start = annotation.span.start
    
    893
    +                line = fluent_content[0:start].count("\n") + 1
    
    894
    +                sample_start = max(start - 15, 0)
    
    895
    +                sample = "…" + fluent_content[sample_start : sample_start + 30] + "…"
    
    896
    +                yield annotation.message or "", line, sample
    
    897
    +
    
    898
    +    def _get_migration_context(
    
    899
    +        self, locale: str, locale_dir: str
    
    900
    +    ) -> TorBrowserMigrationContext:
    
    901
    +        """
    
    902
    +        Create a new migration context.
    
    903
    +        :param locale: The name of the locale.
    
    904
    +        :param locale_dir: The directory path for the locale.
    
    905
    +        :returns: The new context.
    
    906
    +        """
    
    907
    +        prev_missing_resources: set[str] = set()
    
    490 908
             while True:
    
    491
    -            ctx = TorBrowserMigrationContext(locale, self.en_US_dir, locale_dir)
    
    909
    +            ctx = TorBrowserMigrationContext(locale, self.en_US_ref_dir, locale_dir)
    
    492 910
     
    
    493 911
                 try:
    
    494
    -                self.migrate_module.migrate(ctx)
    
    912
    +                self.recipe_module.migrate(ctx)
    
    495 913
                 except EmptyLocalizationError:
    
    496 914
                     # This case will be handled by missing_resources below.
    
    497 915
                     # NOTE: At the time of writing, add_transforms only throws if
    
    ... ... @@ -543,20 +961,28 @@ class TorBrowserMigrator:
    543 961
                 # before localization_resources was fully populated.
    
    544 962
                 prev_missing_resources = missing_resources
    
    545 963
     
    
    546
    -    def _remove_if_empty(self, path):
    
    964
    +    def _remove_if_empty(self, path: str) -> None:
    
    965
    +        """
    
    966
    +        Remove a file if it is empty.
    
    967
    +        :param path: The path to remove.
    
    968
    +        """
    
    547 969
             if os.stat(path).st_size:
    
    548 970
                 self.logger.warning(f"{path} is no longer empty. Not deleting.")
    
    549 971
                 return
    
    550 972
             os.remove(path)
    
    551 973
     
    
    552
    -    def _check_en_US_resources(self):
    
    553
    -        # We pass in the en-US directory as the localization directory, as well
    
    554
    -        # as the reference directory.
    
    555
    -        ctx = TorBrowserMigrationContext("en-US", self.en_US_dir, self.en_US_dir)
    
    974
    +    def _check_en_US_resources(self) -> bool:
    
    975
    +        """
    
    976
    +        Verify that the migration recipe works as expected using en-US strings.
    
    977
    +        :returns: Whether the recipe worked as expected.
    
    978
    +        """
    
    979
    +        ctx = TorBrowserMigrationContext(
    
    980
    +            "en-US", self.en_US_ref_dir, self.en_US_target_dir
    
    981
    +        )
    
    556 982
     
    
    557 983
             have_error = False
    
    558 984
             try:
    
    559
    -            self.migrate_module.migrate(ctx)
    
    985
    +            self.recipe_module.migrate(ctx)
    
    560 986
             except EmptyLocalizationError:
    
    561 987
                 # Handle with localization_resources check.
    
    562 988
                 # NOTE: This throwing may have prevented further add_transforms from
    
    ... ... @@ -566,7 +992,9 @@ class TorBrowserMigrator:
    566 992
                 have_error = True
    
    567 993
             except UnreadableReferenceError:
    
    568 994
                 # Reference filename is printed before this.
    
    569
    -            self.logger.error("Missing an en-US reference file.")
    
    995
    +            self.logger.error(
    
    996
    +                "Missing an en-US reference file. Is --reference-directory correct?"
    
    997
    +            )
    
    570 998
                 return False
    
    571 999
     
    
    572 1000
             # Check each transform would create the same entry in the target as the
    
    ... ... @@ -602,7 +1030,8 @@ class TorBrowserMigrator:
    602 1030
                 reference_entry = ctx.tb_get_reference_entry(target_path, transform_id)
    
    603 1031
                 if reference_entry is None:
    
    604 1032
                     self.logger.error(
    
    605
    -                    f"Missing en-US reference entry for transform {transform_name}"
    
    1033
    +                    f"Missing en-US reference entry for transform {transform_name}. "
    
    1034
    +                    "Is --reference-directory correct?"
    
    606 1035
                     )
    
    607 1036
                     have_error = True
    
    608 1037
                     continue
    
    ... ... @@ -625,9 +1054,50 @@ class TorBrowserMigrator:
    625 1054
             return not have_error
    
    626 1055
     
    
    627 1056
     
    
    1057
    +def main(recipe_path: str, translation_dir: str, locales: str, ref_dir: str) -> None:
    
    1058
    +    """
    
    1059
    +    The main method.
    
    1060
    +    :param recipe_path: The path to the recipe script.
    
    1061
    +    :param translation_dir: The path to the translation repository directory.
    
    1062
    +    :param locales: The locales to run the recipe for, separated by spaces.
    
    1063
    +    :param ref_dir: The path to the reference directory.
    
    1064
    +    """
    
    1065
    +    if not os.path.isfile(recipe_path):
    
    1066
    +        print(f"{in_red(recipe_path)} is not a file.", file=sys.stderr)
    
    1067
    +        sys.exit(1)
    
    1068
    +
    
    1069
    +    recipe_module_spec = importlib.util.spec_from_file_location("_recipe", recipe_path)
    
    1070
    +    recipe_module = cast(
    
    1071
    +        RecipeModule, importlib.util.module_from_spec(recipe_module_spec)
    
    1072
    +    )
    
    1073
    +    recipe_module_spec.loader.exec_module(recipe_module)
    
    1074
    +
    
    1075
    +    def check_dir(path: str) -> str:
    
    1076
    +        if not os.path.isdir(path):
    
    1077
    +            print(f"{in_red(path)} is not a directory.", file=sys.stderr)
    
    1078
    +            sys.exit(1)
    
    1079
    +        return path
    
    1080
    +
    
    1081
    +    translation_dir = check_dir(translation_dir)
    
    1082
    +    en_US_target_dir = check_dir(os.path.join(translation_dir, "en-US"))
    
    1083
    +    en_US_ref_dir = en_US_target_dir if ref_dir is None else check_dir(ref_dir)
    
    1084
    +
    
    1085
    +    TorBrowserMigrator(
    
    1086
    +        en_US_ref_dir,
    
    1087
    +        en_US_target_dir,
    
    1088
    +        {
    
    1089
    +            locale: check_dir(os.path.join(translation_dir, locale))
    
    1090
    +            for locale in (l.strip() for l in locales.split(" "))
    
    1091
    +            if locale
    
    1092
    +        },
    
    1093
    +        recipe_module,
    
    1094
    +        WeblateMetadata(),
    
    1095
    +    ).run()
    
    1096
    +
    
    1097
    +
    
    628 1098
     parser = argparse.ArgumentParser(
    
    629 1099
         description="Run a tor-browser localization migration. "
    
    630
    -    "See documentation within migrate_l10n.py."
    
    1100
    +    "See documentation within migrate.py."
    
    631 1101
     )
    
    632 1102
     parser.add_argument(
    
    633 1103
         "--translation-git",
    
    ... ... @@ -638,35 +1108,30 @@ parser.add_argument(
    638 1108
     parser.add_argument(
    
    639 1109
         "--locales",
    
    640 1110
         required=True,
    
    641
    -    metavar="<locale1> <locale2> ...",
    
    642
    -    help="Set of locales to restrict the migration to, separated by space.",
    
    1111
    +    metavar="'<locale1> <locale2> ...'",
    
    1112
    +    help="Set of locales to run the migration for, separated by space.",
    
    643 1113
     )
    
    644 1114
     parser.add_argument(
    
    645
    -    "migration",
    
    646
    -    help="Migration to run, given as a python module. "
    
    647
    -    'E.g. "l10n_migrations.my-migration-script".',
    
    1115
    +    "--reference-directory",
    
    1116
    +    metavar="<dir>",
    
    1117
    +    help="Location of the en-US directory that contains the reference changes "
    
    1118
    +    "that the recipe corresponds to. E.g. if the recipe constructs the string "
    
    1119
    +    "`my-new-string` in `./tor-browser.ftl`, it should point to a directory "
    
    1120
    +    "that contains the en-US `tor-browser.ftl` with `my-new-string` already "
    
    1121
    +    "within it. Defaults to the `en-US/` sub-directory within the "
    
    1122
    +    "translation-git directory.",
    
    1123
    +)
    
    1124
    +parser.add_argument(
    
    1125
    +    "recipe",
    
    1126
    +    help="The path to the migration recipe to run.",
    
    648 1127
     )
    
    649 1128
     
    
    650 1129
     
    
    651 1130
     parsed_args = parser.parse_args()
    
    652 1131
     
    
    653
    -
    
    654
    -def check_dir(path):
    
    655
    -    if not os.path.isdir(path):
    
    656
    -        print(f"{in_red(path)} is not a directory.", file=sys.stderr)
    
    657
    -        sys.exit(1)
    
    658
    -    return path
    
    659
    -
    
    660
    -
    
    661
    -translation_dir = check_dir(os.path.abspath(parsed_args.translation_git))
    
    662
    -
    
    663
    -TorBrowserMigrator(
    
    664
    -    check_dir(os.path.join(translation_dir, "en-US")),
    
    665
    -    {
    
    666
    -        locale: check_dir(os.path.join(translation_dir, locale))
    
    667
    -        for locale in (l.strip() for l in parsed_args.locales.split(" "))
    
    668
    -        if locale
    
    669
    -    },
    
    670
    -    importlib.import_module(parsed_args.migration),
    
    671
    -    WeblateMetadata(),
    
    672
    -).run()
    1132
    +main(
    
    1133
    +    parsed_args.recipe,
    
    1134
    +    parsed_args.translation_git,
    
    1135
    +    parsed_args.locales,
    
    1136
    +    parsed_args.reference_directory,
    
    1137
    +)

  • _______________________________________________
    tor-commits mailing list -- tor-commits@xxxxxxxxxxxxxxxxxxxx
    To unsubscribe send an email to tor-commits-leave@xxxxxxxxxxxxxxxxxxxx