diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9f649883d..d267f497c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,96 @@
+# ZyzomysPedunculatus (3.25.0)
+
+## Goals
+
+This is the last major release of our 3.0.0 release cycle. We wanted to mark the occasion with some big improvements to keep everyone happy in case we need to take some more time to prepare our upcoming 4.0.0 release.
+
+## Update notes
+
+This update introduces some major database optimizations that should decrease both CPU and disk usage over time as users request resources and prime an on-disk cache for the next time.
+
+We've also introduce the ability to archive illegal or otherwise objectionable material from the admin panel assuming you possess the ability to load the content in question. It's also possible to restore archived content via an adjacent form field on the admin panel as long as it has not been permanently deleted. Due to a quirk in how ownership of uploaded files works, restored files will not retain their "owners" property. We hope to fix this in a future release.
+
+We've also made some minor changes to the example NGINX config file provided in `cryptpad/docs/example.nginx.confg`, specifically in [this commit](https://github.com/xwiki-labs/cryptpad/commit/2647acbb78643e651b71d2d4f74c2f66e264a258). CryptPad will probably work if you don't apply these changes to your nginx conf, but some functional improvements depend on the exposed headers.
+
+To upgrade from 3.24.0 to 3.25.0:
+
+1. Update your NGINX config as mentioned above.
+2. Stop your nodejs server.
+3. Pull the latest code using git (from the `3.25.0` tag or the `main` branch)
+4. Ensure you have the latest clientside and serverside dependencies with `bower update` and `npm install`.
+5. Restart the nodejs server.
+
+## Features
+
+* This release makes a lot of changes to how content is loaded over the network.
+ * Most notably, CryptPad now employs a client-side cache based on the the _indexedDB API_. Browsers that support this functionality will opportunistically store messages in a local cache for the next time they need them. This should make a considerable difference in how quickly you're able to load a pad, particularly if you accessing the server over a low-bandwidth network.
+ * Uploaded files (images, PDFs, etc.) are also cached in a similar way. Once you'd loaded an asset, your client will prefer to load its local copy instead of the server.
+ * We've updated the code for our _full drive backup_ functionality so that it uses the local cache to load files more quickly. In addition to this, backing up the contents of your drive will also populate the cache as though you had loaded your documents in the normal fashion. This cache will persist until it is invalidated (due to the authoritative document having been deleted or had its history trimmed) or until you have logged out.
+ * We've added the ability to configure the maximum size for automatically downloaded files. Any encrypted files that are above this size will instead require manual interaction to begin downloading. Files that are larger than this limit which are already loaded in your cache will still be automatically displayed.
+* We've also changed a lot of the UI related to encrypted file uploads and downloads:
+ * Encrypted files can display buttons instead of the intended media under a variety of circumstances (if they are larger than your configured limit or if there is no applicable rendering mode). The styles for these buttons are now much more consistent with those found throughout the rest of the platform.
+ * The same assets should now display progress bars when downloading and decrypting encrypted media.
+ * When the same asset is embedded into a document in more than one location it used to be possible to trigger two (or more) concurrent decryption processes. We've modified the rendering process so that duplicates are detected and rendered simultaneously after the relevant assets have been decrypted (once).
+ * We noticed that some old code to filter out forbidden content from rich text pads was interfering with encrypted media. We've clarified the filtering rules to preserve such content (audio, video, iframes) when it occurs within an acceptable context.
+ * We've fixed some inconsistencies with media styles and functionality across different editors. Most types of media now allow you to right-click and choose to _share_ (open that asset's share menu) or open it in a different context (in the file app or in the relevant editor where this behaviour is supported).
+ * The _file_ app has been greatly simplified. It now uses the same methods to render encrypted media as is used elsewhere, so it also displays progress and has a more consistent UI.
+ * The file uploads/downloads table has also been improved somewhat:
+ * Download progress is displayed for groups of items when downloading a folder from your drive.
+ * We found and removed a hard-coded translation from the table's header.
+* In keeping with the theme of network traffic and files we've also made some improvements to policies for users' storage:
+ * Users should now be prompted to trim the history of very large documents when viewing them, saving space for the server operator as well as freeing up some of the user's quota.
+ * Users will also be prompted to use similar functionality available through the settings page when the history of their drive and other account-related functionality is consuming a significant amount of their quota.
+ * Documents that you own used to be automatically added to your drive when viewed if they weren't already present. This was originally intended as an integrity check and a means to recover from incorrectly removed entries in your drive, however, as we now support the removal of owned elements from your drive without destroying them this only serves as an annoyance. As such, we have dropped this functionality.
+ * The whiteboard editor allows users to insert encrypted images into whiteboards, but only up to a certain size. Before it would just warn you that your image was too large. Now it provides the actual size limit that you've exceeded.
+ * The prompt to store uploads in your drive is now suppressed when uploading images via the support ticket panel.
+
+## Bug fixes
+
+* This release includes a fix for a very severe bug in Chrome and its derivatives where attempting to open a URL from within our sandboxing system would crash the browser entirely. This version works around the problem by _not doing that_.
+* We've improved offline detection such that "offline" status is specific to particular resources like your drive, teams, and shared folders rather than treating your account as simply "online or offline".
+* We've optimized one of our less style sheet mixins that was used in a lot of places at a more specific scope than was necessary. This resulted in more time compiling styles and higher storage space requirements for the css cache in localStorage.
+* A small helper function that was intended to stop listening for `enter` and `esc` keypresses after closing a modal was overly zealous and stopped listening after _any keypress_. This made it so that any prompt with an input field did not correctly submit or cancel when pressing `enter` or `esc` after typing some text.
+* Various browsers now require the request for the permission to send notifications to originate from a "click" event, so CryptPad now opens a dialog prompting you to allow (or disallow) permission if you haven't already made that decision.
+* Modern browsers commonly prevent tabs from opening new windows unless you've explicitly enabled that behaviour (it's an important feature), however, in some cases the indication that a new tab was blocked can be very subtle and some of our users did not notice it. We now check whether attempts to open a new tab were successful, and prompt the user to enable this behaviour so that CryptPad can perform regular actions like opening a pad from the drive.
+* After some deep investigation we identified a number of scenarios where contact requests would behave incorrectly, such as not triggering a notification. Contact requests should now be much more stable. On a related note, it's now possible to cancel a pending contact request from the concerned user's profile.
+
+# YunnanLakeNewt (3.24.0)
+
+## Goals
+
+We are once again working to develop some significant new features. This release is fairly small but includes some significant changes to detect and handle a variety of errors.
+
+## Update notes
+
+This release includes some minor corrections the recommended NGINX configuration supplied in `cryptpad/docs/example.nginx.conf`.
+
+To update from 3.23.2 to 3.24.0:
+
+1. Update your NGINX config to replicate the most recent changes and reload NGINX to apply them.
+2. Stop the nodejs server.
+3. Pull the latest code from the `3.24.0` tag or the `main` branch using `git`.
+4. Ensure you have the latest clientside and serverside dependencies with `bower update` and `npm install`.
+5. Restart the nodejs server.
+
+## Features
+
+* A variety of CryptPad's pages now feature a much-improved loading screen which provides a more informative account of what is being loaded. It also implements some generic error handling to detect and report when something has failed in a catastrophic way. This is intended to both inform users that the page is in a broken state as well as to improve the quality of the debugging information they can provide to us so that we can fix the underlying cause.
+* It is now possible to create spreadsheets from templates. Template functionality has existed for a long time in our other editors, however, OnlyOffice's architecture differs significantly and required the implementation of a wholly different system.
+* One user reported some confusion regarding the use of the Kanban app's _tag_ functionality. We've updated the UI to be a little more informative.
+* The "table of contents" in rich text pads now includes "anchors" created via the editor's toolbar.
+
+## Bug fixes
+
+* Recent changes to CryptPad's recommended CSP headers enabled Firefox to export spreadsheets to XLSX format, but they also triggered some regressions due to a number of incompatible APIs.
+ * Our usage of the `sessionStorage` for the purpose of passing important information to editors opened in a new tab stopped working. This meant that when you created a document in a folder, the resulting new tab would not receive the argument describing where it should be stored, and would instead save it to the default location. We've addressed this by replacing our usage of sessionStorage with a new format for passing the same arguments via the hash in the new document's URL.
+ * The `window.print` API also failed in a variety of cases. We've updated the relevant CSP headers to only be applied on the sheet editor (to support XSLX export) but allow printing elsewhere. We've also updated some print styles to provide more appealing results.
+* The table of contents available in rich text pads failed to scroll when there were a sufficient number of heading to flow beyond the length of the page. Now a scrollbar appears when necessary.
+* We discovered a number of cases where the presence of an allow list prevented some valid behaviour due to the server incorrectly concluding that users were not authenticated. We've improved the client's ability to detect these cases and re-authenticate when necessary.
+* We also found that when the server was under very heavy load some database queries were timing out because they were slow (but not stopped). We've addressed this to only terminate such queries if they have been entirely inactive for several minutes.
+* It was possible for "safe links" to include a mode ("edit" or "view") which did not match the rights of the user opening them. For example, if a user loaded a safe link with edit rights though they only had read-only access via their "viewer" role in a team. CryptPad will now recover from such cases and open the document with the closest set of access rights that they possess.
+* We found that the server query `"IS_NEW_PAD"` could return an error but that clients would incorrectly interpret such a response as a `false`. This has been corrected.
+* Finally, we've modified the "trash" UI for user and team drives such that when users attempt to empty their trash of owned shared folders they are prompted to remove the items or delete them from the server entirely, as they would be with other owned assets.
+
# XerusDaamsi reloaded (3.23.2)
A number of instance administrators reported issues following our 3.23.1 release. We suspect the issues were caused by applying the recommended update steps out of order which would result in the incorrect HTTP header values getting cached for the most recent version of a file. Since the most recently updated headers modified some security settings, this caused a catastrophic error on clients receiving the incorrect headers which caused them to fail to load under certain circumstances.
diff --git a/bower.json b/bower.json
index bd9cebdd6..f9a3dc5be 100644
--- a/bower.json
+++ b/bower.json
@@ -30,7 +30,7 @@
"secure-fabric.js": "secure-v1.7.9",
"hyperjson": "~1.4.0",
"chainpad-crypto": "^0.2.0",
- "chainpad-listmap": "^0.9.0",
+ "chainpad-listmap": "^0.10.0",
"chainpad": "^5.2.0",
"file-saver": "1.3.1",
"alertifyjs": "1.0.11",
diff --git a/config/config.example.js b/config/config.example.js
index 32bcb20cf..a49d66d90 100644
--- a/config/config.example.js
+++ b/config/config.example.js
@@ -42,7 +42,7 @@ module.exports = {
*
* In a production instance this should be available ONLY over HTTPS
* using the default port for HTTPS (443) ie. https://cryptpad.fr
- * In such a case this should be handled by NGINX, as documented in
+ * In such a case this should be also handled by NGINX, as documented in
* cryptpad/docs/example.nginx.conf (see the $main_domain variable)
*
*/
@@ -228,12 +228,12 @@ module.exports = {
*/
/*
customLimits: {
- "https://my.awesome.website/user/#/1/cryptpad-user1/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=": {
+ "[cryptpad-user1@my.awesome.website/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=]": {
limit: 20 * 1024 * 1024 * 1024,
plan: 'insider',
note: 'storage space donated by my.awesome.website'
},
- "https://my.awesome.website/user/#/1/cryptpad-user2/GdflkgdlkjeworijfkldfsdflkjeEAsdlEnkbx1vVOo=": {
+ "[cryptpad-user2@my.awesome.website/GdflkgdlkjeworijfkldfsdflkjeEAsdlEnkbx1vVOo=]": {
limit: 10 * 1024 * 1024 * 1024,
plan: 'insider',
note: 'storage space donated by my.awesome.website'
diff --git a/customize.dist/ckeditor-contents.css b/customize.dist/ckeditor-contents.css
index 000162c00..0d5b5cff7 100644
--- a/customize.dist/ckeditor-contents.css
+++ b/customize.dist/ckeditor-contents.css
@@ -213,3 +213,61 @@ media-tag * {
width: 100%;
height: 100%;
}
+media-tag button.btn {
+ background-color: #fff;
+ box-sizing: border-box;
+ outline: 0;
+ display: inline-flex;
+ align-items: center;
+ padding: 0 6px;
+ min-height: 36px;
+ line-height: 22px;
+ white-space: nowrap;
+ text-align: center;
+ text-transform: uppercase;
+ font-size: 14px;
+ text-decoration: none;
+ cursor: pointer;
+ border-radius: 0;
+ transition: none;
+ color: #3F4141;
+ border: 1px solid #3F4141;
+ max-width: 250px;
+}
+media-tag button.mediatag-download-btn {
+ flex-flow: column;
+ min-height: 38px;
+ justify-content: center;
+}
+media-tag button.mediatag-download-btn > span {
+ display: flex;
+ line-height: 1.5;
+ align-items: center;
+ justify-content: center;
+}
+media-tag button.mediatag-download-btn * {
+ width: auto;
+}
+media-tag button.mediatag-download-btn > span.mediatag-download-name {
+ max-width: 100%;
+}
+media-tag button.mediatag-download-btn > span.mediatag-download-name b {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+media-tag button.btn:hover, media-tag button.btn:active, media-tag button.btn:focus {
+ background-color: #ccc;
+}
+media-tag button.btn b {
+ margin-left: 5px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+media-tag button.btn .fa {
+ display: inline;
+ margin-right: 5px;
+ flex: 0;
+}
diff --git a/customize.dist/loading.js b/customize.dist/loading.js
index 29e5ac92b..af73a9b42 100644
--- a/customize.dist/loading.js
+++ b/customize.dist/loading.js
@@ -253,7 +253,7 @@ p.cp-password-info{
animation-timing-function: cubic-bezier(.6,0.15,0.4,0.85);
}
-button.primary{
+button:not(.btn).primary{
border: 1px solid #4591c4;
padding: 8px 12px;
text-transform: uppercase;
@@ -262,7 +262,7 @@ button.primary{
font-weight: bold;
}
-button.primary:hover{
+button:not(.btn).primary:hover{
background-color: rgb(52, 118, 162);
}
@@ -291,7 +291,7 @@ button.primary:hover{
var built = false;
var types = ['less', 'drive', 'migrate', 'sf', 'team', 'pad', 'end'];
- var current;
+ var current, progress;
var makeList = function (data) {
var c = types.indexOf(data.type);
current = c;
@@ -307,7 +307,7 @@ button.primary:hover{
};
var list = '
';
types.forEach(function (el, i) {
- if (i >= 6) { return; }
+ if (el === "end") { return; }
list += getLi(i);
});
list += '
';
@@ -315,7 +315,7 @@ button.primary:hover{
};
var makeBar = function (data) {
var c = types.indexOf(data.type);
- var l = types.length;
+ var l = types.length - 1; // don't count "end" as a type
var progress = Math.min(data.progress, 100);
var p = (progress / l) + (100 * c / l);
var bar = '
'+
@@ -327,20 +327,34 @@ button.primary:hover{
var hasErrored = false;
var updateLoadingProgress = function (data) {
if (!built || !data) { return; }
+
+ // Make sure progress doesn't go backward
var c = types.indexOf(data.type);
- if (c < current) { return console.error(data); }
+ if (c < current) { return console.debug(data); }
+ if (c === current && progress > data.progress) { return console.debug(data); }
+ progress = data.progress;
+
try {
- document.querySelector('.cp-loading-spinner-container').style.display = 'none';
- document.querySelector('.cp-loading-progress-list').innerHTML = makeList(data);
- document.querySelector('.cp-loading-progress-container').innerHTML = makeBar(data);
+ var el1 = document.querySelector('.cp-loading-spinner-container');
+ if (el1) { el1.style.display = 'none'; }
+ var el2 = document.querySelector('.cp-loading-progress-list');
+ if (el2) { el2.innerHTML = makeList(data); }
+ var el3 = document.querySelector('.cp-loading-progress-container');
+ if (el3) { el3.innerHTML = makeBar(data); }
} catch (e) {
- if (!hasErrored) { console.error(e); }
+ //if (!hasErrored) { console.error(e); }
}
};
window.CryptPad_updateLoadingProgress = updateLoadingProgress;
window.CryptPad_loadingError = function (err) {
if (!built) { return; }
+
+ if (err === 'Error: XDR encoding failure') {
+ console.warn(err);
+ return;
+ }
+
hasErrored = true;
var err2;
if (err === 'Script error.') {
diff --git a/customize.dist/pages.js b/customize.dist/pages.js
index fe8ac73fd..4cc7c59b0 100644
--- a/customize.dist/pages.js
+++ b/customize.dist/pages.js
@@ -69,7 +69,7 @@ define([
Msg.footer_team = "Contributors"; // XXX existing key
Msg.footer_tos = "Terms of Service"; // XXX existing key
- Pages.versionString = "v3.24.0 (YunnanLakeNewt)";
+ Pages.versionString = "v3.25.0 (ZyzomysPedunculatus)";
// used for the about menu
Pages.imprintLink = AppConfig.imprint ? footLink(imprintUrl, 'imprint') : undefined;
diff --git a/customize.dist/src/less2/include/fileupload.less b/customize.dist/src/less2/include/fileupload.less
index 0d39b342a..d2f2fff18 100644
--- a/customize.dist/src/less2/include/fileupload.less
+++ b/customize.dist/src/less2/include/fileupload.less
@@ -14,7 +14,7 @@
right: 10vw;
bottom: 10vh;
box-sizing: border-box;
- z-index: 100000; //Z file upload table container
+ z-index: 100001; //Z file upload table container: just above the file picker
display: none;
color: darken(@colortheme_static_apps[default], 10%);
max-height: 180px;
diff --git a/customize.dist/src/less2/include/forms.less b/customize.dist/src/less2/include/forms.less
index cad9427de..b6d909a73 100644
--- a/customize.dist/src/less2/include/forms.less
+++ b/customize.dist/src/less2/include/forms.less
@@ -2,6 +2,10 @@
@import (reference) "./variables.less";
.forms_main() {
+ --LessLoader_require: LessLoader_currentFile();
+}
+
+& {
@alertify-fore: @colortheme_modal-fg;
@alertify-btn-fg: @alertify-fore;
@alertify-light-bg: fade(@alertify-fore, 25%);
@@ -114,7 +118,9 @@
margin: 0;
}
- &:hover, &:active, &:focus {
+ &:hover, &:not(:disabled):not(.disabled):active, &:focus {
+ color: @alertify-btn-fg;
+ border: 1px solid @alertify-btn-fg;
background-color: lighten(@alertify-fore, 35%);
}
@@ -124,19 +130,32 @@
font-weight: bold;
}
+ &.btn-default {
+ border-color: @cryptpad_text_col;
+ color: @cryptpad_text_col;
+ &:hover, &:not(:disabled):active, &:focus {
+ border-color: @cryptpad_text_col;
+ color: @cryptpad_text_col;
+ background-color: #ccc;
+ }
+ }
+
&.danger, &.btn-danger {
background-color: @colortheme_alertify-red;
border-color: @colortheme_alertify-red-border;
color: @colortheme_alertify-red-color;
- &:hover, &:active, &:focus {
+ &:hover, &:not(:disabled):active, &:focus {
+ border-color: @colortheme_alertify-red-border;
+ color: @colortheme_alertify-red-color;
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-red, 10%), lighten(@colortheme_alertify-red, 10%));
}
}
- &.danger-alt, &.btn-danger-alt {
+ &.danger-alt, &.btn-danger-alt, &.btn-danger-outline {
border-color: @colortheme_alertify-red;
color: @colortheme_alertify-red;
- &:hover, &:active, &:focus {
+ &:hover, &:not(:disabled):active, &:focus {
+ border-color: @colortheme_alertify-red;
color: @colortheme_alertify-red-color;
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-red, 10%), lighten(@colortheme_alertify-red, 10%));
}
@@ -146,17 +165,21 @@
background-color: @colortheme_alertify-green;
border-color: @colortheme_alertify-green-border;
color: @colortheme_alertify-green-color;
- &:hover, &:active, &:focus {
+ &:hover, &:not(:disabled):active, &:focus {
+ border-color: @colortheme_alertify-green-border;
+ color: @colortheme_alertify-green-color;
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-green, 10%), lighten(@colortheme_alertify-green, 10%));
}
}
- &.primary, &.btn-primary {
+ &.primary, &.btn-primary, &.btn-success {
background-color: @colortheme_alertify-primary;
color: @colortheme_alertify-primary-text;
border-color: @colortheme_alertify-primary-border;
font-weight: bold;
- &:hover, &:active, &:focus {
+ &:hover, &:not(:disabled):active, &:focus {
+ color: @colortheme_alertify-primary-text;
+ border-color: @colortheme_alertify-primary-border;
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-primary, 10%), lighten(@colortheme_alertify-primary, 10%));
}
}
@@ -165,7 +188,9 @@
border-color: @cryptpad_text_col;
color: @cryptpad_text_col;
background-color: transparent;
- &:hover, &:hover, &:focus {
+ &:hover, &:not(:disabled):active, &:focus {
+ border-color: @cryptpad_text_col;
+ color: @cryptpad_text_col;
background-color: fade(@cryptpad_text_col, 25%);
}
}
@@ -173,7 +198,9 @@
&.cancel, &.btn-cancel {
border-color: @colortheme_alertify-cancel-border;
color: @colortheme_alertify-cancel-border;
- &:hover, &:hover, &:focus {
+ &:hover, &:not(:disabled):active, &:focus {
+ border-color: @colortheme_alertify-cancel-border;
+ color: @colortheme_alertify-cancel-border;
background-color: fade(@colortheme_alertify-cancel-border, 25%);
}
}
@@ -185,7 +212,7 @@
&:focus {
//border: 1px dotted @alertify-base;
- box-shadow: 0px 0px 5px @colortheme_alertify-primary;
+ box-shadow: 0px 0px 5px @colortheme_alertify-primary !important;
outline: none;
}
&::-moz-focus-inner {
@@ -202,5 +229,4 @@
}
}
}
-
}
diff --git a/customize.dist/src/less2/include/markdown.less b/customize.dist/src/less2/include/markdown.less
index 23eb056b0..37fd4fce8 100644
--- a/customize.dist/src/less2/include/markdown.less
+++ b/customize.dist/src/less2/include/markdown.less
@@ -64,6 +64,57 @@
}
}
+.mediatag_cryptpad() {
+ media-tag {
+ &:empty {
+ display: none !important;
+ }
+ cursor: pointer;
+ * {
+ max-width: 100%;
+ }
+ iframe[src$=".pdf"] {
+ width: 100%;
+ height: 80vh;
+ max-height: 90vh;
+ }
+ button.mediatag-download-btn {
+ flex-flow: column;
+ & > span {
+ display: flex;
+ line-height: 1.5;
+ align-items: center;
+ &.mediatag-download-name b {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+ }
+ button.btn-default {
+ display: inline-flex;
+ max-width: 250px;
+ min-height: 38px;
+ justify-content: center;
+ .fa {
+ margin-right: 5px;
+ }
+ b {
+ margin-left: 5px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+ }
+ media-tag:empty {
+ width: 100px;
+ height: 100px;
+ display: inline-block;
+ border: 1px solid #BBB;
+ }
+}
+
.markdown_cryptpad() {
word-wrap: break-word;
@@ -84,23 +135,8 @@
margin-top: 4px;
}
}
- media-tag {
- cursor: pointer;
- * {
- max-width: 100%;
- }
- iframe[src$=".pdf"] {
- width: 100%;
- height: 80vh;
- max-height: 90vh;
- }
- }
- media-tag:empty {
- width: 100px;
- height: 100px;
- display: inline-block;
- border: 1px solid #BBB;
- }
+
+ .mediatag_cryptpad();
pre.markmap {
border: 1px solid #ddd;
diff --git a/customize.dist/src/less2/include/modals-ui-elements.less b/customize.dist/src/less2/include/modals-ui-elements.less
index 27eb233da..ff40729a6 100644
--- a/customize.dist/src/less2/include/modals-ui-elements.less
+++ b/customize.dist/src/less2/include/modals-ui-elements.less
@@ -1,6 +1,7 @@
@import (reference) "./colortheme-all.less";
@import (reference) "./variables.less";
@import (reference) "./browser.less";
+@import (reference) "./markdown.less";
.modals-ui-elements_main() {
--LessLoader_require: LessLoader_currentFile();
@@ -214,6 +215,7 @@
flex: 1;
min-width: 0;
overflow: auto;
+ .mediatag_cryptpad();
media-tag {
& > * {
max-width: 100%;
diff --git a/customize.dist/src/less2/include/sidebar-layout.less b/customize.dist/src/less2/include/sidebar-layout.less
index ace7350df..4273b0b9a 100644
--- a/customize.dist/src/less2/include/sidebar-layout.less
+++ b/customize.dist/src/less2/include/sidebar-layout.less
@@ -118,7 +118,7 @@
//border-radius: 0 0.25em 0.25em 0;
//border: 1px solid #adadad;
border-left: 0px;
- height: @variables_input-height;
+ height: 40px;
margin: 0 !important;
}
}
diff --git a/customize.dist/src/less2/include/support.less b/customize.dist/src/less2/include/support.less
index d83746b51..105599ada 100644
--- a/customize.dist/src/less2/include/support.less
+++ b/customize.dist/src/less2/include/support.less
@@ -78,7 +78,7 @@
}
&.cp-support-list-closed {
.cp-support-list-actions {
- display: block !important;
+ display: flex !important;
.cp-support-answer, .cp-support-close {
display: none;
}
diff --git a/customize.dist/src/less2/pages/page-login.less b/customize.dist/src/less2/pages/page-login.less
index 1cc2fcffc..98eafe483 100644
--- a/customize.dist/src/less2/pages/page-login.less
+++ b/customize.dist/src/less2/pages/page-login.less
@@ -2,10 +2,12 @@
@import (reference) "../include/colortheme-all.less";
@import (reference) "../include/alertify.less";
@import (reference) "../include/checkmark.less";
+@import (reference) "../include/forms.less";
&.cp-page-login {
.infopages_main();
.alertify_main();
+ .forms_main();
.checkmark_main(20px);
.form-group {
diff --git a/docs/cryptpad.service b/docs/cryptpad.service
index eee8b2af5..43d8652f6 100644
--- a/docs/cryptpad.service
+++ b/docs/cryptpad.service
@@ -17,7 +17,7 @@ SyslogIdentifier=cryptpad
User=cryptpad
Group=cryptpad
# modify to match your working directory
-Environment='PWD="/home/cryptpad/cryptpad/cryptpad"'
+Environment='PWD="/home/cryptpad/cryptpad"'
# systemd sets the open file limit to 4000 unless you override it
# cryptpad stores its data with the filesystem, so you should increase this to match the value of `ulimit -n`
diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf
index 8319c657b..2c677436b 100644
--- a/docs/example.nginx.conf
+++ b/docs/example.nginx.conf
@@ -32,6 +32,9 @@ server {
server_name your-main-domain.com your-sandbox-domain.com;
# You'll need to Set the path to your certificates and keys here
+ # IMPORTANT: this config is intended to serve assets for at least two domains
+ # (your main domain and your sandbox domain). As such, you'll need to generate a single SSL certificate
+ # that includes both domains in order for things to work as expected.
ssl_certificate /home/cryptpad/.acme.sh/your-main-domain.com/fullchain.cer;
ssl_certificate_key /home/cryptpad/.acme.sh/your-main-domain.com/your-main-domain.com.key;
ssl_trusted_certificate /home/cryptpad/.acme.sh/your-main-domain.com/ca.cer;
@@ -177,8 +180,8 @@ server {
add_header Cache-Control max-age=31536000;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
- add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
- add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
+ add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Content-Length';
+ add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Content-Length';
try_files $uri =404;
}
diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js
index d7f22825d..595b4c6dc 100644
--- a/lib/commands/admin-rpc.js
+++ b/lib/commands/admin-rpc.js
@@ -56,6 +56,11 @@ var getCacheStats = function (env, server, cb) {
});
};
+// CryptPad_AsyncStore.rpc.send('ADMIN', ['GET_WORKER_PROFILES'], console.log)
+var getWorkerProfiles = function (Env, Server, cb) {
+ cb(void 0, Env.commandTimers);
+};
+
var getActiveSessions = function (Env, Server, cb) {
var stats = Server.getSessionStats();
cb(void 0, [
@@ -155,9 +160,19 @@ var archiveDocument = function (Env, Server, cb, data) {
switch (id.length) {
case 32:
// TODO disconnect users from active sessions
- return void Env.msgStore.archiveChannel(id, cb);
+ return void Env.msgStore.archiveChannel(id, Util.both(cb, function (err) {
+ Env.Log.info("ARCHIVAL_CHANNEL_BY_ADMIN_RPC", {
+ channelId: id,
+ status: err? String(err): "SUCCESS",
+ });
+ }));
case 48:
- return void Env.blobStore.archive.blob(id, cb);
+ return void Env.blobStore.archive.blob(id, Util.both(cb, function (err) {
+ Env.Log.info("ARCHIVAL_BLOB_BY_ADMIN_RPC", {
+ id: id,
+ status: err? String(err): "SUCCESS",
+ });
+ }));
default:
return void cb("INVALID_ID_LENGTH");
}
@@ -167,12 +182,30 @@ var archiveDocument = function (Env, Server, cb, data) {
// Env.blobStore.archive.proof(userSafeKey, blobId, cb)
};
-var restoreArchivedDocument = function (Env, Server, cb) {
- // Env.msgStore.restoreArchivedChannel(channelName, cb)
- // Env.blobStore.restore.blob(blobId, cb)
- // Env.blobStore.restore.proof(userSafekey, blobId, cb)
+var restoreArchivedDocument = function (Env, Server, cb, data) {
+ var id = Array.isArray(data) && data[1];
+ if (typeof(id) !== 'string' || id.length < 32) { return void cb("EINVAL"); }
- cb("NOT_IMPLEMENTED");
+ switch (id.length) {
+ case 32:
+ return void Env.msgStore.restoreArchivedChannel(id, Util.both(cb, function (err) {
+ Env.Log.info("RESTORATION_CHANNEL_BY_ADMIN_RPC", {
+ id: id,
+ status: err? String(err): 'SUCCESS',
+ });
+ }));
+ case 48:
+ // FIXME this does not yet restore blob ownership
+ // Env.blobStore.restore.proof(userSafekey, id, cb)
+ return void Env.blobStore.restore.blob(id, Util.both(cb, function (err) {
+ Env.Log.info("RESTORATION_BLOB_BY_ADMIN_RPC", {
+ id: id,
+ status: err? String(err): 'SUCCESS',
+ });
+ }));
+ default:
+ return void cb("INVALID_ID_LENGTH");
+ }
};
// CryptPad_AsyncStore.rpc.send('ADMIN', ['CLEAR_CACHED_CHANNEL_INDEX', documentID], console.log)
@@ -315,6 +348,7 @@ var commands = {
INSTANCE_STATUS: instanceStatus,
GET_LIMITS: getLimits,
SET_LAST_EVICTION: setLastEviction,
+ GET_WORKER_PROFILES: getWorkerProfiles,
};
Admin.command = function (Env, safeKey, data, _cb, Server) {
diff --git a/lib/commands/metadata.js b/lib/commands/metadata.js
index 3d20f36e6..896c89f31 100644
--- a/lib/commands/metadata.js
+++ b/lib/commands/metadata.js
@@ -13,11 +13,19 @@ Data.getMetadataRaw = function (Env, channel /* channelName */, _cb) {
var cached = Env.metadata_cache[channel];
if (HK.isMetadataMessage(cached)) {
+ Env.checkCache(channel);
return void cb(void 0, cached);
}
Env.batchMetadata(channel, cb, function (done) {
- Env.computeMetadata(channel, done);
+ Env.computeMetadata(channel, function (err, meta) {
+ if (!err && HK.isMetadataMessage(meta)) {
+ Env.metadata_cache[channel] = meta;
+ // clear metadata after a delay if nobody has joined the channel within 30s
+ Env.checkCache(channel);
+ }
+ done(err, meta);
+ });
});
};
diff --git a/lib/commands/upload.js b/lib/commands/upload.js
index 7286caa93..346262716 100644
--- a/lib/commands/upload.js
+++ b/lib/commands/upload.js
@@ -75,21 +75,9 @@ Upload.upload = function (Env, safeKey, chunk, cb) {
Env.blobStore.upload(safeKey, chunk, cb);
};
-var reportStatus = function (Env, label, safeKey, err, id) {
- var data = {
- safeKey: safeKey,
- err: err && err.message || err,
- id: id,
- };
- var method = err? 'error': 'info';
- Env.Log[method](label, data);
-};
-
Upload.complete = function (Env, safeKey, arg, cb) {
- Env.blobStore.complete(safeKey, arg, function (err, id) {
- reportStatus(Env, 'UPLOAD_COMPLETE', safeKey, err, id);
- cb(err, id);
- });
+ Env.blobStore.closeBlobstage(safeKey);
+ Env.completeUpload(safeKey, arg, false, cb);
};
Upload.cancel = function (Env, safeKey, arg, cb) {
@@ -97,9 +85,9 @@ Upload.cancel = function (Env, safeKey, arg, cb) {
};
Upload.complete_owned = function (Env, safeKey, arg, cb) {
- Env.blobStore.completeOwned(safeKey, arg, function (err, id) {
- reportStatus(Env, 'UPLOAD_COMPLETE_OWNED', safeKey, err, id);
- cb(err, id);
- });
+ Env.blobStore.closeBlobstage(safeKey);
+ var user = Core.getSession(Env.Sessions, safeKey);
+ var size = user.pendingUploadSize;
+ Env.completeUpload(safeKey, arg, true, size, cb);
};
diff --git a/lib/env.js b/lib/env.js
index b1fc6680b..322f629c6 100644
--- a/lib/env.js
+++ b/lib/env.js
@@ -42,6 +42,8 @@ module.exports.create = function (config) {
metadata_cache: {},
channel_cache: {},
+ cache_checks: {},
+
queueStorage: WriteQueue(),
queueDeletes: WriteQueue(),
queueValidation: WriteQueue(),
@@ -94,6 +96,7 @@ module.exports.create = function (config) {
disableIntegratedEviction: config.disableIntegratedEviction || false,
lastEviction: +new Date(),
evictionReport: {},
+ commandTimers: {},
};
(function () {
@@ -116,8 +119,14 @@ module.exports.create = function (config) {
}
}());
-
-
+ Env.checkCache = function (channel) {
+ var f = Env.cache_checks[channel] || Util.throttle(function () {
+ delete Env.cache_checks[channel];
+ if (Env.channel_cache[channel]) { return; }
+ delete Env.metadata_cache[channel];
+ }, 30000);
+ f();
+ };
(function () {
var custom = config.customLimits;
diff --git a/lib/hk-util.js b/lib/hk-util.js
index 14263e481..495f4ff81 100644
--- a/lib/hk-util.js
+++ b/lib/hk-util.js
@@ -419,9 +419,11 @@ const getHistoryOffset = (Env, channelName, lastKnownHash, _cb) => {
// fall through to the next block if the offset of the hash in question is not in memory
if (lastKnownHash && typeof(lkh) !== "number") { return; }
+ // If we have a lastKnownHash or we didn't ask for one, we don't need the next blocks
+ waitFor.abort();
+
// Since last 2 checkpoints
if (!lastKnownHash) {
- waitFor.abort();
// Less than 2 checkpoints in the history: return everything
if (index.cpIndex.length < 2) { return void cb(null, 0); }
// Otherwise return the second last checkpoint's index
@@ -436,7 +438,15 @@ const getHistoryOffset = (Env, channelName, lastKnownHash, _cb) => {
to reconcile their differences. */
}
- offset = lkh;
+ // If our lastKnownHash is older than the 2nd to last checkpoint, send
+ // EUNKNOWN to tell the user to empty their cache
+ if (lkh && index.cpIndex.length >= 2 && lkh < index.cpIndex[0].offset) {
+ waitFor.abort();
+ return void cb(new Error('EUNKNOWN'));
+ }
+
+ // Otherwise use our lastKnownHash
+ cb(null, lkh);
}));
}).nThen((w) => {
// skip past this block if the offset is anything other than -1
diff --git a/lib/storage/blob.js b/lib/storage/blob.js
index dfbc802b4..044eeaeaa 100644
--- a/lib/storage/blob.js
+++ b/lib/storage/blob.js
@@ -139,6 +139,15 @@ var upload = function (Env, safeKey, content, cb) {
}
};
+var closeBlobstage = function (Env, safeKey) {
+ var session = Env.getSession(safeKey);
+ if (!(session && session.blobstage && typeof(session.blobstage.close) === 'function')) {
+ return;
+ }
+ session.blobstage.close();
+ delete session.blobstage;
+};
+
// upload_cancel
var upload_cancel = function (Env, safeKey, fileSize, cb) {
var session = Env.getSession(safeKey);
@@ -159,27 +168,22 @@ var upload_cancel = function (Env, safeKey, fileSize, cb) {
// upload_complete
var upload_complete = function (Env, safeKey, id, cb) {
- var session = Env.getSession(safeKey);
-
- if (session.blobstage && session.blobstage.close) {
- session.blobstage.close();
- delete session.blobstage;
- }
+ closeBlobstage(Env, safeKey);
var oldPath = makeStagePath(Env, safeKey);
var newPath = makeBlobPath(Env, id);
nThen(function (w) {
// make sure the path to your final location exists
- Fse.mkdirp(Path.dirname(newPath), function (e) {
+ Fse.mkdirp(Path.dirname(newPath), w(function (e) {
if (e) {
w.abort();
return void cb('RENAME_ERR');
}
- });
+ }));
}).nThen(function (w) {
// make sure there's not already something in that exact location
- isFile(newPath, function (e, yes) {
+ isFile(newPath, w(function (e, yes) {
if (e) {
w.abort();
return void cb(e);
@@ -188,8 +192,8 @@ var upload_complete = function (Env, safeKey, id, cb) {
w.abort();
return void cb('RENAME_ERR');
}
- cb(void 0, newPath, id);
- });
+ cb(void 0, id);
+ }));
}).nThen(function () {
// finally, move the old file to the new path
// FIXME we could just move and handle the EEXISTS instead of the above block
@@ -217,15 +221,7 @@ var tryId = function (path, cb) {
// owned_upload_complete
var owned_upload_complete = function (Env, safeKey, id, cb) {
- var session = Env.getSession(safeKey);
-
- // the file has already been uploaded to the staging area
- // close the pending writestream
- if (session.blobstage && session.blobstage.close) {
- session.blobstage.close();
- delete session.blobstage;
- }
-
+ closeBlobstage(Env, safeKey);
if (!isValidId(id)) {
return void cb('EINVAL_ID');
}
@@ -582,6 +578,9 @@ BlobStore.create = function (config, _cb) {
},
},
+ closeBlobstage: function (safeKey) {
+ closeBlobstage(Env, safeKey);
+ },
complete: function (safeKey, id, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (!isValidSafeKey(safeKey)) { return void cb('INVALID_SAFEKEY'); }
diff --git a/lib/storage/file.js b/lib/storage/file.js
index b1ccfde0f..d890cb0b9 100644
--- a/lib/storage/file.js
+++ b/lib/storage/file.js
@@ -1,6 +1,6 @@
/*@flow*/
/* jshint esversion: 6 */
-/* global Buffer */
+/* globals Buffer */
var Fs = require("fs");
var Fse = require("fs-extra");
var Path = require("path");
@@ -66,6 +66,10 @@ var mkTempPath = function (env, channelId) {
return mkPath(env, channelId) + '.temp';
};
+var mkOffsetPath = function (env, channelId) {
+ return mkPath(env, channelId) + '.offset';
+};
+
// pass in the path so we can reuse the same function for archived files
var channelExists = function (filepath, cb) {
Fs.stat(filepath, function (err, stat) {
@@ -131,7 +135,9 @@ const readMessagesBin = (env, id, start, msgHandler, cb) => {
const collector = createIdleStreamCollector(stream);
const handleMessageAndKeepStreamAlive = Util.both(msgHandler, collector.keepAlive);
const done = Util.both(cb, collector);
- return void readFileBin(stream, handleMessageAndKeepStreamAlive, done);
+ return void readFileBin(stream, handleMessageAndKeepStreamAlive, done, {
+ offset: start,
+ });
};
// reads classic metadata from a channel log and aborts
@@ -190,6 +196,37 @@ var closeChannel = function (env, channelName, cb) {
}
};
+var clearOffset = function (env, channelId, cb) {
+ var path = mkOffsetPath(env, channelId);
+ // we should always be able to recover from invalid offsets, so failure to delete them
+ // is not catastrophic. Anything calling this function can optionally ignore errors it might report
+ Fs.unlink(path, cb);
+};
+
+var writeOffset = function (env, channelId, data, cb) {
+ var path = mkOffsetPath(env, channelId);
+ var s_data;
+ try {
+ s_data = JSON.stringify(data);
+ } catch (err) {
+ return void cb(err);
+ }
+ Fs.writeFile(path, s_data, cb);
+};
+
+var getOffset = function (env, channelId, cb) {
+ var path = mkOffsetPath(env, channelId);
+ Fs.readFile(path, function (err, content) {
+ if (err) { return void cb(err); }
+ try {
+ var json = JSON.parse(content);
+ cb(void 0, json);
+ } catch (err2) {
+ cb(err2);
+ }
+ });
+};
+
// truncates a file to the end of its metadata line
// TODO write the metadata in a dedicated file
var clearChannel = function (env, channelId, _cb) {
@@ -213,6 +250,7 @@ var clearChannel = function (env, channelId, _cb) {
cb();
});
});
+ clearOffset(env, channelId, function () {});
});
};
@@ -389,6 +427,7 @@ var removeChannel = function (env, channelName, cb) {
CB(labelError("E_METADATA_REMOVAL", err));
}
}));
+ clearOffset(env, channelName, w());
}).nThen(function () {
if (errors === 2) {
return void CB(labelError('E_REMOVE_CHANNEL', new Error("ENOENT")));
@@ -604,6 +643,8 @@ var archiveChannel = function (env, channelName, cb) {
return void cb(err);
}
}));
+ }).nThen(function (w) {
+ clearOffset(env, channelName, w());
}).nThen(function (w) {
// archive the dedicated metadata channel
var metadataPath = mkMetadataPath(env, channelName);
@@ -861,6 +902,7 @@ var trimChannel = function (env, channelName, hash, _cb) {
}
}));
}).nThen(function (w) {
+ clearOffset(env, channelName, w());
cleanUp(w(function (err) {
if (err) {
w.abort();
@@ -1177,6 +1219,25 @@ module.exports.create = function (conf, _cb) {
});
},
+ // OFFSETS
+// these exist strictly as an optimization
+// you can always remove them without data loss
+ clearOffset: function (channelName, _cb) {
+ var cb = Util.once(Util.mkAsync(_cb));
+ if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
+ clearOffset(env, channelName, cb);
+ },
+ writeOffset: function (channelName, data, _cb) {
+ var cb = Util.once(Util.mkAsync(_cb));
+ if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
+ writeOffset(env, channelName, data, cb);
+ },
+ getOffset: function (channelName, _cb) {
+ var cb = Util.once(Util.mkAsync(_cb));
+ if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
+ getOffset(env, channelName, cb);
+ },
+
// METADATA METHODS
// fetch the metadata for a channel
getChannelMetadata: function (channelName, cb) {
diff --git a/lib/stream-file.js b/lib/stream-file.js
index c3130365b..a164ceffc 100644
--- a/lib/stream-file.js
+++ b/lib/stream-file.js
@@ -44,8 +44,8 @@ const mkBufferSplit = () => {
// return a streaming function which transforms buffers into objects
// containing the buffer and the offset from the start of the stream
-const mkOffsetCounter = () => {
- let offset = 0;
+const mkOffsetCounter = (offset) => {
+ offset = offset || 0;
return Pull.map((buff) => {
const out = { offset: offset, buff: buff };
// +1 for the eaten newline
@@ -59,13 +59,14 @@ const mkOffsetCounter = () => {
// that this function has a lower memory profile than our classic method
// of reading logs line by line.
// it also allows the handler to abort reading at any time
-Stream.readFileBin = (stream, msgHandler, cb) => {
+Stream.readFileBin = (stream, msgHandler, cb, opt) => {
+ opt = opt || {};
//const stream = Fs.createReadStream(path, { start: start });
let keepReading = true;
Pull(
ToPull.read(stream),
mkBufferSplit(),
- mkOffsetCounter(),
+ mkOffsetCounter(opt.offset),
Pull.asyncMap((data, moreCb) => {
msgHandler(data, moreCb, () => {
try {
diff --git a/lib/workers/db-worker.js b/lib/workers/db-worker.js
index 42c75fdca..5750ff7ac 100644
--- a/lib/workers/db-worker.js
+++ b/lib/workers/db-worker.js
@@ -1,5 +1,5 @@
/* jshint esversion: 6 */
-/* global process */
+/* globals process, Buffer */
const HK = require("../hk-util");
const Store = require("../storage/file");
@@ -30,6 +30,11 @@ Logger.levels.forEach(function (level) {
};
});
+var DETAIL = 1000;
+var round = function (n) {
+ return Math.floor(n * DETAIL) / DETAIL;
+};
+
var ready = false;
var store;
var pinStore;
@@ -114,14 +119,15 @@ const init = function (config, _cb) {
* including the initial metadata line, if it exists
*/
-const computeIndex = function (data, cb) {
- if (!data || !data.channel) {
- return void cb('E_NO_CHANNEL');
- }
- const channelName = data.channel;
+const OPEN_CURLY_BRACE = Buffer.from('{');
+const CHECKPOINT_PREFIX = Buffer.from('cp|');
+const isValidOffsetNumber = function (n) {
+ return typeof(n) === 'number' && n >= 0;
+};
- const cpIndex = [];
+const computeIndexFromOffset = function (channelName, offset, cb) {
+ let cpIndex = [];
let messageBuf = [];
let i = 0;
@@ -129,27 +135,42 @@ const computeIndex = function (data, cb) {
const offsetByHash = {};
let offsetCount = 0;
- let size = 0;
+ let size = offset || 0;
+ var start = offset || 0;
+ let unconventional = false;
+
nThen(function (w) {
// iterate over all messages in the channel log
// old channels can contain metadata as the first message of the log
// skip over metadata as that is handled elsewhere
// otherwise index important messages in the log
- store.readMessagesBin(channelName, 0, (msgObj, readMore) => {
+ store.readMessagesBin(channelName, start, (msgObj, readMore, abort) => {
let msg;
// keep an eye out for the metadata line if you haven't already seen it
// but only check for metadata on the first line
- if (!i && msgObj.buff.indexOf('{') === 0) {
- i++; // always increment the message counter
+ if (i) {
+ // fall through intentionally because the following blocks are invalid
+ // for all but the first message
+ } else if (msgObj.buff.includes(OPEN_CURLY_BRACE)) {
msg = HK.tryParse(Env, msgObj.buff.toString('utf8'));
- if (typeof msg === "undefined") { return readMore(); }
+ if (typeof msg === "undefined") {
+ i++; // always increment the message counter
+ return readMore();
+ }
// validate that the current line really is metadata before storing it as such
// skip this, as you already have metadata...
- if (HK.isMetadataMessage(msg)) { return readMore(); }
+ if (HK.isMetadataMessage(msg)) {
+ i++; // always increment the message counter
+ return readMore();
+ }
+ } else if (!(msg = HK.tryParse(Env, msgObj.buff.toString('utf8')))) {
+ w.abort();
+ abort();
+ return CB("OFFSET_ERROR");
}
i++;
- if (msgObj.buff.indexOf('cp|') > -1) {
+ if (msgObj.buff.includes(CHECKPOINT_PREFIX)) {
msg = msg || HK.tryParse(Env, msgObj.buff.toString('utf8'));
if (typeof msg === "undefined") { return readMore(); }
// cache the offsets of checkpoints if they can be parsed
@@ -164,6 +185,7 @@ const computeIndex = function (data, cb) {
}
} else if (messageBuf.length > 100 && cpIndex.length === 0) {
// take the last 50 messages
+ unconventional = true;
messageBuf = messageBuf.slice(-50);
}
// if it's not metadata or a checkpoint then it should be a regular message
@@ -192,11 +214,40 @@ const computeIndex = function (data, cb) {
size = msgObj.offset + msgObj.buff.length + 1;
});
}));
+ }).nThen(function (w) {
+ cpIndex = HK.sliceCpIndex(cpIndex, i);
+
+ var new_start;
+ if (cpIndex.length) {
+ new_start = cpIndex[0].offset;
+ } else if (unconventional && messageBuf.length && isValidOffsetNumber(messageBuf[0].offset)) {
+ new_start = messageBuf[0].offset;
+ }
+
+ if (new_start === start) { return; }
+ if (!isValidOffsetNumber(new_start)) { return; }
+
+ // store the offset of the earliest relevant line so that you can start from there next time...
+ store.writeOffset(channelName, {
+ start: new_start,
+ created: +new Date(),
+ }, w(function () {
+ var diff = new_start - start;
+ Env.Log.info('WORKER_OFFSET_UPDATE', {
+ channel: channelName,
+ start: start,
+ startMB: round(start / 1024 / 1024),
+ update: new_start,
+ updateMB: round(new_start / 1024 / 1024),
+ diff: diff,
+ diffMB: round(diff / 1024 / 1024),
+ });
+ }));
}).nThen(function () {
// return the computed index
CB(null, {
// Only keep the checkpoints included in the last 100 messages
- cpIndex: HK.sliceCpIndex(cpIndex, i),
+ cpIndex: cpIndex,
offsetByHash: offsetByHash,
offsets: offsetCount,
size: size,
@@ -206,6 +257,47 @@ const computeIndex = function (data, cb) {
});
};
+const computeIndex = function (data, cb) {
+ if (!data || !data.channel) {
+ return void cb('E_NO_CHANNEL');
+ }
+
+ const channelName = data.channel;
+ const CB = Util.once(cb);
+
+ var start = 0;
+ nThen(function (w) {
+ store.getOffset(channelName, w(function (err, obj) {
+ if (err) { return; }
+ if (obj && typeof(obj.start) === 'number' && obj.start > 0) {
+ start = obj.start;
+ Env.Log.verbose('WORKER_OFFSET_RECOVERY', {
+ channel: channelName,
+ start: start,
+ startMB: round(start / 1024 / 1024),
+ });
+ }
+ }));
+ }).nThen(function (w) {
+ computeIndexFromOffset(channelName, start, w(function (err, index) {
+ if (err === 'OFFSET_ERROR') {
+ return Env.Log.error("WORKER_OFFSET_ERROR", {
+ channel: channelName,
+ });
+ }
+ w.abort();
+ CB(err, index);
+ }));
+ }).nThen(function (w) {
+ // if you're here there was an OFFSET_ERROR..
+ // first remove the offset that caused the problem to begin with
+ store.clearOffset(channelName, w());
+ }).nThen(function () {
+ // now get the history as though it were the first time
+ computeIndexFromOffset(channelName, 0, CB);
+ });
+};
+
const computeMetadata = function (data, cb) {
const ref = {};
const lineHandler = Meta.createLineHandler(ref, Env.Log.error);
@@ -457,6 +549,41 @@ const evictInactive = function (data, cb) {
Eviction(Env, cb);
};
+var reportStatus = function (Env, label, safeKey, err, id, size) {
+ var data = {
+ safeKey: safeKey,
+ err: err && err.message || err,
+ id: id,
+ size: size,
+ sizeMB: round((size || 0) / 1024 / 1024),
+ };
+ var method = err? 'error': 'info';
+ Env.Log[method](label, data);
+};
+
+const completeUpload = function (data, cb) {
+ if (!data) { return void cb('INVALID_ARGS'); }
+ var owned = data.owned;
+ var safeKey = data.safeKey;
+ var arg = data.arg;
+ var size = data.size;
+
+ var method;
+ var label;
+ if (owned) {
+ method = 'completeOwned';
+ label = 'UPLOAD_COMPLETE_OWNED';
+ } else {
+ method = 'complete';
+ label = 'UPLOAD_COMPLETE';
+ }
+
+ Env.blobStore[method](safeKey, arg, function (err, id) {
+ reportStatus(Env, label, safeKey, err, id, size);
+ cb(err, id);
+ });
+};
+
const COMMANDS = {
COMPUTE_INDEX: computeIndex,
COMPUTE_METADATA: computeMetadata,
@@ -471,6 +598,7 @@ const COMMANDS = {
RUN_TASKS: runTasks,
WRITE_TASK: writeTask,
EVICT_INACTIVE: evictInactive,
+ COMPLETE_UPLOAD: completeUpload,
};
COMMANDS.INLINE = function (data, cb) {
@@ -568,7 +696,7 @@ process.on('message', function (data) {
const cb = function (err, value) {
process.send({
- error: err,
+ error: Util.serializeError(err),
txid: data.txid,
pid: data.pid,
value: value,
@@ -577,7 +705,7 @@ process.on('message', function (data) {
if (!ready) {
return void init(data.config, function (err) {
- if (err) { return void cb(err); }
+ if (err) { return void cb(Util.serializeError(err)); }
ready = true;
cb();
});
diff --git a/lib/workers/index.js b/lib/workers/index.js
index 6e9f57e88..25c18d947 100644
--- a/lib/workers/index.js
+++ b/lib/workers/index.js
@@ -9,10 +9,19 @@ const PID = process.pid;
const DB_PATH = 'lib/workers/db-worker';
const MAX_JOBS = 16;
+const DEFAULT_QUERY_TIMEOUT = 60000 * 15; // increased from three to fifteen minutes because queries for very large files were taking as long as seven minutes
Workers.initialize = function (Env, config, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
+ var incrementTime = function (command, start) {
+ if (!command) { return; }
+ var end = +new Date();
+ var T = Env.commandTimers;
+ var diff = (end - start);
+ T[command] = (T[command] || 0) + (diff / 1000);
+ };
+
const workers = [];
const response = Util.response(function (errLabel, info) {
@@ -111,8 +120,11 @@ Workers.initialize = function (Env, config, _cb) {
}
const txid = guid();
+ var start = +new Date();
var cb = Util.once(Util.mkAsync(Util.both(_cb, function (err /*, value */) {
+ incrementTime(msg && msg.command, start);
if (err !== 'TIMEOUT') { return; }
+ Log.debug("WORKER_TIMEOUT_CAUSE", msg);
// in the event of a timeout the user will receive an error
// but the state used to resend a query in the event of a worker crash
// won't be cleared. This also leaks a slot that could be used to keep
@@ -132,7 +144,7 @@ Workers.initialize = function (Env, config, _cb) {
state.tasks[txid] = msg;
// default to timing out affter 180s if no explicit timeout is passed
- var timeout = typeof(opt.timeout) !== 'undefined'? opt.timeout: 180000;
+ var timeout = typeof(opt.timeout) !== 'undefined'? opt.timeout: DEFAULT_QUERY_TIMEOUT;
response.expect(txid, cb, timeout);
state.worker.send(msg);
};
@@ -422,6 +434,16 @@ Workers.initialize = function (Env, config, _cb) {
}, cb);
};
+ Env.completeUpload = function (safeKey, arg, owned, size, cb) {
+ sendCommand({
+ command: "COMPLETE_UPLOAD",
+ owned: owned, // Boolean
+ safeKey: safeKey, // String (public key)
+ arg: arg, // String (file id)
+ size: size, // Number || undefined
+ }, cb);
+ };
+
cb(void 0);
});
};
diff --git a/package-lock.json b/package-lock.json
index 2c31e74bf..ef1573c7e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "cryptpad",
- "version": "3.24.0",
+ "version": "3.25.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -389,16 +389,16 @@
"optional": true
},
"chainpad-crypto": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/chainpad-crypto/-/chainpad-crypto-0.2.4.tgz",
- "integrity": "sha512-fWbVyeAv35vf/dkkQaefASlJcEfpEvfRI23Mtn+/TBBry7+LYNuJMXJiovVY35pfyw2+trKh1Py5Asg9vrmaVg==",
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/chainpad-crypto/-/chainpad-crypto-0.2.5.tgz",
+ "integrity": "sha512-K9vRsAspuX+uU1goXPz0CawpLIaOHq+1JP3WfDLqaz67LbCX/MLIUt9aMcSeIJcwZ9uMpqnbMGRktyVPoz6MCA==",
"requires": {
- "tweetnacl": "git://github.com/dchest/tweetnacl-js.git#v0.12.2"
+ "tweetnacl": "git+https://github.com/dchest/tweetnacl-js.git#v0.12.2"
},
"dependencies": {
"tweetnacl": {
- "version": "git://github.com/dchest/tweetnacl-js.git#8a21381d696acdc4e99c9f706f1ad23285795f79",
- "from": "git://github.com/dchest/tweetnacl-js.git#v0.12.2"
+ "version": "git+https://github.com/dchest/tweetnacl-js.git#8a21381d696acdc4e99c9f706f1ad23285795f79",
+ "from": "git+https://github.com/dchest/tweetnacl-js.git#v0.12.2"
}
}
},
diff --git a/package.json b/package.json
index 5c452b864..24041cb7b 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,11 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
- "version": "3.24.0",
+ "version": "3.25.0",
"license": "AGPL-3.0+",
"repository": {
"type": "git",
- "url": "git://github.com/xwiki-labs/cryptpad.git"
+ "url": "git+https://github.com/xwiki-labs/cryptpad.git"
},
"funding": {
"type": "opencollective",
@@ -13,7 +13,7 @@
},
"dependencies": {
"@mcrowe/minibloom": "^0.2.0",
- "chainpad-crypto": "^0.2.2",
+ "chainpad-crypto": "^0.2.5",
"chainpad-server": "^4.0.9",
"express": "~4.16.0",
"fs-extra": "^7.0.0",
diff --git a/readme.md b/readme.md
index 833de540f..2213b9e27 100644
--- a/readme.md
+++ b/readme.md
@@ -65,7 +65,7 @@ CryptPad is actively developed by a team at [XWiki SAS](https://www.xwiki.com),
# Contributing
-We love Open Source and we love contribution. Learn more about [contributing](https://github.com/xwiki-labs/cryptpad/wiki/Contributor-overview).
+We love Open Source and we love contribution. Learn more about [contributing](https://docs.cryptpad.fr/en/how_to_contribute.html).
If you have any questions or comments, or if you're interested in contributing to Cryptpad, come say hi on IRC, `#cryptpad` on Freenode.
diff --git a/server.js b/server.js
index 60247f47a..3869af509 100644
--- a/server.js
+++ b/server.js
@@ -136,6 +136,20 @@ app.head(/^\/common\/feedback\.html/, function (req, res, next) {
});
}());
+app.use('/blob', function (req, res, next) {
+ if (req.method === 'HEAD') {
+ Express.static(Path.join(__dirname, (config.blobPath || './blob')), {
+ setHeaders: function (res, path, stat) {
+ res.set('Access-Control-Allow-Origin', '*');
+ res.set('Access-Control-Allow-Headers', 'Content-Length');
+ res.set('Access-Control-Expose-Headers', 'Content-Length');
+ }
+ })(req, res, next);
+ return;
+ }
+ next();
+});
+
app.use(function (req, res, next) {
if (req.method === 'OPTIONS' && /\/blob\//.test(req.url)) {
res.setHeader('Access-Control-Allow-Origin', '*');
@@ -202,6 +216,7 @@ var serveConfig = (function () {
adminKeys: Env.admins,
inactiveTime: Env.inactiveTime,
supportMailbox: Env.supportMailbox,
+ defaultStorageLimit: Env.defaultStorageLimit,
maxUploadSize: Env.maxUploadSize,
premiumUploadSize: Env.premiumUploadSize,
}, null, '\t'),
diff --git a/www/admin/app-admin.less b/www/admin/app-admin.less
index 1f90fbfff..d19e6b4d7 100644
--- a/www/admin/app-admin.less
+++ b/www/admin/app-admin.less
@@ -27,6 +27,9 @@
margin-top: 5px;
}
}
+ .cp-admin-setlimit-form + button {
+ margin-top: 5px !important;
+ }
.cp-admin-getlimits {
code {
cursor: pointer;
@@ -58,14 +61,62 @@
.cp-support-container {
display: flex;
- flex-flow: column;
+ flex-wrap: wrap;
+ .cp-support-column {
+ min-width: 700px;
+ flex: 1 0 50%;
+ h1 {
+ display: flex;
+ align-items: center;
+ button {
+ margin-left: 50px !important;
+ }
+ }
+ .cp-support-count {
+ margin-left: 10px;
+ }
+ &.cp-support-column-collapsed {
+ .cp-support-list-ticket {
+ display: none;
+ }
+ }
+ }
}
.cp-support-list-actions {
margin: 10px 0px 10px 2px;
}
+ .cp-support-list-ticket {
+ h2 {
+ font-size: 1.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: top;
+ .cp-support-title-buttons {
+ flex-shrink: 0;
+ }
+ }
+ .cp-support-collapsed {
+ display: flex;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ color: #666;
+ .cp-support-ispremium {
+ padding: 0 5px;
+ color: @colortheme_cp-red;
+ background-color: lighten(@colortheme_cp-red, 25%);
+ }
+ }
+ }
+
.cp-support-list-ticket:not(.cp-support-list-closed) {
+ .cp-support-list-actions {
+ .cp-button-confirm, .cp-support-close {
+ order: 20;
+ margin-left: auto !important;
+ }
+ }
.cp-support-list-message {
&:last-child:not(.cp-support-fromadmin) {
color: @colortheme_cp-red;
@@ -85,6 +136,28 @@
}
}
}
+ .cp-support-list-ticket:not(.cp-support-open) {
+ span:first-child {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ & > :not(h2):not(.cp-support-collapsed) {
+ display: none;
+ }
+ .cp-support-collapse {
+ display: none;
+ }
+ cursor: pointer;
+ }
+ .cp-support-list-ticket.cp-support-open {
+ .cp-support-collapsed {
+ display: none;
+ }
+ .cp-support-expand {
+ display: none;
+ }
+ }
.cp-support-fromadmin {
color: @colortheme_logo-2;
@@ -93,5 +166,15 @@
color: @colortheme_logo-2;
}
}
+
+ input.cp-admin-inval {
+ border-color: red !important;
+ }
+ .cp-admin-nopassword {
+ .cp-admin-pw {
+ display: none !important;
+ }
+ }
+
}
diff --git a/www/admin/inner.js b/www/admin/inner.js
index db5afca54..755b60ddc 100644
--- a/www/admin/inner.js
+++ b/www/admin/inner.js
@@ -43,6 +43,8 @@ define([
'general': [
'cp-admin-flush-cache',
'cp-admin-update-limit',
+ 'cp-admin-archive',
+ 'cp-admin-unarchive',
// 'cp-admin-registration',
],
'quota': [
@@ -107,6 +109,129 @@ define([
});
return $div;
};
+
+ var archiveForm = function (archive, $div, $button) {
+ var label = h('label', { for: 'cp-admin-archive' }, Messages.admin_archiveInput);
+ var input = h('input#cp-admin-archive', {
+ type: 'text'
+ });
+
+ var label2 = h('label.cp-admin-pw', {
+ for: 'cp-admin-archive-pw'
+ }, Messages.admin_archiveInput2);
+ var input2 = UI.passwordInput({
+ id: 'cp-admin-archive-pw',
+ placeholder: Messages.login_password
+ });
+ var $pw = $(input2);
+ $pw.addClass('cp-admin-pw');
+ var $pwInput = $pw.find('input');
+
+
+ $button.before(h('div.cp-admin-setlimit-form', [
+ label,
+ input,
+ label2,
+ input2
+ ]));
+
+ $div.addClass('cp-admin-nopassword');
+
+ var parsed;
+ var $input = $(input).on('keypress change paste', function () {
+ setTimeout(function () {
+ $input.removeClass('cp-admin-inval');
+ var val = $input.val().trim();
+ if (!val) {
+ $div.toggleClass('cp-admin-nopassword', true);
+ return;
+ }
+
+ parsed = Hash.isValidHref(val);
+ $pwInput.val('');
+
+ if (!parsed || !parsed.hashData) {
+ $div.toggleClass('cp-admin-nopassword', true);
+ return void $input.addClass('cp-admin-inval');
+ }
+
+ var pw = parsed.hashData.version !== 3 && parsed.hashData.password;
+ $div.toggleClass('cp-admin-nopassword', !pw);
+ });
+ });
+ $pw.on('keypress change', function () {
+ setTimeout(function () {
+ $pw.toggleClass('cp-admin-inval', !$pwInput.val());
+ });
+ });
+
+ var clicked = false;
+ $button.click(function () {
+ if (!parsed || !parsed.hashData) {
+ UI.warn(Messages.admin_archiveInval);
+ return;
+ }
+ var pw = parsed.hashData.password ? $pwInput.val() : undefined;
+ var channel;
+ if (parsed.hashData.version === 3) {
+ channel = parsed.hashData.channel;
+ } else {
+ var secret = Hash.getSecrets(parsed.type, parsed.hash, pw);
+ channel = secret && secret.channel;
+ }
+
+ if (!channel) {
+ UI.warn(Messages.admin_archiveInval);
+ return;
+ }
+
+ if (clicked) { return; }
+ clicked = true;
+
+ nThen(function (waitFor) {
+ if (!archive) { return; }
+ common.getFileSize(channel, waitFor(function (err, size) {
+ if (!err && size === 0) {
+ clicked = false;
+ waitFor.abort();
+ return void UI.warn(Messages.admin_archiveInval);
+ }
+ }));
+ }).nThen(function () {
+ sFrameChan.query('Q_ADMIN_RPC', {
+ cmd: archive ? 'ARCHIVE_DOCUMENT' : 'RESTORE_ARCHIVED_DOCUMENT',
+ data: channel
+ }, function (err, obj) {
+ var e = err || (obj && obj.error);
+ clicked = false;
+ if (e) {
+ UI.warn(Messages.error);
+ console.error(e);
+ return;
+ }
+ UI.log(archive ? Messages.archivedFromServer : Messages.restoredFromServer);
+ $input.val('');
+ $pwInput.val('');
+ });
+ });
+ });
+ };
+
+ create['archive'] = function () {
+ var key = 'archive';
+ var $div = makeBlock(key, true);
+ var $button = $div.find('button');
+ archiveForm(true, $div, $button);
+ return $div;
+ };
+ create['unarchive'] = function () {
+ var key = 'unarchive';
+ var $div = makeBlock(key, true);
+ var $button = $div.find('button');
+ archiveForm(false, $div, $button);
+ return $div;
+ };
+
create['registration'] = function () {
var key = 'registration';
var $div = makeBlock(key, true);
@@ -222,7 +347,7 @@ define([
var keyEl = h('code.cp-limit-key', key);
$(keyEl).click(function () {
$('.cp-admin-setlimit-form').find('.cp-setlimit-key').val(key);
- $('.cp-admin-setlimit-form').find('.cp-setlimit-quota').val(Math.floor(user.limit/1024));
+ $('.cp-admin-setlimit-form').find('.cp-setlimit-quota').val(Math.floor(user.limit / 1024 / 1024));
$('.cp-admin-setlimit-form').find('.cp-setlimit-note').val(user.note);
});
if (compact) {
@@ -427,7 +552,53 @@ define([
var $div = $(h('div.cp-support-container')).appendTo($container);
var catContainer = h('div.cp-dropdown-container');
- $div.append(catContainer);
+ Messages.admin_support_premium = "Premium tickets:"; // XXX
+ Messages.admin_support_normal = "Unanswered tickets:";
+ Messages.admin_support_answered = "Answered tickets:";
+ Messages.admin_support_closed = "Closed tickets:";
+ Messages.admin_support_open = "Show";
+ Messages.admin_support_collapse = "Collapse";
+ Messages.admin_support_first = "Created on: ";
+ Messages.admin_support_last = "Updated on: ";
+ var col1 = h('div.cp-support-column', h('h1', [
+ h('span', Messages.admin_support_premium),
+ h('span.cp-support-count'),
+ h('button.btn.cp-support-column-button', Messages.admin_support_collapse)
+ ]));
+ var col2 = h('div.cp-support-column', h('h1', [
+ h('span', Messages.admin_support_normal),
+ h('span.cp-support-count'),
+ h('button.btn.cp-support-column-button', Messages.admin_support_collapse)
+ ]));
+ var col3 = h('div.cp-support-column', h('h1', [
+ h('span', Messages.admin_support_answered),
+ h('span.cp-support-count'),
+ h('button.btn.cp-support-column-button', Messages.admin_support_collapse)
+ ]));
+ var col4 = h('div.cp-support-column', h('h1', [
+ h('span', Messages.admin_support_closed),
+ h('span.cp-support-count'),
+ h('button.btn.cp-support-column-button', Messages.admin_support_collapse)
+ ]));
+ var $col1 = $(col1), $col2 = $(col2), $col3 = $(col3), $col4 = $(col4);
+ $div.append([
+ //catContainer
+ col1,
+ col2,
+ col3,
+ col4
+ ]);
+ $div.find('.cp-support-column-button').click(function () {
+ var $col = $(this).closest('.cp-support-column');
+ $col.toggleClass('cp-support-column-collapsed');
+ if ($col.hasClass('cp-support-column-collapsed')) {
+ $(this).text(Messages.admin_support_open);
+ $(this).toggleClass('btn-primary');
+ } else {
+ $(this).text(Messages.admin_support_collapse);
+ $(this).toggleClass('btn-primary');
+ }
+ });
var category = 'all';
var $drop = APP.support.makeCategoryDropdown(catContainer, function (key) {
category = key;
@@ -447,37 +618,128 @@ define([
var hashesById = {};
- var reorder = function () {
- var order = Object.keys(hashesById);
- order.sort(function (id1, id2) {
- var t1 = hashesById[id1];
- var t2 = hashesById[id2];
- if (!Array.isArray(t1)) { return 1; }
- if (!Array.isArray(t2)) { return -1; }
- var lastMsg1 = t1[t1.length - 1];
- var lastMsg2 = t2[t2.length - 1];
- var time1 = Util.find(lastMsg1, ['content', 'msg', 'content', 'time']);
- var time2 = Util.find(lastMsg2, ['content', 'msg', 'content', 'time']);
- var authorEd1 = Util.find(lastMsg1, ['content', 'msg', 'content', 'sender', 'edPublic']);
- var authorEd2 = Util.find(lastMsg2, ['content', 'msg', 'content', 'sender', 'edPublic']);
- var admin1 = ApiConfig.adminKeys.indexOf(authorEd1) !== -1;
- var admin2 = ApiConfig.adminKeys.indexOf(authorEd2) !== -1;
- // If one is answered and not the other, put the unanswered first
- if (admin1 && !admin2) { return 1; }
- if (!admin1 && admin2) { return -1; }
- // Otherwise, sort them by time
- return time2 - time1;
+ var getTicketData = function (id) {
+ var t = hashesById[id];
+ if (!Array.isArray(t) || !t.length) { return; }
+ var ed = Util.find(t[0], ['content', 'msg', 'content', 'sender', 'edPublic']);
+ // If one of their ticket was sent as a premium user, mark them as premium
+ var premium = t.some(function (msg) {
+ var _ed = Util.find(msg, ['content', 'msg', 'content', 'sender', 'edPublic']);
+ if (ed !== _ed) { return; }
+ return Util.find(t[0], ['content', 'msg', 'content', 'sender', 'plan']);
+ });
+ var lastMsg = t[t.length - 1];
+ var lastMsgEd = Util.find(lastMsg, ['content', 'msg', 'content', 'sender', 'edPublic']);
+ return {
+ lastMsg: lastMsg,
+ time: Util.find(lastMsg, ['content', 'msg', 'content', 'time']),
+ lastMsgEd: lastMsgEd,
+ lastAdmin: lastMsgEd !== ed && ApiConfig.adminKeys.indexOf(lastMsgEd) !== -1,
+ premium: premium,
+ authorEd: ed,
+ closed: Util.find(lastMsg, ['content', 'msg', 'type']) === 'CLOSE'
+ };
+ };
+
+ var addClickHandler = function ($ticket) {
+ $ticket.on('click', function () {
+ $ticket.toggleClass('cp-support-open', true);
+ $ticket.off('click');
+ });
+ };
+ var makeOpenButton = function ($ticket) {
+ var button = h('button.btn.btn-primary.cp-support-expand', Messages.admin_support_open);
+ var collapse = h('button.btn.cp-support-collapse', Messages.admin_support_collapse);
+ $(button).click(function () {
+ $ticket.toggleClass('cp-support-open', true);
+ });
+ addClickHandler($ticket);
+ $(collapse).click(function (e) {
+ $ticket.toggleClass('cp-support-open', false);
+ e.stopPropagation();
+ setTimeout(function () {
+ addClickHandler($ticket);
+ });
});
- order.forEach(function (id, i) {
- $div.find('[data-id="'+id+'"]').css('order', i);
+ $ticket.find('.cp-support-title-buttons').prepend([button, collapse]);
+ $ticket.append(h('div.cp-support-collapsed'));
+ };
+ var updateTicketDetails = function ($ticket, isPremium) {
+ var $first = $ticket.find('.cp-support-message-from').first();
+ var user = $first.find('span').first().html();
+ var time = $first.find('.cp-support-message-time').text();
+ var last = $ticket.find('.cp-support-message-from').last().find('.cp-support-message-time').text();
+ var $c = $ticket.find('.cp-support-collapsed');
+ var txtClass = isPremium ? ".cp-support-ispremium" : "";
+ $c.html('').append([
+ UI.setHTML(h('span'+ txtClass), user),
+ h('span', [
+ h('b', Messages.admin_support_first),
+ h('span', time)
+ ]),
+ h('span', [
+ h('b', Messages.admin_support_last),
+ h('span', last)
+ ])
+ ]);
+
+ };
+
+ var sort = function (id1, id2) {
+ var t1 = getTicketData(id1);
+ var t2 = getTicketData(id2);
+ if (!t1) { return 1; }
+ if (!t2) { return -1; }
+ /*
+ // If one is answered and not the other, put the unanswered first
+ if (t1.lastAdmin && !t2.lastAdmin) { return 1; }
+ if (!t1.lastAdmin && t2.lastAdmin) { return -1; }
+ */
+ // Otherwise, sort them by time
+ return t1.time - t2.time;
+ };
+
+ var _reorder = function () {
+ var orderAnswered = Object.keys(hashesById).filter(function (id) {
+ var d = getTicketData(id);
+ return d && d.lastAdmin && !d.closed;
+ }).sort(sort);
+ var orderPremium = Object.keys(hashesById).filter(function (id) {
+ var d = getTicketData(id);
+ return d && d.premium && !d.lastAdmin && !d.closed;
+ }).sort(sort);
+ var orderNormal = Object.keys(hashesById).filter(function (id) {
+ var d = getTicketData(id);
+ return d && !d.premium && !d.lastAdmin && !d.closed;
+ }).sort(sort);
+ var orderClosed = Object.keys(hashesById).filter(function (id) {
+ var d = getTicketData(id);
+ return d && d.closed;
+ }).sort(sort);
+ var cols = [$col1, $col2, $col3, $col4];
+ [orderPremium, orderNormal, orderAnswered, orderClosed].forEach(function (list, j) {
+ list.forEach(function (id, i) {
+ var $t = $div.find('[data-id="'+id+'"]');
+ var d = getTicketData(id);
+ $t.css('order', i).appendTo(cols[j]);
+ updateTicketDetails($t, d.premium);
+ });
+ if (!list.length) {
+ cols[j].hide();
+ } else {
+ cols[j].show();
+ cols[j].find('.cp-support-count').text(list.length);
+ }
});
};
+ var reorder = Util.throttle(_reorder, 150);
var to = Util.throttle(function () {
var $ticket = $div.find('.cp-support-list-ticket[data-id="'+linkedId+'"]');
+ $ticket.addClass('cp-support-open');
$ticket[0].scrollIntoView();
linkedId = undefined;
- }, 100);
+ }, 200);
// Register to the "support" mailbox
common.mailbox.subscribe(['supportadmin'], {
@@ -505,6 +767,7 @@ define([
if (!$ticket.length) { return; }
$ticket.addClass('cp-support-list-closed');
$ticket.append(APP.support.makeCloseMessage(content, hash));
+ reorder();
return;
}
if (msg.type !== 'TICKET') { return; }
@@ -525,13 +788,19 @@ define([
}));
});
}).nThen(function () {
- if (!error) { return void $ticket.remove(); }
+ if (!error) {
+ $ticket.remove();
+ delete hashesById[id];
+ reorder();
+ return;
+ }
// if deletion failed then reactivate the button and warn
hideButton.removeAttribute('disabled');
// and show a generic error message
UI.alert(Messages.error);
});
});
+ makeOpenButton($ticket);
if (category !== 'all' && $ticket.attr('data-cat') !== category) {
$ticket.hide();
}
diff --git a/www/assert/main.js b/www/assert/main.js
index 054a17c0f..db417c761 100644
--- a/www/assert/main.js
+++ b/www/assert/main.js
@@ -334,6 +334,12 @@ define([
!secret.hashData.present);
}, "test support for ugly tracking query paramaters in url");
+ assert(function (cb) {
+ var url = '//cryptpad.fr/pad/#/2/pad/edit/oRE0oLCtEXusRDyin7GyLGcS/';
+ var parsed = Hash.isValidHref(url);
+ cb(!parsed);
+ }, "test that protocol relative URLs are rejected");
+
assert(function (cb) {
var keys = Block.genkeys(Nacl.randomBytes(64));
var hash = Block.getBlockHash(keys);
@@ -349,7 +355,7 @@ define([
var v3 = Hash.isValidHref('/pad');
var v4 = Hash.isValidHref('/pad/');
- var res = v1 && v2 && v3 && v4;
+ var res = Boolean(v1 && v2 && v3 && v4);
cb(res);
if (!res) {
console.log(v1, v2, v3, v4);
@@ -361,7 +367,7 @@ define([
var v3 = Hash.isValidHref('/pad#'); // Invalid
var v4 = Hash.isValidHref('/pad/#');
- var res = v1 && v2 && v3 && v4;
+ var res = Boolean(v1 && v2 && v3 && v4);
cb(res);
if (!res) {
console.log(v1, v2, v3, v4);
@@ -373,7 +379,7 @@ define([
var v3 = Hash.isValidHref('https://cryptpad.fr/pad/#67b8385b07352be53e40746d2be6ccd7XAYSuJYYqa9NfmInyHci7LNy');
var v4 = Hash.isValidHref('/pad/#/2/pad/edit/HGu0tK2od-2BBnwAz2ZNS-t4/p/embed');
- var res = v1 && v2 && v3 && v4;
+ var res = Boolean(v1 && v2 && v3 && v4);
cb(res);
if (!res) {
console.log(v1, v2, v3, v4);
diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js
index 734b762ac..496e65f1f 100644
--- a/www/common/application_config_internal.js
+++ b/www/common/application_config_internal.js
@@ -169,7 +169,7 @@ define(function() {
// make them have a very slow loading time. To avoid impacting the user experience
// significantly, we're limiting the number of teams per user to 3 by default.
// You can change this value here.
- //config.maxTeamsSlots = 3;
+ //config.maxTeamsSlots = 5;
// Each team is considered as a registered user by the server. Users and teams are indistinguishable
// in the database so teams will offer the same storage limits as users by default.
@@ -177,7 +177,7 @@ define(function() {
// We're limiting the number of teams each user is able to own to 1 in order to make sure
// users don't use "fake" teams (1 member) just to increase their storage limit.
// You can change the value here.
- // config.maxOwnedTeams = 1;
+ // config.maxOwnedTeams = 5;
return config;
});
diff --git a/www/common/common-constants.js b/www/common/common-constants.js
index 17db2302c..fdae2b5de 100644
--- a/www/common/common-constants.js
+++ b/www/common/common-constants.js
@@ -12,8 +12,8 @@ define(['/customize/application_config.js'], function (AppConfig) {
tokenKey: 'loginToken',
displayPadCreationScreen: 'displayPadCreationScreen',
deprecatedKey: 'deprecated',
- MAX_TEAMS_SLOTS: AppConfig.maxTeamsSlots || 3,
- MAX_TEAMS_OWNED: AppConfig.maxOwnedTeams || 1,
+ MAX_TEAMS_SLOTS: AppConfig.maxTeamsSlots || 5,
+ MAX_TEAMS_OWNED: AppConfig.maxOwnedTeams || 5,
// Apps
criticalApps: ['profile', 'settings', 'debug', 'admin', 'support', 'notifications']
};
diff --git a/www/common/common-hash.js b/www/common/common-hash.js
index 199c2cb54..afdecbf57 100644
--- a/www/common/common-hash.js
+++ b/www/common/common-hash.js
@@ -464,6 +464,8 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app)
};
if (!/^https*:\/\//.test(href)) {
+ // If it doesn't start with http(s), it should be a relative href
+ if (!/^\/($|[^\/])/.test(href)) { return ret; }
idx = href.indexOf('/#');
ret.type = href.slice(1, idx);
if (idx === -1) { return ret; }
@@ -661,7 +663,7 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app)
if (parsed.hashData.key && !/^[a-zA-Z0-9+-/=]+$/.test(parsed.hashData.key)) { return; }
}
}
- return true;
+ return parsed;
};
Hash.decodeDataOptions = function (opts) {
diff --git a/www/common/common-interface.js b/www/common/common-interface.js
index 66c066d3f..f7f3f082f 100644
--- a/www/common/common-interface.js
+++ b/www/common/common-interface.js
@@ -65,12 +65,13 @@ define([
switch (e.which) {
case 27: // cancel
if (typeof(no) === 'function') { no(e); }
+ $(el || window).off('keydown', handler);
break;
case 13: // enter
if (typeof(yes) === 'function') { yes(e); }
+ $(el || window).off('keydown', handler);
break;
}
- $(el || window).off('keydown', handler);
};
$(el || window).keydown(handler);
@@ -773,7 +774,8 @@ define([
$(originalBtn).show();
};
- $button.click(function () {
+ $button.click(function (e) {
+ e.stopPropagation();
done(true);
});
@@ -791,7 +793,8 @@ define([
to = setTimeout(todo, INTERVAL);
};
- $(originalBtn).addClass('cp-button-confirm-placeholder').click(function () {
+ $(originalBtn).addClass('cp-button-confirm-placeholder').click(function (e) {
+ e.stopPropagation();
// If we have a validation function, continue only if it's true
if (config.validate && !config.validate()) { return; }
i = 1;
@@ -981,6 +984,11 @@ define([
setTimeout(cb, 750);
};
UI.errorLoadingScreen = function (error, transparent, exitable) {
+ if (error === 'Error: XDR encoding failure') {
+ console.warn(error);
+ return;
+ }
+
var $loading = $('#' + LOADING);
if (!$loading.is(':visible') || $loading.hasClass('cp-loading-hidden')) {
UI.addLoadingScreen();
diff --git a/www/common/common-messaging.js b/www/common/common-messaging.js
index 15d0408f7..65f05961e 100644
--- a/www/common/common-messaging.js
+++ b/www/common/common-messaging.js
@@ -91,7 +91,7 @@ define([
Msg.updateMyData = function (store, curve) {
var myData = createData(store.proxy, false);
if (store.proxy.friends) {
- store.proxy.friends.me = myData;
+ store.proxy.friends.me = Util.clone(myData);
delete store.proxy.friends.me.channel;
}
if (store.modules['team']) {
@@ -99,6 +99,7 @@ define([
}
var todo = function (friend) {
if (!friend || !friend.notifications) { return; }
+ delete friend.user;
myData.channel = friend.channel;
store.mailbox.sendTo('UPDATE_DATA', myData, {
channel: friend.notifications,
diff --git a/www/common/common-thumbnail.js b/www/common/common-thumbnail.js
index 8c78d9d6d..012c2c9b4 100644
--- a/www/common/common-thumbnail.js
+++ b/www/common/common-thumbnail.js
@@ -3,9 +3,9 @@ define([
'/common/common-util.js',
'/common/visible.js',
'/common/common-hash.js',
- '/file/file-crypto.js',
+ '/common/media-tag.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
-], function ($, Util, Visible, Hash, FileCrypto) {
+], function ($, Util, Visible, Hash, MediaTag) {
var Nacl = window.nacl;
var Thumb = {
dimension: 100,
@@ -314,7 +314,7 @@ define([
var hexFileName = secret.channel;
var src = fileHost + Hash.getBlobPathFromHex(hexFileName);
var key = secret.keys && secret.keys.cryptKey;
- FileCrypto.fetchDecryptedMetadata(src, key, function (e, metadata) {
+ MediaTag.fetchDecryptedMetadata(src, key, function (e, metadata) {
if (e) {
if (e === 'XHR_ERROR') { return; }
return console.error(e);
diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js
index 54c5f4921..5501ca813 100644
--- a/www/common/common-ui-elements.js
+++ b/www/common/common-ui-elements.js
@@ -1602,9 +1602,9 @@ define([
content: h('span', Messages.profileButton),
action: function () {
if (padType) {
- window.open(origin+'/profile/');
+ Common.openURL(origin+'/profile/');
} else {
- window.parent.location = origin+'/profile/';
+ Common.gotoURL(origin+'/profile/');
}
},
});
@@ -1613,33 +1613,36 @@ define([
options.push({
tag: 'a',
attributes: {
- 'target': '_blank',
- 'href': origin+'/drive/',
'class': 'fa fa-hdd-o'
},
- content: h('span', Messages.type.drive)
+ content: h('span', Messages.type.drive),
+ action: function () {
+ Common.openURL(origin+'/drive/');
+ },
});
}
if (padType !== 'teams' && accountName) {
options.push({
tag: 'a',
attributes: {
- 'target': '_blank',
- 'href': origin+'/teams/',
'class': 'fa fa-users'
},
- content: h('span', Messages.type.teams)
+ content: h('span', Messages.type.teams),
+ action: function () {
+ Common.openURL('/teams/');
+ },
});
}
if (padType !== 'contacts' && accountName) {
options.push({
tag: 'a',
attributes: {
- 'target': '_blank',
- 'href': origin+'/contacts/',
'class': 'fa fa-address-book'
},
- content: h('span', Messages.type.contacts)
+ content: h('span', Messages.type.contacts),
+ action: function () {
+ Common.openURL('/contacts/');
+ },
});
}
if (padType !== 'settings') {
@@ -1649,9 +1652,9 @@ define([
content: h('span', Messages.settingsButton),
action: function () {
if (padType) {
- window.open(origin+'/settings/');
+ Common.openURL(origin+'/settings/');
} else {
- window.parent.location = origin+'/settings/';
+ Common.gotoURL(origin+'/settings/');
}
},
});
@@ -1666,9 +1669,9 @@ define([
content: h('span', Messages.adminPage || 'Admin'),
action: function () {
if (padType) {
- window.open(origin+'/admin/');
+ Common.openURL(origin+'/admin/');
} else {
- window.parent.location = origin+'/admin/';
+ Common.gotoURL(origin+'/admin/');
}
},
});
@@ -1690,29 +1693,28 @@ define([
content: h('span', Messages.supportPage || 'Support'),
action: function () {
if (padType) {
- window.open(origin+'/support/');
+ Common.openURL(origin+'/support/');
} else {
- window.parent.location = origin+'/support/';
+ Common.gotoURL(origin+'/support/');
}
},
});
}
- // XXX Trade the survey for documentation
- // if (AppConfig.surveyURL) {
- // options.push({
- // tag: 'a',
- // attributes: {
- // 'target': '_blank',
- // 'rel': 'noopener',
- // 'href': AppConfig.surveyURL,
- // 'class': 'cp-toolbar-survey fa fa-graduation-cap'
- // },
- // content: h('span', Messages.survey),
- // action: function () {
- // Feedback.send('SURVEY_CLICKED');
- // },
- // });
- // }
+/*
+ if (AppConfig.surveyURL) {
+ options.push({
+ tag: 'a',
+ attributes: {
+ 'class': 'cp-toolbar-survey fa fa-graduation-cap'
+ },
+ content: h('span', Messages.survey),
+ action: function () {
+ Common.openUnsafeURL(AppConfig.surveyURL);
+ Feedback.send('SURVEY_CLICKED');
+ },
+ });
+ }
+*/
options.push({
tag: 'a',
attributes: {
@@ -1727,11 +1729,12 @@ define([
options.push({
tag: 'a',
attributes: {
- 'target': '_blank',
- 'href': origin+'/index.html',
'class': 'fa fa-home'
},
- content: h('span', Messages.homePage)
+ content: h('span', Messages.homePage),
+ action: function () {
+ Common.openURL('/index.html');
+ },
});
// Add the change display name button if not in read only mode
/*
@@ -1747,23 +1750,24 @@ define([
options.push({
tag: 'a',
attributes: {
- 'target': '_blank',
- 'href': priv.plan ? priv.accounts.upgradeURL : origin+'/features.html',
'class': 'fa fa-star-o'
},
- content: h('span', priv.plan ? Messages.settings_cat_subscription : Messages.pricing)
+ content: h('span', priv.plan ? Messages.settings_cat_subscription : Messages.pricing),
+ action: function () {
+ Common.openURL(priv.plan ? priv.accounts.upgradeURL :'/features.html');
+ },
});
}
if (!priv.plan && !Config.removeDonateButton) {
options.push({
tag: 'a',
attributes: {
- 'target': '_blank',
- 'rel': 'noopener',
- 'href': priv.accounts.donateURL,
'class': 'fa fa-gift'
},
- content: h('span', Messages.crowdfunding_button2)
+ content: h('span', Messages.crowdfunding_button2),
+ action: function () {
+ Common.openUnsafeURL(priv.accounts.donateURL);
+ },
});
}
@@ -1778,7 +1782,7 @@ define([
content: h('span', Messages.logoutEverywhere),
action: function () {
Common.getSframeChannel().query('Q_LOGOUT_EVERYWHERE', null, function () {
- window.parent.location = origin + '/';
+ Common.gotoURL(origin + '/');
});
},
});
@@ -1788,7 +1792,7 @@ define([
content: h('span', Messages.logoutButton),
action: function () {
Common.logout(function () {
- window.parent.location = origin+'/';
+ Common.gotoURL(origin+'/');
});
},
});
@@ -2603,7 +2607,9 @@ define([
var block = h('div#cp-loading-burn-after-reading', [
info,
- button
+ h('nav', {
+ style: 'text-align: right'
+ }, button),
]);
UI.errorLoadingScreen(block);
};
@@ -2738,6 +2744,43 @@ define([
};
+ UIElements.displayTrimHistoryPrompt = function (common, data) {
+ var mb = Util.bytesToMegabytes(data.size);
+ var text = Messages._getKey('history_trimPrompt', [
+ Messages._getKey('formattedMB', [mb])
+ ]);
+ var yes = h('button.cp-corner-primary', [
+ h('span.fa.fa-trash-o'),
+ Messages.trimHistory_button
+ ]);
+ var no = h('button.cp-corner-cancel', Messages.crowdfunding_popup_no); // Not now
+ var actions = h('div', [no, yes]);
+
+ var dontShowAgain = function () {
+ var until = (+new Date()) + (7 * 24 * 3600 * 1000); // 7 days from now
+ if (data.drive) {
+ common.setAttribute(['drive', 'trim'], until);
+ return;
+ }
+ common.setPadAttribute('trim', until);
+ };
+
+ var modal = UI.cornerPopup(text, actions, '', {});
+
+ $(yes).click(function () {
+ modal.delete();
+ if (data.drive) {
+ common.openURL('/settings/#drive');
+ return;
+ }
+ common.getSframeChannel().event('EV_PROPERTIES_OPEN');
+ });
+ $(no).click(function () {
+ dontShowAgain();
+ modal.delete();
+ });
+ };
+
UIElements.displayFriendRequestModal = function (common, data) {
var msg = data.content.msg;
var userData = msg.content.user;
diff --git a/www/common/common-util.js b/www/common/common-util.js
index 41797e7c8..603e38a30 100644
--- a/www/common/common-util.js
+++ b/www/common/common-util.js
@@ -30,6 +30,15 @@
return JSON.parse(JSON.stringify(o));
};
+ Util.serializeError = function (err) {
+ if (!(err instanceof Error)) { return err; }
+ var ser = {};
+ Object.getOwnPropertyNames(err).forEach(function (key) {
+ ser[key] = err[key];
+ });
+ return ser;
+ };
+
Util.tryParse = function (s) {
try { return JSON.parse(s); } catch (e) { return;}
};
@@ -113,13 +122,13 @@
var handle = function (id, args) {
var fn = pending[id];
if (typeof(fn) !== 'function') {
- errorHandler("MISSING_CALLBACK", {
+ return void errorHandler("MISSING_CALLBACK", {
id: id,
args: args,
});
}
try {
- pending[id].apply(null, Array.isArray(args)? args : [args]);
+ fn.apply(null, Array.isArray(args)? args : [args]);
} catch (err) {
errorHandler('HANDLER_ERROR', {
error: err,
@@ -265,28 +274,73 @@
// given a path, asynchronously return an arraybuffer
- Util.fetch = function (src, cb, progress) {
- var CB = Util.once(cb);
+ var getCacheKey = function (src) {
+ var _src = src.replace(/(\/)*$/, ''); // Remove trailing slashes
+ var idx = _src.lastIndexOf('/');
+ var cacheKey = _src.slice(idx+1);
+ if (!/^[a-f0-9]{48}$/.test(cacheKey)) { cacheKey = undefined; }
+ return cacheKey;
+ };
+ Util.fetch = function (src, cb, progress, cache) {
+ var CB = Util.once(Util.mkAsync(cb));
+
+ var cacheKey = getCacheKey(src);
+ var getBlobCache = function (id, cb) {
+ if (!cache || typeof(cache.getBlobCache) !== "function") { return void cb('EINVAL'); }
+ cache.getBlobCache(id, cb);
+ };
+ var setBlobCache = function (id, u8, cb) {
+ if (!cache || typeof(cache.setBlobCache) !== "function") { return void cb('EINVAL'); }
+ cache.setBlobCache(id, u8, cb);
+ };
- var xhr = new XMLHttpRequest();
- xhr.open("GET", src, true);
- if (progress) {
- xhr.addEventListener("progress", function (evt) {
- if (evt.lengthComputable) {
- var percentComplete = evt.loaded / evt.total;
- progress(percentComplete);
+ var xhr;
+
+ var fetch = function () {
+ xhr = new XMLHttpRequest();
+ xhr.open("GET", src, true);
+ if (progress) {
+ xhr.addEventListener("progress", function (evt) {
+ if (evt.lengthComputable) {
+ var percentComplete = evt.loaded / evt.total;
+ progress(percentComplete);
+ }
+ }, false);
+ }
+ xhr.responseType = "arraybuffer";
+ xhr.onerror = function (err) { CB(err); };
+ xhr.onload = function () {
+ if (/^4/.test(''+this.status)) {
+ return CB('XHR_ERROR');
}
- }, false);
- }
- xhr.responseType = "arraybuffer";
- xhr.onerror = function (err) { CB(err); };
- xhr.onload = function () {
- if (/^4/.test(''+this.status)) {
- return CB('XHR_ERROR');
+
+ var arrayBuffer = xhr.response;
+ if (arrayBuffer) {
+ var u8 = new Uint8Array(arrayBuffer);
+ if (cacheKey) {
+ return void setBlobCache(cacheKey, u8, function () {
+ CB(null, u8);
+ });
+ }
+ return void CB(void 0, u8);
+ }
+ CB('ENOENT');
+ };
+ xhr.send(null);
+ };
+
+ if (!cacheKey) { return void fetch(); }
+
+ getBlobCache(cacheKey, function (err, u8) {
+ if (err || !u8) { return void fetch(); }
+ CB(void 0, u8);
+ });
+
+ return {
+ cancel: function () {
+ if (xhr && xhr.abort) { xhr.abort(); }
}
- return void CB(void 0, new Uint8Array(xhr.response));
};
- xhr.send(null);
};
Util.dataURIToBlob = function (dataURI) {
diff --git a/www/common/cryptget.js b/www/common/cryptget.js
index e394788d7..1cbd5056e 100644
--- a/www/common/cryptget.js
+++ b/www/common/cryptget.js
@@ -6,10 +6,11 @@ define([
'/common/common-hash.js',
'/common/common-realtime.js',
'/common/outer/network-config.js',
+ '/common/outer/cache-store.js',
'/common/pinpad.js',
'/bower_components/nthen/index.js',
'/bower_components/chainpad/chainpad.dist.js',
-], function (Crypto, CPNetflux, Netflux, Util, Hash, Realtime, NetConfig, Pinpad, nThen) {
+], function (Crypto, CPNetflux, Netflux, Util, Hash, Realtime, NetConfig, Cache, Pinpad, nThen) {
var finish = function (S, err, doc) {
if (S.done) { return; }
S.cb(err, doc);
@@ -92,7 +93,8 @@ define([
validateKey: secret.keys.validateKey || undefined,
crypto: Crypto.createEncryptor(secret.keys),
logLevel: 0,
- initialState: opt.initialState
+ initialState: opt.initialState,
+ Cache: Cache
};
return config;
};
@@ -132,9 +134,11 @@ define([
};
config.onError = function (info) {
+ console.warn(info);
finish(Session, info.error);
};
config.onChannelError = function (info) {
+ console.error(info);
finish(Session, info.error);
};
diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js
index ca65ce77b..006f150d7 100644
--- a/www/common/cryptpad-common.js
+++ b/www/common/cryptpad-common.js
@@ -3,6 +3,7 @@ define([
'/customize/messages.js',
'/common/common-util.js',
'/common/common-hash.js',
+ '/common/outer/cache-store.js',
'/common/common-messaging.js',
'/common/common-constants.js',
'/common/common-feedback.js',
@@ -14,7 +15,7 @@ define([
'/customize/application_config.js',
'/bower_components/nthen/index.js',
-], function (Config, Messages, Util, Hash,
+], function (Config, Messages, Util, Hash, Cache,
Messaging, Constants, Feedback, Visible, UserObject, LocalStore, Channel, Block,
AppConfig, Nthen) {
@@ -147,6 +148,22 @@ define([
};
send();
};
+ common.fixRosterHash = function () {
+ // Push teams keys
+ postMessage("GET", {
+ key: ['teams'],
+ }, function (obj) {
+ if (obj.error) { return console.error(obj.error); }
+ Object.keys(obj || {}).forEach(function (id) {
+ postMessage("SET", {
+ key: ['teams', id, 'keys', 'roster', 'lastKnownHash'],
+ value: ''
+ }, function () {
+ console.log('done, please close all your CryptPad tabs before testing the fix');
+ });
+ });
+ });
+ };
(function () {
var bypassHashChange = function (key) {
@@ -701,7 +718,7 @@ define([
});
};
- common.useFile = function (Crypt, cb, optsPut) {
+ common.useFile = function (Crypt, cb, optsPut, onProgress) {
var fileHost = Config.fileHost || window.location.origin;
var data = common.fromFileData;
var parsed = Hash.parsePadUrl(data.href);
@@ -758,7 +775,9 @@ define([
return void cb(err);
}
u8 = _u8;
- }));
+ }), function (progress) {
+ onProgress(progress * 50);
+ }, Cache);
}).nThen(function (waitFor) {
require(["/file/file-crypto.js"], waitFor(function (FileCrypto) {
FileCrypto.decrypt(u8, key, waitFor(function (err, _res) {
@@ -767,7 +786,9 @@ define([
return void cb(err);
}
res = _res;
- }));
+ }), function (progress) {
+ onProgress(50 + progress * 50);
+ });
}));
}).nThen(function (waitFor) {
var ext = Util.parseFilename(data.title).ext;
@@ -991,6 +1012,8 @@ define([
pad.onJoinEvent = Util.mkEvent();
pad.onLeaveEvent = Util.mkEvent();
pad.onDisconnectEvent = Util.mkEvent();
+ pad.onCacheEvent = Util.mkEvent();
+ pad.onCacheReadyEvent = Util.mkEvent();
pad.onConnectEvent = Util.mkEvent();
pad.onErrorEvent = Util.mkEvent();
pad.onMetadataEvent = Util.mkEvent();
@@ -1003,6 +1026,10 @@ define([
postMessage("GIVE_PAD_ACCESS", data, cb);
};
+ common.onCorruptedCache = function (channel) {
+ postMessage("CORRUPTED_CACHE", channel);
+ };
+
common.setPadMetadata = function (data, cb) {
postMessage('SET_PAD_METADATA', data, cb);
};
@@ -1876,12 +1903,12 @@ define([
var requestLogin = function () {
// log out so that you don't go into an endless loop...
- LocalStore.logout();
-
- // redirect them to log in, and come back when they're done.
- var href = Hash.hashToHref('', 'login');
- var url = Hash.getNewPadURL(href, { href: currentPad.href });
- window.location.href = url;
+ LocalStore.logout(function () {
+ // redirect them to log in, and come back when they're done.
+ var href = Hash.hashToHref('', 'login');
+ var url = Hash.getNewPadURL(href, { href: currentPad.href });
+ window.location.href = url;
+ });
};
common.startAccountDeletion = function (data, cb) {
@@ -1956,6 +1983,8 @@ define([
PAD_JOIN: common.padRpc.onJoinEvent.fire,
PAD_LEAVE: common.padRpc.onLeaveEvent.fire,
PAD_DISCONNECT: common.padRpc.onDisconnectEvent.fire,
+ PAD_CACHE: common.padRpc.onCacheEvent.fire,
+ PAD_CACHE_READY: common.padRpc.onCacheReadyEvent.fire,
PAD_CONNECT: common.padRpc.onConnectEvent.fire,
PAD_ERROR: common.padRpc.onErrorEvent.fire,
PAD_METADATA: common.padRpc.onMetadataEvent.fire,
@@ -2057,6 +2086,32 @@ define([
var userHash;
+ (function iOSFirefoxFix () {
+/*
+ For some bizarre reason Firefox on iOS throws an error during the
+ loading process unless we call this function. Drawing these elements
+ to the DOM presumably causes the JS engine to wait just a little bit longer
+ until some APIs we need are ready. This occurs despite all this code being
+ run after the usual dom-ready events. This fix was discovered while trying
+ to log the error messages to the DOM because it's extremely difficult
+ to debug Firefox iOS in the usual ways. In summary, computers are terrible.
+*/
+ try {
+ var style = document.createElement('style');
+ style.type = 'text/css';
+ style.appendChild(document.createTextNode('#cp-logger { display: none; }'));
+ document.head.appendChild(style);
+
+ var logger = document.createElement('div');
+ logger.setAttribute('id', 'cp-logger');
+ document.body.appendChild(logger);
+
+ var pre = document.createElement('pre');
+ pre.innerText = 'x';
+ logger.appendChild(pre);
+ } catch (err) { console.error(err); }
+ }());
+
Nthen(function (waitFor) {
if (AppConfig.beforeLogin) {
AppConfig.beforeLogin(LocalStore.isLoggedIn(), waitFor());
diff --git a/www/common/diffMarked.js b/www/common/diffMarked.js
index 734551983..71149b59f 100644
--- a/www/common/diffMarked.js
+++ b/www/common/diffMarked.js
@@ -303,14 +303,22 @@ define([
return renderParagraph(p);
};
+ // Note: iframe, video and audio are used in mediatags and are allowed in rich text pads.
var forbiddenTags = [
'SCRIPT',
- 'IFRAME',
+ //'IFRAME',
'OBJECT',
'APPLET',
- 'VIDEO', // privacy implications of videos are the same as images
- 'AUDIO', // same with audio
+ //'VIDEO', // privacy implications of videos are the same as images
+ //'AUDIO', // same with audio
+ 'SOURCE'
+ ];
+ var restrictedTags = [
+ 'IFRAME',
+ 'VIDEO',
+ 'AUDIO'
];
+
var unsafeTag = function (info) {
/*if (info.node && $(info.node).parents('media-tag').length) {
// Do not remove elements inside a media-tag
@@ -343,13 +351,20 @@ define([
if (!(node && node.parentElement)) { return; }
var parent = node.parentElement;
if (!parent) { return; }
- console.log('removing %s tag', node.nodeName);
+ console.debug('removing %s tag', node.nodeName);
parent.removeChild(node);
};
+ // Only allow iframe, video and audio with local source
+ var checkSrc = function (root) {
+ if (restrictedTags.indexOf(root.nodeName.toUpperCase()) === -1) { return true; }
+ return root.getAttribute && /^(blob\:|\/common\/pdfjs)/.test(root.getAttribute('src'));
+ };
+
var removeForbiddenTags = function (root) {
if (!root) { return; }
if (forbiddenTags.indexOf(root.nodeName.toUpperCase()) !== -1) { removeNode(root); }
+ if (!checkSrc(root)) { removeNode(root); }
slice(root.children).forEach(removeForbiddenTags);
};
@@ -658,7 +673,7 @@ define([
$(contextMenu.menu).find('li').show();
contextMenu.show(e);
});
- if ($mt.children().length) {
+ if ($mt.children().length && $mt[0]._mediaObject) {
$mt.off('click dblclick preview');
$mt.on('preview', onPreview($mt));
if ($mt.find('img').length) {
@@ -668,15 +683,15 @@ define([
}
return;
}
- MediaTag(el);
+ var mediaObject = MediaTag(el);
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
- var list_values = slice(mutation.target.children)
+ var list_values = slice(el.children)
.map(function (el) { return el.outerHTML; })
.join('');
- mediaMap[mutation.target.getAttribute('src')] = list_values;
- observer.disconnect();
+ mediaMap[el.getAttribute('src')] = list_values;
+ if (mediaObject.complete) { observer.disconnect(); }
}
});
$mt.off('click dblclick preview');
@@ -689,6 +704,7 @@ define([
});
observer.observe(el, {
attributes: false,
+ subtree: true,
childList: true,
characterData: false
});
diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js
index eba793e0f..2ad6e1fa3 100644
--- a/www/common/drive-ui.js
+++ b/www/common/drive-ui.js
@@ -42,6 +42,7 @@ define([
var APP = window.APP = {
editable: false,
+ online: true,
mobile: function () {
if (window.matchMedia) { return !window.matchMedia('(any-pointer:fine)').matches; }
else { return $('body').width() <= 600; }
@@ -267,13 +268,25 @@ define([
};
// Handle disconnect/reconnect
- var setEditable = function (state, isHistory) {
+ // If isHistory and isSf are both false, update the "APP.online" flag
+ // If isHistory is true, update the "APP.history" flag
+ // isSf is used to detect offline shared folders: setEditable is called on displayDirectory
+ var setEditable = function (state, isHistory, isSf) {
if (APP.closed || !APP.$content || !$.contains(document.documentElement, APP.$content[0])) { return; }
+ if (isHistory) {
+ APP.history = !state;
+ } else if (!isSf) {
+ APP.online = state;
+ }
+ state = APP.online && !APP.history && state;
APP.editable = !APP.readOnly && state;
+
if (!state) {
APP.$content.addClass('cp-app-drive-readonly');
- if (!isHistory) {
+ if (!APP.history || !APP.online) {
$('#cp-app-drive-connection-state').show();
+ } else {
+ $('#cp-app-drive-connection-state').hide();
}
$('[draggable="true"]').attr('draggable', false);
}
@@ -3661,6 +3674,15 @@ define([
}
var readOnlyFolder = false;
+
+ // If the shared folder is offline, add the "DISCONNECTED" banner, otherwise
+ // use the normal "editable" behavior (based on drive offline or history mode)
+ if (sfId && manager.folders[sfId].offline) {
+ setEditable(false, false, true);
+ } else {
+ setEditable(true, false, true);
+ }
+
if (APP.readOnly) {
// Read-only drive (team?)
$content.prepend($readOnly.clone());
@@ -4140,6 +4162,17 @@ define([
data.name = Util.fixFileName(folderName);
data.folderName = Util.fixFileName(folderName) + '.zip';
+ var uo = manager.user.userObject;
+ if (sfId && manager.folders[sfId]) {
+ uo = manager.folders[sfId].userObject;
+ }
+ if (uo.getFilesRecursively) {
+ data.list = uo.getFilesRecursively(folderElement).map(function (el) {
+ var d = uo.getFileData(el);
+ return d.channel;
+ });
+ }
+
APP.FM.downloadFolder(data, function (err, obj) {
console.log(err, obj);
console.log('DONE');
diff --git a/www/common/inner/cache.js b/www/common/inner/cache.js
new file mode 100644
index 000000000..be5b781c2
--- /dev/null
+++ b/www/common/inner/cache.js
@@ -0,0 +1,33 @@
+define([
+], function () {
+ var S = {};
+
+ S.create = function (sframeChan) {
+ var getBlobCache = function (id, cb) {
+ sframeChan.query('Q_GET_BLOB_CACHE', {id:id}, function (err, data) {
+ var e = err || (data && data.error);
+ if (e) { return void cb(e); }
+ if (!data || typeof(data) !== "object") { return void cb('EINVAL'); }
+ cb(null, data);
+ }, { raw: true });
+ };
+ var setBlobCache = function (id, u8, cb) {
+ sframeChan.query('Q_SET_BLOB_CACHE', {
+ id: id,
+ u8: u8
+ }, function (err, data) {
+ var e = err || (data && data.error) || undefined;
+ cb(e);
+ }, { raw: true });
+ };
+
+
+ return {
+ getBlobCache: getBlobCache,
+ setBlobCache: setBlobCache
+ };
+ };
+
+ return S;
+});
+
diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js
index 3d28d126a..80195c71c 100644
--- a/www/common/inner/common-mediatag.js
+++ b/www/common/inner/common-mediatag.js
@@ -17,11 +17,17 @@ define([
var Nacl = window.nacl;
// Configure MediaTags to use our local viewer
+ // This file is loaded by sframe-common so the following config is used in all the inner apps
if (MediaTag) {
MediaTag.setDefaultConfig('pdf', {
viewer: '/common/pdfjs/web/viewer.html'
});
+ MediaTag.setDefaultConfig('download', {
+ text: Messages.mediatag_saveButton,
+ textDl: Messages.mediatag_loadButton,
+ });
}
+ MT.MediaTag = MediaTag;
// Cache of the avatars outer html (including )
var avatars = {};
@@ -68,7 +74,7 @@ define([
childList: true,
characterData: false
});
- MediaTag($tag[0]).on('error', function (data) {
+ MediaTag($tag[0], {force: true}).on('error', function (data) {
console.error(data);
});
};
@@ -241,7 +247,6 @@ define([
var locked = false;
var show = function (_i) {
if (locked) { return; }
- locked = true;
if (_i < 0) { i = 0; }
else if (_i > tags.length -1) { i = tags.length - 1; }
else { i = _i; }
@@ -285,7 +290,6 @@ define([
if (_key) { key = 'cryptpad:' + Nacl.util.encodeBase64(_key); }
}
if (!src || !key) {
- locked = false;
$spinner.hide();
return void UI.log(Messages.error);
}
@@ -299,13 +303,18 @@ define([
locked = false;
$spinner.hide();
UI.log(Messages.error);
+ }).on('progress', function () {
+ $spinner.hide();
+ locked = true;
+ }).on('complete', function () {
+ locked = false;
+ $spinner.hide();
});
});
}
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function() {
- locked = false;
$spinner.hide();
});
});
@@ -377,6 +386,14 @@ define([
'tabindex': '-1',
'data-icon': "fa-eye",
}, Messages.pad_mediatagPreview)),
+ h('li.cp-svg', h('a.cp-app-code-context-openin.dropdown-item', {
+ 'tabindex': '-1',
+ 'data-icon': "fa-external-link",
+ }, Messages.pad_mediatagOpen)),
+ h('li.cp-svg', h('a.cp-app-code-context-share.dropdown-item', {
+ 'tabindex': '-1',
+ 'data-icon': "fa-shhare-alt",
+ }, Messages.pad_mediatagShare)),
h('li', h('a.cp-app-code-context-saveindrive.dropdown-item', {
'tabindex': '-1',
'data-icon': "fa-cloud-upload",
@@ -413,12 +430,29 @@ define([
}
else if ($this.hasClass("cp-app-code-context-download")) {
var media = Util.find($mt, [0, '_mediaObject']);
+ if (!media) { return void console.error('no media'); }
+ if (!media.complete) { return void UI.warn(Messages.mediatag_notReady); }
if (!(media && media._blob)) { return void console.error($mt); }
window.saveAs(media._blob.content, media.name);
}
else if ($this.hasClass("cp-app-code-context-open")) {
$mt.trigger('preview');
}
+ else if ($this.hasClass("cp-app-code-context-openin")) {
+ var hash = common.getHashFromMediaTag($mt);
+ common.openURL(Hash.hashToHref(hash, 'file'));
+ }
+ else if ($this.hasClass("cp-app-code-context-share")) {
+ var data = {
+ file: true,
+ pathname: '/file/',
+ hashes: {
+ fileHash: common.getHashFromMediaTag($mt)
+ },
+ title: Util.find($mt[0], ['_mediaObject', 'name']) || ''
+ };
+ common.getSframeChannel().event('EV_SHARE_OPEN', data);
+ }
});
return m;
diff --git a/www/common/inner/properties.js b/www/common/inner/properties.js
index 58f10e738..ad8571367 100644
--- a/www/common/inner/properties.js
+++ b/www/common/inner/properties.js
@@ -19,6 +19,13 @@ define([
var $d = $('
');
if (!data) { return void cb(void 0, $d); }
+ if (data.channel) {
+ $('
'}),define("spreadsheeteditor/main/app/view/ImageSettingsAdvanced",["text!spreadsheeteditor/main/app/template/ImageSettingsAdvanced.template","common/main/lib/view/AdvancedSettingsWindow","common/main/lib/component/InputField","common/main/lib/component/MetricSpinner","common/main/lib/component/CheckBox"],function(t){"use strict";SSE.Views.ImageSettingsAdvanced=Common.Views.AdvancedSettingsWindow.extend(_.extend({options:{contentWidth:300,height:342,toggleGroup:"image-adv-settings-group",properties:null,storageName:"sse-image-settings-adv-category"},initialize:function(e){_.extend(this.options,{title:this.textTitle,items:[{panelId:"id-adv-image-rotate",panelCaption:this.textRotation},{panelId:"id-adv-image-alttext",panelCaption:this.textAlt}],contentTemplate:_.template(t)({scope:this})},e),Common.Views.AdvancedSettingsWindow.prototype.initialize.call(this,this.options),this._originalProps=this.options.imageProps,this._changedProps=null},render:function(){Common.Views.AdvancedSettingsWindow.prototype.render.call(this);var t=this;this.spnAngle=new Common.UI.MetricSpinner({el:$("#image-advanced-spin-angle"),step:1,width:80,defaultUnit:"°",value:"0 °",maxValue:3600,minValue:-3600}),this.chFlipHor=new Common.UI.CheckBox({el:$("#image-advanced-checkbox-hor"),labelText:this.textHorizontally}),this.chFlipVert=new Common.UI.CheckBox({el:$("#image-advanced-checkbox-vert"),labelText:this.textVertically}),this.inputAltTitle=new Common.UI.InputField({el:$("#image-advanced-alt-title"),allowBlank:!0,validateOnBlur:!1,style:"width: 100%;"}).on("changed:after",function(){t.isAltTitleChanged=!0}),this.textareaAltDescription=this.$window.find("textarea"),this.textareaAltDescription.keydown(function(e){e.keyCode==Common.UI.Keys.RETURN&&e.stopPropagation(),t.isAltDescChanged=!0}),this.afterRender()},afterRender:function(){if(this._setDefaults(this._originalProps),this.storageName){var t=Common.localStorage.getItem(this.storageName);this.setActiveCategory(null!==t?parseInt(t):0)}},_setDefaults:function(t){if(t){var e=t.asc_getTitle();this.inputAltTitle.setValue(e||""),e=t.asc_getDescription(),this.textareaAltDescription.val(e||""),e=t.asc_getRot(),this.spnAngle.setValue(void 0==e||null===e?"":Math.floor(180*e/3.14159265358979+.5),!0),this.chFlipHor.setValue(t.asc_getFlipH()),this.chFlipVert.setValue(t.asc_getFlipV());var i=t.asc_getPluginGuid();this.btnsCategory[0].setVisible(null===i||void 0===i),this._changedProps=new Asc.asc_CImgProperty}},getSettings:function(){return this.isAltTitleChanged&&this._changedProps.asc_putTitle(this.inputAltTitle.getValue()),this.isAltDescChanged&&this._changedProps.asc_putDescription(this.textareaAltDescription.val()),this._changedProps.asc_putRot(3.14159265358979*this.spnAngle.getNumberValue()/180),this._changedProps.asc_putFlipH("checked"==this.chFlipHor.getValue()),this._changedProps.asc_putFlipV("checked"==this.chFlipVert.getValue()),{imageProps:this._changedProps}},textTitle:"Image - Advanced Settings",cancelButtonText:"Cancel",okButtonText:"Ok",textAlt:"Alternative Text",textAltTitle:"Title",textAltDescription:"Description",textAltTip:"The alternative text-based representation of the visual object information, which will be read to the people with vision or cognitive impairments to help them better understand what information there is in the image, autoshape, chart or table.",textRotation:"Rotation",textAngle:"Angle",textFlipped:"Flipped",textHorizontally:"Horizontally",textVertically:"Vertically"},SSE.Views.ImageSettingsAdvanced||{}))}),define("spreadsheeteditor/main/app/view/SetValueDialog",["common/main/lib/component/Window","common/main/lib/component/ComboBox"],function(){"use strict";SSE.Views.SetValueDialog=Common.UI.Window.extend(_.extend({options:{width:214,header:!0,style:"min-width: 214px;",cls:"modal-dlg"},initialize:function(t){_.extend(this.options,{title:this.textTitle},t||{}),this.template=['
','
','',"
",'"].join(""),this.options.tpl=_.template(this.template)(this.options),this.startvalue=this.options.startvalue,this.maxvalue=this.options.maxvalue,this.defaultUnit=this.options.defaultUnit,this.step=this.options.step,Common.UI.Window.prototype.initialize.call(this,this.options)},render:function(){if(Common.UI.Window.prototype.render.call(this),this.spnSize=new Common.UI.MetricSpinner({el:$("#id-spin-set-value"),width:182,step:this.step,defaultUnit:this.defaultUnit,minValue:0,maxValue:this.maxvalue,value:null!==this.startvalue?this.startvalue+" "+this.defaultUnit:""}),null!==this.startvalue){var t=this;setTimeout(function(){var e=t.spnSize.$input[0];document.selection?t.spnSize.$input.select():(e.selectionStart=0,e.selectionEnd=t.startvalue.toString().length)},10)}this.getChild().find(".dlg-btn").on("click",_.bind(this.onBtnClick,this)),this.spnSize.on("entervalue",_.bind(this.onPrimary,this)),this.options.rounding&&this.spnSize.on("change",_.bind(this.onChange,this)),this.spnSize.$el.find("input").focus()},_handleInput:function(t){this.options.handler&&this.options.handler.call(this,this,t),this.close()},onBtnClick:function(t){this._handleInput(t.currentTarget.attributes.result.value)},onChange:function(){var t=this.spnSize.getNumberValue();t/=this.step,t=(t|t)*this.step,this.spnSize.setValue(t,!0)},getSettings:function(){return this.spnSize.getNumberValue()},onPrimary:function(){return this._handleInput("ok"),!1},cancelButtonText:"Cancel",okButtonText:"Ok",txtMinText:"The minimum value for this field is {0}",txtMaxText:"The maximum value for this field is {0}"},SSE.Views.SetValueDialog||{}))}),void 0===Common)var Common={};define("common/main/lib/component/ColorPaletteExt",["common/main/lib/component/BaseView"],function(){"use strict";Common.UI.ColorPaletteExt=Common.UI.BaseView.extend({options:{dynamiccolors:10,allowReselect:!0,cls:"",style:""},template:_.template(['
',"<% var me = this; %>","<% $(colors).each(function(num, item) { %>","<% if (me.isColor(item)) { %>",'
",'','"].join(""),this.api=t.api,this.handler=t.handler,this.type=t.type||"number",i.tpl=_.template(this.template)(i),Common.UI.Window.prototype.initialize.call(this,i)},render:function(){Common.UI.Window.prototype.render.call(this),this.conditions=[{value:Asc.c_oAscCustomAutoFilter.equals,displayValue:this.capCondition1},{value:Asc.c_oAscCustomAutoFilter.doesNotEqual,displayValue:this.capCondition2},{value:Asc.c_oAscCustomAutoFilter.isGreaterThan,displayValue:this.capCondition3},{value:Asc.c_oAscCustomAutoFilter.isGreaterThanOrEqualTo,displayValue:this.capCondition4},{value:Asc.c_oAscCustomAutoFilter.isLessThan,displayValue:this.capCondition5},{value:Asc.c_oAscCustomAutoFilter.isLessThanOrEqualTo,displayValue:this.capCondition6}],"text"==this.type&&(this.conditions=this.conditions.concat([{value:Asc.c_oAscCustomAutoFilter.beginsWith,displayValue:this.capCondition7},{value:Asc.c_oAscCustomAutoFilter.doesNotBeginWith,displayValue:this.capCondition8},{value:Asc.c_oAscCustomAutoFilter.endsWith,displayValue:this.capCondition9},{value:Asc.c_oAscCustomAutoFilter.doesNotEndWith,displayValue:this.capCondition10},{value:Asc.c_oAscCustomAutoFilter.contains,displayValue:this.capCondition11},{value:Asc.c_oAscCustomAutoFilter.doesNotContain,displayValue:this.capCondition12}])),this.cmbCondition1=new Common.UI.ComboBox({el:$("#id-search-begin-digital-combo",this.$window),menuStyle:"min-width: 225px;max-height: 135px;",cls:"input-group-nr",data:this.conditions,scrollAlwaysVisible:!0,editable:!1}),this.cmbCondition1.setValue(Asc.c_oAscCustomAutoFilter.equals),this.conditions.splice(0,0,{value:0,displayValue:this.textNoFilter}),this.cmbCondition2=new Common.UI.ComboBox({el:$("#id-search-end-digital-combo",this.$window),menuStyle:"min-width: 225px;max-height: 135px;",cls:"input-group-nr",data:this.conditions,scrollAlwaysVisible:!0,editable:!1}),this.cmbCondition2.setValue(0),this.rbAnd=new Common.UI.RadioBox({el:$("#id-and-radio",this.$window),labelText:this.capAnd,name:"asc-radio-filter-tab",checked:!0}),this.rbOr=new Common.UI.RadioBox({el:$("#id-or-radio",this.$window),labelText:this.capOr,name:"asc-radio-filter-tab"}),this.cmbValue1=new Common.UI.ComboBox({el:$("#id-sd-cell-search-begin",this.$window),cls:"input-group-nr",menuStyle:"min-width: 225px;max-height: 135px;",scrollAlwaysVisible:!0,data:[]}),this.cmbValue2=new Common.UI.ComboBox({el:$("#id-sd-cell-search-end",this.$window),cls:"input-group-nr",menuStyle:"min-width: 225px;max-height: 135px;",scrollAlwaysVisible:!0,data:[]});var t=function(t,e){var i=t.get("intval"),n=e.get("intval"),o=void 0!==i;return o!==(void 0!==n)?o?-1:1:(!o&&(i=t.get("value").toLowerCase())&&(n=e.get("value").toLowerCase()),i==n?0:""==n||""!==i&&i1?n[1].asc_getOperator()||0:0),this.cmbValue1.setValue(null===n[0].asc_getVal()?"":n[0].asc_getVal()),this.cmbValue2.setValue(n.length>1?null===n[1].asc_getVal()?"":n[1].asc_getVal():"")}}},save:function(){if(this.api&&this.properties&&this.rbOr&&this.rbAnd&&this.cmbCondition1&&this.cmbCondition2&&this.cmbValue1&&this.cmbValue2){var t=this.properties.asc_getFilterObj();t.asc_setFilter(new Asc.CustomFilters),t.asc_setType(Asc.c_oAscAutoFilterTypes.CustomFilters);var e=t.asc_getFilter();e.asc_setCustomFilters(0==this.cmbCondition2.getValue()?[new Asc.CustomFilter]:[new Asc.CustomFilter,new Asc.CustomFilter]);var i=e.asc_getCustomFilters();e.asc_setAnd(this.rbAnd.getValue()),i[0].asc_setOperator(this.cmbCondition1.getValue()),i[0].asc_setVal(this.cmbValue1.getValue()),0!==this.cmbCondition2.getValue()&&(i[1].asc_setOperator(this.cmbCondition2.getValue()||void 0),i[1].asc_setVal(this.cmbValue2.getValue())),this.api.asc_applyAutoFilter(this.properties)}},onPrimary:function(){return this.save(),this.close(),!1},cancelButtonText:"Cancel",capAnd:"And",capCondition1:"equals",capCondition10:"does not end with",capCondition11:"contains",capCondition12:"does not contain",capCondition2:"does not equal",capCondition3:"is greater than",capCondition4:"is greater than or equal to",capCondition5:"is less than",capCondition6:"is less than or equal to",capCondition7:"begins with",capCondition8:"does not begin with",capCondition9:"ends with",capOr:"Or",textNoFilter:"no filter",textShowRows:"Show rows where",textUse1:"Use ? to present any single character",textUse2:"Use * to present any series of character",txtTitle:"Custom Filter"},SSE.Views.DigitalFilterDialog||{})),SSE.Views.Top10FilterDialog=Common.UI.Window.extend(_.extend({initialize:function(t){var e=this,i={};_.extend(i,{width:318,height:160,contentWidth:180,header:!0,cls:"filter-dlg",contentTemplate:"",title:e.txtTitle,items:[]},t),this.template=t.template||['
'),n.cmpEl.append(e),i.btnAutoCorrectPaste=new Common.UI.Button({cls:"btn-toolbar",iconCls:"btn-paste",menu:new Common.UI.Menu({items:[]})}),i.btnAutoCorrectPaste.render($("#id-document-holder-btn-autocorrect-paste"))),s.length>0){for(var a=i.btnAutoCorrectPaste.menu,l=0;lr||p>c||o.asc_getX()<0||o.asc_getY()<0?e.is(":visible")&&e.hide():(e.css({left:d-h[0],top:p-h[1]}),e.show())},onCellsRange:function(t){this.rangeSelectionMode=t!=Asc.c_oAscSelectionDialogType.None},onApiEditCell:function(t){this.isEditFormula=t==Asc.c_oAscCellEditorState.editFormula,this.isEditCell=t!=Asc.c_oAscCellEditorState.editEnd},onLockDefNameManager:function(t){this.namedrange_locked=t==Asc.c_oAscDefinedNameReason.LockDefNameManager},onChangeCropState:function(t){this.documentHolder.menuImgCrop.menu.items[0].setChecked(t,!0)},initEquationMenu:function(){if(this._currentMathObj){var t,e=this,i=e._currentMathObj.get_Type(),n=e._currentMathObj,o=[];switch(i){case Asc.c_oAscMathInterfaceType.Accent:t=new Common.UI.MenuItem({caption:e.txtRemoveAccentChar,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"remove_AccentCharacter"}}),o.push(t);break;case Asc.c_oAscMathInterfaceType.BorderBox:t=new Common.UI.MenuItem({caption:e.txtBorderProps,equation:!0,disabled:e._currentParaObjDisabled,menu:new Common.UI.Menu({menuAlign:"tl-tr",items:[{caption:n.get_HideTop()?e.txtAddTop:e.txtHideTop,equationProps:{type:i,callback:"put_HideTop",value:!n.get_HideTop()}},{caption:n.get_HideBottom()?e.txtAddBottom:e.txtHideBottom,equationProps:{type:i,callback:"put_HideBottom",value:!n.get_HideBottom()}},{caption:n.get_HideLeft()?e.txtAddLeft:e.txtHideLeft,equationProps:{type:i,callback:"put_HideLeft",value:!n.get_HideLeft()}},{caption:n.get_HideRight()?e.txtAddRight:e.txtHideRight,equationProps:{type:i,callback:"put_HideRight",value:!n.get_HideRight()}},{caption:n.get_HideHor()?e.txtAddHor:e.txtHideHor,equationProps:{type:i,callback:"put_HideHor",value:!n.get_HideHor()}},{caption:n.get_HideVer()?e.txtAddVer:e.txtHideVer,equationProps:{type:i,callback:"put_HideVer",value:!n.get_HideVer()}},{caption:n.get_HideTopLTR()?e.txtAddLT:e.txtHideLT,equationProps:{type:i,callback:"put_HideTopLTR",value:!n.get_HideTopLTR()}},{caption:n.get_HideTopRTL()?e.txtAddLB:e.txtHideLB,equationProps:{type:i,callback:"put_HideTopRTL",value:!n.get_HideTopRTL()}}]})}),o.push(t);break;case Asc.c_oAscMathInterfaceType.Bar:t=new Common.UI.MenuItem({caption:e.txtRemoveBar,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"remove_Bar"}}),o.push(t),t=new Common.UI.MenuItem({caption:n.get_Pos()==Asc.c_oAscMathInterfaceBarPos.Top?e.txtUnderbar:e.txtOverbar,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_Pos",value:n.get_Pos()==Asc.c_oAscMathInterfaceBarPos.Top?Asc.c_oAscMathInterfaceBarPos.Bottom:Asc.c_oAscMathInterfaceBarPos.Top}}),o.push(t);break;case Asc.c_oAscMathInterfaceType.Script:var s=n.get_ScriptType();s==Asc.c_oAscMathInterfaceScript.PreSubSup?(t=new Common.UI.MenuItem({caption:e.txtScriptsAfter,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_ScriptType",value:Asc.c_oAscMathInterfaceScript.SubSup}}),o.push(t),t=new Common.UI.MenuItem({caption:e.txtRemScripts,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_ScriptType",value:Asc.c_oAscMathInterfaceScript.None}}),o.push(t)):(s==Asc.c_oAscMathInterfaceScript.SubSup&&(t=new Common.UI.MenuItem({caption:e.txtScriptsBefore,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_ScriptType",value:Asc.c_oAscMathInterfaceScript.PreSubSup}}),o.push(t)),s!=Asc.c_oAscMathInterfaceScript.SubSup&&s!=Asc.c_oAscMathInterfaceScript.Sub||(t=new Common.UI.MenuItem({caption:e.txtRemSubscript,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_ScriptType",value:s==Asc.c_oAscMathInterfaceScript.SubSup?Asc.c_oAscMathInterfaceScript.Sup:Asc.c_oAscMathInterfaceScript.None}}),o.push(t)),s!=Asc.c_oAscMathInterfaceScript.SubSup&&s!=Asc.c_oAscMathInterfaceScript.Sup||(t=new Common.UI.MenuItem({caption:e.txtRemSuperscript,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_ScriptType",value:s==Asc.c_oAscMathInterfaceScript.SubSup?Asc.c_oAscMathInterfaceScript.Sub:Asc.c_oAscMathInterfaceScript.None}}),o.push(t)));break;case Asc.c_oAscMathInterfaceType.Fraction:var a=n.get_FractionType();a!=Asc.c_oAscMathInterfaceFraction.Skewed&&a!=Asc.c_oAscMathInterfaceFraction.Linear||(t=new Common.UI.MenuItem({caption:e.txtFractionStacked,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_FractionType",value:Asc.c_oAscMathInterfaceFraction.Bar}}),o.push(t)),a!=Asc.c_oAscMathInterfaceFraction.Bar&&a!=Asc.c_oAscMathInterfaceFraction.Linear||(t=new Common.UI.MenuItem({caption:e.txtFractionSkewed,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_FractionType",value:Asc.c_oAscMathInterfaceFraction.Skewed}}),o.push(t)),a!=Asc.c_oAscMathInterfaceFraction.Bar&&a!=Asc.c_oAscMathInterfaceFraction.Skewed||(t=new Common.UI.MenuItem({caption:e.txtFractionLinear,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_FractionType",value:Asc.c_oAscMathInterfaceFraction.Linear}}),o.push(t)),a!=Asc.c_oAscMathInterfaceFraction.Bar&&a!=Asc.c_oAscMathInterfaceFraction.NoBar||(t=new Common.UI.MenuItem({caption:a==Asc.c_oAscMathInterfaceFraction.Bar?e.txtRemFractionBar:e.txtAddFractionBar,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_FractionType",value:a==Asc.c_oAscMathInterfaceFraction.Bar?Asc.c_oAscMathInterfaceFraction.NoBar:Asc.c_oAscMathInterfaceFraction.Bar}}),o.push(t));break;case Asc.c_oAscMathInterfaceType.Limit:t=new Common.UI.MenuItem({caption:n.get_Pos()==Asc.c_oAscMathInterfaceLimitPos.Top?e.txtLimitUnder:e.txtLimitOver,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_Pos",value:n.get_Pos()==Asc.c_oAscMathInterfaceLimitPos.Top?Asc.c_oAscMathInterfaceLimitPos.Bottom:Asc.c_oAscMathInterfaceLimitPos.Top}}),o.push(t),t=new Common.UI.MenuItem({caption:e.txtRemLimit,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_Pos",value:Asc.c_oAscMathInterfaceLimitPos.None}}),o.push(t);break;case Asc.c_oAscMathInterfaceType.Matrix:t=new Common.UI.MenuItem({caption:n.get_HidePlaceholder()?e.txtShowPlaceholder:e.txtHidePlaceholder,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_HidePlaceholder",value:!n.get_HidePlaceholder()}}),o.push(t),t=new Common.UI.MenuItem({caption:e.insertText,equation:!0,disabled:e._currentParaObjDisabled,menu:new Common.UI.Menu({menuAlign:"tl-tr",items:[{caption:e.insertRowAboveText,equationProps:{type:i,callback:"insert_MatrixRow",value:!0}},{caption:e.insertRowBelowText,equationProps:{type:i,callback:"insert_MatrixRow",value:!1}},{caption:e.insertColumnLeftText,equationProps:{type:i,callback:"insert_MatrixColumn",value:!0}},{caption:e.insertColumnRightText,equationProps:{type:i,callback:"insert_MatrixColumn",value:!1}}]})}),o.push(t),t=new Common.UI.MenuItem({caption:e.deleteText,equation:!0,disabled:e._currentParaObjDisabled,menu:new Common.UI.Menu({menuAlign:"tl-tr",items:[{caption:e.deleteRowText,equationProps:{type:i,callback:"delete_MatrixRow"}},{caption:e.deleteColumnText,equationProps:{type:i,callback:"delete_MatrixColumn"}}]})}),o.push(t),t=new Common.UI.MenuItem({caption:e.txtMatrixAlign,equation:!0,disabled:e._currentParaObjDisabled,menu:new Common.UI.Menu({menuAlign:"tl-tr",items:[{caption:e.txtTop,checkable:!0,checked:n.get_MatrixAlign()==Asc.c_oAscMathInterfaceMatrixMatrixAlign.Top,equationProps:{type:i,callback:"put_MatrixAlign",value:Asc.c_oAscMathInterfaceMatrixMatrixAlign.Top}},{caption:e.centerText,checkable:!0,checked:n.get_MatrixAlign()==Asc.c_oAscMathInterfaceMatrixMatrixAlign.Center,equationProps:{type:i,callback:"put_MatrixAlign",value:Asc.c_oAscMathInterfaceMatrixMatrixAlign.Center}},{caption:e.txtBottom,checkable:!0,checked:n.get_MatrixAlign()==Asc.c_oAscMathInterfaceMatrixMatrixAlign.Bottom,equationProps:{type:i,callback:"put_MatrixAlign",value:Asc.c_oAscMathInterfaceMatrixMatrixAlign.Bottom}}]})}),o.push(t),t=new Common.UI.MenuItem({caption:e.txtColumnAlign,equation:!0,disabled:e._currentParaObjDisabled,menu:new Common.UI.Menu({menuAlign:"tl-tr",items:[{caption:e.leftText,checkable:!0,checked:n.get_ColumnAlign()==Asc.c_oAscMathInterfaceMatrixColumnAlign.Left,equationProps:{type:i,callback:"put_ColumnAlign",value:Asc.c_oAscMathInterfaceMatrixColumnAlign.Left}},{caption:e.centerText,checkable:!0,checked:n.get_ColumnAlign()==Asc.c_oAscMathInterfaceMatrixColumnAlign.Center,equationProps:{type:i,callback:"put_ColumnAlign",value:Asc.c_oAscMathInterfaceMatrixColumnAlign.Center}},{caption:e.rightText,checkable:!0,checked:n.get_ColumnAlign()==Asc.c_oAscMathInterfaceMatrixColumnAlign.Right,equationProps:{type:i,callback:"put_ColumnAlign",value:Asc.c_oAscMathInterfaceMatrixColumnAlign.Right}}]})}),o.push(t);break;case Asc.c_oAscMathInterfaceType.EqArray:t=new Common.UI.MenuItem({caption:e.txtInsertEqBefore,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"insert_Equation",value:!0}}),o.push(t),t=new Common.UI.MenuItem({caption:e.txtInsertEqAfter,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"insert_Equation",value:!1}}),o.push(t),t=new Common.UI.MenuItem({caption:e.txtDeleteEq,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"delete_Equation"}}),o.push(t),t=new Common.UI.MenuItem({caption:e.alignmentText,equation:!0,disabled:e._currentParaObjDisabled,menu:new Common.UI.Menu({menuAlign:"tl-tr",items:[{caption:e.txtTop,checkable:!0,checked:n.get_Align()==Asc.c_oAscMathInterfaceEqArrayAlign.Top,equationProps:{type:i,callback:"put_Align",value:Asc.c_oAscMathInterfaceEqArrayAlign.Top}},{caption:e.centerText,checkable:!0,checked:n.get_Align()==Asc.c_oAscMathInterfaceEqArrayAlign.Center,equationProps:{type:i,callback:"put_Align",value:Asc.c_oAscMathInterfaceEqArrayAlign.Center}},{caption:e.txtBottom,checkable:!0,checked:n.get_Align()==Asc.c_oAscMathInterfaceEqArrayAlign.Bottom,equationProps:{type:i,callback:"put_Align",value:Asc.c_oAscMathInterfaceEqArrayAlign.Bottom}}]})}),o.push(t);break;case Asc.c_oAscMathInterfaceType.LargeOperator:t=new Common.UI.MenuItem({caption:e.txtLimitChange,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_LimitLocation",value:n.get_LimitLocation()==Asc.c_oAscMathInterfaceNaryLimitLocation.UndOvr?Asc.c_oAscMathInterfaceNaryLimitLocation.SubSup:Asc.c_oAscMathInterfaceNaryLimitLocation.UndOvr}}),o.push(t),void 0!==n.get_HideUpper()&&(t=new Common.UI.MenuItem({caption:n.get_HideUpper()?e.txtShowTopLimit:e.txtHideTopLimit,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_HideUpper",value:!n.get_HideUpper()}}),o.push(t)),void 0!==n.get_HideLower()&&(t=new Common.UI.MenuItem({caption:n.get_HideLower()?e.txtShowBottomLimit:e.txtHideBottomLimit,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_HideLower",value:!n.get_HideLower()}}),o.push(t));break;case Asc.c_oAscMathInterfaceType.Delimiter:t=new Common.UI.MenuItem({caption:e.txtInsertArgBefore,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"insert_DelimiterArgument",value:!0}}),o.push(t),t=new Common.UI.MenuItem({caption:e.txtInsertArgAfter,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"insert_DelimiterArgument",value:!1}}),o.push(t),n.can_DeleteArgument()&&(t=new Common.UI.MenuItem({caption:e.txtDeleteArg,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"delete_DelimiterArgument"}}),o.push(t)),t=new Common.UI.MenuItem({caption:n.has_Separators()?e.txtDeleteCharsAndSeparators:e.txtDeleteChars,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"remove_DelimiterCharacters"}}),o.push(t),t=new Common.UI.MenuItem({caption:n.get_HideOpeningBracket()?e.txtShowOpenBracket:e.txtHideOpenBracket,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_HideOpeningBracket",value:!n.get_HideOpeningBracket()}}),o.push(t),t=new Common.UI.MenuItem({caption:n.get_HideClosingBracket()?e.txtShowCloseBracket:e.txtHideCloseBracket,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_HideClosingBracket",value:!n.get_HideClosingBracket()}}),o.push(t),t=new Common.UI.MenuItem({caption:e.txtStretchBrackets,equation:!0,disabled:e._currentParaObjDisabled,checkable:!0,checked:n.get_StretchBrackets(),equationProps:{type:i,callback:"put_StretchBrackets",value:!n.get_StretchBrackets()}}),o.push(t),t=new Common.UI.MenuItem({caption:e.txtMatchBrackets,equation:!0,disabled:!n.get_StretchBrackets()||e._currentParaObjDisabled,checkable:!0,checked:n.get_StretchBrackets()&&n.get_MatchBrackets(),equationProps:{type:i,callback:"put_MatchBrackets",value:!n.get_MatchBrackets()}}),o.push(t);break;case Asc.c_oAscMathInterfaceType.GroupChar:n.can_ChangePos()&&(t=new Common.UI.MenuItem({caption:n.get_Pos()==Asc.c_oAscMathInterfaceGroupCharPos.Top?e.txtGroupCharUnder:e.txtGroupCharOver,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_Pos",value:n.get_Pos()==Asc.c_oAscMathInterfaceGroupCharPos.Top?Asc.c_oAscMathInterfaceGroupCharPos.Bottom:Asc.c_oAscMathInterfaceGroupCharPos.Top}}),o.push(t),t=new Common.UI.MenuItem({caption:e.txtDeleteGroupChar,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_Pos",value:Asc.c_oAscMathInterfaceGroupCharPos.None}}),o.push(t));break;case Asc.c_oAscMathInterfaceType.Radical:void 0!==n.get_HideDegree()&&(t=new Common.UI.MenuItem({caption:n.get_HideDegree()?e.txtShowDegree:e.txtHideDegree,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"put_HideDegree",value:!n.get_HideDegree()}}),o.push(t)),t=new Common.UI.MenuItem({caption:e.txtDeleteRadical,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"remove_Radical"}}),o.push(t)}return n.can_IncreaseArgumentSize()&&(t=new Common.UI.MenuItem({caption:e.txtIncreaseArg,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"increase_ArgumentSize"}}),o.push(t)),n.can_DecreaseArgumentSize()&&(t=new Common.UI.MenuItem({caption:e.txtDecreaseArg,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"decrease_ArgumentSize"}}),o.push(t)),n.can_InsertManualBreak()&&(t=new Common.UI.MenuItem({caption:e.txtInsertBreak,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"insert_ManualBreak"}}),o.push(t)),n.can_DeleteManualBreak()&&(t=new Common.UI.MenuItem({caption:e.txtDeleteBreak,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"delete_ManualBreak"}}),o.push(t)),n.can_AlignToCharacter()&&(t=new Common.UI.MenuItem({caption:e.txtAlignToChar,equation:!0,disabled:e._currentParaObjDisabled,equationProps:{type:i,callback:"align_ToCharacter"}}),o.push(t)),o}},addEquationMenu:function(t){var e=this;e.clearEquationMenu(t);var i=e.documentHolder.textInShapeMenu,n=e.initEquationMenu();return n.length>0&&_.each(n,function(n,o){n.menu?_.each(n.menu.items,function(t){t.on("click",_.bind(e.equationCallback,e,t.options.equationProps))}):n.on("click",_.bind(e.equationCallback,e,n.options.equationProps)),i.insertItem(t,n),t++}),n.length},clearEquationMenu:function(t){for(var e=this,i=e.documentHolder.textInShapeMenu,n=t;n')}),e.paraBulletsPicker.on("item:click",_.bind(this.onSelectBullets,this)),i&&e.paraBulletsPicker.selectRecord(i.rec,!0)},onSignatureClick:function(t){var e=t.cmpEl.attr("data-value");switch(t.value){case 0:Common.NotificationCenter.trigger("protect:sign",e);break;case 1:this.api.asc_ViewCertificate(e);break;case 2:Common.NotificationCenter.trigger("protect:signature","visible",this._isDisabled,e);break;case 3:this.api.asc_RemoveSignature(e)}},onOriginalSizeClick:function(t){if(this.api){var e=this.api.asc_getOriginalImageSize(),i=e.asc_getImageWidth(),n=e.asc_getImageHeight(),o=new Asc.asc_CImgProperty;o.asc_putWidth(i),o.asc_putHeight(n),o.put_ResetCrop(!0),this.api.asc_setGraphicObjectProps(o),Common.NotificationCenter.trigger("edit:complete",this.documentHolder),Common.component.Analytics.trackEvent("DocumentHolder","Set Image Original Size")}},onImgReplace:function(t,e){var i=this;this.api&&("file"==e.value?setTimeout(function(){i.api&&i.api.asc_changeImageFromFile(),Common.NotificationCenter.trigger("edit:complete",i.documentHolder)},10):new Common.Views.ImageFromUrlDialog({handler:function(t,e){if("ok"==t&&i.api){var n=e.replace(/ /g,"");if(!_.isEmpty(n)){var o=new Asc.asc_CImgProperty;o.asc_putImageUrl(n),i.api.asc_setGraphicObjectProps(o)}}Common.NotificationCenter.trigger("edit:complete",i.documentHolder)}}).show())},onNumberFormatSelect:function(t,e){void 0!==e.value&&"advanced"!==e.value&&this.api&&this.api.asc_setCellFormat(e.options.format),Common.NotificationCenter.trigger("edit:complete",this.documentHolder)},onCustomNumberFormat:function(t){var e=this,i=e.api.asc_getLocale();!i&&(i=e.permissions.lang?parseInt(Common.util.LanguageInfo.getLocalLanguageCode(e.permissions.lang)):1033),new SSE.Views.FormatSettingsDialog({api:e.api,handler:function(t,i){i&&e.api.asc_setCellFormat(i.format),Common.NotificationCenter.trigger("edit:complete",e.documentHolder)},props:{format:t.options.numformat,formatInfo:t.options.numformatinfo,langId:i}}).show(),Common.NotificationCenter.trigger("edit:complete",this.documentHolder)},onNumberFormatOpenAfter:function(t){if(this.api){var e=this,i=e.api.asc_getLocale();if(!i&&(i=e.permissions.lang?parseInt(Common.util.LanguageInfo.getLocalLanguageCode(e.permissions.lang)):1033),this._state.langId!==i){this._state.langId=i;var n=new Asc.asc_CFormatCellsInfo
;n.asc_setType(Asc.c_oAscNumFormatType.None),n.asc_setSymbol(this._state.langId);for(var o=this.api.asc_getFormatCells(n),s=0;sOnly text values from the column can be selected for replacement.",txtExpandSort:"The data next to the selection will not be sorted. Do you want to expand the selection to include the adjacent data or continue with sorting the currently selected cells only?",txtExpand:"Expand and sort",txtSorting:"Sorting",txtSortSelected:"Sort selected",txtPaste:"Paste",txtPasteFormulas:"Paste only formula",txtPasteFormulaNumFormat:"Formula + number format",txtPasteKeepSourceFormat:"Formula + all formatting",txtPasteBorders:"Formula without borders",txtPasteColWidths:"Formula + column width",txtPasteMerge:"Merge conditional formatting",txtPasteTranspose:"Transpose",txtPasteValues:"Paste only value",txtPasteValNumFormat:"Value + number format",txtPasteValFormat:"Value + all formatting",txtPasteFormat:"Paste only formatting",txtPasteLink:"Paste Link",txtPastePicture:"Picture",txtPasteLinkPicture:"Linked Picture",txtPasteSourceFormat:"Source formatting",txtPasteDestFormat:"Destination formatting",txtKeepTextOnly:"Keep text only",txtUseTextImport:"Use text import wizard",txtUndoExpansion:"Undo table autoexpansion",txtRedoExpansion:"Redo table autoexpansion",txtAnd:"and",txtOr:"or",txtEquals:"Equals",txtNotEquals:"Does not equal",txtGreater:"Greater than",txtGreaterEquals:"Greater than or equal to",txtLess:"Less than",txtLessEquals:"Less than or equal to",txtAboveAve:"Above average",txtBelowAve:"Below average",txtBegins:"Begins with",txtNotBegins:"Does not begin with",txtEnds:"Ends with",txtNotEnds:"Does not end with",txtContains:"Contains",txtNotContains:"Does not contain",txtFilterTop:"Top",txtFilterBottom:"Bottom",txtItems:"items",txtPercent:"percent",txtEqualsToCellColor:"Equals to cell color",txtEqualsToFontColor:"Equals to font color",txtAll:"(All)",txtBlanks:"(Blanks)",txtColumn:"Column",txtImportWizard:"Text Import Wizard"},SSE.Controllers.DocumentHolder||{}))}),define("text!spreadsheeteditor/main/app/template/CellEditor.template",[],function(){return'
"].join(""))}),this.rangeList.store.comparator=function(e,i){var n=e.get(t.sort.type).toLowerCase(),o=i.get(t.sort.type).toLowerCase();return n==o?0:n0?this.textnoNames:this.textEmpty)}var a=this,l=this.rangeList.store,r=this.rangesStore.models,c=this.cmbFilter.getValue(),h=c<3?2==c:-1,d=c>2?4==c:-1;if(c>0&&(r=this.rangesStore.filter(function(t){return-1!==h?h===t.get("isTable"):-1!==d&&d===(null===t.get("scope"))})),l.reset(r,{silent:!1}),c=l.length,this.btnEditRange.setDisabled(!c),this.btnDeleteRange.setDisabled(!c),c>0){if(void 0!==e&&null!==e||(e=0),_.isNumber(e))e>c-1&&(e=c-1),this.rangeList.selectByIndex(e),setTimeout(function(){a.rangeList.scrollToRecord(l.at(e))},50);else if(e){var p=l.findWhere({name:e.asc_getName(!0),scope:e.asc_getScope()});p&&(this.rangeList.selectRecord(p),setTimeout(function(){a.rangeList.scrollToRecord(p)},50))}!0===this.userTooltip&&this.rangeList.cmpEl.find(".lock-user").length>0&&this.rangeList.cmpEl.on("mouseover",_.bind(a.onMouseOverLock,a)).on("mouseout",_.bind(a.onMouseOutLock,a))}_.delay(function(){a.rangeList.cmpEl.find(".listview").focus(),a.rangeList.scroller.update({alwaysVisibleY:!0})},100,this)},onMouseOverLock:function(t,e,i){if(!0===this.userTooltip&&$(t.target).hasClass("lock-user")){var n=this,o=$(t.target).tooltip({title:this.tipIsLocked,trigger:"manual"}).data("bs.tooltip");this.userTooltip=o.tip(),this.userTooltip.css("z-index",parseInt(this.$window.css("z-index"))+10),o.show(),setTimeout(function(){n.userTipHide()},5e3)}},userTipHide:function(){"object"==typeof this.userTooltip&&(this.userTooltip.remove(),this.userTooltip=void 0,this.rangeList.cmpEl.off("mouseover").off("mouseout"))},onMouseOutLock:function(t,e,i){"object"==typeof this.userTooltip&&this.userTipHide()},onEditRange:function(t){if(this.locked)return void Common.NotificationCenter.trigger("namedrange:locked");var e=this,i=e.$window.offset(),n=this.rangeList.getSelectedRec(),o=(_.indexOf(this.rangeList.store.models,n),t&&n?new Asc.asc_CDefName(n.get("name"),n.get("range"),n.get("scope"),n.get("isTable"),void 0,void 0,void 0,!0):null),s=new SSE.Views.NamedRangeEditDlg({api:e.api,sheets:this.sheets,props:t?o:this.props,isEdit:t,handler:function(i,n){"ok"==i&&n&&(t?(e.currentNamedRange=n,e.api.asc_editDefinedNames(o,n)):(e.cmbFilter.setValue(0),e.currentNamedRange=n,e.api.asc_setDefinedNames(n)))}}).on("close",function(){e.show(),_.delay(function(){e.rangeList.cmpEl.find(".listview").focus()},100,e)});e.hide(),s.show(i.left+65,i.top+77)},onDeleteRange:function(){var t=this.rangeList.getSelectedRec();t&&(this.currentNamedRange=_.indexOf(this.rangeList.store.models,t),this.api.asc_delDefinedNames(new Asc.asc_CDefName(t.get("name"),t.get("range"),t.get("scope"),t.get("isTable"),void 0,void 0,void 0,!0)))},getSettings:function(){return this.sort},onPrimary:function(){return!0},onDlgBtnClick:function(t){this.handler&&this.handler.call(this,t.currentTarget.attributes.result.value),this.close()},onSortNames:function(t){t!==this.sort.type?(this.sort={type:t,direction:1},this.spanSortName.toggleClass("hidden"),this.spanSortScope.toggleClass("hidden")):this.sort.direction=-this.sort.direction;var e="name"==t?this.spanSortName:this.spanSortScope;this.sort.direction>0?e.removeClass("sort-desc"):e.addClass("sort-desc"),this.rangeList.store.sort(),this.rangeList.onResetItems(),this.rangeList.scroller.update({alwaysVisibleY:!0})},getUserName:function(t){var e=SSE.getCollection("Common.Collections.Users");if(e){var i=e.findUser(t);if(i)return i.get("username")}return this.guestText},onSelectRangeItem:function(t,e,i){this.userTipHide();var n={};if(_.isFunction(i.toJSON)){if(!i.get("selected"))return;n=i.toJSON(),this.currentNamedRange=_.indexOf(this.rangeList.store.models,i),this.btnEditRange.setDisabled(n.lock),this.btnDeleteRange.setDisabled(n.lock||n.isTable)}},hide:function(){this.userTipHide(),Common.UI.Window.prototype.hide.call(this)},close:function(){this.userTipHide(),this.api.asc_unregisterCallback("asc_onLockDefNameManager",this.wrapEvents.onLockDefNameManager),this.api.asc_unregisterCallback("asc_onRefreshDefNameList",this.wrapEvents.onRefreshDefNameList),Common.UI.Window.prototype.close.call(this)},onKeyDown:function(t,e,i){i.keyCode!=Common.UI.Keys.DELETE||this.btnDeleteRange.isDisabled()||this.onDeleteRange()},onDblClickItem:function(t,e,i){this.btnEditRange.isDisabled()||this.onEditRange(!0)},onLockDefNameManager:function(t){this.locked=t==Asc.c_oAscDefinedNameReason.LockDefNameManager},txtTitle:"Name Manager",closeButtonText:"Close",okButtonText:"Ok",textDataRange:"Data Range",textNew:"New",textEdit:"Edit",textDelete:"Delete",textRanges:"Named Ranges",textScope:"Scope",textFilter:"Filter",textEmpty:"No named ranges have been created yet. Create at least one named range and it will appear in this field.",textnoNames:"No named ranges matching your filter could be found.",textFilterAll:"All",textFilterDefNames:"Defined names",textFilterTableNames:"Table names",textFilterSheet:"Names Scoped to Sheet",textFilterWorkbook:"Names Scoped to Workbook",textWorkbook:"Workbook",guestText:"Guest",tipIsLocked:"This element is being edited by another user."},SSE.Views.NameManagerDlg||{}))}),define("spreadsheeteditor/main/app/controller/CellEditor",["core","spreadsheeteditor/main/app/view/CellEditor","spreadsheeteditor/main/app/view/NameManagerDlg"],function(t){"use strict";SSE.Controllers.CellEditor=Backbone.Controller.extend({views:["CellEditor"],events:function(){return{"keyup input#ce-cell-name":_.bind(this.onCellName,this),"keyup textarea#ce-cell-content":_.bind(this.onKeyupCellEditor,this),"blur textarea#ce-cell-content":_.bind(this.onBlurCellEditor,this),"click button#ce-btn-expand":_.bind(this.expandEditorField,this),"click button#ce-func-label":_.bind(this.onInsertFunction,this)}},initialize:function(){this.addListeners({CellEditor:{},Viewport:{"layout:resizedrag":_.bind(this.onLayoutResize,this)},"Common.Views.Header":{"formulabar:hide":function(t){this.editor.setVisible(!t),Common.localStorage.setBool("sse-hidden-formula",t),Common.NotificationCenter.trigger("layout:changed","celleditor",t?"hidden":"showed")}.bind(this)}})},setApi:function(t){return this.api=t,this.api.isCEditorFocused=!1,this.api.asc_registerCallback("asc_onSelectionNameChanged",_.bind(this.onApiCellSelection,this)),this.api.asc_registerCallback("asc_onEditCell",_.bind(this.onApiEditCell,this)),this.api.asc_registerCallback("asc_onCoAuthoringDisconnect",_.bind(this.onApiDisconnect,this)),Common.NotificationCenter.on("api:disconnect",_.bind(this.onApiDisconnect,this)),Common.NotificationCenter.on("cells:range",_.bind(this.onCellsRange,this)),this.api.asc_registerCallback("asc_onLockDefNameManager",_.bind(this.onLockDefNameManager,this)),this.api.asc_registerCallback("asc_onInputKeyDown",_.bind(this.onInputKeyDown,this)),this},setMode:function(t){this.mode=t,this.editor.$btnfunc[this.mode.isEdit?"removeClass":"addClass"]("disabled"),this.editor.btnNamedRanges.setVisible(this.mode.isEdit&&!this.mode.isEditDiagram&&!this.mode.isEditMailMerge),this.mode.isEdit&&this.api.asc_registerCallback("asc_onSelectionChanged",_.bind(this.onApiSelectionChanged,this))},onInputKeyDown:function(t){if(Common.UI.Keys.UP===t.keyCode||Common.UI.Keys.DOWN===t.keyCode||Common.UI.Keys.TAB===t.keyCode||Common.UI.Keys.RETURN===t.keyCode||Common.UI.Keys.ESC===t.keyCode||Common.UI.Keys.LEFT===t.keyCode||Common.UI.Keys.RIGHT===t.keyCode){var e=$("#menu-formula-selection");e.hasClass("open")&&e.find(".dropdown-menu").trigger("keydown",t)}},onLaunch:function(){this.editor=this.createView("CellEditor",{el:"#cell-editing-box"}).render(),this.bindViewEvents(this.editor,this.events),this.editor.$el.parent().find(".after").css({zIndex:"4"}),this.editor.btnNamedRanges.menu.on("item:click",_.bind(this.onNamedRangesMenu,this)).on("show:before",_.bind(this.onNameBeforeShow,this)),this.namedrange_locked=!1},onApiEditCell:function(t){t==Asc.c_oAscCellEditorState.editStart?(this.api.isCellEdited=!0,this.editor.cellNameDisabled(!0)):t==Asc.c_oAscCellEditorState.editInCell?this.api.isCEditorFocused="clear":t==Asc.c_oAscCellEditorState.editEnd&&(this.api.isCellEdited=!1,this.api.isCEditorFocused=!1,this.editor.cellNameDisabled(!1)),this.editor.$btnfunc.toggleClass("disabled",t==Asc.c_oAscCellEditorState.editText)},onApiCellSelection:function(t){this.editor.updateCellInfo(t)},onApiSelectionChanged:function(t){var e=t.asc_getFlags().asc_getSelectionType(),i=!this.mode.isEditMailMerge&&!this.mode.isEditDiagram&&(!0===t.asc_getLocked()||!0===t.asc_getLockedTable()),n=e==Asc.c_oAscSelectionType.RangeChartText,o=e==Asc.c_oAscSelectionType.RangeChart,s=e==Asc.c_oAscSelectionType.RangeShapeText,a=e==Asc.c_oAscSelectionType.RangeShape,l=e==Asc.c_oAscSelectionType.RangeImage,r=s||a||n||o;this.editor.$btnfunc.toggleClass("disabled",l||r||i)},onApiDisconnect:function(){this.mode.isEdit=!1;var t=this.getApplication().getController("FormulaDialog");t&&t.hideDialog(),this.mode.isEdit||($("#ce-func-label",this.editor.el).addClass("disabled"),this.editor.btnNamedRanges.setVisible(!1))},onCellsRange:function(t){this.editor.cellNameDisabled(t!=Asc.c_oAscSelectionDialogType.None),this.editor.$btnfunc.toggleClass("disabled",t!=Asc.c_oAscSelectionDialogType.None)},onLayoutResize:function(t,e){"cell:edit"==e&&(this.editor.$el.height()>19?this.editor.$btnexpand.hasClass("btn-collapse")||this.editor.$btnexpand.addClass("btn-collapse"):this.editor.$btnexpand.removeClass("btn-collapse"))},onCellName:function(t){if(t.keyCode==Common.UI.Keys.RETURN){var e=this.editor.$cellname.val();e&&e.length&&this.api.asc_findCell(e),Common.NotificationCenter.trigger("edit:complete",this.editor)}},onBlurCellEditor:function(){"clear"==this.api.isCEditorFocused?this.api.isCEditorFocused=void 0:this.api.isCellEdited&&(this.api.isCEditorFocused=!0)},onKeyupCellEditor:function(t){t.keyCode!=Common.UI.Keys.RETURN||t.altKey||(this.api.isCEditorFocused="clear")},expandEditorField:function(){this.editor.$el.height()>19?(this.editor.keep_height=this.editor.$el.height(),this.editor.$el.height(19),this.editor.$btnexpand.removeClass("btn-collapse")):(this.editor.$el.height(this.editor.keep_height||74),this.editor.$btnexpand.addClass("btn-collapse")),Common.NotificationCenter.trigger("layout:changed","celleditor"),Common.NotificationCenter.trigger("edit:complete",this.editor,{restorefocus:!0})},onInsertFunction:function(){if(this.mode.isEdit&&!this.editor.$btnfunc.hasClass("disabled")){var t=this.getApplication().getController("FormulaDialog");t&&($("#ce-func-label",this.editor.el).blur(),t.showDialog())}},onNamedRangesMenu:function(t,e){var i=this;if("manager"==e.options.value){for(var n=this.api.asc_getWorksheetsCount(),o=-1,s=[],a=[];++o2)},onLockDefNameManager:function(t){this.namedrange_locked=t==Asc.c_oAscDefinedNameReason.LockDefNameManager}})}),define("common/main/lib/view/ImageFromUrlDialog",["common/main/lib/component/Window"],function(){"use strict";Common.Views.ImageFromUrlDialog=Common.UI.Window.extend(_.extend({options:{width:330,header:!1,cls:"modal-dlg"},initialize:function(t){_.extend(this.options,t||{}),this.template=['
','
',""+this.textUrl+"","
",'',"
",'"].join(""),this.options.tpl=_.template(this.template)(this.options),Common.UI.Window.prototype.initialize.call(this,this.options)},render:function(){Common.UI.Window.prototype.render.call(this);var t=this;t.inputUrl=new Common.UI.InputField({el:$("#id-dlg-url"),allowBlank:!1,blankError:t.txtEmpty,style:"width: 100%;",validateOnBlur:!1,validation:function(e){return!!/((^https?)|(^ftp)):\/\/.+/i.test(e)||t.txtNotUrl}}),this.getChild().find(".btn").on("click",_.bind(this.onBtnClick,this))},show:function(){Common.UI.Window.prototype.show.apply(this,arguments);var t=this;_.delay(function(){t.getChild("input").focus()},500)},onPrimary:function(t){return this._handleInput("ok"),!1},onBtnClick:function(t){this._handleInput(t.currentTarget.attributes.result.value)},_handleInput:function(t){if(this.options.handler){if("ok"==t&&!0!==this.inputUrl.checkValidate())return void this.inputUrl.cmpEl.find("input").focus();this.options.handler.call(this,t,this.inputUrl.getValue())}this.close()},textUrl:"Paste an image URL:",cancelButtonText:"Cancel",okButtonText:"Ok",txtEmpty:"This field is required",txtNotUrl:'This field should be a URL in the format "http://www.example.com"'},Common.Views.ImageFromUrlDialog||{}))}),void 0===Common)var Common={};if(define("common/main/lib/component/LoadMask",["common/main/lib/component/BaseView"],function(){"use strict";Common.UI.LoadMask=Common.UI.BaseView.extend(function(){var t,e,i;return{options:{cls:"",style:"",title:"Loading...",owner:document.body},template:_.template(['