Часть 1. Разбор общего файла Common.js
Для удобства понимания и поддержки сообществом материал разделен на три части: анализ общего файла утилит Common.js, анализ логики взаимодействия Service.js и детальный разбор работы сетевого моста QuillBotBridge.ps1.
Архитектура расширений QTranslate
Так как ядро QTranslate закрыто и работает на базе встроенного движка Active Scripting (JScript/IE), в нем отсутствуют современные API вроде Fetch или WebSockets.
Для обхода этого ограничения разработчики использовали следующую схему:
Common.js: Предоставляет стандартные структуры данных, полифиллы массивов, работу с кодировками и строками.
Service.js: Выступает фасадом для QTranslate. Он собирает параметры перевода, кодирует текст в Base64, записывает параметры в переменные окружения ОС и запускает внешний PowerShell-скрипт через ActiveX (WScript.Shell).
QuillBotBridge.ps1: PowerShell-скрипт запускается в скрытом режиме, считывает переменные окружения, открывает современное WebSocket-соединение с сервером QuillBot, обменивается пакетами по их собственному протоколу, сохраняет результат работы в JSON-файл во временную папку и завершает работу. JScript считывает этот файл и отдает результат обратно в программу.
Часть 1. Разбор общего файла Common.js
Этот скрипт загружается глобально для всех переводчиков. Он содержит базовую инфраструктуру, вспомогательные функции манипуляции с HTML, кодирования параметров, JSON-сериализации и полифиллы для старых версий JScript (эквивалент ES3/ES5 в Internet Explorer).
Вот этот код с подробными построчными комментариями и описанием функций:
code JavaScript
// ============================================================================
// ГЛОБАЛЬНЫЕ КОНСТАНТЫ И ОПРЕДЕЛЕНИЯ
// ============================================================================
// Базовые ограничения и символы переноса строк
var Const = {
MAX_URI_LEN: 1800, // Максимальная длина URI для GET-запросов
MAX_SOURCE_LEN: 5E3, // Максимальная длина исходного текста (5000 символов)
NL: "\r\n", // Стандартный перевод строки Windows
NL2: "\r\n\r\n" // Двойной перевод строки
};
// Битовая маска возможностей сервиса (для QTranslate)
var Capability = {
TRANSLATE: 1, // Сервис умеет переводить текст
DETECT_LANGUAGE: 2, // Сервис умеет определять язык
LISTEN: 4, // Сервис поддерживает озвучку (TTS)
DICTIONARY: 8 // Сервис поддерживает словарные статьи
};
// Перечисление используемых HTTP-методов
var HttpMethod = {
UNDEFINED: 0,
GET: 1,
POST: 2
};
// Поддерживаемые кодовые страницы (используются при отправке запросов)
var CodePage = {
WINDOWS1251: 1251,
UTF8: 65001,
ISO8859_1: 28591
};
var toString = Object.prototype.toString,
isArray = Array.isArray || function(a) {
return "[object Array]" === toString.call(a);
};
// ============================================================================
// КОНСТРУКТОРЫ КЛАССОВ И СТРУКТУР QTRANSLATE
// ============================================================================
/**
* Описывает метаданные сервиса перевода для интерфейса QTranslate.
* @param {number} a - Уникальный ID сервиса.
* @param {string} b - Отображаемое название сервиса.
* @param {string} c - Описание/копирайт сервиса.
* @param {number} d - Битовая маска возможностей (Capability).
*/
function ServiceHeader(a, b, c, d) {
this.id = a;
this.name = b || "";
this.info = c || "";
this.capabilities = d;
}
/**
* Описывает структуру HTTP-запроса, который QTranslate выполнит сам.
* @param {number} a - Метод (HttpMethod).
* @param {string} b - URI запроса.
* @param {string} c - Данные для POST/PUT тела запроса.
* @param {string} d - HTTP заголовки.
* @param {number} e - Кодовая страница (CodePage).
* @param {string} g - Имя callback-функции JScript, которая обработает ответ.
*/
function RequestData(a, b, c, d, e, g) {
this.method = a;
this.uri = b || "";
this.data = c || "";
this.headers = d || (a === HttpMethod.UNDEFINED ? "" : a === HttpMethod.GET ? getHeader() : postHeader());
this.codepage = e || CodePage.UTF8;
this.responseHandler = g || "";
}
/**
* Структура ответа, возвращаемая обработчиком QTranslate.
* @param {string} a - Текст перевода.
* @param {number} b - Определенный исходный язык (ID).
* @param {number} c - Язык перевода (ID).
* @param {string} d - Дополнительные данные (для отладки/словари).
* @param {string} e - Имя следующего обработчика запроса (для многоэтапных запросов).
*/
function ResponseData(a, b, c, d, e) {
this.translation = a || "";
this.sourceLanguage = b;
this.translationLanguage = c;
this.data = d || "";
this.nextRequestHandler = e || "";
}
// Глобальный объект настроек, заполняемый самой программой QTranslate
var Options = {};
/** Добавление опции в конфигурацию */
function addOption(a, b) {
Options[a] = b;
}
// ============================================================================
// ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ФОРМИРОВАНИЯ HTTP
// ============================================================================
/** Генерация заголовков по умолчанию для GET-запроса */
function getHeader() {
var a = format("Accept-Language: {0};q=0.8,en-US;q=0.6,en;q=0.4", Options.LanguageCode);
return "Accept: */*" + Const.NL + a + Const.NL + "Accept-Encoding: gzip,deflate" + Const.NL + "Accept-Charset: utf-8";
}
/**
* Генерация заголовков по умолчанию для POST-запроса.
* @param {boolean} a - Флаг формата JSON (true = application/json, false = urlencoded).
*/
function postHeader(a) {
a = a ? "application/json" : "application/x-www-form-urlencoded";
return getHeader() + Const.NL + "Content-Type: " + a + "; charset=utf-8";
}
/** Кодирование параметра для URL (стандартный URI Encode) */
function encodeUriParam(a) {
return a ? encodeURIComponent(a) : "";
}
/**
* Кодирование параметра для GET-запроса с учетом ограничений длины URL.
* Обрезает строку по пробелу (%20), чтобы ссылка осталась валидной.
*/
function encodeGetParam(a) {
a = encodeUriParam(a);
if (a.length > Const.MAX_URI_LEN) {
a = a.slice(0, Const.MAX_URI_LEN);
var b = a.lastIndexOf("%20");
if (0 < b) return a.slice(0, b);
}
return a;
}
/** Кодирование параметра для POST тела */
function encodePostParam(a) {
return encodeUriParam(a);
}
/** Нормализация переносов строк (превращает Windows CRLF и Mac CR в Linux LF) */
function prepareSource(a) {
return (a || "").replace(/(\r\n|\r)/g, "\n");
}
/** Ограничение текста по лимиту символов */
function limitSource(a, b) {
b = b || Const.MAX_SOURCE_LEN;
return a && a.length > b ? a.slice(0, b) : a;
}
// ============================================================================
// РАБОТА СО СТРОКАМИ И HTML
// ============================================================================
/** Форматирование строки а-ля C# String.Format ("Hello {0}", name) */
function format() {
for (var a = arguments[0], b = 0; b < arguments.length - 1; b++) {
a = a.replace(new RegExp("\\{" + b + "\\}", "gm"), arguments[b + 1]);
}
return a;
}
/** Поиск подстроки или регулярного выражения в строке, возвращает объект {index, length} */
function stringFind(a, b) {
if (b) {
if ("string" === typeof b) {
var c = a.indexOf(b);
if (-1 < c) return { index: c, length: b.length };
} else if (c = a.match(b)) {
return { index: c.index, length: c[0].length };
}
}
}
/** Извлечение подстроки между двумя маркерами */
function stringFindSub(a, b, c, d, e) {
return a && b && d && (b = stringFind(a, b)) &&
(a = a.slice(c ? b.index : b.index + b.length), b = stringFind(a, d)) ?
a.slice(0, e ? b.index + b.length : b.index) : "";
}
/** Обрезка пробельных символов в начале и конце строки */
function trimString(a) {
return a ? a.replace(/(^\s+)|(\s+$)/g, "") : "";
}
function startsWith(a, b) {
return a.slice(0, b.length) === b;
}
function endsWith(a, b) {
return -1 !== a.indexOf(b, a.length - b.length);
}
function removeIfStartsWith(a, b) {
return startsWith(a, b) ? a.slice(b.length) : a;
}
// Проверка на баг разделения строк регулярными выражениями в старых IE
var IE_SPLIT_ISSUE = 3 !== "a'b".split(/(')/g).length;
/** Безопасный split с регулярными выражениями для совместимости со старыми IE */
function stringSplit(a, b) {
if (!IE_SPLIT_ISSUE) return a.split(b);
var c = [];
b.global = !0;
for (var d = b.exec(a); d;) {
c.push(a.substring(0, d.index));
for (var e = 1, g = d.length; e < g; e++) c.push(d[e]);
a = a.substring(b.lastIndex, a.length);
d = b.exec(a);
}
c.push(a);
return c;
}
/** Декодирование основных сущностей HTML (& -> &, < -> < и т.д.) */
function unquoteHtml(a) {
if (!a) return "";
a = a.replace(/&[;]?/g, "&");
a = a.replace(/<[;]?/g, "<");
a = a.replace(/>[;]?/g, ">");
a = a.replace(/"[;]?/g, '"');
return a.replace(/[0]?39[;]/g, "'");
}
/** Удаление лишних пустых строк и приведение структуры текста к CRLF */
function removeEmptyLines(a) {
if (!a) return "";
a = a.replace(/\r/gi, "");
a = a.split("\n");
for (var b = 0; b < a.length; b++) {
a[b].match(/[\S]/g) || (a[b] = "");
}
a = a.join("\n").replace(/\n{3,}/g, "\n\n");
a = a.replace(/\n/gi, Const.NL);
return trimString(a);
}
/** Очистка текста от HTML тегов, стилей, скриптов с сохранением форматирования переносов */
function stripHtml(a) {
if (!a) return "";
a = a.replace(/\x3c!--[\s\S]*?--\x3e/g, ""); // Удаление комментариев <!-- ... -->
a = a.replace(/<(style|script)(.|\s)*?(style|script)>/g, ""); // Удаление CSS и JS тегов
a = a.replace(/<br\/>/gi, Const.NL); // Преобразование BR в переносы
a = a.replace(/<[^>]+>/g, ""); // Удаление всех тегов <...>
a = a.replace(/ /g, " ");
a = a.replace(/'/gi, "'");
a = a.replace(/ +(?= )/g, ""); // Сжатие множественных пробелов
return removeEmptyLines(a);
}
/** Служебная функция удаления по регулярному выражению на основе массива ключевых слов */
function regExpRemove(a, b, c) {
if (!b) return a;
b = format(c, b.join("|"));
return a.replace(new RegExp(b, "g"), "");
}
/** Удаление конкретных HTML атрибутов из строки */
function removeAttributes(a, b) {
return regExpRemove(a, b, " ({0})(?:\\s*=\\s*(??:\"((?:\\.|[^\"])*)\")|(?:'((?:\\.|[^'])*)')|([^>\\s]+)))?");
}
/** Удаление тегов по списку имён (например, ['span', 'div']) */
function removeTags(a, b) {
return regExpRemove(a, b, "</?({0})[^>]*>");
}
/** Удаление элементов целиком вместе с содержимым */
function removeElements(a, b) {
return regExpRemove(a, b, "<\\s*({0})[^>]*>((.|\n)*?)<\\s*/\\s*({0})>");
}
/** Преобразование относительных путей в HTML ссылках (href/src) в абсолютные */
function updateHtmlLinks(a, b) {
if (!a) return "";
var c = startsWith(b, "https") ? "https://" : "http://";
b = removeIfStartsWith(b, c);
endsWith(b, "/") || (b += "/");
a = a.replace(/href="\/\//gi, 'href="' + c);
a = a.replace(/href="\//gi, 'href="' + c + b);
a = a.replace(/src="\/\//gi, 'src="' + c);
a = a.replace(/src="\//gi, 'src="' + c + b);
a = a.replace(/href=' \/\//gi, "href='" + c);
a = a.replace(/href='\/ /gi, "href='" + c + b);
a = a.replace(/src=' \/\//gi, "src='" + c);
return a = a.replace(/src='\/ /gi, "src='" + c + b);
}
// ============================================================================
// РАБОТА С ЯЗЫКАМИ И КОДАМИ
// ============================================================================
var SupportedLanguages = null,
UNKNOWN_LANGUAGE_CODE = -1,
UNKNOWN_LANGUAGE = 0,
AUTO_DETECT_LANGUAGE = 1,
ENGLISH_LANGUAGE = 17;
/** Проверка, валидный ли ID языка в массиве поддержки */
function isLanguage(a) {
return a > AUTO_DETECT_LANGUAGE && a < SupportedLanguages.length;
}
/** Получение строкового кода (например, "ru") по ID языка */
function codeFromLanguage(a) {
return a === AUTO_DETECT_LANGUAGE || isLanguage(a) ? SupportedLanguages[a] : UNKNOWN_LANGUAGE_CODE;
}
/** Получение ID языка по его строковому коду */
function languageFromCode(a) {
if (SupportedLanguages[ENGLISH_LANGUAGE] === a) return ENGLISH_LANGUAGE;
for (var b = AUTO_DETECT_LANGUAGE; b < SupportedLanguages.length; b++) {
if (SupportedLanguages[b] === a) return b;
}
return UNKNOWN_LANGUAGE;
}
/** Поддерживает ли сервис автоопределение языка */
function usesAutoDetectCode() {
return SupportedLanguages[AUTO_DETECT_LANGUAGE] !== UNKNOWN_LANGUAGE_CODE;
}
// ============================================================================
// JSON ПАРСЕРЫ (СОВМЕСТИМОСТЬ)
// ============================================================================
/** Безопасный парсинг JSON без нативного JSON.parse (использует конструктор Function) */
function parseJSON(a) {
return (new Function("return " + a))();
}
/** Сериализатор JSON объектов в строку для старых версий JScript */
var stringifyJSON = function() {
var a = {
'"': '\\"', "\\": "\\\\", "\b": "\\b", "\f": "\\f",
"\n": "\\n", "\r": "\\r", "\t": "\\t"
},
b = function(b) {
return a[b] || "\\u" + (b.charCodeAt(0) + 65536).toString(16).substr(1);
},
c = /[\\"\u0000-\u001F\u2028\u2029]/g;
return function e(a) {
if (null === a) return "null";
if ("number" === typeof a) return isFinite(a) ? a.toString() : "null";
if ("boolean" === typeof a) return a.toString();
if ("object" === typeof a) {
if ("function" === typeof a.toJSON) return e(a.toJSON());
if (isArray(a)) {
for (var h = "[", f = 0; f < a.length; f++) {
h += (f ? ", " : "") + e(a[f]);
}
return h + "]";
}
if ("[object Object]" === toString.call(a)) {
h = [];
for (f in a) {
a.hasOwnProperty(f) && h.push(e(f) + ": " + e(a[f]));
}
return "{" + h.join(", ") + "}";
}
}
return '"' + a.toString().replace(c, b) + '"';
};
}();
// ============================================================================
// ПОЛИФИЛЛЫ ДЛЯ СТАНДАРТНЫХ МЕТОДОВ МАССИВОВ (ES5)
// ============================================================================
Array.prototype.map || (Array.prototype.map = function(a) {
for (var b = Object(this), c = b.length >>> 0, d = Array(c), e = 0; e < c;) {
e in b && (d[e] = a(b[e], e, b)), e++;
}
return d;
});
Array.prototype.forEach || (Array.prototype.forEach = function(a) {
for (var b = Object(this), c = b.length >>> 0, d = 0; d < c;) {
d in b && a(b[d], d, b), d++;
}
});
Array.prototype.filter || (Array.prototype.filter = function(a) {
for (var b = Object(this), c = b.length >>> 0, d = 0, e = [], g; d < c;) {
d in b && (g = b[d], a(g, d, b) && e.push(g)), d++;
}
return e;
});