diff --git a/Common/sources/constants.js b/Common/sources/constants.js index 6354a46c..3e8e7aaa 100644 --- a/Common/sources/constants.js +++ b/Common/sources/constants.js @@ -86,6 +86,7 @@ exports.LICENSE_RESULT = { }; exports.LICENSE_CONNECTIONS = 20; +exports.LICENSE_USERS = 3; exports.LICENSE_EXPIRE_USERS_ONE_DAY = 24 * 60 * 60; // day in seconds exports.AVS_OFFICESTUDIO_FILE_UNKNOWN = 0x0000; diff --git a/Common/sources/tenantManager.js b/Common/sources/tenantManager.js index aebbed9c..9ea972a6 100644 --- a/Common/sources/tenantManager.js +++ b/Common/sources/tenantManager.js @@ -42,7 +42,6 @@ const utils = require('./../../Common/sources/utils'); const { readFile } = require('fs/promises'); const path = require('path'); -const oPackageType = config.get('license.packageType'); const cfgTenantsBaseDomain = config.get('tenants.baseDomain'); const cfgTenantsBaseDir = config.get('tenants.baseDir'); const cfgTenantsFilenameSecret = config.get('tenants.filenameSecret'); @@ -210,22 +209,22 @@ function fixTenantLicense(ctx, licenseInfo, licenseInfoTenant) { errors.push('advanced_api'); } //can not up limits - if (licenseInfo.connections < licenseInfoTenant.connections) { - licenseInfoTenant.connections = licenseInfo.connections; - errors.push('connections'); - } - if (licenseInfo.connectionsView < licenseInfoTenant.connectionsView) { - licenseInfoTenant.connectionsView = licenseInfo.connectionsView; - errors.push('connections_view'); - } - if (licenseInfo.usersCount < licenseInfoTenant.usersCount) { - licenseInfoTenant.usersCount = licenseInfo.usersCount; - errors.push('users_count'); - } - if (licenseInfo.usersViewCount < licenseInfoTenant.usersViewCount) { - licenseInfoTenant.usersViewCount = licenseInfo.usersViewCount; - errors.push('users_view_count'); - } + // if (licenseInfo.connections < licenseInfoTenant.connections) { + // licenseInfoTenant.connections = licenseInfo.connections; + // errors.push('connections'); + // } + // if (licenseInfo.connectionsView < licenseInfoTenant.connectionsView) { + // licenseInfoTenant.connectionsView = licenseInfo.connectionsView; + // errors.push('connections_view'); + // } + // if (licenseInfo.usersCount < licenseInfoTenant.usersCount) { + // licenseInfoTenant.usersCount = licenseInfo.usersCount; + // errors.push('users_count'); + // } + // if (licenseInfo.usersViewCount < licenseInfoTenant.usersViewCount) { + // licenseInfoTenant.usersViewCount = licenseInfo.usersViewCount; + // errors.push('users_view_count'); + // } if (licenseInfo.endDate && licenseInfoTenant.endDate && licenseInfo.endDate < licenseInfoTenant.endDate) { licenseInfoTenant.endDate = licenseInfo.endDate; errors.push('end_date'); @@ -325,6 +324,9 @@ async function readLicenseTenant(ctx, licenseFile, baseVerifiedLicense) { if (oLicense.hasOwnProperty('advanced_api')) { res.advancedApi = !!oLicense['advanced_api']; } + if (oLicense.hasOwnProperty('process')) { + res.connections = Math.max(res.count, oLicense['process'] >> 0) * 75; + } if (oLicense.hasOwnProperty('connections')) { res.connections = oLicense['connections'] >> 0; } @@ -344,19 +346,29 @@ async function readLicenseTenant(ctx, licenseFile, baseVerifiedLicense) { const timeLimited = 0 !== (res.mode & c_LM.Limited); - const checkDate = ((res.mode & c_LM.Trial) || timeLimited) ? new Date() : oBuildDate; + const checkDate = ((res.mode & c_LM.Trial) || timeLimited) ? new Date() : licenseInfo.buildDate; //Calendar check of start_date allows to issue a license for old versions const checkStartDate = new Date(); if (startDate <= checkStartDate && checkDate <= endDate && (!oLicense.hasOwnProperty('version') || 2 <= oLicense['version'])) { - if (oLicense.hasOwnProperty('process')) { - res.connections = Math.max(res.count, oLicense['process'] >> 0) * 75; - } res.type = c_LR.Success; } else if (startDate > checkStartDate) { res.type = c_LR.NotBefore; ctx.logger.warn('License: License not active before start_date:%s.', startDate.toISOString()); } else if (timeLimited) { - res.type = c_LR.ExpiredLimited; + // 30 days after end license = limited mode with 20 Connections + if (endDate.setDate(checkDate.getDate() + 30) >= checkDate) { + res.type = c_LR.SuccessLimit; + res.connections = Math.min(res.connections, constants.LICENSE_CONNECTIONS); + res.connectionsView = Math.min(res.connectionsView, constants.LICENSE_CONNECTIONS); + res.usersCount = Math.min(res.usersCount, constants.LICENSE_USERS); + 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 ` + + 'license to restore the full access'); + } else { + res.type = c_LR.ExpiredLimited; + } } else if (0 !== (res.mode & c_LM.Trial)) { res.type = c_LR.ExpiredTrial; } else { @@ -389,7 +401,7 @@ async function readLicenseTenant(ctx, licenseFile, baseVerifiedLicense) { } return [res, oLicense]; -}; +} exports.getDefautTenant = getDefautTenant; exports.getTenantByConnection = getTenantByConnection; diff --git a/Common/sources/utils.js b/Common/sources/utils.js index 29303716..7a5f2b81 100644 --- a/Common/sources/utils.js +++ b/Common/sources/utils.js @@ -310,8 +310,8 @@ function downloadUrlPromiseWithoutRedirect(ctx, uri, optTimeout, optLimit, opt_A let options = config.util.extendDeep({}, tenTenantRequestDefaults); Object.assign(options, {uri: urlParsed, encoding: null, timeout: connectionAndInactivity, followRedirect: false}); if (opt_filterPrivate) { - const options = Object.assign({}, https.globalAgent.options, tenRequesFilteringAgent); - options.agent = getRequestFilterAgent(uri, options); + const agentOptions = Object.assign({}, https.globalAgent.options, tenRequesFilteringAgent); + options.agent = getRequestFilterAgent(uri, agentOptions); } else { //baseRequest creates new agent(win-ca injects in globalAgent) options.agentOptions = https.globalAgent.options; diff --git a/DocService/sources/DocsCoServer.js b/DocService/sources/DocsCoServer.js index a045a83b..3a5ef7d0 100644 --- a/DocService/sources/DocsCoServer.js +++ b/DocService/sources/DocsCoServer.js @@ -898,7 +898,7 @@ async function applyForceSaveCache(ctx, docId, forceSave, type, opt_userConnecti } return res; } -async function startForceSave(ctx, docId, type, opt_userdata, opt_formdata, opt_userId, opt_userConnectionId, opt_userConnectionDocId, opt_userIndex, opt_responseKey, opt_baseUrl, opt_queue, opt_pubsub) { +async function startForceSave(ctx, docId, type, opt_userdata, opt_formdata, opt_userId, opt_userConnectionId, opt_userConnectionDocId, opt_userIndex, opt_responseKey, opt_baseUrl, opt_queue, opt_pubsub, opt_conn) { ctx.logger.debug('startForceSave start'); let res = {code: commonDefines.c_oAscServerCommandErrors.NoError, time: null, inProgress: false}; let startedForceSave; @@ -910,6 +910,16 @@ async function startForceSave(ctx, docId, type, opt_userdata, opt_formdata, opt_ }); if (!hasEncrypted) { let forceSave = await editorData.getForceSave(ctx, docId); + if (!forceSave && commonDefines.c_oAscForceSaveTypes.Form === type && opt_conn) { + //stub to send forms without changes + let newChangesLastDate = new Date(); + newChangesLastDate.setMilliseconds(0);//remove milliseconds avoid issues with MySQL datetime rounding + let newChangesLastTime = newChangesLastDate.getTime(); + let baseUrl = utils.getBaseUrlByConnection(ctx, opt_conn); + let changeInfo = getExternalChangeInfo(opt_conn.user, newChangesLastTime); + await editorData.setForceSave(ctx, docId, newChangesLastTime, 0, baseUrl, changeInfo, null); + forceSave = await editorData.getForceSave(ctx, docId); + } let applyCacheRes = await applyForceSaveCache(ctx, docId, forceSave, type, opt_userConnectionId, opt_userConnectionDocId, opt_responseKey, opt_formdata); startedForceSave = applyCacheRes.startedForceSave; if (applyCacheRes.notModified) { @@ -1008,7 +1018,9 @@ function* startRPC(ctx, conn, responseKey, data) { case 'sendForm': { let forceSaveRes; if (conn.user) { - forceSaveRes = yield startForceSave(ctx, docId, commonDefines.c_oAscForceSaveTypes.Form, undefined, data.formdata, conn.user.idOriginal, conn.user.id, undefined, conn.user.indexUser, responseKey); + forceSaveRes = yield startForceSave(ctx, docId, commonDefines.c_oAscForceSaveTypes.Form, undefined, + data.formdata, conn.user.idOriginal, conn.user.id, undefined, conn.user.indexUser, + responseKey, undefined, undefined, undefined, conn); } if (!forceSaveRes || commonDefines.c_oAscServerCommandErrors.NoError !== forceSaveRes.code || forceSaveRes.inProgress) { sendDataRpc(ctx, conn, responseKey, forceSaveRes); @@ -2548,7 +2560,7 @@ exports.install = function(server, callbackFunction) { aggregationCtx.init(tenantManager.getDefautTenant(), ctx.docId, ctx.userId); //yield ctx.initTenantCache(); //no need licenseInfoAggregation = tenantManager.getServerLicense(); - licenseType = yield* _checkLicenseAuth(aggregationCtx, licenseInfoAggregation, conn.user.idOriginal, isLiveViewer, logPrefixServer); + licenseType = yield* _checkLicenseAuth(aggregationCtx, licenseInfoAggregation, `${ctx.tenant}:${ conn.user.idOriginal}`, isLiveViewer, logPrefixServer); } conn.licenseType = licenseType; if ((c_LR.Success !== licenseType && c_LR.SuccessLimit !== licenseType) || (!tenIsAnonymousSupport && data.IsAnonymousUser)) { @@ -2562,7 +2574,7 @@ exports.install = function(server, callbackFunction) { yield* updateEditUsers(ctx, licenseInfo, conn.user.idOriginal, !!data.IsAnonymousUser, isLiveViewer); if (aggregationCtx && licenseInfoAggregation) { //update server aggregation license - yield* updateEditUsers(aggregationCtx, licenseInfoAggregation, conn.user.idOriginal, !!data.IsAnonymousUser, isLiveViewer); + yield* updateEditUsers(aggregationCtx, licenseInfoAggregation, `${ctx.tenant}:${ conn.user.idOriginal}`, !!data.IsAnonymousUser, isLiveViewer); } } } @@ -3793,10 +3805,10 @@ exports.install = function(server, callbackFunction) { } yield addPresence(ctx, conn, false); if (utils.isLiveViewer(conn)) { - countViewByShard++; + countLiveViewByShard++; tenant.countLiveViewByShard++; } else if(conn.isCloseCoAuthoring || (conn.user && conn.user.view)) { - countLiveViewByShard++; + countViewByShard++; tenant.countViewByShard++; } else { countEditByShard++; @@ -3827,9 +3839,9 @@ exports.install = function(server, callbackFunction) { let aggregationCtx = new operationContext.Context(); aggregationCtx.init(tenantManager.getDefautTenant(), ctx.docId, ctx.userId); //yield ctx.initTenantCache();//no need - yield editorData.setEditorConnectionsCountByShard(ctx, SHARD_ID, countEditByShard); - yield editorData.setLiveViewerConnectionsCountByShard(ctx, SHARD_ID, countLiveViewByShard); - yield editorData.setViewerConnectionsCountByShard(ctx, SHARD_ID, countViewByShard); + yield editorData.setEditorConnectionsCountByShard(aggregationCtx, SHARD_ID, countEditByShard); + yield editorData.setLiveViewerConnectionsCountByShard(aggregationCtx, SHARD_ID, countLiveViewByShard); + yield editorData.setViewerConnectionsCountByShard(aggregationCtx, SHARD_ID, countViewByShard); } ctx.initDefault(); } catch (err) { @@ -3985,9 +3997,12 @@ exports.healthCheck = function(req, res) { exports.licenseInfo = function(req, res) { return co(function*() { let isError = false; + let serverDate = new Date(); + //security risk of high-precision time + serverDate.setMilliseconds(0); let output = { connectionsStat: {}, licenseInfo: {}, serverInfo: { - buildVersion: commonDefines.buildVersion, buildNumber: commonDefines.buildNumber, + buildVersion: commonDefines.buildVersion, buildNumber: commonDefines.buildNumber, date: serverDate.toISOString() }, quota: { edit: { connectionsCount: 0, diff --git a/DocService/sources/canvasservice.js b/DocService/sources/canvasservice.js index 9015c495..e14bde5b 100644 --- a/DocService/sources/canvasservice.js +++ b/DocService/sources/canvasservice.js @@ -52,6 +52,7 @@ var statsDClient = require('./../../Common/sources/statsdclient'); var operationContext = require('./../../Common/sources/operationContext'); var tenantManager = require('./../../Common/sources/tenantManager'); var config = require('config'); +//const sharp = require("sharp"); const cfgTypesUpload = config.get('services.CoAuthoring.utils.limits_image_types_upload'); const cfgImageSize = config.get('services.CoAuthoring.server.limits_image_size'); @@ -347,12 +348,14 @@ function* saveParts(ctx, cmd, filename) { } if (cmd.getUrl()) { result = true; - } else { + } else if (cmd.getData() && cmd.getData().length > 0) { var buffer = cmd.getData(); yield storage.putObject(ctx, cmd.getSaveKey() + '/' + filename, buffer, buffer.length); //delete data to prevent serialize into json cmd.data = null; result = (SAVE_TYPE_COMPLETE_ALL === saveType || SAVE_TYPE_COMPLETE === saveType); + } else { + result = true; } return result; } @@ -725,6 +728,13 @@ function* commandImgurls(ctx, conn, cmd, outputData) { const filterPrivate = !authorizations[i] || !tenAllowPrivateIPAddressForSignedRequests; let getRes = yield utils.downloadUrlPromise(ctx, urlSource, tenImageDownloadTimeout, tenImageSize, authorizations[i], filterPrivate); data = getRes.body; + // //fix exif rotation + // //todo move to commons + // let sharpTransform = sharp(data); + // let metadata = yield sharpTransform.metadata(); + // if (undefined !== metadata.orientation && metadata.orientation > 1) { + // data = yield sharpTransform.rotate().toBuffer(); + // } urlParsed = urlModule.parse(urlSource); } catch (e) { data = undefined; @@ -818,14 +828,17 @@ function* commandPathUrl(ctx, conn, cmd, outputData) { } } function* commandSaveFromOrigin(ctx, cmd, outputData, password) { - let docPassword = sqlBase.DocumentPassword.prototype.getDocPassword(ctx, password); - if (docPassword.initial) { - cmd.setPassword(docPassword.initial); + var completeParts = yield* saveParts(ctx, cmd, "changes0.json"); + if (completeParts) { + let docPassword = sqlBase.DocumentPassword.prototype.getDocPassword(ctx, password); + if (docPassword.initial) { + cmd.setPassword(docPassword.initial); + } + var queueData = getSaveTask(ctx, cmd); + queueData.setFromOrigin(true); + queueData.setFromChanges(true); + yield* docsCoServer.addTask(queueData, constants.QUEUE_PRIORITY_LOW); } - yield* addRandomKeyTaskCmd(ctx, cmd); - var queueData = getSaveTask(ctx, cmd); - queueData.setFromOrigin(true); - yield* docsCoServer.addTask(queueData, constants.QUEUE_PRIORITY_LOW); outputData.setStatus('ok'); outputData.setData(cmd.getSaveKey()); } diff --git a/DocService/sources/fileuploaderservice.js b/DocService/sources/fileuploaderservice.js index 11b6d9f1..e0dc348b 100644 --- a/DocService/sources/fileuploaderservice.js +++ b/DocService/sources/fileuploaderservice.js @@ -43,7 +43,7 @@ var storageBase = require('./../../Common/sources/storage-base'); var formatChecker = require('./../../Common/sources/formatchecker'); const commonDefines = require('./../../Common/sources/commondefines'); const operationContext = require('./../../Common/sources/operationContext'); - +//const sharp = require("sharp"); var config = require('config'); const cfgImageSize = config.get('services.CoAuthoring.server.limits_image_size'); @@ -101,7 +101,8 @@ function checkJwtUploadTransformRes(ctx, errorName, checkJwtRes){ if (checkJwtRes.decoded) { var doc = checkJwtRes.decoded.document; var edit = checkJwtRes.decoded.editorConfig; - if (!edit.ds_view && !edit.ds_isCloseCoAuthoring) { + //todo check view and pdf editor (temporary fix) + if (!edit.ds_isCloseCoAuthoring) { res.err = false; res.docId = doc.key; res.encrypted = doc.ds_encrypted; @@ -255,6 +256,14 @@ exports.uploadImageFile = function(req, res) { var strImageName = crypto.randomBytes(16).toString("hex"); var strPathRel = 'media/' + strImageName + '.' + formatStr; var strPath = docId + '/' + strPathRel; + // //fix exif rotation + // //todo move to commons + // let sharpTransform = sharp(buffer); + // let metadata = yield sharpTransform.metadata(); + // if (undefined !== metadata.orientation && metadata.orientation > 1) { + // buffer = yield sharpTransform.rotate().toBuffer(); + // } + yield storageBase.putObject(ctx, strPath, buffer, buffer.length); output[strPathRel] = yield storageBase.getSignedUrl(ctx, utils.getBaseUrlByRequest(ctx, req), strPath, commonDefines.c_oAscUrlTypes.Session); diff --git a/FileConverter/sources/converter.js b/FileConverter/sources/converter.js index 4e8a43dc..29982f6f 100644 --- a/FileConverter/sources/converter.js +++ b/FileConverter/sources/converter.js @@ -409,19 +409,33 @@ function* downloadFileFromStorage(ctx, strPath, dir, opt_specialDir) { var data = yield storage.getObject(ctx, file, opt_specialDir); fs.writeFileSync(path.join(dir, fileRel), data); } + return list.length; } function* processDownloadFromStorage(ctx, dataConvert, cmd, task, tempDirs, authorProps) { const tenEditor = ctx.getCfg('services.CoAuthoring.editor', cfgEditor); let res = constants.NO_ERROR; - let needConcatFiles = false; + let concatDir; + let concatTemplate; if (task.getFromOrigin() || task.getFromSettings()) { + if (task.getFromChanges()) { + let changesDir = path.join(tempDirs.source, constants.CHANGES_NAME); + fs.mkdirSync(changesDir); + let filesCount = yield* downloadFileFromStorage(ctx, cmd.getSaveKey(), changesDir); + if (filesCount > 0) { + concatDir = changesDir; + concatTemplate = "changes0"; + } else { + dataConvert.fromChanges = false; + task.setFromChanges(dataConvert.fromChanges); + } + } dataConvert.fileFrom = path.join(tempDirs.source, 'origin.' + cmd.getFormat()); } else { //overwrite some files from m_sKey (for example Editor.bin or changes) yield* downloadFileFromStorage(ctx, cmd.getSaveKey(), tempDirs.source); let format = cmd.getFormat() || 'bin'; dataConvert.fileFrom = path.join(tempDirs.source, 'Editor.' + format); - needConcatFiles = true; + concatDir = tempDirs.source; } if (!utils.checkPathTraversal(ctx, dataConvert.key, tempDirs.source, dataConvert.fileFrom)) { return constants.CONVERT_PARAMS; @@ -430,12 +444,20 @@ function* processDownloadFromStorage(ctx, dataConvert, cmd, task, tempDirs, auth let mailMergeSend = cmd.getMailMergeSend(); if (mailMergeSend) { yield* downloadFileFromStorage(ctx, mailMergeSend.getJsonKey(), tempDirs.source); - needConcatFiles = true; + concatDir = tempDirs.source; } - if (needConcatFiles) { - yield* concatFiles(tempDirs.source); + if (concatDir) { + yield* concatFiles(concatDir, concatTemplate); + if (concatTemplate) { + let filenames = fs.readdirSync(concatDir); + filenames.forEach(file => { + if (file.match(new RegExp(`${concatTemplate}\\d+\\.`))) { + fs.rmSync(path.join(concatDir, file)); + } + }); + } } - if (task.getFromChanges()) { + if (task.getFromChanges() && !(task.getFromOrigin() || task.getFromSettings())) { if(tenEditor['binaryChanges']) { res = yield* processChangesBin(ctx, tempDirs, task, cmd, authorProps); } else { @@ -458,15 +480,16 @@ function* processDownloadFromStorage(ctx, dataConvert, cmd, task, tempDirs, auth return res; } -function* concatFiles(source) { +function* concatFiles(source, template) { + template = template || "Editor"; //concatenate EditorN.ext parts in Editor.ext let list = yield utils.listObjects(source, true); list.sort(utils.compareStringByLength); let writeStreams = {}; for (let i = 0; i < list.length; ++i) { let file = list[i]; - if (file.match(/Editor\d+\./)) { - let target = file.replace(/(Editor)\d+(\..*)/, '$1$2'); + if (file.match(new RegExp(`${template}\\d+\\.`))) { + let target = file.replace(new RegExp(`(${template})\\d+(\\..*)`), '$1$2'); let writeStream = writeStreams[target]; if (!writeStream) { writeStream = yield utils.promiseCreateWriteStream(target); diff --git a/branding/info/index.html b/branding/info/index.html index f781ff67..51976db2 100644 --- a/branding/info/index.html +++ b/branding/info/index.html @@ -290,27 +290,42 @@ elem = document.getElementById('build-version'); elem.innerText = 'Version: ' + serverInfo.buildVersion + '.' + serverInfo.buildNumber; + let limitText, limitEdit, limitView; + if (licenseInfo.usersCount > 0) { + limitText = 'Users limit'; + limitEdit = licenseInfo.usersCount; + limitView = licenseInfo.usersViewCount; + } else { + limitText = 'Connections limit'; + limitEdit = licenseInfo.connections; + limitView = licenseInfo.connectionsView; + } elem = document.getElementById('limit-type'); - elem.innerText = (licenseInfo.usersCount>0) ? 'Users limit' : 'Connections limit' ; + elem.innerText = limitText; elem = document.getElementById('lic-limit-edit'); - elem.innerText = 'Editors: ' + (licenseInfo.usersCount || licenseInfo.connections); + elem.innerText = 'Editors: ' + limitEdit; elem = document.getElementById('lic-limit-view'); - elem.innerText = 'Live Viewer: ' + (licenseInfo.usersViewCount || licenseInfo.connectionsView); + elem.innerText = 'Live Viewer: ' + limitView; if (licenseInfo.endDate===null) { elem = document.getElementById('lic-valid-type'); elem.innerText = 'No license'; } else { + var isLimited = (licenseInfo.mode & 1) || (licenseInfo.mode & 4); elem = document.getElementById('lic-valid-type'); - elem.innerText = ((licenseInfo.mode & 1) || (licenseInfo.mode & 4)) ? 'Valid: ' : 'Updates available: '; + elem.innerText = isLimited ? 'Valid: ' : 'Updates available: '; var licdate = new Date(licenseInfo.endDate); + var licType = licenseInfo.type; elem = document.getElementById('lic-valid'); elem.innerText = licdate.toLocaleDateString(); - if (Date.now() > licdate) + var isInvalid = 2 === licType || 1 === licType || 6 === licType || 16 === licType || 11 === licType; + var isUpdateUnavailible = !isLimited && new Date(serverInfo.date) > licdate; + if (isInvalid || isUpdateUnavailible) { elem.classList.add('critical'); + } elem = document.getElementById('trial'); elem.innerText = (licenseInfo.mode & 1) ? 'Trial' : '';