// v4.6.16 regression test: WebDAV remote-path normalization. // Reproduces the mindtech incident: AI passed full URLs / absolute // '/spaces//' paths which got blindly joined onto the config URL, // creating literal 'https:/' and nested 'spaces//' junk dirs. // Run: node scripts/test-webdav-paths.mjs (repo root = spaces/newos4) // Run: node scripts/test-webdav-paths.mjs (repo root = spaces/newos4) import { readFileSync } from 'fs'; const src = readFileSync(new URL('../modules/ext/webdav.js', import.meta.url), 'utf8'); class TerminalStub { print() {} } // Mirrors ModuleManager.createModuleClass(): extract the first top-level class. const cleanCode = src.replace(/^\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*\s*/, ''); const classMatch = cleanCode.match(/class\s+(\w+)\s+(extends\s+\S+)?\s*\{/); if (!classMatch) throw new Error('no module class found in webdav.js'); const WebDAVModule = new Function('window', 'localStorage', 'fetch', 'XMLHttpRequest', 'URL', 'crypto', 'return ' + cleanCode + '\nreturn ' + classMatch[1])( { location: { pathname: '/spaces/mindtech/newos4/main.html' } }, { getItem: () => null, setItem: () => {} }, async () => { throw new Error('network disabled in test'); }, function () { this.open = () => {}; this.setRequestHeader = () => {}; this.send = () => {}; }, URL, undefined); function makeMod() { const m = new WebDAVModule(new TerminalStub()); m.config = { url: 'https://webx11.duckdns.org/spaces/mindtech/', username: 'mindtech', password: 'tok' }; return m; } let pass = 0, fail = 0; function check(name, cond, got) { if (cond) { pass++; console.log(' ok - ' + name); } else { fail++; console.log(' FAIL - ' + name + (got !== undefined ? ' got: ' + JSON.stringify(got) : '')); } } // --- _normalizeRemotePath --- { const m = makeMod(); const cases = [ // [input, expected] [null, '/'], ['', '/'], ['file.txt', '/file.txt'], ['/file.txt', '/file.txt'], // the mindtech incident shapes: ['https://webx11.duckdns.org/spaces/mindtech/thaimassage-kiefersfelden/index-traditional.html', '/thaimassage-kiefersfelden/index-traditional.html'], ['/spaces/mindtech/thaimassage-kiefersfelden/index-professional.html', '/thaimassage-kiefersfelden/index-professional.html'], ['https://webx11.duckdns.org/spaces/mindtech', '/'], ['spaces/mindtech/skills/navigating-the-terminal.json', '/skills/navigating-the-terminal.json'], // relative + traversal + dupes: ['./a//b/../c.txt', '/a/c.txt'], ['/a/b/../../d.txt', '/d.txt'], ['/skills/', '/skills/'], // trailing slash PRESERVED (nginx dav collection ops need it) ['backups', '/backups'], // foreign space stays intact (only own space prefix is stripped): ['/spaces/otheruser/file.txt', '/spaces/otheruser/file.txt'], ['https://evil.example.com/spaces/mindtech/x?query=1#frag', '/x'], ]; for (const [input, expected] of cases) { const got = m._normalizeRemotePath(input); check(`normalize(${JSON.stringify(input)}) => ${expected}`, got === expected, got); } } // --- _absoluteUrl (MOVE/COPY Destination) --- { const m = makeMod(); const got = m._absoluteUrl('https://webx11.duckdns.org/spaces/mindtech/a/b.html'); check('_absoluteUrl strips to full absolute URI', got === 'https://webx11.duckdns.org/spaces/mindtech/a/b.html', got); const got2 = m._absoluteUrl('c.html'); check('_absoluteUrl builds from config base', got2 === 'https://webx11.duckdns.org/spaces/mindtech/c.html', got2); } // --- _ensureDir normalizes before MKCOL walk --- { const m = makeMod(); const mkcols = []; m._webdavRequest = async (method, path) => { if (method === 'MKCOL') mkcols.push(path); return { ok: true, status: 201 }; }; m._listDir = async () => { throw new Error('ENOENT'); }; await m._ensureDir('https://webx11.duckdns.org/spaces/mindtech/thaimassage-kiefersfelden'); check('_ensureDir MKCOLs normalized path (no https:/ junk, trailing slash for nginx)', mkcols.length === 1 && mkcols[0] === '/thaimassage-kiefersfelden/', mkcols); // Existing dir fast-path: __list__ probe succeeds -> no MKCOL at all const m2 = makeMod(); const mkcols2 = []; m2._listDir = async (p) => { if (p === '/skills') return []; throw new Error('ENOENT'); }; m2._webdavRequest = async (method, path) => { if (method === 'MKCOL') mkcols2.push(path); return { ok: true, status: 201 }; }; await m2._ensureDir('/skills'); check('_ensureDir skips MKCOL when dir already exists', mkcols2.length === 0, mkcols2); // Partial existing: /backups exists, /backups/2026 does not const m3 = makeMod(); const mkcols3 = []; m3._listDir = async (p) => { if (p === '/backups') return []; throw new Error('ENOENT'); }; m3._webdavRequest = async (method, path) => { if (method === 'MKCOL') mkcols3.push(path); return { ok: true, status: 201 }; }; await m3._ensureDir('/backups/2026'); check('_ensureDir MKCOLs only missing tail (trailing slash for nginx)', mkcols3.length === 1 && mkcols3[0] === '/backups/2026/', mkcols3); } // --- PUT via _webdavRequest hits normalized URL --- { // The class closes over the injected fetch param — rebuild with a // recording fetch so we can assert the request URL. let requested = null; const recordFetch = async (url) => { requested = url; return { ok: true, status: 201 }; }; const WebDAVModule2 = new Function('window', 'localStorage', 'fetch', 'XMLHttpRequest', 'URL', 'crypto', 'return ' + cleanCode + '\nreturn ' + classMatch[1])( { location: { pathname: '/spaces/mindtech/newos4/main.html' } }, { getItem: () => null, setItem: () => {} }, recordFetch, function () { this.open = () => {}; this.setRequestHeader = () => {}; this.send = () => {}; }, URL, undefined); const m = new WebDAVModule2(new TerminalStub()); m.config = { url: 'https://webx11.duckdns.org/spaces/mindtech/', username: 'mindtech', password: 'tok' }; m._resolvePassword = async () => {}; m._getAuthHeader = () => ({}); await m._webdavRequest('PUT', 'https://webx11.duckdns.org/spaces/mindtech/site/index.html', { body: 'x' }); check('PUT goes to normalized URL', requested === 'https://webx11.duckdns.org/spaces/mindtech/site/index.html', requested); } console.log(`\n${pass} passed, ${fail} failed`); process.exit(fail ? 1 : 0);