diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-08 05:06:06 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-08 05:06:06 +0200 |
| commit | 7deb5e35160c001b29c890cbb1a74e6c65a6e0ba (patch) | |
| tree | f2c22c369d60d7ff4329891b881f31b99d12cd5f /modules/ext/webdav.js | |
| parent | 6a0cb8cc84ad2cf9bf17d008823d07e5958c6e0a (diff) | |
auto: 2026-07-29_20:45 changes
Diffstat (limited to 'modules/ext/webdav.js')
| -rw-r--r-- | modules/ext/webdav.js | 430 |
1 files changed, 424 insertions, 6 deletions
diff --git a/modules/ext/webdav.js b/modules/ext/webdav.js index e989ac9..5cacea0 100644 --- a/modules/ext/webdav.js +++ b/modules/ext/webdav.js @@ -7,7 +7,7 @@ class WebDAVModule { 'webdav-mkdir': { argType: 'dir' }, 'webdav-rename': { argType: 'file' }, 'webdav-move': { argType: 'file' }, 'webdav-backup': { argType: 'file' }, 'webdav-restore': { argType: 'file' }, 'webdav-browser': { argType: 'file' }, - 'webdav-config': { argType: 'file' }, + 'webdav-config': { argType: 'file' }, 'webdav-login': { argType: 'none' }, }; this.terminal = null; this.config = null; // { url, username, password } @@ -71,6 +71,25 @@ class WebDAVModule { } catch (e) { return ''; } } + async _fetchSpaceToken(spaceName) { + // Fetch the CURRENT token for a space via the session-authenticated + // endpoint (self-heals after server-side rotations). Admins may + // fetch any space; regular users only their own. + try { + let sid = ''; + try { sid = localStorage.getItem('webx11_session_id') || ''; } catch (e) {} + const res = await fetch('/api/auth/webdav-token?space=' + encodeURIComponent(spaceName), { + credentials: 'same-origin', + headers: sid ? { 'X-Session-Id': sid } : {} + }); + if (res.ok) { + const d = await res.json(); + return (d.webdav_token || '').trim() || null; + } + } catch (e) { /* offline or not logged in */ } + return null; + } + _promptForToken(spaceName) { const input = window.prompt('Enter your WebDAV token for space "' + spaceName + '":', ''); if (input && input.trim()) return input.trim(); @@ -92,6 +111,7 @@ class WebDAVModule { getHelpInfo() { return { 'webdav-config': 'webdav-config [url user pass] — Configure WebDAV. No args = auto-detect from current page URL', + 'webdav-login': 'webdav-login [username] [password] — Open the login panel (Telegram or password) in THIS browser and configure WebDAV', 'webdav-status': 'Show current WebDAV configuration status', 'webdav-ls': 'List files/directories on the WebDAV remote', 'webdav-push': 'Upload a VFS file to WebDAV', @@ -164,9 +184,117 @@ class WebDAVModule { _getAuthHeader() { if (!this.config || !this.config.password) return {}; const token = btoa(this.config.username + ':' + this.config.password); - return { 'Authorization': 'Basic ' + token }; + // Send BOTH: dav-auth prefers X-WebDAV-Token, so a stale browser-cached + // Basic credential can never outrank the fresh token (2026-10-05). + return { 'Authorization': 'Basic ' + token, 'X-WebDAV-Token': this.config.password }; + } + + async _ensureToken() { + // Guarantee a token BEFORE any request goes out: a credential-less + // write makes dav-auth answer 401 + a Basic challenge, and the browser + // then pops its native username/password dialog mid-upload ("username + // and password query" — the exact complaint fixed 2026-10-05). + if (!this.config || this.config.password) return; + // 1) SecretVault-stored token (lazy decrypt) + if (this._cachedPassUser) { + try { await this._resolvePassword(); } catch (e) { /* vault miss */ } + } + if (this.config.password) return; + // 2) Server-side: current space token via session-authenticated API. + const spaceName = this._resolveSpaceName(); + if (spaceName) { + const fresh = await this._fetchSpaceToken(spaceName); + if (fresh) { + this.config.password = fresh; + await this._saveConfig(); + } + } + if (this.config.password) return; + // Last resort (2026-10-05): a device-code login may have been approved + // in Telegram while this page was frozen (Android suspends timers), so + // the panel's 3s poll never saw it. Pick it up now — the next push + // (or visibilitychange) completes the login by itself. + await this._pollPendingDevice(); + } + + async _pollPendingDevice() { + let pend = null; + try { pend = JSON.parse(localStorage.getItem('webx11_pending_device') || 'null'); } catch (e) { pend = null; } + if (!pend || !pend.code || !pend.poll_key) return false; + if (Date.now() - (pend.started || 0) > ((pend.expires_in || 600) + 10) * 1000) { + try { localStorage.removeItem('webx11_pending_device'); } catch (e0) {} + return false; + } + try { + const pr = await fetch('/api/auth/device-poll', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ code: pend.code, poll_key: pend.poll_key }) + }); + const pd = await pr.json().catch(function () { return {}; }); + if (pd.status === 'approved') { + try { localStorage.removeItem('webx11_pending_device'); } catch (e1) {} + const p = document.getElementById('wdav-login-panel'); + if (p) p.remove(); + if (this.terminal && this.terminal.print) { + this.terminal.print('\uD83D\uDD13 Telegram code approved \u2014 logging in\u2026', 'info'); + } + await this._applySession(pd); + return true; + } + if (pd.status === 'expired' || pd.status === 'invalid') { + try { localStorage.removeItem('webx11_pending_device'); } catch (e2) {} + } + } catch (e3) { /* transient \u2014 keep pending for the next attempt */ } + return false; } + async _autoBootstrap() { + // Zero-command auto-configuration (2026-10-05). Restores the + // "it configured automatically" flow: when no config is saved and + // this browser holds a WebX11 session, fetch the current token + // silently and configure WebDAV for the page space (fallback: the + // logged-in user's own space). Called on module load and again as a + // fallback before a push — after ONE login in a browser, every later + // load auto-configures itself; no webdav-config / webdav-login needed. + try { + if (this.config && this.config.url) return; // already configured + const candidates = []; + if (this._spaceName) candidates.push(this._spaceName); + let su = this._detectSessionUser(); + if (!su) { + // HttpOnly cookie -> JS cannot read it, but the server can. + // Resolve the logged-in user via me-cookie (2026-10-05). + try { + const mr = await fetch('/api/auth/me-cookie', { credentials: 'same-origin' }); + if (mr.ok) { + const md = await mr.json(); + if (md && md.username) { + su = md.username; + try { localStorage.setItem('webx11_username', su); } catch (eS) {} + } + } + } catch (em) { /* not logged in */ } + } + if (su && candidates.indexOf(su) === -1) candidates.push(su); + for (const space of candidates) { + const tok = await this._fetchSpaceToken(space); + if (!tok) continue; // no session in this browser — stay quiet + this.config = { + url: 'https://webx11.duckdns.org/spaces/' + space + '/', + username: space, + password: tok + }; + await this._saveConfig(); + this._autoConfigured = true; + if (this.terminal && this.terminal.print) { + this.terminal.print('\u2705 WebDAV auto-configured for space "' + space + '" (browser session detected)', 'success'); + } + return; + } + } catch (e) { /* never break load */ } + } + _normalizeRemotePath(path) { // v4.6.16: Paths can arrive as VFS-relative ('file.txt'), space-relative // ('/file.txt'), space-prefixed ('/spaces/<user>/file.txt') or even a @@ -224,6 +352,8 @@ class WebDAVModule { if (this._cachedPassUser) { await this._resolvePassword(); } + // Never send a credential-less write — fetch the token first (2026-10-05) + await this._ensureToken(); const baseUrl = this.config.url.replace(/\/+$/, ''); const urlPath = this._normalizeRemotePath(path); const url = baseUrl + urlPath; @@ -239,7 +369,23 @@ class WebDAVModule { body: options.body || undefined }; - const response = await fetch(url, fetchOpts); + let response = await fetch(url, fetchOpts); + // Self-heal on 401: fetch the space's current token once and retry. + if (response.status === 401) { + const spaceName = this._resolveSpaceName(); + if (spaceName) { + const fresh = await this._fetchSpaceToken(spaceName); + if (fresh && fresh !== this.config.password) { + this.config.password = fresh; + await this._saveConfig(); + response = await fetch(url, { + method: method, + headers: { ...this._getAuthHeader(), ...(options.headers || {}) }, + body: options.body || undefined + }); + } + } + } return response; } @@ -513,12 +659,14 @@ class WebDAVModule { const sessionToken = self._detectSessionToken(); if (sessionUser && sessionToken) { - // Auto-mount the logged-in user's own space + // Auto-mount the logged-in user's own space. + // Prefer a freshly fetched token (self-heals after rotations). + const freshTok = await self._fetchSpaceToken(sessionUser); const autoUrl = autoUrlFor(sessionUser); self.config = { url: autoUrl, username: sessionUser, - password: sessionToken + password: freshTok || sessionToken }; await self._saveConfig(); self._autoConfigured = true; @@ -560,6 +708,11 @@ class WebDAVModule { token = localStorage.getItem('webx11_spaces_' + spaceName); } + // Logged-in session: fetch this space's current token server-side + if (!token && sessionUserOther && spaceName) { + token = await self._fetchSpaceToken(spaceName); + } + if (token) { self.config = { url: autoUrl, @@ -615,6 +768,228 @@ class WebDAVModule { } }; + // --- webdav-login: authenticate in THIS browser + configure WebDAV --- + // Sessions do NOT cross browser contexts: being logged in in Chrome or + // the dashboard does not log in the browser that runs this terminal, + // so the session-authenticated token fetch returns "No session found" + // and pushes fail with a bare 401 (2026-10-05). + // Two ways in: Telegram widget (primary) or username+password. + this._applySession = async function (data) { + const uname = ((data && data.username) || '').toLowerCase(); + if (!uname) return false; + try { if (data.session_id) localStorage.setItem('webx11_session_id', data.session_id); } catch (e) {} + try { localStorage.setItem('webx11_username', uname); } catch (e) {} + try { if (data.webdav_token) localStorage.setItem('webx11_webdav_token', data.webdav_token); } catch (e) {} + const pageSpace = self._spaceName || ''; + let token = data.webdav_token || ''; + let targetSpace = uname; + // Admin on a space page (e.g. /spaces/newos4/main.html): prefer the + // PAGE space token, so pushes land inside that space (their docs + // subfolder keeps working). Non-admins fall back to their own. + if (pageSpace && pageSpace !== uname) { + const pt = await self._fetchSpaceToken(pageSpace); + if (pt) { token = pt; targetSpace = pageSpace; } + } else if (pageSpace === uname && !token) { + token = (await self._fetchSpaceToken(pageSpace)) || ''; + } + let url = 'https://webx11.duckdns.org/spaces/' + targetSpace + '/'; + if (self.config && self.config.url && self.config.url.indexOf('/spaces/' + targetSpace + '/') !== -1) { + url = self.config.url; // preserve an existing sub-folder target (e.g. /spaces/newos4/agando/) + } + self.config = { url: url, username: targetSpace, password: token }; + await self._saveConfig(); + self._autoConfigured = true; + self.terminal.print('✅ Logged in as ' + uname + ' — WebDAV configured for space "' + targetSpace + '"', 'success'); + if (!token) { + self.terminal.print('No WebDAV token returned for this space — try: webdav-config', 'warning'); + return true; + } + try { + const r = await self._webdavRequest('OPTIONS', '/'); + self.terminal.print('Connection check: HTTP ' + r.status, r.status < 400 ? 'success' : 'warning'); + } catch (e) { + self.terminal.print('Connection check failed: ' + e.message, 'warning'); + } + return true; + }; + + this.commands['webdav-login'] = async (args) => { + // 1) Direct form: webdav-login <username> <password> + if (args && args[0] && args[1]) { + self.terminal.print('Logging in as "' + args[0] + '"...', 'info'); + try { + const res = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ username: String(args[0]).trim().toLowerCase(), password: args[1] }) + }); + const data = await res.json().catch(function () { return {}; }); + if (!res.ok) { + self.terminal.print('❌ Login failed: ' + (data.error || ('HTTP ' + res.status)), 'error'); + return; + } + await self._applySession(data); + } catch (e) { + self.terminal.print('Login error: ' + e.message, 'error'); + } + return; + } + // 2) Login panel: Telegram button (preferred) + password form + const oldPanel = document.getElementById('wdav-login-panel'); + if (oldPanel) oldPanel.remove(); + const panel = document.createElement('div'); + panel.id = 'wdav-login-panel'; + panel.style.cssText = 'position:fixed; left:50%; top:50%; transform:translate(-50%,-50%); z-index:10010; width:340px; max-width:92vw; background:var(--bg-tertiary,#141414); border:1px solid var(--border,#333); border-radius:10px; padding:16px; box-shadow:0 8px 32px rgba(0,0,0,.6); font-size:0.9em;'; + panel.innerHTML = '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;"><b>Log in to WebX11</b><span id="wdav-login-close" style="cursor:pointer;opacity:.7;padding:2px 6px;">✕</span></div>' + + '<div style="text-align:center;margin-bottom:10px;"><button id="wdav-code-btn" style="width:100%;padding:10px;background:var(--accent,#4a9eff);border:none;border-radius:6px;color:#fff;cursor:pointer;font-weight:600;">📲 Login with a code — no popup</button></div>' + + '<div id="wdav-code-area" style="display:none;margin-bottom:10px;">' + + '<div id="wdav-code-value" style="font-size:1.35em;font-family:monospace;letter-spacing:2px;text-align:center;padding:8px;background:var(--bg-secondary,#222);border-radius:6px;">WX11-XXXXXX</div>' + + '<div style="font-size:0.82em;color:var(--text-muted,#888);margin-top:6px;text-align:center;">Open Telegram → <b>@roducbot</b> → send this code.<br>This terminal logs in automatically (waits up to 10 min).</div>' + + '<div id="wdav-code-status" style="margin-top:6px;font-size:0.85em;text-align:center;color:var(--warning,#ffaa00);">Requesting code…</div>' + + '</div>' + + '<div style="text-align:center;color:var(--text-muted,#888);margin:6px 0;">— or —</div>' + + '<div id="wdav-tg-mount" style="display:flex;justify-content:center;margin-bottom:6px;min-height:40px;"></div>' + + '<div id="wdav-tg-status" style="text-align:center;color:var(--text-muted,#888);margin-bottom:10px;font-size:0.85em;">Telegram widget (in some apps this opens another browser)</div>' + + '<div style="text-align:center;color:var(--text-muted,#888);margin:6px 0;">— or —</div>' + + '<input id="wdav-user" placeholder="username" autocomplete="username" style="width:100%;margin-bottom:6px;padding:8px;background:var(--bg-secondary,#222);border:1px solid var(--border,#333);border-radius:6px;color:inherit;box-sizing:border-box;">' + + '<input id="wdav-pass" type="password" placeholder="password" autocomplete="current-password" style="width:100%;margin-bottom:8px;padding:8px;background:var(--bg-secondary,#222);border:1px solid var(--border,#333);border-radius:6px;color:inherit;box-sizing:border-box;">' + + '<button id="wdav-login-btn" style="width:100%;padding:9px;background:var(--accent,#4a9eff);border:none;border-radius:6px;color:#fff;cursor:pointer;">Log in</button>' + + '<div id="wdav-login-msg" style="margin-top:8px;color:var(--warning,#ffaa00);min-height:14px;font-size:0.85em;"></div>'; + document.body.appendChild(panel); + let _wdavIv = null; + document.getElementById('wdav-login-close').onclick = function () { if (_wdavIv) clearInterval(_wdavIv); panel.remove(); }; + document.getElementById('wdav-code-btn').onclick = async function () { + const area = document.getElementById('wdav-code-area'); + const val = document.getElementById('wdav-code-value'); + const stt = document.getElementById('wdav-code-status'); + area.style.display = 'block'; + stt.textContent = 'Requesting code…'; + try { + const r = await fetch('/api/auth/device-start', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', body: '{}' + }); + const dd = await r.json().catch(function () { return {}; }); + if (!r.ok) { stt.textContent = '❌ ' + (dd.error || ('HTTP ' + r.status)); return; } + val.textContent = dd.code; + try { localStorage.setItem('webx11_pending_device', JSON.stringify({ code: dd.code, poll_key: dd.poll_key, started: Date.now(), expires_in: dd.expires_in || 600 })); } catch (eS) {} + try { + var dl = document.getElementById('wdav-code-link'); + if (!dl) { + dl = document.createElement('a'); + dl.id = 'wdav-code-link'; + dl.target = '_blank'; + dl.rel = 'noopener'; + dl.style.cssText = 'display:block;margin-top:8px;text-align:center;font-size:0.9em;color:var(--accent,#4a9eff);'; + dl.textContent = '\uD83D\uDC49 Open @roducbot in Telegram (one tap)'; + var areaEl = document.getElementById('wdav-code-area'); + if (areaEl) areaEl.appendChild(dl); + } + dl.href = 'https://t.me/roducbot?start=' + encodeURIComponent(dd.code); + } catch (eL) {} + try { await navigator.clipboard.writeText(dd.code); stt.textContent = 'Code copied — send it to @roducbot. Waiting…'; } + catch (e2) { stt.textContent = 'Waiting for approval…'; } + if (_wdavIv) clearInterval(_wdavIv); + const started = Date.now(); + _wdavIv = setInterval(async function () { + if (Date.now() - started > ((dd.expires_in || 600) + 5) * 1000) { + clearInterval(_wdavIv); + try { localStorage.removeItem('webx11_pending_device'); } catch (eT) {} + stt.textContent = '❌ Code expired. Press the button to get a new one.'; + return; + } + try { + const pr = await fetch('/api/auth/device-poll', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ code: dd.code, poll_key: dd.poll_key }) + }); + const pd = await pr.json().catch(function () { return {}; }); + if (pd.status === 'approved') { + clearInterval(_wdavIv); + try { localStorage.removeItem('webx11_pending_device'); } catch (e0) {} + panel.remove(); + self.terminal.print('🔓 Telegram code approved — logging in…', 'info'); + await self._applySession(pd); + } else if (pd.status === 'expired') { + clearInterval(_wdavIv); + try { localStorage.removeItem('webx11_pending_device'); } catch (eX) {} + stt.textContent = '❌ Code expired. Press the button to get a new one.'; + } + } catch (e3) { /* transient — keep polling */ } + }, 3000); + } catch (e) { stt.textContent = 'Error: ' + e.message; } + }; + window.webx11TgLogin = async function (user) { + const st = document.getElementById('wdav-tg-status'); + if (st) st.textContent = 'Verifying Telegram login...'; + try { + const res = await fetch('/api/auth/telegram-login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify(user) + }); + const data = await res.json().catch(function () { return {}; }); + if (!res.ok) { + if (st) st.textContent = '❌ Telegram login failed: ' + (data.error || res.status); + return; + } + const p = document.getElementById('wdav-login-panel'); + if (p) p.remove(); + await self._applySession(data); + } catch (e) { + if (st) st.textContent = 'Error: ' + e.message; + } + }; + const mount = document.getElementById('wdav-tg-mount'); + const tw = document.createElement('script'); + tw.src = 'https://telegram.org/js/telegram-widget.js?22'; + tw.setAttribute('data-telegram-login', 'roducbot'); + tw.setAttribute('data-size', 'large'); + tw.setAttribute('data-onauth', 'webx11TgLogin(user)'); + tw.setAttribute('data-request-access', 'write'); + tw.async = true; + mount.appendChild(tw); + document.getElementById('wdav-login-btn').onclick = async function () { + const u = (document.getElementById('wdav-user').value || '').trim().toLowerCase(); + const pw = document.getElementById('wdav-pass').value || ''; + const msg = document.getElementById('wdav-login-msg'); + if (!u || !pw) { msg.textContent = 'Enter username and password.'; return; } + msg.textContent = 'Logging in...'; + try { + const res = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ username: u, password: pw }) + }); + const data = await res.json().catch(function () { return {}; }); + if (!res.ok) { msg.textContent = '❌ ' + (data.error || ('HTTP ' + res.status)); return; } + panel.remove(); + await self._applySession(data); + } catch (e) { + msg.textContent = 'Error: ' + e.message; + } + }; + self.terminal.print('🔐 Login panel opened — use the Telegram button, or the username/password form.', 'info'); + }; + + // Resume-pending-login hooks (2026-10-05): Android freezes timers while + // the user is in Telegram approving the code; on return the 3s interval + // may never fire again. Poll immediately when visible + once on load, so + // a reload after approval still completes the login. + try { + document.addEventListener('visibilitychange', function () { + if (document.visibilityState === 'visible') { self._pollPendingDevice(); } + }); + } catch (eV) {} + try { self._pollPendingDevice(); } catch (eI) {} + // Zero-command auto-config: configure WebDAV silently whenever + // this browser has a WebX11 session (2026-10-05) + try { self._autoBootstrap(); } catch (eB) {} + // --- webdav-status: show current WebDAV config --- this.commands['webdav-status'] = async () => { if (!self.config || !self.config.url) { @@ -668,6 +1043,8 @@ class WebDAVModule { self.terminal.print('File not found in VFS: ' + vfsPath, 'error'); return; } + // No saved config? Try session auto-config first (2026-10-05) + if (!self.config || !self.config.url) { await self._autoBootstrap(); } try { // Ensure remote dir exists const parts = remotePath.replace(/\/$/, '').split('/').filter(Boolean); @@ -683,9 +1060,20 @@ class WebDAVModule { self.terminal.print('Pushed: ' + vfsPath + ' → ' + remotePath, 'success'); } else { self.terminal.print('Push failed: HTTP ' + resp.status, 'error'); + if (resp.status === 401) { + if (!self.config.password) { + self.terminal.print('Hint: this browser has no WebX11 session (sessions do not cross browsers).', 'info'); + } else { + self.terminal.print('Hint: token rejected — the browser session may be stale.', 'info'); + } + self.terminal.print(' Run: webdav-login (opens the login panel)', 'info'); + } } } catch (e) { self.terminal.print('Push error: ' + e.message, 'error'); + if (/not configured/i.test(e.message || '')) { + self.terminal.print('Hint: no saved WebDAV config in this browser. Log in once with: webdav-login (Telegram code \u2014 no popup).', 'info'); + } } }; @@ -1802,6 +2190,9 @@ class WebDAVModule { }); // XHR PUT so we get upload progress events (fetch has none) + // Ensure a token BEFORE connecting: a credential-less write + // triggers the browser's native Basic-auth dialog (2026-10-05). + await self._ensureToken(); await new Promise(function(resolve, reject) { const baseUrl = self.config.url.replace(/\/+$/, ''); const urlPath = self._normalizeRemotePath(remotePath); @@ -1809,6 +2200,7 @@ class WebDAVModule { xhr.open('PUT', baseUrl + urlPath, true); const auth = self._getAuthHeader(); if (auth.Authorization) xhr.setRequestHeader('Authorization', auth.Authorization); + if (auth['X-WebDAV-Token']) xhr.setRequestHeader('X-WebDAV-Token', auth['X-WebDAV-Token']); xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream'); xhr.upload.onprogress = function(ev) { if (!ev.lengthComputable) return; @@ -1823,7 +2215,33 @@ class WebDAVModule { lastTime = now; } }; - xhr.onload = function() { + xhr.onload = async function() { + // 401 self-heal: refresh the space token once, then retry. + // The XHR path previously had NO retry — a stale token + // failed the upload permanently (fixed 2026-10-05). + if (xhr.status === 401 && !xhr._wdavRetried) { + xhr._wdavRetried = true; + const sp = self._resolveSpaceName(); + const fresh = sp ? await self._fetchSpaceToken(sp) : null; + if (fresh && fresh !== (self.config && self.config.password)) { + self.config.password = fresh; + await self._saveConfig(); + const retry = new XMLHttpRequest(); + retry.open('PUT', baseUrl + urlPath, true); + const a2 = self._getAuthHeader(); + if (a2.Authorization) retry.setRequestHeader('Authorization', a2.Authorization); + if (a2['X-WebDAV-Token']) retry.setRequestHeader('X-WebDAV-Token', a2['X-WebDAV-Token']); + retry.setRequestHeader('Content-Type', file.type || 'application/octet-stream'); + retry.upload.onprogress = xhr.upload.onprogress; + retry.onload = function() { + if (retry.status >= 200 && retry.status < 300) resolve(); + else reject(new Error('HTTP ' + retry.status)); + }; + retry.onerror = function() { reject(new Error('Network error')); }; + retry.send(buf); + return; + } + } if (xhr.status >= 200 && xhr.status < 300) resolve(); else reject(new Error('HTTP ' + xhr.status)); }; |