// ==UserScript==
// @name Storage Monitor/Debugger Hook
// @namespace https://github.com/CC11001100/crawler-js-hook-framework-public/tree/master/005-storage-hook
// @version 0.1
// @description 用于监控js对localStorage/sessionStorage的任何操作,或者在符合给定条件时进入断点
// @document https://github.com/CC11001100/crawler-js-hook-framework-public/tree/master/005-storage-hook
// @author CC11001100
// @match *://*/*
// @run-at document-start
// @grant none
// ==/UserScript==
(() => {
// 简介: 用于检测、调试浏览器中的localStorage和sessionStorage的任何操作
// 本工具详细文档见:
// Storage是什么: https://developer.mozilla.org/zh-CN/docs/Web/API/Storage
// 修改这里来打断点
const storageDebuggerList = [
"947e722bbefb8a455c278113042beadb",
// 允许使用字符串,字符串用来对name做完全相等的匹配
// 对LocalStorage或SessionStorage的key为foo-name进行的任何操作都会进入断点
// "foo-name",
// 字符串形式的增强版,允许使用正则表达式,正则表达式只用来匹配name
// /^foo-prefix*/,
// 这才是一个完整的配置,可以比较精细的打断点
// {
//
// // storageType { "local" | "session" | "all" }
// "storageType": "all",
//
// // operationType { "get" | "set" | "remove" | "clear" | "key" | "all" }
// "operationType": "all",
//
// // nameFilter { "string" | RegExp | null }
// "nameFilter": "foo-name",
//
// // valueFilter { "string" | RegExp | null }
// "valueFilter": "foo-value"
//
// }
]
// 可以禁用storage来辅助调试,不需要每次都去傻啦吧唧的删除,让它写不进去读不出来即可
// 可以这样同时控制localStorage和sessionStorage是否可读和可写
const enableStorage = {
read: true,
write: true
}
// 支持的另一种配置方式:
// 也可以精确的为每一个类型指定可读和可写
// const enableStorage = {
// localStorage: {
// // localStorage是否是可读的
// read: true,
// // localStorage是否是可写的
// write: true
// },
// sessionStorage: {
// // sessionStorage是否是可读的
// read: true,
// // sessionStorage是否是可写的
// write: true
// }
// }
// 在控制台打印日志时字体大小,根据自己喜好调整
// 众所周知,12px是宇宙通用大小
const consoleLogFontSize = 12;
// --------------------------------- 以下为程序内部逻辑,可忽略 ---------------------------------------------
// 防止重复注入
const _cc11001100_hook_storage = window._cc11001100_hook_storage = window._cc11001100_hook_storage || {};
if ("isInjectHook" in _cc11001100_hook_storage) {
return
}
_cc11001100_hook_storage["isInjectHook"] = true
addHook("session", window.sessionStorage);
addHook("local", window.localStorage);
/**
* 为一个storage对象添加Hook,可以是localStorage或者sessionStorage
*
* @param storageTypeName { "local" | "session" }
* @param storageObject { window.localStorage | window.sessionStorage}
*/
function addHook(storageTypeName, storageObject) {
// getItem
const storageGetItem = storageObject.getItem;
storageObject.getItem = function (itemName) {
const itemValue = storageGetItem.apply(this, [itemName]);
const valueStyle = `color: black; background: #85C1E9; font-size: ${consoleLogFontSize}px; font-weight: bold;`;
const normalStyle = `color: black; background: #D6EAF8; font-size: ${consoleLogFontSize}px;`;
const message = [
normalStyle,
now(),
normalStyle,
"Storage Monitor: ",
valueStyle,
"get",
normalStyle,
" ",
valueStyle,
`${storageTypeName} storage`,
normalStyle,
", name = ",
valueStyle,
`${itemName}`,
normalStyle,
", value = ",
valueStyle,
`${itemValue}`,
normalStyle,
`, code location = ${cc11001100_getCodeLocation()}`
];
console.log(genFormatArray(message), ...message);
testStorageDebugger(storageTypeName, "get", itemName, itemValue);
// 如果关闭读功能的话,则阻止其能够读到值
if (!isStorageEnable(storageTypeName, "read")) {
const message = [
normalStyle,
now(),
normalStyle,
"Storage Monitor: ",
normalStyle,
"ignore ",
valueStyle,
"get",
normalStyle,
` because disable `,
valueStyle,
`${storageTypeName}`,
normalStyle,
" ",
valueStyle,
"read",
normalStyle,
`, code location = ${cc11001100_getCodeLocation()}`
];
console.log(genFormatArray(message), ...message);
return null;
}
return itemValue;
}
storageObject.getItem.toString = () => "function getItem() { [native code] }";
// setItem
const storageSetItem = storageObject.setItem;
storageObject.setItem = function (itemName, itemValue) {
const oldValue = storageGetItem.apply(this, [itemName]);
let valueStyle = "";
let normalStyle = "";
if (oldValue == null) {
// 认为是新增
valueStyle = `color: black; background: #669934; font-size: ${consoleLogFontSize}px; font-weight: bold;`;
normalStyle = `color: black; background: #65CC66; font-size: ${consoleLogFontSize}px;`;
const message = [
normalStyle,
now(),
normalStyle,
"Storage Monitor: ",
valueStyle,
"set",
normalStyle,
" ",
valueStyle,
`${storageTypeName} storage`,
normalStyle,
", name = ",
valueStyle,
`${itemName}`,
normalStyle,
", value = ",
valueStyle,
`${itemValue}`,
normalStyle,
`, code location = ${cc11001100_getCodeLocation()}`
];
console.log(genFormatArray(message), ...message);
} else {
// 认为是修改
valueStyle = `color: black; background: #FE9900; font-size: ${consoleLogFontSize}px; font-weight: bold;`;
normalStyle = `color: black; background: #FFCC00; font-size: ${consoleLogFontSize}px;`;
const message = [
normalStyle,
now(),
normalStyle,
"Storage Monitor: ",
valueStyle,
"set",
normalStyle,
" ",
valueStyle,
`${storageTypeName} storage`,
normalStyle,
", name = ",
valueStyle,
`${itemName}`,
normalStyle,
", newValue = ",
valueStyle,
`${itemValue}`,
...(() => {
if (oldValue === itemValue) {
// 值没有发生改变
return [
normalStyle,
", value changed = ",
valueStyle,
`false`
]
} else {
// 值发生了改变
return [
normalStyle,
", oldValue = ",
valueStyle,
`${oldValue}`,
normalStyle,
", value changed = ",
valueStyle,
`true`
]
}
})(),
normalStyle,
`, code location = ${cc11001100_getCodeLocation()}`
];
console.log(genFormatArray(message), ...message);
}
testStorageDebugger(storageTypeName, "set", itemName, itemValue);
// 如果关闭写功能的话,则阻止其能够修改值
if (!isStorageEnable(storageTypeName, "write")) {
const message = [
normalStyle,
now(),
normalStyle,
"Storage Monitor: ",
normalStyle,
"ignore ",
valueStyle,
"set",
normalStyle,
` because disable `,
valueStyle,
`${storageTypeName}`,
normalStyle,
" ",
valueStyle,
"write",
normalStyle,
`, code location = ${cc11001100_getCodeLocation()}`
];
console.log(genFormatArray(message), ...message);
return null;
}
return storageSetItem.apply(this, [itemName, itemValue]);
}
storageObject.setItem.toString = () => "function setItem() { [native code] }";
// removeItem
const storageRemoveItem = storageObject.removeItem;
storageObject.removeItem = function (itemName) {
const oldValue = storageGetItem.apply(this, [itemName]);
const valueStyle = `color: black; background: #E50000; font-size: ${consoleLogFontSize}px; font-weight: bold;`;
const normalStyle = `color: black; background: #FF6766; font-size: ${consoleLogFontSize}px;`;
const message = [
normalStyle,
now(),
normalStyle,
"Storage Monitor: ",
valueStyle,
"remove",
normalStyle,
" ",
valueStyle,
`${storageTypeName} storage`,
normalStyle,
", name = ",
valueStyle,
`${itemName}`,
normalStyle,
", value = ",
valueStyle,
`${oldValue}`,
normalStyle,
`, code location = ${cc11001100_getCodeLocation()}`
];
console.log(genFormatArray(message), ...message);
testStorageDebugger(storageTypeName, "remove", itemName, null);
// 如果关闭写功能的话,则阻止其能够修改值
if (!isStorageEnable(storageTypeName, "write")) {
const message = [
normalStyle,
now(),
normalStyle,
"Storage Monitor: ",
normalStyle,
"ignore ",
valueStyle,
"remove",
normalStyle,
` because disable `,
valueStyle,
`${storageTypeName}`,
normalStyle,
" ",
valueStyle,
"write",
normalStyle,
`, code location = ${cc11001100_getCodeLocation()}`
];
console.log(genFormatArray(message), ...message);
return null;
}
return storageRemoveItem.apply(this, [itemName]);
}
storageObject.removeItem.toString = () => "function removeItem() { [native code] }";
// clear
const storageClear = storageObject.clear;
storageObject.clear = function () {
const valueStyle = `color: black; background: #E50000; font-size: ${consoleLogFontSize}px; font-weight: bold;`;
const normalStyle = `color: black; background: #FF6766; font-size: ${consoleLogFontSize}px;`;
const message = [
normalStyle,
now(),
normalStyle,
"Storage Monitor: ",
valueStyle,
"clear",
normalStyle,
" ",
valueStyle,
`${storageTypeName} storage`,
normalStyle,
`, code location = ${cc11001100_getCodeLocation()}`
];
console.log(genFormatArray(message), ...message);
testStorageDebugger(storageTypeName, "clear", null, null);
// 如果关闭写功能的话,则阻止其能够修改值
if (!isStorageEnable(storageTypeName, "write")) {
const message = [
normalStyle,
now(),
normalStyle,
"Storage Monitor: ",
normalStyle,
"ignore ",
valueStyle,
"clear",
normalStyle,
` because disable `,
valueStyle,
`${storageTypeName}`,
normalStyle,
" ",
valueStyle,
"write",
normalStyle,
`, code location = ${cc11001100_getCodeLocation()}`
];
console.log(genFormatArray(message), ...message);
return null;
}
return storageClear.apply(this);
}
storageObject.clear.toString = () => "function clear() { [native code] }";
// key
const storageKey = storageObject.key;
storageObject.key = function (itemIndex) {
const value = storageKey.apply(this, [itemIndex]);
const valueStyle = `color: black; background: #85C1E9; font-size: ${consoleLogFontSize}px; font-weight: bold;`;
const normalStyle = `color: black; background: #D6EAF8; font-size: ${consoleLogFontSize}px;`;
const message = [
normalStyle,
now(),
normalStyle,
"Storage Monitor: ",
valueStyle,
`key`,
normalStyle,
" ",
valueStyle,
`${storageTypeName} storage`,
normalStyle,
`, itemIndex = `,
valueStyle,
`${itemIndex}`,
normalStyle,
", value = ",
valueStyle,
`${value}`,
normalStyle,
`, code location = ${cc11001100_getCodeLocation()}`
];
console.log(genFormatArray(message), ...message);
testStorageDebugger(storageTypeName, "key", null, value);
// 如果关闭读功能的话,则阻止其能够读到值
if (!isStorageEnable(storageTypeName, "read")) {
const message = [
normalStyle,
now(),
normalStyle,
"Storage Monitor: ",
normalStyle,
"ignore ",
valueStyle,
"key",
normalStyle,
` because disable `,
valueStyle,
`${storageTypeName}`,
normalStyle,
" ",
valueStyle,
"read",
normalStyle,
`, code location = ${cc11001100_getCodeLocation()}`
];
console.log(genFormatArray(message), ...message);
return null;
}
return value;
}
storageObject.key.toString = () => "function key() { [native code] }";
}
/**
* 对应类型的storage是否开启
*
* @param storageTypeName { "local" | "session" }
* @param operationType { "read" | "write" }
*/
function isStorageEnable(storageTypeName, operationType) {
if (storageTypeName === "local") {
return enableStorage["localStorage"][operationType]
} else if (storageTypeName === "session") {
return enableStorage["sessionStorage"][operationType]
} else {
return true
}
}
/**
* 测试是否要进入断点
*
* @param storageType { "local" | "session" | "all" }
* @param operationType { "get" | "set" | "remove" | "clear" | "key" | "all" }
* @param name { "string" | null }
* @param value { "string" | null }
*/
function testStorageDebugger(storageType, operationType, name, value) {
for (let storageDebugger of storageDebuggerList) {
// 将鼠标移动到这里在变量上悬停查看其值,能够知道是命中了什么规则
if (storageDebugger.testDebugger(storageType, operationType, name, value)) {
debugger;
}
}
}
// 断点规则
class StorageDebugger {
/**
*
* @param storageType { "local" | "session" | "all" }
* @param operationType { "get" | "set" | "remove" | "clear" | "key" | "all" }
* @param nameFilter { "string" | RegExp | null }
* @param valueFilter { "string" | RegExp | null }
*/
constructor(storageType, operationType, nameFilter, valueFilter) {
this.storageType = storageType;
this.operationType = operationType;
this.nameFilter = nameFilter;
this.valueFilter = valueFilter;
}
testDebugger(storageType, operationType, name, value) {
if (!this.testByStorageType(storageType)) {
return false
}
if (!this.testByOperationType(operationType)) {
return false
}
if (this.nameFilter && !this.testByName(name)) {
return false;
}
if (this.valueFilter && !this.testByValue(value)) {
return false;
}
return true;
}
testByStorageType(storageType) {
if (storageType === "all" || this.storageType === "all") {
return true
}
return this.storageType === storageType;
}
testByOperationType(operationType) {
if (operationType === "all" || this.operationType === "all") {
return true
}
return this.operationType === operationType;
}
testByName(name) {
if (!this.nameFilter) {
return false
}
if (!name) {
return false
}
if (typeof this.nameFilter === "string") {
return this.nameFilter === name;
} else if (typeof this.nameFilter instanceof RegExp) {
return this.nameFilter.test(name)
} else {
return false;
}
}
testByValue(value) {
if (!this.valueFilter) {
return false
}
if (!value) {
return false
}
if (typeof this.valueFilter === "string") {
return this.valueFilter === value;
} else if (typeof this.valueFilter instanceof RegExp) {
return this.valueFilter.test(value)
} else {
return false;
}
}
}
// 把storage的读写属性统一,方便后面程序处理
(function convertEnableStorage() {
// 设置默认值
enableStorage["localStorage"] = enableStorage["localStorage"] || {}
enableStorage["sessionStorage"] = enableStorage["sessionStorage"] || {}
// 扩展read
if ("read" in enableStorage) {
enableStorage["localStorage"]["read"] = enableStorage["read"]
enableStorage["sessionStorage"]["read"] = enableStorage["read"]
delete enableStorage["read"]
}
// 扩展write
if ("write" in enableStorage) {
enableStorage["localStorage"]["write"] = enableStorage["write"]
enableStorage["sessionStorage"]["write"] = enableStorage["write"]
delete enableStorage["write"]
}
// 如果没有配置的话,则设置默认值
if (!("write" in enableStorage["localStorage"])) {
enableStorage["localStorage"]["write"] = true
}
if (!("read" in enableStorage["localStorage"])) {
enableStorage["localStorage"]["read"] = true
}
if (!("write" in enableStorage["sessionStorage"])) {
enableStorage["sessionStorage"]["write"] = true
}
if (!("read" in enableStorage["sessionStorage"])) {
enableStorage["sessionStorage"]["read"] = true
}
})();
// 把storage的断点规则转换为程序内部使用的格式
(function convertStorageDebugger() {
// const valueStyle = `color: black; background: #FF2121; font-size: ${Math.round(consoleLogFontSize * 1.5)}px; font-weight: bold;`;
const normalStyle = `color: black; background: #FF2121; font-size: ${Math.round(consoleLogFontSize * 1.5)}px;`;
const newStorageDebuggerList = [];
for (let x of storageDebuggerList) {
if (typeof x === "string" || x instanceof RegExp) {
// 如果设置的是名字,则只针对按名称操作的操作打断点
newStorageDebuggerList.push(new StorageDebugger("all", "get", x, null));
newStorageDebuggerList.push(new StorageDebugger("all", "set", x, null));
newStorageDebuggerList.push(new StorageDebugger("all", "remove", x, null));
} else {
// 检查设置项的合法性
if ("storageType" in x && ["local", "session", "all"].indexOf(x["storageType"].toLowerCase()) === -1) {
const message = [
normalStyle,
`${now()} Storage Monitor: storageType error, value = ${x["storageType"]}, need to be = { "local", "session", "all" }, so ignore this debugger = ${JSON.stringify(x)}`,
];
console.log(genFormatArray(message), ...message);
continue
}
if ("operationType" in x && ["get", "set", "remove", "clear", "key", "all"].indexOf(x["operationType"].toLowerCase()) === -1) {
const message = [
normalStyle,
`${now()} Storage Monitor: storageType error, value = ${x["operationType"]}, need to be { "get" | "set" | "remove" | "clear" | "key" | "all" }, so ignore this debugger = ${JSON.stringify(x)}`,
];
console.log(genFormatArray(message), ...message);
continue
}
if (["nameFilter"] in x && (typeof x["nameFilter"] != "string") && !(x["nameFilter"] instanceof RegExp)) {
const message = [
normalStyle,
`${now()} Storage Monitor: nameFilter config error, value = ${x["nameFilter"]}, need to be { string | Regexp | null }, so ignore this debugger = ${JSON.stringify(x)}`,
];
console.log(genFormatArray(message), ...message);
continue
}
if (["valueFilter"] in x && (typeof x["valueFilter"] != "string") && !(x["valueFilter"] instanceof RegExp)) {
const message = [
normalStyle,
`${now()} Storage Monitor: valueFilter config error, value = ${x["valueFilter"]}, need to be { string | Regexp | null }, so ignore this debugger = ${JSON.stringify(x)}`,
];
console.log(genFormatArray(message), ...message);
continue
}
// TODO 出现了其它类型的key,是否配置错误呢?
const storageType = x["storageType"] || "all";
if ((x["name"] || x["value"]) && x["operationType"]) {
const name = x["name"] || null;
const value = x["value"] || null;
newStorageDebuggerList.push(new StorageDebugger("all", "get", name, value));
newStorageDebuggerList.push(new StorageDebugger("all", "set", name, value));
newStorageDebuggerList.push(new StorageDebugger("all", "remove", name, value));
} else {
const operationType = x["operationType"] || "all";
const name = x["name"] || null;
const value = x["value"] || null;
newStorageDebuggerList.push(new StorageDebugger(storageType, operationType, name, value));
}
}
}
// 把原来的规则替换掉
while (storageDebuggerList.pop()) {
}
for (let x of newStorageDebuggerList) {
storageDebuggerList.push(x);
}
})();
// 奇奇怪怪的模板方式竟然一路被沿用下来...(*/ω\*)
function genFormatArray(messageAndStyleArray) {
const formatArray = [];
for (let i = 0, end = messageAndStyleArray.length / 2; i < end; i++) {
formatArray.push("%c%s");
}
return formatArray.join("");
}
function now() {
// 东八区专属...
return "[" + new Date(new Date().getTime() + 1000 * 60 * 60 * 8).toJSON().replace("T", " ").replace("Z", "") + "] ";
}
function cc11001100_getCodeLocation() {
const callstack = new Error().stack.split("\n");
while (callstack.length && callstack[0].indexOf("cc11001100_getCodeLocation") === -1) {
callstack.shift();
}
callstack.shift();
callstack.shift();
return callstack[0].trim();
}
})();