You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cryptpad/www/common/onlyoffice/inner.js

3271 lines
133 KiB
JavaScript

define([
'jquery',
'/common/toolbar.js',
'json.sortify',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/common/common-interface.js',
'/common/common-hash.js',
'/common/common-util.js',
'/common/common-ui-elements.js',
'/common/common-feedback.js',
'/common/hyperscript.js',
'/api/config',
'/customize/messages.js',
'/customize/application_config.js',
'/bower_components/chainpad/chainpad.dist.js',
'/file/file-crypto.js',
'/common/onlyoffice/history.js',
'/common/onlyoffice/oocell_base.js',
'/common/onlyoffice/oodoc_base.js',
'/common/onlyoffice/ooslide_base.js',
'/common/outer/worker-channel.js',
'/common/outer/x2t.js',
'/bower_components/file-saver/FileSaver.min.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'less!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/common/onlyoffice/app-oo.less',
], function (
$,
Toolbar,
JSONSortify,
nThen,
SFCommon,
UI,
Hash,
Util,
UIElements,
Feedback,
h,
ApiConfig,
Messages,
AppConfig,
ChainPad,
FileCrypto,
History,
EmptyCell,
EmptyDoc,
EmptySlide,
Channel,
X2T)
{
var saveAs = window.saveAs;
var Nacl = window.nacl;
var APP = window.APP = {
4 years ago
$: $,
urlArgs: Util.find(ApiConfig, ['requireConf', 'urlArgs'])
};
var CHECKPOINT_INTERVAL = 100;
var FORCE_CHECKPOINT_INTERVAL = 10000;
var DISPLAY_RESTORE_BUTTON = false;
var NEW_VERSION = 4; // version of the .bin, patches and ChainPad formats
var PENDING_TIMEOUT = 30000;
var CURRENT_VERSION = X2T.CURRENT_VERSION;
4 years ago
//var READONLY_REFRESH_TO = 15000;
var debug = function (x, type) {
if (!window.CP_DEV_MODE) { return; }
console.debug(x, type);
};
var stringify = function (obj) {
return JSONSortify(obj);
};
var toolbar;
4 years ago
var cursor;
var andThen = function (common) {
6 years ago
var Title;
var sframeChan = common.getSframeChannel();
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var readOnly = false;
var offline = false;
var ooLoaded = false;
var pendingChanges = {};
var config = {};
var content = {
hashes: {},
ids: {},
5 years ago
mediasSources: {},
version: privateData.ooForceVersion ? Number(privateData.ooForceVersion) : NEW_VERSION
};
var oldHashes = {};
var oldIds = {};
var oldLocks = {};
var myUniqueOOId;
var myOOId;
var sessionId = Hash.createChannelId();
var cpNfInner;
var evOnPatch = Util.mkEvent();
var evOnSync = Util.mkEvent();
// This structure is used for caching media data and blob urls for each media cryptpad url
var mediasData = {};
5 years ago
var startOO = function () {};
var supportsXLSX = function () {
return privateData.supportsWasm;
};
var getMediasSources = APP.getMediasSources = function() {
5 years ago
content.mediasSources = content.mediasSources || {};
5 years ago
return content.mediasSources;
5 years ago
};
var getId = function () {
return metadataMgr.getNetfluxId() + '-' + privateData.clientId;
};
var getWindow = function () {
return window.frames && window.frames[0];
};
var getEditor = function () {
var w = getWindow();
if (!w) { return; }
return w.editor || w.editorCell;
};
var setEditable = function (state, force) {
$('#cp-app-oo-editor').find('#cp-app-oo-offline').remove();
/*
try {
getEditor().asc_setViewMode(!state);
//window.frames[0].editor.setViewModeDisconnect(true);
} catch (e) {}
*/
if (!state && (!readOnly || force)) {
$('#cp-app-oo-editor').append(h('div#cp-app-oo-offline'));
}
};
var deleteOffline = function () {
var ids = content.ids;
var users = Object.keys(metadataMgr.getMetadata().users);
Object.keys(ids).forEach(function (id) {
var nId = id.slice(0,32);
if (users.indexOf(nId) === -1) {
delete ids[id];
}
});
APP.onLocal();
};
var isRegisteredUserOnline = function () {
var users = metadataMgr.getMetadata().users || {};
return Object.keys(users).some(function (id) {
return users[id] && users[id].curvePublic;
});
};
var isUserOnline = function (ooid) {
// Remove ids for users that have left the channel
deleteOffline();
var ids = content.ids;
// Check if the provided id is in the ID list
return Object.keys(ids).some(function (id) {
return ooid === ids[id].ooid;
});
};
var getUserIndex = function () {
var i = 1;
var ids = content.ids || {};
Object.keys(ids).forEach(function (k) {
if (ids[k] && ids[k].index && ids[k].index >= i) {
i = ids[k].index + 1;
}
});
return i;
};
var setMyId = function () {
// Remove ids for users that have left the channel
deleteOffline();
var ids = content.ids;
if (!myOOId) {
myOOId = Util.createRandomInteger();
6 years ago
// f: function used in .some(f) but defined outside of the while
var f = function (id) {
return ids[id] === myOOId;
6 years ago
};
while (Object.keys(ids).some(f)) {
myOOId = Util.createRandomInteger();
}
}
var myId = getId();
ids[myId] = {
ooid: myOOId,
index: getUserIndex(),
netflux: metadataMgr.getNetfluxId()
};
oldIds = JSON.parse(JSON.stringify(ids));
APP.onLocal();
};
// Another tab from our worker has left: remove its id from the list
var removeClient = function (obj) {
var tabId = metadataMgr.getNetfluxId() + '-' + obj.id;
if (content.ids[tabId]) {
delete content.ids[tabId];
delete content.locks[tabId];
APP.onLocal();
}
};
// Make sure a former tab on the same worker doesn't have remaining locks
var checkClients = function (clients) {
if (!clients) { return; }
Object.keys(content.ids).forEach(function (id) {
var tabId = Number(id.slice(33)); // remove the netflux ID and the "-"
if (clients.indexOf(tabId) === -1) {
removeClient({
id: tabId
});
}
});
};
var getFileType = function () {
var type = common.getMetadataMgr().getPrivateData().ooType;
var title = common.getMetadataMgr().getMetadataLazy().title;
if (APP.downloadType) {
type = APP.downloadType;
title = "download";
}
var file = {};
switch(type) {
case 'doc':
file.type = 'docx';
file.title = title + '.docx' || 'document.docx';
file.doc = 'text';
break;
case 'sheet':
file.type = 'xlsx';
file.title = title + '.xlsx' || 'spreadsheet.xlsx';
file.doc = 'spreadsheet';
break;
case 'presentation':
file.type = 'pptx';
7 years ago
file.title = title + '.pptx' || 'presentation.pptx';
file.doc = 'presentation';
break;
}
return file;
};
var now = function () { return +new Date(); };
4 years ago
var sortCpIndex = function (hashes) {
return Object.keys(hashes).map(Number).sort(function (a, b) {
return a-b;
});
};
var getLastCp = function (old, i) {
var hashes = old ? oldHashes : content.hashes;
if (!hashes || !Object.keys(hashes).length) { return {}; }
i = i || 0;
4 years ago
var idx = sortCpIndex(hashes);
var lastIndex = idx[idx.length - 1 - i];
if (typeof(lastIndex) === "undefined" || !hashes[lastIndex]) {
return {};
}
var last = JSON.parse(JSON.stringify(hashes[lastIndex]));
return last;
};
var rtChannel = {
ready: false,
readyCb: undefined,
sendCmd: function (data, cb) {
if (APP.history) { return; }
sframeChan.query('Q_OO_COMMAND', data, cb);
},
getHistory: function (cb) {
rtChannel.sendCmd({
cmd: 'GET_HISTORY',
data: {}
}, function () {
APP.onHistorySynced = cb;
});
},
sendMsg: function (msg, cp, cb) {
evOnPatch.fire();
rtChannel.sendCmd({
cmd: 'SEND_MESSAGE',
data: {
msg: msg,
isCp: cp
}
}, function (err, h) {
if (!err) { evOnSync.fire(); }
cb(err, h);
});
},
};
var ooChannel = {
ready: false,
queue: [],
send: function () {},
cpIndex: 0
};
var getContent = function () {
try {
return getEditor().asc_nativeGetFile();
} catch (e) {
console.error(e);
return;
}
};
/*
5 years ago
var checkDrawings = function () {
var editor = getEditor();
if (!editor || !editor.GetSheets) { return false; }
5 years ago
var s = editor.GetSheets();
return s.some(function (obj) {
return obj.worksheet.Drawings.length;
});
};
*/
5 years ago
4 years ago
// DEPRECATED from version 3
// Loading a checkpoint reorder the sheet starting from ID "5".
// We have to reorder it manually when a checkpoint is created
// so that the messages we send to the realtime channel are
// loadable by users joining after the checkpoint
var fixSheets = function () {
4 years ago
// Starting from version 3, we don't need to fix the sheet IDs anymore
// because we reload onlyoffice whenever we receive a checkpoint
if (!APP.migrate || (content && content.version > 2)) { return; }
try {
var editor = getEditor();
// if we are not in the sheet app
// we should not call this code
5 years ago
if (typeof editor.GetSheets === 'undefined') { return; }
var s = editor.GetSheets();
if (s.length === 0) { return; }
var wb = s[0].worksheet.workbook;
s.forEach(function (obj, i) {
var id = String(i + 5);
obj.worksheet.Id = id;
wb.aWorksheetsById[id] = obj.worksheet;
});
} catch (e) {
console.error(e);
}
};
4 years ago
// Add a lock
var isLockedModal = {
content: UI.dialog.customModal(h('div.cp-oo-x2tXls', [
h('span.fa.fa-spin.fa-spinner'),
h('span', Messages.oo_isLocked)
]))
};
var onUploaded = function (ev, data, err) {
if (ev.newTemplate) {
if (err) {
console.error(err);
return void UI.warn(Messages.error);
}
var _content = ev.newTemplate;
_content.hashes = {};
_content.hashes[1] = {
file: data.url,
index: 0,
version: NEW_VERSION
};
_content.version = NEW_VERSION;
_content.channel = Hash.createChannelId();
_content.ids = {};
sframeChan.query('Q_SAVE_AS_TEMPLATE', {
toSave: JSON.stringify({
content: _content,
metadata: {
title: '',
defaultTitle: ev.title
}
}),
title: ev.title
}, function () {
UI.alert(Messages.templateSaved);
Feedback.send('OO_TEMPLATE_CREATED');
});
return;
}
content.saveLock = undefined;
if (err) {
console.error(err);
if (content.saveLock === myOOId) { delete content.saveLock; } // Unlock checkpoints
if (APP.migrateModal) {
try { getEditor().asc_setRestriction(true); } catch (e) {}
setEditable(true);
delete content.migration;
APP.migrateModal.closeModal();
APP.onLocal();
}
if (isLockedModal.modal && err === "TOO_LARGE") {
if (APP.migrate) {
UI.warn(Messages.oo_cantMigrate);
}
APP.cantCheckpoint = true;
isLockedModal.modal.closeModal();
delete isLockedModal.modal;
if (content.saveLock === myOOId) {
delete content.saveLock;
}
APP.onLocal();
return;
}
return void UI.alert(Messages.oo_saveError);
}
// Get the last cp idx
4 years ago
var all = sortCpIndex(content.hashes || {});
var current = all[all.length - 1] || 0;
var i = current + 1;
content.hashes[i] = {
file: data.url,
hash: ev.hash,
index: ev.index,
version: NEW_VERSION
};
oldHashes = JSON.parse(JSON.stringify(content.hashes));
content.locks = {};
content.ids = {};
5 years ago
// If this is a migration, set the new version
if (APP.migrate) {
delete content.migration;
content.version = NEW_VERSION;
}
APP.onLocal();
APP.realtime.onSettle(function () {
UI.log(Messages.saved);
APP.realtime.onSettle(function () {
5 years ago
if (APP.migrate) {
UI.removeModals();
UI.alert(Messages.oo_sheetMigration_complete, function () {
5 years ago
common.gotoURL();
});
return;
5 years ago
}
if (ev.callback) {
return void ev.callback();
}
});
});
sframeChan.query('Q_OO_COMMAND', {
cmd: 'UPDATE_HASH',
data: ev.hash
}, function (err, obj) {
if (err || (obj && obj.error)) { console.error(err || obj.error); }
});
};
var fmConfig = {
noHandlers: true,
noStore: true,
body: $('body'),
onUploaded: function (ev, data) {
if (!data || !data.url) { return; }
data.hash = ev.hash;
sframeChan.query('Q_OO_SAVE', data, function (err) {
onUploaded(ev, data, err);
});
},
onError: function (err) {
onUploaded(null, null, err);
}
};
APP.FM = common.createFileManager(fmConfig);
var resetData = function (blob, type) {
// If a read-only refresh popup was planned, abort it
delete APP.refreshPopup;
clearTimeout(APP.refreshRoTo);
// Don't create the initial checkpoint indefinitely in a loop
delete APP.initCheckpoint;
if (!isLockedModal.modal) {
isLockedModal.modal = UI.openCustomModal(isLockedModal.content);
}
myUniqueOOId = undefined;
setMyId();
var editor = getEditor();
if (editor) {
var app = common.getMetadataMgr().getPrivateData().ooType;
var d;
if (app === 'doc') {
d = editor.GetDocument().Document;
} else if (app === 'presentation') {
d = editor.GetPresentation().Presentation;
}
if (d) {
APP.oldCursor = d.GetSelectionState();
}
}
if (APP.docEditor) { APP.docEditor.destroyEditor(); } // Kill the old editor
4 years ago
$('iframe[name="frameEditor"]').after(h('div#cp-app-oo-placeholder-a')).remove();
ooLoaded = false;
oldLocks = {};
Object.keys(pendingChanges).forEach(function (key) {
clearTimeout(pendingChanges[key]);
delete pendingChanges[key];
});
if (APP.stopHistory || APP.template) { APP.history = false; }
startOO(blob, type, true);
};
var saveToServer = function (blob, title) {
if (APP.cantCheckpoint) { return; } // TOO_LARGE
var text = getContent();
if (!text && !blob) {
setEditable(false, true);
sframeChan.query('Q_CLEAR_CACHE_CHANNELS', [
'chainpad',
content.channel,
], function () {});
UI.alert(Messages.realtime_unrecoverableError, function () {
common.gotoURL();
});
return;
}
blob = blob || new Blob([text], {type: 'plain/text'});
var file = getFileType();
blob.name = title || (metadataMgr.getMetadataLazy().title || file.doc) + '.' + file.type;
var data = {
hash: (APP.history || APP.template) ? ooChannel.historyLastHash : ooChannel.lastHash,
index: (APP.history || APP.template) ? ooChannel.currentIndex : ooChannel.cpIndex
};
fixSheets();
if (!isLockedModal.modal) {
isLockedModal.modal = UI.openCustomModal(isLockedModal.content);
}
ooChannel.ready = false;
ooChannel.queue = [];
data.callback = function () {
if (APP.template) { APP.template = false; }
resetData(blob, file);
};
APP.FM.handleFile(blob, data);
};
var noLogin = false;
var makeCheckpoint = function (force) {
if (APP.cantCheckpoint) { return; } // TOO_LARGE
var locked = content.saveLock;
var lastCp = getLastCp();
var currentIdx = ooChannel.cpIndex;
var needCp = force || (currentIdx - (lastCp.index || 0)) > FORCE_CHECKPOINT_INTERVAL;
if (!needCp) { return; }
if (!locked || !isUserOnline(locked) || force) {
if (!common.isLoggedIn() && !isRegisteredUserOnline() && !noLogin) {
var login = h('button.cp-corner-primary', Messages.login_login);
var register = h('button.cp-corner-primary', Messages.login_register);
var cancel = h('button.cp-corner-cancel', Messages.cancel);
var actions = h('div', [cancel, register, login]);
var modal = UI.cornerPopup(Messages.oo_login, actions, '', {alt: true});
$(register).click(function () {
common.setLoginRedirect('register');
modal.delete();
});
$(login).click(function () {
common.setLoginRedirect('login');
modal.delete();
});
$(cancel).click(function () {
modal.delete();
noLogin = true;
});
return;
}
if (!common.isLoggedIn()) { return; }
content.saveLock = myOOId;
APP.onLocal();
APP.realtime.onSettle(function () {
saveToServer();
});
}
};
var deleteLastCp = function () {
var hashes = content.hashes;
if (!hashes || !Object.keys(hashes).length) { return; }
5 years ago
var i = 0;
var idx = Object.keys(hashes).map(Number).sort(function (a, b) {
return a-b;
});
var lastIndex = idx[idx.length - 1 - i];
delete content.hashes[lastIndex];
APP.onLocal();
APP.realtime.onSettle(function () {
UI.log(Messages.saved);
});
};
var restoreLastCp = function () {
content.saveLock = myOOId;
APP.onLocal();
APP.realtime.onSettle(function () {
onUploaded({
hash: ooChannel.lastHash,
index: ooChannel.cpIndex
}, {
url: getLastCp().file,
});
});
};
// Add a timeout to check if a checkpoint was correctly saved by the locking user
// and "unlock the sheet" or "make a checkpoint" if needed
var cpTo;
var checkCheckpoint = function () {
clearTimeout(cpTo);
var saved = stringify(content.hashes);
var locked = content.saveLock;
var to = 20000 + (Math.random() * 20000);
cpTo = setTimeout(function () {
// If no checkpoint was added and the same user still has the lock
// then make a checkpoint if needed (cp interval)
if (stringify(content.hashes) === saved && locked === content.saveLock) {
content.saveLock = undefined;
makeCheckpoint();
}
}, to);
};
5 years ago
var loadInitDocument = function (type, useNewDefault) {
var newText;
switch (type) {
case 'sheet' :
newText = EmptyCell(useNewDefault);
break;
case 'doc':
5 years ago
newText = EmptyDoc();
break;
case 'presentation':
5 years ago
newText = EmptySlide();
break;
default:
newText = '';
}
return new Blob([newText], {type: 'text/plain'});
};
var loadLastDocument = function (lastCp, onCpError, cb) {
if (!lastCp || !lastCp.file) {
return void onCpError('EEMPTY');
}
5 years ago
ooChannel.cpIndex = lastCp.index || 0;
ooChannel.lastHash = lastCp.hash;
5 years ago
var parsed = Hash.parsePadUrl(lastCp.file);
var secret = Hash.getSecrets('file', parsed.hash);
if (!secret || !secret.channel) { return; }
var hexFileName = secret.channel;
var fileHost = privateData.fileHost || privateData.origin;
var src = fileHost + Hash.getBlobPathFromHex(hexFileName);
var key = secret.keys && secret.keys.cryptKey;
var xhr = new XMLHttpRequest();
xhr.open('GET', src, true);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
if (/^4/.test('' + this.status)) {
4 years ago
onCpError(this.status);
5 years ago
return void console.error('XHR error', this.status);
}
var arrayBuffer = xhr.response;
if (arrayBuffer) {
var u8 = new Uint8Array(arrayBuffer);
FileCrypto.decrypt(u8, key, function (err, decrypted) {
if (err) {
if (err === "DECRYPTION_ERROR") {
console.warn(err);
return void onCpError(err);
}
return void console.error(err);
}
5 years ago
var blob = new Blob([decrypted.content], {type: 'plain/text'});
if (cb) {
return cb(blob, getFileType());
}
startOO(blob, getFileType());
});
}
};
4 years ago
xhr.onerror = function (err) {
onCpError(err);
5 years ago
};
xhr.send(null);
};
4 years ago
/*
var refreshReadOnly = function () {
var cancel = h('button.cp-corner-cancel', Messages.cancel);
var reload = h('button.cp-corner-primary', [
h('i.fa.fa-refresh'),
Messages.oo_refresh
]);
var actions = h('div', [cancel, reload]);
var m = UI.cornerPopup(Messages.oo_refreshText, actions, '');
$(reload).click(function () {
ooChannel.ready = false;
var lastCp = getLastCp();
loadLastDocument(lastCp, function () {
var file = getFileType();
var type = common.getMetadataMgr().getPrivateData().ooType;
var blob = loadInitDocument(type, true);
resetData(blob, file);
}, function (blob, file) {
resetData(blob, file);
});
delete APP.refreshPopup;
m.delete();
});
$(cancel).click(function () {
delete APP.refreshPopup;
m.delete();
});
};
4 years ago
*/
var openVersionHash = function (version) {
readOnly = true;
var hashes = content.hashes || {};
var sortedCp = Object.keys(hashes).map(Number).sort(function (a, b) {
return hashes[a].index - hashes[b].index;
});
var s = version.split('.');
if (s.length !== 2) { return UI.errorLoadingScreen(Messages.error); }
var major = Number(s[0]);
var cpId = sortedCp[major - 1];
var nextCpId = sortedCp[major];
var cp = hashes[cpId] || {};
var minor = Number(s[1]) + 1;
if (APP.isDownload) { minor = undefined; }
var toHash = cp.hash || 'NONE';
var fromHash = nextCpId ? hashes[nextCpId].hash : 'NONE';
sframeChan.query('Q_GET_HISTORY_RANGE', {
channel: content.channel,
lastKnownHash: fromHash,
toHash: toHash,
isDownload: APP.isDownload
}, function (err, data) {
if (err) { console.error(err); return void UI.errorLoadingScreen(Messages.error); }
if (!Array.isArray(data.messages)) {
console.error('Not an array');
return void UI.errorLoadingScreen(Messages.error);
}
// The first "cp" in history is the empty doc. It doesn't include the first patch
// of the history
var initialCp = major === 0 || !cp.hash;
var messages = (data.messages || []).slice(initialCp ? 0 : 1, minor);
messages.forEach(function (obj) {
try { obj.msg = JSON.parse(obj.msg); } catch (e) { console.error(e); }
});
// The version exists if we have results in the "messages" array
// or if we requested a x.0 version
var exists = !Number(s[1]) || messages.length;
var vHashEl;
if (!privateData.embed) {
var vTime = (messages[messages.length - 1] || {}).time;
var vTimeStr = vTime ? new Date(vTime).toLocaleString()
: 'v' + privateData.ooVersionHash;
var vTxt = Messages._getKey('infobar_versionHash',  [vTimeStr]);
// If we expected patched and we don't have any, it means this part
// of the history has been deleted
var vType = "warning";
if (!exists) {
4 years ago
vTxt = Messages.oo_deletedVersion;
vType = "danger";
}
vHashEl = h('div.alert.alert-'+vType+'.cp-burn-after-reading', vTxt);
$('#cp-app-oo-editor').prepend(vHashEl);
}
if (!exists) { return void UI.removeLoadingScreen(); }
loadLastDocument(cp, function () {
if (cp.hash && vHashEl) {
// We requested a checkpoint but we can't find it...
UI.removeLoadingScreen();
vHashEl.innerText = Messages.oo_deletedVersion;
$(vHashEl).removeClass('alert-warning').addClass('alert-danger');
return;
}
var file = getFileType();
var type = common.getMetadataMgr().getPrivateData().ooType;
if (APP.downloadType) { type = APP.downloadType; }
var blob = loadInitDocument(type, true);
ooChannel.queue = messages;
resetData(blob, file);
UI.removeLoadingScreen();
}, function (blob, file) {
ooChannel.queue = messages;
resetData(blob, file);
UI.removeLoadingScreen();
});
});
};
var openRtChannel = function (cb) {
if (rtChannel.ready) { return void cb(); }
var chan = content.channel || Hash.createChannelId();
if (!content.channel) {
content.channel = chan;
APP.onLocal();
}
sframeChan.query('Q_OO_OPENCHANNEL', {
channel: content.channel,
lastCpHash: getLastCp().hash
}, function (err, obj) {
if (err || (obj && obj.error)) { console.error(err || (obj && obj.error)); }
});
sframeChan.on('EV_OO_EVENT', function (obj) {
switch (obj.ev) {
case 'READY':
checkClients(obj.data);
cb();
break;
case 'LEAVE':
removeClient(obj.data);
break;
case 'MESSAGE':
if (APP.history) {
ooChannel.historyLastHash = obj.data.hash;
ooChannel.currentIndex++;
return;
}
if (ooChannel.ready) {
// In read-only mode, push the message to the queue and prompt
// the user to refresh OO (without reloading the page)
/*if (readOnly) {
ooChannel.queue.push(obj.data);
if (APP.refreshPopup) { return; }
APP.refreshPopup = true;
// Don't "spam" the user instantly and no more than
// 1 popup every 15s
APP.refreshRoTo = setTimeout(refreshReadOnly, READONLY_REFRESH_TO);
return;
}*/
ooChannel.send(obj.data.msg);
ooChannel.lastHash = obj.data.hash;
ooChannel.cpIndex++;
} else {
ooChannel.queue.push(obj.data);
}
break;
case 'HISTORY_SYNCED':
if (typeof(APP.onHistorySynced) !== "function") { return; }
APP.onHistorySynced();
delete APP.onHistorySynced;
break;
}
});
};
var getParticipants = function () {
var users = metadataMgr.getMetadata().users;
var i = 1;
var p = Object.keys(content.ids || {}).map(function (id) {
var nId = id.slice(0,32);
if (!users[nId]) { return; }
var ooId = content.ids[id].ooid;
var idx = content.ids[id].index;
if (!ooId || ooId === myOOId) { return; }
if (idx >= i) { i = idx + 1; }
return {
id: String(ooId) + idx,
idOriginal: String(ooId),
username: (users[nId] || {}).name || Messages.anonymous,
indexUser: idx,
connectionId: content.ids[id].netflux || Hash.createChannelId(),
isCloseCoAuthoring:false,
view: false
};
});
// Add an history keeper user to show that we're never alone
var hkId = Util.createRandomInteger();
p.push({
id: hkId,
idOriginal: String(hkId),
username: "History",
indexUser: i,
connectionId: Hash.createChannelId(),
isCloseCoAuthoring:false,
view: false
});
i++;
if (!myUniqueOOId) { myUniqueOOId = String(myOOId) + i; }
p.push({
id: myUniqueOOId,
idOriginal: String(myOOId),
username: metadataMgr.getUserData().name || Messages.anonymous,
indexUser: i,
connectionId: metadataMgr.getNetfluxId() || Hash.createChannelId(),
isCloseCoAuthoring:false,
view: false
});
return {
index: i,
list: p.filter(Boolean)
};
};
6 years ago
// Get all existing locks
var getUserLock = function (id, forceArray) {
var type = common.getMetadataMgr().getPrivateData().ooType;
content.locks = content.locks || {};
var l = content.locks[id] || {};
if (type === "sheet" || forceArray) {
return Object.keys(l).map(function (uid) { return l[uid]; });
}
var res = {};
Object.keys(l).forEach(function (uid) {
res[uid] = l[uid];
});
return res;
};
6 years ago
var getLock = function () {
var type = common.getMetadataMgr().getPrivateData().ooType;
var locks = [];
if (type === "sheet") {
Object.keys(content.locks || {}).forEach(function (id) {
Array.prototype.push.apply(locks, getUserLock(id));
});
return locks;
}
locks = {};
Object.keys(content.locks || {}).forEach(function (id) {
Util.extend(locks, getUserLock(id));
6 years ago
});
return locks;
6 years ago
};
// Update the userlist in onlyoffice
var handleNewIds = function (o, n) {
if (stringify(o) === stringify(n)) { return; }
var p = getParticipants();
ooChannel.send({
type: "connectState",
participantsTimestamp: +new Date(),
participants: p.list,
waitAuth: false
});
};
// Update the locks status in onlyoffice
var handleNewLocks = function (o, n) {
var hasNew = false;
// Check if we have at least one new lock
Object.keys(n || {}).some(function (id) {
if (typeof(n[id]) !== "object") { return; } // Ignore old format
// n[id] = { uid: lock, uid2: lock2 };
return Object.keys(n[id]).some(function (uid) {
// New lock
if (!o[id] || !o[id][uid]) {
hasNew = true;
return true;
}
});
});
// Remove old locks
Object.keys(o || {}).forEach(function (id) {
if (typeof(o[id]) !== "object") { return; } // Ignore old format
Object.keys(o[id]).forEach(function (uid) {
// Removed lock
if (!n[id] || !n[id][uid]) {
ooChannel.send({
type: "releaseLock",
locks: [o[id][uid]]
});
}
});
});
if (hasNew) {
ooChannel.send({
type: "getLock",
locks: getLock()
});
}
};
6 years ago
// Remove locks from offline users
var deleteOfflineLocks = function () {
var locks = content.locks || {};
var users = Object.keys(metadataMgr.getMetadata().users);
Object.keys(locks).forEach(function (id) {
var nId = id.slice(0,32);
if (users.indexOf(nId) === -1) {
// Offline locks: support old format
var l = (locks[id] && !locks[id].block) ? getUserLock(id) : [locks[id]];
6 years ago
ooChannel.send({
type: "releaseLock",
locks: l
6 years ago
});
delete content.locks[id];
}
});
if (content.saveLock && !isUserOnline(content.saveLock)) {
delete content.saveLock;
}
6 years ago
};
var handleAuth = function (obj, send) {
4 years ago
//setEditable(false);
var changes = [];
if (content.version > 2) {
ooChannel.queue.forEach(function (data) {
Array.prototype.push.apply(changes, data.msg.changes);
});
ooChannel.ready = true;
ooChannel.cpIndex += ooChannel.queue.length;
var last = ooChannel.queue.pop();
if (last) { ooChannel.lastHash = last.hash; }
} else {
setEditable(false, true);
4 years ago
}
send({
type: "authChanges",
4 years ago
changes: changes
});
4 years ago
// Answer to the auth command
var p = getParticipants();
send({
type: "auth",
result: 1,
sessionId: sessionId,
participants: p.list,
locks: [],
changes: [],
changesIndex: 0,
indexUser: p.index,
buildVersion: "5.2.6",
buildNumber: 2,
licenseType: 3,
//"g_cAscSpellCheckUrl": "/spellchecker",
//"settings":{"spellcheckerUrl":"/spellchecker","reconnection":{"attempts":50,"delay":2000}}
});
// Open the document
send({
type: "documentOpen",
data: {"type":"open","status":"ok","data":{"Editor.bin":obj.openCmd.url}}
});
/*
// TODO: make sure we don't have new popups that can break our integration
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === "childList") {
for (var i = 0; i < mutation.addedNodes.length; i++) {
if (mutation.addedNodes[i].classList.contains('asc-window') &&
mutation.addedNodes[i].classList.contains('alert')) {
$(mutation.addedNodes[i]).find('button').not('.custom').click();
}
}
}
});
});
observer.observe(window.frames[0].document.body, {
childList: true,
});
*/
};
var handleLock = function (obj, send) {
if (APP.history) { return; }
if (content.saveLock) {
if (!isLockedModal.modal) {
isLockedModal.modal = UI.openCustomModal(isLockedModal.content);
}
setTimeout(function () {
handleLock(obj, send);
}, 50);
return;
}
var type = common.getMetadataMgr().getPrivateData().ooType;
var b = obj.block && obj.block[0];
var msg = {
time: now(),
user: myUniqueOOId,
block: b
6 years ago
};
var editor = getEditor();
if (type === "presentation" && (APP.themeChanged || APP.themeRemote) && b &&
b.guid === editor.GetPresentation().Presentation.themeLock.Id) {
APP.themeLocked = APP.themeChanged;
APP.themeChanged = undefined;
var fakeLocks = getLock();
fakeLocks[Util.uid()] = msg;
send({
type: "getLock",
locks: fakeLocks
});
return;
}
content.locks = content.locks || {};
// Send the lock to other users
var myId = getId();
content.locks[myId] = content.locks[myId] || {};
if (type === "sheet" || typeof(b) !== "string") {
var uid = Util.uid();
content.locks[myId][uid] = msg;
} else {
if (typeof(b) === "string") { content.locks[myId][b] = msg; }
}
oldLocks = JSON.parse(JSON.stringify(content.locks));
// Remove old locks
deleteOfflineLocks();
// Prepare callback
if (cpNfInner) {
var waitLock = APP.waitLock = Util.mkEvent(true);
setTimeout(function () {
// Make sure the waitLock is never stuck
waitLock.fire();
if (waitLock === APP.waitLock) { delete APP.waitLock; }
}, 5000);
var onPatchSent = function (again) {
if (!again) { cpNfInner.offPatchSent(onPatchSent); }
// Answer to our onlyoffice
if (!content.saveLock) {
if (isLockedModal.modal) {
isLockedModal.modal.closeModal();
5 years ago
delete isLockedModal.modal;
if (!APP.history) {
$('#cp-app-oo-editor > iframe')[0].contentWindow.focus();
}
}
send({
type: "getLock",
locks: getLock()
});
waitLock.fire();
if (waitLock === APP.waitLock) { delete APP.waitLock; }
} else {
if (!isLockedModal.modal) {
isLockedModal.modal = UI.openCustomModal(isLockedModal.content);
}
setTimeout(function () {
onPatchSent(true);
}, 50);
}
};
cpNfInner.onPatchSent(onPatchSent);
}
// Commit
APP.onLocal();
APP.realtime.sync();
};
var parseChanges = function (changes, isObj) {
try {
changes = JSON.parse(changes);
} catch (e) {
return [];
}
return changes.map(function (change) {
return {
docid: "fresh",
change: isObj ? change : '"' + change + '"',
time: now(),
user: myUniqueOOId,
useridoriginal: String(myOOId)
};
});
};
var handleChanges = function (obj, send) {
if (APP.history) {
send({
type: "unSaveLock",
index: ooChannel.cpIndex,
time: +new Date()
});
return;
}
// Add a new entry to the pendingChanges object.
// If we can't send the patch within 30s, force a page reload
var uid = Util.uid();
pendingChanges[uid] = setTimeout(function () {
// If we're offline, force a reload on reconnect
if (offline) {
pendingChanges.force = true;
return;
}
// We're online: force a reload now
setEditable(false);
UI.alert(Messages.realtime_unrecoverableError, function () {
common.gotoURL();
});
}, PENDING_TIMEOUT);
if (offline) {
pendingChanges.force = true;
return;
}
var changes = obj.changes;
if (obj.type === "cp_theme") {
changes = JSON.stringify([JSON.stringify(obj)]);
}
// Send the changes
content.locks = content.locks || {};
rtChannel.sendMsg({
type: "saveChanges",
changes: parseChanges(changes, obj.type === "cp_theme"),
changesIndex: ooChannel.cpIndex || 0,
locks: getUserLock(getId(), true),
4 years ago
excelAdditionalInfo: obj.excelAdditionalInfo
}, null, function (err, hash) {
if (err) {
return void console.error(err);
}
4 years ago
if (pendingChanges[uid]) {
clearTimeout(pendingChanges[uid]);
delete pendingChanges[uid];
}
// Call unSaveLock to tell onlyoffice that the patch was sent.
// It will allow you to make changes to another cell.
// If there is an error and unSaveLock is not called, onlyoffice
// will try to send the patch again
send({
type: "unSaveLock",
index: ooChannel.cpIndex,
time: +new Date()
});
// Increment index and update latest hash
ooChannel.cpIndex++;
ooChannel.lastHash = hash;
// Check if a checkpoint is needed
makeCheckpoint();
// Remove my locks
delete content.locks[getId()];
oldLocks = JSON.parse(JSON.stringify(content.locks));
APP.onLocal();
});
};
APP.testArr = [
['a','b',1,'d'],
['e',undefined,'g','h']
];
var makePatch = APP.makePatch = function (arr) {
var w = getWindow();
if (!w) { return; }
// Define OO classes
var AscCommonExcel = w.AscCommonExcel;
var CellValueData = AscCommonExcel.UndoRedoData_CellValueData;
var CCellValue = AscCommonExcel.CCellValue;
//var History = w.AscCommon.History;
var AscCH = w.AscCH;
var Asc = w.Asc;
var UndoRedoData_CellSimpleData = AscCommonExcel.UndoRedoData_CellSimpleData;
var editor = getEditor();
var Id = editor.GetSheet(0).worksheet.Id;
//History.Create_NewPoint();
var patches = [];
arr.forEach(function (arr2, i) {
arr2.forEach(function (v, j) {
var obj = {};
if (typeof(v) === "string") { obj.text = v; obj.type = 1; }
else if (typeof(v) === "number") { obj.number = v; obj.type = 0; }
else { return; }
var newValue = new CellValueData(undefined, new CCellValue(obj));
var nCol = j;
var nRow = i;
var patch = new AscCommonExcel.UndoRedoItemSerializable(AscCommonExcel.g_oUndoRedoCell, AscCH.historyitem_Cell_ChangeValue, Id,
new Asc.Range(nCol, nRow, nCol, nRow),
new UndoRedoData_CellSimpleData(nRow, nCol, undefined, newValue), undefined);
patches.push(patch);
/*
History.Add(AscCommonExcel.g_oUndoRedoCell, AscCH.historyitem_Cell_ChangeValue, Id,
new Asc.Range(nCol, nRow, nCol, nRow),
new UndoRedoData_CellSimpleData(nRow, nCol, undefined, newValue), undefined, true);
*/
});
});
var oMemory = new w.AscCommon.CMemory();
var aRes = [];
patches.forEach(function (item) {
editor.GetSheet(0).worksheet.workbook._SerializeHistoryBase64(oMemory, item, aRes);
});
// Make the patch
var msg = {
type: "saveChanges",
changes: parseChanges(JSON.stringify(aRes)),
changesIndex: ooChannel.cpIndex || 0,
locks: getUserLock(getId(), true),
excelAdditionalInfo: null
};
// Send the patch
rtChannel.sendMsg(msg, null, function (err, hash) {
if (err) {
return void console.error(err);
}
// Apply it on our side
ooChannel.send(msg);
ooChannel.lastHash = hash;
ooChannel.cpIndex++;
});
};
var makeChannel = function () {
var msgEv = Util.mkEvent();
var iframe = $('#cp-app-oo-editor > iframe')[0].contentWindow;
var type = common.getMetadataMgr().getPrivateData().ooType;
window.addEventListener('message', function (msg) {
if (msg.source !== iframe) { return; }
msgEv.fire(msg);
});
var postMsg = function (data) {
iframe.postMessage(data, '*');
};
Channel.create(msgEv, postMsg, function (chan) {
APP.chan = chan;
var send = ooChannel.send = function (obj, force) {
// can't push to OO before reloading cp
if (APP.onStrictSaveChanges && !force) { return; }
// We only need to release locks for sheets
if (type !== "sheet" && obj.type === "releaseLock") { return; }
if (type === "presentation" && obj.type === "cp_theme") {
// XXX
console.error(obj);
return;
}
debug(obj, 'toOO');
chan.event('CMD', obj);
};
chan.on('CMD', function (obj) {
debug(obj, 'fromOO');
switch (obj.type) {
case "auth":
handleAuth(obj, send);
break;
case "isSaveLock":
// TODO ping the server to check if we're online first?
if (!offline) {
if (APP.waitLock) {
APP.waitLock.reg(function () {
send({
type: "saveLock",
saveLock: false
}, true);
});
} else {
send({
type: "saveLock",
saveLock: false
}, true);
}
}
break;
4 years ago
case "cursor":
cursor.updateCursor({
type: "cursor",
messages: [{
cursor: obj.cursor,
time: +new Date(),
user: myUniqueOOId,
useridoriginal: myOOId
}]
});
break;
case "getLock":
handleLock(obj, send);
break;
case "getMessages":
// OO chat messages?
send({ type: "message" });
break;
case "saveChanges":
// If we have unsaved data before reloading for a checkpoint...
if (APP.onStrictSaveChanges) {
delete APP.unsavedLocks;
APP.unsavedChanges = {
type: "saveChanges",
changes: parseChanges(obj.changes),
changesIndex: ooChannel.cpIndex || 0,
locks: type === "sheet" ? [] : APP.unsavedLocks,
excelAdditionalInfo: null,
recover: true
};
APP.onStrictSaveChanges();
return;
}
var AscCommon = window.frames[0] && window.frames[0].AscCommon;
if (Util.find(AscCommon, ['CollaborativeEditing','m_bFast'])
&& APP.themeLocked) {
obj = APP.themeLocked;
APP.themeLocked = undefined;
obj.type = "cp_theme";
console.error(obj);
}
if (APP.themeRemote) {
delete APP.themeRemote;
send({
type: "unSaveLock",
index: ooChannel.cpIndex,
time: +new Date()
});
return;
}
// We're sending our changes to netflux
handleChanges(obj, send);
// If we're alone, clean up the medias
var m = metadataMgr.getChannelMembers().slice().filter(function (nId) {
return nId.length === 32;
});
if (m.length === 1 && APP.loadingImage <= 0) {
try {
// "docs" contains the correct images that we've just uploaded
// "docs2" contains the correct images from the .bin checkpoint
// both of them are not reliable in the other case
var docs = getWindow().AscCommon.g_oDocumentUrls.urls;
var docs2 = getEditor().ImageLoader.map_image_index;
var mediasSources = getMediasSources();
Object.keys(mediasSources).forEach(function (name) {
if (!docs && !docs2) { return; }
if (!docs['media/'+name] && !docs2[name]) {
delete mediasSources[name];
}
});
APP.onLocal();
} catch (e) {}
}
break;
case "unLockDocument":
if (obj.releaseLocks && content.locks && content.locks[getId()]) {
send({
type: "releaseLock",
locks: getUserLock(getId())
});
delete content.locks[getId()];
APP.onLocal();
}
if (obj.isSave) {
send({
type: "unSaveLock",
time: -1,
index: -1
});
}
if (APP.onDocumentUnlock) {
APP.onDocumentUnlock();
APP.onDocumentUnlock = undefined;
}
break;
case 'openDocument':
// When duplicating a slide, OO may ask the URLs of the images
// in that slide
var _obj = obj.message;
if (_obj.c === "imgurls") {
var _mediasSources = getMediasSources();
var images = _obj.data || [];
if (!Array.isArray(images)) { return; }
var urls = images.map(function (name) {
var data = _mediasSources[name];
if (!data) { return; }
var media = mediasData[data.src];
if (!media) { return; }
return {
path: name,
url: media.blobUrl,
};
}).filter(Boolean);
send({
type: "documentOpen",
data: {
type: "imgurls",
status: "ok",
data: {
urls: urls,
error: 0
}
}
});
}
break;
}
});
});
};
var x2tConvertData = function (data, fileName, format, cb) {
var sframeChan = common.getSframeChannel();
var e = getEditor();
var fonts = e && e.FontLoader.fontInfos;
var files = e && e.FontLoader.fontFiles.map(function (f) {
return { 'Id': f.Id, };
});
var type = common.getMetadataMgr().getPrivateData().ooType;
var images = (e && window.frames[0].AscCommon.g_oDocumentUrls.urls) || {};
// Fix race condition which could drop images sometimes
// ==> make sure each image has a 'media/image_name.ext' entry as well
Object.keys(images).forEach(function (img) {
if (/^media\//.test(img)) { return; }
if (images['media/'+img]) { return; }
images['media/'+img] = images[img];
});
// Add theme images
var theme = e && window.frames[0].AscCommon.g_image_loader.map_image_index;
if (theme) {
Object.keys(theme).forEach(function (url) {
if (!/^(\/|blob:|data:)/.test(url)) {
images[url] = url;
}
});
}
sframeChan.query('Q_OO_CONVERT', {
data: data,
type: type,
fileName: fileName,
outputFormat: format,
images: (e && window.frames[0].AscCommon.g_oDocumentUrls.urls) || {},
fonts: fonts,
fonts_files: files,
mediasSources: getMediasSources(),
mediasData: mediasData
}, function (err, obj) {
if (err || !obj || !obj.data) {
UI.warn(Messages.error);
return void cb();
}
cb(obj.data, obj.images);
}, {
raw: true
});
};
// When download a sheet from the drive, we must wait for all the images
// to be downloaded and decrypted before converting to xlsx
var downloadImages = {};
var firstOO = true;
5 years ago
startOO = function (blob, file, force) {
if (APP.ooconfig && !force) { return void console.error('already started'); }
var url = URL.createObjectURL(blob);
var lock = !APP.history && (APP.migrate);
var fromContent = metadataMgr.getPrivateData().fromContent;
if (!firstOO) { fromContent = undefined; }
firstOO = false;
4 years ago
// Starting from version 3, we can use the view mode again
4 years ago
// defined but never used
//var mode = (content && content.version > 2 && lock) ? "view" : "edit";
4 years ago
var lang = (window.cryptpadLanguage || navigator.language || navigator.userLanguage || '').slice(0,2);
// Config
APP.ooconfig = {
"document": {
"fileType": file.type,
"key": "fresh",
"title": file.title,
"url": url,
"permissions": {
"download": false,
"print": true,
}
},
"documentType": file.doc,
"editorConfig": {
customization: {
chat: false,
logo: {
url: "/bounce/#" + encodeURIComponent('https://www.onlyoffice.com')
}
},
"user": {
"id": String(myOOId), //"c0c3bf82-20d7-4663-bf6d-7fa39c598b1d",
"firstname": metadataMgr.getUserData().name || Messages.anonymous,
"name": metadataMgr.getUserData().name || Messages.anonymous,
},
"mode": "edit",
"lang": lang
},
"events": {
"onAppReady": function(/*evt*/) {
var $iframe = $('iframe[name="frameEditor"]').contents();
$iframe.prop('tabindex', '-1');
var $tb = $iframe.find('head');
5 years ago
var css = // Old OO
//'#id-toolbar-full .toolbar-group:nth-child(2), #id-toolbar-full .separator:nth-child(3) { display: none; }' +
//'#fm-btn-save { display: none !important; }' +
//'#panel-settings-general tr.autosave { display: none !important; }' +
//'#panel-settings-general tr.coauth { display: none !important; }' +
//'#header { display: none !important; }' +
5 years ago
'#title-doc-name { display: none !important; }' +
'#title-user-name { display: none !important; }' +
(supportsXLSX() ? '' : '#slot-btn-dt-print { display: none !important; }') +
5 years ago
// New OO:
'section[data-tab="ins"] .separator:nth-last-child(2) { display: none !important; }' + // separator
'#slot-btn-insequation { display: none !important; }' + // Insert equation
//'#asc-gen125 { display: none !important; }' + // Disable presenter mode
//'.toolbar .tabs .ribtab:not(.canedit) { display: none !important; }' + // Switch collaborative mode
5 years ago
'#fm-btn-info { display: none !important; }' + // Author name, doc title, etc. in "File" (menu entry)
'#panel-info { display: none !important; }' + // Same but content
'#image-button-from-url { display: none !important; }' + // Inline image settings: replace with url
'.cp-from-url, #textart-button-from-url { display: none !important; }' + // Spellcheck language
'.statusbar .cnt-lang { display: none !important; }' + // Spellcheck language
'.statusbar #btn-doc-spell { display: none !important; }' + // Spellcheck button
5 years ago
'#file-menu-panel .devider { display: none !important; }' + // separator in the "File" menu
'#left-btn-spellcheck, #left-btn-about { display: none !important; }'+
'div.btn-users.dropdown-toggle { display: none; !important }';
if (readOnly) {
css += '#toolbar { display: none !important; }';
//css += '#app-title { display: none !important; }'; // OnlyOffice logo + doc title
//css += '#file-menu-panel { top: 28px !important; }'; // Position of the "File" menu
}
$('<style>').text(css).appendTo($tb);
setTimeout(function () {
$(window).trigger('resize');
});
if (UI.findOKButton().length) {
UI.findOKButton().on('focusout', function () {
window.setTimeout(function () { UI.findOKButton().focus(); });
});
}
},
"onError": function () {
console.error(arguments);
if (APP.isDownload) {
var sframeChan = common.getSframeChannel();
sframeChan.event('EV_OOIFRAME_DONE', '');
}
},
"onDocumentReady": function () {
evOnSync.fire();
var onMigrateRdy = Util.mkEvent();
onMigrateRdy.reg(function () {
var div = h('div.cp-oo-x2tXls', [
h('span.fa.fa-spin.fa-spinner'),
h('span', Messages.oo_sheetMigration_loading)
]);
APP.migrateModal = UI.openCustomModal(UI.dialog.customModal(div, {buttons: []}));
makeCheckpoint(true);
});
4 years ago
// DEPRECATED: from version 3, the queue is sent again during init
if (APP.migrate && ((content.version || 1) <= 2)) {
// The doc is ready, fix the worksheets IDs and push the queue
fixSheets();
// Push changes since last cp
ooChannel.ready = true;
var changes = [];
var changesIndex;
4 years ago
ooChannel.queue.forEach(function (data) {
Array.prototype.push.apply(changes, data.msg.changes);
changesIndex = data.msg.changesIndex;
//ooChannel.send(data.msg);
4 years ago
});
ooChannel.cpIndex += ooChannel.queue.length;
var last = ooChannel.queue.pop();
if (last) { ooChannel.lastHash = last.hash; }
4 years ago
var onDocUnlock = function () {
// Migration required but read-only: continue...
if (readOnly) {
4 years ago
setEditable(true);
try { getEditor().asc_setRestriction(true); } catch (e) {}
} else {
// No changes after the cp: migrate now
onMigrateRdy.fire();
}
};
// Send the changes all at once
if (changes.length) {
setTimeout(function () {
ooChannel.send({
type: 'saveChanges',
changesIndex: changesIndex,
changes: changes,
locks: []
});
APP.onDocumentUnlock = onDocUnlock;
}, 5000);
return;
4 years ago
}
onDocUnlock();
return;
}
if (lock || readOnly) {
try { getEditor().asc_setRestriction(true); } catch (e) {}
//getEditor().setViewModeDisconnect(); // can't be used anymore, display an OO error popup
} else {
setEditable(true);
4 years ago
deleteOfflineLocks();
handleNewLocks({}, content.locks);
if (APP.unsavedChanges) {
var unsaved = APP.unsavedChanges;
delete APP.unsavedChanges;
rtChannel.sendMsg(unsaved, null, function (err, hash) {
4 years ago
if (err) { return void UI.alert(Messages.oo_lostEdits); }
// This is supposed to be a "send" function to tell our OO
// to unlock the cell. We use this to know that the patch was
// correctly sent so that we can apply it to our OO too.
ooChannel.send(unsaved);
ooChannel.cpIndex++;
ooChannel.lastHash = hash;
});
}
if (APP.startNew) {
var w = getWindow();
if (lang === "fr") { lang = 'fr-fr'; }
var l = w.Common.util.LanguageInfo.getLocalLanguageCode(lang);
getEditor().asc_setDefaultLanguage(l);
}
if (APP.oldCursor) {
var app = common.getMetadataMgr().getPrivateData().ooType;
var d;
if (app === 'doc') {
d = getEditor().GetDocument().Document;
} else if (app === 'presentation') {
d = getEditor().GetPresentation().Presentation;
}
if (d) {
d.SetSelectionState(APP.oldCursor);
d.UpdateSelection();
}
delete APP.oldCursor;
}
}
delete APP.startNew;
if (fromContent && !lock && Array.isArray(fromContent.content)) {
makePatch(fromContent.content);
}
if (APP.isDownload) {
var bin = getContent();
if (!supportsXLSX()) {
return void sframeChan.event('EV_OOIFRAME_DONE', bin, {raw: true});
}
nThen(function (waitFor) {
// wait for all the images to be loaded before converting
Object.keys(downloadImages).forEach(function (name) {
downloadImages[name].reg(waitFor());
});
}).nThen(function () {
x2tConvertData(bin, 'filename.bin', file.type, function (xlsData) {
sframeChan.event('EV_OOIFRAME_DONE', xlsData, {raw: true});
});
});
return;
}
if (isLockedModal.modal && force) {
isLockedModal.modal.closeModal();
delete isLockedModal.modal;
if (!APP.history) {
$('#cp-app-oo-editor > iframe')[0].contentWindow.focus();
}
}
if (APP.template) {
try { getEditor().asc_setRestriction(true); } catch (e) {}
//getEditor().setViewModeDisconnect();
UI.removeLoadingScreen();
makeCheckpoint(true);
return;
}
APP.onLocal(); // Add our data to the userlist
if (APP.history) {
try {
getEditor().asc_setRestriction(true);
} catch (e) {}
}
if (lock && !readOnly) { // Lock = !history && migrate
onMigrateRdy.fire();
}
if (APP.initCheckpoint) {
getEditor().asc_setRestriction(true);
makeCheckpoint(true);
}
// Check if history can/should be trimmed
var cp = getLastCp();
if (cp && cp.file && cp.hash) {
var channels = [{
channel: content.channel,
lastKnownHash: cp.hash
}];
common.checkTrimHistory(channels);
}
}
}
};
/*
// NOTE: Make sure it won't break anaything new (Firefox setTimeout bug)
window.onbeforeunload = function () {
var ifr = document.getElementsByTagName('iframe')[0];
if (ifr) { ifr.remove(); }
};
*/
4 years ago
APP.getUserColor = function (userId) {
var hex;
Object.keys(content.ids || {}).some(function (k) {
var u = content.ids[k];
if (Number(u.ooid) === Number(userId)) {
var md = common.getMetadataMgr().getMetadataLazy();
if (md && md.users && md.users[u.netflux]) {
hex = md.users[u.netflux].color;
}
return true;
}
});
if (hex) {
var rgb = Util.hexToRGB(hex);
return {
r: rgb[0],
g: rgb[1],
b: rgb[2],
a: 255
};
}
};
APP.UploadImageFiles = function (files, type, id, jwt, cb) {
return void cb();
};
APP.AddImage = function(cb1, cb2) {
APP.AddImageSuccessCallback = cb1;
APP.AddImageErrorCallback = cb2;
common.openFilePicker({
types: ['file'],
where: ['root'],
filter: {
fileType: ['image/']
}
}, function (data) {
5 years ago
if (data.type !== 'file') {
debug("Unexpected data type picked " + data.type);
return;
}
var name = data.name;
// Add image to the list
var mediasSources = getMediasSources();
// Check if name already exists
var getUniqueName = function (name, mediasSources) {
var get = function () {
var s = name.split('.');
if (s.length > 1) {
s[s.length - 2] = s[s.length - 2] + '-' + Util.uid();
name = s.join('.');
} else {
name += '-'+ Util.uid();
}
};
while (mediasSources[name]) { get(); }
return name;
};
if (mediasSources[name]) {
name = getUniqueName(name, mediasSources);
data.name = name;
}
5 years ago
mediasSources[name] = data;
APP.onLocal();
APP.realtime.onSettle(function () {
APP.getImageURL(name, function(url) {
debug("CRYPTPAD success add " + name);
common.setPadAttribute('atime', +new Date(), null, data.href);
APP.AddImageSuccessCallback({
name: name,
url: url
});
5 years ago
});
});
});
};
APP.remoteTheme = function () {
/*
APP.themeRemote = true;
*/
};
APP.changeTheme = function (id) {
/*
// XXX disabled:
Uncaught TypeError: Cannot read property 'calculatedType' of null
at CPresentation.changeTheme (sdk-all.js?ver=4.11.0-1633612942653-1633619288217:15927)
*/
id = id;
/*
APP.themeChanged = {
id: id
};
*/
};
APP.openURL = function (url) {
common.openUnsafeURL(url);
};
APP.loadingImage = 0;
APP.getImageURL = function(name, callback) {
4 years ago
if (name && /^data:image/.test(name)) {
return void callback('');
4 years ago
}
5 years ago
var mediasSources = getMediasSources();
var data = mediasSources[name];
downloadImages[name] = Util.mkEvent(true);
5 years ago
if (typeof data === 'undefined') {
if (/^http/.test(name) && /slide\/themes\/theme/.test(name)) {
Util.fetch(name, function (err, u8) {
if (err) { return; }
mediasData[name] = {
blobUrl: name,
content: u8,
name: name
};
var b = new Blob([u8], {type: "image/jpeg"});
var blobUrl = URL.createObjectURL(b);
return void callback(blobUrl);
});
return;
}
5 years ago
debug("CryptPad - could not find matching media for " + name);
return void callback("");
}
5 years ago
var blobUrl = (typeof mediasData[data.src] === 'undefined') ? "" : mediasData[data.src].blobUrl;
5 years ago
if (blobUrl) {
debug("CryptPad Image already loaded " + blobUrl);
return void callback(blobUrl);
}
APP.loadingImage++;
5 years ago
Util.fetch(data.src, function (err, u8) {
if (err) {
APP.loadingImage--;
console.error(err);
return void callback("");
}
5 years ago
try {
debug("Decrypt with key " + data.key);
FileCrypto.decrypt(u8, Nacl.util.decodeBase64(data.key), function (err, res) {
APP.loadingImage--;
5 years ago
if (err || !res.content) {
debug("Decrypting failed");
return void callback("");
}
try {
var blobUrl = URL.createObjectURL(res.content);
// store media blobUrl and content for cache and export
var mediaData = {
blobUrl : blobUrl,
content : "",
name: name
};
mediasData[data.src] = mediaData;
var reader = new FileReader();
reader.onloadend = function () {
debug("MediaData set");
mediaData.content = reader.result;
downloadImages[name].fire();
};
reader.readAsArrayBuffer(res.content);
debug("Adding CryptPad Image " + data.name + ": " + blobUrl);
window.frames[0].AscCommon.g_oDocumentUrls.addImageUrl(data.name, blobUrl);
callback(blobUrl);
} catch (e) {}
5 years ago
});
} catch (e) {
APP.loadingImage--;
5 years ago
debug("Exception decrypting image " + data.name);
console.error(e);
callback("");
}
}, void 0, common.getCache());
5 years ago
};
4 years ago
APP.docEditor = new window.DocsAPI.DocEditor("cp-app-oo-placeholder-a", APP.ooconfig);
ooLoaded = true;
makeChannel();
};
4 years ago
APP.printPdf = function (obj, cb) {
var bin = getContent();
x2tConvertData({
buffer: obj.data,
bin: bin
}, 'output.bin', 'pdf', function (xlsData) {
if (!xlsData) { return; }
var md = common.getMetadataMgr().getMetadataLazy();
var type = common.getMetadataMgr().getPrivateData().ooType;
var title = md.title || md.defaultTitle || type;
var blob = new Blob([xlsData], {type: "application/pdf"});
UI.removeModals();
cb();
saveAs(blob, APP.exportPdfName || title+'.pdf');
delete APP.exportPdfName;
4 years ago
});
};
var x2tSaveAndConvertData = function(data, filename, extension, finalFilename) {
var type = common.getMetadataMgr().getPrivateData().ooType;
var e = getEditor();
// PDF
if (type === "sheet" && extension === "pdf") {
var d = e.asc_nativePrint(undefined, undefined, 0x101).ImData;
x2tConvertData({
4 years ago
buffer: d.data,
bin: data
}, filename, extension, function (res) {
if (res) {
var _blob = new Blob([res], {type: "application/pdf;charset=utf-8"});
UI.removeModals();
saveAs(_blob, finalFilename);
}
});
return;
}
if (extension === "pdf") {
APP.exportPdfName = finalFilename;
return void e.asc_Print({});
}
x2tConvertData(data, filename, extension, function (xlsData) {
if (xlsData) {
var blob = new Blob([xlsData], {type: "application/bin;charset=utf-8"});
UI.removeModals();
saveAs(blob, finalFilename);
return;
}
UI.warn(Messages.error);
});
};
var exportXLSXFile = function() {
var text = getContent();
var suggestion = Title.suggestTitle(Title.defaultTitle);
var ext = ['.xlsx', '.ods', '.bin',
//'.csv', // XXX 4.11.0
'.pdf'];
var type = common.getMetadataMgr().getPrivateData().ooType;
var warning = '';
if (type==="presentation") {
ext = ['.pptx', '.odp', '.bin', '.pdf'];
} else if (type==="doc") {
ext = ['.docx', '.odt', '.bin', '.pdf'];
5 years ago
}
if (!supportsXLSX()) {
ext = ['.bin'];
warning = h('div.alert.alert-info.cp-alert-top', Messages.oo_conversionSupport);
}
var types = ext.map(function (val) {
return {
tag: 'a',
attributes: {
'data-value': val,
href: '#'
},
content: val
};
5 years ago
});
var dropdownConfig = {
text: ext[0], // Button initial text
caretDown: true,
options: types, // Entries displayed in the menu
isSelect: true,
initialValue: ext[0],
common: common
};
var $select = UIElements.createDropdown(dropdownConfig);
var promptMessage = h('span', [
Messages.exportPrompt,
warning
]);
UI.prompt(promptMessage, Util.fixFileName(suggestion), function (filename) {
// $select.getValue()
if (!(typeof(filename) === 'string' && filename)) { return; }
var ext = ($select.getValue() || '').slice(1);
if (ext === 'bin') {
var blob = new Blob([text], {type: "application/bin;charset=utf-8"});
saveAs(blob, filename+'.bin');
return;
}
var content = h('div.cp-oo-x2tXls', [
h('span.fa.fa-spin.fa-spinner'),
h('span', Messages.oo_exportInProgress)
]);
5 years ago
UI.openCustomModal(UI.dialog.customModal(content, {buttons: []}));
setTimeout(function () {
x2tSaveAndConvertData(text, "filename.bin", ext, filename+'.'+ext);
}, 100);
}, {
typeInput: $select[0]
}, true);
$select.find('button').addClass('btn');
};
var x2tImportImagesInternal = function(images, i, callback) {
5 years ago
if (i >= images.length) {
callback();
} else {
5 years ago
debug("Import image " + i);
var handleFileData = {
name: images[i].name,
5 years ago
mediasSources: getMediasSources(),
callback: function() {
debug("next image");
x2tImportImagesInternal(images, i+1, callback);
5 years ago
},
};
var fileData = images[i].data;
5 years ago
debug("Buffer");
debug(fileData.buffer);
var blob = new Blob([fileData.buffer], {type: 'image/png'});
blob.name = images[i].name;
5 years ago
APP.FMImages.handleFile(blob, handleFileData);
}
5 years ago
};
var x2tImportImages = function (images, callback) {
5 years ago
if (!APP.FMImages) {
var fmConfigImages = {
noHandlers: true,
noStore: true,
body: $('body'),
onUploaded: function (ev, data) {
if (!ev.callback) { return; }
debug("Image uploaded at " + data.url);
var parsed = Hash.parsePadUrl(data.url);
if (parsed.type === 'file') {
var secret = Hash.getSecrets('file', parsed.hash, data.password);
var fileHost = privateData.fileHost || privateData.origin;
var src = fileHost + Hash.getBlobPathFromHex(secret.channel);
var key = Hash.encodeBase64(secret.keys.cryptKey);
debug("Final src: " + src);
ev.mediasSources[ev.name] = { name : ev.name, src : src, key : key };
}
ev.callback();
}
};
APP.FMImages = common.createFileManager(fmConfigImages);
5 years ago
}
5 years ago
// Import Images
debug("Import Images");
debug(images);
x2tImportImagesInternal(images, 0, function() {
5 years ago
debug("Sync media sources elements");
debug(getMediasSources());
APP.onLocal();
debug("Import Images finalized");
callback();
});
};
5 years ago
var x2tImportData = function (data, filename, extension, callback) {
x2tConvertData(new Uint8Array(data), filename, extension, function (binData, images) {
if (!binData) { return void callback(); }
x2tImportImages(images, function() {
callback(binData);
});
5 years ago
});
5 years ago
};
var importFile = function(content) {
// Abort if there is another real user in the channel (history keeper excluded)
var m = metadataMgr.getChannelMembers().slice().filter(function (nId) {
return nId.length === 32;
});
if (m.length > 1) {
UI.removeModals();
return void UI.alert(Messages.oo_cantUpload);
}
5 years ago
if (!content) {
UI.removeModals();
return void UI.alert(Messages.oo_invalidFormat);
   }
var blob = new Blob([content], {type: 'plain/text'});
var file = getFileType();
blob.name = (metadataMgr.getMetadataLazy().title || file.doc) + '.' + file.type;
var uploadedCallback = function() {
UI.removeModals();
UI.confirm(Messages.oo_uploaded, function (yes) {
try {
getEditor().asc_setRestriction(true);
} catch (e) {}
if (!yes) { return; }
common.gotoURL();
});
};
var data = {
hash: ooChannel.lastHash,
index: ooChannel.cpIndex,
callback: uploadedCallback
};
APP.FM.handleFile(blob, data);
6 years ago
};
var importXLSXFile = function(content, filename, ext) {
5 years ago
// Perform the x2t conversion
debug("Filename");
debug(filename);
if (ext === "bin") {
return void importFile(content);
}
if (!supportsXLSX()) {
return void UI.alert(Messages.oo_invalidFormat);
}
var div = h('div.cp-oo-x2tXls', [
h('span.fa.fa-spin.fa-spinner'),
h('span', Messages.oo_importInProgress)
]);
5 years ago
UI.openCustomModal(UI.dialog.customModal(div, {buttons: []}));
setTimeout(function () {
x2tImportData(new Uint8Array(content), filename.name, "bin", function(c) {
if (!c) {
UI.removeModals();
return void UI.warn(Messages.error);
}
importFile(c);
});
}, 100);
5 years ago
};
var loadDocument = function (noCp, useNewDefault, i) {
if (ooLoaded) { return; }
var type = common.getMetadataMgr().getPrivateData().ooType;
var file = getFileType();
if (!noCp) {
var lastCp = getLastCp(false, i);
// If the last checkpoint is empty, load the "initial" doc instead
if (!lastCp || !lastCp.file) { return void loadDocument(true, useNewDefault); }
// Load latest checkpoint
return void loadLastDocument(lastCp, function () {
// Checkpoint error: load the previous one
i = i || 0;
loadDocument(noCp, useNewDefault, ++i);
});
}
var newText;
switch (type) {
case 'sheet' :
newText = EmptyCell(useNewDefault);
break;
case 'doc':
newText = EmptyDoc();
break;
case 'presentation':
newText = EmptySlide();
break;
default:
newText = '';
}
var blob = loadInitDocument(type, useNewDefault);
startOO(blob, file);
};
var initializing = true;
var $bar = $('#cp-toolbar');
config = {
patchTransformer: ChainPad.SmartJSONTransformer,
// cryptpad debug logging (default is 1)
// logLevel: 0,
validateContent: function (content) {
try {
JSON.parse(content);
return true;
} catch (e) {
5 years ago
debug("Failed to parse, rejecting patch");
return false;
}
}
};
var stringifyInner = function () {
var obj = {
content: content,
metadata: metadataMgr.getMetadataLazy()
};
// stringify the json and send it into chainpad
return stringify(obj);
};
var pinImages = function () {
if (content.mediasSources) {
var toPin = Object.keys(content.mediasSources || {}).map(function (id) {
var data = content.mediasSources[id] || {};
var src = data.src;
if (!src) { return; }
// Remove trailing slash
if (src.slice(-1) === '/') {
src = src.slice(0, -1);
}
// Extract the channel id from the source href
return src.slice(src.lastIndexOf('/') + 1);
}).filter(Boolean);
sframeChan.query('EV_OO_PIN_IMAGES', toPin);
}
};
var wasEditing = false;
var setStrictEditing = function () {
if (APP.isFast) { return; }
var editor = getEditor();
var editing = editor.asc_isDocumentModified ? editor.asc_isDocumentModified() : editor.isDocumentModified();
if (editing) {
evOnPatch.fire();
} else {
evOnSync.fire();
}
wasEditing = Boolean(editing);
};
APP.onFastChange = function (isFast) {
APP.isFast = isFast;
if (isFast) {
wasEditing = false;
if (APP.hasChangedInterval) {
4 years ago
window.clearInterval(APP.hasChangedInterval);
}
return;
}
setStrictEditing();
APP.hasChangedInterval = window.setInterval(setStrictEditing, 500);
};
APP.getContent = function () { return content; };
APP.onLocal = config.onLocal = function () {
if (initializing) { return; }
if (readOnly) { return; }
// Update metadata
var content = stringifyInner();
APP.realtime.contentUpdate(content);
pinImages();
};
var loadCp = function (cp, keepQueue) {
if (!isLockedModal.modal) {
isLockedModal.modal = UI.openCustomModal(isLockedModal.content);
}
loadLastDocument(cp, function () {
var file = getFileType();
var type = common.getMetadataMgr().getPrivateData().ooType;
var blob = loadInitDocument(type, true);
if (!keepQueue) { ooChannel.queue = []; }
resetData(blob, file);
}, function (blob, file) {
if (!keepQueue) { ooChannel.queue = []; }
resetData(blob, file);
});
};
var loadTemplate = function (href, pw, parsed) {
APP.history = true;
APP.template = true;
var editor = getEditor();
if (editor) {
try { getEditor().asc_setRestriction(true); } catch (e) {}
}
var _content = parsed.content;
// Get checkpoint
var hashes = _content.hashes || {};
var medias = _content.mediasSources;
var idx = sortCpIndex(hashes);
var lastIndex = idx[idx.length - 1];
var lastCp = hashes[lastIndex] || {};
// Current cp or initial hash (invalid hash ==> initial hash)
var toHash = lastCp.hash || 'NONE';
// Last hash
var fromHash = 'NONE';
content.mediasSources = medias;
sframeChan.query('Q_GET_HISTORY_RANGE', {
href: href,
password: pw,
channel: _content.channel,
lastKnownHash: fromHash,
toHash: toHash,
}, function (err, data) {
if (err) { return void console.error(err); }
if (!Array.isArray(data.messages)) { return void console.error('Not an array!'); }
// The first "cp" in history is the empty doc. It doesn't include the first patch
// of the history
var initialCp = !lastCp.hash;
var messages = (data.messages || []).slice(initialCp ? 0 : 1);
ooChannel.queue = messages.map(function (obj) {
return {
hash: obj.serverHash,
msg: JSON.parse(obj.msg)
};
});
ooChannel.historyLastHash = ooChannel.lastHash;
ooChannel.currentIndex = ooChannel.cpIndex;
loadCp(lastCp, true);
});
};
var openTemplatePicker = function () {
var metadataMgr = common.getMetadataMgr();
var type = metadataMgr.getPrivateData().app;
var sframeChan = common.getSframeChannel();
var pickerCfgInit = {
types: [type],
where: ['template'],
hidden: true
};
var pickerCfg = {
types: [type],
where: ['template'],
};
var onConfirm = function () {
common.openFilePicker(pickerCfg, function (data) {
if (data.type !== type) { return; }
UI.addLoadingScreen({hideTips: true});
sframeChan.query('Q_OO_TEMPLATE_USE', {
href: data.href,
}, function (err, val) {
var parsed;
try {
parsed = JSON.parse(val);
} catch (e) {
console.error(e, val);
UI.removeLoadingScreen();
return void UI.warn(Messages.error);
}
console.error(data);
loadTemplate(data.href, data.password, parsed);
});
});
};
sframeChan.query("Q_TEMPLATE_EXIST", type, function (err, data) {
if (data) {
common.openFilePicker(pickerCfgInit);
onConfirm();
} else {
UI.alert(Messages.template_empty);
}
});
};
sframeChan.on('EV_OOIFRAME_REFRESH', function (data) {
// We want to get the "bin" content of a sheet from its json in order to download
// something useful from a non-onlyoffice app (download from drive or settings).
// We don't want to initialize a full pad in async-store because we only need a
// static version, so we can use "openVersionHash" which is based on GET_HISTORY_RANGE
APP.isDownload = data.downloadId;
APP.downloadType = data.type;
downloadImages = {};
var json = data && data.json;
if (!json || !json.content) {
return void sframeChan.event('EV_OOIFRAME_DONE', '');
}
content = json.content;
readOnly = true;
var version = (!content.version || content.version === 1) ? 'v1/' :
(content.version <= 3 ? 'v2b/' : CURRENT_VERSION+'/');
var s = h('script', {
type:'text/javascript',
src: '/common/onlyoffice/'+version+'web-apps/apps/api/documents/api.js'
});
$('#cp-app-oo-editor').append(s);
var hashes = content.hashes || {};
var idx = sortCpIndex(hashes);
var lastIndex = idx[idx.length - 1];
// We're going to open using "openVersionHash" to avoid reimplementing existing code.
// To do so, we're using a version corresponding to the latest checkpoint with a
// minor version of 0. "openVersionHash" knows that it needs to give us the latest
// version when "APP.isDownload" is true.
var sheetVersion = lastIndex + '.0';
openVersionHash(sheetVersion);
});
config.onInit = function (info) {
var privateData = metadataMgr.getPrivateData();
metadataMgr.setDegraded(false); // FIXME degraded moded unsupported (no cursor channel)
readOnly = privateData.readOnly;
Title = common.createTitle({});
var configTb = {
displayed: ['pad'],
title: Title.getTitleConfig(),
metadataMgr: metadataMgr,
readOnly: readOnly,
realtime: info.realtime,
spinner: {
onPatch: evOnPatch,
onSync: evOnSync
},
sfCommon: common,
$container: $bar,
$contentContainer: $('#cp-app-oo-container')
};
toolbar = APP.toolbar = Toolbar.create(configTb);
4 years ago
toolbar.showColors();
Title.setToolbar(toolbar);
if (window.CP_DEV_MODE) {
var $save = common.createButton('save', true, {}, function () {
makeCheckpoint(true);
});
$save.appendTo(toolbar.$bottomM);
var $dlMedias = common.createButton('', true, {
name: 'dlmedias',
icon: 'fa-download',
}, function () {
require(['/bower_components/jszip/dist/jszip.min.js'], function (JsZip) {
var zip = new JsZip();
Object.keys(mediasData || {}).forEach(function (url) {
var obj = mediasData[url];
var b = new Blob([obj.content]);
zip.file(obj.name, b, {binary: true});
});
setTimeout(function () {
zip.generateAsync({type: 'blob'}).then(function (content) {
saveAs(content, 'media.zip');
});
}, 100);
});
}).attr('title', "Download medias");
$dlMedias.appendTo(toolbar.$bottomM);
}
if (!privateData.ooVersionHash) {
(function () {
/* add a history button */
var commit = function () {
// Wait for the checkpoint to be uploaded before leaving history mode
// (race condition). We use "stopHistory" to remove the history
// flag only when the checkpoint is ready.
APP.stopHistory = true;
makeCheckpoint(true);
};
var onPatch = function (patch) {
// Patch on the current cp
ooChannel.send(JSON.parse(patch.msg));
};
var onCheckpoint = function (cp) {
// We want to load a checkpoint:
loadCp(cp);
};
var setHistoryMode = function (bool) {
if (bool) {
APP.history = true;
try { getEditor().asc_setRestriction(true); } catch (e) {}
return;
}
// Cancel button: redraw from lastCp
APP.history = false;
ooChannel.queue = [];
ooChannel.ready = false;
// Fill the queue and then load the last CP
rtChannel.getHistory(function () {
var lastCp = getLastCp();
loadCp(lastCp, true);
});
};
var deleteSnapshot = function (hash) {
var md = Util.clone(cpNfInner.metadataMgr.getMetadata());
var snapshots = md.snapshots = md.snapshots || {};
delete snapshots[hash];
metadataMgr.updateMetadata(md);
APP.onLocal();
};
var makeSnapshot = function (title, cb, obj) {
var hash, time;
if (obj && obj.hash && obj.time) {
hash = obj.hash;
4 years ago
time = obj.time;
} else {
var major = Object.keys(content.hashes).length;
var cpIndex = getLastCp().index || 0;
var minor = ooChannel.cpIndex - cpIndex;
hash = major+'.'+minor;
time = +new Date();
}
var md = Util.clone(metadataMgr.getMetadata());
var snapshots = md.snapshots = md.snapshots || {};
if (snapshots[hash]) { cb('EEXISTS'); return void UI.warn(Messages.snapshot_error_exists); }
snapshots[hash] = {
title: title,
time: time
};
metadataMgr.updateMetadata(md);
APP.onLocal();
APP.realtime.onSettle(cb);
};
var loadSnapshot = function (hash) {
sframeChan.event('EV_OO_OPENVERSION', {
hash: hash
});
};
common.createButton('', true, {
name: 'history',
icon: 'fa-history',
text: Messages.historyText,
tippy: Messages.historyButton
}).click(function () {
ooChannel.historyLastHash = ooChannel.lastHash;
ooChannel.currentIndex = ooChannel.cpIndex;
Feedback.send('OO_HISTORY');
var histConfig = {
onPatch: onPatch,
onCheckpoint: onCheckpoint,
onRevert: commit,
setHistory: setHistoryMode,
makeSnapshot: makeSnapshot,
onlyoffice: {
hashes: content.hashes || {},
channel: content.channel,
lastHash: ooChannel.lastHash
},
$toolbar: $('.cp-toolbar-container')
};
History.create(common, histConfig);
}).appendTo(toolbar.$drawer);
// Snapshots
var $snapshot = common.createButton('snapshots', true, {
remove: deleteSnapshot,
make: makeSnapshot,
load: loadSnapshot
});
toolbar.$drawer.append($snapshot);
// Import template
var $template = common.createButton('importtemplate', true, {}, openTemplatePicker);
if ($template && typeof($template.appendTo) === 'function') {
$template.appendTo(toolbar.$drawer);
}
// Save as template
if (!metadataMgr.getPrivateData().isTemplate) {
var templateObj = {
//rt: cpNfInner.chainpad,
getTitle: function () { return cpNfInner.metadataMgr.getMetadata().title; },
callback: function (title) {
var newContent = {};
newContent.mediasSources = content.mediasSources;
var text = getContent();
var blob = new Blob([text], {type: 'plain/text'});
var file = getFileType();
blob.name = title || (metadataMgr.getMetadataLazy().title || file.doc) + '.' + file.type;
var data = {
newTemplate: newContent,
title: title
};
APP.FM.handleFile(blob, data);
}
};
var $templateButton = common.createButton('template', true, templateObj);
toolbar.$drawer.append($templateButton);
}
})();
}
if (window.CP_DEV_MODE || DISPLAY_RESTORE_BUTTON) {
common.createButton('', true, {
name: 'delete',
icon: 'fa-trash',
hiddenReadOnly: true
}).click(function () {
if (initializing) { return void console.error('initializing'); }
deleteLastCp();
}).attr('title', 'Delete last checkpoint').appendTo(toolbar.$bottomM);
common.createButton('', true, {
name: 'restore',
icon: 'fa-history',
hiddenReadOnly: true
}).click(function () {
if (initializing) { return void console.error('initializing'); }
restoreLastCp();
}).attr('title', 'Restore last checkpoint').appendTo(toolbar.$bottomM);
}
var $exportXLSX = common.createButton('export', true, {}, exportXLSXFile);
$exportXLSX.appendTo(toolbar.$drawer);
var type = privateData.ooType;
var accept = [".bin", ".ods", ".xlsx"];
if (type === "presentation") {
accept = ['.bin', '.odp', '.pptx'];
} else if (type === "doc") {
accept = ['.bin', '.odt', '.docx'];
}
var first;
if (!supportsXLSX()) {
accept = ['.bin'];
first = function (cb) {
var msg = h('span', [
Messages.oo_conversionSupport,
' ', h('span', Messages.oo_importBin),
]);
UI.confirm(msg, function (yes) {
if (yes) {
cb();
}
});
};
}
if (common.isLoggedIn()) {
window.CryptPad_deleteLastCp = deleteLastCp;
var $importXLSX = common.createButton('import', true, {
accept: accept,
binary : ["ods", "xlsx", "odt", "docx", "odp", "pptx"],
first: first,
}, importXLSXFile);
$importXLSX.appendTo(toolbar.$drawer);
common.createButton('hashtag', true).appendTo(toolbar.$drawer);
}
var $store = common.createButton('storeindrive', true);
toolbar.$drawer.append($store);
var $forget = common.createButton('forget', true, {}, function (err) {
if (err) { return; }
setEditable(false);
});
toolbar.$drawer.append($forget);
if (!privateData.isEmbed) {
var helpMenu = APP.helpMenu = common.createHelpMenu(['beta', 'oo']);
$('#cp-app-oo-editor').prepend(common.getBurnAfterReadingWarning());
$('#cp-app-oo-editor').prepend(helpMenu.menu);
toolbar.$drawer.append(helpMenu.button);
}
var $properties = common.createButton('properties', true);
toolbar.$drawer.append($properties);
};
var noCache = false; // Prevent reload loops
var onCorruptedCache = function () {
if (noCache) {
UI.errorLoadingScreen(Messages.unableToDisplay, false, function () {
common.gotoURL('');
});
}
noCache = true;
var sframeChan = common.getSframeChannel();
sframeChan.event("EV_CORRUPTED_CACHE");
};
var firstReady = true;
config.onReady = function (info) {
if (APP.realtime !== info.realtime) {
APP.realtime = info.realtime;
}
var userDoc = APP.realtime.getUserDoc();
var isNew = false;
var newDoc = true;
if (userDoc === "" || userDoc === "{}") { isNew = true; }
if (userDoc !== "") {
var hjson = JSON.parse(userDoc);
if (hjson && hjson.metadata) {
metadataMgr.updateMetadata(hjson.metadata);
}
if (typeof (hjson) !== 'object' || Array.isArray(hjson) ||
(hjson.metadata && typeof(hjson.metadata.type) !== 'undefined' &&
hjson.metadata.type !== 'oo')) {
var errorText = Messages.typeError;
UI.errorLoadingScreen(errorText);
throw new Error(errorText);
}
content = hjson.content || content;
var newLatest = getLastCp();
sframeChan.query('Q_OO_SAVE', {
hash: newLatest.hash,
url: newLatest.file
}, function () { });
newDoc = !content.hashes || Object.keys(content.hashes).length === 0;
} else if (!privateData.isNewFile) {
// This is an empty doc but not a new file: error
onCorruptedCache();
return void console.error("Empty chainpad for a non-empty doc");
} else {
Title.updateTitle(Title.defaultTitle);
}
APP.startNew = isNew;
var version = CURRENT_VERSION + '/';
4 years ago
var msg;
5 years ago
// Old version detected: use the old OO and start the migration if we can
if (privateData.ooForceVersion) {
if (privateData.ooForceVersion === "1") {
version = "v1/";
}
} else if (content && (!content.version || content.version === 1)) {
5 years ago
version = 'v1/';
APP.migrate = true;
// Registedred ~~users~~ editors can start the migration
if (common.isLoggedIn() && !readOnly) {
5 years ago
content.migration = true;
APP.onLocal();
} else {
msg = h('div.alert.alert-warning.cp-burn-after-reading', Messages.oo_sheetMigration_anonymousEditor);
if (APP.helpMenu) {
$(APP.helpMenu.menu).after(msg);
} else {
$('#cp-app-oo-editor').prepend(msg);
}
4 years ago
readOnly = true;
}
} else if (content && content.version <= 3) { // V2 or V3
version = 'v2b/';
4 years ago
APP.migrate = true;
// Registedred ~~users~~ editors can start the migration
if (common.isLoggedIn() && !readOnly) {
4 years ago
content.migration = true;
APP.onLocal();
} else {
4 years ago
msg = h('div.alert.alert-warning.cp-burn-after-reading', Messages.oo_sheetMigration_anonymousEditor);
if (APP.helpMenu) {
$(APP.helpMenu.menu).after(msg);
} else {
$('#cp-app-oo-editor').prepend(msg);
}
5 years ago
readOnly = true;
}
}
// NOTE: don't forget to also update the version in 'EV_OOIFRAME_REFRESH'
5 years ago
// If the sheet is locked by an offline user, remove it
if (content && content.saveLock && !isUserOnline(content.saveLock)) {
content.saveLock = undefined;
APP.onLocal();
} else if (content && content.saveLock) {
// If someone is currently creating a checkpoint (and locking the sheet),
// make sure it will end (maybe you'll have to make the checkpoint yourself)
checkCheckpoint();
}
5 years ago
var s = h('script', {
type:'text/javascript',
src: '/common/onlyoffice/'+version+'web-apps/apps/api/documents/api.js'
});
$('#cp-app-oo-editor').append(s);
if (metadataMgr.getPrivateData().burnAfterReading && content && content.channel) {
sframeChan.event('EV_BURN_PAD', content.channel);
}
if (privateData.ooVersionHash) {
return void openVersionHash(privateData.ooVersionHash);
}
// Only execute the following code the first time we call onReady
if (!firstReady) {
setMyId();
oldHashes = JSON.parse(JSON.stringify(content.hashes));
initializing = false;
return void setEditable(!readOnly);
}
firstReady = false;
var useNewDefault = content.version && content.version >= 2;
openRtChannel(Util.once(function () {
setMyId();
oldHashes = JSON.parse(JSON.stringify(content.hashes));
initializing = false;
// If we have more than CHECKPOINT_INTERVAL patches in the initial history
// and we're the only editor in the pad, make a checkpoint
var v = metadataMgr.getViewers();
var m = metadataMgr.getChannelMembers().filter(function (str) {
return str.length === 32;
}).length;
if ((m - v) === 1 && !readOnly) {
var needCp = ooChannel.queue.length > CHECKPOINT_INTERVAL;
APP.initCheckpoint = needCp;
}
common.openPadChat(APP.onLocal);
4 years ago
if (!readOnly) {
var cursors = {};
4 years ago
common.openCursorChannel(APP.onLocal);
cursor = common.createCursor(APP.onLocal);
cursor.onCursorUpdate(function (data) {
// Leaving user
if (data && data.leave && data.id) {
// When a netflux user leaves, remove all their cursors
Object.keys(cursors).forEach(function (ooid) {
var d = cursors[ooid];
if (d !== data.id) { return; } // Only continue for the leaving user
// Remove from OO UI
ooChannel.send({
type: "cursor",
messages: [{
cursor: "10;AgAAADIAAAAAAA==",
time: +new Date(),
user: ooid,
useridoriginal: String(ooid).slice(0,-1),
}]
});
// Remove from memory
delete cursors[ooid];
});
handleNewIds({}, content.ids);
}
// Cursor update
4 years ago
if (!data || !data.cursor) { return; }
// Store the new cursor in memory for this user, with their netflux ID
4 years ago
var ooid = Util.find(data.cursor, ['messages', 0, 'user']);
if (ooid) { cursors[ooid] = data.id.slice(0,32); }
// Update cursor in the UI
4 years ago
ooChannel.send(data.cursor);
});
}
if (APP.startWithTemplate) {
var template = APP.startWithTemplate;
loadTemplate(template.href, template.password, template.content);
return;
}
var next = function () {
loadDocument(newDoc, useNewDefault);
setEditable(!readOnly);
UI.removeLoadingScreen();
};
if (privateData.isNewFile && privateData.fromFileData) {
try {
(function () {
var data = privateData.fromFileData;
var type = data.fileType;
var title = data.title;
// Fix extension if the file was renamed
if (Util.isSpreadsheet(type) && !Util.isSpreadsheet(data.title)) {
if (type === 'application/vnd.oasis.opendocument.spreadsheet') {
data.title += '.ods';
}
if (type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
data.title += '.xlsx';
}
}
if (Util.isOfficeDoc(type) && !Util.isOfficeDoc(data.title)) {
if (type === 'application/vnd.oasis.opendocument.text') {
data.title += '.odt';
}
if (type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
data.title += '.docx';
}
}
if (Util.isPresentation(type) && !Util.isPresentation(data.title)) {
if (type === 'application/vnd.oasis.opendocument.presentation') {
data.title += '.odp';
}
if (type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation') {
data.title += '.pptx';
}
}
var href = data.href;
var password = data.password;
var parsed = Hash.parsePadUrl(href);
var secret = Hash.getSecrets('file', parsed.hash, password);
var hexFileName = secret.channel;
var fileHost = privateData.fileHost || privateData.origin;
var src = fileHost + Hash.getBlobPathFromHex(hexFileName);
var key = secret.keys && secret.keys.cryptKey;
var xhr = new XMLHttpRequest();
xhr.open('GET', src, true);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
if (/^4/.test('' + this.status)) {
// fallback to empty sheet
console.error(this.status);
return void next();
}
var arrayBuffer = xhr.response;
if (arrayBuffer) {
var u8 = new Uint8Array(arrayBuffer);
FileCrypto.decrypt(u8, key, function (err, decrypted) {
if (err) {
// fallback to empty sheet
console.error(err);
return void next();
}
var blobXlsx = decrypted.content;
new Response(blobXlsx).arrayBuffer().then(function (buffer) {
var u8Xlsx = new Uint8Array(buffer);
x2tImportData(u8Xlsx, data.title, 'bin', function (bin) {
if (!bin) {
return void UI.errorLoadingScreen(Messages.error);
}
var blob = new Blob([bin], {type: 'text/plain'});
saveToServer(blob, data.title);
Title.updateTitle(title);
UI.removeLoadingScreen();
});
});
});
}
};
xhr.onerror = function (err) {
// fallback to empty sheet
console.error(err);
next();
};
xhr.send(null);
})();
} catch (e) {
console.error(e);
next();
}
return;
}
next();
}));
};
config.onError = function (err) {
common.onServerError(err, toolbar, function () {
setEditable(false);
});
};
5 years ago
var reloadPopup = false;
var checkNewCheckpoint = function () {
if (!isLockedModal.modal) {
isLockedModal.modal = UI.openCustomModal(isLockedModal.content);
}
var lastCp = getLastCp();
4 years ago
loadLastDocument(lastCp, function (err) {
console.error(err);
// On error, do nothing
// FIXME lock the document or ask for a page reload?
}, function (blob, type) {
resetData(blob, type);
});
};
config.onRemote = function () {
if (initializing) { return; }
var userDoc = APP.realtime.getUserDoc();
var json = JSON.parse(userDoc);
if (json.metadata) {
metadataMgr.updateMetadata(json.metadata);
}
5 years ago
var wasLocked = content.saveLock;
5 years ago
var wasMigrating = content.migration;
var myLocks = getUserLock(getId(), true);
content = json.content;
if (content.saveLock && wasLocked !== content.saveLock) {
// Someone new is creating a checkpoint: fix the sheets ids
fixSheets();
// If the checkpoint is not saved in 20s to 40s, do it ourselves
checkCheckpoint();
}
var editor = getEditor();
if (content.hashes) {
var latest = getLastCp(true);
var newLatest = getLastCp();
if (newLatest.index > latest.index || (newLatest.index && !latest.index)) {
ooChannel.queue = [];
ooChannel.ready = false;
var reload = function () {
// New checkpoint
sframeChan.query('Q_OO_SAVE', {
hash: newLatest.hash,
url: newLatest.file
}, function () {
checkNewCheckpoint();
});
};
var editing = editor.asc_isDocumentModified ? editor.asc_isDocumentModified() : editor.isDocumentModify;
if (editing) {
setEditable(false);
APP.unsavedLocks = myLocks;
APP.onStrictSaveChanges = function () {
reload();
delete APP.onStrictSaveChanges;
};
editor.asc_Save();
} else {
reload();
}
}
oldHashes = JSON.parse(JSON.stringify(content.hashes));
}
if (content.ids) {
handleNewIds(oldIds, content.ids);
oldIds = JSON.parse(JSON.stringify(content.ids));
}
if (content.locks) {
handleNewLocks(oldLocks, content.locks);
oldLocks = JSON.parse(JSON.stringify(content.locks));
}
5 years ago
if (content.migration) {
setEditable(false);
}
if (wasMigrating && !content.migration && !reloadPopup) {
reloadPopup = true;
UI.alert(Messages.oo_sheetMigration_complete, function () {
5 years ago
common.gotoURL();
});
}
pinImages();
};
config.onConnectionChange = function (info) {
if (info.state) {
// If we tried to send changes while we were offline, force a page reload
UIElements.reconnectAlert();
if (Object.keys(pendingChanges).length) {
return void UI.confirm(Messages.oo_reconnect, function (yes) {
if (!yes) { return; }
common.gotoURL();
});
}
//setEditable(true);
try { getEditor().asc_setViewMode(false); } catch (e) {}
offline = false;
} else {
try { getEditor().asc_setViewMode(true); } catch (e) {}
//setEditable(false);
offline = true;
UI.findOKButton().click();
UIElements.disconnectAlert();
}
};
cpNfInner = common.startRealtime(config);
cpNfInner.onInfiniteSpinner(function () {
if (offline) { return; }
setEditable(false);
UI.confirm(Messages.realtime_unrecoverableError, function (yes) {
if (!yes) { return; }
common.gotoURL();
});
});
common.onLogout(function () { setEditable(false); });
};
var main = function () {
var common;
nThen(function (waitFor) {
$(waitFor(function () {
UI.addLoadingScreen();
}));
SFCommon.create(waitFor(function (c) { APP.common = common = c; }));
}).nThen(function (waitFor) {
common.getSframeChannel().on('EV_OO_TEMPLATE', function (data) {
APP.startWithTemplate = data;
});
common.handleNewFile(waitFor, {
//noTemplates: true
});
}).nThen(function (/*waitFor*/) {
andThen(common);
});
};
main();
});