uBlock Origin

extension name.

Finally, an efficient blocker. Easy on CPU and memory.

this will be in the Chrome web store: must be 132 characters or less

uBlock₀ — Dashboard

English: uBlock₀ — Dashboard

Warning! You have unsaved changes

A warning in the dashboard when navigating away from unsaved changes

Stay

Label for button to prevent navigating away from unsaved changes

Ignore

Label for button to ignore unsaved changes

Settings

appears as tab name in dashboard

Filter lists

appears as tab name in dashboard

My filters

appears as tab name in dashboard

My rules

appears as tab name in dashboard

Trusted sites

appears as tab name in dashboard

Shortcuts

appears as tab name in dashboard

uBlock₀ — Logger

Title for the logger window

About

appears as tab name in dashboard

Support

appears as tab name in dashboard

uBlock₀ — Asset viewer

Title for the asset viewer page

Advanced settings

Title for the advanced settings page

English: Click: disable/enable uBlock₀ for this site.

Ctrl+click: disable uBlock₀ only on this page.

Click to disable uBlock₀ for this site.

Ctrl+click to disable uBlock₀ only on this page.

Message to be read by screen readers

Click to enable uBlock₀ for this site.

Message to be read by screen readers

requests blocked

English: requests blocked

on this page

English: on this page

{{count}} ({{percent}}%)

Example: 15 (13%)

since install

English: since install

or

English: or

Blocked on this page

For the new mobile-friendly popup design

Blocked since install

Domains connected

Open the dashboard

English: Click to open the dashboard

Enter element zapper mode

Tooltip for the element-zapper icon in the popup panel

Enter element picker mode

English: Enter element picker mode

Open the logger

Tooltip used for the logger icon in the panel

Report an issue on this website

Tooltip used for the 'chat' icon in the panel

Toggle the blocking of all popups for this site

Tooltip for the no-popups per-site switch

Click to block all popups on this site

Click to no longer block all popups on this site

Toggle the blocking of large media elements for this site

Tooltip for the no-large-media per-site switch

Click to block large media elements on this site

Click to no longer block large media elements on this site

Toggle cosmetic filtering for this site

Tooltip for the no-cosmetic-filtering per-site switch

Click to disable cosmetic filtering on this site

Click to enable cosmetic filtering on this site

Toggle the blocking of remote fonts for this site

Tooltip for the no-remote-fonts per-site switch

Click to block remote fonts on this site

Click to no longer block remote fonts on this site

Click to disable JavaScript on this site

Tooltip for the no-scripting per-site switch

Click to no longer disable JavaScript on this site

Pop-up windows

Caption for the no-popups per-site switch

Large media elements

Caption for the no-large-media per-site switch

Cosmetic filtering

Caption for the no-cosmetic-filtering per-site switch

Remote fonts

Caption for the no-remote-fonts per-site switch

JavaScript

Caption for the no-scripting per-site switch

More

Label to be used to show popup panel sections

Less

Label to be used to hide popup panel sections

Global rules: this column is for rules which apply to all sites.

Tooltip when hovering the top-most cell of the global-rules column.

Local rules: this column is for rules which apply to the current site only.
Local rules override global rules.

Tooltip when hovering the top-most cell of the local-rules column.

Click to make your changes permanent.

Tooltip when hovering over the padlock in the dynamic filtering pane.

Click to revert your changes.

Tooltip when hovering over the eraser in the dynamic filtering pane.

all

images

3rd-party CSS/images

inline scripts

1st-party scripts

3rd-party scripts

3rd-party frames

domains connected

appears in popup

{{count}} out of {{total}}

appears in popup

Version

Example of use: Version 1.26.4

↑: Emphasize rows which have 3rd-party scripts

Tooltip shown when hovering small filtering widget in '3rd-party scripts' cell

↑: Emphasize rows which have 3rd-party frames

Tooltip shown when hovering small filtering widget in '3rd-party frames' cell

Create

English: Create

Pick

English: Pick

Quit

English: Quit

Preview

Element picker preview mode: will cause the elements matching the current filter to be removed from the page

Network filters

English: header for a type of filter in the element picker dialog

Cosmetic filters

English: Cosmetic filters

Click, Ctrl-click

English: Click, Ctrl-click

Block element…

An entry in the browser's contextual menu

Hide placeholders of blocked elements

English: Hide placeholders of blocked elements

Show the number of blocked requests on the icon

English: Show the number of blocked requests on the icon

Disable tooltips

A checkbox in the Settings pane

Make use of context menu where appropriate

English: Make use of context menu where appropriate

Color-blind friendly

English: Color-blind friendly

Appearance

Section for controlling user interface appearance

Theme

Label for checkbox to enable a custom dark theme

Custom accent color

Label for checkbox to pick an accent color

Enable cloud storage support

I am an advanced user

Checkbox to let user access advanced, technical features

Disable pre-fetching (to prevent any connection for blocked network requests)

English:

Disable hyperlink auditing

English:

Prevent WebRTC from leaking local IP addresses

English:

Default behavior

These default behaviors can be overridden on a per-site basis

Disable cosmetic filtering

Block media elements larger than {{input}} KB

Block remote fonts

Disable JavaScript

The default state for the per-site no-scripting switch

Block CSP reports

background information: https://github.com/gorhill/uBlock/issues/3150

Uncloak canonical names

background information: https://github.com/uBlockOrigin/uBlock-issues/issues/1513

Advanced

Section for controlling advanced-user settings

Features suitable only for technical users

Description of section controlling advanced-user settings

advanced settings

For the tooltip of a link which gives access to advanced settings

Last restore:

English: Last restore:

Last backup:

English: Last backup:

{{netFilterCount}} network filters ＋ {{cosmeticFilterCount}} cosmetic filters from:

Appears at the top of the _3rd-party filters_ pane

{{used}} used out of {{total}}

Appears aside each filter list in the _3rd-party filters_ pane

Auto-update filter lists

A checkbox in the _3rd-party filters_ pane

Update now

A button in the in the _3rd-party filters_ pane

Purge all caches

Parse and enforce cosmetic filters

English: Parse and enforce Adblock+ element hiding filters.

Cosmetic filters serve to hide elements in a web page which are deemed to be a visual nuisance, and which can't be blocked by the network request-based filtering engines.

Describes the purpose of the 'Parse and enforce cosmetic filters' feature.

Ignore generic cosmetic filters

This will cause uBO to ignore all generic cosmetic filters.

Generic cosmetic filters are those cosmetic filters which are meant to apply on all web sites. Enabling this option will eliminate the memory and CPU overhead added to web pages as a result of handling generic cosmetic filters.

It is recommended to enable this option on less powerful devices.

Describes the purpose of the 'Ignore generic cosmetic filters' feature.

Suspend network activity until all filter lists are loaded

A checkbox in the 'Filter lists' pane

Lists of blocked hosts

English: Lists of blocked hosts

Apply changes

English: Apply changes

Built-in

Header for the uBlock filters section in 'Filter lists pane'

Ads

English: Ads

Privacy

English: Privacy

Malware domains

English: Malware domains

Annoyances

The header identifying the filter lists in the category 'annoyances'

Multipurpose

English: Multipurpose

Regions, languages

English: Regions, languages

Custom

English: Custom

Import…

The label for the checkbox used to import external filter lists

One URL per line. Invalid URLs will be silently ignored.

Short information about how to use the textarea to import external filter lists by URL

Out of date.

used as a tooltip for the out-of-date icon beside a list

view content

used as a tooltip for eye icon beside a list

Last update: {{ago}}.
Click to force an update.

used as a tooltip for the clock icon beside a list

Updating…

used as a tooltip for the spinner icon beside a list

A network error prevented the resource from being updated.

used as a tooltip for error icon beside a list

One filter per line. A filter can be a plain hostname, or an EasyList-compatible filter. Lines prefixed with <code>!</code> will be ignored.

Short information about how to create custom filters

Import and append

English: Import and append

Export

English: Export

my-ublock-static-filters_{{datetime}}.txt

English: my-ublock-static-filters_{{datetime}}.txt

Apply changes

English: Apply changes

Permanent rules

header

Temporary rules

header

Revert

This will remove all temporary rules

Commit

This will persist temporary rules

Edit

Will enable manual-edit mode (textarea)

Save

Will save manually-edited content and exit manual-edit mode

Discard

Will discard manually-edited content and exit manual-edit mode

Import from file…

Export to file

my-ublock-dynamic-rules_{{datetime}}.txt

default file name to use

List of your dynamic filtering rules.

English: List of your dynamic filtering rules.

Rule syntax: source destination type action (full documentation).

English: dynamic rule syntax and full documentation.

Sort:

English: label for sort option.

Rule type

English: a sort option for list of rules.

Source

Destination

The trusted site directives dictate on which web pages uBlock Origin should be disabled. One entry per line.

A concise description of the 'Trusted sites' pane.

Import and append

English: Import and append

Export

English: Export

my-ublock-trusted-sites_{{datetime}}.txt

The default filename to use for import/export purpose

Apply changes

English: Apply changes

Type

English: Type

Domain

English: Domain

URL

English: URL

Filter

English: Filter

All

Appears in the logger's tab selector

Tabless

Pretty name for behind-the-scene network requests

Current tab

Appears in the logger's tab selector

Reload the tab content

Tooltip for the reload button in the logger page

Toggle the DOM inspector

Tooltip for the DOM inspector button in the logger page

Toggle the popup panel

Tooltip for the popup panel button in the logger page

uBlock Origin wiki: The logger

Tooltip for the top-right info label in the logger page

Clear logger

Tooltip for the eraser in the logger page; used to blank the content of the logger

Pause logger (discard all incoming data)

Tooltip for the pause button in the logger page

Unpause logger

Tooltip for the play button in the logger page

Toggle logger filtering

Tooltip for the row filterer button in the logger page

filter logger content

Placeholder string for logger output filtering input field

Logger filtering options

Tooltip for the button to bring up logger output filtering options

Not

A keyword in the built-in row filtering expression

eventful

A keyword in the built-in row filtering expression: all items corresponding to uBO doing something (blocked, allowed, redirected, etc.)

blocked

allowed

modified

Details

Small header to identify the 'Details' pane for a specific logger entry

Filter

Label to identify a filter field

Filter list

Label to identify a filter list field

Rule

Label to identify a rule field

Context

Label to identify a context field (typically a hostname)

Root context

Label to identify a root context field (typically a hostname)

Partyness

Label to identify a field providing partyness information

Type

Label to identify the type of an entry

URL

Label to identify the URL of an entry

URL rule

Small header to identify the dynamic URL filtering section

Context:

Label for the context selector

Type:

Label for the type selector

Static filter

Small header to identify the static filtering section

{{action}} network requests of {{type}} {{br}}which URL address matches {{url}} {{br}}and which originates {{origin}},{{br}}{{importance}} there is a matching exception filter.

Used in the static filtering wizard

Block

Used in the static filtering wizard

Allow

Used in the static filtering wizard

type “{{type}}”

Used in the static filtering wizard

any type

Used in the static filtering wizard

from “{{origin}}”

Used in the static filtering wizard

from anywhere

Used in the static filtering wizard

except when

Used in the static filtering wizard

even if

Used in the static filtering wizard

Static filter <code>{{filter}}</code> found in:

Below this sentence, the filter list(s) in which the filter was found

Static filter could not be found in any of the currently enabled filter lists

Message to show when a filter cannot be found in any filter lists

Logger entries which do not fulfill all three conditions below will be automatically discarded:

Logger setting: A sentence to describe the purpose of the settings below

Preserve entries from the last {{input}} minutes

A logger setting

Preserve at most {{input}} page loads per tab

A logger setting

Preserve at most {{input}} entries per tab

A logger setting

Use {{input}} lines per entry in vertically expanded mode

A logger setting

Hide columns:

Logger settings: a sentence to describe the purpose of the checkboxes below

{{input}} Time

A label for the time column

{{input}} Filter/rule

A label for the filter or rule column

{{input}} Context

A label for the context column

{{input}} Partyness

A label for the partyness column

List

Label for radio-button to pick export format

Table

Plain

Label for radio-button to pick export text format

Markdown

Open

Text for button which open an external webpage in Support pane

Create new report

Find similar reports

A clickable link in the filter issue reporter section

Documentation

Header of 'Documentation' section in Support pane

Read the documentation at uBlock/wiki to learn about all of uBlock Origin's features.

First paragraph of 'Documentation' section in Support pane

Questions and support

Header of 'Questions and support' section in Support pane

Answers to questions and other kinds of help support is provided on the subreddit <code>/r/uBlockOrigin</code>.

First paragraph of 'Questions and support' section in Support pane

Filter issues/website is broken

Header of 'Filter issues' section in Support pane

Report filter issues with specific websites to the uBlockOrigin/uAssets issue tracker. Requires a GitHub account.

First paragraph of 'Filter issues' section in Support pane

<b>Important:</b> Avoid using other similarly-purposed blockers along with uBlock Origin, as this may cause filter issues on specific websites.

Second paragraph of 'Filter issues' section in Support pane

Tips: Be sure your filter lists are up to date. The logger is the primary tool to diagnose filter-related issues.

Third paragraph of 'Filter issues' section in Support pane

Bug report

Header of 'Bug report' section in Support pane

Report issues with uBlock Origin itself to the uBlockOrigin/uBlock-issue issue tracker. Requires a GitHub account.

First paragraph of 'Bug report' section in Support pane

Troubleshooting Information

Header of 'Troubleshooting Information' section in Support pane

Below is technical information that might be useful when volunteers are trying to help you solve a problem.

First paragraph of 'Troubleshooting Information' section in Support pane

<b>Important:</b> Potentially private or sensitive information is redacted by default. Redacted information may make it more difficult to solve a problem.

Second paragraph of 'Troubleshooting Information' section in Support pane

Report a filter issue

Header of 'Report a filter issue' section in Support pane

To avoid burdening volunteers with duplicate reports, please verify that the issue has not already been reported.

A paragraph in the filter issue reporter section

Address of the web page:

Label for the URL of the page

The web page…

Label for widget to select type of issue

-- Pick an entry --

An entry in the widget used to select the type of issue

Shows ads or ad leftovers

Has overlays or other nuisances

Detects uBlock Origin

Has privacy-related issues

Malfunctions when uBlock Origin is enabled

Opens unwanted tabs or windows

Label the web page as “NSFW” (“Not Safe For Work”)

A checkbox to use for NSFW sites

Redact

Text for 'Redact' button

Unredact

Text for 'Unredact' button

Privacy policy

Link to privacy policy on GitHub (English)

Changelog

Source code (GPLv3)

English: Source code (GPLv3)

Contributors

English: Contributors

Source code

Link text to source code repo

Translations

Link text to translations repo

Filter lists

Link text to uBO's own filter lists repo

External dependencies (GPLv3-compatible):

Shown in the About pane

uBO's own filter lists are freely hosted on the following CDNs:

Shown in the About pane

A randomly picked CDN is used when a filter list needs to be updated

Shown in the About pane

Back up to file

Text for button to create a backup of all settings

English: my-ublock-backup_{{datetime}}.txt

Restore from file…

English: Restore from file...

Reset to default settings…

English: Reset to default settings...

All your settings will be overwritten using data backed up on {{time}}, and uBlock₀ will restart.

Overwrite all existing settings using backed up data?

Message asking user to confirm restore

The data could not be read or is invalid

Message to display when an error occurred during restore

All your settings will be removed, and uBlock₀ will restart.

Reset uBlock₀ to factory settings?

Message asking user to confirm reset

Network error: {{msg}}

English: Network error: {{msg}}

Add the following URL to your custom filter lists?

Title: "{{title}}"
URL: {{url}}

No longer used

Subscribe

For the button used to subscribe to a filter list

a minute ago

English: a minute ago

{{value}} minutes ago

English: {{value}} minutes ago

an hour ago

English: an hour ago

{{value}} hours ago

English: {{value}} hours ago

a day ago

English: a day ago

{{value}} days ago

English: {{value}} days ago

Show Dashboard

Firefox/Fennec-specific: Show Dashboard

Show Logger

Firefox/Fennec-specific: Show Logger

off

Firefox-specific: appears as 'uBlock₀ (off)'

uBlock Origin has prevented the following page from loading:

Used in the strict-blocking page

Because of the following filter:

Used in the strict-blocking page

without parameters

label to be used for the parameter-less URL: https://cloud.githubusercontent.com/assets/585534/9832014/bfb1b8f0-593b-11e5-8a27-fba472a5529a.png

Found in:

English: List of filter list names follows

Go back

English: Go back

Close this window

English: Close this window

Disable strict blocking for {{hostname}}

English: Disable strict blocking for {{hostname}} ...

Temporarily

English: Temporarily

Permanently

English: Permanently

Export to cloud storage

tooltip

Import from cloud storage

tooltip

Import from cloud storage and merge with current settings

tooltip

This device name:

used as a prompt for the user to provide a custom device name

Warning! Change these advanced settings at your own risk.

A warning to users at the top of 'Advanced settings' page

Submit

for generic 'Submit' buttons

Apply changes

for generic 'Apply changes' buttons

Revert

for generic 'Revert' buttons

bytes

Block element in frame…

An entry in the browser's contextual menu

Subscribe to filter list…

Temporarily allow large media elements

A context menu entry, present when large media elements have been blocked on the current site

Type a shortcut

Placeholder string for input field used to capture a keyboard shortcut

Toggle locked scrolling

Tooltip for the button used to lock scrolling between the views in the 'My rules' pane

Copy to clipboard

Label for buttons used to copy something to the clipboard

Select all

Label for buttons used to select all text in editor

Toggle blocking profile

Label for keyboard shortcut used to toggle blocking profile

Relax blocking mode

Label for keyboard shortcut used to relax blocking mode (meant to replace 'Toggle blocking profile')

Storage used: {{value}} {{unit}}

In Setting pane, renders as (example): Storage used: 13.2 MB

KB

short for 'kilobytes'

MB

short for 'megabytes'

GB

short for 'gigabytes'

Click to load

Message used in frame placeholders

This entry must be the last one

so we dont need to deal with comma for last entry


* */
:root {
--font-size: 14px;
--font-size-smaller: 13px;
--font-family: Inter, sans-serif;
}
:root.mobile {
--font-size: 16px;
--font-size-smaller: 14px;

keep keep keep keep add keep keep keep keep keep

/*******************************************************************************

    uBlock Origin - a browser extension to block requests.
    Copyright (C) 2014-present Raymond Hill

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see {http://www.gnu.org/licenses/}.

    Home: https://github.com/gorhill/uBlock
*/

'use strict';

/******************************************************************************/

import contextMenu from './contextmenu.js';
import logger from './logger.js';
import staticNetFilteringEngine from './static-net-filtering.js';
import µb from './background.js';
import webext from './webext.js';
import { orphanizeString } from './text-utils.js';
import { redirectEngine } from './redirect-engine.js';

import {
    sessionFirewall,
    sessionSwitches,
    sessionURLFiltering,
} from './filtering-engines.js';

import {
    domainFromHostname,
    hostnameFromURI,
    isNetworkURI,
} from './uri-utils.js';

/*******************************************************************************

A PageRequestStore object is used to store net requests in two ways:

To record distinct net requests
To create a log of net requests

**/

/******************************************************************************/

const NetFilteringResultCache = class {
    constructor() {
        this.init();
    }

    init() {
        this.blocked = new Map();
        this.results = new Map();
        this.hash = 0;
        this.timer = undefined;
        return this;
    }

    // https://github.com/gorhill/uBlock/issues/3619
    //   Don't collapse redirected resources
    rememberResult(fctxt, result) {
        if ( fctxt.tabId <= 0 ) { return; }
        if ( this.results.size === 0 ) {
            this.pruneAsync();
        }
        const key = `${fctxt.getDocHostname()} ${fctxt.type} ${fctxt.url}`;
        this.results.set(key, {
            result,
            redirectURL: fctxt.redirectURL,
            logData: fctxt.filter,
            tstamp: Date.now()
        });
        if ( result !== 1 || fctxt.redirectURL !== undefined ) { return; }
        const now = Date.now();
        this.blocked.set(key, now);
        this.hash = now;
    }

    rememberBlock(fctxt) {
        if ( fctxt.tabId <= 0 ) { return; }
        if ( this.blocked.size === 0 ) {
            this.pruneAsync();
        }
        if ( fctxt.redirectURL !== undefined ) { return; }
        const now = Date.now();
        this.blocked.set(
            `${fctxt.getDocHostname()} ${fctxt.type} ${fctxt.url}`,
            now
        );
        this.hash = now;
    }

    forgetResult(docHostname, type, url) {
        const key = `${docHostname} ${type} ${url}`;
        this.results.delete(key);
        this.blocked.delete(key);
    }

    empty() {
        this.blocked.clear();
        this.results.clear();
        this.hash = 0;
        if ( this.timer !== undefined ) {
            clearTimeout(this.timer);
            this.timer = undefined;
        }
    }

    prune() {
        const obsolete = Date.now() - this.shelfLife;
        for ( const entry of this.blocked ) {
            if ( entry[1] <= obsolete ) {
                this.results.delete(entry[0]);
                this.blocked.delete(entry[0]);
            }
        }
        for ( const entry of this.results ) {
            if ( entry[1].tstamp <= obsolete ) {
                this.results.delete(entry[0]);
            }
        }
        if ( this.blocked.size !== 0 || this.results.size !== 0 ) {
            this.pruneAsync();
        }
    }

    pruneAsync() {
        if ( this.timer !== undefined ) { return; }
        this.timer = vAPI.setTimeout(
            ( ) => {
                this.timer = undefined;
                this.prune();
            },
            this.shelfLife
        );
    }

    lookupResult(fctxt) {
        const entry = this.results.get(
            fctxt.getDocHostname() + ' ' +
            fctxt.type + ' ' +
            fctxt.url
        );
        if ( entry === undefined ) { return; }
        // We need to use a new WAR secret if one is present since WAR secrets
        // can only be used once.
        if (
            entry.redirectURL !== undefined &&
            entry.redirectURL.startsWith(this.extensionOriginURL)
        ) {
            const redirectURL = new URL(entry.redirectURL);
            redirectURL.searchParams.set('secret', vAPI.warSecret());
            entry.redirectURL = redirectURL.href;
        }
        return entry;
    }

    lookupAllBlocked(hostname) {
        const result = [];
        for ( const entry of this.blocked ) {
            const pos = entry[0].indexOf(' ');
            if ( entry[0].slice(0, pos) === hostname ) {
                result[result.length] = entry[0].slice(pos + 1);
            }
        }
        return result;
    }

    static factory() {
        return new NetFilteringResultCache();
    }
};

NetFilteringResultCache.prototype.shelfLife = 15000;
NetFilteringResultCache.prototype.extensionOriginURL = vAPI.getURL('/');

/******************************************************************************/

// Frame stores are used solely to associate a URL with a frame id.

const FrameStore = class {
    constructor(frameURL, parentId) {
        this.init(frameURL, parentId);
    }

    init(frameURL, parentId) {
        this.t0 = Date.now();
        this.parentId = parentId;
        this.exceptCname = undefined;
        this.clickToLoad = false;
        this.rawURL = frameURL;
        if ( frameURL !== undefined ) {
            this.hostname = hostnameFromURI(frameURL);
            this.domain = domainFromHostname(this.hostname) || this.hostname;
        }
        // Evaluated on-demand
        // - 0b01: specific cosmetic filtering
        // - 0b10: generic cosmetic filtering
        this._cosmeticFilteringBits = undefined;
        return this;
    }

    dispose() {
        this.rawURL = this.hostname = this.domain = '';
        if ( FrameStore.junkyard.length < FrameStore.junkyardMax ) {
            FrameStore.junkyard.push(this);
        }
        return null;
    }

    updateURL(url) {
        if ( typeof url !== 'string' ) { return; }
        this.rawURL = url;
        this.hostname = hostnameFromURI(url);
        this.domain = domainFromHostname(this.hostname) || this.hostname;
        this._cosmeticFilteringBits = undefined;
    }

    getCosmeticFilteringBits(tabId) {
        if ( this._cosmeticFilteringBits !== undefined ) {
            return this._cosmeticFilteringBits;
        }
        this._cosmeticFilteringBits = 0b11;
        {
            const result = staticNetFilteringEngine.matchRequestReverse(
                'specifichide',
                this.rawURL
            );
            if ( result !== 0 && logger.enabled ) {
                µb.filteringContext
                    .duplicate()
                    .fromTabId(tabId)
                    .setURL(this.rawURL)
                    .setDocOriginFromURL(this.rawURL)
                    .setRealm('network')
                    .setType('specifichide')
                    .setFilter(staticNetFilteringEngine.toLogData())
                    .toLogger();
            }
            if ( result === 2 ) {
                this._cosmeticFilteringBits &= ~0b01;
            }
        }
        {
            const result = staticNetFilteringEngine.matchRequestReverse(
                'generichide',
                this.rawURL
            );
            if ( result !== 0 && logger.enabled ) {
                µb.filteringContext
                    .duplicate()
                    .fromTabId(tabId)
                    .setURL(this.rawURL)
                    .setDocOriginFromURL(this.rawURL)
                    .setRealm('network')
                    .setType('generichide')
                    .setFilter(staticNetFilteringEngine.toLogData())
                    .toLogger();
            }
            if ( result === 2 ) {
                this._cosmeticFilteringBits &= ~0b10;
            }
        }
        return this._cosmeticFilteringBits;
    }

    shouldApplySpecificCosmeticFilters(tabId) {
        return (this.getCosmeticFilteringBits(tabId) & 0b01) !== 0;
    }

    shouldApplyGenericCosmeticFilters(tabId) {
        return (this.getCosmeticFilteringBits(tabId) & 0b10) !== 0;
    }

    static factory(frameURL, parentId = -1) {
        const entry = FrameStore.junkyard.pop();
        if ( entry === undefined ) {
            return new FrameStore(frameURL, parentId);
        }
        return entry.init(frameURL, parentId);
    }
};

// To mitigate memory churning
FrameStore.junkyard = [];
FrameStore.junkyardMax = 50;

/******************************************************************************/

const CountDetails = class {
    constructor() {
        this.allowed = { any: 0, frame: 0, script: 0 };
        this.blocked = { any: 0, frame: 0, script: 0 };
    }
    reset() {
        const { allowed, blocked } = this;
        blocked.any = blocked.frame = blocked.script =
        allowed.any = allowed.frame = allowed.script = 0;
    }
    inc(blocked, type = undefined) {
        const stat = blocked ? this.blocked : this.allowed;
        if ( type !== undefined ) { stat[type] += 1; }
        stat.any += 1;
    }
};

const HostnameDetails = class {
    constructor(hostname) {
        this.counts = new CountDetails();
        this.init(hostname);
    }
    init(hostname) {
        this.hostname = hostname;
        this.counts.reset();
    }
    dispose() {
        this.hostname = '';
        if ( HostnameDetails.junkyard.length < HostnameDetails.junkyardMax ) {
            HostnameDetails.junkyard.push(this);
        }
    }
};

HostnameDetails.junkyard = [];
HostnameDetails.junkyardMax = 100;

const HostnameDetailsMap = class extends Map {
    reset() {
        this.clear();
    }
    dispose() {
        for ( const item of this.values() ) {
            item.dispose();
        }
        this.reset();
    }
};

/******************************************************************************/

const PageStore = class {
    constructor(tabId, details) {
        this.extraData = new Map();
        this.journal = [];
        this.journalTimer = undefined;
        this.journalLastCommitted = this.journalLastUncommitted = -1;
        this.journalLastUncommittedOrigin = undefined;
        this.netFilteringCache = NetFilteringResultCache.factory();
        this.hostnameDetailsMap = new HostnameDetailsMap();
        this.counts = new CountDetails();
        this.init(tabId, details);
    }

    static factory(tabId, details) {
        let entry = PageStore.junkyard.pop();
        if ( entry === undefined ) {
            entry = new PageStore(tabId, details);
        } else {
            entry.init(tabId, details);
        }
        return entry;
    }

    // https://github.com/gorhill/uBlock/issues/3201
    //   The context is used to determine whether we report behavior change
    //   to the logger.

    init(tabId, details) {
        const tabContext = µb.tabContextManager.mustLookup(tabId);
        this.tabId = tabId;

        // If we are navigating from-to same site, remember whether large
        // media elements were temporarily allowed.
        if (
            typeof this.allowLargeMediaElementsUntil !== 'number' ||
            tabContext.rootHostname !== this.tabHostname
        ) {
            this.allowLargeMediaElementsUntil = Date.now();
        }

        this.tabHostname = tabContext.rootHostname;
        this.rawURL = tabContext.rawURL;
        this.hostnameDetailsMap.reset();
        this.contentLastModified = 0;
        this.logData = undefined;
        this.counts.reset();
        this.remoteFontCount = 0;
        this.popupBlockedCount = 0;
        this.largeMediaCount = 0;
        this.largeMediaTimer = null;
        this.allowLargeMediaElementsRegex = undefined;
        this.extraData.clear();

        this.frameAddCount = 0;
        this.frames = new Map();
        this.setFrameURL({ url: tabContext.rawURL });

        if ( this.titleFromDetails(details) === false ) {
            this.title = tabContext.rawURL;
        }

        // Evaluated on-demand
        this._noCosmeticFiltering = undefined;

        return this;
    }

    reuse(context, details) {
        // When force refreshing a page, the page store data needs to be reset.

        // If the hostname changes, we can't merely just update the context.
        const tabContext = µb.tabContextManager.mustLookup(this.tabId);
        if ( tabContext.rootHostname !== this.tabHostname ) {
            context = '';
        }

        // If URL changes without a page reload (more and more common), then
        // we need to keep all that we collected for reuse. In particular,
        // not doing so was causing a problem in `videos.foxnews.com`:
        // clicking a video thumbnail would not work, because the frame
        // hierarchy structure was flushed from memory, while not really being
        //  flushed on the page.
        if ( context === 'tabUpdated' ) {
            // As part of https://github.com/chrisaljoudi/uBlock/issues/405
            // URL changed, force a re-evaluation of filtering switch
            this.rawURL = tabContext.rawURL;
            this.setFrameURL({ url: this.rawURL });
            this.titleFromDetails(details);
            return this;
        }

        // A new page is completely reloaded from scratch, reset all.
        if ( this.largeMediaTimer !== null ) {
            clearTimeout(this.largeMediaTimer);
            this.largeMediaTimer = null;
        }
        this.disposeFrameStores();
        this.init(this.tabId, details);
        return this;
    }

    dispose() {
        this.tabHostname = '';
        this.title = '';
        this.rawURL = '';
        this.hostnameDetailsMap.dispose();
        this.netFilteringCache.empty();
        this.allowLargeMediaElementsUntil = Date.now();
        this.allowLargeMediaElementsRegex = undefined;
        if ( this.largeMediaTimer !== null ) {
            clearTimeout(this.largeMediaTimer);
            this.largeMediaTimer = null;
        }
        this.disposeFrameStores();
        if ( this.journalTimer !== undefined ) {
            clearTimeout(this.journalTimer);
            this.journalTimer = undefined;
        }
        this.journal = [];
        this.journalLastUncommittedOrigin = undefined;
        this.journalLastCommitted = this.journalLastUncommitted = -1;
        if ( PageStore.junkyard.length < PageStore.junkyardMax ) {
            PageStore.junkyard.push(this);
        }
        return null;
    }

    titleFromDetails(details) {
        if (
            details instanceof Object === false ||
            details.title === undefined
        ) {
            return false;
        }
        this.title = orphanizeString(details.title.slice(0, 128));
        return true;
    }

    disposeFrameStores() {
        for ( const frameStore of this.frames.values() ) {
            frameStore.dispose();
        }
        this.frames.clear();
    }

    getFrameStore(frameId) {
        return this.frames.get(frameId) || null;
    }

    // https://github.com/uBlockOrigin/uBlock-issues/issues/1858
    //   Mind that setFrameURL() can be called from navigation event handlers.
    setFrameURL(details) {
        let { frameId, url, parentFrameId } = details;
        if ( frameId === undefined ) { frameId = 0; }
        if ( parentFrameId === undefined ) { parentFrameId = -1; }
        let frameStore = this.frames.get(frameId);
        if ( frameStore !== undefined ) {
            if ( url === frameStore.rawURL ) {
                frameStore.parentId = parentFrameId;
            } else {
                frameStore.init(url, parentFrameId);
            }
            return frameStore;
        }
        frameStore = FrameStore.factory(url, parentFrameId);
        this.frames.set(frameId, frameStore);
        this.frameAddCount += 1;
        if ( url.startsWith('about:') ) {
            frameStore.updateURL(this.getEffectiveFrameURL({ frameId }));
        }
        if ( (this.frameAddCount & 0b111111) === 0 ) {
            this.pruneFrames();
        }
        return frameStore;
    }

    getEffectiveFrameURL(sender) {
        let { frameId } = sender;
        for (;;) {
            const frameStore = this.getFrameStore(frameId);
            if ( frameStore === null ) { break; }
            if ( frameStore.rawURL.startsWith('about:') === false ) {
                return frameStore.rawURL;
            }
            frameId = frameStore.parentId;
            if ( frameId === -1 ) { break; }
        }
        return sender.frameURL;
    }

    // There is no event to tell us a specific subframe has been removed from
    // the main document. The code below will remove subframes which are no
    // longer present in the root document. Removing obsolete subframes is
    // not a critical task, so this is executed just once on a while, to avoid
    // bloated dictionary of subframes.
    // A TTL is used to avoid race conditions when new iframes are added
    // through the webRequest API but still not yet visible through the
    // webNavigation API.
    async pruneFrames() {
        let entries;
        try {
            entries = await webext.webNavigation.getAllFrames({
                tabId: this.tabId
            });
        } catch(ex) {
        }
        if ( Array.isArray(entries) === false ) { return; }
        const toKeep = new Set();
        for ( const { frameId } of entries ) {
            toKeep.add(frameId);
        }
        const obsolete = Date.now() - 60000;
        for ( const [ frameId, { t0 } ] of this.frames ) {
            if ( toKeep.has(frameId) || t0 >= obsolete ) { continue; }
            this.frames.delete(frameId);
        }
    }

    getNetFilteringSwitch() {
        return µb.tabContextManager
                 .mustLookup(this.tabId)
                 .getNetFilteringSwitch();
    }

    toggleNetFilteringSwitch(url, scope, state) {
        µb.toggleNetFilteringSwitch(url, scope, state);
        this.netFilteringCache.empty();
    }

    shouldApplyCosmeticFilters(frameId = 0) {
        if ( this._noCosmeticFiltering === undefined ) {
            this._noCosmeticFiltering = this.getNetFilteringSwitch() === false;
            if ( this._noCosmeticFiltering === false ) {
                this._noCosmeticFiltering = sessionSwitches.evaluateZ(
                    'no-cosmetic-filtering',
                    this.tabHostname
                ) === true;
                if ( this._noCosmeticFiltering && logger.enabled ) {
                    µb.filteringContext
                        .duplicate()
                        .fromTabId(this.tabId)
                        .setURL(this.rawURL)
                        .setRealm('cosmetic')
                        .setType('dom')
                        .setFilter(sessionSwitches.toLogData())
                        .toLogger();
                }
            }
        }
        if ( this._noCosmeticFiltering ) { return false; }
        if ( frameId === -1 ) { return true; }
        // Cosmetic filtering can be effectively disabled when both specific
        // and generic cosmetic filters are disabled.
        return this.shouldApplySpecificCosmeticFilters(frameId) ||
               this.shouldApplyGenericCosmeticFilters(frameId);
    }

    shouldApplySpecificCosmeticFilters(frameId) {
        if ( this.shouldApplyCosmeticFilters(-1) === false ) { return false; }
        const frameStore = this.getFrameStore(frameId);
        if ( frameStore === null ) { return false; }
        return frameStore.shouldApplySpecificCosmeticFilters(this.tabId);
    }

    shouldApplyGenericCosmeticFilters(frameId) {
        if ( this.shouldApplyCosmeticFilters(-1) === false ) { return false; }
        const frameStore = this.getFrameStore(frameId);
        if ( frameStore === null ) { return false; }
        return frameStore.shouldApplyGenericCosmeticFilters(this.tabId);
    }

    // https://github.com/gorhill/uBlock/issues/2105
    //   Be sure to always include the current page's hostname -- it might not
    //   be present when the page itself is pulled from the browser's
    //   short-term memory cache.
    getAllHostnameDetails() {
        if (
            this.hostnameDetailsMap.has(this.tabHostname) === false &&
            isNetworkURI(this.rawURL)
        ) {
            this.hostnameDetailsMap.set(
                this.tabHostname,
                new HostnameDetails(this.tabHostname)
            );
        }
        return this.hostnameDetailsMap;
    }

    injectLargeMediaElementScriptlet() {
        vAPI.tabs.executeScript(this.tabId, {
            file: '/js/scriptlets/load-large-media-interactive.js',
            allFrames: true,
            runAt: 'document_idle',
        });
        contextMenu.update(this.tabId);
    }

    temporarilyAllowLargeMediaElements(state) {
        this.largeMediaCount = 0;
        contextMenu.update(this.tabId);
        if ( state ) {
            this.allowLargeMediaElementsUntil = 0;
            this.allowLargeMediaElementsRegex = undefined;
        } else {
            this.allowLargeMediaElementsUntil = Date.now();
        }
        vAPI.tabs.executeScript(this.tabId, {
            file: '/js/scriptlets/load-large-media-all.js',
            allFrames: true,
        });
    }

    // https://github.com/gorhill/uBlock/issues/2053
    //   There is no way around using journaling to ensure we deal properly with
    //   potentially out of order navigation events vs. network request events.
    journalAddRequest(fctxt, result) {
        const hostname = fctxt.getHostname();
        if ( hostname === '' ) { return; }
        this.journal.push(hostname, result, fctxt.itype);
        if ( this.journalTimer !== undefined ) { return; }
        this.journalTimer = vAPI.setTimeout(
            ( ) => { this.journalProcess(true); },
            µb.hiddenSettings.requestJournalProcessPeriod
        );
    }

    journalAddRootFrame(type, url) {
        if ( type === 'committed' ) {
            this.journalLastCommitted = this.journal.length;
            if (
                this.journalLastUncommitted !== -1 &&
                this.journalLastUncommitted < this.journalLastCommitted &&
                this.journalLastUncommittedOrigin === hostnameFromURI(url)
            ) {
                this.journalLastCommitted = this.journalLastUncommitted;
            }
        } else if ( type === 'uncommitted' ) {
            const newOrigin = hostnameFromURI(url);
            if (
                this.journalLastUncommitted === -1 ||
                this.journalLastUncommittedOrigin !== newOrigin
            ) {
                this.journalLastUncommitted = this.journal.length;
                this.journalLastUncommittedOrigin = newOrigin;
            }
        }
        if ( this.journalTimer !== undefined ) {
            clearTimeout(this.journalTimer);
        }
        this.journalTimer = vAPI.setTimeout(
            ( ) => { this.journalProcess(true); },
            µb.hiddenSettings.requestJournalProcessPeriod
        );
    }

    journalProcess(fromTimer = false) {
        if ( fromTimer === false ) { clearTimeout(this.journalTimer); }
        this.journalTimer = undefined;

        const journal = this.journal;
        const pivot = Math.max(0, this.journalLastCommitted);
        const now = Date.now();
        const { SCRIPT, SUB_FRAME } = µb.FilteringContext;
        let aggregateAllowed = 0;
        let aggregateBlocked = 0;

        // Everything after pivot originates from current page.
        for ( let i = pivot; i < journal.length; i += 3 ) {
            const hostname = journal[i+0];
            let hnDetails = this.hostnameDetailsMap.get(hostname);
            if ( hnDetails === undefined ) {
                hnDetails = new HostnameDetails(hostname);
                this.hostnameDetailsMap.set(hostname, hnDetails);
                this.contentLastModified = now;
            }
            const blocked = journal[i+1] === 1;
            const itype = journal[i+2];
            if ( itype === SCRIPT ) {
                hnDetails.counts.inc(blocked, 'script');
                this.counts.inc(blocked, 'script');
            } else if ( itype === SUB_FRAME ) {
                hnDetails.counts.inc(blocked, 'frame');
                this.counts.inc(blocked, 'frame');
            } else {
                hnDetails.counts.inc(blocked);
                this.counts.inc(blocked);
            }
            if ( blocked ) {
                aggregateBlocked += 1;
            } else {
                aggregateAllowed += 1;
            }
        }
        this.journalLastUncommitted = this.journalLastCommitted = -1;

        // https://github.com/chrisaljoudi/uBlock/issues/905#issuecomment-76543649
        //   No point updating the badge if it's not being displayed.
        if ( aggregateBlocked !== 0 && µb.userSettings.showIconBadge ) {
            µb.updateToolbarIcon(this.tabId, 0x02);
        }

        // Everything before pivot does not originate from current page -- we
        // still need to bump global blocked/allowed counts.
        for ( let i = 0; i < pivot; i += 3 ) {
            if ( journal[i+1] === 1 ) {
                aggregateBlocked += 1;
            } else {
                aggregateAllowed += 1;
            }
        }
        if ( aggregateAllowed !== 0 || aggregateBlocked !== 0 ) {
            µb.localSettings.blockedRequestCount += aggregateBlocked;
            µb.localSettings.allowedRequestCount += aggregateAllowed;
            µb.localSettingsLastModified = now;
        }
        journal.length = 0;
    }

    filterRequest(fctxt) {
        fctxt.filter = undefined;
        fctxt.redirectURL = undefined;

        if ( this.getNetFilteringSwitch(fctxt) === false ) {
            return 0;
        }

        if (
            fctxt.itype === fctxt.CSP_REPORT &&
            this.filterCSPReport(fctxt) === 1
        ) {
            return 1;
        }

        if (
            (fctxt.itype & fctxt.FONT_ANY) !== 0 &&
            this.filterFont(fctxt) === 1 )
        {
            return 1;
        }

        if (
            fctxt.itype === fctxt.SCRIPT &&
            this.filterScripting(fctxt, true) === 1
        ) {
            return 1;
        }

        const cacheableResult =
            this.cacheableResults.has(fctxt.itype) &&
            fctxt.aliasURL === undefined;

        if ( cacheableResult ) {
            const entry = this.netFilteringCache.lookupResult(fctxt);
            if ( entry !== undefined ) {
                fctxt.redirectURL = entry.redirectURL;
                fctxt.filter = entry.logData;
                return entry.result;
            }
        }

        const requestType = fctxt.type;
        const loggerEnabled = logger.enabled;

        // Dynamic URL filtering.
        let result = sessionURLFiltering.evaluateZ(
            fctxt.getTabHostname(),
            fctxt.url,
            requestType
        );
        if ( result !== 0 && loggerEnabled ) {
            fctxt.filter = sessionURLFiltering.toLogData();
        }

        // Dynamic hostname/type filtering.
        if ( result === 0 && µb.userSettings.advancedUserEnabled ) {
            result = sessionFirewall.evaluateCellZY(
                fctxt.getTabHostname(),
                fctxt.getHostname(),
                requestType
            );
            if ( result !== 0 && result !== 3 && loggerEnabled ) {
                fctxt.filter = sessionFirewall.toLogData();
            }
        }

        // Static filtering has lowest precedence.
        const snfe = staticNetFilteringEngine;
        if ( result === 0 || result === 3 ) {
            result = snfe.matchRequest(fctxt);
            if ( result !== 0 ) {
                if ( loggerEnabled ) {
                    fctxt.setFilter(snfe.toLogData());
                }
                // https://github.com/uBlockOrigin/uBlock-issues/issues/943
                //   Blanket-except blocked aliased canonical hostnames?
                if (
                    result === 1 &&
                    fctxt.aliasURL !== undefined &&
                    snfe.isBlockImportant() === false &&
                    this.shouldExceptCname(fctxt)
                ) {
                    return 2;
                }
            }
        }

        // Click-to-load?
        // When frameId is not -1, the resource is always sub_frame.
        if ( result === 1 && fctxt.frameId !== -1 ) {
            const frameStore = this.getFrameStore(fctxt.frameId);
            if ( frameStore !== null && frameStore.clickToLoad ) {
                result = 2;
                if ( loggerEnabled ) {
                    fctxt.pushFilter({
                        result,
                        source: 'network',
                        raw: 'click-to-load',
                    });
                }
            }
        }

        // Modifier(s)?
        // A modifier is an action which transform the original network request.
        // https://github.com/gorhill/uBlock/issues/949
        //   Redirect blocked request?
        // https://github.com/uBlockOrigin/uBlock-issues/issues/760
        //   Redirect non-blocked request?
        if ( (fctxt.itype & fctxt.INLINE_ANY) === 0 ) {
            if ( result === 1 ) {
                this.redirectBlockedRequest(fctxt);
            } else if ( snfe.hasQuery(fctxt) ) {
                this.redirectNonBlockedRequest(fctxt);
            }
        }

        if ( cacheableResult ) {
            this.netFilteringCache.rememberResult(fctxt, result);
        } else if ( result === 1 && this.collapsibleResources.has(fctxt.itype) ) {
            this.netFilteringCache.rememberBlock(fctxt);
        }

        return result;
    }

    filterOnHeaders(fctxt, headers) {
        fctxt.filter = undefined;

        if ( this.getNetFilteringSwitch(fctxt) === false ) { return 0; }

        let result = staticNetFilteringEngine.matchHeaders(fctxt, headers);
        if ( result === 0 ) { return 0; }

        const loggerEnabled = logger.enabled;
        if ( loggerEnabled ) {
            fctxt.filter = staticNetFilteringEngine.toLogData();
        }

        // Dynamic filtering allow rules
        // URL filtering
        if (
            result === 1 &&
            sessionURLFiltering.evaluateZ(
                fctxt.getTabHostname(),
                fctxt.url,
                fctxt.type
            ) === 2
        ) {
            result = 2;
            if ( loggerEnabled ) {
                fctxt.filter = sessionURLFiltering.toLogData();
            }
        }
        // Hostname filtering
        if (
            result === 1 &&
            µb.userSettings.advancedUserEnabled &&
            sessionFirewall.evaluateCellZY(
                fctxt.getTabHostname(),
                fctxt.getHostname(),
                fctxt.type
            ) === 2
        ) {
            result = 2;
            if ( loggerEnabled ) {
                fctxt.filter = sessionFirewall.toLogData();
            }
        }

        return result;
    }

    redirectBlockedRequest(fctxt) {
        const directives = staticNetFilteringEngine.redirectRequest(
            redirectEngine,
            fctxt
        );
        if ( directives === undefined ) { return; }
        if ( logger.enabled !== true ) { return; }
        fctxt.pushFilters(directives.map(a => a.logData()));
        if ( fctxt.redirectURL === undefined ) { return; }
        fctxt.pushFilter({
            source: 'redirect',
            raw: redirectEngine.resourceNameRegister
        });
    }

    redirectNonBlockedRequest(fctxt) {
        const directives = staticNetFilteringEngine.filterQuery(fctxt);
        if ( directives === undefined ) { return; }
        if ( logger.enabled !== true ) { return; }
        fctxt.pushFilters(directives.map(a => a.logData()));
        if ( fctxt.redirectURL === undefined ) { return; }
        fctxt.pushFilter({
            source: 'redirect',
            raw: fctxt.redirectURL
        });
    }

    filterCSPReport(fctxt) {
        if (
            sessionSwitches.evaluateZ(
                'no-csp-reports',
                fctxt.getHostname()
            )
        ) {
            if ( logger.enabled ) {
                fctxt.filter = sessionSwitches.toLogData();
            }
            return 1;
        }
        return 0;
    }

    filterFont(fctxt) {
        if ( fctxt.itype === fctxt.FONT ) {
            this.remoteFontCount += 1;
        }
        if (
            sessionSwitches.evaluateZ(
                'no-remote-fonts',
                fctxt.getTabHostname()
            ) !== false
        ) {
            if ( logger.enabled ) {
                fctxt.filter = sessionSwitches.toLogData();
            }
            return 1;
        }
        return 0;
    }

    filterScripting(fctxt, netFiltering) {
        fctxt.filter = undefined;
        if ( netFiltering === undefined ) {
            netFiltering = this.getNetFilteringSwitch(fctxt);
        }
        if (
            netFiltering === false ||
            sessionSwitches.evaluateZ(
                'no-scripting',
                fctxt.getTabHostname()
            ) === false
        ) {
            return 0;
        }
        if ( logger.enabled ) {
            fctxt.filter = sessionSwitches.toLogData();
        }
        return 1;
    }

    // The caller is responsible to check whether filtering is enabled or not.
    filterLargeMediaElement(fctxt, size) {
        fctxt.filter = undefined;

        if ( this.allowLargeMediaElementsUntil === 0 ) {
            return 0;
        }
        // Disregard large media elements previously allowed: for example, to
        // seek inside a previously allowed audio/video.
        if (
            this.allowLargeMediaElementsRegex instanceof RegExp &&
            this.allowLargeMediaElementsRegex.test(fctxt.url)
        ) {
            return 0;
        }
        if ( Date.now() < this.allowLargeMediaElementsUntil ) {
            const sources = this.allowLargeMediaElementsRegex instanceof RegExp
                ? [ this.allowLargeMediaElementsRegex.source ]
                : [];
            sources.push('^' + µb.escapeRegex(fctxt.url));
            this.allowLargeMediaElementsRegex = new RegExp(sources.join('|'));
            return 0;
        }
        if (
            sessionSwitches.evaluateZ(
                'no-large-media',
                fctxt.getTabHostname()
            ) !== true
        ) {
            this.allowLargeMediaElementsUntil = 0;
            return 0;
        }
        if ( (size >>> 10) < µb.userSettings.largeMediaSize ) {
            return 0;
        }

        this.largeMediaCount += 1;
        if ( this.largeMediaTimer === null ) {
            this.largeMediaTimer = vAPI.setTimeout(( ) => {
                this.largeMediaTimer = null;
                this.injectLargeMediaElementScriptlet();
            }, 500);
        }

        if ( logger.enabled ) {
            fctxt.filter = sessionSwitches.toLogData();
        }

        return 1;
    }

    clickToLoad(frameId, frameURL) {
        let frameStore = this.getFrameStore(frameId);
        if ( frameStore === null ) {
            frameStore = this.setFrameURL({ frameId, url: frameURL });
        }
        this.netFilteringCache.forgetResult(
            this.tabHostname,
            'sub_frame',
            frameURL
        );
        frameStore.clickToLoad = true;
    }

    shouldExceptCname(fctxt) {
        let exceptCname;
        let frameStore;
        if ( fctxt.docId !== undefined ) {
            frameStore = this.getFrameStore(fctxt.docId);
            if ( frameStore instanceof Object ) {
                exceptCname = frameStore.exceptCname;
            }
        }
        if ( exceptCname === undefined ) {
            const result = staticNetFilteringEngine.matchRequestReverse(
                'cname',
                frameStore instanceof Object
                    ? frameStore.rawURL
                    : fctxt.getDocOrigin()
            );
            exceptCname = result === 2
                ? staticNetFilteringEngine.toLogData()
                : false;
            if ( frameStore instanceof Object ) {
                frameStore.exceptCname = exceptCname;
            }
        }
        if ( exceptCname === false ) { return false; }
        if ( exceptCname instanceof Object ) {
            fctxt.setFilter(exceptCname);
        }
        return true;
    }

    getBlockedResources(request, response) {
        const normalURL = µb.normalizeTabURL(this.tabId, request.frameURL);
        const resources = request.resources;
        const fctxt = µb.filteringContext;
        fctxt.fromTabId(this.tabId)
             .setDocOriginFromURL(normalURL);
        // Force some resources to go through the filtering engine in order to
        // populate the blocked-resources cache. This is required because for
        // some resources it's not possible to detect whether they were blocked
        // content script-side (i.e. `iframes` -- unlike `img`).
        if ( Array.isArray(resources) && resources.length !== 0 ) {
            for ( const resource of resources ) {
                this.filterRequest(
                    fctxt.setType(resource.type).setURL(resource.url)
                );
            }
        }
        if ( this.netFilteringCache.hash === response.hash ) { return; }
        response.hash = this.netFilteringCache.hash;
        response.blockedResources =
            this.netFilteringCache.lookupAllBlocked(fctxt.getDocHostname());
    }
};

PageStore.prototype.cacheableResults = new Set([
    µb.FilteringContext.SUB_FRAME,
]);

PageStore.prototype.collapsibleResources = new Set([
    µb.FilteringContext.IMAGE,
    µb.FilteringContext.MEDIA,
    µb.FilteringContext.OBJECT,
    µb.FilteringContext.SUB_FRAME,
]);

// To mitigate memory churning
PageStore.junkyard = [];
PageStore.junkyardMax = 10;

/******************************************************************************/

export { PageStore };


/*******************************************************************************

    uBlock Origin - a browser extension to block requests.
    Copyright (C) 2019-present Raymond Hill

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see {http://www.gnu.org/licenses/}.

    Home: https://github.com/gorhill/uBlock
*/

(function() {
    'use strict';
    let arg1 = '{{1}}';
    if ( arg1 === '{{1}}' ) { arg1 = ''; }
    let arg2 = '{{2}}';
    if ( arg2 === '{{2}}' ) { arg2 = ''; }
    let arg3 = '{{3}}';
    if ( arg3 === '{{3}}' ) { arg3 = ''; }
    const log = /\blog\b/.test(arg3)
        ? console.log.bind(console)
        : ( ) => { };
    const newSyntax = /^[01]?$/.test(arg1) === false;
    let pattern = '';
    let targetResult = true;
    let autoRemoveAfter = -1;
    if ( newSyntax ) {
        pattern = arg1;
        if ( pattern.startsWith('!') ) {
            targetResult = false;
            pattern = pattern.slice(1);
        }
        autoRemoveAfter = parseInt(arg2);
        if ( isNaN(autoRemoveAfter) ) {
            autoRemoveAfter = -1;
        }
    } else {
        pattern = arg2;
        if ( arg1 === '0' ) {
            targetResult = false;
        }
    }
    if ( pattern === '' ) {
        pattern = '.?';
    } else if ( /^\/.+\/$/.test(pattern) ) {
        pattern = pattern.slice(1,-1);
    } else {
        pattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }
    const rePattern = new RegExp(pattern);
    const createDecoy = function(tag, urlProp, url) {
        const decoy = document.createElement(tag);
        decoy[urlProp] = url;
        decoy.style.setProperty('height','1px', 'important');
        decoy.style.setProperty('position','fixed', 'important');
        decoy.style.setProperty('top','-1px', 'important');
        decoy.style.setProperty('width','1px', 'important');
        document.body.appendChild(decoy);
        setTimeout(( ) => decoy.remove(), autoRemoveAfter * 1000);
        return decoy;
    };
    window.open = new Proxy(window.open, {
        apply: function(target, thisArg, args) {
            log('window.open:', ...args);
            const url = args[0];
            if ( rePattern.test(url) !== targetResult ) {
                return target.apply(thisArg, args);
            }
            if ( autoRemoveAfter < 0 ) { return null; }
            const decoy = /\bobj\b/.test(arg3)
                ? createDecoy('object', 'data', url)
                : createDecoy('iframe', 'src', url);
            let popup = decoy.contentWindow;
            if ( typeof popup === 'object' && popup !== null ) {
                Object.defineProperty(popup, 'closed', { value: false });
            } else {
                const noopFunc = (function(){}).bind(self);
                popup = new Proxy(self, {
                    get: function(target, prop) {
                        if ( prop === 'closed' ) { return false; }
                        const r = Reflect.get(...arguments);
                        if ( typeof r === 'function' ) { return noopFunc; }
                        return target[prop];
                    },
                    set: function() {
                        return Reflect.set(...arguments);
                    },
                });
            }
            if ( /\blog\b/.test(arg3) ) {
                popup = new Proxy(popup, {
                    get: function(target, prop) {
                        log('window.open / get', prop, '===', target[prop]);
                        return Reflect.get(...arguments);
                    },
                    set: function(target, prop, value) {
                        log('window.open / set', prop, '=', value);
                        return Reflect.set(...arguments);
                    },
                });
            }
            return popup;
        }
    });
})();


:root.mobile {
--font-size: 16px;
--font-size-smaller: 14px;
}
/*
* Default color theme

body {
    display: flex;
    flex-direction: column;
    height: 100vh;
    overflow: hidden;
    width: 100vw;
    }
textarea {
    box-sizing: border-box;
    direction: ltr;
    resize: none;
    width: 100%;
    }
.permatoolbar {
    background-color: var(--surface-1);
    border: 0;
    box-sizing: border-box;
    display: flex;
    flex-shrink: 0;
    font-size: 120%;
    justify-content: space-between;
    margin: 0;
    padding: 0.25em;
    }
.permatoolbar > div {
    display: flex;
    }
.permatoolbar button.iconified {
    padding-left: var(--default-gap-xsmall);
    padding-right: var(--default-gap-xsmall);
    }
.permatoolbar button.active {
    fill: rgb(var(--primary-50));
    }
.permatoolbar button > .fa-icon {
    font-size: 180%;
    }
#pageSelector {
    min-width: 10em;
    padding: 0.25em 0;
    width: 50vw;
    }
#showpopup {
    display: inline-flex;
    align-items: center;
    }
#showpopup img {
    filter: grayscale(100%);
    height: auto;
    width: 1em;
    }
#info {
    fill: #ccc;
    }
#info:hover {
    fill: #000;
    }

/*
    https://github.com/gorhill/uBlock/issues/3293
    => https://devhints.io/css-system-font-stack
*/
#inspectors {
    flex-grow: 1;
    font-family: "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
    }
.inspector {
    border-top: 1px solid #ccc;
    display: flex;
    flex-direction: column;
    }
.vscrollable {
    direction: ltr;
    flex-grow: 1;
    font-size: small;
    overflow-x: hidden;
    overflow-y: auto;
}

.inspector:not(.vExpanded) button.vCompactToggler > .fa-icon {
    transform: scaleY(-1)
    }
.hCompact button.hCompactToggler > .fa-icon {
    transform: scaleX(-1)
    }

#inspectors.dom #netInspector {
    display: none;
    }

#netInspector #pause > .fa-icon[data-i18n-title="loggerUnpauseTip"] {
    display: none;
}
#netInspector.paused #pause > .fa-icon[data-i18n-title="loggerPauseTip"] {
    display: none;
}
#netInspector.paused #pause > .fa-icon[data-i18n-title="loggerUnpauseTip"] {
    display: inline-flex;
    fill: #5F9EA0;
}
#netInspector #filterExprGroup {
    display: flex;
    margin: 0 1em;
    position: relative;
    }
#netInspector #filterButton {
    opacity: 0.25;
    }
#netInspector.f #filterButton {
    opacity: 1;
    }
#netInspector #filterInput {
    border: 1px solid gray;
    display: inline-flex;
    }
#netInspector #filterInput > input {
    border: 0;
    min-width: 16em;
    }
#netInspector #filterExprButton {
    transform: scaleY(-1);
    }
#netInspector #filterExprButton:hover {
    background-color: transparent;
    }
#netInspector #filterExprButton.expanded {
    transform: scaleY(1);
    }
#netInspector #filterExprPicker {
    background-color: var(--surface-0);
    border: 1px solid gray;
    display: none;
    position: absolute;
    flex-direction: column;
    font-size: small;
    top: 100%;
    z-index: 100;
    }
body[dir="ltr"] #netInspector #filterExprPicker {
    right: 0;
    }
body[dir="rtl"] #netInspector #filterExprPicker {
    left: 0;
    }

#netInspector #filterExprGroup:hover #filterExprButton.expanded ~ #filterExprPicker {
    display: flex;
    }
#netInspector #filterExprPicker > div {
    border: 1px dotted #ddd;
    border-left: 0;
    border-right: 0;
    display: flex;
    padding: 0.5em;
    }
#netInspector #filterExprPicker > div:first-of-type {
    border-top: 0;
    }
#netInspector #filterExprPicker > div:last-of-type {
    border-bottom: 0;
    }
#netInspector #filterExprPicker div {
    display: flex;
    }
#netInspector #filterExprPicker span[data-filtex] {
    align-items: center;
    border: 1px solid transparent;
    cursor: pointer;
    display: inline-flex;
    margin: 0 0.5em 0 0;
    padding: 0.5em;
    white-space: nowrap;
    }
#netInspector #filterExprPicker span[data-filtex]:last-of-type {
    margin: 0;
    }
#netInspector #filterExprPicker span[data-filtex]:hover {
    background-color: rgb(var(--primary-70) / 25%);
    border: 1px solid rgb(var(--primary-70));
    }
#netInspector #filterExprPicker span.on[data-filtex] {
    background-color: rgb(var(--primary-70) / 40%);
    border: 1px solid rgb(var(--primary-70));
    }

#netInspector .vscrollable {
    overflow: hidden;
    }
#vwRenderer {
    box-sizing: border-box;
    height: 100%;
    overflow: hidden;
    position: relative;
    width: 100%;
    }
#vwRenderer #vwScroller {
    height: 100%;
    overflow-x: hidden;
    overflow-y: auto;
    position: absolute;
    width: 100%;
    }
#vwRenderer #vwScroller #vwVirtualContent {
    overflow: hidden;
    }
#vwRenderer #vwContent {
    left: 0;
    overflow: hidden;
    position: absolute;
    width: 100%;
    }
#vwRenderer .logEntry {
    display: block;
    left: 0;
    overflow: hidden;
    position: absolute;
    width: 100%;
    }
#vwRenderer .logEntry:empty {
    display: none;
    }
#vwRenderer .logEntry > div {
    height: 100%;
    white-space: nowrap;
    }
#vwRenderer .logEntry > div[data-status="1"],
#netFilteringDialog > .panes > .details > div[data-status="1"] {
    background-color: rgb(var(--popup-cell-block-surface-rgb) / 50%);
    }
#vwRenderer .logEntry > div[data-status="1"][data-modifier],
#netFilteringDialog > .panes > .details > div[data-status="1"][data-modifier] {
    background-color: var(--logger-modified-surface);
    }
#vwRenderer .logEntry > div[data-status="3"] {
    background-color: rgba(108, 108, 108, 0.1);
    }
:root.colorBlind #vwRenderer .logEntry > div[data-status="3"] {
    background-color: rgba(96, 96, 96, 0.1);
    }
#vwRenderer .logEntry > div[data-status="2"],
#netFilteringDialog > .panes > .details > div[data-status="2"] {
    background-color: rgb(var(--popup-cell-allow-surface-rgb) / 50%);
    }
#vwRenderer .logEntry > div[data-tabid="-1"] {
    text-shadow: 0 0.2em 0.4em #aaa;
    }
#vwRenderer .logEntry > div.extendedRealm,
#vwRenderer .logEntry > div.redirect {
    background-color: var(--logger-redirected-surface);
    }
:root.colorBlind #vwRenderer .logEntry > div.extendedRealm,
:root.colorBlind #vwRenderer .logEntry > div.redirect {
    background-color: rgba(0, 19, 110, 0.1);
    }
#vwRenderer .logEntry > div[data-aliasid] {
    color: var(--popup-cell-cname-ink);
    }
#vwRenderer .logEntry > div[data-type="tabLoad"] {
    background-color: #666;
    color: white;
    }
#vwRenderer .logEntry > div[data-type="error"] {
    color: #800;
    }
#vwRenderer .logEntry > div[data-type="info"] {
    color: #008;
    }
#vwRenderer .logEntry > div.voided {
    opacity: 0.3;
    }
#vwRenderer .logEntry > div.voided:hover {
    opacity: 0.7;
    }

#vwRenderer .logEntry > div > span {
    border: 1px dotted var(--border-1);
    border-top: 0;
    border-right: 0;
    box-sizing: border-box;
    display: inline-block;
    height: 100%;
    overflow: hidden;
    padding: 0.2em;
    vertical-align: middle;
    white-space: nowrap;
    word-break: break-all;
    }
#vwRenderer .logEntry > div.canDetails:hover > span {
    background-color: rgba(0,0,0,0.04);
    }
body[dir="ltr"] #vwRenderer .logEntry > div > span:first-child {
    border-left: 0;
    }
body[dir="rtl"] #vwRenderer .logEntry > div > span:first-child {
    border-right: 0;
    }
#vwRenderer .logEntry > div > span:nth-of-type(1) {
    }
#vwRenderer .logEntry > div > span:nth-of-type(2) {
    }
#vwRenderer #vwContent .logEntry > div > span:nth-of-type(2) {
    text-overflow: ellipsis;
    }
.vExpanded #vwRenderer #vwContent .logEntry > div > span:nth-of-type(2) {
    overflow-y: auto;
    white-space: pre-line;
    }
#vwRenderer .logEntry > div.messageRealm[data-type="tabLoad"] > span:nth-of-type(2) {
    text-align: center;
    }
#vwRenderer .logEntry > div.extendedRealm > span:nth-of-type(2) > span:first-of-type {
    display: none;
    }
#vwRenderer .logEntry > div.extendedRealm > span:nth-of-type(2) > span:last-of-type {
    pointer-events: none;
    }
#vwRenderer .logEntry > div.extendedRealm.isException > span:nth-of-type(2) > span:last-of-type {
    text-decoration: line-through rgba(0,0,255,0.7);
    }
#vwRenderer .logEntry > div > span:nth-of-type(3) {
    font: 12px monospace;
    padding-left: 0.3em;
    padding-right: 0.3em;
    text-align: center;
    }
#vwRenderer .logEntry > div.canDetails:hover > span:nth-of-type(2),
#vwRenderer .logEntry > div.canDetails:hover > span:nth-of-type(3),
#vwRenderer .logEntry > div.canDetails:hover > span:nth-of-type(5) {
    background: rgba(0, 0, 255, 0.1);
    cursor: zoom-in;
    }
#netInspector:not(.vExpanded) #vwRenderer .logEntry > div > span:nth-of-type(4) {
    direction: rtl;
    text-align: right;
    unicode-bidi: plaintext;
    }
#vwRenderer #vwContent .logEntry > div > span:nth-of-type(4) {
    text-overflow: ellipsis;
    }
.vExpanded #vwRenderer #vwContent .logEntry > div > span:nth-of-type(4) {
    overflow-y: auto;
    text-overflow: clip;
    white-space: pre-line;
    }
#vwRenderer .logEntry > div > span:nth-of-type(5) {
    text-align: center;
    }
/* visual for tabless network requests */
#vwRenderer .logEntry > div > span:nth-of-type(5) {
    position: relative;
    }
#vwRenderer .logEntry > div > span:nth-of-type(6) {
    }
#vwRenderer #vwContent .logEntry > div > span:nth-of-type(6) {
    }
#vwRenderer .logEntry > div > span:nth-of-type(7) {
    position: relative;
    }
#vwRenderer #vwContent .logEntry > div > span:nth-of-type(7) {
    text-overflow: ellipsis;
    }
.vExpanded #vwRenderer #vwContent .logEntry > div > span:nth-of-type(7) {
    overflow-y: auto;
    white-space: pre-line;
    }
#vwRenderer .logEntry > div > span:nth-of-type(7) b {
    font-weight: bold;
    }
#vwRenderer .logEntry > div[data-status="1"] > span:nth-of-type(7) b,
#netFilteringDialog > .panes > .details > div[data-status="1"] b {
    background-color: rgb(var(--popup-cell-block-surface-rgb) / 100%);
    }
#vwRenderer .logEntry > div[data-status="1"][data-modifier] > span:nth-of-type(7) b,
#netFilteringDialog > .panes > .details > div[data-status="1"][data-modifier] b {
    background-color: var(--logger-modified-em-surface);
    }
#vwRenderer .logEntry > div[data-status="3"] > span:nth-of-type(7) b {
    background-color: rgba(108, 108, 108, 0.2);
    }
:root.colorBlind #vwRenderer .logEntry > div[data-status="3"] > span:nth-of-type(7) b {
    background-color: rgba(96, 96, 96, 0.2);
    }
#vwRenderer .logEntry > div[data-status="2"] > span:nth-of-type(7) b,
#netFilteringDialog > .panes > .details > div[data-status="2"] b {
    background-color: rgb(var(--popup-cell-allow-surface-rgb) / 100%);
    }
#vwRenderer .logEntry > div > span:nth-of-type(7) a {
    background-color: dimgray;
    color: white;
    display: none;
    height: 100%;
    padding: 0 0.25em;
    opacity: 0.4;
    position: absolute;
    right: 0;
    text-decoration: none;
    top: 0;
    }
#netInspector.vExpanded #vwRenderer .logEntry > div > span:nth-of-type(7) a {
    bottom: 0px;
    height: unset;
    padding: 0.25em;
    top: unset;
    }
#vwRenderer .logEntry > div > span:nth-of-type(7) a::after {
    content: '\2197';
    }
#vwRenderer .logEntry > div.networkRealm > span:nth-of-type(7):hover a {
    align-items: center;
    display: inline-flex;
    }
#vwRenderer .logEntry > div > span:nth-of-type(7) a:hover {
    opacity: 1;
    }

#vwRenderer #vwBottom {
    background-color: #00F;
    height: 0;
    overflow: hidden;
    width: 100%;
    }
#vwRenderer #vwLineSizer {
    left: 0;
    pointer-events: none;
    position: absolute;
    top: 0;
    visibility: hidden;
    width: 100%;
    }

#popupContainer {
    background-color: var(--surface-1);
    border: 1px solid gray;
    bottom: 0;
    display: none;
    max-height: 70vh;
    min-width: 360px;
    overflow: hidden;
    position: fixed;
    right: 0;
    z-index: 200;
    }
#inspectors.popupOn #popupContainer {
    display: block;
    }

#modalOverlay {
    align-items: center;
    background-color: rgba(0, 0, 0, 0.5);
    border: 0;
    bottom: 0;
    display: none;
    justify-content: center;
    left: 0;
    margin: 0;
    position: fixed;
    right: 0;
    top: 0;
    z-index: 400;
    }
#modalOverlay.on {
    display: flex;
    }
#modalOverlay > div {
    position: relative;
    }
#modalOverlay > div > div:nth-of-type(1) {
    background-color: var(--surface-1);
    border: 0;
    box-sizing: border-box;
    padding: 1em;
    max-height: 90vh;
    overflow-y: auto;
    width: 90vw;
    }
#modalOverlay > div > div:nth-of-type(2) {
    stroke: var(--ink-1);
    stroke-width: 3px;
    position: absolute;
    width: 1.6em;
    height: 1.6em;
    bottom: calc(100% + 2px);
    background-color: var(--surface-1);
    }
body[dir="ltr"] #modalOverlay > div > div:nth-of-type(2) {
    right: 0;
    }
body[dir="rtl"] #modalOverlay > div > div:nth-of-type(2) {
    left: 0;
    }
#modalOverlay > div > div:nth-of-type(2):hover {
    background-color: var(--surface-2);
    }
#modalOverlay > div > div:nth-of-type(2) > * {
    pointer-events: none;
    }

#netFilteringDialog {
    font-size: 95%;
    }
#netFilteringDialog a {
    text-decoration: none;
    }
#netFilteringDialog select {
    max-width: 75%;
    outline: none;
}
#netFilteringDialog > .preview {
    align-items: center;
    /* http://lea.verou.me/css3patterns/ */
    background-color: #aaa;
    background-image:
        linear-gradient(
            45deg,
            #666 25%,
            transparent 25%,
            transparent 75%,
            #666 75%,
            #666
        ),
        linear-gradient(
            45deg,
            #666 25%,
            transparent 25%,
            transparent 75%,
            #666 75%,
            #666
        );
    background-position:0 0, 9px 9px;
    background-size: 18px 18px;
    display: flex;
    justify-content: center;
    margin-bottom: 1em;
    padding: 0.5em;
    text-align: center;
    }
#netFilteringDialog > .preview > * {
    max-width: 100%;
    max-height: 20vh;
    }
#netFilteringDialog > .preview > span {
    background-color: var(--surface-3);
    cursor: pointer;
    padding: 1em;
    }

#netFilteringDialog > .headers {
    border-bottom: 1px solid var(--border-4);
    line-height: 2;
    position: relative;
    }
#netFilteringDialog > .headers > .header {
    border: 1px solid var(--border-2);
    border-bottom: 1px solid var(--border-4);
    border-top-left-radius: 4px;
    border-top-right-radius: 4px;
    color: var(--border-2);
    cursor: pointer;
    display: inline-block;
    padding: 0 1em;
    position: relative;
    text-align: center;
    top: 1px;
    }
#netFilteringDialog[data-pane="details"] > .headers > [data-pane="details"],
#netFilteringDialog[data-pane="dynamic"] > .headers > [data-pane="dynamic"],
#netFilteringDialog[data-pane="static"] > .headers > [data-pane="static"] {
    background-color: var(--surface-1);
    border-color: var(--border-4);
    border-bottom: 1px solid transparent;
    color: var(--ink-1);
    }
#netFilteringDialog > .headers > .tools {
    bottom: 0;
    display: flex;
    position: absolute;
    }
body[dir="ltr"] #netFilteringDialog > .headers > .tools {
    right: 0;
    }
body[dir="rtl"] #netFilteringDialog > .headers > .tools {
    left: 0;
    }
#netFilteringDialog > .headers > .tools > span {
    cursor: pointer;
    font-size: 1.5em;
    padding: 0 0.25em;
    text-align: center;
    }
#netFilteringDialog > .headers > .tools > span:hover {
    background-color: #eee;
    }
#netFilteringDialog.extendedRealm > .headers > .dynamic,
#netFilteringDialog.extendedRealm > .panes > .dynamic {
    display: none;
    }
#netFilteringDialog.extendedRealm > .headers > .static,
#netFilteringDialog.extendedRealm > .panes > .static {
    display: none;
    }
#netFilteringDialog > div.panes {
    min-height: 40vh;
    overflow: hidden;
    overflow-y: auto;
    padding-top: 1em;
    }
#netFilteringDialog > div.panes > div {
    display: none;
    }
#netFilteringDialog[data-pane="details"] > .panes > [data-pane="details"],
#netFilteringDialog[data-pane="dynamic"] > .panes > [data-pane="dynamic"],
#netFilteringDialog[data-pane="static"] > .panes > [data-pane="static"] {
    display: flex;
    flex-direction: column;
    }
#netFilteringDialog > .panes > .details > div {
    align-items: stretch;
    background-color: var(--surface-2);
    border: 0;
    border-bottom: 1px solid var(--surface-0);
    display: flex;
    }
#netFilteringDialog > .panes > .details > div > span {
    padding: 0.5em;
    }
#netFilteringDialog > .panes > .details > div > span:nth-of-type(1) {
    border: 0;
    flex-grow: 0;
    flex-shrink: 0;
    text-align: right;
    width: 8em;
    }
body[dir="ltr"] #netFilteringDialog > .panes > .details > div > span:nth-of-type(1) {
    border-right: 1px solid var(--surface-0);
    }
body[dir="rtl"] #netFilteringDialog > .panes > .details > div > span:nth-of-type(1) {
    border-left: 1px solid var(--surface-0);
    }
#netFilteringDialog > .panes > .details > div > span:nth-of-type(2) {
    flex-grow: 1;
    max-height: 20vh;
    overflow: hidden auto;
    white-space: pre-line
    }
#netFilteringDialog > .panes > .details > div > span:nth-of-type(2):not(.prose) {
    word-break: break-all;
    }
#netFilteringDialog > .panes > .details > div > span:nth-of-type(2) .fa-icon {
    font-size: 110%;
    opacity: 0.5;
    vertical-align: bottom;
    }
#netFilteringDialog > .panes > .details > div > span:nth-of-type(2) .fa-icon:hover {
    opacity: 1;
    }
#netFilteringDialog > .panes > .details .exceptor {
    align-items: center;
    border-left: 1px solid var(--surface-0);
    cursor: pointer;
    display: inline-flex;
    font-family: monospace;
    opacity: 0.8;
    }
#netFilteringDialog > .panes > .details .exceptor:hover {
    opacity: 1;
    }
#netFilteringDialog > .panes > .details .exceptored .filter {
    text-decoration: line-through;
    }
#netFilteringDialog > .panes > .details .exceptored .exceptor {
    background-color: rgb(var(--primary-50) / 50%);
    }
#netFilteringDialog > .panes > .details .exceptor::before {
    content: '#@#';
    }
#netFilteringDialog > div.panes > .dynamic > .toolbar {
    padding-bottom: 1em;
    }
#netFilteringDialog > div.panes > .dynamic .row {
    display: flex;
    min-height: 2.2em;
    }
#netFilteringDialog > div.panes > .dynamic .row > span:nth-of-type(1) {
    align-self: stretch;
    border: 0;
    display: inline-flex;
    flex-grow: 0;
    flex-shrink: 0;
    text-align: center;
    width: 4.5em;
    }
body[dir="ltr"] #netFilteringDialog > div.panes > .dynamic .row > span:nth-of-type(1) {
    border-right: 1px solid var(--surface-0);
    }
body[dir="rtl"] #netFilteringDialog > div.panes > .dynamic .row > span:nth-of-type(1) {
    border-left: 1px solid var(--surface-0);
    }
#netFilteringDialog > div.panes > .dynamic .row > span:nth-of-type(2) {
    align-self: center;
    padding: 0 0.5em;
    }
#netFilteringDialog > div.panes > .dynamic > .toolbar #saveRules {
    background-color: #ffe;
    border: 1px solid #ddc;
    border-radius: 4px;
    fill: #888;
    cursor: pointer;
    font-size: 2em;
    visibility: hidden;
    width: 100%;
    }
body.dirty #netFilteringDialog > div.panes > .dynamic > .toolbar #saveRules {
    visibility: visible;
    }
#netFilteringDialog > div.panes > .dynamic > .toolbar #saveRules:hover {
    fill: black;
    }
#netFilteringDialog > div.panes > .dynamic > .toolbar .entry {
    display: none;
    }
#netFilteringDialog > div.panes > .dynamic .entry {
    background-color: var(--surface-2);
    border: 0;
    border-bottom: 1px solid var(--surface-0);
    }
#netFilteringDialog > div.panes > .dynamic .entry:hover {
    background-color: var(--surface-3);
    }
#netFilteringDialog > div.panes > .dynamic .entry > .action {
    background-color: transparent;
    border: 0;
    cursor: pointer;
    }
#netFilteringDialog > div.panes > .dynamic .entry > .action > span {
    background-color: transparent;
    border: 0;
    display: inline-block;
    height: 100%;
    opacity: 0.2;
    visibility: hidden;
    width: 33.33%;
    }
#netFilteringDialog > div.panes > .dynamic .entry > .action.allow {
    background-color: rgba(0, 160, 0, 0.3);
    }
:root.colorBlind #netFilteringDialog > div.panes > .dynamic .entry > .action.allow {
    background-color: rgba(255, 194, 57, 0.4);
    }
#netFilteringDialog > div.panes > .dynamic .entry > .action.noop {
    background-color: rgba(108, 108, 108, 0.3);
    }
:root.colorBlind #netFilteringDialog > div.panes > .dynamic .entry > .action.noop {
    background-color: rgba(96, 96, 96, 0.4);
    }
#netFilteringDialog > div.panes > .dynamic .entry > .action.block {
    background-color: rgba(192, 0, 0, 0.3);
    }
:root.colorBlind #netFilteringDialog > div.panes > .dynamic .entry > .action.block {
    background-color: rgba(0, 19, 110, 0.4);
    }
#netFilteringDialog > div.panes > .dynamic .entry > .action.own.allow {
    background-color: rgba(0, 160, 0, 1);
    }
:root.colorBlind #netFilteringDialog > div.panes > .dynamic .entry > .action.own.allow {
    background-color: rgba(255, 194, 57, 1);
    }
#netFilteringDialog > div.panes > .dynamic .entry > .action.own.noop,
:root.colorBlind #netFilteringDialog > div.panes > .dynamic .entry > .action.own.noop {
    background-color: rgba(108, 108, 108, 1);
    }
#netFilteringDialog > div.panes > .dynamic .entry > .action.own.block {
    background-color: rgba(192, 0, 0, 1);
    }
:root.colorBlind #netFilteringDialog > div.panes > .dynamic .entry > .action.own.block {
    background-color: rgba(0, 19, 110, 1);
    }
#netFilteringDialog > div.panes > .dynamic .entry > .action:not(.own):hover > span {
    opacity: 0.2;
    visibility: visible;
    }
#netFilteringDialog > div.panes > .dynamic .entry > .action:not(.own):hover > span:hover {
    opacity: 0.75;
    }
#netFilteringDialog > div.panes > .dynamic .entry > .action > .allow {
    background-color: rgb(0, 160, 0);
    }
:root.colorBlind  #netFilteringDialog > div.panes > .dynamic .entry > .action > .allow {
    background-color: rgb(255, 194, 57);
    }
#netFilteringDialog > div.panes > .dynamic .entry > .action > .noop {
    background-color: rgb(108, 108, 108);
    }
#netFilteringDialog > div.panes > .dynamic .entry > .action > .block {
    background-color: rgb(192, 0, 0);
    }
:root.colorBlind  #netFilteringDialog > div.panes > .dynamic .entry > .action > .block {
    background-color: rgb(0, 19, 110);
    }
#netFilteringDialog > div.panes > .dynamic .entry > .url {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    }
#netFilteringDialog > div.panes > div.static > div {
    line-height: 2;
    }
#netFilteringDialog > div.panes > div.static > div {
    padding-bottom: 1em;
    }
#netFilteringDialog > div.panes > div.static textarea {
    height: 6em;
    max-height: 20vh;
    min-height: 10vh;
    word-break: break-all;
    }
#netFilteringDialog > div.panes > div.static > div:nth-of-type(2) {
    text-align: center;
    }

#filterFinderDialog {
    word-break: break-all;
    }
#filterFinderDialog code {
    background: #eee;
    font-size: 85%;
    padding: 3px;
    unicode-bidi: plaintext;
    white-space: pre-wrap;
    }
#filterFinderDialog ul {
    font-size: larger;
    }
#filterFinderDialog .filterFinderListEntry {
    align-items: flex-end;
    display: flex;
    }
#filterFinderDialog .filterFinderListEntry a {
    text-decoration: none;
    }
#filterFinderDialog .filterFinderListEntry a.fa-icon {
    margin: 0 0.5em;
    opacity: 0.6;
    }
#filterFinderDialog .filterFinderListEntry a.fa-icon:hover {
    opacity: 1;
    }
#filterFinderDialog .filterFinderListEntry a.fa-icon[href=""] {
    display: none;
    }
#filterFinderDialog > *:first-child {
    margin-top: 0;
    }
#filterFinderDialog > *:last-child {
    margin-bottom: 0;
    }

#loggerStatsDialog .sortedEntries {
    display: flex;
    flex-direction: column;
    font-size: smaller;
    }
#loggerStatsDialog .sortedEntries > div {
    display: flex;
    }
#loggerStatsDialog .sortedEntries > div > span:first-of-type {
    flex-grow: 0;
    flex-shrink: 0;
    padding: 0 2em 0 0;
    text-align: right;
    width: 3em;
    }
#loggerStatsDialog .sortedEntries > div > span:last-of-type {
    flex-grow: 1;
    flex-shrink: 1;
    white-space: pre;
    }

#loggerExportDialog {
    display: flex;
    flex-direction: column;
    }
#loggerExportDialog .options {
    display: flex;
    justify-content: space-between;
    margin-bottom: 1em;
    }
#loggerExportDialog .options > div {
    display: inline-flex;
    }
#loggerExportDialog .options span[data-i18n] {
    border: 1px solid rgb(var(--primary-70));
    cursor: pointer;
    font-size: 90%;
    margin: 0;
    padding: 0.5em;
    white-space: nowrap;
    }
#loggerExportDialog .options span[data-i18n]:hover {
    background-color: rgb(var(--primary-70) / 40%);
    }
#loggerExportDialog .options span.on[data-i18n],
#loggerExportDialog .options span.pushbutton:active {
    background-color: rgb(var(--primary-70) / 40%);
    }
#loggerExportDialog .output {
    font: smaller mono;
    height: 60vh;
    padding: 0.5em;
    white-space: pre;
    }

#loggerSettingsDialog {
    display: flex;
    flex-direction: column;
    }
#loggerSettingsDialog > div {
    padding-bottom: 1em;
    }
#loggerSettingsDialog > div:last-of-type {
    padding-bottom: 0;
    }
#loggerSettingsDialog ul {
    padding: 0;
    }
body[dir="ltr"] #loggerSettingsDialog ul {
    padding-left: 2em;
    }
body[dir="rtl"] #loggerSettingsDialog ul {
    padding-right: 2em;
    }
#loggerSettingsDialog li {
    list-style-type: none;
    margin: 0.5em 0 0 0;
    }
#loggerSettingsDialog input {
    max-width: 6em;
    }

.hide {
    display: none !important;
    }


An efficient blocker: easy on memory and CPU footprint, and yet can load and enforce thousands more filters than other popular blockers out there.

Illustrated overview of its efficiency: https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared

Usage: The big power button in the popup is to permanently disable/enable uBlock for the current web site. It applies to the current web site only, it is not a global power button.

***

Flexible, it's more than an "ad blocker": it can also read and create filters from hosts files.

Out of the box, these lists of filters are loaded and enforced:

- EasyList
- Peter Lowe’s Ad server list
- EasyPrivacy
- Malware domains

More lists are available for you to select if you wish:

- Fanboy’s Enhanced Tracking List
- Dan Pollock’s hosts file
- hpHosts’s Ad and tracking servers
- MVPS HOSTS
- Spam404
- And many others

Of course, the more filters enabled, the higher the memory footprint. Yet, even after adding Fanboy's two extra lists, hpHosts’s Ad and tracking servers, uBlock still has a lower memory footprint than other very popular blockers out there.

Also, be aware that selecting some of these extra lists may lead to higher likelihood of web site breakage -- especially those lists which are normally used as hosts file.

***

Without the preset lists of filters, this extension is nothing. So if ever you really do want to contribute something, think about the people working hard to maintain the filter lists you are using, which were made available to use by all for free.

***

Free.
Open source with public license (GPLv3)
For users by users.

Contributors @ Github: https://github.com/gorhill/uBlock/graphs/contributors
Contributors @ Crowdin: https://crowdin.net/project/ublock

***

It's quite an early version, keep this in mind when you review.

Project change log:
https://github.com/gorhill/uBlock/releases


#!/usr/bin/env python3

import os
import json
import re
import sys

if len(sys.argv) == 1 or not sys.argv[1]:
    raise SystemExit('Build dir missing.')

proj_dir = os.path.join(os.path.split(os.path.abspath(__file__))[0], '..')
build_dir = os.path.abspath(sys.argv[1])

version = ''
with open(os.path.join(proj_dir, 'dist', 'version')) as f:
    version = f.read().strip()

manifest_out = {}
manifest_out_file = os.path.join(build_dir, 'manifest.json')
with open(manifest_out_file) as f:
    manifest_out = json.load(f)

manifest_out['version'] = version

# Development build? If so, modify name accordingly.
match = re.search('^\d+\.\d+\.\d+\.\d+$', version)
if match:
    manifest_out['name'] += ' development build'
    manifest_out['short_name'] += ' dev build'
    manifest_out['browser_action']['default_title'] += ' dev build'

with open(manifest_out_file, 'w') as f:
    json.dump(manifest_out, f, indent=2, separators=(',', ': '), sort_keys=True)
    f.write('\n')