From 2973cac28fad900b5696da43ca1e4545e1f44861 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 6 Mar 2016 20:27:17 -0600 Subject: [PATCH] Switch to IndexedDB #167 --- apply.js | 4 +- background.js | 283 ---------------------------------------------- edit.js | 2 +- manage.js | 7 +- manifest.json | 2 +- popup.js | 4 +- storage-websql.js | 152 +++++++++++++++++++++++++ storage.js | 281 ++++++++++++++++++++++++++++++++------------- 8 files changed, 363 insertions(+), 372 deletions(-) create mode 100644 storage-websql.js diff --git a/apply.js b/apply.js index a130e7c1..74399622 100644 --- a/apply.js +++ b/apply.js @@ -29,7 +29,7 @@ chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { removeStyle(request.id, document); break; case "styleUpdated": - if (request.style.enabled == "true") { + if (request.style.enabled) { retireStyle(request.style.id); // fallthrough to "styleAdded" } else { @@ -37,7 +37,7 @@ chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { break; } case "styleAdded": - if (request.style.enabled == "true") { + if (request.style.enabled) { chrome.runtime.sendMessage({method: "getStyles", matchUrl: location.href, enabled: true, id: request.style.id, asHash: true}, applyStyles); } break; diff --git a/background.js b/background.js index 2d090167..6608c3d6 100644 --- a/background.js +++ b/background.js @@ -66,9 +66,6 @@ chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { case "saveStyle": saveStyle(request, sendResponse); return true; - case "styleChanged": - cachedStyles = null; - break; case "healthCheck": getDatabase(function() { sendResponse(true); }, function() { sendResponse(false); }); break; @@ -126,279 +123,6 @@ function disableAllStylesToggle(newState) { prefs.set("disableAll", newState); } -function getStyles(options, callback) { - - var enabled = fixBoolean(options.enabled); - var url = "url" in options ? options.url : null; - var id = "id" in options ? options.id : null; - var matchUrl = "matchUrl" in options ? options.matchUrl : null; - // Return as a hash from style to applicable sections? Can only be used with matchUrl. - var asHash = "asHash" in options ? options.asHash : false; - - var callCallback = function() { - var styles = asHash ? {disableAll: prefs.get("disableAll", false)} : []; - cachedStyles.forEach(function(style) { - if (enabled != null && fixBoolean(style.enabled) != enabled) { - return; - } - if (url != null && style.url != url) { - return; - } - if (id != null && style.id != id) { - return; - } - if (matchUrl != null) { - var applicableSections = getApplicableSections(style, matchUrl); - if (applicableSections.length > 0) { - if (asHash) { - styles[style.id] = applicableSections; - } else { - styles.push(style) - } - } - } else { - styles.push(style); - } - }); - callback(styles); - return styles; - } - - if (cachedStyles) { - return callCallback(); - } - - getDatabase(function(db) { - db.readTransaction(function (t) { - var where = ""; - var params = []; - - t.executeSql('SELECT DISTINCT s.*, se.id section_id, se.code, sm.name metaName, sm.value metaValue FROM styles s LEFT JOIN sections se ON se.style_id = s.id LEFT JOIN section_meta sm ON sm.section_id = se.id WHERE 1' + where + ' ORDER BY s.id, se.id, sm.id', params, function (t, r) { - cachedStyles = []; - var currentStyle = null; - var currentSection = null; - for (var i = 0; i < r.rows.length; i++) { - var values = r.rows.item(i); - var metaName = null; - switch (values.metaName) { - case null: - break; - case "url": - metaName = "urls"; - break; - case "url-prefix": - metaName = "urlPrefixes"; - break; - case "domain": - var metaName = "domains"; - break; - case "regexps": - var metaName = "regexps"; - break; - default: - var metaName = values.metaName + "s"; - } - var metaValue = values.metaValue; - if (currentStyle == null || currentStyle.id != values.id) { - currentStyle = {id: values.id, url: values.url, updateUrl: values.updateUrl, md5Url: values.md5Url, name: values.name, enabled: values.enabled, originalMd5: values.originalMd5, sections: []}; - cachedStyles.push(currentStyle); - } - if (values.section_id != null) { - if (currentSection == null || currentSection.id != values.section_id) { - currentSection = {id: values.section_id, code: values.code}; - currentStyle.sections.push(currentSection); - } - if (metaName && metaValue) { - if (currentSection[metaName]) { - currentSection[metaName].push(metaValue); - } else { - currentSection[metaName] = [metaValue]; - } - } - } - } - callCallback(); - }, reportError); - }, reportError); - }, reportError); -} - -function fixBoolean(b) { - if (typeof b != "undefined") { - return b != "false"; - } - return null; -} - -var namespacePattern = /^\s*(@namespace[^;]+;\s*)+$/; -function getApplicableSections(style, url) { - var sections = style.sections.filter(function(section) { - return sectionAppliesToUrl(section, url); - }); - // ignore if it's just namespaces - if (sections.length == 1 && namespacePattern.test(sections[0].code)) { - return []; - } - return sections; -} - -function sectionAppliesToUrl(section, url) { - // only http, https, file, and chrome-extension allowed - if (url.indexOf("http") != 0 && url.indexOf("file") != 0 && url.indexOf("chrome-extension") != 0 && url.indexOf("ftp") != 0) { - return false; - } - // other extensions can't be styled - if (url.indexOf("chrome-extension") == 0 && url.indexOf(chrome.extension.getURL("")) != 0) { - return false; - } - if (!section.urls && !section.domains && !section.urlPrefixes && !section.regexps) { - //console.log(section.id + " is global"); - return true; - } - if (section.urls && section.urls.indexOf(url) != -1) { - //console.log(section.id + " applies to " + url + " due to URL rules"); - return true; - } - if (section.urlPrefixes && section.urlPrefixes.some(function(prefix) { - return url.indexOf(prefix) == 0; - })) { - //console.log(section.id + " applies to " + url + " due to URL prefix rules"); - return true; - } - if (section.domains && getDomains(url).some(function(domain) { - return section.domains.indexOf(domain) != -1; - })) { - //console.log(section.id + " applies due to " + url + " due to domain rules"); - return true; - } - if (section.regexps && section.regexps.some(function(regexp) { - // we want to match the full url, so add ^ and $ if not already present - if (regexp[0] != "^") { - regexp = "^" + regexp; - } - if (regexp[regexp.length - 1] != "$") { - regexp += "$"; - } - var re = runTryCatch(function() { return new RegExp(regexp) }); - if (re) { - return (re).test(url); - } else { - console.log(section.id + "'s regexp '" + regexp + "' is not valid"); - } - })) { - //console.log(section.id + " applies to " + url + " due to regexp rules"); - return true; - } - //console.log(section.id + " does not apply due to " + url); - return false; -} - -var cachedStyles = null; - -function saveStyle(o, callback) { - getDatabase(function(db) { - db.transaction(function(t) { - if (o.id) { - // update whatever's been passed - if ("name" in o) { - t.executeSql('UPDATE styles SET name = ? WHERE id = ?;', [o.name, o.id]); - } - if ("enabled" in o) { - t.executeSql('UPDATE styles SET enabled = ? WHERE id = ?;', [o.enabled, o.id]); - } - if ("url" in o) { - t.executeSql('UPDATE styles SET url = ? WHERE id = ?;', [o.url, o.id]); - } - if ("updateUrl" in o) { - t.executeSql('UPDATE styles SET updateUrl = ? WHERE id = ?;', [o.updateUrl, o.id]); - } - if ("md5Url" in o) { - t.executeSql('UPDATE styles SET md5Url = ? WHERE id = ?;', [o.md5Url, o.id]); - } - if ("originalMd5" in o) { - t.executeSql('UPDATE styles SET originalMd5 = ? WHERE id = ?;', [o.originalMd5, o.id]); - } - } else { - // create a new record - // set optional things to null if they're undefined - ["updateUrl", "md5Url", "url", "originalMd5"].filter(function(att) { - return !(att in o); - }).forEach(function(att) { - o[att] = null; - }); - t.executeSql('INSERT INTO styles (name, enabled, url, updateUrl, md5Url, originalMd5) VALUES (?, ?, ?, ?, ?, ?);', [o.name, true, o.url, o.updateUrl, o.md5Url, o.originalMd5]); - } - - if ("sections" in o) { - if (o.id) { - // clear existing records - t.executeSql('DELETE FROM section_meta WHERE section_id IN (SELECT id FROM sections WHERE style_id = ?);', [o.id]); - t.executeSql('DELETE FROM sections WHERE style_id = ?;', [o.id]); - } - - o.sections.forEach(function(section) { - if (o.id) { - t.executeSql('INSERT INTO sections (style_id, code) VALUES (?, ?);', [o.id, section.code]); - } else { - t.executeSql('INSERT INTO sections (style_id, code) SELECT id, ? FROM styles ORDER BY id DESC LIMIT 1;', [section.code]); - } - if (section.urls) { - section.urls.forEach(function(u) { - t.executeSql("INSERT INTO section_meta (section_id, name, value) SELECT id, 'url', ? FROM sections ORDER BY id DESC LIMIT 1;", [u]); - }); - } - if (section.urlPrefixes) { - section.urlPrefixes.forEach(function(u) { - t.executeSql("INSERT INTO section_meta (section_id, name, value) SELECT id, 'url-prefix', ? FROM sections ORDER BY id DESC LIMIT 1;", [u]); - }); - } - if (section.domains) { - section.domains.forEach(function(u) { - t.executeSql("INSERT INTO section_meta (section_id, name, value) SELECT id, 'domain', ? FROM sections ORDER BY id DESC LIMIT 1;", [u]); - }); - } - if (section.regexps) { - section.regexps.forEach(function(u) { - t.executeSql("INSERT INTO section_meta (section_id, name, value) SELECT id, 'regexp', ? FROM sections ORDER BY id DESC LIMIT 1;", [u]); - }); - } - }); - } - }, reportError, function() {saveFromJSONComplete(o.id, callback)}); - }, reportError); -} - -function saveFromJSONComplete(id, callback) { - cachedStyles = null; - - if (id) { - getStyles({method: "getStyles", id: id}, function(styles) { - saveFromJSONStyleReloaded("styleUpdated", styles[0], callback); - }); - return; - } - - // we need to load the id for new ones - getDatabase(function(db) { - db.readTransaction(function (t) { - t.executeSql('SELECT id FROM styles ORDER BY id DESC LIMIT 1', [], function(t, r) { - var id = r.rows.item(0).id; - getStyles({method: "getStyles", id: id}, function(styles) { - saveFromJSONStyleReloaded("styleAdded", styles[0], callback); - }); - }, reportError) - }, reportError) - }); - -} - -function saveFromJSONStyleReloaded(updateType, style, callback) { - notifyAllTabs({method: updateType, style: style}); - if (callback) { - callback(style); - } -} - // Get the DB so that any first run actions will be performed immediately when the background page loads. getDatabase(function() {}, reportError); @@ -430,13 +154,6 @@ function openURL(options) { }); } -// js engine can't optimize the entire function if it contains try-catch -// so we should keep it isolated from normal code in a minimal wrapper -function runTryCatch(func) { - try { return func() } - catch(e) {} -} - var codeMirrorThemes; getCodeMirrorThemes(function(themes) { codeMirrorThemes = themes; diff --git a/edit.js b/edit.js index 43685dea..230586d9 100644 --- a/edit.js +++ b/edit.js @@ -1093,7 +1093,7 @@ function init() { function initWithStyle(style) { document.getElementById("name").value = style.name; - document.getElementById("enabled").checked = style.enabled == "true"; + document.getElementById("enabled").checked = style.enabled; document.getElementById("url").href = style.url; // if this was done in response to an update, we need to clear existing sections getSections().forEach(function(div) { div.remove(); }); diff --git a/manage.js b/manage.js index b1bd0388..35f8e9af 100644 --- a/manage.js +++ b/manage.js @@ -29,7 +29,7 @@ function showStyles(styles) { function createStyleElement(style) { var e = template.style.cloneNode(true); - e.setAttribute("class", style.enabled == "true" ? "enabled" : "disabled"); + e.setAttribute("class", style.enabled ? "enabled" : "disabled"); e.setAttribute("style-id", style.id); if (style.updateUrl) { e.setAttribute("style-update-url", style.updateUrl); @@ -190,7 +190,10 @@ function handleUpdate(style) { } function handleDelete(id) { - installed.removeChild(installed.querySelector("[style-id='" + id + "']")); + var node = installed.querySelector("[style-id='" + id + "']"); + if (node) { + installed.removeChild(node); + } } function doCheckUpdate(event) { diff --git a/manifest.json b/manifest.json index e03241c2..d2533e47 100644 --- a/manifest.json +++ b/manifest.json @@ -23,7 +23,7 @@ "https://userstyles.org/" ], "background": { - "scripts": ["messaging.js", "storage.js", "background.js"] + "scripts": ["messaging.js", "storage-websql.js", "storage.js", "background.js"] }, "commands": { "openManage": { diff --git a/popup.js b/popup.js index 127b9459..b1eeda5f 100644 --- a/popup.js +++ b/popup.js @@ -88,9 +88,9 @@ function createStyleElement(style) { var e = template.style.cloneNode(true); var checkbox = e.querySelector(".checker"); checkbox.id = "style-" + style.id; - checkbox.checked = style.enabled == "true"; + checkbox.checked = style.enabled; - e.setAttribute("class", "entry " + (style.enabled == "true" ? "enabled" : "disabled")); + e.setAttribute("class", "entry " + (style.enabled ? "enabled" : "disabled")); e.setAttribute("style-id", style.id); var styleName = e.querySelector(".style-name"); styleName.appendChild(document.createTextNode(style.name)); diff --git a/storage-websql.js b/storage-websql.js new file mode 100644 index 00000000..65202821 --- /dev/null +++ b/storage-websql.js @@ -0,0 +1,152 @@ +var webSqlStorage = { + + migrate: function() { + webSqlStorage.getStyles(function(styles) { + getDatabase(function(db) { + var tx = db.transaction(["styles"], "readwrite"); + var os = tx.objectStore("styles"); + styles.forEach(function(s) { + webSqlStorage.cleanStyle(s) + os.add(s); + }); + }); + }, null); + }, + + cleanStyle: function(s) { + delete s.id; + s.sections.forEach(function(section) { + delete section.id; + ["urls", "urlPrefixes", "domains", "regexps"].forEach(function(property) { + if (!section[property]) { + section[property] = []; + } + }); + }); + }, + + getStyles: function(callback) { + webSqlStorage.getDatabase(function(db) { + db.readTransaction(function (t) { + var where = ""; + var params = []; + + t.executeSql('SELECT DISTINCT s.*, se.id section_id, se.code, sm.name metaName, sm.value metaValue FROM styles s LEFT JOIN sections se ON se.style_id = s.id LEFT JOIN section_meta sm ON sm.section_id = se.id WHERE 1' + where + ' ORDER BY s.id, se.id, sm.id', params, function (t, r) { + var styles = []; + var currentStyle = null; + var currentSection = null; + for (var i = 0; i < r.rows.length; i++) { + var values = r.rows.item(i); + var metaName = null; + switch (values.metaName) { + case null: + break; + case "url": + metaName = "urls"; + break; + case "url-prefix": + metaName = "urlPrefixes"; + break; + case "domain": + var metaName = "domains"; + break; + case "regexps": + var metaName = "regexps"; + break; + default: + var metaName = values.metaName + "s"; + } + var metaValue = values.metaValue; + if (currentStyle == null || currentStyle.id != values.id) { + currentStyle = {id: values.id, url: values.url, updateUrl: values.updateUrl, md5Url: values.md5Url, name: values.name, enabled: values.enabled == "true", originalMd5: values.originalMd5, sections: []}; + styles.push(currentStyle); + } + if (values.section_id != null) { + if (currentSection == null || currentSection.id != values.section_id) { + currentSection = {id: values.section_id, code: values.code}; + currentStyle.sections.push(currentSection); + } + if (metaName && metaValue) { + if (currentSection[metaName]) { + currentSection[metaName].push(metaValue); + } else { + currentSection[metaName] = [metaValue]; + } + } + } + } + callback(styles); + }, reportError); + }, reportError); + }, reportError); + }, + + getDatabase: function(ready, error) { + try { + stylishDb = openDatabase('stylish', '', 'Stylish Styles', 5*1024*1024); + } catch (ex) { + error(); + throw ex; + } + if (stylishDb.version == "1.0" || stylishDb.version == "") { + webSqlStorage.dbV11(stylishDb, error, ready); + } else if (stylishDb.version == "1.1") { + webSqlStorage.dbV12(stylishDb, error, ready); + } else if (stylishDb.version == "1.2") { + webSqlStorage.dbV13(stylishDb, error, ready); + } else if (stylishDb.version == "1.3") { + webSqlStorage.dbV14(stylishDb, error, ready); + } else if (stylishDb.version == "1.4") { + webSqlStorage.dbV15(stylishDb, error, ready); + } else { + ready(stylishDb); + } + }, + + dbV11: function(d, error, done) { + d.changeVersion(d.version, '1.1', function (t) { + t.executeSql('CREATE TABLE styles (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, url TEXT, updateUrl TEXT, md5Url TEXT, name TEXT NOT NULL, code TEXT NOT NULL, enabled INTEGER NOT NULL, originalCode TEXT NULL);'); + t.executeSql('CREATE TABLE style_meta (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, style_id INTEGER NOT NULL, name TEXT NOT NULL, value TEXT NOT NULL);'); + t.executeSql('CREATE INDEX style_meta_style_id ON style_meta (style_id);'); + }, error, function() { webSqlStorage.dbV12(d, error, done)}); + }, + + dbV12: function(d, error, done) { + d.changeVersion(d.version, '1.2', function (t) { + // add section table + t.executeSql('CREATE TABLE sections (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, style_id INTEGER NOT NULL, code TEXT NOT NULL);'); + t.executeSql('INSERT INTO sections (style_id, code) SELECT id, code FROM styles;'); + // switch meta to sections + t.executeSql('DROP INDEX style_meta_style_id;'); + t.executeSql('CREATE TABLE section_meta (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, section_id INTEGER NOT NULL, name TEXT NOT NULL, value TEXT NOT NULL);'); + t.executeSql('INSERT INTO section_meta (section_id, name, value) SELECT s.id, sm.name, sm.value FROM sections s INNER JOIN style_meta sm ON sm.style_id = s.style_id;'); + t.executeSql('CREATE INDEX section_meta_section_id ON section_meta (section_id);'); + t.executeSql('DROP TABLE style_meta;'); + // drop extra fields from styles table + t.executeSql('CREATE TABLE newstyles (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, url TEXT, updateUrl TEXT, md5Url TEXT, name TEXT NOT NULL, enabled INTEGER NOT NULL);'); + t.executeSql('INSERT INTO newstyles (id, url, updateUrl, md5Url, name, enabled) SELECT id, url, updateUrl, md5Url, name, enabled FROM styles;'); + t.executeSql('DROP TABLE styles;'); + t.executeSql('ALTER TABLE newstyles RENAME TO styles;'); + }, error, function() { webSqlStorage.dbV13(d, error, done)}); + }, + + dbV13: function(d, error, done) { + d.changeVersion(d.version, '1.3', function (t) { + // clear out orphans + t.executeSql('DELETE FROM section_meta WHERE section_id IN (SELECT sections.id FROM sections LEFT JOIN styles ON styles.id = sections.style_id WHERE styles.id IS NULL);'); + t.executeSql('DELETE FROM sections WHERE id IN (SELECT sections.id FROM sections LEFT JOIN styles ON styles.id = sections.style_id WHERE styles.id IS NULL);'); + }, error, function() { webSqlStorage.dbV14(d, error, done)}); + }, + + dbV14: function(d, error, done) { + d.changeVersion(d.version, '1.4', function (t) { + t.executeSql('UPDATE styles SET url = null WHERE url = "undefined";'); + }, error, function() { webSqlStorage.dbV15(d, error, done)}); + }, + + dbV15: function(d, error, done) { + d.changeVersion(d.version, '1.5', function (t) { + t.executeSql('ALTER TABLE styles ADD COLUMN originalMd5 TEXT NULL;'); + }, error, function() { done(d); }); + } +} diff --git a/storage.js b/storage.js index 43779bcc..03f4154e 100644 --- a/storage.js +++ b/storage.js @@ -1,102 +1,144 @@ -var stylishDb = null; function getDatabase(ready, error) { - if (stylishDb != null && stylishDb.version == "1.5") { - ready(stylishDb); - return; - } - try { - stylishDb = openDatabase('stylish', '', 'Stylish Styles', 5*1024*1024); - } catch (ex) { - error(); - throw ex; - } - if (stylishDb.version == "1.0" || stylishDb.version == "") { - dbV11(stylishDb, error, ready); - } else if (stylishDb.version == "1.1") { - dbV12(stylishDb, error, ready); - } else if (stylishDb.version == "1.2") { - dbV13(stylishDb, error, ready); - } else if (stylishDb.version == "1.3") { - dbV14(stylishDb, error, ready); - } else if (stylishDb.version == "1.4") { - dbV15(stylishDb, error, ready); - } else { - ready(stylishDb); + var dbOpenRequest = window.indexedDB.open("stylish", 2); + dbOpenRequest.onsuccess = function(e) { + ready(e.target.result); + }; + dbOpenRequest.onerror = function(event) { + console.log(event.target.errorCode); + if (error) { + error(event); + } + }; + dbOpenRequest.onupgradeneeded = function(event) { + if (event.oldVersion == 0) { + var os = event.target.result.createObjectStore("styles", {keyPath: 'id', autoIncrement: true}); + webSqlStorage.migrate(); + } } +}; + +function getStyles(options, callback) { + getDatabase(function(db) { + var tx = db.transaction(["styles"], "readonly"); + var os = tx.objectStore("styles"); + var all = []; + os.openCursor().onsuccess = function(event) { + var cursor = event.target.result; + if (cursor) { + var s = cursor.value + s.id = cursor.key + all.push(cursor.value); + cursor.continue(); + } else { + callback(filterStyles(all, options)); + } + }; + }, null); } -function dbV11(d, error, done) { - d.changeVersion(d.version, '1.1', function (t) { - t.executeSql('CREATE TABLE styles (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, url TEXT, updateUrl TEXT, md5Url TEXT, name TEXT NOT NULL, code TEXT NOT NULL, enabled INTEGER NOT NULL, originalCode TEXT NULL);'); - t.executeSql('CREATE TABLE style_meta (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, style_id INTEGER NOT NULL, name TEXT NOT NULL, value TEXT NOT NULL);'); - t.executeSql('CREATE INDEX style_meta_style_id ON style_meta (style_id);'); - }, error, function() {dbV12(d, error, done)}); +function filterStyles(unfilteredStyles, options) { + var enabled = fixBoolean(options.enabled); + var url = "url" in options ? options.url : null; + var id = "id" in options ? Number(options.id) : null; + var matchUrl = "matchUrl" in options ? options.matchUrl : null; + // Return as a hash from style to applicable sections? Can only be used with matchUrl. + var asHash = "asHash" in options ? options.asHash : false; + + var styles = asHash ? {disableAll: prefs.get("disableAll", false)} : []; + unfilteredStyles.forEach(function(style) { + if (enabled != null && style.enabled != enabled) { + return; + } + if (url != null && style.url != url) { + return; + } + if (id != null && style.id != id) { + return; + } + if (matchUrl != null) { + var applicableSections = getApplicableSections(style, matchUrl); + if (applicableSections.length > 0) { + if (asHash) { + styles[style.id] = applicableSections; + } else { + styles.push(style) + } + } + } else { + styles.push(style); + } + }); + return styles; } -function dbV12(d, error, done) { - d.changeVersion(d.version, '1.2', function (t) { - // add section table - t.executeSql('CREATE TABLE sections (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, style_id INTEGER NOT NULL, code TEXT NOT NULL);'); - t.executeSql('INSERT INTO sections (style_id, code) SELECT id, code FROM styles;'); - // switch meta to sections - t.executeSql('DROP INDEX style_meta_style_id;'); - t.executeSql('CREATE TABLE section_meta (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, section_id INTEGER NOT NULL, name TEXT NOT NULL, value TEXT NOT NULL);'); - t.executeSql('INSERT INTO section_meta (section_id, name, value) SELECT s.id, sm.name, sm.value FROM sections s INNER JOIN style_meta sm ON sm.style_id = s.style_id;'); - t.executeSql('CREATE INDEX section_meta_section_id ON section_meta (section_id);'); - t.executeSql('DROP TABLE style_meta;'); - // drop extra fields from styles table - t.executeSql('CREATE TABLE newstyles (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, url TEXT, updateUrl TEXT, md5Url TEXT, name TEXT NOT NULL, enabled INTEGER NOT NULL);'); - t.executeSql('INSERT INTO newstyles (id, url, updateUrl, md5Url, name, enabled) SELECT id, url, updateUrl, md5Url, name, enabled FROM styles;'); - t.executeSql('DROP TABLE styles;'); - t.executeSql('ALTER TABLE newstyles RENAME TO styles;'); - }, error, function() {dbV13(d, error, done)}); -} +function saveStyle(o, callback) { + getDatabase(function(db) { + var tx = db.transaction(["styles"], "readwrite"); + var os = tx.objectStore("styles"); -function dbV13(d, error, done) { - d.changeVersion(d.version, '1.3', function (t) { - // clear out orphans - t.executeSql('DELETE FROM section_meta WHERE section_id IN (SELECT sections.id FROM sections LEFT JOIN styles ON styles.id = sections.style_id WHERE styles.id IS NULL);'); - t.executeSql('DELETE FROM sections WHERE id IN (SELECT sections.id FROM sections LEFT JOIN styles ON styles.id = sections.style_id WHERE styles.id IS NULL);'); - }, error, function() { dbV14(d, error, done)}); -} + // Update + if (o.id) { + var request = os.get(Number(o.id)); + request.onsuccess = function(event) { + var style = request.result; + for (var prop in o) { + if (prop == "id") { + continue; + } + style[prop] = o[prop]; + } + request = os.put(style); + request.onsuccess = function(event) { + notifyAllTabs({method: "styleUpdated", style: style}); + if (callback) { + callback(style); + } + }; + }; + return; + } -function dbV14(d, error, done) { - d.changeVersion(d.version, '1.4', function (t) { - t.executeSql('UPDATE styles SET url = null WHERE url = "undefined";'); - }, error, function() { dbV15(d, error, done)}); -} - -function dbV15(d, error, done) { - d.changeVersion(d.version, '1.5', function (t) { - t.executeSql('ALTER TABLE styles ADD COLUMN originalMd5 TEXT NULL;'); - }, error, function() { done(d); }); + // Create + // Set optional things to null if they're undefined + ["updateUrl", "md5Url", "url", "originalMd5"].filter(function(att) { + return !(att in o); + }).forEach(function(att) { + o[att] = null; + }); + // Set to enabled if not set + if (!("enabled" in o)) { + o.enabled = true; + } + // Make sure it's not null - that makes indexeddb sad + delete o["id"]; + var request = os.add(o); + request.onsuccess = function(event) { + // Give it the ID that was generated + o.id = event.target.result; + notifyAllTabs({method: "styleAdded", style: o}); + if (callback) { + callback(o); + } + }; + }); } function enableStyle(id, enabled) { - getDatabase(function(db) { - db.transaction(function (t) { - t.executeSql("UPDATE styles SET enabled = ? WHERE id = ?;", [enabled, id]); - }, reportError, function() { - chrome.runtime.sendMessage({method: "styleChanged"}); - chrome.runtime.sendMessage({method: "getStyles", id: id}, function(styles) { - handleUpdate(styles[0]); - notifyAllTabs({method: "styleUpdated", style: styles[0]}); - }); - }); + saveStyle({id: id, enabled: enabled}, function(style) { + handleUpdate(style); + notifyAllTabs({method: "styleUpdated", style: style}); }); } function deleteStyle(id) { getDatabase(function(db) { - db.transaction(function (t) { - t.executeSql('DELETE FROM section_meta WHERE section_id IN (SELECT id FROM sections WHERE style_id = ?);', [id]); - t.executeSql('DELETE FROM sections WHERE style_id = ?;', [id]); - t.executeSql("DELETE FROM styles WHERE id = ?;", [id]); - }, reportError, function() { - chrome.runtime.sendMessage({method: "styleChanged"}); + var tx = db.transaction(["styles"], "readwrite"); + var os = tx.objectStore("styles"); + var request = os.delete(Number(id)); + request.onsuccess = function(event) { handleDelete(id); notifyAllTabs({method: "styleDeleted", id: id}); - }); + }; }); } @@ -109,6 +151,13 @@ function reportError() { } } +function fixBoolean(b) { + if (typeof b != "undefined") { + return b != "false"; + } + return null; +} + function getDomains(url) { if (url.indexOf("file:") == 0) { return []; @@ -132,10 +181,80 @@ function getType(o) { throw "Not supported - " + o; } +var namespacePattern = /^\s*(@namespace[^;]+;\s*)+$/; +function getApplicableSections(style, url) { + var sections = style.sections.filter(function(section) { + return sectionAppliesToUrl(section, url); + }); + // ignore if it's just namespaces + if (sections.length == 1 && namespacePattern.test(sections[0].code)) { + return []; + } + return sections; +} + +function sectionAppliesToUrl(section, url) { + // only http, https, file, and chrome-extension allowed + if (url.indexOf("http") != 0 && url.indexOf("file") != 0 && url.indexOf("chrome-extension") != 0 && url.indexOf("ftp") != 0) { + return false; + } + // other extensions can't be styled + if (url.indexOf("chrome-extension") == 0 && url.indexOf(chrome.extension.getURL("")) != 0) { + return false; + } + if (section.urls.length == 0 && section.domains.length == 0 && section.urlPrefixes.length == 0 && section.regexps.length == 0) { + //console.log(section.id + " is global"); + return true; + } + if (section.urls.indexOf(url) != -1) { + //console.log(section.id + " applies to " + url + " due to URL rules"); + return true; + } + if (section.urlPrefixes.some(function(prefix) { + return url.indexOf(prefix) == 0; + })) { + //console.log(section.id + " applies to " + url + " due to URL prefix rules"); + return true; + } + if (section.domains.length > 0 && getDomains(url).some(function(domain) { + return section.domains.indexOf(domain) != -1; + })) { + //console.log(section.id + " applies due to " + url + " due to domain rules"); + return true; + } + if (section.regexps.some(function(regexp) { + // we want to match the full url, so add ^ and $ if not already present + if (regexp[0] != "^") { + regexp = "^" + regexp; + } + if (regexp[regexp.length - 1] != "$") { + regexp += "$"; + } + var re = runTryCatch(function() { return new RegExp(regexp) }); + if (re) { + return (re).test(url); + } else { + console.log(section.id + "'s regexp '" + regexp + "' is not valid"); + } + })) { + //console.log(section.id + " applies to " + url + " due to regexp rules"); + return true; + } + //console.log(section.id + " does not apply due to " + url); + return false; +} + function isCheckbox(el) { return el.nodeName.toLowerCase() == "input" && "checkbox" == el.type.toLowerCase(); } +// js engine can't optimize the entire function if it contains try-catch +// so we should keep it isolated from normal code in a minimal wrapper +function runTryCatch(func) { + try { return func() } + catch(e) {} +} + // Accepts an array of pref names (values are fetched via prefs.get) // and establishes a two-way connection between the document elements and the actual prefs function setupLivePrefs(IDs) {