rammerhead uyghsurhgush
This commit is contained in:
parent
5b08275dfe
commit
6e79bc92f9
2 changed files with 26859 additions and 0 deletions
26362
public/hammerhead.js
Normal file
26362
public/hammerhead.js
Normal file
File diff suppressed because it is too large
Load diff
497
public/rammerhead.js
Normal file
497
public/rammerhead.js
Normal file
|
|
@ -0,0 +1,497 @@
|
||||||
|
(function () {
|
||||||
|
var hammerhead = window['%hammerhead%'];
|
||||||
|
if (!hammerhead) throw new Error('hammerhead not loaded yet');
|
||||||
|
if (hammerhead.settings._settings.sessionId) {
|
||||||
|
// task.js already loaded. this will likely never happen though since this file loads before task.js
|
||||||
|
console.warn('unexpected task.js to load before rammerhead.js. url shuffling cannot be used');
|
||||||
|
main();
|
||||||
|
} else {
|
||||||
|
// wait for task.js to load
|
||||||
|
hookHammerheadStartOnce(main);
|
||||||
|
// before task.js, we need to add url shuffling
|
||||||
|
addUrlShuffling();
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
fixUrlRewrite();
|
||||||
|
fixElementGetter();
|
||||||
|
fixCrossWindowLocalStorage();
|
||||||
|
|
||||||
|
delete window.overrideGetProxyUrl;
|
||||||
|
delete window.overrideParseProxyUrl;
|
||||||
|
delete window.overrideIsCrossDomainWindows;
|
||||||
|
|
||||||
|
// other code if they want to also hook onto hammerhead start //
|
||||||
|
if (window.rammerheadStartListeners) {
|
||||||
|
for (const eachListener of window.rammerheadStartListeners) {
|
||||||
|
try {
|
||||||
|
eachListener();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete window.rammerheadStartListeners;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync localStorage code //
|
||||||
|
// disable if other code wants to implement their own localStorage site wrapper
|
||||||
|
if (window.rammerheadDisableLocalStorageImplementation) {
|
||||||
|
delete window.rammerheadDisableLocalStorageImplementation;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// consts
|
||||||
|
var timestampKey = 'rammerhead_synctimestamp';
|
||||||
|
var updateInterval = 5000;
|
||||||
|
var isSyncing = false;
|
||||||
|
|
||||||
|
var proxiedLocalStorage = localStorage;
|
||||||
|
var realLocalStorage = proxiedLocalStorage.internal.nativeStorage;
|
||||||
|
var sessionId = hammerhead.settings._settings.sessionId;
|
||||||
|
var origin = window.__get$(window, 'location').origin;
|
||||||
|
var keyChanges = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
syncLocalStorage();
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message !== 'server wants to disable localStorage syncing') {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
proxiedLocalStorage.addChangeEventListener(function (event) {
|
||||||
|
if (isSyncing) return;
|
||||||
|
if (keyChanges.indexOf(event.key) === -1) keyChanges.push(event.key);
|
||||||
|
});
|
||||||
|
setInterval(function () {
|
||||||
|
var update = compileUpdate();
|
||||||
|
if (!update) return;
|
||||||
|
localStorageRequest({ type: 'update', updateData: update }, function (data) {
|
||||||
|
updateTimestamp(data.timestamp);
|
||||||
|
});
|
||||||
|
|
||||||
|
keyChanges = [];
|
||||||
|
}, updateInterval);
|
||||||
|
document.addEventListener('visibilitychange', function () {
|
||||||
|
if (document.visibilityState === 'hidden') {
|
||||||
|
var update = compileUpdate();
|
||||||
|
if (update) {
|
||||||
|
// even though we'll never get the timestamp, it's fine. this way,
|
||||||
|
// the data is safer
|
||||||
|
hammerhead.nativeMethods.sendBeacon.call(
|
||||||
|
window.navigator,
|
||||||
|
getSyncStorageEndpoint(),
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'update',
|
||||||
|
updateData: update
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function syncLocalStorage() {
|
||||||
|
isSyncing = true;
|
||||||
|
var timestamp = getTimestamp();
|
||||||
|
var response;
|
||||||
|
if (!timestamp) {
|
||||||
|
// first time syncing
|
||||||
|
response = localStorageRequest({ type: 'sync', fetch: true });
|
||||||
|
if (response.timestamp) {
|
||||||
|
updateTimestamp(response.timestamp);
|
||||||
|
overwriteLocalStorage(response.data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// resync
|
||||||
|
response = localStorageRequest({ type: 'sync', timestamp: timestamp, data: proxiedLocalStorage });
|
||||||
|
if (response.timestamp) {
|
||||||
|
updateTimestamp(response.timestamp);
|
||||||
|
overwriteLocalStorage(response.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isSyncing = false;
|
||||||
|
|
||||||
|
function overwriteLocalStorage(data) {
|
||||||
|
if (!data || typeof data !== 'object') throw new TypeError('data must be an object');
|
||||||
|
proxiedLocalStorage.clear();
|
||||||
|
for (var prop in data) {
|
||||||
|
proxiedLocalStorage[prop] = data[prop];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function updateTimestamp(timestamp) {
|
||||||
|
if (!timestamp) throw new TypeError('timestamp must be defined');
|
||||||
|
if (isNaN(parseInt(timestamp))) throw new TypeError('timestamp must be a number. received' + timestamp);
|
||||||
|
realLocalStorage[timestampKey] = timestamp;
|
||||||
|
}
|
||||||
|
function getTimestamp() {
|
||||||
|
var rawTimestamp = realLocalStorage[timestampKey];
|
||||||
|
var timestamp = parseInt(rawTimestamp);
|
||||||
|
if (isNaN(timestamp)) {
|
||||||
|
if (rawTimestamp) {
|
||||||
|
console.warn('invalid timestamp retrieved from storage: ' + rawTimestamp);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
function getSyncStorageEndpoint() {
|
||||||
|
return (
|
||||||
|
'/syncLocalStorage?sessionId=' + encodeURIComponent(sessionId) + '&origin=' + encodeURIComponent(origin)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function localStorageRequest(data, callback) {
|
||||||
|
if (!data || typeof data !== 'object') throw new TypeError('data must be an object');
|
||||||
|
|
||||||
|
var request = hammerhead.createNativeXHR();
|
||||||
|
// make synchronous if there is no callback
|
||||||
|
request.open('POST', getSyncStorageEndpoint(), !!callback);
|
||||||
|
request.setRequestHeader('content-type', 'application/json');
|
||||||
|
request.send(JSON.stringify(data));
|
||||||
|
function check() {
|
||||||
|
if (request.status === 404) {
|
||||||
|
throw new Error('server wants to disable localStorage syncing');
|
||||||
|
}
|
||||||
|
if (request.status !== 200)
|
||||||
|
throw new Error(
|
||||||
|
'server sent a non 200 code. got ' + request.status + '. Response: ' + request.responseText
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!callback) {
|
||||||
|
check();
|
||||||
|
return JSON.parse(request.responseText);
|
||||||
|
} else {
|
||||||
|
request.onload = function () {
|
||||||
|
check();
|
||||||
|
callback(JSON.parse(request.responseText));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function compileUpdate() {
|
||||||
|
if (!keyChanges.length) return null;
|
||||||
|
|
||||||
|
var updates = {};
|
||||||
|
for (var i = 0; i < keyChanges.length; i++) {
|
||||||
|
updates[keyChanges[i]] = proxiedLocalStorage[keyChanges[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
keyChanges = [];
|
||||||
|
return updates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var noShuffling = false;
|
||||||
|
function addUrlShuffling() {
|
||||||
|
const request = new XMLHttpRequest();
|
||||||
|
const sessionId = (location.pathname.slice(1).match(/^[a-z0-9]+/i) || [])[0];
|
||||||
|
if (!sessionId) {
|
||||||
|
console.warn('cannot get session id from url');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
request.open('GET', '/api/shuffleDict?id=' + sessionId, false);
|
||||||
|
request.send();
|
||||||
|
if (request.status !== 200) {
|
||||||
|
console.warn(
|
||||||
|
`received a non 200 status code while trying to fetch shuffleDict:\nstatus: ${request.status}\nresponse: ${request.responseText}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const shuffleDict = JSON.parse(request.responseText);
|
||||||
|
if (!shuffleDict) return;
|
||||||
|
|
||||||
|
// pasting entire thing here "because lazy" - m28
|
||||||
|
const mod = (n, m) => ((n % m) + m) % m;
|
||||||
|
const baseDictionary = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~-';
|
||||||
|
const shuffledIndicator = '_rhs';
|
||||||
|
const generateDictionary = function () {
|
||||||
|
let str = '';
|
||||||
|
const split = baseDictionary.split('');
|
||||||
|
while (split.length > 0) {
|
||||||
|
str += split.splice(Math.floor(Math.random() * split.length), 1)[0];
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
class StrShuffler {
|
||||||
|
constructor(dictionary = generateDictionary()) {
|
||||||
|
this.dictionary = dictionary;
|
||||||
|
}
|
||||||
|
shuffle(str) {
|
||||||
|
if (str.startsWith(shuffledIndicator)) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
let shuffledStr = '';
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const char = str.charAt(i);
|
||||||
|
const idx = baseDictionary.indexOf(char);
|
||||||
|
if (char === '%' && str.length - i >= 3) {
|
||||||
|
shuffledStr += char;
|
||||||
|
shuffledStr += str.charAt(++i);
|
||||||
|
shuffledStr += str.charAt(++i);
|
||||||
|
} else if (idx === -1) {
|
||||||
|
shuffledStr += char;
|
||||||
|
} else {
|
||||||
|
shuffledStr += this.dictionary.charAt(mod(idx + i, baseDictionary.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shuffledIndicator + shuffledStr;
|
||||||
|
}
|
||||||
|
unshuffle(str) {
|
||||||
|
if (!str.startsWith(shuffledIndicator)) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
str = str.slice(shuffledIndicator.length);
|
||||||
|
|
||||||
|
let unshuffledStr = '';
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const char = str.charAt(i);
|
||||||
|
const idx = this.dictionary.indexOf(char);
|
||||||
|
if (char === '%' && str.length - i >= 3) {
|
||||||
|
unshuffledStr += char;
|
||||||
|
unshuffledStr += str.charAt(++i);
|
||||||
|
unshuffledStr += str.charAt(++i);
|
||||||
|
} else if (idx === -1) {
|
||||||
|
unshuffledStr += char;
|
||||||
|
} else {
|
||||||
|
unshuffledStr += baseDictionary.charAt(mod(idx - i, baseDictionary.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unshuffledStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function patch(url) {
|
||||||
|
// url = _rhsEPrcb://bqhQko.tHR/
|
||||||
|
// remove slash
|
||||||
|
return url.replace(/(^.*?:\/)\//, '$1');
|
||||||
|
}
|
||||||
|
|
||||||
|
function unpatch(url) {
|
||||||
|
// url = _rhsEPrcb:/bqhQko.tHR/
|
||||||
|
// restore slash
|
||||||
|
return url.replace(/^.*?:\/(?!\/)/, '$&/');
|
||||||
|
}
|
||||||
|
|
||||||
|
const replaceUrl = (url, replacer) => {
|
||||||
|
// regex: https://google.com/ sessionid/ url
|
||||||
|
return (url || '').replace(/^((?:[a-z0-9]+:\/\/[^/]+)?(?:\/[^/]+\/))([^]+)/i, function (_, g1, g2) {
|
||||||
|
return g1 + replacer(g2);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const shuffler = new StrShuffler(shuffleDict);
|
||||||
|
|
||||||
|
// shuffle current url if it isn't already shuffled (unshuffled urls likely come from user input)
|
||||||
|
const oldUrl = location.href;
|
||||||
|
const newUrl = replaceUrl(location.href, (url) => shuffler.shuffle(url));
|
||||||
|
if (oldUrl !== newUrl) {
|
||||||
|
history.replaceState(null, null, newUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProxyUrl = hammerhead.utils.url.getProxyUrl;
|
||||||
|
const parseProxyUrl = hammerhead.utils.url.parseProxyUrl;
|
||||||
|
hammerhead.utils.url.overrideGetProxyUrl(function (url, opts) {
|
||||||
|
if (noShuffling) {
|
||||||
|
return getProxyUrl(url, opts);
|
||||||
|
}
|
||||||
|
return replaceUrl(getProxyUrl(url, opts), (u) => patch(shuffler.shuffle(u)), true)
|
||||||
|
});
|
||||||
|
hammerhead.utils.url.overrideParseProxyUrl(function (url) {
|
||||||
|
return parseProxyUrl(replaceUrl(url, (u) => shuffler.unshuffle(unpatch(u)), false));
|
||||||
|
});
|
||||||
|
// manual hooks //
|
||||||
|
window.overrideGetProxyUrl(
|
||||||
|
(getProxyUrl$1) =>
|
||||||
|
function (url, opts) {
|
||||||
|
if (noShuffling) {
|
||||||
|
return getProxyUrl$1(url, opts);
|
||||||
|
}
|
||||||
|
return replaceUrl(getProxyUrl$1(url, opts), (u) => patch(shuffler.shuffle(u)), true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
window.overrideParseProxyUrl(
|
||||||
|
(parseProxyUrl$1) =>
|
||||||
|
function (url) {
|
||||||
|
return parseProxyUrl$1(replaceUrl(url, (u) => shuffler.unshuffle(unpatch(u)), false));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function fixUrlRewrite() {
|
||||||
|
const port = location.port || (location.protocol === 'https:' ? '443' : '80');
|
||||||
|
const getProxyUrl = hammerhead.utils.url.getProxyUrl;
|
||||||
|
hammerhead.utils.url.overrideGetProxyUrl(function (url, opts = {}) {
|
||||||
|
if (!opts.proxyPort) {
|
||||||
|
opts.proxyPort = port;
|
||||||
|
}
|
||||||
|
return getProxyUrl(url, opts);
|
||||||
|
});
|
||||||
|
window.overrideParseProxyUrl(
|
||||||
|
(parseProxyUrl$1) =>
|
||||||
|
function (url) {
|
||||||
|
const parsed = parseProxyUrl$1(url);
|
||||||
|
if (!parsed || !parsed.proxy) return parsed;
|
||||||
|
if (!parsed.proxy.port) {
|
||||||
|
parsed.proxy.port = port;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function fixElementGetter() {
|
||||||
|
const fixList = {
|
||||||
|
HTMLAnchorElement: ['href'],
|
||||||
|
HTMLAreaElement: ['href'],
|
||||||
|
HTMLBaseElement: ['href'],
|
||||||
|
HTMLEmbedElement: ['src'],
|
||||||
|
HTMLFormElement: ['action'],
|
||||||
|
HTMLFrameElement: ['src'],
|
||||||
|
HTMLIFrameElement: ['src'],
|
||||||
|
HTMLImageElement: ['src'],
|
||||||
|
HTMLInputElement: ['src'],
|
||||||
|
HTMLLinkElement: ['href'],
|
||||||
|
HTMLMediaElement: ['src'],
|
||||||
|
HTMLModElement: ['cite'],
|
||||||
|
HTMLObjectElement: ['data'],
|
||||||
|
HTMLQuoteElement: ['cite'],
|
||||||
|
HTMLScriptElement: ['src'],
|
||||||
|
HTMLSourceElement: ['src'],
|
||||||
|
HTMLTrackElement: ['src']
|
||||||
|
};
|
||||||
|
const urlRewrite = (url) => (hammerhead.utils.url.parseProxyUrl(url) || {}).destUrl || url;
|
||||||
|
for (const ElementClass in fixList) {
|
||||||
|
for (const attr of fixList[ElementClass]) {
|
||||||
|
if (!window[ElementClass]) {
|
||||||
|
console.warn('unexpected unsupported element class ' + ElementClass);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const desc = Object.getOwnPropertyDescriptor(window[ElementClass].prototype, attr);
|
||||||
|
const originalGet = desc.get;
|
||||||
|
desc.get = function () {
|
||||||
|
return urlRewrite(originalGet.call(this));
|
||||||
|
};
|
||||||
|
if (attr === 'action') {
|
||||||
|
const originalSet = desc.set;
|
||||||
|
// don't shuffle form action urls
|
||||||
|
desc.set = function (value) {
|
||||||
|
noShuffling = true;
|
||||||
|
try {
|
||||||
|
var returnVal = originalSet.call(this, value);
|
||||||
|
} catch (e) {
|
||||||
|
noShuffling = false;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
noShuffling = false;
|
||||||
|
return returnVal;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Object.defineProperty(window[ElementClass].prototype, attr, desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function fixCrossWindowLocalStorage() {
|
||||||
|
// completely replace hammerhead's implementation as restore() and save() on every
|
||||||
|
// call is just not viable (mainly memory issues as the garbage collector is sometimes not fast enough)
|
||||||
|
|
||||||
|
const prefix = `rammerhead|storage-wrapper|${hammerhead.settings._settings.sessionId}|${
|
||||||
|
window.__get$(window, 'location').host
|
||||||
|
}|`;
|
||||||
|
const toRealStorageKey = (key = '') => prefix + key;
|
||||||
|
const fromRealStorageKey = (key = '') => {
|
||||||
|
if (!key.startsWith(prefix)) return null;
|
||||||
|
return key.slice(prefix.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const replaceStorageInstance = (storageProp, realStorage) => {
|
||||||
|
const reservedProps = ['internal', 'clear', 'key', 'getItem', 'setItem', 'removeItem', 'length'];
|
||||||
|
Object.defineProperty(window, storageProp, {
|
||||||
|
// define a value-based instead of getter-based property, since with this localStorage implementation,
|
||||||
|
// we don't need to rely on sharing a single memory-based storage across frames, unlike hammerhead
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
// still use window[storageProp] as basis to allow scripts to access localStorage.internal
|
||||||
|
value: new Proxy(window[storageProp], {
|
||||||
|
get(target, prop, receiver) {
|
||||||
|
if (reservedProps.includes(prop) && prop !== 'length') {
|
||||||
|
return Reflect.get(target, prop, receiver);
|
||||||
|
} else if (prop === 'length') {
|
||||||
|
let len = 0;
|
||||||
|
for (const [key] of Object.entries(realStorage)) {
|
||||||
|
if (fromRealStorageKey(key)) len++;
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
} else {
|
||||||
|
return realStorage[toRealStorageKey(prop)];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set(_, prop, value) {
|
||||||
|
if (!reservedProps.includes(prop)) {
|
||||||
|
realStorage[toRealStorageKey(prop)] = value;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
deleteProperty(_, prop) {
|
||||||
|
delete realStorage[toRealStorageKey(prop)];
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
has(target, prop) {
|
||||||
|
return toRealStorageKey(prop) in realStorage || prop in target;
|
||||||
|
},
|
||||||
|
ownKeys() {
|
||||||
|
const list = [];
|
||||||
|
for (const [key] of Object.entries(realStorage)) {
|
||||||
|
const proxyKey = fromRealStorageKey(key);
|
||||||
|
if (proxyKey && !reservedProps.includes(proxyKey)) list.push(proxyKey);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
},
|
||||||
|
getOwnPropertyDescriptor(_, prop) {
|
||||||
|
return Object.getOwnPropertyDescriptor(realStorage, toRealStorageKey(prop));
|
||||||
|
},
|
||||||
|
defineProperty(_, prop, desc) {
|
||||||
|
if (!reservedProps.includes(prop)) {
|
||||||
|
Object.defineProperty(realStorage, toRealStorageKey(prop), desc);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const rewriteFunction = (prop, newFunc) => {
|
||||||
|
Storage.prototype[prop] = new Proxy(Storage.prototype[prop], {
|
||||||
|
apply(_, thisArg, args) {
|
||||||
|
return newFunc.apply(thisArg, args);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
replaceStorageInstance('localStorage', hammerhead.storages.localStorageProxy.internal.nativeStorage);
|
||||||
|
replaceStorageInstance('sessionStorage', hammerhead.storages.sessionStorageProxy.internal.nativeStorage);
|
||||||
|
rewriteFunction('clear', function () {
|
||||||
|
for (const [key] of Object.entries(this)) {
|
||||||
|
delete this[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
rewriteFunction('key', function (keyNum) {
|
||||||
|
return (Object.entries(this)[keyNum] || [])[0] || null;
|
||||||
|
});
|
||||||
|
rewriteFunction('getItem', function (key) {
|
||||||
|
return this.internal.nativeStorage[toRealStorageKey(key)] || null;
|
||||||
|
});
|
||||||
|
rewriteFunction('setItem', function (key, value) {
|
||||||
|
if (key) {
|
||||||
|
this.internal.nativeStorage[toRealStorageKey(key)] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
rewriteFunction('removeItem', function (key) {
|
||||||
|
delete this.internal.nativeStorage[toRealStorageKey(key)];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hookHammerheadStartOnce(callback) {
|
||||||
|
var originalStart = hammerhead.__proto__.start;
|
||||||
|
hammerhead.__proto__.start = function () {
|
||||||
|
originalStart.apply(this, arguments);
|
||||||
|
hammerhead.__proto__.start = originalStart;
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})();
|
||||||
Loading…
Add table
Reference in a new issue