Merge remote-tracking branch 'remotes/origin/release/v9.0.0' into develop
@ -23,7 +23,7 @@
|
||||
- axios 2.88.0 ([MIT](https://raw.githubusercontent.com/axios/axios/v1.x/LICENSE))
|
||||
- request-filtering-agent 1.0.5 ([MIT](https://raw.githubusercontent.com/azu/request-filtering-agent/master/LICENSE))
|
||||
- rhea 3.0.3 ([Apache-2.0](https://raw.githubusercontent.com/amqp/rhea/main/LICENSE))
|
||||
- uri-js 4.4.1 ([BSD-2-Clause](https://raw.githubusercontent.com/garycourt/uri-js/master/LICENSE))
|
||||
- uri-js-replace 1.0.1 ([MIT](https://github.com/andreinwald/uri-js-replace?tab=MIT-0-1-ov-file#readme))
|
||||
- win-ca 3.5.1 ([MIT](https://raw.githubusercontent.com/ukoloff/win-ca/master/LICENSE))
|
||||
- apicache 1.6.3 ([MIT](https://raw.githubusercontent.com/kwhitley/apicache/master/LICENSE))
|
||||
- body-parser 1.20.3 ([MIT](https://raw.githubusercontent.com/expressjs/body-parser/master/LICENSE))
|
||||
@ -32,7 +32,7 @@
|
||||
- co 4.6.0 ([MIT](https://raw.githubusercontent.com/tj/co/master/LICENSE))
|
||||
- config 3.3.12 ([MIT](https://raw.githubusercontent.com/node-config/node-config/master/LICENSE))
|
||||
- cron 1.5.0 ([MIT](https://raw.githubusercontent.com/kelektiv/node-cron/main/LICENSE))
|
||||
- dmdb 1.0.33801 ([none](https://www.npmjs.com/package/dmdb))
|
||||
- dmdb 1.0.36002 ([none](https://www.npmjs.com/package/dmdb))
|
||||
- ejs 3.1.10 ([Apache-2.0](https://raw.githubusercontent.com/mde/ejs/main/LICENSE))
|
||||
- exif-parser 0.1.12 ([MIT](https://raw.githubusercontent.com/bwindels/exif-parser/master/LICENSE.md))
|
||||
- express 4.21.2 ([MIT](https://raw.githubusercontent.com/expressjs/express/master/LICENSE))
|
||||
@ -43,7 +43,7 @@
|
||||
- mime 2.3.1 ([MIT](https://raw.githubusercontent.com/broofa/mime/main/LICENSE))
|
||||
- mime-db 1.53.0 ([MIT](https://raw.githubusercontent.com/jshttp/mime-db/master/LICENSE))
|
||||
- ms 2.1.3 ([MIT](https://raw.githubusercontent.com/vercel/ms/master/license.md))
|
||||
- mssql 9.1.1 ([MIT](https://raw.githubusercontent.com/tediousjs/node-mssql/master/LICENSE.md))
|
||||
- mssql 11.0.1 ([MIT](https://raw.githubusercontent.com/tediousjs/node-mssql/master/LICENSE.md))
|
||||
- multer 1.4.4 ([MIT](https://raw.githubusercontent.com/expressjs/multer/master/LICENSE))
|
||||
- multi-integer-range 5.2.0 ([MIT](https://raw.githubusercontent.com/smikitky/node-multi-integer-range/master/LICENSE))
|
||||
- multiparty 4.2.3 ([MIT](https://raw.githubusercontent.com/pillarjs/multiparty/master/LICENSE))
|
||||
|
||||
@ -5,12 +5,34 @@
|
||||
"port": "8125",
|
||||
"prefix": "ds."
|
||||
},
|
||||
"aiSettings": {
|
||||
"actions": {
|
||||
},
|
||||
"models": [
|
||||
],
|
||||
"providers": {
|
||||
},
|
||||
"version": 3,
|
||||
"timeout": "30s",
|
||||
"allowedCorsOrigins": [
|
||||
"https://onlyoffice.github.io", "https://onlyoffice-plugins.github.io"
|
||||
],
|
||||
"pluginDir" : "../branding/info/ai"
|
||||
},
|
||||
"log": {
|
||||
"filePath": "",
|
||||
"options": {
|
||||
"replaceConsole": true
|
||||
}
|
||||
},
|
||||
"runtimeConfig": {
|
||||
"filePath": "",
|
||||
"cache": {
|
||||
"stdTTL": 300,
|
||||
"checkperiod": 60,
|
||||
"useClones": false
|
||||
}
|
||||
},
|
||||
"queue": {
|
||||
"type": "rabbitmq",
|
||||
"visibilityTimeout": 300,
|
||||
@ -107,7 +129,8 @@
|
||||
"secretAccessKey": "",
|
||||
"sslEnabled": false,
|
||||
"s3ForcePathStyle": true,
|
||||
"externalHost": ""
|
||||
"externalHost": "",
|
||||
"useDirectStorageUrls": true
|
||||
},
|
||||
"persistentStorage": {
|
||||
},
|
||||
|
||||
@ -2,6 +2,12 @@
|
||||
"log": {
|
||||
"filePath": "../Common/config/log4js/development.json"
|
||||
},
|
||||
"runtimeConfig": {
|
||||
"filePath": "./../runtime.json"
|
||||
},
|
||||
"queue": {
|
||||
"visibilityTimeout": 900
|
||||
},
|
||||
"storage": {
|
||||
"fs": {
|
||||
"folderPath": "../App_Data"
|
||||
@ -63,10 +69,39 @@
|
||||
},
|
||||
"FileConverter": {
|
||||
"converter": {
|
||||
"maxDownloadBytes": 1073741824,
|
||||
"fontDir": "/usr/share/fonts",
|
||||
"presentationThemesDir": "../../sdkjs/slide/themes",
|
||||
"x2tPath": "../FileConverter/bin/x2t",
|
||||
"docbuilderPath": "../FileConverter/bin/docbuilder"
|
||||
"docbuilderPath": "../FileConverter/bin/docbuilder",
|
||||
"spawnOptions": {
|
||||
"env": {
|
||||
"X2T_MEMORY_LIMIT": "16GB"
|
||||
}
|
||||
},
|
||||
"inputLimits": [
|
||||
{
|
||||
"type": "docx;dotx;docm;dotm",
|
||||
"zip": {
|
||||
"uncompressed": "1GB",
|
||||
"template": "*.xml"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "xlsx;xltx;xlsm;xltm",
|
||||
"zip": {
|
||||
"uncompressed": "1GB",
|
||||
"template": "*.xml"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "pptx;ppsx;potx;pptm;ppsm;potm",
|
||||
"zip": {
|
||||
"uncompressed": "1GB",
|
||||
"template": "*.xml"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"SpellChecker": {
|
||||
|
||||
@ -2,6 +2,12 @@
|
||||
"log": {
|
||||
"filePath": "../Common/config/log4js/development.json"
|
||||
},
|
||||
"runtimeConfig": {
|
||||
"filePath": "./../runtime.json"
|
||||
},
|
||||
"queue": {
|
||||
"visibilityTimeout": 900
|
||||
},
|
||||
"storage": {
|
||||
"fs": {
|
||||
"folderPath": "../App_Data"
|
||||
@ -69,11 +75,40 @@
|
||||
},
|
||||
"FileConverter": {
|
||||
"converter": {
|
||||
"maxDownloadBytes": 1073741824,
|
||||
"fontDir": "",
|
||||
"presentationThemesDir": "../../sdkjs/slide/themes",
|
||||
"x2tPath": "../FileConverter/bin/x2t",
|
||||
"docbuilderPath": "../FileConverter/Bin/docbuilder",
|
||||
"errorfiles": "error"
|
||||
"spawnOptions": {
|
||||
"env": {
|
||||
"X2T_MEMORY_LIMIT": "16GB"
|
||||
}
|
||||
},
|
||||
"errorfiles": "error",
|
||||
"inputLimits": [
|
||||
{
|
||||
"type": "docx;dotx;docm;dotm",
|
||||
"zip": {
|
||||
"uncompressed": "1GB",
|
||||
"template": "*.xml"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "xlsx;xltx;xlsm;xltm",
|
||||
"zip": {
|
||||
"uncompressed": "1GB",
|
||||
"template": "*.xml"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "pptx;ppsx;potx;pptm;ppsm;potm",
|
||||
"zip": {
|
||||
"uncompressed": "1GB",
|
||||
"template": "*.xml"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"SpellChecker": {
|
||||
|
||||
@ -2,6 +2,12 @@
|
||||
"log": {
|
||||
"filePath": "../Common/config/log4js/development.json"
|
||||
},
|
||||
"runtimeConfig": {
|
||||
"filePath": "./../runtime.json"
|
||||
},
|
||||
"queue": {
|
||||
"visibilityTimeout": 900
|
||||
},
|
||||
"storage": {
|
||||
"fs": {
|
||||
"folderPath": "../App_Data"
|
||||
@ -69,11 +75,40 @@
|
||||
},
|
||||
"FileConverter": {
|
||||
"converter": {
|
||||
"maxDownloadBytes": 1073741824,
|
||||
"fontDir": "",
|
||||
"presentationThemesDir": "../../sdkjs/slide/themes",
|
||||
"x2tPath": "../FileConverter/Bin/x2t.exe",
|
||||
"docbuilderPath": "../FileConverter/Bin/docbuilder.exe",
|
||||
"errorfiles": "error"
|
||||
"spawnOptions": {
|
||||
"env": {
|
||||
"X2T_MEMORY_LIMIT": "16GB"
|
||||
}
|
||||
},
|
||||
"errorfiles": "error",
|
||||
"inputLimits": [
|
||||
{
|
||||
"type": "docx;dotx;docm;dotm",
|
||||
"zip": {
|
||||
"uncompressed": "1GB",
|
||||
"template": "*.xml"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "xlsx;xltx;xlsm;xltm",
|
||||
"zip": {
|
||||
"uncompressed": "1GB",
|
||||
"template": "*.xml"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "pptx;ppsx;potx;pptm;ppsm;potm",
|
||||
"zip": {
|
||||
"uncompressed": "1GB",
|
||||
"template": "*.xml"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"SpellChecker": {
|
||||
|
||||
@ -2,6 +2,12 @@
|
||||
"log": {
|
||||
"filePath": "/etc/onlyoffice/documentserver/log4js/production.json"
|
||||
},
|
||||
"aiSettings": {
|
||||
"pluginDir" : "/var/www/onlyoffice/documentserver/server/info/ai"
|
||||
},
|
||||
"runtimeConfig": {
|
||||
"filePath": "/var/www/onlyoffice/documentserver/../Data/runtime.json"
|
||||
},
|
||||
"storage": {
|
||||
"fs": {
|
||||
"folderPath": "/var/lib/onlyoffice/documentserver/App_Data/cache/files"
|
||||
|
||||
@ -2,6 +2,12 @@
|
||||
"log": {
|
||||
"filePath": "../../config/log4js/production.json"
|
||||
},
|
||||
"runtimeConfig": {
|
||||
"filePath": "./../runtime.json"
|
||||
},
|
||||
"aiSettings": {
|
||||
"pluginDir" : "../info/ai"
|
||||
},
|
||||
"storage": {
|
||||
"fs": {
|
||||
"folderPath": "../App_Data/cache/files"
|
||||
|
||||
22
Common/npm-shrinkwrap.json
generated
@ -5317,7 +5317,7 @@
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.8.3",
|
||||
@ -5423,7 +5423,7 @@
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||
},
|
||||
"dnscache": {
|
||||
"version": "1.0.2",
|
||||
@ -5484,7 +5484,7 @@
|
||||
"escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
||||
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="
|
||||
},
|
||||
"events": {
|
||||
"version": "3.3.0",
|
||||
@ -5814,11 +5814,6 @@
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
|
||||
},
|
||||
"querystringify": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
@ -5905,13 +5900,10 @@
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
|
||||
},
|
||||
"uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||
"requires": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
"uri-js-replace": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz",
|
||||
"integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g=="
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.5.10",
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
"nodemailer": "6.10.0",
|
||||
"request-filtering-agent": "1.0.5",
|
||||
"rhea": "3.0.3",
|
||||
"uri-js": "4.4.1",
|
||||
"uri-js-replace": "1.0.1",
|
||||
"win-ca": "3.5.1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,10 +32,12 @@
|
||||
|
||||
'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');
|
||||
|
||||
function Context(){
|
||||
this.logger = logger.getLogger('nodeJS');
|
||||
@ -51,6 +53,8 @@ Context.prototype.init = function(tenant, docId, userId, opt_shardKey, opt_WopiS
|
||||
this.config = null;
|
||||
this.secret = null;
|
||||
this.license = null;
|
||||
//cache
|
||||
this.taskResultCache = null;
|
||||
};
|
||||
Context.prototype.initDefault = function() {
|
||||
this.init(tenantManager.getDefautTenant(), constants.DEFAULT_DOC_ID, constants.DEFAULT_USER_ID, undefined);
|
||||
@ -85,7 +89,10 @@ Context.prototype.initFromPubSub = function(data) {
|
||||
this.init(ctx.tenant, ctx.docId, ctx.userId, ctx.shardKey, ctx.wopiSrc);
|
||||
};
|
||||
Context.prototype.initTenantCache = async function() {
|
||||
this.config = await tenantManager.getTenantConfig(this);
|
||||
const runtimeConfig = await runtimeConfigManager.getConfig(this);
|
||||
const tenantConfig = await tenantManager.getTenantConfig(this);
|
||||
this.config = utils.deepMergeObjects({}, runtimeConfig, tenantConfig);
|
||||
|
||||
//todo license and secret
|
||||
};
|
||||
|
||||
@ -122,6 +129,13 @@ Context.prototype.getCfg = function(property, defaultValue) {
|
||||
}
|
||||
return defaultValue;
|
||||
};
|
||||
/**
|
||||
* Get the full configuration by combining system config with context config
|
||||
* @returns {object} The merged configuration object
|
||||
*/
|
||||
Context.prototype.getFullCfg = function() {
|
||||
return utils.deepMergeObjects(config.util.toObject(), this.config);
|
||||
};
|
||||
|
||||
/**
|
||||
* Underlying get mechanism
|
||||
|
||||
135
Common/sources/runtimeConfigManager.js
Normal file
@ -0,0 +1,135 @@
|
||||
/*
|
||||
* (c) Copyright Ascensio System SIA 2010-2024
|
||||
*
|
||||
* This program is a free software product. You can redistribute it and/or
|
||||
* modify it under the terms of the GNU Affero General Public License (AGPL)
|
||||
* version 3 as published by the Free Software Foundation. In accordance with
|
||||
* Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
|
||||
* that Ascensio System SIA expressly excludes the warranty of non-infringement
|
||||
* of any third-party rights.
|
||||
*
|
||||
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
|
||||
* details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
||||
*
|
||||
* You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
|
||||
* street, Riga, Latvia, EU, LV-1050.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of the Program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU AGPL version 3.
|
||||
*
|
||||
* Pursuant to Section 7(b) of the License you must retain the original Product
|
||||
* logo when distributing the program. Pursuant to Section 7(e) we decline to
|
||||
* grant you any rights under trademark law for use of our trademarks.
|
||||
*
|
||||
* All the Product's GUI elements, including illustrations and icon sets, as
|
||||
* well as technical writing content are licensed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International. See the License
|
||||
* terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
*
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs/promises');
|
||||
const fsWatch = require('fs');
|
||||
const path = require('path');
|
||||
const config = require('config');
|
||||
const NodeCache = require("node-cache");
|
||||
const operationContext = require('./operationContext');
|
||||
const utils = require('./utils');
|
||||
|
||||
const cfgRuntimeConfig = config.get('runtimeConfig');
|
||||
const configFilePath = cfgRuntimeConfig.filePath;
|
||||
const configFileName = path.basename(configFilePath);
|
||||
|
||||
// Initialize cache with TTL and check for expired keys every minute
|
||||
const nodeCache = new NodeCache(cfgRuntimeConfig.cache);
|
||||
let isInitConfigWatcher = false;
|
||||
|
||||
/**
|
||||
* Get runtime configuration for the current context
|
||||
* @param {operationContext} ctx - Operation context
|
||||
* @returns {Object} Runtime configuration object
|
||||
*/
|
||||
async function getConfigFromFile(ctx) {
|
||||
if (!isInitConfigWatcher) {
|
||||
isInitConfigWatcher = true;
|
||||
initConfigWatcher(ctx);
|
||||
}
|
||||
try {
|
||||
const configData = await fs.readFile(configFilePath, 'utf8');
|
||||
return JSON.parse(configData);
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
ctx.logger.error('getConfigFromFile error: %s', err.stack);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached configuration for the current context
|
||||
* @param {operationContext} ctx - Operation context
|
||||
* @returns {Object} Cached configuration object
|
||||
*/
|
||||
async function getConfig(ctx) {
|
||||
let config = nodeCache.get(configFileName);
|
||||
if (undefined === config) {
|
||||
config = await getConfigFromFile(ctx);
|
||||
nodeCache.set(configFileName, config);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
/**
|
||||
* Save runtime configuration for the current context
|
||||
* @param {operationContext} ctx - Operation context
|
||||
* @param {Object} config - Configuration data to save
|
||||
* @returns {Object} Saved configuration object
|
||||
*/
|
||||
async function saveConfig(ctx, config) {
|
||||
await fs.mkdir(path.dirname(configFilePath), { recursive: true });
|
||||
let newConfig = await getConfig(ctx);
|
||||
newConfig = utils.deepMergeObjects(newConfig || {}, config);
|
||||
await fs.writeFile(configFilePath, JSON.stringify(newConfig, null, 2), 'utf8');
|
||||
nodeCache.set(configFileName, newConfig);
|
||||
return newConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle configuration file change events
|
||||
* @param {string} eventType - Type of file system event (change, rename)
|
||||
* @param {string} filename - Name of the file that triggered the event
|
||||
*/
|
||||
function handleConfigFileChange(eventType, filename) {
|
||||
try {
|
||||
if (configFileName === filename) {
|
||||
nodeCache.del(configFileName);
|
||||
operationContext.global.logger.info(`handleConfigFileChange configuration ${eventType}: ${configFileName}`);
|
||||
}
|
||||
} catch (err) {
|
||||
operationContext.global.logger.error(`handleConfigFileChange error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the configuration directory watcher
|
||||
*/
|
||||
function initConfigWatcher(ctx) {
|
||||
try {
|
||||
const configDir = path.dirname(configFilePath);
|
||||
const watcher = fsWatch.watch(configDir, handleConfigFileChange);
|
||||
watcher.on('error', (err) => {
|
||||
ctx.logger.error(`initConfigWatcher error: ${err.message}`);
|
||||
});
|
||||
ctx.logger.info(`initConfigWatcherWatching for changes in: ${configDir}`);
|
||||
} catch (watchErr) {
|
||||
ctx.logger.error(`initConfigWatcher error: ${watchErr.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getConfig,
|
||||
saveConfig
|
||||
};
|
||||
@ -4,12 +4,12 @@ const path = require('path');
|
||||
const { BlobServiceClient, StorageSharedKeyCredential, generateBlobSASQueryParameters, BlobSASPermissions } = require('@azure/storage-blob');
|
||||
const mime = require('mime');
|
||||
const config = require('config');
|
||||
const { Readable } = require('stream');
|
||||
const utils = require('../utils');
|
||||
const ms = require('ms');
|
||||
const commonDefines = require('../commondefines');
|
||||
|
||||
const cfgExpSessionAbsolute = ms(config.get('services.CoAuthoring.expire.sessionabsolute'));
|
||||
const cfgCacheStorage = config.get('storage');
|
||||
const MAX_DELETE_OBJECTS = 1000;
|
||||
const blobServiceClients = {};
|
||||
|
||||
@ -20,22 +20,22 @@ const blobServiceClients = {};
|
||||
* @returns {BlobServiceClient} The Azure Blob Service client
|
||||
*/
|
||||
function getBlobServiceClient(storageCfg) {
|
||||
const configKey = `${storageCfg.accessKeyId}_${storageCfg.bucketName}`;
|
||||
if (!blobServiceClients[configKey]) {
|
||||
const credential = new StorageSharedKeyCredential(
|
||||
storageCfg.accessKeyId,
|
||||
storageCfg.secretAccessKey
|
||||
);
|
||||
if (storageCfg.endpoint.includes(storageCfg.accessKeyId)) {
|
||||
blobServiceClients[configKey] = new BlobServiceClient(storageCfg.endpoint, credential);
|
||||
} else {
|
||||
const endpointUrl = new URL(storageCfg.endpoint.replace(/\/+$/, ''));
|
||||
blobServiceClients[configKey] = new BlobServiceClient(
|
||||
`${endpointUrl.protocol}//${storageCfg.accessKeyId}.${endpointUrl.host}`,
|
||||
credential);
|
||||
}
|
||||
const configKey = `${storageCfg.accessKeyId}_${storageCfg.bucketName}`;
|
||||
if (!blobServiceClients[configKey]) {
|
||||
const credential = new StorageSharedKeyCredential(
|
||||
storageCfg.accessKeyId,
|
||||
storageCfg.secretAccessKey
|
||||
);
|
||||
if (storageCfg.endpoint.includes(storageCfg.accessKeyId)) {
|
||||
blobServiceClients[configKey] = new BlobServiceClient(storageCfg.endpoint, credential);
|
||||
} else {
|
||||
const endpointUrl = new URL(storageCfg.endpoint.replace(/\/+$/, ''));
|
||||
blobServiceClients[configKey] = new BlobServiceClient(
|
||||
`${endpointUrl.protocol}//${storageCfg.accessKeyId}.${endpointUrl.host}`,
|
||||
credential);
|
||||
}
|
||||
return blobServiceClients[configKey];
|
||||
}
|
||||
return blobServiceClients[configKey];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -45,8 +45,8 @@ function getBlobServiceClient(storageCfg) {
|
||||
* @returns {ContainerClient} The Azure Container client
|
||||
*/
|
||||
function getContainerClient(storageCfg) {
|
||||
const blobServiceClient = getBlobServiceClient(storageCfg);
|
||||
return blobServiceClient.getContainerClient(storageCfg.bucketName);
|
||||
const blobServiceClient = getBlobServiceClient(storageCfg);
|
||||
return blobServiceClient.getContainerClient(storageCfg.bucketName);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -69,162 +69,157 @@ function getBlobClient(storageCfg, blobName) {
|
||||
* @returns {string} The full file path
|
||||
*/
|
||||
function getFilePath(storageCfg, strPath) {
|
||||
const storageFolderName = storageCfg.storageFolderName;
|
||||
return `${storageFolderName}/${strPath}`;
|
||||
const storageFolderName = storageCfg.storageFolderName;
|
||||
return `${storageFolderName}/${strPath}`;
|
||||
}
|
||||
|
||||
async function listObjectsExec(storageCfg, prefix, output = []) {
|
||||
const containerClient = getContainerClient(storageCfg);
|
||||
const storageFolderName = storageCfg.storageFolderName;
|
||||
const prefixWithFolder = storageFolderName ? `${storageFolderName}/${prefix}` : prefix;
|
||||
const containerClient = getContainerClient(storageCfg);
|
||||
const storageFolderName = storageCfg.storageFolderName;
|
||||
const prefixWithFolder = storageFolderName ? `${storageFolderName}/${prefix}` : prefix;
|
||||
|
||||
for await (const blob of containerClient.listBlobsFlat({ prefix: prefixWithFolder })) {
|
||||
const relativePath = storageFolderName ?
|
||||
blob.name.substring(storageFolderName.length + 1) : blob.name;
|
||||
output.push(relativePath);
|
||||
}
|
||||
return output;
|
||||
for await (const blob of containerClient.listBlobsFlat({prefix: prefixWithFolder})) {
|
||||
const relativePath = storageFolderName ?
|
||||
blob.name.substring(storageFolderName.length + 1) : blob.name;
|
||||
output.push(relativePath);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async function deleteObjectsHelp(storageCfg, aKeys) {
|
||||
const containerClient = getContainerClient(storageCfg);
|
||||
await Promise.all(
|
||||
aKeys.map(key => containerClient.deleteBlob(key.Key))
|
||||
);
|
||||
const containerClient = getContainerClient(storageCfg);
|
||||
await Promise.all(
|
||||
aKeys.map(key => containerClient.deleteBlob(key.Key))
|
||||
);
|
||||
}
|
||||
|
||||
async function headObject(storageCfg, strPath) {
|
||||
const blobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath));
|
||||
const properties = await blobClient.getProperties();
|
||||
return { ContentLength: properties.contentLength };
|
||||
const blobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath));
|
||||
const properties = await blobClient.getProperties();
|
||||
return {ContentLength: properties.contentLength};
|
||||
}
|
||||
|
||||
async function getObject(storageCfg, strPath) {
|
||||
const blobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath));
|
||||
const response = await blobClient.download();
|
||||
return await utils.stream2Buffer(response.readableStreamBody);
|
||||
const blobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath));
|
||||
const response = await blobClient.download();
|
||||
return await utils.stream2Buffer(response.readableStreamBody);
|
||||
}
|
||||
|
||||
async function createReadStream(storageCfg, strPath) {
|
||||
const blobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath));
|
||||
const response = await blobClient.download();
|
||||
return {
|
||||
contentLength: response.contentLength,
|
||||
readStream: response.readableStreamBody
|
||||
};
|
||||
const blobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath));
|
||||
const response = await blobClient.download();
|
||||
return {
|
||||
contentLength: response.contentLength,
|
||||
readStream: response.readableStreamBody
|
||||
};
|
||||
}
|
||||
|
||||
async function putObject(storageCfg, strPath, buffer, contentLength) {
|
||||
const blobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath));
|
||||
|
||||
const uploadOptions = {
|
||||
blobHTTPHeaders: {
|
||||
contentType: mime.getType(strPath),
|
||||
contentDisposition: utils.getContentDisposition(path.basename(strPath))
|
||||
}
|
||||
};
|
||||
if (buffer instanceof Buffer) {
|
||||
// Handle Buffer upload
|
||||
await blobClient.uploadData(buffer, uploadOptions);
|
||||
} else if (typeof buffer.pipe === 'function') {
|
||||
// Handle Stream upload
|
||||
await blobClient.uploadStream(buffer, undefined, undefined, uploadOptions);
|
||||
} else {
|
||||
throw new TypeError('Input must be Buffer or Readable stream');
|
||||
const blobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath));
|
||||
|
||||
const uploadOptions = {
|
||||
blobHTTPHeaders: {
|
||||
contentType: mime.getType(strPath),
|
||||
contentDisposition: utils.getContentDisposition(path.basename(strPath))
|
||||
}
|
||||
};
|
||||
if (buffer instanceof Buffer) {
|
||||
// Handle Buffer upload
|
||||
await blobClient.uploadData(buffer, uploadOptions);
|
||||
} else if (typeof buffer.pipe === 'function') {
|
||||
// Handle Stream upload
|
||||
await blobClient.uploadStream(buffer, undefined, undefined, uploadOptions);
|
||||
} else {
|
||||
throw new TypeError('Input must be Buffer or Readable stream');
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadObject(storageCfg, strPath, filePath) {
|
||||
const blockBlobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath));
|
||||
const uploadStream = fs.createReadStream(filePath);
|
||||
|
||||
await blockBlobClient.uploadStream(
|
||||
uploadStream,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
blobHTTPHeaders: {
|
||||
contentType: mime.getType(strPath),
|
||||
contentDisposition: utils.getContentDisposition(path.basename(strPath))
|
||||
}
|
||||
}
|
||||
);
|
||||
const blockBlobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath));
|
||||
const uploadStream = fs.createReadStream(filePath);
|
||||
|
||||
await blockBlobClient.uploadStream(
|
||||
uploadStream,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
blobHTTPHeaders: {
|
||||
contentType: mime.getType(strPath),
|
||||
contentDisposition: utils.getContentDisposition(path.basename(strPath))
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function copyObject(storageCfgSrc, storageCfgDst, sourceKey, destinationKey) {
|
||||
const sourceBlobClient = getBlobClient(storageCfgSrc, getFilePath(storageCfgSrc, sourceKey));
|
||||
const destBlobClient = getBlobClient(storageCfgDst, getFilePath(storageCfgDst, destinationKey));
|
||||
const sasToken = generateBlobSASQueryParameters({
|
||||
containerName: storageCfgSrc.bucketName,
|
||||
blobName: getFilePath(storageCfgSrc, sourceKey),
|
||||
permissions: BlobSASPermissions.parse("r"),
|
||||
startsOn: new Date(),
|
||||
expiresOn: new Date(Date.now() + 3600 * 1000)
|
||||
}, new StorageSharedKeyCredential(storageCfgSrc.accessKeyId, storageCfgSrc.secretAccessKey)).toString();
|
||||
const sourceBlobClient = getBlobClient(storageCfgSrc, getFilePath(storageCfgSrc, sourceKey));
|
||||
const destBlobClient = getBlobClient(storageCfgDst, getFilePath(storageCfgDst, destinationKey));
|
||||
const sasToken = generateBlobSASQueryParameters({
|
||||
containerName: storageCfgSrc.bucketName,
|
||||
blobName: getFilePath(storageCfgSrc, sourceKey),
|
||||
permissions: BlobSASPermissions.parse("r"),
|
||||
startsOn: new Date(),
|
||||
expiresOn: new Date(Date.now() + 3600 * 1000)
|
||||
}, new StorageSharedKeyCredential(storageCfgSrc.accessKeyId, storageCfgSrc.secretAccessKey)).toString();
|
||||
|
||||
await destBlobClient.syncCopyFromURL(`${sourceBlobClient.url}?${sasToken}`);
|
||||
await destBlobClient.syncCopyFromURL(`${sourceBlobClient.url}?${sasToken}`);
|
||||
}
|
||||
|
||||
async function listObjects(storageCfg, strPath) {
|
||||
return await listObjectsExec(storageCfg, strPath);
|
||||
return await listObjectsExec(storageCfg, strPath);
|
||||
}
|
||||
|
||||
async function deleteObject(storageCfg, strPath) {
|
||||
const blobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath));
|
||||
await blobClient.delete();
|
||||
const blobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath));
|
||||
await blobClient.delete();
|
||||
}
|
||||
|
||||
async function deleteObjects(storageCfg, strPaths) {
|
||||
let aKeys = strPaths.map(path => ({ Key: getFilePath(storageCfg, path) }));
|
||||
for (let i = 0; i < aKeys.length; i += MAX_DELETE_OBJECTS) {
|
||||
await deleteObjectsHelp(storageCfg, aKeys.slice(i, i + MAX_DELETE_OBJECTS));
|
||||
}
|
||||
let aKeys = strPaths.map(path => ({Key: getFilePath(storageCfg, path)}));
|
||||
for (let i = 0; i < aKeys.length; i += MAX_DELETE_OBJECTS) {
|
||||
await deleteObjectsHelp(storageCfg, aKeys.slice(i, i + MAX_DELETE_OBJECTS));
|
||||
}
|
||||
}
|
||||
|
||||
async function deletePath(storageCfg, strPath) {
|
||||
let list = await listObjects(storageCfg, strPath);
|
||||
await deleteObjects(storageCfg, list);
|
||||
let list = await listObjects(storageCfg, strPath);
|
||||
await deleteObjects(storageCfg, list);
|
||||
}
|
||||
|
||||
async function getSignedUrlWrapper(ctx, storageCfg, baseUrl, strPath, urlType, optFilename, opt_creationDate) {
|
||||
const storageUrlExpires = storageCfg.fs.urlExpires;
|
||||
let expires = (commonDefines.c_oAscUrlTypes.Session === urlType ? cfgExpSessionAbsolute / 1000 : storageUrlExpires) || 31536000;
|
||||
expires = Math.min(expires, 604800);
|
||||
|
||||
const userFriendlyName = optFilename ? optFilename.replace(/\//g, "%2f") : path.basename(strPath);
|
||||
const contentDisposition = utils.getContentDisposition(userFriendlyName, null, null);
|
||||
async function getDirectSignedUrl(ctx, storageCfg, baseUrl, strPath, urlType, optFilename, opt_creationDate) {
|
||||
const storageUrlExpires = storageCfg.fs.urlExpires;
|
||||
let expires = (commonDefines.c_oAscUrlTypes.Session === urlType ? cfgExpSessionAbsolute / 1000 : storageUrlExpires) || 31536000;
|
||||
expires = Math.min(expires, 604800);
|
||||
|
||||
const blobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath));
|
||||
const userFriendlyName = optFilename ? optFilename.replace(/\//g, "%2f") : path.basename(strPath);
|
||||
const contentDisposition = utils.getContentDisposition(userFriendlyName, null, null);
|
||||
|
||||
const sasOptions = {
|
||||
permissions: BlobSASPermissions.parse("r"),
|
||||
expiresOn: new Date(Date.now() + expires * 1000),
|
||||
contentDisposition,
|
||||
contentType: mime.getType(strPath)
|
||||
};
|
||||
const blobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath));
|
||||
|
||||
return await blobClient.generateSasUrl(sasOptions);
|
||||
const sasOptions = {
|
||||
permissions: BlobSASPermissions.parse("r"),
|
||||
expiresOn: new Date(Date.now() + expires * 1000),
|
||||
contentDisposition,
|
||||
contentType: mime.getType(strPath)
|
||||
};
|
||||
|
||||
return await blobClient.generateSasUrl(sasOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if static routs is needed for cacheFolder
|
||||
*
|
||||
* @returns {boolean} Always returns false for Azure Blob Storage
|
||||
*/
|
||||
function needServeStatic() {
|
||||
return false;
|
||||
return !cfgCacheStorage.useDirectStorageUrls;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
headObject,
|
||||
getObject,
|
||||
createReadStream,
|
||||
putObject,
|
||||
uploadObject,
|
||||
copyObject,
|
||||
listObjects,
|
||||
deleteObject,
|
||||
deletePath,
|
||||
getSignedUrl: getSignedUrlWrapper,
|
||||
needServeStatic
|
||||
headObject,
|
||||
getObject,
|
||||
createReadStream,
|
||||
putObject,
|
||||
uploadObject,
|
||||
copyObject,
|
||||
listObjects,
|
||||
deleteObject,
|
||||
deletePath,
|
||||
getDirectSignedUrl,
|
||||
needServeStatic
|
||||
};
|
||||
|
||||
@ -33,12 +33,21 @@
|
||||
'use strict';
|
||||
const os = require('os');
|
||||
const cluster = require('cluster');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
var config = require('config');
|
||||
var utils = require('../utils');
|
||||
|
||||
const commonDefines = require('../commondefines');
|
||||
const constants = require('../constants');
|
||||
const ms = require('ms');
|
||||
const cfgExpSessionAbsolute = ms(config.get('services.CoAuthoring.expire.sessionabsolute'));
|
||||
const cfgCacheStorage = config.get('storage');
|
||||
const cfgPersistentStorage = utils.deepMergeObjects({}, cfgCacheStorage, config.get('persistentStorage'));
|
||||
|
||||
// Stubs are needed until integrators pass these parameters to all requests
|
||||
let shardKeyCached;
|
||||
let wopiSrcCached;
|
||||
|
||||
const cacheStorage = require('./' + cfgCacheStorage.name);
|
||||
const persistentStorage = require('./' + cfgPersistentStorage.name);
|
||||
const tenantManager = require('../tenantManager');
|
||||
@ -50,10 +59,10 @@ function getStoragePath(ctx, strPath, opt_specialDir) {
|
||||
return opt_specialDir + '/' + tenantManager.getTenantPathPrefix(ctx) + strPath.replace(/\\/g, '/');
|
||||
}
|
||||
function getStorage(opt_specialDir) {
|
||||
return opt_specialDir ? persistentStorage : cacheStorage;
|
||||
return (opt_specialDir && opt_specialDir !== cfgCacheStorage.cacheFolderName) ? persistentStorage : cacheStorage;
|
||||
}
|
||||
function getStorageCfg(ctx, opt_specialDir) {
|
||||
return opt_specialDir ? cfgPersistentStorage : cfgCacheStorage;
|
||||
return (opt_specialDir && opt_specialDir !== cfgCacheStorage.cacheFolderName) ? cfgPersistentStorage : cfgCacheStorage;
|
||||
}
|
||||
function canCopyBetweenStorage(storageCfgSrc, storageCfgDst) {
|
||||
return storageCfgSrc.name === storageCfgDst.name && storageCfgSrc.endpoint === storageCfgDst.endpoint;
|
||||
@ -135,28 +144,65 @@ async function deletePath(ctx, strPath, opt_specialDir) {
|
||||
async function getSignedUrl(ctx, baseUrl, strPath, urlType, optFilename, opt_creationDate, opt_specialDir) {
|
||||
let storage = getStorage(opt_specialDir);
|
||||
let storageCfg = getStorageCfg(ctx, opt_specialDir);
|
||||
return await storage.getSignedUrl(ctx, storageCfg, baseUrl, getStoragePath(ctx, strPath, opt_specialDir), urlType, optFilename, opt_creationDate);
|
||||
let storagePath = getStoragePath(ctx, strPath, opt_specialDir);
|
||||
|
||||
if (storageCfg.useDirectStorageUrls && storage.getDirectSignedUrl) {
|
||||
return await storage.getDirectSignedUrl(ctx, storageCfg, baseUrl, storagePath, urlType, optFilename, opt_creationDate);
|
||||
} else {
|
||||
const storageSecretString = storageCfg.fs.secretString;
|
||||
const storageUrlExpires = storageCfg.fs.urlExpires;
|
||||
//use fixed bucket name because it hard-coded in nginx
|
||||
const bucketName = storageCfg.name === 'storage-fs' ? 'cache' : 'storage-cache';
|
||||
const storageFolderName = storageCfg.storageFolderName;
|
||||
//replace '/' with %2f before encodeURIComponent becase nginx determine %2f as '/' and get wrong system path
|
||||
const userFriendlyName = optFilename ? encodeURIComponent(optFilename.replace(/\//g, "%2f")) : path.basename(strPath);
|
||||
var uri = '/' + bucketName + '/' + storageFolderName + '/' + storagePath + '/' + userFriendlyName;
|
||||
//RFC 1123 does not allow underscores https://stackoverflow.com/questions/2180465/can-domain-name-subdomains-have-an-underscore-in-it
|
||||
var url = utils.checkBaseUrl(ctx, baseUrl, storageCfg).replace(/_/g, "%5f");
|
||||
url += uri;
|
||||
|
||||
var date = Date.now();
|
||||
let creationDate = opt_creationDate || date;
|
||||
let expiredAfter = (commonDefines.c_oAscUrlTypes.Session === urlType ? (cfgExpSessionAbsolute / 1000) : storageUrlExpires) || 31536000;
|
||||
//todo creationDate can be greater because mysql CURRENT_TIMESTAMP uses local time, not UTC
|
||||
var expires = creationDate + Math.ceil(Math.abs(date - creationDate) / expiredAfter) * expiredAfter;
|
||||
expires = Math.ceil(expires / 1000);
|
||||
expires += expiredAfter;
|
||||
var md5 = crypto.createHash('md5').update(expires + decodeURIComponent(uri) + storageSecretString).digest("base64");
|
||||
md5 = md5.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||
|
||||
url += '?md5=' + encodeURIComponent(md5);
|
||||
url += '&expires=' + encodeURIComponent(expires);
|
||||
if (ctx.shardKey) {
|
||||
shardKeyCached = ctx.shardKey;
|
||||
url += `&${constants.SHARD_KEY_API_NAME}=${encodeURIComponent(ctx.shardKey)}`;
|
||||
} else if (ctx.wopiSrc) {
|
||||
wopiSrcCached = ctx.wopiSrc;
|
||||
url += `&${constants.SHARD_KEY_WOPI_NAME}=${encodeURIComponent(ctx.wopiSrc)}`;
|
||||
} else if (process.env.DEFAULT_SHARD_KEY) {
|
||||
//Set DEFAULT_SHARD_KEY from environment as shardkey in case of integrator did not pass this param
|
||||
url += `&${constants.SHARD_KEY_API_NAME}=${encodeURIComponent(process.env.DEFAULT_SHARD_KEY)}`;
|
||||
} else if (shardKeyCached) {
|
||||
//Add stubs for shardkey params until integrators pass these parameters to all requests
|
||||
url += `&${constants.SHARD_KEY_API_NAME}=${encodeURIComponent(shardKeyCached)}`;
|
||||
} else if (wopiSrcCached) {
|
||||
url += `&${constants.SHARD_KEY_WOPI_NAME}=${encodeURIComponent(wopiSrcCached)}`;
|
||||
}
|
||||
url += '&filename=' + userFriendlyName;
|
||||
return url;
|
||||
}
|
||||
}
|
||||
async function getSignedUrls(ctx, baseUrl, strPath, urlType, opt_creationDate, opt_specialDir) {
|
||||
let storagePathSrc = getStoragePath(ctx, strPath, opt_specialDir);
|
||||
let storage = getStorage(opt_specialDir);
|
||||
let storageCfg = getStorageCfg(ctx, opt_specialDir);
|
||||
let list = await storage.listObjects(storageCfg, storagePathSrc, storageCfg);
|
||||
let urls = await Promise.all(list.map(function(curValue) {
|
||||
return storage.getSignedUrl(ctx, storageCfg, baseUrl, curValue, urlType, undefined, opt_creationDate);
|
||||
}));
|
||||
let list = await listObjects(ctx, strPath, opt_specialDir);
|
||||
let outputMap = {};
|
||||
for (let i = 0; i < list.length && i < urls.length; ++i) {
|
||||
outputMap[getRelativePath(storagePathSrc, list[i])] = urls[i];
|
||||
for (let i = 0; i < list.length; ++i) {
|
||||
outputMap[getRelativePath(strPath, list[i])] = await getSignedUrl(ctx, baseUrl, list[i], urlType, undefined, opt_creationDate, opt_specialDir);
|
||||
}
|
||||
return outputMap;
|
||||
}
|
||||
async function getSignedUrlsArrayByArray(ctx, baseUrl, list, urlType, opt_specialDir) {
|
||||
return await Promise.all(list.map(function (curValue) {
|
||||
let storage = getStorage(opt_specialDir);
|
||||
let storageCfg = getStorageCfg(ctx, opt_specialDir);
|
||||
let storagePathSrc = getStoragePath(ctx, curValue, opt_specialDir);
|
||||
return storage.getSignedUrl(ctx, storageCfg, baseUrl, storagePathSrc, urlType, undefined);
|
||||
return getSignedUrl(ctx, baseUrl, curValue, urlType, undefined, undefined, opt_specialDir);
|
||||
}));
|
||||
}
|
||||
async function getSignedUrlsByArray(ctx, baseUrl, list, optPath, urlType, opt_specialDir) {
|
||||
|
||||
@ -36,17 +36,7 @@ const { cp, rm, mkdir } = require('fs/promises');
|
||||
const { stat, readFile, writeFile } = require('fs/promises');
|
||||
var path = require('path');
|
||||
var utils = require("../utils");
|
||||
var crypto = require('crypto');
|
||||
const ms = require('ms');
|
||||
const config = require('config');
|
||||
const commonDefines = require('../commondefines');
|
||||
const constants = require('../constants');
|
||||
|
||||
const cfgExpSessionAbsolute = ms(config.get('services.CoAuthoring.expire.sessionabsolute'));
|
||||
|
||||
//Stubs are needed until integrators pass these parameters to all requests
|
||||
let shardKeyCached;
|
||||
let wopiSrcCached;
|
||||
|
||||
function getFilePath(storageCfg, strPath) {
|
||||
const storageFolderPath = storageCfg.fs.folderPath;
|
||||
@ -120,50 +110,6 @@ async function deletePath(storageCfg, strPath) {
|
||||
return rm(fsPath, {force: true, recursive: true, maxRetries: 3});
|
||||
}
|
||||
|
||||
async function getSignedUrl(ctx, storageCfg, baseUrl, strPath, urlType, optFilename, opt_creationDate) {
|
||||
const storageSecretString = storageCfg.fs.secretString;
|
||||
const storageUrlExpires = storageCfg.fs.urlExpires;
|
||||
const bucketName = storageCfg.bucketName;
|
||||
const storageFolderName = storageCfg.storageFolderName;
|
||||
//replace '/' with %2f before encodeURIComponent becase nginx determine %2f as '/' and get wrong system path
|
||||
const userFriendlyName = optFilename ? encodeURIComponent(optFilename.replace(/\//g, "%2f")) : path.basename(strPath);
|
||||
var uri = '/' + bucketName + '/' + storageFolderName + '/' + strPath + '/' + userFriendlyName;
|
||||
//RFC 1123 does not allow underscores https://stackoverflow.com/questions/2180465/can-domain-name-subdomains-have-an-underscore-in-it
|
||||
var url = utils.checkBaseUrl(ctx, baseUrl, storageCfg).replace(/_/g, "%5f");
|
||||
url += uri;
|
||||
|
||||
var date = Date.now();
|
||||
let creationDate = opt_creationDate || date;
|
||||
let expiredAfter = (commonDefines.c_oAscUrlTypes.Session === urlType ? (cfgExpSessionAbsolute / 1000) : storageUrlExpires) || 31536000;
|
||||
//todo creationDate can be greater because mysql CURRENT_TIMESTAMP uses local time, not UTC
|
||||
var expires = creationDate + Math.ceil(Math.abs(date - creationDate) / expiredAfter) * expiredAfter;
|
||||
expires = Math.ceil(expires / 1000);
|
||||
expires += expiredAfter;
|
||||
|
||||
var md5 = crypto.createHash('md5').update(expires + decodeURIComponent(uri) + storageSecretString).digest("base64");
|
||||
md5 = md5.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||
|
||||
url += '?md5=' + encodeURIComponent(md5);
|
||||
url += '&expires=' + encodeURIComponent(expires);
|
||||
if (ctx.shardKey) {
|
||||
shardKeyCached = ctx.shardKey;
|
||||
url += `&${constants.SHARD_KEY_API_NAME}=${encodeURIComponent(ctx.shardKey)}`;
|
||||
} else if (ctx.wopiSrc) {
|
||||
wopiSrcCached = ctx.wopiSrc;
|
||||
url += `&${constants.SHARD_KEY_WOPI_NAME}=${encodeURIComponent(ctx.wopiSrc)}`;
|
||||
} else if (process.env.DEFAULT_SHARD_KEY) {
|
||||
//Set DEFAULT_SHARD_KEY from environment as shardkey in case of integrator did not pass this param
|
||||
url += `&${constants.SHARD_KEY_API_NAME}=${encodeURIComponent(process.env.DEFAULT_SHARD_KEY)}`;
|
||||
} else if (shardKeyCached) {
|
||||
//Add stubs for shardkey params until integrators pass these parameters to all requests
|
||||
url += `&${constants.SHARD_KEY_API_NAME}=${encodeURIComponent(shardKeyCached)}`;
|
||||
} else if (wopiSrcCached) {
|
||||
url += `&${constants.SHARD_KEY_WOPI_NAME}=${encodeURIComponent(wopiSrcCached)}`;
|
||||
}
|
||||
url += '&filename=' + userFriendlyName;
|
||||
return url;
|
||||
}
|
||||
|
||||
function needServeStatic() {
|
||||
return true;
|
||||
}
|
||||
@ -178,6 +124,5 @@ module.exports = {
|
||||
listObjects,
|
||||
deleteObject,
|
||||
deletePath,
|
||||
getSignedUrl,
|
||||
needServeStatic
|
||||
};
|
||||
|
||||
@ -32,7 +32,6 @@
|
||||
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const url = require('url');
|
||||
const { Agent: HttpsAgent } = require('https');
|
||||
const { Agent: HttpAgent } = require('http');
|
||||
const path = require('path');
|
||||
@ -49,6 +48,7 @@ const commonDefines = require('../commondefines');
|
||||
|
||||
const cfgExpSessionAbsolute = ms(config.get('services.CoAuthoring.expire.sessionabsolute'));
|
||||
const cfgRequestDefaults = config.util.cloneDeep(config.get('services.CoAuthoring.requestDefaults'));
|
||||
const cfgCacheStorage = config.get('storage');
|
||||
|
||||
//This operation enables you to delete multiple objects from a bucket using a single HTTP request. You may specify up to 1000 keys.
|
||||
const MAX_DELETE_OBJECTS = 1000;
|
||||
@ -225,13 +225,15 @@ async function deletePath(storageCfg, strPath) {
|
||||
let list = await listObjects(storageCfg, strPath);
|
||||
await deleteObjects(storageCfg, list);
|
||||
}
|
||||
async function getSignedUrlWrapper(ctx, storageCfg, baseUrl, strPath, urlType, optFilename, opt_creationDate) {
|
||||
|
||||
async function getDirectSignedUrl(ctx, storageCfg, baseUrl, strPath, urlType, optFilename, opt_creationDate) {
|
||||
const storageUrlExpires = storageCfg.fs.urlExpires;
|
||||
let expires = (commonDefines.c_oAscUrlTypes.Session === urlType ? cfgExpSessionAbsolute / 1000 : storageUrlExpires) || 31536000;
|
||||
// Signature version 4 presigned URLs must have an expiration date less than one week in the future
|
||||
expires = Math.min(expires, 604800);
|
||||
let userFriendlyName = optFilename ? optFilename.replace(/\//g, "%2f") : path.basename(strPath);
|
||||
let contentDisposition = utils.getContentDisposition(userFriendlyName, null, null);
|
||||
|
||||
let userFriendlyName = optFilename ? optFilename.replace(/\//g, "%2f") : path.basename(strPath);
|
||||
let contentDisposition = utils.getContentDisposition(userFriendlyName, null, null);
|
||||
|
||||
const input = {
|
||||
Bucket: storageCfg.bucketName,
|
||||
@ -242,7 +244,7 @@ async function getSignedUrlWrapper(ctx, storageCfg, baseUrl, strPath, urlType, o
|
||||
//default Expires 900 seconds
|
||||
let options = {
|
||||
expiresIn: expires
|
||||
};
|
||||
};
|
||||
return await getSignedUrl(getS3Client(storageCfg), command, options);
|
||||
//extra query params cause SignatureDoesNotMatch
|
||||
//https://stackoverflow.com/questions/55503009/amazon-s3-signature-does-not-match-when-extra-query-params-ga-added-in-url
|
||||
@ -250,7 +252,7 @@ async function getSignedUrlWrapper(ctx, storageCfg, baseUrl, strPath, urlType, o
|
||||
}
|
||||
|
||||
function needServeStatic() {
|
||||
return false;
|
||||
return !cfgCacheStorage.useDirectStorageUrls;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@ -263,6 +265,6 @@ module.exports = {
|
||||
listObjects,
|
||||
deleteObject,
|
||||
deletePath,
|
||||
getSignedUrl: getSignedUrlWrapper,
|
||||
getDirectSignedUrl,
|
||||
needServeStatic
|
||||
};
|
||||
|
||||
@ -39,7 +39,7 @@ const license = require('./../../Common/sources/license');
|
||||
const constants = require('./../../Common/sources/constants');
|
||||
const commonDefines = require('./../../Common/sources/commondefines');
|
||||
const utils = require('./../../Common/sources/utils');
|
||||
const { readFile, readdir } = require('fs/promises');
|
||||
const { readFile, readdir, writeFile } = require('fs/promises');
|
||||
const path = require('path');
|
||||
|
||||
const cfgTenantsBaseDomain = config.get('tenants.baseDomain');
|
||||
@ -123,6 +123,24 @@ async function getTenantConfig(ctx) {
|
||||
}
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* Set tenant configuration for the current context
|
||||
* @param {operationContext} ctx - Operation context
|
||||
* @param {Object} config - Configuration data to save
|
||||
* @returns {Object} Saved configuration object
|
||||
*/
|
||||
async function setTenantConfig(ctx, config) {
|
||||
let newConfig = await getTenantConfig(ctx);
|
||||
if (isMultitenantMode(ctx) && !isDefaultTenant(ctx)) {
|
||||
newConfig = utils.deepMergeObjects(newConfig || {}, config);
|
||||
let tenantPath = utils.removeIllegalCharacters(ctx.tenant);
|
||||
let configPath = path.join(cfgTenantsBaseDir, tenantPath, cfgTenantsFilenameConfig);
|
||||
await writeFile(configPath, JSON.stringify(newConfig, null, 2), 'utf8');
|
||||
nodeCache.set(configPath, newConfig);
|
||||
}
|
||||
return newConfig;
|
||||
}
|
||||
|
||||
function getTenantSecret(ctx, type) {
|
||||
return co(function*() {
|
||||
let cfgTenant;
|
||||
@ -353,6 +371,11 @@ async function readLicenseTenant(ctx, licenseFile, baseVerifiedLicense) {
|
||||
res.usersExpire = Math.max(constants.LICENSE_EXPIRE_USERS_ONE_DAY, (oLicense['users_expire'] >> 0) *
|
||||
constants.LICENSE_EXPIRE_USERS_ONE_DAY);
|
||||
}
|
||||
|
||||
// Read grace_days setting from license file if available
|
||||
if (oLicense.hasOwnProperty('grace_days')) {
|
||||
res.graceDays = Math.max(0, oLicense['grace_days'] >> 0);
|
||||
}
|
||||
|
||||
const timeLimited = 0 !== (res.mode & c_LM.Limited);
|
||||
|
||||
@ -365,8 +388,8 @@ async function readLicenseTenant(ctx, licenseFile, baseVerifiedLicense) {
|
||||
res.type = c_LR.NotBefore;
|
||||
ctx.logger.warn('License: License not active before start_date:%s.', startDate.toISOString());
|
||||
} else if (timeLimited) {
|
||||
// 30 days after end license = limited mode with 20 Connections
|
||||
if (res.endDate.setUTCDate(res.endDate.getUTCDate() + 30) >= checkDate) {
|
||||
// Grace period after end license = limited mode with limited connections
|
||||
if (res.endDate.setUTCDate(res.endDate.getUTCDate() + res.graceDays) >= checkDate) {
|
||||
res.type = c_LR.SuccessLimit;
|
||||
res.connections = Math.min(res.connections, constants.LICENSE_CONNECTIONS);
|
||||
res.connectionsView = Math.min(res.connectionsView, constants.LICENSE_CONNECTIONS);
|
||||
@ -374,7 +397,7 @@ async function readLicenseTenant(ctx, licenseFile, baseVerifiedLicense) {
|
||||
res.usersViewCount = Math.min(res.usersViewCount, constants.LICENSE_USERS);
|
||||
let errStr = res.usersCount ? `${res.usersCount} unique users` : `${res.connections} concurrent connections`;
|
||||
ctx.logger.error(`License: License needs to be renewed.\nYour users have only ${errStr} ` +
|
||||
`available for document editing for the next 30 days.\nPlease renew the ` +
|
||||
`available for document editing for the next ${graceDays} days.\nPlease renew the ` +
|
||||
'license to restore the full access');
|
||||
} else {
|
||||
res.type = c_LR.ExpiredLimited;
|
||||
@ -423,6 +446,7 @@ exports.getTenantSecret = getTenantSecret;
|
||||
exports.getTenantLicense = getTenantLicense;
|
||||
exports.getServerLicense = getServerLicense;
|
||||
exports.setDefLicense = setDefLicense;
|
||||
exports.setTenantConfig = setTenantConfig;
|
||||
exports.isMultitenantMode = isMultitenantMode;
|
||||
exports.setMultitenantMode = setMultitenantMode;
|
||||
exports.isDefaultTenant = isDefaultTenant;
|
||||
|
||||
@ -45,7 +45,7 @@ const crypto = require('crypto');
|
||||
var url = require('url');
|
||||
var axios = require('../node_modules/axios/dist/node/axios.cjs');
|
||||
var co = require('co');
|
||||
var URI = require("uri-js");
|
||||
var URI = require("uri-js-replace");
|
||||
const escapeStringRegexp = require('escape-string-regexp');
|
||||
const ipaddr = require('ipaddr.js');
|
||||
const getDnsCache = require('dnscache');
|
||||
@ -494,6 +494,88 @@ async function postRequestPromise(ctx, uri, postData, postDataStream, postDataSi
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Performs an HTTP request with specified method and returns the raw response with a stream.
|
||||
* @param {operationContext.Context} ctx - The operation context.
|
||||
* @param {string} method - HTTP method (GET, POST, PUT, DELETE, etc).
|
||||
* @param {string} uri - The URL for the request.
|
||||
* @param {object} opt_headers - Optional headers to include in the request.
|
||||
* @param {*} opt_body - Optional request body data.
|
||||
* @param {object} opt_timeout - Optional timeout configuration.
|
||||
* @param {number} opt_limit - Optional limit on the size of the response.
|
||||
* @param {boolean} opt_filterPrivate - Optional flag to filter private requests.
|
||||
* @returns {Promise<{response: axios.AxiosResponse, stream: SizeLimitStream}>} - A promise that resolves to an object containing the raw Axios response and a SizeLimitStream.
|
||||
*/
|
||||
async function httpRequest(ctx, method, uri, opt_headers, opt_body, opt_timeout, opt_limit, opt_filterPrivate) {
|
||||
const tenTenantRequestDefaults = ctx.getCfg('services.CoAuthoring.requestDefaults', cfgRequestDefaults);
|
||||
uri = URI.serialize(URI.parse(uri));
|
||||
const options = config.util.cloneDeep(tenTenantRequestDefaults);
|
||||
|
||||
const httpsAgentOptions = { ...https.globalAgent.options, ...options};
|
||||
const httpAgentOptions = { ...http.globalAgent.options, ...options};
|
||||
changeOptionsForCompatibilityWithRequest(options, httpAgentOptions, httpsAgentOptions);
|
||||
|
||||
if (!addExternalRequestOptions(ctx, uri, opt_filterPrivate, options, httpAgentOptions, httpsAgentOptions)) {
|
||||
throw new Error('Block external request. See externalRequest config options');
|
||||
}
|
||||
|
||||
if (!options.httpsAgent || !options.httpAgent) {
|
||||
options.httpsAgent = new https.Agent(httpsAgentOptions);
|
||||
options.httpAgent = new http.Agent(httpAgentOptions);
|
||||
}
|
||||
|
||||
const requestHeaders = { ...options.headers };
|
||||
if (opt_headers) {
|
||||
Object.assign(requestHeaders, opt_headers);
|
||||
}
|
||||
|
||||
const axiosConfig = {
|
||||
...options,
|
||||
url: uri,
|
||||
method: method,
|
||||
headers: requestHeaders,
|
||||
responseType: 'stream',
|
||||
signal: opt_timeout?.wholeCycle && AbortSignal.timeout ? AbortSignal.timeout(ms(opt_timeout.wholeCycle)) : undefined,
|
||||
timeout: opt_timeout?.connectionAndInactivity ? ms(opt_timeout.connectionAndInactivity) : undefined,
|
||||
};
|
||||
|
||||
if (opt_body) {
|
||||
axiosConfig.data = opt_body;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios(axiosConfig);
|
||||
const { status, headers, data } = response;
|
||||
|
||||
const contentLength = headers['content-length'];
|
||||
if (opt_limit && contentLength && parseInt(contentLength) > opt_limit) {
|
||||
const error = new Error('EMSGSIZE: Error response: content-length:' + contentLength);
|
||||
error.code = 'EMSGSIZE';
|
||||
response.data.destroy(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const limitedStream = new SizeLimitStream(opt_limit || Number.MAX_VALUE);
|
||||
response.data.pipe(limitedStream);
|
||||
|
||||
return {
|
||||
response,
|
||||
stream: limitedStream
|
||||
};
|
||||
} catch (err) {
|
||||
if ('ERR_CANCELED' === err.code) {
|
||||
err.code = 'ETIMEDOUT';
|
||||
} else if (['ECONNABORTED', 'ECONNRESET'].includes(err.code)) {
|
||||
err.code = 'ESOCKETTIMEDOUT';
|
||||
}
|
||||
if (err.status) {
|
||||
err.statusCode = err.status;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
exports.httpRequest = httpRequest;
|
||||
exports.postRequestPromise = postRequestPromise;
|
||||
exports.downloadUrlPromise = downloadUrlPromise;
|
||||
exports.mapAscServerErrorToOldError = function(error) {
|
||||
@ -1118,6 +1200,7 @@ exports.convertLicenseInfoToFileParams = function(licenseInfo) {
|
||||
license.customer_id = licenseInfo.customerId;
|
||||
license.alias = licenseInfo.alias;
|
||||
license.multitenancy = licenseInfo.multitenancy;
|
||||
license.grace_days = licenseInfo.graceDays;
|
||||
return license;
|
||||
};
|
||||
exports.convertLicenseInfoToServerParams = function(licenseInfo) {
|
||||
@ -1276,3 +1359,5 @@ function deepMergeObjects(target, ...sources) {
|
||||
}
|
||||
exports.isObject = isObject;
|
||||
exports.deepMergeObjects = deepMergeObjects;
|
||||
exports.NodeCache = NodeCache;//todo via require
|
||||
|
||||
|
||||
1180
DocService/npm-shrinkwrap.json
generated
@ -9,6 +9,7 @@
|
||||
"prepare4shutdown": "sources/shutdown.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "8.17.1",
|
||||
"apicache": "1.6.3",
|
||||
"body-parser": "1.20.3",
|
||||
"bottleneck": "2.19.5",
|
||||
@ -16,7 +17,7 @@
|
||||
"co": "4.6.0",
|
||||
"config": "3.3.12",
|
||||
"cron": "1.5.0",
|
||||
"dmdb": "1.0.33801",
|
||||
"dmdb": "1.0.36002",
|
||||
"ejs": "3.1.10",
|
||||
"exif-parser": "0.1.12",
|
||||
"express": "4.21.2",
|
||||
@ -26,7 +27,7 @@
|
||||
"mime": "2.3.1",
|
||||
"mime-db": "1.53.0",
|
||||
"ms": "2.1.3",
|
||||
"mssql": "9.1.1",
|
||||
"mssql": "11.0.1",
|
||||
"multer": "1.4.4",
|
||||
"multi-integer-range": "5.2.0",
|
||||
"multiparty": "4.2.3",
|
||||
|
||||
@ -103,6 +103,7 @@ const queueService = require('./../../Common/sources/taskqueueRabbitMQ');
|
||||
const operationContext = require('./../../Common/sources/operationContext');
|
||||
const tenantManager = require('./../../Common/sources/tenantManager');
|
||||
const { notificationTypes, ...notificationService } = require('../../Common/sources/notificationService');
|
||||
const aiProxyHandler = require('./ai/aiProxyHandler');
|
||||
|
||||
const cfgEditorDataStorage = config.get('services.CoAuthoring.server.editorDataStorage');
|
||||
const cfgEditorStatStorage = config.get('services.CoAuthoring.server.editorStatStorage');
|
||||
@ -865,22 +866,60 @@ function* setForceSave(ctx, docId, forceSave, cmd, success, url) {
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {commonDefines.InputCommand} cmd - Information about the document conversion
|
||||
* @returns {string|null} The constructed document path if saveKey and outputPath exist, null otherwise
|
||||
*/
|
||||
function getForceSaveDocPath(cmd) {
|
||||
if (cmd) {
|
||||
const saveKey = cmd.getDocId() + cmd.getSaveKey();
|
||||
const outputPath = cmd.getOutputPath();
|
||||
if (saveKey && outputPath) {
|
||||
return saveKey + '/' + outputPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Checks if a force save cache exists and is valid for the provided conversion information
|
||||
* @param {operationContext.Context} ctx - The request context
|
||||
* @param {Object} convertInfo - Information about the document conversion
|
||||
* @returns {Promise<Object>} Object containing cache status information:
|
||||
* - hasCache {boolean} - Whether cache information exists
|
||||
* - hasValidCache {boolean} - Whether the cache is valid
|
||||
* - cmd {commonDefines.InputCommand|null} - Command object (if available)
|
||||
*/
|
||||
async function checkForceSaveCache(ctx, convertInfo) {
|
||||
let res = {hasCache: false, hasValidCache: false, cmd: null};
|
||||
if (convertInfo) {
|
||||
res.hasCache = true;
|
||||
let cmd = new commonDefines.InputCommand(convertInfo, true);
|
||||
const saveKey = cmd.getDocId() + cmd.getSaveKey();
|
||||
const outputPath = cmd.getOutputPath();
|
||||
if (saveKey && outputPath) {
|
||||
const savePathDoc = saveKey + '/' + outputPath;
|
||||
const metadata = await storage.headObject(ctx, savePathDoc);
|
||||
let docPath = getForceSaveDocPath(cmd);
|
||||
if (docPath) {
|
||||
const metadata = await storage.headObject(ctx, docPath);
|
||||
res.hasValidCache = !!metadata;
|
||||
res.cmd = cmd;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a signed URL for accessing a force-saved document
|
||||
* @param {operationContext.Context} ctx - The request context
|
||||
* @param {string} baseUrl - Base URL for the document
|
||||
* @param {Object} convertInfo - Information about the document conversion
|
||||
* @returns {Promise<string|null>} The signed URL for the force-saved document or null if path cannot be generated
|
||||
*/
|
||||
async function getForceSaveUrl(ctx, baseUrl, convertInfo) {
|
||||
let cmd = new commonDefines.InputCommand(convertInfo, true);
|
||||
let docPath = getForceSaveDocPath(cmd);
|
||||
if (docPath) {
|
||||
return await storage.getSignedUrl(ctx, baseUrl, docPath, commonDefines.c_oAscUrlTypes.Temporary);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function applyForceSaveCache(ctx, docId, forceSave, type, opt_userConnectionId, opt_userConnectionDocId,
|
||||
opt_responseKey, opt_formdata, opt_userId, opt_userIndex, opt_prevTime) {
|
||||
let res = {ok: false, notModified: false, inProgress: false, startedForceSave: null};
|
||||
@ -896,6 +935,7 @@ async function applyForceSaveCache(ctx, docId, forceSave, type, opt_userConnecti
|
||||
let cacheHasSameOptions = (commonDefines.c_oAscForceSaveTypes.Form === type && commonDefines.c_oAscForceSaveTypes.Form === forceSaveCached) ||
|
||||
(commonDefines.c_oAscForceSaveTypes.Form !== type && commonDefines.c_oAscForceSaveTypes.Form !== forceSaveCached);
|
||||
if (forceSaveCache.hasValidCache && cacheHasSameOptions) {
|
||||
//compare opt_prevTime because Internal command can be called by different users
|
||||
if (commonDefines.c_oAscForceSaveTypes.Internal === type && forceSave.time === opt_prevTime) {
|
||||
res.notModified = true;
|
||||
} else {
|
||||
@ -922,9 +962,21 @@ async function applyForceSaveCache(ctx, docId, forceSave, type, opt_userConnecti
|
||||
res.notModified = true;
|
||||
}
|
||||
} else if (!forceSave.started) {
|
||||
res.startedForceSave = await editorData.checkAndStartForceSave(ctx, docId);
|
||||
res.ok = !!res.startedForceSave;
|
||||
return res;
|
||||
const isTypeToSendFile = commonDefines.c_oAscForceSaveTypes.Command === type ||
|
||||
commonDefines.c_oAscForceSaveTypes.Button === type ||
|
||||
commonDefines.c_oAscForceSaveTypes.Timeout === type ||
|
||||
commonDefines.c_oAscForceSaveTypes.Form === type;
|
||||
if (isTypeToSendFile) {
|
||||
const selectRes = await taskResult.selectWithCache(ctx, docId);
|
||||
if (selectRes.length > 0 && !selectRes[0].callback) {
|
||||
ctx.logger.debug('applyForceSaveCache empty callback: %s', docId);
|
||||
res.notModified = true;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
res.startedForceSave = await editorData.checkAndStartForceSave(ctx, docId);
|
||||
res.ok = !!res.startedForceSave;
|
||||
return res;
|
||||
} else if (commonDefines.c_oAscForceSaveTypes.Form === type || commonDefines.c_oAscForceSaveTypes.Internal === type) {
|
||||
res.ok = true;
|
||||
res.inProgress = true;
|
||||
@ -948,6 +1000,10 @@ async function startForceSave(ctx, docId, type, opt_userdata, opt_formdata, opt_
|
||||
return !!JSON.parse(currentValue).encrypted;
|
||||
});
|
||||
if (!hasEncrypted) {
|
||||
let baseUrl = opt_baseUrl || "";
|
||||
if (opt_conn) {
|
||||
baseUrl = utils.getBaseUrlByConnection(ctx, opt_conn);
|
||||
}
|
||||
let forceSave = await editorData.getForceSave(ctx, docId);
|
||||
let forceSaveWithConnection = opt_conn && (commonDefines.c_oAscForceSaveTypes.Form === type ||
|
||||
(commonDefines.c_oAscForceSaveTypes.Button === type && tenForceSaveUsingButtonWithoutChanges));
|
||||
@ -957,10 +1013,8 @@ async function startForceSave(ctx, docId, type, opt_userdata, opt_formdata, opt_
|
||||
let newChangesLastDate = new Date();
|
||||
newChangesLastDate.setMilliseconds(0);//remove milliseconds avoid issues with MySQL datetime rounding
|
||||
let newChangesLastTime = newChangesLastDate.getTime();
|
||||
let baseUrl = opt_baseUrl || "";
|
||||
let changeInfo = opt_changeInfo;
|
||||
if (opt_conn) {
|
||||
baseUrl = utils.getBaseUrlByConnection(ctx, opt_conn);
|
||||
changeInfo = getExternalChangeInfo(opt_conn.user, newChangesLastTime, opt_conn.lang);
|
||||
}
|
||||
await editorData.setForceSave(ctx, docId, newChangesLastTime, 0, baseUrl, changeInfo, null);
|
||||
@ -973,6 +1027,9 @@ async function startForceSave(ctx, docId, type, opt_userdata, opt_formdata, opt_
|
||||
let selectRes = await taskResult.select(ctx, docId);
|
||||
if (selectRes.length > 0) {
|
||||
res.code = commonDefines.c_oAscServerCommandErrors.NotModified;
|
||||
if (forceSave) {
|
||||
res.url = await getForceSaveUrl(ctx, baseUrl, forceSave.convertInfo);
|
||||
}
|
||||
} else {
|
||||
res.code = commonDefines.c_oAscServerCommandErrors.DocumentIdError;
|
||||
}
|
||||
@ -1034,7 +1091,8 @@ let resetForceSaveAfterChanges = co.wrap(function*(ctx, docId, newChangesLastTim
|
||||
}
|
||||
}
|
||||
});
|
||||
let saveRelativeFromChanges = co.wrap(function*(ctx, conn, responseKey, data) {
|
||||
|
||||
async function saveRelativeFromChanges(ctx, conn, responseKey, data) {
|
||||
const tenTokenEnableBrowser = ctx.getCfg('services.CoAuthoring.token.enable.browser', cfgTokenEnableBrowser);
|
||||
|
||||
let docId = data.docId;
|
||||
@ -1042,7 +1100,7 @@ let saveRelativeFromChanges = co.wrap(function*(ctx, conn, responseKey, data) {
|
||||
let forceSaveRes;
|
||||
if (tenTokenEnableBrowser) {
|
||||
docId = null;
|
||||
let checkJwtRes = yield checkJwt(ctx, token, commonDefines.c_oAscSecretType.Browser);
|
||||
let checkJwtRes = await checkJwt(ctx, token, commonDefines.c_oAscSecretType.Browser);
|
||||
if (checkJwtRes.decoded) {
|
||||
docId = checkJwtRes.decoded.key;
|
||||
} else {
|
||||
@ -1051,13 +1109,13 @@ let saveRelativeFromChanges = co.wrap(function*(ctx, conn, responseKey, data) {
|
||||
}
|
||||
}
|
||||
if (!forceSaveRes) {
|
||||
forceSaveRes = yield startForceSave(ctx, docId, commonDefines.c_oAscForceSaveTypes.Internal, undefined, undefined, undefined, conn.user.id, conn.docId, undefined, responseKey,
|
||||
undefined, undefined, undefined, undefined, undefined, undefined, undefined, data.time);
|
||||
forceSaveRes = await startForceSave(ctx, docId, commonDefines.c_oAscForceSaveTypes.Internal, undefined, undefined, undefined, conn.user.id, conn.docId, undefined, responseKey,
|
||||
undefined, undefined, undefined, conn, undefined, undefined, undefined, data.time);
|
||||
}
|
||||
if (commonDefines.c_oAscServerCommandErrors.NoError !== forceSaveRes.code || forceSaveRes.inProgress) {
|
||||
sendDataRpc(ctx, conn, responseKey, forceSaveRes);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function startWopiRPC(ctx, docId, userId, userIdOriginal, data) {
|
||||
let res;
|
||||
@ -1072,6 +1130,11 @@ async function startWopiRPC(ctx, docId, userId, userIdOriginal, data) {
|
||||
switch (data.type) {
|
||||
case 'wopi_RenameFile':
|
||||
res = await wopiClient.renameFile(ctx, wopiParams, data.name);
|
||||
//publish for coeditors
|
||||
if (res?.Name) {
|
||||
const meta = {"title": res.Name};
|
||||
await publish(ctx, {type: commonDefines.c_oPublishType.meta, ctx: ctx, docId: docId, meta});
|
||||
}
|
||||
break;
|
||||
case 'wopi_RefreshFile':
|
||||
res = await wopiClient.refreshFile(ctx, wopiParams, row.baseurl);
|
||||
@ -1356,13 +1419,13 @@ function* cleanDocumentOnExit(ctx, docId, deleteChanges, opt_userIndex) {
|
||||
}
|
||||
yield unlockWopiDoc(ctx, docId, opt_userIndex);
|
||||
}
|
||||
function* cleanDocumentOnExitNoChanges(ctx, docId, opt_userId, opt_userIndex, opt_forceClose) {
|
||||
function* cleanDocumentOnExitNoChanges(ctx, docId, opt_userId, opt_userIndex, opt_forceClose, opt_deleteChanges) {
|
||||
var userAction = opt_userId ? new commonDefines.OutputAction(commonDefines.c_oAscUserAction.Out, opt_userId) : null;
|
||||
// We send that everyone is gone and there are no changes (to set the status on the server about the end of editing)
|
||||
yield sendStatusDocument(ctx, docId, c_oAscChangeBase.No, userAction, opt_userIndex, undefined, undefined, undefined, opt_forceClose);
|
||||
//if the user entered the document, the connection was broken, all information was deleted on the server,
|
||||
//when the connection is restored, the userIndex will be saved and it will match the userIndex of the next user
|
||||
yield* cleanDocumentOnExit(ctx, docId, false, opt_userIndex);
|
||||
yield* cleanDocumentOnExit(ctx, docId, opt_deleteChanges || false, opt_userIndex);
|
||||
}
|
||||
|
||||
function createSaveTimer(ctx, docId, opt_userId, opt_userIndex, opt_userLcid, opt_queue, opt_noDelay, opt_initShardKey) {
|
||||
@ -3438,9 +3501,10 @@ exports.install = function(server, callbackFunction) {
|
||||
}
|
||||
|
||||
let [licenseInfo] = yield tenantManager.getTenantLicense(ctx);
|
||||
|
||||
let pluginSettings = yield aiProxyHandler.getPluginSettingsForInterface(ctx);
|
||||
sendData(ctx, conn, {
|
||||
type: 'license', license: {
|
||||
type: 'license',
|
||||
license: {
|
||||
type: licenseInfo.type,
|
||||
light: false,//todo remove in sdk
|
||||
mode: licenseInfo.mode,
|
||||
@ -3453,7 +3517,8 @@ exports.install = function(server, callbackFunction) {
|
||||
branding: licenseInfo.branding,
|
||||
customization: licenseInfo.customization,
|
||||
advancedApi: licenseInfo.advancedApi
|
||||
}
|
||||
},
|
||||
aiPluginSettings: pluginSettings
|
||||
});
|
||||
ctx.logger.info('_checkLicense end');
|
||||
} catch (err) {
|
||||
@ -3934,6 +3999,8 @@ exports.install = function(server, callbackFunction) {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
void aiProxyHandler.getPluginSettings(operationContext.global);
|
||||
};
|
||||
exports.setLicenseInfo = async function(globalCtx, data, original) {
|
||||
tenantManager.setDefLicense(data, original);
|
||||
|
||||
269
DocService/sources/ai/aiEngineWrapper.js
Normal file
@ -0,0 +1,269 @@
|
||||
/*
|
||||
* (c) Copyright Ascensio System SIA 2010-2024
|
||||
*
|
||||
* This program is a free software product. You can redistribute it and/or
|
||||
* modify it under the terms of the GNU Affero General Public License (AGPL)
|
||||
* version 3 as published by the Free Software Foundation. In accordance with
|
||||
* Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
|
||||
* that Ascensio System SIA expressly excludes the warranty of non-infringement
|
||||
* of any third-party rights.
|
||||
*
|
||||
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
|
||||
* details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
||||
*
|
||||
* You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
|
||||
* street, Riga, Latvia, EU, LV-1050.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of the Program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU AGPL version 3.
|
||||
*
|
||||
* Pursuant to Section 7(b) of the License you must retain the original Product
|
||||
* logo when distributing the program. Pursuant to Section 7(e) we decline to
|
||||
* grant you any rights under trademark law for use of our trademarks.
|
||||
*
|
||||
* All the Product's GUI elements, including illustrations and icon sets, as
|
||||
* well as technical writing content are licensed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International. See the License
|
||||
* terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
*
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { buffer } = require('node:stream/consumers');
|
||||
const config = require('config');
|
||||
const utils = require('../../../Common/sources/utils');
|
||||
const operationContext = require('../../../Common/sources/operationContext');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
// Configuration constants
|
||||
const cfgAiApiTimeout = config.get('aiSettings.timeout');
|
||||
const cfgAiPluginDir = config.get('aiSettings.pluginDir');
|
||||
|
||||
const engineScriptsDir = path.join(cfgAiPluginDir, 'scripts/engine');
|
||||
|
||||
function setCtx(ctx) {
|
||||
sandbox.ctx = ctx;
|
||||
console.log = ctx.logger.debug.bind(ctx.logger);//todo make default in logger
|
||||
console.error = ctx.logger.error.bind(ctx.logger);
|
||||
}
|
||||
|
||||
// Set up the environment for the client-side engine.js
|
||||
const sandbox = {
|
||||
ctx: null,
|
||||
window: {
|
||||
AI: {
|
||||
TmpProviderForModels: null,
|
||||
Providers: {},
|
||||
InternalProviders: [],
|
||||
_getHeaders: function() {return {};},
|
||||
_getEndpointUrl: function() {return "";},
|
||||
serializeProviders: function() {return [];},
|
||||
ActionsGetSorted: function() {return [];},
|
||||
getModels: function() {return [];},
|
||||
onLoadInternalProviders: function() {},
|
||||
Storage: {
|
||||
serializeModels: function() {return [];}
|
||||
},
|
||||
CapabilitiesUI: {}
|
||||
}
|
||||
},
|
||||
Asc: {
|
||||
plugin: {
|
||||
tr: function(text) {
|
||||
// Just return the original text in the stub
|
||||
return text;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Implementation of fetch that delegates to utils.httpRequest
|
||||
*
|
||||
* @param {string} url - The URL to fetch
|
||||
* @param {Object} options - Fetch options (method, headers, body)
|
||||
* @returns {Promise<Object>} - A promise that resolves to a response-like object
|
||||
*/
|
||||
fetch: function(url, options = {}) {
|
||||
const ctx = sandbox.ctx;
|
||||
const method = options.method || 'GET';
|
||||
|
||||
// Configure timeout options for the request
|
||||
const timeoutOptions = {
|
||||
connectionAndInactivity: cfgAiApiTimeout,
|
||||
wholeCycle: cfgAiApiTimeout
|
||||
};
|
||||
ctx.logger.debug("engineWrapper fetch", url, options);
|
||||
return utils.httpRequest(
|
||||
sandbox.ctx,
|
||||
method,
|
||||
url,
|
||||
options.headers || {},
|
||||
options.body || null,
|
||||
timeoutOptions,
|
||||
null,
|
||||
false
|
||||
)
|
||||
.then(async (result) => {
|
||||
const responseBuffer = await buffer(result.stream);
|
||||
const text = responseBuffer.toString('utf8');
|
||||
|
||||
return {
|
||||
status: result.response.status,
|
||||
statusText: result.response.statusText,
|
||||
ok: result.response.status >= 200 && result.response.status < 300,
|
||||
headers: result.response.headers,
|
||||
text: () => Promise.resolve(text),
|
||||
json: () => Promise.resolve(JSON.parse(text)),
|
||||
arrayBuffer: () => Promise.resolve(responseBuffer.buffer)
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize minimal AI object with required functionality
|
||||
sandbox.AI = sandbox.window.AI;
|
||||
setCtx(operationContext.global);
|
||||
|
||||
/**
|
||||
* Simple loadInternalProviders implementation
|
||||
*/
|
||||
function loadInternalProviders() {
|
||||
// Add simple provider loading logic
|
||||
const enginePath = path.join(engineScriptsDir, 'providers/internal');
|
||||
|
||||
// Check if the providers directory exists before trying to read it
|
||||
if (!fs.existsSync(enginePath)) {
|
||||
sandbox.ctx.logger.warn('Internal providers directory not found:', enginePath);
|
||||
sandbox.AI.onLoadInternalProviders();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read providers directory
|
||||
const files = fs.readdirSync(enginePath);
|
||||
|
||||
// Load each provider
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.js')) {
|
||||
const providerPath = path.join(enginePath, file);
|
||||
const providerCode = fs.readFileSync(providerPath, 'utf8');
|
||||
|
||||
try {
|
||||
//sandbox.ctx.logger.debug(`Loading provider ${file}:`);
|
||||
let content = "(function(){\n" + providerCode + "\nreturn new Provider();})();";
|
||||
// Execute provider code in sandbox
|
||||
let provider = vm.runInNewContext(content, sandbox, {
|
||||
filename: file,
|
||||
timeout: 5000
|
||||
});
|
||||
sandbox.AI.InternalProviders.push(provider);
|
||||
} catch (error) {
|
||||
sandbox.ctx.logger.error(`Error loading provider ${file}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sandbox.AI.onLoadInternalProviders();
|
||||
} catch (error) {
|
||||
sandbox.ctx.logger.error('Error loading internal providers:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load engine.js
|
||||
let engineCode = '';
|
||||
|
||||
try {
|
||||
engineCode += fs.readFileSync(path.join(engineScriptsDir, 'storage.js'), 'utf8');
|
||||
engineCode += fs.readFileSync(path.join(engineScriptsDir, 'local_storage.js'), 'utf8');
|
||||
engineCode += fs.readFileSync(path.join(engineScriptsDir, 'providers/base.js'), 'utf8');
|
||||
engineCode += fs.readFileSync(path.join(engineScriptsDir, 'providers/provider.js'), 'utf8');
|
||||
engineCode += fs.readFileSync(path.join(engineScriptsDir, 'engine.js'), 'utf8');
|
||||
} catch (error) {
|
||||
sandbox.ctx.logger.warn('Error reading engine script files:', error);
|
||||
}
|
||||
|
||||
// Run engine code if available
|
||||
if (engineCode) {
|
||||
try {
|
||||
vm.runInNewContext(engineCode, sandbox);
|
||||
} catch (error) {
|
||||
sandbox.ctx.logger.error('Error executing engine scripts:', error);
|
||||
}
|
||||
}
|
||||
|
||||
//start from engine/register.js
|
||||
(function() {
|
||||
const AI = sandbox.AI;
|
||||
const Asc = sandbox.Asc;
|
||||
|
||||
AI.ActionType = {
|
||||
Chat : "Chat",
|
||||
Summarization : "Summarization",
|
||||
Translation : "Translation",
|
||||
TextAnalyze : "TextAnalyze",
|
||||
ImageGeneration : "ImageGeneration",
|
||||
OCR : "OCR",
|
||||
Vision : "Vision"
|
||||
};
|
||||
|
||||
AI.Actions = {};
|
||||
|
||||
function ActionUI(name, icon, modelId, capabilities) {
|
||||
this.name = name || "";
|
||||
this.icon = icon || "";
|
||||
this.model = modelId || "";
|
||||
this.capabilities = (capabilities === undefined) ? AI.CapabilitiesUI.Chat : capabilities;
|
||||
}
|
||||
|
||||
AI.Actions[AI.ActionType.Chat] = new ActionUI("Chatbot", "ask-ai");
|
||||
AI.Actions[AI.ActionType.Summarization] = new ActionUI("Summarization", "summarization");
|
||||
AI.Actions[AI.ActionType.Translation] = new ActionUI("Translation", "translation");
|
||||
AI.Actions[AI.ActionType.TextAnalyze] = new ActionUI("Text analysis", "text-analysis-ai");
|
||||
AI.Actions[AI.ActionType.ImageGeneration] = new ActionUI("Image generation", "image-ai", "", AI.CapabilitiesUI.Image);
|
||||
AI.Actions[AI.ActionType.OCR] = new ActionUI("OCR", "text-analysis-ai", "", AI.CapabilitiesUI.Vision);
|
||||
AI.Actions[AI.ActionType.Vision] = new ActionUI("Vision", "vision-ai", "", AI.CapabilitiesUI.Vision);
|
||||
|
||||
AI.ActionsGetKeys = function()
|
||||
{
|
||||
return [
|
||||
AI.ActionType.Chat,
|
||||
AI.ActionType.Summarization,
|
||||
AI.ActionType.Translation,
|
||||
AI.ActionType.TextAnalyze,
|
||||
AI.ActionType.ImageGeneration,
|
||||
AI.ActionType.OCR,
|
||||
AI.ActionType.Vision
|
||||
];
|
||||
};
|
||||
|
||||
AI.ActionsGetSorted = function()
|
||||
{
|
||||
let keys = AI.ActionsGetKeys();
|
||||
let count = keys.length;
|
||||
let actions = new Array(count);
|
||||
for (let i = 0; i < count; i++)
|
||||
{
|
||||
let src = AI.Actions[keys[i]];
|
||||
actions[i] = {
|
||||
id : keys[i],
|
||||
name : Asc.plugin.tr(src.name),
|
||||
icon : src.icon,
|
||||
model : src.model,
|
||||
capabilities : src.capabilities
|
||||
}
|
||||
}
|
||||
return actions;
|
||||
};
|
||||
//end from engine/register.js
|
||||
})();
|
||||
|
||||
sandbox.AI.loadInternalProviders = loadInternalProviders;
|
||||
loadInternalProviders();
|
||||
|
||||
exports.setCtx = setCtx;
|
||||
exports.AI = sandbox.AI;
|
||||
470
DocService/sources/ai/aiProxyHandler.js
Normal file
@ -0,0 +1,470 @@
|
||||
/*
|
||||
* (c) Copyright Ascensio System SIA 2010-2024
|
||||
*
|
||||
* This program is a free software product. You can redistribute it and/or
|
||||
* modify it under the terms of the GNU Affero General Public License (AGPL)
|
||||
* version 3 as published by the Free Software Foundation. In accordance with
|
||||
* Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
|
||||
* that Ascensio System SIA expressly excludes the warranty of non-infringement
|
||||
* of any third-party rights.
|
||||
*
|
||||
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
|
||||
* details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
||||
*
|
||||
* You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
|
||||
* street, Riga, Latvia, EU, LV-1050.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of the Program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU AGPL version 3.
|
||||
*
|
||||
* Pursuant to Section 7(b) of the License you must retain the original Product
|
||||
* logo when distributing the program. Pursuant to Section 7(e) we decline to
|
||||
* grant you any rights under trademark law for use of our trademarks.
|
||||
*
|
||||
* All the Product's GUI elements, including illustrations and icon sets, as
|
||||
* well as technical writing content are licensed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International. See the License
|
||||
* terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
*
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { pipeline } = require('stream/promises');
|
||||
const { URL } = require('url');
|
||||
const config = require('config');
|
||||
const utils = require('./../../../Common/sources/utils');
|
||||
const operationContext = require('./../../../Common/sources/operationContext');
|
||||
const commonDefines = require('./../../../Common/sources/commondefines');
|
||||
const docsCoServer = require('./../DocsCoServer');
|
||||
const statsDClient = require('./../../../Common/sources/statsdclient');
|
||||
|
||||
// Import the new aiEngine module
|
||||
const aiEngine = require('./aiEngineWrapper');
|
||||
|
||||
const cfgAiApiAllowedOrigins = config.get('aiSettings.allowedCorsOrigins');
|
||||
const cfgAiApiTimeout = config.get('aiSettings.timeout');
|
||||
const cfgTokenEnableBrowser = config.get('services.CoAuthoring.token.enable.browser');
|
||||
const cfgAiSettings = config.get('aiSettings');
|
||||
|
||||
const AI = aiEngine.AI;
|
||||
const clientStatsD = statsDClient.getClient();
|
||||
/**
|
||||
* Helper function to set CORS headers if the request origin is allowed
|
||||
*
|
||||
* @param {object} req - Express request object
|
||||
* @param {object} res - Express response object
|
||||
* @param {operationContext.Context} ctx - Operation context for logging
|
||||
* @param {boolean} handleOptions - Whether to handle OPTIONS requests (default: true)
|
||||
* @returns {boolean} - True if this was an OPTIONS request that was handled
|
||||
*/
|
||||
function handleCorsHeaders(req, res, ctx, handleOptions = true) {
|
||||
const requestOrigin = req.headers.origin;
|
||||
|
||||
const tenAiApiAllowedOrigins = ctx.getCfg('aiSettings.allowedCorsOrigins', cfgAiApiAllowedOrigins);
|
||||
|
||||
// If no origin in request or allowed origins list is empty, do nothing
|
||||
if (!requestOrigin || tenAiApiAllowedOrigins.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the origin is in our allowed list
|
||||
if (tenAiApiAllowedOrigins.includes(requestOrigin)) {
|
||||
res.setHeader('Access-Control-Allow-Origin', requestOrigin);
|
||||
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||
res.setHeader('Vary', 'Origin'); // Important when using dynamic origin
|
||||
|
||||
// If debug logging is available
|
||||
if (ctx && ctx.logger) {
|
||||
ctx.logger.debug('CORS headers set for origin: %s (matched allowed list)', requestOrigin);
|
||||
}
|
||||
|
||||
// Handle preflight OPTIONS requests if requested
|
||||
if (handleOptions && req.method === 'OPTIONS') {
|
||||
res.setHeader('Access-Control-Allow-Methods', 'DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT');
|
||||
// Allow all headers with wildcard
|
||||
res.setHeader('Access-Control-Allow-Headers', '*');
|
||||
|
||||
// For preflight request, we should also set non-CORS headers to match the API
|
||||
res.setHeader('Allow', 'OPTIONS, HEAD, GET, POST, PUT, DELETE, PATCH');
|
||||
res.setHeader('Content-Length', '0');
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
|
||||
// Return 204 which is standard for OPTIONS preflight
|
||||
res.sendStatus(204); // No Content response for OPTIONS
|
||||
return true; // Signal that we handled an OPTIONS request
|
||||
}
|
||||
}
|
||||
|
||||
return false; // Not an OPTIONS request or origin not allowed
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends API key to the request URI if the provider passes it as a query parameter.
|
||||
*
|
||||
* @param {operationContext.Context} ctx - The operation context for logging.
|
||||
* @param {object} provider - The AI provider configuration.
|
||||
* @param {string} uri - The original request URI.
|
||||
* @returns {string} The updated URI with API key as a query parameter, if applicable.
|
||||
*/
|
||||
function appendApiKeyToQuery(ctx, provider, uri) {
|
||||
const urlWithKey = AI._getEndpointUrl(provider, AI.Endpoints.Types.v1.Models);
|
||||
|
||||
// To check if the key is part of the query, we get the URL without the key.
|
||||
const originalKey = provider.key;
|
||||
provider.key = undefined;
|
||||
const urlWithoutKey = AI._getEndpointUrl(provider, AI.Endpoints.Types.v1.Models);
|
||||
provider.key = originalKey; // Restore the key on the provider object.
|
||||
|
||||
if (urlWithKey !== urlWithoutKey) {
|
||||
try {
|
||||
const parsedUrlWithKey = new URL(urlWithKey);
|
||||
if (parsedUrlWithKey.search) {
|
||||
const parsedUri = new URL(uri);
|
||||
for (const [key, value] of parsedUrlWithKey.searchParams) {
|
||||
if (originalKey === value) {
|
||||
parsedUri.searchParams.set(key, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
ctx.logger.debug(`appendApiKeyToQuery: Appended API key to URI for provider ${provider.name}`);
|
||||
return parsedUri.toString();
|
||||
}
|
||||
} catch (error) {
|
||||
ctx.logger.error(`appendApiKeyToQuery: Failed to parse provider URL for ${provider.name}: ${urlWithKey}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes an HTTP request to an AI API endpoint using the provided request and response objects
|
||||
*
|
||||
* @param {object} req - Express request object
|
||||
* @param {object} res - Express response object
|
||||
* @returns {Promise<void>} - Promise resolving when the request is complete
|
||||
*/
|
||||
async function proxyRequest(req, res) {
|
||||
// Create operation context for logging
|
||||
const ctx = new operationContext.Context();
|
||||
ctx.initFromRequest(req);
|
||||
const startDate = new Date();
|
||||
let success = false;
|
||||
|
||||
try {
|
||||
ctx.logger.info('Start proxyRequest');
|
||||
await ctx.initTenantCache();
|
||||
const tenTokenEnableBrowser = ctx.getCfg('services.CoAuthoring.token.enable.browser', cfgTokenEnableBrowser);
|
||||
const tenAiApiTimeout = ctx.getCfg('aiSettings.timeout', cfgAiApiTimeout);
|
||||
const tenAiApi = ctx.getCfg('aiSettings', cfgAiSettings);
|
||||
|
||||
// 1. Handle CORS preflight (OPTIONS) requests if necessary
|
||||
if (handleCorsHeaders(req, res, ctx) === true) {
|
||||
return; // OPTIONS request handled, stop further processing
|
||||
}
|
||||
|
||||
if (tenTokenEnableBrowser) {
|
||||
let checkJwtRes = await docsCoServer.checkJwtHeader(ctx, req, 'Authorization', 'Bearer ', commonDefines.c_oAscSecretType.Session);
|
||||
if (!checkJwtRes || checkJwtRes.err) {
|
||||
ctx.logger.error('proxyRequest: checkJwtHeader error: %s', checkJwtRes?.err);
|
||||
res.status(403).json({
|
||||
"error": {
|
||||
"message": "proxyRequest: checkJwtHeader error",
|
||||
"code": "403"
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!tenAiApi?.providers) {
|
||||
ctx.logger.error('proxyRequest: No providers configured');
|
||||
res.status(403).json({
|
||||
"error": {
|
||||
"message": "proxyRequest: No providers configured",
|
||||
"code": "403"
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let body = JSON.parse(req.body);
|
||||
let uri = body.target;
|
||||
|
||||
let providerHeaders;
|
||||
let providerMatched = false;
|
||||
// Determine which API key to use based on the target URL
|
||||
if (uri) {
|
||||
for (const providerName in tenAiApi.providers) {
|
||||
const tenProvider = tenAiApi.providers[providerName];
|
||||
if (uri.startsWith(tenProvider.url) && AI.Providers[tenProvider.name]) {
|
||||
providerMatched = true;
|
||||
const provider = AI.Providers[tenProvider.name];
|
||||
provider.key = tenProvider.key;
|
||||
provider.url = tenProvider.url;
|
||||
providerHeaders = AI._getHeaders(provider);
|
||||
|
||||
uri = appendApiKeyToQuery(ctx, provider, uri);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If body.target was provided but no provider was matched, return 403
|
||||
if (!providerHeaders) {
|
||||
ctx.logger.warn(`proxyRequest: target '${uri}' does not match any configured AI provider. Denying access.`);
|
||||
res.status(403).json({
|
||||
"error": {
|
||||
"message": "proxyRequest: target does not match any configured AI provider",
|
||||
"code": "403"
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Merge key in headers
|
||||
const headers = { ...body.headers, ...providerHeaders };
|
||||
|
||||
// Configure timeout options for the request
|
||||
const timeoutOptions = {
|
||||
connectionAndInactivity: tenAiApiTimeout,
|
||||
wholeCycle: tenAiApiTimeout
|
||||
};
|
||||
// Create request parameters object
|
||||
const requestParams = {
|
||||
method: body.method,
|
||||
uri: uri,
|
||||
headers,
|
||||
body: body.data,
|
||||
timeout: timeoutOptions,
|
||||
limit: null,
|
||||
filterPrivate: false
|
||||
};
|
||||
|
||||
// Log the sanitized request parameters
|
||||
ctx.logger.debug(`Proxying request: %j`, requestParams);
|
||||
|
||||
// Use utils.httpRequest to make the request
|
||||
const result = await utils.httpRequest(
|
||||
ctx, // Operation context
|
||||
requestParams.method, // HTTP method
|
||||
requestParams.uri, // Target URL
|
||||
requestParams.headers, // Request headers
|
||||
requestParams.body, // Request body
|
||||
requestParams.timeout, // Timeout configuration
|
||||
requestParams.limit, // Size limit
|
||||
requestParams.filterPrivate // Filter private requests
|
||||
);
|
||||
|
||||
// Set the response headers to match the target response
|
||||
res.set(result.response.headers);
|
||||
|
||||
// Use pipeline to pipe the response data to the client
|
||||
await pipeline(result.stream, res);
|
||||
success = true;
|
||||
|
||||
} catch (error) {
|
||||
ctx.logger.error(`proxyRequest: AI API request error: %s`, error);
|
||||
if (error.response){
|
||||
// Set the response headers to match the target response
|
||||
res.set(error.response.headers);
|
||||
|
||||
// Use pipeline to pipe the response data to the client
|
||||
await pipeline(error.response.data, res);
|
||||
} else {
|
||||
res.status(500).json({
|
||||
"error": {
|
||||
"message": "proxyRequest: AI API request error",
|
||||
"code": "500"
|
||||
}
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
// Record the time taken for the proxyRequest in StatsD (skip cors requests and errors)
|
||||
if (clientStatsD && success) {
|
||||
clientStatsD.timing('coauth.aiProxy', new Date() - startDate);
|
||||
}
|
||||
ctx.logger.info('End proxyRequest');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single AI provider and its models
|
||||
*
|
||||
* @param {operationContext.Context} ctx - Operation context
|
||||
* @param {Object} provider - Provider configuration
|
||||
* @returns {Promise<Object|null>} Processed provider with models or null if provider is invalid
|
||||
*/
|
||||
async function processProvider(ctx, provider) {
|
||||
const logger = ctx.logger;
|
||||
|
||||
if (!provider.url) {
|
||||
return null;
|
||||
}
|
||||
let engineModels = [];
|
||||
let engineModelsUI = [];
|
||||
try {
|
||||
// Call getModels from engine.js
|
||||
if (provider.key && AI.Providers[provider.name]) {
|
||||
AI.Providers[provider.name].key = provider.key;
|
||||
// aiEngine.setCtx(ctx);
|
||||
// await AI.getModels(provider);
|
||||
// // Process result
|
||||
// if (AI.TmpProviderForModels?.models) {
|
||||
// engineModels = AI.TmpProviderForModels.models;
|
||||
// engineModelsUI = AI.TmpProviderForModels.modelsUI;
|
||||
// }
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error processing provider ${provider.name}:`, error);
|
||||
}
|
||||
// Return provider with any models we were able to get from config
|
||||
return {
|
||||
name: provider.name,
|
||||
url: provider.url,
|
||||
key: "",
|
||||
models: engineModels,
|
||||
modelsUI: engineModelsUI
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all AI models from the configuration and dynamically from providers
|
||||
*
|
||||
* @param {operationContext.Context} ctx - Operation context
|
||||
* @returns {Promise<Object>} Object containing providers and their models along with action configurations
|
||||
*/
|
||||
async function getPluginSettings(ctx) {
|
||||
const logger = ctx.logger;
|
||||
logger.info('Starting getPluginSettings');
|
||||
const result = {
|
||||
version: 3,
|
||||
actions: {},
|
||||
providers: {},
|
||||
models: [],
|
||||
customProviders: {}
|
||||
};
|
||||
try {
|
||||
// Get AI API configuration
|
||||
const tenProviders = ctx.getCfg('aiSettings.providers', cfgAiSettings.providers);
|
||||
// Process providers and their models if configuration exists
|
||||
if (tenProviders && Object.keys(tenProviders).length > 0) {
|
||||
result.providers = tenProviders
|
||||
} else {
|
||||
const providers = AI.serializeProviders();
|
||||
for (let i = 0; i < providers.length; i++) {
|
||||
const provider = providers[i];
|
||||
// const cfgProvider = aiApi.providers[provider.name];
|
||||
// if (cfgProvider) {
|
||||
// //todo clone
|
||||
// provider.key = cfgProvider.key;
|
||||
// }
|
||||
|
||||
try {
|
||||
const providerProcessed = await processProvider(ctx, provider);
|
||||
provider.models.push(...providerProcessed.models);
|
||||
} catch (error) {
|
||||
logger.warn('Error processing provider:', error);
|
||||
}
|
||||
|
||||
result.providers[provider.name] = provider;
|
||||
}
|
||||
}
|
||||
const tenModels = ctx.getCfg('aiSettings.models', cfgAiSettings.models);
|
||||
// Process AI actions
|
||||
if (tenModels && tenModels.length > 0) {
|
||||
result.models = tenModels;
|
||||
} else {
|
||||
// result.actions = aiApi.actions;
|
||||
result.models = AI.Storage.serializeModels();
|
||||
}
|
||||
|
||||
// Process AI actions
|
||||
const tenActions = ctx.getCfg('aiSettings.actions', cfgAiSettings.actions);
|
||||
if (tenActions && Object.keys(tenActions).length > 0) {
|
||||
result.actions = tenActions;
|
||||
} else {
|
||||
// result.actions = aiApi.actions;
|
||||
const actionSoted = AI.ActionsGetSorted();
|
||||
result.actions = {};
|
||||
for (let i = 0; i < actionSoted.length; i++) {
|
||||
const action = actionSoted[i];
|
||||
result.actions[action.id] = action;
|
||||
}
|
||||
}
|
||||
const tenVersion = ctx.getCfg('aiSettings.version', cfgAiSettings.version);
|
||||
result.version = tenVersion;
|
||||
} catch (error) {
|
||||
logger.error('Error retrieving AI models from config:', error);
|
||||
}
|
||||
finally {
|
||||
logger.info('Completed getPluginSettings');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function getPluginSettingsForInterface(ctx) {
|
||||
let pluginSettings = await getPluginSettings(ctx);
|
||||
//check empty settings
|
||||
if (pluginSettings && pluginSettings.actions) {
|
||||
let isEmptySettings = true;
|
||||
for (let key in pluginSettings.actions) {
|
||||
if (pluginSettings.actions[key].model) {
|
||||
isEmptySettings = false;
|
||||
}
|
||||
}
|
||||
if (isEmptySettings) {
|
||||
pluginSettings = undefined;
|
||||
}
|
||||
}
|
||||
//remove keys from providers
|
||||
if (pluginSettings && pluginSettings.providers) {
|
||||
for (let key in pluginSettings.providers) {
|
||||
pluginSettings.providers[key].key = "";
|
||||
}
|
||||
}
|
||||
return pluginSettings;
|
||||
}
|
||||
|
||||
async function requestSettings(req, res) {
|
||||
const ctx = new operationContext.Context();
|
||||
ctx.initFromRequest(req);
|
||||
try {
|
||||
await ctx.initTenantCache();
|
||||
const result = await getPluginSettings(ctx);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
ctx.logger.error('getSettings error: %s', error.stack);
|
||||
res.sendStatus(400);
|
||||
}
|
||||
}
|
||||
|
||||
async function requestModels(req, res) {
|
||||
const ctx = new operationContext.Context();
|
||||
ctx.initFromRequest(req);
|
||||
try {
|
||||
await ctx.initTenantCache();
|
||||
let body = JSON.parse(req.body);
|
||||
if (AI.Providers[body.name]) {
|
||||
AI.Providers[body.name].key = body.key;
|
||||
AI.Providers[body.name].url = body.url;
|
||||
}
|
||||
let getRes = await AI.getModels(body);
|
||||
getRes.modelsApi = AI.TmpProviderForModels?.models;
|
||||
res.json(getRes);
|
||||
} catch (error) {
|
||||
ctx.logger.error('getModels error: %s', error.stack);
|
||||
res.sendStatus(400);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
proxyRequest,
|
||||
getPluginSettings,
|
||||
getPluginSettingsForInterface,
|
||||
requestSettings,
|
||||
requestModels
|
||||
};
|
||||
@ -44,6 +44,7 @@ var sqlBase = require('./databaseConnectors/baseConnector');
|
||||
const utilsDocService = require('./utilsDocService');
|
||||
var docsCoServer = require('./DocsCoServer');
|
||||
var taskResult = require('./taskresult');
|
||||
var wopiUtils = require('./wopiUtils');
|
||||
var wopiClient = require('./wopiClient');
|
||||
var logger = require('./../../Common/sources/logger');
|
||||
var utils = require('./../../Common/sources/utils');
|
||||
@ -650,7 +651,7 @@ function* commandSendMailMerge(ctx, cmd, outputData) {
|
||||
}
|
||||
}
|
||||
let commandSfctByCmd = co.wrap(function*(ctx, cmd, opt_priority, opt_expiration, opt_queue, opt_initShardKey) {
|
||||
var selectRes = yield taskResult.select(ctx, cmd.getDocId());
|
||||
var selectRes = yield taskResult.selectWithCache(ctx, cmd.getDocId());
|
||||
var row = selectRes.length > 0 ? selectRes[0] : null;
|
||||
if (!row) {
|
||||
return false;
|
||||
@ -886,9 +887,11 @@ function* commandSetPassword(ctx, conn, cmd, outputData) {
|
||||
|
||||
let hasDocumentPassword = false;
|
||||
let isDocumentPasswordModified = true;
|
||||
let originFormat;
|
||||
let selectRes = yield taskResult.select(ctx, cmd.getDocId());
|
||||
if (selectRes.length > 0) {
|
||||
let row = selectRes[0];
|
||||
originFormat = row.change_id;
|
||||
hasPasswordCol = undefined !== row.password;
|
||||
if (commonDefines.FileStatus.Ok === row.status) {
|
||||
let documentPasswordCurEnc = sqlBase.DocumentPassword.prototype.getCurPassword(ctx, row.password);
|
||||
@ -920,7 +923,7 @@ function* commandSetPassword(ctx, conn, cmd, outputData) {
|
||||
var task = new taskResult.TaskResultData();
|
||||
task.password = cmd.getPassword() || "";
|
||||
let changeInfo = null;
|
||||
if (conn.user) {
|
||||
if (conn.user && (hasDocumentPassword || !formatChecker.isBrowserEditorFormat(originFormat))) {
|
||||
changeInfo = task.innerPasswordChange = docsCoServer.getExternalChangeInfo(conn.user, newChangesLastDate.getTime(), conn.lang);
|
||||
}
|
||||
|
||||
@ -930,9 +933,11 @@ function* commandSetPassword(ctx, conn, cmd, outputData) {
|
||||
if (!conn.isEnterCorrectPassword) {
|
||||
yield docsCoServer.modifyConnectionForPassword(ctx, conn, true);
|
||||
}
|
||||
let forceSave = yield docsCoServer.editorData.getForceSave(ctx, cmd.getDocId());
|
||||
let index = forceSave?.index || 0;
|
||||
yield docsCoServer.resetForceSaveAfterChanges(ctx, cmd.getDocId(), newChangesLastDate.getTime(), index, utils.getBaseUrlByConnection(ctx, conn), changeInfo);
|
||||
if (changeInfo) {
|
||||
let forceSave = yield docsCoServer.editorData.getForceSave(ctx, cmd.getDocId());
|
||||
let index = forceSave?.index || 0;
|
||||
yield docsCoServer.resetForceSaveAfterChanges(ctx, cmd.getDocId(), newChangesLastDate.getTime(), index, utils.getBaseUrlByConnection(ctx, conn), changeInfo);
|
||||
}
|
||||
} else {
|
||||
ctx.logger.debug('commandSetPassword sql update error');
|
||||
outputData.setStatus('err');
|
||||
@ -1697,7 +1702,7 @@ exports.downloadFile = function(req, res) {
|
||||
//editnew case
|
||||
fromTemplate = pathModule.extname(decoded.fileInfo.BaseFileName).substring(1);
|
||||
} else {
|
||||
({url, headers} = yield wopiClient.getWopiFileUrl(ctx, decoded.fileInfo, decoded.userAuth));
|
||||
({url, headers} = yield wopiUtils.getWopiFileUrl(ctx, decoded.fileInfo, decoded.userAuth));
|
||||
let filterStatus = yield wopiClient.checkIpFilter(ctx, url);
|
||||
if (0 === filterStatus) {
|
||||
//todo false? (true because it passed checkIpFilter for wopi)
|
||||
@ -1802,7 +1807,7 @@ exports.saveFromChanges = function(ctx, docId, statusInfo, optFormat, opt_userId
|
||||
//we do a select, because during the timeout the information could change
|
||||
var selectRes = yield taskResult.select(ctx, docId);
|
||||
var row = selectRes.length > 0 ? selectRes[0] : null;
|
||||
if (row && row.status == commonDefines.FileStatus.SaveVersion && row.status_info == statusInfo) {
|
||||
if (row && row.status == commonDefines.FileStatus.SaveVersion && row.status_info == statusInfo && row.callback) {
|
||||
if (null == optFormat) {
|
||||
optFormat = changeFormatByOrigin(ctx, row, constants.AVS_OFFICESTUDIO_FILE_OTHER_OOXML);
|
||||
}
|
||||
@ -1832,6 +1837,10 @@ exports.saveFromChanges = function(ctx, docId, statusInfo, optFormat, opt_userId
|
||||
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);
|
||||
|
||||
89
DocService/sources/routes/config.js
Normal file
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* (c) Copyright Ascensio System SIA 2010-2024
|
||||
*
|
||||
* This program is a free software product. You can redistribute it and/or
|
||||
* modify it under the terms of the GNU Affero General Public License (AGPL)
|
||||
* version 3 as published by the Free Software Foundation. In accordance with
|
||||
* Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
|
||||
* that Ascensio System SIA expressly excludes the warranty of non-infringement
|
||||
* of any third-party rights.
|
||||
*
|
||||
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
|
||||
* details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
||||
*
|
||||
* You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
|
||||
* street, Riga, Latvia, EU, LV-1050.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of the Program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU AGPL version 3.
|
||||
*
|
||||
* Pursuant to Section 7(b) of the License you must retain the original Product
|
||||
* logo when distributing the program. Pursuant to Section 7(e) we decline to
|
||||
* grant you any rights under trademark law for use of our trademarks.
|
||||
*
|
||||
* All the Product's GUI elements, including illustrations and icon sets, as
|
||||
* well as technical writing content are licensed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International. See the License
|
||||
* terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
*
|
||||
*/
|
||||
|
||||
const config = require('config');
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const tenantManager = require('../../../Common/sources/tenantManager');
|
||||
const operationContext = require('../../../Common/sources/operationContext');
|
||||
const runtimeConfigManager = require('../../../Common/sources/runtimeConfigManager');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const rawFileParser = bodyParser.raw(
|
||||
{inflate: true, limit: config.get('services.CoAuthoring.server.limits_tempfile_upload'), type: function() {return true;}});
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
let ctx = new operationContext.Context();
|
||||
let result = '{}';
|
||||
try {
|
||||
ctx.initFromRequest(req);
|
||||
await ctx.initTenantCache();
|
||||
ctx.logger.debug('config get start');
|
||||
let cfg = ctx.getFullCfg();
|
||||
result = JSON.stringify(cfg);
|
||||
} catch (error) {
|
||||
ctx.logger.error('config get error: %s', error.stack);
|
||||
}
|
||||
finally {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.send(result);
|
||||
ctx.logger.debug('config end');
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', rawFileParser, async (req, res) => {
|
||||
let ctx = new operationContext.Context();
|
||||
try {
|
||||
ctx.initFromRequest(req);
|
||||
await ctx.initTenantCache();
|
||||
|
||||
let newConfig = JSON.parse(req.body);
|
||||
|
||||
if (tenantManager.isMultitenantMode(ctx) && !tenantManager.isDefaultTenant(ctx)) {
|
||||
await tenantManager.setTenantConfig(ctx, newConfig);
|
||||
} else {
|
||||
await runtimeConfigManager.saveConfig(ctx, newConfig);
|
||||
}
|
||||
|
||||
res.sendStatus(200);
|
||||
} catch (error) {
|
||||
ctx.logger.error('Configuration save error: %s', error.stack);
|
||||
res.status(500).json({
|
||||
error: 'Failed to save configuration',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
@ -31,6 +31,8 @@
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { pipeline } = require('node:stream/promises');
|
||||
const express = require('express');
|
||||
const config = require('config');
|
||||
const operationContext = require('./../../../Common/sources/operationContext');
|
||||
@ -39,6 +41,7 @@ const storage = require('./../../../Common/sources/storage/storage-base');
|
||||
const urlModule = require("url");
|
||||
const path = require("path");
|
||||
const mime = require("mime");
|
||||
const crypto = require('crypto');
|
||||
|
||||
const cfgStaticContent = config.has('services.CoAuthoring.server.static_content') ? config.util.cloneDeep(config.get('services.CoAuthoring.server.static_content')) : {};
|
||||
const cfgCacheStorage = config.get('storage');
|
||||
@ -49,40 +52,97 @@ const cfgErrorFiles = config.get('FileConverter.converter.errorfiles');
|
||||
const router = express.Router();
|
||||
|
||||
function initCacheRouter(cfgStorage, routs) {
|
||||
const bucketName = cfgStorage.bucketName;
|
||||
const storageFolderName = cfgStorage.storageFolderName;
|
||||
const folderPath = cfgStorage.fs.folderPath;
|
||||
const { storageFolderName, fs: { folderPath, secretString: secret } } = cfgStorage;
|
||||
|
||||
routs.forEach((rout) => {
|
||||
//special dirs are empty by default
|
||||
if (!rout) {
|
||||
if (!rout) return;
|
||||
|
||||
const rootPath = path.join(folderPath, rout);
|
||||
|
||||
['cache', 'storage-cache'].forEach(prefix => {
|
||||
const route = `/${prefix}/${storageFolderName}/${rout}`;
|
||||
router.use(route, createCacheMiddleware(prefix, rootPath, cfgStorage, secret, rout));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createCacheMiddleware(prefix, rootPath, cfgStorage, secret, rout) {
|
||||
return async (req, res) => {
|
||||
const index = req.url.lastIndexOf('/');
|
||||
if (req.method !== 'GET' || index <= 0) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
let rootPath = path.join(folderPath, rout);
|
||||
router.use(`/${bucketName}/${storageFolderName}/${rout}`, (req, res, next) => {
|
||||
const index = req.url.lastIndexOf('/');
|
||||
if ('GET' === req.method && index > 0) {
|
||||
let sendFileOptions = {
|
||||
root: rootPath, dotfiles: 'deny', headers: {
|
||||
'Content-Disposition': 'attachment'
|
||||
|
||||
try {
|
||||
const urlParsed = urlModule.parse(req.url, true);
|
||||
const { md5, expires } = urlParsed.query;
|
||||
const numericExpires = parseInt(expires);
|
||||
|
||||
if (!md5 || !numericExpires) {
|
||||
res.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
if (currentTime > numericExpires) {
|
||||
res.sendStatus(410);
|
||||
return;
|
||||
}
|
||||
|
||||
const uri = req.url.split('?')[0];
|
||||
const fullPath = `/${prefix}/${cfgStorage.storageFolderName}/${rout}${uri}`;
|
||||
const signatureData = numericExpires + decodeURIComponent(fullPath) + secret;
|
||||
|
||||
const expectedMd5 = crypto
|
||||
.createHash('md5')
|
||||
.update(signatureData)
|
||||
.digest('base64')
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
|
||||
if (md5 !== expectedMd5) {
|
||||
res.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = urlParsed.pathname && decodeURIComponent(path.basename(urlParsed.pathname));
|
||||
const filePath = decodeURI(req.url.substring(1, index));
|
||||
if (cfgStorage.name === 'storage-fs') {
|
||||
const sendFileOptions = {
|
||||
root: rootPath,
|
||||
dotfiles: 'deny',
|
||||
headers: {
|
||||
'Content-Disposition': 'attachment',
|
||||
...(filename && { 'Content-Type': mime.getType(filename) })
|
||||
}
|
||||
};
|
||||
const urlParsed = urlModule.parse(req.url);
|
||||
if (urlParsed && urlParsed.pathname) {
|
||||
const filename = decodeURIComponent(path.basename(urlParsed.pathname));
|
||||
sendFileOptions.headers['Content-Type'] = mime.getType(filename);
|
||||
}
|
||||
const realUrl = decodeURI(req.url.substring(0, index));
|
||||
res.sendFile(realUrl, sendFileOptions, (err) => {
|
||||
|
||||
res.sendFile(filePath, sendFileOptions, (err) => {
|
||||
if (err) {
|
||||
operationContext.global.logger.error(err);
|
||||
res.status(400).end();
|
||||
}
|
||||
});
|
||||
} else if (['storage-s3', 'storage-az'].includes(cfgStorage.name)) {
|
||||
const ctx = new operationContext.Context();
|
||||
ctx.initFromRequest(req);
|
||||
await ctx.initTenantCache();
|
||||
const result = await storage.createReadStream(ctx, filePath, rout);
|
||||
|
||||
res.setHeader('Content-Type', mime.getType(filename));
|
||||
res.setHeader('Content-Length', result.contentLength);
|
||||
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
|
||||
await pipeline(result.readStream, res);
|
||||
} else {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
operationContext.global.logger.error(e);
|
||||
res.sendStatus(400);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for (let i in cfgStaticContent) {
|
||||
@ -95,9 +155,9 @@ if (storage.needServeStatic()) {
|
||||
}
|
||||
if (storage.needServeStatic(cfgForgottenFiles)) {
|
||||
let persistentRouts = [cfgForgottenFiles, cfgErrorFiles];
|
||||
persistentRouts.filter((rout) => {return rout && rout.length > 0;});
|
||||
persistentRouts = persistentRouts.filter((rout) => {return rout && rout.length > 0;});
|
||||
if (persistentRouts.length > 0) {
|
||||
initCacheRouter(cfgPersistentStorage, [cfgForgottenFiles, cfgErrorFiles]);
|
||||
initCacheRouter(cfgPersistentStorage, persistentRouts);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -58,7 +58,9 @@ const commonDefines = require('./../../Common/sources/commondefines');
|
||||
const operationContext = require('./../../Common/sources/operationContext');
|
||||
const tenantManager = require('./../../Common/sources/tenantManager');
|
||||
const staticRouter = require('./routes/static');
|
||||
const configRouter = require('./routes/config');
|
||||
const ms = require('ms');
|
||||
const aiProxyHandler = require('./ai/aiProxyHandler');
|
||||
|
||||
const cfgWopiEnable = config.get('wopi.enable');
|
||||
const cfgWopiDummyEnable = config.get('wopi.dummy.enable');
|
||||
@ -235,6 +237,9 @@ docsCoServer.install(server, () => {
|
||||
converterService.builder(req, res);
|
||||
});
|
||||
app.get('/info/info.json', utils.checkClientIp, docsCoServer.licenseInfo);
|
||||
app.use('/info/config', utils.checkClientIp, configRouter);
|
||||
app.get('/info/plugin/settings', utils.checkClientIp, aiProxyHandler.requestSettings);
|
||||
app.post('/info/plugin/models', utils.checkClientIp, rawFileParser, aiProxyHandler.requestModels);
|
||||
app.put('/internal/cluster/inactive', utils.checkClientIp, docsCoServer.shutdown);
|
||||
app.delete('/internal/cluster/inactive', utils.checkClientIp, docsCoServer.shutdown);
|
||||
app.get('/internal/connections/edit', docsCoServer.getEditorConnectionsCount);
|
||||
@ -288,6 +293,8 @@ docsCoServer.install(server, () => {
|
||||
app.get('/wopi/files/:docid/contents', apicache.middleware("5 minutes"), checkWopiDummyEnable, wopiClient.dummyGetFile);
|
||||
app.post('/wopi/files/:docid/contents', checkWopiDummyEnable, wopiClient.dummyOk);
|
||||
|
||||
app.use('/ai-proxy', rawFileParser, aiProxyHandler.proxyRequest);
|
||||
|
||||
app.post('/dummyCallback', utils.checkClientIp, apicache.middleware("5 minutes"), rawFileParser, function(req, res){
|
||||
let ctx = new operationContext.Context();
|
||||
ctx.initFromRequest(req);
|
||||
|
||||
@ -98,7 +98,20 @@ TaskResultData.prototype.completeDefaults = function() {
|
||||
function upsert(ctx, task) {
|
||||
return sqlBase.upsert(ctx, task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return TaskResult rows for docId, caching the last query result on ctx.taskResultCache or fetching from the database.
|
||||
* @param {object} ctx
|
||||
* @param {string} docId
|
||||
* @returns {Promise<Array<object>>}
|
||||
*/
|
||||
async function selectWithCache(ctx, docId) {
|
||||
//todo merge with select and remove on update
|
||||
if (ctx.taskResultCache && ctx.taskResultCache[0].id === docId) {
|
||||
return ctx.taskResultCache;
|
||||
}
|
||||
ctx.taskResultCache = await select(ctx, docId);
|
||||
return ctx.taskResultCache;
|
||||
}
|
||||
function select(ctx, docId) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
let values = [];
|
||||
@ -316,6 +329,7 @@ function removeIf(ctx, mask) {
|
||||
exports.TaskResultData = TaskResultData;
|
||||
exports.upsert = upsert;
|
||||
exports.select = select;
|
||||
exports.selectWithCache = selectWithCache;
|
||||
exports.update = update;
|
||||
exports.updateIf = updateIf;
|
||||
exports.restoreInitialPassword = restoreInitialPassword;
|
||||
|
||||
@ -34,8 +34,6 @@
|
||||
|
||||
const path = require('path');
|
||||
const { pipeline } = require('node:stream/promises');
|
||||
const crypto = require('crypto');
|
||||
let util = require('util');
|
||||
const {URL} = require('url');
|
||||
const co = require('co');
|
||||
const jwt = require('jsonwebtoken');
|
||||
@ -49,7 +47,7 @@ const logger = require('./../../Common/sources/logger');
|
||||
const utils = require('./../../Common/sources/utils');
|
||||
const constants = require('./../../Common/sources/constants');
|
||||
const commonDefines = require('./../../Common/sources/commondefines');
|
||||
const formatChecker = require('./../../Common/sources/formatchecker');
|
||||
const wopiUtils = require('./wopiUtils');
|
||||
const operationContext = require('./../../Common/sources/operationContext');
|
||||
const tenantManager = require('./../../Common/sources/tenantManager');
|
||||
const sqlBase = require('./databaseConnectors/baseConnector');
|
||||
@ -95,8 +93,6 @@ const cfgWopiPrivateKeyOld = config.get('wopi.privateKeyOld');
|
||||
const cfgWopiHost = config.get('wopi.host');
|
||||
const cfgWopiDummySampleFilePath = config.get('wopi.dummy.sampleFilePath');
|
||||
|
||||
let cryptoSign = util.promisify(crypto.sign);
|
||||
|
||||
let templatesFolderLocalesCache = null;
|
||||
let templatesFolderExtsCache = null;
|
||||
const templateFilesSizeCache = {};
|
||||
@ -373,22 +369,7 @@ function getFileTypeByInfo(fileInfo) {
|
||||
fileType = fileInfo.FileExtension ? fileInfo.FileExtension.substr(1) : fileType;
|
||||
return fileType.toLowerCase();
|
||||
}
|
||||
async function getWopiFileUrl(ctx, fileInfo, userAuth) {
|
||||
const tenMaxDownloadBytes = ctx.getCfg('FileConverter.converter.maxDownloadBytes', cfgMaxDownloadBytes);
|
||||
let url;
|
||||
let headers = {'X-WOPI-MaxExpectedSize': tenMaxDownloadBytes};
|
||||
if (fileInfo?.FileUrl) {
|
||||
//Requests to the FileUrl can not be signed using proof keys. The FileUrl is used exactly as provided by the host, so it does not necessarily include the access token, which is required to construct the expected proof.
|
||||
url = fileInfo.FileUrl;
|
||||
} else if (fileInfo?.TemplateSource) {
|
||||
url = fileInfo.TemplateSource;
|
||||
} else if (userAuth) {
|
||||
url = `${userAuth.wopiSrc}/contents?access_token=${encodeURIComponent(userAuth.access_token)}`;
|
||||
await fillStandardHeaders(ctx, headers, url, userAuth.access_token);
|
||||
}
|
||||
ctx.logger.debug('getWopiFileUrl url=%s; headers=%j', url, headers);
|
||||
return {url, headers};
|
||||
}
|
||||
|
||||
function isWopiJwtToken(decoded) {
|
||||
return !!decoded.fileInfo;
|
||||
}
|
||||
@ -751,7 +732,7 @@ function putFile(ctx, wopiParams, data, dataStream, dataSize, userLastChangeId,
|
||||
let commonInfo = wopiParams.commonInfo;
|
||||
//todo add all the users who contributed changes to the document in this PutFile request to X-WOPI-Editors
|
||||
let headers = {'X-WOPI-Override': 'PUT', 'X-WOPI-Lock': commonInfo.lockId, 'X-WOPI-Editors': userLastChangeId};
|
||||
yield fillStandardHeaders(ctx, headers, uri, userAuth.access_token);
|
||||
yield wopiUtils.fillStandardHeaders(ctx, headers, uri, userAuth.access_token);
|
||||
headers['X-LOOL-WOPI-IsModifiedByUser'] = isModifiedByUser;
|
||||
headers['X-LOOL-WOPI-IsAutosave'] = isAutosave;
|
||||
headers['X-LOOL-WOPI-IsExitSave'] = isExitSave;
|
||||
@ -795,7 +776,7 @@ function putRelativeFile(ctx, wopiSrc, access_token, data, dataStream, dataSize,
|
||||
if (isFileConversion) {
|
||||
headers['X-WOPI-FileConversion'] = isFileConversion;
|
||||
}
|
||||
yield fillStandardHeaders(ctx, headers, uri, access_token);
|
||||
yield wopiUtils.fillStandardHeaders(ctx, headers, uri, access_token);
|
||||
headers['Content-Type'] = mime.getType(suggestedExt);
|
||||
|
||||
ctx.logger.debug('wopi putRelativeFile request uri=%s headers=%j', uri, headers);
|
||||
@ -813,53 +794,58 @@ function putRelativeFile(ctx, wopiSrc, access_token, data, dataStream, dataSize,
|
||||
return res;
|
||||
});
|
||||
}
|
||||
function renameFile(ctx, wopiParams, name) {
|
||||
return co(function* () {
|
||||
let res = undefined;
|
||||
try {
|
||||
ctx.logger.info('wopi RenameFile start');
|
||||
const tenCallbackRequestTimeout = ctx.getCfg('services.CoAuthoring.server.callbackRequestTimeout', cfgCallbackRequestTimeout);
|
||||
/**
|
||||
* Renames a file using the WOPI protocol
|
||||
* @param {operationContext.Context} ctx - The operation context.
|
||||
* @param {object} wopiParams - The WOPI parameters.
|
||||
* @param {string} name - The new name for the file.
|
||||
* @returns {Promise<{Name: string}|undefined>}
|
||||
*/
|
||||
async function renameFile(ctx, wopiParams, name) {
|
||||
let res = undefined;
|
||||
try {
|
||||
ctx.logger.info('wopi RenameFile start');
|
||||
const tenCallbackRequestTimeout = ctx.getCfg('services.CoAuthoring.server.callbackRequestTimeout', cfgCallbackRequestTimeout);
|
||||
|
||||
if (!wopiParams.userAuth || !wopiParams.commonInfo) {
|
||||
return res;
|
||||
}
|
||||
let fileInfo = wopiParams.commonInfo.fileInfo;
|
||||
let userAuth = wopiParams.userAuth;
|
||||
let uri = `${userAuth.wopiSrc}?access_token=${encodeURIComponent(userAuth.access_token)}`;
|
||||
let filterStatus = yield checkIpFilter(ctx, uri);
|
||||
if (0 !== filterStatus) {
|
||||
return res;
|
||||
}
|
||||
|
||||
if (fileInfo && fileInfo.SupportsRename) {
|
||||
let fileNameMaxLength = fileInfo.FileNameMaxLength || 255;
|
||||
name = name.substring(0, fileNameMaxLength);
|
||||
let commonInfo = wopiParams.commonInfo;
|
||||
|
||||
let headers = {'X-WOPI-Override': 'RENAME_FILE', 'X-WOPI-Lock': commonInfo.lockId, 'X-WOPI-RequestedName': utf7.encode(name)};
|
||||
yield fillStandardHeaders(ctx, headers, uri, userAuth.access_token);
|
||||
|
||||
ctx.logger.debug('wopi RenameFile request uri=%s headers=%j', uri, headers);
|
||||
//isInJwtToken is true because it passed checkIpFilter for wopi
|
||||
let isInJwtToken = true;
|
||||
let postRes = yield utils.postRequestPromise(ctx, uri, undefined, undefined, undefined, tenCallbackRequestTimeout, undefined, isInJwtToken, headers);
|
||||
ctx.logger.debug('wopi RenameFile response headers=%j body=%s', postRes.response.headers, postRes.body);
|
||||
if (postRes.body) {
|
||||
res = JSON.parse(postRes.body);
|
||||
} else {
|
||||
//sharepoint send empty body(2016 allways, 2019 with same name)
|
||||
res = {"Name": name};
|
||||
}
|
||||
} else {
|
||||
ctx.logger.info('wopi SupportsRename = false');
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.logger.error('wopi error RenameFile:%s', err.stack);
|
||||
} finally {
|
||||
ctx.logger.info('wopi RenameFile end');
|
||||
if (!wopiParams.userAuth || !wopiParams.commonInfo) {
|
||||
return res;
|
||||
}
|
||||
return res;
|
||||
});
|
||||
const fileInfo = wopiParams.commonInfo.fileInfo;
|
||||
const userAuth = wopiParams.userAuth;
|
||||
const uri = `${userAuth.wopiSrc}?access_token=${encodeURIComponent(userAuth.access_token)}`;
|
||||
const filterStatus = await checkIpFilter(ctx, uri);
|
||||
if (0 !== filterStatus) {
|
||||
return res;
|
||||
}
|
||||
|
||||
if (fileInfo && fileInfo.SupportsRename) {
|
||||
const fileNameMaxLength = fileInfo.FileNameMaxLength || 255;
|
||||
name = name.substring(0, fileNameMaxLength);
|
||||
const commonInfo = wopiParams.commonInfo;
|
||||
|
||||
const headers = {'X-WOPI-Override': 'RENAME_FILE', 'X-WOPI-Lock': commonInfo.lockId, 'X-WOPI-RequestedName': utf7.encode(name)};
|
||||
await wopiUtils.fillStandardHeaders(ctx, headers, uri, userAuth.access_token);
|
||||
|
||||
ctx.logger.debug('wopi RenameFile request uri=%s headers=%j', uri, headers);
|
||||
//isInJwtToken is true because it passed checkIpFilter for wopi
|
||||
const isInJwtToken = true;
|
||||
const postRes = await utils.postRequestPromise(ctx, uri, undefined, undefined, undefined, tenCallbackRequestTimeout, undefined, isInJwtToken, headers);
|
||||
ctx.logger.debug('wopi RenameFile response headers=%j body=%s', postRes.response.headers, postRes.body);
|
||||
if (postRes.body) {
|
||||
res = JSON.parse(postRes.body);
|
||||
} else {
|
||||
//sharepoint send empty body(2016 allways, 2019 with same name)
|
||||
res = {"Name": name};
|
||||
}
|
||||
} else {
|
||||
ctx.logger.info('wopi SupportsRename = false');
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.logger.error('wopi error RenameFile:%s', err.stack);
|
||||
} finally {
|
||||
ctx.logger.info('wopi RenameFile end');
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async function refreshFile(ctx, wopiParams, baseUrl) {
|
||||
@ -918,7 +904,7 @@ function checkFileInfo(ctx, wopiSrc, access_token, opt_sc) {
|
||||
if (opt_sc) {
|
||||
headers['X-WOPI-SessionContext'] = opt_sc;
|
||||
}
|
||||
yield fillStandardHeaders(ctx, headers, uri, access_token);
|
||||
yield wopiUtils.fillStandardHeaders(ctx, headers, uri, access_token);
|
||||
ctx.logger.debug('wopi checkFileInfo request uri=%s headers=%j', uri, headers);
|
||||
//isInJwtToken is true because it passed checkIpFilter for wopi
|
||||
let isInJwtToken = true;
|
||||
@ -953,7 +939,7 @@ function lock(ctx, command, lockId, fileInfo, userAuth) {
|
||||
}
|
||||
|
||||
let headers = {"X-WOPI-Override": command, "X-WOPI-Lock": lockId};
|
||||
yield fillStandardHeaders(ctx, headers, uri, access_token);
|
||||
yield wopiUtils.fillStandardHeaders(ctx, headers, uri, access_token);
|
||||
ctx.logger.debug('wopi %s request uri=%s headers=%j', command, uri, headers);
|
||||
//isInJwtToken is true because it passed checkIpFilter for wopi
|
||||
let isInJwtToken = true;
|
||||
@ -992,7 +978,7 @@ async function unlock(ctx, wopiParams) {
|
||||
}
|
||||
|
||||
let headers = {"X-WOPI-Override": "UNLOCK", "X-WOPI-Lock": lockId};
|
||||
await fillStandardHeaders(ctx, headers, uri, access_token);
|
||||
await wopiUtils.fillStandardHeaders(ctx, headers, uri, access_token);
|
||||
ctx.logger.debug('wopi Unlock request uri=%s headers=%j', uri, headers);
|
||||
//isInJwtToken is true because it passed checkIpFilter for wopi
|
||||
let isInJwtToken = true;
|
||||
@ -1009,31 +995,6 @@ async function unlock(ctx, wopiParams) {
|
||||
}
|
||||
return res;
|
||||
}
|
||||
function generateProofBuffer(url, accessToken, timeStamp) {
|
||||
const accessTokenBytes = Buffer.from(accessToken, 'utf8');
|
||||
const urlBytes = Buffer.from(url.toUpperCase(), 'utf8');
|
||||
|
||||
let offset = 0;
|
||||
let buffer = Buffer.alloc(4 + accessTokenBytes.length + 4 + urlBytes.length + 4 + 8);
|
||||
buffer.writeUInt32BE(accessTokenBytes.length, offset);
|
||||
offset += 4;
|
||||
accessTokenBytes.copy(buffer, offset, 0, accessTokenBytes.length);
|
||||
offset += accessTokenBytes.length;
|
||||
buffer.writeUInt32BE(urlBytes.length, offset);
|
||||
offset += 4;
|
||||
urlBytes.copy(buffer, offset, 0, urlBytes.length);
|
||||
offset += urlBytes.length;
|
||||
buffer.writeUInt32BE(8, offset);
|
||||
offset += 4;
|
||||
buffer.writeBigUInt64BE(timeStamp, offset);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async function generateProofSign(url, accessToken, timeStamp, privateKey) {
|
||||
let data = generateProofBuffer(url, accessToken, timeStamp);
|
||||
let sign = await cryptoSign('RSA-SHA256', data, privateKey);
|
||||
return sign.toString('base64');
|
||||
}
|
||||
|
||||
function numberToBase64(val) {
|
||||
// Convert to hexadecimal
|
||||
@ -1047,23 +1008,6 @@ function numberToBase64(val) {
|
||||
return buffer.toString('base64');
|
||||
}
|
||||
|
||||
async function fillStandardHeaders(ctx, headers, url, access_token) {
|
||||
let timeStamp = utils.getDateTimeTicks(new Date());
|
||||
const tenWopiPrivateKey = ctx.getCfg('wopi.privateKey', cfgWopiPrivateKey);
|
||||
const tenWopiPrivateKeyOld = ctx.getCfg('wopi.privateKeyOld', cfgWopiPrivateKeyOld);
|
||||
if (tenWopiPrivateKey && tenWopiPrivateKeyOld) {
|
||||
headers['X-WOPI-Proof'] = await generateProofSign(url, access_token, timeStamp, tenWopiPrivateKey);
|
||||
headers['X-WOPI-ProofOld'] = await generateProofSign(url, access_token, timeStamp, tenWopiPrivateKeyOld);
|
||||
}
|
||||
headers['X-WOPI-TimeStamp'] = timeStamp;
|
||||
headers['X-WOPI-ClientVersion'] = commonDefines.buildVersion + '.' + commonDefines.buildNumber;
|
||||
// todo
|
||||
// headers['X-WOPI-CorrelationId '] = "";
|
||||
// headers['X-WOPI-SessionId'] = "";
|
||||
//remove redundant header https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/common-headers#request-headers
|
||||
// headers['Authorization'] = `Bearer ${access_token}`;
|
||||
}
|
||||
|
||||
function checkIpFilter(ctx, uri){
|
||||
return co(function* () {
|
||||
let urlParsed = new URL(uri);
|
||||
@ -1169,11 +1113,9 @@ exports.renameFile = renameFile;
|
||||
exports.refreshFile = refreshFile;
|
||||
exports.lock = lock;
|
||||
exports.unlock = unlock;
|
||||
exports.fillStandardHeaders = fillStandardHeaders;
|
||||
exports.getWopiUnlockMarker = getWopiUnlockMarker;
|
||||
exports.getWopiModifiedMarker = getWopiModifiedMarker;
|
||||
exports.getFileTypeByInfo = getFileTypeByInfo;
|
||||
exports.getWopiFileUrl = getWopiFileUrl;
|
||||
exports.isWopiJwtToken = isWopiJwtToken;
|
||||
exports.setIsShutdown = setIsShutdown;
|
||||
exports.dummyCheckFileInfo = dummyCheckFileInfo;
|
||||
|
||||
144
DocService/sources/wopiUtils.js
Normal file
@ -0,0 +1,144 @@
|
||||
/*
|
||||
* (c) Copyright Ascensio System SIA 2010-2024
|
||||
*
|
||||
* This program is a free software product. You can redistribute it and/or
|
||||
* modify it under the terms of the GNU Affero General Public License (AGPL)
|
||||
* version 3 as published by the Free Software Foundation. In accordance with
|
||||
* Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
|
||||
* that Ascensio System SIA expressly excludes the warranty of non-infringement
|
||||
* of any third-party rights.
|
||||
*
|
||||
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
|
||||
* details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
||||
*
|
||||
* You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
|
||||
* street, Riga, Latvia, EU, LV-1050.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of the Program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU AGPL version 3.
|
||||
*
|
||||
* Pursuant to Section 7(b) of the License you must retain the original Product
|
||||
* logo when distributing the program. Pursuant to Section 7(e) we decline to
|
||||
* grant you any rights under trademark law for use of our trademarks.
|
||||
*
|
||||
* All the Product's GUI elements, including illustrations and icon sets, as
|
||||
* well as technical writing content are licensed under the terms of the
|
||||
* Creative Commons Attribution-ShareAlike 4.0 International. See the License
|
||||
* terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
*
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const util = require('util');
|
||||
const config = require('config');
|
||||
const utils = require('./../../Common/sources/utils');
|
||||
const commonDefines = require('./../../Common/sources/commondefines');
|
||||
|
||||
// Configuration constants
|
||||
const cfgMaxDownloadBytes = config.get('FileConverter.converter.maxDownloadBytes');
|
||||
const cfgWopiPrivateKey = config.get('wopi.privateKey');
|
||||
const cfgWopiPrivateKeyOld = config.get('wopi.privateKeyOld');
|
||||
|
||||
const cryptoSign = util.promisify(crypto.sign);
|
||||
|
||||
/**
|
||||
* Generates a proof buffer for WOPI requests
|
||||
*
|
||||
* @param {string} url - The URL to generate proof for
|
||||
* @param {string} accessToken - The access token
|
||||
* @param {bigint} timeStamp - The timestamp in ticks
|
||||
* @returns {Buffer} - The proof buffer
|
||||
*/
|
||||
function generateProofBuffer(url, accessToken, timeStamp) {
|
||||
const accessTokenBytes = Buffer.from(accessToken, 'utf8');
|
||||
const urlBytes = Buffer.from(url.toUpperCase(), 'utf8');
|
||||
|
||||
let offset = 0;
|
||||
let buffer = Buffer.alloc(4 + accessTokenBytes.length + 4 + urlBytes.length + 4 + 8);
|
||||
buffer.writeUInt32BE(accessTokenBytes.length, offset);
|
||||
offset += 4;
|
||||
accessTokenBytes.copy(buffer, offset, 0, accessTokenBytes.length);
|
||||
offset += accessTokenBytes.length;
|
||||
buffer.writeUInt32BE(urlBytes.length, offset);
|
||||
offset += 4;
|
||||
urlBytes.copy(buffer, offset, 0, urlBytes.length);
|
||||
offset += urlBytes.length;
|
||||
buffer.writeUInt32BE(8, offset);
|
||||
offset += 4;
|
||||
buffer.writeBigUInt64BE(timeStamp, offset);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a proof signature for WOPI requests
|
||||
*
|
||||
* @param {string} url - The URL to generate proof for
|
||||
* @param {string} accessToken - The access token
|
||||
* @param {bigint} timeStamp - The timestamp in ticks
|
||||
* @param {string} privateKey - The private key for signing
|
||||
* @returns {string} - The base64-encoded signature
|
||||
*/
|
||||
async function generateProofSign(url, accessToken, timeStamp, privateKey) {
|
||||
let data = generateProofBuffer(url, accessToken, timeStamp);
|
||||
let sign = await cryptoSign('RSA-SHA256', data, privateKey);
|
||||
return sign.toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills standard WOPI headers for requests
|
||||
*
|
||||
* @param {Object} ctx - The operation context
|
||||
* @param {Object} headers - The headers object to fill
|
||||
* @param {string} url - The URL for the request
|
||||
* @param {string} access_token - The access token
|
||||
*/
|
||||
async function fillStandardHeaders(ctx, headers, url, access_token) {
|
||||
let timeStamp = utils.getDateTimeTicks(new Date());
|
||||
const tenWopiPrivateKey = ctx.getCfg('wopi.privateKey', cfgWopiPrivateKey);
|
||||
const tenWopiPrivateKeyOld = ctx.getCfg('wopi.privateKeyOld', cfgWopiPrivateKeyOld);
|
||||
if (tenWopiPrivateKey && tenWopiPrivateKeyOld) {
|
||||
headers['X-WOPI-Proof'] = await generateProofSign(url, access_token, timeStamp, tenWopiPrivateKey);
|
||||
headers['X-WOPI-ProofOld'] = await generateProofSign(url, access_token, timeStamp, tenWopiPrivateKeyOld);
|
||||
}
|
||||
headers['X-WOPI-TimeStamp'] = timeStamp;
|
||||
headers['X-WOPI-ClientVersion'] = commonDefines.buildVersion + '.' + commonDefines.buildNumber;
|
||||
// todo
|
||||
// headers['X-WOPI-CorrelationId '] = "";
|
||||
// headers['X-WOPI-SessionId'] = "";
|
||||
//remove redundant header https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/common-headers#request-headers
|
||||
// headers['Authorization'] = `Bearer ${access_token}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a WOPI file URL with appropriate headers
|
||||
*
|
||||
* @param {Object} ctx - The operation context
|
||||
* @param {Object} fileInfo - Information about the file
|
||||
* @param {Object} userAuth - User authentication details
|
||||
* @returns {Object} - Object containing URL and headers
|
||||
*/
|
||||
async function getWopiFileUrl(ctx, fileInfo, userAuth) {
|
||||
const tenMaxDownloadBytes = ctx.getCfg('FileConverter.converter.maxDownloadBytes', cfgMaxDownloadBytes);
|
||||
let url;
|
||||
let headers = {'X-WOPI-MaxExpectedSize': tenMaxDownloadBytes};
|
||||
if (fileInfo?.FileUrl) {
|
||||
//Requests to the FileUrl can not be signed using proof keys. The FileUrl is used exactly as provided by the host, so it does not necessarily include the access token, which is required to construct the expected proof.
|
||||
url = fileInfo.FileUrl;
|
||||
} else if (fileInfo?.TemplateSource) {
|
||||
url = fileInfo.TemplateSource;
|
||||
} else if (userAuth) {
|
||||
url = `${userAuth.wopiSrc}/contents?access_token=${encodeURIComponent(userAuth.access_token)}`;
|
||||
await fillStandardHeaders(ctx, headers, url, userAuth.access_token);
|
||||
}
|
||||
ctx.logger.debug('getWopiFileUrl url=%s; headers=%j', url, headers);
|
||||
return {url, headers};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getWopiFileUrl,
|
||||
fillStandardHeaders
|
||||
};
|
||||
@ -47,7 +47,7 @@ var storage = require('./../../Common/sources/storage/storage-base');
|
||||
var utils = require('./../../Common/sources/utils');
|
||||
var constants = require('./../../Common/sources/constants');
|
||||
var baseConnector = require('../../DocService/sources/databaseConnectors/baseConnector');
|
||||
const wopiClient = require('./../../DocService/sources/wopiClient');
|
||||
const wopiUtils = require('./../../DocService/sources/wopiUtils');
|
||||
const taskResult = require('./../../DocService/sources/taskresult');
|
||||
var statsDClient = require('./../../Common/sources/statsdclient');
|
||||
var queueService = require('./../../Common/sources/taskqueueRabbitMQ');
|
||||
@ -345,15 +345,13 @@ function* isUselessConvertion(ctx, task, cmd) {
|
||||
return constants.NO_ERROR;
|
||||
}
|
||||
async function changeFormatToExtendedPdf(ctx, dataConvert, cmd) {
|
||||
let forceSave = cmd.getForceSave();
|
||||
let isSendForm = forceSave && forceSave.getType() === commonDefines.c_oAscForceSaveTypes.Form;
|
||||
let originFormat = cmd.getOriginFormat();
|
||||
let isOriginFormatWithForms = constants.AVS_OFFICESTUDIO_FILE_CROSSPLATFORM_PDF === originFormat ||
|
||||
constants.AVS_OFFICESTUDIO_FILE_DOCUMENT_OFORM === originFormat ||
|
||||
constants.AVS_OFFICESTUDIO_FILE_DOCUMENT_DOCXF === originFormat;
|
||||
let isFormatToPdf = constants.AVS_OFFICESTUDIO_FILE_CROSSPLATFORM_PDF === dataConvert.formatTo ||
|
||||
constants.AVS_OFFICESTUDIO_FILE_CROSSPLATFORM_PDFA === dataConvert.formatTo;
|
||||
if (isFormatToPdf && isOriginFormatWithForms && !isSendForm) {
|
||||
if (isFormatToPdf && isOriginFormatWithForms) {
|
||||
let format = await formatChecker.getDocumentFormatByFile(dataConvert.fileFrom);
|
||||
if (constants.AVS_OFFICESTUDIO_FILE_CANVAS_WORD === format) {
|
||||
ctx.logger.debug('change format to extended pdf');
|
||||
@ -1074,7 +1072,7 @@ function* ExecuteTask(ctx, task) {
|
||||
isInJwtToken = true;
|
||||
let fileInfo = wopiParams.commonInfo?.fileInfo;
|
||||
fileSize = fileInfo?.Size;
|
||||
({url, headers} = yield wopiClient.getWopiFileUrl(ctx, fileInfo, wopiParams.userAuth));
|
||||
({url, headers} = yield wopiUtils.getWopiFileUrl(ctx, fileInfo, wopiParams.userAuth));
|
||||
}
|
||||
if (undefined === fileSize || fileSize > 0) {
|
||||
error = yield* downloadFile(ctx, url, dataConvert.fileFrom, withAuthorization, isInJwtToken, headers);
|
||||
|
||||
91
branding/info/ai/CHANGELOG.md
Normal file
@ -0,0 +1,91 @@
|
||||
# Change Log
|
||||
|
||||
## 1.0.0
|
||||
|
||||
* Initial release.
|
||||
|
||||
## 1.0.1
|
||||
|
||||
* Add new model.
|
||||
|
||||
## 1.0.2
|
||||
|
||||
* Change plugin structure. Now it's non visual plugin and it can work by context menu.
|
||||
* Add new model for chat variation.
|
||||
|
||||
## 1.1.0
|
||||
|
||||
* Commont improvements.
|
||||
|
||||
## 1.1.1
|
||||
|
||||
* Add new languages for translating and add translations for it.
|
||||
* Change plugin type from "system" to "background".
|
||||
* Increase minimal editor version to "7.5.0".
|
||||
* Change type for chat window (now it's not a modal window).
|
||||
|
||||
## 1.1.2
|
||||
|
||||
* Disable plugin for IE.
|
||||
|
||||
## 1.1.3
|
||||
|
||||
* Add notification, that service isn't enable in user region (if we can't load list of models).
|
||||
|
||||
## 1.1.4
|
||||
* Change list of models for custom requests (add new models and remove old).
|
||||
* Change default model for request by context menu (now we use gpt-3.5-turbo-16k model for chat and gpt-4 for other request).
|
||||
* Add new functions: "Fix spelling & grammar", "Rewrite differently", "Make longer", "Make shorter", "Make simpler" (it has restriction by gpt-4 model: 8k tokens)
|
||||
|
||||
## 2.0.0
|
||||
* The plugin has been completely redesigned.
|
||||
|
||||
## 2.1.0
|
||||
* Bug fix.
|
||||
|
||||
## 2.1.1
|
||||
* Bug fix.
|
||||
|
||||
## 2.1.2
|
||||
* Fix add provider.
|
||||
|
||||
## 2.1.3
|
||||
* Bug fix. Remove v1 suffix for endpoints.
|
||||
|
||||
## 2.1.4
|
||||
* Add proxy for together.ai.
|
||||
* Add Groc as internal provider.
|
||||
* Bug fix
|
||||
|
||||
## 2.1.5
|
||||
* Bug fix
|
||||
|
||||
## 2.2.0
|
||||
* Refactoring
|
||||
|
||||
## 2.2.1
|
||||
* Bug fix
|
||||
|
||||
## 2.2.2
|
||||
* Add xAI as internal provider.
|
||||
|
||||
## 2.2.3
|
||||
* Fix translations.
|
||||
|
||||
## 2.2.4
|
||||
* Refactoring chat. Add docked mode for chat window.
|
||||
|
||||
## 2.2.5
|
||||
* Bug fix
|
||||
|
||||
## 2.2.6
|
||||
* Add interface for system role detection.
|
||||
* Fixed the work of providers antropic and gemini when working with a system role.
|
||||
* Fix bug with custom functions in spreadsheets editor.
|
||||
|
||||
## 2.2.7
|
||||
* Add image actions.
|
||||
* Add Stability AI provider.
|
||||
|
||||
## 2.2.8
|
||||
* Fix image actions. Add "OCR" and "Image to text" support.
|
||||
114
branding/info/ai/aiModelEdit.html
Normal file
@ -0,0 +1,114 @@
|
||||
<!--
|
||||
(c) Copyright Ascensio System SIA 2020
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<title>Edit action</title>
|
||||
|
||||
<script type="text/javascript" src="../js/plugins.js"></script>
|
||||
<script type="text/javascript" src="../js/plugins-ui.js"></script>
|
||||
<script type="text/javascript" src="scripts/engine/providers/base.js"></script>
|
||||
<script type="text/javascript" src="components/Tooltip/script.js"></script>
|
||||
<script type="text/javascript" src="../js/plugin-stub.js"></script>
|
||||
<script src="vendor/jquery/jquery-3.7.1.min.js"></script>
|
||||
<script src="vendor/select2-4.0.6-rc.1/dist/js/select2.js"></script>
|
||||
<link rel="stylesheet" href="../css/plugins.css">
|
||||
<link rel="stylesheet" href="vendor/select2-4.0.6-rc.1/dist/css/select2.css"/>
|
||||
<link rel="stylesheet" href="./resources/styles/common.css">
|
||||
<link rel="stylesheet" href="./resources/styles/aiModelEdit.css">
|
||||
<link rel="stylesheet" href="components/Tooltip/style.css">
|
||||
</head>
|
||||
<body class="noselect">
|
||||
<div class="section">
|
||||
<div class="form-vertical-item">
|
||||
<div class="section-label">
|
||||
<label class="d-flex-center">
|
||||
<span class="i18n">Model name</span>
|
||||
<span class="required-mark">*</span>
|
||||
</label>
|
||||
</div>
|
||||
<input id="name-input" type="text" class="form-control" maxlength="80"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-label" id="provider-section-label">
|
||||
<label class="i18n">Provider</label>
|
||||
</div>
|
||||
<div class="form-horizontal-item">
|
||||
<label class="d-flex-center">
|
||||
<span class="i18n">Name</span>
|
||||
<span class="required-mark">*</span>
|
||||
</label>
|
||||
<select id="provider-name-cmb" class="form-control"></select>
|
||||
</div>
|
||||
<div class="form-horizontal-item">
|
||||
<label class="d-flex-center">
|
||||
<span class="i18n">URL</span>
|
||||
<span class="required-mark">*</span>
|
||||
</label>
|
||||
<input id="provider-url-input" type="text" class="form-control"/>
|
||||
</div>
|
||||
<div class="form-horizontal-item">
|
||||
<label class="i18n">Key</label>
|
||||
<input id="provider-key-input" type="text" class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="form-vertical-item">
|
||||
<div class="section-label">
|
||||
<label class="d-flex-center">
|
||||
<span class="i18n">Model</span>
|
||||
<span class="required-mark">*</span>
|
||||
</label>
|
||||
<div id="update-models-row">
|
||||
<div id="update-models-loader-container" class="asc-loader-container"></div>
|
||||
<button id="update-models-btn" class="action-btn i18n">Update models list</button>
|
||||
<img id="update-models-error" class="icon" src="resources/icons/light/error.png" style="display: none;"/>
|
||||
</div>
|
||||
</div>
|
||||
<select id="model-name-cmb" class="form-control"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" style="margin-bottom: 0;">
|
||||
<div class="form-vertical-item">
|
||||
<div class="section-label">
|
||||
<label class="i18n">Use model for</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-horizontal-item" id="block-use-for" style="margin-bottom: 0;">
|
||||
<div id="use-for-text"></div>
|
||||
<div id="use-for-image"></div>
|
||||
<div id="use-for-embeddings"></div>
|
||||
<div id="use-for-audio"></div>
|
||||
<div id="use-for-moderations"></div>
|
||||
<div id="use-for-realtime"></div>
|
||||
<div id="use-for-code"></div>
|
||||
<div id="use-for-vision"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="custom-providers-button">
|
||||
<label class="i18n">
|
||||
Custom providers
|
||||
</label>
|
||||
</div>
|
||||
<script type="text/javascript" src="scripts/aiModelEdit.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
50
branding/info/ai/aiModelsList.html
Normal file
@ -0,0 +1,50 @@
|
||||
<!--
|
||||
(c) Copyright Ascensio System SIA 2020
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<title>AI Models list</title>
|
||||
|
||||
<script type="text/javascript" src="../js/plugins.js"></script>
|
||||
<script type="text/javascript" src="../js/plugins-ui.js"></script>
|
||||
<script type="text/javascript" src="./components/ListView/script.js"></script>
|
||||
<script type="text/javascript" src="../js/plugin-stub.js"></script>
|
||||
<script src="vendor/jquery/jquery-3.7.1.min.js"></script>
|
||||
<link rel="stylesheet" href="../css/plugins.css">
|
||||
<link rel="stylesheet" href="./resources/styles/common.css">
|
||||
<link rel="stylesheet" href="./components/ListView/style.css">
|
||||
<link rel="stylesheet" href="./resources/styles/aiModelsList.css">
|
||||
</head>
|
||||
<body class="noselect">
|
||||
<div id="ai-models-list" class="list">
|
||||
<!-- Dynamic render items -->
|
||||
</div>
|
||||
<div id="buttons-block">
|
||||
<button id="add-btn" class="btn-text-default">
|
||||
<img src="resources/icons/light/btn-zoomup.png" class="icon"/>
|
||||
</button>
|
||||
<button id="edit-btn" class="btn-text-default">
|
||||
<img src="resources/icons/light/btn-edit.png" class="icon"/>
|
||||
</button>
|
||||
<button id="delete-btn" class="btn-text-default">
|
||||
<img src="resources/icons/light/btn-remove.png" class="icon"/>
|
||||
</button>
|
||||
</div>
|
||||
<script type="text/javascript" src="scripts/aiModelsList.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
87
branding/info/ai/chat.html
Normal file
@ -0,0 +1,87 @@
|
||||
<!--
|
||||
(c) Copyright Ascensio System SIA 2020
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>OpenAI</title>
|
||||
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.js"></script>
|
||||
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins-ui.js"></script>
|
||||
<link rel="stylesheet" href="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.css">
|
||||
<script src="vendor/jquery/jquery-3.7.1.min.js"></script>
|
||||
<script type="text/javascript" src="vendor/GPT-3-Encoder/encoder.js"></script>
|
||||
<script src="vendor/md/markdown-it.js"></script>
|
||||
<script type="text/javascript" src="components/Tooltip/script.js"></script>
|
||||
<link rel="stylesheet" href="components/Tooltip/style.css">
|
||||
<link rel="stylesheet" href="./resources/styles/chat.css">
|
||||
<script type="text/javascript" src="scripts/chat.js"></script>
|
||||
</head>
|
||||
<body style="display: flex;">
|
||||
<div id="chat_window" class="chat_window hidden">
|
||||
<div id="chat_wrapper" class="form-control empty">
|
||||
<div id="chat" class="noselect">
|
||||
<div id="start_panel">
|
||||
<div id="welcome_text">
|
||||
<br/>
|
||||
<br/>
|
||||
<span class="i18n">I'm here to assist you with all your text creation and editing needs. Feel free to ask me anything about your document or anything else that's on your mind.</span>
|
||||
<br/>
|
||||
<br/>
|
||||
<span class="i18n">Let’s make your content shine together!</span>
|
||||
</div>
|
||||
<div id="welcome_buttons_list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tokens_info" class="tokens_info">
|
||||
<div class="form-control div_title_chat hidden">
|
||||
<span class="i18n">Maximum 16000 tokens are available.</span>
|
||||
<br>
|
||||
<span class="i18n">For work with this model we should save chat history and sent it into a request.</span>
|
||||
<span class="i18n">But we have a limit on the number of tokens in the request.</span>
|
||||
<span class="i18n">That's why sometimes you should clear your chat history.</span>
|
||||
<div class="text_align_end">
|
||||
<button id="clear_history" style="margin-top: 5px;" class="form-control btn-text-default i18n">Clear history</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text_align_end">
|
||||
<span class="i18n">Tokens in the request about: </span>
|
||||
<span id="cur_tokens">0</span>
|
||||
</div>
|
||||
<div class="text_align_end">
|
||||
<span class="i18n">Total tokens are used in last request: </span>
|
||||
<span id="total_tokens">0</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="attached_text_wrapper" class="hidden">
|
||||
<div id="attached_text"></div>
|
||||
<img id="attached_text_close" draggable="false" src="resources/icons/light/close.png" class="icon"/>
|
||||
</div>
|
||||
<div id="input_message_wrapper">
|
||||
<textarea id="input_message" rows="1" class="form-control" spellcheck="false"></textarea>
|
||||
<div id="input_message_submit" class="noselect">
|
||||
<img src="resources/icons/light/btn-demote.png" draggable="false" class="icon"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="loader-container" class="loader hidden"></div>
|
||||
<div id="div_err" class="hidden div_error err_background">
|
||||
<label class="lb_err i18n" style="font-weight: bold;">Error:</label>
|
||||
<label id="lb_err" class="lb_err"></label>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
175
branding/info/ai/components/ListView/script.js
Normal file
@ -0,0 +1,175 @@
|
||||
function ListView($el, options) {
|
||||
this._init = function() {
|
||||
var defaults = {
|
||||
emptyText: '',
|
||||
renderItem: function(item, index) {
|
||||
var itemEl = document.createElement('div');
|
||||
itemEl.classList.add('item');
|
||||
itemEl.innerText = item.label;
|
||||
return itemEl;
|
||||
}
|
||||
};
|
||||
this.options = Object.assign({}, defaults, options);
|
||||
|
||||
$el.classList.add('list-view');
|
||||
this.$el = $el;
|
||||
this.list = [];
|
||||
this.selectedItem = null;
|
||||
this.events = {
|
||||
set: [],
|
||||
add: [],
|
||||
edit: [],
|
||||
delete: [],
|
||||
select: [],
|
||||
deselect: [],
|
||||
};
|
||||
|
||||
this._render();
|
||||
};
|
||||
|
||||
this._render = function() {
|
||||
var me = this;
|
||||
this.$el.innerHTML = '';
|
||||
if(this.list.length > 0) {
|
||||
this.list.forEach(function(item, index) {
|
||||
let itemEl = me.options.renderItem(item, index);
|
||||
itemEl.addEventListener('click', function() {
|
||||
me.setSelected(index);
|
||||
});
|
||||
me.$el.appendChild(itemEl);
|
||||
});
|
||||
} else if(this.options.emptyText) {
|
||||
let emptyEl = document.createElement('div');
|
||||
emptyEl.innerText = this.options.emptyText;
|
||||
emptyEl.classList.add('empty-text');
|
||||
this.$el.appendChild(emptyEl);
|
||||
}
|
||||
};
|
||||
|
||||
this._trigger = function(event, data) {
|
||||
if(this.events[event]) {
|
||||
this.events[event].forEach(function(callback) {
|
||||
callback(data);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.getList = function() {
|
||||
return this.list;
|
||||
};
|
||||
|
||||
this.set = function(list) {
|
||||
this.list = list;
|
||||
this._render();
|
||||
if(list.length > 0) {
|
||||
this.setSelected(0);
|
||||
} else {
|
||||
this.deselect();
|
||||
}
|
||||
scrollbarList.update();
|
||||
scrollbarList.update();
|
||||
|
||||
this._trigger('set', list);
|
||||
};
|
||||
|
||||
this.add = function(item) {
|
||||
this.list.push(item);
|
||||
this._render();
|
||||
this.setSelected(this.list.length - 1);
|
||||
this.$el.scrollTop = this.$el.scrollHeight;
|
||||
scrollbarList.update();
|
||||
scrollbarList.update();
|
||||
|
||||
this._trigger('add', item);
|
||||
};
|
||||
|
||||
this.edit = function(item) {
|
||||
var findedItem = this.list.filter(function(el) {
|
||||
return el.id == item.id;
|
||||
})[0];
|
||||
|
||||
if(!findedItem) return;
|
||||
|
||||
for (var key in item) {
|
||||
if (findedItem[key]) {
|
||||
findedItem[key] = item[key];
|
||||
}
|
||||
}
|
||||
this._render();
|
||||
|
||||
this._trigger('edit', findedItem, item);
|
||||
};
|
||||
|
||||
this.delete = function(item) {
|
||||
var indexDeletedItem = this.list.indexOf(item);
|
||||
if(indexDeletedItem != -1) {
|
||||
this.list = this.list.filter(function(el) {
|
||||
return el != item;
|
||||
});
|
||||
this._render();
|
||||
|
||||
if(this.list.length == 0) {
|
||||
this.deselect();
|
||||
} else if(indexDeletedItem < this.list.length) {
|
||||
this.setSelected(indexDeletedItem);
|
||||
} else {
|
||||
this.setSelected(this.list.length - 1);
|
||||
}
|
||||
|
||||
this._trigger('delete', item);
|
||||
}
|
||||
};
|
||||
|
||||
this.deleteByIndex = function(index) {
|
||||
if(!this.list[index]) return;
|
||||
|
||||
this.delete(this.list[index]);
|
||||
};
|
||||
|
||||
this.setSelected = function(index) {
|
||||
if(this.list.length == 0) return;
|
||||
|
||||
if(index == -1) {
|
||||
index = this.list.length - 1;
|
||||
}
|
||||
if(!this.list[index]) return;
|
||||
|
||||
this.deselect();
|
||||
this.selectedItem = this.list[index];
|
||||
this.$el.children[index].classList.add('selected');
|
||||
|
||||
this._trigger('select', this.selectedItem);
|
||||
};
|
||||
|
||||
this.getSelected = function() {
|
||||
return this.selectedItem;
|
||||
};
|
||||
|
||||
this.deselect = function() {
|
||||
var previouslySelectedItem = this.selectedItem;
|
||||
this.selectedItem = null;
|
||||
|
||||
var itemsEl = this.$el.getElementsByClassName('item');
|
||||
for (var i = 0; i < itemsEl.length; i++) {
|
||||
itemsEl[i].classList.remove('selected');
|
||||
}
|
||||
|
||||
if (previouslySelectedItem) {
|
||||
this._trigger('deselect', previouslySelectedItem);
|
||||
}
|
||||
};
|
||||
|
||||
this.setEmptyText = function(text) {
|
||||
this.options.emptyText = text;
|
||||
this._render();
|
||||
};
|
||||
|
||||
this.on = function(event, callback) {
|
||||
if(this.events[event]) {
|
||||
this.events[event].push(callback);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
this._init();
|
||||
}
|
||||
85
branding/info/ai/components/ListView/style.css
Normal file
@ -0,0 +1,85 @@
|
||||
.list-view {
|
||||
flex: 1;
|
||||
border: 1px solid;
|
||||
position: relative;
|
||||
}
|
||||
.list-view .item {
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
.list-view .empty-text {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Themes style for Models List */
|
||||
body.theme-classic-light .list-view {
|
||||
border-color: #cfcfcf;
|
||||
}
|
||||
body.theme-light .list-view,
|
||||
body.theme-gray .list-view {
|
||||
border-color: #c0c0c0;
|
||||
}
|
||||
body.theme-dark .list-view {
|
||||
border-color: #666;
|
||||
}
|
||||
body.theme-contrast-dark .list-view {
|
||||
border-color: #696969;
|
||||
}
|
||||
|
||||
/* Themes style for Item Models List */
|
||||
body.theme-classic-light .list-view .item {
|
||||
border-color: #cfcfcf;
|
||||
}
|
||||
body.theme-classic-light .list-view .item:hover {
|
||||
background-color: #d8dadc;
|
||||
color: #444;
|
||||
}
|
||||
body.theme-classic-light .list-view .item.selected {
|
||||
background-color: #7d858c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
body.theme-light .list-view .item,
|
||||
body.theme-gray .list-view .item {
|
||||
border-color: #c0c0c0;
|
||||
}
|
||||
body.theme-light .list-view .item:hover,
|
||||
body.theme-gray .list-view .item:hover {
|
||||
background-color: #e0e0e0;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
body.theme-light .list-view .item.selected,
|
||||
body.theme-gray .list-view .item.selected {
|
||||
background-color: #cbcbcb;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
body.theme-dark .list-view .item {
|
||||
border-color: #666;
|
||||
}
|
||||
body.theme-dark .list-view .item:hover {
|
||||
background-color: #555;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
body.theme-dark .list-view .item.selected {
|
||||
background-color: #707070;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
body.theme-contrast-dark .list-view .item {
|
||||
border-color: #696969;
|
||||
}
|
||||
body.theme-contrast-dark .list-view .item:hover {
|
||||
background-color: #424242;
|
||||
color: #e8e8e8;
|
||||
}
|
||||
body.theme-contrast-dark .list-view .item.selected {
|
||||
background-color: #666666;
|
||||
color: #e8e8e8;
|
||||
}
|
||||
128
branding/info/ai/components/Tooltip/script.js
Normal file
@ -0,0 +1,128 @@
|
||||
function Tooltip(targetEl, options) {
|
||||
this._init = function() {
|
||||
var me = this;
|
||||
var defaults = {
|
||||
renderInner: null,
|
||||
text: '',
|
||||
xAnchor: 'center',
|
||||
yAnchor: 'bottom',
|
||||
xOffset: 0,
|
||||
yOffset: 3,
|
||||
align: 'center',
|
||||
width: null,
|
||||
hasShadow: false,
|
||||
keepAliveOnHover: false,
|
||||
delay: 0,
|
||||
hideDelay: 0
|
||||
};
|
||||
this.options = Object.assign({}, defaults, options);
|
||||
|
||||
|
||||
targetEl.addEventListener('mouseover', function() {
|
||||
// Set a timer for the delay before showing
|
||||
me.showTimeout = setTimeout(function() {
|
||||
me._createTooltipElement();
|
||||
if(me.options.renderInner) {
|
||||
me.tooltipEl.appendChild(me.options.renderInner());
|
||||
} else {
|
||||
me.tooltipEl.innerText = me.options.text;
|
||||
}
|
||||
if (me.options.width) {
|
||||
me.tooltipEl.style.width = me.options.width + 'px';
|
||||
me.tooltipEl.style.whiteSpace = '';
|
||||
} else {
|
||||
me.tooltipEl.style.width = '';
|
||||
me.tooltipEl.style.whiteSpace = 'nowrap';
|
||||
}
|
||||
|
||||
if (me.options.hasShadow) {
|
||||
me.tooltipEl.classList.add('has-shadow');
|
||||
} else {
|
||||
me.tooltipEl.classList.remove('has-shadow');
|
||||
}
|
||||
me._updatePosition();
|
||||
}, me.options.delay);
|
||||
});
|
||||
|
||||
targetEl.addEventListener('mouseleave', function() {
|
||||
clearTimeout(me.showTimeout); // Clear the show timer if the user leaves the element
|
||||
|
||||
// Set a timer for the delay before hiding
|
||||
me.hideTimeout = setTimeout(function() {
|
||||
me._deleteTooltipElement();
|
||||
}, me.options.hideDelay);
|
||||
});
|
||||
};
|
||||
|
||||
this._createTooltipElement = function () {
|
||||
if (!this.tooltipEl) {
|
||||
let me = this;
|
||||
this.tooltipEl = document.createElement("div");
|
||||
this.tooltipEl.classList.add("tooltip");
|
||||
|
||||
this.tooltipEl.addEventListener('mouseenter', function() {
|
||||
if (me.options.keepAliveOnHover) {
|
||||
clearTimeout(me.hideTimeout); // Clear the hide timer when hovering over the tooltip
|
||||
}
|
||||
});
|
||||
|
||||
this.tooltipEl.addEventListener('mouseleave', function() {
|
||||
if (me.options.keepAliveOnHover) {
|
||||
// Set a timer for the delay before hiding the tooltip
|
||||
me.hideTimeout = setTimeout(function() {
|
||||
me._deleteTooltipElement();
|
||||
}, me.options.hideDelay);
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(this.tooltipEl);
|
||||
}
|
||||
};
|
||||
|
||||
this._deleteTooltipElement = function() {
|
||||
if (this.tooltipEl) {
|
||||
this.tooltipEl.remove();
|
||||
this.tooltipEl = null;
|
||||
}
|
||||
};
|
||||
|
||||
this._updatePosition = function() {
|
||||
var rectTooltip = this.tooltipEl.getBoundingClientRect();
|
||||
var rectEl = targetEl.getBoundingClientRect();
|
||||
var yOffset = this.options.yOffset;
|
||||
var xOffset = this.options.xOffset;
|
||||
if (this.options.align == 'right') {
|
||||
xOffset = -rectTooltip.width;
|
||||
} else if (this.options.align == 'center') {
|
||||
xOffset = -rectTooltip.width / 2;
|
||||
}
|
||||
|
||||
if (this.options.xAnchor == 'right') {
|
||||
this.tooltipEl.style.left = rectEl.right + xOffset + 'px';
|
||||
} else if (this.options.xAnchor == 'left') {
|
||||
this.tooltipEl.style.left = rectEl.left + xOffset + 'px';
|
||||
} else if (this.options.xAnchor == 'center') {
|
||||
this.tooltipEl.style.left = rectEl.left + rectEl.width / 2 + xOffset + 'px';
|
||||
}
|
||||
|
||||
if (this.options.yAnchor == 'bottom') {
|
||||
this.tooltipEl.style.top = rectEl.bottom + yOffset + 'px';
|
||||
} else if (this.options.yAnchor == 'top') {
|
||||
this.tooltipEl.style.top = rectEl.top - yOffset - rectTooltip.height + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
this.getText = function() {
|
||||
return this.options.text;
|
||||
};
|
||||
|
||||
this.setText = function(text) {
|
||||
this.options.text = text;
|
||||
if(this.tooltipEl) {
|
||||
this.tooltipEl.innerText = text;
|
||||
this._updatePosition();
|
||||
}
|
||||
};
|
||||
|
||||
this._init();
|
||||
}
|
||||
29
branding/info/ai/components/Tooltip/style.css
Normal file
@ -0,0 +1,29 @@
|
||||
.tooltip {
|
||||
line-height: 12px;
|
||||
word-break: break-word;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.tooltip.has-shadow {
|
||||
box-shadow: 0px 2px 5px #00000033;
|
||||
}
|
||||
body.theme-light .tooltip,
|
||||
body.theme-gray .tooltip {
|
||||
background-color: #fff;
|
||||
border-color: #c0c0c0;
|
||||
}
|
||||
body.theme-classic-light .tooltip {
|
||||
background-color: #fff;
|
||||
border-color: #cfcfcf;
|
||||
}
|
||||
body.theme-dark .tooltip {
|
||||
background-color: #333;
|
||||
border-color: #666;
|
||||
}
|
||||
body.theme-contrast-dark .tooltip {
|
||||
background-color: #1e1e1e;
|
||||
border-color: #696969;
|
||||
}
|
||||
73
branding/info/ai/config.json
Normal file
@ -0,0 +1,73 @@
|
||||
{
|
||||
"name" : "AI",
|
||||
"nameLocale": {
|
||||
"ru": "ИИ",
|
||||
"fr": "AI",
|
||||
"es": "AI",
|
||||
"de": "AI",
|
||||
"cs": "AI",
|
||||
"zh": "AI",
|
||||
"pt-BR": "AI",
|
||||
"sr-Cyrl-RS": "AI",
|
||||
"sr-Latn-RS": "AI",
|
||||
"ja-JA": "AI",
|
||||
"sq-AL": "AI",
|
||||
"it": "IA"
|
||||
},
|
||||
|
||||
"guid" : "asc.{9DC93CDB-B576-4F0C-B55E-FCC9C48DD007}",
|
||||
"version": "2.2.8",
|
||||
"minVersion" : "8.2.0",
|
||||
|
||||
"variations" : [
|
||||
{
|
||||
"description": "Use the AI chatbot to perform tasks which involve understanding or generating natural language or code.",
|
||||
"descriptionLocale": {
|
||||
"ru": "Используйте чат-бот AI для выполнения задач, связанных с пониманием или генерацией естественного языка или кода.",
|
||||
"fr": "Utilisez le chatbot AI pour effectuer des tâches qui impliquent la compréhension ou la génération de langage naturel ou de code.",
|
||||
"es": "Utilice el chatbot AI para realizar tareas que impliquen la comprensión o generación de lenguaje natural o de código.",
|
||||
"pt-BR": "Use o chatbot AI para realizar tarefas que envolvam compreensão ou geração de linguagem ou código natural.",
|
||||
"de": "Verwenden Sie den AI-Chatbot, um Aufgaben auszuführen, die das Verstehen oder Generieren von natürlicher Sprache oder Code beinhalten.",
|
||||
"cs": "Použijte chatbota AI k provádění úkolů, který zahrnuje porozumění nebo generování přirozeného jazyka nebo kódu.",
|
||||
"zh": "使用 AI 聊天机器人完成有关理解、生成自然语言或代码的任务。",
|
||||
"sr-Cyrl-RS": "Користите AI чет робота за обављање задатака који укључују разумевање или генерисање природног језика или кода.",
|
||||
"sr-Latn-RS": "Koristite AI čet robota za obavljanje zadataka koji uključuju razumevanje ili generisanje prirodnog jezika ili koda.",
|
||||
"ja-JA": "自然言語やコードの理解または生成が必要なタスクを行うには、AIチャットボットを使用できます。",
|
||||
"sq-AL": "Shtoni dhe selektoni modele AI për detyra të ndryshme.",
|
||||
"it": "Utilizza il chatbot dell'IA per eseguire attività che implicano la comprensione o la generazione di codice o linguaggio naturale."
|
||||
},
|
||||
|
||||
"url" : "index.html",
|
||||
|
||||
"icons": "resources/%theme-type%(light|dark)/icon%scale%(default).%extension%(png)",
|
||||
|
||||
"isViewer" : false,
|
||||
"EditorsSupport" : ["word", "cell", "slide", "pdf"],
|
||||
"type" : "background",
|
||||
"initDataType" : "none",
|
||||
"buttons" : [],
|
||||
"events" : ["onContextMenuShow", "onContextMenuClick", "onToolbarMenuClick"],
|
||||
|
||||
"store": {
|
||||
"background": {
|
||||
"light" : "linear-gradient(90deg, #F9B6FF 0%, #E370EE 102.01%)",
|
||||
"dark" : "linear-gradient(90deg, #F9B6FF 0%, #E370EE 102.01%)"
|
||||
},
|
||||
"screenshots" :
|
||||
[
|
||||
"resources/store/screenshots/screen_1.png",
|
||||
"resources/store/screenshots/screen_2.png",
|
||||
"resources/store/screenshots/screen_3.png",
|
||||
"resources/store/screenshots/screen_4.png",
|
||||
"resources/store/screenshots/screen_5.png",
|
||||
"resources/store/screenshots/screen_6.png"
|
||||
],
|
||||
"icons" : {
|
||||
"light" : "resources/store/icons",
|
||||
"dark" : "resources/store/icons"
|
||||
},
|
||||
"categories": ["specAbilities", "work", "recommended"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
56
branding/info/ai/customProviders.html
Normal file
@ -0,0 +1,56 @@
|
||||
<!--
|
||||
(c) Copyright Ascensio System SIA 2020
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<title>Custom providers</title>
|
||||
|
||||
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.js"></script>
|
||||
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins-ui.js"></script>
|
||||
<script type="text/javascript" src="components/Tooltip/script.js"></script>
|
||||
<script type="text/javascript" src="components/ListView/script.js"></script>
|
||||
<script src="vendor/jquery/jquery-3.7.1.min.js"></script>
|
||||
<link rel="stylesheet" href="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.css">
|
||||
<link rel="stylesheet" href="./resources/styles/common.css">
|
||||
<link rel="stylesheet" href="./resources/styles/customProviders.css">
|
||||
<link rel="stylesheet" href="components/Tooltip/style.css">
|
||||
<link rel="stylesheet" href="components/ListView/style.css">
|
||||
</head>
|
||||
<body class="noselect">
|
||||
<div id="label-row">
|
||||
<label class="i18n">Connected custom providers</label>
|
||||
<img id="alert-icon" class="icon" src="resources/icons/light/info.png"/>
|
||||
</div>
|
||||
<div id="list-row">
|
||||
<div id="providers-list" class="empty">
|
||||
<!-- Dynamic render items -->
|
||||
</div>
|
||||
<div id="buttons-block">
|
||||
<button id="add-btn" class="btn-text-default">
|
||||
<img src="resources/icons/light/btn-zoomup.png" class="icon"/>
|
||||
</button>
|
||||
<button id="delete-btn" class="btn-text-default" disabled>
|
||||
<img src="resources/icons/light/btn-remove.png" class="icon"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<label id="error-label" class="hide"></label>
|
||||
<input id="file-input" type="file" multiple accept=".js"/>
|
||||
<script type="text/javascript" src="scripts/customProviders.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
branding/info/ai/deploy/ai.plugin
Normal file
35
branding/info/ai/hyperlink.html
Normal file
@ -0,0 +1,35 @@
|
||||
<!--
|
||||
(c) Copyright Ascensio System SIA 2020
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>ChatGPT</title>
|
||||
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.js"></script>
|
||||
<script type="text/javascript" src="scripts/hyperlink.js"></script>
|
||||
<style>
|
||||
html,body,iframe {
|
||||
margin: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<iframe id="iframe"></iframe>
|
||||
</body>
|
||||
</html>
|
||||
43
branding/info/ai/index.html
Normal file
@ -0,0 +1,43 @@
|
||||
<!--
|
||||
(c) Copyright Ascensio System SIA 2020
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>AI Constructor</title>
|
||||
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.js"></script>
|
||||
<script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins-ui.js"></script>
|
||||
<link rel="stylesheet" href="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.css">
|
||||
|
||||
<script type="text/javascript" src="vendor/GPT-3-Encoder/encoder.js"></script>
|
||||
|
||||
<script type="text/javascript" src="scripts/engine/storage.js"></script>
|
||||
<script type="text/javascript" src="scripts/engine/local_storage.js"></script>
|
||||
<script type="text/javascript" src="scripts/engine/providers/base.js"></script>
|
||||
<script type="text/javascript" src="scripts/engine/providers/provider.js"></script>
|
||||
<script type="text/javascript" src="scripts/engine/engine.js"></script>
|
||||
|
||||
<script type="text/javascript" src="scripts/engine/library.js"></script>
|
||||
<script type="text/javascript" src="scripts/engine/buttons.js"></script>
|
||||
<script type="text/javascript" src="scripts/engine/register.js"></script>
|
||||
|
||||
<script type="text/javascript" src="scripts/code.js"></script>
|
||||
|
||||
<script src="vendor/md/markdown-it.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
BIN
branding/info/ai/resources/dark/icon.png
Normal file
|
After Width: | Height: | Size: 245 B |
BIN
branding/info/ai/resources/dark/icon@1.25x.png
Normal file
|
After Width: | Height: | Size: 277 B |
BIN
branding/info/ai/resources/dark/icon@1.5x.png
Normal file
|
After Width: | Height: | Size: 346 B |
BIN
branding/info/ai/resources/dark/icon@1.75x.png
Normal file
|
After Width: | Height: | Size: 381 B |
BIN
branding/info/ai/resources/dark/icon@2x.png
Normal file
|
After Width: | Height: | Size: 456 B |
BIN
branding/info/ai/resources/icons/dark/ai-audio.png
Normal file
|
After Width: | Height: | Size: 117 B |
BIN
branding/info/ai/resources/icons/dark/ai-audio@1.25x.png
Normal file
|
After Width: | Height: | Size: 121 B |
BIN
branding/info/ai/resources/icons/dark/ai-audio@1.5x.png
Normal file
|
After Width: | Height: | Size: 125 B |
BIN
branding/info/ai/resources/icons/dark/ai-audio@1.75x.png
Normal file
|
After Width: | Height: | Size: 133 B |
BIN
branding/info/ai/resources/icons/dark/ai-audio@2x.png
Normal file
|
After Width: | Height: | Size: 147 B |
BIN
branding/info/ai/resources/icons/dark/ai-code.png
Normal file
|
After Width: | Height: | Size: 163 B |
BIN
branding/info/ai/resources/icons/dark/ai-code@1.25x.png
Normal file
|
After Width: | Height: | Size: 173 B |
BIN
branding/info/ai/resources/icons/dark/ai-code@1.5x.png
Normal file
|
After Width: | Height: | Size: 247 B |
BIN
branding/info/ai/resources/icons/dark/ai-code@1.75x.png
Normal file
|
After Width: | Height: | Size: 213 B |
BIN
branding/info/ai/resources/icons/dark/ai-code@2x.png
Normal file
|
After Width: | Height: | Size: 238 B |
BIN
branding/info/ai/resources/icons/dark/ai-embeddings.png
Normal file
|
After Width: | Height: | Size: 187 B |
BIN
branding/info/ai/resources/icons/dark/ai-embeddings@1.25x.png
Normal file
|
After Width: | Height: | Size: 209 B |
BIN
branding/info/ai/resources/icons/dark/ai-embeddings@1.5x.png
Normal file
|
After Width: | Height: | Size: 227 B |
BIN
branding/info/ai/resources/icons/dark/ai-embeddings@1.75x.png
Normal file
|
After Width: | Height: | Size: 264 B |
BIN
branding/info/ai/resources/icons/dark/ai-embeddings@2x.png
Normal file
|
After Width: | Height: | Size: 306 B |
BIN
branding/info/ai/resources/icons/dark/ai-images.png
Normal file
|
After Width: | Height: | Size: 177 B |
BIN
branding/info/ai/resources/icons/dark/ai-images@1.25x.png
Normal file
|
After Width: | Height: | Size: 177 B |
BIN
branding/info/ai/resources/icons/dark/ai-images@1.5x.png
Normal file
|
After Width: | Height: | Size: 215 B |
BIN
branding/info/ai/resources/icons/dark/ai-images@1.75x.png
Normal file
|
After Width: | Height: | Size: 215 B |
BIN
branding/info/ai/resources/icons/dark/ai-images@2x.png
Normal file
|
After Width: | Height: | Size: 294 B |
BIN
branding/info/ai/resources/icons/dark/ai-moderations.png
Normal file
|
After Width: | Height: | Size: 172 B |
BIN
branding/info/ai/resources/icons/dark/ai-moderations@1.25x.png
Normal file
|
After Width: | Height: | Size: 204 B |
BIN
branding/info/ai/resources/icons/dark/ai-moderations@1.5x.png
Normal file
|
After Width: | Height: | Size: 236 B |
BIN
branding/info/ai/resources/icons/dark/ai-moderations@1.75x.png
Normal file
|
After Width: | Height: | Size: 255 B |
BIN
branding/info/ai/resources/icons/dark/ai-moderations@2x.png
Normal file
|
After Width: | Height: | Size: 269 B |
BIN
branding/info/ai/resources/icons/dark/ai-realtime.png
Normal file
|
After Width: | Height: | Size: 229 B |
BIN
branding/info/ai/resources/icons/dark/ai-realtime@1.25x.png
Normal file
|
After Width: | Height: | Size: 279 B |
BIN
branding/info/ai/resources/icons/dark/ai-realtime@1.5x.png
Normal file
|
After Width: | Height: | Size: 297 B |
BIN
branding/info/ai/resources/icons/dark/ai-realtime@1.75x.png
Normal file
|
After Width: | Height: | Size: 335 B |
BIN
branding/info/ai/resources/icons/dark/ai-realtime@2x.png
Normal file
|
After Width: | Height: | Size: 417 B |
BIN
branding/info/ai/resources/icons/dark/ai-texts.png
Normal file
|
After Width: | Height: | Size: 101 B |
BIN
branding/info/ai/resources/icons/dark/ai-texts@1.25x.png
Normal file
|
After Width: | Height: | Size: 104 B |
BIN
branding/info/ai/resources/icons/dark/ai-texts@1.5x.png
Normal file
|
After Width: | Height: | Size: 108 B |
BIN
branding/info/ai/resources/icons/dark/ai-texts@1.75x.png
Normal file
|
After Width: | Height: | Size: 113 B |
BIN
branding/info/ai/resources/icons/dark/ai-texts@2x.png
Normal file
|
After Width: | Height: | Size: 123 B |
BIN
branding/info/ai/resources/icons/dark/ai-visual-analysis.png
Normal file
|
After Width: | Height: | Size: 201 B |
|
After Width: | Height: | Size: 237 B |
|
After Width: | Height: | Size: 262 B |
|
After Width: | Height: | Size: 289 B |
BIN
branding/info/ai/resources/icons/dark/ai-visual-analysis@2x.png
Normal file
|
After Width: | Height: | Size: 340 B |
BIN
branding/info/ai/resources/icons/dark/ask-ai.png
Normal file
|
After Width: | Height: | Size: 162 B |
BIN
branding/info/ai/resources/icons/dark/ask-ai@1.25x.png
Normal file
|
After Width: | Height: | Size: 179 B |
BIN
branding/info/ai/resources/icons/dark/ask-ai@1.5x.png
Normal file
|
After Width: | Height: | Size: 202 B |
BIN
branding/info/ai/resources/icons/dark/ask-ai@1.75x.png
Normal file
|
After Width: | Height: | Size: 210 B |
BIN
branding/info/ai/resources/icons/dark/ask-ai@2x.png
Normal file
|
After Width: | Height: | Size: 245 B |
BIN
branding/info/ai/resources/icons/dark/big/ask-ai.png
Normal file
|
After Width: | Height: | Size: 202 B |
BIN
branding/info/ai/resources/icons/dark/big/ask-ai@1.25x.png
Normal file
|
After Width: | Height: | Size: 210 B |
BIN
branding/info/ai/resources/icons/dark/big/ask-ai@1.5x.png
Normal file
|
After Width: | Height: | Size: 263 B |
BIN
branding/info/ai/resources/icons/dark/big/ask-ai@1.75x.png
Normal file
|
After Width: | Height: | Size: 289 B |
BIN
branding/info/ai/resources/icons/dark/big/ask-ai@2x.png
Normal file
|
After Width: | Height: | Size: 345 B |
BIN
branding/info/ai/resources/icons/dark/big/default.png
Normal file
|
After Width: | Height: | Size: 284 B |
BIN
branding/info/ai/resources/icons/dark/big/default@1.25.png
Normal file
|
After Width: | Height: | Size: 327 B |