// String identifiers for use in hashes. var CUSTOM_OPTIONS_IDENTIFIER = "custom_options_list"; var SPOOF_OPTIONS_IDENTIFIER = "spoofer_list"; var BASE_OPTIONS_IDENTIFIER = "base_options_list"; var USE_PRESETS_IDENTIFIER = "use_presets"; var REPORT_ERRORS_IDENTIFIER = "report_errors"; var USE_SPOOF_PER_TAB = "use_per_tab"; var USE_SPOOF_PER_TAB_CACHE = "use_per_tab_cache"; var PRESET_LIST_IDENTIFIER = "preset_list"; var ERROR_IDENTIFIER = "error_"; var SPOOFER_LIST = "spoofer_list"; var HOTLIST_INDEX = "hotlist_index"; var PERMANENT_OVERRIDE = "permanent_override"; var NO_SPOOF_PER_TAB = "no_spoof"; var USE_SYNC = "use_sync"; var MANAGED_EDIT_RIGHTS = "EditRights"; var MANAGED_GROUP_NAME = "Managed"; // Legacy URL where updates used to exist. var update_url = "http://spoofer-extension.appspot.com/update"; // URL where error codes are sent for debugging purposes. var error_url = "http://spoofer-extension.appspot.com/error"; var last_update_time = null; var storage_location = null; // Hardcoded list of navigator.platform spoofs. var PLATFORM_SPOOF = {"Macintosh" : "MacIntel", "Android" : "Android", "Linux" : "Linux", "iPhone" : "iPhone", "iPod" : "iPod", "iPad" : "iPad", "Windows" : "Windows"}; // The sync storage API has throttling limits, this is to minimize the potential // of throttling on import. var MAX_IMPORT_COUNT = 450; var MAX_MEMORY_USAGE = 0.98; // Object for making XHRs. var req; // Hardcoded enumeration of error codes. var TEST_ERROR_CODE = "0"; var OUT_OF_SPACE = "1"; var MIGRATION_INCOMPLETE = "2"; var MIGRATION_FAILED_THROTTLED_USER_AGENTS = "3"; var MIGRATION_FAILED_THROTTLED_SPOOFS = "4"; var THROTTLED_WRITES = "5"; var ERROR_CODES = [TEST_ERROR_CODE, OUT_OF_SPACE, MIGRATION_INCOMPLETE, MIGRATION_FAILED_THROTTLED_USER_AGENTS, MIGRATION_FAILED_THROTTLED_SPOOFS, THROTTLED_WRITES]; var CUSTOM_OPTIONS_LIST_PREFIX = "c_"; var SPOOF_OPTIONS_LIST_PREFIX = "s_"; var MANAGED_USER_AGENT_LIST_PREFIX = "cm_"; var MANAGED_SPOOF_LIST_PREFIX = "sm_"; // Because http requests coming through the extension API have to be processed // super fast, we can't rely on using local storage to respond fast enough to // swap out the user-agent. Instead, we have to keep the same list in memory so // the swap will work. We keep a cache of both user-agents and hardcoded swaps. var user_agent_cache = []; var spoof_cache = []; var permanent_override_cache = false; function updateCache(callback) { _getUserAgentList(function(ua_list) { user_agent_cache = ua_list; _getSpoofList(function(spoof_list) { spoof_cache = spoof_list; getPermanentSpoofOverride(function(override) { getSpoofPerTab(function (spoof) { setSpoofPerTabCache(spoof); callback(); }); }); }); }); } updateCache(function(){}); // Object that encapsulates a user-agent. Makes passing around a set of values // related to a user-agent much easier. function UserAgent(title, ua_string, vendor_string, badge, is_preset, group) { this.title = title; this.ua_string = ua_string; this.vendor = vendor_string; this.badge = badge; this.is_preset = is_preset; this.append_to_default_ua = false; this.group = group; this.show_in_list = false; this.is_managed = false; this.key = ""; this.getUserAgentString = new function(current_user_agent_string) { if (this.append_to_default_ua) { return current_user_agent_string + ' ' + this.ua_string; } return this.ua_string; } } function PresetSpoof(domain, user_agent) { this.domain = domain; this.user_agent = user_agent; this.is_managed = false; this.key = ""; } function ErrorReport(code, message) { this.error_code = code; this.error_message = message; } /*****************************************************************************/ /* Legacy functions for getting localstorage data */ /*****************************************************************************/ // Storage used to be done in the extension in localStorage, but that proved // to be limited in ability and size. These functions remain to both migrate // legacy settings to the chrome.storage API, or to set fast n' dirty settings // that are not meant to be persisted across sessions. function getItem(identifier_obj) { return localStorage[identifier_obj]; } // The value needs to be stringified before setting it here. (JSON.stringify) function storeItem(key, value) { localStorage[key] = value; } function _isUsingLocalStorage() { return (localStorage[CUSTOM_OPTIONS_IDENTIFIER] > 0 || localStorage[SPOOF_OPTIONS_IDENTIFIER] > 0); } // Wipes all settings. Usually useful for catastrophic data corruption. function resetEverything(callback) { getIsOtherSettingsManaged(function(managed) { if (!managed) { var pointer_values = {}; pointer_values[("" + CUSTOM_OPTIONS_IDENTIFIER)] = []; var spoof_values = {}; spoof_values[("" + SPOOF_OPTIONS_IDENTIFIER)] = []; getStorageLocation().set(pointer_values, function() { getStorageLocation().set(spoof_values, function() { updateCache(function() {}); }); }); localStorage[CUSTOM_OPTIONS_IDENTIFIER] = ""; localStorage[SPOOF_OPTIONS_IDENTIFIER] = ""; } callback(); }); } function getSpoofPerTabCache() { var cache = getItem(USE_SPOOF_PER_TAB_CACHE); return (cache && cache == "true"); } function setSpoofPerTabCache(spoof) { storeItem(USE_SPOOF_PER_TAB_CACHE, ""+spoof); } /*****************************************************************************/ /* Legacy migration methods */ /*****************************************************************************/ // Adds a fake user-agent and spoof to local storage, then attempts to move // those objects to sync storage. function _testMigration() { // Add some stuff to local storage, then call the migration, // then see if its in sync storage. var test1 = new UserAgent("Opera 10", "Opera/9.80 (Macintosh; Intel Mac OS X; U; en) Presto/2.2.15 Version/10.00", "Mozilla, Inc.", "O10", true, "Opera"); var test2 = new PresetSpoof("www.test.com", test1); var ary1 = new Array(); ary1.push(test1); var ary2 = new Array(); ary2.push(test2); storeItem(CUSTOM_OPTIONS_IDENTIFIER, JSON.stringify(ary1)); storeItem(SPOOF_OPTIONS_IDENTIFIER, JSON.stringify(ary2)); _moveLocalStorageToSyncStorage(); } // Finds how much of the current storage location is used by our data. // Useful for showing the user how much space they have left, or giving // a reason why they can't save anything more. function getPercentOfStorageUsed(callback) { getStorageLocation().getBytesInUse(null, function(bytes) { callback(bytes / getStorageLocation().QUOTA_BYTES); }); } function _moveLocalStorageToSyncStorage() { // Move all items from localStorage until everything is moved. _moveSpoofsToSyncStorage(function() { console.log("Migrated spoofs to sync storage."); }); _moveUserAgentsToSyncStorage(function() { console.log("Migrated user agents to sync storage.") }); if (_isUsingLocalStorage()) { console.log("Migration still incomplete -- waiting for next time to finish up."); recordError(MIGRATION_INCOMPLETE, ""); } else console.log("Migration complete!"); } // For migrating legacy configurations from localStorage to chrome.storage. function _moveUserAgentsToSyncStorage(callback) { var ua_list = JSON.parse(getItem(CUSTOM_OPTIONS_IDENTIFIER)); _addMultipleUserAgents(ua_list, function(pointer_list, unset_ua_list) { if (pointer_list && unset_ua_list && unset_ua_list.length > 0) { // Not everything got added. Likely, we hit a throttle. // Keep the old stuff around until we can migrate it next time. recordError(MIGRATION_FAILED_THROTTLED_USER_AGENTS, ""); console.log("" + unset_ua_list.length + " user-agents were not able to be "); console.log("migrated. Leaving them in local storage until we can re-try."); storeItem(CUSTOM_OPTIONS_IDENTIFIER, unset_ua_list); } else { storeItem(CUSTOM_OPTIONS_IDENTIFIER, ""); } callback(); }); } // For migrating legacy configurations from localStorage to chrome.storage. function _moveSpoofsToSyncStorage(callback) { var spoof_list = JSON.parse(getItem(SPOOF_OPTIONS_IDENTIFIER)); _addMultipleSpoofs(spoof_list, function(pointer_list, unset_spoof_list) { if (pointer_list && unset_spoof_list && unset_spoof_list.length > 0) { // Not everything got added. Likely, we hit a throttle. // Keep the old stuff around until we can migrate it next time. recordError(MIGRATION_FAILED_THROTTLED_SPOOFS, ""); console.log("" + unset_spoof_list.length + " spoofs were not able to be "); console.log("migrated. Leaving them in local storage until we can re-try.") storeItem(SPOOF_OPTIONS_IDENTIFIER, unset_spoof_list); } else { storeItem(SPOOF_OPTIONS_IDENTIFIER, ""); } callback(); }); } function _syncSpoofsAcrossStorageLocations(from_storage, to_storage, callback) { storage_location = from_storage; _getSpoofList(function(spoof_list) { storage_location = to_storage; _addMultipleSpoofs(spoof_list, function(pointer_list, unset_spoof_list) { console.log("Migrated spoofs from " + from_storage + " to " + to_storage); }); }); storage_location = from_storage; callback(); } function _syncUserAgentsAcrossStorageLocations(from_storage, to_storage, callback) { storage_location = from_storage; _getUserAgentList(function(user_agent_list) { storage_location = to_storage; _addMultipleUserAgents(user_agent_list, function(pointer_list, unset_ua_list) { console.log("Migrated user_agents from " + from_storage + " to " + to_storage); }); }); storage_location = from_storage; callback(); } function _syncDataAcrossStorageLocations(from_storage, to_storage, callback) { to_storage.clear(function() { _syncUserAgentsAcrossStorageLocations(from_storage, to_storage, function() { _syncSpoofsAcrossStorageLocations(from_storage, to_storage, function () { console.log("Data migrated from " + from_storage + " to " + to_storage + "."); }); }); }); callback(); } function getStorageLocation() { if (!storage_location) storage_location = (getUseSync() ? chrome.storage.sync : chrome.storage.local); return storage_location; } /*****************************************************************************/ /* Configuration getter/setter methods */ /*****************************************************************************/ function setUseSync(use_sync) { getIsUserAgentManaged(function(managed) { if (!managed) { if (use_sync) { storeItem(USE_SYNC, "true"); storage_location = chrome.storage.sync; } else { storeItem(USE_SYNC, ""); storage_location = chrome.storage.local; } } }); } function getUseSync() { var use_sync = getItem(USE_SYNC); return (use_sync && use_sync == "true"); } // Sets whether errors get reported to the hardcoded error URL. function setReportErrors(report_errors) { getIsOtherSettingsManaged(function(managed) { if (!managed) { storeItem(REPORT_ERRORS_IDENTIFIER, "" + report_errors); }}); } function getReportErrors(callback) { getSendErrorsManaged(function(managed) { if (managed != null) { callback(managed); } var builtin = getItem(REPORT_ERRORS_IDENTIFIER); callback(!builtin || builtin == "true"); }); } // Sets whether spoofs are across all tabs or only for the current active tab. function setSpoofPerTab(spoof_per_tab) { getSpoofPerTabManaged(function(managed) { if (managed == null) { storeItem(USE_SPOOF_PER_TAB, "" + spoof_per_tab); // Clear out any settings per tab. storeItem(HOTLIST_INDEX, JSON.stringify({})); updateCache(function(){}); } }); } function getSpoofPerTab(callback) { getSpoofPerTabManaged(function(managed) { var spoof_per_tab = false; if (managed == null) { var builtin = getItem(USE_SPOOF_PER_TAB); spoof_per_tab = (builtin && builtin == "true"); } else spoof_per_tab = managed; if (callback) callback(spoof_per_tab); }); } // Sets whether permanent spoofs override the hot-switch. function setPermanentSpoofOverride(permanent_override) { getSpoofOverrideManaged(function(managed) { if (managed == null) { storeItem(PERMANENT_OVERRIDE, "" + permanent_override); updateCache(function(){}); // we store this setting in the cache } }); } function getPermanentSpoofOverride(callback) { getSpoofOverrideManaged(function(managed) { if (managed == null) { var permanent_override = getItem(PERMANENT_OVERRIDE); permanent_override_cache = (permanent_override && permanent_override == "true"); } else permanent_override_cache = managed; callback(permanent_override_cache); }); } /*****************************************************************************/ /* Enterprise configuration getter methods */ /*****************************************************************************/ function getIsUserAgentManaged(callback) { chrome.storage.managed.get(MANAGED_EDIT_RIGHTS, function(edit_rights) { callback(edit_rights && MANAGED_EDIT_RIGHTS in edit_rights && "user_agents" in edit_rights[MANAGED_EDIT_RIGHTS]); }); } function getIsSpoofManaged(callback) { chrome.storage.managed.get(MANAGED_EDIT_RIGHTS, function(edit_rights) { callback((edit_rights && MANAGED_EDIT_RIGHTS in edit_rights && "permanent_spoofs" in edit_rights[MANAGED_EDIT_RIGHTS])); }); } function getIsOtherSettingsManaged(callback) { chrome.storage.managed.get(MANAGED_EDIT_RIGHTS, function(edit_rights) { callback(edit_rights && MANAGED_EDIT_RIGHTS in edit_rights && "other_settings" in edit_rights[MANAGED_EDIT_RIGHTS]); }); } function getSpoofOverrideManaged(callback) { chrome.storage.managed.get("OtherSettings", function(other_settings) { if(other_settings && other_settings["OtherSettings"] && "spoof_override" in other_settings["OtherSettings"]) { callback(other_settings["OtherSettings"]["spoof_override"]); } else callback(null); }); } function getHotlistEnabledManaged(callback) { chrome.storage.managed.get("OtherSettings", function(other_settings) { if(other_settings && other_settings["OtherSettings"] && "hotlist_enabled" in other_settings["OtherSettings"]) { callback(other_settings["OtherSettings"]["hotlist_enabled"]); } else callback(null); }); } function getSpoofPerTabManaged(callback) { chrome.storage.managed.get("OtherSettings", function(other_settings) { if(other_settings && other_settings["OtherSettings"] && "spoof_per_tab" in other_settings["OtherSettings"]) { callback(other_settings["OtherSettings"]["spoof_per_tab"]); } else callback(null); }); } function getSendErrorsManaged(callback) { chrome.storage.managed.get("OtherSettings", function(other_settings) { if(other_settings && other_settings["OtherSettings"] && "send_errors" in other_settings["OtherSettings"]) { callback(other_settings["OtherSettings"]["send_errors"]); } else callback(null); }); } function getErrors() { var errors = getItem(ERROR_IDENTIFIER); if (!errors) errors = {}; else errors = JSON.parse(errors); return errors; } function recordError(code, message) { var errors = getErrors(); if (errors[code]) { errors[code] = parseInt(errors[code]) + 1; } else errors[code] = 1; storeItem(ERROR_IDENTIFIER, JSON.stringify(errors)); } function clearErrors() { storeItem(ERROR_IDENTIFIER, JSON.stringify({})); } function generateTestError() { recordError(TEST_ERROR_CODE, ""); } /*****************************************************************************/ /* Functions for accessing data from sync storage */ /*****************************************************************************/ // Data is stored in chrome.storage as a list of pointers, and those pointers // point at the data itself. The size of a single object is capped with // chrome.storage, so it could not be all saved as one list. // The two major lists of things to be saved and retrieved are: // 1. a list of all user-agents // 2. a list of all hard coded spoofs // Both of these has its own pointer list. // Each of the objects stored this way has a "key" field where it saves its // own pointer, in case you need to access it. // Most of these functions require callback methods because they rely on // chrome.storage APIs, which all require callbacks. This makes them all // asynchronous, and blocking has to be done at a higher scope than this UI. function _getUserAgentPointerList(callback) { getStorageLocation().get(CUSTOM_OPTIONS_IDENTIFIER, function(pointer_list) { if (callback) return callback(pointer_list ? pointer_list[("" + CUSTOM_OPTIONS_IDENTIFIER)] : []); return null; }); } // Access a User Agent object by its pointer. function _getUserAgentByKey(key, callback) { getStorageLocation().get(key, function(user_agent) { return callback(user_agent[key]); }); } function _getUserAgentList(callback) { _getManagedUserAgentList(function(managed_list) { if (!managed_list) managed_list = new Array(); _getUserAgentPointerList(function(pointer_list) { if (!pointer_list || pointer_list.length == 0) return callback(managed_list); getStorageLocation().get(pointer_list, function(user_agent_map) { for (var key in user_agent_map) managed_list.push(user_agent_map[key]); return callback(managed_list); }) return callback(managed_list); }); }); } function _getManagedUserAgentList(callback) { var managed_list = []; chrome.storage.managed.get("UserAgents", function(obj) { if (obj && obj["UserAgents"]) { var list = obj["UserAgents"]; for (var i = 0; i < list.length; i++) { var ua = new UserAgent(); ua.is_managed = true; ua.title = list[i].title; ua.ua_string = list[i].ua_string; ua.vendor = list[i].vendor; ua.badge = list[i].badge; ua.append_to_default_ua = list[i].append; ua.group = MANAGED_GROUP_NAME; ua.key = MANAGED_USER_AGENT_LIST_PREFIX + i; managed_list.push(ua); } } }); callback(managed_list); } // Add a signle user-agent object to storage. function _addUserAgent(user_agent, callback) { getIsUserAgentManaged(function(managed) { if (!managed) { _getUserAgentPointerList(function(pointer_list) { if (!pointer_list) pointer_list = []; if (!user_agent.key || user_agent.key == "") { var max = 0; if (pointer_list && pointer_list.length > 0) { for (var i = 0; i < pointer_list.length; i++) { var index = parseInt(pointer_list[i].substring(CUSTOM_OPTIONS_LIST_PREFIX.length)); if (index >= max) max = index; } } user_agent.key = CUSTOM_OPTIONS_LIST_PREFIX + (max + 1); } // Can't just store the user-agent by itself, we have to // create a dict and point one key value to our user-agent object, // then save the dict. var foo = user_agent.key; var values = {}; values[(""+foo)] = user_agent; getStorageLocation().set(values, function() { pointer_list.push(user_agent.key); var pointer_values = {}; pointer_values[("" + CUSTOM_OPTIONS_IDENTIFIER)] = pointer_list; callback = (callback ? callback : function() {}); getStorageLocation().set(pointer_values, function() { updateCache(callback); }); }); }); } }); } function _editUserAgent(user_agent, callback) { getIsUserAgentManaged(function(managed) { if (!managed && user_agent && user_agent.key) { var foo = user_agent.key; var values = {}; values[(""+foo)] = user_agent; getStorageLocation().set(values, function() { updateCache(callback(user_agent)); }); // no need to update the pointer list, since we're keeping // all of our pointers the same. } }); } // Adds multiple user agent objects at once. // Calls back with a list of the pointers to the user-agent objects added. // If it hit a throttling error, calls back with a list of succesfully added // pointers *and* a list of user-agents not added. function _addMultipleUserAgents(user_agent_list, callback) { if (!user_agent_list || user_agent_list.length == 0) return callback(); getIsUserAgentManaged(function(managed) { if (!managed) { _getUserAgentPointerList(function(pointer_list) { pointer_list = (pointer_list ? pointer_list : []); var max = 0; if (pointer_list && pointer_list.length > 0) { for (var i = 0; i < pointer_list.length; i++) { var index = parseInt(pointer_list[i].substring(CUSTOM_OPTIONS_LIST_PREFIX.length)); if (index >= max) max = index; } } for (var j = 0; j < user_agent_list.length; j++) { user_agent_list[j].key = CUSTOM_OPTIONS_LIST_PREFIX + (max + 1 + j); // pointer_list.push(user_agent_list[j].key); } var set_user_agent = function(list, pointer_list, callback) { if (!list || list.length == 0) return callback(pointer_list); getPercentOfStorageUsed(function(pct) { if (pct > MAX_MEMORY_USAGE) { recordError(OUT_OF_SPACE, ""); console.log("Error in adding user-agents: ran out of space."); //updateCache(callback(pointer_list, list)); } }); var user_agent = list.pop(); if (!user_agent) return callback(pointer_list); var foo = user_agent.key; if (!foo) { console.log("User agent does not have a key -- bailing out."); return callback(pointer_list); } var values = {}; values[(""+foo)] = user_agent; getStorageLocation().set(values, function() { if(chrome.runtime.lastError) { // We likely exceeded our quota. Back off from recursively // hitting this over and over. // TODO(gwilson): Show an error to the user somewhere. recordError(OUT_OF_SPACE, ""); console.log("Error in adding user-agent: " + user_agent.title); console.log("We likely hit the maximum writes per hour."); updateCache(callback(pointer_list)); } else { pointer_list.push(""+foo); // user_agent.key set_user_agent(list, pointer_list, callback); // maybe wrap callback in updateCache. } }); } set_user_agent(user_agent_list, pointer_list, function(pointers) { // Some horrible error (likely a write maximum hit). Bail. if (pointers == null) { recordError(THROTTLED_WRITES, ""); return; } var pointer_values = {}; pointer_values[("" + CUSTOM_OPTIONS_IDENTIFIER)] = pointers; callback = (callback ? callback : function() {}); getStorageLocation().set(pointer_values, function() { updateCache(callback); }); }); }); } }); callback(); } // Managed user agents don't need to be protected because they are stored in // a different data space that cannot be manipulated. It will simply fail. function _deleteUserAgentByKey(key, callback) { getIsUserAgentManaged(function(managed) { if (!managed) { _getUserAgentPointerList(function(pointer_list) { for (var i = 0; i < pointer_list.length; i++) if (pointer_list[i] == key) { pointer_list.splice(i, 1); break; } getStorageLocation().set({CUSTOM_OPTIONS_IDENTIFIER: pointer_list}, function() { getStorageLocation().remove(key, function() { updateCache(callback); }); }); }); } }); } // TODO (gwilson): refactor these functions with the ones above function _getSpoofPointerList(callback) { getStorageLocation().get(SPOOF_OPTIONS_IDENTIFIER, function(pointer_list) { if (callback) callback( (pointer_list ? pointer_list[("" + SPOOF_OPTIONS_IDENTIFIER)] : []) ); }); } function _getSpoofList(callback) { _getManagedSpoofList(function(managed_list) { if (!managed_list) managed_list = new Array(); _getSpoofPointerList(function(pointer_list) { if (!pointer_list || pointer_list.length == 0) return callback(managed_list); return getStorageLocation().get(pointer_list, function(spoof_map) { for (var key in spoof_map) managed_list.push(spoof_map[key]); return callback(managed_list); }) }); }); } function _getManagedSpoofList(callback) { var managed_list = []; chrome.storage.managed.get("PermanentSpoofs", function(obj) { if (obj && obj["PermanentSpoofs"]) { var list = obj["PermanentSpoofs"]; for (var i = 0; i < list.length; i++) { var spoof = new PresetSpoof(); spoof.domain = list[i].domain; spoof.is_managed = true; spoof.key = MANAGED_SPOOF_LIST_PREFIX + i; var ua = new UserAgent(); ua.is_managed = true; ua.title = list[i].user_agent.title; ua.ua_string = list[i].user_agent.ua_string; ua.vendor = list[i].user_agent.vendor; ua.badge = list[i].user_agent.badge; ua.append_to_default_ua = list[i].user_agent.append; ua.group = MANAGED_GROUP_NAME; spoof.user_agent = ua; managed_list.push(spoof); } } }); callback(managed_list); } function _addSpoof(preset_spoof, callback) { getIsSpoofManaged(function(managed) { if (!managed) { // preset_spoof.user_agent will be empty if they want to spoof with the default UA always. if (!preset_spoof) { console.log("Preset spoof to add doesn't exist, bailing."); return; } _getSpoofPointerList(function(pointer_list) { pointer_list = (pointer_list ? pointer_list : []); if (!preset_spoof.key || preset_spoof.key == "") { var max = 0; if (pointer_list && pointer_list.length > 0) { for (var i = 0; i < pointer_list.length; i++) { var index = parseInt(pointer_list[i].substring(SPOOF_OPTIONS_LIST_PREFIX.length)); if (index >= max) max = index; } } preset_spoof.key = SPOOF_OPTIONS_LIST_PREFIX + (max + 1); } var values = {}; values[("" + preset_spoof.key)] = preset_spoof; getStorageLocation().set(values, function() { pointer_list.push(preset_spoof.key); var pointer_values = {}; pointer_values[(""+SPOOF_OPTIONS_IDENTIFIER)] = pointer_list; callback = (callback ? callback : function() {}); getStorageLocation().set(pointer_values, function() { updateCache(callback); }); }); }); } else callback(false); }); } // TODO(gwilson): Refactor this with _addMultipleUserAgents. They're almost // exactly the same. // To add a long set of spoofs at once, we can't do a bulk pointer commit, then // a bulk value commit. We don't know when the API will run out of space, so // we have to do one at a time. function _addMultipleSpoofs(spoof_list, callback) { getIsSpoofManaged(function(managed) { if (!managed) { if (!spoof_list || spoof_list.length == 0) return callback(); _getSpoofPointerList(function(pointer_list) { pointer_list = (pointer_list ? pointer_list : []); var max = 0; if (pointer_list && pointer_list.length > 0) { for (var i = 0; i < pointer_list.length; i++) { var index = parseInt(pointer_list[i].substring(SPOOF_OPTIONS_LIST_PREFIX.length)); if (index >= max) max = index; } } for (var j = 0; j < spoof_list.length; j++) { spoof_list[j].key = SPOOF_OPTIONS_LIST_PREFIX + (max + 1 + j); } var set_spoof = function(list, pointer_list, callback) { if (!list || list.length == 0) callback(pointer_list); getPercentOfStorageUsed(function(pct) { if (pct > MAX_MEMORY_USAGE) { console.log("Error in adding spoofs: Ran out of space"); //updateCache(callback(pointer_list, list)); } }); var spoof = list.pop(); if (!spoof) return callback(pointer_list); var foo = spoof.key; if (!foo) { console.log("Spoof does not have a key. Bailing out."); return callback(pointer_list); } var values = {}; values[(""+foo)] = spoof; getStorageLocation().set(values, function() { if(chrome.runtime.lastError) { // We likely exceeded our quota. Back off from recursively // hitting this over and over. // TODO(gwilson): Show an error to the user somewhere. console.log("Error in adding user-agents: " + chrome.runtime.lastError); console.log("We likely hit the maximum writes per hour."); recordError(THROTTLED_WRITES, ""); // TODO(gwilson): should we not call back if we're already at write // maximums? updateCache(callback(pointer_list, list)); } else { pointer_list.push(""+foo); //spoof.key set_spoof(list, pointer_list, callback); } }); } set_spoof(spoof_list, pointer_list, function(pointers) { // Some horrible error (likely a write maximum hit). Bail. if (pointers == null) { recordError(MIGRATION_FAILED_THROTTLED_SPOOFS, ""); return; } var pointer_values = {}; pointer_values[("" + SPOOF_OPTIONS_IDENTIFIER)] = pointers; callback = (callback ? callback : function() {}); getStorageLocation().set(pointer_values, function() { updateCache(callback); }); }); }); } else callback(false); }); } function _deleteSpoofByKey(key, callback) { getIsSpoofManaged(function(managed) { if (!managed) { _getSpoofPointerList(function(pointer_list) { for (var i = 0; i < pointer_list.length; i++) if (pointer_list[i] == key) { pointer_list.splice(i, 1); break; } getStorageLocation().set({SPOOF_OPTIONS_IDENTIFIER: pointer_list}, function() { getStorageLocation().remove(key, function() { updateCache(callback); }); }); }); } else callback(false); }); } // Returns the full spoof list grouped by the string title of each UA's group. // This is useful for the UI, so we can do something like show 'Title' - 'list of UAs'. function getOptionsByGroup(callback) { _getUserAgentList(function(user_agent_list) { var groups = {}; for (var i = 0; i < user_agent_list.length; i++) { if (!user_agent_list[i]) continue; var group = getUserAgentGroup(user_agent_list[i]); if (!groups[group]) { groups[group] = new Array(); } groups[group].push(user_agent_list[i]); } callback(groups); }); } /*****************************************************************************/ /* "Hotlist" functions */ /*****************************************************************************/ // Functions for getting and setting the "hotlist" or the on-the-fly spoof that // the user chooses from the icon in the upper right of Chrome. function getHotlistMap() { var index_map = getItem(HOTLIST_INDEX); if (index_map) { index_map = JSON.parse(index_map); if (!index_map) index_map = {}; } else index_map = {}; return index_map; } function getHotlistIndex(tab, callback) { getHotlistIndexById(tab.id, callback); } function getHotlistIndexById(tabId, callback) { if (!tabId) { console.log("no tab provided, returning nothing"); if (callback) callback(null); return null; } var index_map = getHotlistMap(); if(!getSpoofPerTabCache()) tabId = NO_SPOOF_PER_TAB; var index = (index_map[tabId] ? index_map[tabId] : null); if (callback) callback(index); return index; } function setHotlistIndex(tab, index) { // Makes sure the items we want to use as a hotlist are in the cache. if (!tab || !tab.id) { console.log("no tab passed, not saving anything.") return; } getHotlistEnabledManaged(function(managed) { if (managed == false) { console.log("Hotlist enabled is managed. ignoring input."); return; } var tabId = (getSpoofPerTabCache() ? tab.id : NO_SPOOF_PER_TAB); var map = getHotlistMap(); map[tabId] = index; console.log("Storing " + index + " under tab " + tabId); storeItem(HOTLIST_INDEX, JSON.stringify(map)); updateCache(function(){ }); }); } function getHotlist(tab, callback) { getHotlistEnabledManaged(function(managed) { if (managed != null && managed == false) { callback(null); return; } }); getHotlistIndex(tab, function(hotlist_ua_index) { if (!hotlist_ua_index || hotlist_ua_index == "") callback(null); else { _getUserAgentByKey(hotlist_ua_index, callback); } }); } function setHotlist(tab, user_agent_index) { setHotlistIndex(tab, user_agent_index); } /*****************************************************************************/ /* Helper / utility functions */ /*****************************************************************************/ // Parses the hostname/domain out of the given URL. // Probably a better way to do this, but this way was consistent // with how the browser interprets hosts. function findHostname(url) { var a = document.createElement("a"); a.href = url; return a.host; } function isDomainName(str) { var pattern = new RegExp('^(https?:\/\/)?'+ // protocol '((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|'+ // domain name '((\d{1,3}\.){3}\d{1,3}))'+ // OR ip (v4) address '(\:\d+)?(\/[-a-z\d%_.~+]*)*$','i'); return pattern.test(str); } // Returns a "base" set of user-agent strings, while // saving them to storage. function getBaseOptionsList(hard_reset, callback) { var base_options_list = new Array(); callback = (callback ? callback : function() {}); base_options_list.push(new UserAgent("Default", "", "", "", true, "Chrome")); base_options_list.push(new UserAgent("Windows Firefox 33", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:33.0) Gecko/20120101 Firefox/33.0", "Mozilla, Inc.", "FFW", true, "Firefox")); base_options_list.push(new UserAgent("Mac Firefox 33", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10; rv:33.0) Gecko/20100101 Firefox/33.0", "Mozilla, Inc.", "FFM", true, "Firefox")); base_options_list.push(new UserAgent("Opera 12.14", "Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14", "Mozilla, Inc.", "O12", true, "Opera")); base_options_list.push(new UserAgent("Mac Safari 7", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A", "Apple, Inc.", "S7", true, "Safari")); base_options_list.push(new UserAgent("Internet Explorer 6", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; WOW64; Trident/4.0; SLCC1)", "Microsoft", "IE6", true, "Internet Explorer")); base_options_list.push(new UserAgent("Internet Explorer 7", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; WOW64; Trident/4.0; SLCC1)", "Microsoft", "IE7", true, "Internet Explorer")); base_options_list.push(new UserAgent("Internet Explorer 8", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; WOW64; Trident/4.0; SLCC1)", "Microsoft", "IE8", true, "Internet Explorer")); base_options_list.push(new UserAgent("Internet Explorer 9", "Mozilla/5.0 (MSIE 9.0; Windows NT 6.1; Trident/5.0)", "Microsoft", "IE9", true, "Internet Explorer")); base_options_list.push(new UserAgent("Internet Explorer 10", "Mozilla/5.0 (MSIE 10.0; Windows NT 6.1; Trident/5.0)", "Microsoft", "IE10", true, "Internet Explorer")); base_options_list.push(new UserAgent("iPhone 6", "Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25", "Apple, Inc.", "IP6", true, "iOS")); base_options_list.push(new UserAgent("iPad", "Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25", "Apple, Inc.", "iPad", true, "iOS")); base_options_list.push(new UserAgent("Android KitKat", "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Mobile Safari/537.36", "", "AND", true, "Android")); base_options_list.push(new UserAgent("Windows Phone 8", "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 920)", "", "WP7", true, "Windows Phone")); if (hard_reset) { _getUserAgentList(function(list) { var key_list = []; for (var i = 0; i < list.length; i++) { key_list.push(list[i].key); } if (key_list && key_list.length > 0) getStorageLocation().remove(key_list, function() { getStorageLocation().remove(CUSTOM_OPTIONS_IDENTIFIER, function() { _addMultipleUserAgents(base_options_list, callback); }); }); else _addMultipleUserAgents(base_options_list, callback); }); } callback(base_options_list); } function __addBaseOption(option_list) { if (!option_list || option_list.length == 0) return; _addUserAgent(option_list.pop(), function() { __addBaseOption(option_list); }); } function addCustomUAOption(name, user_agent, append_to_default_ua, indicator, group, callback) { var new_user_agent = new UserAgent(name, user_agent, "", indicator, false, group); new_user_agent.append_to_default_ua = append_to_default_ua; _addUserAgent(new_user_agent, callback); } function deleteCustomUAOption(name, callback) { _getUserAgentList(function(options_list) { for (var i = 0; i < options_list.length; i++) { if (options_list[i].title == name) { _deleteUserAgentByKey(options_list[i].key, callback); break; } } }); } // For legacy support. Probably no longer needed. function isRemovable(user_agent) { return (!isDefault(user_agent)); } function getDisplayUserAgentString(user_agent) { return (user_agent.ua_string == "" ? "[Use default User-agent string]" : user_agent.ua_string); } function getDisplayAppendOrReplaceString(user_agent) { return (user_agent.ua_string == "" ? "N/A" : (user_agent.append_to_default_ua ? "Append" : "Replace")); } function guessUserAgentGroup(str) { if (!str) return ""; // If there is no group defined, guess one. var guesses = [["Chrome", "Chrome"], ["Firefox", "Firefox"], ["Opera", "Opera"], ["Safari", "Safari"], ["IE", "Internet Explorer"], ["Internet Explorer", "Internet Explorer"], ["iPhone", "iPhone"], ["iPad", "iPad"], ["iOS", "iOS"]]; for (var i = 0; i < guesses.length; i++) { if (str.toUpperCase().indexOf(guesses[i][0].toUpperCase()) > -1) return guesses[i][1]; } return ""; } function getUserAgentGroup(user_agent) { if (!user_agent) return ""; if (user_agent.group) return user_agent.group; return ""; } // Given a string of JSON, if it is well formed as determined by // schema.json, import the settings. // Does not overwrite local settings. function importJson(raw_data, callback) { try { var result = JSON.parse(raw_data); // Read in UserAgents var user_agents = result["UserAgents"]; var hash = {}; if (user_agents && user_agents.length > 0) { _getUserAgentList(function(options) { // Make sure we don't keep importing the same object repeatedly. // Make a hash of all existing options and if there is a collision with // items to import, don't import it. for (var i = 0; i < options.length; i++) hash[options[i].ua_string] = "1"; var list_to_add = []; for (var j = 0; j < user_agents.length; j++) { // Don't test for user_agent.append, because it's boolean and // could be false. if (user_agents[j].title && user_agents[j].ua_string && user_agents[j].badge) { var ua = new UserAgent(user_agents[j].title, user_agents[j].ua_string, user_agents[j].vendor, user_agents[j].badge, false, user_agents[j].group); ua.append_to_default_ua = user_agents[j].append_to_default_ua; ua.is_managed = false; if (!hash[ua.ua_string]) { hash[ua.ua_string] = "1"; list_to_add.push(ua); } } else { console.log("Ignoring incomplete user agent entry " + user_agents[j].title); } } _addMultipleUserAgents(list_to_add, callback()); }); } var permanent_spoofs = result["PermanentSpoofs"]; hash = new Array(); if (permanent_spoofs && permanent_spoofs.length > 0) { _getSpoofList(function(spoofs) { // Make sure we don't keep importing the same object repeatedly. // Make a hash of all existing options and if there is a collision with // items to import, don't import it. for (var i = 0; i < spoofs.length; i++) hash[spoofs[i].domain + " " + spoofs[i].user_agent.ua_string] = "1"; var list_to_add = []; for (var j = 0; j < permanent_spoofs.length; j++) { // Don't test for user_agent.append, because it's boolean and // could be false. if (permanent_spoofs[j].domain && permanent_spoofs[j].user_agent && permanent_spoofs[j].user_agent.title && permanent_spoofs[j].user_agent.ua_string && permanent_spoofs[j].user_agent.vendor && permanent_spoofs[j].user_agent.badge) { var ua = new UserAgent(permanent_spoofs[j].user_agent.title, permanent_spoofs[j].user_agent.ua_string, permanent_spoofs[j].user_agent.vendor, permanent_spoofs[j].user_agent.badge, false, permanent_spoofs[j].group); ua.append_to_default_ua = permanent_spoofs[j].user_agent.append_to_default_ua; ua.is_managed = false; var spoof = new PresetSpoof(permanent_spoofs[j].domain, ua); spoof.is_managed = false; if (!hash[spoof.domain + " " + spoof.user_agent.ua_string]) { hash[spoof.domain + " " + spoof.user_agent.ua_string] = "1"; list_to_add.push(spoof); } } else { console.log("Ignoring incomplete spoof entry " + permanent_spoofs[j]); } } _addMultipleSpoofs(list_to_add, callback()); }); } } catch(err) { console.log("The given text was not valid JSON. Import aborted: " + err); } } // Returns a string of JSON that encapsulates all of the settings data. // Structure will look like this, returned as a string: // { // "UserAgents": [ // {"title": "1", "ua_string": "ua_1", "vendor": "v_1", "badge": "A1", "append_to_default_ua" : false}, // {"title": "2", "ua_string": "ua_2", "vendor": "v_2", "badge": "A2", "append_to_default_ua" : false} // ], // "PermanentSpoofs": [ // {"domain" : "foo.com", "user_agent": {"title": "4", "ua_string": "ua_4", "vendor": "v_4", "badge": "A4", "append_to_default_ua" : false}}, // {"domain" : "bar.com", "user_agent": {"title": "5", "ua_string": "ua_5", "vendor": "v_5", "badge": "A5", "append_to_default_ua" : true}} // ] // } function exportJson(callback) { var export_obj = {}; _getUserAgentList(function(ua_list) { export_obj["UserAgents"] = ua_list; _getSpoofList(function(spoof_list) { export_obj["PermanentSpoofs"] = spoof_list; callback(JSON.stringify(export_obj)); }); }); } // Method for pulling a long list of structured user-agent data into memory. function importUserAgentData(raw_data, callback) { // Credit goes to jeffd at http://techpatterns.com/forums/about304.html for // posting a whole bunch of ua strings. Thanks! // // Assume that the input consists of XML that takes the form of: // // // // // // // // _getUserAgentList(function(options) { var import_count = 0; var duplicates = 0; var hash = {}; for (var i = 0; i < options.length; i++) hash[options[i].ua_string] = "1"; var list_to_add = []; $('useragent', raw_data).each(function() { var ua = new UserAgent($(this).attr("description"), // title $(this).attr("useragent"), // ua string "", // Vendor string "X", // badge false, // is preset _getParentFolderName($(this).parent())); // Ignore the "about" group. if (ua.group != "UA List :: About") { if (hash[ua.ua_string]) { duplicates++; } else if (import_count < MAX_IMPORT_COUNT) { hash[ua.ua_string] = "1"; list_to_add.push(ua); import_count++; } } }); _addMultipleUserAgents(list_to_add, callback({"import_count": import_count, "duplicates" : duplicates})); }); } function _getParentFolderName(obj) { var parent_folder_name = ($(obj) && $(obj).parent().length > 0) ? _getParentFolderName($(obj).parent()) : null; var my_folder_name = $(obj).attr("description"); return (parent_folder_name ? parent_folder_name + " - " + my_folder_name : (my_folder_name ? my_folder_name : null)); } // Method for reporting errors to the hardcoded error URL. function _reportErrors() { // Do an async XHR to the error reporting URL, and clear out the errors // from local storage. req = new XMLHttpRequest(); req.open("POST", error_url, true); req.setRequestHeader("Accept", "text/plain"); req.onreadystatechange = function () { if (req.readyState == 4) { if (req.status == 200 || req.responseText) { try { var result = JSON.parse(req.responseText); console.log("Error reporting succeeded"); clearErrors(); } catch(err) { console.log("Update failed: " + err); console.log("response text:" + req.responseText); } } else { console.log("Update failed. Will re-attempt later."); } } }; var error_string = "cv=" + _getChromeVersion() + "&ev=" + chrome.app.getDetails().version + "&"; var errors = getErrors(); var count = 0; for (var error_code in ERROR_CODES) { if (errors[error_code]) { error_string = error_string + error_code + "=" + errors[error_code] + "&"; count++; } } if (!count) return; console.log("error string is" + error_string); req.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); req.send(error_string); } function _getChromeVersion() { // Look for the regular expression of "Chrome/....." in the user-agent. var pattern = new RegExp("Chrome/[0-9\\.]*"); return pattern.exec(window.navigator.userAgent); } // TODO(gwilson): remove this, deprecated. function getPresets() { // Check if we should look for updates. if (shouldUsePresets()) { // Update if our last attempt was at least a day ago. var my_date = new Date(); my_date.setDate(my_date.getDate() - 1); if (!last_update_time || my_date > last_update_time) { last_update_time = new Date(); // Update the preset list here } } // Return the preset list here? } // Given a user-agent string, returns what badge should appear. function getBadge(user_agent, callback) { if (!user_agent || user_agent == "") callback(""); _getUserAgentList(function(user_agent_list) { for (var i = 0; i < user_agent_list.length; i++) { if (user_agent_list[i].ua_string == user_agent) { callback(user_agent_list[i].badge); return; } } callback(""); return; }); } function _addOption(domain, user_agent_key, callback) { console.log("Adding new preset spoof with key of " + user_agent_key ); _getUserAgentList(function(list) { var user_agent = null; for (var i = 0; i < list.length; i++) { if (list[i].key == user_agent_key) { user_agent = list[i]; } } if (!user_agent || user_agent == "") { user_agent = new UserAgent("", "", "", "", true, ""); } console.log("Adding new preset spoof with " + user_agent ); _addSpoof(new PresetSpoof(domain, user_agent), callback); }); } // Determines if the given match pattern fits with the given URL. function _isApplicableMatch(match_pattern, url) { if (!url) return false; var domain = findHostname(url); var pattern_is_domain = isDomainName(match_pattern); var pattern = new RegExp(match_pattern); return ((!pattern_is_domain && pattern.test(url)) || // If the pattern is not a hostname, match it anywhere. pattern.test(domain) || match_pattern == domain || match_pattern == url || (pattern_is_domain && domain.indexOf(match_pattern) > -1)); // If the pattern is a hostname, match it anywhere only in the url's hostname. } // Returns only the FIRST permanent spoof match for a given URL. // This means if the user adds 10 spoofs for a given URL, // this will return the first one it finds, and the other 9 will // be ignored. function getSpoofValuesForUrl(url, callback) { if (!url) { callback(null); return; } _getSpoofList(function(list) { for (var i = 0; i < list.length; i++) { if (url && list[i] && list[i].domain && list[i].user_agent && _isApplicableMatch(list[i].domain, url)) { callback(list[i].user_agent); return; } } callback(null); }); } // Is the user agent the "default" user agent (i.e. the normal browser UA) function isDefault(user_agent) { return (!user_agent || (user_agent.ua_string == "")); } // Returns the configuration values for the given url given a tab. // Returns a UserAgent object. function getSpoofValues(url, callback) { getPermanentSpoofOverride(function (permanent_override) { chrome.tabs.getSelected(function(tab) { getHotlist(tab, function(hotlist) { getSpoofValuesForUrl(url, function(spoof) { var selected = (permanent_override ? (spoof ? spoof : hotlist) : (!isDefault(hotlist) ? hotlist : spoof)); if (!selected) { callback({}); return; } selected.platform = getPlatformSpoofValues(selected); selected.appname = getAppnameSpoofValues(selected); callback(selected); }); }); }); }); } // Returns the configuration values for the given url from our cache. // Input is the URL, output is a user-agent object, which is null if // no spoof is needed. function getCacheSpoofValuesForUrl(url) { if (!url) { return null; } var list = spoof_cache; for (var i = 0; i < list.length; i++) { if (url && list[i] && list[i].domain && _isApplicableMatch(list[i].domain, url)) { if (list[i].user_agent && list[i].user_agent.ua_string) { return list[i].user_agent; } else if ((typeof list[i].user_agent) == "string") { return new UserAgent("", list[i].user_agent, "", "", false, ""); } else { return new UserAgent("", "", "", "", false, ""); } } } return null; } // Returns the configuration values for the given url from our cache. // Input is the URL and tab, output is a user-agent object, which is null if // no spoof is needed. function getCacheSpoofValues(url, tabId) { var list = user_agent_cache; var hotlist_index = getHotlistIndexById(tabId); var hotlist = null; for (var i = 0; (i < user_agent_cache.length && !hotlist); i++) { if (user_agent_cache[i].key == hotlist_index) hotlist = user_agent_cache[i]; } if (isDefault(hotlist)) { hotlist = null; } var spoof = getCacheSpoofValuesForUrl(url); var selected = (permanent_override_cache ? (spoof ? spoof : hotlist) : (hotlist ? hotlist : spoof)); if (selected) { selected.platform = getPlatformSpoofValues(selected); selected.appname = getAppnameSpoofValues(selected); } return selected; } // Given a UserAgent object, try to infer the platform spoof needed. function getPlatformSpoofValues(user_agent) { // Loop through all platforms and attempt to deduce which one to spoof. for (var key in PLATFORM_SPOOF) { if (user_agent && user_agent.ua_string && user_agent.ua_string.indexOf(key) > -1) return PLATFORM_SPOOF[key]; } return null; } // Given a UserAgent object, try to infer the navigator.appname spoof needed. // Firefox and Chrome typically return 'Netscape' and // IE returns 'Microsoft Internet Explorer'. function getAppnameSpoofValues(user_agent) { if (user_agent && user_agent.ua_string && user_agent.ua_string.indexOf("MSIE") > -1) // TODO: make this smarter. return "Microsoft Internet Explorer"; return "Netscape"; } // Updates the badge on the browserAction given the current active tab. function updateBadge(tab) { if (tab) getSpoofValues(tab.url, function(values) { var badge = ""; if (values && values.badge && values.badge != "") { badge = values.badge; } chrome.browserAction.setBadgeText({tabId:tab.id, text:badge}); }); }