For contributors

Linkumori runtime architecture

Status
Current
Version
main
Source
docs/runtime-architecture.md

How Linkumori works inside the browser: what runs where, how rules are loaded, and what happens to a request. It is written for people changing the code. The rule format itself is in filter-syntax.md; building and linting rules are in CONTRIBUTING.md.

File and function names are given so you can jump to the code. Line numbers are not, because they move.

1Overview

Linkumori is a Manifest V3 Firefox extension (desktop 140+, Android 142+). It has three kinds of runtime code:

PartWhere it runsWhat it does
Background pageone event page, 23 classic scripts (manifest.json background.scripts)loads rules, holds settings, cleans requests with blocking webRequest, answers the pages
Extension pageshtml/*.html with their core_js/*.js scriptpopup, settings, custom rules editor, log, tools; they talk to the background by messages
Content scriptsregistered at runtime, only while search link fix is onGoogle and Yandex result-link fixes

There is no server. The only network requests Linkumori makes on its own are for remote rule lists, and only when the user configures them (§5.2).

Diagram: 1. Overview. Its Mermaid source follows. Diagram: 1. Overview. Its Mermaid source follows.
Diagram source
flowchart LR
    subgraph Browser
        WR[webRequest events] --> BG
        NAV[webNavigation events] --> BG
        subgraph BG[Background page]
            ST[storage.js<br/>settings + rule loading]
            EN[clearurls.js<br/>engine]
            OT[history, context menu,<br/>DNS, whitelist, ETag,<br/>badge, log, watchdog]
        end
        PG[Extension pages<br/>popup, settings, editor, log, …] <-- runtime messages --> BG
        BG -- scripting.executeScript --> TAB[Web page]
        CS[Content scripts<br/>Google / Yandex link fix] --> TAB
    end
    BG <-- browser.storage.local --> DISK[(Storage)]
    BG -. optional .-> REMOTE[Remote rule list + hash]

2The background page

All background scripts are classic scripts in one global scope. They call each other's functions directly (pureCleaning(), getData(), isWhitelisted(), …), so load order matters: a script may only call another script's functions at runtime, never while loading, unless that script is listed earlier.

OrderScriptRole
1external_js/linkumori-i18n.jsLinkumoriI18n: replacement for browser.i18n that loads _locales/<lang>/messages.json itself
2core_js/message_handler.jsruntime.onMessage bridge for the pages (§10)
3external_js/IP-Ranger.jsIP address parsing and classification
4external_js/sha256.jsSHA-256
5core_js/tools.jsshared helpers: translate, URL helpers, counters, icon, log (pushToLog), keep-alive
6–7external_js/light-punycode.js, external_js/publicsuffixlist.jspunycode; Public Suffix List parser (linkumoriPsl)
8core_js/badgedHandler.jsper-tab badge counter
9core_js/pureCleaning.jscleaning outside webRequest, and the rule test lab (§7.1)
10core_js/context_menu.jsCopy clean link context menu
11core_js/historyListener.jscleans URLs changed with the History API (§7.2)
12external_js/regex_analyzer.jsregex analysis
13core_js/linkumori_rule_ids.jsLinkumoriRuleIds: generated rule ids, shared with the editor and CLI (§6.2)
14core_js/linkumori_rule_pins.jsLinkumoriRulePins: ids pinned for switched-off rules, shared with the editor and CLI (§6.2)
14acore_js/linkumori_rule_patches.jsLinkumoriRulePatches: checks and applies remote rule patches, shared with the CLI (§5.3)
15clearurls.jsthe engine: providers, request cleaning, start() (§6)
16core_js/linkumori_dns.jsLinkumoriDNS: CNAME uncloaking (§6.5)
17core_js/whitelist.jswhitelists and the temporary tab whitelist (§8)
18external_js/linkumori_lz4_block.jsLinkumoriLZ4: decompresses the bundled rules
19core_js/storage.jssettings, persistence, rule loading; calls genesis() last (§3–§5)
20core_js/content_script_manager.jsregisters the search link fix content scripts (§7.3)
21core_js/watchdog.jsself-test every minute (§9)
22core_js/eTagFilter.jsETag header filtering (§7.4)
23external_js/decode-uri-component.jslenient URI decoding for redirect targets
24core_js/consent_config.jscurrent policy and POSAR versions for the consent gate

consent_config.js loads last, but genesis() reads its values only after an asynchronous storage read, so they are defined by then.

Main globals

GlobalDefined inHolds
storagestorage.jsin-memory copy of every setting and of the loaded rules (§4)
providers, providersByToken, globalProvidersclearurls.jsthe compiled providers and the lookup index (§6.3)
requestContextManagerclearurls.jsURLs of each tab and frame, for context checks (§6.6)
clearurlsProviderSnapshotclearurls.jsevery active and disabled rule with its ids, read by the editor's Disabled rules page
badgesbadgedHandler.jsper-tab badge counts
temporaryTabWhitelistwhitelist.jsper-tab domains allowed for this tab only

Keeping the page alive

Firefox can suspend an MV3 background event page when it is idle. tools.js calls browser.runtime.getPlatformInfo every 20 seconds (keepAlive) so the page, its in-memory storage and its compiled providers stay loaded.

3Startup

storage.js calls genesis() once all background scripts have loaded:

Diagram: 3. Startup. Its Mermaid source follows. Diagram: 3. Startup. Its Mermaid source follows.
Diagram source
sequenceDiagram
    participant G as genesis()
    participant L as browser.storage.local
    participant P as Public Suffix List
    participant R as loadBundledRules()
    participant E as start() (clearurls.js)
    G->>L: get(null)
    L-->>G: all saved keys
    G->>G: initStorage(): defaults, then setData() for each saved key
    G->>G: consumePostReloadOpenUrl()
    G->>P: initLinkumoriPublicSuffixList()
    G->>R: load and merge rules (§5)
    R-->>G: storage.ClearURLsData
    G->>E: startClearurlsIfConsentGranted()
    G->>G: changeIcon(), contextMenuStart(), historyListenerStart()

If rule loading fails, the same last four steps still run.

Cleaning only starts once the user has accepted the current privacy policy, the current POSAR version and the adult confirmation:

On first install, storage.js opens html/legal.html?source=first_install.

start()

start() in clearurls.js:

  1. starts Public Suffix List support (initPslSupport) and requestContextManager;
  2. calls tryInitialize(), which builds the providers from storage.ClearURLsData and registers the webRequest listener. If no rules are loaded yet, it retries every 200 ms, up to 50 times, then registers the listener anyway;
  3. calls setBadgedStatus().

It also defines globalThis.updateProviderData(), which rebuilds the providers from storage.ClearURLsData without a restart (§5.5).

4Settings and persistence

storage is an in-memory object holding every setting. It is the source of truth while the extension runs; browser.storage.local is its saved copy.

Keys that shape the runtime:

KeyMeaning
ClearURLsDatathe merged rules the engine runs (§5)
custom_rulesthe user's custom providers, as saved by the editor
customrules_import_exclusionsproviders the user switched off (by provider signature)
clearurls_disabled_rule_idsrules the user switched off, as <scope>::<ruleId> (§6.2)
userWhitelist, historyApiWhitelistwhitelists (§8)
builtInRulesEnabled, remoteRulesEnabled, overloadModeEnabled, overloadModeStrategywhich rule sources to use and how overload mode handles patches (§5)
ruleURL + hashURL, remoteRuleSetsremote rule sources
remoteRulescache, remoteRulesHealthlast verified remote rules; fetch history
hashStatus, hashValidationStatus, dataHash, mergeStats, rulesMetadatawhat was loaded, from where, and how it merged; shown in settings
globalStatus, temporaryPauseUntilcleaning on/off; pause end time
types, pingRequestTypesrequest types the webRequest listener receives
redirectionEnabled, domainBlocking, pingBlocking, referralMarketing, localHostsSkipping, eTagFiltering, linkumoriCNAMEUncloakEnabled, …feature switches
totalCounter, cleanedCounter, log, logLimit, loggingStatusstatistics and the log (§9)
statisticsStatuscounters on/off (default on)
badgedStatus, badged_colorthe toolbar badge on/off (default on) and its colour (default #2563eb) (§9)
historyListenerEnabledHistory API cleaning on/off (default on) (§7.2)
unboundedCyclesEnabledlift the 10-cycle limit on cleaning a request (default off) (§6.4)
ruleDefaultsMode, userRuleDefaults, sourceRuleDefaultsrule defaults: where the user's own defaults replace the files' (source, final or unified; default source), the user's own defaults block, and the user's edits of a file's own block, by source (defaults)
remoteRuleCapabilitieswhat each remote file can do and what the user accepted, as { files: { [ruleURL]: { derived, accepted } } } (§5.2)
disableGatekeeperskip the confirmation for remote rule addresses (default off) (§11.2)

The interface language is not in storage: the language selector in settings writes linkumori_language to browser.storage.sync, and external_js/linkumori-i18n.js reads it on every page. Left unset, the browser's language is used.

5Rule loading

loadBundledRules() in storage.js decides which rule sources to use, merges them with the custom rules, and stores the result in storage.ClearURLsData.

5.1Sources

SourceWhere it comes from
Bundleddata/linkumori-clearurls-min.json.lz4, built by node linkumori-cli-tool.js clearurls from data/linkumori-clearurls.json and decompressed with LinkumoriLZ4
Remoteone or more ruleURL + hashURL pairs set by the user (§5.2)
Remote cacheremoteRulescache: the last remote rules that passed verification
Customcustom_rules, from the custom rules editor
Fallbacka small built-in list in getEnhancedFallbackRules(), used when nothing else loads

5.2Remote rules

Remote rules are off by default. Each source is a pair of URLs, and both must be valid (areValidRemoteURLsPresent(), isValidRuleURL()).

  1. Fetch the hash URL: 32–128 hex characters (fetchRemoteHash).
  2. Fetch the rule URL, and check that the SHA-256 of the raw text matches (fetchRemoteRules, verifyRulesHash). A mismatch rejects the source.
  3. Parse it as Linkumori JSON with a providers object.

Both requests use cache: 'no-store', so the rules and their hash always come from the same moment. Sources are fetched in parallel. The ones that pass are merged (mergeRemoteRulesSources): providers with the same pattern and settings become one provider (getProviderGroupKey, mergeRemoteProviderGroup). The result is saved as remoteRulescache.

Within a merged provider, two rules count as the same when everything that changes what they do matches, targetId, order and historyBypassProtection included (getStableRuleSignature). Only one is kept, and the other's id and aliases become aliases of it. An inactive provider adds nothing to its group, and a provider-level historyBypassProtection is copied onto its rules, since the merged provider can hold only one.

Each source is checked for rule collisions (LinkumoriRuleIds.findRuleCollisions), and so is each merge, for the collisions merging creates. A merge is also checked for the same text in rules from one file and referralMarketing from another (LinkumoriRuleIds.findReferralTextCollisions). They are logged and shown in settings, and the rules still load.

The outcome of every attempt is recorded in remoteRulesHealth, which settings shows.

Capabilities. When a remote file loads, resolveRemoteFileCapabilities records what its entries can do (LinkumoriRuleIds.deriveRuleFileCapabilities) in remoteRuleCapabilities. strip, rewrite and except are active at once; redirect and block stay off for that file until the user accepts them in Remote Rules Health, which calls acceptRemoteRuleCapabilities and reloads the remote rules. Entries that need a capability the file lacks are left out (restrictRuleFileCapabilities). Without redirect, every raw rule is kept but tagged _linkumoriNoRedirect: a raw rule can take the URL to another scheme, host or port by a rewrite or just by deleting text, which only shows when it runs, so rawRuleMayChangeOrigin() in clearurls.js then leaves such a URL unchanged. The verified cache can't be traced to one file, so when it loads, strip, rewrite and except are active and redirect and block only if every configured file has accepted them (getCapabilitiesAcceptedByAllRemoteFiles). See CLN 1.0 §Remote file capabilities.

5.3Choosing the sources

Diagram: 5.3 Choosing the sources. Its Mermaid source follows. Diagram: 5.3 Choosing the sources. Its Mermaid source follows.
Diagram source
flowchart TD
    A[loadBundledRules] --> B{built-in off<br/>and remote off?}
    B -- yes --> C[custom rules only]
    B -- no --> D{remote sources<br/>configured and valid?}
    D -- no --> E{built-in on?}
    E -- yes --> F[bundled LZ4]
    E -- no --> C
    D -- yes --> G[fetch + verify every source]
    G -- at least one passed --> H{overload mode?}
    H -- yes --> I{overload strategy}
    I -- patch-only --> I1[bundled rules + remote patches]
    I -- patch-and-merge --> I2[remote merged with bundled,<br/>then patched]
    I -- merge-only --> I3[remote merged with bundled,<br/>patches skipped]
    H -- no --> J[remote]
    G -- all failed --> K{verified cache<br/>for these sources?}
    K -- yes --> L[cached remote<br/>+ bundled in overload mode]
    K -- no --> E2{built-in on?}
    E2 -- yes --> F
    E2 -- no --> C
    F -- LZ4 fails --> M[remote cache, else last<br/>ClearURLsData, else fallback list]
    I1 & I2 & I3 & J & L & F & M --> N[mergeCustomRules]
    C --> O[ClearURLsData]
    N --> O

Rule patches. A remote file's patches (see filter syntax §Rule patches) are applied by applyRemoteRulePatches() to the rules chosen above, just before mergeCustomRules, including when the remote cache is used. With overload mode on, overloadModeStrategy selects patch-only (use bundled rules as the base and omit remote providers), patch-and-merge (merge remote and bundled providers, then apply patches), or merge-only (merge providers and skip patches). With overload mode off, the strategy is ignored and existing remote rule behavior applies. When every loaded remote file holds only patches, the base is the bundled rules, unmerged so their provider names stay (withPatchOnlyBuiltInRules). Each patch's result (applied, unmatched or skipped) is stored in remoteRulesHealth.rulePatches.

hashStatus records which path was taken, for example bundled_rules_loaded, remote_verified, remote_rules_partially_merged, cache_remote_rules_after_remote_failure or fallback_rules_used. When custom rules are merged in, it gets a custom variant, such as custom_rules_merged.

5.4Merging custom rules

mergeCustomRules(baseRules) builds the final list:

  1. Providers the user switched off (customrules_import_exclusions) are removed from both the base rules and the custom rules.
  2. Providers without urlPattern or domainPatterns are dropped.
  3. A custom provider replaces every base provider with the same pattern (the same urlPattern, or the same domainPatterns in any order), whatever their methods, resourceTypes, completeProvider and forceRedirection (getProviderPatternOnlyGroupKey). Their rules are not combined. Overload mode still merges built-in and remote providers only when the pattern and all four settings match (§5.2). Switching a provider off (step 1) uses the pattern-only key too (isProviderSignatureDisabled), so it removes every provider with that pattern.
  4. Other custom providers are added. A name already in use gets _1, _2, ….
  5. Every provider goes through attachProviderActivationIds(): each rule gets its activation ids (_linkumoriActivationIds) and, if its id changed, its old ids (_linkumoriLegacyRuleIds); see §6.2.
  6. The result is saved as ClearURLsData, with dataHash (SHA-256 of a key-sorted serialisation) and mergeStats.

5.5When rules reload

Rules are loaded at startup, and again only when:

Nothing refreshes remote rules on a timer.

6The engine (clearurls.js)

6.1Providers

createProviders() turns every provider in ClearURLsData into a Provider object:

Rules that fail to compile, or use an option the engine does not accept, are skipped. The custom rules editor and lint-rules report such rules before they are saved.

6.2Rule ids and switched-off rules

Every rule has an id: its own id, or one generated by LinkumoriRuleIds from its list and text (see filter-syntax.md: Rule objects). The engine, storage.js, the custom rules editor and the CLI all use core_js/linkumori_rule_ids.js, so they always produce the same ids.

A rule can be switched off in two ways; both are stored in clearurls_disabled_rule_ids:

ScopeStored asSwitches the rule off
Provider<provider>::<ruleId>everywhere in that provider
PatterndomainPattern:<pattern>::<ruleId> or urlPattern:<pattern>::<ruleId>only for URLs that match that one pattern

While providers are built, attachCoreRuleIdentity() gives each rule its ids, and filterCoreRuleActivationIds() drops the switched-off ones.

A setting saved under an old id also counts, and is then moved to the current id (migrateCoreRuleAliasActivationIds). Old ids come from two places:

A generated id changes with the rule's text, so the first switch-off of a rule without an id fixes it (core_js/linkumori_rule_pins.js). The editor writes the id onto a custom rule; for a rule from a built-in or remote list it stores a pin in clearurls_rule_id_pins: { provider, section, generatedId, sourceListId, fingerprintAtToggle, disableKeys }. createProviders() matches each provider's pins to its current rules (same text, same generated id, or text at least 80% alike, resolveProviderPins) and gives a matched rule the pinned id, activation ids included (applyCoreRulePin). A switched-off rule that has no pin yet (switched off before pins existed) gets one, and a pin matched to drifted text remembers it as lastSeenText. Pins that match no rule are listed in clearurlsProviderSnapshot.rulePins.orphaned, which the editor shows as Orphaned toggles.

The result is published as clearurlsProviderSnapshot (via getData), which the editor's Disabled rules page lists.

6.3Finding the providers for a URL

Checking every provider against every URL would be slow, so the engine indexes providers by hostname token:

6.4Cleaning a request

The webRequest.onBeforeRequest listener (blocking) covers <all_urls> and the request types in types + pingRequestTypes. It records tab and frame URLs for requestContextManager, skips data: URLs, and calls clearUrl(request):

Diagram: 6.4 Cleaning a request. Its Mermaid source follows. Diagram: 6.4 Cleaning a request. Its Mermaid source follows.
Diagram source
flowchart TD
    A[request] --> B{paused?}
    B -- yes --> Z[let it through]
    B -- no --> C{whitelisted?<br/>request or its page}
    C -- yes --> Z
    C -- no --> D{cleaning on?}
    D -- no --> Z
    D -- yes --> E{ping or beacon,<br/>ping blocking on?}
    E -- yes --> X[cancel]
    E -- no --> F[collect @@ $removeparam exceptions<br/>from providers matching the page]
    F --> G[candidates: providers indexed under<br/>the request host + global providers]
    G --> H{next candidate matches<br/>method, type, URL?}
    H -- no more --> Y{CNAME uncloaking?}
    H -- yes --> I[removeFieldsFormURL]
    I -- redirect --> R[redirect, or navigate the tab<br/>if forceRedirection]
    I -- block --> K[main frame: siteBlockedAlert page<br/>other: redirect to javascript:void 0]
    I -- changed --> M[redirect to the cleaned URL]
    I -- unchanged --> H
    Y -- decision for the CNAME --> R2[same decision,<br/>mapped back to the original host]
    Y -- none --> Z

The first provider that redirects, blocks or changes the URL decides the result. A redirect is a new request, so the cleaned URL goes through clearUrl() again. The browser keeps the requestId across redirects, so clearUrl() counts the cycles of each request and stops after LINKUMORI_MAX_CLEANING_CYCLES (10), unless unbounded mode (unboundedCyclesEnabled) is on, and an entry with a replacePattern rewrites at most once per request (Termination). pureCleaning() (clipboard, context menu, rule tester) loops the same way, with the same cap (shouldRunAnotherCleaningCycle). In unbounded mode no browser stops a redirect loop there, so it also stops when the URL comes back to one it already produced.

A provider's exceptions are checked while matching the URL (matchRequestURL), so an excepted URL never reaches the next step. removeFieldsFormURL(provider, url, …) then applies the provider in the order described in filter-syntax.md: Processing order:

  1. It skips local hosts (localHostsSkipping).
  2. redirections and fieldRedirections apply (only while redirection is on). A target that isn't an absolute http or https URL makes the entry not match (getLinkumoriRedirectTarget(), Redirect targets).
  3. completeProvider blocks the request (only while domain blocking is on).
  4. The ordered cleaning steps run (getOrderedCleaningSteps()): raw rules on the URL text, then field rules and referral-marketing rules on query and fragment parameters. Inside each stage, entries with an order come first, lowest first; then by array (rules, flagged rules, referralMarketing) and array position (Run order). The provider keeps every definition by group and text and resolves them once, in finalizeRules(): the last definition wins and inherits the first one's place (Same text twice). Parameters already handled by a $removeparam filter are left to that filter.
  5. $removeparam filters run, minus their @@ exceptions and the exceptions collected from the page's providers. @@ entries are checked against the URL the provider received, before its raw rules ran.

It logs each change and updates the badge unless called in quiet mode.

6.5CNAME uncloaking

If no provider made a decision, and CNAME uncloaking is on, LinkumoriDNS.replayCNAMEIfNeeded() (linkumori_dns.js) checks whether the request host is an alias:

6.6Request context

Some decisions depend on the page a request comes from:

requestContextManager.collectContextURLs(request) returns every URL that can stand for that page:

6.7Public Suffix List

initPslSupport() loads data/public_suffix_list.dat into linkumoriPsl. The engine uses it for:

If it fails to load, hostname tokens fall back to plain labels, .* patterns (in rules and in the whitelist) stop matching, and first-party / third-party compare full hostnames instead of registrable domains.

7Other cleaning paths

7.1pureCleaning(url)

pureCleaning() (pureCleaning.js) cleans a URL outside webRequest. It runs every provider over the URL until it stops changing. It does not use the token index, and there is no request, so options that depend on the request (party, method) do not apply. For request types the URL counts as main_frame (getLinkumoriRequestType()), the page a link opens: a rule or $removeparam filter limited to other types is skipped, and one limited to main_frame applies. It is used by:

runRuleTestLab() and pureCleaningTrace() use the same loop, but record which provider and rule changed what. The custom rules editor's test lab shows this trace. traceClearURLWebRequestTest() does the same through the real clearUrl() for the regression page.

7.2History API

webNavigation.onHistoryStateUpdated (historyListener.js) fires when a page changes its own URL with pushState or replaceState. The listener:

  1. does nothing before cleaning has started (§3), and skips the URL if it is paused or whitelisted, including the History API whitelist and the temporary tab whitelist;
  2. cleans it with pureCleaning(url, false, true), where rules with historyBypassProtection: false do not run;
  3. if the URL changed, runs history.replaceState(null, "", cleanUrl) in the frame with scripting.executeScript.

content_script_manager.js registers two content scripts with scripting.registerContentScripts, and only while searchLinkFixEnabled is on (default off). They run at document_end in all frames:

The registration is refreshed on install, on browser start, and when the setting changes.

7.4ETag filtering

eTagFilter.js registers a blocking webRequest.onHeadersReceived listener with responseHeaders. While eTagFiltering is on (default off), it replaces the value of each ETag response header with a random value of the same form, so the ETag cannot be used as a tracking id. It does nothing before cleaning has started (§3), and skips local hosts, whitelisted requests, and paused cleaning.

8Whitelists and pausing

MechanismStored inSkips
WhitelistuserWhitelistall cleaning for matching hosts, and for requests made by pages on them
History API whitelisthistoryApiWhitelistonly History API cleaning (§7.2)
Temporary tab whitelistmemory only (temporaryTabWhitelist), per tabcleaning for those hosts in that tab until it closes
PausetemporaryPauseUntil (time), or memory only until restartall cleaning, until the time passes or the browser restarts

Whitelist entries are hostname patterns (example.com also covers its subdomains) or IP addresses (matchWhitelistHostnamePattern). The popup adds and removes entries over messaging.

9Counters, badge, log and watchdog

10Messaging between the pages and the background

Pages call the background with:

browser.runtime.sendMessage({ function: 'getData', params: ['globalStatus'] })

handleMessage() (message_handler.js) always answers with a promise of { response, error? }.

functionWhat it does
getData / setDataread or write one key of storage. For custom_rules, clearurls_disabled_rule_ids, userWhitelist, historyApiWhitelist and ClearURLsData, setData answers only after the key is saved to disk, so a restart that follows cannot lose it
reloadCustomRules, mergeCustomRules, getCustomRulesStats, getBundledRulesOnly, getExistingLinkumoriDataForImportrule loading and statistics for the settings page and the editor
any other namecalled on the background's global scope, but only if it is in the allowlist in handleRegularFunction()

The allowlist covers:

To add a background function a page can call: define it in a background script, then add its name to the allowlist.

Pages also read and write some keys directly with browser.storage.local: the popup's consent keys, and settings that background listeners watch (§4).

11Extension pages

PageScriptPurposeMain background calls
popup.htmlpopup.jstoolbar popup: on/off, counters, pause, whitelist this site, consent gategetData, setData, temporary whitelist, pause functions
settings.html (options page)settings.jsall settings, rule sources, remote rules, export/import, statisticsgetData, setData, getRuleSourceInfo, getMergeStatistics, getRemoteRulesHealth, refreshRemoteRulesNow, storageAsJSON, reload
customrules.htmlcustom_rules_editor.jscustom providers, rule on/off, import, test lab, run-order panelgetData, setData, reloadCustomRules, getBundledRulesOnly, runRuleTestLab, getProviderRunOrder, simulateProviderRunOrder, generateProviderTestUrl
log.htmllog.jsthe cleaning log, livegetData, setData, port linkumori-logger
cleaningTool.htmlcleaning_tool.jsclean pasted URLspureCleaning
siteBlockedAlert.htmlsiteBlockedAlert.jsshown when a provider blocks a page (completeProvider); links to the blocked address so it can still be opened—
legal.htmlabout.jsprivacy policy and consentsetData, saveOnExit, reload
report.htmlreport.jsprepares a GitHub issue about a broken page—
audit.htmlaudit.jsshows the bundled, merged, custom and cached remote rulesgetData
regression.htmlregression_page.jsruns tests/regression-suite.json inside the extensionapplyRegressionRuleData, traceClearURLWebRequestTest, runRuleTestLab, storageAsJSON, start
guide.htmlguide.jsuser guide—

Every page loads theme_bootstrap.js (themes), linkumori-i18n.js (text), accessibility_tools.js (accessibility options) and usually write_version.js (version footer).

11.1Run-order panel

Under the custom rules editor's JSON editor, the run-order panel lists the provider's rawRules, rules and referralMarketing entries in the order they run, with each step's sort key (stage, orderGroup, order, rank, arrayIndex), followed by its $removeparam filters. It is worked out by the engine, not by the page: on every edit (debounced) the page sends the unsaved provider and the custom rules' defaults to getProviderRunOrder. That function, in clearurls.js, builds the provider the way createProviders() does (fillProvider(), the custom rules' defaults, the user's rule pins), but as a dry run: its rules are not added to the snapshot the rule controls read, and no pin or alias migration is recorded. Provider.describeRunOrder() returns both variants, with referral-marketing rules and without, and marks the steps the user switched off. The panel starts on the variant the user's referral-marketing setting selects.

Steps can be rearranged in the variant that lists every entry, by dragging or with each step's arrow buttons, but only within their stage: raw rules always run before field rules, and the step a completeProvider adds stays where it is. A move gives every step of that stage an order of 10, 20, 30, … in the new sequence, written on the entry that defines the step (the last definition of its text, which describeRunOrder() reports as source). A plain string entry becomes { "matchPattern": …, "order": … }; its generated id comes from the text, so it doesn't change. Each movable step shows its order (or "no order"); while a step is dragged over a drop point, every step of its stage shows the number the drop would give it (order 5 → 20), and the labels go back if the drag is cancelled. Undo move restores the editor text from before the last move, until the user types. Restore order numbers puts every order back as it was when the provider was opened: an entry that had none loses it again (and a { "matchPattern", "order" } object goes back to a plain string), one that had one gets it back, and entries whose text changed since are left alone. It shows only while some order differs.

Simulate runs the provider on a test URL and steps through it: start, previous, play, next and end, with the URL after each step (removed text struck through, added text highlighted, compared by URL part) and the step's row lit in the list. simulateProviderRunOrder(name, provider, defaults, url, referralAllowed) builds the same dry-run provider with the user's switched-off rules, then calls removeFieldsFormURL() with a stepRecorder array that receives the URL before and after every ordered step and after the $removeparam filters. It runs like cleaning a copied link (no request, quiet), pass after pass while the URL changes, within the cycle limit (shouldRunAnotherCleaningCycle), and stops at a redirect or block. Only this provider runs. storage.referralMarketing is set to the panel's choice for the duration of the call and restored; the call is synchronous, so nothing else can read it meanwhile. The simulation runs again after each edit or move.

Generate test URL fills the test URL from the provider's own rules (generateProviderTestUrl(name, provider, defaults)): a host its domainPatterns or urlPattern match, text in the path for each raw rule, a parameter for each rules, referralMarketing and named $removeparam entry, and keep=1, which no rule should remove. Samples come from Regex.Analyzer (external_js/regex_analyzer.js), and each is checked against the rule it is for and against the provider's exceptions; what can't be sampled, or would make an exception match, is left out and listed. Redirects and blocks are left out too, since they would end the run before the other rules. On the 704 bundled providers it makes a matching URL for all but the two whose exceptions cover every URL they match.

11.2Settings page safeguards

settings.js asks before a few risky changes. These checks live in the page only; the background accepts the keys whatever their value.

12Code shared between the background, pages and CLI

Some logic has to agree between the background, the custom rules editor (which does not load the background scripts) and linkumori-cli-tool.js (Node):

LogicWhere it lives
Generated rule idsone shared module, core_js/linkumori_rule_ids.js, loaded by the background and the editor and imported by the CLI
Pinned rule idscore_js/linkumori_rule_pins.js, loaded and imported in the same places; pins live in clearurls_rule_id_pins
Rule validation (keys, options, rawrule=, targetId, …)written twice: in the editor (assert… and rawRuleOptionProblem in custom_rules_editor.js) and in the CLI linter (lintClearURLsRules). Change both together
Pattern matching and option parsingthe engine (clearurls.js); the CLI smoke tests use a simplified copy

13Build time and runtime

Build time (CLI)Runtime (extension)
data/linkumori-clearurls.json is edited by handnever read
node linkumori-cli-tool.js clearurls builds data/linkumori-clearurls-min.json.lz4 with metadataloaded at startup (§5.1)
lint-rules, show-rule check and inspect rulesthe editor runs the same checks on save
data/public_suffix_list.dat is refreshed by the CLIloaded by linkumoriPsl (§6.7)
tests/regression-suite.json is maintained in the reporun by regression.html

14Things to know

Facts that matter when changing the code:

Generated from docs/runtime-architecture.md on the main branch. Found a problem? Open an issue.