A TinyMCE plugin that lets users browse/pick files from multiple cloud providers and insert them into editor content as links, images, or embeds.
Built-in provider adapters:
- Google Drive
- OneDrive
- Dropbox
- BayernCloud
No open-source TinyMCE plugin existed that lets users pick files directly from their own Google Drive, OneDrive, or Dropbox accounts without routing files through a paid third-party service. The commercial alternatives (Filestack, Uploadcare) work by copying the selected file to their own CDN — which adds per-month costs, creates a vendor dependency, and moves file ownership away from the user.
This plugin was built to fill that gap: files stay in the user's own cloud account, the plugin just brokers the picker and returns a URL. No CDN, no per-upload quota, no vendor lock-in.
| TinyMCE MultiCloud Plugin | Filestack | Uploadcare | |
|---|---|---|---|
| License / cost | MIT, free | $69–$379+/month | $0–$119+/month |
| File storage | User's own cloud account | Filestack CDN (vendor) | Uploadcare CDN (vendor) |
| Cloud sources | Google Drive, OneDrive, Dropbox, Nextcloud/BayernCloud | Drive, Dropbox, OneDrive, Box, + more | Drive, Dropbox, OneDrive, + more |
| Embed support | ✅ link / image / iframe / audio+video | ✅ via CDN URL | ✅ via CDN URL |
| Files stay in user's account | ✅ Yes | ❌ Copied to vendor CDN | ❌ Copied to vendor CDN |
| Bandwidth quota | None (provider handles delivery) | 75–400 GB/month (plan limit) | Varies by plan |
| Self-hostable | ✅ Fully (drop a JS file) | ❌ SaaS only | ❌ SaaS only |
| Open source | ✅ MIT | ❌ | ❌ |
The trade-off: because files remain in the user's cloud storage, they must be publicly accessible for embedded content to be viewable by readers. The plugin handles this automatically — it creates the public share link via the provider's API so the user does not need to configure sharing manually. A warning is shown in the upload dialog to make clear that the file will be publicly accessible to anyone with the link.
Different cloud providers have different OAuth flows, SDKs, and picker UX constraints. The plugin uses a provider adapter contract and popup bridge protocol, so each provider can implement its own picker while TinyMCE integration stays consistent.
Design-by-Contract validation (XDBC)
This plugin uses XDBC for Design-by-Contract (DBC) validation of all plugin options and provider configurations. Instead of scattered if/throw guards, every precondition is expressed as a typed contract that fires before any plugin logic runs.
- Plugin options object — must be a plain object (not null, not array)
providersmap — must be a plain object if provideddefaultProvider— must be a string and must exist in theprovidersmapdefaultInsertMode— must be"link","image", or"embed"if providedpopupTimeoutMs— must be a positive number if provideddialogTitle/defaultProvider— must be non-empty strings if provided- Per-provider SDK credentials — validated when SDK mode is active (i.e.
enabled: trueand nopickerUrloverride):- Google Drive:
clientIdandapiKeymust be defined, string, and match the API-key pattern - OneDrive:
clientIdmust be defined, string, and match the API-key pattern - Dropbox:
appKeymust be defined, string, and match the API-key pattern - BayernCloud:
baseUrl(valid URL),username, and eitherpasswordorbearerToken(at least one non-empty)
- Google Drive:
- Fail-fast with actionable messages — errors are surfaced at plugin init with a clear hint (e.g. "Did you set Google Drive clientId?"), not as opaque SDK failures deep in an OAuth flow.
- Contracts as documentation — the decorator stack on each validator class is a machine-readable, always-up-to-date specification of what the configuration must look like.
- Uniform error shape — all validation failures throw
DBC.Infringement(subclass ofError), making them easy tocatchand distinguish from runtime errors. - Configurable behaviour — two independent named DBC instances allow each layer to be configured separately (see Soft logging mode below):
globalThis.MultiCloud.Validation.Config— governs configuration contract checksglobalThis.MultiCloud.Validation.Boundary— governs Zod boundary schema checks on provider API data
- Single error type — all validation failures, whether from DBC contracts or Zod schema checks, throw
DBC.Infringement(XDBC'sZOD.tsCheckroutes throughDBC.reportTsCheckInfringementinternally).
Zod boundary validation
In addition to XDBC DBC contracts on configuration, the plugin validates all data that crosses provider API boundaries at runtime using XDBC's Zod integration (ZOD.tsCheck from xdbc/src/DBC/ZOD). Zod schemas are defined with the zod library but validation is always run through XDBC — keeping the error shape and behaviour consistent with the rest of the contract layer.
Where DBC validates input configuration before any logic runs, XDBC's Zod implementation validates what providers return before that data is trusted and used.
| Boundary | Schema | Validates |
|---|---|---|
| Any provider result | pickerResultSchema |
item.id, item.name, item.url non-empty; all URL fields are valid URLs; mode is a known enum value |
| Google Picker callback | googleDocSchema |
id required and non-empty; url, thumbnailLink are valid URLs when present |
| OneDrive navigable picker | oneDriveFileSchema |
name required; webUrl, @microsoft.graph.downloadUrl are valid URLs when present; file.mimeType is a string when present |
| Dropbox Chooser callback | dropboxFileSchema |
link required and a valid URL; thumbnailLink is a valid URL when present |
| BayernCloud WebDAV node | webDavNodeSchema |
id, name, url, webdavPath non-empty; url is a valid URL; isDirectory is a boolean |
Because validation runs through ZOD.tsCheck which calls DBC.reportTsCheckInfringement on failure, both DBC contract violations and Zod schema failures throw DBC.Infringement. The boundary layer uses its own DBC instance (globalThis.MultiCloud.Validation.Boundary) so it can be configured independently from config-layer checks.
import { DBC } from "xdbc";
try {
tinymce.init({ plugins: "multicloud", multicloud_providers: myConfig });
} catch (e) {
if (e instanceof DBC.Infringement) {
// either a configuration contract or a provider boundary schema was violated
}
}npm install
npm run build| TinyMCE version | Status |
|---|---|
| 6.x | ✅ Tested and supported |
| 7.x | ✅ Tested and supported |
npm install
npm run dev # watches src/ and rebuilds dist/ on changeFor local development with real cloud provider SDKs:
- Copy
demo/multicloud.config.example.jstodemo/multicloud.config.js. - Fill in your real credentials (this file is gitignored and will never be committed).
- Open
demo/tinymce-demo.htmlin a browser (via a local HTTP server, notfile://).
- Copy
demo/multicloud.config.example.jstodemo/multicloud.config.js. - Fill in your real cloud app IDs/keys and BayernCloud endpoint values.
- Follow the provider console checklists in
docs/PRODUCTION_SETUP.md.
Note:
demo/multicloud.config.jsis gitignored — never commit real credentials to the repository.
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/7/tinymce.min.js"></script>
<script src="./dist/index.global.js"></script>
<textarea id="editor"></textarea>
<script>
tinymce.init({
selector: "#editor",
plugins: "link image media multicloud",
toolbar: "undo redo | bold italic | link image media | multicloud multicloud_upload",
multicloud_providers: {
googleDrive: {
enabled: true,
clientId: "GOOGLE_OAUTH_CLIENT_ID",
apiKey: "GOOGLE_BROWSER_API_KEY",
scopes: ["https://www.googleapis.com/auth/drive.file"]
},
oneDrive: {
enabled: true,
clientId: "ONEDRIVE_CLIENT_ID",
action: "query"
},
dropbox: {
enabled: true,
appKey: "DROPBOX_APP_KEY",
linkType: "preview"
},
bayerncloud: {
enabled: true,
// Option 1: Use interactive picker (prompts user for credentials)
pickerUrl: "./pickers/bayerncloud.html"
// Option 2: Use WebDAV with pre-configured credentials
// mode: "nextcloud-webdav",
// baseUrl: "https://your-nextcloud.example.com",
// username: "your-username",
// password: "app-password", // Use app-specific password
// webdavPath: "", // Optional subfolder
// createPublicShare: true // Creates public share links
}
},
multicloud_default_provider: "googleDrive",
multicloud_default_insert_mode: "link",
multicloud_dialog_title: "Insert From Cloud",
multicloud_popup_timeout_ms: 120000
});
</script>The plugin provides two toolbar buttons:
multicloud: Opens a file picker to browse and select files from cloud providersmulticloud_upload: Opens a dialog to upload local files to cloud providers
Some providers support uploading local files directly to the cloud:
Google Drive: ✅ Full upload support
- Uploads files to user's Drive
- Creates public sharing link
- Embeds PDFs and Office documents
Nextcloud/BayernCloud: ✅ Full upload support (both modes)
- Picker mode (
pickerUrl): Opens upload UI in picker, uses OAuth authentication - WebDAV mode: Uses pre-configured credentials for direct upload
- Uploads via WebDAV PUT request
- Creates public share links (if
createPublicShare: true) - Embeds PDFs and Office documents
- Requires CORS proxy for browser deployments (see docs)
OneDrive: ✅ Full upload support
- Uploads files to the user's OneDrive via the Microsoft Graph API
- Creates a public sharing link
- Embeds PDFs and Office documents
Dropbox: ✅ Full upload support
- Uploads files to the user's Dropbox via the Dropbox API
- Uses OAuth implicit grant flow; token is cached in
localStoragewith expiry tracking - Creates a shared link for embedding
To use upload, add multicloud_upload to your toolbar:
toolbar: "undo redo | bold italic | link image media | multicloud multicloud_upload"Each provider picker page should call window.opener.postMessage with this payload:
{
source: "tinymce-multicloud-plugin",
type: "picked", // or "cancelled"
providerId: "googleDrive",
payload: {
item: {
id: "file-id",
name: "filename.png",
url: "https://...",
embedUrl: "https://..." // optional
},
mode: "image" // "link" | "image" | "embed"
}
}A mock bridge page is available at demo/picker-bridge-example.html.
- Uses Google Identity Services for OAuth token retrieval.
- Uses gapi client initialization and Google Picker SDK for file selection.
- Required config:
clientId,apiKey.
- Uses Microsoft OneDrive JavaScript picker SDK (
OneDrive.open). - Required config:
clientId.
- Uses Dropbox Chooser SDK (
Dropbox.choose). - Required config:
appKey.
⚠️ Requires CORS Proxy: Nextcloud APIs have CORS restrictions - browser deployments require a proxy (see below)- Supports two modes:
- Interactive Picker (
pickerUrl): Opens a popup with Nextcloud Login Flow v2 (OAuth-like), browses files via WebDAV, creates public share links - Pre-configured WebDAV (
mode: "nextcloud-webdav"): Uses pre-configured credentials for programmatic file access
- Interactive Picker (
- Uses Nextcloud Login Flow v2 for secure authentication
- Uses WebDAV
PROPFINDto list files - Optional OCS share creation for public links
- Works with any Nextcloud instance (BayernCloud, private Nextcloud servers, etc.)
- CORS Proxy Setup: See
docs/CLOUDFLARE_WORKER_SETUP.mdfor free Cloudflare Worker proxy (100k requests/day free) - Alternative: Deploy plugin on same domain as Nextcloud (no proxy needed)
Nextcloud instances block browser requests from different domains. To enable browser-based access:
Option 1: Cloudflare Worker (Recommended - Free)
- Follow
docs/CLOUDFLARE_WORKER_SETUP.md - Deploy the worker from
cloudflare-worker/nextcloud-proxy.js - Update picker config with worker URL
- ✅ Works from GitHub Pages, any domain
Option 2: Same-Origin Deployment
- Host plugin on same domain as Nextcloud
- No proxy needed (same-origin = no CORS)
Option 3: Server-Side Integration
- Use Nextcloud provider in backend/Node.js
- No CORS issues in server-to-server requests
Every built-in provider supports pickerUrl. If pickerUrl is set, the plugin opens that custom picker page and uses the bridge contract instead of the built-in SDK flow.
- Configure OAuth consent screen and allowed JS origins in Google Cloud.
⚠️ Google Drive embeds require the viewer to be signed in to GoogleAll Google Drive embeds (
drive.google.com/file/d/.../preview) require Google cookies inside the iframe. Modern browsers block third-party cookies by default, so any viewer who is not already signed in to Google in their browser will see a login prompt instead of the file.Uploaded files — the plugin automatically sets sharing to "Anyone with the link" after upload, so the file itself is accessible, but the embed still requires Google cookies.
Picked files — sharing is not changed by the plugin. Files remain at whatever sharing setting they had in Drive (usually "Restricted" / private). Viewers who are not the file owner will not be able to see them at all.
Recommendations:
- For content that must be publicly visible to all readers, use the upload button (↑) rather than the picker (↓), or manually set the file to "Anyone with the link" in Google Drive first.
- To set a file to "Anyone with the link" in Google Drive: right-click the file → Share → click "Restricted" dropdown → select "Anyone with the link" → click Done.
- For truly public embeds with no sign-in requirement, prefer Dropbox or OneDrive — their embed mechanisms do not depend on the viewer having a Google account.
- Configure redirect URI and tenant restrictions in Entra/Microsoft app registration.
- Ensure Chooser domain allowlist matches your deployment domain.
- For production, prefer bearer tokens or backend proxy endpoints over raw credentials in browser config.
- Do not embed long-lived secrets in frontend plugin config.
- Prefer backend endpoints for OAuth code exchange and token management.
- Explicitly inform users before changing sharing permissions (public link/embed).
- Validate inserted URLs server-side if your application later renders them in high-trust contexts.
The plugin uses two independent DBC instances — one for configuration checks and one for Zod boundary schema checks — so each layer can be configured separately. By default both layers throw DBC.Infringement on violations.
Use configureMultiCloudValidation with config and/or boundary sub-keys to change the behaviour of either layer:
import { configureMultiCloudValidation } from 'tinymce-multicloud-plugin';
// Soft-log config violations only (useful for hardened production deployments).
// Boundary checks remain strict — unexpected API shapes still throw.
configureMultiCloudValidation({
config: { throwOnInfringement: false, logToConsole: true },
});
// Both layers in soft logging mode:
configureMultiCloudValidation({
config: { throwOnInfringement: false, logToConsole: true },
boundary: { throwOnInfringement: false, logToConsole: true },
});
// Then initialize TinyMCE as usual
tinymce.init({ ... });Or using the global bundle:
<script src="./dist/index.global.js"></script>
<script>
TinyMceMultiCloudPlugin.configureMultiCloudValidation({
config: { throwOnInfringement: false, logToConsole: true },
});
tinymce.init({ ... });
</script>| Layer | DBC path | What it covers |
|---|---|---|
config |
globalThis.MultiCloud.Validation.Config |
Plugin options, provider credential shape, defaultProvider contract |
boundary |
globalThis.MultiCloud.Validation.Boundary |
Provider API response shapes (Zod schemas) |
Note:
configureMultiCloudValidationmust be called beforetinymce.init(). Contract checks run at plugin initialization time — once the plugin is registered and your options have been validated, changing these settings has no retroactive effect.
Recommendation: Keep both layers at their default
throwOnInfringement: trueduring development. Soft logging mode forconfigis intended for hardened production deployments where all configuration has been verified. Theboundarylayer should generally stay strict — unexpected shapes in provider API responses indicate a real integration problem.
Open demo/tinymce-demo.html after building. The demo includes mock pickers under demo/pickers/ for all providers.
The demo auto-loads demo/multicloud.config.js if present; otherwise it falls back to local mock picker pages.
How each provider handles different file types. Insert modes: image = <img>, embed = <iframe>, audio = <audio>, video = <video>, link = <a>.
| File type | Extensions | Insert mode | Notes |
|---|---|---|---|
| Images | png, jpg, jpeg, gif, webp, bmp, svg, tiff | image | Direct <img> via Drive preview URL |
| Audio | mp3, wav, ogg, aac, m4a, flac, opus | audio | <iframe> preview (Drive transcodes audio) |
| Video | mp4, webm, mov, avi, mkv, m4v, wmv, flv | embed | <iframe> preview (Drive transcodes video) |
| embed | <iframe> via Drive preview URL |
||
| Office (OOXML) | docx, xlsx, pptx | embed | <iframe> via Drive preview URL |
| Office (legacy) | doc, xls, ppt | embed | <iframe> via Drive preview URL |
| OpenDocument | odt, ods, odp | embed | <iframe> via Drive preview URL |
| Archives | zip, rar, 7z, tar, gz, bz2, xz | link | Download link only |
| Other | anything else | link | Download link |
| File type | Extensions | Insert mode | Notes |
|---|---|---|---|
| Images | png, jpg, jpeg, gif, svg, webp, bmp | image | Raw CDN URL via dl.dropboxusercontent.com |
| Audio | mp3, wav, ogg, aac, m4a, flac, opus, oga, weba | audio | <audio> with raw CDN URL |
| Video | mp4, webm, ogg, mov, m4v, avi, wmv, flv, mkv | embed | <video> with raw CDN URL |
| embed | <iframe> via Google Docs Viewer |
||
| Office (OOXML) | docx, xlsx, pptx, doc, xls, ppt | embed | <iframe> via Microsoft Office Online viewer |
| OpenDocument | odt, ods, odp | link | Google Docs Viewer cannot reliably load ODF |
| Archives | zip, rar, 7z, tar, gz, bz2, xz | link | Download link only |
| Other | anything else | link | Download link |
| File type | Extensions | Insert mode | Notes |
|---|---|---|---|
| Images | png, jpg, jpeg, gif, svg, webp, bmp, tiff, apng, avif | image | Direct embed URL from OneDrive |
| Audio | mp3, wav, ogg, aac, m4a, flac, opus | audio | <audio> or <iframe> depending on download URL availability |
| Video | mp4, webm, ogg, mov, m4v, avi, wmv, flv, mkv | embed | <video> or <iframe> |
| embed | <iframe> |
||
| Office (OOXML) | docx, xlsx, pptx, doc, xls, ppt | embed | <iframe> |
| OpenDocument | odt, ods, odp | embed | <iframe> |
| Archives | zip, rar, 7z, tar, gz, bz2, xz | link | Download link only |
| Other | anything else | link | Download link |
| File type | Extensions | Insert mode | Notes |
|---|---|---|---|
| Images | png, jpg, jpeg, gif, webp, bmp, tiff, apng, avif | image | <img> via public share URL |
| SVG | svg | link | Cross-origin SVG cannot be embedded reliably |
| Audio | mp3, wav, ogg, aac, m4a, flac, opus | link | Cross-origin streaming unreliable |
| Video | mp4, webm, ogg, mov, avi, wmv, flv, mkv | link | Cross-origin streaming unreliable |
| embed | <iframe> via Google Docs Viewer (requires public share) |
||
| Office (OOXML) | docx, xlsx, pptx | embed | <iframe> via Google Docs Viewer (requires public share) |
| Office (legacy) | doc, xls, ppt | link | Viewer support unreliable for legacy formats |
| OpenDocument | odt, ods, odp | link | Viewer support unreliable for ODF |
| Archives | zip, rar, 7z, tar, gz, bz2, xz | link | Download link only |
| Other | anything else | link | Download link |
Note on Nextcloud embedding: Google Docs Viewer fetches files from its own servers, so the Nextcloud share link must be publicly accessible (not password-protected or on a private network). Embedding may fail intermittently due to Google's rate limiting on the viewer service.
This repository gives you:
- production-ready TinyMCE plugin shell
- multi-provider adapter model
- real Google Drive integration (GIS + Picker)
- real OneDrive and Dropbox picker integrations
- BayernCloud Nextcloud/WebDAV adapter
- popup bridge fallback protocol
What you still need per provider:
- production credential/token strategy (server-backed where possible)
- tenant/security policy integration