aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/core/module-fs.js88
-rw-r--r--modules/ext/webdav.js149
2 files changed, 157 insertions, 80 deletions
diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js
index b119343..160dccc 100644
--- a/modules/core/module-fs.js
+++ b/modules/core/module-fs.js
@@ -277,46 +277,60 @@ class FileSystemModule {
}
};
this.commands['upload'] = () => { document.getElementById('file-upload-input').click(); };
- this.commands['download'] = (args) => {
+ this.commands['download'] = async (args) => {
if(!args[0]) return this.terminal.print('Usage: download <filename>', 'warning');
const fname = args[0];
const content = this.terminal.fs.readFile(fname);
- if(content !== null && content !== undefined) {
- const size = content.length;
- const sizeStr = size > 1024 ? (size / 1024).toFixed(1) + ' KB' : size + ' B';
- // Desktop: programmatic click (works on desktop Chrome/Firefox)
- const blob = new Blob([content], {type: 'application/octet-stream'});
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url; a.download = fname;
- a.style.display = 'none';
- document.body.appendChild(a);
- a.click();
- setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 2000);
- // Mobile fallback: build a DOM button with real user gesture
- const line = document.createElement('div');
- line.className = 'output-line';
- const msg = document.createElement('span');
- msg.textContent = '⬇ ' + fname + ' (' + sizeStr + ') ';
- const btn = document.createElement('button');
- btn.textContent = '⬇ Download';
- btn.className = 'btn';
- btn.style.cssText = 'background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--success-color);padding:4px 12px;border-radius:4px;cursor:pointer;font-size:0.85em;min-height:36px;';
- btn.onclick = () => {
- const b = new Blob([this.terminal.fs.readFile(fname)], {type: 'application/octet-stream'});
- const u = URL.createObjectURL(b);
- const el = document.createElement('a');
- el.href = u; el.download = fname;
- el.style.display = 'none';
- document.body.appendChild(el);
- el.click();
- setTimeout(() => { document.body.removeChild(el); URL.revokeObjectURL(u); }, 2000);
- };
- line.appendChild(msg);
- line.appendChild(btn);
- this.terminal.outputContainer.appendChild(line);
- this.terminal.scrollToBottom();
- } else { this.terminal.print('download: ' + fname + ': No such file', 'error'); }
+ if(content === null || content === undefined) {
+ return this.terminal.print('download: ' + fname + ': No such file', 'error');
+ }
+ // Push to WebDAV first — this is the only path to a real file download on Telegram WebView
+ // because browser-based file downloads are blocked in WebViews
+ let downloadUrl = null;
+ if (this.terminal.moduleManager && this.terminal.moduleManager.modules.WebDAVModule) {
+ const wdav = this.terminal.moduleManager.modules.WebDAVModule;
+ if (wdav.config && wdav.config.url) {
+ try {
+ // Make sure /downloads/ dir exists
+ try { await wdav._ensureDir('/downloads'); } catch(e) {}
+ const resp = await wdav._webdavRequest('PUT', '/downloads/' + fname, {
+ body: content,
+ headers: { 'Content-Type': 'application/octet-stream' }
+ });
+ if (resp.ok || resp.status === 201 || resp.status === 204) {
+ // Build the public URL the user can tap
+ const baseUrl = wdav.config.url.replace(/\/+$/, '');
+ const hostMatch = baseUrl.match(/^(https?:\/\/[^\/]+)/);
+ const host = hostMatch ? hostMatch[1] : 'https://webx11.duckdns.org';
+ downloadUrl = host + '/spaces/' + wdav._spaceName + '/downloads/' + encodeURIComponent(fname);
+ this.terminal.print('Uploaded to: ' + downloadUrl, 'success');
+ } else {
+ this.terminal.print('WebDAV push failed: HTTP ' + resp.status, 'warning');
+ }
+ } catch (e) {
+ this.terminal.print('WebDAV push error: ' + e.message, 'warning');
+ }
+ } else {
+ this.terminal.print('WebDAV not configured. Run: webdav-config', 'warning');
+ }
+ }
+ // Build a visible anchor link — tap = real user gesture = native browser download bar
+ const size = content.length;
+ const sizeStr = size > 1024 ? (size / 1024).toFixed(1) + ' KB' : size + ' B';
+ const line = document.createElement('div');
+ line.className = 'output-line';
+ line.style.cssText = 'padding:8px 0;display:flex;align-items:center;gap:10px;flex-wrap:wrap;';
+ if (downloadUrl) {
+ line.innerHTML = '<span>⬇ ' + fname + ' (' + sizeStr + ') </span>' +
+ '<a href="' + downloadUrl + '" target="_blank" rel="noopener" ' +
+ 'style="background:var(--success-color);color:#000;border:none;padding:8px 16px;' +
+ 'border-radius:6px;cursor:pointer;font-size:0.9em;min-height:44px;' +
+ 'display:inline-flex;align-items:center;text-decoration:none;font-weight:bold;">⬇ Tap to download</a>';
+ } else {
+ line.innerHTML = '<span>⬇ ' + fname + ' (' + sizeStr + ') — run <code>webdav-config</code> first to enable downloads</span>';
+ }
+ this.terminal.outputContainer.appendChild(line);
+ this.terminal.scrollToBottom();
};
this.commands['vfs-export'] = () => {
const data = this.terminal.fs.root;
diff --git a/modules/ext/webdav.js b/modules/ext/webdav.js
index eea513b..01856ce 100644
--- a/modules/ext/webdav.js
+++ b/modules/ext/webdav.js
@@ -24,6 +24,27 @@ class WebDAVModule {
return '';
}
+ _detectSessionUser() {
+ try {
+ const cookies = document.cookie.split(';').map(c => c.trim());
+ for (const c of cookies) {
+ if (c.startsWith('webx11_user=')) {
+ return decodeURIComponent(c.substring('webx11_user='.length));
+ }
+ }
+ } catch (e) {}
+ return null;
+ }
+
+ _promptForToken(spaceName) {
+ // Use textarea+execCommand for mobile WebView compatibility
+ const input = window.prompt('Enter your WebDAV token for space "' + spaceName + '":', '');
+ if (input && input.trim()) {
+ return input.trim();
+ }
+ return null;
+ }
+
requirements() {
const cfg = localStorage.getItem('webx11_webdav_config');
if (!cfg) return { met: false, missing: ['WebDAV endpoint not configured. Use webdav-config to set it up.'] };
@@ -59,24 +80,33 @@ class WebDAVModule {
const saved = localStorage.getItem(this._STORAGE_KEY);
if (saved) {
this.config = JSON.parse(saved);
- // Try to get password from SecretVault
+ // Try to get password from SecretVault (async — will be resolved on first use)
if (window.webx11_secretVault && this.config && this.config.username) {
- let encryptedPass = window.webx11_secretVault.decrypt('webdav_pass_' + this.config.username);
- if (encryptedPass !== null && encryptedPass !== undefined) {
- this.config.password = encryptedPass;
- } else {
- // Fallback to old token key
- encryptedPass = window.webx11_secretVault.decrypt('webdav_token_' + this.config.username);
- if (encryptedPass !== null && encryptedPass !== undefined) {
- this.config.password = encryptedPass;
- }
- }
+ // Store a placeholder; actual decrypt happens on first auth call
+ this._cachedPassUser = this.config.username;
+ }
+ }
+ } catch (e) { /* ignore */ }
+ }
+
+ async _resolvePassword() {
+ if (!window.webx11_secretVault || !this._cachedPassUser) return;
+ try {
+ const pass = await window.webx11_secretVault.decrypt('webdav_pass_' + this._cachedPassUser);
+ if (pass !== null && typeof pass === 'string') {
+ this.config.password = pass;
+ } else {
+ // Fallback to old token key
+ const tokenPass = await window.webx11_secretVault.decrypt('webdav_token_' + this._cachedPassUser);
+ if (tokenPass !== null && typeof tokenPass === 'string') {
+ this.config.password = tokenPass;
}
}
} catch (e) { /* ignore */ }
+ this._cachedPassUser = null;
}
- _saveConfig() {
+ async _saveConfig() {
try {
if (this.config) {
const toStore = {
@@ -85,14 +115,13 @@ class WebDAVModule {
};
// Try SecretVault for encrypted password storage
if (window.webx11_secretVault && this.config.password) {
- window.webx11_secretVault.encrypt('webdav_pass_' + this.config.username, this.config.password);
+ await window.webx11_secretVault.encrypt('webdav_pass_' + this.config.username, this.config.password);
// Also store space token
if (this._spaceName) {
- window.webx11_secretVault.encrypt('webdav_token_' + this._spaceName, this.config.password);
+ await window.webx11_secretVault.encrypt('webdav_token_' + this._spaceName, this.config.password);
}
}
- // Also store in localStorage for backward compat (SecretVault decrypts later)
- // Store only url and username hint in plaintext lS, encrypted password handled above
+ // Also store in localStorage for backward compat (url and username only)
localStorage.setItem(this._STORAGE_KEY, JSON.stringify(toStore));
} else {
localStorage.removeItem(this._STORAGE_KEY);
@@ -110,6 +139,10 @@ class WebDAVModule {
if (!this.config || !this.config.url) {
throw new Error('WebDAV not configured. Use webdav-config first.');
}
+ // Resolve password from SecretVault if needed (lazy load)
+ if (this._cachedPassUser) {
+ await this._resolvePassword();
+ }
const baseUrl = this.config.url.replace(/\/+$/, '');
const urlPath = path ? (path.startsWith('/') ? path : '/' + path) : '/';
const url = baseUrl + urlPath;
@@ -372,7 +405,12 @@ class WebDAVModule {
// Check SecretVault first
if (window.webx11_secretVault) {
- token = window.webx11_secretVault.decrypt('webdav_token_' + spaceName);
+ const vaultToken = window.webx11_secretVault.decrypt('webdav_token_' + spaceName);
+ if (vaultToken && typeof vaultToken.then === 'function') {
+ token = await vaultToken;
+ } else if (vaultToken !== null && vaultToken !== undefined) {
+ token = vaultToken;
+ }
}
// Fallback to localStorage
@@ -380,18 +418,30 @@ class WebDAVModule {
token = localStorage.getItem('webx11_spaces_' + spaceName);
}
- // Fallback: fetch .token directly from server
+ // If still no token, use session-aware prompting
if (!token) {
- try {
- const tokenResp = await fetch('/spaces/' + spaceName + '/.token');
- if (tokenResp.ok) {
- token = (await tokenResp.text()).trim();
- // Cache in SecretVault for future use
- if (window.webx11_secretVault) {
- window.webx11_secretVault.encrypt('webdav_token_' + spaceName, token);
- }
- }
- } catch (e) { /* .token not accessible */ }
+ const sessionUser = self._detectSessionUser();
+ if (sessionUser && sessionUser === spaceName) {
+ self.terminal.print('👋 Welcome back, ' + sessionUser + '!', 'success');
+ self.terminal.print('No saved WebDAV config found. Enter your token:', 'info');
+ } else if (sessionUser) {
+ self.terminal.print('👋 You are logged in as "' + sessionUser + '" — this space is "' + spaceName + '".', 'info');
+ self.terminal.print('Enter the WebDAV token for space "' + spaceName + '":', 'info');
+ } else {
+ self.terminal.print('No saved WebDAV config found for space "' + spaceName + '".', 'info');
+ self.terminal.print('Enter the WebDAV token (get it from the WebX11 dashboard):', 'info');
+ }
+ self.terminal.print('', 'text-muted');
+ self.terminal.print(' webdav-config ' + autoUrl + ' ' + spaceName + ' <your-token>', 'success');
+ self.terminal.print('', 'text-muted');
+ // Pre-configure URL + username so user only needs token
+ self.config = {
+ url: autoUrl,
+ username: spaceName,
+ password: ''
+ };
+ await self._saveConfig();
+ return;
}
if (token) {
@@ -400,7 +450,7 @@ class WebDAVModule {
username: spaceName,
password: token
};
- self._saveConfig();
+ await self._saveConfig();
self._autoConfigured = true;
self.terminal.print('Auto-configured WebDAV for space: ' + spaceName, 'success');
try {
@@ -413,7 +463,7 @@ class WebDAVModule {
}
} else {
self.terminal.print('No WebDAV token found for space: ' + spaceName, 'warning');
- self.terminal.print('Get your token from /spaces/' + spaceName + '/.token or the dashboard.', 'info');
+ self.terminal.print('Get your token from the WebX11 dashboard.', 'info');
self.terminal.print('Auto-setting URL: ' + autoUrl, 'info');
self.terminal.print('Then configure with: webdav-config ' + autoUrl + ' ' + spaceName + ' <your-token>', 'info');
// At minimum set the URL
@@ -422,7 +472,7 @@ class WebDAVModule {
username: spaceName,
password: ''
};
- self._saveConfig();
+ await self._saveConfig();
self._autoConfigured = true;
}
return;
@@ -434,7 +484,7 @@ class WebDAVModule {
username: args[1] || (self.config ? self.config.username : ''),
password: args[2] || (self.config ? self.config.password : '')
};
- self._saveConfig();
+ await self._saveConfig();
self.terminal.print('Testing WebDAV connection...', 'info');
try {
const resp = await self._webdavRequest('OPTIONS', '/');
@@ -447,11 +497,15 @@ class WebDAVModule {
};
// --- webdav-status: show current WebDAV config ---
- this.commands['webdav-status'] = () => {
+ this.commands['webdav-status'] = async () => {
if (!self.config || !self.config.url) {
self.terminal.print('WebDAV not configured. Use webdav-config <url> [username] [password]', 'warning');
return;
}
+ // Resolve password from SecretVault if needed
+ if (self._cachedPassUser) {
+ await self._resolvePassword();
+ }
self.terminal.print('WebDAV Status:', 'text-muted');
self.terminal.print(' URL: ' + self.config.url);
self.terminal.print(' User: ' + (self.config.username || '(none)'));
@@ -1197,24 +1251,31 @@ class WebDAVModule {
async previewWebdavFile(entry) {
try {
- var resp = await this._webdavRequest('GET', entry.href);
+ var basePath = (this._browsingPath || '/').replace(/\/+$/, '');
+ var fullPath = basePath + '/' + entry.name;
+ var resp = await this._webdavRequest('GET', fullPath);
if (!resp.ok) { this.terminal.print('Failed to fetch: HTTP ' + resp.status, 'error'); return; }
- var content = await resp.text();
var isHtml = entry.name.match(/\.html?$/i);
var isImage = entry.name.match(/\.(png|jpg|jpeg|gif|svg|webp)$/i);
- var size = content.length;
+ var size = parseInt(resp.headers.get('content-length') || '0');
if (size > 512000 && !isHtml && !isImage) {
// Large non-html file — offer download instead
this.terminal.print(entry.name + ' is ' + this._formatSize(size) + ' — double-click to download instead of preview.', 'warning');
+ var textPreview = await resp.text();
this.terminal.print('--- ' + entry.name + ' (first 2000 chars) ---');
- this.terminal.print(content.substring(0, 2000));
- if (content.length > 2000) this.terminal.print('...(truncated ' + this._formatSize(size) + ' total)');
+ this.terminal.print(textPreview.substring(0, 2000));
+ if (textPreview.length > 2000) this.terminal.print('...(truncated ' + this._formatSize(size) + ' total)');
return;
}
if (isImage) {
- var dataUrl = 'data:image/' + (entry.name.split('.').pop()) + ';base64,' + btoa(unescape(encodeURIComponent(content)));
+ var blob = await resp.blob();
+ var dataUrl = await new Promise(function(resolve) {
+ var reader = new FileReader();
+ reader.onloadend = function() { resolve(reader.result); };
+ reader.readAsDataURL(blob);
+ });
// Use preview modal
var previewTitle = document.getElementById('preview-title');
var previewIframe = document.getElementById('preview-iframe');
@@ -1227,6 +1288,7 @@ class WebDAVModule {
}
if (isHtml) {
+ var content = await resp.text();
var previewTitle = document.getElementById('preview-title');
var previewIframe = document.getElementById('preview-iframe');
if (previewTitle && previewIframe) {
@@ -1238,7 +1300,8 @@ class WebDAVModule {
}
// Text file — print to terminal
- this.terminal.print('--- ' + entry.name + ' (' + this._formatSize(size) + ') ---', 'info');
+ var content = await resp.text();
+ this.terminal.print('--- ' + entry.name + ' (' + this._formatSize(content.length) + ') ---', 'info');
this.terminal.print(content);
this.terminal.print('--- End: ' + entry.name + ' ---', 'text-muted');
} catch (e) {
@@ -1286,8 +1349,8 @@ class WebDAVModule {
var fullPath = basePath + '/' + entry.name;
var resp = await this._webdavRequest('GET', fullPath);
if (!resp.ok) { this.terminal.print('Download failed: HTTP ' + resp.status, 'error'); return; }
- var content = await resp.text();
- var blob = new Blob([content], { type: 'application/octet-stream' });
+ // Use blob() instead of text() to preserve binary data
+ var blob = await resp.blob();
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
@@ -1299,7 +1362,7 @@ class WebDAVModule {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 200);
- this.terminal.print('⬇ Downloaded: ' + entry.name + ' (' + this._formatSize(content.length) + ')', 'success');
+ this.terminal.print('⬇ Downloaded: ' + entry.name + ' (' + this._formatSize(blob.size) + ')', 'success');
} catch (e) {
this.terminal.print('Download error: ' + e.message, 'error');
}