diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 8caa673..26da089 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -168,6 +168,28 @@ "message": "Blockiermodus", "description": "" }, + + "urlSettings_title": { + "message": "Seitenspezifische Einstellungen", + "description": "" + }, + "urlSettings_description": { + "message": "", + "description": "" + }, + + "url_title": { + "message": "URL", + "description": "" + }, + "url_description": { + "message": "", + "description": "" + }, + "inputURL": { + "message": "Input domain or URL RegExp:", + "description": "" + }, "minFakeSize_description": { "message": "Canvas, die eine kleiner oder gleich große Fläche als die hier angegebene Zahl haben, werden nicht vorgetäuscht. Dies ist ein Parameter, der die Detektion des Addons erschweren soll.\nACHTUNG: Dies verringert die Sicherheit des Addons. Deswegen wird stark empfohlen, diesen Wert nicht über 100 zu setzen.", diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 7cdf3ca..618eb36 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -169,6 +169,28 @@ "description": "" }, + "urlSettings_title": { + "message": "Site specific settings", + "description": "" + }, + "urlSettings_description": { + "message": "", + "description": "" + }, + + "url_title": { + "message": "URL", + "description": "" + }, + "url_description": { + "message": "", + "description": "" + }, + "inputURL": { + "message": "Input domain or URL RegExp:", + "description": "" + }, + "minFakeSize_description": { "message": "Canvas with a smaller or equal area than this number will not be faked. This is a parameter to prevent detection.\nCAUTION: This lowers the safety of the addon, therefore it is highly recommended not to set this value above 100.", "description": "" diff --git a/lib/check.js b/lib/check.js index 8d2314a..ae269f7 100644 --- a/lib/check.js +++ b/lib/check.js @@ -19,7 +19,8 @@ const logging = require("./logging"); scope.check = function check({url, errorStack}){ - var match = checkBoth(errorStack, url, settings.blockMode).match( + url = new URL(url || "about:blank"); + var match = checkBoth(errorStack, url, settings.get("blockMode", url)).match( /^(block|allow|fake|ask)(|Readout|Everything|Context|Input|Internal)$/ ); if (match){ @@ -50,7 +51,6 @@ function checkURL(url, blockMode){ logging.message("check url %s for block mode %s", url, blockMode); - url = new URL(url || "about:blank"); switch (url.protocol){ case "about:": if (url.href === "about:blank"){ diff --git a/lib/frame.js b/lib/frame.js index 55b9373..f536ca3 100644 --- a/lib/frame.js +++ b/lib/frame.js @@ -78,8 +78,8 @@ port.postMessage({"canvasBlocker-notify": data}); } - function prefs(name){ - return settings[name]; + function prefs(...args){ + return settings.get(...args); } diff --git a/lib/main.js b/lib/main.js index 2c03b29..61f89fa 100644 --- a/lib/main.js +++ b/lib/main.js @@ -58,7 +58,7 @@ port.onMessage.addListener(function(data){ if (data.hasOwnProperty("canvasBlocker-notify")){ if ( - settings.showNotifications && + settings.get("showNotifications", url) && !lists.get("ignore").match(url) ){ browser.pageAction.show(port.sender.tab.id); diff --git a/lib/settingDefinitions.js b/lib/settingDefinitions.js index 45bc530..ecb11c8 100644 --- a/lib/settingDefinitions.js +++ b/lib/settingDefinitions.js @@ -10,6 +10,25 @@ defaultValue: 1, options: [0, 1, 25, 50, 75, 100] }, + { + name: "urlSettings", + defaultValue: [], + urlContainer: true, + entries: [ + {name: "url", defaultValue: ""} + ] + }, + { + name: "urls", + defaultValue: [], + dynamic: true, + dependencies: ["urlSettings"], + getter: function(settings){ + return settings.urlSettings.map(function(urlSetting){ + return urlSetting.url; + }); + } + }, { name: "whiteList", defaultValue: "" @@ -21,6 +40,7 @@ { name: "blockMode", defaultValue: "fakeReadout", + urlSpecific: true, options: [ "blockReadout", "fakeReadout", "fakeInput", "askReadout", null, "blockEverything", "block", "ask", "allow", "allowEverything" @@ -84,7 +104,8 @@ }, { name: "showNotifications", - defaultValue: true + defaultValue: true, + urlSpecific: true }, { name: "storeImageForInspection", diff --git a/lib/settings.js b/lib/settings.js index fc48bdd..599a9cc 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -31,64 +31,240 @@ }); }()); const settingDefinitions = require("./settingDefinitions.js"); + const definitionsByName = {}; + const defaultSymbol = ""; - const eventHandler = {any: []}; + const eventHandler = {any: {}}; + eventHandler.any[defaultSymbol] = []; eventHandler.all = eventHandler.any; const settings = {}; + let urlContainer; - settingDefinitions.forEach(function(settingDefinition){ - var name = settingDefinition.name; - settings[name] = settingDefinition.defaultValue; - eventHandler[name] = []; - - settingDefinition.on = function on(callback){ - scope.on(name, callback); - }; - settingDefinition.invalid = function invalid(newValue){ - if (settingDefinition.fixed){ - return "fixed"; - } - else if ((typeof newValue) !== (typeof settingDefinition.defaultValue)){ + function isDefinitionInvalid(settingDefinition, newValue){ + if (newValue === undefined && settingDefinition.optional){ + return false; + } + else if (settingDefinition.fixed){ + return "fixed"; + } + else if ((typeof newValue) !== (typeof settingDefinition.defaultValue)){ + return "wrongType"; + } + else if (Array.isArray(settingDefinition.defaultValue)){ + if (!Array.isArray(newValue)){ return "wrongType"; } - else if ( - settingDefinition.options && - !settingDefinition.options.includes(newValue) - ){ - return "noOption"; + var entriesInvalid = newValue.reduce(function(v, entry){ + v = v || settingDefinition.entries.reduce(function(v, entryDefinition){ + return v || isDefinitionInvalid(entryDefinition, entry[entryDefinition.name]); + }, false); + if (!v){ + if (Object.keys(entry).some(function(key){ + return !settingDefinition.entries.some(function(entryDefinition){ + return key === entryDefinition.name; + }); + })){ + return "noOption"; + } + } + return v; + }, false); + if (entriesInvalid){ + return entriesInvalid; } - return false; - }; - settingDefinition.get = function getValue(){ - return settings[name]; - }; - settingDefinition.set = function setValue(newValue){ - logging.verbose("New value for %s:", name, newValue); - var invalid = settingDefinition.invalid(newValue); - if (invalid){ - if (invalid === "fixed"){ - logging.warning("Trying to set the fixed setting", name, ":", newValue); + } + else if ( + settingDefinition.options && + !settingDefinition.options.includes(newValue) + ){ + return "noOption"; + } + return false; + } + + function createGetter(settingDefinition){ + if (settingDefinition.dynamic){ + return function getValue(){ + return settingDefinition.getter(scope); + }; + } + else if (settingDefinition.urlSpecific){ + return function getValue(url){ + if (url){ + var matching = urlContainer.get().filter(function(urlSetting){ + return urlSetting.hasOwnProperty(settingDefinition.name); + }).filter(function(urlSetting){ + return urlSetting.match(url); + }); + if (matching.length){ + return matching[0][settingDefinition.name]; + } } - else if (invalid === "wrongType"){ - logging.warning("Wrong type provided for setting", name, ":", newValue); + return settings[settingDefinition.name]; + }; + } + else { + return function getValue(){ + return settings[settingDefinition.name]; + }; + } + } + + function createSetter(settingDefinition){ + if (settingDefinition.dynamic){ + return function setValue(newValue){ + settingDefinition.setter(scope); + }; + } + else { + const name = settingDefinition.name; + const isValid = function isValid(newValue){ + var invalid = settingDefinition.invalid(newValue); + if (invalid){ + if (invalid === "fixed"){ + logging.warning("Trying to set the fixed setting", name, ":", newValue); + } + else if (invalid === "wrongType"){ + logging.warning("Wrong type provided for setting", name, ":", newValue); + } + else if (invalid === "noOption"){ + logging.warning("Provided value outside specified options for ", name, ":", newValue); + } + else { + logging.warning("Unknown invalid state:", invalid); + } + return false; } - else if (invalid === "noOption"){ - logging.warning("Provided value outside specified options for ", name, ":", newValue); - } - else { - logging.warning("Unknown invalid state:", invalid); - } - } - else { + return true; + }; + const storeValue = function storeValue(newValue){ settings[name] = newValue; if (!settingDefinition.transient){ var storeObject = {}; storeObject[name] = newValue; browser.storage.local.set(storeObject); } - + }; + + if (settingDefinition.urlSpecific){ + return function setValue(newValue, url){ + logging.verbose("New value for %s:", name, newValue); + if (isValid(newValue)){ + if (url){ + var urlContainerValue = urlContainer.get(); + var matching = urlContainerValue.filter(function(urlSetting){ + return urlSetting.match(url); + }); + if (!matching.length){ + let newEntry = {url}; + newEntry[settingDefinition.name] = newValue; + urlContainerValue.push(newEntry); + matching = [newEntry]; + } + matching[0][settingDefinition.name] = newValue; + urlContainer.set(urlContainerValue); + } + else { + storeValue(newValue); + } + } + }; + } + else { + return function setValue(newValue){ + logging.verbose("New value for %s:", name, newValue); + if (isValid(newValue)){ + storeValue(newValue); + } + }; + } + } + } + + function createResetter(settingDefinition){ + if (settingDefinition.dynamic){ + return function(){}; + } + else { + const name = settingDefinition.name; + let reset = function(){ + settings[name] = settingDefinition.defaultValue; + browser.storage.local.remove(name); + }; + if (settingDefinition.urlSpecific){ + return function(url){ + if (url){ + var urlContainerValue = urlContainer.get(); + var matching = urlContainerValue.filter(function(urlSetting){ + return urlSetting.match(url); + }); + if (matching.length){ + delete matching[0][name]; + if (Object.keys(matching[0]).every(function(key){return key === "url";})){ + urlContainerValue = urlContainerValue.filter(function(urlSetting){ + return urlSetting !== matching[0]; + }); + } + urlContainer.set(urlContainerValue); + } + } + else { + reset(); + } + }; + } + else { + return reset; + } + } + } + + settingDefinitions.forEach(function(settingDefinition){ + if (settingDefinition.urlContainer){ + urlContainer = settingDefinition; + settingDefinition.refresh = function(){ + settingDefinition.set(settingDefinition.get()); + }; + } + + var name = settingDefinition.name; + definitionsByName[name] = settingDefinition; + settings[name] = settingDefinition.defaultValue; + eventHandler[name] = {}; + + settingDefinition.on = function on(callback, url){ + if (!settingDefinition.dynamic){ + scope.on(name, callback, url); + } + if (settingDefinition.dependencies){ + settingDefinition.dependencies.forEach(function(dependency){ + scope.on(dependency, function(){ + callback({name, newValue: settingDefinition.get()}); + }, url); + }); } }; + settingDefinition.invalid = function invalid(newValue){ + return isDefinitionInvalid(settingDefinition, newValue); + }; + settingDefinition.get = createGetter(settingDefinition); + + settingDefinition.set = createSetter(settingDefinition); + + settingDefinition.reset = createResetter(settingDefinition); + + if (settingDefinition.urlSpecific){ + if (!urlContainer){ + logging.error("Unable to use url specific settings without url-container"); + } + else { + settingDefinition.urlContainer = urlContainer; + let entry = Object.create(settingDefinition); + entry.optional = true; + urlContainer.entries.push(entry); + } + } + Object.defineProperty( scope, name, @@ -101,31 +277,57 @@ }); scope.getDefinition = function(name){ - var foundDefinitions = settingDefinitions.filter(function(settingDefinition){ - return name === settingDefinition.name; - }); - if (foundDefinitions.length){ - return Object.create(foundDefinitions[0]); + var foundDefinition = definitionsByName[name]; + if (foundDefinition){ + return Object.create(foundDefinition); } else { return undefined; } }; + scope.set = function(name, ...args){ + var foundDefinition = definitionsByName[name]; + if (foundDefinition){ + return foundDefinition.set(...args); + } + else { + logging.error("Try to set unkown setting:", name); + } + }; + scope.get = function(name, ...args){ + var foundDefinition = definitionsByName[name]; + if (foundDefinition){ + return foundDefinition.get(...args); + } + else { + logging.error("Try to get unkown setting:", name); + } + }; + scope.forEach = function forEachSetting(...args){ - settingDefinitions.map(function(settingDefinition){ + settingDefinitions.filter(function(settingDefinition){ + return !settingDefinition.dynamic; + }).map(function(settingDefinition){ return Object.create(settingDefinition); }).forEach(...args); }; - scope.on = function onSettingsChange(name, callback){ + + scope.on = function onSettingsChange(name, callback, url){ if (Array.isArray(name)){ name.forEach(function(name){ - onSettingsChange(name, callback); + onSettingsChange(name, callback, url); }); } else { if (eventHandler.hasOwnProperty(name)){ - eventHandler[name].push(callback); + if (!url){ + url = defaultSymbol; + } + if (!eventHandler[name].hasOwnProperty(url)){ + eventHandler[name][url] = []; + } + eventHandler[name][url].push(callback); } else { logging.warning("Unable to register event handler for unknown setting", name); @@ -133,28 +335,136 @@ } }; + const resetSymbol = Symbol("reset"); function changeValue(name, newValue){ + var settingDefinition = scope.getDefinition(name); var oldValue = settings[name]; + if (newValue === resetSymbol){ + newValue = settingDefinition.defaultValue; + } settings[name] = newValue; - (eventHandler[name] || []).forEach(function(callback){ + ((eventHandler[name] || {})[defaultSymbol] || []).forEach(function(callback){ callback({name, newValue, oldValue}); }); + + if (settingDefinition.urlSpecific){ + urlContainer.get().forEach(function(entry){ + if (!entry.hasOwnProperty(name)){ + ((eventHandler[name] || {})[entry.url] || []).forEach(function(callback){ + callback({name, newValue, oldValue, url: entry.url}); + }); + } + }); + } } logging.verbose("registering storage onchange listener"); browser.storage.onChanged.addListener(function(changes, area){ if (area === "local"){ logging.notice("settings changed", changes); + var delayedChange = []; Object.entries(changes).forEach(function(entry){ const [name, change] = entry; - changeValue(name, change.newValue); + if (urlContainer && name === urlContainer.name){ + // changes in the url container have to trigger after the other changes + delayedChange.push(entry); + } + else { + if (change.hasOwnProperty("newValue")){ + changeValue(name, change.newValue); + } + else { + changeValue(name, resetSymbol); + } + } }); - eventHandler.any.forEach(function(callback){ + delayedChange.forEach(function(entry){ + const [name, change] = entry; + if (change.hasOwnProperty("newValue")){ + changeValue(name, change.newValue); + } + else { + changeValue(name, resetSymbol); + } + }); + eventHandler.any[""].forEach(function(callback){ callback(); }); } }); + if (urlContainer){ + urlContainer.on(function({newValue, oldValue}){ + newValue.forEach(function(urlSetting){ + var regExp; + var domain = !!urlSetting.url.match(/^[\w.]+$/); + if (domain){ + regExp = new RegExp( + "(?:^|\\.)" + urlSetting.url.replace(/([\\+*?[^\]$(){}=!|.])/g, "\\$1") + "\\.?$", + "i" + ); + } + else { + regExp = new RegExp(urlSetting.url, "i"); + } + const match = function(url){ + if (!url){ + return false; + } + else if ( + url instanceof String || + (typeof url) === "string" + ){ + return url === urlSetting.url; + } + else if (domain){ + return (url.hostname || "").match(regExp); + } + else { + return url.href.match(regExp); + } + }; + Object.defineProperty( + urlSetting, + "match", + { + enumerable: false, + writable: true, + configurable: true, + value: match + } + ); + }); + + var newUrls = newValue.map(function(entry){return entry.url;}); + var oldUrls = oldValue.map(function(entry){return entry.url;}); + var matching = {}; + newUrls.forEach(function(url, i){ + matching[url] = {new: i, old: oldUrls.indexOf(url)}; + }); + oldUrls.forEach(function(url, i){ + if (!matching[url]){ + matching[url] = {new: -1, old: i}; + } + }); + Object.keys(matching).forEach(function(url){ + var oldEntry = oldValue[matching[url].old] || {}; + var newEntry = newValue[matching[url].new] || {}; + urlContainer.entries.forEach(function(settingDefinition){ + var name = settingDefinition.name; + var oldValue = oldEntry[name]; + var newValue = newEntry[name]; + + if (oldValue !== newValue){ + ((eventHandler[name] || {})[url] || []).forEach(function(callback){ + callback({name, newValue, oldValue, url}); + }); + } + }); + }); + }); + } + const settingsMigration = { validVersions: [undefined, 0.1, 0.2], transitions: { @@ -170,6 +480,47 @@ if (oldStorage.hasOwnProperty("askOnlyOnce")){ newStorage.askOnlyOnce = oldStorage.askOnlyOnce? "individual": "no"; } + return newStorage; + }, + 0.2: function(oldStorage){ + var newStorage = { + storageVersion: 0.3, + urlSettings: ( + oldStorage.urlSettings && + Array.isArray(oldStorage.urlSettings) + )? oldStorage.urlSettings: [] + }; + + var urlSettings = {}; + + (oldStorage.blackList || "").split(",").forEach(function(url){ + var entry = urlSettings[url]; + if (!entry){ + entry = {url, blockMode: "block"}; + urlSettings[url] = entry; + newStorage.urlSettings.push(entry); + } + }); + (oldStorage.whiteList || "").split(",").forEach(function(url){ + var entry = urlSettings[url]; + if (!entry){ + entry = {url, blockMode: "allow"}; + urlSettings[url] = entry; + newStorage.urlSettings.push(entry); + } + }); + (oldStorage.ignoreList || "").split(",").forEach(function(url){ + var entry = urlSettings[url]; + if (!entry){ + entry = {url, showNotifications: false}; + urlSettings[url] = entry; + newStorage.urlSettings.push(entry); + } + else { + entry.showNotifications = false; + } + }); + return newStorage; } } @@ -210,7 +561,18 @@ logging.notice("Changed settings:", toChange); browser.storage.local.set(toChange); } + var delayedChange = []; Object.entries(storage).forEach(function(entry){ + const [name, value] = entry; + if (urlContainer && name === urlContainer.name){ + // changes in the url container have to trigger after the other changes + delayedChange.push(entry); + } + else { + changeValue(name, value); + } + }); + delayedChange.forEach(function(entry){ const [name, value] = entry; changeValue(name, value); }); diff --git a/options/options.css b/options/options.css index 95b3581..54ebb5e 100644 --- a/options/options.css +++ b/options/options.css @@ -1,6 +1,7 @@ .settings { width: 100%; border-spacing: 0; + border-collapse: collapse; } .settings.displayDescriptions { table-layout: fixed; @@ -70,4 +71,45 @@ input[type=""], input[type="text"], input[type="number"], select { } *.multiple4 { width: 25% !important; +} + +.urlValues { + padding-right: 1em; + position: relative; +} +.urlValues.collapsed table { + display: none; +} +.urlValues.expanded table { + display: block; + margin: 0.5em auto; +} +.urlValues table caption { + text-align: center; + font-weight: bold; + white-space: nowrap; +} +.urlValues table td { + vertical-align: middle; +} +.urlValues table .reset, .urlValues table .add, .urlValues table .url { + cursor: pointer; +} +.urlValues .collapser { + position: absolute; + top: 0; + right: 0; + text-align: center; + width: 1em; + height: 1em; + cursor: pointer; +} +.urlValues.collapsed .collapser::after { + content: "\25B6"; +} +.urlValues.expanded .collapser::after { + content: "\25BC"; +} +.urlValues .notSpecifiedForUrl { + opacity: 0.5; } \ No newline at end of file diff --git a/options/optionsGui.js b/options/optionsGui.js index 7a289ad..ccb2adf 100644 --- a/options/optionsGui.js +++ b/options/optionsGui.js @@ -66,12 +66,11 @@ input.value = value; return input; }, - updateCallback: function(input, setting){ - input.value = setting.get(); + updateCallback: function(input, value){ + input.value = value; return input.value; }, - changeCallback: function(input, setting){ - setting.set(parseFloat(input.value)); + getValue: function(input){ return parseFloat(input.value); } }, @@ -81,12 +80,11 @@ input.value = value; return input; }, - updateCallback: function(input, setting){ - input.value = setting.get(); + updateCallback: function(input, value){ + input.value = value; return input.value; }, - changeCallback: function(input, setting){ - setting.set(input.value); + getValue: function(input){ return input.value; } }, @@ -97,38 +95,110 @@ input.style.display = "inline"; return input; }, - updateCallback: function(input, setting){ - input.checked = setting.get(); + updateCallback: function(input, value){ + input.checked = value; return input.checked; }, - changeCallback: function(input, setting){ - setting.set(input.checked); + getValue: function(input){ return input.checked; } - } + }, + object: false }; - function createInput(setting){ + function createInput(setting, url = ""){ var type = inputTypes[typeof setting.defaultValue]; var input; if (setting.options){ input = createSelect(setting); } else { - input = document.createElement("input"); if (type){ + input = document.createElement("input"); type.input(input, setting.defaultValue); } } if (type){ - setting.on(function(){type.updateCallback(input, setting);}); + setting.on(function(){type.updateCallback(input, setting.get(url));}, url); input.addEventListener("change", function(){ - var value = type.changeCallback(input, setting); + var value = type.getValue(input); + setting.set(value, url); logging.message("changed setting", setting.name, ":", value); }); } - return input; + if (setting.urlSpecific && url === ""){ + let container = document.createElement("div"); + container.className = "urlValues collapsed"; + container.appendChild(input); + var collapser = document.createElement("span"); + collapser.classList.add("collapser"); + container.appendChild(collapser); + collapser.addEventListener("click", function(){ + container.classList.toggle("collapsed"); + container.classList.toggle("expanded"); + }); + let urlTable = document.createElement("table"); + let caption = document.createElement("caption"); + caption.textContent = browser.i18n.getMessage(setting.urlContainer.name + "_title"); + urlTable.appendChild(caption); + let body = document.createElement("tbody"); + urlTable.appendChild(body); + let foot = document.createElement("tfoot"); + let footRow = document.createElement("tr"); + let footCell = document.createElement("td"); + footCell.colSpan = 3; + let footPlus = document.createElement("span"); + footPlus.classList.add("add"); + footPlus.textContent = "+"; + footPlus.addEventListener("click", function(){ + var url = prompt(browser.i18n.getMessage("inputURL")).trim(); + if (url){ + setting.set(setting.get(url), url); + } + }); + footCell.appendChild(footPlus); + footRow.appendChild(footCell); + foot.appendChild(footRow); + urlTable.appendChild(foot); + container.appendChild(urlTable); + + setting.urlContainer.on(function({newValue}){ + body.innerHTML = ""; + newValue.forEach(function(entry){ + let row = document.createElement("tr"); + let urlCell = document.createElement("td"); + urlCell.classList.add("url"); + urlCell.addEventListener("click", function(){ + var url = prompt(browser.i18n.getMessage("inputURL"), entry.url).trim(); + if (url){ + entry.url = url; + setting.urlContainer.refresh(); + } + }); + urlCell.textContent = entry.url; + row.appendChild(urlCell); + let input = createInput(setting, entry.url); + type.updateCallback(input, setting.get(entry.url)); + if (!entry.hasOwnProperty(setting.name)){ + input.classList.add("notSpecifiedForUrl"); + } + let inputCell = document.createElement("td"); + inputCell.appendChild(input); + row.appendChild(inputCell); + let clearCell = document.createElement("td"); + clearCell.className = "reset"; + clearCell.textContent = "\xD7"; + clearCell.addEventListener("click", function(){ + setting.reset(entry.url); + }); + row.appendChild(clearCell); + body.appendChild(row); + }); + }); + return container; + } + return input || document.createElement("span"); } function createButton(setting){ @@ -158,7 +228,7 @@ interaction = createInput(setting); } - interaction.className = "setting"; + interaction.classList.add("setting"); interaction.dataset.storageName = setting.name; interaction.dataset.storageType = typeof setting.defaultValue; diff --git a/releaseNotes.txt b/releaseNotes.txt index 3a6d569..170faa3 100644 --- a/releaseNotes.txt +++ b/releaseNotes.txt @@ -8,6 +8,7 @@ Version 0.4.3: new features: - reset settings - new white random generator - creates output similar to Tor browser + - blockMode and showNotifications can now be chosen url specific fixes: - page action was not always showing