Merge branch 'release/v9.1.0' into feature/admin-panel2

This commit is contained in:
PauI Ostrovckij
2025-09-12 16:18:49 +03:00
parent 3172491fd3
commit 7ef64b28b4
24 changed files with 2348 additions and 4828 deletions

View File

@ -433,8 +433,7 @@
},
"mysqlExtraOptions": {
"connectTimeout": 60000,
"queryTimeout": 60000,
"autoCommit": false
"queryTimeout": 60000
}
},
"redis": {

View File

@ -70,6 +70,36 @@
"description": "HTTP error code to return when IP is not allowed"
}
}
},
"sql": {
"type": "object",
"description": "Database connection settings for the CoAuthoring service",
"additionalProperties": false,
"x-scope": "admin",
"properties": {
"type": {
"type": "string",
"description": "Database type (e.g., 'mysql', 'mariadb', 'mssql', 'postgres', 'dameng', 'oracle')",
"examples": ["postgres"]
},
"dbHost": {
"type": "string",
"description": "Database host name or IP address",
"examples": ["localhost"]
},
"dbPort": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"description": "Database TCP port",
"examples": [5432]
},
"dbName": {
"type": "string",
"description": "Database name",
"examples": ["onlyoffice"]
}
}
}
}
}

View File

@ -76,6 +76,13 @@ Context.prototype.initFromConnection = function (conn) {
const userSessionId = utils.getSessionIdByConnection(this, conn);
this.init(tenant, docId || this.docId, userId || this.userId, shardKey, wopiSrc, userSessionId);
};
Context.prototype.initFromConnectionRequest = function (req) {
this.initFromRequest(req);
const docIdParsed = constants.DOC_ID_SOCKET_PATTERN.exec(req.url);
if (docIdParsed && 1 < docIdParsed.length) {
this.setDocId(docIdParsed[1]);
}
};
Context.prototype.initFromRequest = function (req) {
const tenant = tenantManager.getTenantByRequest(this, req);
const shardKey = utils.getShardKeyByRequest(this, req);

View File

@ -924,6 +924,25 @@ exports.getSessionIdByConnection = getSessionIdByConnection;
exports.getShardKeyByRequest = getShardKeyByRequest;
exports.getWopiSrcByRequest = getWopiSrcByRequest;
exports.getSessionIdByRequest = getSessionIdByRequest;
/**
* Adapt a raw Node/engine.io IncomingMessage to behave like an Express Request.
* @param {http.IncomingMessage} rawReq
* @param {Express} app
*/
exports.expressifyIncomingMessage = function (rawReq, app) {
if (!rawReq || !app?.request || rawReq.app) {
return;
}
Object.setPrototypeOf(rawReq, app.request);
rawReq.app = app;
// Initialize Express-like properties
rawReq.originalUrl = rawReq.originalUrl || rawReq.url || '/';
rawReq.query = rawReq.query || (rawReq.url ? url.parse(rawReq.url, true).query : {});
};
function stream2Buffer(stream) {
return new Promise((resolve, reject) => {
if (!stream.readable) {

View File

@ -1806,7 +1806,7 @@ async function encryptPasswordParams(ctx, data) {
}
exports.encryptPasswordParams = encryptPasswordParams;
exports.getOpenFormatByEditor = getOpenFormatByEditor;
exports.install = function (server, callbackFunction) {
exports.install = function (server, app, callbackFunction) {
const io = new Server(server, cfgSocketIoConnection);
io.use((socket, next) => {
@ -2022,7 +2022,15 @@ exports.install = function (server, callbackFunction) {
}
});
io.engine.on('connection_error', err => {
operationContext.global.logger.warn('io.connection_error code=%s, message=%s', err.code, err.message);
let logger = operationContext.global.logger;
if (err.req) {
const ctx = new operationContext.Context();
// Ensure raw IncomingMessage has Express properties for consistent context init
utils.expressifyIncomingMessage(err.req, app);
ctx.initFromConnectionRequest(err.req);
logger = ctx.logger;
}
logger.warn('io.connection_error code=%s, message=%s, url=%s', err?.code, err?.message, err?.req?.url);
});
/**
*

View File

@ -34,7 +34,6 @@
const mysql = require('mysql2/promise');
const connectorUtilities = require('./connectorUtilities');
const operationContext = require('../../../Common/sources/operationContext');
const config = require('config');
const configSql = config.get('services.CoAuthoring.sql');
@ -59,25 +58,9 @@ if (configuration.queryTimeout) {
queryTimeout = configuration.queryTimeout;
delete configuration.queryTimeout;
}
let autoCommit = false;
if (configuration.autoCommit !== undefined) {
//delete to fix issue with invalid configuration option
autoCommit = configuration.autoCommit;
delete configuration.autoCommit;
}
const pool = mysql.createPool(configuration);
// Set autocommit once per connection
if (autoCommit === true) {
pool.on('connection', async conn => {
conn
.promise()
.query('SET autocommit=1')
.catch(err => operationContext.global.logger.error('Failed to set autocommit=1:', err.message));
});
}
function sqlQuery(ctx, sqlCommand, callbackFunction, opt_noModifyRes = false, opt_noLog = false, opt_values = []) {
return executeQuery(ctx, sqlCommand, opt_values, opt_noModifyRes, opt_noLog).then(
result => callbackFunction?.(null, result),
@ -90,6 +73,12 @@ async function executeQuery(ctx, sqlCommand, values = [], noModifyRes = false, n
try {
connection = await pool.getConnection();
// Ensure session autocommit=1 once per physical connection; avoids pool 'connection' race and per-query overhead
if (!connection.__autocommitSet) {
await connection.query('SET autocommit=1');
connection.__autocommitSet = true;
}
const result = await connection.query({sql: sqlCommand, timeout: queryTimeout, values});
let output;

View File

@ -87,25 +87,30 @@ const checkFileExpire = function (expireSeconds) {
const shardKey = sqlBase.DocumentAdditional.prototype.getShardKey(expired[i].additional);
const wopiSrc = sqlBase.DocumentAdditional.prototype.getWopiSrc(expired[i].additional);
if (currentTenant !== tenant) {
ctx.init(tenant, docId, ctx.userId, shardKey, wopiSrc);
yield ctx.initTenantCache();
currentTenant = tenant;
} else {
ctx.setDocId(docId);
ctx.setShardKey(shardKey);
ctx.setWopiSrc(wopiSrc);
}
//todo tenant
//check that no one is in the document
const editorsCount = yield docsCoServer.getEditorsCountPromise(ctx, docId);
if (0 === editorsCount) {
if (yield canvasService.cleanupCache(ctx, docId)) {
currentRemovedCount++;
try {
if (currentTenant !== tenant) {
ctx.init(tenant, docId, ctx.userId, shardKey, wopiSrc);
yield ctx.initTenantCache();
currentTenant = tenant;
} else {
ctx.setDocId(docId);
ctx.setShardKey(shardKey);
ctx.setWopiSrc(wopiSrc);
}
} else {
ctx.logger.debug('checkFileExpire expire but presence: editorsCount = %d', editorsCount);
//todo tenant
//check that no one is in the document
const editorsCount = yield docsCoServer.getEditorsCountPromise(ctx, docId);
if (0 === editorsCount) {
if (yield canvasService.cleanupCache(ctx, docId)) {
currentRemovedCount++;
}
} else {
ctx.logger.debug('checkFileExpire expire but presence: editorsCount = %d', editorsCount);
}
} catch (error) {
ctx.logger.error('checkFileExpire file error: %s', error.stack);
// Continue processing other files
}
}
removedCount += currentRemovedCount;

View File

@ -18,8 +18,7 @@ const cfgEditorStatStorage =
// Initialize editor stat storage
const editorStatStorage = require(`../${cfgEditorStatStorage}`);
const editorStat = new editorStatStorage.EditorStat();
console.error(`../${cfgEditorStatStorage}`);
console.error(editorStat);
// Constants
const PRECISION = [
{name: 'hour', val: ms('1h')},
@ -242,3 +241,5 @@ function createInfoRouter() {
}
module.exports = createInfoRouter;
// Export handler for reuse
module.exports.licenseInfo = licenseInfo;

View File

@ -157,7 +157,7 @@ try {
// If you want to use 'development' and 'production',
// then with app.settings.env (https://github.com/strongloop/express/issues/936)
// If error handling is needed, now it's like this https://github.com/expressjs/errorhandler
docsCoServer.install(server, () => {
docsCoServer.install(server, app, () => {
operationContext.global.logger.info('Start callbackFunction');
server.listen(config.get('services.CoAuthoring.server.port'), () => {

View File

@ -75,13 +75,12 @@ for file in filesReplace:
testDevelopVersion = sdkjsDirectory + "/.git"
if not os.path.isdir(testDevelopVersion):
print("Not in develop version, skipping x2t cache update")
print("Update x2t cache...")
x2tDir = curDirectory + "/../FileConverter/bin"
cur_dir = os.getcwd()
os.chdir(x2tDir)
x2tBin = curDirectory + "/../FileConverter/bin/x2t"
if ("windows" == platform.system().lower()):
subprocess.call(["x2t.exe", "-create-js-cache"], stderr=subprocess.STDOUT, shell=True)
else:
subprocess.call("x2t -create-js-cache", stderr=subprocess.STDOUT, shell=True)
subprocess.call("./x2t -create-js-cache", stderr=subprocess.STDOUT, shell=True)
os.chdir(cur_dir)

6660
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,9 +19,9 @@
"eslint-plugin-react-hooks": "^5.2.0",
"express": "4.21.2",
"globals": "15.12.0",
"husky": "^9.1.7",
"husky": "8.0.3",
"jest": "29.7.0",
"lint-staged": "^16.1.5",
"lint-staged": "15.2.10",
"prettier": "3.4.2"
},
"scripts": {
@ -31,7 +31,6 @@
"format:check": "prettier . --check",
"code:check": "run-s lint:check format:check",
"code:fix": "run-s lint:fix format:fix",
"prepare": "husky",
"perf-expired": "cd ./DocService&& cross-env NODE_ENV=development-windows NODE_CONFIG_DIR=../Common/config node ../tests/perf/checkFileExpire.js",
"perf-exif": "cd ./DocService&& cross-env NODE_ENV=development-windows NODE_CONFIG_DIR=../Common/config node ../tests/perf/fixImageExifRotation.js",
"perf-png": "cd ./DocService&& cross-env NODE_ENV=development-windows NODE_CONFIG_DIR=../Common/config node ../tests/perf/convertImageToPng.js",
@ -46,7 +45,7 @@
"install:FileConverter": "npm ci --prefix ./FileConverter",
"install:Metrics": "npm ci --prefix ./Metrics",
"install:AdminPanel/server": "npm ci --prefix ./AdminPanel/server",
"install:AdminPanel/client": "npm ci --prefix ./AdminPanel/client && npm --prefix ./AdminPanel/client run build",
"install:AdminPanel/client": "npm ci --include=dev --prefix ./AdminPanel/client && npm --prefix ./AdminPanel/client run build",
"3d-party-lic-json:Common": "license-report --output=json --package=./Common/package.json --config ./3d-party-lic-report/license-report-config.json > ./3d-party-lic-report/license-report.json",
"3d-party-lic-json:DocService": "license-report --output=json --package=./DocService/package.json --config ./3d-party-lic-report/license-report-config.json > ./3d-party-lic-report/license-report.json",
"3d-party-lic-json:FileConverter": "license-report --output=json --package=./FileConverter/package.json --config ./3d-party-lic-report/license-report-config.json > ./3d-party-lic-report/license-report.json",

46
tests/fixtures/README.md vendored Normal file
View File

@ -0,0 +1,46 @@
# info.json Fixtures for Rendering Tests
This directory contains sample `info.json` payloads that exercise different rendering paths in:
- Static page: `branding/info/index.html`
- React AdminPanel: `AdminPanel/client/src/components/Statistics/`
Each file is self-contained and adheres to the server `info.json` schema used by the UI.
## Automatic Fixture Cycling
To enable automatic cycling through fixtures on each request, add this code to your `licenseInfo` function:
```javascript
const path = require('path');
const fs = require('fs');
// Request counter for cycling through fixtures (persistent across calls)
licenseInfo.requestCounter = (licenseInfo.requestCounter || 0) + 1;
licenseInfo.fixtureFiles = licenseInfo.fixtureFiles || [];
// Load fixture files list on first call
if (licenseInfo.fixtureFiles.length === 0) {
try {
const fixturesDir = path.join(__dirname, '../../../tests/fixtures/info');
const files = fs.readdirSync(fixturesDir);
licenseInfo.fixtureFiles = files.filter(file => file.endsWith('.json'));
} catch (e) {
// If fixtures directory doesn't exist, continue with normal flow
}
}
// Cycle through fixtures on every request
if (licenseInfo.fixtureFiles.length > 0) {
const fixtureIndex = (licenseInfo.requestCounter - 1) % licenseInfo.fixtureFiles.length;
const fixturePath = path.join(__dirname, '../../../tests/fixtures/info', licenseInfo.fixtureFiles[fixtureIndex]);
try {
const fixtureData = JSON.parse(fs.readFileSync(fixturePath, 'utf8'));
return res.json(fixtureData);
} catch (e) {
// If fixture fails to load, continue with normal flow
}
}
```
## Files

View File

@ -0,0 +1,42 @@
{
"licenseInfo": {
"packageType": 1,
"buildDate": "2025-08-20T00:00:00Z",
"mode": 4,
"endDate": "2026-08-20T00:00:00Z",
"type": 0,
"startDate": "2025-08-01T00:00:00Z",
"connections": 50,
"connectionsView": 30,
"usersCount": 0,
"usersViewCount": 0
},
"serverInfo": {
"buildVersion": "8.2",
"buildNumber": "1234",
"date": "2025-09-05T12:00:00Z"
},
"quota": {
"edit": {"connectionsCount": 20},
"view": {"connectionsCount": 28},
"byMonth": []
},
"connectionsStat": {
"hour": {
"edit": {"max": 52, "avr": 40},
"liveview": {"max": 29, "avr": 27}
},
"day": {
"edit": {"max": 45, "avr": 30},
"liveview": {"max": 30, "avr": 22}
},
"week": {
"edit": {"max": 50, "avr": 35},
"liveview": {"max": 31, "avr": 26}
},
"month": {
"edit": {"max": 40, "avr": 28},
"liveview": {"max": 25, "avr": 20}
}
}
}

View File

@ -0,0 +1,25 @@
{
"licenseInfo": {
"packageType": 1,
"buildDate": "2025-08-10T00:00:00Z",
"mode": 4,
"endDate": "2026-08-10T00:00:00Z",
"type": 0,
"startDate": "2025-08-01T00:00:00Z",
"connections": 20,
"connectionsView": 10,
"usersCount": 0,
"usersViewCount": 0
},
"serverInfo": {
"buildVersion": "8.2",
"buildNumber": "1236",
"date": "2025-09-05T12:00:00Z"
},
"quota": {
"edit": {"connectionsCount": 19},
"view": {"connectionsCount": 9},
"byMonth": []
},
"connectionsStat": {}
}

View File

@ -0,0 +1,32 @@
{
"licenseInfo": {
"packageType": 1,
"buildDate": "2025-08-15T00:00:00Z",
"mode": 4,
"endDate": "2026-08-15T00:00:00Z",
"type": 0,
"startDate": "2025-08-01T00:00:00Z",
"connections": 40,
"connectionsView": 25,
"usersCount": 0,
"usersViewCount": 0
},
"serverInfo": {
"buildVersion": "8.2",
"buildNumber": "1235",
"date": "2025-09-05T12:00:00Z"
},
"quota": {
"edit": {"connectionsCount": 12},
"view": {"connectionsCount": 9},
"byMonth": []
},
"connectionsStat": {
"hour": {
"edit": {"max": 10, "avr": 8}
},
"day": {
"liveview": {"max": 20, "avr": 12}
}
}
}

View File

@ -0,0 +1,35 @@
{
"licenseInfo": {
"packageType": 2,
"buildDate": "2025-08-18T00:00:00Z",
"mode": 4,
"endDate": "2026-08-18T00:00:00Z",
"type": 0,
"startDate": "2025-08-01T00:00:00Z",
"usersCount": 200,
"usersViewCount": 150,
"usersExpire": 2592000
},
"serverInfo": {
"buildVersion": "8.2",
"buildNumber": "2007",
"date": "2025-09-05T12:00:00Z"
},
"quota": {
"edit": {"usersCount": {"unique": 120, "anonymous": 30}},
"view": {"usersCount": {"unique": 100, "anonymous": 20}},
"byMonth": [
{
"date": "2025-06-01T00:00:00Z",
"users": {"a": {}, "b": {}, "c": {"anonym": true}},
"usersView": {"d": {}, "e": {"anonym": true}}
},
{
"date": "2025-07-01T00:00:00Z",
"users": {"f": {}, "g": {"anonym": true}},
"usersView": {"h": {}, "i": {}, "j": {"anonym": true}}
}
]
},
"connectionsStat": {}
}

View File

@ -0,0 +1,27 @@
{
"licenseInfo": {
"packageType": 0,
"buildDate": "2025-08-12T00:00:00Z",
"mode": 4,
"endDate": "2026-08-12T00:00:00Z",
"type": 0,
"startDate": "2025-08-01T00:00:00Z",
"connections": 15,
"connectionsView": 10,
"usersCount": 0,
"usersViewCount": 0
},
"serverInfo": {
"buildVersion": "8.2",
"buildNumber": "1237",
"date": "2025-09-05T12:00:00Z"
},
"quota": {
"edit": {"connectionsCount": 5},
"view": {"connectionsCount": 3},
"byMonth": []
},
"connectionsStat": {
"hour": {"edit": {"max": 12, "avr": 7}, "liveview": {"max": 9, "avr": 5}}
}
}

35
tests/fixtures/info/users_basic.json vendored Normal file
View File

@ -0,0 +1,35 @@
{
"licenseInfo": {
"packageType": 1,
"buildDate": "2025-08-01T00:00:00Z",
"mode": 4,
"endDate": "2026-08-01T00:00:00Z",
"type": 0,
"startDate": "2025-08-01T00:00:00Z",
"usersCount": 100,
"usersViewCount": 60,
"usersExpire": 2592000
},
"serverInfo": {
"buildVersion": "8.2",
"buildNumber": "2001",
"date": "2025-09-05T12:00:00Z"
},
"quota": {
"edit": {"usersCount": {"unique": 55, "anonymous": 5}},
"view": {"usersCount": {"unique": 45, "anonymous": 2}},
"byMonth": [
{
"date": "2025-06-01T00:00:00Z",
"users": {"1": {}, "2": {"anonym": true}, "3": {}},
"usersView": {"10": {}, "11": {"anonym": true}}
},
{
"date": "2025-07-01T00:00:00Z",
"users": {"4": {}, "5": {}, "6": {"anonym": true}},
"usersView": {"12": {}, "13": {"anonym": true}, "14": {"anonym": true}}
}
]
},
"connectionsStat": {}
}

View File

@ -0,0 +1,24 @@
{
"licenseInfo": {
"packageType": 1,
"buildDate": "2025-08-05T00:00:00Z",
"mode": 4,
"endDate": "2026-08-05T00:00:00Z",
"type": 0,
"startDate": "2025-08-01T00:00:00Z",
"usersCount": 10,
"usersViewCount": 5,
"usersExpire": 604800
},
"serverInfo": {
"buildVersion": "8.2",
"buildNumber": "2002",
"date": "2025-09-05T12:00:00Z"
},
"quota": {
"edit": {"usersCount": {"unique": 10, "anonymous": 2}},
"view": {"usersCount": {"unique": 5, "anonymous": 1}},
"byMonth": []
},
"connectionsStat": {}
}

View File

@ -0,0 +1,24 @@
{
"licenseInfo": {
"packageType": 1,
"buildDate": "2025-08-01T00:00:00Z",
"mode": 4,
"endDate": "2025-12-01T00:00:00Z",
"type": 2,
"startDate": "2025-08-01T00:00:00Z",
"usersCount": 100,
"usersViewCount": 80,
"usersExpire": 2592000
},
"serverInfo": {
"buildVersion": "8.2",
"buildNumber": "2005",
"date": "2025-09-05T12:00:00Z"
},
"quota": {
"edit": {"usersCount": {"unique": 50, "anonymous": 10}},
"view": {"usersCount": {"unique": 40, "anonymous": 5}},
"byMonth": []
},
"connectionsStat": {}
}

View File

@ -0,0 +1,24 @@
{
"licenseInfo": {
"packageType": 1,
"buildDate": "2025-08-01T00:00:00Z",
"mode": 0,
"endDate": null,
"type": 0,
"startDate": "2025-08-01T00:00:00Z",
"usersCount": 50,
"usersViewCount": 40,
"usersExpire": 1209600
},
"serverInfo": {
"buildVersion": "8.2",
"buildNumber": "2003",
"date": "2025-09-05T12:00:00Z"
},
"quota": {
"edit": {"usersCount": {"unique": 5, "anonymous": 1}},
"view": {"usersCount": {"unique": 2, "anonymous": 0}},
"byMonth": []
},
"connectionsStat": {}
}

View File

@ -0,0 +1,24 @@
{
"licenseInfo": {
"packageType": 1,
"buildDate": "2025-09-01T00:00:00Z",
"mode": 5,
"endDate": "2026-09-01T00:00:00Z",
"type": 16,
"startDate": "2025-09-10T00:00:00Z",
"usersCount": 100,
"usersViewCount": 100,
"usersExpire": 2592000
},
"serverInfo": {
"buildVersion": "8.2",
"buildNumber": "2004",
"date": "2025-09-05T12:00:00Z"
},
"quota": {
"edit": {"usersCount": {"unique": 1, "anonymous": 0}},
"view": {"usersCount": {"unique": 1, "anonymous": 0}},
"byMonth": []
},
"connectionsStat": {}
}

View File

@ -0,0 +1,24 @@
{
"licenseInfo": {
"packageType": 1,
"buildDate": "2025-07-01T00:00:00Z",
"mode": 0,
"endDate": "2025-09-01T00:00:00Z",
"type": 0,
"startDate": "2025-07-01T00:00:00Z",
"usersCount": 100,
"usersViewCount": 80,
"usersExpire": 2592000
},
"serverInfo": {
"buildVersion": "8.2",
"buildNumber": "2006",
"date": "2025-09-10T12:00:00Z"
},
"quota": {
"edit": {"usersCount": {"unique": 50, "anonymous": 5}},
"view": {"usersCount": {"unique": 40, "anonymous": 4}},
"byMonth": []
},
"connectionsStat": {}
}