修复一些资源加载问题
This commit is contained in:
parent
d697928fb0
commit
1965137191
@ -9,13 +9,17 @@ export default class DBWriter implements Writer {
|
|||||||
this.dao = dao;
|
this.dao = dao;
|
||||||
}
|
}
|
||||||
|
|
||||||
write(level: LogLevel, message: string, label: LogLabel): void {
|
async write(level: LogLevel, message: string, label: LogLabel): Promise<void> {
|
||||||
this.dao.save({
|
try {
|
||||||
id: 0,
|
await this.dao.save({
|
||||||
level,
|
id: 0,
|
||||||
message,
|
level,
|
||||||
label,
|
message,
|
||||||
createtime: new Date().getTime(),
|
label,
|
||||||
});
|
createtime: new Date().getTime(),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("DBWriter error", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,28 +32,27 @@ export default class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log(level: LogLevel, message: string, ...label: LogLabel[]) {
|
log(level: LogLevel, message: string, ...label: LogLabel[]) {
|
||||||
|
const newLabel = buildLabel(this.label, label);
|
||||||
if (levelNumber[level] >= levelNumber[this.core.level]) {
|
if (levelNumber[level] >= levelNumber[this.core.level]) {
|
||||||
this.core.writer.write(level, message, buildLabel(this.label, label));
|
this.core.writer.write(level, message, newLabel);
|
||||||
}
|
}
|
||||||
if (this.core.debug !== "none" && levelNumber[level] >= levelNumber[this.core.debug]) {
|
if (this.core.debug !== "none" && levelNumber[level] >= levelNumber[this.core.debug]) {
|
||||||
if (typeof message === "object") {
|
if (typeof message === "object") {
|
||||||
message = JSON.stringify(message);
|
message = JSON.stringify(message);
|
||||||
}
|
}
|
||||||
const msg = `${dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss")} [${level}] msg=${message} label=${JSON.stringify(
|
const msg = `${dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss")} [${level}] ${message}`;
|
||||||
buildLabel(this.label, label)
|
|
||||||
)}`;
|
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case "error":
|
case "error":
|
||||||
console.error(msg);
|
console.error(msg, newLabel);
|
||||||
break;
|
break;
|
||||||
case "warn":
|
case "warn":
|
||||||
console.warn(msg);
|
console.warn(msg, newLabel);
|
||||||
break;
|
break;
|
||||||
case "trace":
|
case "trace":
|
||||||
console.info(msg);
|
console.info(msg, newLabel);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.info(msg);
|
console.info(msg, newLabel);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,6 +79,11 @@ export default class ContentRuntime {
|
|||||||
const nodeId = (this.msg as CustomEventMessage).sendRelatedTarget(el);
|
const nodeId = (this.msg as CustomEventMessage).sendRelatedTarget(el);
|
||||||
return nodeId;
|
return nodeId;
|
||||||
}
|
}
|
||||||
|
case "GM_log":
|
||||||
|
// 拦截GM_log,打印到控制台
|
||||||
|
// 由于某些页面会处理掉console.log,所以丢到这里来打印
|
||||||
|
console.log(...data.params);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ export default class ExecScript {
|
|||||||
this.scriptRes = scriptRes;
|
this.scriptRes = scriptRes;
|
||||||
this.logger = LoggerCore.getInstance().logger({
|
this.logger = LoggerCore.getInstance().logger({
|
||||||
component: "exec",
|
component: "exec",
|
||||||
script: this.scriptRes.uuid,
|
uuid: this.scriptRes.uuid,
|
||||||
name: this.scriptRes.name,
|
name: this.scriptRes.name,
|
||||||
});
|
});
|
||||||
this.GM_info = GMApi.GM_info(this.scriptRes);
|
this.GM_info = GMApi.GM_info(this.scriptRes);
|
||||||
|
@ -198,7 +198,6 @@ export class PopupService {
|
|||||||
scriptMenu.push(script);
|
scriptMenu.push(script);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log("popup脚本菜单", runScript);
|
|
||||||
// 后台脚本只显示开启或者运行中的脚本
|
// 后台脚本只显示开启或者运行中的脚本
|
||||||
return { scriptList: scriptMenu, backScriptList: await this.getScriptMenu(-1) };
|
return { scriptList: scriptMenu, backScriptList: await this.getScriptMenu(-1) };
|
||||||
}
|
}
|
||||||
|
@ -193,6 +193,19 @@ export class ResourceService {
|
|||||||
(u.hash.sha512 && u.hash.sha512 !== resource.hash.sha512)
|
(u.hash.sha512 && u.hash.sha512 !== resource.hash.sha512)
|
||||||
) {
|
) {
|
||||||
resource.content = `console.warn("ScriptCat: couldn't load resource from URL ${url} due to a SRI error ");`;
|
resource.content = `console.warn("ScriptCat: couldn't load resource from URL ${url} due to a SRI error ");`;
|
||||||
|
// 尝试重新加载
|
||||||
|
this.loadByUrl(u.url, resource.type).then((reloadRes) => {
|
||||||
|
this.logger.info("reload resource success", {
|
||||||
|
url: u.url,
|
||||||
|
hash: {
|
||||||
|
expected: u.hash,
|
||||||
|
old: resource.hash,
|
||||||
|
new: reloadRes.hash,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
reloadRes.updatetime = new Date().getTime();
|
||||||
|
this.resourceDAO.save(reloadRes);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.resolve(resource);
|
return Promise.resolve(resource);
|
||||||
@ -214,12 +227,13 @@ export class ResourceService {
|
|||||||
sha512: "",
|
sha512: "",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const wordArray = crypto.lib.WordArray.create(<ArrayBuffer>reader.result);
|
||||||
resolve({
|
resolve({
|
||||||
md5: crypto.MD5(<string>reader.result).toString(),
|
md5: crypto.MD5(wordArray).toString(),
|
||||||
sha1: crypto.SHA1(<string>reader.result).toString(),
|
sha1: crypto.SHA1(wordArray).toString(),
|
||||||
sha256: crypto.SHA256(<string>reader.result).toString(),
|
sha256: crypto.SHA256(wordArray).toString(),
|
||||||
sha384: crypto.SHA384(<string>reader.result).toString(),
|
sha384: crypto.SHA384(wordArray).toString(),
|
||||||
sha512: crypto.SHA512(<string>reader.result).toString(),
|
sha512: crypto.SHA512(wordArray).toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -22,6 +22,8 @@ import { ExtensionContentMessageSend } from "@Packages/message/extension_message
|
|||||||
import { sendMessage } from "@Packages/message/client";
|
import { sendMessage } from "@Packages/message/client";
|
||||||
import { compileInjectScript } from "../content/utils";
|
import { compileInjectScript } from "../content/utils";
|
||||||
import { PopupService } from "./popup";
|
import { PopupService } from "./popup";
|
||||||
|
import Logger from "@App/app/logger/logger";
|
||||||
|
import LoggerCore from "@App/app/logger/core";
|
||||||
|
|
||||||
// 为了优化性能,存储到缓存时删除了code与value
|
// 为了优化性能,存储到缓存时删除了code与value
|
||||||
export interface ScriptMatchInfo extends ScriptRunResouce {
|
export interface ScriptMatchInfo extends ScriptRunResouce {
|
||||||
@ -389,22 +391,27 @@ export class RuntimeService {
|
|||||||
world: "MAIN",
|
world: "MAIN",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 排除由loadPage时决定, 不使用userScript的excludeMatches处理
|
||||||
if (script.metadata["exclude"]) {
|
if (script.metadata["exclude"]) {
|
||||||
const excludeMatches = script.metadata["exclude"];
|
const excludeMatches = script.metadata["exclude"];
|
||||||
const result = dealPatternMatches(excludeMatches);
|
const result = dealPatternMatches(excludeMatches, {
|
||||||
|
exclude: true,
|
||||||
|
});
|
||||||
|
|
||||||
registerScript.excludeMatches = result.patternResult;
|
// registerScript.excludeMatches = result.patternResult;
|
||||||
scriptMatchInfo.excludeMatches = result.result;
|
scriptMatchInfo.excludeMatches = result.result;
|
||||||
}
|
}
|
||||||
// 自定义排除
|
// 自定义排除
|
||||||
if (script.selfMetadata && script.selfMetadata.exclude) {
|
if (script.selfMetadata && script.selfMetadata.exclude) {
|
||||||
const excludeMatches = script.selfMetadata.exclude;
|
const excludeMatches = script.selfMetadata.exclude;
|
||||||
const result = dealPatternMatches(excludeMatches);
|
const result = dealPatternMatches(excludeMatches, {
|
||||||
|
exclude: true,
|
||||||
|
});
|
||||||
|
|
||||||
if (!registerScript.excludeMatches) {
|
if (!registerScript.excludeMatches) {
|
||||||
registerScript.excludeMatches = [];
|
registerScript.excludeMatches = [];
|
||||||
}
|
}
|
||||||
registerScript.excludeMatches.push(...result.patternResult);
|
// registerScript.excludeMatches.push(...result.patternResult);
|
||||||
scriptMatchInfo.customizeExcludeMatches = result.result;
|
scriptMatchInfo.customizeExcludeMatches = result.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -419,10 +426,22 @@ export class RuntimeService {
|
|||||||
if (script.metadata["run-at"]) {
|
if (script.metadata["run-at"]) {
|
||||||
registerScript.runAt = getRunAt(script.metadata["run-at"]);
|
registerScript.runAt = getRunAt(script.metadata["run-at"]);
|
||||||
}
|
}
|
||||||
|
console.log("registerScript", script.name, registerScript, scriptMatchInfo);
|
||||||
if (await Cache.getInstance().get("registryScript:" + script.uuid)) {
|
if (await Cache.getInstance().get("registryScript:" + script.uuid)) {
|
||||||
await chrome.userScripts.update([registerScript]);
|
await chrome.userScripts.update([registerScript]);
|
||||||
} else {
|
} else {
|
||||||
await chrome.userScripts.register([registerScript]);
|
await chrome.userScripts.register([registerScript], () => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
LoggerCore.logger().error("registerScript error", {
|
||||||
|
error: chrome.runtime.lastError,
|
||||||
|
name: script.name,
|
||||||
|
registerMatch: {
|
||||||
|
matches: registerScript.matches,
|
||||||
|
excludeMatches: registerScript.excludeMatches,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
await Cache.getInstance().set("registryScript:" + script.uuid, true);
|
await Cache.getInstance().set("registryScript:" + script.uuid, true);
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,10 @@ import DBWriter from "./app/logger/db_writer";
|
|||||||
import { LoggerDAO } from "./app/repo/logger";
|
import { LoggerDAO } from "./app/repo/logger";
|
||||||
import { OffscreenManager } from "./app/service/offscreen";
|
import { OffscreenManager } from "./app/service/offscreen";
|
||||||
|
|
||||||
|
// 初始化数据库
|
||||||
|
migrate();
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
// 初始化数据库
|
|
||||||
migrate();
|
|
||||||
// 初始化日志组件
|
// 初始化日志组件
|
||||||
const loggerCore = new LoggerCore({
|
const loggerCore = new LoggerCore({
|
||||||
writer: new DBWriter(new LoggerDAO()),
|
writer: new DBWriter(new LoggerDAO()),
|
||||||
|
@ -43,10 +43,10 @@ describe("UrlMatch-google-error", () => {
|
|||||||
url.add("https://*foo/bar", "ok1");
|
url.add("https://*foo/bar", "ok1");
|
||||||
}).toThrow(Error);
|
}).toThrow(Error);
|
||||||
});
|
});
|
||||||
|
// 从v0.17.0开始允许这种
|
||||||
it("error-2", () => {
|
it("error-2", () => {
|
||||||
expect(() => {
|
url.add("https://foo.*.bar/baz", "ok1");
|
||||||
url.add("https://foo.*.bar/baz", "ok1");
|
expect(url.match("https://foo.api.bar/baz")).toEqual(["ok1"]);
|
||||||
}).toThrow(Error);
|
|
||||||
});
|
});
|
||||||
it("error-3", () => {
|
it("error-3", () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
@ -70,6 +70,13 @@ describe("UrlMatch-search", () => {
|
|||||||
expect(url.match("https://bbs.tampermonkey.net.cn/")).toEqual(["ok2"]);
|
expect(url.match("https://bbs.tampermonkey.net.cn/")).toEqual(["ok2"]);
|
||||||
expect(url.match("https://bbs.tampermonkey.net.cn/foo/bar.html")).toEqual([]);
|
expect(url.match("https://bbs.tampermonkey.net.cn/foo/bar.html")).toEqual([]);
|
||||||
});
|
});
|
||||||
|
it("http://api.*.example.com/*", () => {
|
||||||
|
const url = new UrlMatch<string>();
|
||||||
|
url.add("http://api.*.example.com/*", "ok1");
|
||||||
|
expect(url.match("http://api.foo.example.com/")).toEqual(["ok1"]);
|
||||||
|
expect(url.match("http://api.bar.example.com/")).toEqual(["ok1"]);
|
||||||
|
expect(url.match("http://api.example.com/")).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("UrlMatch-port1", () => {
|
describe("UrlMatch-port1", () => {
|
||||||
@ -106,13 +113,47 @@ describe("UrlMatch-port2", () => {
|
|||||||
// https://developer.chrome.com/docs/extensions/mv3/match_patterns/
|
// https://developer.chrome.com/docs/extensions/mv3/match_patterns/
|
||||||
describe("dealPatternMatches", () => {
|
describe("dealPatternMatches", () => {
|
||||||
it("https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns?hl=zh-cn#examples", () => {
|
it("https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns?hl=zh-cn#examples", () => {
|
||||||
const matches = dealPatternMatches(["https://*/*", "http://127.0.0.1/*", "http://127.0.0.1/"]);
|
const matches = dealPatternMatches([
|
||||||
expect(matches.patternResult).toEqual(["https://*/*", "http://127.0.0.1/*", "http://127.0.0.1/"]);
|
"https://*/*",
|
||||||
|
"http://127.0.0.1/*",
|
||||||
|
"http://127.0.0.1/",
|
||||||
|
"https://*.example.com/*",
|
||||||
|
]);
|
||||||
|
expect(matches.patternResult).toEqual([
|
||||||
|
"https://*/*",
|
||||||
|
"http://127.0.0.1/*",
|
||||||
|
"http://127.0.0.1/",
|
||||||
|
"https://*.example.com/*",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
// 处理一些特殊情况
|
// 处理一些特殊情况
|
||||||
it("*://link.17173.com*", () => {
|
it("特殊情况", () => {
|
||||||
const matches = dealPatternMatches(["*://link.17173.com*"]);
|
const matches = dealPatternMatches([
|
||||||
expect(matches.patternResult).toEqual(["*://link.17173.com/*"]);
|
"*://www.example.com*",
|
||||||
|
"*://api.*.example.com/*",
|
||||||
|
"*://api.*.*.example.com/*",
|
||||||
|
"*://*example.com/*",
|
||||||
|
]);
|
||||||
|
expect(matches.patternResult).toEqual([
|
||||||
|
"*://www.example.com/*",
|
||||||
|
"*://*.example.com/*",
|
||||||
|
"*://*.example.com/*",
|
||||||
|
"*://example.com/*",
|
||||||
|
]);
|
||||||
|
expect(matches.result).toEqual([
|
||||||
|
"*://www.example.com*",
|
||||||
|
"*://api.*.example.com/*",
|
||||||
|
"*://api.*.*.example.com/*",
|
||||||
|
"*://*example.com/*",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it("特殊情况-exclude", () => {
|
||||||
|
const matches = dealPatternMatches(["*://api.*.example.com/*", "*://api.*.*.example.com/*"], {
|
||||||
|
exclude: true,
|
||||||
|
});
|
||||||
|
console.log(matches);
|
||||||
|
expect(matches.patternResult).toEqual(["*://example.com/*", "*://example.com/*"]);
|
||||||
|
expect(matches.result).toEqual(["*://api.*.example.com/*", "*://api.*.*.example.com/*"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -146,11 +187,19 @@ describe("parsePatternMatchesURL", () => {
|
|||||||
path: "*",
|
path: "*",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("*://link.17173.com*", () => {
|
it("*://www.example.com*", () => {
|
||||||
const matches = parsePatternMatchesURL("*://link.17173.com*");
|
const matches = parsePatternMatchesURL("*://www.example.com*");
|
||||||
expect(matches).toEqual({
|
expect(matches).toEqual({
|
||||||
scheme: "*",
|
scheme: "*",
|
||||||
host: "link.17173.com",
|
host: "www.example.com",
|
||||||
|
path: "*",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("*://api.*.example.com/*", () => {
|
||||||
|
const matches = parsePatternMatchesURL("*://api.*.example.com/*");
|
||||||
|
expect(matches).toEqual({
|
||||||
|
scheme: "*",
|
||||||
|
host: "*.example.com",
|
||||||
path: "*",
|
path: "*",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -69,8 +69,6 @@ export default class Match<T> {
|
|||||||
if (!u.host.endsWith(":*")) {
|
if (!u.host.endsWith(":*")) {
|
||||||
u.host = u.host.substring(0, u.host.length - 1);
|
u.host = u.host.substring(0, u.host.length - 1);
|
||||||
}
|
}
|
||||||
} else if (pos !== -1 && pos !== 0) {
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
u.host = u.host.replace(/\*/g, "[^/]*?");
|
u.host = u.host.replace(/\*/g, "[^/]*?");
|
||||||
// 处理 *.开头
|
// 处理 *.开头
|
||||||
@ -225,7 +223,12 @@ export interface PatternMatchesUrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 解析URL, 根据https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns?hl=zh-cn进行处理
|
// 解析URL, 根据https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns?hl=zh-cn进行处理
|
||||||
export function parsePatternMatchesURL(url: string): PatternMatchesUrl | undefined {
|
export function parsePatternMatchesURL(
|
||||||
|
url: string,
|
||||||
|
options?: {
|
||||||
|
exclude?: boolean;
|
||||||
|
}
|
||||||
|
): PatternMatchesUrl | undefined {
|
||||||
let result: PatternMatchesUrl | undefined;
|
let result: PatternMatchesUrl | undefined;
|
||||||
const match = /^(.+?):\/\/(.*?)(\/(.*?)(\?.*?|)|)$/.exec(url);
|
const match = /^(.+?):\/\/(.*?)(\/(.*?)(\?.*?|)|)$/.exec(url);
|
||||||
if (match) {
|
if (match) {
|
||||||
@ -260,17 +263,38 @@ export function parsePatternMatchesURL(url: string): PatternMatchesUrl | undefin
|
|||||||
if (result.host.endsWith("*")) {
|
if (result.host.endsWith("*")) {
|
||||||
result.host = result.host.slice(0, -1);
|
result.host = result.host.slice(0, -1);
|
||||||
}
|
}
|
||||||
|
// 处理 www.*.example.com 的情况为 *.example.com
|
||||||
|
const pos = result.host.lastIndexOf("*");
|
||||||
|
if (pos > 0 && pos < result.host.length - 1) {
|
||||||
|
if (options && options.exclude) {
|
||||||
|
// 如果是exclude, 按最小匹配处理
|
||||||
|
// 包括*也去掉
|
||||||
|
result.host = result.host.substring(pos + 1);
|
||||||
|
if (result.host.startsWith(".")) {
|
||||||
|
result.host = result.host.substring(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果不是exclude
|
||||||
|
// 将*前面的全部去掉
|
||||||
|
result.host = result.host.substring(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理油猴的match和include为chrome的pattern-matche
|
// 处理油猴的match和include为chrome的pattern-matche
|
||||||
export function dealPatternMatches(matches: string[]) {
|
export function dealPatternMatches(
|
||||||
|
matches: string[],
|
||||||
|
options?: {
|
||||||
|
exclude?: boolean;
|
||||||
|
}
|
||||||
|
) {
|
||||||
const patternResult: string[] = [];
|
const patternResult: string[] = [];
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
for (let i = 0; i < matches.length; i++) {
|
for (let i = 0; i < matches.length; i++) {
|
||||||
const url = parsePatternMatchesURL(matches[i]);
|
const url = parsePatternMatchesURL(matches[i], options);
|
||||||
if (url) {
|
if (url) {
|
||||||
patternResult.push(`${url.scheme}://${url.host}/${url.path}`);
|
patternResult.push(`${url.scheme}://${url.host}/${url.path}`);
|
||||||
result.push(matches[i]);
|
result.push(matches[i]);
|
||||||
|
@ -8,6 +8,9 @@ import { Server } from "@Packages/message/server";
|
|||||||
import { MessageQueue } from "@Packages/message/message_queue";
|
import { MessageQueue } from "@Packages/message/message_queue";
|
||||||
import { ServiceWorkerMessageSend } from "@Packages/message/window_message";
|
import { ServiceWorkerMessageSend } from "@Packages/message/window_message";
|
||||||
|
|
||||||
|
// 初始化数据库
|
||||||
|
migrate();
|
||||||
|
|
||||||
const OFFSCREEN_DOCUMENT_PATH = "src/offscreen.html";
|
const OFFSCREEN_DOCUMENT_PATH = "src/offscreen.html";
|
||||||
|
|
||||||
let creating: Promise<void> | null;
|
let creating: Promise<void> | null;
|
||||||
@ -46,8 +49,6 @@ async function setupOffscreenDocument() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// 初始化数据库
|
|
||||||
migrate();
|
|
||||||
// 初始化日志组件
|
// 初始化日志组件
|
||||||
const loggerCore = new LoggerCore({
|
const loggerCore = new LoggerCore({
|
||||||
writer: new DBWriter(new LoggerDAO()),
|
writer: new DBWriter(new LoggerDAO()),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user