Documentation
¶
Overview ¶
Package external is the one place the runtime asks "how is OnlyOffice / drawio configured right now?".
Why this exists ¶
Before it, the answer came from two places that could not agree. The admin UI wrote `external_services` rows and the capability probe read them, so `GET /api/files/capabilities` — the thing the explorer consults to decide whether to offer the Office editor at all — reported the operator's edit immediately. But the code that USES the configuration read `cfg.ExternalServices`, a snapshot taken from env/YAML once at boot: the OnlyOffice service was constructed (or not) in server.New, and the converter URL was baked into the AI handlers in routes.go. An operator who configured the services in the admin UI — the documented way, and the only way when the services live in a separate compose file — therefore got a green Test button, a capability payload that said "configured", and a 503 `{"error":"onlyoffice not configured"}` the moment they opened a document (issue #17). Thumbnails kept working throughout, because the thumbnailer shells out to local binaries and never consults external services at all — that asymmetry is what named the bug.
The rule ¶
The DB row is the single runtime source of truth. Env/YAML is declarative configuration for it: a non-empty value is ASSERTED onto the row at every boot (see server.seedExternalDefaults), so a compose-managed install keeps behaving exactly as before and an edit to FILEX_ONLYOFFICE_URL still takes effect. Anything env does not pin is owned by the admin UI and applies live, with no restart.
Index ¶
- Constants
- func CallbackURLFromOptions(optionsJSON string) string
- func EditorLangFromOptions(optionsJSON string) string
- func HasWarning(list []Advisory) bool
- func HostClass(rawURL string) string
- func WithCallbackURL(optionsJSON, rawURL string) (string, error)
- func WithEditorLang(optionsJSON, lang string) (string, error)
- type Advisory
- type AdvisoryInput
- type LookupFunc
- type Resolver
- type Settings
Constants ¶
const ( SeverityWarning = "warning" SeverityNote = "note" )
Advisory severities. `warning` colours the card and withholds the "configuration complete" badge; `note` is information the operator may want and never blocks anything.
const ( FieldURL = "url" FieldPublicURL = "public_url" )
Advisory fields.
const ( // CodeBrowserBareHost — the Document Server URL's host is a single label // with no dot: `http://onlyoffice`, a container name. It resolves only // inside the container network. A browser never can. CodeBrowserBareHost = "browser_bare_host" // CodeBrowserLoopbackHost — the Document Server URL is a loopback address // while filex itself is published somewhere else, so the people who reach // filex are not on this host and `localhost` means their own machine. CodeBrowserLoopbackHost = "browser_loopback_host" // CodePublicURLLoopback — FILEX_PUBLIC_URL is loopback. Inside the // Document Server's own container, that address is the Document Server. CodePublicURLLoopback = "public_url_loopback" // CodePublicURLBareHost — FILEX_PUBLIC_URL is a container name. Correct // for the callback, wrong for every share link built from it. This is the // one setup that genuinely wants two different values; see docs/ONLYOFFICE.md. CodePublicURLBareHost = "public_url_bare_host" // CodePublicURLUnresolved — FILEX_PUBLIC_URL's hostname does not resolve // on this machine. A note, not a warning: split-horizon DNS is a real and // working arrangement. CodePublicURLUnresolved = "public_url_unresolved" // CodeJWTNotEnforced: the document server took a conversion request that // carried no token, so JWT is off on it. Measured by the Test, never // guessed from the configuration (onlyoffice.CheckReversePath). CodeJWTNotEnforced = "jwt_not_enforced" // CodeEditorSameOrigin - ONLYOFFICE is configured and its editor has no // frame on another origin (neither FILEX_ONLYOFFICE_FRAME_ORIGIN nor // FILEX_APP_UI_ORIGIN), so its api.js runs in filex's own page, with the // signed-in person's session (task #92). A note, not a warning: the setup // works, and the document server is infrastructure the operator chose to // trust. CodeEditorSameOrigin = "editor_same_origin" )
Advisory codes. The admin UI translates by code (EN + TR); Message is the English fallback for API and MCP callers, who get no i18n bundle.
const ( ClassEmpty = "" ClassLoopback = "loopback" ClassUnspecified = "unspecified" ClassIP = "ip" ClassDotted = "dotted" ClassBare = "bare" )
Host classes returned by HostClass.
const ( OnlyOffice = "onlyoffice" Drawio = "drawio" )
Known service names. These are the rows server.seedExternalDefaults creates.
const EnvPinEditorLang = OnlyOffice + ".editor_lang"
EnvPinEditorLang is the key, in the "pinned by the environment" map the admin handler is given (api.envManagedExternal), that says FILEX_ONLYOFFICE_LANG pins ONLYOFFICE's editor language. The map's other keys are service names; this one is a field of one.
const FieldSecret = "secret"
FieldSecret is the JWT secret field, for an advisory about JWT.
const OptionKeyCallbackURL = "callback_url"
OptionKeyCallbackURL is where CallbackURL lives inside options_json.
const OptionKeyEditorLang = "editor_lang"
OptionKeyEditorLang is where EditorLang lives inside options_json.
Variables ¶
This section is empty.
Functions ¶
func CallbackURLFromOptions ¶
CallbackURLFromOptions reads the callback URL out of a row's options blob. A malformed blob reads as empty rather than failing: this is configuration the operator can retype, not a reason to refuse to serve documents.
func EditorLangFromOptions ¶ added in v0.54.0
EditorLangFromOptions reads the editor language out of a row's options blob, as stored ("" when absent or the blob is malformed).
func HasWarning ¶
HasWarning reports whether any advisory is severity warning — the signal the admin UI uses to withhold a "configuration complete" badge.
func HostClass ¶
HostClass buckets a URL's host into the only distinctions that matter for "can a machine that is not this one reach it?".
loopback localhost, *.localhost, 127.0.0.0/8, ::1 unspecified 0.0.0.0, :: ip any other literal address (LAN or public — both routable) dotted a name with a dot: office.example.com, files.lan bare a single-label name: onlyoffice, filex — container-network only
func WithCallbackURL ¶
WithCallbackURL returns optionsJSON with the callback URL set (or removed, when url is empty), preserving every other key the blob carries.
func WithEditorLang ¶ added in v0.54.0
WithEditorLang returns optionsJSON with the editor language set, or removed when lang is "" or "auto" (the default needs no key), preserving every other key the blob carries. The caller has checked lang (onlyoffice.NormalizeEditorLang).
Types ¶
type Advisory ¶
type Advisory struct {
Code string `json:"code"`
Field string `json:"field"`
Severity string `json:"severity"`
// Detail is the offending value, so a log line or an MCP caller can see
// what triggered the advisory without re-reading the row.
Detail string `json:"detail,omitempty"`
// Message is English. The admin UI prefers its own translation of Code.
Message string `json:"message"`
}
Advisory is one thing worth saying about a configuration that no probe from this process can settle.
func Advise ¶
func Advise(in AdvisoryInput) []Advisory
Advise is Advisories with the callback address included.
func Advisories ¶
func Advisories(service, serviceURL, publicURL string, publicURLSet bool, lookup LookupFunc) []Advisory
Advisories returns everything worth saying about one service's addresses.
serviceURL is the row's URL; publicURL and publicURLSet come from the process config (publicURLSet is false when nobody chose one and it defaulted to http://localhost:5212 — worth saying out loud, because the operator does not know a default is in play). lookup may be nil, which skips the DNS note.
func JWTNotEnforced ¶ added in v0.50.0
JWTNotEnforced is the warning for a document server that does not enforce JWT. A warning, not a note: measured with ONLYOFFICE Docs 9.4 (e2e/realenv, issue #80 S2), such a server still checks the token in filex's editor configuration against its own secret and refuses it ("checkJwt error ... invalid signature" in its log), so no document opens; it refuses to fetch from private addresses, and its save callbacks arrive unsigned and filex refuses them. It does NOT ignore filex's signature, as this said before.
type AdvisoryInput ¶
type AdvisoryInput struct {
Service string
ServiceURL string
PublicURL string
PublicURLSet bool
// CallbackURL is the separate address the service uses to reach filex,
// when the operator set one. It, not PublicURL, is what the callback
// advisories must judge: the whole point of the field is that the two can
// differ.
CallbackURL string
Lookup LookupFunc
// AppUIOrigin is FILEX_APP_UI_ORIGIN and OfficeFrameOrigin
// FILEX_ONLYOFFICE_FRAME_ORIGIN: with either the ONLYOFFICE editor runs in
// a frame on another origin, with neither in filex's own page
// (CodeEditorSameOrigin).
AppUIOrigin string
OfficeFrameOrigin string
}
AdvisoryInput is one service's addresses, as they are right now.
type LookupFunc ¶
LookupFunc reports whether host is DEFINITELY absent from DNS as this machine sees it.
⚠ It must return false for every inconclusive answer — timeout, SERVFAIL, no resolver configured. An advisory raised on a slow resolver would fire on a working setup, which is the failure mode this whole file exists to avoid.
func SystemLookup ¶
func SystemLookup(ctx context.Context, timeout time.Duration) LookupFunc
SystemLookup returns a LookupFunc backed by this machine's resolver.
⚠ It answers true ONLY for an authoritative "no such host". A timeout, a SERVFAIL or a machine with no resolver at all answers false, because an advisory raised on those would fire on setups that work.
type Resolver ¶
type Resolver struct {
// contains filtered or unexported fields
}
Resolver answers from the `external_services` table, with a short cache so a per-request lookup does not become a per-request query.
The cache is deliberately short (not "until invalidated"): the admin PATCH handler does call Invalidate, but a second filex process behind the same database would never see that call, and a stale answer that heals itself in a second is a far smaller problem than one that needs a restart — which is the bug this package exists to remove.
func New ¶
New constructs a Resolver. A nil store yields a Resolver that reports everything unconfigured, which is what tests and the no-DB paths want.
func (*Resolver) Get ¶
Get returns the live settings for one service. A missing row, a disabled row or a row with no URL all read as "not configured" — Settings.Enabled is only true when the service can actually be used.
func (*Resolver) Invalidate ¶
func (r *Resolver) Invalidate()
Invalidate drops the cache so the next Get re-reads the table. Called by the admin PATCH handler so an operator's change is visible on the very next request rather than up to `ttl` later.
type Settings ¶
type Settings struct {
Enabled bool
URL string
Secret string
// CallbackURL is the address the SERVICE uses to reach filex — the third
// address in the three-address problem (see advisory.go). Empty means "use
// filex's public URL", which is right whenever one address serves both
// purposes.
//
// ⚠ It exists because that is not always true. filex has ONE public URL
// and it builds both the share links people click and the document URL it
// hands the document server; a container that cannot resolve the public
// hostname needs a different address for the second, and until this field
// there was no way to give it one — the document opened and the save never
// came back (issue #17, third round). Stored in options_json so no
// migration is needed and the row keeps one shape.
CallbackURL string
// EditorLang is the administrator's ONLYOFFICE editor language: "" or
// "auto" (each person's own) or a language code, AS STORED - the
// onlyoffice package reads it (onlyoffice.NormalizeEditorLang) and treats
// anything it does not offer as "auto". Set on External services or by
// FILEX_ONLYOFFICE_LANG; in options_json like CallbackURL, so no migration.
EditorLang string
}
Settings is one service's live configuration.