mirror of
https://github.com/ONLYOFFICE/server.git
synced 2026-04-07 14:04:35 +08:00
Merge pull request 'fix/admin-panel-bugs-3' (#73) from fix/admin-panel-bugs-3 into release/v9.1.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/server/pulls/73
This commit is contained in:
@ -5,7 +5,8 @@ coverage
|
||||
.next
|
||||
out
|
||||
AdminPanel/client/src/pages/AiIntegration/ai/**
|
||||
AdminPanel/client/src/pages/AiIntegration/js/**
|
||||
AdminPanel/client/src/pages/AiIntegration/js/plugins.js
|
||||
AdminPanel/client/src/pages/AiIntegration/js/plugins-ui.js
|
||||
*.min.js
|
||||
*.min.css
|
||||
package-lock.json
|
||||
|
||||
@ -257,14 +257,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"connectionConfiguration": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"disableFileAccess": {"type": "boolean"},
|
||||
"disableUrlAccess": {"type": "boolean"}
|
||||
}
|
||||
},
|
||||
"contactDefaults": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -53,6 +53,15 @@ function reloadNpmModule(moduleName) {
|
||||
// Backup original NODE_CONFIG to avoid growing environment
|
||||
const prevNodeConfig = process.env.NODE_CONFIG;
|
||||
let nodeConfigOverridden = false;
|
||||
let baseConfigSnapshot = null;
|
||||
|
||||
/**
|
||||
* Returns the base configuration as plain object before runtime configuration is applied
|
||||
* @returns {Object} Base configuration object
|
||||
*/
|
||||
function getBaseConfig() {
|
||||
return baseConfigSnapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires config module with runtime configuration support.
|
||||
@ -64,6 +73,9 @@ function requireConfigWithRuntime(opt_additionalConfig) {
|
||||
let config = require('config');
|
||||
|
||||
try {
|
||||
// Save base config before reloading with runtime modifications
|
||||
baseConfigSnapshot = config.util.toObject();
|
||||
|
||||
const configFilePath = config.get('runtimeConfig.filePath');
|
||||
if (configFilePath) {
|
||||
const configData = fs.readFileSync(configFilePath, 'utf8');
|
||||
@ -105,6 +117,7 @@ function finalizeConfigWithRuntime() {
|
||||
|
||||
module.exports = {
|
||||
reloadNpmModule,
|
||||
getBaseConfig,
|
||||
requireConfigWithRuntime,
|
||||
finalizeConfigWithRuntime
|
||||
};
|
||||
|
||||
@ -36,8 +36,6 @@ const ms = require('ms');
|
||||
|
||||
const mailService = require('./mailService');
|
||||
|
||||
const cfgMailServer = config.util.cloneDeep(config.get('email.smtpServerConfiguration'));
|
||||
const cfgMailMessageDefaults = config.util.cloneDeep(config.get('email.contactDefaults'));
|
||||
const cfgEditorDataStorage = config.get('services.CoAuthoring.server.editorDataStorage');
|
||||
const cfgEditorStatStorage = config.get('services.CoAuthoring.server.editorStatStorage');
|
||||
const editorStatStorage = require('./../../DocService/sources/' + (cfgEditorStatStorage || cfgEditorDataStorage));
|
||||
@ -56,13 +54,15 @@ class TransportInterface {
|
||||
}
|
||||
|
||||
class MailTransport extends TransportInterface {
|
||||
host = cfgMailServer.host;
|
||||
port = cfgMailServer.port;
|
||||
auth = cfgMailServer.auth;
|
||||
|
||||
constructor(ctx) {
|
||||
super();
|
||||
|
||||
const mailServerConfig = ctx.getCfg('email.smtpServerConfiguration');
|
||||
this.host = mailServerConfig.host;
|
||||
this.port = mailServerConfig.port;
|
||||
this.auth = mailServerConfig.auth;
|
||||
const cfgMailMessageDefaults = ctx.getCfg('email.contactDefaults');
|
||||
|
||||
mailService.createTransporter(ctx, this.host, this.port, this.auth, cfgMailMessageDefaults);
|
||||
}
|
||||
|
||||
|
||||
@ -32,12 +32,14 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const utils = require('./utils');
|
||||
const logger = require('./logger');
|
||||
const constants = require('./constants');
|
||||
const tenantManager = require('./tenantManager');
|
||||
const runtimeConfigManager = require('./runtimeConfigManager');
|
||||
const moduleReloader = require('./moduleReloader');
|
||||
|
||||
let configCache = null;
|
||||
|
||||
function Context() {
|
||||
this.logger = logger.getLogger('nodeJS');
|
||||
@ -99,12 +101,27 @@ Context.prototype.initFromPubSub = function (data) {
|
||||
this.init(ctx.tenant, ctx.docId, ctx.userId, ctx.shardKey, ctx.wopiSrc, ctx.userSessionId);
|
||||
};
|
||||
Context.prototype.initTenantCache = async function () {
|
||||
const runtimeConfig = await runtimeConfigManager.getConfig(this);
|
||||
const tenantConfig = await tenantManager.getTenantConfig(this);
|
||||
this.config = utils.deepMergeObjects(config.util.toObject(), runtimeConfig, tenantConfig);
|
||||
if (!configCache) {
|
||||
configCache = Object.create(null);
|
||||
}
|
||||
this.config = configCache[this.tenant];
|
||||
if (!this.config) {
|
||||
const runtimeConfig = await runtimeConfigManager.getConfig(this);
|
||||
const tenantConfig = await tenantManager.getTenantConfig(this);
|
||||
this.config = utils.deepMergeObjects({}, moduleReloader.getBaseConfig(), runtimeConfig, tenantConfig);
|
||||
configCache[this.tenant] = this.config;
|
||||
}
|
||||
|
||||
//todo license and secret
|
||||
};
|
||||
Context.prototype.cleanRuntimeConfigCache = function () {
|
||||
configCache = null;
|
||||
};
|
||||
Context.prototype.cleanTenantConfigCache = function (tenant) {
|
||||
if (configCache) {
|
||||
configCache[tenant] = null;
|
||||
}
|
||||
};
|
||||
|
||||
Context.prototype.setTenant = function (tenant) {
|
||||
this.tenant = tenant;
|
||||
@ -151,7 +168,7 @@ Context.prototype.getCfg = function (property, defaultValue) {
|
||||
* @returns {object} The merged configuration object
|
||||
*/
|
||||
Context.prototype.getFullCfg = function () {
|
||||
return utils.deepMergeObjects(config.util.toObject(), this.config);
|
||||
return utils.deepMergeObjects({}, moduleReloader.getBaseConfig(), this.config);
|
||||
};
|
||||
|
||||
exports.Context = Context;
|
||||
|
||||
@ -146,6 +146,8 @@ function handleConfigFileChange(eventTypeOrCurrent, filenameOrPrevious) {
|
||||
reloadTimer = null;
|
||||
nodeCache.del(configFileName);
|
||||
operationContext.global.logger.info(`handleConfigFileChange reloading config: ${configFileName}`);
|
||||
|
||||
operationContext.global.cleanRuntimeConfigCache();
|
||||
getConfig(operationContext.global)
|
||||
.then(config => {
|
||||
logger.configureLogger(config?.log?.options);
|
||||
|
||||
@ -116,6 +116,7 @@ async function getTenantConfig(ctx) {
|
||||
} catch (e) {
|
||||
ctx.logger.debug('getTenantConfig error: %s', e.stack);
|
||||
} finally {
|
||||
ctx.cleanTenantConfigCache(ctx.tenant);
|
||||
nodeCache.set(configPath, res);
|
||||
}
|
||||
}
|
||||
@ -135,6 +136,8 @@ async function setTenantConfig(ctx, config) {
|
||||
const tenantPath = utils.removeIllegalCharacters(ctx.tenant);
|
||||
const configPath = path.join(cfgTenantsBaseDir, tenantPath, cfgTenantsFilenameConfig);
|
||||
await writeFile(configPath, JSON.stringify(newConfig, null, 2), 'utf8');
|
||||
|
||||
ctx.cleanTenantConfigCache(ctx.tenant);
|
||||
nodeCache.set(configPath, newConfig);
|
||||
}
|
||||
return newConfig;
|
||||
@ -151,6 +154,8 @@ async function replaceTenantConfig(ctx, config) {
|
||||
const tenantPath = utils.removeIllegalCharacters(ctx.tenant);
|
||||
const configPath = path.join(cfgTenantsBaseDir, tenantPath, cfgTenantsFilenameConfig);
|
||||
await writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
|
||||
|
||||
ctx.cleanTenantConfigCache(ctx.tenant);
|
||||
nodeCache.set(configPath, config);
|
||||
return config;
|
||||
}
|
||||
|
||||
@ -1100,6 +1100,22 @@ function getSecretByElem(secretElem) {
|
||||
return secret;
|
||||
}
|
||||
exports.getSecretByElem = getSecretByElem;
|
||||
const jwtKeyCache = Object.create(null);
|
||||
/**
|
||||
* Gets or creates a cached symmetric key for JWT verification (HS256/HS384/HS512).
|
||||
* Caches crypto.KeyObject to avoid expensive key creation on every request.
|
||||
* @param {string} secret - JWT symmetric secret
|
||||
* @returns {crypto.KeyObject} Cached secret key object
|
||||
*/
|
||||
function getJwtHsKey(secret) {
|
||||
let res = jwtKeyCache[secret];
|
||||
if (!res) {
|
||||
res = jwtKeyCache[secret] = crypto.createSecretKey(Buffer.from(secret, 'utf8'));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
exports.getJwtHsKey = getJwtHsKey;
|
||||
|
||||
function fillJwtForRequest(ctx, payload, secret, opt_inBody) {
|
||||
const tenTokenOutboxAlgorithm = ctx.getCfg('services.CoAuthoring.token.outbox.algorithm', cfgTokenOutboxAlgorithm);
|
||||
const tenTokenOutboxExpires = ctx.getCfg('services.CoAuthoring.token.outbox.expires', cfgTokenOutboxExpires);
|
||||
@ -1114,7 +1130,7 @@ function fillJwtForRequest(ctx, payload, secret, opt_inBody) {
|
||||
}
|
||||
|
||||
const options = {algorithm: tenTokenOutboxAlgorithm, expiresIn: tenTokenOutboxExpires};
|
||||
return jwt.sign(data, secret, options);
|
||||
return jwt.sign(data, getJwtHsKey(secret), options);
|
||||
}
|
||||
exports.fillJwtForRequest = fillJwtForRequest;
|
||||
exports.forwarded = forwarded;
|
||||
|
||||
@ -512,7 +512,7 @@ function signToken(ctx, payload, algorithm, expiresIn, secretElem) {
|
||||
return co(function* () {
|
||||
const options = {algorithm, expiresIn};
|
||||
const secret = yield tenantManager.getTenantSecret(ctx, secretElem);
|
||||
return jwt.sign(payload, secret, options);
|
||||
return jwt.sign(payload, utils.getJwtHsKey(secret), options);
|
||||
});
|
||||
}
|
||||
function needSendChanges(conn) {
|
||||
@ -1556,6 +1556,7 @@ function createSaveTimer(ctx, docId, opt_userId, opt_userIndex, opt_userLcid, op
|
||||
updateMask.tenant = ctx.tenant;
|
||||
updateMask.key = docId;
|
||||
updateMask.status = commonDefines.FileStatus.Ok;
|
||||
updateMask.callback = 'NOT_EMPTY';
|
||||
const updateTask = new taskResult.TaskResultData();
|
||||
updateTask.status = commonDefines.FileStatus.SaveVersion;
|
||||
updateTask.statusInfo = utils.getMillisecondsOfHour(new Date());
|
||||
@ -1582,9 +1583,15 @@ function createSaveTimer(ctx, docId, opt_userId, opt_userIndex, opt_userLcid, op
|
||||
yield utils.sleep(c_oAscLockTimeOutDelay);
|
||||
}
|
||||
} else {
|
||||
//if it didn't work, it means FileStatus=SaveVersion(someone else started building) or UpdateVersion(build completed)
|
||||
// in this case, nothing needs to be done
|
||||
ctx.logger.debug('createSaveTimer updateIf no effect');
|
||||
const selectRes = yield taskResult.select(ctx, docId);
|
||||
if (selectRes.length > 0 && selectRes[0].callback) {
|
||||
//if it didn't work, it means FileStatus=SaveVersion(someone else started building) or UpdateVersion(build completed)
|
||||
// in this case, nothing needs to be done
|
||||
ctx.logger.debug('createSaveTimer updateIf no effect');
|
||||
} else {
|
||||
ctx.logger.debug('createSaveTimer empty callback: %s', docId);
|
||||
yield* cleanDocumentOnExitNoChanges(ctx, docId, opt_userId, opt_userIndex, false, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1599,7 +1606,7 @@ function checkJwt(ctx, token, type) {
|
||||
ctx.logger.warn('empty secret: token = %s', token);
|
||||
}
|
||||
try {
|
||||
res.decoded = jwt.verify(token, secret, tenTokenVerifyOptions);
|
||||
res.decoded = jwt.verify(token, utils.getJwtHsKey(secret), tenTokenVerifyOptions);
|
||||
ctx.logger.debug('checkJwt success: decoded = %j', res.decoded);
|
||||
} catch (err) {
|
||||
ctx.logger.warn('checkJwt error: name = %s message = %s token = %s', err.name, err.message, token);
|
||||
|
||||
@ -1891,7 +1891,7 @@ exports.saveFromChanges = function (ctx, docId, statusInfo, optFormat, opt_userI
|
||||
//we do a select, because during the timeout the information could change
|
||||
const selectRes = yield taskResult.select(ctx, docId);
|
||||
const row = selectRes.length > 0 ? selectRes[0] : null;
|
||||
if (row && row.status == commonDefines.FileStatus.SaveVersion && row.status_info == statusInfo && row.callback) {
|
||||
if (row && row.status == commonDefines.FileStatus.SaveVersion && row.status_info == statusInfo) {
|
||||
if (null == optFormat) {
|
||||
optFormat = changeFormatByOrigin(ctx, row, constants.AVS_OFFICESTUDIO_FILE_OTHER_OOXML);
|
||||
}
|
||||
@ -1921,10 +1921,6 @@ exports.saveFromChanges = function (ctx, docId, statusInfo, optFormat, opt_userI
|
||||
yield docsCoServer.editorStat.addShutdown(redisKeyShutdown, docId);
|
||||
}
|
||||
ctx.logger.debug('AddTask saveFromChanges');
|
||||
} else if (row && !row.callback) {
|
||||
ctx.logger.debug('saveFromChanges empty callback: %s', docId);
|
||||
yield docsCoServer.cleanDocumentOnExitNoChangesPromise(ctx, docId, opt_userId, opt_userIndex, false, true);
|
||||
//todo restore status
|
||||
} else {
|
||||
if (row) {
|
||||
ctx.logger.debug('saveFromChanges status mismatch: row: %d; %d; expected: %d', row.status, row.status_info, statusInfo);
|
||||
|
||||
@ -134,6 +134,19 @@ function select(ctx, docId) {
|
||||
);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Convert task object to SQL update/condition array
|
||||
* @param {TaskResultData} task - Task data object
|
||||
* @param {boolean} updateTime - Whether to update last_open_date
|
||||
* @param {boolean} isMask - Whether this is for WHERE clause (mask mode)
|
||||
* @param {Array} values - SQL parameter values array
|
||||
* @param {boolean} setPassword - Whether to set password directly
|
||||
* @returns {Array<string>} Array of SQL conditions/assignments
|
||||
*
|
||||
* Special mask values:
|
||||
* - Use 'NOT_EMPTY' as field value in mask mode to check for non-empty callback
|
||||
* - Example: {callback: 'NOT_EMPTY'} generates "callback IS NOT NULL AND callback != ''"
|
||||
*/
|
||||
function toUpdateArray(task, updateTime, isMask, values, setPassword) {
|
||||
const res = [];
|
||||
if (null != task.status) {
|
||||
@ -162,6 +175,10 @@ function toUpdateArray(task, updateTime, isMask, values, setPassword) {
|
||||
const sqlParam = addSqlParam(userCallback.toSQLInsert(), values);
|
||||
res.push(`callback=${concatParams('callback', sqlParam)}`);
|
||||
}
|
||||
// Add callback non-empty check for mask
|
||||
if (isMask && task.callback === 'NOT_EMPTY') {
|
||||
res.push(`callback IS NOT NULL AND callback != ''`);
|
||||
}
|
||||
if (null != task.baseurl) {
|
||||
const sqlParam = addSqlParam(task.baseurl, values);
|
||||
res.push(`baseurl=${sqlParam}`);
|
||||
|
||||
@ -677,7 +677,7 @@ function getEditorHtml(req, res) {
|
||||
|
||||
const options = {algorithm: tenTokenOutboxAlgorithm, expiresIn: tenTokenOutboxExpires};
|
||||
const secret = yield tenantManager.getTenantSecret(ctx, commonDefines.c_oAscSecretType.Browser);
|
||||
params.token = jwt.sign(params, secret, options);
|
||||
params.token = jwt.sign(params, utils.getJwtHsKey(secret), options);
|
||||
} catch (err) {
|
||||
ctx.logger.error('wopiEditor error: %s', err.stack);
|
||||
params.fileInfo = {};
|
||||
@ -743,7 +743,7 @@ function getConverterHtml(req, res) {
|
||||
const tokenData = {docId};
|
||||
const options = {algorithm: tenTokenOutboxAlgorithm, expiresIn: tenTokenOutboxExpires};
|
||||
const secret = yield tenantManager.getTenantSecret(ctx, commonDefines.c_oAscSecretType.Browser);
|
||||
const token = jwt.sign(tokenData, secret, options);
|
||||
const token = jwt.sign(tokenData, utils.getJwtHsKey(secret), options);
|
||||
|
||||
params.statusHandler += `&token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
@ -942,7 +942,7 @@ async function refreshFile(ctx, wopiParams, baseUrl) {
|
||||
}
|
||||
const options = {algorithm: tenTokenOutboxAlgorithm, expiresIn: tenTokenOutboxExpires};
|
||||
const secret = await tenantManager.getTenantSecret(ctx, commonDefines.c_oAscSecretType.Browser);
|
||||
res.token = jwt.sign(res, secret, options);
|
||||
res.token = jwt.sign(res, utils.getJwtHsKey(secret), options);
|
||||
} catch (err) {
|
||||
res = undefined;
|
||||
ctx.logger.error('wopi error RefreshFile:%s', err.stack);
|
||||
|
||||
@ -19,7 +19,8 @@ module.exports = [
|
||||
'.next/',
|
||||
'out/',
|
||||
'AdminPanel/client/src/pages/AiIntegration/ai/**',
|
||||
'AdminPanel/client/src/pages/AiIntegration/js/**',
|
||||
'AdminPanel/client/src/pages/AiIntegration/js/plugins.js',
|
||||
'AdminPanel/client/src/pages/AiIntegration/js/plugins-ui.js',
|
||||
'*.min.js',
|
||||
'package-lock.json',
|
||||
'npm-shrinkwrap.json',
|
||||
|
||||
Reference in New Issue
Block a user