Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 89 additions & 16 deletions admin/documentate-converter-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,31 @@
$nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
$use_channel = isset( $_GET['use_channel'] ) && '1' === $_GET['use_channel'];

// Iframe mode parameters (for WordPress Playground compatibility).
$is_iframe_mode = isset( $_GET['mode'] ) && 'iframe' === sanitize_key( $_GET['mode'] );
$parent_origin = isset( $_GET['parent_origin'] ) ? esc_url_raw( wp_unslash( $_GET['parent_origin'] ) ) : '';
$request_id = isset( $_GET['request_id'] ) ? sanitize_text_field( wp_unslash( $_GET['request_id'] ) ) : '';

// Helper and thread URLs are local, WASM loads from CDN.
$helper_url = plugins_url( 'admin/vendor/zetajs/zetaHelper.js', DOCUMENTATE_PLUGIN_FILE );
$thread_url = plugins_url( 'admin/vendor/zetajs/converterThread.js', DOCUMENTATE_PLUGIN_FILE );

// Service Worker URL for Cross-Origin Isolation (iframe mode).
$coi_sw_url = plugins_url( 'admin/js/coi-serviceworker.js', DOCUMENTATE_PLUGIN_FILE );

?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><?php esc_html_e( 'Documentate Converter', 'documentate' ); ?></title>
<?php if ( $is_iframe_mode ) : ?>
<!-- Service Worker for Cross-Origin Isolation in iframe mode.
Must load before anything else to intercept requests and add COOP/COEP headers.
Cannot use wp_enqueue_script() as this must execute synchronously before page load. -->
<?php // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript -- Service Worker must load first. ?>
<script src="<?php echo esc_url( $coi_sw_url ); ?>"></script>
<?php endif; ?>
<style>
body {
margin: 0;
Expand Down Expand Up @@ -98,24 +113,50 @@
ajaxUrl: <?php echo wp_json_encode( admin_url( 'admin-ajax.php' ) ); ?>,
helperUrl: <?php echo wp_json_encode( $helper_url ); ?>,
threadUrl: <?php echo wp_json_encode( $thread_url ); ?>,
useChannel: <?php echo $use_channel ? 'true' : 'false'; ?>
useChannel: <?php echo $use_channel ? 'true' : 'false'; ?>,
// Iframe mode parameters (for WordPress Playground).
isIframeMode: <?php echo $is_iframe_mode ? 'true' : 'false'; ?>,
parentOrigin: <?php echo wp_json_encode( $parent_origin ); ?> || '*',
requestId: <?php echo wp_json_encode( $request_id ); ?> || Date.now().toString()
};

// BroadcastChannel for sending results to opener (when useChannel is true)
// Detect if we're in an iframe
const isInIframe = window.parent !== window;

// BroadcastChannel for sending results to opener (when useChannel is true, popup mode)
const channel = conversionConfig.useChannel ? new BroadcastChannel('documentate_converter') : null;

// Helper to send progress/results via channel
function sendToChannel(status, data, error) {
if (channel) {
channel.postMessage({
type: 'conversion_result',
status,
data,
error
});
/**
* Send progress/results to parent window.
* Uses postMessage for iframe mode, BroadcastChannel for popup mode.
*
* @param {string} status - 'progress', 'success', 'preview_ready', or 'error'
* @param {Object} data - Data to send
* @param {string} error - Error message (if status is 'error')
*/
function sendResult(status, data, error) {
const message = {
type: 'conversion_result',
status,
data,
error,
requestId: conversionConfig.requestId
};

if (isInIframe && conversionConfig.isIframeMode) {
// Iframe mode: use postMessage to parent
window.parent.postMessage(message, conversionConfig.parentOrigin);
} else if (channel) {
// Popup mode: use BroadcastChannel
channel.postMessage(message);
}
}

// Legacy alias for compatibility
function sendToChannel(status, data, error) {
sendResult(status, data, error);
}

// Debug info
console.log('Documentate: crossOriginIsolated =', window.crossOriginIsolated);
console.log('Documentate: SharedArrayBuffer =', typeof SharedArrayBuffer !== 'undefined');
Expand Down Expand Up @@ -268,7 +309,36 @@ function updateStatus(title, message, isError = false, isSuccess = false) {
const blob = new Blob([result.outputData], { type: mimeTypes[result.outputFormat] || 'application/octet-stream' });
const blobUrl = URL.createObjectURL(blob);

if (conversionConfig.useChannel) {
// Handle result based on mode: iframe vs popup
if (isInIframe && conversionConfig.isIframeMode) {
// IFRAME MODE: Always send data to parent via postMessage
// Parent will handle display/download

if (conversionConfig.outputAction === 'preview' && result.outputFormat === 'pdf') {
// For preview: send ArrayBuffer to parent, it will open in new window
sendResult('preview_ready', {
outputData: result.outputData,
outputFormat: result.outputFormat,
mimeType: mimeTypes[result.outputFormat] || 'application/octet-stream'
});
} else {
// For download: send ArrayBuffer to parent
sendResult('success', {
outputData: result.outputData,
outputFormat: result.outputFormat,
mimeType: mimeTypes[result.outputFormat] || 'application/octet-stream'
});
}

updateStatus(
<?php echo wp_json_encode( __( 'Completed!', 'documentate' ) ); ?>,
<?php echo wp_json_encode( __( 'Document sent to parent.', 'documentate' ) ); ?>,
false,
true
);

} else if (conversionConfig.useChannel) {
// POPUP MODE with BroadcastChannel
if (conversionConfig.outputAction === 'preview' && result.outputFormat === 'pdf') {
// For preview: reuse this popup window to show the PDF
// Notify opener that we're done (so it hides the loading modal)
Expand Down Expand Up @@ -331,15 +401,18 @@ function updateStatus(title, message, isError = false, isSuccess = false) {

} catch (error) {
console.error('Documentate conversion error:', error);
const errorMessage = error.message || <?php echo wp_json_encode( __( 'Conversion error.', 'documentate' ) ); ?>;

if (conversionConfig.useChannel) {
// Send error to opener via channel
sendToChannel('error', null, error.message || <?php echo wp_json_encode( __( 'Conversion error.', 'documentate' ) ); ?>);
// Send error to parent (works for both iframe and popup modes)
if (isInIframe && conversionConfig.isIframeMode) {
sendResult('error', null, errorMessage);
} else if (conversionConfig.useChannel) {
sendToChannel('error', null, errorMessage);
}

updateStatus(
<?php echo wp_json_encode( __( 'Error', 'documentate' ) ); ?>,
error.message || <?php echo wp_json_encode( __( 'Conversion error.', 'documentate' ) ); ?>,
errorMessage,
true
);
}
Expand Down
137 changes: 137 additions & 0 deletions admin/js/coi-serviceworker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*! coi-serviceworker v0.1.7 - Guido Zuidhof, licensed under MIT */
/*
* Adapted for Documentate WordPress Plugin.
*
* This Service Worker enables Cross-Origin Isolation for environments
* where server headers cannot be configured (like WordPress Playground).
*
* It intercepts all fetch requests and adds the necessary COOP/COEP headers
* to enable SharedArrayBuffer support required by ZetaJS/LibreOffice WASM.
*/

let coepCredentialless = false;
if (typeof window === 'undefined') {
self.addEventListener("install", () => self.skipWaiting());
self.addEventListener("activate", (e) => e.waitUntil(self.clients.claim()));

self.addEventListener("message", (ev) => {
if (!ev.data) {
return;
} else if (ev.data.type === "deregister") {
self.registration
.unregister()
.then(() => {
return self.clients.matchAll();
})
.then((clients) => {
clients.forEach((client) => client.navigate(client.url));
});
} else if (ev.data.type === "coepCredentialless") {
coepCredentialless = ev.data.value;
}
});

self.addEventListener("fetch", function (event) {
const r = event.request;
if (r.cache === "only-if-cached" && r.mode !== "same-origin") {
return;
}

const request =
coepCredentialless && r.mode === "no-cors"
? new Request(r, {
credentials: "omit",
})
: r;

event.respondWith(
fetch(request)
.then((response) => {
if (response.status === 0) {
return response;
}

const newHeaders = new Headers(response.headers);
newHeaders.set("Cross-Origin-Embedder-Policy",
coepCredentialless ? "credentialless" : "require-corp"
);
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin");

return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders,
});
})
.catch((e) => console.error(e))
);
});

} else {
(() => {
const reloadedBySelf = window.sessionStorage.getItem("coiReloadedBySelf");
window.sessionStorage.removeItem("coiReloadedBySelf");
const coepDegrading = (reloadedBySelf === "coepDegrade");

// You can customize the behavior of this script by setting coi configuration
// on the global scope before loading.
const coi = {
shouldRegister: () => !reloadedBySelf,
shouldDeregister: () => false,
coepCredentialless: () => true,
coepDegrade: () => true,
doReload: () => {
window.sessionStorage.setItem("coiReloadedBySelf",
coepDegrading ? "coepDegrade" : "true");
window.location.reload();
},
quiet: false,
...window.coi
};

const n = navigator;

if (coi.shouldDeregister()) {
n.serviceWorker &&
n.serviceWorker.controller &&
n.serviceWorker.controller.postMessage({ type: "deregister" });
}

// If we're already cross-origin isolated, no need for the service worker
if (window.crossOriginIsolated) {
!coi.quiet && console.log("Documentate COI: already cross-origin isolated");
return;
}

if (!coi.shouldRegister()) {
!coi.quiet && console.log("Documentate COI: will not register (already reloaded)");
return;
}

if (!n.serviceWorker) {
!coi.quiet && console.error("Documentate COI: ServiceWorker API not available");
return;
}

n.serviceWorker.register(window.document.currentScript.src).then(
(registration) => {
!coi.quiet && console.log("Documentate COI: Service Worker registered", registration.scope);

registration.addEventListener("updatefound", () => {
!coi.quiet && console.log("Documentate COI: update found, reloading page");
coi.doReload();
});

// If the service worker is already active, reload immediately
if (registration.active && !n.serviceWorker.controller) {
!coi.quiet && console.log("Documentate COI: active, reloading page");
coi.doReload();
}
},
(err) => {
!coi.quiet && console.error("Documentate COI: registration failed", err);
}
);
})();
}
Loading
Loading