mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-05-04 11:51:21 +02:00
feat!: replace Abiword with LibreOffice and add DOCX export (#7539)
* feat!: replace Abiword with LibreOffice and add DOCX export (#4805) The Abiword converter is dropped. Abiword's DOCX export is weak and the project is niche on modern platforms; LibreOffice (soffice) is the common deployment path and now serves as the sole converter backend. DOCX is added as an export format and becomes the new target for the "Microsoft Word" UI button. The /export/doc URL still works for legacy API consumers. BREAKING CHANGE: The 'abiword' setting, the INSTALL_ABIWORD Dockerfile build arg, the abiwordAvailable clientVar, and the #importmessageabiword UI element (with locale key pad.importExport.abiword.innerHTML) are removed. Deployments relying on Abiword must configure 'soffice' instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: add docxExport feature flag and abiword deprecation WARN - Add `docxExport: true` setting to opt out of DOCX (use legacy DOC) - Pass `docxExport` to client via clientVars - Use `docxExport` flag in pad_impexp.ts for Word button format - Emit a specific WARN when deprecated `abiword` config is detected - Update settings.json.template and settings.json.docker with docxExport - Add docxExport to ClientVarPayload type in SocketIOMessage.ts Agent-Logs-Url: https://github.com/ether/etherpad/sessions/9afc5291-73b2-4b66-b028-feed39e7056f Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com> * refactor: extract wordFormat variable and improve docxExport comment Agent-Logs-Url: https://github.com/ether/etherpad/sessions/9afc5291-73b2-4b66-b028-feed39e7056f Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com> * fix: restore import-limitation message when no converter is configured The abiword removal dropped both the #importmessageabiword DOM element and its locale key, but Copilot's refactor still expected the show() call to surface a message when exportAvailable === 'no'. Result: users with no soffice binary got silent failure instead of an explanation. Add #importmessagenoconverter back with updated, LibreOffice-focused copy (new locale key pad.importExport.noConverter.innerHTML) and flip the hidden prop when the client knows no converter is available. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * i18n: inline English fallback for noConverter import message The original abiword message existed in ~70 locale files and was removed from all of them by this PR. The replacement key was only added to en.json, so non-English users had an empty div until translators localize. Follow the project's usual pad.html pattern (e.g. line 146's "Font type:") and include the English text inside the div as the fallback content; html10n replaces it when a translation is available. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Revert "i18n: inline English fallback for noConverter import message" This reverts commit f336f24d. Follow the project convention: add the new locale key to en.json only and let translations catch up via the translation system, rather than putting inline fallback in the template. * i18n: leave non-English locale files untouched The PR had removed pad.importExport.abiword.innerHTML from ~82 locale files alongside its removal from en.json. The replacement message uses a new key (pad.importExport.noConverter.innerHTML) in en.json only, so churning every localisation file for a key that is no longer referenced produces useless translation diffs. Restore every non-en locale file to its pre-PR state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com>
This commit is contained in:
parent
66f49bb808
commit
7ec581afca
1
.gitignore
vendored
1
.gitignore
vendored
@ -18,7 +18,6 @@ out/
|
||||
.nyc_output
|
||||
.idea
|
||||
/package-lock.json
|
||||
/src/bin/abiword.exe
|
||||
/src/bin/convertSettings.json
|
||||
/src/bin/etherpad-1.deb
|
||||
/src/bin/node.exe
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@ -62,15 +62,7 @@ ARG ETHERPAD_LOCAL_PLUGINS=
|
||||
# ETHERPAD_GITHUB_PLUGINS="ether/ep_plugin"
|
||||
ARG ETHERPAD_GITHUB_PLUGINS=
|
||||
|
||||
# Control whether abiword will be installed, enabling exports to DOC/PDF/ODT formats.
|
||||
# By default, it is not installed.
|
||||
# If given any value, abiword will be installed.
|
||||
#
|
||||
# EXAMPLE:
|
||||
# INSTALL_ABIWORD=true
|
||||
ARG INSTALL_ABIWORD=
|
||||
|
||||
# Control whether libreoffice will be installed, enabling exports to DOC/PDF/ODT formats.
|
||||
# Control whether libreoffice will be installed, enabling exports to DOC/DOCX/PDF/ODT formats.
|
||||
# By default, it is not installed.
|
||||
# If given any value, libreoffice will be installed.
|
||||
#
|
||||
@ -110,7 +102,6 @@ RUN \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
${INSTALL_ABIWORD:+abiword abiword-plugin-command} \
|
||||
${INSTALL_SOFFICE:+libreoffice openjdk8-jre libreoffice-common} && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
|
||||
@ -62,24 +62,11 @@ The variable value has to be a space separated, double quoted list of plugin nam
|
||||
|
||||
Some plugins will need personalized settings. Just refer to the previous section, and include them in your custom `settings.json.docker`.
|
||||
|
||||
==== Rebuilding including export functionality for DOC/PDF/ODT
|
||||
==== Rebuilding including export functionality for DOC/DOCX/PDF/ODT
|
||||
|
||||
If you want to be able to export your pads to DOC/PDF/ODT files, you can install
|
||||
either Abiword or Libreoffice via setting a build variable.
|
||||
|
||||
===== Via Abiword
|
||||
|
||||
For installing Abiword, set the `INSTALL_ABIWORD` build variable to any value.
|
||||
|
||||
Also, you will need to configure the path to the abiword executable
|
||||
via setting the `abiword` property in `<BASEDIR>/settings.json.docker` to
|
||||
`/usr/bin/abiword` or via setting the environment variable `ABIWORD` to
|
||||
`/usr/bin/abiword`.
|
||||
|
||||
===== Via Libreoffice
|
||||
|
||||
For installing Libreoffice instead, set the `INSTALL_SOFFICE` build variable
|
||||
to any value.
|
||||
If you want to be able to export your pads to DOC/DOCX/PDF/ODT files, you can
|
||||
install Libreoffice via setting the `INSTALL_SOFFICE` build variable to any
|
||||
value.
|
||||
|
||||
Also, you will need to configure the path to the libreoffice executable
|
||||
via setting the `soffice` property in `<BASEDIR>/settings.json.docker` to
|
||||
@ -464,12 +451,8 @@ For the editor container, you can also make it full width by adding `full-width-
|
||||
| How long may clients use served javascript code (in seconds)? Not setting this may cause problems during deployment. Set to 0 to disable caching.
|
||||
| `21600` (6 hours)
|
||||
|
||||
| `ABIWORD`
|
||||
| Absolute path to the Abiword executable. Abiword is needed to get advanced import/export features of pads. Setting it to null disables Abiword and will only allow plain text and HTML import/exports.
|
||||
| `null`
|
||||
|
||||
| `SOFFICE`
|
||||
| This is the absolute path to the soffice executable. LibreOffice can be used in lieu of Abiword to export pads. Setting it to null disables LibreOffice exporting.
|
||||
| Absolute path to the soffice (LibreOffice) executable. Needed for advanced import/export of pads (docx, pdf, odt). Setting it to null disables LibreOffice and will only allow plain text and HTML import/exports.
|
||||
| `null`
|
||||
|
||||
| `ALLOW_UNKNOWN_FILE_ENDS`
|
||||
|
||||
@ -29,24 +29,11 @@ The variable value has to be a space separated, double quoted list of plugin nam
|
||||
|
||||
Some plugins will need personalized settings. Just refer to the previous section, and include them in your custom `settings.json.docker`.
|
||||
|
||||
### Rebuilding including export functionality for DOC/PDF/ODT
|
||||
### Rebuilding including export functionality for DOC/DOCX/PDF/ODT
|
||||
|
||||
If you want to be able to export your pads to DOC/PDF/ODT files, you can install
|
||||
either Abiword or Libreoffice via setting a build variable.
|
||||
|
||||
#### Via Abiword
|
||||
|
||||
For installing Abiword, set the `INSTALL_ABIWORD` build variable to any value.
|
||||
|
||||
Also, you will need to configure the path to the abiword executable
|
||||
via setting the `abiword` property in `<BASEDIR>/settings.json.docker` to
|
||||
`/usr/bin/abiword` or via setting the environment variable `ABIWORD` to
|
||||
`/usr/bin/abiword`.
|
||||
|
||||
#### Via Libreoffice
|
||||
|
||||
For installing Libreoffice instead, set the `INSTALL_SOFFICE` build variable
|
||||
to any value.
|
||||
If you want to be able to export your pads to DOC/DOCX/PDF/ODT files, you can
|
||||
install Libreoffice via setting the `INSTALL_SOFFICE` build variable to any
|
||||
value.
|
||||
|
||||
Also, you will need to configure the path to the libreoffice executable
|
||||
via setting the `soffice` property in `<BASEDIR>/settings.json.docker` to
|
||||
@ -202,8 +189,7 @@ For the editor container, you can also make it full width by adding `full-width-
|
||||
| `EDIT_ONLY` | Users may edit pads but not create new ones. Pad creation is only via the API. This applies both to group pads and regular pads. | `false` |
|
||||
| `MINIFY` | If true, all css & js will be minified before sending to the client. This will improve the loading performance massively, but makes it difficult to debug the javascript/css | `true` |
|
||||
| `MAX_AGE` | How long may clients use served javascript code (in seconds)? Not setting this may cause problems during deployment. Set to 0 to disable caching. | `21600` (6 hours) |
|
||||
| `ABIWORD` | Absolute path to the Abiword executable. Abiword is needed to get advanced import/export features of pads. Setting it to null disables Abiword and will only allow plain text and HTML import/exports. | `null` |
|
||||
| `SOFFICE` | This is the absolute path to the soffice executable. LibreOffice can be used in lieu of Abiword to export pads. Setting it to null disables LibreOffice exporting. | `null` |
|
||||
| `SOFFICE` | Absolute path to the soffice (LibreOffice) executable. Needed for advanced import/export of pads (docx, pdf, odt). Setting it to null disables LibreOffice and will only allow plain text and HTML import/exports. | `null` |
|
||||
| `ALLOW_UNKNOWN_FILE_ENDS` | Allow import of file types other than the supported ones: txt, doc, docx, rtf, odt, html & htm | `true` |
|
||||
| `REQUIRE_AUTHENTICATION` | This setting is used if you require authentication of all users. Note: "/admin" always requires authentication. | `false` |
|
||||
| `REQUIRE_AUTHORIZATION` | Require authorization by a module, or a user with is_admin set, see below. | `false` |
|
||||
|
||||
@ -91,7 +91,7 @@
|
||||
* "password": "${PASSW:}" // if PASSW is not defined would result in password === ''
|
||||
*
|
||||
* If you want to use an empty value (null) as default value for a variable,
|
||||
* simply do not set it, without putting any colons: "${ABIWORD}".
|
||||
* simply do not set it, without putting any colons: "${SOFFICE}".
|
||||
*
|
||||
* 3) if you want to use newlines in the default value of a string parameter,
|
||||
* use "\n" as usual.
|
||||
@ -348,25 +348,23 @@
|
||||
*/
|
||||
"maxAge": "${MAX_AGE:21600}", // 60 * 60 * 6 = 6 hours
|
||||
|
||||
/*
|
||||
* Absolute path to the Abiword executable.
|
||||
*
|
||||
* Abiword is needed to get advanced import/export features of pads. Setting
|
||||
* it to null disables Abiword and will only allow plain text and HTML
|
||||
* import/exports.
|
||||
*/
|
||||
"abiword": "${ABIWORD:null}",
|
||||
|
||||
/*
|
||||
* This is the absolute path to the soffice executable.
|
||||
*
|
||||
* LibreOffice can be used in lieu of Abiword to export pads.
|
||||
* Setting it to null disables LibreOffice exporting.
|
||||
* LibreOffice is used for advanced import/export of pads (docx, pdf, odt).
|
||||
* Setting it to null disables LibreOffice and will only allow plain text
|
||||
* and HTML import/exports.
|
||||
*/
|
||||
"soffice": "${SOFFICE:null}",
|
||||
|
||||
/*
|
||||
* Allow import of file types other than the supported ones:
|
||||
* When true (the default), the "Microsoft Word" export button downloads a .docx file via
|
||||
* LibreOffice (requires "soffice" to be set). Set to false to revert to legacy .doc output
|
||||
* (which also requires "soffice").
|
||||
*/
|
||||
"docxExport": "${DOCX_EXPORT:true}",
|
||||
|
||||
/*
|
||||
* txt, doc, docx, rtf, odt, html & htm
|
||||
*/
|
||||
"allowUnknownFileEnds": "${ALLOW_UNKNOWN_FILE_ENDS:true}",
|
||||
|
||||
@ -82,7 +82,7 @@
|
||||
* "password": "${PASSW:}" // if PASSW is not defined would result in password === ''
|
||||
*
|
||||
* If you want to use an empty value (null) as default value for a variable,
|
||||
* simply do not set it, without putting any colons: "${ABIWORD}".
|
||||
* simply do not set it, without putting any colons: "${SOFFICE}".
|
||||
*
|
||||
* 3) if you want to use newlines in the default value of a string parameter,
|
||||
* use "\n" as usual.
|
||||
@ -335,25 +335,23 @@
|
||||
*/
|
||||
"maxAge": 21600, // 60 * 60 * 6 = 6 hours
|
||||
|
||||
/*
|
||||
* Absolute path to the Abiword executable.
|
||||
*
|
||||
* Abiword is needed to get advanced import/export features of pads. Setting
|
||||
* it to null disables Abiword and will only allow plain text and HTML
|
||||
* import/exports.
|
||||
*/
|
||||
"abiword": null,
|
||||
|
||||
/*
|
||||
* This is the absolute path to the soffice executable.
|
||||
*
|
||||
* LibreOffice can be used in lieu of Abiword to export pads.
|
||||
* Setting it to null disables LibreOffice exporting.
|
||||
* LibreOffice is used for advanced import/export of pads (docx, pdf, odt).
|
||||
* Setting it to null disables LibreOffice and will only allow plain text
|
||||
* and HTML import/exports.
|
||||
*/
|
||||
"soffice": null,
|
||||
|
||||
/*
|
||||
* Allow import of file types other than the supported ones:
|
||||
* When true (the default), the "Microsoft Word" export button downloads a .docx file via
|
||||
* LibreOffice (requires "soffice" to be set). Set to false to revert to legacy .doc output
|
||||
* (which also requires "soffice").
|
||||
*/
|
||||
"docxExport": true,
|
||||
|
||||
/*
|
||||
* txt, doc, docx, rtf, odt, html & htm
|
||||
*/
|
||||
"allowUnknownFileEnds": true,
|
||||
|
||||
@ -108,7 +108,7 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "You only can import from plain text or HTML formats. For more advanced import features please <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">install AbiWord or LibreOffice</a>.",
|
||||
"pad.importExport.noConverter.innerHTML": "You can only import from plain text or HTML formats. For more advanced import features, please <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">install LibreOffice</a>.",
|
||||
|
||||
"pad.modals.connected": "Connected.",
|
||||
"pad.modals.reconnecting": "Reconnecting to your pad…",
|
||||
|
||||
@ -95,7 +95,7 @@ exports.doExport = async (req: any, res: any, padId: string, readOnlyId: string,
|
||||
// ensure html can be collected by the garbage collector
|
||||
html = null;
|
||||
|
||||
// send the convert job to the converter (abiword, libreoffice, ..)
|
||||
// send the convert job to the converter (libreoffice)
|
||||
const destFile = `${tempDirectory}/etherpad_export_${randNum}.${type}`;
|
||||
|
||||
// Allow plugins to overwrite the convert in export process
|
||||
@ -103,10 +103,7 @@ exports.doExport = async (req: any, res: any, padId: string, readOnlyId: string,
|
||||
if (result.length > 0) {
|
||||
// console.log("export handled by plugin", destFile);
|
||||
} else {
|
||||
const converter =
|
||||
settings.soffice != null ? require('../utils/LibreOffice')
|
||||
: settings.abiword != null ? require('../utils/Abiword')
|
||||
: null;
|
||||
const converter = require('../utils/LibreOffice');
|
||||
await converter.convertFile(srcFile, destFile, type);
|
||||
}
|
||||
|
||||
|
||||
@ -59,11 +59,6 @@ const rm = async (path: string) => {
|
||||
let converter:any = null;
|
||||
let exportExtension = 'htm';
|
||||
|
||||
// load abiword only if it is enabled and if soffice is disabled
|
||||
if (settings.abiword != null && settings.soffice == null) {
|
||||
converter = require('../utils/Abiword');
|
||||
}
|
||||
|
||||
// load soffice only if it is enabled
|
||||
if (settings.soffice != null) {
|
||||
converter = require('../utils/LibreOffice');
|
||||
@ -81,7 +76,7 @@ const tmpDirectory = os.tmpdir();
|
||||
*/
|
||||
const doImport = async (req:any, res:any, padId:string, authorId:string) => {
|
||||
// pipe to a file
|
||||
// convert file to html via abiword or soffice
|
||||
// convert file to html via soffice
|
||||
// set html in the pad
|
||||
const randNum = Math.floor(Math.random() * 0xFFFFFFFF);
|
||||
|
||||
|
||||
@ -32,7 +32,6 @@ import padutils from '../../static/js/pad_utils';
|
||||
import readOnlyManager from '../db/ReadOnlyManager';
|
||||
import settings, {
|
||||
exportAvailable,
|
||||
abiwordAvailable,
|
||||
sofficeAvailable
|
||||
} from '../utils/Settings';
|
||||
const securityManager = require('../db/SecurityManager');
|
||||
@ -1043,9 +1042,9 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
|
||||
serverTimestamp: Date.now(),
|
||||
sessionRefreshInterval: settings.cookie.sessionRefreshInterval,
|
||||
userId: sessionInfo.author,
|
||||
abiwordAvailable: abiwordAvailable(),
|
||||
sofficeAvailable: sofficeAvailable(),
|
||||
exportAvailable: exportAvailable(),
|
||||
docxExport: settings.docxExport,
|
||||
plugins: {
|
||||
plugins: plugins.plugins,
|
||||
parts: plugins.parts,
|
||||
|
||||
@ -28,22 +28,22 @@ exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Functio
|
||||
args.app.use('/p/:pad{/:rev}/export/:type', limiter);
|
||||
args.app.get('/p/:pad{/:rev}/export/:type', (req:any, res:any, next:Function) => {
|
||||
(async () => {
|
||||
const types = ['pdf', 'doc', 'txt', 'html', 'odt', 'etherpad'];
|
||||
const types = ['pdf', 'doc', 'docx', 'txt', 'html', 'odt', 'etherpad'];
|
||||
// send a 404 if we don't support this filetype
|
||||
if (types.indexOf(req.params.type) === -1) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// if abiword is disabled, and this is a format we only support with abiword, output a message
|
||||
// if soffice is disabled, and this is a format we only support with soffice, output a message
|
||||
if (exportAvailable() === 'no' &&
|
||||
['odt', 'pdf', 'doc'].indexOf(req.params.type) !== -1) {
|
||||
['odt', 'pdf', 'doc', 'docx'].indexOf(req.params.type) !== -1) {
|
||||
console.error(`Impossible to export pad "${req.params.pad}" in ${req.params.type} format.` +
|
||||
' There is no converter configured');
|
||||
|
||||
// ACHTUNG: do not include req.params.type in res.send() because there is
|
||||
// no HTML escaping and it would lead to an XSS
|
||||
res.send('This export is not enabled at this Etherpad instance. Set the path to Abiword' +
|
||||
' or soffice (LibreOffice) in settings.json to enable this feature');
|
||||
res.send('This export is not enabled at this Etherpad instance. Set the path to soffice ' +
|
||||
'(LibreOffice) in settings.json to enable this feature');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -1,93 +0,0 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Controls the communication with the Abiword application
|
||||
*/
|
||||
|
||||
/*
|
||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import {ChildProcess} from "node:child_process";
|
||||
import {AsyncQueueTask} from "../types/AsyncQueueTask";
|
||||
|
||||
const spawn = require('child_process').spawn;
|
||||
const async = require('async');
|
||||
import settings from './Settings';
|
||||
const os = require('os');
|
||||
|
||||
// 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) {
|
||||
exports.convertFile = async (srcFile: string, destFile: string, type: string) => {
|
||||
const abiword = spawn(settings.abiword, [`--to=${destFile}`, srcFile]);
|
||||
let stdoutBuffer = '';
|
||||
abiword.stdout.on('data', (data: string) => { stdoutBuffer += data.toString(); });
|
||||
abiword.stderr.on('data', (data: string) => { stdoutBuffer += data.toString(); });
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
abiword.on('exit', (code: number) => {
|
||||
if (code !== 0) return reject(new Error(`Abiword died with exit code ${code}`));
|
||||
if (stdoutBuffer !== '') {
|
||||
console.log(stdoutBuffer);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
// on unix operating systems, we can start abiword with abicommand and
|
||||
// communicate with it via stdin/stdout
|
||||
// thats much faster, about factor 10
|
||||
} else {
|
||||
let abiword: ChildProcess;
|
||||
let stdoutCallback: Function|null = null;
|
||||
const spawnAbiword = () => {
|
||||
abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']);
|
||||
let stdoutBuffer = '';
|
||||
let firstPrompt = true;
|
||||
abiword.stderr!.on('data', (data) => { stdoutBuffer += data.toString(); });
|
||||
abiword.on('exit', (code) => {
|
||||
spawnAbiword();
|
||||
if (stdoutCallback != null) {
|
||||
stdoutCallback(new Error(`Abiword died with exit code ${code}`));
|
||||
stdoutCallback = null;
|
||||
}
|
||||
});
|
||||
abiword.stdout!.on('data', (data) => {
|
||||
stdoutBuffer += data.toString();
|
||||
// we're searching for the prompt, cause this means everything we need is in the buffer
|
||||
if (stdoutBuffer.search('AbiWord:>') !== -1) {
|
||||
const err = stdoutBuffer.search('OK') !== -1 ? null : new Error(stdoutBuffer);
|
||||
stdoutBuffer = '';
|
||||
if (stdoutCallback != null && !firstPrompt) {
|
||||
stdoutCallback(err);
|
||||
stdoutCallback = null;
|
||||
}
|
||||
firstPrompt = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
spawnAbiword();
|
||||
|
||||
const queue = async.queue((task: AsyncQueueTask, callback:Function) => {
|
||||
abiword.stdin!.write(`convert ${task.srcFile} ${task.destFile} ${task.type}\n`);
|
||||
stdoutCallback = (err: string) => {
|
||||
if (err != null) console.error('Abiword File failed to convert', err);
|
||||
callback(err);
|
||||
};
|
||||
}, 1);
|
||||
|
||||
exports.convertFile = async (srcFile: string, destFile: string, type: string) => {
|
||||
await queue.pushAsync({srcFile, destFile, type});
|
||||
};
|
||||
}
|
||||
@ -254,8 +254,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||
|
||||
let s = taker.take(chars);
|
||||
|
||||
// removes the characters with the code 12. Don't know where they come
|
||||
// from but they break the abiword parser and are completly useless
|
||||
// form feed (0x0C) is a legacy control char with no meaning in HTML
|
||||
s = s.replace(String.fromCharCode(12), '');
|
||||
|
||||
assem.append(_encodeWhitespace(Security.escapeHTML(s)));
|
||||
|
||||
@ -159,8 +159,7 @@ const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
||||
|
||||
const s = taker.take(chars);
|
||||
|
||||
// removes the characters with the code 12. Don't know where they come
|
||||
// from but they break the abiword parser and are completly useless
|
||||
// form feed (0x0C) stripping — left commented historically
|
||||
// s = s.replace(String.fromCharCode(12), "");
|
||||
|
||||
// remove * from s, it's just not needed on a blank line.. This stops
|
||||
|
||||
@ -237,8 +237,8 @@ export type SettingsType = {
|
||||
editOnly: boolean,
|
||||
maxAge: number,
|
||||
minify: boolean,
|
||||
abiword: string | null,
|
||||
soffice: string | null,
|
||||
docxExport: boolean,
|
||||
allowUnknownFileEnds: boolean,
|
||||
loglevel: string,
|
||||
logLayoutType: string,
|
||||
@ -475,14 +475,15 @@ const settings: SettingsType = {
|
||||
* A flag that shows if minification is enabled or not
|
||||
*/
|
||||
minify: true,
|
||||
/**
|
||||
* The path of the abiword executable
|
||||
*/
|
||||
abiword: null,
|
||||
/**
|
||||
* The path of the libreoffice executable
|
||||
*/
|
||||
soffice: null,
|
||||
/**
|
||||
* When true, the "Microsoft Word" export button downloads a .docx file (requires soffice).
|
||||
* Set to false to revert to legacy .doc output.
|
||||
*/
|
||||
docxExport: true,
|
||||
/**
|
||||
* Should we support none natively supported file types on import?
|
||||
*/
|
||||
@ -684,15 +685,6 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
settings.dbSettings = {filename: path.join(settings.root, 'var/rusty.db')};
|
||||
// END OF SETTINGS
|
||||
|
||||
// checks if abiword is avaiable
|
||||
export const abiwordAvailable = () => {
|
||||
if (settings.abiword != null) {
|
||||
return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes';
|
||||
} else {
|
||||
return 'no';
|
||||
}
|
||||
};
|
||||
|
||||
export const sofficeAvailable = () => {
|
||||
if (settings.soffice != null) {
|
||||
return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes';
|
||||
@ -701,19 +693,7 @@ export const sofficeAvailable = () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const exportAvailable = () => {
|
||||
const abiword = abiwordAvailable();
|
||||
const soffice = sofficeAvailable();
|
||||
|
||||
if (abiword === 'no' && soffice === 'no') {
|
||||
return 'no';
|
||||
} else if ((abiword === 'withoutPDF' && soffice === 'no') ||
|
||||
(abiword === 'no' && soffice === 'withoutPDF')) {
|
||||
return 'withoutPDF';
|
||||
} else {
|
||||
return 'yes';
|
||||
}
|
||||
};
|
||||
export const exportAvailable = () => sofficeAvailable();
|
||||
|
||||
|
||||
// Return etherpad version from package.json
|
||||
@ -767,7 +747,7 @@ const storeSettings = (settingsObj: any) => {
|
||||
* no coercition for "null" values.
|
||||
*
|
||||
* If the user wants a variable to be null by default, he'll have to use the
|
||||
* short syntax "${ABIWORD}", and not "${ABIWORD:null}": the latter would result
|
||||
* short syntax "${SOFFICE}", and not "${SOFFICE:null}": the latter would result
|
||||
* in the literal string "null", instead.
|
||||
*/
|
||||
const coerceValue = (stringValue: string) => {
|
||||
@ -962,6 +942,15 @@ export const reloadSettings = () => {
|
||||
storeSettings(settingsParsed);
|
||||
storeSettings(credentials);
|
||||
|
||||
// Emit a clear migration warning when the deprecated abiword setting is detected.
|
||||
if (settingsParsed && (settingsParsed as any).abiword != null) {
|
||||
logger.warn(
|
||||
'The "abiword" setting is no longer supported and has been ignored. ' +
|
||||
'Abiword import/export support has been removed. ' +
|
||||
'Please install LibreOffice and set "soffice" to its executable path instead.'
|
||||
);
|
||||
}
|
||||
|
||||
// Init logging config
|
||||
settings.logconfig = defaultLogConfig(
|
||||
settings.loglevel ? settings.loglevel : defaultLogLevel,
|
||||
@ -1015,20 +1004,6 @@ export const reloadSettings = () => {
|
||||
logger.info(`Using skin "${settings.skinName}" in dir: ${skinPath}`);
|
||||
}
|
||||
|
||||
if (settings.abiword) {
|
||||
// Check abiword actually exists
|
||||
fs.exists(settings.abiword, (exists: boolean) => {
|
||||
if (!exists) {
|
||||
const abiwordError = 'Abiword does not exist at this path, check your settings file.';
|
||||
if (!settings.suppressErrorsInPadText) {
|
||||
settings.defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`;
|
||||
}
|
||||
logger.error(`${abiwordError} File location: ${settings.abiword}`);
|
||||
settings.abiword = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (settings.soffice) {
|
||||
fs.exists(settings.soffice, (exists: boolean) => {
|
||||
if (!exists) {
|
||||
|
||||
@ -11,16 +11,10 @@
|
||||
|
||||
/* hidden element */
|
||||
#importstatusball,
|
||||
#importmessagesuccess,
|
||||
#importmessageabiword {
|
||||
#importmessagesuccess {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#importmessageabiword {
|
||||
color: #900;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
#importsubmitinput {
|
||||
margin-top: 10px;
|
||||
}
|
||||
@ -144,23 +144,23 @@ const padimpexp = (() => {
|
||||
$('#exportetherpada').attr('href', `${padRootPath}/export/etherpad`);
|
||||
$('#exportplaina').attr('href', `${padRootPath}/export/txt`);
|
||||
|
||||
// hide stuff thats not avaible if abiword/soffice is disabled
|
||||
// hide stuff thats not avaible if soffice is disabled
|
||||
const wordFormat = clientVars.docxExport ? 'docx' : 'doc';
|
||||
if (clientVars.exportAvailable === 'no') {
|
||||
$('#exportworda').remove();
|
||||
$('#exportpdfa').remove();
|
||||
$('#exportopena').remove();
|
||||
|
||||
$('#importmessageabiword').show();
|
||||
$('#importmessagenoconverter').prop('hidden', false);
|
||||
} else if (clientVars.exportAvailable === 'withoutPDF') {
|
||||
$('#exportpdfa').remove();
|
||||
|
||||
$('#exportworda').attr('href', `${padRootPath}/export/doc`);
|
||||
$('#exportworda').attr('href', `${padRootPath}/export/${wordFormat}`);
|
||||
$('#exportopena').attr('href', `${padRootPath}/export/odt`);
|
||||
|
||||
$('#importexport').css({height: '142px'});
|
||||
$('#importexportline').css({height: '142px'});
|
||||
} else {
|
||||
$('#exportworda').attr('href', `${padRootPath}/export/doc`);
|
||||
$('#exportworda').attr('href', `${padRootPath}/export/${wordFormat}`);
|
||||
$('#exportpdfa').attr('href', `${padRootPath}/export/pdf`);
|
||||
$('#exportopena').attr('href', `${padRootPath}/export/odt`);
|
||||
}
|
||||
|
||||
@ -80,13 +80,13 @@ export type ClientVarPayload = {
|
||||
skinName: string
|
||||
skinVariants: string,
|
||||
exportAvailable: string
|
||||
docxExport: boolean
|
||||
savedRevisions: PadRevision[],
|
||||
initialRevisionList: number[],
|
||||
padShortcutEnabled: MapArrayType<boolean>,
|
||||
initialTitle: string,
|
||||
opts: {}
|
||||
numConnectedUsers: number
|
||||
abiwordAvailable: string
|
||||
sofficeAvailable: string
|
||||
plugins: {
|
||||
plugins: MapArrayType<any>
|
||||
|
||||
@ -1,15 +1,3 @@
|
||||
#importmessageabiword {
|
||||
font-style: italic;
|
||||
color: #64d29b;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
#importmessageabiword > a {
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
color: #64d29b;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
#importmessagefail {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@ -192,7 +192,7 @@
|
||||
<div class="acl-write">
|
||||
<% e.begin_block("importColumn"); %>
|
||||
<h2 data-l10n-id="pad.importExport.import"></h2>
|
||||
<div class="importmessage" id="importmessageabiword" data-l10n-id="pad.importExport.abiword.innerHTML"></div><br>
|
||||
<div class="importmessage" id="importmessagenoconverter" data-l10n-id="pad.importExport.noConverter.innerHTML" hidden></div>
|
||||
<form id="importform" method="post" action="" target="importiframe" enctype="multipart/form-data">
|
||||
<div class="importformdiv" id="importformfilediv">
|
||||
<input type="file" name="file" size="10" id="importfileinput">
|
||||
|
||||
@ -199,17 +199,16 @@ describe(__filename, function () {
|
||||
}
|
||||
});
|
||||
|
||||
describe('Import/Export tests requiring AbiWord/LibreOffice', function () {
|
||||
describe('Import/Export tests requiring LibreOffice', function () {
|
||||
before(async function () {
|
||||
if ((!settings.abiword || settings.abiword.indexOf('/') === -1) &&
|
||||
(!settings.soffice || settings.soffice.indexOf('/') === -1)) {
|
||||
if (!settings.soffice || settings.soffice.indexOf('/') === -1) {
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
// For some reason word import does not work in testing..
|
||||
// TODO: fix support for .doc files..
|
||||
it('Tries to import .doc that uses soffice or abiword', async function () {
|
||||
it('Tries to import .doc that uses soffice', async function () {
|
||||
await agent.post(`/p/${testPadId}/import`)
|
||||
.set("authorization", await common.generateJWTToken())
|
||||
.attach('file', wordDoc, {filename: '/test.doc', contentType: 'application/msword'})
|
||||
@ -230,7 +229,15 @@ describe(__filename, function () {
|
||||
.expect((res:any) => assert(res.body.length >= 9000));
|
||||
});
|
||||
|
||||
it('Tries to import .docx that uses soffice or abiword', async function () {
|
||||
it('exports DOCX', async function () {
|
||||
await agent.get(`/p/${testPadId}/export/docx`)
|
||||
.set("authorization", await common.generateJWTToken())
|
||||
.buffer(true).parse(superagent.parse['application/octet-stream'])
|
||||
.expect(200)
|
||||
.expect((res:any) => assert(res.body.length >= 4000));
|
||||
});
|
||||
|
||||
it('Tries to import .docx that uses soffice', async function () {
|
||||
await agent.post(`/p/${testPadId}/import`)
|
||||
.set("authorization", await common.generateJWTToken())
|
||||
.attach('file', wordXDoc, {
|
||||
@ -255,7 +262,15 @@ describe(__filename, function () {
|
||||
.expect((res:any) => assert(res.body.length >= 9100));
|
||||
});
|
||||
|
||||
it('Tries to import .pdf that uses soffice or abiword', async function () {
|
||||
it('exports DOCX from imported DOCX', async function () {
|
||||
await agent.get(`/p/${testPadId}/export/docx`)
|
||||
.set("authorization", await common.generateJWTToken())
|
||||
.buffer(true).parse(superagent.parse['application/octet-stream'])
|
||||
.expect(200)
|
||||
.expect((res:any) => assert(res.body.length >= 4000));
|
||||
});
|
||||
|
||||
it('Tries to import .pdf that uses soffice', async function () {
|
||||
await agent.post(`/p/${testPadId}/import`)
|
||||
.set("authorization", await common.generateJWTToken())
|
||||
.attach('file', pdfDoc, {filename: '/test.pdf', contentType: 'application/pdf'})
|
||||
@ -276,7 +291,7 @@ describe(__filename, function () {
|
||||
.expect((res:any) => assert(res.body.length >= 1000));
|
||||
});
|
||||
|
||||
it('Tries to import .odt that uses soffice or abiword', async function () {
|
||||
it('Tries to import .odt that uses soffice', async function () {
|
||||
await agent.post(`/p/${testPadId}/import`)
|
||||
.set("authorization", await common.generateJWTToken())
|
||||
.attach('file', odtDoc, {filename: '/test.odt', contentType: 'application/odt'})
|
||||
@ -296,7 +311,7 @@ describe(__filename, function () {
|
||||
.expect(200)
|
||||
.expect((res:any) => assert(res.body.length >= 7000));
|
||||
});
|
||||
}); // End of AbiWord/LibreOffice tests.
|
||||
}); // End of LibreOffice tests.
|
||||
|
||||
it('Tries to import .etherpad', async function () {
|
||||
this.timeout(3000);
|
||||
|
||||
@ -1 +1 @@
|
||||
{"title":"Etherpad","favicon":null,"skinName":"colibris","skinVariants":"super-light-toolbar super-light-editor light-background","ip":"0.0.0.0","port":9001,"showSettingsInAdminPage":true,"dbType":"dirty","dbSettings":{"filename":"var/dirty.db"},"defaultPadText":"Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nGet involved with Etherpad at https://etherpad.org\n","padOptions":{"noColors":false,"showControls":true,"showChat":true,"showLineNumbers":true,"useMonospaceFont":false,"userName":null,"userColor":null,"rtl":false,"alwaysShowChat":false,"chatAndUsers":false,"lang":null},"padShortcutEnabled":{"altF9":true,"altC":true,"cmdShift2":true,"delete":true,"return":true,"esc":true,"cmdS":true,"tab":true,"cmdZ":true,"cmdY":true,"cmdI":true,"cmdB":true,"cmdU":true,"cmd5":true,"cmdShiftL":true,"cmdShiftN":true,"cmdShift1":true,"cmdShiftC":true,"cmdH":true,"ctrlHome":true,"pageUp":true,"pageDown":true},"suppressErrorsInPadText":false,"requireSession":false,"editOnly":false,"minify":true,"maxAge":21600,"abiword":null,"soffice":null,"allowUnknownFileEnds":true,"requireAuthentication":false,"requireAuthorization":false,"trustProxy":false,"cookie":{"keyRotationInterval":86400000,"sameSite":"Lax","sessionLifetime":864000000,"sessionRefreshInterval":86400000},"disableIPlogging":false,"automaticReconnectionTimeout":0,"scrollWhenFocusLineIsOutOfViewport":{"percentage":{"editionAboveViewport":0,"editionBelowViewport":0},"duration":0,"scrollWhenCaretIsInTheLastLineOfViewport":false,"percentageToScrollWhenUserPressesArrowUp":0},"users":{"admin":{"password":"changeme1","is_admin":true},"user":{"password":"changeme1","is_admin":false}},"socketTransportProtocols":["websocket","polling"],"socketIo":{"maxHttpBufferSize":1000000},"loadTest":false,"dumpOnUncleanExit":false,"importExportRateLimiting":{"windowMs":90000,"max":10},"importMaxFileSize":52428800,"commitRateLimiting":{"duration":1,"points":10},"exposeVersion":false,"loglevel":"INFO","customLocaleStrings":{},"enableAdminUITests":true,"lowerCasePadIds":false,"sso":{"issuer":"${SSO_ISSUER:http://localhost:9001}","clients":[{"client_id":"${ADMIN_CLIENT:admin_client}","client_secret":"${ADMIN_SECRET:admin}","grant_types":["authorization_code"],"response_types":["code"],"redirect_uris":["${ADMIN_REDIRECT:http://localhost:9001/admin/}","https://oauth.pstmn.io/v1/callback"]},{"client_id":"${USER_CLIENT:user_client}","client_secret":"${USER_SECRET:user}","grant_types":["authorization_code"],"response_types":["code"],"redirect_uris":["${USER_REDIRECT:http://localhost:9001/}"]}]}}
|
||||
{"title":"Etherpad","favicon":null,"skinName":"colibris","skinVariants":"super-light-toolbar super-light-editor light-background","ip":"0.0.0.0","port":9001,"showSettingsInAdminPage":true,"dbType":"dirty","dbSettings":{"filename":"var/dirty.db"},"defaultPadText":"Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nGet involved with Etherpad at https://etherpad.org\n","padOptions":{"noColors":false,"showControls":true,"showChat":true,"showLineNumbers":true,"useMonospaceFont":false,"userName":null,"userColor":null,"rtl":false,"alwaysShowChat":false,"chatAndUsers":false,"lang":null},"padShortcutEnabled":{"altF9":true,"altC":true,"cmdShift2":true,"delete":true,"return":true,"esc":true,"cmdS":true,"tab":true,"cmdZ":true,"cmdY":true,"cmdI":true,"cmdB":true,"cmdU":true,"cmd5":true,"cmdShiftL":true,"cmdShiftN":true,"cmdShift1":true,"cmdShiftC":true,"cmdH":true,"ctrlHome":true,"pageUp":true,"pageDown":true},"suppressErrorsInPadText":false,"requireSession":false,"editOnly":false,"minify":true,"maxAge":21600,"soffice":null,"allowUnknownFileEnds":true,"requireAuthentication":false,"requireAuthorization":false,"trustProxy":false,"cookie":{"keyRotationInterval":86400000,"sameSite":"Lax","sessionLifetime":864000000,"sessionRefreshInterval":86400000},"disableIPlogging":false,"automaticReconnectionTimeout":0,"scrollWhenFocusLineIsOutOfViewport":{"percentage":{"editionAboveViewport":0,"editionBelowViewport":0},"duration":0,"scrollWhenCaretIsInTheLastLineOfViewport":false,"percentageToScrollWhenUserPressesArrowUp":0},"users":{"admin":{"password":"changeme1","is_admin":true},"user":{"password":"changeme1","is_admin":false}},"socketTransportProtocols":["websocket","polling"],"socketIo":{"maxHttpBufferSize":1000000},"loadTest":false,"dumpOnUncleanExit":false,"importExportRateLimiting":{"windowMs":90000,"max":10},"importMaxFileSize":52428800,"commitRateLimiting":{"duration":1,"points":10},"exposeVersion":false,"loglevel":"INFO","customLocaleStrings":{},"enableAdminUITests":true,"lowerCasePadIds":false,"sso":{"issuer":"${SSO_ISSUER:http://localhost:9001}","clients":[{"client_id":"${ADMIN_CLIENT:admin_client}","client_secret":"${ADMIN_SECRET:admin}","grant_types":["authorization_code"],"response_types":["code"],"redirect_uris":["${ADMIN_REDIRECT:http://localhost:9001/admin/}","https://oauth.pstmn.io/v1/callback"]},{"client_id":"${USER_CLIENT:user_client}","client_secret":"${USER_SECRET:user}","grant_types":["authorization_code"],"response_types":["code"],"redirect_uris":["${USER_REDIRECT:http://localhost:9001/}"]}]}}
|
||||
@ -402,7 +402,6 @@
|
||||
<div class="acl-write">
|
||||
|
||||
<h2 data-l10n-id="pad.importExport.import"></h2>
|
||||
<div class="importmessage" id="importmessageabiword" data-l10n-id="pad.importExport.abiword.innerHTML"></div><br>
|
||||
<form id="importform" method="post" action="" target="importiframe" enctype="multipart/form-data">
|
||||
<div class="importformdiv" id="importformfilediv">
|
||||
<input type="file" name="file" size="10" id="importfileinput">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user