# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """ Sphinx extension to auto-generate ETP capability matrix during doc build. How it works: 1. This extension is loaded by Sphinx via docs/conf.py 2. On the 'builder-inited' event, it parses browser/app/profile/firefox.js to extract the `browser.contentblocking.features.strict` pref value 3. It parses modules/libpref/init/StaticPrefList.yaml for pref defaults 4. It generates etp-matrix/index.md in the build staging directory 5. Sphinx then renders it to HTML along with the rest of the docs The FEATURES list below maps feature codes (like "tp", "fp") to their corresponding prefs. Pref documentation and defaults are sourced from StaticPrefList.yaml. If a new feature code is added to firefox.js, it will appear as "UNKNOWN" in the output until FEATURES is updated. Output is published to: https://firefox-source-docs.mozilla.org/toolkit/components/antitracking/anti-tracking/etp-matrix/ """ import re from pathlib import Path from urllib.parse import quote # Other privacy-related prefs that aren't controlled by the ETP feature string # but are relevant to privacy. These don't change between Standard/Strict modes. # Format: {category: [(feature_name, normal_pref, pbmode_pref_or_None, description), ...]} OTHER_PRIVACY_PREFS = { "Safe Browsing": [ ( "Malware Protection", "browser.safebrowsing.malware.enabled", None, "Checks URLs against Google Safe Browsing malware list.", ), ( "Phishing Protection", "browser.safebrowsing.phishing.enabled", None, "Checks URLs against Google Safe Browsing phishing list.", ), ( "Downloads Protection", "browser.safebrowsing.downloads.enabled", None, "Checks downloaded file hashes against Google Safe Browsing download protection lists.", ), ( "Block Potentially Unwanted Software", "browser.safebrowsing.downloads.remote.block_potentially_unwanted", None, "Blocks downloads flagged as potentially unwanted programs (PUPs) by Safe Browsing.", ), ], "Private Browsing": [ ( "Reset Private Browsing", "browser.privatebrowsing.resetPBM.enabled", None, "Clears all private browsing mode data when last private window is closed.", ), ( "Show Reset Confirmation", "browser.privatebrowsing.resetPBM.showConfirmationDialog", None, "Shows confirmation dialog before clearing private browsing data on window close.", ), ], "Cookie Behavior": [ ( "CHIPS Support", "network.cookie.CHIPS.enabled", None, "Enables Cookies Having Independent Partitioned State (CHIPS) per draft spec.", ), ( "Opt-in Cookie Partitioning", "network.cookie.cookieBehavior.optInPartitioning", "network.cookie.cookieBehavior.optInPartitioning.pbmode", "Changes cookieBehavior=5 from dynamic partitioning to block-by-default with opt-in.", ), ( "Tracker Cookie Blocking", "network.cookie.cookieBehavior.trackerCookieBlocking", None, "Blocks third-party cookies from domains on the tracking protection list.", ), ( "Social Tracker Cookie Blocking", "privacy.socialtracking.block_cookies.enabled", None, "Treats domains on social tracking list as trackers for cookie blocking.", ), ], "Privacy Headers": [ ( "Do Not Track", "privacy.donottrackheader.enabled", None, "Sends DNT: 1 HTTP header with all requests.", ), ( "Global Privacy Control", "privacy.globalprivacycontrol.enabled", "privacy.globalprivacycontrol.pbmode.enabled", "Sends Sec-GPC: 1 HTTP header and exposes navigator.globalPrivacyControl=true.", ), ( "GPC Functionality", "privacy.globalprivacycontrol.functionality.enabled", None, "Master switch that controls whether GPC signals are sent (requires .enabled to also be true).", ), ], "Fingerprinting Resistance": [ ( "Resist Fingerprinting", "privacy.resistFingerprinting", "privacy.resistFingerprinting.pbmode", "Enables comprehensive fingerprinting resistance including canvas noise, reduced timer precision, and spoofed system info.", ), ], "Anti-fraud": [ ( "Skip Anti-fraud Resources", "privacy.trackingprotection.antifraud.skip.enabled", "privacy.trackingprotection.antifraud.skip.pbmode.enabled", "Exempts domains with 'fingerprinting' or 'tracking' annotations from blocking if they have 'anti-fraud' annotation.", ), ], "Other Privacy Features": [ ( "Cookie Banner UI", "cookiebanners.ui.desktop.enabled", None, "Shows cookie banner reduction controls in Firefox settings.", ), ( "Network State Partitioning", "privacy.partition.network_state", None, "Partitions HTTP cache, connection pools, and other network state by top-level site.", ), ( "Strip on Share", "privacy.query_stripping.strip_on_share.enabled", None, "Strips tracking query parameters when copying URLs via context menu 'Copy Link'.", ), ], } # Features controlled by the ETP feature string in firefox.js. # Each feature has: # - normal_code: feature code for normal browsing (or None if no separate pref) # - pb_code: feature code for private browsing (or None if no separate pref) # - pref_normal: pref name for normal browsing # - pref_pb: pref name for private browsing (or None if same as normal) FEATURES = [ { "name": "Tracking Protection", "normal_code": "tp", "pb_code": "tpPrivate", "pref_normal": "privacy.trackingprotection.enabled", "pref_pb": "privacy.trackingprotection.pbmode.enabled", "desc": "Blocks resources from domains on the Disconnect tracking protection list.", }, { "name": "Fingerprinting Blocking", "normal_code": "fp", "pb_code": None, "pref_normal": "privacy.trackingprotection.fingerprinting.enabled", "pref_pb": None, "desc": "Blocks resources from domains on the Disconnect fingerprinting list.", }, { "name": "Fingerprinting Protection", "normal_code": "fpp", "pb_code": "fppPrivate", "pref_normal": "privacy.fingerprintingProtection", "pref_pb": "privacy.fingerprintingProtection.pbmode", "desc": "Applies fingerprinting protections including canvas randomization and reducing exposed system info.", }, { "name": "Cryptominer Blocking", "normal_code": "cryptoTP", "pb_code": None, "pref_normal": "privacy.trackingprotection.cryptomining.enabled", "pref_pb": None, "desc": "Blocks resources from domains on the Disconnect cryptomining list.", }, { "name": "Social Tracker Blocking", "normal_code": "stp", "pb_code": None, "pref_normal": "privacy.trackingprotection.socialtracking.enabled", "pref_pb": None, "desc": "Blocks resources from domains on the Disconnect social tracking list.", }, { "name": "Email Tracking Protection", "normal_code": "emailTP", "pb_code": "emailTPPrivate", "pref_normal": "privacy.trackingprotection.emailtracking.enabled", "pref_pb": "privacy.trackingprotection.emailtracking.pbmode.enabled", "desc": "Blocks resources from domains on the email tracking list.", }, { "name": "Cookie Behavior", "normal_code": "cookieBehavior5", "pb_code": "cookieBehaviorPBM5", "pref_normal": "network.cookie.cookieBehavior", "pref_pb": "network.cookie.cookieBehavior.pbmode", "desc": "Controls third-party cookie blocking strategy. See [Bug 2016714](https://bugzilla.mozilla.org/show_bug.cgi?id=2016714) for value definitions.", }, { "name": "Query Parameter Stripping", "normal_code": "qps", "pb_code": "qpsPBM", "pref_normal": "privacy.query_stripping.enabled", "pref_pb": "privacy.query_stripping.enabled.pbmode", "desc": "Strips known tracking query parameters from URLs during navigation.", }, { "name": "Level 2 Tracking List", "normal_code": "lvl2", "pb_code": None, "pref_normal": "privacy.annotate_channels.strict_list.enabled", "pref_pb": None, "desc": "Annotates channels with the Level 2 (strict) Disconnect tracking list in addition to Level 1.", }, { "name": "Strict Referrer Policy", "normal_code": "rp", "pb_code": None, "pref_normal": "network.http.referer.disallowCrossSiteRelaxingDefault", "pref_pb": None, "desc": "Prevents referrer policy from being relaxed to unsafe-url for cross-site requests.", }, { "name": "Strict Referrer Policy (Top Nav)", "normal_code": "rpTop", "pb_code": None, "pref_normal": "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation", "pref_pb": None, "desc": "Applies strict referrer policy to top-level navigation (not just subresources).", }, { "name": "OCSP Cache Partitioning", "normal_code": "ocsp", "pb_code": None, "pref_normal": "privacy.partition.network_state.ocsp_cache", "pref_pb": None, "desc": "Partitions OCSP cache by top-level origin key.", }, { "name": "Bounce Tracking Protection", "normal_code": "btp", "pb_code": None, "pref_normal": "privacy.bounceTrackingProtection.mode", "pref_pb": None, "desc": "Clears state for sites used as bounce trackers. See [Bounce Tracking Protection docs](../bounce-tracking-protection) for mode values.", }, { "name": "Local Network Access Blocking", "normal_code": "lna", "pb_code": None, "pref_normal": "network.lna.blocking", "pref_pb": None, "desc": "Blocks public websites from making requests to private IP ranges (RFC1918, loopback, link-local).", }, { "name": "Consent Manager Skipping", "normal_code": "consentmanagerSkip", "pb_code": "consentmanagerSkipPrivate", "pref_normal": "privacy.trackingprotection.consentmanager.skip.enabled", "pref_pb": "privacy.trackingprotection.consentmanager.skip.pbmode.enabled", "desc": "Skips blocking for known consent management platform domains.", }, ] # For backwards compatibility and unknown feature detection KNOWN_CODES = set() for f in FEATURES: if f["normal_code"]: KNOWN_CODES.add(f["normal_code"]) if f["pb_code"]: KNOWN_CODES.add(f["pb_code"]) def parse_static_pref_list(yaml_path): """ Parse StaticPrefList.yaml to extract pref defaults and comments. Returns dict of {pref_name: {"value": ..., "ifdef_block": str_or_None, "comment": str}}. When an #ifdef block wraps the value, the release default is stored in "value" and the full raw #ifdef..#endif text is stored in "ifdef_block". When the value uses a macro like @IS_NIGHTLY_BUILD@, the macro name is stored in "macro" and the release default in "value". """ content = yaml_path.read_text(encoding="utf-8") prefs = {} lines = content.split("\n") for i, line in enumerate(lines): match = re.match(r"^-\s*name:\s*(.+)$", line) if not match: continue pref_name = match.group(1).strip() # Collect comment from preceding lines comment_lines = [] for j in range(i - 1, -1, -1): prev_line = lines[j].strip() if prev_line.startswith("#") and not prev_line.startswith("#ifdef"): if re.match(r"^#-+$", prev_line): break comment_lines.insert(0, prev_line.lstrip("# ")) elif prev_line == "": pass else: break comment = " ".join(comment_lines).strip() # Parse the pref block to find value value = None ifdef_block = None macro = None in_ifdef = False in_else = False ifdef_lines = [] ifdef_nightly_value = None ifdef_else_value = None for k in range(i + 1, len(lines)): pref_line = lines[k] if re.match(r"^-\s*name:", pref_line): break # Track ifdef/else/endif blocks ifdef_start = re.match(r"^#ifdef\s+(\w+)", pref_line) if ifdef_start: in_ifdef = True in_else = False ifdef_lines = [pref_line] elif "#else" in pref_line and in_ifdef: in_else = True ifdef_lines.append(pref_line) elif "#endif" in pref_line and in_ifdef: ifdef_lines.append(pref_line) ifdef_block = "\n".join(ifdef_lines) in_ifdef = False in_else = False elif in_ifdef: ifdef_lines.append(pref_line) # Parse value line value_match = re.match(r"^\s*value:\s*(.+)$", pref_line) if value_match: raw_value = value_match.group(1).strip() # Check for macros if raw_value.startswith("@") and raw_value.endswith("@"): macro = raw_value value = raw_value elif in_ifdef and not in_else: ifdef_nightly_value = raw_value.strip('"') elif in_ifdef and in_else: ifdef_else_value = raw_value.strip('"') elif value is None: value = raw_value.strip('"') # For ifdef blocks: the #else value is the release default if ifdef_block is not None and ifdef_else_value is not None: value = ifdef_else_value elif ( ifdef_block is not None and ifdef_nightly_value is not None and value is None ): # ifdef with no else: only defined on nightly value = ifdef_nightly_value prefs[pref_name] = { "value": value, "ifdef_block": ifdef_block, "macro": macro, "comment": comment, } return prefs def make_searchfox_link(pref_name, path): """Generate a Searchfox URL that links to the pref in the given source file.""" escaped_pref = re.escape(pref_name) if path.endswith(".yaml"): query = f"^- name: {escaped_pref}$" else: # For .js files, search for pref("pref.name", query = f'pref\\("{escaped_pref}"' url = ( f"https://searchfox.org/mozilla-central/search" f"?q={quote(query)}&path={quote(path)}&case=true®exp=true" ) return url def _build_pref_links(pref_name, pref_info, firefox_js_overrides, all_js_prefs): """Build Searchfox links for all source files where a pref is found.""" links = [] if pref_name in pref_info: path = "modules/libpref/init/StaticPrefList.yaml" links.append(f"[StaticPrefList.yaml]({make_searchfox_link(pref_name, path)})") if pref_name in all_js_prefs: path = "modules/libpref/init/all.js" links.append(f"[all.js]({make_searchfox_link(pref_name, path)})") if pref_name in firefox_js_overrides: path = "browser/app/profile/firefox.js" links.append(f"[firefox.js]({make_searchfox_link(pref_name, path)})") if not links: return f"`{pref_name}`" return f"`{pref_name}`: " + ", ".join(links) def parse_firefox_js_overrides(firefox_js_path): """ Parse a .js pref file to extract pref values, handling #if/#ifdef blocks. Returns dict of {pref_name: {"value": ..., "ifdef_block": str|None}}. For prefs inside #if/#ifdef blocks with #else, the #else value is used as the release default. """ content = firefox_js_path.read_text(encoding="utf-8") overrides = {} pref_pattern = re.compile(r'pref\("([^"]+)",\s*([^)]+)\)') in_ifdef = False in_else = False ifdef_lines = [] ifdef_prefs = {} else_prefs = {} for line in content.split("\n"): stripped = line.strip() if re.match(r"^#if\b|^#ifdef\b|^#ifndef\b", stripped) and not in_ifdef: in_ifdef = True in_else = False ifdef_lines = [stripped] ifdef_prefs = {} else_prefs = {} continue elif stripped == "#else" and in_ifdef: in_else = True ifdef_lines.append(stripped) continue elif stripped.startswith("#endif") and in_ifdef: ifdef_lines.append(stripped) ifdef_block_text = "\n".join(ifdef_lines) all_names = set(ifdef_prefs) | set(else_prefs) for pref_name in all_names: if pref_name in else_prefs: value = else_prefs[pref_name] else: value = ifdef_prefs[pref_name] overrides[pref_name] = { "value": value, "ifdef_block": ifdef_block_text, } in_ifdef = False in_else = False continue if in_ifdef: ifdef_lines.append(stripped) match = pref_pattern.search(line) if match: pref_name = match.group(1) raw_value = match.group(2).strip() stripped_value = raw_value.strip('"') if in_ifdef and not in_else: ifdef_prefs[pref_name] = stripped_value elif in_ifdef and in_else: else_prefs[pref_name] = stripped_value else: overrides[pref_name] = {"value": stripped_value, "ifdef_block": None} return overrides def _get_pref_value(pref_name, pref_info, firefox_js_overrides, all_js_prefs): """Get the effective value of a pref, checking sources in override order.""" # firefox.js overrides have highest priority if pref_name in firefox_js_overrides: return firefox_js_overrides[pref_name]["value"] # all.js overrides come next if pref_name in all_js_prefs: return all_js_prefs[pref_name]["value"] # StaticPrefList.yaml has lowest priority if pref_name in pref_info: return pref_info[pref_name]["value"] return None def get_standard_defaults(pref_info, firefox_js_overrides, all_js_prefs): """ Compute standard mode defaults from pref sources. Returns dict of {feature_code: raw_value}. """ defaults = {} for feature in FEATURES: normal_code = feature["normal_code"] pb_code = feature["pb_code"] pref_normal = feature["pref_normal"] pref_pb = feature.get("pref_pb") if normal_code: defaults[normal_code] = _get_pref_value( pref_normal, pref_info, firefox_js_overrides, all_js_prefs ) if pb_code and pref_pb: defaults[pb_code] = _get_pref_value( pref_pb, pref_info, firefox_js_overrides, all_js_prefs ) return defaults def extract_strict_features(firefox_js_path): """Extract the strict mode feature string from firefox.js.""" content = firefox_js_path.read_text(encoding="utf-8") pattern = r'pref\("browser\.contentblocking\.features\.strict",\s*"([^"]+)"\)' match = re.search(pattern, content) if not match: raise ValueError( "Could not find browser.contentblocking.features.strict in firefox.js" ) return match.group(1) def parse_feature_string(feature_str): """Parse feature string into dict of {code: enabled}.""" features = {} for raw_token in feature_str.split(","): token = raw_token.strip() if not token: continue if token.startswith("-"): features[token[1:]] = False else: features[token] = True return features def _resolve_strict_value(feature, strict_features, standard_value): """Resolve the strict mode value for a feature given the feature string overrides.""" code = feature["normal_code"] if not code: return standard_value if code not in strict_features: return standard_value enabled = strict_features[code] pref_value = standard_value # For boolean prefs, the feature string directly sets true/false if pref_value in ("true", "false"): return "true" if enabled else "false" # For integer prefs (cookieBehavior, BTP mode), the feature code name # encodes the enabled value. E.g. "cookieBehavior5" means set to 5. int_match = re.search(r"(\d+)$", code) if int_match and enabled: return int_match.group(1) # If the feature string disables it, use the standard default if not enabled: return standard_value return pref_value def _find_ifdef_block(pref, pref_info, firefox_js_overrides, all_js_prefs): """Find ifdef block for a pref across all sources.""" if not pref: return None for source in [firefox_js_overrides, all_js_prefs]: src_info = source.get(pref, {}) if src_info.get("ifdef_block"): return src_info["ifdef_block"] yaml_info = pref_info.get(pref, {}) return yaml_info.get("ifdef_block") def _get_footnote_ref(pref, ifdef_block, footnotes): """Get or create a footnote for a pref, return the reference string.""" for idx, (existing_pref, _) in enumerate(footnotes): if existing_pref == pref: return f"[{idx + 1}]" footnotes.append((pref, ifdef_block)) return f"[{len(footnotes)}]" def _render_footnotes(footnotes, start_idx): """Render footnotes from start_idx to end of list as markdown lines.""" if start_idx >= len(footnotes): return [] lines = [""] for idx in range(start_idx, len(footnotes)): pref_name, ifdef_block = footnotes[idx] lines.append(f"[{idx + 1}] `{pref_name}` has build-specific defaults:") lines.append("") lines.append("```") for ifdef_line in ifdef_block.split("\n"): lines.append(ifdef_line) lines.append("```") lines.append("") return lines def generate_markdown( strict_features, standard_defaults, pref_info, firefox_js_overrides, all_js_prefs ): """Generate Markdown tables from parsed features.""" nav_links = [ "- [Enhanced Tracking Protection](#enhanced-tracking-protection-etp)", ] for category in OTHER_PRIVACY_PREFS.keys(): anchor = category.lower().replace(" ", "-").replace("&", "") nav_links.append(f"- [{category}](#{anchor})") lines = [ "# Privacy Capabilities Overview", "", "```{note}", "This page is auto-generated by `docs/_addons/etp_matrix.py` from", "`browser/app/profile/firefox.js`, `modules/libpref/init/all.js`, and", "`modules/libpref/init/StaticPrefList.yaml` during the documentation build.", "To modify the content, update the extension or the source files.", "```", "", "This page documents Firefox desktop privacy features and their default configurations.", "", "**Quick Navigation:**", "", ] lines.extend(nav_links) lines.extend([ "", "---", "", "## Enhanced Tracking Protection (ETP)", "", "Enhanced Tracking Protection features that change between **Standard** and **Strict** modes. ", "Users select their ETP mode in Firefox Settings.", "", "The **Normal** and **Private** columns indicate whether each feature is enabled in normal ", "browsing and private browsing modes, respectively.", "", "Pref defaults are sourced from ", "[StaticPrefList.yaml](https://searchfox.org/mozilla-central/source/modules/libpref/init/StaticPrefList.yaml), ", "[all.js](https://searchfox.org/mozilla-central/source/modules/libpref/init/all.js), and ", "[firefox.js](https://searchfox.org/mozilla-central/source/browser/app/profile/firefox.js) ", "(applied in that order). **ETP Strict** additionally enables features based on the ", "[`browser.contentblocking.features.strict`](https://searchfox.org/mozilla-central/search?q=%22browser.contentblocking.features.strict%22&path=%5Ebrowser%2Fapp%2Fprofile%2Ffirefox.js%24&case=true®exp=false) string in firefox.js.", "", ]) # Combined Standard + Strict table footnotes = [] lines.extend([ "| Feature | Standard Normal | Standard Private | Strict Normal | Strict Private |", "|:--------|:--------------:|:----------------:|:-------------:|:--------------:|", ]) for feature in FEATURES: name = feature["name"] normal_code = feature["normal_code"] pb_code = feature["pb_code"] pref_normal = feature["pref_normal"] pref_pb = feature.get("pref_pb") # Standard mode values std_normal_val = standard_defaults.get(normal_code) if pb_code and pref_pb: std_pb_val = standard_defaults.get(pb_code) else: std_pb_val = None # Strict mode values strict_normal_val = _resolve_strict_value( feature, strict_features, std_normal_val ) if pb_code and pref_pb: strict_pb_val = _resolve_strict_value( {**feature, "normal_code": pb_code}, strict_features, std_pb_val ) else: strict_pb_val = None # Format cell values, putting footnote refs in cells for ifdef prefs normal_ifdef = _find_ifdef_block( pref_normal, pref_info, firefox_js_overrides, all_js_prefs ) pb_ifdef = _find_ifdef_block( pref_pb, pref_info, firefox_js_overrides, all_js_prefs ) if normal_ifdef: fn_ref = _get_footnote_ref(pref_normal, normal_ifdef, footnotes) std_normal_status = fn_ref strict_normal_status = fn_ref else: std_normal_status = f"`{std_normal_val}`" strict_normal_status = f"`{strict_normal_val}`" if pref_pb: if pb_ifdef: fn_ref = _get_footnote_ref(pref_pb, pb_ifdef, footnotes) std_pb_status = fn_ref strict_pb_status = fn_ref else: std_pb_status = f"`{std_pb_val}`" strict_pb_status = f"`{strict_pb_val}`" else: std_pb_status = "" strict_pb_status = "" display_name = f"**{name}**" # Build pref links for all source files pref_links = [] if pref_normal: pref_links.append( _build_pref_links( pref_normal, pref_info, firefox_js_overrides, all_js_prefs ) ) if pref_pb: pref_links.append( _build_pref_links( pref_pb, pref_info, firefox_js_overrides, all_js_prefs ) ) pref_text = "
".join(pref_links) desc = feature.get("desc", "") if desc: cell_name = f"{display_name}
{desc}
{pref_text}
" else: cell_name = f"{display_name}
{pref_text}" lines.append( f"| {cell_name} | {std_normal_status} | {std_pb_status} " f"| {strict_normal_status} | {strict_pb_status} |" ) # ETP table footnotes (placed directly under the table) lines.extend(_render_footnotes(footnotes, 0)) # Unknown features unknown = set(strict_features.keys()) - KNOWN_CODES if unknown: lines.extend([ "", "### Unknown Features", "", "The following feature codes were found but are not mapped:", "", ]) for code in sorted(unknown): status = "enabled" if strict_features[code] else "disabled" lines.append(f"- `{code}` ({status})") # Other privacy prefs tables (footnotes continue global count) lines.extend( generate_other_privacy_table( pref_info, firefox_js_overrides, all_js_prefs, footnotes ) ) lines.extend([ "", "---", "", "*Sources: `browser/app/profile/firefox.js`, `modules/libpref/init/StaticPrefList.yaml`, `modules/libpref/init/all.js`*", ]) return "\n".join(lines) def generate_other_privacy_table( pref_info, firefox_js_overrides, all_js_prefs, footnotes ): """Generate tables for other privacy-related prefs not controlled by ETP.""" lines = [] for category, features in OTHER_PRIVACY_PREFS.items(): footnote_start = len(footnotes) lines.extend([ "", f"## {category}", "", "| Feature | Normal | Private |", "|:--------|:------:|:-------:|", ]) for feature_name, normal_pref, pb_pref, description in features: normal_value = _get_pref_value( normal_pref, pref_info, firefox_js_overrides, all_js_prefs ) # Put footnote refs in value cells for ifdef prefs normal_ifdef = _find_ifdef_block( normal_pref, pref_info, firefox_js_overrides, all_js_prefs ) if normal_ifdef: normal_status = _get_footnote_ref(normal_pref, normal_ifdef, footnotes) else: normal_status = f"`{normal_value}`" if pb_pref: pb_value = _get_pref_value( pb_pref, pref_info, firefox_js_overrides, all_js_prefs ) pb_ifdef = _find_ifdef_block( pb_pref, pref_info, firefox_js_overrides, all_js_prefs ) if pb_ifdef: pb_status = _get_footnote_ref(pb_pref, pb_ifdef, footnotes) else: pb_status = f"`{pb_value}`" else: pb_status = "" display_name = f"**{feature_name}**" # Build pref links for all source files pref_links = [] for pref in [normal_pref, pb_pref]: if pref: pref_links.append( _build_pref_links( pref, pref_info, firefox_js_overrides, all_js_prefs ) ) pref_text = "
".join(pref_links) if description: lines.append( f"| {display_name}
{description}
{pref_text}
| {normal_status} | {pb_status} |" ) else: lines.append( f"| {display_name}
{pref_text} | {normal_status} | {pb_status} |" ) # Footnotes directly under this category's table lines.extend(_render_footnotes(footnotes, footnote_start)) return lines def validate_prefs_exist(pref_info): """ Validate that all prefs used in FEATURES exist in StaticPrefList.yaml. Raises an error with instructions if any are missing. """ missing_prefs = [] for feature in FEATURES: pref_normal = feature["pref_normal"] pref_pb = feature.get("pref_pb") if pref_normal not in pref_info: missing_prefs.append(pref_normal) if pref_pb and pref_pb not in pref_info: missing_prefs.append(pref_pb) if missing_prefs: prefs_list = "\n - ".join(missing_prefs) raise ValueError( f"ETP feature prefs not found in StaticPrefList.yaml:\n - {prefs_list}\n\n" "To fix this:\n" "1. If the pref was renamed, update docs/_addons/etp_matrix.py FEATURES list\n" "2. If the pref is new, add it to modules/libpref/init/StaticPrefList.yaml\n" " with a descriptive comment above the entry" ) def generate_etp_matrix(app): """Generate ETP matrix markdown file during Sphinx build.""" import sphinx.util.logging logger = sphinx.util.logging.getLogger(__name__) topsrcdir = Path(app.confdir).parent firefox_js = topsrcdir / "browser" / "app" / "profile" / "firefox.js" if not firefox_js.exists(): raise FileNotFoundError( f"Could not find {firefox_js}, cannot generate ETP matrix" ) all_js = topsrcdir / "modules" / "libpref" / "init" / "all.js" if not all_js.exists(): raise FileNotFoundError(f"Could not find {all_js}, cannot generate ETP matrix") static_pref_list = ( topsrcdir / "modules" / "libpref" / "init" / "StaticPrefList.yaml" ) if not static_pref_list.exists(): raise FileNotFoundError( f"Could not find {static_pref_list}, cannot generate ETP matrix" ) output_dir = ( Path(app.outdir) / "_staging" / "toolkit" / "components" / "antitracking" / "anti-tracking" / "etp-matrix" ) output_dir.mkdir(parents=True, exist_ok=True) output_path = output_dir / "index.md" pref_info = parse_static_pref_list(static_pref_list) validate_prefs_exist(pref_info) all_js_prefs = parse_firefox_js_overrides(all_js) firefox_js_overrides = parse_firefox_js_overrides(firefox_js) standard_defaults = get_standard_defaults( pref_info, firefox_js_overrides, all_js_prefs ) feature_str = extract_strict_features(firefox_js) strict_features = parse_feature_string(feature_str) markdown = generate_markdown( strict_features, standard_defaults, pref_info, firefox_js_overrides, all_js_prefs, ) output_path.write_text(markdown, encoding="utf-8") logger.info(f"Generated ETP matrix: {output_path}") def setup(app): """Sphinx extension setup.""" app.connect("builder-inited", generate_etp_matrix) return { "version": "1.0", "parallel_read_safe": True, "parallel_write_safe": True, }