|
|
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',
|
|
|
|
|
|
'/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)
|
|
|
{
|
|
|
var saveAs = window.saveAs;
|
|
|
var Nacl = window.nacl;
|
|
|
|
|
|
var APP = window.APP = {
|
|
|
$: $,
|
|
|
urlArgs: Util.find(ApiConfig, ['requireConf', 'urlArgs'])
|
|
|
};
|
|
|
|
|
|
var CHECKPOINT_INTERVAL = 100;
|
|
|
var DISPLAY_RESTORE_BUTTON = false;
|
|
|
var NEW_VERSION = 4;
|
|
|
var PENDING_TIMEOUT = 30000;
|
|
|
var CURRENT_VERSION = 'v4';
|
|
|
//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 supportsXLSX = function () {
|
|
|
return !(typeof(Atomics) === "undefined" || typeof (SharedArrayBuffer) === "undefined");
|
|
|
};
|
|
|
|
|
|
|
|
|
var toolbar;
|
|
|
|
|
|
|
|
|
var andThen = function (common) {
|
|
|
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: {},
|
|
|
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 = {};
|
|
|
|
|
|
var startOO = function () {};
|
|
|
|
|
|
var getMediasSources = APP.getMediasSources = function() {
|
|
|
content.mediasSources = content.mediasSources || {};
|
|
|
return content.mediasSources;
|
|
|
};
|
|
|
|
|
|
var getId = function () {
|
|
|
return metadataMgr.getNetfluxId() + '-' + privateData.clientId;
|
|
|
};
|
|
|
|
|
|
var getEditor = function () {
|
|
|
if (!window.frames || !window.frames[0]) { return; }
|
|
|
return window.frames[0].editor || window.frames[0].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();
|
|
|
// f: function used in .some(f) but defined outside of the while
|
|
|
var f = function (id) {
|
|
|
return ids[id] === myOOId;
|
|
|
};
|
|
|
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) {
|
|
|
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;
|
|
|
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';
|
|
|
file.title = title + '.pptx' || 'presentation.pptx';
|
|
|
file.doc = 'presentation';
|
|
|
break;
|
|
|
}
|
|
|
return file;
|
|
|
};
|
|
|
|
|
|
var now = function () { return +new Date(); };
|
|
|
|
|
|
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;
|
|
|
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;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/*
|
|
|
var checkDrawings = function () {
|
|
|
var editor = getEditor();
|
|
|
if (!editor || !editor.GetSheets) { return false; }
|
|
|
var s = editor.GetSheets();
|
|
|
return s.some(function (obj) {
|
|
|
return obj.worksheet.Drawings.length;
|
|
|
});
|
|
|
};
|
|
|
*/
|
|
|
|
|
|
// 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 () {
|
|
|
// 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
|
|
|
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);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
var onUploaded = function (ev, data, err) {
|
|
|
content.saveLock = undefined;
|
|
|
if (err) {
|
|
|
console.error(err);
|
|
|
return void UI.alert(Messages.oo_saveError);
|
|
|
}
|
|
|
// Get the last cp idx
|
|
|
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
|
|
|
};
|
|
|
oldHashes = JSON.parse(JSON.stringify(content.hashes));
|
|
|
content.locks = {};
|
|
|
content.ids = {};
|
|
|
// 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 () {
|
|
|
if (APP.migrate) {
|
|
|
UI.removeModals();
|
|
|
UI.alert(Messages.oo_sheetMigration_complete, function () {
|
|
|
common.gotoURL();
|
|
|
});
|
|
|
return;
|
|
|
}
|
|
|
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);
|
|
|
|
|
|
// 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 resetData = function (blob, type) {
|
|
|
// If a read-only refresh popup was planned, abort it
|
|
|
delete APP.refreshPopup;
|
|
|
clearTimeout(APP.refreshRoTo);
|
|
|
|
|
|
if (!isLockedModal.modal) {
|
|
|
isLockedModal.modal = UI.openCustomModal(isLockedModal.content);
|
|
|
}
|
|
|
myUniqueOOId = undefined;
|
|
|
setMyId();
|
|
|
if (APP.docEditor) { APP.docEditor.destroyEditor(); } // Kill the old editor
|
|
|
$('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 () {
|
|
|
var text = getContent();
|
|
|
var blob = new Blob([text], {type: 'plain/text'});
|
|
|
var file = getFileType();
|
|
|
blob.name = (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();
|
|
|
|
|
|
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) {
|
|
|
var locked = content.saveLock;
|
|
|
var lastCp = getLastCp();
|
|
|
|
|
|
var needCp = force || ooChannel.cpIndex % CHECKPOINT_INTERVAL === 0 ||
|
|
|
(ooChannel.cpIndex - (lastCp.index || 0)) > 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; }
|
|
|
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);
|
|
|
};
|
|
|
|
|
|
var loadInitDocument = function (type, useNewDefault) {
|
|
|
var newText;
|
|
|
switch (type) {
|
|
|
case 'sheet' :
|
|
|
newText = EmptyCell(useNewDefault);
|
|
|
break;
|
|
|
case 'doc':
|
|
|
newText = EmptyDoc();
|
|
|
break;
|
|
|
case 'presentation':
|
|
|
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');
|
|
|
}
|
|
|
ooChannel.cpIndex = lastCp.index || 0;
|
|
|
ooChannel.lastHash = lastCp.hash;
|
|
|
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)) {
|
|
|
onCpError(this.status);
|
|
|
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) { return void console.error(err); }
|
|
|
var blob = new Blob([decrypted.content], {type: 'plain/text'});
|
|
|
if (cb) {
|
|
|
return cb(blob, getFileType());
|
|
|
}
|
|
|
startOO(blob, getFileType());
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
xhr.onerror = function (err) {
|
|
|
onCpError(err);
|
|
|
};
|
|
|
xhr.send(null);
|
|
|
};
|
|
|
|
|
|
/*
|
|
|
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();
|
|
|
});
|
|
|
};
|
|
|
*/
|
|
|
|
|
|
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;
|
|
|
|
|
|
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,
|
|
|
}, 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;
|
|
|
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) {
|
|
|
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;
|
|
|
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)
|
|
|
};
|
|
|
};
|
|
|
|
|
|
// Get all existing locks
|
|
|
var getUserLock = function (id) {
|
|
|
var l = content.locks[id] || {};
|
|
|
return Object.keys(l).map(function (uid) { return l[uid]; });
|
|
|
};
|
|
|
var getLock = function () {
|
|
|
var locks = [];
|
|
|
Object.keys(content.locks).forEach(function (id) {
|
|
|
Array.prototype.push.apply(locks, getUserLock(id));
|
|
|
});
|
|
|
return locks;
|
|
|
};
|
|
|
|
|
|
// 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()
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 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]];
|
|
|
ooChannel.send({
|
|
|
type: "releaseLock",
|
|
|
locks: l
|
|
|
});
|
|
|
delete content.locks[id];
|
|
|
}
|
|
|
});
|
|
|
if (content.saveLock && !isUserOnline(content.saveLock)) {
|
|
|
delete content.saveLock;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
var handleAuth = function (obj, send) {
|
|
|
//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);
|
|
|
}
|
|
|
send({
|
|
|
type: "authChanges",
|
|
|
changes: changes
|
|
|
});
|
|
|
|
|
|
// 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;
|
|
|
}
|
|
|
content.locks = content.locks || {};
|
|
|
// Send the lock to other users
|
|
|
var msg = {
|
|
|
time: now(),
|
|
|
user: myUniqueOOId,
|
|
|
block: obj.block && obj.block[0],
|
|
|
};
|
|
|
var myId = getId();
|
|
|
content.locks[myId] = content.locks[myId] || {};
|
|
|
var uid = Util.uid();
|
|
|
content.locks[myId][uid] = 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();
|
|
|
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) {
|
|
|
try {
|
|
|
changes = JSON.parse(changes);
|
|
|
} catch (e) {
|
|
|
return [];
|
|
|
}
|
|
|
return changes.map(function (change) {
|
|
|
return {
|
|
|
docid: "fresh",
|
|
|
change: '"' + change + '"',
|
|
|
time: now(),
|
|
|
user: myUniqueOOId,
|
|
|
useridoriginal: String(myOOId)
|
|
|
};
|
|
|
});
|
|
|
};
|
|
|
|
|
|
var handleChanges = function (obj, send) {
|
|
|
// 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;
|
|
|
}
|
|
|
|
|
|
// Send the changes
|
|
|
content.locks = content.locks || {};
|
|
|
rtChannel.sendMsg({
|
|
|
type: "saveChanges",
|
|
|
changes: parseChanges(obj.changes),
|
|
|
changesIndex: ooChannel.cpIndex || 0,
|
|
|
locks: getUserLock(getId()),
|
|
|
excelAdditionalInfo: obj.excelAdditionalInfo
|
|
|
}, null, function (err, hash) {
|
|
|
if (err) {
|
|
|
return void console.error(err);
|
|
|
}
|
|
|
|
|
|
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();
|
|
|
});
|
|
|
};
|
|
|
|
|
|
|
|
|
var makeChannel = function () {
|
|
|
var msgEv = Util.mkEvent();
|
|
|
var iframe = $('#cp-app-oo-editor > iframe')[0].contentWindow;
|
|
|
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) {
|
|
|
if (APP.onStrictSaveChanges && !force) { return; } // can't push to OO before reloading cp
|
|
|
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;
|
|
|
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) {
|
|
|
APP.unsavedChanges = {
|
|
|
type: "saveChanges",
|
|
|
changes: parseChanges(obj.changes),
|
|
|
changesIndex: ooChannel.cpIndex || 0,
|
|
|
locks: getUserLock(getId()),
|
|
|
excelAdditionalInfo: null
|
|
|
};
|
|
|
APP.onStrictSaveChanges();
|
|
|
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 {
|
|
|
var docs = window.frames[0].AscCommon.g_oDocumentUrls.urls || {};
|
|
|
var mediasSources = getMediasSources();
|
|
|
Object.keys(mediasSources).forEach(function (name) {
|
|
|
if (!docs['media/'+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;
|
|
|
}
|
|
|
});
|
|
|
});
|
|
|
};
|
|
|
|
|
|
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);
|
|
|
|
|
|
// Starting from version 3, we can use the view mode again
|
|
|
// defined but never used
|
|
|
//var mode = (content && content.version > 2 && lock) ? "view" : "edit";
|
|
|
|
|
|
// 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": (window.cryptpadLanguage || navigator.language || navigator.userLanguage || '').slice(0,2)
|
|
|
},
|
|
|
"events": {
|
|
|
"onAppReady": function(/*evt*/) {
|
|
|
var $iframe = $('iframe[name="frameEditor"]').contents();
|
|
|
$iframe.prop('tabindex', '-1');
|
|
|
var $tb = $iframe.find('head');
|
|
|
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; }' +
|
|
|
'#title-doc-name { display: none !important; }' +
|
|
|
'#title-user-name { display: none !important; }' +
|
|
|
(supportsXLSX() ? '' : '#slot-btn-dt-print { display: none !important; }') +
|
|
|
// 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
|
|
|
'#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
|
|
|
|
|
|
'#asc-gen257 { display: none !important; }' + // Insert image from url
|
|
|
'#asc-gen1839 { display: none !important; }' + // Image context menu: replace with url
|
|
|
'#asc-gen5883 { display: none !important; }' + // Rightside image menu: replace with url
|
|
|
|
|
|
'#asc-gen1211 { display: none !important; }' + // Slide Image context menu: replace with url
|
|
|
'#asc-gen3880 { display: none !important; }' + // Rightside slide image menu: replace with url
|
|
|
'#asc-gen2218 { display: none !important; }' + // Rightside slide menu: fill slide with image url
|
|
|
'#asc-gen849 { display: none !important; }' + // Toolbar slide: insert image from url
|
|
|
'#asc-gen857 { display: none !important; }' + // Toolbar slide: insert image from url (insert tab)
|
|
|
|
|
|
'#asc-gen180 { display: none !important; }' + // Doc Insert image from url
|
|
|
'#asc-gen1760 { display: none !important; }' + // Doc Image context menu: replace with url
|
|
|
'#asc-gen3319 { display: none !important; }' + // Doc Rightside image menu: replace with url
|
|
|
|
|
|
'#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(); });
|
|
|
});
|
|
|
}
|
|
|
},
|
|
|
"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)
|
|
|
]);
|
|
|
UI.openCustomModal(UI.dialog.customModal(div, {buttons: []}));
|
|
|
makeCheckpoint(true);
|
|
|
});
|
|
|
// 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;
|
|
|
ooChannel.queue.forEach(function (data) {
|
|
|
Array.prototype.push.apply(changes, data.msg.changes);
|
|
|
changesIndex = data.msg.changesIndex;
|
|
|
//ooChannel.send(data.msg);
|
|
|
});
|
|
|
ooChannel.cpIndex += ooChannel.queue.length;
|
|
|
var last = ooChannel.queue.pop();
|
|
|
if (last) { ooChannel.lastHash = last.hash; }
|
|
|
|
|
|
var onDocUnlock = function () {
|
|
|
// Migration required but read-only: continue...
|
|
|
if (readOnly) {
|
|
|
setEditable(true);
|
|
|
getEditor().setViewModeDisconnect();
|
|
|
} 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;
|
|
|
}
|
|
|
onDocUnlock();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (lock) {
|
|
|
getEditor().setViewModeDisconnect();
|
|
|
} else if (readOnly) {
|
|
|
try {
|
|
|
getEditor().asc_setRestriction(true);
|
|
|
} catch (e) {}
|
|
|
} else {
|
|
|
setEditable(true);
|
|
|
deleteOfflineLocks();
|
|
|
handleNewLocks({}, content.locks);
|
|
|
if (APP.unsavedChanges) {
|
|
|
var unsaved = APP.unsavedChanges;
|
|
|
delete APP.unsavedChanges;
|
|
|
rtChannel.sendMsg(unsaved, null, function (err, hash) {
|
|
|
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 (isLockedModal.modal && force) {
|
|
|
isLockedModal.modal.closeModal();
|
|
|
delete isLockedModal.modal;
|
|
|
if (!APP.history) {
|
|
|
$('#cp-app-oo-editor > iframe')[0].contentWindow.focus();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (APP.template) {
|
|
|
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 (APP.migrate && !readOnly) {
|
|
|
onMigrateRdy.fire();
|
|
|
}
|
|
|
|
|
|
// 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(); }
|
|
|
};
|
|
|
*/
|
|
|
|
|
|
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) {
|
|
|
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;
|
|
|
}
|
|
|
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
|
|
|
});
|
|
|
});
|
|
|
});
|
|
|
});
|
|
|
};
|
|
|
|
|
|
APP.openURL = function (url) {
|
|
|
common.openUnsafeURL(url);
|
|
|
};
|
|
|
|
|
|
APP.loadingImage = 0;
|
|
|
APP.getImageURL = function(name, callback) {
|
|
|
if (name && /^data:image/.test(name)) {
|
|
|
return void callback('');
|
|
|
var b = Util.dataURIToBlob(name);
|
|
|
var url = URL.createObjectURL(blob);
|
|
|
return void callback(url);
|
|
|
}
|
|
|
|
|
|
var mediasSources = getMediasSources();
|
|
|
var data = mediasSources[name];
|
|
|
|
|
|
if (typeof data === 'undefined') {
|
|
|
debug("CryptPad - could not find matching media for " + name);
|
|
|
return void callback("");
|
|
|
}
|
|
|
|
|
|
var blobUrl = (typeof mediasData[data.src] === 'undefined') ? "" : mediasData[data.src].src;
|
|
|
if (blobUrl) {
|
|
|
debug("CryptPad Image already loaded " + blobUrl);
|
|
|
return void callback(blobUrl);
|
|
|
}
|
|
|
|
|
|
APP.loadingImage++;
|
|
|
Util.fetch(data.src, function (err, u8) {
|
|
|
if (err) {
|
|
|
APP.loadingImage--;
|
|
|
console.error(err);
|
|
|
return void callback("");
|
|
|
}
|
|
|
try {
|
|
|
debug("Decrypt with key " + data.key);
|
|
|
FileCrypto.decrypt(u8, Nacl.util.decodeBase64(data.key), function (err, res) {
|
|
|
APP.loadingImage--;
|
|
|
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 : "" };
|
|
|
mediasData[data.src] = mediaData;
|
|
|
var reader = new FileReader();
|
|
|
reader.onloadend = function () {
|
|
|
debug("MediaData set");
|
|
|
mediaData.content = reader.result;
|
|
|
};
|
|
|
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) {}
|
|
|
});
|
|
|
} catch (e) {
|
|
|
APP.loadingImage--;
|
|
|
debug("Exception decrypting image " + data.name);
|
|
|
console.error(e);
|
|
|
callback("");
|
|
|
}
|
|
|
}, void 0, common.getCache());
|
|
|
};
|
|
|
|
|
|
APP.docEditor = new window.DocsAPI.DocEditor("cp-app-oo-placeholder-a", APP.ooconfig);
|
|
|
ooLoaded = true;
|
|
|
makeChannel();
|
|
|
};
|
|
|
|
|
|
var x2tReady = Util.mkEvent(true);
|
|
|
var fetchFonts = function (x2t) {
|
|
|
var path = '/common/onlyoffice/'+CURRENT_VERSION+'/fonts/';
|
|
|
var e = getEditor();
|
|
|
var fonts = e.FontLoader.fontInfos;
|
|
|
var files = e.FontLoader.fontFiles;
|
|
|
var suffixes = {
|
|
|
indexR: '',
|
|
|
indexB: '_Bold',
|
|
|
indexBI: '_Bold_Italic',
|
|
|
indexI: '_Italic',
|
|
|
};
|
|
|
nThen(function (waitFor) {
|
|
|
fonts.forEach(function (font) {
|
|
|
// Check if the font is already loaded
|
|
|
if (!font.NeedStyles) { return; }
|
|
|
// Pick the variants we need (regular, bold, italic)
|
|
|
['indexR', 'indexB', 'indexI', 'indexBI'].forEach(function (k) {
|
|
|
if (typeof(font[k]) !== "number" || font[k] === -1) { return; } // No matching file
|
|
|
var file = files[font[k]];
|
|
|
|
|
|
var name = font.Name + suffixes[k] + '.ttf';
|
|
|
Util.fetch(path + file.Id, waitFor(function (err, buffer) {
|
|
|
if (buffer) {
|
|
|
x2t.FS.writeFile('/working/fonts/' + name, buffer);
|
|
|
}
|
|
|
}));
|
|
|
});
|
|
|
});
|
|
|
}).nThen(function () {
|
|
|
x2tReady.fire();
|
|
|
});
|
|
|
};
|
|
|
|
|
|
var x2tInitialized = false;
|
|
|
var x2tInit = function(x2t) {
|
|
|
debug("x2t mount");
|
|
|
// x2t.FS.mount(x2t.MEMFS, {} , '/');
|
|
|
x2t.FS.mkdir('/working');
|
|
|
x2t.FS.mkdir('/working/media');
|
|
|
x2t.FS.mkdir('/working/fonts');
|
|
|
x2tInitialized = true;
|
|
|
fetchFonts(x2t);
|
|
|
debug("x2t mount done");
|
|
|
};
|
|
|
var getX2T = function (cb) {
|
|
|
// Perform the x2t conversion
|
|
|
require(['/common/onlyoffice/x2t/x2t.js'], function() { // FIXME why does this fail without an access-control-allow-origin header?
|
|
|
var x2t = window.Module;
|
|
|
x2t.run();
|
|
|
if (x2tInitialized) {
|
|
|
debug("x2t runtime already initialized");
|
|
|
return void x2tReady.reg(function () {
|
|
|
cb(x2t);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
x2t.onRuntimeInitialized = function() {
|
|
|
debug("x2t in runtime initialized");
|
|
|
// Init x2t js module
|
|
|
x2tInit(x2t);
|
|
|
x2tReady.reg(function () {
|
|
|
cb(x2t);
|
|
|
});
|
|
|
};
|
|
|
});
|
|
|
};
|
|
|
|
|
|
|
|
|
/*
|
|
|
Converting Data
|
|
|
|
|
|
This function converts a data in a specific format to the outputformat
|
|
|
The filename extension needs to represent the input format
|
|
|
Example: fileName=cryptpad.bin outputFormat=xlsx
|
|
|
*/
|
|
|
var getFormatId = function (ext) {
|
|
|
// Sheets
|
|
|
if (ext === 'xlsx') { return 257; }
|
|
|
if (ext === 'xls') { return 258; }
|
|
|
if (ext === 'ods') { return 259; }
|
|
|
if (ext === 'csv') { return 260; }
|
|
|
if (ext === 'pdf') { return 513; }
|
|
|
return;
|
|
|
};
|
|
|
var getFromId = function (ext) {
|
|
|
var id = getFormatId(ext);
|
|
|
if (!id) { return ''; }
|
|
|
return '<m_nFormatFrom>'+id+'</m_nFormatFrom>';
|
|
|
};
|
|
|
var getToId = function (ext) {
|
|
|
var id = getFormatId(ext);
|
|
|
if (!id) { return ''; }
|
|
|
return '<m_nFormatTo>'+id+'</m_nFormatTo>';
|
|
|
};
|
|
|
var x2tConvertDataInternal = function(x2t, data, fileName, outputFormat) {
|
|
|
debug("Converting Data for " + fileName + " to " + outputFormat);
|
|
|
|
|
|
// PDF
|
|
|
var pdfData = '';
|
|
|
if (outputFormat === "pdf" && typeof(data) === "object" && data.bin && data.buffer) {
|
|
|
// Add conversion rules
|
|
|
pdfData = "<m_bIsNoBase64>false</m_bIsNoBase64>" +
|
|
|
"<m_sFontDir>/working/fonts/</m_sFontDir>";
|
|
|
// writing file to mounted working disk (in memory)
|
|
|
x2t.FS.writeFile('/working/' + fileName, data.bin);
|
|
|
x2t.FS.writeFile('/working/pdf.bin', data.buffer);
|
|
|
} else {
|
|
|
// writing file to mounted working disk (in memory)
|
|
|
x2t.FS.writeFile('/working/' + fileName, data);
|
|
|
}
|
|
|
|
|
|
// Adding images
|
|
|
Object.keys(window.frames[0].AscCommon.g_oDocumentUrls.urls || {}).forEach(function (_mediaFileName) {
|
|
|
var mediaFileName = _mediaFileName.substring(6);
|
|
|
var mediasSources = getMediasSources();
|
|
|
var mediaSource = mediasSources[mediaFileName];
|
|
|
var mediaData = mediaSource ? mediasData[mediaSource.src] : undefined;
|
|
|
if (mediaData) {
|
|
|
debug("Writing media data " + mediaFileName);
|
|
|
debug("Data");
|
|
|
var fileData = mediaData.content;
|
|
|
x2t.FS.writeFile('/working/media/' + mediaFileName, new Uint8Array(fileData));
|
|
|
} else {
|
|
|
debug("Could not find media content for " + mediaFileName);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
|
|
|
var inputFormat = fileName.split('.').pop();
|
|
|
|
|
|
var params = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
|
|
|
+ "<TaskQueueDataConvert xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
|
|
|
+ "<m_sFileFrom>/working/" + fileName + "</m_sFileFrom>"
|
|
|
+ "<m_sFileTo>/working/" + fileName + "." + outputFormat + "</m_sFileTo>"
|
|
|
+ pdfData
|
|
|
+ getFromId(inputFormat)
|
|
|
+ getToId(outputFormat)
|
|
|
+ "<m_bIsNoBase64>false</m_bIsNoBase64>"
|
|
|
+ "</TaskQueueDataConvert>";
|
|
|
// writing params file to mounted working disk (in memory)
|
|
|
x2t.FS.writeFile('/working/params.xml', params);
|
|
|
// running conversion
|
|
|
x2t.ccall("runX2T", ["number"], ["string"], ["/working/params.xml"]);
|
|
|
// reading output file from working disk (in memory)
|
|
|
var result;
|
|
|
try {
|
|
|
result = x2t.FS.readFile('/working/' + fileName + "." + outputFormat);
|
|
|
} catch (e) {
|
|
|
debug("Failed reading converted file");
|
|
|
UI.removeModals();
|
|
|
UI.warn(Messages.error);
|
|
|
return "";
|
|
|
}
|
|
|
return result;
|
|
|
};
|
|
|
|
|
|
APP.printPdf = function (obj, cb) {
|
|
|
getX2T(function (x2t) {
|
|
|
//var e = getEditor();
|
|
|
//var d = e.asc_nativePrint(undefined, undefined, 0x100 + opts.printType).ImData;
|
|
|
var bin = getContent();
|
|
|
var xlsData = x2tConvertDataInternal(x2t, {
|
|
|
buffer: obj.data,
|
|
|
bin: bin
|
|
|
}, 'output.bin', 'pdf');
|
|
|
if (xlsData) {
|
|
|
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"});
|
|
|
//var url = URL.createObjectURL(blob, { type: "application/pdf" });
|
|
|
saveAs(blob, title+'.pdf');
|
|
|
//window.open(url);
|
|
|
cb({
|
|
|
"type":"save",
|
|
|
"status":"ok",
|
|
|
//"data":url + "?disposition=inline&ooname=output.pdf"
|
|
|
});
|
|
|
/*
|
|
|
ooChannel.send({
|
|
|
"type":"documentOpen",
|
|
|
"data": {
|
|
|
"type":"save",
|
|
|
"status":"ok",
|
|
|
"data":url + "?disposition=inline&ooname=output.pdf"
|
|
|
}
|
|
|
});
|
|
|
*/
|
|
|
}
|
|
|
});
|
|
|
};
|
|
|
|
|
|
var x2tSaveAndConvertDataInternal = function(x2t, data, filename, extension, finalFilename) {
|
|
|
var type = common.getMetadataMgr().getPrivateData().ooType;
|
|
|
var xlsData;
|
|
|
|
|
|
// PDF
|
|
|
if (type === "sheet" && extension === "pdf") {
|
|
|
var e = getEditor();
|
|
|
var d = e.asc_nativePrint(undefined, undefined, 0x101).ImData;
|
|
|
xlsData = x2tConvertDataInternal(x2t, {
|
|
|
buffer: d.data,
|
|
|
bin: data
|
|
|
}, filename, extension);
|
|
|
if (xlsData) {
|
|
|
var _blob = new Blob([xlsData], {type: "application/bin;charset=utf-8"});
|
|
|
UI.removeModals();
|
|
|
saveAs(_blob, finalFilename);
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
if (type === "sheet" && extension !== 'xlsx') {
|
|
|
xlsData = x2tConvertDataInternal(x2t, data, filename, 'xlsx');
|
|
|
filename += '.xlsx';
|
|
|
} else if (type === "presentation" && extension !== "pptx") {
|
|
|
xlsData = x2tConvertDataInternal(x2t, data, filename, 'pptx');
|
|
|
filename += '.pptx';
|
|
|
} else if (type === "doc" && extension !== "docx") {
|
|
|
xlsData = x2tConvertDataInternal(x2t, data, filename, 'docx');
|
|
|
filename += '.docx';
|
|
|
}
|
|
|
xlsData = x2tConvertDataInternal(x2t, data, filename, extension);
|
|
|
if (xlsData) {
|
|
|
var blob = new Blob([xlsData], {type: "application/bin;charset=utf-8"});
|
|
|
UI.removeModals();
|
|
|
saveAs(blob, finalFilename);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
var x2tSaveAndConvertData = function(data, filename, extension, finalName) {
|
|
|
getX2T(function (x2t) {
|
|
|
x2tSaveAndConvertDataInternal(x2t, data, filename, extension, finalName);
|
|
|
});
|
|
|
};
|
|
|
|
|
|
var exportXLSXFile = function() {
|
|
|
var text = getContent();
|
|
|
var suggestion = Title.suggestTitle(Title.defaultTitle);
|
|
|
var ext = ['.xlsx', '.ods', '.bin', '.csv', '.pdf'];
|
|
|
var type = common.getMetadataMgr().getPrivateData().ooType;
|
|
|
var warning = '';
|
|
|
if (type==="presentation") {
|
|
|
ext = ['.pptx', /*'.odp',*/ '.bin'];
|
|
|
} else if (type==="doc") {
|
|
|
ext = ['.docx', /*'.odt',*/ '.bin'];
|
|
|
}
|
|
|
|
|
|
if (!supportsXLSX()) {
|
|
|
ext = ['.bin'];
|
|
|
warning = '<div class="alert alert-info cp-alert-top">'+Messages.oo_exportChrome+'</div>';
|
|
|
}
|
|
|
|
|
|
var types = ext.map(function (val) {
|
|
|
return {
|
|
|
tag: 'a',
|
|
|
attributes: {
|
|
|
'data-value': val,
|
|
|
href: '#'
|
|
|
},
|
|
|
content: val
|
|
|
};
|
|
|
});
|
|
|
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);
|
|
|
|
|
|
UI.prompt(Messages.exportPrompt+warning, 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)
|
|
|
]);
|
|
|
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(x2t, images, i, callback) {
|
|
|
if (i >= images.length) {
|
|
|
callback();
|
|
|
} else {
|
|
|
debug("Import image " + i);
|
|
|
var handleFileData = {
|
|
|
name: images[i],
|
|
|
mediasSources: getMediasSources(),
|
|
|
callback: function() {
|
|
|
debug("next image");
|
|
|
x2tImportImagesInternal(x2t, images, i+1, callback);
|
|
|
},
|
|
|
};
|
|
|
var filePath = "/working/media/" + images[i];
|
|
|
debug("Import filename " + filePath);
|
|
|
var fileData = x2t.FS.readFile("/working/media/" + images[i], { encoding : "binary" });
|
|
|
debug("Importing data");
|
|
|
debug("Buffer");
|
|
|
debug(fileData.buffer);
|
|
|
var blob = new Blob([fileData.buffer], {type: 'image/png'});
|
|
|
blob.name = images[i];
|
|
|
APP.FMImages.handleFile(blob, handleFileData);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
var x2tImportImages = function (x2t, callback) {
|
|
|
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);
|
|
|
}
|
|
|
|
|
|
// Import Images
|
|
|
debug("Import Images");
|
|
|
var files = x2t.FS.readdir("/working/media/");
|
|
|
var images = [];
|
|
|
files.forEach(function (file) {
|
|
|
if (file !== "." && file !== "..") {
|
|
|
images.push(file);
|
|
|
}
|
|
|
});
|
|
|
x2tImportImagesInternal(x2t, images, 0, function() {
|
|
|
debug("Sync media sources elements");
|
|
|
debug(getMediasSources());
|
|
|
APP.onLocal();
|
|
|
debug("Import Images finalized");
|
|
|
callback();
|
|
|
});
|
|
|
};
|
|
|
|
|
|
|
|
|
var x2tConvertData = function (x2t, data, filename, extension, callback) {
|
|
|
var convertedContent;
|
|
|
// Convert from ODF format:
|
|
|
// first convert to Office format then to the selected extension
|
|
|
if (filename.endsWith(".ods")) {
|
|
|
console.log(x2t, data, filename, extension);
|
|
|
convertedContent = x2tConvertDataInternal(x2t, new Uint8Array(data), filename, "xlsx");
|
|
|
console.log(convertedContent);
|
|
|
convertedContent = x2tConvertDataInternal(x2t, convertedContent, filename + ".xlsx", extension);
|
|
|
} else if (filename.endsWith(".odt")) {
|
|
|
convertedContent = x2tConvertDataInternal(x2t, new Uint8Array(data), filename, "docx");
|
|
|
convertedContent = x2tConvertDataInternal(x2t, convertedContent, filename + ".docx", extension);
|
|
|
} else if (filename.endsWith(".odp")) {
|
|
|
convertedContent = x2tConvertDataInternal(x2t, new Uint8Array(data), filename, "pptx");
|
|
|
convertedContent = x2tConvertDataInternal(x2t, convertedContent, filename + ".pptx", extension);
|
|
|
} else {
|
|
|
convertedContent = x2tConvertDataInternal(x2t, new Uint8Array(data), filename, extension);
|
|
|
}
|
|
|
x2tImportImages(x2t, function() {
|
|
|
callback(convertedContent);
|
|
|
});
|
|
|
};
|
|
|
|
|
|
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);
|
|
|
}
|
|
|
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().setViewModeDisconnect();
|
|
|
} catch (e) {}
|
|
|
if (!yes) { return; }
|
|
|
common.gotoURL();
|
|
|
});
|
|
|
};
|
|
|
var data = {
|
|
|
hash: ooChannel.lastHash,
|
|
|
index: ooChannel.cpIndex,
|
|
|
callback: uploadedCallback
|
|
|
};
|
|
|
APP.FM.handleFile(blob, data);
|
|
|
};
|
|
|
|
|
|
var importXLSXFile = function(content, filename, ext) {
|
|
|
// 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)
|
|
|
]);
|
|
|
UI.openCustomModal(UI.dialog.customModal(div, {buttons: []}));
|
|
|
setTimeout(function () {
|
|
|
getX2T(function (x2t) {
|
|
|
x2tConvertData(x2t, new Uint8Array(content), filename.name, "bin", function(c) {
|
|
|
importFile(c);
|
|
|
});
|
|
|
});
|
|
|
}, 100);
|
|
|
};
|
|
|
|
|
|
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) {
|
|
|
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 setStrictEditing = function () {
|
|
|
if (APP.isFast) { return; }
|
|
|
var editor = getEditor();
|
|
|
var editing = editor.asc_isDocumentModified();
|
|
|
if (editing) {
|
|
|
evOnPatch.fire();
|
|
|
} else {
|
|
|
evOnSync.fire();
|
|
|
}
|
|
|
};
|
|
|
APP.onFastChange = function (isFast) {
|
|
|
APP.isFast = isFast;
|
|
|
if (isFast) {
|
|
|
if (APP.hasChangedInterval) {
|
|
|
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) {
|
|
|
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) { editor.setViewModeDisconnect(); }
|
|
|
var content = parsed.content;
|
|
|
|
|
|
// Get checkpoint
|
|
|
var hashes = content.hashes || {};
|
|
|
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';
|
|
|
|
|
|
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);
|
|
|
}
|
|
|
});
|
|
|
};
|
|
|
|
|
|
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);
|
|
|
toolbar.showColors();
|
|
|
Title.setToolbar(toolbar);
|
|
|
|
|
|
if (window.CP_DEV_MODE) {
|
|
|
|
|
|
var $save = common.createButton('save', true, {}, function () {
|
|
|
makeCheckpoint(true);
|
|
|
});
|
|
|
$save.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;
|
|
|
getEditor().setViewModeDisconnect();
|
|
|
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;
|
|
|
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);
|
|
|
}
|
|
|
})();
|
|
|
}
|
|
|
|
|
|
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'];
|
|
|
}
|
|
|
if (!supportsXLSX()) {
|
|
|
accept = ['.bin'];
|
|
|
}
|
|
|
|
|
|
if (common.isLoggedIn()) {
|
|
|
window.CryptPad_deleteLastCp = deleteLastCp;
|
|
|
var $importXLSX = common.createButton('import', true, {
|
|
|
accept: accept,
|
|
|
binary : ["ods", "xlsx", "odt", "docx", "odp", "pptx"]
|
|
|
}, importXLSXFile);
|
|
|
$importXLSX.appendTo(toolbar.$drawer);
|
|
|
common.createButton('hashtag', true).appendTo(toolbar.$drawer);
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
};
|
|
|
|
|
|
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
|
|
|
UI.errorLoadingScreen(Messages.unableToDisplay, false, function () {
|
|
|
common.gotoURL('');
|
|
|
});
|
|
|
throw new Error("Empty chainpad for a non-empty doc");
|
|
|
} else {
|
|
|
Title.updateTitle(Title.defaultTitle);
|
|
|
}
|
|
|
|
|
|
var version = CURRENT_VERSION + '/';
|
|
|
var msg;
|
|
|
// 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)) {
|
|
|
version = 'v1/';
|
|
|
APP.migrate = true;
|
|
|
// Registedred ~~users~~ editors can start the migration
|
|
|
if (common.isLoggedIn() && !readOnly) {
|
|
|
content.migration = true;
|
|
|
APP.onLocal();
|
|
|
} else {
|
|
|
msg = h('div.alert.alert-warning.cp-burn-after-reading', Messages.oo_sheetMigration_anonymousEditor);
|
|
|
$(APP.helpMenu.menu).after(msg);
|
|
|
readOnly = true;
|
|
|
}
|
|
|
} else if (content && content.version <= 3) { // V2 or V3
|
|
|
version = 'v2b/';
|
|
|
APP.migrate = true;
|
|
|
// Registedred ~~users~~ editors can start the migration
|
|
|
if (common.isLoggedIn() && !readOnly) {
|
|
|
content.migration = true;
|
|
|
APP.onLocal();
|
|
|
} else {
|
|
|
msg = h('div.alert.alert-warning.cp-burn-after-reading', Messages.oo_sheetMigration_anonymousEditor);
|
|
|
$(APP.helpMenu.menu).after(msg);
|
|
|
readOnly = true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 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();
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
}
|
|
|
|
|
|
|
|
|
var useNewDefault = content.version && content.version >= 2;
|
|
|
openRtChannel(function () {
|
|
|
setMyId();
|
|
|
oldHashes = JSON.parse(JSON.stringify(content.hashes));
|
|
|
initializing = false;
|
|
|
common.openPadChat(APP.onLocal);
|
|
|
|
|
|
|
|
|
if (!readOnly) {
|
|
|
var cursors = {};
|
|
|
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
|
|
|
if (!data || !data.cursor) { return; }
|
|
|
// Store the new cursor in memory for this user, with their netflux ID
|
|
|
var ooid = Util.find(data.cursor, ['messages', 0, 'user'])
|
|
|
if (ooid) { cursors[ooid] = data.id.slice(0,32); }
|
|
|
// Update cursor in the UI
|
|
|
ooChannel.send(data.cursor);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if (APP.startWithTemplate) {
|
|
|
var template = APP.startWithTemplate;
|
|
|
loadTemplate(template.href, template.password, template.content);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
loadDocument(newDoc, useNewDefault);
|
|
|
setEditable(!readOnly);
|
|
|
UI.removeLoadingScreen();
|
|
|
});
|
|
|
};
|
|
|
|
|
|
config.onError = function (err) {
|
|
|
common.onServerError(err, toolbar, function () {
|
|
|
setEditable(false);
|
|
|
});
|
|
|
};
|
|
|
|
|
|
var reloadPopup = false;
|
|
|
|
|
|
var checkNewCheckpoint = function () {
|
|
|
var lastCp = getLastCp();
|
|
|
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);
|
|
|
}
|
|
|
|
|
|
var wasLocked = content.saveLock;
|
|
|
|
|
|
var wasMigrating = content.migration;
|
|
|
|
|
|
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();
|
|
|
});
|
|
|
};
|
|
|
if (editor.asc_isDocumentModified()) {
|
|
|
setEditable(false);
|
|
|
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));
|
|
|
}
|
|
|
if (content.migration) {
|
|
|
setEditable(false);
|
|
|
}
|
|
|
if (wasMigrating && !content.migration && !reloadPopup) {
|
|
|
reloadPopup = true;
|
|
|
UI.alert(Messages.oo_sheetMigration_complete, function () {
|
|
|
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);
|
|
|
offline = false;
|
|
|
} else {
|
|
|
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();
|
|
|
});
|