Merge branch 'staging' into broadcast

pull/1/head
yflory 4 years ago
commit 96892bff48

@ -11,6 +11,7 @@ www/common/onlyoffice/web-apps
www/common/onlyoffice/x2t
www/common/onlyoffice/v1
www/common/onlyoffice/v2*
www/common/onlyoffice/v4
server.js
www/common/old-media-tag.js

@ -1,3 +1,74 @@
# 4.3.1 (WIP)
This minor release addresses some bugs discovered after deploying and tagging 4.3.0
* better isLoggedIn() check
* fix templates in sheets
* include onlyOffice version along with checkpoint hashes
* send feedback when opening the readme
* so we can decide whether to remove it
* handle decryption errors for blobs
* prompted by a badly formed sheet checkpoint
* fix broken team creation
* CKEditor
* broken table of contents scrollTo
* show the link bubble for links inside of comments
* fix title reset in polls
# 4.3.0 (D)
## Goals
This release is a continuation of our recent efforts to stabilize the platform, fixing small bugs and inconsistencies that we missed when developing larger features. In the meantime we've received reports of the platform performing poorly under various unusual circumstances, so we've developed some targeted fixes to both improve user experience and decrease the load on our server.
## Update notes
This release should be fairly simple for admins.
To update from 4.2.1 to 4.3.0:
1. Stop your server
2. Get the latest code with git
3. Install the latest dependencies with `bower update` and `npm i`
4. Restart your server
## Features
* We're introducing a "degraded mode" for most of our editors (all except polls and sheets). This follows reports we received that CryptPad performed poorly in settings where a relatively large number of users with *edit* rights were connected simultaneously. To alleviate this, some non-essential features will be disabled when a number of concurrent editors is reached, in order to save computing power on client devices. The user-list will stop being updated as users join and leave, users cursors will stop being displayed, and the chat will not be disabled. Sessions will enter this mode when 8 or more editors are present. This threshold can be configured via `customize/application_config.js` by setting a `degradedLimit` attribute.
* CryptPad was recently used to distribute some high-profile documents. For the first time we were able to observe our server supporting more than 1000 concurrent viewers in a single pad and around 350000 unique visitors over the course of a few days. While the distributed document incurred very little load, CryptPad created a drive for each visitor the first time they visited. Most of these drives were presumably abandoned as these users did not return to create or edit their own documents. Such users that directly load an existing document without having previously visited the platform will no longer create a drive automatically, unless they explicitly visit a page which requires it. This behaviour is supported in most of our editors except sheets and polls. This should result in faster load times for new users, but just in case it causes any issues we've made it easy to disable. Instance admins can disable "no-drive mode" via `customize/application_config.js` by setting `allowDrivelessMode` to `false`.
* We've updated our sheet editor to use OnlyOffice 6.2, which includes support for pivot tables, among a range of other improvements.
* Our rich text editor now features some keyboard shortcuts to apply some commonly used styles:
* heading size 1-6: ctrl+alt+1-6
* "div": ctrl+alt+8
* "preformatted": ctrl+alt+9
* paragraph: ctrl+alt+0
* remove styles from selection: ctrl+space
* We've removed a large number of strings that were included in the "Getting started" box that was displayed to new users in each of our editors. Instead, this box simply contains a link to the relevant page in our documentation. Our intent is to both simplify the interface for newcomers and reduce the number of strings that require translation.
* We've continued to progress on our "checkup page" which performs some routine checks to see whether the host instance is correctly configured. While its hints are not especially helpful for admins without reading the code to understand what they are testing, they do detect a fairly wide range of issues and have already helped us to identify some inconsistencies in our recommended configuration. We plan to link directly from this page to the relevant sections of a configuration guide an in upcoming release.
* The admin support ticket interface has been updated to collapse very long messages in response to some ticket threads submitted in the last few weeks. We also found that sometimes we needed more information after a ticket had been closed, so we added the ability to re-open closed tickets.
* Some time ago we removed the "Survey link" option from the user admin dropdown menu (found in the top-right corner of the page). This release re-enables it for instances that explicitly provide a link to a survey, however, we no longer provide a link to a survey by default.
## Bug fixes
* We finally reviewed and merged a number of pull-requests that had been pending for some time. Collectively, they fixed some configuration issues and type errors in some of our older scripts.
* Sheets can now contain multiple images with the same name, whereas before they would conflict and one would be displayed multiple times.
* A recent change in our code to conditionally display size measurements in different magnitudes (GB, MB) removed support for Kilobytes (KB). This release restores the previous behaviour.
* We believe we've identified and corrected an issue that caused the rich text editor to scroll to the top of the document when the button to add a comment was clicked.
* We recently made it such that documents owned by a particular user would not be automatically re-added to that user's drive when they viewed them. This change revealed a number of odd cases where various commands (destroy, add password, get document size, etc.) did not work as expected unless the document was first added to their drive. We reviewed many of these features and corrected the underlying issues that caused these commands to fail.
* We performed a similar review of various commands related to user accounts and identified a number of issues that caused account deletion to fail.
# 4.2.1
This minor release addresses a few bugs discovered after deploying 4.2.0:
* The 4.2.0 release included major improvements to the sheet application. This introduced breaking changes to the "lock" system in the application. Existing spreadsheets (before 4.2.0) that were closed by a user without "unlocking" all cells first became impossible to open after the 4.2.0 changes. This has been fixed.
* Team owners can now properly upload a team avatar.
* We've improved the file upload script to better recognize markdown files.
* We've fixed a few issues resulting in an error screen:
* New users were unable to create a drive without registering first.
* Snapshots in the sheet application couldn't be loaded.
* Loading an existing drive as an unregistered user could fail.
# 4.2.0 (C)
## Goals

@ -54,7 +54,7 @@ module.exports = {
* and it may have unintended consequences in practice.
*
*/
httpUnsafeOrigin: 'http://localhost:3000/',
httpUnsafeOrigin: 'http://localhost:3000',
/* httpSafeOrigin is the URL that is used for the 'sandbox' described above.
* If you're testing or developing with CryptPad on your local machine then

@ -40,11 +40,6 @@ body
margin: 0;
padding: 20px;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
/* Remove margin-top for the first element */

@ -62,7 +62,7 @@ define([
var imprintUrl = AppConfig.imprint && (typeof(AppConfig.imprint) === "boolean" ?
'/imprint.html' : AppConfig.imprint);
Pages.versionString = "v4.2.0";
Pages.versionString = "v4.3.1";
// used for the about menu
Pages.imprintLink = AppConfig.imprint ? footLink(imprintUrl, 'imprint') : undefined;

@ -13,8 +13,8 @@
whiteboard: #a72ba7;
kanban: #8C4;
sheet: #40865c;
oodoc: #5170B5;
ooslide: #C65D27;
doc: #5170B5;
presentation: #C65D27;
file: #CD2532;
}

@ -13,8 +13,8 @@
whiteboard: #a72ba7;
kanban: #8C4;
sheet: #40865c;
oodoc: #5170B5;
ooslide: #C65D27;
doc: #5170B5;
presentation: #C65D27;
file: #CD2532;
}

@ -184,6 +184,7 @@
}
&.btn-register {
margin-top: 10px !important;
white-space: normal;
}

@ -278,6 +278,7 @@
cursor: pointer;
}
&> p {
text-align: center;
margin: 20px;
}
&> div {

@ -662,6 +662,8 @@ const handleGetHistory = function (Env, Server, seq, userId, parsed) {
if (txid) { msg[0] = txid; }
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(msg)], readMore);
}, (err) => {
// Any error but ENOENT: abort
// ENOENT is allowed in case we want to create a new pad
if (err && err.code !== 'ENOENT') {
if (err.message === "EUNKNOWN") {
Log.error("HK_GET_HISTORY", {
@ -676,11 +678,28 @@ const handleGetHistory = function (Env, Server, seq, userId, parsed) {
err: err && err.message || err,
stack: err && err.stack,
}); }
// FIXME err.message isn't useful for users
const parsedMsg = {error:err.message, channel: channelName, txid: txid};
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(parsedMsg)]);
return;
}
// If we're asking for a specific version (lastKnownHash) but we receive an
// ENOENT, this is not a pad creation so we need to abort.
if (err && err.code === 'ENOENT' && lastKnownHash) { // XXX && lastKnownHash !== -1
/*
This informs clients that the pad they're trying to load was deleted by its owner.
The user in question might be reconnecting or might have loaded the document from their cache.
The owner that deleted it could be another user or the same user from a different device.
Either way, the respectful thing to do is display an error screen informing them that the content
is no longer on the server so they don't abuse the data and so that they don't unintentionally continue
to edit it in a broken state.
*/
const parsedMsg2 = {error:'EDELETED', channel: channelName, txid: txid};
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(parsedMsg2)]);
return;
}
if (msgCount === 0 && !metadata_cache[channelName] && Server.channelContainsUser(channelName, userId)) {
handleFirstMessage(Env, channelName, metadata);
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(metadata)]);
@ -899,6 +918,7 @@ HK.onChannelMessage = function (Env, Server, channel, msgStruct, cb) {
// more straightforward and reliable.
if (Array.isArray(id) && id[2] && id[2] === channel.lastSavedCp) {
// Reject duplicate checkpoints
// XXX not an error? the checkpoint is already here so we can assume it's stored
return void cb('DUPLICATE');
}
}

2
package-lock.json generated

@ -1,6 +1,6 @@
{
"name": "cryptpad",
"version": "4.2.0",
"version": "4.3.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

@ -1,7 +1,7 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
"version": "4.2.0",
"version": "4.3.1",
"license": "AGPL-3.0+",
"repository": {
"type": "git",

@ -0,0 +1,27 @@
var EN = require("../www/common/translations/messages.json");
var simpleTags = [
'<br>',
'<br />',
];
['a', 'b', 'em', 'p', 'i'].forEach(function (tag) {
simpleTags.push('<' + tag + '>');
simpleTags.push('</' + tag + '>');
});
Object.keys(EN).forEach(function (k) {
var s = EN[k];
if (typeof(s) !== 'string') { return; }
var usesHTML;
s.replace(/<.*?>/g, function (html) {
if (simpleTags.indexOf(html) !== -1) { return; }
usesHTML = true;
console.log("{%s}", html);
});
if (usesHTML) {
console.log("[%s] %s\n", k, s);
}
});

@ -30,6 +30,7 @@ var grep = function (pattern, cb) {
'www/common/translations/*',
'www/common/onlyoffice/v1/*',
'www/common/onlyoffice/v2b*',
'www/common/onlyoffice/v4*',
'www/common/onlyoffice/x2t/*',
//'www/common/onlyoffice/build/*',
'www/lib/*',

@ -16,15 +16,19 @@ var Env = require("./lib/env").create(config);
var app = Express();
var canonicalizeOrigin = function (s) {
return (s || '').trim().replace(/\/+$/, '');
};
(function () {
// you absolutely must provide an 'httpUnsafeOrigin'
if (typeof(config.httpUnsafeOrigin) !== 'string') {
throw new Error("No 'httpUnsafeOrigin' provided");
}
config.httpUnsafeOrigin = config.httpUnsafeOrigin.trim();
config.httpUnsafeOrigin = canonicalizeOrigin(config.httpUnsafeOrigin);
if (typeof(config.httpSafeOrigin) === 'string') {
config.httpSafeOrigin = config.httpSafeOrigin.trim().replace(/\/$/, '');
config.httpSafeOrigin = canonicalizeOrigin(config.httpSafeOrigin);
}
// fall back to listening on a local address
@ -114,7 +118,7 @@ var setHeaders = (function () {
// targeted CSP, generic policies, maybe custom headers
const h = [
/^\/common\/onlyoffice\/.*\/index\.html.*/,
/^\/(sheet|ooslide|oodoc)\/inner\.html.*/,
/^\/(sheet|presentation|doc)\/inner\.html.*/,
].some((regex) => {
return regex.test(req.url);
}) ? padHeaders : headers;

@ -506,7 +506,7 @@ define([
}
var size = Array.isArray(obj) && obj[0];
if (typeof(size) !== "number") { return; }
UI.alert(Util.getPrettySize(size, Messages));
UI.alert(getPrettySize(size));
});
});

@ -43,7 +43,12 @@ define([
}, _alert('Sandbox configuration: httpUnsafeOrigin !== httpSafeOrigin'));
assert(function (cb) {
cb((window.location.origin + '/') === ApiConfig.httpUnsafeOrigin);
cb(trimmedSafe === ApiConfig.httpSafeOrigin);
}, "httpSafeOrigin must not have a trailing slash");
assert(function (cb) {
var origin = window.location.origin;
return void cb(ApiConfig.httpUnsafeOrigin === origin);
}, _alert('Sandbox configuration: loading via httpUnsafeOrigin'));

@ -12,7 +12,7 @@ define(function() {
* You should never remove the drive from this list.
*/
config.availablePadTypes = ['drive', 'teams', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard',
/*'oodoc', 'ooslide',*/ 'file', /*'todo',*/ 'contacts'];
/*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts'];
/* The registered only types are apps restricted to registered users.
* You should never remove apps from this list unless you know what you're doing. The apps
* listed here by default can't work without a user account.
@ -20,7 +20,7 @@ define(function() {
* users and these users will be redirected to the login page if they still try to access
* the app
*/
config.registeredOnlyTypes = ['file', 'contacts', 'oodoc', 'ooslide', 'notifications', 'support'];
config.registeredOnlyTypes = ['file', 'contacts', 'notifications', 'support'];
/* CryptPad is available is multiple languages, but only English and French are maintained
* by the developers. The other languages may be outdated, and any missing string for a langauge
@ -115,8 +115,8 @@ define(function() {
todo: 'cptools-todo',
contacts: 'fa-address-book',
kanban: 'cptools-kanban',
oodoc: 'fa-file-word-o',
ooslide: 'fa-file-powerpoint-o',
doc: 'fa-file-word-o',
presentation: 'fa-file-powerpoint-o',
sheet: 'cptools-sheet',
drive: 'fa-hdd-o',
teams: 'fa-users',
@ -162,7 +162,7 @@ define(function() {
// making it much faster to open new tabs.
config.disableWorkers = false;
config.surveyURL = "https://survey.cryptpad.fr/index.php/672782";
//config.surveyURL = "";
// Teams are always loaded during the initial loading screen (for the first tab only if
// SharedWorkers are available). Allowing users to be members of multiple teams can
@ -179,5 +179,23 @@ define(function() {
// You can change the value here.
// config.maxOwnedTeams = 5;
// The userlist displayed in collaborative documents is stored alongside the document data.
// Everytime someone with edit rights joins a document or modify their user data (display
// name, avatar, color, etc.), they update the "userlist" part of the document. When too many
// editors are in the same document, all these changes increase the risks of conflicts which
// require CPU time to solve. A "degraded" mode can now be set when a certain number of editors
// are in a document at the same time. This mode disables the userlist, the chat and the
// position of other users' cursor. You can configure the number of user from which the session
// will enter into degraded mode. A big number may result in collaborative edition being broken,
// but this number depends on the network and CPU performances of each user's device.
config.degradedLimit = 8;
// In "legacy" mode, one-time users were always creating an "anonymous" drive when visiting CryptPad
// in which they could store their pads. The new "driveless" mode allow users to open an existing
// pad without creating a drive in the background. The drive will only be created if they visit
// a different page (Drive, Settings, etc.) or try to create a new pad themselves. You can disable
// the driveless mode by changing the following value to "false"
config.allowDrivelessMode = true;
return config;
});

@ -644,6 +644,10 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app)
'/' + curvePublic.replace(/\//g, '-') + '/';
};
Hash.isValidChannel = function (channelId) {
return /^[a-zA-Z0-9]{32,48}$/.test(channelId);
};
Hash.isValidHref = function (href) {
// Non-empty href?
if (!href) { return; }

@ -1106,8 +1106,12 @@ define([
href = "https://docs.cryptpad.fr/en/user_guide/apps/" + apps[type] + ".html";
}
var content = setHTML(h('p'), Messages.help.generic.more);
$(content).find('a').attr('href', href);
var content = setHTML(h('p'), Messages.help_genericMore);
$(content).find('a').attr({
href: href,
target: '_blank',
rel: 'noopener noreferrer',
});
var text = h('p.cp-help-text', [
content
@ -1532,7 +1536,7 @@ define([
var legalLine = template(Messages.info_imprintFlavour, Pages.imprintLink);
var privacyLine = template(Messages.info_privacyFlavour, Pages.privacyLink);
var faqLine = template(Messages.help.generic.more, Pages.docsLink);
var faqLine = template(Messages.help_genericMore, Pages.docsLink);
var content = h('div.cp-info-menu-container', [
h('div.logo-block', [
@ -1548,6 +1552,8 @@ define([
faqLine,
]);
$(content).find('a').attr('target', '_blank');
var buttons = [
{
className: 'primary',
@ -1940,7 +1946,8 @@ define([
$body: $('body')
});
var $modal = modal.$modal;
var $title = $('<h3>').text(Messages.fm_newFile);
var $title = $(h('h3', [ h('i.fa.fa-plus'), ' ', Messages.fm_newButton ]));
var $description = $('<p>').html(Messages.creation_newPadModalDescription);
$modal.find('.cp-modal').append($title);
$modal.find('.cp-modal').append($description);
@ -2534,9 +2541,11 @@ define([
$creation.focus();
};
var autoStoreModal = {};
UIElements.onServerError = function (common, err, toolbar, cb) {
//if (["EDELETED", "EEXPIRED", "ERESTRICTED"].indexOf(err.type) === -1) { return; }
var priv = common.getMetadataMgr().getPrivateData();
var sframeChan = common.getSframeChannel();
var msg = err.type;
if (err.type === 'EEXPIRED') {
msg = Messages.expiredError;
@ -2546,10 +2555,36 @@ define([
if (toolbar && typeof toolbar.deleted === "function") { toolbar.deleted(); }
} else if (err.type === 'EDELETED') {
if (priv.burnAfterReading) { return void cb(); }
if (autoStoreModal[priv.channel]) {
autoStoreModal[priv.channel].delete();
delete autoStoreModal[priv.channel];
}
if (err.ownDeletion) {
if (toolbar && typeof toolbar.deleted === "function") { toolbar.deleted(); }
(cb || function () {})();
return;
}
// View users have the wrong seed, thay can't retireve access directly
// Version 1 hashes don't support passwords
if (!priv.readOnly && !priv.oldVersionHash) {
sframeChan.event('EV_SHARE_OPEN', {hidden: true}); // Close share modal
UIElements.displayPasswordPrompt(common, {
fromServerError: true,
loaded: err.loaded,
});
if (toolbar && typeof toolbar.deleted === "function") { toolbar.deleted(); }
(cb || function () {})();
return;
}
msg = Messages.deletedError;
if (err.loaded) {
msg += Messages.errorCopy;
}
if (toolbar && typeof toolbar.deleted === "function") { toolbar.deleted(); }
} else if (err.type === 'ERESTRICTED') {
msg = Messages.restrictedError;
@ -2558,7 +2593,6 @@ define([
msg = Messages.oo_deletedVersion;
if (toolbar && typeof toolbar.failed === "function") { toolbar.failed(true); }
}
var sframeChan = common.getSframeChannel();
sframeChan.event('EV_SHARE_OPEN', {hidden: true});
UI.errorLoadingScreen(msg, Boolean(err.loaded), Boolean(err.loaded));
(cb || function () {})();
@ -2567,7 +2601,10 @@ define([
UIElements.displayPasswordPrompt = function (common, cfg, isError) {
var error;
if (isError) { error = setHTML(h('p.cp-password-error'), Messages.password_error); }
var info = h('p.cp-password-info', Messages.password_info);
var info_loaded = setHTML(h('p.cp-password-info'), Messages.errorCopy);
var password = UI.passwordInput({placeholder: Messages.password_placeholder});
var $password = $(password);
var button = h('button.btn.btn-primary', Messages.password_submit);
@ -2579,6 +2616,21 @@ define([
var submit = function () {
var value = $password.find('.cp-password-input').val();
// Password-prompt called from UIElements.onServerError
if (cfg.fromServerError) {
common.getSframeChannel().query('Q_PASSWORD_CHECK', value, function (err, obj) {
if (obj && obj.error) {
console.error(obj.error);
return void UI.warn(Messages.error);
}
// On success, outer will reload the page: this is a wrong password
UIElements.displayPasswordPrompt(common, cfg, true);
});
return;
}
// Initial load
UI.addLoadingScreen({newProgress: true});
if (window.CryptPad_updateLoadingProgress) {
window.CryptPad_updateLoadingProgress({
@ -2592,6 +2644,8 @@ define([
}
});
};
$password.find('.cp-password-input').on('keydown', function (e) { if (e.which === 13) { submit(); } });
$(button).on('click', function () { submit(); });
@ -2599,12 +2653,13 @@ define([
var block = h('div#cp-loading-password-prompt', [
error,
info,
cfg.loaded ? info_loaded : undefined,
h('p.cp-password-form', [
password,
button
])
]),
]);
UI.errorLoadingScreen(block);
UI.errorLoadingScreen(block, Boolean(cfg.loaded), Boolean(cfg.loaded));
$password.find('.cp-password-input').focus();
};
@ -2697,7 +2752,6 @@ define([
};
var storePopupState = false;
var autoStoreModal = {};
UIElements.displayStorePadPopup = function (common, data) {
if (storePopupState) { return; }
storePopupState = true;

@ -481,10 +481,20 @@ define([
});
};
common.isNewChannel = function (href, password, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var channel = Hash.hrefToHexChannelId(href, password);
postMessage('IS_NEW_CHANNEL', {channel: channel}, function (obj) {
var error = obj && obj.error;
if (error) { return void cb(error); }
if (!obj) { return void cb('ERROR'); }
cb (null, obj.isNew);
}, {timeout: -1});
};
// This function is used when we want to open a pad. We first need
// to check if it exists. With the cached drive, we need to wait for
// the network to be available before we can continue.
common.isNewChannel = function (href, password, _cb) {
common.hasChannelHistory = function (href, password, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var channel = Hash.hrefToHexChannelId(href, password);
var error;
@ -1095,6 +1105,7 @@ define([
common.changePadPassword = function (Crypt, Crypto, data, cb) {
var href = data.href;
var oldPassword = data.oldPassword;
var newPassword = data.password;
var teamId = data.teamId;
if (!href) { return void cb({ error: 'EINVAL_HREF' }); }
@ -1123,7 +1134,9 @@ define([
var isSharedFolder = parsed.type === 'drive';
var optsGet = {};
var optsGet = {
password: oldPassword
};
var optsPut = {
password: newPassword,
metadata: {},
@ -1133,7 +1146,7 @@ define([
var cryptgetVal;
Nthen(function (waitFor) {
if (parsed.hashData && parsed.hashData.password) {
if (parsed.hashData && parsed.hashData.password && !oldPassword) {
common.getPadAttribute('password', waitFor(function (err, password) {
optsGet.password = password;
}), href);
@ -1179,7 +1192,6 @@ define([
} else if (mailbox && typeof(mailbox) === "object") {
m = {};
Object.keys(mailbox).forEach(function (ed) {
console.log(mailbox[ed]);
try {
m[ed] = newCrypto.encrypt(oldCrypto.decrypt(mailbox[ed], true, true));
} catch (e) {
@ -1214,6 +1226,7 @@ define([
cryptgetVal = JSON.stringify(parsed);
}
}), optsGet);
Cache.clearChannel(newSecret.channel, waitFor());
}).nThen(function (waitFor) {
optsPut.metadata.restricted = oldMetadata.restricted;
optsPut.metadata.allowed = oldMetadata.allowed;
@ -1418,6 +1431,7 @@ define([
common.changeOOPassword = function (data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var href = data.href;
var oldPassword = data.oldPassword;
var newPassword = data.password;
var teamId = data.teamId;
if (!href) { return void cb({ error: 'EINVAL_HREF' }); }
@ -1431,7 +1445,6 @@ define([
var oldMetadata;
var oldRtChannel;
var privateData;
var padData;
var newSecret;
if (parsed.hashData.version >= 2) {
@ -1452,19 +1465,22 @@ define([
validateKey: newSecret.keys.validateKey
},
};
var optsGet = {};
var optsGet = {
password: oldPassword
};
Nthen(function (waitFor) {
common.getPadAttribute('', waitFor(function (err, _data) {
padData = _data;
optsGet.password = padData.password;
if (!oldPassword && _data) {
optsGet.password = _data.password;
}
}), href);
common.getAccessKeys(waitFor(function (keys) {
optsGet.accessKeys = keys;
optsPut.accessKeys = keys;
}));
}).nThen(function (waitFor) {
oldSecret = Hash.getSecrets(parsed.type, parsed.hash, padData.password);
oldSecret = Hash.getSecrets(parsed.type, parsed.hash, optsGet.password);
require([
'/common/cryptget.js',
@ -1592,6 +1608,7 @@ define([
}
}));
}));
Cache.clearChannel(newSecret.channel, waitFor());
}).nThen(function (waitFor) {
// The new rt channel is ready
// The blob uses its own encryption and doesn't need to be reencrypted
@ -2011,7 +2028,6 @@ define([
});
};
var provideFeedback = function () {
if (typeof(window.Proxy) === 'undefined') {
Feedback.send("NO_PROXIES");
@ -2048,7 +2064,6 @@ define([
if (!common.hasCSSVariables()) {
Feedback.send('NO_CSS_VARIABLES');
}
Feedback.reportScreenDimensions();
Feedback.reportLanguage();
};
@ -2271,8 +2286,9 @@ define([
localToken: tryParsing(localStorage.getItem(Constants.tokenKey)), // TODO move this to LocalStore ?
language: common.getLanguage(),
cache: rdyCfg.cache,
noDrive: rdyCfg.noDrive,
disableCache: localStorage['CRYPTPAD_STORE|disableCache'],
driveEvents: true //rdyCfg.driveEvents // Boolean
driveEvents: !rdyCfg.noDrive //rdyCfg.driveEvents // Boolean
};
common.userHash = userHash;
@ -2468,6 +2484,9 @@ define([
data = data.returned;
}
if (data.loggedIn) {
window.CP_logged_in = true;
}
if (data.anonHash && !cfg.userHash) { LocalStore.setFSHash(data.anonHash); }
initialized = true;
@ -2499,6 +2518,24 @@ define([
}
if (parsedNew.hashData) { oldHref = newHref; }
};
// If you're in noDrive mode, check if an FS_hash is added and reload if that's the case
if (rdyCfg.noDrive && !localStorage[Constants.fileHashKey]) {
window.addEventListener('storage', function (e) {
if (e.key !== Constants.fileHashKey) { return; }
// New entry added to FS_hash: drive created in another tab, reload
var o = e.oldValue;
var n = e.newValue;
if (!o && n) {
postMessage('HAS_DRIVE', null, function(obj) {
// If we're still in noDrive mode, reload
if (!obj.state) {
LocalStore.loginReload();
}
// Otherwise this worker is connected, nothing to do
});
}
});
}
// Listen for login/logout in other tabs
window.addEventListener('storage', function (e) {
if (e.key !== Constants.userHashKey) { return; }

@ -3067,14 +3067,14 @@ define([
'class': 'cp-app-drive-element-row cp-app-drive-new-ghost'
}).prepend($addIcon.clone()).appendTo($list);
$element.append($('<span>', {'class': 'cp-app-drive-element-name'})
.text(Messages.fm_newFile));
.text(Messages.fm_newButton));
$element.click(function () {
var modal = UI.createModal({
id: 'cp-app-drive-new-ghost-dialog',
$body: $('body')
});
var $modal = modal.$modal;
var $title = $('<h3>').text(Messages.fm_newFile);
var $title = $(h('h3', [ h('i.fa.fa-plus'), ' ', Messages.fm_newButton ]));
var $description = $('<p>').text(Messages.fm_newButtonTitle);
$modal.find('.cp-modal').append($title);
$modal.find('.cp-modal').append($description);

@ -25,10 +25,12 @@ define([
var sframeChan = common.getSframeChannel();
var metadataMgr = common.getMetadataMgr();
var channel = data.channel;
var priv = metadataMgr.getPrivateData();
var channel = data.channel || priv.channel;
var owners = data.owners || [];
var pending_owners = data.pending_owners || [];
var teamOwner = data.teamId;
var title = opts.title;
opts = opts || {};
var redrawAll = function () {};
@ -115,7 +117,7 @@ define([
if (!friend) { return; }
common.mailbox.sendTo("RM_OWNER", {
channel: channel,
title: data.title,
title: data.title || title,
pending: pending
}, {
channel: friend.notifications,
@ -271,7 +273,7 @@ define([
href: data.href || data.rohref,
password: data.password,
path: isTemplate ? ['template'] : undefined,
title: data.title || '',
title: data.title || title || "",
teamId: obj.id
}, waitFor(function (err) {
if (err) { return void console.error(err); }
@ -320,6 +322,12 @@ define([
}));
}
}).nThen(function (waitFor) {
var href = data.href;
var hashes = priv.hashes || {};
var bestHash = hashes.editHash || hashes.viewHash || hashes.fileHash;
if (data.fakeHref) {
href = Hash.hashToHref(bestHash, priv.app);
}
sel.forEach(function (el) {
var curve = $(el).attr('data-curve');
if (curve === user.curvePublic) { return; }
@ -327,9 +335,9 @@ define([
if (!friend) { return; }
common.mailbox.sendTo("ADD_OWNER", {
channel: channel,
href: data.href,
password: data.password,
title: data.title
href: href,
password: data.password || priv.password,
title: data.title || title
}, {
channel: friend.notifications,
curvePublic: friend.curvePublic
@ -398,7 +406,8 @@ define([
var sframeChan = common.getSframeChannel();
var metadataMgr = common.getMetadataMgr();
var channel = data.channel;
var priv = metadataMgr.getPrivateData();
var channel = data.channel || priv.channel;
var owners = data.owners || [];
var restricted = data.restricted || false;
var allowed = data.allowed || [];
@ -888,9 +897,17 @@ define([
});
}
var href = data.href;
var hashes = priv.hashes || {};
var bestHash = hashes.editHash || hashes.viewHash || hashes.fileHash;
if (data.fakeHref) {
href = Hash.hashToHref(bestHash, priv.app);
}
var isNotStored = Boolean(data.fakeHref);
sframeChan.query(q, {
teamId: typeof(owned) !== "boolean" ? owned : undefined,
href: data.href,
href: href,
oldPassword: priv.password,
password: newPass
}, function (err, data) {
$(passwordOk).text(Messages.properties_changePasswordButton);
@ -924,22 +941,26 @@ define([
// Pad password changed: update the href
// Use hidden hash if needed (we're an owner of this pad so we know it is stored)
var useUnsafe = Util.find(priv, ['settings', 'security', 'unsafeLinks']);
var href = (priv.readOnly && data.roHref) ? data.roHref : data.href;
if (isNotStored) { useUnsafe = true; }
var _href = (priv.readOnly && data.roHref) ? data.roHref : data.href;
if (useUnsafe !== true) {
var newParsed = Hash.parsePadUrl(href);
var newParsed = Hash.parsePadUrl(_href);
var newSecret = Hash.getSecrets(newParsed.type, newParsed.hash, newPass);
var newHash = Hash.getHiddenHashFromKeys(parsed.type, newSecret, {});
href = Hash.hashToHref(newHash, parsed.type);
_href = Hash.hashToHref(newHash, parsed.type);
}
// Trigger a page reload if the href didn't change
if (_href === href) { _href = undefined; }
if (data.warning) {
return void UI.alert(Messages.properties_passwordWarning, function () {
common.gotoURL(href);
common.gotoURL(_href);
}, {force: true});
}
return void UI.alert(Messages.properties_passwordSuccess, function () {
if (!isSharedFolder) {
common.gotoURL(href);
common.gotoURL(_href);
}
}, {force: true});
});
@ -956,7 +977,7 @@ define([
spinner.spin();
sframeChan.query('Q_DELETE_OWNED', {
teamId: typeof(owned) !== "boolean" ? owned : undefined,
channel: data.channel
channel: data.channel || priv.channel
}, function (err, obj) {
spinner.done();
UI.findCancelButton().click();

@ -1,7 +1,7 @@
@import (reference) "../../customize/src/less2/include/framework.less";
// body
body.cp-app-sheet, body.cp-app-oodoc, body.cp-app-ooslide {
body.cp-app-sheet, body.cp-app-doc, body.cp-app-presentation {
display: flex;
flex-flow: column;
@ -10,14 +10,14 @@ body.cp-app-sheet, body.cp-app-oodoc, body.cp-app-ooslide {
@bg-color: @colortheme_apps[sheet],
);
}
&.cp-app-oodoc {
&.cp-app-doc {
.framework_main(
@bg-color: @colortheme_apps[oodoc],
@bg-color: @colortheme_apps[doc],
);
}
&.cp-app-ooslide {
&.cp-app-presentation {
.framework_main(
@bg-color: @colortheme_apps[ooslide],
@bg-color: @colortheme_apps[presentation],
);
}

@ -59,9 +59,9 @@ define([
var CHECKPOINT_INTERVAL = 100;
var DISPLAY_RESTORE_BUTTON = false;
var NEW_VERSION = 3;
var NEW_VERSION = 4;
var PENDING_TIMEOUT = 30000;
var CURRENT_VERSION = 'v2b';
var CURRENT_VERSION = 'v4';
//var READONLY_REFRESH_TO = 15000;
var debug = function (x, type) {
@ -231,7 +231,7 @@ define([
var title = common.getMetadataMgr().getMetadataLazy().title;
var file = {};
switch(type) {
case 'oodoc':
case 'doc':
file.type = 'docx';
file.title = title + '.docx' || 'document.docx';
file.doc = 'text';
@ -241,7 +241,7 @@ define([
file.title = title + '.xlsx' || 'spreadsheet.xlsx';
file.doc = 'spreadsheet';
break;
case 'ooslide':
case 'presentation':
file.type = 'pptx';
file.title = title + '.pptx' || 'presentation.pptx';
file.doc = 'presentation';
@ -263,6 +263,9 @@ define([
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;
};
@ -366,7 +369,8 @@ define([
content.hashes[i] = {
file: data.url,
hash: ev.hash,
index: ev.index
index: ev.index,
version: NEW_VERSION
};
oldHashes = JSON.parse(JSON.stringify(content.hashes));
content.locks = {};
@ -557,10 +561,10 @@ define([
case 'sheet' :
newText = EmptyCell(useNewDefault);
break;
case 'oodoc':
case 'doc':
newText = EmptyDoc();
break;
case 'ooslide':
case 'presentation':
newText = EmptySlide();
break;
default:
@ -593,7 +597,13 @@ define([
if (arrayBuffer) {
var u8 = new Uint8Array(arrayBuffer);
FileCrypto.decrypt(u8, key, function (err, decrypted) {
if (err) { return void console.error(err); }
if (err) {
if (err === "DECRYPTION_ERROR") {
console.warn(err);
return void onCpError(err);
}
return void console.error(err);
}
var blob = new Blob([decrypted.content], {type: 'plain/text'});
if (cb) {
return cb(blob, getFileType());
@ -788,6 +798,7 @@ define([
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; }
@ -831,16 +842,44 @@ define([
};
// Get all existing locks
var getUserLock = function (id) {
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;
};
var getLock = function () {
var type = common.getMetadataMgr().getPrivateData().ooType;
var locks = [];
Object.keys(content.locks).forEach(function (id) {
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));
});
return locks;
};
var locksArrayToObject = function (arr) {
var l = {};
if (!Array.isArray(arr)) { return l; }
arr.forEach(function (lock) {
var uid = lock.block;
if (!uid) { return; }
l[uid] = lock;
});
return l;
};
// Update the userlist in onlyoffice
@ -858,7 +897,7 @@ define([
var handleNewLocks = function (o, n) {
var hasNew = false;
// Check if we have at least one new lock
Object.keys(n).some(function (id) {
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) {
@ -870,7 +909,7 @@ define([
});
});
// Remove old locks
Object.keys(o).forEach(function (id) {
Object.keys(o || {}).forEach(function (id) {
if (typeof(o[id]) !== "object") { return; } // Ignore old format
Object.keys(o[id]).forEach(function (uid) {
// Removed lock
@ -987,6 +1026,8 @@ define([
}, 50);
return;
}
var type = common.getMetadataMgr().getPrivateData().ooType;
content.locks = content.locks || {};
// Send the lock to other users
var msg = {
@ -996,8 +1037,13 @@ define([
};
var myId = getId();
content.locks[myId] = content.locks[myId] || {};
var b = obj.block && obj.block[0];
if (type === "sheet") {
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();
@ -1088,8 +1134,8 @@ define([
type: "saveChanges",
changes: parseChanges(obj.changes),
changesIndex: ooChannel.cpIndex || 0,
locks: getUserLock(getId()),
excelAdditionalInfo: null
locks: getUserLock(getId(), true),
excelAdditionalInfo: obj.excelAdditionalInfo
}, null, function (err, hash) {
if (err) {
return void console.error(err);
@ -1124,6 +1170,7 @@ define([
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);
@ -1135,7 +1182,11 @@ define([
APP.chan = chan;
var send = ooChannel.send = function (obj, force) {
if (APP.onStrictSaveChanges && !force) { return; } // can't push to OO before reloading cp
// 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; }
debug(obj, 'toOO');
chan.event('CMD', obj);
};
@ -1164,6 +1215,17 @@ define([
}
}
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;
@ -1174,12 +1236,14 @@ define([
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: getUserLock(getId()),
excelAdditionalInfo: null
locks: type === "sheet" ? [] : APP.unsavedLocks,
excelAdditionalInfo: null,
recover: true
};
APP.onStrictSaveChanges();
return;
@ -1233,6 +1297,7 @@ define([
if (APP.ooconfig && !force) { return void console.error('already started'); }
var url = URL.createObjectURL(blob);
var lock = !APP.history && (APP.migrate);
var type = common.getMetadataMgr().getPrivateData().ooType;
// Starting from version 3, we can use the view mode again
// defined but never used
@ -1274,20 +1339,37 @@ define([
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; }' +
//'#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:
'#asc-gen566 { display: none !important; }' + // Insert image from url
'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
'.statusbar .cnt-lang { display: none !important; }' + // Spellcheck language
'.statusbar #btn-doc-spell { display: none !important; }' + // Spellcheck button
'#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 }';
@ -1436,6 +1518,29 @@ define([
};
*/
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();
};
@ -1457,6 +1562,25 @@ define([
// 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();
@ -1479,6 +1603,13 @@ define([
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];
@ -1754,10 +1885,10 @@ define([
if (type === "sheet" && extension !== 'xlsx') {
xlsData = x2tConvertDataInternal(x2t, data, filename, 'xlsx');
filename += '.xlsx';
} else if (type === "ooslide" && extension !== "pptx") {
} else if (type === "presentation" && extension !== "pptx") {
xlsData = x2tConvertDataInternal(x2t, data, filename, 'pptx');
filename += '.pptx';
} else if (type === "oodoc" && extension !== "docx") {
} else if (type === "doc" && extension !== "docx") {
xlsData = x2tConvertDataInternal(x2t, data, filename, 'docx');
filename += '.docx';
}
@ -1781,9 +1912,9 @@ define([
var ext = ['.xlsx', '.ods', '.bin', '.csv', '.pdf'];
var type = common.getMetadataMgr().getPrivateData().ooType;
var warning = '';
if (type==="ooslide") {
if (type==="presentation") {
ext = ['.pptx', /*'.odp',*/ '.bin'];
} else if (type==="oodoc") {
} else if (type==="doc") {
ext = ['.docx', /*'.odt',*/ '.bin'];
}
@ -2006,10 +2137,10 @@ define([
case 'sheet' :
newText = EmptyCell(useNewDefault);
break;
case 'oodoc':
case 'doc':
newText = EmptyDoc();
break;
case 'ooslide':
case 'presentation':
newText = EmptySlide();
break;
default:
@ -2066,7 +2197,8 @@ define([
var setStrictEditing = function () {
if (APP.isFast) { return; }
var editor = getEditor();
var editing = editor.asc_isDocumentModified();
var isModified = editor.asc_isDocumentModified || editor.isDocumentModified;
var editing = isModified();
if (editing) {
evOnPatch.fire();
} else {
@ -2222,6 +2354,7 @@ define([
$contentContainer: $('#cp-app-oo-container')
};
toolbar = APP.toolbar = Toolbar.create(configTb);
toolbar.showColors();
Title.setToolbar(toolbar);
if (window.CP_DEV_MODE) {
@ -2368,9 +2501,9 @@ define([
var type = privateData.ooType;
var accept = [".bin", ".ods", ".xlsx"];
if (type === "ooslide") {
if (type === "presentation") {
accept = ['.bin', '.odp', '.pptx'];
} else if (type === "oodoc") {
} else if (type === "doc") {
accept = ['.bin', '.odt', '.docx'];
}
if (!supportsXLSX()) {
@ -2463,7 +2596,8 @@ define([
$(APP.helpMenu.menu).after(msg);
readOnly = true;
}
} else if (content && content.version === 2) {
} 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) {
@ -2508,6 +2642,44 @@ define([
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);
@ -2551,6 +2723,8 @@ define([
var wasMigrating = content.migration;
var myLocks = getUserLock(getId(), true);
content = json.content;
if (content.saveLock && wasLocked !== content.saveLock) {
@ -2576,8 +2750,12 @@ define([
checkNewCheckpoint();
});
};
if (editor.asc_isDocumentModified()) {
var isModified = editor.asc_isDocumentModified || function () {
return editor.isDocumentModify;
};
if (isModified()) {
setEditable(false);
APP.unsavedLocks = myLocks;
APP.onStrictSaveChanges = function () {
reload();
delete APP.onStrictSaveChanges;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save