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:
John McLear 2026-04-19 09:08:22 +01:00 committed by GitHub
parent 66f49bb808
commit 7ec581afca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 94 additions and 272 deletions

1
.gitignore vendored
View File

@ -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

View File

@ -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/*

View File

@ -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`

View File

@ -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` |

View File

@ -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}",

View File

@ -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,

View File

@ -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…",

View File

@ -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);
}

View File

@ -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);

View File

@ -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,

View File

@ -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;
}

View File

@ -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});
};
}

View File

@ -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)));

View File

@ -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

View File

@ -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) {

View File

@ -11,16 +11,10 @@
/* hidden element */
#importstatusball,
#importmessagesuccess,
#importmessageabiword {
#importmessagesuccess {
display: none;
}
#importmessageabiword {
color: #900;
font-size: small;
}
#importsubmitinput {
margin-top: 10px;
}

View File

@ -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`);
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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">

View File

@ -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);

View File

@ -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/}"]}]}}

View File

@ -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">