/******************************************************************************* uBlock - a browser extension to block requests. Copyright (C) 2015 The uBlock authors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see {http://www.gnu.org/licenses/}. Home: https://github.com/chrisaljoudi/uBlock */ /* global self, safari, SafariBrowserTab, µBlock */ // For background page /******************************************************************************/ (function() { "use strict"; var vAPI = self.vAPI = self.vAPI || {}; vAPI.isMainProcess = true; vAPI.safari = true; /******************************************************************************/ vAPI.app = { name: "uBlock", version: safari.extension.displayVersion }; /******************************************************************************/ if(navigator.userAgent.indexOf("Safari/6") === -1) { // If we're not on at least Safari 8 var _open = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(m, u) { if(u.lastIndexOf("safari-extension:", 0) === 0) { var i = u.length, seeDot = false; while(i --) { if(u[i] === ".") { seeDot = true; } else if(u[i] === "/") { break; } } if(seeDot === false) { throw 'InvalidAccessError'; // Avoid crash return; } } _open.apply(this, arguments); }; } /******************************************************************************/ vAPI.app.restart = function() { µBlock.restart(); }; /******************************************************************************/ safari.extension.addContentScriptFromURL(vAPI.getURL("js/subscriber.js"), [ "https://*.adblockplus.org/*", "https://*.adblockplus.me/*", "https://www.fanboy.co.nz/*", "http://*.adblockplus.org/*", "http://*.adblockplus.me/*", "http://www.fanboy.co.nz/*" ], [], true); /******************************************************************************/ safari.extension.settings.addEventListener('change', function(e) { if(e.key === 'open_prefs') { vAPI.tabs.open({ url: 'dashboard.html', active: true }); } }, false); /******************************************************************************/ initStorageLib(); // Initialize storage library /******************************************************************************/ var storageQuota = 104857600; // copied from Info.plist localforage.config({ name: "ublock", size: storageQuota, storeName: "keyvaluepairs" }); vAPI.storage = { QUOTA_BYTES: storageQuota, // copied from Info.plist get: function(keys, callback) { if(typeof callback !== "function") { return; } var result = {}; if(keys === null) { localforage.iterate(function(value, key) { if(typeof value === "string") { result[key] = JSON.parse(value); } }, function() { callback(result); }); } else if(typeof keys === "string") { localforage.getItem(keys, function(err, value) { if(typeof value === "string") { result[keys] = JSON.parse(value); } callback(result); }); } else if(Array.isArray(keys)) { var toSatisfy = keys.length, n = toSatisfy; if(n === 0) { callback(result); return; } for(var i = 0; i < n; i++) { var key = keys[i]; var func = function(err, value) { toSatisfy--; if(typeof value === "string") { result[arguments.callee.myKey] = JSON.parse(value); } if(toSatisfy === 0) { callback(result); } }; func.myKey = key; localforage.getItem(key, func); } } else if(typeof keys === "object") { for(var key in keys) { if(!keys.hasOwnProperty(key)) { continue; } result[key] = keys[key]; } localforage.iterate(function(value, key) { if(!keys.hasOwnProperty(key)) return; if(typeof value === "string") { result[key] = JSON.parse(value); } }, function() { callback(result); }); } }, set: function(details, callback) { var toSatisfy = 0; for(var key in details) { if(!details.hasOwnProperty(key)) { continue; } toSatisfy++; } for(var key in details) { if(!details.hasOwnProperty(key)) { continue; } localforage.setItem(key, JSON.stringify(details[key]), function() { if(--toSatisfy === 0) { callback && callback(); } }); } }, remove: function(keys) { if(typeof keys === "string") { keys = [keys]; } for(var i = 0, n = keys.length; i < n; i++) { localforage.removeItem(keys[i]); } }, clear: function(callback) { this.preferences.clear(); localforage.clear(function() { callback(); }); }, getBytesInUse: function(keys, callback) { if(typeof callback !== "function") { return; } var size = 0; localforage.iterate(function(value, key) { size += (value || "").length; }, function() { callback(size); }); } }; /******************************************************************************/ if(!safari.extension.settings.migratedStorage) { var migrationMap = { "cached_asset_content://assets/user/filters.txt": "userFilters" }; var delayed = []; vAPI.storage.preferences = { get: function(a, b) { delayed.push(settingsStorage.get.bind(settingsStorage, a, b)); }, set: function(a, b) { delayed.push(settingsStorage.set.bind(settingsStorage, a, b)); }, remove: function(a, b) { delayed.push(settingsStorage.remove.bind(settingsStorage, a, b)); }, clear: function() { delayed.push(settingsStorage.clear.bind(settingsStorage)); }, }; localforage.iterate(function(value, key) { if(migrationMap[key]) { safari.extension.settings[migrationMap[key]] = value; return; } if(key.lastIndexOf("cached_asset", 0) === 0) { return; } safari.extension.settings[key] = value; localforage.removeItem(key); }, function() { var func; while(func = delayed.pop()) { func(); } delayed = null; vAPI.storage.preferences = settingsStorage; }); safari.extension.settings.migratedStorage = true; } var settingsStorage = { _storage: safari.extension.settings, get: function(keys, callback) { if(typeof callback !== "function") { return; } var i, value, result = {}; if(keys === null) { for(i in this._storage) { if(!this._storage.hasOwnProperty(i)) continue; value = this._storage[i]; if(typeof value === "string") { result[i] = JSON.parse(value); } } } else if(typeof keys === "string") { value = this._storage[keys]; if(typeof value === "string") { result[keys] = JSON.parse(value); } } else if(Array.isArray(keys)) { for(i = 0; i < keys.length; i++) { value = this._storage[i]; if(typeof value === "string") { result[keys[i]] = JSON.parse(value); } } } else if(typeof keys === "object") { for(i in keys) { if(!keys.hasOwnProperty(i)) { continue; } value = this._storage[i]; if(typeof value === "string") { result[i] = JSON.parse(value); } else { result[i] = keys[i]; } } } callback(result); }, set: function(details, callback) { for(var key in details) { if(!details.hasOwnProperty(key)) { continue; } this._storage.setItem(key, JSON.stringify(details[key])); } if(typeof callback === "function") { callback(); } }, remove: function(keys) { if(typeof keys === "string") { keys = [keys]; } for(var i = 0; i < keys.length; i++) { this._storage.removeItem(keys[i]); } }, clear: function() { this._storage.clear(); } }; /******************************************************************************/ vAPI.tabs = { stack: {}, stackId: 1 }; /******************************************************************************/ vAPI.isBehindTheSceneTabId = function(tabId) { return tabId.toString() === this.noTabId; }; vAPI.noTabId = '-1'; /******************************************************************************/ vAPI.tabs.registerListeners = function() { safari.application.addEventListener("beforeNavigate", function(e) { if(!vAPI.tabs.popupCandidate || !e.target || e.url === "about:blank") { return; } var url = e.url, tabId = vAPI.tabs.getTabId(e.target); var details = { targetURL: url, targetTabId: tabId.toString(), openerTabId: vAPI.tabs.popupCandidate }; vAPI.tabs.popupCandidate = false; if(vAPI.tabs.onPopup(details)) { e.preventDefault(); if(vAPI.tabs.stack[details.openerTabId]) { vAPI.tabs.stack[details.openerTabId].activate(); } } }, true); // onClosed handled in the main tab-close event // onUpdated handled via monitoring the history.pushState on web-pages // onPopup is handled in window.open on web-pages }; /******************************************************************************/ vAPI.tabs.getTabId = function(tab) { if(typeof tab.uBlockCachedID !== "undefined") { return tab.uBlockCachedID; } for(var i in vAPI.tabs.stack) { if(vAPI.tabs.stack[i] === tab) { return (tab.uBlockCachedID = +i); } } return -1; }; /******************************************************************************/ vAPI.tabs.get = function(tabId, callback) { var tab; if(tabId === null) { tab = safari.application.activeBrowserWindow.activeTab; tabId = this.getTabId(tab); } else { tab = this.stack[tabId]; } if(!tab) { callback(); return; } callback({ id: tabId, index: tab.browserWindow.tabs.indexOf(tab), windowId: safari.application.browserWindows.indexOf(tab.browserWindow), active: tab === tab.browserWindow.activeTab, url: tab.url || "about:blank", title: tab.title }); }; /******************************************************************************/ // properties of the details object: // url: 'URL', // the address that will be opened // tabId: 1, // the tab is used if set, instead of creating a new one // index: -1, // undefined: end of the list, -1: following tab, or after index // active: false, // opens the tab in background - true and undefined: foreground // select: true // if a tab is already opened with that url, then select it instead of opening a new one vAPI.tabs.open = function(details) { if(!details.url) { return null; } // extension pages if(/^[\w-]{2,}:/.test(details.url) === false) { details.url = vAPI.getURL(details.url); } var curWin, tab; if(details.select) { tab = safari.application.browserWindows.some(function(win) { var rgxHash = /#.*/; // this is questionable var url = details.url.replace(rgxHash, ''); for(var i = 0; i < win.tabs.length; i++) { // Some tabs don't have a URL if(win.tabs[i].url && win.tabs[i].url.replace(rgxHash, '') === url) { win.tabs[i].activate(); return true; } } }); if(tab) { return; } } if(details.active === undefined) { details.active = true; } curWin = safari.application.activeBrowserWindow; // it must be calculated before opening a new tab, // otherwise the new tab will be the active tab here if(details.index === -1) { details.index = curWin.tabs.indexOf(curWin.activeTab) + 1; } tab = (details.tabId ? this.stack[details.tabId] : curWin.openTab(details.active ? 'foreground' : 'background')); if(details.index !== undefined) { curWin.insertTab(tab, details.index); } tab.url = details.url; }; /******************************************************************************/ // Replace the URL of a tab. Noop if the tab does not exist. vAPI.tabs.replace = function(tabId, url) { var targetURL = url; // extension pages if ( /^[\w-]{2,}:/.test(targetURL) !== true ) { targetURL = vAPI.getURL(targetURL); } var tab = this.stack[tabId]; if ( tab ) { tab.url = targetURL; } }; /******************************************************************************/ vAPI.tabs.remove = function(tabIds) { if(tabIds instanceof SafariBrowserTab) { tabIds = this.getTabId(tabIds); } if(!Array.isArray(tabIds)) { tabIds = [tabIds]; } for(var i = 0; i < tabIds.length; i++) { if(this.stack[tabIds[i]]) { this.stack[tabIds[i]].close(); } } }; /******************************************************************************/ vAPI.tabs.reload = function(tabId) { var tab = this.stack[tabId]; if(tab) { tab.url = tab.url; } }; /******************************************************************************/ vAPI.tabs.injectScript = function(tabId, details, callback) { var tab; if(tabId) { tab = this.stack[tabId]; } else { tab = safari.application.activeBrowserWindow.activeTab; } if(details.file) { var xhr = new XMLHttpRequest(); xhr.open('GET', details.file, true); xhr.addEventListener("readystatechange", function() { if(this.readyState === 4) { details.code = xhr.responseText; tab.page.dispatchMessage('broadcast', { channelName: 'vAPI', msg: { cmd: 'injectScript', details: details } }); if(typeof callback === 'function') { setTimeout(callback, 13); } } }); xhr.send(); } }; /******************************************************************************/ // reload the popup when it's opened safari.application.addEventListener("popover", function(event) { var w = event.target.contentWindow, body = w.document.body, child; while(child = body.firstChild) { body.removeChild(child); } w.location.reload(); }, true); /******************************************************************************/ var ICON_URLS = { "on": vAPI.getURL("img/browsericons/safari-icon16.png"), "off": vAPI.getURL("img/browsericons/safari-icon16-off.png") }; var IconState = function(badge, img, icon) { this.badge = badge; // ^ a number -- the badge 'value' this.img = img; // ^ a string -- 'on' or 'off' this.active = false; // ^ is this IconState active for rendering? this.icon = typeof icon !== "undefined" ? icon : null; // ^ the corresponding browser toolbar-icon object this.dirty = (1 << 1) | (1 << 0); /* ^ bitmask AB: two bits, A and B where A is whether img has changed and needs render and B is whether badge has changed and needs render */ }; var iconStateForTabId = {}; // {tabId: IconState} var getIconForWindow = function(whichWindow) { // do we already have the right icon cached? if(typeof whichWindow.uBlockIcon !== "undefined") { return whichWindow.uBlockIcon; } // iterate through the icons to find the one which // belongs to this window (whichWindow) var items = safari.extension.toolbarItems; for(var i = 0; i < items.length; i++) { if(items[i].browserWindow === whichWindow) { return (whichWindow.uBlockIcon = items[i]); } } }; safari.application.addEventListener("activate", function(event) { if(!(event.target instanceof SafariBrowserTab)) { return; } // when a tab is activated... var tab = event.target; if(tab.browserWindow !== tab.oldBrowserWindow) { // looks like tab is now associated with a new window tab.oldBrowserWindow = tab.browserWindow; // so, unvalidate icon tab.uBlockKnowsIcon = false; } var tabId = vAPI.tabs.getTabId(tab), state = iconStateForTabId[tabId]; if(typeof state === "undefined") { state = iconStateForTabId[tabId] = new IconState(0, "on"); // need to get the icon for this newly-encountered tab... // uBlockKnowsIcon should be undefined here, so in theory // we don't need this -- but to be sure, // go ahead and explicitly unvalidate tab.uBlockKnowsIcon = false; } if(!tab.uBlockKnowsIcon) { // need to find the icon for this tab's window state.icon = getIconForWindow(tab.browserWindow); tab.uBlockKnowsIcon = true; } state.active = true; // force re-render since we probably switched tabs state.dirty = (1 << 1) | (1 << 0); renderIcon(state); }, true); safari.application.addEventListener("deactivate", function(event) { if(!(event.target instanceof SafariBrowserTab)) { return; } // when a tab is deactivated... var tabId = vAPI.tabs.getTabId(event.target), state = iconStateForTabId[tabId]; if(typeof state === "undefined") { return; } // mark its iconState as inactive so we don't visually // render changes for now state.active = false; }, true); var renderIcon = function(iconState) { if(iconState.dirty === 0) { // quit if we don't need to touch the "DOM" return; } var icon = iconState.icon; // only update the image if needed: if(iconState.dirty & 2) { icon.badge = iconState.badge; } if((iconState.dirty & 1) && icon.image !== ICON_URLS[iconState.img]) { icon.image = ICON_URLS[iconState.img]; } iconState.dirty = 0; }; vAPI.setIcon = function(tabId, iconStatus, badge) { badge = badge || 0; var state = iconStateForTabId[tabId]; if(typeof state === "undefined") { state = iconStateForTabId[tabId] = new IconState(badge, iconStatus); } else { state.dirty = ((state.badge !== badge) << 1) | ((state.img !== iconStatus) << 0); state.badge = badge; state.img = iconStatus; } if(state.active === true) { renderIcon(state); } }; /******************************************************************************/ // bind tabs to unique IDs (function() { var wins = safari.application.browserWindows, i = wins.length, j, curTab, curTabId, curWindow; while(i--) { curWindow = wins[i]; j = curWindow.tabs.length; while(j--) { curTab = wins[i].tabs[j], curTabId = vAPI.tabs.stackId++; iconStateForTabId[curTabId] = new IconState(0, "on", getIconForWindow(curWindow)); curTab.uBlockKnowsIcon = true; if(curWindow.activeTab === curTab) { iconStateForTabId[curTabId].active = true; } vAPI.tabs.stack[curTabId] = curTab; } } })(); /******************************************************************************/ safari.application.addEventListener('open', function(e) { // ignore windows if(e.target instanceof SafariBrowserTab) { vAPI.tabs.stack[vAPI.tabs.stackId++] = e.target; } }, true); /******************************************************************************/ safari.application.addEventListener('close', function(e) { // ignore windows if(!(e.target instanceof SafariBrowserTab)) { return; } var tabId = vAPI.tabs.getTabId(e.target); if(tabId !== -1) { // to not add another listener, put this here // instead of vAPI.tabs.registerListeners if(typeof vAPI.tabs.onClosed === 'function') { vAPI.tabs.onClosed(tabId); } delete vAPI.tabs.stack[tabId]; delete iconStateForTabId[tabId]; } }, true); /******************************************************************************/ vAPI.messaging = { listeners: {}, defaultHandler: null, NOOPFUNC: function() {}, UNHANDLED: 'vAPI.messaging.notHandled' }; /******************************************************************************/ vAPI.messaging.listen = function(listenerName, callback) { this.listeners[listenerName] = callback; }; /******************************************************************************/ var CallbackWrapper = function(request, port) { // No need to bind every single time this.callback = this.proxy.bind(this); this.messaging = vAPI.messaging; this.init(request, port); }; CallbackWrapper.junkyard = []; CallbackWrapper.factory = function(request, port) { var wrapper = CallbackWrapper.junkyard.pop(); if(wrapper) { wrapper.init(request, port); return wrapper; } return new CallbackWrapper(request, port); }; CallbackWrapper.prototype.init = function(request, port) { this.request = request; this.port = port; }; CallbackWrapper.prototype.proxy = function(response) { this.port.dispatchMessage(this.request.name, { requestId: this.request.message.requestId, channelName: this.request.message.channelName, msg: response !== undefined ? response: null }); this.port = this.request = null; CallbackWrapper.junkyard.push(this); }; vAPI.messaging.onMessage = function(request) { var callback = vAPI.messaging.NOOPFUNC; if(request.message.requestId !== undefined) { callback = CallbackWrapper.factory(request, request.target.page).callback; } var sender = { tab: { id: vAPI.tabs.getTabId(request.target) } }; // Specific handler var r = vAPI.messaging.UNHANDLED; var listener = vAPI.messaging.listeners[request.message.channelName]; if(typeof listener === 'function') { r = listener(request.message.msg, sender, callback); } if(r !== vAPI.messaging.UNHANDLED) { return; } // Default handler r = vAPI.messaging.defaultHandler(request.message.msg, sender, callback); if(r !== vAPI.messaging.UNHANDLED) { return; } console.error('µBlock> messaging > unknown request: %o', request.message); // Unhandled: // Need to callback anyways in case caller expected an answer, or // else there is a memory leak on caller's side callback(); }; /******************************************************************************/ vAPI.messaging.setup = function(defaultHandler) { // Already setup? if(this.defaultHandler !== null) { return; } if(typeof defaultHandler !== 'function') { defaultHandler = function() { return vAPI.messaging.UNHANDLED; }; } this.defaultHandler = defaultHandler; // the third parameter must stay false (bubbling), so later // onBeforeRequest will use true (capturing), where we can invoke // stopPropagation() (this way this.onMessage won't be fired) safari.application.addEventListener('message', this.onMessage, false); }; /******************************************************************************/ vAPI.messaging.broadcast = function(message) { message = { broadcast: true, msg: message }; for(var tabId in vAPI.tabs.stack) { vAPI.tabs.stack[tabId].page.dispatchMessage('broadcast', message); } }; /******************************************************************************/ vAPI.net = {}; /******************************************************************************/ // Fast `contains` Array.prototype.contains = function(a) { var b = this.length; while(b--) { if(this[b] === a) { return true; } } return false; }; /******************************************************************************/ vAPI.net.registerListeners = function() { var µb = µBlock; // Until Safari has more specific events, those are instead handled // in the onBeforeRequestAdapter; clean them up so they're garbage-collected vAPI.net.onBeforeSendHeaders = null; var onBeforeRequest = vAPI.net.onBeforeRequest, onBeforeRequestClient = onBeforeRequest.callback, onHeadersReceivedClient = vAPI.net.onHeadersReceived.callback, blockableTypes = onBeforeRequest.types; var onBeforeRequestAdapter = function(e) { if(e.name !== "canLoad") { return; } e.stopPropagation && e.stopPropagation(); if(e.message.type === "main_frame") { vAPI.tabs.onNavigation({ url: e.message.url, frameId: 0, tabId: vAPI.tabs.getTabId(e.target) }); e.message.hostname = µb.URI.hostnameFromURI(e.message.url); e.message.tabId = vAPI.tabs.getTabId(e.target); e.message.responseHeaders = []; onBeforeRequestClient(e.message); var blockVerdict = onHeadersReceivedClient(e.message); if(blockVerdict && blockVerdict.responseHeaders) { e.message = false; } else { e.message = true; } return; } switch(e.message.type) { case "popup": var openerTabId = vAPI.tabs.getTabId(e.target).toString(); var shouldBlock = !!vAPI.tabs.onPopup({ targetURL: e.message.url, targetTabId: "preempt", openerTabId: openerTabId }); if(shouldBlock) { e.message = false; } else { vAPI.tabs.popupCandidate = openerTabId; e.message = true; } break; case "popstate": vAPI.tabs.onUpdated(vAPI.tabs.getTabId(e.target), { url: e.message.url }, { url: e.message.url }); break; default: e.message.hostname = µb.URI.hostnameFromURI(e.message.url); e.message.tabId = vAPI.tabs.getTabId(e.target); var blockVerdict = onBeforeRequestClient(e.message); if(blockVerdict && blockVerdict.cancel) { e.message = false; return; } else { e.message = true; return; } } return; }; safari.application.addEventListener("message", onBeforeRequestAdapter, true); }; /******************************************************************************/ vAPI.contextMenu = { contextMap: { frame: 'insideFrame', link: 'linkHref', image: 'srcUrl', editable: 'editable' } }; /******************************************************************************/ vAPI.contextMenu.create = function(details, callback) { var contexts = details.contexts; var menuItemId = details.id; var menuTitle = details.title; if(Array.isArray(contexts) && contexts.length) { contexts = contexts.indexOf('all') === -1 ? contexts : null; } else { // default in Chrome contexts = ['page']; } this.onContextMenu = function(e) { var uI = e.userInfo; if(!uI || /^https?:\/\//i.test(uI.pageUrl) === false) { return; } if(contexts) { var invalidContext = true; var ctxMap = vAPI.contextMenu.contextMap; for(var i = 0; i < contexts.length; i++) { var ctx = contexts[i]; if(ctx === 'audio' || ctx === 'video') { if(uI[ctxMap['image']] && uI.tagName === ctx) { invalidContext = false; break; } } else if(uI[ctxMap[ctx]]) { invalidContext = false; break; } else if(ctx === 'page') { if(!(uI.insideFrame || uI.linkHref || uI.mediaType || uI.editable)) { invalidContext = false; break; } } } if(invalidContext) { return; } } e.contextMenu.appendContextMenuItem(menuItemId, menuTitle); }; this.onContextMenuCmd = function(e) { if(e.command === menuItemId) { var tab = e.currentTarget.activeBrowserWindow.activeTab; e.userInfo.menuItemId = menuItemId; callback(e.userInfo, tab ? { id: vAPI.tabs.getTabId(tab), url: tab.url } : undefined); } }; safari.application.addEventListener('contextmenu', this.onContextMenu); safari.application.addEventListener('command', this.onContextMenuCmd); }; /******************************************************************************/ vAPI.contextMenu.remove = function() { safari.application.removeEventListener('contextmenu', this.onContextMenu); safari.application.removeEventListener('command', this.onContextMenuCmd); this.onContextMenu = null; this.onContextMenuCmd = null; }; /******************************************************************************/ vAPI.lastError = function() { return null; }; /******************************************************************************/ // This is called only once, when everything has been loaded in memory after // the extension was launched. It can be used to inject content scripts // in already opened web pages, to remove whatever nuisance could make it to // the web pages before uBlock was ready. vAPI.onLoadAllCompleted = function() {}; /******************************************************************************/ vAPI.punycodeHostname = function(hostname) { return hostname; }; vAPI.punycodeURL = function(url) { return url; }; /******************************************************************************/ function initStorageLib() { /*! localForage -- Offline Storage, Improved Version 1.2.2 https://mozilla.github.io/localForage (c) 2013-2015 Mozilla, Apache License 2.0 */ !function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)k.push("exports"===i[l]?g={}:b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;jb;b+=4)c=e.indexOf(a[b]),d=e.indexOf(a[b+1]),f=e.indexOf(a[b+2]),g=e.indexOf(a[b+3]),l[j++]=c<<2|d>>4,l[j++]=(15&d)<<4|f>>2,l[j++]=(3&f)<<6|63&g;return k}function d(a){var b,c=new Uint8Array(a),d="";for(b=0;b>2],d+=e[(3&c[b])<<4|c[b+1]>>4],d+=e[(15&c[b+1])<<2|c[b+2]>>6],d+=e[63&c[b+2]];return c.length%3===2?d=d.substring(0,d.length-1)+"=":c.length%3===1&&(d=d.substring(0,d.length-2)+"=="),d}var e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",f="__lfsc__:",g=f.length,h="arbf",i="blob",j="si08",k="ui08",l="uic8",m="si16",n="si32",o="ur16",p="ui32",q="fl32",r="fl64",s=g+h.length,t={serialize:a,deserialize:b,stringToBuffer:c,bufferToString:d};"undefined"!=typeof module&&module.exports?module.exports=t:"function"==typeof define&&define.amd?define("localforageSerializer",function(){return t}):this.localforageSerializer=t}.call(window),function(){"use strict";function a(a){var b=this,c={db:null};if(a)for(var d in a)c[d]=a[d];return new m(function(a,d){var e=n.open(c.name,c.version);e.onerror=function(){d(e.error)},e.onupgradeneeded=function(){e.result.createObjectStore(c.storeName)},e.onsuccess=function(){c.db=e.result,b._dbInfo=c,a()}})}function b(a,b){var c=this;"string"!=typeof a&&(window.console.warn(a+" used as a key, but it is not a string."),a=String(a));var d=new m(function(b,d){c.ready().then(function(){var e=c._dbInfo,f=e.db.transaction(e.storeName,"readonly").objectStore(e.storeName),g=f.get(a);g.onsuccess=function(){var a=g.result;void 0===a&&(a=null),b(a)},g.onerror=function(){d(g.error)}})["catch"](d)});return k(d,b),d}function c(a,b){var c=this,d=new m(function(b,d){c.ready().then(function(){var e=c._dbInfo,f=e.db.transaction(e.storeName,"readonly").objectStore(e.storeName),g=f.openCursor(),h=1;g.onsuccess=function(){var c=g.result;if(c){var d=a(c.value,c.key,h++);void 0!==d?b(d):c["continue"]()}else b()},g.onerror=function(){d(g.error)}})["catch"](d)});return k(d,b),d}function d(a,b,c){var d=this;"string"!=typeof a&&(window.console.warn(a+" used as a key, but it is not a string."),a=String(a));var e=new m(function(c,e){d.ready().then(function(){var f=d._dbInfo,g=f.db.transaction(f.storeName,"readwrite"),h=g.objectStore(f.storeName);null===b&&(b=void 0);var i=h.put(b,a);g.oncomplete=function(){void 0===b&&(b=null),c(b)},g.onabort=g.onerror=function(){e(i.error)}})["catch"](e)});return k(e,c),e}function e(a,b){var c=this;"string"!=typeof a&&(window.console.warn(a+" used as a key, but it is not a string."),a=String(a));var d=new m(function(b,d){c.ready().then(function(){var e=c._dbInfo,f=e.db.transaction(e.storeName,"readwrite"),g=f.objectStore(e.storeName),h=g["delete"](a);f.oncomplete=function(){b()},f.onerror=function(){d(h.error)},f.onabort=function(a){var b=a.target.error;"QuotaExceededError"===b&&d(b)}})["catch"](d)});return k(d,b),d}function f(a){var b=this,c=new m(function(a,c){b.ready().then(function(){var d=b._dbInfo,e=d.db.transaction(d.storeName,"readwrite"),f=e.objectStore(d.storeName),g=f.clear();e.oncomplete=function(){a()},e.onabort=e.onerror=function(){c(g.error)}})["catch"](c)});return k(c,a),c}function g(a){var b=this,c=new m(function(a,c){b.ready().then(function(){var d=b._dbInfo,e=d.db.transaction(d.storeName,"readonly").objectStore(d.storeName),f=e.count();f.onsuccess=function(){a(f.result)},f.onerror=function(){c(f.error)}})["catch"](c)});return j(c,a),c}function h(a,b){var c=this,d=new m(function(b,d){return 0>a?void b(null):void c.ready().then(function(){var e=c._dbInfo,f=e.db.transaction(e.storeName,"readonly").objectStore(e.storeName),g=!1,h=f.openCursor();h.onsuccess=function(){var c=h.result;return c?void(0===a?b(c.key):g?b(c.key):(g=!0,c.advance(a))):void b(null)},h.onerror=function(){d(h.error)}})["catch"](d)});return j(d,b),d}function i(a){var b=this,c=new m(function(a,c){b.ready().then(function(){var d=b._dbInfo,e=d.db.transaction(d.storeName,"readonly").objectStore(d.storeName),f=e.openCursor(),g=[];f.onsuccess=function(){var b=f.result;return b?(g.push(b.key),void b["continue"]()):void a(g)},f.onerror=function(){c(f.error)}})["catch"](c)});return j(c,a),c}function j(a,b){b&&a.then(function(a){b(null,a)},function(a){b(a)})}function k(a,b){b&&a.then(function(a){l(b,a)},function(a){b(a)})}function l(a,b){return a?setTimeout(function(){return a(null,b)},0):void 0}var m="undefined"!=typeof module&&module.exports?require("promise"):this.Promise,n=n||this.indexedDB||this.webkitIndexedDB||this.mozIndexedDB||this.OIndexedDB||this.msIndexedDB;if(n){var o={_driver:"asyncStorage",_initStorage:a,iterate:c,getItem:b,setItem:d,removeItem:e,clear:f,length:g,key:h,keys:i};"undefined"!=typeof module&&module.exports?module.exports=o:"function"==typeof define&&define.amd?define("asyncStorage",function(){return o}):this.asyncStorage=o}}.call(window),function(){"use strict";function a(a){var b=this,c={};if(a)for(var d in a)c[d]=a[d];c.keyPrefix=c.name+"/",b._dbInfo=c;var e=new k(function(a){q===p.DEFINE?require(["localforageSerializer"],a):a(q===p.EXPORT?require("./../utils/serializer"):l.localforageSerializer)});return e.then(function(a){return m=a,k.resolve()})}function b(a){var b=this,c=b.ready().then(function(){for(var a=b._dbInfo.keyPrefix,c=n.length-1;c>=0;c--){var d=n.key(c);0===d.indexOf(a)&&n.removeItem(d)}});return j(c,a),c}function c(a,b){var c=this;"string"!=typeof a&&(window.console.warn(a+" used as a key, but it is not a string."),a=String(a));var d=c.ready().then(function(){var b=c._dbInfo,d=n.getItem(b.keyPrefix+a);return d&&(d=m.deserialize(d)),d});return j(d,b),d}function d(a,b){var c=this,d=c.ready().then(function(){for(var b=c._dbInfo.keyPrefix,d=b.length,e=n.length,f=0;e>f;f++){var g=n.key(f),h=n.getItem(g);if(h&&(h=m.deserialize(h)),h=a(h,g.substring(d),f+1),void 0!==h)return h}});return j(d,b),d}function e(a,b){var c=this,d=c.ready().then(function(){var b,d=c._dbInfo;try{b=n.key(a)}catch(e){b=null}return b&&(b=b.substring(d.keyPrefix.length)),b});return j(d,b),d}function f(a){var b=this,c=b.ready().then(function(){for(var a=b._dbInfo,c=n.length,d=[],e=0;c>e;e++)0===n.key(e).indexOf(a.keyPrefix)&&d.push(n.key(e).substring(a.keyPrefix.length));return d});return j(c,a),c}function g(a){var b=this,c=b.keys().then(function(a){return a.length});return j(c,a),c}function h(a,b){var c=this;"string"!=typeof a&&(window.console.warn(a+" used as a key, but it is not a string."),a=String(a));var d=c.ready().then(function(){var b=c._dbInfo;n.removeItem(b.keyPrefix+a)});return j(d,b),d}function i(a,b,c){var d=this;"string"!=typeof a&&(window.console.warn(a+" used as a key, but it is not a string."),a=String(a));var e=d.ready().then(function(){void 0===b&&(b=null);var c=b;return new k(function(e,f){m.serialize(b,function(b,g){if(g)f(g);else try{var h=d._dbInfo;n.setItem(h.keyPrefix+a,b),e(c)}catch(i){("QuotaExceededError"===i.name||"NS_ERROR_DOM_QUOTA_REACHED"===i.name)&&f(i),f(i)}})})});return j(e,c),e}function j(a,b){b&&a.then(function(a){b(null,a)},function(a){b(a)})}var k="undefined"!=typeof module&&module.exports?require("promise"):this.Promise,l=this,m=null,n=null;try{if(!(this.localStorage&&"setItem"in this.localStorage))return;n=this.localStorage}catch(o){return}var p={DEFINE:1,EXPORT:2,WINDOW:3},q=p.WINDOW;"undefined"!=typeof module&&module.exports?q=p.EXPORT:"function"==typeof define&&define.amd&&(q=p.DEFINE);var r={_driver:"localStorageWrapper",_initStorage:a,iterate:d,getItem:c,setItem:i,removeItem:h,clear:b,length:g,key:e,keys:f};q===p.EXPORT?module.exports=r:q===p.DEFINE?define("localStorageWrapper",function(){return r}):this.localStorageWrapper=r}.call(window),function(){"use strict";function a(a){var b=this,c={db:null};if(a)for(var d in a)c[d]="string"!=typeof a[d]?a[d].toString():a[d];var e=new k(function(a){p===o.DEFINE?require(["localforageSerializer"],a):a(p===o.EXPORT?require("./../utils/serializer"):l.localforageSerializer)}),f=new k(function(d,e){try{c.db=n(c.name,String(c.version),c.description,c.size)}catch(f){return b.setDriver(b.LOCALSTORAGE).then(function(){return b._initStorage(a)}).then(d)["catch"](e)}c.db.transaction(function(a){a.executeSql("CREATE TABLE IF NOT EXISTS "+c.storeName+" (id INTEGER PRIMARY KEY, key unique, value)",[],function(){b._dbInfo=c,d()},function(a,b){e(b)})})});return e.then(function(a){return m=a,f})}function b(a,b){var c=this;"string"!=typeof a&&(window.console.warn(a+" used as a key, but it is not a string."),a=String(a));var d=new k(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){c.executeSql("SELECT * FROM "+e.storeName+" WHERE key = ? LIMIT 1",[a],function(a,c){var d=c.rows.length?c.rows.item(0).value:null;d&&(d=m.deserialize(d)),b(d)},function(a,b){d(b)})})})["catch"](d)});return j(d,b),d}function c(a,b){var c=this,d=new k(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){c.executeSql("SELECT * FROM "+e.storeName,[],function(c,d){for(var e=d.rows,f=e.length,g=0;f>g;g++){var h=e.item(g),i=h.value;if(i&&(i=m.deserialize(i)),i=a(i,h.key,g+1),void 0!==i)return void b(i)}b()},function(a,b){d(b)})})})["catch"](d)});return j(d,b),d}function d(a,b,c){var d=this;"string"!=typeof a&&(window.console.warn(a+" used as a key, but it is not a string."),a=String(a));var e=new k(function(c,e){d.ready().then(function(){void 0===b&&(b=null);var f=b;m.serialize(b,function(b,g){if(g)e(g);else{var h=d._dbInfo;h.db.transaction(function(d){d.executeSql("INSERT OR REPLACE INTO "+h.storeName+" (key, value) VALUES (?, ?)",[a,b],function(){c(f)},function(a,b){e(b)})},function(a){a.code===a.QUOTA_ERR&&e(a)})}})})["catch"](e)});return j(e,c),e}function e(a,b){var c=this;"string"!=typeof a&&(window.console.warn(a+" used as a key, but it is not a string."),a=String(a));var d=new k(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){c.executeSql("DELETE FROM "+e.storeName+" WHERE key = ?",[a],function(){b()},function(a,b){d(b)})})})["catch"](d)});return j(d,b),d}function f(a){var b=this,c=new k(function(a,c){b.ready().then(function(){var d=b._dbInfo;d.db.transaction(function(b){b.executeSql("DELETE FROM "+d.storeName,[],function(){a()},function(a,b){c(b)})})})["catch"](c)});return j(c,a),c}function g(a){var b=this,c=new k(function(a,c){b.ready().then(function(){var d=b._dbInfo;d.db.transaction(function(b){b.executeSql("SELECT COUNT(key) as c FROM "+d.storeName,[],function(b,c){var d=c.rows.item(0).c;a(d)},function(a,b){c(b)})})})["catch"](c)});return j(c,a),c}function h(a,b){var c=this,d=new k(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){c.executeSql("SELECT key FROM "+e.storeName+" WHERE id = ? LIMIT 1",[a+1],function(a,c){var d=c.rows.length?c.rows.item(0).key:null;b(d)},function(a,b){d(b)})})})["catch"](d)});return j(d,b),d}function i(a){var b=this,c=new k(function(a,c){b.ready().then(function(){var d=b._dbInfo;d.db.transaction(function(b){b.executeSql("SELECT key FROM "+d.storeName,[],function(b,c){for(var d=[],e=0;e