@brftech/filex-core 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/filex-core.js +4322 -4214
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +38 -38
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index.d.ts +110 -0
- package/package.json +1 -1
- package/src/index.ts +15 -0
- package/src/lib/externalReach.ts +270 -0
- package/src/modals/PreviewModal.vue +5 -1
package/dist/index.d.ts
CHANGED
|
@@ -443,6 +443,95 @@ export declare type AuthConfig = {
|
|
|
443
443
|
csrf: string;
|
|
444
444
|
};
|
|
445
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Injection seam. The defaults touch the DOM and the network; tests replace
|
|
448
|
+
* them so the classification logic can be asserted without either.
|
|
449
|
+
*/
|
|
450
|
+
export declare interface BrowserProbeDeps {
|
|
451
|
+
loadScript(url: string, timeoutMs: number): Promise<'load' | 'error' | 'timeout'>;
|
|
452
|
+
loadFramed(url: string, timeoutMs: number): Promise<'ready' | 'timeout'>;
|
|
453
|
+
reachable(url: string): Promise<boolean>;
|
|
454
|
+
hasDocsAPI(): boolean;
|
|
455
|
+
pageProtocol(): string;
|
|
456
|
+
now(): number;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export declare interface BrowserProbeResult {
|
|
460
|
+
service: string;
|
|
461
|
+
state: BrowserProbeState;
|
|
462
|
+
/** The exact URL attempted — the same one the real viewer loads. */
|
|
463
|
+
url: string;
|
|
464
|
+
/** Milliseconds elapsed. Worth showing: a 6 s "unreachable" is a DNS story. */
|
|
465
|
+
ms: number;
|
|
466
|
+
mechanism: 'script' | 'iframe' | 'none';
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* externalReach — "can THIS browser reach the external service?"
|
|
471
|
+
*
|
|
472
|
+
* # Why this exists
|
|
473
|
+
*
|
|
474
|
+
* Three machines must reach three addresses before the Office editor works,
|
|
475
|
+
* and until now only one of them was ever checked:
|
|
476
|
+
*
|
|
477
|
+
* | address | who must reach it | who checked it |
|
|
478
|
+
* | ---------------------- | ------------------------------------- | ---------------- |
|
|
479
|
+
* | the Document Server URL| the **browser** (loads the editor JS) | nobody |
|
|
480
|
+
* | the Document Server URL| the filex process | the Test button |
|
|
481
|
+
* | `FILEX_PUBLIC_URL` | the **document server** (fetch + save)| nobody |
|
|
482
|
+
*
|
|
483
|
+
* So an operator on podman types `http://onlyoffice`, filex reaches it, Test
|
|
484
|
+
* goes green, and the browser cannot resolve that name at all. The editor then
|
|
485
|
+
* fails with the same message as a missing configuration — issue #17, twice.
|
|
486
|
+
*
|
|
487
|
+
* The admin page runs **in the browser that will actually open the editor**,
|
|
488
|
+
* so it can answer the first row directly instead of disclaiming it. That is
|
|
489
|
+
* what this module does.
|
|
490
|
+
*
|
|
491
|
+
* # Mechanism, and why not `fetch`
|
|
492
|
+
*
|
|
493
|
+
* ⚠ A plain `fetch()` is the obvious choice and the wrong one: a document
|
|
494
|
+
* server that works perfectly will usually answer without CORS headers, the
|
|
495
|
+
* promise rejects, and a naive `catch` reports failure for a healthy service.
|
|
496
|
+
*
|
|
497
|
+
* Instead each service is probed **the same way the real viewer loads it**:
|
|
498
|
+
*
|
|
499
|
+
* - **onlyoffice** — a `<script>` pointing at
|
|
500
|
+
* `<base>/web-apps/apps/api/documents/api.js`. Script `load`/`error` is not
|
|
501
|
+
* subject to CORS for load detection, and a successful load leaves
|
|
502
|
+
* `window.DocsAPI.DocEditor` defined, which proves the thing that answered
|
|
503
|
+
* really is a Document Server. (Verified against a live OnlyOffice Docs
|
|
504
|
+
* instance: `load` + `DocsAPI` in ~0.5 s.)
|
|
505
|
+
* - **drawio** — a hidden `<iframe>` at `<base>/?embed=1&proto=json`, which
|
|
506
|
+
* posts `{"event":"init"}` to its parent when the editor is ready. Same
|
|
507
|
+
* handshake `DrawioViewer.vue` uses. (Verified against a live drawio.)
|
|
508
|
+
*
|
|
509
|
+
* Both distinguish **"could not reach"** from **"reached, wrong thing"** with
|
|
510
|
+
* a second signal: a `no-cors` `fetch`, whose promise resolves for any HTTP
|
|
511
|
+
* status and rejects only on a network-level failure. Reached + no editor =
|
|
512
|
+
* `wrong-content`; not reached at all = `unreachable`.
|
|
513
|
+
*
|
|
514
|
+
* Every probe is bounded by a timeout and reports the timeout as its own
|
|
515
|
+
* state, so a black-holed address stops the spinner instead of hanging.
|
|
516
|
+
*/
|
|
517
|
+
/** The result of one probe. Each value is a different sentence to the operator. */
|
|
518
|
+
export declare type BrowserProbeState =
|
|
519
|
+
/** Loaded, and it really is the service it claims to be. */
|
|
520
|
+
'ok'
|
|
521
|
+
/** Something answered at that address, but it is not this service. */
|
|
522
|
+
| 'wrong-content'
|
|
523
|
+
/** Nothing answered: DNS failure, connection refused, blocked. */
|
|
524
|
+
| 'unreachable'
|
|
525
|
+
/** Nothing answered and nothing refused either — a black hole. */
|
|
526
|
+
| 'timeout'
|
|
527
|
+
/** The admin page is HTTPS and the service URL is HTTP; the browser will refuse. */
|
|
528
|
+
| 'blocked-mixed-content'
|
|
529
|
+
/** No URL configured, or a service with no browser-side entry point to probe. */
|
|
530
|
+
| 'skipped';
|
|
531
|
+
|
|
532
|
+
/** The URL each service is probed at — the same one its viewer loads. */
|
|
533
|
+
export declare function browserProbeURL(service: string, base: string): string;
|
|
534
|
+
|
|
446
535
|
/**
|
|
447
536
|
* Every command here was RUN against the endpoint on 2026-08-16 with curl
|
|
448
537
|
* 8.5, lftp 4.9 and rclone 1.73 — including the settings each of them needs to
|
|
@@ -659,6 +748,14 @@ export declare function declineEscrowSlot(marker: E2eMarker, when: string): E2eM
|
|
|
659
748
|
*/
|
|
660
749
|
export declare function decryptFile(fmk: CryptoKey, data: ArrayBuffer): Promise<ArrayBuffer>;
|
|
661
750
|
|
|
751
|
+
/**
|
|
752
|
+
* Default timeout. ⚠ Measured, not guessed: a browser takes ~5 s to give up on
|
|
753
|
+
* a DNS name that does not exist (`http://onlyoffice`), and that case must
|
|
754
|
+
* report `unreachable`, not `timeout`. 10 s leaves room and still bounds a
|
|
755
|
+
* black hole.
|
|
756
|
+
*/
|
|
757
|
+
export declare const DEFAULT_PROBE_TIMEOUT_MS = 10000;
|
|
758
|
+
|
|
662
759
|
export declare const DEFAULT_THEME_ID = "default";
|
|
663
760
|
|
|
664
761
|
/** The default backing store, or null where the platform has none. Reading
|
|
@@ -2395,6 +2492,19 @@ monacoEl: HTMLDivElement;
|
|
|
2395
2492
|
officeEl: HTMLDivElement;
|
|
2396
2493
|
}, any>;
|
|
2397
2494
|
|
|
2495
|
+
/**
|
|
2496
|
+
* Probe one external service from the browser running this page.
|
|
2497
|
+
*
|
|
2498
|
+
* Returns `skipped` — never a failure — for a service with no URL and for a
|
|
2499
|
+
* service filex does not load in the browser at all (the converter), because
|
|
2500
|
+
* reporting "unreachable" for something the browser never fetches would be a
|
|
2501
|
+
* new lie in place of the old one.
|
|
2502
|
+
*/
|
|
2503
|
+
export declare function probeExternalFromBrowser(service: string, base: string | null | undefined, opts?: {
|
|
2504
|
+
timeoutMs?: number;
|
|
2505
|
+
deps?: Partial<BrowserProbeDeps>;
|
|
2506
|
+
}): Promise<BrowserProbeResult>;
|
|
2507
|
+
|
|
2398
2508
|
export declare interface ProtocolGuide {
|
|
2399
2509
|
id: string;
|
|
2400
2510
|
/** Protocol name — a wire protocol is not translated either. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brftech/filex-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "filex core — Vue 3 source of truth for the filex file manager (FileExplorer + ConnectionsPanel SFCs, composables, types)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/filex-core.umd.cjs",
|
package/src/index.ts
CHANGED
|
@@ -59,6 +59,21 @@ export { useFileApi, resolveEndpoints } from './composables/useFileApi';
|
|
|
59
59
|
export type { FileApi, ManagerResponse, PendingOpDto } from './composables/useFileApi';
|
|
60
60
|
/* bul:s3 — global-search contract types + snippet helpers */
|
|
61
61
|
export type { GlobalSearchHit, GlobalSearchScope } from './composables/useFileApi';
|
|
62
|
+
/* Browser-side reachability probe for external services (OnlyOffice, drawio).
|
|
63
|
+
Lives here, not in the admin app, because the admin page and every embedder
|
|
64
|
+
need the same answer to "can THIS browser reach the document server?" — see
|
|
65
|
+
lib/externalReach.ts for why a plain fetch() is the wrong mechanism. */
|
|
66
|
+
export {
|
|
67
|
+
probeExternalFromBrowser,
|
|
68
|
+
browserProbeURL,
|
|
69
|
+
DEFAULT_PROBE_TIMEOUT_MS,
|
|
70
|
+
} from './lib/externalReach';
|
|
71
|
+
export type {
|
|
72
|
+
BrowserProbeState,
|
|
73
|
+
BrowserProbeResult,
|
|
74
|
+
BrowserProbeDeps,
|
|
75
|
+
} from './lib/externalReach';
|
|
76
|
+
|
|
62
77
|
export { snippetSegments, matchedInContent } from './lib/snippet';
|
|
63
78
|
export type { SnippetSegment, SearchMatched } from './lib/snippet';
|
|
64
79
|
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* externalReach — "can THIS browser reach the external service?"
|
|
3
|
+
*
|
|
4
|
+
* # Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Three machines must reach three addresses before the Office editor works,
|
|
7
|
+
* and until now only one of them was ever checked:
|
|
8
|
+
*
|
|
9
|
+
* | address | who must reach it | who checked it |
|
|
10
|
+
* | ---------------------- | ------------------------------------- | ---------------- |
|
|
11
|
+
* | the Document Server URL| the **browser** (loads the editor JS) | nobody |
|
|
12
|
+
* | the Document Server URL| the filex process | the Test button |
|
|
13
|
+
* | `FILEX_PUBLIC_URL` | the **document server** (fetch + save)| nobody |
|
|
14
|
+
*
|
|
15
|
+
* So an operator on podman types `http://onlyoffice`, filex reaches it, Test
|
|
16
|
+
* goes green, and the browser cannot resolve that name at all. The editor then
|
|
17
|
+
* fails with the same message as a missing configuration — issue #17, twice.
|
|
18
|
+
*
|
|
19
|
+
* The admin page runs **in the browser that will actually open the editor**,
|
|
20
|
+
* so it can answer the first row directly instead of disclaiming it. That is
|
|
21
|
+
* what this module does.
|
|
22
|
+
*
|
|
23
|
+
* # Mechanism, and why not `fetch`
|
|
24
|
+
*
|
|
25
|
+
* ⚠ A plain `fetch()` is the obvious choice and the wrong one: a document
|
|
26
|
+
* server that works perfectly will usually answer without CORS headers, the
|
|
27
|
+
* promise rejects, and a naive `catch` reports failure for a healthy service.
|
|
28
|
+
*
|
|
29
|
+
* Instead each service is probed **the same way the real viewer loads it**:
|
|
30
|
+
*
|
|
31
|
+
* - **onlyoffice** — a `<script>` pointing at
|
|
32
|
+
* `<base>/web-apps/apps/api/documents/api.js`. Script `load`/`error` is not
|
|
33
|
+
* subject to CORS for load detection, and a successful load leaves
|
|
34
|
+
* `window.DocsAPI.DocEditor` defined, which proves the thing that answered
|
|
35
|
+
* really is a Document Server. (Verified against a live OnlyOffice Docs
|
|
36
|
+
* instance: `load` + `DocsAPI` in ~0.5 s.)
|
|
37
|
+
* - **drawio** — a hidden `<iframe>` at `<base>/?embed=1&proto=json`, which
|
|
38
|
+
* posts `{"event":"init"}` to its parent when the editor is ready. Same
|
|
39
|
+
* handshake `DrawioViewer.vue` uses. (Verified against a live drawio.)
|
|
40
|
+
*
|
|
41
|
+
* Both distinguish **"could not reach"** from **"reached, wrong thing"** with
|
|
42
|
+
* a second signal: a `no-cors` `fetch`, whose promise resolves for any HTTP
|
|
43
|
+
* status and rejects only on a network-level failure. Reached + no editor =
|
|
44
|
+
* `wrong-content`; not reached at all = `unreachable`.
|
|
45
|
+
*
|
|
46
|
+
* Every probe is bounded by a timeout and reports the timeout as its own
|
|
47
|
+
* state, so a black-holed address stops the spinner instead of hanging.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/** The result of one probe. Each value is a different sentence to the operator. */
|
|
51
|
+
export type BrowserProbeState =
|
|
52
|
+
/** Loaded, and it really is the service it claims to be. */
|
|
53
|
+
| 'ok'
|
|
54
|
+
/** Something answered at that address, but it is not this service. */
|
|
55
|
+
| 'wrong-content'
|
|
56
|
+
/** Nothing answered: DNS failure, connection refused, blocked. */
|
|
57
|
+
| 'unreachable'
|
|
58
|
+
/** Nothing answered and nothing refused either — a black hole. */
|
|
59
|
+
| 'timeout'
|
|
60
|
+
/** The admin page is HTTPS and the service URL is HTTP; the browser will refuse. */
|
|
61
|
+
| 'blocked-mixed-content'
|
|
62
|
+
/** No URL configured, or a service with no browser-side entry point to probe. */
|
|
63
|
+
| 'skipped';
|
|
64
|
+
|
|
65
|
+
export interface BrowserProbeResult {
|
|
66
|
+
service: string;
|
|
67
|
+
state: BrowserProbeState;
|
|
68
|
+
/** The exact URL attempted — the same one the real viewer loads. */
|
|
69
|
+
url: string;
|
|
70
|
+
/** Milliseconds elapsed. Worth showing: a 6 s "unreachable" is a DNS story. */
|
|
71
|
+
ms: number;
|
|
72
|
+
mechanism: 'script' | 'iframe' | 'none';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Injection seam. The defaults touch the DOM and the network; tests replace
|
|
77
|
+
* them so the classification logic can be asserted without either.
|
|
78
|
+
*/
|
|
79
|
+
export interface BrowserProbeDeps {
|
|
80
|
+
loadScript(url: string, timeoutMs: number): Promise<'load' | 'error' | 'timeout'>;
|
|
81
|
+
loadFramed(url: string, timeoutMs: number): Promise<'ready' | 'timeout'>;
|
|
82
|
+
reachable(url: string): Promise<boolean>;
|
|
83
|
+
hasDocsAPI(): boolean;
|
|
84
|
+
pageProtocol(): string;
|
|
85
|
+
now(): number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const ONLYOFFICE_API_PATH = '/web-apps/apps/api/documents/api.js';
|
|
89
|
+
const DRAWIO_EMBED_QUERY = '/?embed=1&proto=json';
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Default timeout. ⚠ Measured, not guessed: a browser takes ~5 s to give up on
|
|
93
|
+
* a DNS name that does not exist (`http://onlyoffice`), and that case must
|
|
94
|
+
* report `unreachable`, not `timeout`. 10 s leaves room and still bounds a
|
|
95
|
+
* black hole.
|
|
96
|
+
*/
|
|
97
|
+
export const DEFAULT_PROBE_TIMEOUT_MS = 10_000;
|
|
98
|
+
|
|
99
|
+
function trimBase(base: string): string {
|
|
100
|
+
return base.trim().replace(/\/+$/, '');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function domLoadScript(url: string, timeoutMs: number): Promise<'load' | 'error' | 'timeout'> {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
const el = document.createElement('script');
|
|
106
|
+
let done = false;
|
|
107
|
+
const finish = (r: 'load' | 'error' | 'timeout') => {
|
|
108
|
+
if (done) return;
|
|
109
|
+
done = true;
|
|
110
|
+
clearTimeout(tid);
|
|
111
|
+
el.remove();
|
|
112
|
+
resolve(r);
|
|
113
|
+
};
|
|
114
|
+
const tid = setTimeout(() => finish('timeout'), timeoutMs);
|
|
115
|
+
el.async = true;
|
|
116
|
+
el.src = url;
|
|
117
|
+
el.onload = () => finish('load');
|
|
118
|
+
el.onerror = () => finish('error');
|
|
119
|
+
document.head.appendChild(el);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function domLoadFramed(url: string, timeoutMs: number): Promise<'ready' | 'timeout'> {
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
let origin: string | null = null;
|
|
126
|
+
try {
|
|
127
|
+
origin = new URL(url, window.location.href).origin;
|
|
128
|
+
} catch {
|
|
129
|
+
origin = null;
|
|
130
|
+
}
|
|
131
|
+
const frame = document.createElement('iframe');
|
|
132
|
+
frame.setAttribute('aria-hidden', 'true');
|
|
133
|
+
frame.style.cssText = 'position:absolute;left:-9999px;width:1px;height:1px;border:0';
|
|
134
|
+
let done = false;
|
|
135
|
+
const finish = (r: 'ready' | 'timeout') => {
|
|
136
|
+
if (done) return;
|
|
137
|
+
done = true;
|
|
138
|
+
clearTimeout(tid);
|
|
139
|
+
window.removeEventListener('message', onMessage);
|
|
140
|
+
frame.remove();
|
|
141
|
+
resolve(r);
|
|
142
|
+
};
|
|
143
|
+
const onMessage = (e: MessageEvent) => {
|
|
144
|
+
// ⚠ Origin check first: any page may postMessage to us, and a probe that
|
|
145
|
+
// accepts an unfiltered message would report a healthy drawio because
|
|
146
|
+
// something else on the page said so.
|
|
147
|
+
if (origin && e.origin !== origin) return;
|
|
148
|
+
let data: unknown = e.data;
|
|
149
|
+
if (typeof data === 'string') {
|
|
150
|
+
try {
|
|
151
|
+
data = JSON.parse(data);
|
|
152
|
+
} catch {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (data && typeof data === 'object' && (data as { event?: string }).event === 'init') {
|
|
157
|
+
finish('ready');
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
window.addEventListener('message', onMessage);
|
|
161
|
+
const tid = setTimeout(() => finish('timeout'), timeoutMs);
|
|
162
|
+
frame.src = url;
|
|
163
|
+
document.body.appendChild(frame);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function domReachable(url: string): Promise<boolean> {
|
|
168
|
+
try {
|
|
169
|
+
// `no-cors` gives an opaque response we cannot read — which is fine, the
|
|
170
|
+
// only question is whether the request completed at all. It resolves for
|
|
171
|
+
// any HTTP status (including 404) and rejects on a network failure, which
|
|
172
|
+
// is exactly the "reached, wrong thing" vs "could not reach" split.
|
|
173
|
+
await fetch(url, { mode: 'no-cors', cache: 'no-store' });
|
|
174
|
+
return true;
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const domDeps: BrowserProbeDeps = {
|
|
181
|
+
loadScript: domLoadScript,
|
|
182
|
+
loadFramed: domLoadFramed,
|
|
183
|
+
reachable: domReachable,
|
|
184
|
+
hasDocsAPI: () =>
|
|
185
|
+
!!(window as unknown as { DocsAPI?: { DocEditor?: unknown } }).DocsAPI?.DocEditor,
|
|
186
|
+
pageProtocol: () => window.location.protocol,
|
|
187
|
+
now: () => (typeof performance !== 'undefined' ? performance.now() : Date.now()),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/** The URL each service is probed at — the same one its viewer loads. */
|
|
191
|
+
export function browserProbeURL(service: string, base: string): string {
|
|
192
|
+
const b = trimBase(base);
|
|
193
|
+
if (!b) return '';
|
|
194
|
+
if (service === 'onlyoffice') return b + ONLYOFFICE_API_PATH;
|
|
195
|
+
if (service === 'drawio') return b + DRAWIO_EMBED_QUERY;
|
|
196
|
+
return '';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Probe one external service from the browser running this page.
|
|
201
|
+
*
|
|
202
|
+
* Returns `skipped` — never a failure — for a service with no URL and for a
|
|
203
|
+
* service filex does not load in the browser at all (the converter), because
|
|
204
|
+
* reporting "unreachable" for something the browser never fetches would be a
|
|
205
|
+
* new lie in place of the old one.
|
|
206
|
+
*/
|
|
207
|
+
export async function probeExternalFromBrowser(
|
|
208
|
+
service: string,
|
|
209
|
+
base: string | null | undefined,
|
|
210
|
+
opts: { timeoutMs?: number; deps?: Partial<BrowserProbeDeps> } = {},
|
|
211
|
+
): Promise<BrowserProbeResult> {
|
|
212
|
+
const deps: BrowserProbeDeps = { ...domDeps, ...(opts.deps ?? {}) };
|
|
213
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
214
|
+
const url = browserProbeURL(service, base ?? '');
|
|
215
|
+
const mechanism: BrowserProbeResult['mechanism'] =
|
|
216
|
+
service === 'onlyoffice' ? 'script' : service === 'drawio' ? 'iframe' : 'none';
|
|
217
|
+
if (!url) {
|
|
218
|
+
return { service, state: 'skipped', url: '', ms: 0, mechanism };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ⚠ Pre-empt mixed content rather than reporting it as "unreachable". The
|
|
222
|
+
// browser blocks an http:// subresource on an https:// page before a packet
|
|
223
|
+
// leaves, and "unreachable" would send the operator to check firewalls.
|
|
224
|
+
if (deps.pageProtocol() === 'https:' && url.startsWith('http://')) {
|
|
225
|
+
return { service, state: 'blocked-mixed-content', url, ms: 0, mechanism };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const t0 = deps.now();
|
|
229
|
+
const elapsed = () => Math.round(deps.now() - t0);
|
|
230
|
+
|
|
231
|
+
if (mechanism === 'script') {
|
|
232
|
+
const ev = await deps.loadScript(url, timeoutMs);
|
|
233
|
+
if (ev === 'timeout') return { service, state: 'timeout', url, ms: elapsed(), mechanism };
|
|
234
|
+
if (ev === 'load') {
|
|
235
|
+
// Loaded AND it defined the global only a Document Server defines.
|
|
236
|
+
return {
|
|
237
|
+
service,
|
|
238
|
+
state: deps.hasDocsAPI() ? 'ok' : 'wrong-content',
|
|
239
|
+
url,
|
|
240
|
+
ms: elapsed(),
|
|
241
|
+
mechanism,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const reached = await deps.reachable(url);
|
|
245
|
+
return {
|
|
246
|
+
service,
|
|
247
|
+
state: reached ? 'wrong-content' : 'unreachable',
|
|
248
|
+
url,
|
|
249
|
+
ms: elapsed(),
|
|
250
|
+
mechanism,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (mechanism === 'iframe') {
|
|
255
|
+
const ev = await deps.loadFramed(url, timeoutMs);
|
|
256
|
+
if (ev === 'ready') return { service, state: 'ok', url, ms: elapsed(), mechanism };
|
|
257
|
+
// No handshake. An iframe fires `load` for error pages too, so the frame
|
|
258
|
+
// itself cannot tell us why — ask the network instead.
|
|
259
|
+
const reached = await deps.reachable(url);
|
|
260
|
+
return {
|
|
261
|
+
service,
|
|
262
|
+
state: reached ? 'wrong-content' : 'unreachable',
|
|
263
|
+
url,
|
|
264
|
+
ms: elapsed(),
|
|
265
|
+
mechanism,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return { service, state: 'skipped', url: '', ms: 0, mechanism };
|
|
270
|
+
}
|
|
@@ -27,6 +27,7 @@ import type { LocaleCode } from '../types/ExplorerConfig';
|
|
|
27
27
|
import Modal from './Modal.vue';
|
|
28
28
|
import { ensureMonaco, getMonaco, ensureHighlight } from '../composables/useMonacoLoader';
|
|
29
29
|
import { useLocale } from '../composables/useLocale';
|
|
30
|
+
import { browserProbeURL } from '../lib/externalReach';
|
|
30
31
|
|
|
31
32
|
const props = defineProps<{
|
|
32
33
|
open: boolean;
|
|
@@ -895,7 +896,10 @@ function loadOnlyOfficeScript(base: string): Promise<void> {
|
|
|
895
896
|
}
|
|
896
897
|
const script = document.createElement('script');
|
|
897
898
|
script.id = ONLYOFFICE_SCRIPT_ID;
|
|
898
|
-
|
|
899
|
+
// Same URL the admin page's browser probe attempts, from the same
|
|
900
|
+
// helper — a probe that tested a different address than the editor loads
|
|
901
|
+
// would be the old lie wearing a new badge.
|
|
902
|
+
script.src = browserProbeURL('onlyoffice', base);
|
|
899
903
|
script.async = true;
|
|
900
904
|
script.onload = () => resolve();
|
|
901
905
|
script.onerror = () => reject(new Error('OnlyOffice api.js load failed'));
|