From ed5d63df69b7fa044a4d9f649916bd2e9116bd82 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sat, 12 Jan 2019 16:36:20 -0500 Subject: [PATCH] Grand refactoring of the logger Performance-related work: the logger data has been decoupled from the DOM -- inspired from CodeMirror's way of efficiently handling large amout of text data. This decoupling now makes the logger highly efficient CPU- and memory-wise, and open the way to more possibilities. Ability to configure some aspect of the logger behavior and visuals: - The hard-coded limit of 5000 entries has been removed and is now replaced with a variety of user-configurable settings to enforce the discarding of logger entries. - Some columns in the logger output can now be hidden. The filter list look-up feature has been merged into the existing overlay dialog used to create URL rules or static filters, as an entry in a new "Details" pane. Other issues addressed during refactoring: - https://github.com/uBlockOrigin/uBlock-issues/issues/280 - https://github.com/gorhill/uBlock/issues/1999 The minimum version supported on Firefox has been bumped up to 55.0. --- platform/firefox/manifest.json | 2 +- src/_locales/en/messages.json | 90 +- src/css/fa-icons.css | 1 + src/css/logger-ui-inspector.css | 4 +- src/css/logger-ui.css | 690 ++++--- src/img/fontawesome/fontawesome-defs.svg | 1 + src/js/asset-viewer.js | 2 +- src/js/assets.js | 4 +- src/js/commands.js | 2 +- src/js/cosmetic-filtering.js | 12 +- src/js/dynamic-net-filtering.js | 18 +- src/js/fa-icons.js | 5 +- src/js/html-filtering.js | 4 +- src/js/i18n.js | 129 +- src/js/logger-ui-inspector.js | 95 +- src/js/logger-ui.js | 2251 ++++++++++++++-------- src/js/logger.js | 22 +- src/js/messaging.js | 2 +- src/js/pagestore.js | 2 +- src/js/popup.js | 2 +- src/js/static-net-filtering.js | 4 +- src/js/tab.js | 2 +- src/js/traffic.js | 22 +- src/js/udom.js | 31 +- src/logger-ui.html | 157 +- src/popup.html | 2 +- src/settings.html | 2 +- 27 files changed, 2218 insertions(+), 1340 deletions(-) diff --git a/platform/firefox/manifest.json b/platform/firefox/manifest.json index 3dc78c22e..ace1bfca9 100644 --- a/platform/firefox/manifest.json +++ b/platform/firefox/manifest.json @@ -2,7 +2,7 @@ "applications": { "gecko": { "id": "uBlock0@raymondhill.net", - "strict_min_version": "52.0" + "strict_min_version": "55.0" } }, "author": "All uBlock Origin contributors", diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 23909cb5f..9d57757eb 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -300,7 +300,7 @@ "description": "" }, "settingsNoLargeMediaPrompt":{ - "message":"Block media elements larger than {{input:number}} kB", + "message":"Block media elements larger than {{input}} kB", "description": "" }, "settingsNoRemoteFontsPrompt":{ @@ -607,9 +607,45 @@ "message":"3rd-party", "description":"A keyword in the built-in row filtering expression" }, - "logMaxEntriesTip":{ - "message":"Maximum number of logger entries", - "description":"Tooltip informaing that the input field is to set the maximum number of entries in the log" + "loggerEntryDetailsHeader":{ + "message":"Details", + "description":"Small header to identify the 'Details' pane for a specific logger entry" + }, + "loggerEntryDetailsFilter":{ + "message":"Filter", + "description":"Label to identify a filter field" + }, + "loggerEntryDetailsFilterList":{ + "message":"Filter list", + "description":"Label to identify a filter list field" + }, + "loggerEntryDetailsRule":{ + "message":"Rule", + "description":"Label to identify a rule field" + }, + "loggerEntryDetailsContext":{ + "message":"Context", + "description":"Label to identify a context field (typically a hostname)" + }, + "loggerEntryDetailsRootContext":{ + "message":"Root context", + "description":"Label to identify a root context field (typically a hostname)" + }, + "loggerEntryDetailsPartyness":{ + "message":"Partyness", + "description":"Label to identify a field providing partyness information" + }, + "loggerEntryDetailsType":{ + "message":"Type", + "description":"Label to identify the type of an entry" + }, + "loggerEntryDetailsURL":{ + "message":"URL", + "description":"Label to identify the URL of an entry" + }, + "loggerURLFilteringHeader":{ + "message":"URL rule", + "description":"Small header to identify the dynamic URL filtering section" }, "loggerURLFilteringContextLabel":{ "message":"Context:", @@ -619,12 +655,8 @@ "message":"Type:", "description":"Label for the type selector" }, - "loggerURLFilteringHeader":{ - "message":"Dynamic URL filtering", - "description":"Small header to identify the dynamic URL filtering section" - }, "loggerStaticFilteringHeader":{ - "message":"Static filtering", + "message":"Filter", "description":"Small header to identify the static filtering section" }, "loggerStaticFilteringSentence":{ @@ -671,6 +703,46 @@ "message":"Static filter {{filter}} could not be found in any of the currently enabled filter lists", "description":"Message to show when a filter cannot be found in any filter lists" }, + "loggerSettingDiscardPrompt":{ + "message":"Logger entries which do not fulfill all three conditions below will be automatically discarded:", + "description":"Logger setting: A sentence to describe the purpose of the settings below" + }, + "loggerSettingPerEntryMaxAge":{ + "message":"Preserve entries from the last {{input}} minutes", + "description":"A logger setting" + }, + "loggerSettingPerTabMaxLoads":{ + "message":"Preserve at most {{input}} page loads per tab", + "description":"A logger setting" + }, + "loggerSettingPerTabMaxEntries":{ + "message":"Preserve at most {{input}} entries per tab", + "description":"A logger setting" + }, + "loggerSettingPerEntryLineCount":{ + "message":"Use {{input}} lines per entry in vertically expanded mode", + "description":"A logger setting" + }, + "loggerSettingHideColumnsPrompt":{ + "message":"Hide columns:", + "description":"Logger settings: a sentence to describe the purpose of the checkboxes below" + }, + "loggerSettingHideColumnTime":{ + "message":"{{input}} Time", + "description":"A label for the time column" + }, + "loggerSettingHideColumnFilter":{ + "message":"{{input}} Filter/rule", + "description":"A label for the filter or rule column" + }, + "loggerSettingHideColumnContext":{ + "message":"{{input}} Context", + "description":"A label for the context column" + }, + "loggerSettingHideColumnPartyness":{ + "message":"{{input}} Partyness", + "description":"A label for the partyness column" + }, "aboutChangelog":{ "message":"Changelog", "description":"" diff --git a/src/css/fa-icons.css b/src/css/fa-icons.css index aca86a1f8..907dcadd8 100644 --- a/src/css/fa-icons.css +++ b/src/css/fa-icons.css @@ -63,6 +63,7 @@ .fa-icon > .fa-icon_home { width: calc(1em * 1612 / 1792); } +.fa-icon > .fa-icon_cog, .fa-icon > .fa-icon_floppy-o, .fa-icon > .fa-icon_info-circle, .fa-icon > .fa-icon_pause-circle-o, diff --git a/src/css/logger-ui-inspector.css b/src/css/logger-ui-inspector.css index f3afd47a6..2055f6bea 100644 --- a/src/css/logger-ui-inspector.css +++ b/src/css/logger-ui-inspector.css @@ -85,13 +85,13 @@ display: none; } -#domInspector.vCompact li:not(.hasCosmeticHide):not(.isCosmeticHide) { +#domInspector:not(.vExpanded) li:not(.hasCosmeticHide):not(.isCosmeticHide) { display: none; } #domInspector #domTree > li { display: block; } -#domInspector.vCompact ul { +#domInspector:not(.vExpanded) ul { display: block; } #domInspector li > ul > li:not(.hasCosmeticHide):not(.isCosmeticHide) { diff --git a/src/css/logger-ui.css b/src/css/logger-ui.css index 15adb86b4..b87c8dd87 100644 --- a/src/css/logger-ui.css +++ b/src/css/logger-ui.css @@ -38,10 +38,16 @@ textarea { background-color: #eee; } #pageSelector { - margin-right: 1em; padding: 0.25em 0; width: 28em; } +body[dir="ltr"] #pageSelector { + margin-right: 1em; + } +body[dir="rtl"] #pageSelector { + margin-left: 1em; + } + #showpopup { display: inline-flex; align-items: center; @@ -56,11 +62,16 @@ textarea { padding-left: 0.5em; padding-right: 0.5em; position: absolute; - right: 0; } #info:hover { fill: #000; } +body[dir="ltr"] #info { + right: 0; + } +body[dir="rtl"] #info { + left: 0; + } @media (max-width: 600px) { #info { display: none; @@ -81,13 +92,14 @@ textarea { flex-direction: column; } .vscrollable { + direction: ltr; flex-grow: 1; font-size: small; overflow-x: hidden; overflow-y: auto; } -.vCompact .vCompactToggler.button { +.inspector:not(.vExpanded) .vCompactToggler.button { transform: scaleY(-1) } .hCompact .hCompactToggler.button { @@ -125,14 +137,16 @@ textarea { #netInspector #filterInput > input { min-width: 18em; } -#netInspector #maxEntries { - margin: 0 2em; - } #netInspector #filterExprButton { position: absolute; - right: 0; transform: scaleY(-1); } +body[dir="ltr"] #netInspector #filterExprButton { + right: 0; + } +body[dir="rtl"] #netInspector #filterExprButton { + left: 0; + } #netInspector #filterExprButton:hover { background-color: transparent; } @@ -146,10 +160,16 @@ textarea { position: absolute; flex-direction: column; font-size: small; - right: 0; 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; } @@ -184,145 +204,169 @@ textarea { background-color: lightblue; border: 1px solid lightblue; } +#netInspector #settings { + padding-left: 0.5em; + padding-right: 0.5em; + position: absolute; +} +body[dir="ltr"] #netInspector #settings { + right: 0; + } +body[dir="rtl"] #netInspector #settings { + left: 0; + } -#netInspector table { - border: 0; - border-collapse: collapse; - direction: ltr; - table-layout: fixed; +#netInspector .vscrollable { + overflow: hidden; + } +#vwRenderer { + box-sizing: border-box; + height: 100%; + overflow: hidden; + position: relative; width: 100%; } -#netInspector table > colgroup > col:nth-of-type(1) { - width: 4.6em; +#vwRenderer #vwScroller { + height: 100%; + overflow-x: hidden; + overflow-y: auto; + position: absolute; + width: 100%; } -#netInspector table > colgroup > col:nth-of-type(2) { - width: 16%; +#vwRenderer #vwScroller #vwVirtualContent { + overflow: hidden; } -#netInspector table > colgroup > col:nth-of-type(3) { - width: 2.1em; +#vwRenderer #vwContent { + left: 0; + overflow: hidden; + position: absolute; + width: 100%; } -#netInspector table > colgroup > col:nth-of-type(4) { - width: 20%; +#vwRenderer .logEntry { + display: block; + left: 0; + overflow: hidden; + position: absolute; + width: 100%; } -#netInspector table > colgroup > col:nth-of-type(5) { - width: 2.4em; - } -#netInspector table > colgroup > col:nth-of-type(6) { - width: 6em; - } -#netInspector table > colgroup > col:nth-of-type(7) { - width: calc(100% - 4.6em - 16% - 2.1em - 20% - 2.4em - 6em); - } -#netInspector.f table tr.f { +#vwRenderer .logEntry:empty { display: none; } - -#netInspector tr.cat_info { - color: #00f; - } -#netInspector tr.blocked { - background-color: rgba(192, 0, 0, 0.1); - } -body.colorBlind #netInspector tr.blocked { - background-color: rgba(0, 19, 110, 0.1); - } -#netInspector tr.nooped { - background-color: rgba(108, 108, 108, 0.1); - } -body.colorBlind #netInspector tr.nooped { - background-color: rgba(96, 96, 96, 0.1); - } -#netInspector tr.allowed { - background-color: rgba(0, 160, 0, 0.1); - } -body.colorBlind #netInspector tr.allowed { - background-color: rgba(255, 194, 57, 0.1) - } -#netInspector tr.cosmetic, -#netInspector tr.redirect { - background-color: rgba(255, 255, 0, 0.1); - } -body.colorBlind #netInspector tr.cosmetic, -body.colorBlind #netInspector tr.redirect { - background-color: rgba(0, 19, 110, 0.1); - } -#netInspector tr.maindoc { - background-color: #666; - color: white; - text-align: center; - } - -body #netInspector td { - border: 1px solid #ccc; - border-top: none; - min-width: 0.5em; - padding: 3px; - vertical-align: top; - white-space: normal; - word-break: break-all; - word-wrap: break-word; - } -#netInspector tr td { - border-top: 1px solid #ccc; - } -#netInspector tr td:first-of-type { - border-left: none; - } -#netInspector tr td:last-of-type { - border-right: none; - } -#netInspector.vCompact tr:not(.vExpanded) td { - overflow: hidden; - text-overflow: ellipsis; +#vwRenderer .logEntry > div { + height: 100%; white-space: nowrap; } -#netInspector tr[data-tabid].void { +#vwRenderer .logEntry > div.blocked { + background-color: rgba(192, 0, 0, 0.1); + } +body.colorBlind #vwRenderer .logEntry > div.blocked { + background-color: rgba(0, 19, 110, 0.1); + } +#vwRenderer .logEntry > div.nooped { + background-color: rgba(108, 108, 108, 0.1); + } +body.colorBlind #vwRenderer .logEntry > div.nooped { + background-color: rgba(96, 96, 96, 0.1); + } +#vwRenderer .logEntry > div.allowed { + background-color: rgba(0, 160, 0, 0.1); + } +body.colorBlind #vwRenderer .logEntry > div.allowed { + background-color: rgba(255, 194, 57, 0.1) + } +#vwRenderer .logEntry > div.cosmetic, +#vwRenderer .logEntry > div.redirect { + background-color: rgba(255, 255, 0, 0.1); + } +body.colorBlind #vwRenderer .logEntry > div.cosmetic, +body.colorBlind #vwRenderer .logEntry > div.redirect { + background-color: rgba(0, 19, 110, 0.1); + } +#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; } -#netInspector tr[data-tabid].void:hover { +#vwRenderer .logEntry > div.voided:hover { opacity: 0.7; } -#netInspector tr td:nth-of-type(1) { - cursor: default; - text-align: right; +#vwRenderer .logEntry > div > span { + border: 1px solid #ccc; + 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; } -#netInspector tr td:nth-of-type(2) { +#vwRenderer .logEntry > div.canDetails:hover > span { + background-color: rgba(0,0,0,0.04); } -#netInspector tr.canLookup td:nth-of-type(2) { +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 > 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, 0, 0.08); cursor: zoom-in; } -#netInspector tr.cat_net td:nth-of-type(3), -#netInspector tr.cat_cosmetic td:nth-of-type(3), -#netInspector tr.cat_redirect td:nth-of-type(3) { - font: 12px monospace; - text-align: center; - white-space: nowrap; - } -#netInspector tr.cat_net td:nth-of-type(3) { - cursor: pointer; - } -#netInspector tr.cat_net td:nth-of-type(3):hover { - background: #ccc; - } -#netInspector tr td:nth-of-type(4) { - } -#netInspector tr[data-dochn] td:nth-of-type(4) { +#netInspector:not(.vExpanded) #vwRenderer .logEntry > div > span:nth-of-type(4) { direction: rtl; } -#netInspector tr td:nth-of-type(5) { - cursor: default; - overflow: visible !important; - position: relative; +#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; } -#netInspector tr td[data-parties]:nth-of-type(5) { - cursor: zoom-in; - } /* visual for tabless network requests */ -#netInspector tr.tab_bts td:nth-of-type(5)::before { - border: 5px solid #bbb; +#vwRenderer .logEntry > div > span:nth-of-type(5) { + position: relative; + } +#vwRenderer .logEntry > div[data-tabid="-1"] > span:nth-of-type(5)::before { + border: 5px solid #ccc; border-bottom: 0; border-top: 0; bottom: 0; @@ -334,49 +378,61 @@ body #netInspector td { width: calc(100% - 10px); z-index: -1; } -/* visual for quick tooltip */ -#netInspector tr td[data-parties]:nth-of-type(5):active::after { - background-color: #feb; - border: 1px outset #feb; - border-left: 5px solid gray; - color: black; - content: attr(data-parties); - left: 100%; - padding: 0.4em 0.6em; - position: absolute; - text-align: left; - top: -50%; - white-space: pre; -} -#netInspector tr.cat_net td:nth-of-type(7) > span > b { +#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) { + } +#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) > span > b { font-weight: bold; } -#netInspector tr td:nth-of-type(7) b { +#vwRenderer .logEntry > div > span:nth-of-type(7) b { font-weight: normal; } -#netInspector tr.blocked td:nth-of-type(7) b { +#vwRenderer .logEntry > div.blocked > span:nth-of-type(7) b { background-color: rgba(192, 0, 0, 0.2); } -body.colorBlind #netInspector tr.blocked td:nth-of-type(7) b { +body.colorBlind #vwRenderer .logEntry > div.blocked > span:nth-of-type(7) b { background-color: rgba(0, 19, 110, 0.2); } -#netInspector tr.nooped td:nth-of-type(7) b { +#vwRenderer .logEntry > div.nooped > span:nth-of-type(7) b { background-color: rgba(108, 108, 108, 0.2); } -body.colorBlind #netInspector tr.nooped td:nth-of-type(7) b { +body.colorBlind #vwRenderer .logEntry > div.nooped > span:nth-of-type(7) b { background-color: rgba(96, 96, 96, 0.2); } -#netInspector tr.allowed td:nth-of-type(7) b { +#vwRenderer .logEntry > div.allowed > span:nth-of-type(7) b { background-color: rgba(0, 160, 0, 0.2); } -body.colorBlind #netInspector tr.allowed td:nth-of-type(7) b { +body.colorBlind #vwRenderer .logEntry > div.allowed > span:nth-of-type(7) b { background-color: rgba(255, 194, 57, 0.2); } +#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: white; border: 1px solid gray; - border-radius: 3px; bottom: 0; box-sizing: border-box; display: none; @@ -389,16 +445,13 @@ body.colorBlind #netInspector tr.allowed td:nth-of-type(7) b { display: block; } -.modalDialog { +#modalOverlay { align-items: center; - -webkit-align-items: center; background-color: rgba(0, 0, 0, 0.5); border: 0; bottom: 0; - display: flex; - display: -webkit-flex; + display: none; justify-content: center; - -webkit-justify-content: center; left: 0; margin: 0; position: fixed; @@ -406,27 +459,56 @@ body.colorBlind #netInspector tr.allowed td:nth-of-type(7) b { top: 0; z-index: 400; } - -.modalDialog .dialog { +#modalOverlay.on { + display: flex; + } +#modalOverlay > div { + position: relative; + } +#modalOverlay > div > div:nth-of-type(1) { background-color: white; - border: 2px solid white; + border: 0; box-sizing: border-box; - width: 90vw; + padding: 1em; max-height: 90vh; overflow-y: auto; + width: 90vw; + } +#modalOverlay > div > div:nth-of-type(2) { + stroke: #000; + stroke-width: 3px; + position: absolute; + width: 1.6em; + height: 1.6em; + bottom: calc(100% + 2px); + background-color: white; + } +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: #eee; + } +#modalOverlay > div > div:nth-of-type(2) > * { + pointer-events: none; } -#netFilteringDialog .dialog p { - line-height: 2em; +#netFilteringDialog { + font-size: 90%; } - -#netFilteringDialog .dialog select { +#netFilteringDialog a { + text-decoration: none; + } +#netFilteringDialog select { max-width: 75%; outline: none; padding: 0.2em; } - -#netFilteringDialog .dialog > div.preview { +#netFilteringDialog > .preview { + align-items: center; /* http://lea.verou.me/css3patterns/ */ background-color: #aaa; background-image: @@ -448,35 +530,28 @@ body.colorBlind #netInspector tr.allowed td:nth-of-type(7) b { ); 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 .dialog > div.preview > * { +#netFilteringDialog > .preview > * { + background-color: white; max-width: 100%; - max-height: 40vh; + max-height: 20vh; + } +#netFilteringDialog > .preview > span { + cursor: pointer; + padding: 0.5em; } -#netFilteringDialog .dialog table { - border: 0; - border-collapse: collapse; - table-layout: fixed; - width: 100%; - } -#netFilteringDialog .dialog table > colgroup > col:nth-of-type(1) { - width: 3.8em; - } -#netFilteringDialog .dialog table > colgroup > col:nth-of-type(2) { - } - -#netFilteringDialog .dialog td { - border: 0; - padding: 0; - vertical-align: middle; - } -#netFilteringDialog .dialog > div.headers { +#netFilteringDialog > .headers { border-bottom: 1px solid #888; + line-height: 2; position: relative; } -#netFilteringDialog .dialog > div.headers > span.header { +#netFilteringDialog > .headers > .header { background-color: #eee; border: 1px solid #aaa; border-bottom: 1px solid #888; @@ -485,98 +560,164 @@ body.colorBlind #netInspector tr.allowed td:nth-of-type(7) b { color: #888; cursor: pointer; display: inline-block; - font-size: small; - line-height: 2em; - margin-left: 0.5em; padding: 0 1em; position: relative; text-align: center; top: 1px; } -#netFilteringDialog .dialog > div.headers > span.header.selected { +#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: white; border-color: #888; border-bottom: 1px solid white; color: black; } -#netFilteringDialog .dialog > div.headers > span.tools { - display: inline-block; +#netFilteringDialog > .headers > .tools { + bottom: 0; + display: flex; position: absolute; - top: 50%; - transform: translate(0, -50%); } -body[dir="ltr"] #netFilteringDialog .dialog > div.headers > span.tools { - right: 0.1em; +body[dir="ltr"] #netFilteringDialog > .headers > .tools { + right: 0; } -body[dir="rtl"] #netFilteringDialog .dialog > div.headers > span.tools { - left: 0.1em; +body[dir="rtl"] #netFilteringDialog > .headers > .tools { + left: 0; } -#netFilteringDialog .dialog > div.headers > span.tools > span { +#netFilteringDialog > .headers > .tools > span { cursor: pointer; - font-size: 1.2em; - padding: 0.2em 0.4em; + font-size: 1.5em; + padding: 0 0.25em; text-align: center; } -#netFilteringDialog .dialog > div.headers > span.tools > span:hover { +#netFilteringDialog > .headers > .tools > span:hover { background-color: #eee; } -#netFilteringDialog .dialog > div.containers { - height: 40vh; - overflow: hidden; - overflow-y: auto; - } -#netFilteringDialog .dialog > div.containers > div { +#netFilteringDialog.cosmeticRealm > .headers > .dynamic, +#netFilteringDialog.cosmeticRealm > .panes > .dynamic { display: none; } -#netFilteringDialog .dialog > div.containers > div.selected { - display: block; +#netFilteringDialog.cosmeticRealm > .headers > .static, +#netFilteringDialog.cosmeticRealm > .panes > .static { + display: none; } -#netFilteringDialog .dialog > div.containers > div.dynamic > table.toolbar select { - font: 14px; - height: 2.5em; +#netFilteringDialog > div.panes { + min-height: 40vh; + overflow: hidden; + overflow-y: auto; + padding-top: 1em; } -#netFilteringDialog .dialog > div.containers > div.dynamic > table.toolbar #saveRules { +#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: #e6e6e6; + border: 0; + border-bottom: 1px solid white; + display: flex; + min-height: 2.2em; + } +#netFilteringDialog > .panes > .details > div > span { + align-items: center; + display: inline-flex; + flex-wrap: wrap; + padding: 0.25em 0.5em; + } +#netFilteringDialog > .panes > .details > div > span:nth-of-type(1) { + border: 0; + flex-grow: 0; + flex-shrink: 0; + justify-content: flex-end; + width: 8em; + } +body[dir="ltr"] #netFilteringDialog > .panes > .details > div > span:nth-of-type(1) { + border-right: 1px solid white; + } +body[dir="rtl"] #netFilteringDialog > .panes > .details > div > span:nth-of-type(1) { + border-left: 1px solid white; + } +#netFilteringDialog > .panes > .details > div > span:nth-of-type(2) { + 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 > 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 white; + } +body[dir="rtl"] #netFilteringDialog > div.panes > .dynamic .row > span:nth-of-type(1) { + border-left: 1px solid white; + } +#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: 1.6em; - margin: 0.1em; - padding: 0.25em 0.5em; + font-size: 2em; visibility: hidden; + width: 100%; } -body.dirty #netFilteringDialog .dialog > div.containers > div.dynamic > table.toolbar #saveRules { +body.dirty #netFilteringDialog > div.panes > .dynamic > .toolbar #saveRules { visibility: visible; } -#netFilteringDialog .dialog > div.containers > div.dynamic > table.toolbar #saveRules:hover { +#netFilteringDialog > div.panes > .dynamic > .toolbar #saveRules:hover { fill: black; } -#netFilteringDialog .dialog > div.containers > div.dynamic > table.toolbar tr.entry { +#netFilteringDialog > div.panes > .dynamic > .toolbar .entry { display: none; } -#netFilteringDialog .dialog > div.containers > div.dynamic tr.entry { +#netFilteringDialog > div.panes > .dynamic .entry { background-color: #e6e6e6; border: 0; border-bottom: 1px solid white; - font-size: 13px; } -#netFilteringDialog .dialog > div.containers > div.dynamic tr.entry:hover { +#netFilteringDialog > div.panes > .dynamic .entry:hover { background-color: #f0f0f0; } -#netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td:first-of-type { - border: 0; - border-right: 1px solid white; - text-align: center; - } -#netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action { +#netFilteringDialog > div.panes > .dynamic .entry > .action { background-color: transparent; border: 0; cursor: pointer; - height: 2em; - width: 100%; } -#netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action > span { +#netFilteringDialog > div.panes > .dynamic .entry > .action > span { background-color: transparent; border: 0; display: inline-block; @@ -585,90 +726,94 @@ body.dirty #netFilteringDialog .dialog > div.containers > div.dynamic > table.to visibility: hidden; width: 33.33%; } -#netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action.allow { +#netFilteringDialog > div.panes > .dynamic .entry > .action.allow { background-color: rgba(0, 160, 0, 0.3); } -body.colorBlind #netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action.allow { +body.colorBlind #netFilteringDialog > div.panes > .dynamic .entry > .action.allow { background-color: rgba(255, 194, 57, 0.4); } -#netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action.noop { +#netFilteringDialog > div.panes > .dynamic .entry > .action.noop { background-color: rgba(108, 108, 108, 0.3); } -body.colorBlind #netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action.noop { +body.colorBlind #netFilteringDialog > div.panes > .dynamic .entry > .action.noop { background-color: rgba(96, 96, 96, 0.4); } -#netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action.block { +#netFilteringDialog > div.panes > .dynamic .entry > .action.block { background-color: rgba(192, 0, 0, 0.3); } -body.colorBlind #netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action.block { +body.colorBlind #netFilteringDialog > div.panes > .dynamic .entry > .action.block { background-color: rgba(0, 19, 110, 0.4); } -#netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action.own.allow { +#netFilteringDialog > div.panes > .dynamic .entry > .action.own.allow { background-color: rgba(0, 160, 0, 1); } -body.colorBlind #netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action.own.allow { +body.colorBlind #netFilteringDialog > div.panes > .dynamic .entry > .action.own.allow { background-color: rgba(255, 194, 57, 1); } -#netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action.own.noop, -body.colorBlind #netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action.own.noop { +#netFilteringDialog > div.panes > .dynamic .entry > .action.own.noop, +body.colorBlind #netFilteringDialog > div.panes > .dynamic .entry > .action.own.noop { background-color: rgba(108, 108, 108, 1); } -#netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action.own.block { +#netFilteringDialog > div.panes > .dynamic .entry > .action.own.block { background-color: rgba(192, 0, 0, 1); } -body.colorBlind #netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action.own.block { +body.colorBlind #netFilteringDialog > div.panes > .dynamic .entry > .action.own.block { background-color: rgba(0, 19, 110, 1); } -#netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action:not(.own):hover > span { +#netFilteringDialog > div.panes > .dynamic .entry > .action:not(.own):hover > span { opacity: 0.2; visibility: visible; } -#netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action:not(.own):hover > span:hover { +#netFilteringDialog > div.panes > .dynamic .entry > .action:not(.own):hover > span:hover { opacity: 0.75; } -#netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action > span.allow { +#netFilteringDialog > div.panes > .dynamic .entry > .action > .allow { background-color: rgb(0, 160, 0); } -body.colorBlind #netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action > span.allow { +body.colorBlind #netFilteringDialog > div.panes > .dynamic .entry > .action > .allow { background-color: rgb(255, 194, 57); } -#netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action > span.noop { +#netFilteringDialog > div.panes > .dynamic .entry > .action > .noop { background-color: rgb(108, 108, 108); } -#netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action > span.block { +#netFilteringDialog > div.panes > .dynamic .entry > .action > .block { background-color: rgb(192, 0, 0); } -body.colorBlind #netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td > div.action > span.block { +body.colorBlind #netFilteringDialog > div.panes > .dynamic .entry > .action > .block { background-color: rgb(0, 19, 110); } -#netFilteringDialog .dialog > div.containers > div.dynamic tr.entry > td.url { +#netFilteringDialog > div.panes > .dynamic .entry > .url { overflow: hidden; - padding-left: 4px; text-overflow: ellipsis; white-space: nowrap; } -#netFilteringDialog .dialog > div.containers > div.static > p { - margin: 0.75em 0; +#netFilteringDialog > div.panes > div.static > div { + line-height: 2; } -#netFilteringDialog .dialog > div.containers > div.static textarea { +#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 .dialog > div.containers > div.static > p:nth-of-type(2) { +#netFilteringDialog > div.panes > div.static > div:nth-of-type(2) { text-align: center; } -#filterFinderDialog .dialog { - padding: 1em; +#filterFinderDialog { word-break: break-all; } -#filterFinderDialog .dialog code { +#filterFinderDialog code { background: #eee; font-size: 85%; padding: 3px; unicode-bidi: plaintext; white-space: pre-wrap; } -#filterFinderDialog .dialog ul { +#filterFinderDialog ul { font-size: larger; } #filterFinderDialog .filterFinderListEntry { @@ -688,13 +833,40 @@ body.colorBlind #netFilteringDialog .dialog > div.containers > div.dynamic tr.e #filterFinderDialog .filterFinderListEntry a.fa-icon[href=""] { display: none; } -#filterFinderDialog .dialog > *:first-child { +#filterFinderDialog > *:first-child { margin-top: 0; } -#filterFinderDialog .dialog > *:last-child { +#filterFinderDialog > *:last-child { margin-bottom: 0; } -.hide { - display: none; +#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; } diff --git a/src/img/fontawesome/fontawesome-defs.svg b/src/img/fontawesome/fontawesome-defs.svg index ca6d168cd..380545c13 100644 --- a/src/img/fontawesome/fontawesome-defs.svg +++ b/src/img/fontawesome/fontawesome-defs.svg @@ -29,6 +29,7 @@ License - https://github.com/FortAwesome/Font-Awesome/tree/a8386aae19e200ddb0f68 + diff --git a/src/js/asset-viewer.js b/src/js/asset-viewer.js index f07901f5a..cc28020c0 100644 --- a/src/js/asset-viewer.js +++ b/src/js/asset-viewer.js @@ -49,7 +49,7 @@ lineNumbers: true, lineWrapping: true, readOnly: true, - styleActiveLine: true + styleActiveLine: true, } ); diff --git a/src/js/assets.js b/src/js/assets.js index b80c6ba21..c6f1410ba 100644 --- a/src/js/assets.js +++ b/src/js/assets.js @@ -130,7 +130,9 @@ api.fetchText = function(url, onLoad, onError) { const onErrorEvent = function() { cleanup(); µBlock.logger.writeOne({ - error: errorCantConnectTo.replace('{{msg}}', actualUrl) + realm: 'message', + type: 'error', + text: errorCantConnectTo.replace('{{msg}}', actualUrl) }); onError({ url, content: '' }); }; diff --git a/src/js/commands.js b/src/js/commands.js index faf074f79..a8036cd4d 100644 --- a/src/js/commands.js +++ b/src/js/commands.js @@ -51,7 +51,7 @@ vAPI.tabs.get(null, function(tab) { let hash = tab.url.startsWith(vAPI.getURL('')) ? '' : - '#tab_active+' + tab.id; + '#_+' + tab.id; µb.openNewTab({ url: 'logger-ui.html' + hash, select: true, diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index d10eb4100..f03fdf680 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -599,7 +599,9 @@ FilterContainer.prototype.compileGenericHideSelector = function( if ( compiled === undefined || compiled !== selector ) { const who = writer.properties.get('assetKey') || '?'; µb.logger.writeOne({ - error: `Invalid generic cosmetic filter in ${who}: ##${selector}` + realm: 'message', + type: 'error', + text: `Invalid generic cosmetic filter in ${who}: ##${selector}` }); return; } @@ -658,7 +660,9 @@ FilterContainer.prototype.compileGenericUnhideSelector = function( if ( compiled === undefined ) { const who = writer.properties.get('assetKey') || '?'; µb.logger.writeOne({ - error: `Invalid cosmetic filter in ${who} : #@#${parsed.suffix}` + realm: 'message', + type: 'error', + text: `Invalid cosmetic filter in ${who}: #@#${parsed.suffix}` }); return; } @@ -687,7 +691,9 @@ FilterContainer.prototype.compileSpecificSelector = function( if ( compiled === undefined ) { const who = writer.properties.get('assetKey') || '?'; µb.logger.writeOne({ - error: `Invalid cosmetic filter in ${who} : ##${parsed.suffix}` + realm: 'message', + type: 'error', + text: `Invalid cosmetic filter in ${who}: ##${parsed.suffix}` }); return; } diff --git a/src/js/dynamic-net-filtering.js b/src/js/dynamic-net-filtering.js index a2aee6333..5cce84101 100644 --- a/src/js/dynamic-net-filtering.js +++ b/src/js/dynamic-net-filtering.js @@ -404,24 +404,18 @@ Matrix.prototype.lookupRuleData = function(src, des, type) { /******************************************************************************/ Matrix.prototype.toLogData = function() { - if ( this.r === 0 || this.type === '' ) { - return; - } - var logData = { + if ( this.r === 0 || this.type === '' ) { return; } + return { source: 'dynamicHost', result: this.r, - raw: this.z + ' ' + - this.y + ' ' + - this.type + ' ' + - this.intToActionMap.get(this.r) + raw: `${this.z} ${this.y} ${this.type} ${this.intToActionMap.get(this.r)}` }; - return logData; }; Matrix.prototype.intToActionMap = new Map([ - [ 1, ' block' ], - [ 2, ' allow' ], - [ 3, ' noop' ] + [ 1, 'block' ], + [ 2, 'allow' ], + [ 3, 'noop' ] ]); /******************************************************************************/ diff --git a/src/js/fa-icons.js b/src/js/fa-icons.js index 21576a1ce..2edfbc3a4 100644 --- a/src/js/fa-icons.js +++ b/src/js/fa-icons.js @@ -26,10 +26,7 @@ let faIconsInit = function(root) { const icons = (root || document).querySelectorAll('.fa-icon'); for ( const icon of icons ) { - if ( - icon.firstChild === null || - icon.firstChild.nodeType !== 3 - ) { + if ( icon.firstChild === null || icon.firstChild.nodeType !== 3 ) { continue; } const name = icon.firstChild.nodeValue; diff --git a/src/js/html-filtering.js b/src/js/html-filtering.js index 0863eec8e..3747bb729 100644 --- a/src/js/html-filtering.js +++ b/src/js/html-filtering.js @@ -244,7 +244,9 @@ if ( compiled === undefined ) { const who = writer.properties.get('assetKey') || '?'; µb.logger.writeOne({ - error: `Invalid HTML filter in ${who} : ##${selector}` + realm: 'message', + type: 'error', + text: `Invalid HTML filter in ${who}: ##${selector}` }); return; } diff --git a/src/js/i18n.js b/src/js/i18n.js index 211958fcf..526390ef1 100644 --- a/src/js/i18n.js +++ b/src/js/i18n.js @@ -31,36 +31,25 @@ /******************************************************************************/ // https://github.com/gorhill/uBlock/issues/2084 -// Anything else than , , , , , , and will +// Anything else than , , , , , and will // be rendered as plain text. -// For , only the type attribute is allowed. // For , only href attribute must be present, and it MUST starts with // `https://`, and includes no single- or double-quotes. // No HTML entities are allowed, there is code to handle existing HTML // entities already present in translation files until they are all gone. -var reSafeTags = /^([\s\S]*?)<(b|code|em|i|span)>(.+?)<\/\2>([\s\S]*)$/, - reSafeInput = /^([\s\S]*?)<(input type="[^"]+")>(.*?)([\s\S]*)$/, - reInput = /^input type=(['"])([a-z]+)\1$/, - reSafeLink = /^([\s\S]*?)<(a href=['"]https:\/\/[^'" <>]+['"])>(.+?)<\/a>([\s\S]*)$/, - reLink = /^a href=(['"])(https:\/\/[^'"]+)\1$/; +const reSafeTags = /^([\s\S]*?)<(b|code|em|i|span)>(.+?)<\/\2>([\s\S]*)$/; +const reSafeLink = /^([\s\S]*?)<(a href=['"]https:\/\/[^'" <>]+['"])>(.+?)<\/a>([\s\S]*)$/; +const reLink = /^a href=(['"])(https:\/\/[^'"]+)\1$/; -var safeTextToTagNode = function(text) { - var matches, node; +const safeTextToTagNode = function(text) { if ( text.lastIndexOf('a ', 0) === 0 ) { - matches = reLink.exec(text); + const matches = reLink.exec(text); if ( matches === null ) { return null; } - node = document.createElement('a'); + const node = document.createElement('a'); node.setAttribute('href', matches[2]); return node; } - if ( text.lastIndexOf('input ', 0) === 0 ) { - matches = reInput.exec(text); - if ( matches === null ) { return null; } - node = document.createElement('input'); - node.setAttribute('type', matches[2]); - return node; - } // Firefox extension validator warns if using a variable as argument for // document.createElement(). switch ( text ) { @@ -79,8 +68,8 @@ var safeTextToTagNode = function(text) { } }; -var safeTextToTextNode = (function() { - let entities = new Map([ +const safeTextToTextNode = (function() { + const entities = new Map([ // TODO: Remove quote entities once no longer present in translation // files. Other entities must stay. [ '“', '“' ], @@ -90,7 +79,7 @@ var safeTextToTextNode = (function() { [ '<', '<' ], [ '>', '>' ], ]); - let decodeEntities = match => { + const decodeEntities = match => { return entities.get(match) || match; }; return function(text) { @@ -101,15 +90,16 @@ var safeTextToTextNode = (function() { }; })(); -var safeTextToDOM = function(text, parent) { +const safeTextToDOM = function(text, parent) { if ( text === '' ) { return; } + // Fast path (most common). if ( text.indexOf('<') === -1 ) { parent.appendChild(safeTextToTextNode(text)); return; } // Slow path. - // `

` no longer allowed. Code below can be remove once all

's are + // `

` no longer allowed. Code below can be removed once all

's are // gone from translation files. text = text.replace(/^

|<\/p>/g, '') .replace(/

/g, '\n\n'); @@ -118,18 +108,17 @@ var safeTextToDOM = function(text, parent) { if ( matches === null ) { matches = reSafeLink.exec(text); if ( matches === null ) { - matches = reSafeInput.exec(text); - if ( matches === null ) { - parent.appendChild(safeTextToTextNode(text)); - return; - } + parent.appendChild(safeTextToTextNode(text)); + return; } } - safeTextToDOM(matches[1], parent); - let node = safeTextToTagNode(matches[2]) || parent; + const fragment = document.createDocumentFragment(); + safeTextToDOM(matches[1], fragment); + let node = safeTextToTagNode(matches[2]); safeTextToDOM(matches[3], node); - parent.appendChild(node); - safeTextToDOM(matches[4], parent); + fragment.appendChild(node); + safeTextToDOM(matches[4], fragment); + parent.appendChild(fragment); }; /******************************************************************************/ @@ -146,7 +135,7 @@ vAPI.i18n.safeTemplateToDOM = function(id, dict, parent) { safeTextToDOM(textin, parent); return parent; } - let re = /\{\{\w+\}\}/g; + const re = /\{\{\w+\}\}/g; let textout = ''; for (;;) { let match = re.exec(textin); @@ -172,44 +161,62 @@ vAPI.i18n.safeTemplateToDOM = function(id, dict, parent) { // Helper to deal with the i18n'ing of HTML files. vAPI.i18n.render = function(context) { - var docu = document; - var root = context || docu; - var elems, n, i, elem, text; + const docu = document; + const root = context || docu; - elems = root.querySelectorAll('[data-i18n]'); - n = elems.length; - for ( i = 0; i < n; i++ ) { - elem = elems[i]; - text = vAPI.i18n(elem.getAttribute('data-i18n')); + for ( const elem of root.querySelectorAll('[data-i18n]') ) { + let text = vAPI.i18n(elem.getAttribute('data-i18n')); if ( !text ) { continue; } - // TODO: remove once it's all replaced with - if ( text.indexOf('{') !== -1 ) { - text = text.replace(/\{\{input:([^}]+)\}\}/g, ''); + if ( text.indexOf('{{') === -1 ) { + safeTextToDOM(text, elem); + continue; } - safeTextToDOM(text, elem); + // Handle selector-based placeholders: these placeholders tell where + // existing child DOM element are to be positioned relative to the + // localized text nodes. + const parts = text.split(/(\{\{[^}]+\}\})/); + const fragment = document.createDocumentFragment(); + let textBefore = ''; + for ( let part of parts ) { + if ( part === '' ) { continue; } + if ( part.startsWith('{{') && part.endsWith('}}') ) { + // TODO: remove detection of ':' once it no longer appears + // in translation files. + const pos = part.indexOf(':'); + if ( pos !== -1 ) { + part = part.slice(0, pos) + part.slice(-2); + } + const node = elem.querySelector(part.slice(2, -2)); + if ( node !== null ) { + safeTextToDOM(textBefore, fragment); + fragment.appendChild(node); + textBefore = ''; + continue; + } + } + textBefore += part; + } + if ( textBefore !== '' ) { + safeTextToDOM(textBefore, fragment); + } + elem.appendChild(fragment); } - elems = root.querySelectorAll('[data-i18n-title]'); - n = elems.length; - for ( i = 0; i < n; i++ ) { - elem = elems[i]; - text = vAPI.i18n(elem.getAttribute('data-i18n-title')); + for ( const elem of root.querySelectorAll('[data-i18n-title]') ) { + const text = vAPI.i18n(elem.getAttribute('data-i18n-title')); if ( !text ) { continue; } elem.setAttribute('title', text); } - elems = root.querySelectorAll('[placeholder]'); - n = elems.length; - for ( i = 0; i < n; i++ ) { - elem = elems[i]; - elem.setAttribute('placeholder', vAPI.i18n(elem.getAttribute('placeholder'))); + for ( const elem of root.querySelectorAll('[placeholder]') ) { + elem.setAttribute( + 'placeholder', + vAPI.i18n(elem.getAttribute('placeholder')) + ); } - elems = root.querySelectorAll('[data-i18n-tip]'); - n = elems.length; - for ( i = 0; i < n; i++ ) { - elem = elems[i]; - text = vAPI.i18n(elem.getAttribute('data-i18n-tip')) + for ( const elem of root.querySelectorAll('[data-i18n-tip]') ) { + const text = vAPI.i18n(elem.getAttribute('data-i18n-tip')) .replace(/
/g, '\n') .replace(/\n{3,}/g, '\n\n'); elem.setAttribute('data-tip', text); @@ -224,7 +231,7 @@ vAPI.i18n.render(); /******************************************************************************/ vAPI.i18n.renderElapsedTimeToString = function(tstamp) { - var value = (Date.now() - tstamp) / 60000; + let value = (Date.now() - tstamp) / 60000; if ( value < 2 ) { return vAPI.i18n('elapsedOneMinuteAgo'); } diff --git a/src/js/logger-ui-inspector.js b/src/js/logger-ui-inspector.js index 28c21559f..e61e07e40 100644 --- a/src/js/logger-ui-inspector.js +++ b/src/js/logger-ui-inspector.js @@ -29,7 +29,7 @@ /******************************************************************************/ -var showdomButton = uDom.nodeFromId('showdom'); +const showdomButton = uDom.nodeFromId('showdom'); // Don't bother if the browser is not modern enough. if ( @@ -340,31 +340,25 @@ var nidFromNode = function(node) { /******************************************************************************/ -var startDialog = (function() { - var dialog = uDom.nodeFromId('cosmeticFilteringDialog'); - var textarea = dialog.querySelector('textarea'); - var hideSelectors = []; - var unhideSelectors = []; - var inputTimer = null; +const startDialog = (function() { + let dialog; + let textarea; + let hideSelectors = []; + let unhideSelectors = []; + let inputTimer; - var onInputChanged = (function() { - var parse = function() { - inputTimer = null; + const onInputChanged = (function() { + const parse = function() { + inputTimer = undefined; hideSelectors = []; unhideSelectors = []; - var line, matches; - var re = /^([^#]*)(#@?#)(.+)$/; - var lines = textarea.value.split(/\s*\n\s*/); - for ( var i = 0; i < lines.length; i++ ) { - line = lines[i].trim(); - if ( line === '' || line.charAt(0) === '!' ) { - continue; - } - matches = re.exec(line); - if ( matches === null || matches.length !== 4 ) { - continue; - } + const re = /^([^#]*)(#@?#)(.+)$/; + for ( let line of textarea.value.split(/\s*\n\s*/) ) { + line = line.trim(); + if ( line === '' || line.charAt(0) === '!' ) { continue; } + const matches = re.exec(line); + if ( matches === null || matches.length !== 4 ) { continue; } if ( inspectedHostname.lastIndexOf(matches[1]) === -1 ) { continue; } @@ -379,19 +373,15 @@ var startDialog = (function() { }; return function parseAsync() { - if ( inputTimer === null ) { + if ( inputTimer === undefined ) { inputTimer = vAPI.setTimeout(parse, 743); } }; })(); - var onClicked = function(ev) { + const onClicked = function(ev) { var target = ev.target; - // click outside the dialog proper - if ( target.classList.contains('modalDialog') ) { - return stop(); - } ev.stopPropagation(); if ( target.id === 'createCosmeticFilters' ) { @@ -402,7 +392,7 @@ var startDialog = (function() { } }; - var showCommitted = function() { + const showCommitted = function() { messaging.sendTo(inspectorConnectionId, { what: 'showCommitted', hide: hideSelectors.join(',\n'), @@ -410,7 +400,7 @@ var startDialog = (function() { }); }; - var showInteractive = function() { + const showInteractive = function() { messaging.sendTo(inspectorConnectionId, { what: 'showInteractive', hide: hideSelectors.join(',\n'), @@ -418,46 +408,45 @@ var startDialog = (function() { }); }; - var start = function() { + const start = function() { + dialog = logger.modalDialog.create('#cosmeticFilteringDialog', stop); + textarea = dialog.querySelector('textarea'); hideSelectors = []; - textarea.addEventListener('input', onInputChanged); - var node; - for ( node of domTree.querySelectorAll('code.off') ) { - if ( node.classList.contains('filter') === false ) { - hideSelectors.push(selectorFromNode(node)); - } + for ( const node of domTree.querySelectorAll('code.off') ) { + if ( node.classList.contains('filter') ) { continue; } + hideSelectors.push(selectorFromNode(node)); } - var taValue = []; - var d = new Date(); - taValue.push('! ' + d.toLocaleString() + ' ' + inspectedURL); - for ( var selector of hideSelectors ) { + const taValue = []; + for ( const selector of hideSelectors ) { taValue.push(inspectedHostname + '##' + selector); } - var ids = new Set(), id; - for ( node of domTree.querySelectorAll('code.filter.off') ) { - id = node.getAttribute('data-filter-id'); + const ids = new Set(); + for ( const node of domTree.querySelectorAll('code.filter.off') ) { + const id = node.getAttribute('data-filter-id'); if ( ids.has(id) ) { continue; } ids.add(id); unhideSelectors.push(node.textContent); taValue.push(inspectedHostname + '#@#' + node.textContent); } textarea.value = taValue.join('\n'); - document.body.appendChild(dialog); + textarea.addEventListener('input', onInputChanged); dialog.addEventListener('click', onClicked, true); showCommitted(); + logger.modalDialog.show(); }; - var stop = function() { - if ( inputTimer !== null ) { + const stop = function() { + if ( inputTimer !== undefined ) { clearTimeout(inputTimer); - inputTimer = null; + inputTimer = undefined; } showInteractive(); - hideSelectors = []; - unhideSelectors = []; textarea.removeEventListener('input', onInputChanged); dialog.removeEventListener('click', onClicked, true); - document.body.removeChild(dialog); + dialog = undefined; + textarea = undefined; + hideSelectors = []; + unhideSelectors = []; }; return start; @@ -585,7 +574,7 @@ var shutdownInspector = function() { inspectorConnectionId = undefined; } logger.removeAllChildren(domTree); - inspector.classList.add('vCompact'); + inspector.classList.remove('vExpanded'); inspectedTabId = 0; }; @@ -605,7 +594,7 @@ var onTabIdChanged = function() { /******************************************************************************/ var toggleVCompactView = function() { - var state = !inspector.classList.toggle('vCompact'); + var state = inspector.classList.toggle('vExpanded'); var branches = document.querySelectorAll('#domInspector li.branch'); for ( var branch of branches ) { branch.classList.toggle('show', state); diff --git a/src/js/logger-ui.js b/src/js/logger-ui.js index f0db8fca6..102d3ad9d 100644 --- a/src/js/logger-ui.js +++ b/src/js/logger-ui.js @@ -31,47 +31,93 @@ const messaging = vAPI.messaging; const logger = self.logger = { ownerId: Date.now() }; +const logDate = new Date(); +const logDateTimezoneOffset = logDate.getTimezoneOffset() * 60000; +const loggerEntries = []; + +let filteredLoggerEntries = []; +let filteredLoggerEntryVoidedCount = 0; + let popupLoggerBox; let popupLoggerTooltips; -let activeTabId; +let activeTabId = 0; +let selectedTabId = 0; let netInspectorPaused = false; /******************************************************************************/ -const removeAllChildren = logger.removeAllChildren = function(node) { - while ( node.firstChild ) { - node.removeChild(node.firstChild); - } -}; - -/******************************************************************************/ - -const tabIdFromClassName = function(className) { - const matches = className.match(/\btab_([^ ]+)\b/); - if ( matches === null ) { return 0; } - if ( matches[1] === 'bts' ) { return -1; } - return parseInt(matches[1], 10); -}; +// Various helpers. const tabIdFromPageSelector = logger.tabIdFromPageSelector = function() { - const tabClass = uDom.nodeFromId('pageSelector').value; - if ( tabClass === 'tab_active' && activeTabId !== undefined ) { - return activeTabId; - } - if ( tabClass === 'tab_bts' ) { return -1; } - return /^tab_\d+$/.test(tabClass) ? parseInt(tabClass.slice(4), 10) : 0; + const value = uDom.nodeFromId('pageSelector').value; + return value !== '_' ? (parseInt(value, 10) || 0) : activeTabId; +}; + +const tabIdFromAttribute = function(elem) { + const value = elem.getAttribute('data-tabid') || ''; + const tabId = parseInt(value, 10); + return isNaN(tabId) ? 0 : tabId; }; /******************************************************************************/ /******************************************************************************/ -const tbody = document.querySelector('#netInspector tbody'); -const trJunkyard = []; -const tdJunkyard = []; -const firstVarDataCol = 1; -const lastVarDataIndex = 6; -const reRFC3986 = /^([^:\/?#]+:)?(\/\/[^\/?#]*)?([^?#]*)(\?[^#]*)?(#.*)?/; -const netFilteringDialog = uDom.nodeFromId('netFilteringDialog'); +// Current design allows for only one modal DOM-based dialog at any given time. +// +const modalDialog = (function() { + const overlay = uDom.nodeFromId('modalOverlay'); + const container = overlay.querySelector( + ':scope > div > div:nth-of-type(1)' + ); + const closeButton = overlay.querySelector( + ':scope > div > div:nth-of-type(2)' + ); + let onDestroyed; + + const removeChildren = logger.removeAllChildren = function(node) { + while ( node.firstChild ) { + node.removeChild(node.firstChild); + } + }; + + const create = function(selector, destroyListener) { + const template = document.querySelector(selector); + const dialog = template.cloneNode(true); + removeChildren(container); + container.appendChild(dialog); + onDestroyed = destroyListener; + return dialog; + }; + + const show = function() { + overlay.classList.add('on'); + }; + + const destroy = function() { + overlay.classList.remove('on'); + const dialog = container.firstElementChild; + removeChildren(container); + if ( typeof onDestroyed === 'function' ) { + onDestroyed(dialog); + } + onDestroyed = undefined; + }; + + const onClose = function(ev) { + if ( ev.target === overlay || ev.target === closeButton ) { + destroy(); + } + }; + overlay.addEventListener('click', onClose); + closeButton.addEventListener('click', onClose); + + return { create, show, destroy }; +})(); + +self.logger.modalDialog = modalDialog; + +/******************************************************************************/ +/******************************************************************************/ const prettyRequestTypes = { 'main_frame': 'doc', @@ -87,39 +133,16 @@ const uglyRequestTypes = { 'xhr': 'xmlhttprequest' }; -const staticFilterTypes = { - 'beacon': 'other', - 'doc': 'document', - 'css': 'stylesheet', - 'frame': 'subdocument', - 'ping': 'other', - 'object_subrequest': 'object', - 'xhr': 'xmlhttprequest' -}; - -let maxEntries = 5000; let allTabIds = new Map(); let allTabIdsToken; -/******************************************************************************/ - -var classNameFromTabId = function(tabId) { - if ( tabId < 0 ) { - return 'tab_bts'; - } - if ( tabId !== 0 ) { - return 'tab_' + tabId; - } - return ''; -}; - /******************************************************************************/ /******************************************************************************/ -var regexFromURLFilteringResult = function(result) { - var beg = result.indexOf(' '); - var end = result.indexOf(' ', beg + 1); - var url = result.slice(beg + 1, end); +const regexFromURLFilteringResult = function(result) { + const beg = result.indexOf(' '); + const end = result.indexOf(' ', beg + 1); + const url = result.slice(beg + 1, end); if ( url === '*' ) { return new RegExp('^.*$', 'gi'); } @@ -130,291 +153,636 @@ var regexFromURLFilteringResult = function(result) { // Emphasize hostname in URL, as this is what matters in uMatrix's rules. -var nodeFromURL = function(url, re) { +const nodeFromURL = function(url, re) { if ( re instanceof RegExp === false ) { return document.createTextNode(url); } - var matches = re.exec(url); + const matches = re.exec(url); if ( matches === null || matches[0].length === 0 ) { return document.createTextNode(url); } - var node = renderedURLTemplate.cloneNode(true); + const node = renderedURLTemplate.cloneNode(true); node.childNodes[0].textContent = url.slice(0, matches.index); node.childNodes[1].textContent = url.slice(matches.index, re.lastIndex); node.childNodes[2].textContent = url.slice(re.lastIndex); return node; }; -var renderedURLTemplate = document.querySelector('#renderedURLTemplate > span'); +const renderedURLTemplate = document.querySelector('#renderedURLTemplate > span'); /******************************************************************************/ -const createCellAt = function(tr, index) { - let td = tr.cells[index]; - const mustAppend = !td; - if ( mustAppend ) { - td = tdJunkyard.pop(); - } - if ( td ) { - td.removeAttribute('colspan'); - td.removeAttribute('data-parties'); - td.textContent = ''; - } else { - td = document.createElement('td'); - } - if ( mustAppend ) { - tr.appendChild(td); - } - return td; -}; - -/******************************************************************************/ - -var createRow = function(layout) { - let tr = trJunkyard.pop(); - if ( tr ) { - tr.className = ''; - tr.removeAttribute('data-tabhn'); - tr.removeAttribute('data-dochn'); - tr.removeAttribute('data-filter'); - tr.removeAttribute('data-tabid'); - } else { - tr = document.createElement('tr'); - } - let index = 0; - for ( ; index < firstVarDataCol; index++ ) { - createCellAt(tr, index); - } - let i = 1, span = 1, td; - for (;;) { - td = createCellAt(tr, index); - if ( i === lastVarDataIndex ) { break; } - if ( layout.charAt(i) !== '1' ) { - span += 1; - } else { - if ( span !== 1 ) { - td.setAttribute('colspan', span); - } - index += 1; - span = 1; - } - i += 1; - } - if ( span !== 1 ) { - td.setAttribute('colspan', span); - } - index += 1; - while ( (td = tr.cells[index]) ) { - tdJunkyard.push(tr.removeChild(td)); - } - return tr; -}; - -/******************************************************************************/ - -var padTo2 = function(v) { +const padTo2 = function(v) { return v < 10 ? '0' + v : v; }; -/******************************************************************************/ - -const createGap = function(tabId, url) { - const tr = createRow('1'); - tr.setAttribute('data-tabid', tabId); - tr.classList.add('tab_' + tabId); - tr.classList.add('maindoc'); - tr.cells[firstVarDataCol].textContent = url; - tbody.insertBefore(tr, tbody.firstChild); +const normalizeToStr = function(s) { + return typeof s === 'string' && s !== '' ? s : ''; }; /******************************************************************************/ -var renderNetLogEntry = function(tr, details) { - const trcl = tr.classList; - const type = details.type; - const url = details.url; - let td; - - // If the request is that of a root frame, insert a gap in the table - // in order to visually separate entries for different documents. - if ( type === 'main_frame' ) { - createGap(details.tabId, url); - } - - tr.classList.add('cat_' + details.realm); - - let filter = details.filter || undefined; - let filteringType; - if ( filter !== undefined ) { - if ( typeof filter.source === 'string' ) { - filteringType = filter.source; - trcl.add(filteringType); +const LogEntry = function(details) { + if ( details instanceof Object === false ) { return; } + const receiver = LogEntry.prototype; + for ( const prop in receiver ) { + if ( + details.hasOwnProperty(prop) && + details[prop] !== receiver[prop] + ) { + this[prop] = details[prop]; } } - - if ( filter !== undefined ) { - td = tr.cells[1]; - if ( filteringType === 'static' ) { - td.textContent = filter.raw; - trcl.add('canLookup'); - tr.setAttribute('data-filter', filter.compiled); - } else if ( filteringType === 'cosmetic' ) { - td.textContent = filter.raw; - trcl.add('canLookup'); - } else { - td.textContent = filter.raw; - } - } - - if ( filter !== undefined ) { - td = tr.cells[2]; - if ( filter.result === 1 ) { - trcl.add('blocked'); - td.textContent = '--'; - } else if ( filter.result === 2 ) { - trcl.add('allowed'); - td.textContent = '++'; - } else if ( filter.result === 3 ) { - trcl.add('nooped'); - td.textContent = '**'; - } else if ( filteringType === 'redirect' ) { - trcl.add('redirect'); - td.textContent = '<<'; - } - } - - if ( details.tabHostname ) { - tr.setAttribute('data-tabhn', details.tabHostname); - } - if ( details.docHostname ) { - tr.setAttribute('data-dochn', details.docHostname); - tr.cells[3].textContent = details.docHostname; - } - - // Partyness - if ( details.realm === 'net' && details.domain !== undefined ) { - td = tr.cells[4]; - let text = ''; - if ( details.tabDomain !== undefined ) { - text += details.domain === details.tabDomain ? '1' : '3'; - } else { - text += '?'; - } - if ( details.docDomain !== details.tabDomain ) { - text += ','; - if ( details.docDomain !== undefined ) { - text += details.domain === details.docDomain ? '1' : '3'; - } else { - text += '?'; - } - } - td.textContent = text; - let indent = '\t'; - text = details.tabDomain; - if ( details.docDomain !== details.tabDomain ) { - text += ` \u21d2\n\t${details.docDomain}`; - indent = '\t\t'; - } - text += ` \u21d2\n${indent}${details.domain}`; - td.setAttribute('data-parties', text); - } - - tr.cells[5].textContent = (prettyRequestTypes[type] || type); - - let re = null; - if ( filteringType === 'static' ) { - re = new RegExp(filter.regex, 'gi'); - } else if ( filteringType === 'dynamicUrl' ) { - re = regexFromURLFilteringResult(filter.rule.join(' ')); - } - tr.cells[6].appendChild(nodeFromURL(url, re)); +}; +LogEntry.prototype = { + dead: false, + docDomain: '', + docHostname: '', + domain: '', + filter: undefined, + realm: '', + tabDomain: '', + tabHostname: '', + tabId: undefined, + textContent: '', + tstamp: 0, + type: '', + voided: false, }; /******************************************************************************/ -var renderLogEntry = function(details) { - const fvdc = firstVarDataCol; - let tr; +const createLogSeparator = function(details, text) { + const separator = new LogEntry(); + separator.tstamp = details.tstamp; + separator.realm = 'message'; + separator.tabId = details.tabId; + separator.type = 'tabLoad'; + separator.textContent = ''; - if ( details.error !== undefined ) { - tr = createRow('1'); - tr.cells[fvdc].textContent = details.error; - } else if ( details.url !== undefined ) { - tr = createRow('111111'); - renderNetLogEntry(tr, details); - } else { - tr = createRow('1'); - tr.cells[fvdc].textContent = '???'; + const textContent = []; + logDate.setTime(separator.tstamp - logDateTimezoneOffset); + textContent.push( + // cell 0 + padTo2(logDate.getUTCHours()) + ':' + + padTo2(logDate.getUTCMinutes()) + ':' + + padTo2(logDate.getSeconds()), + // cell 1 + text + ); + separator.textContent = textContent.join('\t'); + + if ( details.voided ) { + separator.voided = true; } - // Fields common to all rows. - const time = logDate; - time.setTime(details.tstamp - logDateTimezoneOffset); - tr.cells[0].textContent = padTo2(time.getUTCHours()) + ':' + - padTo2(time.getUTCMinutes()) + ':' + - padTo2(time.getSeconds()); - - if ( details.tabId ) { - tr.setAttribute('data-tabid', details.tabId); - tr.classList.add(classNameFromTabId(details.tabId)); - } - - rowFilterer.filterOne(tr, true); - tbody.insertBefore(tr, tbody.firstChild); - return tr; + return separator; }; -// Reuse date objects. -const logDate = new Date(); -const logDateTimezoneOffset = logDate.getTimezoneOffset() * 60000; - /******************************************************************************/ -const renderLogEntries = function(response) { - document.body.classList.toggle('colorBlind', response.colorBlind); - +// TODO: once refactoring is mature, consider using push() instead of +// unshift(). This will require inverting the access logic +// throughout the code. +// +const processLoggerEntries = function(response) { const entries = response.entries; if ( entries.length === 0 ) { return; } - // Preserve scroll position - const height = tbody.offsetHeight; + const autoDeleteVoidedRows = uDom.nodeFromId('pageSelector').value === '_'; + const previousCount = filteredLoggerEntries.length; - const tabIds = allTabIds; for ( const entry of entries ) { - const details = JSON.parse(entry.details); - const tr = renderLogEntry(details); - // https://github.com/gorhill/uBlock/issues/1613#issuecomment-217637122 - // Unlikely, but it may happen: mark as void if associated tab no - // longer exist. - if ( details.tabId && tabIds.has(details.tabId) === false ) { - tr.classList.add('void'); + const unboxed = JSON.parse(entry); + const parsed = parseLogEntry(unboxed); + if ( + parsed.tabId !== undefined && + allTabIds.has(parsed.tabId) === false + ) { + if ( autoDeleteVoidedRows ) { continue; } + parsed.voided = true; + } + if ( parsed.type === 'main_frame' ) { + const separator = createLogSeparator(parsed, unboxed.url); + loggerEntries.unshift(separator); + if ( rowFilterer.filterOne(separator) ) { + filteredLoggerEntries.unshift(separator); + if ( separator.voided ) { + filteredLoggerEntryVoidedCount += 1; + } + } + } + loggerEntries.unshift(parsed); + if ( rowFilterer.filterOne(parsed) ) { + filteredLoggerEntries.unshift(parsed); + if ( parsed.voided ) { + filteredLoggerEntryVoidedCount += 1; + } } } - // Prevent logger from growing infinitely and eating all memory. For - // instance someone could forget that it is left opened for some - // dynamically refreshed pages. - truncateLog(maxEntries); - - // Follow waterfall if not observing top of waterfall. - const yDelta = tbody.offsetHeight - height; - if ( yDelta === 0 ) { return; } - const container = uDom.nodeFromSelector('#netInspector .vscrollable'); - if ( container.scrollTop !== 0 ) { - container.scrollTop += yDelta; + const addedCount = filteredLoggerEntries.length - previousCount; + if ( addedCount !== 0 ) { + viewPort.updateContent(addedCount); + rowJanitor.inserted(addedCount); } }; /******************************************************************************/ -let updateCurrentTabTitle = (function() { - let i18nCurrentTab = vAPI.i18n('loggerCurrentTab'); +const parseLogEntry = function(details) { + const entry = new LogEntry(details); + + // Assemble the text content, i.e. the pre-built string which will be + // used to match logger output filtering expressions. + const textContent = []; + + // Cell 0 + logDate.setTime(details.tstamp - logDateTimezoneOffset); + textContent.push( + padTo2(logDate.getUTCHours()) + ':' + + padTo2(logDate.getUTCMinutes()) + ':' + + padTo2(logDate.getSeconds()) + ); + + // Cell 1 + if ( details.realm === 'message' ) { + textContent.push(details.text); + entry.textContent = textContent.join('\t'); + return entry; + } + + // Cell 1, 2 + if ( entry.filter !== undefined ) { + textContent.push(entry.filter.raw); + if ( entry.filter.result === 1 ) { + textContent.push('--'); + } else if ( entry.filter.result === 2 ) { + textContent.push('++'); + } else if ( entry.filter.result === 3 ) { + textContent.push('**'); + } else if ( entry.filter.source === 'redirect' ) { + textContent.push('<<'); + } else { + textContent.push(''); + } + } else { + textContent.push('', ''); + } + + // Cell 3 + textContent.push(normalizeToStr(entry.docHostname)); + + // Cell 4 + if ( + entry.realm === 'network' && + typeof entry.domain === 'string' && + entry.domain !== '' + ) { + let partyness = ''; + if ( entry.tabDomain !== undefined ) { + partyness += entry.domain === entry.tabDomain ? '1' : '3'; + } else { + partyness += '?'; + } + if ( entry.docDomain !== entry.tabDomain ) { + partyness += ','; + if ( entry.docDomain !== undefined ) { + partyness += entry.domain === entry.docDomain ? '1' : '3'; + } else { + partyness += '?'; + } + } + textContent.push(partyness); + } else { + textContent.push(''); + } + + // Cell 5 + textContent.push( + normalizeToStr(prettyRequestTypes[entry.type] || entry.type) + ); + + // Cell 6 + textContent.push(normalizeToStr(details.url)); + + entry.textContent = textContent.join('\t'); + return entry; +}; + +/******************************************************************************/ + +const viewPort = (function() { + const vwRenderer = document.getElementById('vwRenderer'); + const vwScroller = document.getElementById('vwScroller'); + const vwVirtualContent = document.getElementById('vwVirtualContent'); + const vwContent = document.getElementById('vwContent'); + const vwLineSizer = document.getElementById('vwLineSizer'); + const vwLogEntryTemplate = document.querySelector('#logEntryTemplate > div'); + const vwEntries = []; + + let vwHeight = 0; + let lineHeight = 0; + let wholeHeight = 0; + let lastTopPix = 0; + let lastTopRow = 0; + let scrollTimer; + let resizeTimer; + + const ViewEntry = function() { + this.div = document.createElement('div'); + this.div.className = 'logEntry'; + vwContent.appendChild(this.div); + this.logEntry = undefined; + }; + ViewEntry.prototype = { + dispose: function() { + vwContent.removeChild(this.div); + }, + }; + + const rowFromScrollTopPix = function(px) { + return lineHeight !== 0 ? Math.floor(px / lineHeight) : 0; + }; + + // This is called when the browser fired scroll events + const onScrollChanged = function() { + const newScrollTopPix = vwScroller.scrollTop; + const delta = newScrollTopPix - lastTopPix; + if ( delta === 0 ) { return; } + lastTopPix = newScrollTopPix; + if ( filteredLoggerEntries.length <= 2 ) { return; } + // No entries were rolled = all entries keep their current details + if ( rollLines(rowFromScrollTopPix(newScrollTopPix)) ) { + fillLines(); + } + positionLines(); + vwContent.style.top = `${lastTopPix}px`; + }; + + // Coallesce scroll events + const onScroll = function() { + if ( scrollTimer !== undefined ) { return; } + scrollTimer = setTimeout( + ( ) => { + scrollTimer = requestAnimationFrame(( ) => { + scrollTimer = undefined; + onScrollChanged(); + }); + }, + 1000/32 + ); + }; + + vwScroller.addEventListener('scroll', onScroll, { passive: true }); + + const onLayoutChanged = function() { + vwHeight = vwRenderer.clientHeight; + vwContent.style.height = `${vwScroller.clientHeight}px`; + + const vExpanded = + uDom.nodeFromSelector('#netInspector .vCompactToggler') + .classList + .contains('vExpanded'); + + let newLineHeight = + vwLineSizer.querySelector('.oneLine').clientHeight; + + if ( vExpanded ) { + newLineHeight *= loggerSettings.linesPerEntry; + } + + const lineCount = newLineHeight !== 0 + ? Math.ceil(vwHeight / newLineHeight) + 1 + : 0; + if ( lineCount > vwEntries.length ) { + do { + vwEntries.push(new ViewEntry()); + } while ( lineCount > vwEntries.length ); + } else if ( lineCount < vwEntries.length ) { + do { + vwEntries.pop().dispose(); + } while ( lineCount < vwEntries.length ); + } + + const cellWidths = Array.from( + vwLineSizer.querySelectorAll('.oneLine span') + ).map((el, i) => { + return loggerSettings.columns[i] !== false + ? el.clientWidth + 1 + : 0; + }); + const reservedWidth = + cellWidths[0] + cellWidths[2] + cellWidths[4] + cellWidths[5]; + cellWidths[6] = 0.5; + if ( cellWidths[1] === 0 && cellWidths[3] === 0 ) { + cellWidths[6] = 1; + } else if ( cellWidths[1] === 0 ) { + cellWidths[3] = 0.35; + cellWidths[6] = 0.65; + } else if ( cellWidths[3] === 0 ) { + cellWidths[1] = 0.35; + cellWidths[6] = 0.65; + } else { + cellWidths[1] = 0.25; + cellWidths[3] = 0.25; + cellWidths[6] = 0.5; + } + const style = document.getElementById('vwRendererRuntimeStyles'); + const cssRules = [ + '#vwContent .logEntry {', + ` height: ${newLineHeight}px;`, + '}', + '#vwContent .logEntry > div > span:nth-of-type(1) {', + ` width: ${cellWidths[0]}px;`, + '}', + '#vwContent .logEntry > div > span:nth-of-type(2) {', + ` width: calc(calc(100% - ${reservedWidth}px) * ${cellWidths[1]});`, + '}', + '#vwContent .logEntry > div.messageRealm > span:nth-of-type(2) {', + ` width: calc(100% - ${cellWidths[0]}px);`, + '}', + '#vwContent .logEntry > div > span:nth-of-type(3) {', + ` width: ${cellWidths[2]}px;`, + '}', + '#vwContent .logEntry > div > span:nth-of-type(4) {', + ` width: calc(calc(100% - ${reservedWidth}px) * ${cellWidths[3]});`, + '}', + '#vwContent .logEntry > div > span:nth-of-type(5) {', + ` width: ${cellWidths[4]}px;`, + '}', + '#vwContent .logEntry > div > span:nth-of-type(6) {', + ` width: ${cellWidths[5]}px;`, + '}', + '#vwContent .logEntry > div > span:nth-of-type(7) {', + ` width: calc(calc(100% - ${reservedWidth}px) * ${cellWidths[6]});`, + '}', + '', + ]; + for ( let i = 0; i < cellWidths.length; i++ ) { + if ( cellWidths[i] !== 0 ) { continue; } + cssRules.push( + `#vwContent .logEntry > div > span:nth-of-type(${i + 1}) {`, + ' display: none;', + '}' + ); + } + style.textContent = cssRules.join('\n'); + + lineHeight = newLineHeight; + positionLines(); + uDom.nodeFromId('netInspector') + .classList + .toggle('vExpanded', vExpanded); + + updateContent(0); + }; + + const updateLayout = function() { + if ( resizeTimer !== undefined ) { return; } + resizeTimer = setTimeout( + ( ) => { + resizeTimer = requestAnimationFrame(( ) => { + resizeTimer = undefined; + onLayoutChanged(); + }); + }, + 1000/8 + ); + }; + + window.addEventListener('resize', updateLayout, { passive: true }); + + updateLayout(); + + const renderToDiv = function(vwEntry, i) { + if ( i >= filteredLoggerEntries.length ) { + vwEntry.logEntry = undefined; + return null; + } + + const details = filteredLoggerEntries[i]; + if ( vwEntry.logEntry === details ) { + return vwEntry.div.firstElementChild; + } + + vwEntry.logEntry = details; + + const cells = details.textContent.split('\t'); + const div = vwLogEntryTemplate.cloneNode(true); + const divcl = div.classList; + let span; + + + // Realm + if ( details.realm !== undefined ) { + divcl.add(details.realm + 'Realm'); + } + + // Timestamp + span = div.children[0]; + span.textContent = cells[0]; + + // Tab id + if ( details.tabId !== undefined ) { + div.setAttribute('data-tabid', details.tabId); + if ( details.voided ) { + divcl.add('voided'); + } + } + + if ( details.realm === 'message' ) { + if ( details.type !== undefined ) { + div.setAttribute('data-type', details.type); + } + span = div.children[1]; + span.textContent = cells[1]; + return div; + } + + if ( details.realm === 'network' || details.realm === 'cosmetic' ) { + divcl.add('canDetails'); + } + + // Filter + const filter = details.filter || undefined; + let filteringType; + if ( filter !== undefined ) { + if ( typeof filter.source === 'string' ) { + filteringType = filter.source; + divcl.add(filteringType); + } + if ( filteringType === 'static' ) { + divcl.add('canLookup'); + div.setAttribute('data-filter', filter.compiled); + } else if ( filteringType === 'cosmetic' ) { + divcl.add('canLookup'); + } + } + span = div.children[1]; + span.textContent = cells[1]; + + // Event + if ( cells[2] === '--' ) { + divcl.add('blocked'); + } else if ( cells[2] === '++' ) { + divcl.add('allowed'); + } else if ( cells[2] === '**' ) { + span.add('nooped'); + } else if ( cells[2] === '<<' ) { + divcl.add('redirect'); + } + span = div.children[2]; + span.textContent = cells[2]; + + // Origins + if ( details.tabHostname ) { + div.setAttribute('data-tabhn', details.tabHostname); + } + if ( details.docHostname ) { + div.setAttribute('data-dochn', details.docHostname); + } + span = div.children[3]; + span.textContent = cells[3]; + + // Partyness + if ( + cells[4] !== '' && + details.realm === 'network' && + details.domain !== undefined + ) { + let text = `${details.tabDomain}`; + if ( details.docDomain !== details.tabDomain ) { + text += ` \u22ef ${details.docDomain}`; + } + text += ` \u21d2 ${details.domain}`; + div.setAttribute('data-parties', text); + } + span = div.children[4]; + span.textContent = cells[4]; + + // Type + span = div.children[5]; + span.textContent = cells[5]; + + // URL + let re = null; + if ( filteringType === 'static' ) { + re = new RegExp(filter.regex, 'gi'); + } else if ( filteringType === 'dynamicUrl' ) { + re = regexFromURLFilteringResult(filter.rule.join(' ')); + } + span = div.children[6]; + span.appendChild(nodeFromURL(cells[6], re)); + + return div; + }; + + // The idea is that positioning DOM elements is faster than + // removing/inserting DOM elements. + const positionLines = function() { + if ( lineHeight === 0 ) { return; } + let y = -(lastTopPix % lineHeight); + for ( const vwEntry of vwEntries ) { + vwEntry.div.style.top = `${y}px`; + y += lineHeight; + } + }; + + const rollLines = function(topRow) { + let delta = topRow - lastTopRow; + let deltaLength = Math.abs(delta); + // No point rolling if no rows can be reused + if ( deltaLength > 0 && deltaLength < vwEntries.length ) { + if ( delta < 0 ) { // Move bottom rows to the top + vwEntries.unshift(...vwEntries.splice(delta)); + } else { // Move top rows to the bottom + vwEntries.push(...vwEntries.splice(0, delta)); + } + } + lastTopRow = topRow; + return delta; + }; + + const fillLines = function() { + let rowBeg = lastTopRow; + for ( const vwEntry of vwEntries ) { + const newDiv = renderToDiv(vwEntry, rowBeg); + const container = vwEntry.div; + const oldDiv = container.firstElementChild; + if ( newDiv !== null ) { + if ( oldDiv === null ) { + container.appendChild(newDiv); + } else if ( newDiv !== oldDiv ) { + container.removeChild(oldDiv); + container.appendChild(newDiv); + } + } else if ( oldDiv !== null ) { + container.removeChild(oldDiv); + } + rowBeg += 1; + } + }; + + const contentChanged = function(addedCount) { + lastTopRow += addedCount; + const newWholeHeight = Math.max( + filteredLoggerEntries.length * lineHeight, + vwRenderer.clientHeight + ); + if ( newWholeHeight !== wholeHeight ) { + vwVirtualContent.style.height = `${newWholeHeight}px`; + wholeHeight = newWholeHeight; + } + }; + + const updateContent = function(addedCount) { + contentChanged(addedCount); + // Content changed + if ( addedCount === 0 ) { + if ( + lastTopRow !== 0 && + lastTopRow + vwEntries.length > filteredLoggerEntries.length + ) { + lastTopRow = filteredLoggerEntries.length - vwEntries.length; + if ( lastTopRow < 0 ) { lastTopRow = 0; } + lastTopPix = lastTopRow * lineHeight; + vwContent.style.top = `${lastTopPix}px`; + vwScroller.scrollTop = lastTopPix; + positionLines(); + } + fillLines(); + return; + } + + // Content added + // Preserve scroll position + if ( lastTopPix === 0 ) { + rollLines(0); + positionLines(); + fillLines(); + return; + } + + // Preserve row position + lastTopPix += lineHeight * addedCount; + vwContent.style.top = `${lastTopPix}px`; + vwScroller.scrollTop = lastTopPix; + }; + + return { updateContent, updateLayout, }; +})(); + +/******************************************************************************/ + +const updateCurrentTabTitle = (function() { + const i18nCurrentTab = vAPI.i18n('loggerCurrentTab'); return function() { - let select = uDom.nodeFromId('pageSelector'); - if ( select.value !== 'tab_active' ) { return; } - let opt0 = select.querySelector('[value="tab_active"]'); - let opt1 = select.querySelector('[value="tab_' + activeTabId + '"]'); + const select = uDom.nodeFromId('pageSelector'); + if ( select.value !== '_' || activeTabId === 0 ) { return; } + const opt0 = select.querySelector('[value="_"]'); + const opt1 = select.querySelector('[value="' + activeTabId + '"]'); let text = i18nCurrentTab; if ( opt1 !== null ) { text += ' / ' + opt1.textContent; @@ -427,42 +795,61 @@ let updateCurrentTabTitle = (function() { const synchronizeTabIds = function(newTabIds) { const select = uDom.nodeFromId('pageSelector'); - const selectValue = select.value; + const selectedTabValue = select.value; const oldTabIds = allTabIds; - const autoDeleteVoidRows = selectValue === 'tab_active'; - let rowVoided = false; + + // Collate removed tab ids. + const toVoid = new Set(); for ( const tabId of oldTabIds.keys() ) { if ( newTabIds.has(tabId) ) { continue; } - // Mark or remove voided rows - const trs = uDom('.tab_' + tabId); - if ( autoDeleteVoidRows ) { - toJunkyard(trs); - } else { - trs.addClass('void'); - rowVoided = true; + toVoid.add(tabId); + } + allTabIds = newTabIds; + + // Mark as "void" all logger entries which are linked to now invalid + // tab ids. + // When an entry is voided without being removed, we re-create a new entry + // in order to ensure the entry has a new identity. A new identify ensures + // that identity-based associations elsewhere are automatically + // invalidated. + if ( toVoid.size !== 0 ) { + const autoDeleteVoidedRows = selectedTabValue === '_'; + let rowVoided = false; + for ( let i = 0, n = loggerEntries.length; i < n; i++ ) { + const entry = loggerEntries[i]; + if ( toVoid.has(entry.tabId) === false ) { continue; } + if ( entry.voided ) { continue; } + rowVoided = entry.voided = true; + if ( autoDeleteVoidedRows ) { + entry.dead = true; + } + loggerEntries[i] = new LogEntry(entry); } - // Remove popup if it is currently bound to a removed tab. - if ( tabId === popupManager.tabId ) { - popupManager.toggleOff(); + if ( rowVoided ) { + rowFilterer.filterAll(); } } + // Remove popup if it is currently bound to a removed tab. + if ( toVoid.has(popupManager.tabId) ) { + popupManager.toggleOff(); + } + const tabIds = Array.from(newTabIds.keys()).sort(function(a, b) { return newTabIds.get(a).localeCompare(newTabIds.get(b)); }); let j = 3; for ( let i = 0; i < tabIds.length; i++ ) { const tabId = tabIds[i]; - if ( tabId < 0 ) { continue; } - let option = select.options[j]; - if ( !option ) { - option = document.createElement('option'); - select.appendChild(option); + if ( tabId <= 0 ) { continue; } + if ( j === select.options.length ) { + select.appendChild(document.createElement('option')); } + const option = select.options[j]; // Truncate too long labels. option.textContent = newTabIds.get(tabId).slice(0, 80); - option.value = classNameFromTabId(tabId); - if ( option.value === selectValue ) { + option.setAttribute('value', tabId); + if ( option.value === selectedTabValue ) { select.selectedIndex = j; option.setAttribute('selected', ''); } else { @@ -473,33 +860,14 @@ const synchronizeTabIds = function(newTabIds) { while ( j < select.options.length ) { select.removeChild(select.options[j]); } - if ( select.value !== selectValue ) { + if ( select.value !== selectedTabValue ) { select.selectedIndex = 0; select.value = ''; select.options[0].setAttribute('selected', ''); pageSelectorChanged(); } - allTabIds = newTabIds; - updateCurrentTabTitle(); - - return rowVoided; -}; - -/******************************************************************************/ - -var truncateLog = function(size) { - if ( size === 0 ) { - size = 5000; - } - var tbody = document.querySelector('#netInspector tbody'); - size = Math.min(size, 10000); - var tr; - while ( tbody.childElementCount > size ) { - tr = tbody.lastElementChild; - trJunkyard.push(tbody.removeChild(tr)); - } }; /******************************************************************************/ @@ -525,20 +893,13 @@ const onLogBufferRead = function(response) { activeTabId = response.activeTabId; } - // This may have changed meanwhile - if ( response.maxEntries !== maxEntries ) { - maxEntries = response.maxEntries; - uDom('#maxEntries').val(maxEntries || ''); - } - if ( Array.isArray(response.tabIds) ) { response.tabIds = new Map(response.tabIds); } - // Neuter rows for which a tab does not exist anymore - let rowVoided = false; + // List of tab ids has changed if ( response.tabIds !== undefined ) { - rowVoided = synchronizeTabIds(response.tabIds); + synchronizeTabIds(response.tabIds); allTabIdsToken = response.tabIdsToken; } @@ -547,20 +908,21 @@ const onLogBufferRead = function(response) { } if ( netInspectorPaused === false ) { - renderLogEntries(response); + processLoggerEntries(response); } - if ( rowVoided ) { - uDom('#clean').toggleClass( - 'disabled', - tbody.querySelector('#netInspector tr[data-tabid].void') === null - ); - } - - // Synchronize toolbar with content of log - uDom('#clear').toggleClass( + // Synchronize DOM with sent logger data + document.body.classList.toggle( + 'colorBlind', + response.colorBlind === true + ); + uDom.nodeFromId('clean').classList.toggle( 'disabled', - tbody.querySelector('tr') === null + filteredLoggerEntryVoidedCount === 0 + ); + uDom.nodeFromId('clear').classList.toggle( + 'disabled', + filteredLoggerEntries.length === 0 ); }; @@ -616,81 +978,58 @@ const readLogBuffer = (function() { /******************************************************************************/ -let pageSelectorChanged = function() { - let select = uDom.nodeFromId('pageSelector'); +const pageSelectorChanged = function() { + const select = uDom.nodeFromId('pageSelector'); window.location.replace('#' + select.value); pageSelectorFromURLHash(); }; -let pageSelectorFromURLHash = (function() { - let lastTabClass = ''; - let lastEffectiveTabClass = ''; - let reActiveTabId = /^(tab_[^+]+)\+(.+)$/; - - let selectRows = function(tabClass) { - let effectiveTabClass = tabClass; - if ( tabClass === 'tab_active' ) { - if ( activeTabId === undefined ) { return; } - effectiveTabClass = 'tab_' + activeTabId; - } - if ( effectiveTabClass === lastEffectiveTabClass ) { return; } - lastEffectiveTabClass = effectiveTabClass; - - document.dispatchEvent(new Event('tabIdChanged')); - - let style = uDom.nodeFromId('tabFilterer'); - let sheet = style.sheet; - while ( sheet.cssRules.length !== 0 ) { - sheet.deleteRule(0); - } - if ( effectiveTabClass === '' ) { return; } - sheet.insertRule( - '#netInspector tr:not(.' + effectiveTabClass + '):not(.tab_bts) ' + - '{display:none;}', - 0 - ); - - updateCurrentTabTitle(); - }; +const pageSelectorFromURLHash = (function() { + let lastHash; + let lastSelectedTabId; return function() { - let tabClass = window.location.hash.slice(1); - let match = reActiveTabId.exec(tabClass); + let hash = window.location.hash.slice(1); + let match = /^([^+]+)\+(.+)$/.exec(hash); if ( match !== null ) { - tabClass = match[1]; - activeTabId = parseInt(match[2], 10) || undefined; - window.location.hash = '#' + match[1]; - } - selectRows(tabClass); - if ( tabClass === lastTabClass ) { return; } - lastTabClass = tabClass; - - let select = uDom.nodeFromId('pageSelector'); - let option = select.querySelector('option[value="' + tabClass + '"]'); - if ( option === null ) { - window.location.hash = ''; - tabClass = ''; - option = select.options[0]; + hash = match[1]; + activeTabId = parseInt(match[2], 10) || 0; + window.location.hash = '#' + hash; } - select.selectedIndex = option.index; - select.value = option.value; + if ( hash !== lastHash ) { + const select = uDom.nodeFromId('pageSelector'); + let option = select.querySelector( + 'option[value="' + hash + '"]' + ); + if ( option === null ) { + hash = '0'; + option = select.options[0]; + } + select.selectedIndex = option.index; + select.value = option.value; + lastHash = hash; + } - uDom('.needdom').toggleClass( - 'disabled', - tabClass === '' || tabClass === 'tab_bts' - ); - uDom('.needscope').toggleClass( - 'disabled', - tabClass === '' - ); + selectedTabId = hash === '_' + ? activeTabId + : parseInt(hash, 10) || 0; + + if ( lastSelectedTabId === selectedTabId ) { return; } + + rowFilterer.filterAll(); + document.dispatchEvent(new Event('tabIdChanged')); + updateCurrentTabTitle(); + uDom('.needdom').toggleClass('disabled', selectedTabId <= 0); + uDom('.needscope').toggleClass('disabled', selectedTabId <= 0); + lastSelectedTabId = selectedTabId; }; })(); /******************************************************************************/ -var reloadTab = function(ev) { - var tabId = tabIdFromPageSelector(); +const reloadTab = function(ev) { + const tabId = tabIdFromPageSelector(); if ( tabId <= 0 ) { return; } messaging.send('loggerUI', { what: 'reloadTab', @@ -699,86 +1038,60 @@ var reloadTab = function(ev) { }); }; -/******************************************************************************/ - -var onMaxEntriesChanged = function() { - var input = this; - try { - maxEntries = parseInt(input.value, 10); - if ( maxEntries === 0 || isNaN(maxEntries) ) { - maxEntries = 1000; - } - } catch (e) { - maxEntries = 1000; - } - - maxEntries = Math.min(maxEntries, 5000); - maxEntries = Math.max(maxEntries, 10); - - input.value = maxEntries.toString(10); - - messaging.send( - 'loggerUI', - { - what: 'userSettings', - name: 'requestLogMaxEntries', - value: maxEntries - } - ); - - truncateLog(maxEntries); -}; - /******************************************************************************/ /******************************************************************************/ -var netFilteringManager = (function() { - var targetRow = null; - var dialog = null; - var createdStaticFilters = {}; +const netFilteringManager = (function() { + const reRFC3986 = /^([^:\/?#]+:)?(\/\/[^\/?#]*)?([^?#]*)(\?[^#]*)?(#.*)?/; + const staticFilterTypes = { + 'beacon': 'other', + 'doc': 'document', + 'css': 'stylesheet', + 'frame': 'subdocument', + 'ping': 'other', + 'object_subrequest': 'object', + 'xhr': 'xmlhttprequest' + }; + const createdStaticFilters = {}; - var targetType; - var targetURLs = []; - var targetFrameHostname; - var targetPageHostname; - var targetTabId; - var targetDomain; - var targetPageDomain; - var targetFrameDomain; + let dialog = null; + let targetRow = null; + let targetType; + let targetURLs = []; + let targetFrameHostname; + let targetPageHostname; + let targetTabId; + let targetDomain; + let targetPageDomain; + let targetFrameDomain; - var uglyTypeFromSelector = function(pane) { - var prettyType = selectValue('select.type.' + pane); + const uglyTypeFromSelector = function(pane) { + const prettyType = selectValue('select.type.' + pane); if ( pane === 'static' ) { return staticFilterTypes[prettyType] || prettyType; } return uglyRequestTypes[prettyType] || prettyType; }; - var selectNode = function(selector) { + const selectNode = function(selector) { return dialog.querySelector(selector); }; - var selectValue = function(selector) { + const selectValue = function(selector) { return selectNode(selector).value || ''; }; - var staticFilterNode = function() { - return dialog.querySelector('div.containers > div.static textarea'); + const staticFilterNode = function() { + return dialog.querySelector('div.panes > div.static textarea'); }; - var onColorsReady = function(response) { + const onColorsReady = function(response) { document.body.classList.toggle('dirty', response.dirty); - var colorEntries = response.colors; - var colorEntry, node; - for ( var url in colorEntries ) { - if ( colorEntries.hasOwnProperty(url) === false ) { - continue; - } - colorEntry = colorEntries[url]; - node = dialog.querySelector('.dynamic .entry .action[data-url="' + url + '"]'); - if ( node === null ) { - continue; - } + for ( const url in response.colors ) { + if ( response.colors.hasOwnProperty(url) === false ) { continue; } + const colorEntry = response.colors[url]; + const node = dialog.querySelector('.dynamic .entry .action[data-url="' + url + '"]'); + if ( node === null ) { continue; } node.classList.toggle('allow', colorEntry.r === 2); node.classList.toggle('noop', colorEntry.r === 3); node.classList.toggle('block', colorEntry.r === 1); @@ -786,7 +1099,7 @@ var netFilteringManager = (function() { } }; - var colorize = function() { + const colorize = function() { messaging.send( 'loggerUI', { @@ -799,14 +1112,14 @@ var netFilteringManager = (function() { ); }; - var parseStaticInputs = function() { - var filter = '', - options = [], - block = selectValue('select.static.action') === ''; + const parseStaticInputs = function() { + const options = []; + const block = selectValue('select.static.action') === ''; + let filter = ''; if ( !block ) { filter = '@@'; } - var value = selectValue('select.static.url'); + let value = selectValue('select.static.url'); if ( value !== '' ) { if ( value.slice(-1) === '/' ) { value += '*'; @@ -838,44 +1151,28 @@ var netFilteringManager = (function() { updateWidgets(); }; - var updateWidgets = function() { - var value = staticFilterNode().value; + const updateWidgets = function() { + const value = staticFilterNode().value; dialog.querySelector('#createStaticFilter').classList.toggle( 'disabled', createdStaticFilters.hasOwnProperty(value) || value === '' ); }; - var onClick = function(ev) { - var target = ev.target; - - // click outside the dialog proper - if ( target.classList.contains('modalDialog') ) { - toggleOff(); - return; - } - - ev.stopPropagation(); - - var tcl = target.classList; - var value; + const onClick = function(ev) { + const target = ev.target; + const tcl = target.classList; // Select a mode if ( tcl.contains('header') ) { - if ( tcl.contains('selected') ) { - return; - } - uDom('.header').removeClass('selected'); - uDom('.container').removeClass('selected'); - value = target.getAttribute('data-container'); - uDom('.header.' + value).addClass('selected'); - uDom('.container.' + value).addClass('selected'); + dialog.setAttribute('data-pane', target.getAttribute('data-pane') ); + ev.stopPropagation(); return; } // Create static filter if ( target.id === 'createStaticFilter' ) { - value = staticFilterNode().value; + const value = staticFilterNode().value; // Avoid duplicates if ( createdStaticFilters.hasOwnProperty(value) ) { return; @@ -894,6 +1191,7 @@ var netFilteringManager = (function() { ); } updateWidgets(); + ev.stopPropagation(); return; } @@ -909,10 +1207,11 @@ var netFilteringManager = (function() { }, colorize ); + ev.stopPropagation(); return; } - var persist = !!ev.ctrlKey || !!ev.metaKey; + const persist = !!ev.ctrlKey || !!ev.metaKey; // Remove url filtering rule if ( tcl.contains('action') ) { @@ -928,6 +1227,7 @@ var netFilteringManager = (function() { }, colorize ); + ev.stopPropagation(); return; } @@ -945,6 +1245,7 @@ var netFilteringManager = (function() { }, colorize ); + ev.stopPropagation(); return; } @@ -962,6 +1263,7 @@ var netFilteringManager = (function() { }, colorize ); + ev.stopPropagation(); return; } @@ -979,6 +1281,7 @@ var netFilteringManager = (function() { }, colorize ); + ev.stopPropagation(); return; } @@ -991,6 +1294,7 @@ var netFilteringManager = (function() { tabId: targetTabId } ); + ev.stopPropagation(); return; } @@ -1005,13 +1309,13 @@ var netFilteringManager = (function() { select: true } ); + ev.stopPropagation(); return; } }; - var onSelectChange = function(ev) { - var target = ev.target; - var tcl = target.classList; + const onSelectChange = function(ev) { + const tcl = ev.target.classList; if ( tcl.contains('dynamic') ) { colorize(); @@ -1024,42 +1328,47 @@ var netFilteringManager = (function() { } }; - var onInputChange = function() { + const onInputChange = function() { updateWidgets(); }; - var createPreview = function(type, url) { - // First, whether picker can be used + const createPreview = function(type, url) { + const cantPreview = + type !== 'image' || + targetRow.classList.contains('networkRealm') === false || + targetRow.classList.contains('blocked'); + + // Whether picker can be used dialog.querySelector('.picker').classList.toggle( 'hide', - targetTabId < 0 || - targetType !== 'image' || - /(?:^| )[dlsu]b(?: |$)/.test(targetRow.className) + targetTabId < 0 || cantPreview ); - var preview = null; + // Whether the resource can be previewed + if ( cantPreview ) { return; } - if ( type === 'image' ) { - preview = document.createElement('img'); - preview.setAttribute('src', url); - } + const container = dialog.querySelector('.preview'); + container.querySelector('span').addEventListener( + 'click', + ( ) => { + const preview = document.createElement('img'); + preview.setAttribute('src', url); + container.replaceChild(preview, container.firstElementChild); + }, + { once: true } + ); - var container = dialog.querySelector('div.preview'); - container.classList.toggle('hide', preview === null); - if ( preview === null ) { - return; - } - container.appendChild(preview); + container.classList.remove('hide'); }; // https://github.com/gorhill/uBlock/issues/1511 - var shortenLongString = function(url, max) { - var urlLen = url.length; + const shortenLongString = function(url, max) { + const urlLen = url.length; if ( urlLen <= max ) { return url; } - var n = urlLen - max - 1; - var i = (urlLen - n) / 2 | 0; + const n = urlLen - max - 1; + const i = (urlLen - n) / 2 | 0; return url.slice(0, i) + '…' + url.slice(i + n); }; @@ -1091,14 +1400,153 @@ var netFilteringManager = (function() { return urls; }; + const fillSummaryPaneFilterList = function(row) { + const nodeFromFilter = function(filter, lists) { + if ( Array.isArray(lists) === false || lists.length === 0 ) { + return; + } + const fragment = document.createDocumentFragment(); + const template = document.querySelector( + '#filterFinderListEntry > span' + ); + for ( const list of lists ) { + const span = template.cloneNode(true); + let a = span.querySelector('a:nth-of-type(1)'); + a.href += encodeURIComponent(list.assetKey); + a.textContent = list.title; + if ( list.supportURL ) { + a = span.querySelector('a:nth-of-type(2)'); + a.setAttribute('href', list.supportURL); + } + if ( fragment.childElementCount !== 0 ) { + fragment.appendChild(document.createTextNode('\n')); + } + fragment.appendChild(span); + } + return fragment; + }; + + const handleResponse = function(response) { + if ( response instanceof Object === false ) { + response = {}; + } + const fragment = document.createDocumentFragment(); + for ( const filter in response ) { + const spans = nodeFromFilter(filter, response[filter]); + if ( spans === undefined ) { continue; } + fragment.appendChild(spans); + } + row.children[1].appendChild(fragment); + // https://github.com/gorhill/uBlock/issues/2179 + if ( row.children[1].childElementCount === 0 ) { + vAPI.i18n.safeTemplateToDOM( + 'loggerStaticFilteringFinderSentence2', + { filter: rawFilter }, + row.children[1] + ); + } + }; + const rawFilter = targetRow.children[1].textContent; + const compiledFilter = targetRow.getAttribute('data-filter'); + + if ( targetRow.classList.contains('networkRealm') ) { + messaging.send( + 'loggerUI', + { + what: 'listsFromNetFilter', + compiledFilter: compiledFilter, + rawFilter: rawFilter + }, + handleResponse + ); + } else if ( targetRow.classList.contains('cosmeticRealm') ) { + messaging.send( + 'loggerUI', + { + what: 'listsFromCosmeticFilter', + url: targetRow.children[6].textContent, + rawFilter: rawFilter, + }, + handleResponse + ); + } + }; + + const fillSummaryPane = function() { + const rows = dialog.querySelectorAll('.pane.details > div'); + const tr = targetRow; + const trcl = tr.classList; + const trch = tr.children; + let text; + // Filter and context + text = trch[1].textContent; + if ( + (text !== '') && + (trcl.contains('cosmetic') || trcl.contains('static')) + ) { + rows[0].children[1].textContent = text; + } else { + rows[0].style.display = 'none'; + } + // Rule + if ( + (text !== '') && + (trcl.contains('dynamicHost') || trcl.contains('dynamicUrl')) + ) { + rows[2].children[1].textContent = text; + } else { + rows[2].style.display = 'none'; + } + // Filter list + if ( trcl.contains('canLookup') ) { + fillSummaryPaneFilterList(rows[1]); + } else { + rows[1].style.display = 'none'; + } + // Root and immediate contexts + const tabhn = tr.getAttribute('data-tabhn') || ''; + const dochn = tr.getAttribute('data-dochn') || ''; + if ( tabhn !== '' && tabhn !== dochn ) { + rows[3].children[1].textContent = tabhn; + } else { + rows[3].style.display = 'none'; + } + if ( dochn !== '' ) { + rows[4].children[1].textContent = dochn; + } else { + rows[4].style.display = 'none'; + } + // Partyness + text = tr.getAttribute('data-parties') || ''; + if ( text !== '' ) { + rows[5].children[1].textContent = `${trch[4].textContent}\u2002${text}`; + } else { + rows[5].style.display = 'none'; + } + // Type + text = trch[5].textContent; + if ( text !== '' ) { + rows[6].children[1].textContent = text; + } else { + rows[6].style.display = 'none'; + } + // URL + text = trch[6].textContent; + if ( text !== '' ) { + rows[7].children[1].appendChild(trch[6].cloneNode(true)); + } else { + rows[7].style.display = 'none'; + } + }; + // Fill dynamic URL filtering pane - var fillDynamicPane = function() { - var select; + const fillDynamicPane = function() { + if ( targetRow.classList.contains('cosmeticRealm') ) { return; } + // Fill context selector - select = selectNode('select.dynamic.origin'); - removeAllChildren(select); + let select = selectNode('select.dynamic.origin'); fillOriginSelect(select, targetPageHostname, targetPageDomain); - var option = document.createElement('option'); + const option = document.createElement('option'); option.textContent = '*'; option.setAttribute('value', '*'); select.appendChild(option); @@ -1110,56 +1558,50 @@ var netFilteringManager = (function() { select.selectedIndex = 0; // Fill entries - var menuEntryTemplate = dialog.querySelector('table.toolbar tr.entry'); - var tbody = dialog.querySelector('div.dynamic table.entries tbody'); - var url, menuEntry; - for ( var i = 0; i < targetURLs.length; i++ ) { - url = targetURLs[i]; - menuEntry = menuEntryTemplate.cloneNode(true); - menuEntry.cells[0].children[0].setAttribute('data-url', url); - menuEntry.cells[1].textContent = shortenLongString(url, 128); + const menuEntryTemplate = dialog.querySelector('.dynamic .toolbar .entry'); + const tbody = dialog.querySelector('.dynamic .entries'); + for ( let i = 0; i < targetURLs.length; i++ ) { + const url = targetURLs[i]; + const menuEntry = menuEntryTemplate.cloneNode(true); + menuEntry.children[0].setAttribute('data-url', url); + menuEntry.children[1].textContent = shortenLongString(url, 128); tbody.appendChild(menuEntry); } colorize(); }; - var fillOriginSelect = function(select, hostname, domain) { - var option, pos; - var template = vAPI.i18n('loggerStaticFilteringSentencePartOrigin'); - var value = hostname; + const fillOriginSelect = function(select, hostname, domain) { + const template = vAPI.i18n('loggerStaticFilteringSentencePartOrigin'); + let value = hostname; for (;;) { - option = document.createElement('option'); + const option = document.createElement('option'); option.setAttribute('value', value); option.textContent = template.replace('{{origin}}', value); select.appendChild(option); - if ( value === domain ) { - break; - } - pos = value.indexOf('.'); - if ( pos === -1 ) { - break; - } + if ( value === domain ) { break; } + const pos = value.indexOf('.'); + if ( pos === -1 ) { break; } value = value.slice(pos + 1); } }; // Fill static filtering pane - var fillStaticPane = function() { - var template = vAPI.i18n('loggerStaticFilteringSentence'); - var rePlaceholder = /\{\{[^}]+?\}\}/g; - var nodes = []; - var match, pos = 0; - var select, option, n, i, value; + const fillStaticPane = function() { + if ( targetRow.classList.contains('cosmeticRealm') ) { return; } + + const template = vAPI.i18n('loggerStaticFilteringSentence'); + const rePlaceholder = /\{\{[^}]+?\}\}/g; + const nodes = []; + let pos = 0; for (;;) { - match = rePlaceholder.exec(template); - if ( match === null ) { - break; - } + const match = rePlaceholder.exec(template); + if ( match === null ) { break; } if ( pos !== match.index ) { nodes.push(document.createTextNode(template.slice(pos, match.index))); } pos = rePlaceholder.lastIndex; + let select, option; switch ( match[0] ) { case '{{br}}': nodes.push(document.createElement('br')); @@ -1196,8 +1638,8 @@ var netFilteringManager = (function() { case '{{url}}': select = document.createElement('select'); select.className = 'static url'; - for ( i = 0, n = targetURLs.length; i < n; i++ ) { - value = targetURLs[i].replace(/^[a-z-]+:\/\//, ''); + for ( let i = 0, n = targetURLs.length; i < n; i++ ) { + const value = targetURLs[i].replace(/^[a-z-]+:\/\//, ''); option = document.createElement('option'); option.setAttribute('value', value); option.textContent = shortenLongString(value, 128); @@ -1238,34 +1680,46 @@ var netFilteringManager = (function() { if ( pos < template.length ) { nodes.push(document.createTextNode(template.slice(pos))); } - var parent = dialog.querySelector('div.containers > div.static > p:first-of-type'); - removeAllChildren(parent); - for ( i = 0; i < nodes.length; i++ ) { + const parent = dialog.querySelector('div.panes > .static > div:first-of-type'); + for ( let i = 0; i < nodes.length; i++ ) { parent.appendChild(nodes[i]); } parseStaticInputs(); }; - var fillDialog = function(domains) { + const fillDialog = function(domains) { + dialog = modalDialog.create( + '#netFilteringDialog', + ( ) => { + targetURLs = []; + targetRow = null; + dialog = null; + } + ); + dialog.classList.toggle( + 'cosmeticRealm', + targetRow.classList.contains('cosmeticRealm') + ); targetDomain = domains[0]; targetPageDomain = domains[1]; targetFrameDomain = domains[2]; - createPreview(targetType, targetURLs[0]); + fillSummaryPane(); fillDynamicPane(); fillStaticPane(); - document.body.appendChild(netFilteringDialog); - netFilteringDialog.addEventListener('click', onClick, true); - netFilteringDialog.addEventListener('change', onSelectChange, true); - netFilteringDialog.addEventListener('input', onInputChange, true); + dialog.addEventListener('click', onClick, true); + dialog.addEventListener('change', onSelectChange, true); + dialog.addEventListener('input', onInputChange, true); + modalDialog.show(); }; - var toggleOn = function(ev) { - dialog = netFilteringDialog.querySelector('.dialog'); - targetRow = ev.target.parentElement; - targetTabId = tabIdFromClassName(targetRow.className); - targetType = targetRow.cells[5].textContent.trim() || ''; - targetURLs = createTargetURLs(targetRow.cells[6].textContent); + const toggleOn = function(ev) { + targetRow = ev.target.closest('.canDetails'); + if ( targetRow === null ) { return; } + ev.stopPropagation(); + targetTabId = tabIdFromAttribute(targetRow); + targetType = targetRow.children[5].textContent.trim() || ''; + targetURLs = createTargetURLs(targetRow.children[6].textContent); targetPageHostname = targetRow.getAttribute('data-tabhn') || ''; targetFrameHostname = targetRow.getAttribute('data-dochn') || ''; @@ -1280,21 +1734,7 @@ var netFilteringManager = (function() { ); }; - var toggleOff = function() { - removeAllChildren(dialog.querySelector('div.preview')); - removeAllChildren(dialog.querySelector('div.dynamic table.entries tbody')); - dialog = null; - targetRow = null; - targetURLs = []; - netFilteringDialog.removeEventListener('click', onClick, true); - netFilteringDialog.removeEventListener('change', onSelectChange, true); - netFilteringDialog.removeEventListener('input', onInputChange, true); - document.body.removeChild(netFilteringDialog); - }; - - return { - toggleOn: toggleOn - }; + return { toggleOn }; })(); // https://www.youtube.com/watch?v=XyNYrmmdUd4 @@ -1302,129 +1742,11 @@ var netFilteringManager = (function() { /******************************************************************************/ /******************************************************************************/ -var reverseLookupManager = (function() { - let filterFinderDialog = uDom.nodeFromId('filterFinderDialog'); - let rawFilter = ''; - - let removeAllChildren = function(node) { - while ( node.firstChild ) { - node.removeChild(node.firstChild); - } - }; - - // Clicking outside the dialog will close the dialog - let onClick = function(ev) { - if ( ev.target.classList.contains('modalDialog') ) { - toggleOff(); - return; - } - - ev.stopPropagation(); - }; - - let nodeFromFilter = function(filter, lists) { - if ( Array.isArray(lists) === false || lists.length === 0 ) { - return; - } - - let p = document.createElement('p'); - - vAPI.i18n.safeTemplateToDOM( - 'loggerStaticFilteringFinderSentence1', - { filter: filter }, - p - ); - - let ul = document.createElement('ul'); - for ( let list of lists ) { - let li = document.querySelector('#filterFinderListEntry > li') - .cloneNode(true); - let a = li.querySelector('a:nth-of-type(1)'); - a.href += encodeURIComponent(list.assetKey); - a.textContent = list.title; - if ( list.supportURL ) { - a = li.querySelector('a:nth-of-type(2)'); - a.setAttribute('href', list.supportURL); - } - ul.appendChild(li); - } - p.appendChild(ul); - - return p; - }; - - let reverseLookupDone = function(response) { - if ( response instanceof Object === false ) { - response = {}; - } - - let dialog = filterFinderDialog.querySelector('.dialog'); - removeAllChildren(dialog); - - for ( let filter in response ) { - let p = nodeFromFilter(filter, response[filter]); - if ( p === undefined ) { continue; } - dialog.appendChild(p); - } - - // https://github.com/gorhill/uBlock/issues/2179 - if ( dialog.childElementCount === 0 ) { - vAPI.i18n.safeTemplateToDOM( - 'loggerStaticFilteringFinderSentence2', - { filter: rawFilter }, - dialog - ); - } - - document.body.appendChild(filterFinderDialog); - filterFinderDialog.addEventListener('click', onClick, true); - }; - - let toggleOn = function(ev) { - let row = ev.target.parentElement; - rawFilter = row.cells[1].textContent; - if ( rawFilter === '' ) { return; } - - if ( row.classList.contains('cat_net') ) { - messaging.send( - 'loggerUI', - { - what: 'listsFromNetFilter', - compiledFilter: row.getAttribute('data-filter') || '', - rawFilter: rawFilter - }, - reverseLookupDone - ); - } else if ( row.classList.contains('cat_cosmetic') ) { - messaging.send( - 'loggerUI', - { - what: 'listsFromCosmeticFilter', - url: row.cells[6].textContent, - rawFilter: rawFilter, - }, - reverseLookupDone - ); - } - }; - - let toggleOff = function() { - filterFinderDialog.removeEventListener('click', onClick, true); - document.body.removeChild(filterFinderDialog); - rawFilter = ''; - }; - - return { - toggleOn: toggleOn - }; -})(); - -/******************************************************************************/ -/******************************************************************************/ - const rowFilterer = (function() { const userFilters = []; const builtinFilters = []; + + let masterFilterSwitch = true; let filters = []; const parseInput = function() { @@ -1493,52 +1815,54 @@ const rowFilterer = (function() { filters = builtinFilters.concat(userFilters); }; - const filterOne = function(tr, clean) { - if ( filters.length === 0 && clean === true ) { return; } - // do not filter out doc boundaries, they help separate important - // section of log. - const cl = tr.classList; - if ( cl.contains('maindoc') ) { return; } - if ( filters.length === 0 ) { - cl.remove('f'); - return; + const filterOne = function(logEntry) { + if ( + logEntry.dead || + selectedTabId !== 0 && + logEntry.tabId !== undefined && + logEntry.tabId > 0 && + logEntry.tabId !== selectedTabId + ) { + return false; } - const cc = tr.cells; - const ccount = cc.length; - // each filter expression must hit (implicit and-op) - // if... - // positive filter expression = there must one hit on any field - // negative filter expression = there must be no hit on all fields + + if ( masterFilterSwitch === false || filters.length === 0 ) { + return true; + } + + // Do not filter out tab load event, they help separate key sections + // of logger. + if ( logEntry.type === 'tabLoad' ) { return true; } + for ( const f of filters ) { - let hit = !f.r; - for ( let j = 1; j < ccount; j++ ) { - if ( f.re.test(cc[j].textContent) ) { - hit = f.r; - break; - } - } - if ( !hit ) { - cl.add('f'); - return; - } + if ( f.re.test(logEntry.textContent) !== f.r ) { return false; } } - cl.remove('f'); + return true; }; const filterAll = function() { - const filterCount = filters.length; + filteredLoggerEntries = []; + filteredLoggerEntryVoidedCount = 0; + for ( const entry of loggerEntries ) { + if ( filterOne(entry) === false ) { continue; } + filteredLoggerEntries.push(entry); + if ( entry.voided ) { + filteredLoggerEntryVoidedCount += 1; + } + } + viewPort.updateContent(0); uDom.nodeFromId('filterButton').classList.toggle( 'active', - filterCount !== 0 + filters.length !== 0 + ); + uDom.nodeFromId('clean').classList.toggle( + 'disabled', + filteredLoggerEntryVoidedCount === 0 + ); + uDom.nodeFromId('clear').classList.toggle( + 'disabled', + filteredLoggerEntries.length === 0 ); - // Special case: no filter - if ( filterCount === 0 ) { - uDom('#netInspector tr').removeClass('f'); - return; - } - for ( const row of document.querySelector('#netInspector tbody').rows ) { - filterOne(row); - } }; const onFilterChangedAsync = (function() { @@ -1557,7 +1881,12 @@ const rowFilterer = (function() { })(); const onFilterButton = function() { - uDom.nodeFromId('netInspector').classList.toggle('f'); + masterFilterSwitch = !masterFilterSwitch; + uDom.nodeFromId('netInspector').classList.toggle( + 'f', + masterFilterSwitch + ); + filterAll(); }; const onToggleExtras = function(ev) { @@ -1611,60 +1940,207 @@ const rowFilterer = (function() { parseInput(); filterAll(); - return { - filterOne, - filterAll, - }; + return { filterOne, filterAll }; })(); /******************************************************************************/ -const toJunkyard = function(trs) { - trs.remove(); - var i = trs.length; - while ( i-- ) { - trJunkyard.push(trs.nodeAt(i)); - } -}; +// Discard logger entries to prevent undue memory usage growth. The criteria +// to discard are multiple and user configurable: +// +// - Max number of page load per distinct tab +// - Max number of entry per distinct tab +// - Max entry age -/******************************************************************************/ +const rowJanitor = (function() { + const tabIdToDiscard = new Set(); + const tabIdToLoadCountMap = new Map(); + const tabIdToEntryCountMap = new Map(); -var clearBuffer = function() { - var tabClass = uDom.nodeFromId('pageSelector').value; - var btsAlso = tabClass === '' || tabClass === 'tab_bts'; - var tbody = document.querySelector('#netInspector tbody'); - var tr = tbody.lastElementChild; - var trPrevious; - while ( tr !== null ) { - trPrevious = tr.previousElementSibling; - if ( - (tr.clientHeight > 0) && - (tr.classList.contains('tab_bts') === false || btsAlso) - ) { - trJunkyard.push(tbody.removeChild(tr)); + let rowIndex = 0; + + const discard = function(timeRemaining) { + const opts = loggerSettings.discard; + const maxLoadCount = typeof opts.maxLoadCount === 'number' + ? opts.maxLoadCount + : 0; + const maxEntryCount = typeof opts.maxEntryCount === 'number' + ? opts.maxEntryCount + : 0; + const obsolete = typeof opts.maxAge === 'number' + ? Date.now() - opts.maxAge * 60000 + : 0; + const deadline = Date.now() + Math.ceil(timeRemaining); + + let i = rowIndex; + // TODO: below should not happen -- remove when confirmed. + if ( i >= loggerEntries.length ) { + i = 0; } - tr = trPrevious; - } - uDom.nodeFromId('clear').classList.toggle( - 'disabled', - tbody.childElementCount === 0 - ); - uDom.nodeFromId('clean').classList.toggle( - 'disabled', - tbody.querySelector('#netInspector tr[data-tabid].void') === null - ); -}; -/******************************************************************************/ + if ( i === 0 ) { + tabIdToDiscard.clear(); + tabIdToLoadCountMap.clear(); + tabIdToEntryCountMap.clear(); + } -var cleanBuffer = function() { - var rows = uDom('#netInspector tr[data-tabid].void').remove(); - var i = rows.length; - while ( i-- ) { - trJunkyard.push(rows.nodeAt(i)); - } - uDom('#clean').addClass('disabled'); -}; + let idel = -1; + let bufferedTabId = 0; + let bufferedEntryCount = 0; + let modified = false; + + while ( i < loggerEntries.length ) { + + if ( i % 64 === 0 && Date.now() >= deadline ) { break; } + + const entry = loggerEntries[i]; + const tabId = entry.tabId || 0; + + if ( entry.dead || tabIdToDiscard.has(tabId) ) { + if ( idel === -1 ) { idel = i; } + i += 1; + continue; + } + + if ( maxLoadCount !== 0 && entry.type === 'tabLoad' ) { + let count = (tabIdToLoadCountMap.get(tabId) || 0) + 1; + tabIdToLoadCountMap.set(tabId, count); + if ( count >= maxLoadCount ) { + tabIdToDiscard.add(tabId); + } + } + + if ( maxEntryCount !== 0 ) { + if ( bufferedTabId !== tabId ) { + if ( bufferedEntryCount !== 0 ) { + tabIdToEntryCountMap.set(bufferedTabId, bufferedEntryCount); + } + bufferedTabId = tabId; + bufferedEntryCount = tabIdToEntryCountMap.get(tabId) || 0; + } + bufferedEntryCount += 1; + if ( bufferedEntryCount >= maxEntryCount ) { + tabIdToDiscard.add(bufferedTabId); + } + } + + // Since entries in the logger are chronologically ordered, + // everything below obsolete is to be discarded. + if ( obsolete !== 0 && entry.tstamp <= obsolete ) { + if ( idel === -1 ) { idel = i; } + break; + } + + if ( idel !== -1 ) { + loggerEntries.copyWithin(idel, i); + loggerEntries.length -= i - idel; + idel = -1; + modified = true; + } + + i += 1; + } + + if ( idel !== -1 ) { + loggerEntries.length = idel; + modified = true; + } + + if ( i >= loggerEntries.length ) { i = 0; } + rowIndex = i; + + if ( rowIndex === 0 ) { + tabIdToDiscard.clear(); + tabIdToLoadCountMap.clear(); + tabIdToEntryCountMap.clear(); + } + + if ( modified === false ) { return; } + + rowFilterer.filterAll(); + }; + + const discardAsync = function() { + setTimeout( + ( ) => { + self.requestIdleCallback(deadline => { + discard(deadline.timeRemaining()); + discardAsync(); + }); + }, + 1889 + ); + }; + + // Clear voided entries from the logger's visible content. + // + // Voided entries should be visible only from the "All" option of the + // tab selector. + // + const clean = function() { + if ( filteredLoggerEntries.length === 0 ) { return; } + + let j = 0; + let targetEntry = filteredLoggerEntries[0]; + for ( const entry of loggerEntries ) { + if ( entry !== targetEntry ) { continue; } + if ( entry.voided ) { + entry.dead = true; + } + j += 1; + if ( j === filteredLoggerEntries.length ) { break; } + targetEntry = filteredLoggerEntries[j]; + } + rowFilterer.filterAll(); + }; + + // Clear the logger's visible content. + // + // "Unrelated" entries -- shown for convenience -- will be also cleared + // if and only if the filtered logger content is made entirely of unrelated + // entries. In effect, this means clicking a second time on the eraser will + // cause unrelated entries to also be cleared. + // + const clear = function() { + if ( filteredLoggerEntries.length === 0 ) { return; } + + let clearUnrelated = true; + if ( selectedTabId !== 0 ) { + for ( const entry of filteredLoggerEntries ) { + if ( entry.tabId === selectedTabId ) { + clearUnrelated = false; + break; + } + } + } + + let j = 0; + let targetEntry = filteredLoggerEntries[0]; + for ( const entry of loggerEntries ) { + if ( entry !== targetEntry ) { continue; } + if ( entry.tabId === selectedTabId || clearUnrelated ) { + entry.dead = true; + } + j += 1; + if ( j === filteredLoggerEntries.length ) { break; } + targetEntry = filteredLoggerEntries[j]; + } + rowFilterer.filterAll(); + }; + + discardAsync(); + + uDom.nodeFromId('clean').addEventListener('click', clean); + uDom.nodeFromId('clear').addEventListener('click', clear); + + return { + inserted: function(count) { + if ( rowIndex !== 0 ) { + rowIndex += count; + } + }, + }; +})(); /******************************************************************************/ @@ -1677,12 +2153,10 @@ const pauseNetInspector = function() { /******************************************************************************/ const toggleVCompactView = function() { - uDom.nodeFromId('netInspector').classList.toggle('vCompact'); - uDom('#netInspector .vExpanded').toggleClass('vExpanded'); -}; - -const toggleVCompactRow = function(ev) { - ev.target.parentElement.classList.toggle('vExpanded'); + uDom.nodeFromSelector('#netInspector .vCompactToggler') + .classList + .toggle('vExpanded'); + viewPort.updateLayout(); }; /******************************************************************************/ @@ -1735,7 +2209,7 @@ const popupManager = (function() { const parent = uDom.nodeFromId('inspectors'); const rect = parent.getBoundingClientRect(); - popup.style.setProperty('right', (rect.right - parent.clientWidth) + 'px'); + popup.style.setProperty('right', `${rect.right - parent.clientWidth}px`); parent.classList.add('popupOn'); document.addEventListener('tabIdChanged', onTabIdChanged); @@ -1756,7 +2230,8 @@ const popupManager = (function() { realTabId = 0; }; - const exports = { + const api = { + get tabId() { return realTabId || 0; }, toggleOff: function() { if ( realTabId !== 0 ) { toggleOff(); @@ -1764,15 +2239,132 @@ const popupManager = (function() { } }; - Object.defineProperty(exports, 'tabId', { - get: function() { return realTabId || 0; } - }); + uDom.nodeFromId('showpopup').addEventListener( + 'click', + ( ) => { + void (realTabId === 0 ? toggleOn() : toggleOff()); + } + ); - uDom('#showpopup').on('click', ( ) => { - void (realTabId === 0 ? toggleOn() : toggleOff()); - }); + return api; +})(); - return exports; +/******************************************************************************/ + +// TODO: +// - Give some thoughts to: +// - an option to discard immediately filtered out new entries +// - max entry count _per load_ +// +const loggerSettings = (function() { + const settings = { + discard: { + maxAge: 240, // global + maxEntryCount: 2000, // per-tab + maxLoadCount: 20, // per-tab + }, + columns: [ true, true, true, true, true, true, true, true ], + linesPerEntry: 4, + }; + + { + try { + const stored = JSON.parse(vAPI.localStorage.getItem('loggerSettings')); + if ( typeof stored.discard.maxAge === 'number' ) { + settings.discard.maxAge = stored.discard.maxAge; + } + if ( typeof stored.discard.maxEntryCount === 'number' ) { + settings.discard.maxEntryCount = stored.discard.maxEntryCount; + } + if ( typeof stored.discard.maxLoadCount === 'number' ) { + settings.discard.maxLoadCount = stored.discard.maxLoadCount; + } + if ( typeof stored.linesPerEntry === 'number' ) { + settings.linesPerEntry = stored.linesPerEntry; + } + if ( Array.isArray(stored.columns) ) { + settings.columns = stored.columns; + } + } catch(ex) { + } + } + + const valueFromInput = function(input, def) { + let value = parseInt(input.value, 10); + if ( isNaN(value) ) { value = def; } + const min = parseInt(input.getAttribute('min'), 10); + if ( isNaN(min) === false ) { + value = Math.max(value, min); + } + const max = parseInt(input.getAttribute('max'), 10); + if ( isNaN(max) === false ) { + value = Math.min(value, max); + } + return value; + }; + + const toggleOn = function() { + const dialog = modalDialog.create( + '#loggerSettingsDialog', + dialog => { + toggleOff(dialog); + } + ); + + // Number inputs + let inputs = dialog.querySelectorAll('input[type="number"]'); + inputs[0].value = settings.discard.maxAge; + inputs[1].value = settings.discard.maxLoadCount; + inputs[2].value = settings.discard.maxEntryCount; + inputs[3].value = settings.linesPerEntry; + inputs[3].addEventListener('input', ev => { + settings.linesPerEntry = valueFromInput(ev.target, 4); + viewPort.updateLayout(); + }); + + // Column checkboxs + const onColumnChanged = ev => { + const input = ev.target; + const i = parseInt(input.getAttribute('data-column'), 10); + settings.columns[i] = input.checked !== true; + viewPort.updateLayout(); + }; + inputs = dialog.querySelectorAll('input[type="checkbox"][data-column]'); + for ( const input of inputs ) { + const i = parseInt(input.getAttribute('data-column'), 10); + input.checked = settings.columns[i] === false; + input.addEventListener('change', onColumnChanged); + } + + modalDialog.show(); + }; + + const toggleOff = function(dialog) { + // Number inputs + let inputs = dialog.querySelectorAll('input[type="number"]'); + settings.discard.maxAge = valueFromInput(inputs[0], 240); + settings.discard.maxLoadCount = valueFromInput(inputs[1], 25); + settings.discard.maxEntryCount = valueFromInput(inputs[2], 2000); + settings.linesPerEntry = valueFromInput(inputs[3], 4); + + // Column checkboxs + inputs = dialog.querySelectorAll('input[type="checkbox"][data-column]'); + for ( const input of inputs ) { + const i = parseInt(input.getAttribute('data-column'), 10); + settings.columns[i] = input.checked !== true; + } + + vAPI.localStorage.setItem( + 'loggerSettings', + JSON.stringify(settings) + ); + + viewPort.updateLayout(); + }; + + uDom.nodeFromId('settings').addEventListener('click', toggleOn); + + return settings; })(); /******************************************************************************/ @@ -1835,16 +2427,16 @@ uDom('#pageSelector').on('change', pageSelectorChanged); uDom('#refresh').on('click', reloadTab); uDom('#netInspector .vCompactToggler').on('click', toggleVCompactView); -uDom('#clean').on('click', cleanBuffer); -uDom('#clear').on('click', clearBuffer); uDom('#pause').on('click', pauseNetInspector); -uDom('#maxEntries').on('change', onMaxEntriesChanged); -uDom('#netInspector table').on('click', 'tr > td:nth-of-type(1)', toggleVCompactRow); -uDom('#netInspector').on('click', 'tr.canLookup > td:nth-of-type(2)', reverseLookupManager.toggleOn); -uDom('#netInspector').on('click', 'tr.cat_net > td:nth-of-type(3)', netFilteringManager.toggleOn); + +uDom('#netInspector').on( + 'click', + 'span:nth-of-type(2),span:nth-of-type(3),span:nth-of-type(5)', + netFilteringManager.toggleOn +); // https://github.com/gorhill/uBlock/issues/507 -// Ensure tab selector is in sync with URL hash +// Ensure tab selector is in sync with URL hash pageSelectorFromURLHash(); window.addEventListener('hashchange', pageSelectorFromURLHash); @@ -1852,16 +2444,21 @@ window.addEventListener('hashchange', pageSelectorFromURLHash); // is loaded, to be sure no spurious geometry changes will be triggered due // to the window geometry pontentially not settling fast enough. if ( self.location.search.includes('popup=1') ) { - window.addEventListener('load', ( ) => { - setTimeout(( ) => { - popupLoggerBox = { - x: self.screenX, - y: self.screenY, - w: self.outerWidth, - h: self.outerHeight, - }; - }, 2000); - }, { once: true }); + window.addEventListener( + 'load', + ( ) => { + setTimeout( + ( ) => { + popupLoggerBox = { + x: self.screenX, + y: self.screenY, + w: self.outerWidth, + h: self.outerHeight, + }; + }, 2000); + }, + { once: true } + ); } /******************************************************************************/ diff --git a/src/js/logger.js b/src/js/logger.js index d9a577546..8b5c05c31 100644 --- a/src/js/logger.js +++ b/src/js/logger.js @@ -26,17 +26,6 @@ µBlock.logger = (function() { - const LogEntry = function(details) { - this.init(details); - }; - - LogEntry.prototype.init = function(details) { - if ( details.tstamp === undefined ) { - details.tstamp = Date.now(); - } - this.details = JSON.stringify(details); - }; - let buffer = null; let lastReadTime = 0; let writePtr = 0; @@ -61,15 +50,22 @@ } }; + const boxEntry = function(details) { + if ( details.tstamp === undefined ) { + details.tstamp = Date.now(); + } + return JSON.stringify(details); + }; + const api = { enabled: false, ownerId: undefined, writeOne: function(details) { if ( buffer === null ) { return; } if ( writePtr === buffer.length ) { - buffer.push(new LogEntry(details)); + buffer.push(boxEntry(details)); } else { - buffer[writePtr].init(details); + buffer[writePtr] = boxEntry(details); } writePtr += 1; }, diff --git a/src/js/messaging.js b/src/js/messaging.js index 0f5d0e9a2..877b3c9a3 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -1398,7 +1398,7 @@ var onMessage = function(request, sender, callback) { .setURL(request.docURL) .setDocOriginFromURL(request.docURL); if ( pageStore.filterRequest(fctxt) === 0 ) { - fctxt.setRealm('net').toLogger(); + fctxt.setRealm('network').toLogger(); } } break; diff --git a/src/js/pagestore.js b/src/js/pagestore.js index 255a6e09d..a3e2e6a46 100644 --- a/src/js/pagestore.js +++ b/src/js/pagestore.js @@ -322,7 +322,7 @@ PageStore.prototype.init = function(tabId, context) { µb.logger.enabled && context === 'tabCommitted' ) { - fctxt.setRealm('net') + fctxt.setRealm('network') .setType('generichide') .setFilter(µb.staticNetFilteringEngine.toLogData()) .toLogger(); diff --git a/src/js/popup.js b/src/js/popup.js index 2829f7403..b2c78b01e 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -753,7 +753,7 @@ const gotoURL = function(ev) { let url = this.getAttribute('href'); if ( - url === 'logger-ui.html#tab_active' && + url === 'logger-ui.html#_' && typeof popupData.tabId === 'number' ) { url += '+' + popupData.tabId; diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index dc8382d48..59518f056 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -2192,7 +2192,9 @@ FilterContainer.prototype.compile = function(raw, writer) { if ( parsed.unsupported ) { const who = writer.properties.get('assetKey') || '?'; µb.logger.writeOne({ - error: `Invalid network filter in ${who}: ${raw}` + realm: 'message', + type: 'error', + text: `Invalid network filter in ${who}: ${raw}` }); return false; } diff --git a/src/js/tab.js b/src/js/tab.js index ea8ec1e96..64399c5fd 100644 --- a/src/js/tab.js +++ b/src/js/tab.js @@ -789,7 +789,7 @@ vAPI.tabs.onPopupUpdated = (function() { // Log only for when there was a hit against an actual filter (allow or block). // https://github.com/gorhill/uBlock/issues/2776 if ( µb.logger.enabled ) { - fctxt.setRealm('net').setType(popupType); + fctxt.setRealm('network').setType(popupType); if ( popupType === 'popup' ) { fctxt.setURL(targetURL) .setTabId(openerTabId) diff --git a/src/js/traffic.js b/src/js/traffic.js index cc211e1be..000f76e14 100644 --- a/src/js/traffic.js +++ b/src/js/traffic.js @@ -91,7 +91,7 @@ const onBeforeRequest = function(details) { pageStore.journalAddRequest(fctxt.getHostname(), result); if ( µb.logger.enabled ) { - fctxt.setRealm('net').toLogger(); + fctxt.setRealm('network').toLogger(); } // Not blocked @@ -203,7 +203,7 @@ const onBeforeRootFrameRequest = function(fctxt) { } if ( logEnabled ) { - fctxt.setRealm('net').setFilter(logData).toLogger(); + fctxt.setRealm('network').setFilter(logData).toLogger(); } // Not blocked @@ -311,7 +311,7 @@ const onBeforeBehindTheSceneRequest = function(fctxt) { } if ( µb.logger.enabled ) { - fctxt.setRealm('net').toLogger(); + fctxt.setRealm('network').toLogger(); } // Blocked? @@ -396,7 +396,7 @@ const onBeforeMaybeSpuriousCSPReport = (function() { // At this point, we have a potentially spurious CSP report. if ( µBlock.logger.enabled ) { - fctxt.setRealm('net') + fctxt.setRealm('network') .setType('csp_report') .setFilter({ result: 1, source: 'global', raw: 'no-spurious-csp-report' }) .toLogger(); @@ -775,7 +775,7 @@ const injectCSP = function(fctxt, pageStore, responseHeaders) { if ( pageStore.filterScripting(fctxt, true) === 1 ) { builtinDirectives.push("script-src http: https:"); if ( loggerEnabled ) { - fctxt.setRealm('net').setType('scripting').toLogger(); + fctxt.setRealm('network').setType('scripting').toLogger(); } } else { fctxt.type = 'inline-script'; @@ -784,7 +784,7 @@ const injectCSP = function(fctxt, pageStore, responseHeaders) { builtinDirectives.push("script-src 'unsafe-eval' * blob: data:"); } if ( result !== 0 && loggerEnabled ) { - fctxt.setRealm('net').toLogger(); + fctxt.setRealm('network').toLogger(); } } @@ -794,7 +794,7 @@ const injectCSP = function(fctxt, pageStore, responseHeaders) { if ( pageStore.filterRequest(fctxt) === 1 ) { builtinDirectives.push('font-src *'); if ( loggerEnabled ) { - fctxt.setRealm('net').toLogger(); + fctxt.setRealm('network').toLogger(); } } @@ -825,7 +825,7 @@ const injectCSP = function(fctxt, pageStore, responseHeaders) { ) === 2 ) { if ( loggerEnabled ) { - fctxt.setRealm('net') + fctxt.setRealm('network') .setType('csp') .setFilter(µb.sessionURLFiltering.toLogData()) .toLogger(); @@ -844,7 +844,7 @@ const injectCSP = function(fctxt, pageStore, responseHeaders) { ) === 2 ) { if ( loggerEnabled ) { - fctxt.setRealm('net') + fctxt.setRealm('network') .setType('csp') .setFilter(µb.sessionFirewall.toLogData()) .toLogger(); @@ -856,7 +856,7 @@ const injectCSP = function(fctxt, pageStore, responseHeaders) { // Static CSP policies will be applied. if ( logDataEntries !== undefined ) { - fctxt.setRealm('net').setType('csp'); + fctxt.setRealm('network').setType('csp'); for ( const entry of logDataEntries ) { fctxt.setFilter(entry).toLogger(); } @@ -908,7 +908,7 @@ const foilLargeMediaElement = function(fctxt, pageStore, responseHeaders) { if ( result === 0 ) { return; } if ( µBlock.logger.enabled ) { - fctxt.setRealm('net').toLogger(); + fctxt.setRealm('network').toLogger(); } return { cancel: true }; diff --git a/src/js/udom.js b/src/js/udom.js index dc85e6b21..af8fd2c04 100644 --- a/src/js/udom.js +++ b/src/js/udom.js @@ -132,12 +132,9 @@ var addSelectorToList = function(list, selector, context) { /******************************************************************************/ -var nodeInNodeList = function(node, nodeList) { - var i = nodeList.length; - while ( i-- ) { - if ( nodeList[i] === node ) { - return true; - } +const nodeInNodeList = function(node, nodeList) { + for ( const other of nodeList ) { + if ( other === node ) { return true; } } return false; }; @@ -603,9 +600,9 @@ DOMList.prototype.toggleClasses = function(classNames, targetState) { /******************************************************************************/ -var listenerEntries = []; +const listenerEntries = []; -var ListenerEntry = function(target, type, capture, callback) { +const ListenerEntry = function(target, type, capture, callback) { this.target = target; this.type = type; this.capture = capture; @@ -621,13 +618,16 @@ ListenerEntry.prototype.dispose = function() { /******************************************************************************/ -var makeEventHandler = function(selector, callback) { +const makeEventHandler = function(selector, callback) { return function(event) { - var dispatcher = event.currentTarget; - if ( !dispatcher || typeof dispatcher.querySelectorAll !== 'function' ) { + const dispatcher = event.currentTarget; + if ( + dispatcher instanceof HTMLElement === false || + typeof dispatcher.querySelectorAll !== 'function' + ) { return; } - var receiver = event.target; + const receiver = event.target; if ( nodeInNodeList(receiver, dispatcher.querySelectorAll(selector)) ) { callback.call(receiver, event); } @@ -642,9 +642,10 @@ DOMList.prototype.on = function(etype, selector, callback) { callback = makeEventHandler(selector, callback); } - var i = this.nodes.length; - while ( i-- ) { - listenerEntries.push(new ListenerEntry(this.nodes[i], etype, selector !== undefined, callback)); + for ( const node of this.nodes ) { + listenerEntries.push( + new ListenerEntry(node, etype, selector !== undefined, callback) + ); } return this; }; diff --git a/src/logger-ui.html b/src/logger-ui.html index 70a7a6fee..bdb4b64df 100644 --- a/src/logger-ui.html +++ b/src/logger-ui.html @@ -10,14 +10,15 @@ +

refresh code @@ -26,7 +27,7 @@