// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors // // SPDX-License-Identifier: AGPL-3.0-or-later define([ 'jquery', '/api/config', '/common/hyperscript.js', '/customize/messages.js', '/components/nthen/index.js', '/common/common-hash.js', '/common/common-util.js', '/common/common-constants.js', '/common/cryptget.js', '/common/cryptpad-common.js', '/common/cache-store.js', '/common/common-interface.js', 'chainpad-netflux', '/components/chainpad-crypto/crypto.js', '/common/user-object.js', '/common/clipboard.js', '/common/outer/login-block.js', //'/common/outer/roster.js', '/common/rpc.js', '/common/pinpad.js', '/common/outer/local-store.js', '/components/tweetnacl/nacl-fast.min.js', 'less!/customize/src/less2/pages/page-report.less', ], function ($, ApiConfig, h, Messages, nThen, Hash, Util, Constants, Crypt, Cryptpad, Cache, UI, CPNetflux, Crypto, UserObject, Clipboard, Block, /*Roster,*/ Rpc, Pinpad, LocalStore) { var $report = $('#cp-report'); var blockHash = localStorage.Block_hash; if (!blockHash) { return void UI.alert(Messages.mustLogin, function () { var href = Hash.hashToHref('', 'login'); var url = Hash.getNewPadURL(href, { href: '/report/', }); console.log(url); window.location.href = url; }); } let report = true; let fixRoster = false; let urlHash = window.location.hash.slice(1); if (urlHash) { let s = urlHash.split('='); let key = s[0]; let value = s[1]; if (key === "roster") { report = false; fixRoster = value; // teamid } } var addReport = function (str) { $report.append(h('div', str)); }; var getReportContent = window.getReportContent = function () { try { return $report[0].innerText; } catch (err) { return ''; } }; var copyToClipboard = function () { Clipboard.copy(getReportContent(), (err) => { if (err) { return UI.warn(Messages.error); } UI.log(Messages.genericCopySuccess); }); }; var checkCache = function (chan, cb) { Cache.getChannelCache(chan, function (err, val) { if (err) { addReport('Cache error:' + err); } else { addReport('Cache: ' + val.c.length + ' entries'); } cb(); }); }; var onCacheReady = function (info) { var doc; try { doc = info.realtime.getUserDoc(); JSON.parse(doc); addReport('Cache ready success: ' + info.id + ' - Length: ' + doc.length); } catch (e) { addReport('Cache ready error: ' + info.id + ' - Length: ' + (doc || '').length); } }; var network; var proxy; var hash; nThen(function (waitFor) { let done = waitFor(); var parsed = Block.parseBlockHash(blockHash); var sessionToken = LocalStore.getSessionToken() || undefined; Util.getBlock(parsed.href, { bearer: sessionToken }, waitFor((err, response) => { if (err) { waitFor.abort(); return void UI.alert('Invalid user'); } response.arrayBuffer().then(arraybuffer => { arraybuffer = new Uint8Array(arraybuffer); // use the results to load your user hash and // put your userhash into localStorage try { var block_info = Block.decrypt(arraybuffer, parsed.keys); if (!block_info) { return console.error("Failed to decrypt !"); } hash = block_info[Constants.userHashKey]; if (!hash) { return void console.error("failed to decrypt or decode block content"); } } catch (e) { return void console.error("failed to decrypt or decode block content"); } done(); }); })); }).nThen(function (waitFor) { Cryptpad.makeNetwork(waitFor(function (err, _network) { if (err) { console.error(err); waitFor.abort(); return void UI.errorLoadingScreen(err); } network = _network; })); }).nThen(function (waitFor) { addReport('BEGIN REPORT'); var secret = Hash.getSecrets('drive', hash); addReport('Load drive. ID: ' + secret.channel); checkCache(secret.channel, waitFor()); }).nThen(function (waitFor) { Crypt.get(hash, waitFor(function (err, val) { if (err) { console.error(err); addReport('Load drive error. err: ' + err); return void waitFor.abort(); } try { proxy = JSON.parse(val); } catch (e) { console.error(e); addReport('Load drive error. Parse error: ' + e); waitFor.abort(); } }), { network: network, onCacheReady: onCacheReady}); }).nThen(function (waitFor) { var drive = proxy.drive || {}; if (!proxy.drive) { addReport('ERROR: no drive'); return void waitFor.abort(); } addReport('Load drive success.'); addReport('Public key: '+proxy.edPublic); addReport('Shared folders: ' + Object.keys(drive.sharedFolders || {}).join(', ')); addReport('Teams: ' + Object.keys(proxy.teams || {}).join(', ')); addReport('-------------------'); if (!report) { return; } var n = nThen; Object.keys(drive.sharedFolders || {}).forEach(function (id) { n = n(function (w) { var next = w(); var obj = drive.sharedFolders[id]; addReport('Load shared folder. ID: ' + id + '. Channel ID: '+ obj.channel); if (obj.password) { addReport("Password protected"); } if (!obj.href) { addReport("View only"); } checkCache(obj.channel, function () { var parsed = Hash.parsePadUrl(obj.href || obj.roHref); Crypt.get(parsed.hash, function (err, val, errorObj) { if (err) { addReport('ERROR: ' + err); if (err === "ERESTRICTED") { addReport('RESTRICTED: ' + (errorObj && errorObj.message)); } } else { addReport('Load shared folder: success. Size: ' + val.length); } addReport('-------------------'); next(); }, { network: network, password: obj.password, onCacheReady: onCacheReady }); }); }).nThen; }); n(waitFor()); }).nThen(function () { addReport('==================='); var n = nThen; Object.keys(proxy.teams || {}).forEach(function (id) { // If we're in "repair" mode, only load the affected team var obj = proxy.teams[id]; if (!report && fixRoster !== obj.channel) { return; } n = n(function (w) { var next = w(); var team; addReport('Load team. ID: ' + id + '. Channel ID: '+ obj.channel); addReport('Roster channel: ' + obj.keys.roster.channel); addReport('Roster lkh: ' + obj.keys.roster.lastKnownHash); if (!obj.hash) { addReport("View only"); } var teamSecret = Hash.getSecrets('team', obj.hash || obj.roHash, obj.password); var cryptor = UserObject.createCryptor(teamSecret.keys.secondaryKey); // Repair roster mode /* if (!report) { let roster = obj.keys.roster; let rpc, anon_rpc; if (!obj.owner || !roster.edit) { return void addReport('Roster error: only owners can repair'); } n(w => { Rpc.createAnonymous(network, w(function (e, call) { anon_rpc = call; })); Pinpad.create(network, proxy, w(function (e, call) { rpc = call; })); }).nThen(w => { if (!anon_rpc || !rpc) { return void addReport('RPC error'); } var rosterKeys = Crypto.Team.deriveMemberKeys(roster.edit, { curvePublic: proxy.curvePublic, curvePrivate: proxy.curvePrivate }); let store = { anon_rpc }; Roster.create({ network: network, channel: roster.channel, keys: rosterKeys, store: store, lastKnownHash: roster.lastKnownHash }, w(function (err, _roster) { if (err) { return void addReport('Fix roster error', err); } _roster.checkpoint(w((err, state, hash) => { if (err) { addReport('Fix roster error', err); } rpc.trimHistory({ channel: roster.channel, hash }, w(function (err) { console.error(arguments); if (err) { addReport('Trim roster error', err); } })); })); })); }).nThen(next); return; }*/ // Check team drive nThen(function (ww) { addReport('Team drive'); var _next = ww(); checkCache(obj.channel, function () { Crypt.get(obj.hash || obj.roHash, function (err, val) { if (err) { addReport('ERROR: ' + err); addReport('==================='); next(); ww.abort(); } else { addReport('Team drive success. Size: ' + val.length); try { team = JSON.parse(val); } catch (e) { addReport('PARSE ERROR: ' + e); addReport('==================='); next(); ww.abort(); } addReport('Shared folders: ' + Object.keys(team.drive.sharedFolders || {}).join(', ')); } addReport('-------------------'); _next(); }, { network: network, password: obj.password, onCacheReady: onCacheReady }); }); }).nThen(function (ww) { var _next = ww(); var d = Util.find(obj, ['keys', 'roster']); var rosterKeys = d.edit ? Crypto.Team.deriveMemberKeys(d.edit, proxy) : Crypto.Team.deriveGuestKeys(d.view || ''); if (d.channel !== rosterKeys.channel) { next(); ww.abort(); addReport("Invalid roster keys:", d.channel, rosterKeys.channel); return; } addReport('Roster channel: ' + d.channel); checkCache(d.channel, function () { var crypto = Crypto.Team.createEncryptor(rosterKeys); var m = 0; CPNetflux.start({ lastKnownHash: d.lastKnownHash || -1, network: network, channel: d.channel, crypto: crypto, validateKey: rosterKeys.teamEdPublic, Cache: Cache, noChainPad: true, onCacheReady: onCacheReady, onChannelError: function (obj) { addReport('ERROR:' + obj.error); if (obj.error === "ERESTRICTED") { addReport('RESTRICTED: ' + obj.message); } next(); ww.abort(); addReport('==================='); return; }, onMessage: function () { m++; }, onReady: function () { addReport("Roster success. Length: "+m); addReport('-------------------'); _next(); } }); }); }).nThen(function (ww) { var _next = ww(); var n = nThen; var drive = team.drive; Object.keys(drive.sharedFolders || {}).forEach(function (id) { n = n(function (w) { var next = w(); var _obj = drive.sharedFolders[id]; addReport('Load shared folder. ID: ' + id + '. Channel ID: '+ _obj.channel); if (_obj.password) { addReport("Password protected"); } if (!_obj.href) { addReport("View only"); } checkCache(_obj.channel, function () { if (_obj.href && _obj.href.indexOf('#') === -1) { _obj.href = cryptor.decrypt(_obj.href); } var parsed = Hash.parsePadUrl(_obj.href || _obj.roHref); Crypt.get(parsed.hash, function (err, val, errorObj) { if (err) { addReport('ERROR: ' + err); if (err === "ERESTRICTED") { addReport('RESTRICTED: ' + (errorObj && errorObj.message)); } } else { addReport('Load shared folder: success. Size: ' + val.length); } addReport('-------------------'); next(); }, { network: network, password: _obj.password, onCacheReady: onCacheReady }); }); }).nThen; }); n(_next); }).nThen(next); }).nThen; }); n(function () { addReport('==================='); addReport('DONE'); var copyButton = h('button.btn.btn-primary', Messages.copyToClipboard); copyButton.onclick = copyToClipboard; var buttonContainer = h('div#cp-report-ui', [ copyButton, ]); document.body.appendChild(buttonContainer); }); }); });