Remove code:false mode; show sloppy regexps in popup

* Now that our own pages retrieve the styles directly via getStylesSafe the only 0.001% of cases where code:false would be needed (the browser is starting up with some of the tabs showing our built-in pages like editor or manage) is not worth optimizing for.

* According to CSS4 @document specification the entire URL must match. Stylish-for-Chrome implemented it incorrectly since the very beginning. We detect styles that abuse the bug by finding the sections that would have been applied by Stylish but not by us as we follow the spec. Additionally we'll check for invalid regexps.
This commit is contained in:
tophf 2017-03-31 02:18:41 +03:00
parent 4bc7b55b91
commit f8d13d8dec
9 changed files with 487 additions and 218 deletions

View File

@ -12,6 +12,7 @@ globals:
# messaging.js # messaging.js
OWN_ORIGIN: false OWN_ORIGIN: false
KEEP_CHANNEL_OPEN: false KEEP_CHANNEL_OPEN: false
RX_SUPPORTED_URLS: false
configureCommands: false configureCommands: false
notifyAllTabs: false notifyAllTabs: false
refreshAllTabs: false refreshAllTabs: false

View File

@ -402,6 +402,20 @@
"message": "Invalid regexps skipped", "message": "Invalid regexps skipped",
"description": "RegExp test report: label for the invalid expressions" "description": "RegExp test report: label for the invalid expressions"
}, },
"styleRegexpPartialExplanation": {
"message": "This style uses partially matching regexps in violation of <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>CSS4 @document specification</a> which requires a full URL match. The affected CSS sections were not applied to the page. This style was probably created in Stylish-for-Chrome which incorrectly checks 'regexp()' rules since the very first version (known bug)."
},
"styleRegexpInvalidExplanation": {
"message": "Some 'regexp()' rules that could not be compiled at all."
},
"styleNotAppliedRegexpProblemTooltip": {
"message": "Style was not applied due to its incorrect usage of 'regexp()'",
"description": "Tooltip in the popup for styles that were not applied at all"
},
"styleRegexpProblemTooltip": {
"message": "Number of sections not applied due to incorrect usage of 'regexp()'",
"description": "Tooltip in the popup for styles that were applied only partially"
},
"styleBeautify": { "styleBeautify": {
"message": "Beautify", "message": "Beautify",
"description": "Label for the CSS-beautifier button on the edit style page" "description": "Label for the CSS-beautifier button on the edit style page"

View File

@ -70,7 +70,7 @@ function importFromString(jsonString) {
continue; continue;
} }
item.name = item.name.trim(); item.name = item.name.trim();
const byId = (cachedStyles.byId.get(item.id) || {}).style; const byId = cachedStyles.byId.get(item.id);
const byName = oldStylesByName.get(item.name); const byName = oldStylesByName.get(item.name);
const oldStyle = byId && byId.name.trim() == item.name || !byName ? byId : byName; const oldStyle = byId && byId.name.trim() == item.name || !byName ? byId : byName;
if (oldStyle == byName && byName) { if (oldStyle == byName && byName) {

View File

@ -7,7 +7,7 @@ const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps'];
const TARGET_LIMIT = 10; const TARGET_LIMIT = 10;
getStylesSafe({code: false}) getStylesSafe()
.then(showStyles) .then(showStyles)
.then(initGlobalEvents); .then(initGlobalEvents);
@ -235,7 +235,7 @@ class EntryOnClick {
static delete(event) { static delete(event) {
const styleElement = getClickedStyleElement(event); const styleElement = getClickedStyleElement(event);
const id = styleElement.styleId; const id = styleElement.styleId;
const name = ((cachedStyles.byId.get(id) || {}).style || {}).name; const {name} = cachedStyles.byId.get(id) || {};
animateElement(styleElement, {className: 'highlight'}); animateElement(styleElement, {className: 'highlight'});
messageBox({ messageBox({
title: t('deleteStyleConfirm'), title: t('deleteStyleConfirm'),
@ -436,7 +436,7 @@ function searchStyles({immediately, container}) {
} }
for (const element of (container || installed).children) { for (const element of (container || installed).children) {
const {style} = cachedStyles.byId.get(element.styleId) || {}; const style = cachedStyles.byId.get(element.styleId) || {};
if (style) { if (style) {
const isMatching = !query const isMatching = !query
|| isMatchingText(style.name) || isMatchingText(style.name)

View File

@ -4,6 +4,7 @@
// keep message channel open for sendResponse in chrome.runtime.onMessage listener // keep message channel open for sendResponse in chrome.runtime.onMessage listener
const KEEP_CHANNEL_OPEN = true; const KEEP_CHANNEL_OPEN = true;
const OWN_ORIGIN = chrome.runtime.getURL(''); const OWN_ORIGIN = chrome.runtime.getURL('');
const RX_SUPPORTED_URLS = new RegExp(`^(file|https?|ftps?):|^${OWN_ORIGIN}`);
function notifyAllTabs(request) { function notifyAllTabs(request) {

304
popup.css
View File

@ -1,29 +1,43 @@
body { body {
width: 252px; width: 252px;
font-size: 12px; font-size: 12px;
font-family: Arial,"Helvetica Neue",Helvetica,sans-serif; font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
margin: 0;
} }
body > div:not(#installed) {
margin-left: 0.75em;
margin-right: 0.75em;
}
input[type=checkbox] { input[type=checkbox] {
outline: none; outline: none;
} }
#disable-all-wrapper { #disable-all-wrapper {
padding: 0.3em 0 0.6em; padding: 0.3em 0 0.6em;
} }
#no-styles { #no-styles {
font-style: italic; font-style: italic;
} }
#popup-shortcuts-button { #popup-shortcuts-button {
margin-left: 3px; margin-left: 3px;
} }
.checker { .checker {
display: inline; display: inline;
} }
.style-name { .style-name {
cursor: default; cursor: default;
font-weight: bold; font-weight: bold;
display: block; display: block;
} }
a, a:visited {
a,
a:visited {
color: black; color: black;
text-decoration-skip: ink; text-decoration-skip: ink;
} }
@ -33,53 +47,151 @@ a, a:visited {
width: 16px; width: 16px;
vertical-align: top; vertical-align: top;
} }
.left-gutter input { .left-gutter input {
margin-bottom: 1px; margin-bottom: 1px;
margin-top: 0; margin-top: 0;
margin-left: 0; margin-left: 0;
} }
.main-controls { .main-controls {
display: table-cell; display: table-cell;
} }
.entry {
padding: 0.5em 0;
}
.entry:first-child {
padding-top: 0;
}
#unavailable, #unavailable,
#installed { #installed {
border-bottom: 1px solid black; border-bottom: 1px solid black;
padding-bottom: 2px; padding-bottom: 2px;
} }
body > DIV:last-of-type, body > DIV:last-of-type,
body.blocked > DIV { body.blocked > DIV {
border-bottom: none; border-bottom: none;
} }
#installed { #installed {
padding-top: 2px; padding-top: 2px;
max-height: 434px; max-height: 434px;
overflow-y: auto; overflow-y: auto;
} }
#installed.disabled .style-name { #installed.disabled .style-name {
text-decoration: line-through; text-decoration: line-through;
} }
#installed .actions { #installed .actions {
cursor: default; cursor: default;
} }
#installed .actions a { #installed .actions a {
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
} }
#installed .style-edit-link {
/* entry */
.entry {
display: flex;
align-items: center;
padding: 5px 0.75em;
}
.entry:nth-child(even) {
background-color: rgba(0, 0, 0, 0.05);
}
.entry .style-edit-link {
margin-right: 2px; margin-right: 2px;
} }
#installed .style-edit-link, #installed .delete {
.entry .style-edit-link,
.entry .delete {
display: inline-block; display: inline-block;
padding: 0 1px 0; padding: 0 1px 0;
} }
.entry .main-controls {
display: flex;
flex: 1;
width: calc(100% - 20px);
align-items: center;
}
.entry .main-controls label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 5px;
}
.not-applied .checker,
.not-applied .style-name,
.not-applied .actions > * {
opacity: .2;
transition: opacity .5s ease-in-out .25s, color .5s ease-in-out .25s;
}
.not-applied:hover .checker,
.not-applied:hover .style-name,
.not-applied:hover .actions > * {
opacity: 1;
}
.not-applied:hover .style-name {
color: darkorange;
}
.regexp-problem-indicator {
background-color: darkorange;
width: 15px;
height: 15px;
line-height: 16px;
border-radius: 8px;
margin-right: 4px;
margin-left: 4px;
text-align: center;
color: white;
font-weight: bold;
box-sizing: border-box;
cursor: pointer;
}
.regexp-partial .actions,
.regexp-invalid .actions {
order: 2;
}
#regexp-explanation {
position: fixed;
background-color: white;
left: 0;
right: 0;
padding: .5rem;
font-size: 90%;
border-top: 2px solid black;
border-bottom: 2px solid black;
box-shadow: 0 0 100px black;
display: flex;
flex-direction: column;
}
#regexp-explanation > div {
display: none;
list-style-type: none;
padding: 0;
margin: 0;
}
.regexp-partial #regexp-partial,
.regexp-invalid #regexp-invalid {
display: block;
}
#regexp-explanation > div:not(:last-child) {
margin-bottom: .5rem;
}
.svg-icon { .svg-icon {
pointer-events: none; pointer-events: none;
transition: fill .5s; transition: fill .5s;
@ -92,20 +204,27 @@ body > .actions {
margin-top: 0.5em; margin-top: 0.5em;
} }
.actions > div:not(:last-child):not(#disable-all-wrapper), .actions > .main-controls > div:not(:last-child), #unavailable:not(:last-child), #unavailable + .actions { .actions > div:not(:last-child):not(#disable-all-wrapper),
.actions > .main-controls > div:not(:last-child),
#unavailable:not(:last-child),
#unavailable + .actions {
margin-bottom: 0.75em; margin-bottom: 0.75em;
} }
.actions input, .actions label {
.actions input,
.actions label {
vertical-align: middle; vertical-align: middle;
} }
#unavailable { #unavailable {
border: none; border: none;
display: none; /* flex */ display: none;
margin-top: 0.75em;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 14px; font-size: 14px;
} }
body.blocked #installed, body.blocked #installed,
body.blocked #find-styles, body.blocked #find-styles,
body.blocked #write-style, body.blocked #write-style,
@ -118,41 +237,84 @@ body.blocked #unavailable {
} }
/* Never shown, but can be enabled with a style */ /* Never shown, but can be enabled with a style */
.enable, .disable {
.enable,
.disable {
display: none; display: none;
} }
/* 'New style' links */ /* 'New style' links */
#write-style-for {margin-right: .6ex}
.write-style-link {margin-left: .6ex} #write-style-for {
.write-style-link::before, .write-style-link::after {font-size: 12px} margin-right: .6ex
.write-style-link::before {content: "\00ad"} /* "soft" hyphen */ }
#match {overflow-wrap: break-word;}
.write-style-link {
margin-left: .6ex
}
.write-style-link::before,
.write-style-link::after {
font-size: 12px
}
.write-style-link::before {
content: "\00ad"; /* "soft" hyphen */
}
#match {
overflow-wrap: break-word;
display: inline-block;
}
/* "breadcrumbs" 'new style' links */ /* "breadcrumbs" 'new style' links */
.breadcrumbs > .write-style-link {margin-left: 0} .breadcrumbs > .write-style-link {
.breadcrumbs:hover a {color: #bbb; text-decoration: none} margin-left: 0
}
.breadcrumbs:hover a {
color: #bbb;
text-decoration: none
}
/* use just the subdomain name instead of the full domain name */
.breadcrumbs > .write-style-link[subdomain]:not(:nth-last-child(2)) {
font-size: 0
}
/* use just the subdomain name instead of the full domain name */
.breadcrumbs > .write-style-link[subdomain]:not(:nth-last-child(2)) {font-size: 0}
.breadcrumbs > .write-style-link[subdomain]:not(:nth-last-child(2))::before { .breadcrumbs > .write-style-link[subdomain]:not(:nth-last-child(2))::before {
content: attr(subdomain); content: attr(subdomain);
} }
/* "dot" after each subdomain name */ /* "dot" after each subdomain name */
.breadcrumbs > .write-style-link[subdomain]::after {content: "."} .breadcrumbs > .write-style-link[subdomain]::after {
/* no "dot" after top-level domain */ content: "."
.breadcrumbs > .write-style-link:nth-last-child(2)::after {content: none} }
/* "forward slash" before path ("this URL") */
.breadcrumbs > .write-style-link:last-child::before {content: "\200b/"} /* no "dot" after top-level domain */
.breadcrumbs > .write-style-link:nth-last-child(2)::after {
content: none
}
/* "forward slash" before path ("this URL") */
.breadcrumbs > .write-style-link:last-child::before {
content: "\200b/"
}
.breadcrumbs > .write-style-link:last-child:first-child::before, .breadcrumbs > .write-style-link:last-child:first-child::before,
.breadcrumbs > .write-style-link[subdomain=""] + .write-style-link::before {content: none} .breadcrumbs > .write-style-link[subdomain=""] + .write-style-link::before {
content: none
}
/* suppress TLD-only link */ /* suppress TLD-only link */
.breadcrumbs > .write-style-link[subdomain=""] {display: none} .breadcrumbs > .write-style-link[subdomain=""] {
display: none
}
/* :hover style */ /* :hover style */
.breadcrumbs.url\(\) > .write-style-link, /* :hover or :focus on "this URL" sets class="url()" */ .breadcrumbs.url\(\) > .write-style-link,
/* :hover or :focus on "this URL" sets class="url()" */
.breadcrumbs > .write-style-link:hover, .breadcrumbs > .write-style-link:hover,
.breadcrumbs > .write-style-link:focus, .breadcrumbs > .write-style-link:focus,
.breadcrumbs > .write-style-link:hover ~ .write-style-link[subdomain], .breadcrumbs > .write-style-link:hover ~ .write-style-link[subdomain],
@ -162,13 +324,15 @@ body.blocked #unavailable {
text-decoration-skip: ink; text-decoration-skip: ink;
} }
/* action buttons */ /* action buttons */
#popup-options { #popup-options {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-around; justify-content: space-around;
padding: 1.2em 0; padding: 1.2em 0;
} }
#popup-options button { #popup-options button {
margin: 0 2px; margin: 0 2px;
width: 33%; width: 33%;
@ -177,72 +341,43 @@ body.blocked #unavailable {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
/* margins */ /* confirm */
body {
margin: 0;
}
body>div:not(#installed) {
margin-left:0.75em;
margin-right:0.75em;
}
#unavailable {
margin-top: 0.75em;
}
#installed .entry {
}
/* entries */
#installed .entry {
display: flex;
align-items: center;
padding: 5px 0.75em;
}
#installed .entry:nth-child(even) {
background-color: rgba(0, 0, 0, 0.05);
}
#installed .main-controls {
display: flex;
flex: 1;
width: calc(100% - 20px);
align-items: center;
}
#installed .main-controls label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 5px;
}
/* confirm */
#confirm, #confirm,
#confirm>div>span { #confirm > div > span {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
#confirm { #confirm {
z-index: 2147483647; z-index: 2147483647;
display: none; /* flex */ display: none;
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: 0!important; margin: 0 !important;
box-sizing: border-box; box-sizing: border-box;
background-color: rgba(0, 0, 0, 0.4); background-color: rgba(0, 0, 0, 0.4);
animation: lights-off .5s cubic-bezier(.03, .67, .08, .94); animation: lights-off .5s cubic-bezier(.03, .67, .08, .94);
animation-fill-mode: both; animation-fill-mode: both;
} }
#confirm.lights-on { #confirm.lights-on {
animation: lights-on .25s ease-in-out; animation: lights-on .25s ease-in-out;
animation-fill-mode: both; animation-fill-mode: both;
} }
#confirm.lights-on > div{
#confirm.lights-on > div {
display: none; display: none;
} }
#confirm[data-display=true] { #confirm[data-display=true] {
display: flex; display: flex;
} }
#confirm>div {
#confirm > div {
width: 80%; width: 80%;
height: 100px; height: 100px;
max-height: 80%; max-height: 80%;
@ -252,25 +387,30 @@ body>div:not(#installed) {
flex-direction: column; flex-direction: column;
border: solid 2px rgba(0, 0, 0, 0.5); border: solid 2px rgba(0, 0, 0, 0.5);
} }
#confirm>div>span {
#confirm > div > span {
display: flex; display: flex;
flex: 1; flex: 1;
padding: 0 10px; padding: 0 10px;
} }
#confirm>div>b {
#confirm > div > b {
padding: 10px; padding: 10px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
#confirm>div>div {
#confirm > div > div {
padding: 10px; padding: 10px;
text-align: center; text-align: center;
} }
.non-windows #confirm>div>div {
.non-windows #confirm > div > div {
direction: rtl; direction: rtl;
text-align: right; text-align: right;
} }
@keyframes lights-off { @keyframes lights-off {
from { from {
background-color: transparent; background-color: transparent;
@ -279,6 +419,7 @@ body>div:not(#installed) {
background-color: rgba(0, 0, 0, 0.4); background-color: rgba(0, 0, 0, 0.4);
} }
} }
@keyframes lights-on { @keyframes lights-on {
from { from {
background-color: rgba(0, 0, 0, 0.4); background-color: rgba(0, 0, 0, 0.4);
@ -287,4 +428,3 @@ body>div:not(#installed) {
background-color: transparent; background-color: transparent;
} }
} }

View File

@ -37,7 +37,19 @@
<div id="no-styles" class="entry" i18n-text="noStylesForSite"></div> <div id="no-styles" class="entry" i18n-text="noStylesForSite"></div>
</template> </template>
<script src="localization.js"></script> <template data-id="regexpProblemIndicator">
<div class="regexp-problem-indicator" i18n-title="styleRegexpProblemTooltip"></div>
</template>
<template data-id="regexpProblemExplanation">
<div id="regexp-explanation">
<div id="regexp-partial" i18n-html="styleRegexpPartialExplanation"></div>
<div id="regexp-invalid" i18n-text="styleRegexpInvalidExplanation"></div>
<button i18n-text="confirmOK"></button>
</div>
</template>
<script src="localization.js"></script>
<script src="health.js"></script> <script src="health.js"></script>
<script src="storage.js"></script> <script src="storage.js"></script>
<script src="messaging.js"></script> <script src="messaging.js"></script>
@ -81,7 +93,7 @@
<a id="find-styles-link" href="#" i18n-text="findStylesForSite"></a> <a id="find-styles-link" href="#" i18n-text="findStylesForSite"></a>
</div> </div>
<div id="write-style"> <div id="write-style">
<span id="write-style-for" i18n-text="writeStyleFor"><br></span> <span id="write-style-for" i18n-text="writeStyleFor"></span>
</div> </div>
</div> </div>
<!-- Actions --> <!-- Actions -->

149
popup.js
View File

@ -1,15 +1,19 @@
/* global SLOPPY_REGEXP_PREFIX, compileStyleRegExps */
'use strict'; 'use strict';
let installed; let installed;
let tabURL;
getActiveTabRealURL().then(url => { getActiveTabRealURL().then(url => {
const RX_SUPPORTED_URLS = new RegExp(`^(file|https?|ftps?):|^${OWN_ORIGIN}`); tabURL = RX_SUPPORTED_URLS.test(url) ? url : '';
const isUrlSupported = RX_SUPPORTED_URLS.test(url);
Promise.all([ Promise.all([
isUrlSupported ? getStylesSafe({matchUrl: url}) : null, tabURL && getStylesSafe({matchUrl: tabURL}),
onDOMready().then(() => initPopup(isUrlSupported ? url : '')), onDOMready().then(() => {
]) initPopup(tabURL);
.then(([styles]) => styles && showStyles(styles)); }),
]).then(([styles]) => {
showStyles(styles);
});
}); });
@ -116,30 +120,55 @@ function initPopup(url) {
function showStyles(styles) { function showStyles(styles) {
if (!styles) {
return;
}
if (!styles.length) { if (!styles.length) {
installed.innerHTML = template.noStyles.outerHTML; installed.innerHTML = template.noStyles.outerHTML;
} else { return;
const enabledFirst = prefs.get('popup.enabledFirst');
styles.sort((a, b) => (
enabledFirst && a.enabled !== b.enabled
? !(a.enabled < b.enabled) ? -1 : 1
: a.name.localeCompare(b.name)
));
const fragment = document.createDocumentFragment();
for (const style of styles) {
fragment.appendChild(createStyleElement(style));
}
installed.appendChild(fragment);
} }
const enabledFirst = prefs.get('popup.enabledFirst');
styles.sort((a, b) => (
enabledFirst && a.enabled !== b.enabled
? !(a.enabled < b.enabled) ? -1 : 1
: a.name.localeCompare(b.name)
));
let postponeDetect = false;
const t0 = performance.now();
const container = document.createDocumentFragment();
for (const style of styles) {
createStyleElement({style, container, postponeDetect});
postponeDetect = postponeDetect || performance.now() - t0 > 100;
}
installed.appendChild(container);
getStylesSafe({matchUrl: tabURL, strictRegexp: false})
.then(unscreenedStyles => {
for (const unscreened of unscreenedStyles) {
if (!styles.includes(unscreened)) {
postponeDetect = postponeDetect || performance.now() - t0 > 100;
createStyleElement({
style: Object.assign({appliedSections: [], postponeDetect}, unscreened),
});
}
}
});
} }
// silence the inapplicable warning for async code // silence the inapplicable warning for async code
/* eslint no-use-before-define: [2, {"functions": false, "classes": false}] */ /* eslint no-use-before-define: [2, {"functions": false, "classes": false}] */
function createStyleElement(style) { function createStyleElement({
style,
container = installed,
postponeDetect,
}) {
const entry = template.style.cloneNode(true); const entry = template.style.cloneNode(true);
entry.setAttribute('style-id', style.id); entry.setAttribute('style-id', style.id);
Object.assign(entry, { Object.assign(entry, {
id: 'style-' + style.id,
styleId: style.id, styleId: style.id,
className: entry.className + ' ' + (style.enabled ? 'enabled' : 'disabled'), className: entry.className + ' ' + (style.enabled ? 'enabled' : 'disabled'),
onmousedown: openEditorOnMiddleclick, onmousedown: openEditorOnMiddleclick,
@ -171,7 +200,18 @@ function createStyleElement(style) {
$('.disable', entry).onclick = EntryOnClick.toggle; $('.disable', entry).onclick = EntryOnClick.toggle;
$('.delete', entry).onclick = EntryOnClick.delete; $('.delete', entry).onclick = EntryOnClick.delete;
return entry; if (postponeDetect) {
setTimeout(detectSloppyRegexps, 0, {entry, style});
} else {
detectSloppyRegexps({entry, style});
}
const oldElement = $('#style-' + style.id);
if (oldElement) {
oldElement.parentNode.replaceChild(entry, oldElement);
} else {
container.appendChild(entry);
}
} }
@ -194,7 +234,7 @@ class EntryOnClick {
const box = $('#confirm'); const box = $('#confirm');
box.dataset.display = true; box.dataset.display = true;
box.style.cssText = ''; box.style.cssText = '';
$('b', box).textContent = ((cachedStyles.byId.get(id) || {}).style || {}).name; $('b', box).textContent = (cachedStyles.byId.get(id) || {}).name;
$('[data-cmd="ok"]', box).onclick = () => confirm(true); $('[data-cmd="ok"]', box).onclick = () => confirm(true);
$('[data-cmd="cancel"]', box).onclick = () => confirm(false); $('[data-cmd="cancel"]', box).onclick = () => confirm(false);
window.onkeydown = event => { window.onkeydown = event => {
@ -219,6 +259,18 @@ class EntryOnClick {
} }
} }
static indicator(event) {
const entry = getClickedStyleElement(event);
const info = template.regexpProblemExplanation.cloneNode(true);
$$('#' + info.id).forEach(el => el.remove());
$$('a', info).forEach(el => (el.onclick = openURLandHide));
$$('button', info).forEach(el => (el.onclick = EntryOnClick.closeExplanation));
entry.appendChild(info);
}
static closeExplanation(event) {
$('#regexp-explanation').remove();
}
} }
@ -264,24 +316,51 @@ function openURLandHide(event) {
function handleUpdate(style) { function handleUpdate(style) {
const styleElement = $(`[style-id="${style.id}"]`, installed); if ($('#style-' + style.id)) {
if (styleElement) { createStyleElement({style});
installed.replaceChild(createStyleElement(style), styleElement); return;
} else { }
getActiveTabRealURL().then(url => { // Add an entry when a new style for the current url is installed
if (getApplicableSections(style, url).length) { if (tabURL && getApplicableSections({style, matchUrl: tabURL, stopOnFirst: true}).length) {
// a new style for the current url is installed $('#unavailable').style.display = 'none';
$('#unavailable').style.display = 'none'; createStyleElement({style});
installed.appendChild(createStyleElement(style));
}
});
} }
} }
function handleDelete(id) { function handleDelete(id) {
const styleElement = $(`[style-id="${id}"]`, installed); $$('#style-' + id).forEach(el => el.remove());
if (styleElement) { }
installed.removeChild(styleElement);
/*
According to CSS4 @document specification the entire URL must match.
Stylish-for-Chrome implemented it incorrectly since the very beginning.
We'll detect styles that abuse the bug by finding the sections that
would have been applied by Stylish but not by us as we follow the spec.
Additionally we'll check for invalid regexps.
*/
function detectSloppyRegexps({entry, style}) {
const {
appliedSections = getApplicableSections({style, matchUrl: tabURL}),
wannabeSections = getApplicableSections({style, matchUrl: tabURL, strictRegexp: false}),
} = style;
compileStyleRegExps({style, compileAll: true});
entry.hasInvalidRegexps = wannabeSections.some(section =>
section.regexps.some(rx => !cachedStyles.regexps.has(rx)));
entry.sectionsSkipped = wannabeSections.length - appliedSections.length;
if (!appliedSections.length) {
entry.classList.add('not-applied');
$('.style-name', entry).title = t('styleNotAppliedRegexpProblemTooltip');
}
if (entry.sectionsSkipped || entry.hasInvalidRegexps) {
entry.classList.toggle('regexp-partial', entry.sectionsSkipped);
entry.classList.toggle('regexp-invalid', entry.hasInvalidRegexps);
const indicator = template.regexpProblemIndicator.cloneNode(true);
indicator.appendChild(document.createTextNode(entry.sectionsSkipped || '!'));
indicator.onclick = EntryOnClick.indicator;
$('.main-controls', entry).appendChild(indicator);
} }
} }

View File

@ -28,7 +28,7 @@ const RX_NAMESPACE = new RegExp([/[\s\r\n]*/,
/(@namespace[\s\r\n]+(?:[^\s\r\n]+[\s\r\n]+)?url\(http:\/\/.*?\);)/, /(@namespace[\s\r\n]+(?:[^\s\r\n]+[\s\r\n]+)?url\(http:\/\/.*?\);)/,
/[\s\r\n]*/].map(rx => rx.source).join(''), 'g'); /[\s\r\n]*/].map(rx => rx.source).join(''), 'g');
const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g; const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g;
const SLOPPY_REGEXP_PREFIX = '\0';
// Let manage/popup/edit reuse background page variables // Let manage/popup/edit reuse background page variables
// Note, only 'var'-declared variables are visible from another extension page // Note, only 'var'-declared variables are visible from another extension page
@ -39,11 +39,11 @@ var cachedStyles, prefs;
cachedStyles = bg && bg.cachedStyles || { cachedStyles = bg && bg.cachedStyles || {
bg, bg,
list: null, list: null,
noCode: null,
byId: new Map(), byId: new Map(),
filters: new Map(), filters: new Map(),
regexps: new Map(), regexps: new Map(),
urlDomains: new Map(), urlDomains: new Map(),
emptyCode: new Map(), // entire code is comments/whitespace/@namespace
mutex: { mutex: {
inProgress: false, inProgress: false,
onDone: [], onDone: [],
@ -89,15 +89,12 @@ function getStyles(options, callback) {
const os = tx.objectStore('styles'); const os = tx.objectStore('styles');
os.getAll().onsuccess = event => { os.getAll().onsuccess = event => {
cachedStyles.list = event.target.result || []; cachedStyles.list = event.target.result || [];
cachedStyles.noCode = [];
cachedStyles.byId.clear(); cachedStyles.byId.clear();
for (const style of cachedStyles.list) { for (const style of cachedStyles.list) {
const noCode = getStyleWithNoCode(style); cachedStyles.byId.set(style.id, style);
cachedStyles.noCode.push(noCode); compileStyleRegExps({style});
cachedStyles.byId.set(style.id, {style, noCode});
compileStyleRegExps(style);
} }
//console.log('%s getStyles %s, invoking cached callbacks: %o', (performance.now() - t0).toFixed(1), JSON.stringify(options), cachedStyles.mutex.onDone.map(e => JSON.stringify(e.options))) //console.debug('%s getStyles %s, invoking cached callbacks: %o', (performance.now() - t0).toFixed(1), JSON.stringify(options), cachedStyles.mutex.onDone.map(e => JSON.stringify(e.options))); // eslint-disable-line max-len
callback(filterStyles(options)); callback(filterStyles(options));
cachedStyles.mutex.inProgress = false; cachedStyles.mutex.inProgress = false;
@ -134,19 +131,16 @@ function invalidateCache(andNotify, {added, updated, deletedId} = {}) {
if (updated) { if (updated) {
const cached = cachedStyles.byId.get(updated.id); const cached = cachedStyles.byId.get(updated.id);
if (cached) { if (cached) {
Object.assign(cached.style, updated); Object.assign(cached, updated);
Object.assign(cached.noCode, getStyleWithNoCode(updated)); //console.debug('cache: updated', updated);
//console.log('cache: updated', updated);
} }
cachedStyles.filters.clear(); cachedStyles.filters.clear();
return; return;
} }
if (added) { if (added) {
const noCode = getStyleWithNoCode(added);
cachedStyles.list.push(added); cachedStyles.list.push(added);
cachedStyles.noCode.push(noCode); cachedStyles.byId.set(added.id, added);
cachedStyles.byId.set(added.id, {style: added, noCode}); //console.debug('cache: added', added);
//console.log('cache: added', added);
cachedStyles.filters.clear(); cachedStyles.filters.clear();
return; return;
} }
@ -155,46 +149,47 @@ function invalidateCache(andNotify, {added, updated, deletedId} = {}) {
if (deletedStyle) { if (deletedStyle) {
const cachedIndex = cachedStyles.list.indexOf(deletedStyle); const cachedIndex = cachedStyles.list.indexOf(deletedStyle);
cachedStyles.list.splice(cachedIndex, 1); cachedStyles.list.splice(cachedIndex, 1);
cachedStyles.noCode.splice(cachedIndex, 1);
cachedStyles.byId.delete(deletedId); cachedStyles.byId.delete(deletedId);
//console.log('cache: deleted', deletedStyle); //console.debug('cache: deleted', deletedStyle);
cachedStyles.filters.clear(); cachedStyles.filters.clear();
return; return;
} }
} }
cachedStyles.list = null; cachedStyles.list = null;
cachedStyles.noCode = null; //console.debug('cache cleared');
//console.log('cache cleared');
cachedStyles.filters.clear(); cachedStyles.filters.clear();
} }
function filterStyles(options = {}) { function filterStyles({
enabled,
url = null,
id = null,
matchUrl = null,
asHash = null,
strictRegexp = true, // used by the popup to detect bad regexps
} = {}) {
//const t0 = performance.now(); //const t0 = performance.now();
const enabled = fixBoolean(options.enabled); enabled = fixBoolean(enabled);
const url = 'url' in options ? options.url : null; id = id === null ? null : Number(id);
const id = 'id' in options ? Number(options.id) : null;
const matchUrl = 'matchUrl' in options ? options.matchUrl : null;
const code = 'code' in options ? options.code : true;
const asHash = 'asHash' in options ? options.asHash : false;
if (enabled === null if (enabled === null
&& url === null && url === null
&& id === null && id === null
&& matchUrl === null && matchUrl === null
&& asHash != true) { && asHash != true) {
//console.log('%c%s filterStyles SKIPPED LOOP %s', 'color:gray', (performance.now() - t0).toFixed(1), JSON.stringify(options)) //console.debug('%c%s filterStyles SKIPPED LOOP %s', 'color:gray', (performance.now() - t0).toFixed(1), enabled, id, asHash, strictRegexp, matchUrl); // eslint-disable-line max-len
return code ? cachedStyles.list : cachedStyles.noCode; return cachedStyles.list;
} }
// silence the inapplicable warning for async code // silence the inapplicable warning for async code
// eslint-disable-next-line no-use-before-define // eslint-disable-next-line no-use-before-define
const disableAll = asHash && prefs.get('disableAll', false); const disableAll = asHash && prefs.get('disableAll', false);
// add \t after url to prevent collisions (not sure it can actually happen though) // add \t after url to prevent collisions (not sure it can actually happen though)
const cacheKey = ' ' + enabled + url + '\t' + id + matchUrl + '\t' + code + asHash; const cacheKey = ' ' + enabled + url + '\t' + id + matchUrl + '\t' + asHash + strictRegexp;
const cached = cachedStyles.filters.get(cacheKey); const cached = cachedStyles.filters.get(cacheKey);
if (cached) { if (cached) {
//console.log('%c%s filterStyles REUSED RESPONSE %s', 'color:gray', (performance.now() - t0).toFixed(1), JSON.stringify(options)) //console.debug('%c%s filterStyles REUSED RESPONSE %s', 'color:gray', (performance.now() - t0).toFixed(1), enabled, id, asHash, strictRegexp, matchUrl); // eslint-disable-line max-len
cached.hits++; cached.hits++;
cached.lastHit = Date.now(); cached.lastHit = Date.now();
@ -212,19 +207,22 @@ function filterStyles(options = {}) {
} }
const styles = id === null const styles = id === null
? (code ? cachedStyles.list : cachedStyles.noCode) ? cachedStyles.list
: [(cachedStyles.byId.get(id) || {})[code ? 'style' : 'noCode']]; : [cachedStyles.byId.get(id)];
const filtered = asHash ? {} : []; const filtered = asHash ? {} : [];
if (!styles) { if (!styles) {
// may happen when users [accidentally] reopen an old URL // may happen when users [accidentally] reopen an old URL
// of edit.html with a non-existent style id parameter // of edit.html with a non-existent style id parameter
return filtered; return filtered;
} }
const needSections = asHash || matchUrl !== null;
for (let i = 0, style; (style = styles[i]); i++) { for (let i = 0, style; (style = styles[i]); i++) {
if ((enabled === null || style.enabled == enabled) if ((enabled === null || style.enabled == enabled)
&& (url === null || style.url == url) && (url === null || style.url == url)
&& (id === null || style.id == id)) { && (id === null || style.id == id)) {
const sections = (asHash || matchUrl !== null) && getApplicableSections(style, matchUrl); const sections = needSections &&
getApplicableSections({style, matchUrl, strictRegexp, stopOnFirst: !asHash});
if (asHash) { if (asHash) {
if (sections.length) { if (sections.length) {
filtered[style.id] = sections; filtered[style.id] = sections;
@ -234,7 +232,7 @@ function filterStyles(options = {}) {
} }
} }
} }
//console.log('%s filterStyles %s', (performance.now() - t0).toFixed(1), JSON.stringify(options)) //console.debug('%s filterStyles %s', (performance.now() - t0).toFixed(1), enabled, id, asHash, strictRegexp, matchUrl); // eslint-disable-line max-len
cachedStyles.filters.set(cacheKey, { cachedStyles.filters.set(cacheKey, {
styles: filtered, styles: filtered,
lastHit: Date.now(), lastHit: Date.now(),
@ -307,7 +305,7 @@ function saveStyle(style) {
os.put(style).onsuccess = eventPut => { os.put(style).onsuccess = eventPut => {
style.id = style.id || eventPut.target.result; style.id = style.id || eventPut.target.result;
invalidateCache(notify, existed ? {updated: style} : {added: style}); invalidateCache(notify, existed ? {updated: style} : {added: style});
compileStyleRegExps(style); compileStyleRegExps({style});
if (notify) { if (notify) {
notifyAllTabs({ notifyAllTabs({
method: existed ? 'styleUpdated' : 'styleAdded', method: existed ? 'styleUpdated' : 'styleAdded',
@ -338,7 +336,7 @@ function saveStyle(style) {
// Give it the ID that was generated // Give it the ID that was generated
style.id = event.target.result; style.id = event.target.result;
invalidateCache(notify, {added: style}); invalidateCache(notify, {added: style});
compileStyleRegExps(style); compileStyleRegExps({style});
if (notify) { if (notify) {
notifyAllTabs({method: 'styleAdded', style, reason}); notifyAllTabs({method: 'styleAdded', style, reason});
} }
@ -434,68 +432,88 @@ function getType(o) {
} }
function getApplicableSections(style, url) { function getApplicableSections({style, matchUrl, strictRegexp = true, stopOnFirst}) {
//let t0 = 0;
const sections = []; const sections = [];
checkingSections: checkingSections:
for (const section of style.sections) { for (const section of style.sections) {
// only http, https, file, ftp, and chrome-extension://OWN_EXTENSION_ID allowed andCollect:
if (!url.startsWith('http') do {
&& !url.startsWith('ftp') // only http, https, file, ftp, and chrome-extension://OWN_EXTENSION_ID allowed
&& !url.startsWith('file') if (!matchUrl.startsWith('http')
&& !url.startsWith(OWN_ORIGIN)) { && !matchUrl.startsWith('ftp')
continue checkingSections; && !matchUrl.startsWith('file')
} && !matchUrl.startsWith(OWN_ORIGIN)) {
if (section.urls.length == 0
&& section.domains.length == 0
&& section.urlPrefixes.length == 0
&& section.regexps.length == 0) {
sections.push(section);
continue checkingSections;
}
if (section.urls.indexOf(url) != -1) {
sections.push(section);
continue checkingSections;
}
for (const urlPrefix of section.urlPrefixes) {
if (url.startsWith(urlPrefix)) {
sections.push(section);
continue checkingSections; continue checkingSections;
} }
} if (section.urls.length == 0
const urlDomains = cachedStyles.urlDomains.get(url) || getDomains(url); && section.domains.length == 0
for (const domain of urlDomains) { && section.urlPrefixes.length == 0
if (section.domains.indexOf(domain) != -1) { && section.regexps.length == 0) {
sections.push(section); break andCollect;
continue checkingSections;
} }
} if (section.urls.indexOf(matchUrl) != -1) {
for (const regexp of section.regexps) { break andCollect;
let rx = cachedStyles.regexps.get(regexp);
if (rx == false) {
// bad regexp
continue;
} }
if (!rx) { for (const urlPrefix of section.urlPrefixes) {
rx = tryRegExp('^(?:' + regexp + ')$'); if (matchUrl.startsWith(urlPrefix)) {
cachedStyles.regexps.set(regexp, rx || false); break andCollect;
if (!rx) {
// bad regexp
continue;
} }
} }
if (rx.test(url)) { if (section.domains.length) {
sections.push(section); const urlDomains = cachedStyles.urlDomains.get(matchUrl) || getDomains(matchUrl);
continue checkingSections; for (const domain of urlDomains) {
if (section.domains.indexOf(domain) != -1) {
break andCollect;
}
}
}
for (const regexp of section.regexps) {
for (let pass = 1; pass <= (strictRegexp ? 1 : 2); pass++) {
const cacheKey = pass == 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp;
let rx = cachedStyles.regexps.get(cacheKey);
if (rx == false) {
// invalid regexp
break;
}
if (!rx) {
const anchored = pass == 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
rx = tryRegExp(anchored);
cachedStyles.regexps.set(cacheKey, rx || false);
if (!rx) {
// invalid regexp
break;
}
}
if (rx.test(matchUrl)) {
break andCollect;
}
}
}
continue checkingSections;
} while (0);
// Collect the section if not empty or namespace-only.
// We don't check long code as it's slow both for emptyCode declared as Object
// and as Map in case the string is not the same reference used to add the item
//const t0start = performance.now();
const code = section.code;
let isEmpty = code.length < 1000 && cachedStyles.emptyCode.get(code);
if (isEmpty === undefined) {
isEmpty = !code || !code.trim()
|| code.indexOf('@namespace') >= 0
&& code.replace(RX_CSS_COMMENTS, '').replace(RX_NAMESPACE, '').trim() == '';
cachedStyles.emptyCode.set(code, isEmpty);
}
//t0 += performance.now() - t0start;
if (!isEmpty) {
sections.push(section);
if (stopOnFirst) {
//t0 >= 0.1 && console.debug('%s emptyCode', t0.toFixed(1)); // eslint-disable-line no-unused-expressions
return sections;
} }
} }
} }
// ignore @namespace-only results //t0 >= 0.1 && console.debug('%s emptyCode', t0.toFixed(1)); // eslint-disable-line no-unused-expressions
if (sections.length == 1
&& sections[0].code
&& sections[0].code.indexOf('@namespace') >= 0
&& sections[0].code.replace(RX_CSS_COMMENTS, '').replace(RX_NAMESPACE, '').trim() == '') {
return [];
}
return sections; return sections;
} }
@ -888,18 +906,22 @@ function styleSectionsEqual(styleA, styleB) {
} }
function compileStyleRegExps(style) { function compileStyleRegExps({style, compileAll}) {
const t0 = performance.now(); const t0 = performance.now();
for (const section of style.sections || []) { for (const section of style.sections || []) {
for (const regexp of section.regexps) { for (const regexp of section.regexps) {
// we want to match the full url, so add ^ and $ if not already present for (let pass = 1; pass <= (compileAll ? 2 : 1); pass++) {
if (cachedStyles.regexps.has(regexp)) { const cacheKey = pass == 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp;
continue; if (cachedStyles.regexps.has(cacheKey)) {
} continue;
const rx = tryRegExp('^(?:' + regexp + ')$'); }
cachedStyles.regexps.set(regexp, rx || false); // according to CSS4 @document specification the entire URL must match
if (performance.now() - t0 > 100) { const anchored = pass == 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
return; const rx = tryRegExp(anchored);
cachedStyles.regexps.set(cacheKey, rx || false);
if (!compileAll && performance.now() - t0 > 100) {
return;
}
} }
} }
} }