✨(backend) add legacy conversion policy helpers

Legacy Office formats need an explicit conversion policy before they can be
opened for editing.

Keep the mapping small and configuration-driven so the backend only converts
formats intentionally forced by the WOPI client.
This commit is contained in:
Nicolas Clerc
2026-06-02 15:23:42 +02:00
parent 88e2693704
commit b4fc5ec2a3
4 changed files with 117 additions and 9 deletions
-9
View File
@@ -1398,14 +1398,6 @@ class Base(Configuration):
WOPI_LOCK_TIMEOUT = values.IntegerValue(
30 * 60, environ_name="WOPI_LOCK_TIMEOUT", environ_prefix=None
)
<<<<<<< HEAD
||||||| parent of f8297c68 (✨(backend) add OnlyOffice conversion backend)
WOPI_LEGACY_CONVERSION_TARGETS = {
"doc": "docx",
"xls": "xlsx",
"ppt": "pptx",
}
=======
WOPI_LEGACY_CONVERSION_TARGETS = {
"doc": "docx",
"xls": "xlsx",
@@ -1423,7 +1415,6 @@ class Base(Configuration):
WOPI_ONLYOFFICE_CONVERT_DOWNLOAD_READ_TIMEOUT = values.IntegerValue(
30, environ_name="WOPI_ONLYOFFICE_CONVERT_DOWNLOAD_READ_TIMEOUT", environ_prefix=None
)
>>>>>>> f8297c68 (✨(backend) add OnlyOffice conversion backend)
WOPI_DISABLE_CHAT = values.IntegerValue(
0, environ_name="WOPI_DISABLE_CHAT", environ_prefix=None
)
+1
View File
@@ -0,0 +1 @@
"""Server-to-server conversion of legacy Office files via WOPI providers."""
+42
View File
@@ -0,0 +1,42 @@
"""Forced-conversion policy for WOPI legacy formats."""
from django.conf import settings
def _normalize(value):
return value.lower() if isinstance(value, str) else value
def target_extension_for(source_extension):
"""Return the converted extension for a legacy source extension.
Item.extension preserves filename case (REPORT.DOC -> "DOC"), so the
lookup must be case-insensitive.
"""
if not source_extension:
return None
return settings.WOPI_LEGACY_CONVERSION_TARGETS.get(source_extension.lower())
def is_forced_conversion(item, client_options):
"""Return True when the active WOPI client forces a server-side conversion.
The decision is product policy, not discovery-derived: a client is allowed
to advertise an "edit" action for a legacy format and still require the
Drive backend to convert it first.
"""
if not client_options:
return False
extension = _normalize(item.extension)
forced_extensions = {_normalize(e) for e in client_options.get("ForceConvertExtensions") or []}
if extension and extension in forced_extensions:
return True
mimetype = _normalize(item.mimetype)
forced_mimetypes = {_normalize(m) for m in client_options.get("ForceConvertMimetypes") or []}
if mimetype and mimetype in forced_mimetypes:
return True
return False
@@ -0,0 +1,74 @@
"""Tests for the conversion policy helpers."""
from django.conf import settings
from core import factories
from wopi.conversion.policy import is_forced_conversion, target_extension_for
def _item(filename="document.doc", mimetype="application/msword"):
"""Build a minimal in-memory Item for policy tests."""
return factories.ItemFactory.build(filename=filename, mimetype=mimetype)
def test_target_extension_for_known_legacy_formats():
"""Map known legacy Office extensions to their modern equivalents."""
assert target_extension_for("doc") == "docx"
assert target_extension_for("xls") == "xlsx"
assert target_extension_for("ppt") == "pptx"
def test_target_extension_for_unknown_format_returns_none():
"""Return None for unsupported or empty source extensions."""
assert target_extension_for("docx") is None
assert target_extension_for("") is None
assert target_extension_for(None) is None
def test_target_extension_for_is_case_insensitive():
"""Match the target extension regardless of source extension case."""
assert target_extension_for("DOC") == "docx"
assert target_extension_for("Xls") == "xlsx"
def test_legacy_conversion_targets_only_lists_legacy_formats():
"""Restrict the conversion policy to legacy Office formats."""
assert set(settings.WOPI_LEGACY_CONVERSION_TARGETS) == {"doc", "xls", "ppt"}
def test_is_forced_conversion_true_when_extension_listed():
"""Force conversion when the item extension is listed."""
options = {"ForceConvertExtensions": ["doc", "xls", "ppt"]}
assert is_forced_conversion(_item(filename="document.doc"), options) is True
def test_is_forced_conversion_true_when_mimetype_listed():
"""Force conversion when the item mimetype is listed."""
options = {"ForceConvertMimetypes": ["application/msword"]}
item = _item(filename="document.unknown", mimetype="application/msword")
assert is_forced_conversion(item, options) is True
def test_is_forced_conversion_false_when_neither_matches():
"""Skip conversion when neither extension nor mimetype matches."""
options = {
"ForceConvertExtensions": ["doc"],
"ForceConvertMimetypes": ["application/msword"],
}
item = _item(filename="document.docx", mimetype="application/vnd.openxmlformats")
assert is_forced_conversion(item, options) is False
def test_is_forced_conversion_false_when_options_missing():
"""Skip conversion when client options are missing."""
assert is_forced_conversion(_item(), {}) is False
assert is_forced_conversion(_item(), None) is False
def test_is_forced_conversion_extension_check_is_case_insensitive():
"""Match forced extensions regardless of case."""
options = {"ForceConvertExtensions": ["doc"]}
assert is_forced_conversion(_item(filename="REPORT.DOC"), options) is True
options = {"ForceConvertExtensions": ["DOC"]}
assert is_forced_conversion(_item(filename="document.doc"), options) is True