Merge branch 'develop'

This commit is contained in:
webzwo0i 2021-03-22 16:17:20 +01:00
commit 5db0c8d1cf
30 changed files with 496 additions and 524 deletions

View file

@ -1,3 +1,26 @@
# 1.8.13
### Notable fixes
* Fixed a bug in the safeRun.sh script (#4935)
* Don't create sessions on some static resources (#4921)
* Fixed issue with non-opening device keyboard on smartphones (#4929)
* Add version string to iframe_editor.css to prevent stale cache entry (#4964)
### Notable enhancements
* Refactor pad loading (no document.write anymore) (#4960)
* Improve import/export functionality, logging and tests (#4957)
* Refactor CSS manager creation (#4963)
* Better metrics
* Add test for client height (#4965)
### Dependencies
* ueberDB2 1.3.2 -> 1.4.4
* express-rate-limit 5.2.5 -> 5.2.6
* etherpad-require-kernel 1.0.9 -> 1.0.11
# 1.8.12
Special mention: Thanks to Sauce Labs for additional testing tunnels to help us grow! :)

View file

@ -15,6 +15,7 @@ Etherpad is extremely flexible providing you the means to modify it to solve wha
* [Video Chat](https://video.etherpad.com) - Plugins to enable Video and Audio chat in a pad.
* [Collaboration++](https://collab.etherpad.com) - Plugins to improve the really-real time collaboration experience, suitable for busy pads.
* [Document Analysis](https://analysis.etherpad.com) - Plugins to improve author and document analysis during and post creation.
* [Scale](https://shard.etherpad.com) - Etherpad running at scale with pad sharding which allows Etherpad to scale to ∞ number of Active Pads with up to ~20,000 edits per second, per pad.
# Project Status

View file

@ -24,8 +24,8 @@ fatal() { error "$@"; exit 1; }
LAST_EMAIL_SEND=0
# Move to the Etherpad base directory.
MY_DIR=$(try cd "${0%/*}" && try pwd -P) || exit 1
try cd "${MY_DIR}/../.."
MY_DIR=$(cd "${0%/*}" && pwd -P) || exit 1
cd "${MY_DIR}/../.." || exit 1
# Check if a logfile parameter is set
LOG="$1"

View file

@ -31,7 +31,7 @@
"pad.settings.stickychat": "Di ekranê de hertim çet bike",
"pad.settings.chatandusers": "Çeta û Bikarhênera Nîşan bide",
"pad.settings.colorcheck": "Rengên nivîskarîye",
"pad.settings.linenocheck": "Hejmarên rêze",
"pad.settings.linenocheck": "Hejmarên rêzê",
"pad.settings.rtlcheck": "Bila naverok ji raste ber bi çepe be xwendin?",
"pad.settings.fontType": "Tîpa nivîsê:",
"pad.settings.language": "Ziman:",

View file

@ -24,6 +24,7 @@
const ueberDB = require('ueberdb2');
const settings = require('../utils/Settings');
const log4js = require('log4js');
const stats = require('../stats');
const util = require('util');
// set database settings
@ -48,6 +49,13 @@ exports.init = async () => await new Promise((resolve, reject) => {
process.exit(1);
}
if (db.metrics != null) {
for (const [metric, value] of Object.entries(db.metrics)) {
if (typeof value !== 'number') continue;
stats.gauge(`ueberdb_${metric}`, () => db.metrics[metric]);
}
}
// everything ok, set up Promise-based methods
['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove'].forEach((fn) => {
exports[fn] = util.promisify(db[fn].bind(db));

View file

@ -33,24 +33,12 @@ const util = require('util');
const fsp_writeFile = util.promisify(fs.writeFile);
const fsp_unlink = util.promisify(fs.unlink);
let convertor = null;
// load abiword only if it is enabled
if (settings.abiword != null) {
convertor = require('../utils/Abiword');
}
// Use LibreOffice if an executable has been defined in the settings
if (settings.soffice != null) {
convertor = require('../utils/LibreOffice');
}
const tempDirectory = os.tmpdir();
/**
* do a requested export
*/
const doExport = async (req, res, padId, readOnlyId, type) => {
exports.doExport = async (req, res, padId, readOnlyId, type) => {
// avoid naming the read-only file as the original pad's id
let fileName = readOnlyId ? readOnlyId : padId;
@ -85,7 +73,7 @@ const doExport = async (req, res, padId, readOnlyId, type) => {
const newHTML = await hooks.aCallFirst('exportHTMLSend', html);
if (newHTML.length) html = newHTML;
res.send(html);
throw 'stop';
return;
}
// else write the html export to a file
@ -98,7 +86,7 @@ const doExport = async (req, res, padId, readOnlyId, type) => {
html = null;
await TidyHtml.tidy(srcFile);
// send the convert job to the convertor (abiword, libreoffice, ..)
// send the convert job to the converter (abiword, libreoffice, ..)
const destFile = `${tempDirectory}/etherpad_export_${randNum}.${type}`;
// Allow plugins to overwrite the convert in export process
@ -106,12 +94,11 @@ const doExport = async (req, res, padId, readOnlyId, type) => {
if (result.length > 0) {
// console.log("export handled by plugin", destFile);
} else {
// @TODO no Promise interface for convertors (yet)
await new Promise((resolve, reject) => {
convertor.convertFile(srcFile, destFile, type, (err) => {
err ? reject('convertFailed') : resolve();
});
});
const converter =
settings.soffice != null ? require('../utils/LibreOffice')
: settings.abiword != null ? require('../utils/Abiword')
: null;
await converter.convertFile(srcFile, destFile, type);
}
// send the file
@ -128,11 +115,3 @@ const doExport = async (req, res, padId, readOnlyId, type) => {
await fsp_unlink(destFile);
}
};
exports.doExport = (req, res, padId, readOnlyId, type) => {
doExport(req, res, padId, readOnlyId, type).catch((err) => {
if (err !== 'stop') {
throw err;
}
});
};

View file

@ -55,17 +55,17 @@ const rm = async (path) => {
}
};
let convertor = null;
let converter = null;
let exportExtension = 'htm';
// load abiword only if it is enabled and if soffice is disabled
if (settings.abiword != null && settings.soffice == null) {
convertor = require('../utils/Abiword');
converter = require('../utils/Abiword');
}
// load soffice only if it is enabled
if (settings.soffice != null) {
convertor = require('../utils/LibreOffice');
converter = require('../utils/LibreOffice');
exportExtension = 'html';
}
@ -80,8 +80,8 @@ const doImport = async (req, res, padId) => {
// set html in the pad
const randNum = Math.floor(Math.random() * 0xFFFFFFFF);
// setting flag for whether to use convertor or not
let useConvertor = (convertor != null);
// setting flag for whether to use converter or not
let useConverter = (converter != null);
const form = new formidable.IncomingForm();
form.keepExtensions = true;
@ -170,30 +170,25 @@ const doImport = async (req, res, padId) => {
// convert file to html if necessary
if (!importHandledByPlugin && !directDatabaseAccess) {
if (fileIsTXT) {
// Don't use convertor for text files
useConvertor = false;
// Don't use converter for text files
useConverter = false;
}
// See https://github.com/ether/etherpad-lite/issues/2572
if (fileIsHTML || !useConvertor) {
// if no convertor only rename
if (fileIsHTML || !useConverter) {
// if no converter only rename
await fs.rename(srcFile, destFile);
} else {
// @TODO - no Promise interface for convertors (yet)
await new Promise((resolve, reject) => {
convertor.convertFile(srcFile, destFile, exportExtension, (err) => {
// catch convert errors
if (err) {
logger.warn(`Converting Error: ${err.stack || err}`);
return reject(new ImportError('convertFailed'));
}
resolve();
});
});
try {
await converter.convertFile(srcFile, destFile, exportExtension);
} catch (err) {
logger.warn(`Converting Error: ${err.stack || err}`);
throw new ImportError('convertFailed');
}
}
}
if (!useConvertor && !directDatabaseAccess) {
if (!useConverter && !directDatabaseAccess) {
// Read the file with no encoding for raw buffer access.
const buf = await fs.readFile(destFile);
@ -224,7 +219,7 @@ const doImport = async (req, res, padId) => {
// change text of the pad and broadcast the changeset
if (!directDatabaseAccess) {
if (importHandledByPlugin || useConvertor || fileIsHTML) {
if (importHandledByPlugin || useConverter || fileIsHTML) {
try {
await importHtml.setPadHTML(pad, text);
} catch (err) {

View file

@ -60,7 +60,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
}
console.log(`Exporting pad "${req.params.pad}" in ${req.params.type} format`);
exportHandler.doExport(req, res, padId, readOnlyId, req.params.type);
await exportHandler.doExport(req, res, padId, readOnlyId, req.params.type);
}
})().catch((err) => next(err || new Error(err)));
});

View file

@ -10,12 +10,20 @@ const readOnlyManager = require('../../db/ReadOnlyManager');
hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
const staticPathsRE = new RegExp(`^/(?:${[
'api/.*',
'api(?:/.*)?',
'favicon\\.ico',
'ep/pad/connection-diagnostic-info',
'javascript',
'javascripts/.*',
'jserror/?',
'locales\\.json',
'locales/.*',
'rest/.*',
'pluginfw/.*',
'robots.txt',
'static/.*',
'stats/?',
'tests/frontend(?:/.*)?'
].join('|')})$`);
exports.normalizeAuthzLevel = (level) => {

View file

@ -46,6 +46,7 @@ const hooks = require('../static/js/pluginfw/hooks');
const pluginDefs = require('../static/js/pluginfw/plugin_defs');
const plugins = require('../static/js/pluginfw/plugins');
const settings = require('./utils/Settings');
const stats = require('./stats');
const logger = log4js.getLogger('server');
@ -104,8 +105,6 @@ exports.start = async () => {
// Check if Etherpad version is up-to-date
UpdateCheck.check();
// start up stats counting system
const stats = require('./stats');
stats.gauge('memoryUsage', () => process.memoryUsage().rss);
stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed);
@ -215,6 +214,7 @@ exports.exit = async (err = null) => {
logger.info('Received SIGTERM signal');
err = null;
} else if (err != null) {
logger.error(`Metrics at time of fatal error:\n${JSON.stringify(stats.toJSON(), null, 2)}`);
logger.error(err.stack || err.toString());
process.exitCode = 1;
if (exitCalled) {

View file

@ -24,111 +24,67 @@ const async = require('async');
const settings = require('./Settings');
const os = require('os');
let doConvertTask;
// on windows we have to spawn a process for each convertion,
// cause the plugin abicommand doesn't exist on this platform
if (os.type().indexOf('Windows') > -1) {
let stdoutBuffer = '';
doConvertTask = (task, callback) => {
// span an abiword process to perform the conversion
const abiword = spawn(settings.abiword, [`--to=${task.destFile}`, task.srcFile]);
// delegate the processing of stdout to another function
abiword.stdout.on('data', (data) => {
// add data to buffer
stdoutBuffer += data.toString();
exports.convertFile = async (srcFile, destFile, type) => {
const abiword = spawn(settings.abiword, [`--to=${destFile}`, srcFile]);
let stdoutBuffer = '';
abiword.stdout.on('data', (data) => { stdoutBuffer += data.toString(); });
abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
await new Promise((resolve, reject) => {
abiword.on('exit', (code) => {
if (code !== 0) return reject(new Error(`Abiword died with exit code ${code}`));
if (stdoutBuffer !== '') {
console.log(stdoutBuffer);
}
resolve();
});
});
// append error messages to the buffer
abiword.stderr.on('data', (data) => {
stdoutBuffer += data.toString();
});
// throw exceptions if abiword is dieing
abiword.on('exit', (code) => {
if (code !== 0) {
return callback(`Abiword died with exit code ${code}`);
}
if (stdoutBuffer !== '') {
console.log(stdoutBuffer);
}
callback();
});
};
exports.convertFile = (srcFile, destFile, type, callback) => {
doConvertTask({srcFile, destFile, type}, callback);
};
// on unix operating systems, we can start abiword with abicommand and
// communicate with it via stdin/stdout
// thats much faster, about factor 10
} else {
// spawn the abiword process
let abiword;
let stdoutCallback = null;
const spawnAbiword = () => {
abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']);
let stdoutBuffer = '';
let firstPrompt = true;
// append error messages to the buffer
abiword.stderr.on('data', (data) => {
stdoutBuffer += data.toString();
});
// abiword died, let's restart abiword and return an error with the callback
abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
abiword.on('exit', (code) => {
spawnAbiword();
stdoutCallback(`Abiword died with exit code ${code}`);
if (stdoutCallback != null) {
stdoutCallback(new Error(`Abiword died with exit code ${code}`));
stdoutCallback = null;
}
});
// delegate the processing of stdout to a other function
abiword.stdout.on('data', (data) => {
// add data to buffer
stdoutBuffer += data.toString();
// we're searching for the prompt, cause this means everything we need is in the buffer
if (stdoutBuffer.search('AbiWord:>') !== -1) {
// filter the feedback message
const err = stdoutBuffer.search('OK') !== -1 ? null : stdoutBuffer;
// reset the buffer
const err = stdoutBuffer.search('OK') !== -1 ? null : new Error(stdoutBuffer);
stdoutBuffer = '';
// call the callback with the error message
// skip the first prompt
if (stdoutCallback != null && !firstPrompt) {
stdoutCallback(err);
stdoutCallback = null;
}
firstPrompt = false;
}
});
};
spawnAbiword();
doConvertTask = (task, callback) => {
const queue = async.queue((task, callback) => {
abiword.stdin.write(`convert ${task.srcFile} ${task.destFile} ${task.type}\n`);
// create a callback that calls the task callback and the caller callback
stdoutCallback = (err) => {
callback();
console.log('queue continue');
try {
task.callback(err);
} catch (e) {
console.error('Abiword File failed to convert', e);
}
if (err != null) console.error('Abiword File failed to convert', err);
callback(err);
};
};
}, 1);
// Queue with the converts we have to do
const queue = async.queue(doConvertTask, 1);
exports.convertFile = (srcFile, destFile, type, callback) => {
queue.push({srcFile, destFile, type, callback});
exports.convertFile = async (srcFile, destFile, type) => {
await queue.pushAsync({srcFile, destFile, type});
};
}

View file

@ -18,7 +18,7 @@
*/
const async = require('async');
const fs = require('fs');
const fs = require('fs').promises;
const log4js = require('log4js');
const os = require('os');
const path = require('path');
@ -27,76 +27,50 @@ const spawn = require('child_process').spawn;
const libreOfficeLogger = log4js.getLogger('LibreOffice');
const doConvertTask = (task, callback) => {
const doConvertTask = async (task) => {
const tmpDir = os.tmpdir();
async.series([
/*
* use LibreOffice to convert task.srcFile to another format, given in
* task.type
*/
(callback) => {
libreOfficeLogger.debug(
`Converting ${task.srcFile} to format ${task.type}. The result will be put in ${tmpDir}`
);
const soffice = spawn(settings.soffice, [
'--headless',
'--invisible',
'--nologo',
'--nolockcheck',
'--writer',
'--convert-to',
task.type,
task.srcFile,
'--outdir',
tmpDir,
]);
// Soffice/libreoffice is buggy and often hangs.
// To remedy this we kill the spawned process after a while.
const hangTimeout = setTimeout(() => {
soffice.stdin.pause(); // required to kill hanging threads
soffice.kill();
}, 120000);
let stdoutBuffer = '';
// Delegate the processing of stdout to another function
soffice.stdout.on('data', (data) => {
stdoutBuffer += data.toString();
});
// Append error messages to the buffer
soffice.stderr.on('data', (data) => {
stdoutBuffer += data.toString();
});
soffice.on('exit', (code) => {
clearTimeout(hangTimeout);
if (code !== 0) {
// Throw an exception if libreoffice failed
return callback(`LibreOffice died with exit code ${code} and message: ${stdoutBuffer}`);
}
// if LibreOffice exited succesfully, go on with processing
callback();
});
},
// Move the converted file to the correct place
(callback) => {
const filename = path.basename(task.srcFile);
const sourceFile = `${filename.substr(0, filename.lastIndexOf('.'))}.${task.fileExtension}`;
const sourcePath = path.join(tmpDir, sourceFile);
libreOfficeLogger.debug(`Renaming ${sourcePath} to ${task.destFile}`);
fs.rename(sourcePath, task.destFile, callback);
},
], (err) => {
// Invoke the callback for the local queue
callback();
// Invoke the callback for the task
task.callback(err);
libreOfficeLogger.debug(
`Converting ${task.srcFile} to format ${task.type}. The result will be put in ${tmpDir}`);
const soffice = spawn(settings.soffice, [
'--headless',
'--invisible',
'--nologo',
'--nolockcheck',
'--writer',
'--convert-to',
task.type,
task.srcFile,
'--outdir',
tmpDir,
]);
// Soffice/libreoffice is buggy and often hangs.
// To remedy this we kill the spawned process after a while.
const hangTimeout = setTimeout(() => {
soffice.stdin.pause(); // required to kill hanging threads
soffice.kill();
}, 120000);
let stdoutBuffer = '';
soffice.stdout.on('data', (data) => { stdoutBuffer += data.toString(); });
soffice.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
await new Promise((resolve, reject) => {
soffice.on('exit', (code) => {
clearTimeout(hangTimeout);
if (code !== 0) {
const err =
new Error(`LibreOffice died with exit code ${code} and message: ${stdoutBuffer}`);
libreOfficeLogger.error(err.stack);
return reject(err);
}
resolve();
});
});
const filename = path.basename(task.srcFile);
const sourceFile = `${filename.substr(0, filename.lastIndexOf('.'))}.${task.fileExtension}`;
const sourcePath = path.join(tmpDir, sourceFile);
libreOfficeLogger.debug(`Renaming ${sourcePath} to ${task.destFile}`);
await fs.rename(sourcePath, task.destFile);
};
// Conversion tasks will be queued up, so we don't overload the system
@ -110,7 +84,7 @@ const queue = async.queue(doConvertTask, 1);
* @param {String} type The type to convert into
* @param {Function} callback Standard callback function
*/
exports.convertFile = (srcFile, destFile, type, callback) => {
exports.convertFile = async (srcFile, destFile, type) => {
// Used for the moving of the file, not the conversion
const fileExtension = type;
@ -129,23 +103,10 @@ exports.convertFile = (srcFile, destFile, type, callback) => {
// we need to convert to odt first, then to doc
// to avoid `Error: no export filter for /tmp/xxxx.doc` error
if (type === 'doc') {
queue.push({
srcFile,
destFile: destFile.replace(/\.doc$/, '.odt'),
type: 'odt',
callback: () => {
queue.push(
{
srcFile: srcFile.replace(/\.html$/, '.odt'),
destFile,
type,
callback,
fileExtension,
}
);
},
});
const intermediateFile = destFile.replace(/\.doc$/, '.odt');
await queue.pushAsync({srcFile, destFile: intermediateFile, type: 'odt', fileExtension: 'odt'});
await queue.pushAsync({srcFile: intermediateFile, destFile, type, fileExtension});
} else {
queue.push({srcFile, destFile, type, callback, fileExtension});
await queue.pushAsync({srcFile, destFile, type, fileExtension});
}
};

View file

@ -14,6 +14,7 @@
, "pad_automatic_reconnect.js"
, "ace.js"
, "collab_client.js"
, "cssmanager.js"
, "pad_userlist.js"
, "pad_impexp.js"
, "pad_savedrevs.js"
@ -61,7 +62,6 @@
, "Changeset.js"
, "ChangesetUtils.js"
, "skiplist.js"
, "cssmanager.js"
, "colorutils.js"
, "undomodule.js"
, "$unorm/lib/unorm.js"

45
src/package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "ep_etherpad-lite",
"version": "1.8.12",
"version": "1.8.13",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -15,9 +15,9 @@
}
},
"@azure/abort-controller": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.3.tgz",
"integrity": "sha512-kCibMwqffnwlw3c+e879rCE1Am1I2BfhjOeO54XNA8i/cEuzktnBQbTrzh67XwibHO05YuNgZzSWy9ocVfFAGw==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.4.tgz",
"integrity": "sha512-lNUmDRVGpanCsiUN3NWxFTdwmdFI53xwhkTFfHDGTYk46ca7Ind3nanJc+U6Zj9Tv+9nTCWRBscWEW1DyKOpTw==",
"requires": {
"tslib": "^2.0.0"
},
@ -287,9 +287,9 @@
"integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w=="
},
"@types/node": {
"version": "14.14.31",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.31.tgz",
"integrity": "sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g=="
"version": "14.14.34",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.34.tgz",
"integrity": "sha512-dBPaxocOK6UVyvhbnpFIj2W+S+1cBTkHQbFQfeeJhoKFbzYcVUGHvddeWPSucKATb3F0+pgDq0i6ghEaZjsugA=="
},
"@types/node-fetch": {
"version": "2.5.8",
@ -2051,9 +2051,9 @@
}
},
"etherpad-require-kernel": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/etherpad-require-kernel/-/etherpad-require-kernel-1.0.9.tgz",
"integrity": "sha1-7Y8E6f0szsOgBVu20t/p2ZkS5+I="
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/etherpad-require-kernel/-/etherpad-require-kernel-1.0.11.tgz",
"integrity": "sha512-I03bkNiBMrcsJRSl0IqotUU70s9v6VISrITj/cQgAoVQSoRFbV/NUn2fPIF4LskysTpmwlmwJqgfL2FZpAtxEw=="
},
"etherpad-yajsml": {
"version": "0.0.4",
@ -2110,9 +2110,9 @@
}
},
"express-rate-limit": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.2.5.tgz",
"integrity": "sha512-fv9mf4hWRKZHVlY8ChVNYnGxa49m0zQ6CrJxNiXe2IjJPqicrqoA/JOyBbvs4ufSSLZ6NTzhtgEyLcdfbe+Q6Q=="
"version": "5.2.6",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.2.6.tgz",
"integrity": "sha512-nE96xaxGfxiS5jP3tD3kIW1Jg9yQgX0rXCs3rCkZtmbWHEGyotwaezkLj7bnB41Z0uaOLM8W4AX6qHao4IZ2YA=="
},
"express-session": {
"version": "1.17.1",
@ -7689,9 +7689,9 @@
"optional": true
},
"simple-git": {
"version": "2.35.2",
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.35.2.tgz",
"integrity": "sha512-UjOKsrz92Bx7z00Wla5V6qLSf5X2XSp0sL2gzKw1Bh7iJfDPDaU7gK5avIup0yo1/sMOSUMQer2b9GcnF6nmTQ==",
"version": "2.36.2",
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.36.2.tgz",
"integrity": "sha512-orBEf65GfSiQMsYedbJXSiRNnIRvhbeE5rrxZuEimCpWxDZOav0KLy2IEiPi1YJCF+zaC2quiJF8A4TsxI9/tw==",
"requires": {
"@kwsites/file-exists": "^1.1.1",
"@kwsites/promise-deferred": "^1.1.1",
@ -8436,13 +8436,12 @@
}
},
"ueberdb2": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.3.2.tgz",
"integrity": "sha512-7Ub5jDsIS+qjjsNV7yp1CHXHVe2K9ZUpwaHi9BZf3ai0DxtuHOfMada1wxL6iyEjwYXh/Nsu80iyId51wHFf4A==",
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.4.4.tgz",
"integrity": "sha512-hcexgTdMa6gMquv5r6rOBsr76awMlqAjQiMMJ72qrzuatLYJ6D1EQTK/Jqo4nOD/jklXHM2yFw1mNcHsrlEzrw==",
"requires": {
"async": "^3.2.0",
"cassandra-driver": "^4.5.1",
"channels": "0.0.4",
"dirty": "^1.1.1",
"elasticsearch": "^16.7.1",
"mongodb": "^3.6.3",
@ -8774,9 +8773,9 @@
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
},
"xmldom": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.4.0.tgz",
"integrity": "sha512-2E93k08T30Ugs+34HBSTQLVtpi6mCddaY8uO+pMNk1pqSjV5vElzn4mmh6KLxN3hki8rNcHSYzILoh3TEWORvA=="
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.5.0.tgz",
"integrity": "sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA=="
},
"xmlhttprequest-ssl": {
"version": "1.5.5",

View file

@ -38,10 +38,10 @@
"cookie-parser": "1.4.5",
"cross-spawn": "^7.0.3",
"ejs": "^3.1.6",
"etherpad-require-kernel": "1.0.9",
"etherpad-require-kernel": "1.0.11",
"etherpad-yajsml": "0.0.4",
"express": "4.17.1",
"express-rate-limit": "5.2.5",
"express-rate-limit": "5.2.6",
"express-session": "1.17.1",
"find-root": "1.1.0",
"formidable": "1.2.2",
@ -69,7 +69,7 @@
"threads": "^1.4.0",
"tiny-worker": "^2.3.0",
"tinycon": "0.6.8",
"ueberdb2": "^1.3.2",
"ueberdb2": "^1.4.4",
"underscore": "1.12.0",
"unorm": "1.6.0",
"wtfnode": "^0.8.4"
@ -246,6 +246,6 @@
"test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
"test-container": "mocha --timeout 5000 tests/container/specs/api"
},
"version": "1.8.12",
"version": "1.8.13",
"license": "Apache-2.0"
}

View file

@ -4,8 +4,7 @@
@import url('./lists_and_indents.css');
html.inner-editor {
height: auto !important;
html.outer-editor, html.inner-editor {
background-color: transparent !important;
}
#outerdocbody {
@ -38,6 +37,11 @@ html.inner-editor {
white-space: normal;
word-wrap: break-word;
overflow-wrap: break-word;
/*
* Make the contenteditable area at least as big as the screen so that mobile
* users can tap anywhere to bring up their device's keyboard.
*/
min-height: 100vh;
}
#innerdocbody, #sidediv {

View file

@ -1,11 +1,14 @@
html, body {
width: 100%;
height: 100%;
height: auto;
margin: 0;
padding: 0;
}
html:not(.inner-editor), html:not(.inner-editor) body {
/* used in pad and timeslider */
html.pad, html.pad body {
overflow: hidden;
height: 100%;
}
body {
display: flex;

View file

@ -50,10 +50,9 @@ exports.error = (msg) => {
* @param b {boolean} assertion condition
* @param msgParts {string} error to be passed if it fails
*/
exports.assert = (b, msgParts) => {
exports.assert = (b, ...msgParts) => {
if (!b) {
const msg = Array.prototype.slice.call(arguments, 1).join('');
exports.error(`Failed assertion: ${msg}`);
exports.error(`Failed assertion: ${msgParts.join('')}`);
}
};

View file

@ -25,22 +25,69 @@
// requires: undefined
const hooks = require('./pluginfw/hooks');
const makeCSSManager = require('./cssmanager').makeCSSManager;
const pluginUtils = require('./pluginfw/shared');
const debugLog = (...args) => {};
window.debugLog = debugLog;
// The inner and outer iframe's locations are about:blank, so relative URLs are relative to that.
// Firefox and Chrome seem to do what the developer intends if given a relative URL, but Safari
// errors out unless given an absolute URL for a JavaScript-created element.
const absUrl = (url) => new URL(url, window.location.href).href;
const scriptTag =
(source) => `<script type="text/javascript">\n${source.replace(/<\//g, '<\\/')}</script>`;
const eventFired = async (obj, event, cleanups = [], predicate = () => true) => {
if (typeof cleanups === 'function') {
predicate = cleanups;
cleanups = [];
}
await new Promise((resolve, reject) => {
let cleanup;
const successCb = () => {
if (!predicate()) return;
debugLog(`Ace2Editor.init() ${event} event on`, obj);
cleanup();
resolve();
};
const errorCb = () => {
const err = new Error(`Ace2Editor.init() error event while waiting for ${event} event`);
debugLog(`${err} on object`, obj);
cleanup();
reject(err);
};
cleanup = () => {
cleanup = () => {};
obj.removeEventListener(event, successCb);
obj.removeEventListener('error', errorCb);
};
cleanups.push(cleanup);
obj.addEventListener(event, successCb);
obj.addEventListener('error', errorCb);
});
};
// Resolves when the frame's document is ready to be mutated. Browsers seem to be quirky about
// iframe ready events so this function throws the kitchen sink at the problem. Maybe one day we'll
// find a concise general solution.
const frameReady = async (frame) => {
// Can't do `const doc = frame.contentDocument;` because Firefox seems to asynchronously replace
// the document object after the frame is first created for some reason. ¯\_(ツ)_/¯
const doc = () => frame.contentDocument;
const cleanups = [];
try {
await Promise.race([
eventFired(frame, 'load', cleanups),
eventFired(frame.contentWindow, 'load', cleanups),
eventFired(doc(), 'load', cleanups),
eventFired(doc(), 'DOMContentLoaded', cleanups),
eventFired(doc(), 'readystatechange', cleanups, () => doc.readyState === 'complete'),
]);
} finally {
for (const cleanup of cleanups) cleanup();
}
};
const Ace2Editor = function () {
let info = {editor: this};
window.ace2EditorInfo = info; // Make it accessible to iframes.
let loaded = false;
let actionsPendingInit = [];
@ -109,16 +156,19 @@ const Ace2Editor = function () {
// returns array of {error: <browser Error object>, time: +new Date()}
this.getUnhandledErrors = () => loaded ? info.ace_getUnhandledErrors() : [];
const pushStyleTagsFor = (buffer, files) => {
const addStyleTagsFor = (doc, files) => {
for (const file of files) {
buffer.push(`<link rel="stylesheet" type="text/css" href="${absUrl(encodeURI(file))}"/>`);
const link = doc.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = absUrl(encodeURI(file));
doc.head.appendChild(link);
}
};
this.destroy = pendingInit(() => {
info.ace_dispose();
info.frame.parentNode.removeChild(info.frame);
delete window.ace2EditorInfo;
info = null; // prevent IE 6 closure memory leaks
});
@ -127,7 +177,7 @@ const Ace2Editor = function () {
this.importText(initialCode);
const includedCSS = [
'../static/css/iframe_editor.css',
`../static/css/iframe_editor.css?v=${clientVars.randomVersionString}`,
`../static/css/pad.css?v=${clientVars.randomVersionString}`,
...hooks.callAll('aceEditorCSS').map(
// Allow urls to external CSS - http(s):// and //some/path.css
@ -135,110 +185,135 @@ const Ace2Editor = function () {
`../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`,
];
const doctype = '<!doctype html>';
const skinVariants = clientVars.skinVariants.split(' ').filter((x) => x !== '');
const iframeHTML = [];
iframeHTML.push(doctype);
iframeHTML.push(`<html class='inner-editor ${clientVars.skinVariants}'><head>`);
pushStyleTagsFor(iframeHTML, includedCSS);
const requireKernelUrl =
absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`);
iframeHTML.push(`<script type="text/javascript" src="${requireKernelUrl}"></script>`);
// Pre-fetch modules to improve load performance.
for (const module of ['ace2_inner', 'ace2_common']) {
const url = absUrl(`../javascripts/lib/ep_etherpad-lite/static/js/${module}.js` +
`?callback=require.define&v=${clientVars.randomVersionString}`);
iframeHTML.push(`<script type="text/javascript" src="${url}"></script>`);
}
iframeHTML.push(scriptTag(`(async () => {
parent.parent.debugLog('Ace2Editor.init() inner frame ready');
const require = window.require;
require.setRootURI(${JSON.stringify(absUrl('../javascripts/src'))});
require.setLibraryURI(${JSON.stringify(absUrl('../javascripts/lib'))});
require.setGlobalKeyPath('require');
// intentially moved before requiring client_plugins to save a 307
window.Ace2Inner = require('ep_etherpad-lite/static/js/ace2_inner');
window.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
window.plugins.adoptPluginsFromAncestorsOf(window);
window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery;
parent.parent.debugLog('Ace2Editor.init() waiting for plugins');
await new Promise((resolve, reject) => window.plugins.ensure(
(err) => err != null ? reject(err) : resolve()));
parent.parent.debugLog('Ace2Editor.init() waiting for Ace2Inner.init()');
const editorInfo = parent.parent.ace2EditorInfo;
await new Promise((resolve, reject) => window.Ace2Inner.init(
editorInfo, (err) => err != null ? reject(err) : resolve()));
parent.parent.debugLog('Ace2Editor.init() Ace2Inner.init() returned');
editorInfo.onEditorReady();
})();`));
iframeHTML.push('<style type="text/css" title="dynamicsyntax"></style>');
hooks.callAll('aceInitInnerdocbodyHead', {
iframeHTML,
});
iframeHTML.push('</head><body id="innerdocbody" class="innerdocbody" role="application" ' +
'spellcheck="false">&nbsp;</body></html>');
const outerScript = `(async () => {
await new Promise((resolve) => { window.onload = () => resolve(); });
parent.debugLog('Ace2Editor.init() outer frame ready');
window.onload = null;
await new Promise((resolve) => setTimeout(resolve, 0));
const iframe = document.createElement('iframe');
iframe.name = 'ace_inner';
iframe.title = 'pad';
iframe.scrolling = 'no';
iframe.frameBorder = 0;
iframe.allowTransparency = true; // for IE
iframe.ace_outerWin = window;
document.body.insertBefore(iframe, document.body.firstChild);
const doc = iframe.contentWindow.document;
doc.open();
doc.write(${JSON.stringify(iframeHTML.join('\n'))});
doc.close();
parent.debugLog('Ace2Editor.init() waiting for inner frame');
})();`;
const outerHTML =
[doctype, `<html class="inner-editor outerdoc ${clientVars.skinVariants}"><head>`];
pushStyleTagsFor(outerHTML, includedCSS);
// bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly
// (throbs busy while typing)
const pluginNames = pluginUtils.clientPluginNames();
outerHTML.push(
'<style type="text/css" title="dynamicsyntax"></style>',
'<link rel="stylesheet" type="text/css" href="data:text/css,"/>',
scriptTag(outerScript),
'</head>',
'<body id="outerdocbody" class="outerdocbody ', pluginNames.join(' '), '">',
'<div id="sidediv" class="sidediv"><!-- --></div>',
'<div id="linemetricsdiv">x</div>',
'</body></html>');
const outerFrame = document.createElement('IFRAME');
const outerFrame = document.createElement('iframe');
outerFrame.name = 'ace_outer';
outerFrame.frameBorder = 0; // for IE
outerFrame.title = 'Ether';
// Some browsers do strange things unless the iframe has a src or srcdoc property:
// - Firefox replaces the frame's contentWindow.document object with a different object after
// the frame is created. This can be worked around by waiting for the window's load event
// before continuing.
// - Chrome never fires any events on the frame or document. Eventually the document's
// readyState becomes 'complete' even though it never fires a readystatechange event.
// - Safari behaves like Chrome.
outerFrame.srcdoc = '<!DOCTYPE html>';
info.frame = outerFrame;
document.getElementById(containerId).appendChild(outerFrame);
const editorDocument = outerFrame.contentWindow.document;
const outerWindow = outerFrame.contentWindow;
debugLog('Ace2Editor.init() waiting for outer frame');
await new Promise((resolve, reject) => {
info.onEditorReady = (err) => err != null ? reject(err) : resolve();
editorDocument.open();
editorDocument.write(outerHTML.join(''));
editorDocument.close();
await frameReady(outerFrame);
debugLog('Ace2Editor.init() outer frame ready');
// Firefox might replace the outerWindow.document object after iframe creation so this variable
// is assigned after the Window's load event.
const outerDocument = outerWindow.document;
// <html> tag
outerDocument.documentElement.classList.add('outer-editor', 'outerdoc', ...skinVariants);
// <head> tag
addStyleTagsFor(outerDocument, includedCSS);
const outerStyle = outerDocument.createElement('style');
outerStyle.type = 'text/css';
outerStyle.title = 'dynamicsyntax';
outerDocument.head.appendChild(outerStyle);
// <body> tag
outerDocument.body.id = 'outerdocbody';
outerDocument.body.classList.add('outerdocbody', ...pluginUtils.clientPluginNames());
const sideDiv = outerDocument.createElement('div');
sideDiv.id = 'sidediv';
sideDiv.classList.add('sidediv');
outerDocument.body.appendChild(sideDiv);
const lineMetricsDiv = outerDocument.createElement('div');
lineMetricsDiv.id = 'linemetricsdiv';
lineMetricsDiv.appendChild(outerDocument.createTextNode('x'));
outerDocument.body.appendChild(lineMetricsDiv);
const innerFrame = outerDocument.createElement('iframe');
innerFrame.name = 'ace_inner';
innerFrame.title = 'pad';
innerFrame.scrolling = 'no';
innerFrame.frameBorder = 0;
innerFrame.allowTransparency = true; // for IE
// The iframe MUST have a src or srcdoc property to avoid browser quirks. See the comment above
// outerFrame.srcdoc.
innerFrame.srcdoc = '<!DOCTYPE html>';
innerFrame.ace_outerWin = outerWindow;
outerDocument.body.insertBefore(innerFrame, outerDocument.body.firstChild);
const innerWindow = innerFrame.contentWindow;
debugLog('Ace2Editor.init() waiting for inner frame');
await frameReady(innerFrame);
debugLog('Ace2Editor.init() inner frame ready');
// Firefox might replace the innerWindow.document object after iframe creation so this variable
// is assigned after the Window's load event.
const innerDocument = innerWindow.document;
// <html> tag
innerDocument.documentElement.classList.add('inner-editor', ...skinVariants);
// <head> tag
addStyleTagsFor(innerDocument, includedCSS);
const requireKernel = innerDocument.createElement('script');
requireKernel.type = 'text/javascript';
requireKernel.src =
absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`);
innerDocument.head.appendChild(requireKernel);
// Pre-fetch modules to improve load performance.
for (const module of ['ace2_inner', 'ace2_common']) {
const script = innerDocument.createElement('script');
script.type = 'text/javascript';
script.src = absUrl(`../javascripts/lib/ep_etherpad-lite/static/js/${module}.js` +
`?callback=require.define&v=${clientVars.randomVersionString}`);
innerDocument.head.appendChild(script);
}
const innerStyle = innerDocument.createElement('style');
innerStyle.type = 'text/css';
innerStyle.title = 'dynamicsyntax';
innerDocument.head.appendChild(innerStyle);
const headLines = [];
hooks.callAll('aceInitInnerdocbodyHead', {iframeHTML: headLines});
const tmp = innerDocument.createElement('div');
tmp.innerHTML = headLines.join('\n');
while (tmp.firstChild) innerDocument.head.appendChild(tmp.firstChild);
// <body> tag
innerDocument.body.id = 'innerdocbody';
innerDocument.body.classList.add('innerdocbody');
innerDocument.body.setAttribute('role', 'application');
innerDocument.body.setAttribute('spellcheck', 'false');
innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); // &nbsp;
debugLog('Ace2Editor.init() waiting for require kernel load');
await eventFired(requireKernel, 'load');
debugLog('Ace2Editor.init() require kernel loaded');
const require = innerWindow.require;
require.setRootURI(absUrl('../javascripts/src'));
require.setLibraryURI(absUrl('../javascripts/lib'));
require.setGlobalKeyPath('require');
// intentially moved before requiring client_plugins to save a 307
innerWindow.Ace2Inner = require('ep_etherpad-lite/static/js/ace2_inner');
innerWindow.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
innerWindow.plugins.adoptPluginsFromAncestorsOf(innerWindow);
innerWindow.$ = innerWindow.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery;
debugLog('Ace2Editor.init() waiting for plugins');
await new Promise((resolve, reject) => innerWindow.plugins.ensure(
(err) => err != null ? reject(err) : resolve()));
debugLog('Ace2Editor.init() waiting for Ace2Inner.init()');
await innerWindow.Ace2Inner.init(info, {
inner: makeCSSManager(innerStyle.sheet),
outer: makeCSSManager(outerStyle.sheet),
parent: makeCSSManager(document.querySelector('style[title="dynamicsyntax"]').sheet),
});
debugLog('Ace2Editor.init() Ace2Inner.init() returned');
loaded = true;
doActionsPendingInit();
debugLog('Ace2Editor.init() done');

View file

@ -30,11 +30,10 @@ const htmlPrettyEscape = Ace2Common.htmlPrettyEscape;
const noop = Ace2Common.noop;
const hooks = require('./pluginfw/hooks');
function Ace2Inner(editorInfo) {
function Ace2Inner(editorInfo, cssManagers) {
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
const colorutils = require('./colorutils').colorutils;
const makeContentCollector = require('./contentcollector').makeContentCollector;
const makeCSSManager = require('./cssmanager').makeCSSManager;
const domline = require('./domline').domline;
const AttribPool = require('./AttributePool');
const Changeset = require('./Changeset');
@ -157,10 +156,6 @@ function Ace2Inner(editorInfo) {
const scheduler = parent; // hack for opera required
let dynamicCSS = null;
let outerDynamicCSS = null;
let parentDynamicCSS = null;
const performDocumentReplaceRange = (start, end, newText) => {
if (start === undefined) start = rep.selStart;
if (end === undefined) end = rep.selEnd;
@ -181,12 +176,6 @@ function Ace2Inner(editorInfo) {
performDocumentApplyChangeset(cs);
};
const initDynamicCSS = () => {
dynamicCSS = makeCSSManager('dynamicsyntax');
outerDynamicCSS = makeCSSManager('dynamicsyntax', 'outer');
parentDynamicCSS = makeCSSManager('dynamicsyntax', 'parent');
};
const changesetTracker = makeChangesetTracker(scheduler, rep.apool, {
withCallbacks: (operationName, f) => {
inCallStackIfNecessary(operationName, () => {
@ -214,15 +203,12 @@ function Ace2Inner(editorInfo) {
editorInfo.ace_getAuthorInfos = getAuthorInfos;
const setAuthorStyle = (author, info) => {
if (!dynamicCSS) {
return;
}
const authorSelector = getAuthorColorClassSelector(getAuthorClassName(author));
const authorStyleSet = hooks.callAll('aceSetAuthorStyle', {
dynamicCSS,
parentDynamicCSS,
outerDynamicCSS,
dynamicCSS: cssManagers.inner,
outerDynamicCSS: cssManagers.outer,
parentDynamicCSS: cssManagers.parent,
info,
author,
authorSelector,
@ -234,16 +220,16 @@ function Ace2Inner(editorInfo) {
}
if (!info) {
dynamicCSS.removeSelectorStyle(authorSelector);
parentDynamicCSS.removeSelectorStyle(authorSelector);
cssManagers.inner.removeSelectorStyle(authorSelector);
cssManagers.parent.removeSelectorStyle(authorSelector);
} else if (info.bgcolor) {
let bgcolor = info.bgcolor;
if ((typeof info.fade) === 'number') {
bgcolor = fadeColor(bgcolor, info.fade);
}
const authorStyle = dynamicCSS.selectorStyle(authorSelector);
const parentAuthorStyle = parentDynamicCSS.selectorStyle(authorSelector);
const authorStyle = cssManagers.inner.selectorStyle(authorSelector);
const parentAuthorStyle = cssManagers.parent.selectorStyle(authorSelector);
// author color
authorStyle.backgroundColor = bgcolor;
@ -3895,44 +3881,39 @@ function Ace2Inner(editorInfo) {
editorInfo.ace_performDocumentApplyAttributesToRange =
(...args) => documentAttributeManager.setAttributesOnRange(...args);
this.init = (cb) => {
$(document).ready(() => {
doc = document; // defined as a var in scope outside
inCallStack('setup', () => {
const body = doc.getElementById('innerdocbody');
root = body; // defined as a var in scope outside
if (browser.firefox) $(root).addClass('mozilla');
if (browser.safari) $(root).addClass('safari');
root.classList.toggle('authorColors', true);
root.classList.toggle('doesWrap', doesWrap);
this.init = async () => {
await $.ready;
doc = document; // defined as a var in scope outside
inCallStack('setup', () => {
const body = doc.getElementById('innerdocbody');
root = body; // defined as a var in scope outside
if (browser.firefox) $(root).addClass('mozilla');
if (browser.safari) $(root).addClass('safari');
root.classList.toggle('authorColors', true);
root.classList.toggle('doesWrap', doesWrap);
initDynamicCSS();
enforceEditability();
enforceEditability();
// set up dom and rep
while (root.firstChild) root.removeChild(root.firstChild);
const oneEntry = createDomLineEntry('');
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
insertDomLines(null, [oneEntry.domInfo]);
rep.alines = Changeset.splitAttributionLines(
Changeset.makeAttribution('\n'), '\n');
// set up dom and rep
while (root.firstChild) root.removeChild(root.firstChild);
const oneEntry = createDomLineEntry('');
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
insertDomLines(null, [oneEntry.domInfo]);
rep.alines = Changeset.splitAttributionLines(
Changeset.makeAttribution('\n'), '\n');
bindTheEventHandlers();
});
bindTheEventHandlers();
});
hooks.callAll('aceInitialized', {
editorInfo,
rep,
documentAttributeManager,
});
scheduler.setTimeout(cb, 0);
hooks.callAll('aceInitialized', {
editorInfo,
rep,
documentAttributeManager,
});
};
}
exports.init = (editorInfo, cb) => {
const editor = new Ace2Inner(editorInfo);
editor.init(cb);
exports.init = async (editorInfo, cssManagers) => {
const editor = new Ace2Inner(editorInfo, cssManagers);
await editor.init();
};

View file

@ -468,14 +468,14 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
BroadcastSlider.onSlider(goToRevisionIfEnabled);
const dynamicCSS = makeCSSManager('dynamicsyntax');
const dynamicCSS = makeCSSManager(document.querySelector('style[title="dynamicsyntax"]').sheet);
const authorData = {};
const receiveAuthorData = (newAuthorData) => {
for (const [author, data] of Object.entries(newAuthorData)) {
const bgcolor = typeof data.colorId === 'number'
? clientVars.colorPalette[data.colorId] : data.colorId;
if (bgcolor && dynamicCSS) {
if (bgcolor) {
const selector = dynamicCSS.selectorStyle(`.${linestylefilter.getAuthorClassName(author)}`);
selector.backgroundColor = bgcolor;
selector.color = (colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5)

View file

@ -22,37 +22,7 @@
* limitations under the License.
*/
const makeCSSManager = (emptyStylesheetTitle, doc) => {
if (doc === true) {
doc = 'parent';
} else if (!doc) {
doc = 'inner';
}
const getSheetByTitle = (title) => {
let win;
if (doc === 'parent') {
win = window.parent.parent;
} else if (doc === 'inner') {
win = window;
} else if (doc === 'outer') {
win = window.parent;
} else {
throw new Error('Unknown dynamic style container');
}
const allSheets = win.document.styleSheets;
for (let i = 0; i < allSheets.length; i++) {
const s = allSheets[i];
if (s.title === title) {
return s;
}
}
return null;
};
const browserSheet = getSheetByTitle(emptyStylesheetTitle);
exports.makeCSSManager = (browserSheet) => {
const browserRules = () => (browserSheet.cssRules || browserSheet.rules);
const browserDeleteRule = (i) => {
@ -100,5 +70,3 @@ const makeCSSManager = (emptyStylesheetTitle, doc) => {
info: () => `${selectorList.length}:${browserRules().length}`,
};
};
exports.makeCSSManager = makeCSSManager;

View file

@ -501,7 +501,7 @@ const pad = {
// order of inits is important here:
padimpexp.init(this);
padsavedrevs.init(this);
padeditor.init(postAceInit, pad.padOptions.view || {}, this);
padeditor.init(pad.padOptions.view || {}, this).then(postAceInit);
paduserlist.init(pad.myUserInfo, this);
padconnectionstatus.init();
padmodals.init(this);

View file

@ -34,30 +34,20 @@ const padeditor = (() => {
ace: null,
// this is accessed directly from other files
viewZoom: 100,
init: (readyFunc, initialViewOptions, _pad) => {
init: async (initialViewOptions, _pad) => {
Ace2Editor = require('./ace').Ace2Editor;
pad = _pad;
settings = pad.settings;
const aceReady = () => {
$('#editorloadingbox').hide();
if (readyFunc) {
readyFunc();
// Listen for clicks on sidediv items
const $outerdoc = $('iframe[name="ace_outer"]').contents().find('#outerdocbody');
$outerdoc.find('#sidedivinner').on('click', 'div', function () {
const targetLineNumber = $(this).index() + 1;
window.location.hash = `L${targetLineNumber}`;
});
exports.focusOnLine(self.ace);
}
};
self.ace = new Ace2Editor();
self.ace.init('editorcontainer', '').then(
() => aceReady(), (err) => { throw err || new Error(err); });
await self.ace.init('editorcontainer', '');
$('#editorloadingbox').hide();
// Listen for clicks on sidediv items
const $outerdoc = $('iframe[name="ace_outer"]').contents().find('#outerdocbody');
$outerdoc.find('#sidedivinner').on('click', 'div', function () {
const targetLineNumber = $(this).index() + 1;
window.location.hash = `L${targetLineNumber}`;
});
exports.focusOnLine(self.ace);
self.ace.setProperty('wraps', true);
if (pad.getIsDebugEnabled()) {
self.ace.setProperty('dmesg', pad.dmesg);

View file

@ -0,0 +1 @@
/* intentionally empty */

View file

@ -7,7 +7,7 @@
<!doctype html>
<% e.begin_block("htmlHead"); %>
<html class="<%=pluginUtils.clientPluginNames().join(' '); %> <%=settings.skinVariants%>">
<html class="pad <%=pluginUtils.clientPluginNames().join(' '); %> <%=settings.skinVariants%>">
<% e.end_block(); %>
<title><%=settings.title%></title>

View file

@ -3,7 +3,7 @@
, langs = require("ep_etherpad-lite/node/hooks/i18n").availableLangs
%>
<!doctype html>
<html class="<%=settings.skinVariants%>">
<html class="pad <%=settings.skinVariants%>">
<title data-l10n-id="timeslider.pageTitle" data-l10n-args='{ "appTitle": "<%=settings.title%>" }'><%=settings.title%> Timeslider</title>
<script>
/*

View file

@ -6,6 +6,7 @@
* TODO: unify those two files, and merge in a single one.
*/
const assert = require('assert').strict;
const common = require('../../common');
let agent;
@ -226,6 +227,8 @@ const testImports = {
};
describe(__filename, function () {
this.timeout(1000);
before(async function () { agent = await common.init(); });
Object.keys(testImports).forEach((testName) => {
@ -237,73 +240,34 @@ describe(__filename, function () {
done();
});
}
it('createPad', function (done) {
this.timeout(200);
agent.get(`${endPoint('createPad')}&padID=${testPadId}`)
.expect((res) => {
if (res.body.code !== 0) throw new Error('Unable to create new Pad');
})
.expect('Content-Type', /json/)
.expect(200, done);
it('createPad', async function () {
const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`)
.expect(200)
.expect('Content-Type', /json/);
assert.equal(res.body.code, 0);
});
it('setHTML', function (done) {
this.timeout(150);
agent.get(`${endPoint('setHTML')}&padID=${testPadId}` +
`&html=${encodeURIComponent(test.input)}`)
.expect((res) => {
if (res.body.code !== 0) throw new Error(`Error:${testName}`);
})
.expect('Content-Type', /json/)
.expect(200, done);
it('setHTML', async function () {
const res = await agent.get(`${endPoint('setHTML')}&padID=${testPadId}` +
`&html=${encodeURIComponent(test.input)}`)
.expect(200)
.expect('Content-Type', /json/);
assert.equal(res.body.code, 0);
});
it('getHTML', function (done) {
this.timeout(150);
agent.get(`${endPoint('getHTML')}&padID=${testPadId}`)
.expect((res) => {
const gotHtml = res.body.data.html;
if (gotHtml !== test.wantHTML) {
throw new Error(`HTML received from export is not the one we were expecting.
Test Name:
${testName}
Got:
${JSON.stringify(gotHtml)}
Want:
${JSON.stringify(test.wantHTML)}
Which is a different version of the originally imported one:
${test.input}`);
}
})
.expect('Content-Type', /json/)
.expect(200, done);
it('getHTML', async function () {
const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`)
.expect(200)
.expect('Content-Type', /json/);
assert.equal(res.body.data.html, test.wantHTML);
});
it('getText', function (done) {
this.timeout(100);
agent.get(`${endPoint('getText')}&padID=${testPadId}`)
.expect((res) => {
const gotText = res.body.data.text;
if (gotText !== test.wantText) {
throw new Error(`Text received from export is not the one we were expecting.
Test Name:
${testName}
Got:
${JSON.stringify(gotText)}
Want:
${JSON.stringify(test.wantText)}
Which is a different version of the originally imported one:
${test.input}`);
}
})
.expect('Content-Type', /json/)
.expect(200, done);
it('getText', async function () {
const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`)
.expect(200)
.expect('Content-Type', /json/);
assert.equal(res.body.data.text, test.wantText);
});
});
});

View file

@ -0,0 +1,26 @@
'use strict';
const common = require('../common');
const padManager = require('../../../node/db/PadManager');
const settings = require('../../../node/utils/Settings');
describe(__filename, function () {
let agent;
const settingsBackup = {};
before(async function () {
agent = await common.init();
settingsBackup.soffice = settings.soffice;
await padManager.getPad('testExportPad', 'test content');
});
after(async function () {
Object.assign(settings, settingsBackup);
});
it('returns 500 on export error', async function () {
settings.soffice = 'false'; // '/bin/false' doesn't work on Windows
await agent.get('/p/testExportPad/export/doc')
.expect(500);
});
});

View file

@ -0,0 +1,31 @@
'use strict';
describe('height regression after ace.js refactoring', function () {
before(function (cb) {
helper.newPad(cb);
});
// everything fits inside the viewport
it('clientHeight should equal scrollHeight with few lines', function() {
const aceOuter = helper.padChrome$('iframe')[0].contentDocument;
const clientHeight = aceOuter.documentElement.clientHeight;
const scrollHeight = aceOuter.documentElement.scrollHeight;
expect(clientHeight).to.be(scrollHeight);
});
it('client height should be less than scrollHeight with many lines', async function () {
await helper.clearPad();
await helper.edit('Test line\n' +
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n');
const aceOuter = helper.padChrome$('iframe')[0].contentDocument;
const clientHeight = aceOuter.documentElement.clientHeight;
const scrollHeight = aceOuter.documentElement.scrollHeight;
expect(clientHeight).to.be.lessThan(scrollHeight);
});
});