(rl => { const nsDAV = 'DAV:', nsNC = 'http://nextcloud.org/ns', nsOC = 'http://owncloud.org/ns', nsOCS = 'http://open-collaboration-services.org/ns', nsCalDAV = 'urn:ietf:params:xml:ns:caldav', OC = () => parent.OC, // Nextcloud 19 deprecated generateUrl, but screw `import { generateUrl } from "@nextcloud/router"` generateUrl = path => OC().webroot + (OC().config.modRewriteWorking ? '' : '/index.php') + path, generateRemoteUrl = path => location.protocol + '//' + location.host + generateUrl(path), // shareTypes = {0 = user, 1 = group, 3 = public link} propfindFiles = ` `, /* video/mp4 RGDNVW 3963036 19 [] 3 HTTP/1.1 200 OK HTTP/1.1 404 Not Found */ propfindCal = ` `, xmlParser = new DOMParser(), pathRegex = /.*\/remote.php\/dav\/[^/]+\/[^/]+/g, getElementsByTagName = (parent, namespace, localName) => parent.getElementsByTagNameNS(namespace, localName), getDavElementsByTagName = (parent, localName) => getElementsByTagName(parent, nsDAV, localName), getDavElementByTagName = (parent, localName) => getDavElementsByTagName(parent, localName)?.item(0), getElementByTagName = (parent, localName) => +parent.getElementsByTagName(localName)?.item(0), davFetch = (mode, path, options) => { if (!OC().requestToken) { return Promise.reject(new Error('OC.requestToken missing')); } let cfg = rl.settings.get('Nextcloud'); options = Object.assign({ mode: 'same-origin', cache: 'no-cache', redirect: 'error', credentials: 'same-origin', headers: {} }, options); options.headers.requesttoken = OC().requestToken; // cfg.UID = document.head.dataset.user return fetch(cfg.WebDAV + '/' + mode + '/' + cfg.UID + path, options); }, davFetchFiles = (path, options) => davFetch('files', path, options), createDirectory = path => davFetchFiles(path, { method: 'MKCOL' }), fetchFiles = path => { if (!OC().requestToken) { return Promise.reject(new Error('OC.requestToken missing')); } return davFetchFiles(path, { method: 'PROPFIND', headers: { 'Content-Type': 'application/xml; charset=utf-8' }, body: propfindFiles }) .then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response }))) .then(text => { const elemList = [], responseList = getDavElementsByTagName( xmlParser.parseFromString(text, 'application/xml').documentElement, 'response' ); path = path.replace(/\/$/, ''); for (let i = 0; i < responseList.length; ++i) { const e = responseList.item(i), elem = { name: decodeURIComponent(getDavElementByTagName(e, 'href').textContent .replace(pathRegex, '').replace(/\/$/, '')), isFile: false }; if (getDavElementsByTagName(getDavElementByTagName(e, 'resourcetype'), 'collection').length) { // skip current directory if (elem.name === path) { continue; } } else { elem.isFile = true; elem.id = e.getElementsByTagNameNS(nsOC, 'fileid')?.item(0)?.textContent; elem.size = getDavElementByTagName(e, 'getcontentlength')?.textContent || getElementByTagName(e, 'oc:size')?.textContent; } elemList.push(elem); } return Promise.resolve(elemList); }); }, buildTree = (view, parent, items, path) => { if (items.length) { items.forEach(item => { if (!item.isFile) { let li = document.createElement('li'), details = document.createElement('details'), summary = document.createElement('summary'), ul = document.createElement('ul'); details.addEventListener('toggle', () => { ul.children.length || fetchFiles(item.name).then(items => buildTree(view, ul, items, item.name)); }); summary.textContent = item.name.replace(/^.*\/([^/]+)$/, '$1'); summary.dataset.icon = '📁'; if (!view.files()) { let btn = document.createElement('button'); btn.name = 'select'; btn.textContent = 'select'; btn.className = 'button-vue'; btn.style.marginLeft = '1em'; summary.append(btn); summary.item_name = item.name; } details.append(summary); details.append(ul); // a.append('- ' + item.name.replace(/^\/+/, '')); li.append(details); parent.append(li); } }); if (view.files()) { items.forEach(item => { if (item.isFile) { let li = document.createElement('li'), btn = document.createElement('button'); li.item = item; li.textContent = item.name.replace(/^.*\/([^/]+)$/, '$1'); li.dataset.icon = '🗎'; btn.name = 'select'; btn.textContent = 'select'; btn.className = 'button-vue'; btn.style.marginLeft = '1em'; li.append(btn); btn = document.createElement('button'); btn.name = 'share-internal'; btn.textContent = '🔗 internal'; btn.className = 'button-vue'; btn.style.marginLeft = '1em'; li.append(btn); /* btn = document.createElement('button'); btn.name = 'share-public'; btn.textContent = '🔗 public'; btn.className = 'button-vue'; btn.style.marginLeft = '1em'; li.append(btn); */ parent.append(li); } }); } } if (!view.files()) { let li = document.createElement('li'), input = document.createElement('input'), btn = document.createElement('button'); btn.name = 'create'; btn.textContent = 'create & select'; btn.className = 'button-vue'; btn.input = input; li.item_path = path; li.append(input); li.append(btn); parent.append(li); } }; class NextcloudFilesPopupView extends rl.pluginPopupView { constructor() { super('NextcloudFiles'); this.addObservables({ files: false }); } onBuild(dom) { this.tree = dom.querySelector('#sm-nc-files-tree'); this.tree.addEventListener('click', event => { let el = event.target; if (el.matches('button')) { let parent = el.parentNode; if ('select' == el.name) { this.select = this.files() ? [parent.item] : parent.item_name; this.close(); } else if ('share-internal' == el.name) { this.select = [{url:generateRemoteUrl(`/f/${parent.item.id}`)}]; this.close(); } else if ('share-public' == el.name) { /* if (3 == share-type) { GET generateUrl(`/ocs/v2.php/apps/files_sharing/api/v1/shares?format=json&path=${encodeURIComponent(parent.item.name)}&reshares=true`); } else { POST generateUrl(`/ocs/v2.php/apps/files_sharing/api/v1/shares`) > {"path":"/Nextcloud intro.mp4","shareType":3,"attributes":"[]"} < {"ocs":{"meta":{"status":"ok","statuscode":200,"message":"OK"}, "data":{ "id":"2", "share_type":3, "permissions":17, "token":"7GK9mL9LCTseSgK", "path":"\/Nextcloud intro.mp4", "item_type":"file", "mimetype":"video\/mp4", "storage":1, "item_source":20, "file_source":20, "file_parent":2, "file_target":"\/Nextcloud intro.mp4", "password":null, "url":"https:\/\/example.com\/index.php\/s\/7GK9mL9LCTseSgK", "mail_send":1, "hide_download":0, "attributes":null }}} GET /index.php/s/7GK9mL9LCTseSgK PUT /ocs/v2.php/apps/files_sharing/api/v1/shares/2 > {"expireDate":"\"2022-11-29T23:00:00.000Z\""} > {"password":"ABC09"} } */ } else if ('create' == el.name) { let name = el.input.value.replace(/[|\\?*<":>+[]\/&\s]/g, ''); if (name.length) { name = parent.item_path + '/' + name; createDirectory(name).then(response => { if (response.status == 201) { this.select = name; this.close(); } }); } } } }); } // Happens after showModal() beforeShow(files, fResolve) { this.select = ''; this.files(!!files); this.fResolve = fResolve; this.tree.innerHTML = ''; fetchFiles('/').then(items => { buildTree(this, this.tree, items, '/'); }).catch(err => console.error(err)) } onHide() { this.fResolve(this.select); } /* beforeShow() {} // Happens before showModal() onShow() {} // Happens after showModal() afterShow() {} // Happens after showModal() animation transitionend onHide() {} // Happens before animation transitionend afterHide() {} // Happens after animation transitionend close() {} */ } class NextcloudCalendarsPopupView extends rl.pluginPopupView { constructor() { super('NextcloudCalendars'); } onBuild(dom) { this.tree = dom.querySelector('#sm-nc-calendars'); this.tree.addEventListener('click', event => { let el = event.target; if (el.matches('button')) { this.select = el.href; this.close(); } }); } // Happens after showModal() beforeShow(fResolve) { this.select = ''; this.fResolve = fResolve; this.tree.innerHTML = ''; davFetch('calendars', '/', { method: 'PROPFIND', headers: { 'Content-Type': 'application/xml; charset=utf-8' }, body: propfindCal }) .then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response }))) .then(text => { const responseList = getDavElementsByTagName( xmlParser.parseFromString(text, 'application/xml').documentElement, 'response' ); for (let i = 0; i < responseList.length; ++i) { const e = responseList.item(i); if (getDavElementByTagName(e, 'resourcetype').getElementsByTagNameNS(nsCalDAV, 'calendar').length) { // && getDavElementsByTagName(getDavElementByTagName(e, 'current-user-privilege-set'), 'write').length) { const li = document.createElement('li'), btn = document.createElement('button'); li.dataset.icon = '📅'; li.textContent = getDavElementByTagName(e, 'displayname').textContent; btn.href = getDavElementByTagName(e, 'href').textContent .replace(pathRegex, '').replace(/\/$/, ''); btn.textContent = 'select'; btn.className = 'button-vue'; btn.style.marginLeft = '1em'; li.append(btn); this.tree.append(li); } } }) .catch(err => console.error(err)); } onHide() { this.fResolve(this.select); } /* beforeShow() {} // Happens before showModal() onShow() {} // Happens after showModal() afterShow() {} // Happens after showModal() animation transitionend onHide() {} // Happens before animation transitionend afterHide() {} // Happens after animation transitionend close() {} */ } rl.nextcloud = { selectCalendar: () => new Promise(resolve => { NextcloudCalendarsPopupView.showModal([ href => resolve(href), ]); }), calendarPut: (path, event) => { // Validation error in iCalendar: A calendar object on a CalDAV server MUST NOT have a METHOD property. event = event.replace(/METHOD:.+\r?\n/i, ''); let m = event.match(/UID:(.+)/); davFetch('calendars', path + '/' + m[1] + '.ics', { method: 'PUT', headers: { 'Content-Type': 'text/calendar' }, body: event }) .then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response }))) .then(text => { console.dir({event_response:text}); }); }, selectFolder: () => new Promise(resolve => { NextcloudFilesPopupView.showModal([ false, folder => resolve(folder), ]); }), selectFiles: () => new Promise(resolve => { NextcloudFilesPopupView.showModal([ true, files => resolve(files), ]); }) }; })(window.rl);