diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..979ab47c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM onlyoffice/documentserver:latest + +# --- Patch 1: Remove advancedApi license gate --- +# Uses perl for multi-line regex (available in Ubuntu base image). +# Removes the if(advancedApi)return; check from ALL JS files under sdkjs/. +COPY patch.sh /tmp/patch.sh +COPY createConnector.js /tmp/createConnector.js +RUN chmod +x /tmp/patch.sh && /tmp/patch.sh && rm /tmp/patch.sh /tmp/createConnector.js diff --git a/createConnector.js b/createConnector.js new file mode 100644 index 00000000..81f631b6 --- /dev/null +++ b/createConnector.js @@ -0,0 +1,199 @@ + +/* ===== createConnector patch (appended) ===== */ +(function() { + console.log('[ConnectorPatch] Initializing createConnector patch...'); + if (!window.DocsAPI || !window.DocsAPI.DocEditor) { + console.warn('[ConnectorPatch] DocsAPI.DocEditor not found, skipping patch'); + return; + } + + var _origDocEditor = window.DocsAPI.DocEditor; + console.log('[ConnectorPatch] Wrapping DocsAPI.DocEditor'); + + window.DocsAPI.DocEditor = function(placeholderId, config) { + console.log('[ConnectorPatch] DocsAPI.DocEditor called with id:', placeholderId); + // Save parent before the original replaces the placeholder div with an iframe + var targetDiv = document.getElementById(placeholderId); + var parentEl = targetDiv ? targetDiv.parentNode : null; + console.log('[ConnectorPatch] targetDiv:', !!targetDiv, 'parentEl:', !!parentEl); + + // Call the original constructor + var instance = _origDocEditor.apply(this, arguments); + console.log('[ConnectorPatch] Original constructor returned:', typeof instance, instance ? Object.keys(instance).slice(0, 5) : 'null'); + + // Find the editor iframe that replaced the placeholder + var editorIframe = null; + if (parentEl) { + editorIframe = parentEl.querySelector('iframe[name="frameEditor"]'); + } + if (!editorIframe) { + // Fallback: find any frameEditor iframe + editorIframe = document.querySelector('iframe[name="frameEditor"]'); + } + console.log('[ConnectorPatch] editorIframe found:', !!editorIframe); + + // Add createConnector to the instance + if (instance && editorIframe) { + console.log('[ConnectorPatch] Adding createConnector to instance'); + instance.createConnector = function() { + console.log('[ConnectorPatch] createConnector() called'); + var iframe = editorIframe; + var guid = 'asc.{' + 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16 | 0; + var v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }) + '}'; + + var methodCallbacks = []; + var commandCallbacks = []; + var eventHandlers = {}; + var contextMenuClickHandlers = {}; + var contextMenuIdCounter = 0; + + function sendToIframe(data) { + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage(JSON.stringify({ + type: "onExternalPluginMessage", + subType: "connector", + data: data + }), "*"); + } + } + + function onMessageHandler(event) { + var parsed; + try { + parsed = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; + } catch(e) { + return; + } + if (!parsed) return; + + // Unwrap the callback envelope + if (parsed.type === "onExternalPluginMessageCallback") { + parsed = parsed.data; + if (!parsed) return; + } + + if (parsed.guid !== guid) return; + + switch (parsed.type) { + case "onMethodReturn": { + var cb = methodCallbacks.shift(); + if (cb) cb(parsed.methodReturnData); + break; + } + case "onCommandCallback": { + var cb = commandCallbacks.shift(); + if (cb) cb(parsed.commandReturnData); + break; + } + case "onEvent": { + var evtName = parsed.eventName; + if (evtName === "onContextMenuClick") { + // The plugin system appends "_oo_sep_" + extra data to the item ID. + // Strip it to match our stored handler keys. + var clickData = parsed.eventData || ""; + var sepIdx = clickData.indexOf("_oo_sep_"); + var itemId = sepIdx !== -1 ? clickData.substring(0, sepIdx) : clickData; + var clickHandler = contextMenuClickHandlers[itemId]; + if (clickHandler) clickHandler(); + } + var handler = eventHandlers[evtName]; + if (handler) handler(parsed.eventData); + break; + } + } + } + + window.addEventListener('message', onMessageHandler); + + // Register connector with the editor's plugin system + sendToIframe({ type: "register", guid: guid }); + + var connectorApi = { + executeMethod: function(name, params, callback) { + if (callback) methodCallbacks.push(callback); + sendToIframe({ + type: "method", + guid: guid, + methodName: name, + data: params || [] + }); + }, + callCommand: function(func, isClose, isCalc, callback) { + if (callback) commandCallbacks.push(callback); + sendToIframe({ + type: "command", + guid: guid, + data: typeof func === 'function' ? '(' + func.toString() + ')()' : func, + recalculate: isCalc !== false, + resize: isClose !== false + }); + }, + attachEvent: function(name, callback) { + eventHandlers[name] = callback; + sendToIframe({ type: "attachEvent", guid: guid, name: name }); + }, + detachEvent: function(name) { + delete eventHandlers[name]; + sendToIframe({ type: "detachEvent", guid: guid, name: name }); + }, + addContextMenuItem: function(items) { + if (!eventHandlers["onContextMenuClick"]) { + connectorApi.attachEvent("onContextMenuClick", function() {}); + } + var protocolItems = []; + for (var i = 0; i < items.length; i++) { + var item = items[i]; + var itemId = "connector_item_" + (++contextMenuIdCounter); + if (item.onClick) { + contextMenuClickHandlers[itemId] = item.onClick; + } + var protoItem = { + id: itemId, + text: item.text || "", + data: item.data || "", + disabled: !!item.disabled, + icons: item.icons || "" + }; + // Only include items if there are actual sub-items. + // An empty array causes the web app to render a submenu + // arrow, preventing click events from firing. + if (item.items && item.items.length > 0) { + protoItem.items = item.items; + } + protocolItems.push(protoItem); + } + connectorApi.executeMethod("AddContextMenuItem", [{ + guid: guid, + items: protocolItems + }]); + }, + disconnect: function() { + window.removeEventListener('message', onMessageHandler); + sendToIframe({ type: "unregister", guid: guid }); + methodCallbacks = []; + commandCallbacks = []; + eventHandlers = {}; + contextMenuClickHandlers = {}; + } + }; + + return connectorApi; + }; + } + + return instance; + }; + + // Preserve static properties and methods + window.DocsAPI.DocEditor.defaultConfig = _origDocEditor.defaultConfig; + window.DocsAPI.DocEditor.version = _origDocEditor.version; + if (_origDocEditor.warmUp) window.DocsAPI.DocEditor.warmUp = _origDocEditor.warmUp; + for (var prop in _origDocEditor) { + if (_origDocEditor.hasOwnProperty(prop) && !window.DocsAPI.DocEditor[prop]) { + window.DocsAPI.DocEditor[prop] = _origDocEditor[prop]; + } + } +})(); diff --git a/patch.sh b/patch.sh new file mode 100644 index 00000000..2db00322 --- /dev/null +++ b/patch.sh @@ -0,0 +1,63 @@ +#!/bin/bash +set -e + +ROOT="/var/www/onlyoffice/documentserver" + +echo "=== Patch 1: Remove advancedApi license gate ===" + +PATCHED=0 +for file in $(grep -rl 'advancedApi' "$ROOT" --include='*.js' 2>/dev/null); do + echo "Found advancedApi in: $file" + # The minified code has: .advancedApi&&("connector"===... + # Replace .advancedApi&& with && to bypass the license check. + # This turns X.Y.advancedApi&&(...) into X.Y&&(...), which just + # checks that licenseResult exists without requiring advancedApi flag. + sed -i 's/\.advancedApi&&/\&\&/g' "$file" + # Verify + if grep -q '\.advancedApi' "$file"; then + echo " -> WARNING: .advancedApi still present after sed!" + else + echo " -> Verified: advancedApi gate removed!" + fi + PATCHED=$((PATCHED + 1)) +done + +if [ "$PATCHED" -eq 0 ]; then + echo "WARNING: No files with advancedApi pattern found!" +else + echo "Patched $PATCHED file(s)" +fi + +echo "" +echo "=== Patch 2: Add createConnector() to api.js.tpl ===" + +# The actual api.js is generated at container startup from api.js.tpl +TPL_FILE="$ROOT/web-apps/apps/api/documents/api.js.tpl" + +if [ ! -f "$TPL_FILE" ]; then + echo "ERROR: Template not found at $TPL_FILE" + echo "Searching for api.js.tpl..." + find /var/www -name 'api.js.tpl' -type f 2>/dev/null + exit 1 +fi + +echo "Found template: $TPL_FILE" +echo "File size: $(wc -c < "$TPL_FILE") bytes" + +if grep -q 'createConnector' "$TPL_FILE"; then + echo "Already patched, skipping." +else + # Append the createConnector monkey-patch to the template + cat /tmp/createConnector.js >> "$TPL_FILE" + echo "Appended createConnector to api.js.tpl" +fi + +if grep -q 'createConnector' "$TPL_FILE"; then + echo "SUCCESS: createConnector is present in $TPL_FILE" +else + echo "ERROR: createConnector not found after patching!" + exit 1 +fi + +echo "" +echo "=== All patches applied successfully ==="