resolve conflicts between rebrand and staging

pull/1/head
ansuz 4 years ago
commit c806b97076

@ -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) # 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. 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.

@ -30,7 +30,7 @@
"secure-fabric.js": "secure-v1.7.9", "secure-fabric.js": "secure-v1.7.9",
"hyperjson": "~1.4.0", "hyperjson": "~1.4.0",
"chainpad-crypto": "^0.2.0", "chainpad-crypto": "^0.2.0",
"chainpad-listmap": "^0.9.0", "chainpad-listmap": "^0.10.0",
"chainpad": "^5.2.0", "chainpad": "^5.2.0",
"file-saver": "1.3.1", "file-saver": "1.3.1",
"alertifyjs": "1.0.11", "alertifyjs": "1.0.11",

@ -42,7 +42,7 @@ module.exports = {
* *
* In a production instance this should be available ONLY over HTTPS * In a production instance this should be available ONLY over HTTPS
* using the default port for HTTPS (443) ie. https://cryptpad.fr * 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) * cryptpad/docs/example.nginx.conf (see the $main_domain variable)
* *
*/ */
@ -228,12 +228,12 @@ module.exports = {
*/ */
/* /*
customLimits: { customLimits: {
"https://my.awesome.website/user/#/1/cryptpad-user1/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=": { "[cryptpad-user1@my.awesome.website/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=]": {
limit: 20 * 1024 * 1024 * 1024, limit: 20 * 1024 * 1024 * 1024,
plan: 'insider', plan: 'insider',
note: 'storage space donated by my.awesome.website' 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, limit: 10 * 1024 * 1024 * 1024,
plan: 'insider', plan: 'insider',
note: 'storage space donated by my.awesome.website' note: 'storage space donated by my.awesome.website'

@ -213,3 +213,61 @@ media-tag * {
width: 100%; width: 100%;
height: 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;
}

@ -253,7 +253,7 @@ p.cp-password-info{
animation-timing-function: cubic-bezier(.6,0.15,0.4,0.85); animation-timing-function: cubic-bezier(.6,0.15,0.4,0.85);
} }
button.primary{ button:not(.btn).primary{
border: 1px solid #4591c4; border: 1px solid #4591c4;
padding: 8px 12px; padding: 8px 12px;
text-transform: uppercase; text-transform: uppercase;
@ -262,7 +262,7 @@ button.primary{
font-weight: bold; font-weight: bold;
} }
button.primary:hover{ button:not(.btn).primary:hover{
background-color: rgb(52, 118, 162); background-color: rgb(52, 118, 162);
} }
@ -291,7 +291,7 @@ button.primary:hover{
var built = false; var built = false;
var types = ['less', 'drive', 'migrate', 'sf', 'team', 'pad', 'end']; var types = ['less', 'drive', 'migrate', 'sf', 'team', 'pad', 'end'];
var current; var current, progress;
var makeList = function (data) { var makeList = function (data) {
var c = types.indexOf(data.type); var c = types.indexOf(data.type);
current = c; current = c;
@ -307,7 +307,7 @@ button.primary:hover{
}; };
var list = '<ul>'; var list = '<ul>';
types.forEach(function (el, i) { types.forEach(function (el, i) {
if (i >= 6) { return; } if (el === "end") { return; }
list += getLi(i); list += getLi(i);
}); });
list += '</ul>'; list += '</ul>';
@ -315,7 +315,7 @@ button.primary:hover{
}; };
var makeBar = function (data) { var makeBar = function (data) {
var c = types.indexOf(data.type); 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 progress = Math.min(data.progress, 100);
var p = (progress / l) + (100 * c / l); var p = (progress / l) + (100 * c / l);
var bar = '<div class="cp-loading-progress-bar">'+ var bar = '<div class="cp-loading-progress-bar">'+
@ -327,20 +327,34 @@ button.primary:hover{
var hasErrored = false; var hasErrored = false;
var updateLoadingProgress = function (data) { var updateLoadingProgress = function (data) {
if (!built || !data) { return; } if (!built || !data) { return; }
// Make sure progress doesn't go backward
var c = types.indexOf(data.type); 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 { try {
document.querySelector('.cp-loading-spinner-container').style.display = 'none'; var el1 = document.querySelector('.cp-loading-spinner-container');
document.querySelector('.cp-loading-progress-list').innerHTML = makeList(data); if (el1) { el1.style.display = 'none'; }
document.querySelector('.cp-loading-progress-container').innerHTML = makeBar(data); 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) { } catch (e) {
if (!hasErrored) { console.error(e); } //if (!hasErrored) { console.error(e); }
} }
}; };
window.CryptPad_updateLoadingProgress = updateLoadingProgress; window.CryptPad_updateLoadingProgress = updateLoadingProgress;
window.CryptPad_loadingError = function (err) { window.CryptPad_loadingError = function (err) {
if (!built) { return; } if (!built) { return; }
if (err === 'Error: XDR encoding failure') {
console.warn(err);
return;
}
hasErrored = true; hasErrored = true;
var err2; var err2;
if (err === 'Script error.') { if (err === 'Script error.') {

@ -69,7 +69,7 @@ define([
Msg.footer_team = "Contributors"; // XXX existing key Msg.footer_team = "Contributors"; // XXX existing key
Msg.footer_tos = "Terms of Service"; // 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 // used for the about menu
Pages.imprintLink = AppConfig.imprint ? footLink(imprintUrl, 'imprint') : undefined; Pages.imprintLink = AppConfig.imprint ? footLink(imprintUrl, 'imprint') : undefined;

@ -14,7 +14,7 @@
right: 10vw; right: 10vw;
bottom: 10vh; bottom: 10vh;
box-sizing: border-box; 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; display: none;
color: darken(@colortheme_static_apps[default], 10%); color: darken(@colortheme_static_apps[default], 10%);
max-height: 180px; max-height: 180px;

@ -2,6 +2,10 @@
@import (reference) "./variables.less"; @import (reference) "./variables.less";
.forms_main() { .forms_main() {
--LessLoader_require: LessLoader_currentFile();
}
& {
@alertify-fore: @colortheme_modal-fg; @alertify-fore: @colortheme_modal-fg;
@alertify-btn-fg: @alertify-fore; @alertify-btn-fg: @alertify-fore;
@alertify-light-bg: fade(@alertify-fore, 25%); @alertify-light-bg: fade(@alertify-fore, 25%);
@ -114,7 +118,9 @@
margin: 0; 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%); background-color: lighten(@alertify-fore, 35%);
} }
@ -124,19 +130,32 @@
font-weight: bold; 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 { &.danger, &.btn-danger {
background-color: @colortheme_alertify-red; background-color: @colortheme_alertify-red;
border-color: @colortheme_alertify-red-border; border-color: @colortheme_alertify-red-border;
color: @colortheme_alertify-red-color; 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%)); 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; border-color: @colortheme_alertify-red;
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; color: @colortheme_alertify-red-color;
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-red, 10%), lighten(@colortheme_alertify-red, 10%)); 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; background-color: @colortheme_alertify-green;
border-color: @colortheme_alertify-green-border; border-color: @colortheme_alertify-green-border;
color: @colortheme_alertify-green-color; 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%)); 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; background-color: @colortheme_alertify-primary;
color: @colortheme_alertify-primary-text; color: @colortheme_alertify-primary-text;
border-color: @colortheme_alertify-primary-border; border-color: @colortheme_alertify-primary-border;
font-weight: bold; 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%)); 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; border-color: @cryptpad_text_col;
color: @cryptpad_text_col; color: @cryptpad_text_col;
background-color: transparent; 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%); background-color: fade(@cryptpad_text_col, 25%);
} }
} }
@ -173,7 +198,9 @@
&.cancel, &.btn-cancel { &.cancel, &.btn-cancel {
border-color: @colortheme_alertify-cancel-border; border-color: @colortheme_alertify-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%); background-color: fade(@colortheme_alertify-cancel-border, 25%);
} }
} }
@ -185,7 +212,7 @@
&:focus { &:focus {
//border: 1px dotted @alertify-base; //border: 1px dotted @alertify-base;
box-shadow: 0px 0px 5px @colortheme_alertify-primary; box-shadow: 0px 0px 5px @colortheme_alertify-primary !important;
outline: none; outline: none;
} }
&::-moz-focus-inner { &::-moz-focus-inner {
@ -202,5 +229,4 @@
} }
} }
} }
} }

@ -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() { .markdown_cryptpad() {
word-wrap: break-word; word-wrap: break-word;
@ -84,23 +135,8 @@
margin-top: 4px; margin-top: 4px;
} }
} }
media-tag {
cursor: pointer; .mediatag_cryptpad();
* {
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;
}
pre.markmap { pre.markmap {
border: 1px solid #ddd; border: 1px solid #ddd;

@ -1,6 +1,7 @@
@import (reference) "./colortheme-all.less"; @import (reference) "./colortheme-all.less";
@import (reference) "./variables.less"; @import (reference) "./variables.less";
@import (reference) "./browser.less"; @import (reference) "./browser.less";
@import (reference) "./markdown.less";
.modals-ui-elements_main() { .modals-ui-elements_main() {
--LessLoader_require: LessLoader_currentFile(); --LessLoader_require: LessLoader_currentFile();
@ -214,6 +215,7 @@
flex: 1; flex: 1;
min-width: 0; min-width: 0;
overflow: auto; overflow: auto;
.mediatag_cryptpad();
media-tag { media-tag {
& > * { & > * {
max-width: 100%; max-width: 100%;

@ -118,7 +118,7 @@
//border-radius: 0 0.25em 0.25em 0; //border-radius: 0 0.25em 0.25em 0;
//border: 1px solid #adadad; //border: 1px solid #adadad;
border-left: 0px; border-left: 0px;
height: @variables_input-height; height: 40px;
margin: 0 !important; margin: 0 !important;
} }
} }

@ -78,7 +78,7 @@
} }
&.cp-support-list-closed { &.cp-support-list-closed {
.cp-support-list-actions { .cp-support-list-actions {
display: block !important; display: flex !important;
.cp-support-answer, .cp-support-close { .cp-support-answer, .cp-support-close {
display: none; display: none;
} }

@ -2,10 +2,12 @@
@import (reference) "../include/colortheme-all.less"; @import (reference) "../include/colortheme-all.less";
@import (reference) "../include/alertify.less"; @import (reference) "../include/alertify.less";
@import (reference) "../include/checkmark.less"; @import (reference) "../include/checkmark.less";
@import (reference) "../include/forms.less";
&.cp-page-login { &.cp-page-login {
.infopages_main(); .infopages_main();
.alertify_main(); .alertify_main();
.forms_main();
.checkmark_main(20px); .checkmark_main(20px);
.form-group { .form-group {

@ -17,7 +17,7 @@ SyslogIdentifier=cryptpad
User=cryptpad User=cryptpad
Group=cryptpad Group=cryptpad
# modify to match your working directory # 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 # 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` # cryptpad stores its data with the filesystem, so you should increase this to match the value of `ulimit -n`

@ -32,6 +32,9 @@ server {
server_name your-main-domain.com your-sandbox-domain.com; server_name your-main-domain.com your-sandbox-domain.com;
# You'll need to Set the path to your certificates and keys here # 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 /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_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; 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 Cache-Control max-age=31536000;
add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; 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-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'; 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; try_files $uri =404;
} }

@ -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 getActiveSessions = function (Env, Server, cb) {
var stats = Server.getSessionStats(); var stats = Server.getSessionStats();
cb(void 0, [ cb(void 0, [
@ -155,9 +160,19 @@ var archiveDocument = function (Env, Server, cb, data) {
switch (id.length) { switch (id.length) {
case 32: case 32:
// TODO disconnect users from active sessions // 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: 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: default:
return void cb("INVALID_ID_LENGTH"); return void cb("INVALID_ID_LENGTH");
} }
@ -167,12 +182,30 @@ var archiveDocument = function (Env, Server, cb, data) {
// Env.blobStore.archive.proof(userSafeKey, blobId, cb) // Env.blobStore.archive.proof(userSafeKey, blobId, cb)
}; };
var restoreArchivedDocument = function (Env, Server, cb) { var restoreArchivedDocument = function (Env, Server, cb, data) {
// Env.msgStore.restoreArchivedChannel(channelName, cb) var id = Array.isArray(data) && data[1];
// Env.blobStore.restore.blob(blobId, cb) if (typeof(id) !== 'string' || id.length < 32) { return void cb("EINVAL"); }
// Env.blobStore.restore.proof(userSafekey, blobId, cb)
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) // CryptPad_AsyncStore.rpc.send('ADMIN', ['CLEAR_CACHED_CHANNEL_INDEX', documentID], console.log)
@ -315,6 +348,7 @@ var commands = {
INSTANCE_STATUS: instanceStatus, INSTANCE_STATUS: instanceStatus,
GET_LIMITS: getLimits, GET_LIMITS: getLimits,
SET_LAST_EVICTION: setLastEviction, SET_LAST_EVICTION: setLastEviction,
GET_WORKER_PROFILES: getWorkerProfiles,
}; };
Admin.command = function (Env, safeKey, data, _cb, Server) { Admin.command = function (Env, safeKey, data, _cb, Server) {

@ -13,11 +13,19 @@ Data.getMetadataRaw = function (Env, channel /* channelName */, _cb) {
var cached = Env.metadata_cache[channel]; var cached = Env.metadata_cache[channel];
if (HK.isMetadataMessage(cached)) { if (HK.isMetadataMessage(cached)) {
Env.checkCache(channel);
return void cb(void 0, cached); return void cb(void 0, cached);
} }
Env.batchMetadata(channel, cb, function (done) { 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);
});
}); });
}; };

@ -75,21 +75,9 @@ Upload.upload = function (Env, safeKey, chunk, cb) {
Env.blobStore.upload(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) { Upload.complete = function (Env, safeKey, arg, cb) {
Env.blobStore.complete(safeKey, arg, function (err, id) { Env.blobStore.closeBlobstage(safeKey);
reportStatus(Env, 'UPLOAD_COMPLETE', safeKey, err, id); Env.completeUpload(safeKey, arg, false, cb);
cb(err, id);
});
}; };
Upload.cancel = function (Env, safeKey, arg, 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) { Upload.complete_owned = function (Env, safeKey, arg, cb) {
Env.blobStore.completeOwned(safeKey, arg, function (err, id) { Env.blobStore.closeBlobstage(safeKey);
reportStatus(Env, 'UPLOAD_COMPLETE_OWNED', safeKey, err, id); var user = Core.getSession(Env.Sessions, safeKey);
cb(err, id); var size = user.pendingUploadSize;
}); Env.completeUpload(safeKey, arg, true, size, cb);
}; };

@ -42,6 +42,8 @@ module.exports.create = function (config) {
metadata_cache: {}, metadata_cache: {},
channel_cache: {}, channel_cache: {},
cache_checks: {},
queueStorage: WriteQueue(), queueStorage: WriteQueue(),
queueDeletes: WriteQueue(), queueDeletes: WriteQueue(),
queueValidation: WriteQueue(), queueValidation: WriteQueue(),
@ -94,6 +96,7 @@ module.exports.create = function (config) {
disableIntegratedEviction: config.disableIntegratedEviction || false, disableIntegratedEviction: config.disableIntegratedEviction || false,
lastEviction: +new Date(), lastEviction: +new Date(),
evictionReport: {}, evictionReport: {},
commandTimers: {},
}; };
(function () { (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 () { (function () {
var custom = config.customLimits; var custom = config.customLimits;

@ -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 // 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 (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 // Since last 2 checkpoints
if (!lastKnownHash) { if (!lastKnownHash) {
waitFor.abort();
// Less than 2 checkpoints in the history: return everything // Less than 2 checkpoints in the history: return everything
if (index.cpIndex.length < 2) { return void cb(null, 0); } if (index.cpIndex.length < 2) { return void cb(null, 0); }
// Otherwise return the second last checkpoint's index // Otherwise return the second last checkpoint's index
@ -436,7 +438,15 @@ const getHistoryOffset = (Env, channelName, lastKnownHash, _cb) => {
to reconcile their differences. */ 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) => { }).nThen((w) => {
// skip past this block if the offset is anything other than -1 // skip past this block if the offset is anything other than -1

@ -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 // upload_cancel
var upload_cancel = function (Env, safeKey, fileSize, cb) { var upload_cancel = function (Env, safeKey, fileSize, cb) {
var session = Env.getSession(safeKey); var session = Env.getSession(safeKey);
@ -159,27 +168,22 @@ var upload_cancel = function (Env, safeKey, fileSize, cb) {
// upload_complete // upload_complete
var upload_complete = function (Env, safeKey, id, cb) { var upload_complete = function (Env, safeKey, id, cb) {
var session = Env.getSession(safeKey); closeBlobstage(Env, safeKey);
if (session.blobstage && session.blobstage.close) {
session.blobstage.close();
delete session.blobstage;
}
var oldPath = makeStagePath(Env, safeKey); var oldPath = makeStagePath(Env, safeKey);
var newPath = makeBlobPath(Env, id); var newPath = makeBlobPath(Env, id);
nThen(function (w) { nThen(function (w) {
// make sure the path to your final location exists // 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) { if (e) {
w.abort(); w.abort();
return void cb('RENAME_ERR'); return void cb('RENAME_ERR');
} }
}); }));
}).nThen(function (w) { }).nThen(function (w) {
// make sure there's not already something in that exact location // make sure there's not already something in that exact location
isFile(newPath, function (e, yes) { isFile(newPath, w(function (e, yes) {
if (e) { if (e) {
w.abort(); w.abort();
return void cb(e); return void cb(e);
@ -188,8 +192,8 @@ var upload_complete = function (Env, safeKey, id, cb) {
w.abort(); w.abort();
return void cb('RENAME_ERR'); return void cb('RENAME_ERR');
} }
cb(void 0, newPath, id); cb(void 0, id);
}); }));
}).nThen(function () { }).nThen(function () {
// finally, move the old file to the new path // finally, move the old file to the new path
// FIXME we could just move and handle the EEXISTS instead of the above block // 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 // owned_upload_complete
var owned_upload_complete = function (Env, safeKey, id, cb) { var owned_upload_complete = function (Env, safeKey, id, cb) {
var session = Env.getSession(safeKey); closeBlobstage(Env, 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;
}
if (!isValidId(id)) { if (!isValidId(id)) {
return void cb('EINVAL_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) { complete: function (safeKey, id, _cb) {
var cb = Util.once(Util.mkAsync(_cb)); var cb = Util.once(Util.mkAsync(_cb));
if (!isValidSafeKey(safeKey)) { return void cb('INVALID_SAFEKEY'); } if (!isValidSafeKey(safeKey)) { return void cb('INVALID_SAFEKEY'); }

@ -1,6 +1,6 @@
/*@flow*/ /*@flow*/
/* jshint esversion: 6 */ /* jshint esversion: 6 */
/* global Buffer */ /* globals Buffer */
var Fs = require("fs"); var Fs = require("fs");
var Fse = require("fs-extra"); var Fse = require("fs-extra");
var Path = require("path"); var Path = require("path");
@ -66,6 +66,10 @@ var mkTempPath = function (env, channelId) {
return mkPath(env, channelId) + '.temp'; 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 // pass in the path so we can reuse the same function for archived files
var channelExists = function (filepath, cb) { var channelExists = function (filepath, cb) {
Fs.stat(filepath, function (err, stat) { Fs.stat(filepath, function (err, stat) {
@ -131,7 +135,9 @@ const readMessagesBin = (env, id, start, msgHandler, cb) => {
const collector = createIdleStreamCollector(stream); const collector = createIdleStreamCollector(stream);
const handleMessageAndKeepStreamAlive = Util.both(msgHandler, collector.keepAlive); const handleMessageAndKeepStreamAlive = Util.both(msgHandler, collector.keepAlive);
const done = Util.both(cb, collector); 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 // 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 // truncates a file to the end of its metadata line
// TODO write the metadata in a dedicated file // TODO write the metadata in a dedicated file
var clearChannel = function (env, channelId, _cb) { var clearChannel = function (env, channelId, _cb) {
@ -213,6 +250,7 @@ var clearChannel = function (env, channelId, _cb) {
cb(); cb();
}); });
}); });
clearOffset(env, channelId, function () {});
}); });
}; };
@ -389,6 +427,7 @@ var removeChannel = function (env, channelName, cb) {
CB(labelError("E_METADATA_REMOVAL", err)); CB(labelError("E_METADATA_REMOVAL", err));
} }
})); }));
clearOffset(env, channelName, w());
}).nThen(function () { }).nThen(function () {
if (errors === 2) { if (errors === 2) {
return void CB(labelError('E_REMOVE_CHANNEL', new Error("ENOENT"))); return void CB(labelError('E_REMOVE_CHANNEL', new Error("ENOENT")));
@ -604,6 +643,8 @@ var archiveChannel = function (env, channelName, cb) {
return void cb(err); return void cb(err);
} }
})); }));
}).nThen(function (w) {
clearOffset(env, channelName, w());
}).nThen(function (w) { }).nThen(function (w) {
// archive the dedicated metadata channel // archive the dedicated metadata channel
var metadataPath = mkMetadataPath(env, channelName); var metadataPath = mkMetadataPath(env, channelName);
@ -861,6 +902,7 @@ var trimChannel = function (env, channelName, hash, _cb) {
} }
})); }));
}).nThen(function (w) { }).nThen(function (w) {
clearOffset(env, channelName, w());
cleanUp(w(function (err) { cleanUp(w(function (err) {
if (err) { if (err) {
w.abort(); 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 // METADATA METHODS
// fetch the metadata for a channel // fetch the metadata for a channel
getChannelMetadata: function (channelName, cb) { getChannelMetadata: function (channelName, cb) {

@ -44,8 +44,8 @@ const mkBufferSplit = () => {
// return a streaming function which transforms buffers into objects // return a streaming function which transforms buffers into objects
// containing the buffer and the offset from the start of the stream // containing the buffer and the offset from the start of the stream
const mkOffsetCounter = () => { const mkOffsetCounter = (offset) => {
let offset = 0; offset = offset || 0;
return Pull.map((buff) => { return Pull.map((buff) => {
const out = { offset: offset, buff: buff }; const out = { offset: offset, buff: buff };
// +1 for the eaten newline // +1 for the eaten newline
@ -59,13 +59,14 @@ const mkOffsetCounter = () => {
// that this function has a lower memory profile than our classic method // that this function has a lower memory profile than our classic method
// of reading logs line by line. // of reading logs line by line.
// it also allows the handler to abort reading at any time // 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 }); //const stream = Fs.createReadStream(path, { start: start });
let keepReading = true; let keepReading = true;
Pull( Pull(
ToPull.read(stream), ToPull.read(stream),
mkBufferSplit(), mkBufferSplit(),
mkOffsetCounter(), mkOffsetCounter(opt.offset),
Pull.asyncMap((data, moreCb) => { Pull.asyncMap((data, moreCb) => {
msgHandler(data, moreCb, () => { msgHandler(data, moreCb, () => {
try { try {

@ -1,5 +1,5 @@
/* jshint esversion: 6 */ /* jshint esversion: 6 */
/* global process */ /* globals process, Buffer */
const HK = require("../hk-util"); const HK = require("../hk-util");
const Store = require("../storage/file"); 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 ready = false;
var store; var store;
var pinStore; var pinStore;
@ -114,14 +119,15 @@ const init = function (config, _cb) {
* including the initial metadata line, if it exists * 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 messageBuf = [];
let i = 0; let i = 0;
@ -129,27 +135,42 @@ const computeIndex = function (data, cb) {
const offsetByHash = {}; const offsetByHash = {};
let offsetCount = 0; let offsetCount = 0;
let size = 0; let size = offset || 0;
var start = offset || 0;
let unconventional = false;
nThen(function (w) { nThen(function (w) {
// iterate over all messages in the channel log // iterate over all messages in the channel log
// old channels can contain metadata as the first message of the log // old channels can contain metadata as the first message of the log
// skip over metadata as that is handled elsewhere // skip over metadata as that is handled elsewhere
// otherwise index important messages in the log // otherwise index important messages in the log
store.readMessagesBin(channelName, 0, (msgObj, readMore) => { store.readMessagesBin(channelName, start, (msgObj, readMore, abort) => {
let msg; let msg;
// keep an eye out for the metadata line if you haven't already seen it // keep an eye out for the metadata line if you haven't already seen it
// but only check for metadata on the first line // but only check for metadata on the first line
if (!i && msgObj.buff.indexOf('{') === 0) { if (i) {
i++; // always increment the message counter // 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')); 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 // validate that the current line really is metadata before storing it as such
// skip this, as you already have metadata... // 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++; i++;
if (msgObj.buff.indexOf('cp|') > -1) { if (msgObj.buff.includes(CHECKPOINT_PREFIX)) {
msg = msg || HK.tryParse(Env, msgObj.buff.toString('utf8')); msg = msg || HK.tryParse(Env, msgObj.buff.toString('utf8'));
if (typeof msg === "undefined") { return readMore(); } if (typeof msg === "undefined") { return readMore(); }
// cache the offsets of checkpoints if they can be parsed // 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) { } else if (messageBuf.length > 100 && cpIndex.length === 0) {
// take the last 50 messages // take the last 50 messages
unconventional = true;
messageBuf = messageBuf.slice(-50); messageBuf = messageBuf.slice(-50);
} }
// if it's not metadata or a checkpoint then it should be a regular message // 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; 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 () { }).nThen(function () {
// return the computed index // return the computed index
CB(null, { CB(null, {
// Only keep the checkpoints included in the last 100 messages // Only keep the checkpoints included in the last 100 messages
cpIndex: HK.sliceCpIndex(cpIndex, i), cpIndex: cpIndex,
offsetByHash: offsetByHash, offsetByHash: offsetByHash,
offsets: offsetCount, offsets: offsetCount,
size: size, 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 computeMetadata = function (data, cb) {
const ref = {}; const ref = {};
const lineHandler = Meta.createLineHandler(ref, Env.Log.error); const lineHandler = Meta.createLineHandler(ref, Env.Log.error);
@ -457,6 +549,41 @@ const evictInactive = function (data, cb) {
Eviction(Env, 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 = { const COMMANDS = {
COMPUTE_INDEX: computeIndex, COMPUTE_INDEX: computeIndex,
COMPUTE_METADATA: computeMetadata, COMPUTE_METADATA: computeMetadata,
@ -471,6 +598,7 @@ const COMMANDS = {
RUN_TASKS: runTasks, RUN_TASKS: runTasks,
WRITE_TASK: writeTask, WRITE_TASK: writeTask,
EVICT_INACTIVE: evictInactive, EVICT_INACTIVE: evictInactive,
COMPLETE_UPLOAD: completeUpload,
}; };
COMMANDS.INLINE = function (data, cb) { COMMANDS.INLINE = function (data, cb) {
@ -568,7 +696,7 @@ process.on('message', function (data) {
const cb = function (err, value) { const cb = function (err, value) {
process.send({ process.send({
error: err, error: Util.serializeError(err),
txid: data.txid, txid: data.txid,
pid: data.pid, pid: data.pid,
value: value, value: value,
@ -577,7 +705,7 @@ process.on('message', function (data) {
if (!ready) { if (!ready) {
return void init(data.config, function (err) { return void init(data.config, function (err) {
if (err) { return void cb(err); } if (err) { return void cb(Util.serializeError(err)); }
ready = true; ready = true;
cb(); cb();
}); });

@ -9,10 +9,19 @@ const PID = process.pid;
const DB_PATH = 'lib/workers/db-worker'; const DB_PATH = 'lib/workers/db-worker';
const MAX_JOBS = 16; 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) { Workers.initialize = function (Env, config, _cb) {
var cb = Util.once(Util.mkAsync(_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 workers = [];
const response = Util.response(function (errLabel, info) { const response = Util.response(function (errLabel, info) {
@ -111,8 +120,11 @@ Workers.initialize = function (Env, config, _cb) {
} }
const txid = guid(); const txid = guid();
var start = +new Date();
var cb = Util.once(Util.mkAsync(Util.both(_cb, function (err /*, value */) { var cb = Util.once(Util.mkAsync(Util.both(_cb, function (err /*, value */) {
incrementTime(msg && msg.command, start);
if (err !== 'TIMEOUT') { return; } if (err !== 'TIMEOUT') { return; }
Log.debug("WORKER_TIMEOUT_CAUSE", msg);
// in the event of a timeout the user will receive an error // 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 // 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 // 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; state.tasks[txid] = msg;
// default to timing out affter 180s if no explicit timeout is passed // 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); response.expect(txid, cb, timeout);
state.worker.send(msg); state.worker.send(msg);
}; };
@ -422,6 +434,16 @@ Workers.initialize = function (Env, config, _cb) {
}, 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); cb(void 0);
}); });
}; };

14
package-lock.json generated

@ -1,6 +1,6 @@
{ {
"name": "cryptpad", "name": "cryptpad",
"version": "3.24.0", "version": "3.25.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -389,16 +389,16 @@
"optional": true "optional": true
}, },
"chainpad-crypto": { "chainpad-crypto": {
"version": "0.2.4", "version": "0.2.5",
"resolved": "https://registry.npmjs.org/chainpad-crypto/-/chainpad-crypto-0.2.4.tgz", "resolved": "https://registry.npmjs.org/chainpad-crypto/-/chainpad-crypto-0.2.5.tgz",
"integrity": "sha512-fWbVyeAv35vf/dkkQaefASlJcEfpEvfRI23Mtn+/TBBry7+LYNuJMXJiovVY35pfyw2+trKh1Py5Asg9vrmaVg==", "integrity": "sha512-K9vRsAspuX+uU1goXPz0CawpLIaOHq+1JP3WfDLqaz67LbCX/MLIUt9aMcSeIJcwZ9uMpqnbMGRktyVPoz6MCA==",
"requires": { "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": { "dependencies": {
"tweetnacl": { "tweetnacl": {
"version": "git://github.com/dchest/tweetnacl-js.git#8a21381d696acdc4e99c9f706f1ad23285795f79", "version": "git+https://github.com/dchest/tweetnacl-js.git#8a21381d696acdc4e99c9f706f1ad23285795f79",
"from": "git://github.com/dchest/tweetnacl-js.git#v0.12.2" "from": "git+https://github.com/dchest/tweetnacl-js.git#v0.12.2"
} }
} }
}, },

@ -1,11 +1,11 @@
{ {
"name": "cryptpad", "name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server", "description": "realtime collaborative visual editor with zero knowlege server",
"version": "3.24.0", "version": "3.25.0",
"license": "AGPL-3.0+", "license": "AGPL-3.0+",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/xwiki-labs/cryptpad.git" "url": "git+https://github.com/xwiki-labs/cryptpad.git"
}, },
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@ -13,7 +13,7 @@
}, },
"dependencies": { "dependencies": {
"@mcrowe/minibloom": "^0.2.0", "@mcrowe/minibloom": "^0.2.0",
"chainpad-crypto": "^0.2.2", "chainpad-crypto": "^0.2.5",
"chainpad-server": "^4.0.9", "chainpad-server": "^4.0.9",
"express": "~4.16.0", "express": "~4.16.0",
"fs-extra": "^7.0.0", "fs-extra": "^7.0.0",

@ -65,7 +65,7 @@ CryptPad is actively developed by a team at [XWiki SAS](https://www.xwiki.com),
# Contributing # 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. If you have any questions or comments, or if you're interested in contributing to Cryptpad, come say hi on IRC, `#cryptpad` on Freenode.

@ -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) { app.use(function (req, res, next) {
if (req.method === 'OPTIONS' && /\/blob\//.test(req.url)) { if (req.method === 'OPTIONS' && /\/blob\//.test(req.url)) {
res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Origin', '*');
@ -202,6 +216,7 @@ var serveConfig = (function () {
adminKeys: Env.admins, adminKeys: Env.admins,
inactiveTime: Env.inactiveTime, inactiveTime: Env.inactiveTime,
supportMailbox: Env.supportMailbox, supportMailbox: Env.supportMailbox,
defaultStorageLimit: Env.defaultStorageLimit,
maxUploadSize: Env.maxUploadSize, maxUploadSize: Env.maxUploadSize,
premiumUploadSize: Env.premiumUploadSize, premiumUploadSize: Env.premiumUploadSize,
}, null, '\t'), }, null, '\t'),

@ -27,6 +27,9 @@
margin-top: 5px; margin-top: 5px;
} }
} }
.cp-admin-setlimit-form + button {
margin-top: 5px !important;
}
.cp-admin-getlimits { .cp-admin-getlimits {
code { code {
cursor: pointer; cursor: pointer;
@ -58,14 +61,62 @@
.cp-support-container { .cp-support-container {
display: flex; 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 { .cp-support-list-actions {
margin: 10px 0px 10px 2px; 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-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 { .cp-support-list-message {
&:last-child:not(.cp-support-fromadmin) { &:last-child:not(.cp-support-fromadmin) {
color: @colortheme_cp-red; 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 { .cp-support-fromadmin {
color: @colortheme_logo-2; color: @colortheme_logo-2;
@ -93,5 +166,15 @@
color: @colortheme_logo-2; color: @colortheme_logo-2;
} }
} }
input.cp-admin-inval {
border-color: red !important;
}
.cp-admin-nopassword {
.cp-admin-pw {
display: none !important;
}
}
} }

@ -43,6 +43,8 @@ define([
'general': [ 'general': [
'cp-admin-flush-cache', 'cp-admin-flush-cache',
'cp-admin-update-limit', 'cp-admin-update-limit',
'cp-admin-archive',
'cp-admin-unarchive',
// 'cp-admin-registration', // 'cp-admin-registration',
], ],
'quota': [ 'quota': [
@ -107,6 +109,129 @@ define([
}); });
return $div; 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 () { create['registration'] = function () {
var key = 'registration'; var key = 'registration';
var $div = makeBlock(key, true); var $div = makeBlock(key, true);
@ -222,7 +347,7 @@ define([
var keyEl = h('code.cp-limit-key', key); var keyEl = h('code.cp-limit-key', key);
$(keyEl).click(function () { $(keyEl).click(function () {
$('.cp-admin-setlimit-form').find('.cp-setlimit-key').val(key); $('.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); $('.cp-admin-setlimit-form').find('.cp-setlimit-note').val(user.note);
}); });
if (compact) { if (compact) {
@ -427,7 +552,53 @@ define([
var $div = $(h('div.cp-support-container')).appendTo($container); var $div = $(h('div.cp-support-container')).appendTo($container);
var catContainer = h('div.cp-dropdown-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 category = 'all';
var $drop = APP.support.makeCategoryDropdown(catContainer, function (key) { var $drop = APP.support.makeCategoryDropdown(catContainer, function (key) {
category = key; category = key;
@ -447,37 +618,128 @@ define([
var hashesById = {}; var hashesById = {};
var reorder = function () { var getTicketData = function (id) {
var order = Object.keys(hashesById); var t = hashesById[id];
order.sort(function (id1, id2) { if (!Array.isArray(t) || !t.length) { return; }
var t1 = hashesById[id1]; var ed = Util.find(t[0], ['content', 'msg', 'content', 'sender', 'edPublic']);
var t2 = hashesById[id2]; // If one of their ticket was sent as a premium user, mark them as premium
if (!Array.isArray(t1)) { return 1; } var premium = t.some(function (msg) {
if (!Array.isArray(t2)) { return -1; } var _ed = Util.find(msg, ['content', 'msg', 'content', 'sender', 'edPublic']);
var lastMsg1 = t1[t1.length - 1]; if (ed !== _ed) { return; }
var lastMsg2 = t2[t2.length - 1]; return Util.find(t[0], ['content', 'msg', 'content', 'sender', 'plan']);
var time1 = Util.find(lastMsg1, ['content', 'msg', 'content', 'time']); });
var time2 = Util.find(lastMsg2, ['content', 'msg', 'content', 'time']); var lastMsg = t[t.length - 1];
var authorEd1 = Util.find(lastMsg1, ['content', 'msg', 'content', 'sender', 'edPublic']); var lastMsgEd = Util.find(lastMsg, ['content', 'msg', 'content', 'sender', 'edPublic']);
var authorEd2 = Util.find(lastMsg2, ['content', 'msg', 'content', 'sender', 'edPublic']); return {
var admin1 = ApiConfig.adminKeys.indexOf(authorEd1) !== -1; lastMsg: lastMsg,
var admin2 = ApiConfig.adminKeys.indexOf(authorEd2) !== -1; 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);
});
});
$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 one is answered and not the other, put the unanswered first
if (admin1 && !admin2) { return 1; } if (t1.lastAdmin && !t2.lastAdmin) { return 1; }
if (!admin1 && admin2) { return -1; } if (!t1.lastAdmin && t2.lastAdmin) { return -1; }
*/
// Otherwise, sort them by time // Otherwise, sort them by time
return time2 - time1; return t1.time - t2.time;
}); };
order.forEach(function (id, i) {
$div.find('[data-id="'+id+'"]').css('order', i); 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 to = Util.throttle(function () {
var $ticket = $div.find('.cp-support-list-ticket[data-id="'+linkedId+'"]'); var $ticket = $div.find('.cp-support-list-ticket[data-id="'+linkedId+'"]');
$ticket.addClass('cp-support-open');
$ticket[0].scrollIntoView(); $ticket[0].scrollIntoView();
linkedId = undefined; linkedId = undefined;
}, 100); }, 200);
// Register to the "support" mailbox // Register to the "support" mailbox
common.mailbox.subscribe(['supportadmin'], { common.mailbox.subscribe(['supportadmin'], {
@ -505,6 +767,7 @@ define([
if (!$ticket.length) { return; } if (!$ticket.length) { return; }
$ticket.addClass('cp-support-list-closed'); $ticket.addClass('cp-support-list-closed');
$ticket.append(APP.support.makeCloseMessage(content, hash)); $ticket.append(APP.support.makeCloseMessage(content, hash));
reorder();
return; return;
} }
if (msg.type !== 'TICKET') { return; } if (msg.type !== 'TICKET') { return; }
@ -525,13 +788,19 @@ define([
})); }));
}); });
}).nThen(function () { }).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 // if deletion failed then reactivate the button and warn
hideButton.removeAttribute('disabled'); hideButton.removeAttribute('disabled');
// and show a generic error message // and show a generic error message
UI.alert(Messages.error); UI.alert(Messages.error);
}); });
}); });
makeOpenButton($ticket);
if (category !== 'all' && $ticket.attr('data-cat') !== category) { if (category !== 'all' && $ticket.attr('data-cat') !== category) {
$ticket.hide(); $ticket.hide();
} }

@ -334,6 +334,12 @@ define([
!secret.hashData.present); !secret.hashData.present);
}, "test support for ugly tracking query paramaters in url"); }, "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) { assert(function (cb) {
var keys = Block.genkeys(Nacl.randomBytes(64)); var keys = Block.genkeys(Nacl.randomBytes(64));
var hash = Block.getBlockHash(keys); var hash = Block.getBlockHash(keys);
@ -349,7 +355,7 @@ define([
var v3 = Hash.isValidHref('/pad'); var v3 = Hash.isValidHref('/pad');
var v4 = Hash.isValidHref('/pad/'); var v4 = Hash.isValidHref('/pad/');
var res = v1 && v2 && v3 && v4; var res = Boolean(v1 && v2 && v3 && v4);
cb(res); cb(res);
if (!res) { if (!res) {
console.log(v1, v2, v3, v4); console.log(v1, v2, v3, v4);
@ -361,7 +367,7 @@ define([
var v3 = Hash.isValidHref('/pad#'); // Invalid var v3 = Hash.isValidHref('/pad#'); // Invalid
var v4 = Hash.isValidHref('/pad/#'); var v4 = Hash.isValidHref('/pad/#');
var res = v1 && v2 && v3 && v4; var res = Boolean(v1 && v2 && v3 && v4);
cb(res); cb(res);
if (!res) { if (!res) {
console.log(v1, v2, v3, v4); console.log(v1, v2, v3, v4);
@ -373,7 +379,7 @@ define([
var v3 = Hash.isValidHref('https://cryptpad.fr/pad/#67b8385b07352be53e40746d2be6ccd7XAYSuJYYqa9NfmInyHci7LNy'); var v3 = Hash.isValidHref('https://cryptpad.fr/pad/#67b8385b07352be53e40746d2be6ccd7XAYSuJYYqa9NfmInyHci7LNy');
var v4 = Hash.isValidHref('/pad/#/2/pad/edit/HGu0tK2od-2BBnwAz2ZNS-t4/p/embed'); 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); cb(res);
if (!res) { if (!res) {
console.log(v1, v2, v3, v4); console.log(v1, v2, v3, v4);

@ -169,7 +169,7 @@ define(function() {
// make them have a very slow loading time. To avoid impacting the user experience // 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. // significantly, we're limiting the number of teams per user to 3 by default.
// You can change this value here. // 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 // 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. // 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 // 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. // users don't use "fake" teams (1 member) just to increase their storage limit.
// You can change the value here. // You can change the value here.
// config.maxOwnedTeams = 1; // config.maxOwnedTeams = 5;
return config; return config;
}); });

@ -12,8 +12,8 @@ define(['/customize/application_config.js'], function (AppConfig) {
tokenKey: 'loginToken', tokenKey: 'loginToken',
displayPadCreationScreen: 'displayPadCreationScreen', displayPadCreationScreen: 'displayPadCreationScreen',
deprecatedKey: 'deprecated', deprecatedKey: 'deprecated',
MAX_TEAMS_SLOTS: AppConfig.maxTeamsSlots || 3, MAX_TEAMS_SLOTS: AppConfig.maxTeamsSlots || 5,
MAX_TEAMS_OWNED: AppConfig.maxOwnedTeams || 1, MAX_TEAMS_OWNED: AppConfig.maxOwnedTeams || 5,
// Apps // Apps
criticalApps: ['profile', 'settings', 'debug', 'admin', 'support', 'notifications'] criticalApps: ['profile', 'settings', 'debug', 'admin', 'support', 'notifications']
}; };

@ -464,6 +464,8 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app)
}; };
if (!/^https*:\/\//.test(href)) { 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('/#'); idx = href.indexOf('/#');
ret.type = href.slice(1, idx); ret.type = href.slice(1, idx);
if (idx === -1) { return ret; } 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; } if (parsed.hashData.key && !/^[a-zA-Z0-9+-/=]+$/.test(parsed.hashData.key)) { return; }
} }
} }
return true; return parsed;
}; };
Hash.decodeDataOptions = function (opts) { Hash.decodeDataOptions = function (opts) {

@ -65,12 +65,13 @@ define([
switch (e.which) { switch (e.which) {
case 27: // cancel case 27: // cancel
if (typeof(no) === 'function') { no(e); } if (typeof(no) === 'function') { no(e); }
$(el || window).off('keydown', handler);
break; break;
case 13: // enter case 13: // enter
if (typeof(yes) === 'function') { yes(e); } if (typeof(yes) === 'function') { yes(e); }
$(el || window).off('keydown', handler);
break; break;
} }
$(el || window).off('keydown', handler);
}; };
$(el || window).keydown(handler); $(el || window).keydown(handler);
@ -773,7 +774,8 @@ define([
$(originalBtn).show(); $(originalBtn).show();
}; };
$button.click(function () { $button.click(function (e) {
e.stopPropagation();
done(true); done(true);
}); });
@ -791,7 +793,8 @@ define([
to = setTimeout(todo, INTERVAL); 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 we have a validation function, continue only if it's true
if (config.validate && !config.validate()) { return; } if (config.validate && !config.validate()) { return; }
i = 1; i = 1;
@ -981,6 +984,11 @@ define([
setTimeout(cb, 750); setTimeout(cb, 750);
}; };
UI.errorLoadingScreen = function (error, transparent, exitable) { UI.errorLoadingScreen = function (error, transparent, exitable) {
if (error === 'Error: XDR encoding failure') {
console.warn(error);
return;
}
var $loading = $('#' + LOADING); var $loading = $('#' + LOADING);
if (!$loading.is(':visible') || $loading.hasClass('cp-loading-hidden')) { if (!$loading.is(':visible') || $loading.hasClass('cp-loading-hidden')) {
UI.addLoadingScreen(); UI.addLoadingScreen();

@ -91,7 +91,7 @@ define([
Msg.updateMyData = function (store, curve) { Msg.updateMyData = function (store, curve) {
var myData = createData(store.proxy, false); var myData = createData(store.proxy, false);
if (store.proxy.friends) { if (store.proxy.friends) {
store.proxy.friends.me = myData; store.proxy.friends.me = Util.clone(myData);
delete store.proxy.friends.me.channel; delete store.proxy.friends.me.channel;
} }
if (store.modules['team']) { if (store.modules['team']) {
@ -99,6 +99,7 @@ define([
} }
var todo = function (friend) { var todo = function (friend) {
if (!friend || !friend.notifications) { return; } if (!friend || !friend.notifications) { return; }
delete friend.user;
myData.channel = friend.channel; myData.channel = friend.channel;
store.mailbox.sendTo('UPDATE_DATA', myData, { store.mailbox.sendTo('UPDATE_DATA', myData, {
channel: friend.notifications, channel: friend.notifications,

@ -3,9 +3,9 @@ define([
'/common/common-util.js', '/common/common-util.js',
'/common/visible.js', '/common/visible.js',
'/common/common-hash.js', '/common/common-hash.js',
'/file/file-crypto.js', '/common/media-tag.js',
'/bower_components/tweetnacl/nacl-fast.min.js', '/bower_components/tweetnacl/nacl-fast.min.js',
], function ($, Util, Visible, Hash, FileCrypto) { ], function ($, Util, Visible, Hash, MediaTag) {
var Nacl = window.nacl; var Nacl = window.nacl;
var Thumb = { var Thumb = {
dimension: 100, dimension: 100,
@ -314,7 +314,7 @@ define([
var hexFileName = secret.channel; var hexFileName = secret.channel;
var src = fileHost + Hash.getBlobPathFromHex(hexFileName); var src = fileHost + Hash.getBlobPathFromHex(hexFileName);
var key = secret.keys && secret.keys.cryptKey; 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) {
if (e === 'XHR_ERROR') { return; } if (e === 'XHR_ERROR') { return; }
return console.error(e); return console.error(e);

@ -1602,9 +1602,9 @@ define([
content: h('span', Messages.profileButton), content: h('span', Messages.profileButton),
action: function () { action: function () {
if (padType) { if (padType) {
window.open(origin+'/profile/'); Common.openURL(origin+'/profile/');
} else { } else {
window.parent.location = origin+'/profile/'; Common.gotoURL(origin+'/profile/');
} }
}, },
}); });
@ -1613,33 +1613,36 @@ define([
options.push({ options.push({
tag: 'a', tag: 'a',
attributes: { attributes: {
'target': '_blank',
'href': origin+'/drive/',
'class': 'fa fa-hdd-o' '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) { if (padType !== 'teams' && accountName) {
options.push({ options.push({
tag: 'a', tag: 'a',
attributes: { attributes: {
'target': '_blank',
'href': origin+'/teams/',
'class': 'fa fa-users' '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) { if (padType !== 'contacts' && accountName) {
options.push({ options.push({
tag: 'a', tag: 'a',
attributes: { attributes: {
'target': '_blank',
'href': origin+'/contacts/',
'class': 'fa fa-address-book' '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') { if (padType !== 'settings') {
@ -1649,9 +1652,9 @@ define([
content: h('span', Messages.settingsButton), content: h('span', Messages.settingsButton),
action: function () { action: function () {
if (padType) { if (padType) {
window.open(origin+'/settings/'); Common.openURL(origin+'/settings/');
} else { } else {
window.parent.location = origin+'/settings/'; Common.gotoURL(origin+'/settings/');
} }
}, },
}); });
@ -1666,9 +1669,9 @@ define([
content: h('span', Messages.adminPage || 'Admin'), content: h('span', Messages.adminPage || 'Admin'),
action: function () { action: function () {
if (padType) { if (padType) {
window.open(origin+'/admin/'); Common.openURL(origin+'/admin/');
} else { } else {
window.parent.location = origin+'/admin/'; Common.gotoURL(origin+'/admin/');
} }
}, },
}); });
@ -1690,29 +1693,28 @@ define([
content: h('span', Messages.supportPage || 'Support'), content: h('span', Messages.supportPage || 'Support'),
action: function () { action: function () {
if (padType) { if (padType) {
window.open(origin+'/support/'); Common.openURL(origin+'/support/');
} else { } else {
window.parent.location = origin+'/support/'; Common.gotoURL(origin+'/support/');
} }
}, },
}); });
} }
// XXX Trade the survey for documentation /*
// if (AppConfig.surveyURL) { if (AppConfig.surveyURL) {
// options.push({ options.push({
// tag: 'a', tag: 'a',
// attributes: { attributes: {
// 'target': '_blank', 'class': 'cp-toolbar-survey fa fa-graduation-cap'
// 'rel': 'noopener', },
// 'href': AppConfig.surveyURL, content: h('span', Messages.survey),
// 'class': 'cp-toolbar-survey fa fa-graduation-cap' action: function () {
// }, Common.openUnsafeURL(AppConfig.surveyURL);
// content: h('span', Messages.survey), Feedback.send('SURVEY_CLICKED');
// action: function () { },
// Feedback.send('SURVEY_CLICKED'); });
// }, }
// }); */
// }
options.push({ options.push({
tag: 'a', tag: 'a',
attributes: { attributes: {
@ -1727,11 +1729,12 @@ define([
options.push({ options.push({
tag: 'a', tag: 'a',
attributes: { attributes: {
'target': '_blank',
'href': origin+'/index.html',
'class': 'fa fa-home' '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 // Add the change display name button if not in read only mode
/* /*
@ -1747,23 +1750,24 @@ define([
options.push({ options.push({
tag: 'a', tag: 'a',
attributes: { attributes: {
'target': '_blank',
'href': priv.plan ? priv.accounts.upgradeURL : origin+'/features.html',
'class': 'fa fa-star-o' '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) { if (!priv.plan && !Config.removeDonateButton) {
options.push({ options.push({
tag: 'a', tag: 'a',
attributes: { attributes: {
'target': '_blank',
'rel': 'noopener',
'href': priv.accounts.donateURL,
'class': 'fa fa-gift' '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), content: h('span', Messages.logoutEverywhere),
action: function () { action: function () {
Common.getSframeChannel().query('Q_LOGOUT_EVERYWHERE', null, 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), content: h('span', Messages.logoutButton),
action: function () { action: function () {
Common.logout(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', [ var block = h('div#cp-loading-burn-after-reading', [
info, info,
button h('nav', {
style: 'text-align: right'
}, button),
]); ]);
UI.errorLoadingScreen(block); 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) { UIElements.displayFriendRequestModal = function (common, data) {
var msg = data.content.msg; var msg = data.content.msg;
var userData = msg.content.user; var userData = msg.content.user;

@ -30,6 +30,15 @@
return JSON.parse(JSON.stringify(o)); 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) { Util.tryParse = function (s) {
try { return JSON.parse(s); } catch (e) { return;} try { return JSON.parse(s); } catch (e) { return;}
}; };
@ -113,13 +122,13 @@
var handle = function (id, args) { var handle = function (id, args) {
var fn = pending[id]; var fn = pending[id];
if (typeof(fn) !== 'function') { if (typeof(fn) !== 'function') {
errorHandler("MISSING_CALLBACK", { return void errorHandler("MISSING_CALLBACK", {
id: id, id: id,
args: args, args: args,
}); });
} }
try { try {
pending[id].apply(null, Array.isArray(args)? args : [args]); fn.apply(null, Array.isArray(args)? args : [args]);
} catch (err) { } catch (err) {
errorHandler('HANDLER_ERROR', { errorHandler('HANDLER_ERROR', {
error: err, error: err,
@ -265,10 +274,30 @@
// given a path, asynchronously return an arraybuffer // given a path, asynchronously return an arraybuffer
Util.fetch = function (src, cb, progress) { var getCacheKey = function (src) {
var CB = Util.once(cb); 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 xhr = new XMLHttpRequest(); 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;
var fetch = function () {
xhr = new XMLHttpRequest();
xhr.open("GET", src, true); xhr.open("GET", src, true);
if (progress) { if (progress) {
xhr.addEventListener("progress", function (evt) { xhr.addEventListener("progress", function (evt) {
@ -284,11 +313,36 @@
if (/^4/.test(''+this.status)) { if (/^4/.test(''+this.status)) {
return CB('XHR_ERROR'); return CB('XHR_ERROR');
} }
return void CB(void 0, new Uint8Array(xhr.response));
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); 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(); }
}
};
};
Util.dataURIToBlob = function (dataURI) { Util.dataURIToBlob = function (dataURI) {
var byteString = atob(dataURI.split(',')[1]); var byteString = atob(dataURI.split(',')[1]);
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];

@ -6,10 +6,11 @@ define([
'/common/common-hash.js', '/common/common-hash.js',
'/common/common-realtime.js', '/common/common-realtime.js',
'/common/outer/network-config.js', '/common/outer/network-config.js',
'/common/outer/cache-store.js',
'/common/pinpad.js', '/common/pinpad.js',
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
'/bower_components/chainpad/chainpad.dist.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) { var finish = function (S, err, doc) {
if (S.done) { return; } if (S.done) { return; }
S.cb(err, doc); S.cb(err, doc);
@ -92,7 +93,8 @@ define([
validateKey: secret.keys.validateKey || undefined, validateKey: secret.keys.validateKey || undefined,
crypto: Crypto.createEncryptor(secret.keys), crypto: Crypto.createEncryptor(secret.keys),
logLevel: 0, logLevel: 0,
initialState: opt.initialState initialState: opt.initialState,
Cache: Cache
}; };
return config; return config;
}; };
@ -132,9 +134,11 @@ define([
}; };
config.onError = function (info) { config.onError = function (info) {
console.warn(info);
finish(Session, info.error); finish(Session, info.error);
}; };
config.onChannelError = function (info) { config.onChannelError = function (info) {
console.error(info);
finish(Session, info.error); finish(Session, info.error);
}; };

@ -3,6 +3,7 @@ define([
'/customize/messages.js', '/customize/messages.js',
'/common/common-util.js', '/common/common-util.js',
'/common/common-hash.js', '/common/common-hash.js',
'/common/outer/cache-store.js',
'/common/common-messaging.js', '/common/common-messaging.js',
'/common/common-constants.js', '/common/common-constants.js',
'/common/common-feedback.js', '/common/common-feedback.js',
@ -14,7 +15,7 @@ define([
'/customize/application_config.js', '/customize/application_config.js',
'/bower_components/nthen/index.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, Messaging, Constants, Feedback, Visible, UserObject, LocalStore, Channel, Block,
AppConfig, Nthen) { AppConfig, Nthen) {
@ -147,6 +148,22 @@ define([
}; };
send(); 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 () { (function () {
var bypassHashChange = function (key) { 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 fileHost = Config.fileHost || window.location.origin;
var data = common.fromFileData; var data = common.fromFileData;
var parsed = Hash.parsePadUrl(data.href); var parsed = Hash.parsePadUrl(data.href);
@ -758,7 +775,9 @@ define([
return void cb(err); return void cb(err);
} }
u8 = _u8; u8 = _u8;
})); }), function (progress) {
onProgress(progress * 50);
}, Cache);
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
require(["/file/file-crypto.js"], waitFor(function (FileCrypto) { require(["/file/file-crypto.js"], waitFor(function (FileCrypto) {
FileCrypto.decrypt(u8, key, waitFor(function (err, _res) { FileCrypto.decrypt(u8, key, waitFor(function (err, _res) {
@ -767,7 +786,9 @@ define([
return void cb(err); return void cb(err);
} }
res = _res; res = _res;
})); }), function (progress) {
onProgress(50 + progress * 50);
});
})); }));
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
var ext = Util.parseFilename(data.title).ext; var ext = Util.parseFilename(data.title).ext;
@ -991,6 +1012,8 @@ define([
pad.onJoinEvent = Util.mkEvent(); pad.onJoinEvent = Util.mkEvent();
pad.onLeaveEvent = Util.mkEvent(); pad.onLeaveEvent = Util.mkEvent();
pad.onDisconnectEvent = Util.mkEvent(); pad.onDisconnectEvent = Util.mkEvent();
pad.onCacheEvent = Util.mkEvent();
pad.onCacheReadyEvent = Util.mkEvent();
pad.onConnectEvent = Util.mkEvent(); pad.onConnectEvent = Util.mkEvent();
pad.onErrorEvent = Util.mkEvent(); pad.onErrorEvent = Util.mkEvent();
pad.onMetadataEvent = Util.mkEvent(); pad.onMetadataEvent = Util.mkEvent();
@ -1003,6 +1026,10 @@ define([
postMessage("GIVE_PAD_ACCESS", data, cb); postMessage("GIVE_PAD_ACCESS", data, cb);
}; };
common.onCorruptedCache = function (channel) {
postMessage("CORRUPTED_CACHE", channel);
};
common.setPadMetadata = function (data, cb) { common.setPadMetadata = function (data, cb) {
postMessage('SET_PAD_METADATA', data, cb); postMessage('SET_PAD_METADATA', data, cb);
}; };
@ -1876,12 +1903,12 @@ define([
var requestLogin = function () { var requestLogin = function () {
// log out so that you don't go into an endless loop... // log out so that you don't go into an endless loop...
LocalStore.logout(); LocalStore.logout(function () {
// redirect them to log in, and come back when they're done. // redirect them to log in, and come back when they're done.
var href = Hash.hashToHref('', 'login'); var href = Hash.hashToHref('', 'login');
var url = Hash.getNewPadURL(href, { href: currentPad.href }); var url = Hash.getNewPadURL(href, { href: currentPad.href });
window.location.href = url; window.location.href = url;
});
}; };
common.startAccountDeletion = function (data, cb) { common.startAccountDeletion = function (data, cb) {
@ -1956,6 +1983,8 @@ define([
PAD_JOIN: common.padRpc.onJoinEvent.fire, PAD_JOIN: common.padRpc.onJoinEvent.fire,
PAD_LEAVE: common.padRpc.onLeaveEvent.fire, PAD_LEAVE: common.padRpc.onLeaveEvent.fire,
PAD_DISCONNECT: common.padRpc.onDisconnectEvent.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_CONNECT: common.padRpc.onConnectEvent.fire,
PAD_ERROR: common.padRpc.onErrorEvent.fire, PAD_ERROR: common.padRpc.onErrorEvent.fire,
PAD_METADATA: common.padRpc.onMetadataEvent.fire, PAD_METADATA: common.padRpc.onMetadataEvent.fire,
@ -2057,6 +2086,32 @@ define([
var userHash; 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) { Nthen(function (waitFor) {
if (AppConfig.beforeLogin) { if (AppConfig.beforeLogin) {
AppConfig.beforeLogin(LocalStore.isLoggedIn(), waitFor()); AppConfig.beforeLogin(LocalStore.isLoggedIn(), waitFor());

@ -303,14 +303,22 @@ define([
return renderParagraph(p); return renderParagraph(p);
}; };
// Note: iframe, video and audio are used in mediatags and are allowed in rich text pads.
var forbiddenTags = [ var forbiddenTags = [
'SCRIPT', 'SCRIPT',
'IFRAME', //'IFRAME',
'OBJECT', 'OBJECT',
'APPLET', 'APPLET',
'VIDEO', // privacy implications of videos are the same as images //'VIDEO', // privacy implications of videos are the same as images
'AUDIO', // same with audio //'AUDIO', // same with audio
'SOURCE'
];
var restrictedTags = [
'IFRAME',
'VIDEO',
'AUDIO'
]; ];
var unsafeTag = function (info) { var unsafeTag = function (info) {
/*if (info.node && $(info.node).parents('media-tag').length) { /*if (info.node && $(info.node).parents('media-tag').length) {
// Do not remove elements inside a media-tag // Do not remove elements inside a media-tag
@ -343,13 +351,20 @@ define([
if (!(node && node.parentElement)) { return; } if (!(node && node.parentElement)) { return; }
var parent = node.parentElement; var parent = node.parentElement;
if (!parent) { return; } if (!parent) { return; }
console.log('removing %s tag', node.nodeName); console.debug('removing %s tag', node.nodeName);
parent.removeChild(node); 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) { var removeForbiddenTags = function (root) {
if (!root) { return; } if (!root) { return; }
if (forbiddenTags.indexOf(root.nodeName.toUpperCase()) !== -1) { removeNode(root); } if (forbiddenTags.indexOf(root.nodeName.toUpperCase()) !== -1) { removeNode(root); }
if (!checkSrc(root)) { removeNode(root); }
slice(root.children).forEach(removeForbiddenTags); slice(root.children).forEach(removeForbiddenTags);
}; };
@ -658,7 +673,7 @@ define([
$(contextMenu.menu).find('li').show(); $(contextMenu.menu).find('li').show();
contextMenu.show(e); contextMenu.show(e);
}); });
if ($mt.children().length) { if ($mt.children().length && $mt[0]._mediaObject) {
$mt.off('click dblclick preview'); $mt.off('click dblclick preview');
$mt.on('preview', onPreview($mt)); $mt.on('preview', onPreview($mt));
if ($mt.find('img').length) { if ($mt.find('img').length) {
@ -668,15 +683,15 @@ define([
} }
return; return;
} }
MediaTag(el); var mediaObject = MediaTag(el);
var observer = new MutationObserver(function(mutations) { var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) { mutations.forEach(function(mutation) {
if (mutation.type === 'childList') { if (mutation.type === 'childList') {
var list_values = slice(mutation.target.children) var list_values = slice(el.children)
.map(function (el) { return el.outerHTML; }) .map(function (el) { return el.outerHTML; })
.join(''); .join('');
mediaMap[mutation.target.getAttribute('src')] = list_values; mediaMap[el.getAttribute('src')] = list_values;
observer.disconnect(); if (mediaObject.complete) { observer.disconnect(); }
} }
}); });
$mt.off('click dblclick preview'); $mt.off('click dblclick preview');
@ -689,6 +704,7 @@ define([
}); });
observer.observe(el, { observer.observe(el, {
attributes: false, attributes: false,
subtree: true,
childList: true, childList: true,
characterData: false characterData: false
}); });

@ -42,6 +42,7 @@ define([
var APP = window.APP = { var APP = window.APP = {
editable: false, editable: false,
online: true,
mobile: function () { mobile: function () {
if (window.matchMedia) { return !window.matchMedia('(any-pointer:fine)').matches; } if (window.matchMedia) { return !window.matchMedia('(any-pointer:fine)').matches; }
else { return $('body').width() <= 600; } else { return $('body').width() <= 600; }
@ -267,13 +268,25 @@ define([
}; };
// Handle disconnect/reconnect // 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 (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; APP.editable = !APP.readOnly && state;
if (!state) { if (!state) {
APP.$content.addClass('cp-app-drive-readonly'); APP.$content.addClass('cp-app-drive-readonly');
if (!isHistory) { if (!APP.history || !APP.online) {
$('#cp-app-drive-connection-state').show(); $('#cp-app-drive-connection-state').show();
} else {
$('#cp-app-drive-connection-state').hide();
} }
$('[draggable="true"]').attr('draggable', false); $('[draggable="true"]').attr('draggable', false);
} }
@ -3661,6 +3674,15 @@ define([
} }
var readOnlyFolder = false; 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) { if (APP.readOnly) {
// Read-only drive (team?) // Read-only drive (team?)
$content.prepend($readOnly.clone()); $content.prepend($readOnly.clone());
@ -4140,6 +4162,17 @@ define([
data.name = Util.fixFileName(folderName); data.name = Util.fixFileName(folderName);
data.folderName = Util.fixFileName(folderName) + '.zip'; 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) { APP.FM.downloadFolder(data, function (err, obj) {
console.log(err, obj); console.log(err, obj);
console.log('DONE'); console.log('DONE');

@ -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;
});

@ -17,11 +17,17 @@ define([
var Nacl = window.nacl; var Nacl = window.nacl;
// Configure MediaTags to use our local viewer // 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) { if (MediaTag) {
MediaTag.setDefaultConfig('pdf', { MediaTag.setDefaultConfig('pdf', {
viewer: '/common/pdfjs/web/viewer.html' 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 <media-tag>) // Cache of the avatars outer html (including <media-tag>)
var avatars = {}; var avatars = {};
@ -68,7 +74,7 @@ define([
childList: true, childList: true,
characterData: false characterData: false
}); });
MediaTag($tag[0]).on('error', function (data) { MediaTag($tag[0], {force: true}).on('error', function (data) {
console.error(data); console.error(data);
}); });
}; };
@ -241,7 +247,6 @@ define([
var locked = false; var locked = false;
var show = function (_i) { var show = function (_i) {
if (locked) { return; } if (locked) { return; }
locked = true;
if (_i < 0) { i = 0; } if (_i < 0) { i = 0; }
else if (_i > tags.length -1) { i = tags.length - 1; } else if (_i > tags.length -1) { i = tags.length - 1; }
else { i = _i; } else { i = _i; }
@ -285,7 +290,6 @@ define([
if (_key) { key = 'cryptpad:' + Nacl.util.encodeBase64(_key); } if (_key) { key = 'cryptpad:' + Nacl.util.encodeBase64(_key); }
} }
if (!src || !key) { if (!src || !key) {
locked = false;
$spinner.hide(); $spinner.hide();
return void UI.log(Messages.error); return void UI.log(Messages.error);
} }
@ -299,13 +303,18 @@ define([
locked = false; locked = false;
$spinner.hide(); $spinner.hide();
UI.log(Messages.error); UI.log(Messages.error);
}).on('progress', function () {
$spinner.hide();
locked = true;
}).on('complete', function () {
locked = false;
$spinner.hide();
}); });
}); });
} }
var observer = new MutationObserver(function(mutations) { var observer = new MutationObserver(function(mutations) {
mutations.forEach(function() { mutations.forEach(function() {
locked = false;
$spinner.hide(); $spinner.hide();
}); });
}); });
@ -377,6 +386,14 @@ define([
'tabindex': '-1', 'tabindex': '-1',
'data-icon': "fa-eye", 'data-icon': "fa-eye",
}, Messages.pad_mediatagPreview)), }, 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', { h('li', h('a.cp-app-code-context-saveindrive.dropdown-item', {
'tabindex': '-1', 'tabindex': '-1',
'data-icon': "fa-cloud-upload", 'data-icon': "fa-cloud-upload",
@ -413,12 +430,29 @@ define([
} }
else if ($this.hasClass("cp-app-code-context-download")) { else if ($this.hasClass("cp-app-code-context-download")) {
var media = Util.find($mt, [0, '_mediaObject']); 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); } if (!(media && media._blob)) { return void console.error($mt); }
window.saveAs(media._blob.content, media.name); window.saveAs(media._blob.content, media.name);
} }
else if ($this.hasClass("cp-app-code-context-open")) { else if ($this.hasClass("cp-app-code-context-open")) {
$mt.trigger('preview'); $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; return m;

@ -19,6 +19,13 @@ define([
var $d = $('<div>'); var $d = $('<div>');
if (!data) { return void cb(void 0, $d); } if (!data) { return void cb(void 0, $d); }
if (data.channel) {
$('<label>', { 'for': 'cp-app-prop-id'}).text(Messages.documentID).appendTo($d);
$d.append(UI.dialog.selectable(data.channel, {
id: 'cp-app-prop-id',
}));
}
if (data.href) { if (data.href) {
$('<label>', {'for': 'cp-app-prop-link'}).text(Messages.editShare).appendTo($d); $('<label>', {'for': 'cp-app-prop-link'}).text(Messages.editShare).appendTo($d);
$d.append(UI.dialog.selectable(data.href, { $d.append(UI.dialog.selectable(data.href, {

@ -345,7 +345,7 @@ define([
localStore.get('hide-alert-shareLinkWarning', function (val) { localStore.get('hide-alert-shareLinkWarning', function (val) {
if (val === '1') { return; } if (val === '1') { return; }
$(shareLinkWarning).show(); $(shareLinkWarning).css('display', 'flex');
$(dismissButton).on('click', function () { $(dismissButton).on('click', function () {
localStore.put('hide-alert-shareLinkWarning', '1'); localStore.put('hide-alert-shareLinkWarning', '1');

@ -1,17 +1,18 @@
define([ define([
'jquery', 'jquery',
'/common/cryptget.js',
'/file/file-crypto.js', '/file/file-crypto.js',
'/common/common-hash.js', '/common/common-hash.js',
'/common/common-util.js', '/common/common-util.js',
'/common/common-interface.js', '/common/common-interface.js',
'/common/hyperscript.js', '/common/hyperscript.js',
'/common/common-feedback.js', '/common/common-feedback.js',
'/common/inner/cache.js',
'/customize/messages.js', '/customize/messages.js',
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
'/bower_components/saferphore/index.js', '/bower_components/saferphore/index.js',
'/bower_components/jszip/dist/jszip.min.js', '/bower_components/jszip/dist/jszip.min.js',
], function ($, Crypt, FileCrypto, Hash, Util, UI, h, Feedback, Messages, nThen, Saferphore, JsZip) { ], function ($, FileCrypto, Hash, Util, UI, h, Feedback,
Cache, Messages, nThen, Saferphore, JsZip) {
var saveAs = window.saveAs; var saveAs = window.saveAs;
var sanitize = function (str) { var sanitize = function (str) {
@ -53,9 +54,6 @@ define([
var _downloadFile = function (ctx, fData, cb, updateProgress) { var _downloadFile = function (ctx, fData, cb, updateProgress) {
var cancelled = false; var cancelled = false;
var cancel = function () {
cancelled = true;
};
var href = (fData.href && fData.href.indexOf('#') !== -1) ? fData.href : fData.roHref; var href = (fData.href && fData.href.indexOf('#') !== -1) ? fData.href : fData.roHref;
var parsed = Hash.parsePadUrl(href); var parsed = Hash.parsePadUrl(href);
var hash = parsed.hash; var hash = parsed.hash;
@ -63,10 +61,13 @@ define([
var secret = Hash.getSecrets('file', hash, fData.password); var secret = Hash.getSecrets('file', hash, fData.password);
var src = (ctx.fileHost || '') + Hash.getBlobPathFromHex(secret.channel); var src = (ctx.fileHost || '') + Hash.getBlobPathFromHex(secret.channel);
var key = secret.keys && secret.keys.cryptKey; var key = secret.keys && secret.keys.cryptKey;
Util.fetch(src, function (err, u8) {
var fetchObj, decryptObj;
fetchObj = Util.fetch(src, function (err, u8) {
if (cancelled) { return; } if (cancelled) { return; }
if (err) { return void cb('E404'); } if (err) { return void cb('E404'); }
FileCrypto.decrypt(u8, key, function (err, res) { decryptObj = FileCrypto.decrypt(u8, key, function (err, res) {
if (cancelled) { return; } if (cancelled) { return; }
if (err) { return void cb(err); } if (err) { return void cb(err); }
if (!res.content) { return void cb('EEMPTY'); } if (!res.content) { return void cb('EEMPTY'); }
@ -78,8 +79,25 @@ define([
content: res.content, content: res.content,
download: dl download: dl
}); });
}, updateProgress && updateProgress.progress2); }, function (data) {
}, updateProgress && updateProgress.progress); if (cancelled) { return; }
if (updateProgress && updateProgress.progress2) {
updateProgress.progress2(data);
}
});
}, function (data) {
if (cancelled) { return; }
if (updateProgress && updateProgress.progress) {
updateProgress.progress(data);
}
}, ctx.cache);
var cancel = function () {
cancelled = true;
if (fetchObj && fetchObj.cancel) { fetchObj.cancel(); }
if (decryptObj && decryptObj.cancel) { decryptObj.cancel(); }
};
return { return {
cancel: cancel cancel: cancel
}; };
@ -162,10 +180,10 @@ define([
if (ctx.stop) { return; } if (ctx.stop) { return; }
if (to) { clearTimeout(to); } if (to) { clearTimeout(to); }
//setTimeout(g, 2000); //setTimeout(g, 2000);
g();
w();
ctx.done++; ctx.done++;
ctx.updateProgress('download', {max: ctx.max, current: ctx.done}); ctx.updateProgress('download', {max: ctx.max, current: ctx.done});
g();
w();
}; };
var error = function (err) { var error = function (err) {
@ -274,7 +292,7 @@ define([
}; };
// Main function. Create the empty zip and fill it starting from drive.root // Main function. Create the empty zip and fill it starting from drive.root
var create = function (data, getPad, fileHost, cb, progress) { var create = function (data, getPad, fileHost, cb, progress, cache) {
if (!data || !data.uo || !data.uo.drive) { return void cb('EEMPTY'); } if (!data || !data.uo || !data.uo.drive) { return void cb('EEMPTY'); }
var sem = Saferphore.create(5); var sem = Saferphore.create(5);
var ctx = { var ctx = {
@ -288,7 +306,8 @@ define([
sem: sem, sem: sem,
updateProgress: progress, updateProgress: progress,
max: 0, max: 0,
done: 0 done: 0,
cache: cache
}; };
var filesData = data.sharedFolderId && ctx.sf[data.sharedFolderId] ? ctx.sf[data.sharedFolderId].filesData : ctx.data.filesData; var filesData = data.sharedFolderId && ctx.sf[data.sharedFolderId] ? ctx.sf[data.sharedFolderId].filesData : ctx.data.filesData;
progress('reading', -1); progress('reading', -1);
@ -312,13 +331,14 @@ define([
delete ctx.zip; delete ctx.zip;
}; };
return { return {
stop: stop stop: stop,
cancel: stop
}; };
}; };
var _downloadFolder = function (ctx, data, cb, updateProgress) { var _downloadFolder = function (ctx, data, cb, updateProgress) {
create(data, ctx.get, ctx.fileHost, function (blob, errors) { return create(data, ctx.get, ctx.fileHost, function (blob, errors) {
if (errors && errors.length) { console.error(errors); } // TODO show user errors if (errors && errors.length) { console.error(errors); } // TODO show user errors
var dl = function () { var dl = function () {
saveAs(blob, data.folderName); saveAs(blob, data.folderName);
@ -332,10 +352,13 @@ define([
if (typeof progress.current !== "number") { return; } if (typeof progress.current !== "number") { return; }
updateProgress.folderProgress(progress.current / progress.max); updateProgress.folderProgress(progress.current / progress.max);
} }
else if (state === "compressing") {
updateProgress.folderProgress(2);
}
else if (state === "done") { else if (state === "done") {
updateProgress.folderProgress(1); updateProgress.folderProgress(3);
} }
}); }, ctx.cache);
}; };
var createExportUI = function (origin) { var createExportUI = function (origin) {

@ -1,8 +1,6 @@
(function(name, definition) { (function (window) {
if (typeof module !== 'undefined') { module.exports = definition(); } var factory = function () {
else if (typeof define === 'function' && typeof define.amd === 'object') { define(definition); } var Promise = window.Promise;
else { this[name] = definition(); }
}('MediaTag', function() {
var cache; var cache;
var cypherChunkLength = 131088; var cypherChunkLength = 131088;
@ -50,6 +48,7 @@
'image/jpeg', 'image/jpeg',
'image/jpg', 'image/jpg',
'image/gif', 'image/gif',
'audio/mpeg',
'audio/mp3', 'audio/mp3',
'audio/ogg', 'audio/ogg',
'audio/wav', 'audio/wav',
@ -63,7 +62,8 @@
], ],
pdf: {}, pdf: {},
download: { download: {
text: "Download" text: "Save",
textDl: "Load attachment"
}, },
Plugins: { Plugins: {
/** /**
@ -114,8 +114,8 @@
}, },
download: function (metadata, url, content, cfg, cb) { download: function (metadata, url, content, cfg, cb) {
var btn = document.createElement('button'); var btn = document.createElement('button');
btn.setAttribute('class', 'btn btn-success'); btn.setAttribute('class', 'btn btn-default');
btn.innerHTML = cfg.download.text + '<br>' + btn.innerHTML = '<i class="fa fa-save"></i>' + cfg.download.text + '<br>' +
(metadata.name ? '<b>' + fixHTML(metadata.name) + '</b>' : ''); (metadata.name ? '<b>' + fixHTML(metadata.name) + '</b>' : '');
btn.addEventListener('click', function () { btn.addEventListener('click', function () {
saveFile(content, url, metadata.name); saveFile(content, url, metadata.name);
@ -125,30 +125,188 @@
} }
}; };
var makeProgressBar = function (cfg, mediaObject) {
if (mediaObject.bar) { return; }
mediaObject.bar = true;
var style = (function(){/*
.mediatag-progress-container {
position: relative;
border: 1px solid #0087FF;
background: white;
height: 25px;
display: inline-flex;
width: 200px;
align-items: center;
justify-content: center;
box-sizing: border-box;
vertical-align: top;
}
.mediatag-progress-bar {
position: absolute;
left: 0;
top: 0;
bottom: 0;
background: #0087FF;
width: 0%;
}
.mediatag-progress-text {
font-size: 14px;
height: 25px;
width: 50px;
margin-left: 5px;
line-height: 25px;
vertical-align: top;
display: inline-block;
color: #3F4141;
font-weight: bold;
}
*/}).toString().slice(14, -3);
var container = document.createElement('div');
container.classList.add('mediatag-progress-container');
var bar = document.createElement('div');
bar.classList.add('mediatag-progress-bar');
container.appendChild(bar);
var text = document.createElement('span');
text.classList.add('mediatag-progress-text');
text.innerText = '0%';
mediaObject.on('progress', function (obj) {
var percent = obj.progress;
text.innerText = (Math.round(percent*10))/10+'%';
bar.setAttribute('style', 'width:'+percent+'%;');
});
mediaObject.tag.innerHTML = '<style>'+style+'</style>';
mediaObject.tag.appendChild(container);
mediaObject.tag.appendChild(text);
};
var makeDownloadButton = function (cfg, mediaObject, size, cb) {
var metadata = cfg.metadata || {};
var i = '<i class="fa fa-paperclip"></i>';
var name = metadata.name ? '<span class="mediatag-download-name">'+ i +'<b>'+
fixHTML(metadata.name)+'</b></span>' : '';
var btn = document.createElement('button');
btn.setAttribute('class', 'btn btn-default mediatag-download-btn');
btn.innerHTML = name + '<span>' + (name ? '' : i) +
cfg.download.textDl + ' <b>(' + size + 'MB)</b></span>';
btn.addEventListener('click', function () {
makeProgressBar(cfg, mediaObject);
var a = (cfg.body || document).querySelectorAll('media-tag[src="'+mediaObject.tag.getAttribute('src')+'"] button.mediatag-download-btn');
for(var i = 0; i < a.length; i++) {
if (a[i] !== btn) { a[i].click(); }
}
cb();
});
mediaObject.tag.innerHTML = '';
mediaObject.tag.appendChild(btn);
};
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;
};
var getBlobCache = function (id, cb) {
if (!config.Cache || typeof(config.Cache.getBlobCache) !== "function") {
return void cb('EINVAL');
}
config.Cache.getBlobCache(id, cb);
};
var setBlobCache = function (id, u8, cb) {
if (!config.Cache || typeof(config.Cache.setBlobCache) !== "function") {
return void cb('EINVAL');
}
config.Cache.setBlobCache(id, u8, cb);
};
var getFileSize = function (src, _cb) {
var cb = function (e, res) {
_cb(e, res);
cb = function () {};
};
var cacheKey = getCacheKey(src);
var check = function () {
var xhr = new XMLHttpRequest();
xhr.open("HEAD", src);
xhr.onerror = function () { return void cb("XHR_ERROR"); };
xhr.onreadystatechange = function() {
if (this.readyState === this.DONE) {
cb(null, Number(xhr.getResponseHeader("Content-Length")));
}
};
xhr.onload = function () {
if (/^4/.test('' + this.status)) { return void cb("XHR_ERROR " + this.status); }
};
xhr.send();
};
if (!cacheKey) { return void check(); }
getBlobCache(cacheKey, function (err, u8) {
if (err || !u8) { return void check(); }
cb(null, 0);
});
};
// Download a blob from href // Download a blob from href
var download = function (src, _cb) { var download = function (src, _cb, progressCb) {
var cb = function (e, res) { var cb = function (e, res) {
_cb(e, res); _cb(e, res);
cb = function () {}; cb = function () {};
}; };
var cacheKey = getCacheKey(src);
var fetch = function () {
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open('GET', src, true); xhr.open('GET', src, true);
xhr.responseType = 'arraybuffer'; xhr.responseType = 'arraybuffer';
var progress = function (offset) {
progressCb(offset * 100);
};
xhr.addEventListener("progress", function (evt) {
if (evt.lengthComputable) {
var percentComplete = evt.loaded / evt.total;
progress(percentComplete);
}
}, false);
xhr.onerror = function () { return void cb("XHR_ERROR"); }; xhr.onerror = function () { return void cb("XHR_ERROR"); };
xhr.onload = function () { xhr.onload = function () {
// Error? // Error?
if (/^4/.test('' + this.status)) { return void cb("XHR_ERROR " + this.status); } if (/^4/.test('' + this.status)) { return void cb("XHR_ERROR " + this.status); }
var arrayBuffer = xhr.response; var arrayBuffer = xhr.response;
if (arrayBuffer) { cb(null, new Uint8Array(arrayBuffer)); } if (arrayBuffer) {
var u8 = new Uint8Array(arrayBuffer);
if (cacheKey) {
return void setBlobCache(cacheKey, u8, function () {
cb(null, u8);
});
}
cb(null, u8);
}
}; };
xhr.send(null); xhr.send(null);
}; };
if (!cacheKey) { return void fetch(); }
getBlobCache(cacheKey, function (err, u8) {
if (err || !u8) { return void fetch(); }
cb(null, u8);
});
};
// Decryption tools // Decryption tools
var Decrypt = { var Decrypt = {
// Create a nonce // Create a nonce
@ -192,6 +350,92 @@
} }
}; };
// The metadata size can go up to 65535 (16 bits - 2 bytes)
// The first 8 bits are stored in A[0]
// The last 8 bits are stored in A[0]
var uint8ArrayJoin = function (AA) {
var l = 0;
var i = 0;
for (; i < AA.length; i++) { l += AA[i].length; }
var C = new Uint8Array(l);
i = 0;
for (var offset = 0; i < AA.length; i++) {
C.set(AA[i], offset);
offset += AA[i].length;
}
return C;
};
var fetchMetadata = function (src, _cb) {
var cb = function (e, res) {
_cb(e, res);
cb = function () {};
};
var cacheKey = getCacheKey(src);
var fetch = function () {
var xhr = new XMLHttpRequest();
xhr.open('GET', src, true);
xhr.setRequestHeader('Range', 'bytes=0-1');
xhr.responseType = 'arraybuffer';
xhr.onerror = function () { return void cb("XHR_ERROR"); };
xhr.onload = function () {
// Error?
if (/^4/.test('' + this.status)) { return void cb("XHR_ERROR " + this.status); }
var res = new Uint8Array(xhr.response);
var size = Decrypt.decodePrefix(res);
var xhr2 = new XMLHttpRequest();
xhr2.open("GET", src, true);
xhr2.setRequestHeader('Range', 'bytes=2-' + (size + 2));
xhr2.responseType = 'arraybuffer';
xhr2.onload = function () {
if (/^4/.test('' + this.status)) { return void cb("XHR_ERROR " + this.status); }
var res2 = new Uint8Array(xhr2.response);
var all = uint8ArrayJoin([res, res2]);
cb(void 0, all);
};
xhr2.send(null);
};
xhr.send(null);
};
if (!cacheKey) { return void fetch(); }
getBlobCache(cacheKey, function (err, u8) {
if (err || !u8) { return void fetch(); }
var size = Decrypt.decodePrefix(u8.subarray(0,2));
cb(null, u8.subarray(0, size+2));
});
};
var decryptMetadata = function (u8, key) {
var prefix = u8.subarray(0, 2);
var metadataLength = Decrypt.decodePrefix(prefix);
var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength));
var metaChunk = window.nacl.secretbox.open(metaBox, Decrypt.createNonce(), key);
try {
return JSON.parse(window.nacl.util.encodeUTF8(metaChunk));
}
catch (e) { return null; }
};
var fetchDecryptedMetadata = function (src, strKey, cb) {
if (typeof(src) !== 'string') {
return window.setTimeout(function () {
cb('NO_SOURCE');
});
}
fetchMetadata(src, function (e, buffer) {
if (e) { return cb(e); }
var key = Decrypt.getKeyFromStr(strKey);
cb(void 0, decryptMetadata(buffer, key));
});
};
// Decrypts a Uint8Array with the given key. // Decrypts a Uint8Array with the given key.
var decrypt = function (u8, strKey, done, progressCb) { var decrypt = function (u8, strKey, done, progressCb) {
var Nacl = window.nacl; var Nacl = window.nacl;
@ -372,6 +616,7 @@
var handlers = cfg.handlers || { var handlers = cfg.handlers || {
'progress': [], 'progress': [],
'complete': [], 'complete': [],
'metadata': [],
'error': [] 'error': []
}; };
@ -422,6 +667,7 @@
// End media-tag rendering: display the tag and emit the event // End media-tag rendering: display the tag and emit the event
var end = function (decrypted) { var end = function (decrypted) {
mediaObject.complete = true;
process(mediaObject, decrypted, cfg, function (err) { process(mediaObject, decrypted, cfg, function (err) {
if (err) { return void emit('error', err); } if (err) { return void emit('error', err); }
mediaObject._blob = decrypted; mediaObject._blob = decrypted;
@ -429,33 +675,79 @@
}); });
}; };
// If we have the blob in our cache, don't download & decrypt it again, just display var error = function (err) {
if (cache[uid]) { mediaObject.tag.innerHTML = '<img style="width: 100px; height: 100px;" src="/images/broken.png">';
end(cache[uid]); emit('error', err);
return mediaObject; };
}
var getCache = function () {
var c = cache[uid];
if (!c || !c.promise || !c.mt) { return; }
return c;
};
var dl = function () {
// Download the encrypted blob // Download the encrypted blob
cache[uid] = getCache() || {
promise: new Promise(function (resolve, reject) {
download(src, function (err, u8Encrypted) { download(src, function (err, u8Encrypted) {
if (err) { if (err) {
if (err === "XHR_ERROR 404") { return void reject(err);
mediaObject.tag.innerHTML = '<img style="width: 100px; height: 100px;" src="/images/broken.png">';
}
return void emit('error', err);
} }
// Decrypt the blob // Decrypt the blob
decrypt(u8Encrypted, strKey, function (errDecryption, u8Decrypted) { decrypt(u8Encrypted, strKey, function (errDecryption, u8Decrypted) {
if (errDecryption) { if (errDecryption) {
return void emit('error', errDecryption); return void reject(errDecryption);
} }
// Cache and display the decrypted blob emit('metadata', u8Decrypted.metadata);
cache[uid] = u8Decrypted; resolve(u8Decrypted);
end(u8Decrypted);
}, function (progress) { }, function (progress) {
emit('progress', { emit('progress', {
progress: progress progress: 50+0.5*progress
}); });
}); });
}, function (progress) {
emit('progress', {
progress: 0.5*progress
});
});
}),
mt: mediaObject
};
if (cache[uid].mt !== mediaObject) {
// Add progress for other instances of this tag
cache[uid].mt.on('progress', function (obj) {
if (!mediaObject.bar && !cfg.force) { makeProgressBar(cfg, mediaObject); }
emit('progress', {
progress: obj.progress
});
});
}
cache[uid].promise.then(function (u8) {
end(u8);
}, function (err) {
error(err);
});
};
if (cfg.force) { dl(); return mediaObject; }
var maxSize = typeof(config.maxDownloadSize) === "number" ? config.maxDownloadSize
: (5 * 1024 * 1024);
fetchDecryptedMetadata(src, strKey, function (err, md) {
if (err) { return void error(err); }
cfg.metadata = md;
emit('metadata', md);
getFileSize(src, function (err, size) {
// If the size is smaller than the autodownload limit, load the blob.
// If the blob is already loaded or being loaded, don't show the button.
if (!size || size < maxSize || getCache()) {
makeProgressBar(cfg, mediaObject);
return void dl();
}
var sizeMb = Math.round(10 * size / 1024 / 1024) / 10;
makeDownloadButton(cfg, mediaObject, sizeMb, dl);
});
}); });
return mediaObject; return mediaObject;
@ -468,5 +760,20 @@
config[key] = value; config[key] = value;
}; };
init.fetchDecryptedMetadata = fetchDecryptedMetadata;
return init; return init;
})); };
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory();
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/bower_components/es6-promise/es6-promise.min.js'
], function () {
return factory();
});
} else {
// unsupported initialization
}
}(typeof(window) !== 'undefined'? window : {}));

@ -20,6 +20,8 @@ define(['/api/config'], function (ApiConfig) {
var getPermission = Module.getPermission = function (f) { var getPermission = Module.getPermission = function (f) {
f = f || function () {}; f = f || function () {};
// "Notification.requestPermission is not a function" on Firefox 68.11.0esr
if (!Notification || typeof(Notification.requestPermission) !== 'function') { return void f(false); }
Notification.requestPermission(function (permission) { Notification.requestPermission(function (permission) {
if (permission === "granted") { f(true); } if (permission === "granted") { f(true); }
else { f(false); } else { f(false); }

@ -99,6 +99,9 @@ define([
var sessionId = Hash.createChannelId(); var sessionId = Hash.createChannelId();
var cpNfInner; var cpNfInner;
var evOnPatch = Util.mkEvent();
var evOnSync = Util.mkEvent();
// This structure is used for caching media data and blob urls for each media cryptpad url // This structure is used for caching media data and blob urls for each media cryptpad url
var mediasData = {}; var mediasData = {};
@ -261,13 +264,17 @@ define([
}); });
}, },
sendMsg: function (msg, cp, cb) { sendMsg: function (msg, cp, cb) {
evOnPatch.fire();
rtChannel.sendCmd({ rtChannel.sendCmd({
cmd: 'SEND_MESSAGE', cmd: 'SEND_MESSAGE',
data: { data: {
msg: msg, msg: msg,
isCp: cp isCp: cp
} }
}, cb); }, function (err, h) {
if (!err) { evOnSync.fire(); }
cb(err, h);
});
}, },
}; };
@ -775,6 +782,18 @@ define([
view: false view: false
}; };
}); });
// Add an history keeper user to show that we're never alone
var hkId = Util.createRandomInteger();
p.push({
id: hkId,
idOriginal: String(hkId),
username: "History",
indexUser: i,
connectionId: Hash.createChannelId(),
isCloseCoAuthoring:false,
view: false
});
i++;
if (!myUniqueOOId) { myUniqueOOId = String(myOOId) + i; } if (!myUniqueOOId) { myUniqueOOId = String(myOOId) + i; }
p.push({ p.push({
id: myUniqueOOId, id: myUniqueOOId,
@ -1221,6 +1240,7 @@ define([
} }
}, },
"onDocumentReady": function () { "onDocumentReady": function () {
evOnSync.fire();
var onMigrateRdy = Util.mkEvent(); var onMigrateRdy = Util.mkEvent();
onMigrateRdy.reg(function () { onMigrateRdy.reg(function () {
var div = h('div.cp-oo-x2tXls', [ var div = h('div.cp-oo-x2tXls', [
@ -1301,6 +1321,8 @@ define([
return; return;
} }
APP.onLocal(); // Add our data to the userlist
if (APP.history) { if (APP.history) {
try { try {
getEditor().asc_setRestriction(true); getEditor().asc_setRestriction(true);
@ -1310,6 +1332,16 @@ define([
if (APP.migrate && !readOnly) { if (APP.migrate && !readOnly) {
onMigrateRdy.fire(); onMigrateRdy.fire();
} }
// Check if history can/should be trimmed
var cp = getLastCp();
if (cp && cp.file && cp.hash) {
var channels = [{
channel: content.channel,
lastKnownHash: cp.hash
}];
common.checkTrimHistory(channels);
}
} }
} }
}; };
@ -1355,6 +1387,10 @@ define([
}); });
}; };
APP.openURL = function (url) {
common.openUnsafeURL(url);
};
APP.loadingImage = 0; APP.loadingImage = 0;
APP.getImageURL = function(name, callback) { APP.getImageURL = function(name, callback) {
var mediasSources = getMediasSources(); var mediasSources = getMediasSources();
@ -1409,7 +1445,7 @@ define([
console.error(e); console.error(e);
callback(""); callback("");
} }
}); }, void 0, common.getCache());
}; };
APP.docEditor = new window.DocsAPI.DocEditor("cp-app-oo-placeholder-a", APP.ooconfig); APP.docEditor = new window.DocsAPI.DocEditor("cp-app-oo-placeholder-a", APP.ooconfig);
@ -1949,6 +1985,10 @@ define([
metadataMgr: metadataMgr, metadataMgr: metadataMgr,
readOnly: readOnly, readOnly: readOnly,
realtime: info.realtime, realtime: info.realtime,
spinner: {
onPatch: evOnPatch,
onSync: evOnSync
},
sfCommon: common, sfCommon: common,
$container: $bar, $container: $bar,
$contentContainer: $('#cp-app-oo-container') $contentContainer: $('#cp-app-oo-container')
@ -2166,6 +2206,12 @@ define([
url: newLatest.file url: newLatest.file
}, function () { }); }, function () { });
newDoc = !content.hashes || Object.keys(content.hashes).length === 0; newDoc = !content.hashes || Object.keys(content.hashes).length === 0;
} else if (!privateData.isNewFile) {
// This is an empty doc but not a new file: error
UI.errorLoadingScreen(Messages.unableToDisplay, false, function () {
common.gotoURL('');
});
throw new Error("Empty chainpad for a non-empty doc");
} else { } else {
Title.updateTitle(Title.defaultTitle); Title.updateTitle(Title.defaultTitle);
} }

@ -12,7 +12,7 @@ define([
nThen(function (waitFor) { nThen(function (waitFor) {
DomReady.onReady(waitFor()); DomReady.onReady(waitFor());
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
var obj = SFCommonO.initIframe(waitFor, true, true); var obj = SFCommonO.initIframe(waitFor, true);
href = obj.href; href = obj.href;
hash = obj.hash; hash = obj.hash;
var parsed = Hash.parsePadUrl(href); var parsed = Hash.parsePadUrl(href);

File diff suppressed because one or more lines are too long

@ -10,6 +10,7 @@ define([
'/common/common-realtime.js', '/common/common-realtime.js',
'/common/common-messaging.js', '/common/common-messaging.js',
'/common/pinpad.js', '/common/pinpad.js',
'/common/outer/cache-store.js',
'/common/outer/sharedfolder.js', '/common/outer/sharedfolder.js',
'/common/outer/cursor.js', '/common/outer/cursor.js',
'/common/outer/onlyoffice.js', '/common/outer/onlyoffice.js',
@ -28,7 +29,7 @@ define([
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
'/bower_components/saferphore/index.js', '/bower_components/saferphore/index.js',
], function (Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback, ], function (Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback,
Realtime, Messaging, Pinpad, Realtime, Messaging, Pinpad, Cache,
SF, Cursor, OnlyOffice, Mailbox, Profile, Team, Messenger, History, SF, Cursor, OnlyOffice, Mailbox, Profile, Team, Messenger, History,
NetConfig, AppConfig, NetConfig, AppConfig,
Crypto, ChainPad, CpNetflux, Listmap, nThen, Saferphore) { Crypto, ChainPad, CpNetflux, Listmap, nThen, Saferphore) {
@ -120,10 +121,13 @@ define([
Store.getSharedFolder = function (clientId, data, cb) { Store.getSharedFolder = function (clientId, data, cb) {
var s = getStore(data.teamId); var s = getStore(data.teamId);
var id = data.id; var id = data.id;
var proxy;
if (!s || !s.manager) { return void cb({ error: 'ENOTFOUND' }); } if (!s || !s.manager) { return void cb({ error: 'ENOTFOUND' }); }
if (s.manager.folders[id]) { if (s.manager.folders[id]) {
proxy = Util.clone(s.manager.folders[id].proxy);
proxy.offline = Boolean(s.manager.folders[id].offline);
// If it is loaded, return the shared folder proxy // If it is loaded, return the shared folder proxy
return void cb(s.manager.folders[id].proxy); return void cb(proxy);
} else { } else {
// Otherwise, check if we know this shared folder // Otherwise, check if we know this shared folder
var shared = Util.find(s.proxy, ['drive', UserObject.SHARED_FOLDERS]) || {}; var shared = Util.find(s.proxy, ['drive', UserObject.SHARED_FOLDERS]) || {};
@ -327,6 +331,7 @@ define([
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); } if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
s.rpc.removeOwnedChannel(channel, function (err) { s.rpc.removeOwnedChannel(channel, function (err) {
if (!err) { Cache.clearChannel(channel); }
cb({error:err}); cb({error:err});
}); });
}; };
@ -459,6 +464,7 @@ define([
store.anon_rpc.send("GET_FILE_SIZE", channelId, function (e, response) { store.anon_rpc.send("GET_FILE_SIZE", channelId, function (e, response) {
if (e) { return void cb({error: e}); } if (e) { return void cb({error: e}); }
if (response && response.length && typeof(response[0]) === 'number') { if (response && response.length && typeof(response[0]) === 'number') {
if (response[0] === 0) { Cache.clearChannel(channelId); }
return void cb({size: response[0]}); return void cb({size: response[0]});
} else { } else {
cb({error: 'INVALID_RESPONSE'}); cb({error: 'INVALID_RESPONSE'});
@ -472,6 +478,7 @@ define([
store.anon_rpc.send("IS_NEW_CHANNEL", channelId, function (e, response) { store.anon_rpc.send("IS_NEW_CHANNEL", channelId, function (e, response) {
if (e) { return void cb({error: e}); } if (e) { return void cb({error: e}); }
if (response && response.length && typeof(response[0]) === 'boolean') { if (response && response.length && typeof(response[0]) === 'boolean') {
if (response[0]) { Cache.clearChannel(channelId); }
return void cb({ return void cb({
isNew: response[0] isNew: response[0]
}); });
@ -1133,7 +1140,7 @@ define([
var ownedByMe = Array.isArray(owners) && owners.indexOf(edPublic) !== -1; var ownedByMe = Array.isArray(owners) && owners.indexOf(edPublic) !== -1;
// Add the pad if it does not exist in our drive // Add the pad if it does not exist in our drive
if (!contains || (ownedByMe && !inMyDrive)) { if (!contains) { // || (ownedByMe && !inMyDrive)) {
var autoStore = Util.find(store.proxy, ['settings', 'general', 'autostore']); var autoStore = Util.find(store.proxy, ['settings', 'general', 'autostore']);
if (autoStore !== 1 && !data.forceSave && !data.path && !ownedByMe) { if (autoStore !== 1 && !data.forceSave && !data.path && !ownedByMe) {
// send event to inner to display the corner popup // send event to inner to display the corner popup
@ -1326,13 +1333,16 @@ define([
store.proxy.friends_pending = store.proxy.friends_pending || {}; store.proxy.friends_pending = store.proxy.friends_pending || {};
var twoDaysAgo = +new Date() - (2 * 24 * 3600 * 1000); var p = store.proxy.friends_pending[data.curvePublic];
if (store.proxy.friends_pending[data.curvePublic] && if (p) {
store.proxy.friends_pending[data.curvePublic] > twoDaysAgo) { return void cb({error: 'ALREADY_SENT'});
return void cb({error: 'TIMEOUT'});
} }
store.proxy.friends_pending[data.curvePublic] = +new Date(); store.proxy.friends_pending[data.curvePublic] = {
time: +new Date(),
channel: data.notifications,
curvePublic: data.curvePublic
};
broadcast([], "UPDATE_METADATA"); broadcast([], "UPDATE_METADATA");
store.mailbox.sendTo('FRIEND_REQUEST', { store.mailbox.sendTo('FRIEND_REQUEST', {
@ -1344,6 +1354,37 @@ define([
cb(obj); cb(obj);
}); });
}; };
Store.cancelFriendRequest = function (data, cb) {
if (!data.curvePublic || !data.notifications) {
return void cb({error: 'EINVAL'});
}
var proxy = store.proxy;
var f = Messaging.getFriend(proxy, data.curvePublic);
if (f) {
// Already friend
console.error("You can't cancel an accepted friend request");
return void cb({error: 'ALREADY_FRIEND'});
}
var pending = Util.find(store, ['proxy', 'friends_pending']) || {};
if (!pending) { return void cb(); }
store.mailbox.sendTo('CANCEL_FRIEND_REQUEST', {
user: Messaging.createData(store.proxy)
}, {
channel: data.notifications,
curvePublic: data.curvePublic
}, function (obj) {
if (obj && obj.error) { return void cb(obj); }
delete store.proxy.friends_pending[data.curvePublic];
broadcast([], "UPDATE_METADATA");
onSync(null, function () {
cb(obj);
});
});
};
Store.anonGetPreviewContent = function (clientId, data, cb) { Store.anonGetPreviewContent = function (clientId, data, cb) {
Team.anonGetPreviewContent({ Team.anonGetPreviewContent({
@ -1590,13 +1631,20 @@ define([
Store.leavePad(null, data, function () {}); Store.leavePad(null, data, function () {});
}; };
var conf = { var conf = {
Cache: Cache, // XXX re-enable cache usage
onCacheStart: function () {
postMessage(clientId, "PAD_CACHE");
},
onCacheReady: function () {
postMessage(clientId, "PAD_CACHE_READY");
},
onReady: function (pad) { onReady: function (pad) {
var padData = pad.metadata || {}; var padData = pad.metadata || {};
channel.data = padData; channel.data = padData;
if (padData && padData.validateKey && store.messenger) { if (padData && padData.validateKey && store.messenger) {
store.messenger.storeValidateKey(data.channel, padData.validateKey); store.messenger.storeValidateKey(data.channel, padData.validateKey);
} }
postMessage(clientId, "PAD_READY"); postMessage(clientId, "PAD_READY", pad.noCache);
}, },
onMessage: function (m, user, validateKey, isCp, hash) { onMessage: function (m, user, validateKey, isCp, hash) {
channel.lastHash = hash; channel.lastHash = hash;
@ -1727,6 +1775,14 @@ define([
channel.sendMessage(msg, clientId, cb); channel.sendMessage(msg, clientId, cb);
}; };
Store.corruptedCache = function (clientId, channel) {
var chan = channels[channel];
if (!chan || !chan.cpNf) { return; }
Cache.clearChannel(channel);
if (!chan.cpNf.resetCache) { return; }
chan.cpNf.resetCache();
};
// Unpin and pin the new channel in all team when changing a pad password // Unpin and pin the new channel in all team when changing a pad password
Store.changePadPasswordPin = function (clientId, data, cb) { Store.changePadPasswordPin = function (clientId, data, cb) {
var oldChannel = data.oldChannel; var oldChannel = data.oldChannel;
@ -2224,6 +2280,9 @@ define([
try { try {
store.onlyoffice.leavePad(chanId); store.onlyoffice.leavePad(chanId);
} catch (e) { console.error(e); } } catch (e) { console.error(e); }
try {
Cache.leaveChannel(chanId);
} catch (e) { console.error(e); }
if (!Store.channels[chanId]) { return; } if (!Store.channels[chanId]) { return; }
@ -2429,18 +2488,6 @@ define([
}); });
}; };
var cleanFriendRequests = function () {
try {
if (!store.proxy.friends_pending) { return; }
var twoDaysAgo = +new Date() - (2 * 24 * 3600 * 1000);
Object.keys(store.proxy.friends_pending).forEach(function (curve) {
if (store.proxy.friends_pending[curve] < twoDaysAgo) {
delete store.proxy.friends_pending[curve];
}
});
} catch (e) {}
};
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
/////////////////////// Init ///////////////////////////////////// /////////////////////// Init /////////////////////////////////////
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
@ -2524,7 +2571,6 @@ define([
loadUniversal(Profile, 'profile', waitFor); loadUniversal(Profile, 'profile', waitFor);
loadUniversal(Team, 'team', waitFor, clientId); loadUniversal(Team, 'team', waitFor, clientId);
loadUniversal(History, 'history', waitFor); loadUniversal(History, 'history', waitFor);
cleanFriendRequests();
}).nThen(function () { }).nThen(function () {
var requestLogin = function () { var requestLogin = function () {
broadcast([], "REQUEST_LOGIN"); broadcast([], "REQUEST_LOGIN");
@ -2640,6 +2686,7 @@ define([
readOnly: false, readOnly: false,
validateKey: secret.keys.validateKey || undefined, validateKey: secret.keys.validateKey || undefined,
crypto: Crypto.createEncryptor(secret.keys), crypto: Crypto.createEncryptor(secret.keys),
Cache: Cache, // XXX re-enable cache usage
userName: 'fs', userName: 'fs',
logLevel: 1, logLevel: 1,
ChainPad: ChainPad, ChainPad: ChainPad,

@ -0,0 +1,147 @@
define([
'/common/common-util.js',
'/bower_components/localforage/dist/localforage.min.js',
], function (Util, localForage) {
var S = {};
var onReady = Util.mkEvent(true);
// Check if indexedDB is allowed
var allowed = false;
try {
var request = window.indexedDB.open('test_db', 1);
request.onsuccess = function () {
allowed = true;
onReady.fire();
};
request.onerror = function () {
onReady.fire();
};
} catch (e) {
onReady.fire();
}
var cache = localForage.createInstance({
driver: localForage.INDEXEDDB,
name: "cp_cache"
});
S.getBlobCache = function (id, cb) {
cb = Util.once(Util.mkAsync(cb || function () {}));
onReady.reg(function () {
if (!allowed) { return void cb('NOCACHE'); }
cache.getItem(id, function (err, obj) {
if (err || !obj || !obj.c) {
return void cb(err || 'EINVAL');
}
cb(null, obj.c);
obj.t = +new Date();
cache.setItem(id, obj);
});
});
};
S.setBlobCache = function (id, u8, cb) {
cb = Util.once(Util.mkAsync(cb || function () {}));
onReady.reg(function () {
if (!allowed) { return void cb('NOCACHE'); }
if (!u8) { return void cb('EINVAL'); }
cache.setItem(id, {
c: u8,
t: (+new Date()) // 't' represent the "lastAccess" of this cache (get or set)
}, function (err) {
cb(err);
});
});
};
// id: channel ID or blob ID
// returns array of messages
S.getChannelCache = function (id, cb) {
cb = Util.once(Util.mkAsync(cb || function () {}));
onReady.reg(function () {
if (!allowed) { return void cb('NOCACHE'); }
cache.getItem(id, function (err, obj) {
if (err || !obj || !Array.isArray(obj.c)) {
return void cb(err || 'EINVAL');
}
cb(null, obj);
obj.t = +new Date();
cache.setItem(id, obj);
});
});
};
// Keep the last two checkpoint + any checkpoint that may exist in the last 100 messages
// FIXME: duplicate system with sliceCpIndex from lib/hk-util.js
var checkCheckpoints = function (array) {
if (!Array.isArray(array)) { return; }
// Keep the last 100 messages
if (array.length > 100) {
array.splice(0, array.length - 100);
}
// Remove every message before the first checkpoint
var firstCpIdx;
array.some(function (el, i) {
if (!el.isCheckpoint) { return; }
firstCpIdx = i;
return true;
});
array.splice(0, firstCpIdx);
};
var t = {};
S.storeCache = function (id, validateKey, val, onError) {
onError = Util.once(Util.mkAsync(onError || function () {}));
onReady.reg(function () {
// Make a throttle or use the existing one to avoid calling
// storeCache with the same array multiple times
t[id] = t[id] || Util.throttle(function (validateKey, val, onError) {
if (!allowed) { return void onError('NOCACHE'); }
if (!Array.isArray(val) || !validateKey) { return void onError('EINVAL'); }
checkCheckpoints(val);
cache.setItem(id, {
k: validateKey,
c: val,
t: (+new Date()) // 't' represent the "lastAccess" of this cache (get or set)
}, function (err) {
if (err) { onError(err); }
});
}, 50);
t[id](validateKey, val, onError);
});
};
S.leaveChannel = function (id) {
delete t[id];
};
S.clearChannel = function (id, cb) {
cb = Util.once(Util.mkAsync(cb || function () {}));
onReady.reg(function () {
if (!allowed) { return void cb('NOCACHE'); }
cache.removeItem(id, function () {
cb();
});
});
};
S.clear = function (cb) {
cb = Util.once(Util.mkAsync(cb || function () {}));
onReady.reg(function () {
if (!allowed) { return void cb('NOCACHE'); }
cache.clear(cb);
});
};
self.CryptPad_clearIndexedDB = S.clear;
return S;
});

@ -1,9 +1,11 @@
define([ define([
'/common/common-constants.js', '/common/common-constants.js',
'/common/common-hash.js', '/common/common-hash.js',
'/common/outer/cache-store.js',
'/bower_components/localforage/dist/localforage.min.js', '/bower_components/localforage/dist/localforage.min.js',
'/customize/application_config.js', '/customize/application_config.js',
], function (Constants, Hash, localForage, AppConfig) { '/common/common-util.js',
], function (Constants, Hash, Cache, localForage, AppConfig, Util) {
var LocalStore = {}; var LocalStore = {};
LocalStore.setThumbnail = function (key, value, cb) { LocalStore.setThumbnail = function (key, value, cb) {
@ -119,7 +121,14 @@ define([
return void AppConfig.customizeLogout(cb); return void AppConfig.customizeLogout(cb);
} }
if (cb) { cb(); } cb = Util.once(cb || function () {});
try {
Cache.clear(cb);
} catch (e) {
console.error(e);
cb();
}
}; };
var loginHandlers = []; var loginHandlers = [];
LocalStore.loginReload = function () { LocalStore.loginReload = function () {

@ -33,11 +33,15 @@ define([
// in memory from the same user, dismiss the new one // in memory from the same user, dismiss the new one
if (friendRequest[data.msg.author]) { return void cb(true); } if (friendRequest[data.msg.author]) { return void cb(true); }
friendRequest[data.msg.author] = true; friendRequest[data.msg.author] = {
type: box.type,
hash: data.hash
};
// If the user is already in our friend list, automatically accept the request // If the user is already in our friend list, automatically accept the request
if (Messaging.getFriend(ctx.store.proxy, data.msg.author) || if (Messaging.getFriend(ctx.store.proxy, data.msg.author) ||
ctx.store.proxy.friends_pending[data.msg.author]) { ctx.store.proxy.friends_pending[data.msg.author]) {
delete ctx.store.proxy.friends_pending[data.msg.author]; delete ctx.store.proxy.friends_pending[data.msg.author];
Messaging.acceptFriendRequest(ctx.store, userData, function (obj) { Messaging.acceptFriendRequest(ctx.store, userData, function (obj) {
@ -49,14 +53,17 @@ define([
realtime: ctx.store.realtime, realtime: ctx.store.realtime,
pinPads: ctx.pinPads pinPads: ctx.pinPads
}, userData, function (err) { }, userData, function (err) {
if (err) { return void console.error(err); } if (err) {
console.error(err);
return void cb(true);
}
if (ctx.store.messenger) { if (ctx.store.messenger) {
ctx.store.messenger.onFriendAdded(userData); ctx.store.messenger.onFriendAdded(userData);
} }
});
ctx.updateMetadata(); ctx.updateMetadata();
cb(true); cb(true);
}); });
});
return; return;
} }
@ -79,6 +86,7 @@ define([
// until the message is manually dismissed. // until the message is manually dismissed.
var friendRequestDeclined = {}; var friendRequestDeclined = {};
var friendRequestAccepted = {};
handlers['DECLINE_FRIEND_REQUEST'] = function (ctx, box, data, cb) { handlers['DECLINE_FRIEND_REQUEST'] = function (ctx, box, data, cb) {
// Old format: data was stored directly in "content" // Old format: data was stored directly in "content"
var userData = data.msg.content.user || data.msg.content; var userData = data.msg.content.user || data.msg.content;
@ -97,27 +105,35 @@ define([
ctx.updateMetadata(); ctx.updateMetadata();
if (friendRequestDeclined[data.msg.author]) { return; } if (friendRequestDeclined[data.msg.author]) { return; }
friendRequestDeclined[data.msg.author] = true;
box.sendMessage({ box.sendMessage({
type: 'FRIEND_REQUEST_DECLINED', type: 'FRIEND_REQUEST_DECLINED',
content: { user: userData } content: { user: userData }
}, function () {}); }, function (hash) {
friendRequestDeclined[data.msg.author] = {
type: box.type,
hash: hash
};
});
}, getRandomTimeout(ctx)); }, getRandomTimeout(ctx));
}; };
// UI for declined friend request // UI for declined friend request
handlers['FRIEND_REQUEST_DECLINED'] = function (ctx, box, data, cb) { handlers['FRIEND_REQUEST_DECLINED'] = function (ctx, box, data, cb) {
ctx.updateMetadata(); ctx.updateMetadata();
var curve = data.msg.content.user.curvePublic || data.msg.content.user; var curve = data.msg.content.user.curvePublic || data.msg.content.user;
if (friendRequestDeclined[curve]) { return void cb(true); } var toRemove = friendRequestAccepted[curve];
friendRequestDeclined[curve] = true; delete friendRequestAccepted[curve];
cb(); if (friendRequestDeclined[curve]) { return void cb(true, toRemove); }
friendRequestDeclined[curve] = {
type: box.type,
hash: data.hash
};
cb(false, toRemove);
}; };
removeHandlers['FRIEND_REQUEST_DECLINED'] = function (ctx, box, data) { removeHandlers['FRIEND_REQUEST_DECLINED'] = function (ctx, box, data) {
var curve = data.content.user.curvePublic || data.content.user; var curve = data.content.user.curvePublic || data.content.user;
if (friendRequestDeclined[curve]) { delete friendRequestDeclined[curve]; } if (friendRequestDeclined[curve]) { delete friendRequestDeclined[curve]; }
}; };
var friendRequestAccepted = {};
handlers['ACCEPT_FRIEND_REQUEST'] = function (ctx, box, data, cb) { handlers['ACCEPT_FRIEND_REQUEST'] = function (ctx, box, data, cb) {
// Old format: data was stored directly in "content" // Old format: data was stored directly in "content"
var userData = data.msg.content.user || data.msg.content; var userData = data.msg.content.user || data.msg.content;
@ -148,11 +164,15 @@ define([
if (ctx.store.modules['profile']) { ctx.store.modules['profile'].update(); } if (ctx.store.modules['profile']) { ctx.store.modules['profile'].update(); }
// Display the "accepted" state in the UI // Display the "accepted" state in the UI
if (friendRequestAccepted[data.msg.author]) { return; } if (friendRequestAccepted[data.msg.author]) { return; }
friendRequestAccepted[data.msg.author] = true;
box.sendMessage({ box.sendMessage({
type: 'FRIEND_REQUEST_ACCEPTED', type: 'FRIEND_REQUEST_ACCEPTED',
content: { user: userData } content: { user: userData }
}, function () {}); }, function (hash) {
friendRequestAccepted[data.msg.author] = {
type: box.type,
hash: hash
};
});
}); });
}, getRandomTimeout(ctx)); }, getRandomTimeout(ctx));
}; };
@ -160,20 +180,32 @@ define([
handlers['FRIEND_REQUEST_ACCEPTED'] = function (ctx, box, data, cb) { handlers['FRIEND_REQUEST_ACCEPTED'] = function (ctx, box, data, cb) {
ctx.updateMetadata(); ctx.updateMetadata();
var curve = data.msg.content.user.curvePublic || data.msg.content.user; var curve = data.msg.content.user.curvePublic || data.msg.content.user;
if (friendRequestAccepted[curve]) { return void cb(true); } var toRemove = friendRequestDeclined[curve];
friendRequestAccepted[curve] = true; delete friendRequestDeclined[curve];
cb(); if (friendRequestAccepted[curve]) { return void cb(true, toRemove); }
friendRequestAccepted[curve] = {
type: box.type,
hash: data.hash
};
cb(false, toRemove);
}; };
removeHandlers['FRIEND_REQUEST_ACCEPTED'] = function (ctx, box, data) { removeHandlers['FRIEND_REQUEST_ACCEPTED'] = function (ctx, box, data) {
var curve = data.content.user.curvePublic || data.content.user; var curve = data.content.user.curvePublic || data.content.user;
if (friendRequestAccepted[curve]) { delete friendRequestAccepted[curve]; } if (friendRequestAccepted[curve]) { delete friendRequestAccepted[curve]; }
}; };
handlers['CANCEL_FRIEND_REQUEST'] = function (ctx, box, data, cb) {
var f = friendRequest[data.msg.author];
if (!f) { return void cb(true); }
cb(true, f);
};
handlers['UNFRIEND'] = function (ctx, box, data, cb) { handlers['UNFRIEND'] = function (ctx, box, data, cb) {
var curve = data.msg.author; var curve = data.msg.author;
var friend = Messaging.getFriend(ctx.store.proxy, curve); var friend = Messaging.getFriend(ctx.store.proxy, curve);
if (!friend) { return void cb(true); } if (!friend) { return void cb(true); }
delete ctx.store.proxy.friends[curve]; delete ctx.store.proxy.friends[curve];
delete ctx.store.proxy.friends_pending[curve];
if (ctx.store.messenger) { if (ctx.store.messenger) {
ctx.store.messenger.onFriendRemoved(curve, friend.channel); ctx.store.messenger.onFriendRemoved(curve, friend.channel);
} }

@ -271,7 +271,7 @@ proxy.mailboxes = {
hash: hash hash: hash
}; };
showMessage(ctx, type, message); showMessage(ctx, type, message);
cb(); cb(hash);
}, keys.curvePublic); }, keys.curvePublic);
}; };
box.queue.forEach(function (msg) { box.queue.forEach(function (msg) {
@ -297,6 +297,7 @@ proxy.mailboxes = {
msg: msg, msg: msg,
hash: hash hash: hash
}; };
var notify = box.ready;
Handlers.add(ctx, box, message, function (dismissed, toDismiss) { Handlers.add(ctx, box, message, function (dismissed, toDismiss) {
if (toDismiss) { // List of other messages to remove if (toDismiss) { // List of other messages to remove
dismiss(ctx, toDismiss, '', function () { dismiss(ctx, toDismiss, '', function () {
@ -314,8 +315,7 @@ proxy.mailboxes = {
} }
box.content[hash] = msg; box.content[hash] = msg;
showMessage(ctx, type, message, null, function (obj) { showMessage(ctx, type, message, null, function (obj) {
if (!box.ready) { return; } if (!obj || !obj.msg || !notify) { return; }
if (!obj || !obj.msg) { return; }
Notify.system(undefined, obj.msg); Notify.system(undefined, obj.msg);
}); });
}); });

@ -459,6 +459,13 @@ define([
} }
}; };
// Cancel pending friend requests
var cancelFriend = function (ctx, data, _cb) {
var cb = Util.once(_cb);
if (typeof(cb) !== 'function') { return void console.error('NO_CALLBACK'); }
ctx.Store.cancelFriendRequest(data, cb);
};
var getAllClients = function (ctx) { var getAllClients = function (ctx) {
var all = []; var all = [];
Array.prototype.push.apply(all, ctx.friendsClients); Array.prototype.push.apply(all, ctx.friendsClients);
@ -935,6 +942,7 @@ define([
if (AppConfig.availablePadTypes.indexOf('contacts') === -1) { return; } if (AppConfig.availablePadTypes.indexOf('contacts') === -1) { return; }
var ctx = { var ctx = {
store: store, store: store,
Store: cfg.Store,
updateMetadata: cfg.updateMetadata, updateMetadata: cfg.updateMetadata,
pinPads: cfg.pinPads, pinPads: cfg.pinPads,
emit: emit, emit: emit,
@ -1047,6 +1055,9 @@ define([
if (cmd === 'REMOVE_FRIEND') { if (cmd === 'REMOVE_FRIEND') {
return void removeFriend(ctx, data, cb); return void removeFriend(ctx, data, cb);
} }
if (cmd === 'CANCEL_FRIEND') {
return void cancelFriend(ctx, data, cb);
}
if (cmd === 'MUTE_USER') { if (cmd === 'MUTE_USER') {
return void muteUser(ctx, data, cb); return void muteUser(ctx, data, cb);
} }

@ -1,5 +1,5 @@
(function () { (function () {
var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto, Feedback) {
var Roster = {}; var Roster = {};
// this constant is somewhat arbitrary. // this constant is somewhat arbitrary.
@ -587,6 +587,11 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
// deleted while you are open // deleted while you are open
// emit an event // emit an event
var onChannelError = function (info) { var onChannelError = function (info) {
if (Feedback) { Feedback.send('ROSTER_CHANNEL_ERROR='+(info && info.type)); }
if (info && info.type === "EUNKNOWN") {
// chainpad-netflux should recover by itself
return;
}
if (!ready) { return void cb(info); } if (!ready) { return void cb(info); }
console.error("CHANNEL_ERROR", info); console.error("CHANNEL_ERROR", info);
}; };
@ -870,7 +875,8 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
require("../../bower_components/chainpad-netflux/chainpad-netflux.js"), require("../../bower_components/chainpad-netflux/chainpad-netflux.js"),
require("../../bower_components/json.sortify"), require("../../bower_components/json.sortify"),
require("nthen"), require("nthen"),
require("../../bower_components/chainpad-crypto/crypto") require("../../bower_components/chainpad-crypto/crypto"),
null // no feedback here
); );
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } });
@ -880,16 +886,18 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
'/bower_components/chainpad-netflux/chainpad-netflux.js', '/bower_components/chainpad-netflux/chainpad-netflux.js',
'json.sortify', 'json.sortify',
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
'/bower_components/chainpad-crypto/crypto.js' '/bower_components/chainpad-crypto/crypto.js',
'/common/common-feedback.js',
//'/bower_components/tweetnacl/nacl-fast.min.js', //'/bower_components/tweetnacl/nacl-fast.min.js',
], function (Util, Hash, CPNF, Sortify, nThen, Crypto) { ], function (Util, Hash, CPNF, Sortify, nThen, Crypto, Feedback) {
return factory.apply(null, [ return factory.apply(null, [
Util, Util,
Hash, Hash,
CPNF, CPNF,
Sortify, Sortify,
nThen, nThen,
Crypto Crypto,
Feedback
]); ]);
}); });
} else { } else {

@ -2,12 +2,13 @@ define([
'/common/common-hash.js', '/common/common-hash.js',
'/common/common-util.js', '/common/common-util.js',
'/common/userObject.js', '/common/userObject.js',
'/common/outer/cache-store.js',
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
'/bower_components/chainpad-crypto/crypto.js', '/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js', '/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad/chainpad.dist.js', '/bower_components/chainpad/chainpad.dist.js',
], function (Hash, Util, UserObject, ], function (Hash, Util, UserObject, Cache,
nThen, Crypto, Listmap, ChainPad) { nThen, Crypto, Listmap, ChainPad) {
var SF = {}; var SF = {};
@ -174,6 +175,7 @@ define([
ChainPad: ChainPad, ChainPad: ChainPad,
classic: true, classic: true,
network: network, network: network,
Cache: Cache, // XXX re-enable cache usage
metadata: { metadata: {
validateKey: secret.keys.validateKey || undefined, validateKey: secret.keys.validateKey || undefined,
owners: owners owners: owners

@ -88,6 +88,7 @@ define([
CHANGE_PAD_PASSWORD_PIN: Store.changePadPasswordPin, CHANGE_PAD_PASSWORD_PIN: Store.changePadPasswordPin,
GET_LAST_HASH: Store.getLastHash, GET_LAST_HASH: Store.getLastHash,
GET_SNAPSHOT: Store.getSnapshot, GET_SNAPSHOT: Store.getSnapshot,
CORRUPTED_CACHE: Store.corruptedCache,
// Drive // Drive
DRIVE_USEROBJECT: Store.userObjectCommand, DRIVE_USEROBJECT: Store.userObjectCommand,
// Settings, // Settings,

@ -12,6 +12,7 @@ define([
'/common/common-feedback.js', '/common/common-feedback.js',
'/common/outer/invitation.js', '/common/outer/invitation.js',
'/common/cryptget.js', '/common/cryptget.js',
'/common/outer/cache-store.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js', '/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js', '/bower_components/chainpad-crypto/crypto.js',
@ -21,7 +22,7 @@ define([
'/bower_components/saferphore/index.js', '/bower_components/saferphore/index.js',
'/bower_components/tweetnacl/nacl-fast.min.js', '/bower_components/tweetnacl/nacl-fast.min.js',
], function (Util, Hash, Constants, Realtime, ], function (Util, Hash, Constants, Realtime,
ProxyManager, UserObject, SF, Roster, Messaging, Feedback, Invite, Crypt, ProxyManager, UserObject, SF, Roster, Messaging, Feedback, Invite, Crypt, Cache,
Listmap, Crypto, CpNetflux, ChainPad, nThen, Saferphore) { Listmap, Crypto, CpNetflux, ChainPad, nThen, Saferphore) {
var Team = {}; var Team = {};
@ -57,11 +58,11 @@ define([
}); });
proxy.on('disconnect', function () { proxy.on('disconnect', function () {
team.offline = true; team.offline = true;
team.sendEvent('NETWORK_DISCONNECT'); team.sendEvent('NETWORK_DISCONNECT', team.id);
}); });
proxy.on('reconnect', function () { proxy.on('reconnect', function () {
team.offline = false; team.offline = false;
team.sendEvent('NETWORK_RECONNECT'); team.sendEvent('NETWORK_RECONNECT', team.id);
}); });
} }
proxy.on('change', [], function (o, n, p) { proxy.on('change', [], function (o, n, p) {
@ -426,6 +427,7 @@ define([
channel: secret.channel, channel: secret.channel,
crypto: crypto, crypto: crypto,
ChainPad: ChainPad, ChainPad: ChainPad,
Cache: Cache, // XXX re-enable cache usage
metadata: { metadata: {
validateKey: secret.keys.validateKey || undefined, validateKey: secret.keys.validateKey || undefined,
}, },
@ -573,6 +575,7 @@ define([
logLevel: 1, logLevel: 1,
classic: true, classic: true,
ChainPad: ChainPad, ChainPad: ChainPad,
Cache: Cache,
owners: [ctx.store.proxy.edPublic] owners: [ctx.store.proxy.edPublic]
}; };
nThen(function (waitFor) { nThen(function (waitFor) {
@ -931,7 +934,9 @@ define([
if (!team) { return void cb ({error: 'ENOENT'}); } if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); } if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
var state = team.roster.getState() || {}; var state = team.roster.getState() || {};
cb(state.metadata || {}); var md = state.metadata || {};
md.offline = team.offline;
cb(md);
}; };
var setTeamMetadata = function (ctx, data, cId, cb) { var setTeamMetadata = function (ctx, data, cId, cb) {
@ -1791,7 +1796,11 @@ define([
teams[id].keys.mailbox = deriveMailbox(teams[id]); teams[id].keys.mailbox = deriveMailbox(teams[id]);
} }
openChannel(ctx, teams[id], id, waitFor(function (err) { openChannel(ctx, teams[id], id, waitFor(function (err) {
if (err) { return void console.error(err); } if (err) {
var txt = typeof(err) === "string" ? err : (err.type || err.message);
Feedback.send("TEAM_LOADING_ERROR="+txt);
return void console.error(err);
}
console.debug('Team '+id+' ready'); console.debug('Team '+id+' ready');
})); }));
}); });
@ -1879,15 +1888,15 @@ define([
var t = Util.clone(teams); var t = Util.clone(teams);
Object.keys(t).forEach(function (id) { Object.keys(t).forEach(function (id) {
// If failure to load the team, don't send it // If failure to load the team, don't send it
if (ctx.teams[id]) { return; } if (ctx.teams[id]) {
t[id].offline = ctx.teams[id].offline;
return;
}
t[id].error = true; t[id].error = true;
}); });
cb(t); cb(t);
}; };
team.execCommand = function (clientId, obj, cb) { team.execCommand = function (clientId, obj, cb) {
if (ctx.store.offline) {
return void cb({ error: 'OFFLINE' });
}
var cmd = obj.cmd; var cmd = obj.cmd;
var data = obj.data; var data = obj.data;
@ -1911,30 +1920,36 @@ define([
return void setTeamMetadata(ctx, data, clientId, cb); return void setTeamMetadata(ctx, data, clientId, cb);
} }
if (cmd === 'OFFER_OWNERSHIP') { if (cmd === 'OFFER_OWNERSHIP') {
if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); }
return void offerOwnership(ctx, data, clientId, cb); return void offerOwnership(ctx, data, clientId, cb);
} }
if (cmd === 'ANSWER_OWNERSHIP') { if (cmd === 'ANSWER_OWNERSHIP') {
if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); }
return void answerOwnership(ctx, data, clientId, cb); return void answerOwnership(ctx, data, clientId, cb);
} }
if (cmd === 'DESCRIBE_USER') { if (cmd === 'DESCRIBE_USER') {
return void describeUser(ctx, data, clientId, cb); return void describeUser(ctx, data, clientId, cb);
} }
if (cmd === 'INVITE_TO_TEAM') { if (cmd === 'INVITE_TO_TEAM') {
if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); }
return void inviteToTeam(ctx, data, clientId, cb); return void inviteToTeam(ctx, data, clientId, cb);
} }
if (cmd === 'LEAVE_TEAM') { if (cmd === 'LEAVE_TEAM') {
return void leaveTeam(ctx, data, clientId, cb); return void leaveTeam(ctx, data, clientId, cb);
} }
if (cmd === 'JOIN_TEAM') { if (cmd === 'JOIN_TEAM') {
if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); }
return void joinTeam(ctx, data, clientId, cb); return void joinTeam(ctx, data, clientId, cb);
} }
if (cmd === 'REMOVE_USER') { if (cmd === 'REMOVE_USER') {
return void removeUser(ctx, data, clientId, cb); return void removeUser(ctx, data, clientId, cb);
} }
if (cmd === 'DELETE_TEAM') { if (cmd === 'DELETE_TEAM') {
if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); }
return void deleteTeam(ctx, data, clientId, cb); return void deleteTeam(ctx, data, clientId, cb);
} }
if (cmd === 'CREATE_TEAM') { if (cmd === 'CREATE_TEAM') {
if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); }
return void createTeam(ctx, data, clientId, cb); return void createTeam(ctx, data, clientId, cb);
} }
if (cmd === 'GET_EDITABLE_FOLDERS') { if (cmd === 'GET_EDITABLE_FOLDERS') {
@ -1947,6 +1962,7 @@ define([
return void getPreviewContent(ctx, data, clientId, cb); return void getPreviewContent(ctx, data, clientId, cb);
} }
if (cmd === 'ACCEPT_LINK_INVITATION') { if (cmd === 'ACCEPT_LINK_INVITATION') {
if (ctx.store.offline) { return void cb({ error: 'OFFLINE' }); }
return void acceptLinkInvitation(ctx, data, clientId, cb); return void acceptLinkInvitation(ctx, data, clientId, cb);
} }
}; };

@ -1,9 +1,11 @@
define([ define([
'/file/file-crypto.js', '/file/file-crypto.js',
'/common/common-hash.js', '/common/common-hash.js',
'/common/common-util.js',
'/common/outer/cache-store.js',
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
'/bower_components/tweetnacl/nacl-fast.min.js', '/bower_components/tweetnacl/nacl-fast.min.js',
], function (FileCrypto, Hash, nThen) { ], function (FileCrypto, Hash, Util, Cache, nThen) {
var Nacl = window.nacl; var Nacl = window.nacl;
var module = {}; var module = {};
@ -31,9 +33,11 @@ define([
}; };
var actual = 0; var actual = 0;
var encryptedArr = [];
var again = function (err, box) { var again = function (err, box) {
if (err) { onError(err); } if (err) { onError(err); }
if (box) { if (box) {
encryptedArr.push(box);
actual += box.length; actual += box.length;
var progressValue = (actual / estimate * 100); var progressValue = (actual / estimate * 100);
progressValue = Math.min(progressValue, 100); progressValue = Math.min(progressValue, 100);
@ -55,10 +59,12 @@ define([
var uri = ['', 'blob', id.slice(0,2), id].join('/'); var uri = ['', 'blob', id.slice(0,2), id].join('/');
console.log("encrypted blob is now available as %s", uri); console.log("encrypted blob is now available as %s", uri);
var box_u8 = Util.uint8ArrayJoin(encryptedArr);
Cache.setBlobCache(id, box_u8, function (err) {
if (err) { console.warn(err); }
cb(); cb();
}); });
});
}; };
common.uploadStatus(teamId, estimate, function (e, pending) { common.uploadStatus(teamId, estimate, function (e, pending) {

@ -65,11 +65,13 @@ define([
cb(undefined, data.content, msg); cb(undefined, data.content, msg);
}; };
evReady.reg(function () { evReady.reg(function () {
postMsg(JSON.stringify({ var toSend = {
txid: txid, txid: txid,
content: content, content: content,
q: q q: q,
})); raw: opts.raw
};
postMsg(opts.raw ? toSend : JSON.stringify(toSend));
}); });
}; };
@ -84,12 +86,13 @@ define([
// If the type is a query, your handler will be invoked with a reply function that takes // If the type is a query, your handler will be invoked with a reply function that takes
// one argument (the content to reply with). // one argument (the content to reply with).
chan.on = function (queryType, handler, quiet) { chan.on = function (queryType, handler, quiet) {
var h = function (data, msg) { var h = function (data, msg, raw) {
handler(data.content, function (replyContent) { handler(data.content, function (replyContent) {
postMsg(JSON.stringify({ var toSend = {
txid: data.txid, txid: data.txid,
content: replyContent content: replyContent
})); };
postMsg(raw ? toSend : JSON.stringify(toSend));
}, msg); }, msg);
}; };
(handlers[queryType] = handlers[queryType] || []).push(h); (handlers[queryType] = handlers[queryType] || []).push(h);
@ -150,7 +153,7 @@ define([
onMsg.reg(function (msg) { onMsg.reg(function (msg) {
if (!chanLoaded) { return; } if (!chanLoaded) { return; }
if (!msg.data || msg.data === '_READY') { return; } if (!msg.data || msg.data === '_READY') { return; }
var data = JSON.parse(msg.data); var data = typeof(msg.data) === "object" ? msg.data : JSON.parse(msg.data);
if (typeof(data.ack) !== "undefined") { if (typeof(data.ack) !== "undefined") {
if (acks[data.txid]) { acks[data.txid](!data.ack); } if (acks[data.txid]) { acks[data.txid](!data.ack); }
} else if (typeof(data.q) === 'string') { } else if (typeof(data.q) === 'string') {
@ -163,7 +166,7 @@ define([
})); }));
} }
handlers[data.q].forEach(function (f) { handlers[data.q].forEach(function (f) {
f(data || JSON.parse(msg.data), msg); f(data || JSON.parse(msg.data), msg, data && data.raw);
data = undefined; data = undefined;
}); });
} else { } else {

@ -40,6 +40,14 @@ define([
userObject: userObject, userObject: userObject,
leave: leave leave: leave
}; };
if (proxy.on) {
proxy.on('disconnect', function () {
Env.folders[id].offline = true;
});
proxy.on('reconnect', function () {
Env.folders[id].offline = false;
});
}
return userObject; return userObject;
}; };

@ -221,6 +221,10 @@ define([
evStart.reg(function () { toolbar.deleted(); }); evStart.reg(function () { toolbar.deleted(); });
break; break;
} }
case STATE.READY: {
evStart.reg(function () { toolbar.ready(); });
break;
}
default: default:
} }
var isEditable = (state === STATE.READY && !unsyncMode); var isEditable = (state === STATE.READY && !unsyncMode);
@ -467,7 +471,51 @@ define([
}); });
}; };
var noCache = false; // Prevent reload loops
var onCorruptedCache = function () {
if (noCache) {
UI.errorLoadingScreen(Messages.unableToDisplay, false, function () {
common.gotoURL('');
});
}
noCache = true;
var sframeChan = common.getSframeChannel();
sframeChan.event("EV_CORRUPTED_CACHE");
};
var onCacheReady = function () {
stateChange(STATE.INITIALIZING);
toolbar.offline(true);
var newContentStr = cpNfInner.chainpad.getUserDoc();
if (toolbar) {
// Check if we have a new chainpad instance
toolbar.resetChainpad(cpNfInner.chainpad);
}
// Invalid cache
if (newContentStr === '') { return void onCorruptedCache(); }
var privateDat = cpNfInner.metadataMgr.getPrivateData();
var type = privateDat.app;
var newContent = JSON.parse(newContentStr);
var metadata = extractMetadata(newContent);
// Make sure we're using the correct app for this cache
if (metadata && typeof(metadata.type) !== 'undefined' && metadata.type !== type) {
return void onCorruptedCache();
}
cpNfInner.metadataMgr.updateMetadata(metadata);
newContent = normalize(newContent);
if (!unsyncMode) {
contentUpdate(newContent, function () { return function () {}; });
}
UI.removeLoadingScreen(emitResize);
};
var onReady = function () { var onReady = function () {
toolbar.offline(false);
var newContentStr = cpNfInner.chainpad.getUserDoc(); var newContentStr = cpNfInner.chainpad.getUserDoc();
if (state === STATE.DELETED) { return; } if (state === STATE.DELETED) { return; }
@ -482,7 +530,6 @@ define([
var privateDat = cpNfInner.metadataMgr.getPrivateData(); var privateDat = cpNfInner.metadataMgr.getPrivateData();
var type = privateDat.app; var type = privateDat.app;
// contentUpdate may be async so we need an nthen here // contentUpdate may be async so we need an nthen here
nThen(function (waitFor) { nThen(function (waitFor) {
if (!newPad) { if (!newPad) {
@ -508,14 +555,19 @@ define([
console.log("Either this is an empty document which has not been touched"); console.log("Either this is an empty document which has not been touched");
console.log("Or else something is terribly wrong, reloading."); console.log("Or else something is terribly wrong, reloading.");
Feedback.send("NON_EMPTY_NEWDOC"); Feedback.send("NON_EMPTY_NEWDOC");
setTimeout(function () { common.gotoURL(); }, 1000); // The cache may be wrong, empty it and reload after.
waitFor.abort();
onCorruptedCache();
return; return;
} }
console.log('updating title');
title.updateTitle(title.defaultTitle); title.updateTitle(title.defaultTitle);
evOnDefaultContentNeeded.fire(); evOnDefaultContentNeeded.fire();
} }
}).nThen(function () { }).nThen(function () {
// We have a valid chainpad, reenable cache fix in case with reconnect with
// a corrupted cache
noCache = false;
stateChange(STATE.READY); stateChange(STATE.READY);
firstConnection = false; firstConnection = false;
@ -551,6 +603,8 @@ define([
Thumb.initPadThumbnails(common, options.thumbnail); Thumb.initPadThumbnails(common, options.thumbnail);
} }
} }
common.checkTrimHistory();
}); });
}; };
var onConnectionChange = function (info) { var onConnectionChange = function (info) {
@ -732,6 +786,7 @@ define([
onRemote: onRemote, onRemote: onRemote,
onLocal: onLocal, onLocal: onLocal,
onInit: onInit, onInit: onInit,
onCacheReady: function () { evStart.reg(onCacheReady); },
onReady: function () { evStart.reg(onReady); }, onReady: function () { evStart.reg(onReady); },
onConnectionChange: onConnectionChange, onConnectionChange: onConnectionChange,
onError: onError, onError: onError,

@ -34,6 +34,7 @@ define([
var onLocal = config.onLocal || function () { }; var onLocal = config.onLocal || function () { };
var setMyID = config.setMyID || function () { }; var setMyID = config.setMyID || function () { };
var onReady = config.onReady || function () { }; var onReady = config.onReady || function () { };
var onCacheReady = config.onCacheReady || function () { };
var onError = config.onError || function () { }; var onError = config.onError || function () { };
var userName = config.userName; var userName = config.userName;
var initialState = config.initialState; var initialState = config.initialState;
@ -93,6 +94,9 @@ define([
evInfiniteSpinner.fire(); evInfiniteSpinner.fire();
}, 2000); }, 2000);
sframeChan.on('EV_RT_CACHE_READY', function () {
onCacheReady({realtime: chainpad});
});
sframeChan.on('EV_RT_DISCONNECT', function (isPermanent) { sframeChan.on('EV_RT_DISCONNECT', function (isPermanent) {
isReady = false; isReady = false;
chainpad.abort(); chainpad.abort();

@ -46,6 +46,7 @@ define([], function () {
// shim between chainpad and netflux // shim between chainpad and netflux
var msgIn = function (peer, msg) { var msgIn = function (peer, msg) {
try { try {
if (/^\[/.test(msg)) { return msg; } // Already decrypted
var isHk = peer.length !== 32; var isHk = peer.length !== 32;
var key = isNewHash ? validateKey : false; var key = isNewHash ? validateKey : false;
var decryptedMsg = Crypto.decrypt(msg, key, isHk); var decryptedMsg = Crypto.decrypt(msg, key, isHk);
@ -114,16 +115,25 @@ define([], function () {
if (firstConnection) { if (firstConnection) {
firstConnection = false; firstConnection = false;
// Add the handlers to the WebChannel // Add the handlers to the WebChannel
padRpc.onMessageEvent.reg(function (msg) { onMessage(msg); });
padRpc.onJoinEvent.reg(function (m) { sframeChan.event('EV_RT_JOIN', m); }); padRpc.onJoinEvent.reg(function (m) { sframeChan.event('EV_RT_JOIN', m); });
padRpc.onLeaveEvent.reg(function (m) { sframeChan.event('EV_RT_LEAVE', m); }); padRpc.onLeaveEvent.reg(function (m) { sframeChan.event('EV_RT_LEAVE', m); });
} }
}; };
padRpc.onMessageEvent.reg(function (msg) { onMessage(msg); });
padRpc.onDisconnectEvent.reg(function (permanent) { padRpc.onDisconnectEvent.reg(function (permanent) {
sframeChan.event('EV_RT_DISCONNECT', permanent); sframeChan.event('EV_RT_DISCONNECT', permanent);
}); });
padRpc.onCacheReadyEvent.reg(function () {
sframeChan.event('EV_RT_CACHE_READY');
});
padRpc.onCacheEvent.reg(function () {
sframeChan.event('EV_RT_CACHE');
});
padRpc.onConnectEvent.reg(function (data) { padRpc.onConnectEvent.reg(function (data) {
onOpen(data); onOpen(data);
}); });

@ -1,5 +1,6 @@
define([ define([
'jquery', 'jquery',
'/api/config',
'/file/file-crypto.js', '/file/file-crypto.js',
'/common/make-backup.js', '/common/make-backup.js',
'/common/common-thumbnail.js', '/common/common-thumbnail.js',
@ -12,7 +13,7 @@ define([
'/bower_components/file-saver/FileSaver.min.js', '/bower_components/file-saver/FileSaver.min.js',
'/bower_components/tweetnacl/nacl-fast.min.js', '/bower_components/tweetnacl/nacl-fast.min.js',
], function ($, FileCrypto, MakeBackup, Thumb, UI, UIElements, Util, Hash, h, Messages) { ], function ($, ApiConfig, FileCrypto, MakeBackup, Thumb, UI, UIElements, Util, Hash, h, Messages) {
var Nacl = window.nacl; var Nacl = window.nacl;
var module = {}; var module = {};
@ -48,7 +49,7 @@ define([
}; };
var tableHeader = h('div.cp-fileupload-header', [ var tableHeader = h('div.cp-fileupload-header', [
h('div.cp-fileupload-header-title', h('span', Messages.fileuploadHeader || 'Uploaded files')), h('div.cp-fileupload-header-title', h('span', Messages.fileTableHeader)),
h('div.cp-fileupload-header-close', h('span.fa.fa-times')), h('div.cp-fileupload-header-close', h('span.fa.fa-times')),
]); ]);
@ -136,13 +137,11 @@ define([
file.uid = Util.uid(); file.uid = Util.uid();
response.expect(file.uid, function (href) { response.expect(file.uid, function (href) {
var mdMgr = common.getMetadataMgr();
var origin = mdMgr.getPrivateData().origin;
$link.prepend($('<span>', {'class': 'fa fa-external-link'})); $link.prepend($('<span>', {'class': 'fa fa-external-link'}));
$link.attr('href', href) $link.attr('href', href)
.click(function (e) { .click(function (e) {
e.preventDefault(); e.preventDefault();
window.open(origin + $link.attr('href'), '_blank'); common.openURL($link.attr('href'));
}); });
var title = metadata.name; var title = metadata.name;
if (!config.noStore) { if (!config.noStore) {
@ -168,8 +167,12 @@ define([
if (config.onError) { config.onError(e); } if (config.onError) { config.onError(e); }
if (e === 'TOO_LARGE') { if (e === 'TOO_LARGE') {
$pv.text(Messages.upload_tooLargeBrief); var privateData = common.getMetadataMgr().getPrivateData();
return void UI.alert(Messages.upload_tooLarge); var l = privateData.plan ? ApiConfig.premiumUploadSize : false;
l = l || ApiConfig.maxUploadSize || '?';
var maxSizeStr = Util.bytesToMegabytes(l);
$pv.text(Messages.error);
return void UI.alert(Messages._getKey('upload_tooLargeBrief', [maxSizeStr]));
} }
if (e === 'NOT_ENOUGH_SPACE') { if (e === 'NOT_ENOUGH_SPACE') {
$pv.text(Messages.upload_notEnoughSpaceBrief); $pv.text(Messages.upload_notEnoughSpaceBrief);
@ -262,7 +265,8 @@ define([
// name // name
$('<td>').append($link).appendTo($tr); $('<td>').append($link).appendTo($tr);
// size // size
$('<td>').text(UIElements.prettySize(estimate)).appendTo($tr); var size = estimate ? UIElements.prettySize(estimate) : '';
$(h('td.cp-fileupload-size')).text(size).appendTo($tr);
// progress // progress
$('<td>', {'class': 'cp-fileupload-table-progress'}).append($progressContainer).appendTo($tr); $('<td>', {'class': 'cp-fileupload-table-progress'}).append($progressContainer).appendTo($tr);
// cancel // cancel
@ -590,12 +594,11 @@ define([
queue.next(); queue.next();
}; };
/*
var cancelled = function () { var cancelled = function () {
$row.find('.cp-fileupload-table-cancel').addClass('cancelled').html('').append(h('span.fa.fa-minus')); $row.find('.cp-fileupload-table-cancel').addClass('cancelled').html('').append(h('span.fa.fa-minus'));
queue.inProgress = false; queue.inProgress = false;
queue.next(); queue.next();
};*/ };
/** /**
* Update progress in the download panel, for downloading a file * Update progress in the download panel, for downloading a file
@ -629,6 +632,17 @@ define([
*/ */
var updateProgress = function (progressValue) { var updateProgress = function (progressValue) {
var text = Math.round(progressValue*100) + '%'; var text = Math.round(progressValue*100) + '%';
if (Array.isArray(data.list)) {
text = Messages._getKey('download_zip_file', [Math.round(progressValue * data.list.length), data.list.length]);
}
if (progressValue === 2) {
text = Messages.download_zip;
progressValue = 1;
}
if (progressValue === 3) {
text = "100%";
progressValue = 1;
}
$pv.text(text); $pv.text(text);
$pb.css({ $pb.css({
width: (progressValue * 100) + '%' width: (progressValue * 100) + '%'
@ -640,8 +654,10 @@ define([
fileHost: privateData.fileHost, fileHost: privateData.fileHost,
get: common.getPad, get: common.getPad,
sframeChan: sframeChan, sframeChan: sframeChan,
cache: common.getCache()
}; };
downloadFunction(ctx, data, function (err, obj) {
var dl = downloadFunction(ctx, data, function (err, obj) {
$link.prepend($('<span>', {'class': 'fa fa-external-link'})) $link.prepend($('<span>', {'class': 'fa fa-external-link'}))
.attr('href', '#') .attr('href', '#')
.click(function (e) { .click(function (e) {
@ -657,19 +673,17 @@ define([
folderProgress: updateProgress, folderProgress: updateProgress,
}); });
/* var $cancel = $row.find('.cp-fileupload-table-cancel').html('');
var $cancel = $('<span>', {'class': 'cp-fileupload-table-cancel-button fa fa-times'}).click(function () { if (dl && dl.cancel) {
$('<span>', {
'class': 'cp-fileupload-table-cancel-button fa fa-times'
}).click(function () {
dl.cancel(); dl.cancel();
$cancel.remove(); $cancel.remove();
$row.find('.cp-fileupload-table-progress-value').text(Messages.upload_cancelled); $row.find('.cp-fileupload-table-progress-value').text(Messages.upload_cancelled);
cancelled(); cancelled();
}); }).appendTo($cancel);
*/ }
$row.find('.cp-fileupload-table-cancel')
.html('')
.append(h('span.fa.fa-minus'));
//.append($cancel);
}; };
File.downloadFile = function (fData, cb) { File.downloadFile = function (fData, cb) {

@ -8,7 +8,7 @@ define([
], function (nThen, ApiConfig, RequireConfig, Messages, $) { ], function (nThen, ApiConfig, RequireConfig, Messages, $) {
var common = {}; var common = {};
common.initIframe = function (waitFor, isRt) { common.initIframe = function (waitFor, isRt, pathname) {
var requireConfig = RequireConfig(); var requireConfig = RequireConfig();
var lang = Messages._languageUsed; var lang = Messages._languageUsed;
var req = { var req = {
@ -31,7 +31,7 @@ define([
} }
document.getElementById('sbox-iframe').setAttribute('src', document.getElementById('sbox-iframe').setAttribute('src',
ApiConfig.httpSafeOrigin + window.location.pathname + 'inner.html?' + ApiConfig.httpSafeOrigin + (pathname || window.location.pathname) + 'inner.html?' +
requireConfig.urlArgs + '#' + encodeURIComponent(JSON.stringify(req))); requireConfig.urlArgs + '#' + encodeURIComponent(JSON.stringify(req)));
// This is a cheap trick to avoid loading sframe-channel in parallel with the // This is a cheap trick to avoid loading sframe-channel in parallel with the
@ -97,15 +97,17 @@ define([
'/common/common-hash.js', '/common/common-hash.js',
'/common/common-util.js', '/common/common-util.js',
'/common/common-realtime.js', '/common/common-realtime.js',
'/common/notify.js',
'/common/common-constants.js', '/common/common-constants.js',
'/common/common-feedback.js', '/common/common-feedback.js',
'/common/outer/local-store.js', '/common/outer/local-store.js',
'/common/outer/cache-store.js',
'/customize/application_config.js', '/customize/application_config.js',
'/common/test.js', '/common/test.js',
'/common/userObject.js', '/common/userObject.js',
], waitFor(function (_CpNfOuter, _Cryptpad, _Crypto, _Cryptget, _SFrameChannel, ], waitFor(function (_CpNfOuter, _Cryptpad, _Crypto, _Cryptget, _SFrameChannel,
_SecureIframe, _Messaging, _Notifier, _Hash, _Util, _Realtime, _SecureIframe, _Messaging, _Notifier, _Hash, _Util, _Realtime, _Notify,
_Constants, _Feedback, _LocalStore, _AppConfig, _Test, _UserObject) { _Constants, _Feedback, _LocalStore, _Cache, _AppConfig, _Test, _UserObject) {
CpNfOuter = _CpNfOuter; CpNfOuter = _CpNfOuter;
Cryptpad = _Cryptpad; Cryptpad = _Cryptpad;
Crypto = Utils.Crypto = _Crypto; Crypto = Utils.Crypto = _Crypto;
@ -120,7 +122,9 @@ define([
Utils.Constants = _Constants; Utils.Constants = _Constants;
Utils.Feedback = _Feedback; Utils.Feedback = _Feedback;
Utils.LocalStore = _LocalStore; Utils.LocalStore = _LocalStore;
Utils.Cache = _Cache;
Utils.UserObject = _UserObject; Utils.UserObject = _UserObject;
Utils.Notify = _Notify;
Utils.currentPad = currentPad; Utils.currentPad = currentPad;
AppConfig = _AppConfig; AppConfig = _AppConfig;
Test = _Test; Test = _Test;
@ -478,6 +482,7 @@ define([
// We've received a link without /p/ and it doesn't work without a password: abort // We've received a link without /p/ and it doesn't work without a password: abort
return void todo(); return void todo();
} }
// Wrong password or deleted file? // Wrong password or deleted file?
askPassword(true, passwordCfg); askPassword(true, passwordCfg);
})); }));
@ -532,6 +537,12 @@ define([
var edPublic, curvePublic, notifications, isTemplate; var edPublic, curvePublic, notifications, isTemplate;
var settings = {}; var settings = {};
var isSafe = ['debug', 'profile', 'drive', 'teams'].indexOf(currentPad.app) !== -1; var isSafe = ['debug', 'profile', 'drive', 'teams'].indexOf(currentPad.app) !== -1;
var isDeleted = isNewFile && currentPad.hash.length > 0;
if (isDeleted) {
Utils.Cache.clearChannel(secret.channel);
}
var updateMeta = function () { var updateMeta = function () {
//console.log('EV_METADATA_UPDATE'); //console.log('EV_METADATA_UPDATE');
var metaObj; var metaObj;
@ -555,6 +566,7 @@ define([
defaultTitle: defaultTitle, defaultTitle: defaultTitle,
type: cfg.type || parsed.type type: cfg.type || parsed.type
}; };
var notifs = Utils.Notify.isSupported() && Utils.Notify.hasPermission();
var additionalPriv = { var additionalPriv = {
app: parsed.type, app: parsed.type,
loggedIn: Utils.LocalStore.isLoggedIn(), loggedIn: Utils.LocalStore.isLoggedIn(),
@ -569,12 +581,13 @@ define([
isPresent: parsed.hashData && parsed.hashData.present, isPresent: parsed.hashData && parsed.hashData.present,
isEmbed: parsed.hashData && parsed.hashData.embed, isEmbed: parsed.hashData && parsed.hashData.embed,
isHistoryVersion: parsed.hashData && parsed.hashData.versionHash, isHistoryVersion: parsed.hashData && parsed.hashData.versionHash,
notifications: notifs,
accounts: { accounts: {
donateURL: Cryptpad.donateURL, donateURL: Cryptpad.donateURL,
upgradeURL: Cryptpad.upgradeURL upgradeURL: Cryptpad.upgradeURL
}, },
isNewFile: isNewFile, isNewFile: isNewFile,
isDeleted: isNewFile && currentPad.hash.length > 0, isDeleted: isDeleted,
password: password, password: password,
channel: secret.channel, channel: secret.channel,
enableSF: localStorage.CryptPad_SF === "1", // TODO to remove when enabled by default enableSF: localStorage.CryptPad_SF === "1", // TODO to remove when enabled by default
@ -677,6 +690,22 @@ define([
}); });
}); });
sframeChan.on('Q_GET_BLOB_CACHE', function (data, cb) {
if (!Utils.Cache) { return void cb({error: 'NOCACHE'}); }
Utils.Cache.getBlobCache(data.id, function (err, obj) {
if (err) { return void cb({error: err}); }
cb(obj);
});
});
sframeChan.on('Q_SET_BLOB_CACHE', function (data, cb) {
if (!Utils.Cache) { return void cb({error: 'NOCACHE'}); }
if (!data || !data.u8 || typeof(data.u8) !== "object") { return void cb({error: 'EINVAL'}); }
Utils.Cache.setBlobCache(data.id, data.u8, function (err) {
if (err) { return void cb({error: err}); }
cb();
});
});
sframeChan.on('Q_GET_ATTRIBUTE', function (data, cb) { sframeChan.on('Q_GET_ATTRIBUTE', function (data, cb) {
Cryptpad.getAttribute(data.key, function (e, data) { Cryptpad.getAttribute(data.key, function (e, data) {
cb({ cb({
@ -722,7 +751,16 @@ define([
sframeChan.on('EV_OPEN_URL', function (url) { sframeChan.on('EV_OPEN_URL', function (url) {
if (url) { if (url) {
window.open(url); var a = window.open(url);
if (!a) {
sframeChan.event('EV_POPUP_BLOCKED');
}
}
});
sframeChan.on('EV_OPEN_UNSAFE_URL', function (url) {
if (url) {
window.open(ApiConfig.httpSafeOrigin + '/bounce/#' + encodeURIComponent(url));
} }
}); });
@ -1569,9 +1607,14 @@ define([
}); });
}); });
if (cfg.messaging) { sframeChan.on('Q_ASK_NOTIFICATION', function (data, cb) {
Notifier.getPermission(); if (!Utils.Notify.isSupported()) { return void cb(false); }
Notification.requestPermission(function (s) {
cb(s === "granted");
});
});
if (cfg.messaging) {
sframeChan.on('Q_CHAT_OPENPADCHAT', function (data, cb) { sframeChan.on('Q_CHAT_OPENPADCHAT', function (data, cb) {
Cryptpad.universal.execCommand({ Cryptpad.universal.execCommand({
type: 'messenger', type: 'messenger',
@ -1683,6 +1726,10 @@ define([
}); });
}; };
sframeChan.on('EV_CORRUPTED_CACHE', function () {
Cryptpad.onCorruptedCache(secret.channel);
});
sframeChan.on('Q_CREATE_PAD', function (data, cb) { sframeChan.on('Q_CREATE_PAD', function (data, cb) {
if (!isNewFile || rtStarted) { return; } if (!isNewFile || rtStarted) { return; }
// Create a new hash // Create a new hash
@ -1797,7 +1844,12 @@ define([
} }
startRealtime(); startRealtime();
cb(); cb();
}, cryptputCfg); }, cryptputCfg, function (progress) {
sframeChan.event('EV_LOADING_INFO', {
type: 'pad',
progress: progress
});
});
return; return;
} }
// Start realtime outside the iframe and callback // Start realtime outside the iframe and callback

@ -11,6 +11,7 @@ define([
'/common/sframe-common-codemirror.js', '/common/sframe-common-codemirror.js',
'/common/sframe-common-cursor.js', '/common/sframe-common-cursor.js',
'/common/sframe-common-mailbox.js', '/common/sframe-common-mailbox.js',
'/common/inner/cache.js',
'/common/inner/common-mediatag.js', '/common/inner/common-mediatag.js',
'/common/metadata-manager.js', '/common/metadata-manager.js',
@ -36,6 +37,7 @@ define([
CodeMirror, CodeMirror,
Cursor, Cursor,
Mailbox, Mailbox,
Cache,
MT, MT,
MetadataMgr, MetadataMgr,
AppConfig, AppConfig,
@ -142,7 +144,7 @@ define([
} }
return; return;
}; };
funcs.importMediaTag = function ($mt) { var getMtData = function ($mt) {
if (!$mt || !$mt.is('media-tag')) { return; } if (!$mt || !$mt.is('media-tag')) { return; }
var chanStr = $mt.attr('src'); var chanStr = $mt.attr('src');
var keyStr = $mt.attr('data-crypto-key'); var keyStr = $mt.attr('data-crypto-key');
@ -154,10 +156,27 @@ define([
var channel = src.replace(/\/blob\/[0-9a-f]{2}\//i, ''); var channel = src.replace(/\/blob\/[0-9a-f]{2}\//i, '');
// Get key // Get key
var key = keyStr.replace(/cryptpad:/i, ''); var key = keyStr.replace(/cryptpad:/i, '');
return {
channel: channel,
key: key
};
};
funcs.getHashFromMediaTag = function ($mt) {
var data = getMtData($mt);
if (!data) { return; }
return Hash.getFileHashFromKeys({
version: 1,
channel: data.channel,
keys: { fileKeyStr: data.key }
});
};
funcs.importMediaTag = function ($mt) {
var data = getMtData($mt);
if (!data) { return; }
var metadata = $mt[0]._mediaObject._blob.metadata; var metadata = $mt[0]._mediaObject._blob.metadata;
ctx.sframeChan.query('Q_IMPORT_MEDIATAG', { ctx.sframeChan.query('Q_IMPORT_MEDIATAG', {
channel: channel, channel: data.channel,
key: key, key: data.key,
name: metadata.name, name: metadata.name,
type: metadata.type, type: metadata.type,
owners: metadata.owners owners: metadata.owners
@ -264,6 +283,65 @@ define([
return teamChatChannel; return teamChatChannel;
}; };
// When opening a pad, if were an owner check the history size and prompt for trimming if
// necessary
funcs.checkTrimHistory = function (channels, isDrive) {
channels = channels || [];
var priv = ctx.metadataMgr.getPrivateData();
var limit = 100 * 1024 * 1024; // 100MB
var owned;
nThen(function (w) {
if (isDrive) {
funcs.getAttribute(['drive', 'trim'], w(function (err, val) {
if (err || typeof(val) !== "number") { return; }
if (val < (+new Date())) { return; }
w.abort();
}));
return;
}
funcs.getPadAttribute('trim', w(function (err, val) {
if (err || typeof(val) !== "number") { return; }
if (val < (+new Date())) { return; }
w.abort();
}));
}).nThen(function (w) {
// Check ownership
// DRIVE
if (isDrive) {
if (!priv.isDriveOwned) { return void w.abort(); }
return;
}
// PAD
channels.push({ channel: priv.channel });
funcs.getPadMetadata({
channel: priv.channel
}, w(function (md) {
if (md && md.error) { return void w.abort(); }
var owners = md.owners;
owned = funcs.isOwned(owners);
if (!owned) { return void w.abort(); }
}));
}).nThen(function () {
// We're an owner: check the history size
var history = funcs.makeUniversal('history');
history.execCommand('GET_HISTORY_SIZE', {
account: isDrive,
pad: !isDrive,
channels: channels,
teamId: typeof(owned) === "number" && owned
}, function (obj) {
if (obj && obj.error) { return; } // can't get history size: abort
var bytes = obj.size;
if (!bytes || typeof(bytes) !== "number") { return; } // no history: abort
if (bytes < limit) { return; }
obj.drive = isDrive;
UIElements.displayTrimHistoryPrompt(funcs, obj);
});
});
};
var cursorChannel; var cursorChannel;
// common-ui-elements needs to be able to get the cursor channel to put it in metadata when // common-ui-elements needs to be able to get the cursor channel to put it in metadata when
// importing a template // importing a template
@ -528,6 +606,10 @@ define([
}); });
}; };
funcs.getCache = function () {
return ctx.cache;
};
/* funcs.storeLinkToClipboard = function (readOnly, cb) { /* funcs.storeLinkToClipboard = function (readOnly, cb) {
ctx.sframeChan.query('Q_STORE_LINK_TO_CLIPBOARD', readOnly, function (err) { ctx.sframeChan.query('Q_STORE_LINK_TO_CLIPBOARD', readOnly, function (err) {
if (cb) { cb(err); } if (cb) { cb(err); }
@ -555,7 +637,11 @@ define([
return window.location.origin + '/bounce/#' + encodeURIComponent(url); return window.location.origin + '/bounce/#' + encodeURIComponent(url);
}; };
funcs.openUnsafeURL = function (url) { funcs.openUnsafeURL = function (url) {
var bounceHref = funcs.getBounceURL(url); var app = ctx.metadataMgr.getPrivateData().app;
if (app === "sheet") {
return void ctx.sframeChan.event('EV_OPEN_UNSAFE_URL', url);
}
var bounceHref = window.location.origin + '/bounce/#' + encodeURIComponent(url);
window.open(bounceHref); window.open(bounceHref);
}; };
@ -685,6 +771,10 @@ define([
UI.errorLoadingScreen(Messages.password_error_seed); UI.errorLoadingScreen(Messages.password_error_seed);
}); });
ctx.sframeChan.on("EV_POPUP_BLOCKED", function () {
UI.alert(Messages.errorPopupBlocked);
});
ctx.sframeChan.on("EV_EXPIRED_ERROR", function () { ctx.sframeChan.on("EV_EXPIRED_ERROR", function () {
funcs.onServerError({ funcs.onServerError({
type: 'EEXPIRED' type: 'EEXPIRED'
@ -729,12 +819,24 @@ define([
modules[type].onEvent(obj.data); modules[type].onEvent(obj.data);
}); });
ctx.cache = Cache.create(ctx.sframeChan);
ctx.metadataMgr.onReady(waitFor()); ctx.metadataMgr.onReady(waitFor());
}).nThen(function () { }).nThen(function () {
var privateData = ctx.metadataMgr.getPrivateData(); var privateData = ctx.metadataMgr.getPrivateData();
funcs.addShortcuts(window, Boolean(privateData.app)); funcs.addShortcuts(window, Boolean(privateData.app));
var mt = Util.find(privateData, ['settings', 'general', 'mediatag-size']);
if (MT.MediaTag && typeof(mt) === "number") {
var maxMtSize = mt === -1 ? Infinity : mt * 1024 * 1024;
MT.MediaTag.setDefaultConfig('maxDownloadSize', maxMtSize);
}
if (MT.MediaTag && ctx.cache) {
MT.MediaTag.setDefaultConfig('Cache', ctx.cache);
}
try { try {
var feedback = privateData.feedbackAllowed; var feedback = privateData.feedbackAllowed;
Feedback.init(feedback); Feedback.init(feedback);

@ -229,7 +229,6 @@ MessengerUI, Messages) {
// Editors // Editors
var pendingFriends = Common.getPendingFriends(); // Friend requests sent var pendingFriends = Common.getPendingFriends(); // Friend requests sent
var friendRequests = Common.getFriendRequests(); // Friend requests received var friendRequests = Common.getFriendRequests(); // Friend requests received
var friendTo = +new Date() - (2 * 24 * 3600 * 1000);
editUsersNames.forEach(function (data) { editUsersNames.forEach(function (data) {
var name = data.name || Messages.anonymous; var name = data.name || Messages.anonymous;
var $span = $('<span>', {'class': 'cp-avatar'}); var $span = $('<span>', {'class': 'cp-avatar'});
@ -297,7 +296,7 @@ MessengerUI, Messages) {
} }
} else if (Common.isLoggedIn() && data.curvePublic && !friends[data.curvePublic] } else if (Common.isLoggedIn() && data.curvePublic && !friends[data.curvePublic]
&& !priv.readOnly) { && !priv.readOnly) {
if (pendingFriends[data.curvePublic] && pendingFriends[data.curvePublic] > friendTo) { if (pendingFriends[data.curvePublic]) {
$('<button>', { $('<button>', {
'class': 'fa fa-hourglass-half cp-toolbar-userlist-button', 'class': 'fa fa-hourglass-half cp-toolbar-userlist-button',
'title': Messages.profile_friendRequestSent 'title': Messages.profile_friendRequestSent
@ -322,7 +321,10 @@ MessengerUI, Messages) {
}).appendTo($nameSpan).click(function (e) { }).appendTo($nameSpan).click(function (e) {
e.stopPropagation(); e.stopPropagation();
Common.sendFriendRequest(data, function (err, obj) { Common.sendFriendRequest(data, function (err, obj) {
if (err || (obj && obj.error)) { return void console.error(err || obj.error); } if (err || (obj && obj.error)) {
UI.warn(Messages.error);
return void console.error(err || obj.error);
}
}); });
}); });
} }
@ -338,7 +340,7 @@ MessengerUI, Messages) {
if (data.profile) { if (data.profile) {
$span.addClass('cp-userlist-clickable'); $span.addClass('cp-userlist-clickable');
$span.click(function () { $span.click(function () {
window.open(origin+'/profile/#' + data.profile); Common.openURL(origin+'/profile/#' + data.profile);
}); });
} }
Common.displayAvatar($span, data.avatar, name, function () { Common.displayAvatar($span, data.avatar, name, function () {
@ -838,10 +840,10 @@ MessengerUI, Messages) {
var onClick = function (e) { var onClick = function (e) {
e.preventDefault(); e.preventDefault();
if (e.ctrlKey) { if (e.ctrlKey) {
window.open(href); Common.openURL(href);
return; return;
} }
window.parent.location = href; Common.gotoURL(href);
}; };
var onContext = function (e) { e.stopPropagation(); }; var onContext = function (e) { e.stopPropagation(); };
@ -879,6 +881,14 @@ MessengerUI, Messages) {
$spin.text(Messages.saved); $spin.text(Messages.saved);
}, /*local ? 0 :*/ SPINNER_DISAPPEAR_TIME); }, /*local ? 0 :*/ SPINNER_DISAPPEAR_TIME);
}; };
if (config.spinner) {
var h = function () {
onSynced();
try { config.spinner.onSync.unreg(h); } catch (e) { console.error(e); }
};
config.spinner.onSync.reg(h);
return;
}
config.sfCommon.whenRealtimeSyncs(onSynced); config.sfCommon.whenRealtimeSyncs(onSynced);
}; };
var ks = function (toolbar, config, local) { var ks = function (toolbar, config, local) {
@ -891,6 +901,15 @@ MessengerUI, Messages) {
var $spin = $('<span>', {'class': SPINNER_CLS}).appendTo(toolbar.title); var $spin = $('<span>', {'class': SPINNER_CLS}).appendTo(toolbar.title);
$spin.text(Messages.synchronizing); $spin.text(Messages.synchronizing);
if (config.spinner) {
config.spinner.onPatch.reg(ks(toolbar, config));
typing = 0;
setTimeout(function () {
kickSpinner(toolbar, config);
});
return $spin;
}
if (config.realtime) { if (config.realtime) {
config.realtime.onPatch(ks(toolbar, config)); config.realtime.onPatch(ks(toolbar, config));
config.realtime.onMessage(ks(toolbar, config, true)); config.realtime.onMessage(ks(toolbar, config, true));
@ -990,6 +1009,29 @@ MessengerUI, Messages) {
h('div.cp-notifications-empty', Messages.notifications_empty) h('div.cp-notifications-empty', Messages.notifications_empty)
]); ]);
var pads_options = [div]; var pads_options = [div];
var metadataMgr = config.metadataMgr;
var privateData = metadataMgr.getPrivateData();
if (!privateData.notifications) {
var allowNotif = h('div.cp-notifications-gotoapp', h('p', Messages.allowNotifications));
pads_options.unshift(h("hr"));
pads_options.unshift(allowNotif);
var $allow = $(allowNotif).click(function () {
Common.getSframeChannel().event('Q_ASK_NOTIFICATION', null, function (e, allow) {
if (!allow) { return; }
$(allowNotif).remove();
});
});
var onChange = function () {
var privateData = metadataMgr.getPrivateData();
if (!privateData.notifications) { return; }
$allow.remove();
metadataMgr.off('change', onChange);
};
metadataMgr.onChange(onChange);
}
if (Common.isLoggedIn()) { if (Common.isLoggedIn()) {
pads_options.unshift(h("hr")); pads_options.unshift(h("hr"));
pads_options.unshift(openNotifsApp); pads_options.unshift(openNotifsApp);
@ -1295,6 +1337,10 @@ MessengerUI, Messages) {
toolbar.spinner.text(Messages.reconnecting); toolbar.spinner.text(Messages.reconnecting);
} }
}; };
toolbar.ready = function () {
toolbar.connected = true;
kickSpinner(toolbar, config);
};
toolbar.errorState = function (state, error) { toolbar.errorState = function (state, error) {
toolbar.isErrorState = state; toolbar.isErrorState = state;
@ -1365,6 +1411,18 @@ MessengerUI, Messages) {
} }
}; };
toolbar.offline = function (bool) {
toolbar.connected = !bool; // Can't edit title
toolbar.history = bool; // Stop "Initializing" state
toolbar.isErrorState = bool; // Stop kickSpinner
toolbar.title.toggleClass('cp-toolbar-unsync', bool); // "read only" next to the title
if (bool && toolbar.spinner) {
toolbar.spinner.text(Messages.offline);
} else {
kickSpinner(toolbar, config);
}
};
// On log out, remove permanently the realtime elements of the toolbar // On log out, remove permanently the realtime elements of the toolbar
Common.onLogout(function () { Common.onLogout(function () {
failed(); failed();

@ -40,7 +40,7 @@
"saved": "Gespeichert", "saved": "Gespeichert",
"synced": "Alles gespeichert", "synced": "Alles gespeichert",
"deleted": "Gelöscht", "deleted": "Gelöscht",
"deletedFromServer": "Pad wurde vom Server gelöscht", "deletedFromServer": "Dokument zerstört",
"mustLogin": "Du musst angemeldet sein, um auf diese Seite zuzugreifen", "mustLogin": "Du musst angemeldet sein, um auf diese Seite zuzugreifen",
"disabledApp": "Diese Anwendung wurde deaktiviert. Kontaktiere den Administrator dieses CryptPads, um mehr Informationen zu erhalten.", "disabledApp": "Diese Anwendung wurde deaktiviert. Kontaktiere den Administrator dieses CryptPads, um mehr Informationen zu erhalten.",
"realtime_unrecoverableError": "Es ist ein nicht reparierbarer Fehler aufgetreten. Klicke auf OK, um neu zu laden.", "realtime_unrecoverableError": "Es ist ein nicht reparierbarer Fehler aufgetreten. Klicke auf OK, um neu zu laden.",
@ -547,7 +547,7 @@
"upload_notEnoughSpace": "Der verfügbare Speicherplatz in deinem CryptDrive reicht leider nicht für diese Datei.", "upload_notEnoughSpace": "Der verfügbare Speicherplatz in deinem CryptDrive reicht leider nicht für diese Datei.",
"upload_notEnoughSpaceBrief": "Unzureichender Speicherplatz", "upload_notEnoughSpaceBrief": "Unzureichender Speicherplatz",
"upload_tooLarge": "Diese Datei überschreitet die für deinen Account erlaubte maximale Größe.", "upload_tooLarge": "Diese Datei überschreitet die für deinen Account erlaubte maximale Größe.",
"upload_tooLargeBrief": "Datei zu groß", "upload_tooLargeBrief": "Datei überschreitet die maximale Größe von {0} MB",
"upload_choose": "Eine Datei wählen", "upload_choose": "Eine Datei wählen",
"upload_pending": "In der Warteschlange", "upload_pending": "In der Warteschlange",
"upload_cancelled": "Abgebrochen", "upload_cancelled": "Abgebrochen",
@ -1468,5 +1468,33 @@
"loading_state_1": "Drive laden", "loading_state_1": "Drive laden",
"loading_state_0": "Oberfläche vorbereiten", "loading_state_0": "Oberfläche vorbereiten",
"loading_state_5": "Dokument rekonstruieren", "loading_state_5": "Dokument rekonstruieren",
"error_unhelpfulScriptError": "Skriptfehler: Siehe Konsole im Browser für Details" "error_unhelpfulScriptError": "Skriptfehler: Siehe Konsole im Browser für Details",
"documentID": "Kennung des Dokuments",
"errorPopupBlocked": "Für die Funktionsweise von CryptPad ist es erforderlich, dass neue Tabs geöffnet werden können. Bitte erlaube Pop-up-Fenster in der Adressleiste deines Browsers. Diese Fenster werden niemals dafür verwendet, dir Werbung anzuzeigen.",
"unableToDisplay": "Das Dokument kann nicht angezeigt werden. Drücke ESC, um die Seite neu zu laden. Wenn das Problem weiterhin besteht, kontaktiere bitte den Support.",
"mediatag_notReady": "Bitte schließe den Download ab",
"settings_mediatagSizeTitle": "Limit für automatisches Herunterladen",
"admin_archiveHint": "Ein Dokument unzugänglich machen, ohne es endgültig zu löschen. Es wird in einem Verzeichnis \"archive\" abgelegt und nach einigen Tagen gelöscht (konfigurierbar in der Konfigurationsdatei des Servers).",
"mediatag_loadButton": "Anhang laden",
"settings_mediatagSizeHint": "Maximale Größe in Megabytes (MB) für das automatische Laden von Medienelementen (Bilder, Videos, PDFs), die in Dokumenten eingebettet sind. Größere Elemente können manuell geladen werden. Gib \"-1\" ein, um Medienelemente immer automatisch zu laden.",
"mediatag_saveButton": "Speichern",
"admin_archiveInput2": "Passwort für das Dokument",
"admin_archiveInput": "URL des Dokuments",
"admin_archiveButton": "Archivieren",
"Offline": "Offline",
"download_zip": "ZIP-Datei erstellen...",
"fileTableHeader": "Downloads und Uploads",
"allowNotifications": "Benachrichtigungen erlauben",
"admin_unarchiveHint": "Ein zuvor archiviertes Dokument wiederherstellen",
"pad_mediatagOpen": "Datei öffnen",
"pad_mediatagShare": "Datei teilen",
"download_zip_file": "Datei {0}/{1}",
"archivedFromServer": "Dokument archiviert",
"restoredFromServer": "Dokument wiederhergestellt",
"admin_archiveInval": "Ungültiges Dokument",
"admin_unarchiveButton": "Wiederherstellen",
"admin_unarchiveTitle": "Dokumente wiederherstellen",
"admin_archiveTitle": "Dokumente archivieren",
"history_trimPrompt": "Dieses Dokument hat einen Verlauf von {0} angesammelt, was das Laden verlangsamen kann. Ziehe in Betracht, den Verlauf zu löschen, sofern er nicht benötigt wird.",
"contacts_confirmCancel": "Bist du sicher, dass du die Kontaktanfrage an <b>{0}</b> zurücknehmen möchtest?"
} }

@ -1459,5 +1459,14 @@
"history_cantRestore": "Palauttaminen epäonnistui. Yhteytesi on katkennut.", "history_cantRestore": "Palauttaminen epäonnistui. Yhteytesi on katkennut.",
"history_close": "Sulje", "history_close": "Sulje",
"history_restore": "Palauta", "history_restore": "Palauta",
"share_bar": "Luo linkki" "share_bar": "Luo linkki",
"error_unhelpfulScriptError": "Skriptivirhe: Lisätietoja selaimen kehittäjäkonsolissa",
"tag_edit": "Muokkaa",
"tag_add": "Lisää",
"loading_state_5": "Uudelleenrakenna asiakirja",
"loading_state_4": "Lataa Teams",
"loading_state_3": "Lataa jaetut kansiot",
"loading_state_2": "Päivitä sisältö",
"loading_state_1": "Lataa Drive",
"loading_state_0": "Rakenna käyttöliittymä"
} }

@ -41,7 +41,7 @@
"saved": "Enregistré", "saved": "Enregistré",
"synced": "Tout est enregistré", "synced": "Tout est enregistré",
"deleted": "Supprimé", "deleted": "Supprimé",
"deletedFromServer": "Pad supprimé du serveur", "deletedFromServer": "Document supprimé",
"mustLogin": "Vous devez être enregistré pour avoir accès à cette page", "mustLogin": "Vous devez être enregistré pour avoir accès à cette page",
"disabledApp": "Cette application a été désactivée. Pour plus d'information, veuillez contacter l'administrateur de ce CryptPad.", "disabledApp": "Cette application a été désactivée. Pour plus d'information, veuillez contacter l'administrateur de ce CryptPad.",
"realtime_unrecoverableError": "Une erreur critique est survenue. Cliquez sur OK pour recharger la page.", "realtime_unrecoverableError": "Une erreur critique est survenue. Cliquez sur OK pour recharger la page.",
@ -554,7 +554,7 @@
"upload_notEnoughSpace": "Il n'y a pas assez d'espace libre dans votre CryptDrive pour ce fichier.", "upload_notEnoughSpace": "Il n'y a pas assez d'espace libre dans votre CryptDrive pour ce fichier.",
"upload_notEnoughSpaceBrief": "Pas assez d'espace", "upload_notEnoughSpaceBrief": "Pas assez d'espace",
"upload_tooLarge": "Ce fichier dépasse la taille maximale autorisée pour votre compte.", "upload_tooLarge": "Ce fichier dépasse la taille maximale autorisée pour votre compte.",
"upload_tooLargeBrief": "Fichier trop volumineux", "upload_tooLargeBrief": "Le fichier dépasse la limite de {0} Mo",
"upload_choose": "Choisir un fichier", "upload_choose": "Choisir un fichier",
"upload_pending": "En attente", "upload_pending": "En attente",
"upload_cancelled": "Annulé", "upload_cancelled": "Annulé",
@ -1468,5 +1468,33 @@
"loading_state_0": "Construction de l'interface", "loading_state_0": "Construction de l'interface",
"tag_edit": "Modifier", "tag_edit": "Modifier",
"tag_add": "Ajouter", "tag_add": "Ajouter",
"error_unhelpfulScriptError": "Erreur de script : consultez la console du navigateur pour plus de détails" "error_unhelpfulScriptError": "Erreur de script : consultez la console du navigateur pour plus de détails",
"documentID": "Référence du document",
"unableToDisplay": "Impossible d'afficher le document. Veuillez recharger la page avec la touche Échap. Si le problème persiste, veuillez contacter le support.",
"errorPopupBlocked": "CryptPad doit pouvoir ouvrir de nouveaux onglets pour fonctionner. Veuillez autoriser les fenêtres pop-up dans la barre d'adresse de votre navigateur. Ces fenêtres ne seront jamais utilisées pour vous montrer de la publicité.",
"settings_mediatagSizeHint": "Taille maximale en mégaoctets (Mo) pour le chargement automatique des pièces jointes (images, vidéos, pdf) intégrés dans les documents. Les pièces jointes dont la taille est supérieure à la taille spécifiée peuvent être chargés manuellement. Utilisez \"-1\" pour toujours charger automatiquement les pièces jointes.",
"settings_mediatagSizeTitle": "Limite de téléchargement automatique",
"mediatag_notReady": "Merci de compléter le téléchargement",
"pad_mediatagOpen": "Ouvrir ce fichier",
"pad_mediatagShare": "Partager ce fichier",
"mediatag_saveButton": "Sauvegarder",
"Offline": "Déconnecté",
"download_zip_file": "Fichier {0}/{1}",
"download_zip": "Construction du fichier ZIP...",
"fileTableHeader": "Téléchargements et imports",
"allowNotifications": "Autoriser les notifications",
"archivedFromServer": "Document archivé",
"restoredFromServer": "Document restauré",
"admin_archiveInval": "Document invalide",
"admin_archiveInput2": "Mot de passe du document",
"admin_archiveInput": "URL du document",
"admin_unarchiveButton": "Restaurer",
"admin_unarchiveHint": "Restaurer un document qui avait été précédemment archivé",
"admin_unarchiveTitle": "Restaurer les documents",
"admin_archiveButton": "Archiver",
"admin_archiveHint": "Rendre un document indisponible sans le supprimer définitivement. Il sera placé dans un répertoire \"archive\" et supprimé après quelques jours (configurable dans le fichier de configuration du serveur).",
"admin_archiveTitle": "Archiver les documents",
"mediatag_loadButton": "Charger la pièce jointe",
"history_trimPrompt": "Ce document a accumulé {0} d'historique qui peut ralentir le temps de chargement. Envisagez de supprimer l'historique s'il n'est pas nécessaire.",
"contacts_confirmCancel": "Êtes-vous sûr de vouloir annuler votre demande de contact avec <b>{0}</b> ?"
} }

@ -249,7 +249,7 @@
"settings_autostoreMaybe": "手動 (確認しない)", "settings_autostoreMaybe": "手動 (確認しない)",
"settings_autostoreNo": "手動 (常に確認する)", "settings_autostoreNo": "手動 (常に確認する)",
"settings_autostoreHint": "<b>自動</b> あなたがアクセスしたすべてのパッドを、あなたの CryptDrive に保存します。<br><b>手動 (常に確認する)</b> まだ保存していないパッドにアクセスした場合に、あなたの CryptDrive に保存するかどうか尋ねます。<br><b>手動 (確認しない)</b> アクセス先のパッドがあなたの CryptDrive に自動的に保存されなくなります。保存オプションは表示されなくなります。", "settings_autostoreHint": "<b>自動</b> あなたがアクセスしたすべてのパッドを、あなたの CryptDrive に保存します。<br><b>手動 (常に確認する)</b> まだ保存していないパッドにアクセスした場合に、あなたの CryptDrive に保存するかどうか尋ねます。<br><b>手動 (確認しない)</b> アクセス先のパッドがあなたの CryptDrive に自動的に保存されなくなります。保存オプションは表示されなくなります。",
"settings_userFeedback": "ユーザーフィードバックを有効", "settings_userFeedback": "ユーザーフィードバックを有効にする",
"settings_userFeedbackHint2": "あなたのパッドのコンテンツがサーバーと共有されることはありません。", "settings_userFeedbackHint2": "あなたのパッドのコンテンツがサーバーと共有されることはありません。",
"settings_userFeedbackHint1": "CryptPad は、あなたの経験を向上させる方法を知るために、サーバーにいくつかの非常に基本的なフィードバックを提供します。 ", "settings_userFeedbackHint1": "CryptPad は、あなたの経験を向上させる方法を知るために、サーバーにいくつかの非常に基本的なフィードバックを提供します。 ",
"settings_userFeedbackTitle": "フィードバック", "settings_userFeedbackTitle": "フィードバック",
@ -356,5 +356,72 @@
"crowdfunding_button": "CryptPad を支援", "crowdfunding_button": "CryptPad を支援",
"crowdfunding_home1": "CryptPad はあなたの支援を必要としています!", "crowdfunding_home1": "CryptPad はあなたの支援を必要としています!",
"policy_choices_vpn": "私たちがホストするインスタンスを使用したいが、IP アドレスを私たちに公開したくない場合は、<a href=\"https://www.torproject.org/projects/torbrowser.html.en\" title=\"downloads from the Tor project\" target=\"_blank\" rel=\"noopener noreferrer\">Tor Browser</a> または <a href=\"https://riseup.net/en/vpn\" title=\"VPNs provided by Riseup\" target=\"_blank\" rel=\"noopener noreferrer\">VPN</a> を使用してあなたの IP アドレスを保護できます。", "policy_choices_vpn": "私たちがホストするインスタンスを使用したいが、IP アドレスを私たちに公開したくない場合は、<a href=\"https://www.torproject.org/projects/torbrowser.html.en\" title=\"downloads from the Tor project\" target=\"_blank\" rel=\"noopener noreferrer\">Tor Browser</a> または <a href=\"https://riseup.net/en/vpn\" title=\"VPNs provided by Riseup\" target=\"_blank\" rel=\"noopener noreferrer\">VPN</a> を使用してあなたの IP アドレスを保護できます。",
"contacts_removeHistoryTitle": "チャット履歴を削除" "contacts_removeHistoryTitle": "チャット履歴を削除",
"properties_passwordSuccessFile": "パスワードは正常に変更されました。",
"drive_sfPasswordError": "誤ったパスワードです",
"team_title": "チーム: {0}",
"password_error": "パッドが存在しません!<br>このエラーは「誤ったパスワードが入力された」場合、または「パッドがサーバーから削除された」場合に発生します。",
"password_error_seed": "パッドが存在しません!<br>このエラーは「パスワードが追加・変更された」場合、または「パッドがサーバーから削除された」場合に発生します。",
"password_submit": "送信",
"password_placeholder": "パスワードを入力...",
"password_info": "開こうとしているパッドが存在しないか、パスワードで保護されています。コンテンツにアクセスするには、正しいパスワードを入力してください。",
"properties_confirmNew": "パスワードを追加すると、このパッドの URL が変更され、履歴が削除されます。パスワードを知らないユーザーは、このパッドへアクセスできなくなります。続行してよろしいですか?",
"properties_changePassword": "パスワードの変更",
"password_show": "表示",
"properties_addPassword": "パスワードの追加",
"history_close": "閉じる",
"history_restore": "復元",
"fm_emptyTrashOwned": "ごみ箱に、あなたが所有しているドキュメントが入っています。あなたのドライブからのみ<b>削除</b>するか、すべてのユーザーから<b>完全削除</b>するかを選択できます。",
"access_destroyPad": "このドキュメントまたはフォルダを完全に削除する",
"accessButton": "アクセス",
"access_allow": "リスト",
"makeACopy": "コピーを作成",
"trimHistory_noHistory": "削除可能な履歴がありません",
"areYouSure": "クリックして実行",
"settings_safeLinksCheckbox": "安全なリンクを有効にする",
"settings_safeLinksTitle": "安全なリンク",
"settings_safeLinksHint": "CryptPad では、あなたのパッドを解読するための鍵がリンクに含まれています。これは、あなたのブラウザの閲覧履歴にアクセスできる人が、潜在的にあなたの CryptPad のデータを解読・閲覧できることを意味します。この「ブラウザの閲覧履歴にアクセスできる人」には、デバイス間で履歴を同期させる侵入的なブラウザ拡張機能やブラウザが含まれます。「安全なリンク」を有効にすると、鍵がブラウザの閲覧履歴に残ったり、アドレスバーに表示されたりするのを可能な限り防ぐことができます。この機能を有効にした上で共有メニューを使用することを強く推奨します。",
"settings_autostoreTitle": "CryptDrive へのパッドの保存",
"settings_logoutEverywhereConfirm": "すべてのデバイスでログインが取り消されるため、今後利用する際にもう一度ログインするよう求められます。続行しますか?",
"settings_logoutEverywhere": "他のすべてのウェブセッションからログアウトします",
"settings_logoutEverywhereTitle": "リモートセッションを閉じる",
"loading_state_5": "ドキュメントを再構築",
"loading_state_4": "チームを読み込み",
"loading_state_3": "共有フォルダを読み込み",
"loading_state_2": "コンテンツを更新",
"loading_state_1": "ドライブを読み込み",
"loading_state_0": "インターフェースを構築",
"error_unhelpfulScriptError": "スクリプトエラー: ブラウザコンソールで詳細をご確認ください",
"tag_edit": "編集",
"tag_add": "追加",
"team_exportButton": "ダウンロード",
"admin_cat_quota": "ユーザーストレージ",
"admin_invalKey": "無効な公開鍵です",
"admin_limitPlan": "プラン: {0}",
"admin_registrationAllow": "開く",
"admin_registrationButton": "閉じる",
"oo_version": "バージョン: ",
"snapshots_delete": "削除",
"snapshots_close": "閉じる",
"snapshots_restore": "復元",
"snapshots_open": "開く",
"snapshots_placeholder": "スナップショット名",
"snapshots_new": "新規スナップショット",
"snaphot_title": "スナップショット",
"snapshots_button": "スナップショット",
"filePicker_description": "埋め込むファイルを CryptDrive から選択するか、新規にアップロードしてください",
"uploadButtonTitle": "CryptDrive に新規ファイルをアップロード",
"uploadFolderButton": "フォルダをアップロード",
"uploadButton": "ファイルをアップロード",
"filePicker_filter": "ファイル名で検索",
"toolbar_theme": "テーマ",
"themeButton": "テーマ",
"team_cat_back": "チームに戻る",
"team_cat_list": "チーム",
"allow_checkbox": "アクセスリストを有効にする",
"allow_label": "アクセスリスト: {0}",
"share_contactCategory": "連絡先",
"share_linkCategory": "リンク",
"share_linkEdit": "編集",
"previewButtonTitle": "マークダウンのプレビューを表示または非表示にします"
} }

@ -43,7 +43,7 @@
"saved": "Saved", "saved": "Saved",
"synced": "Everything is saved", "synced": "Everything is saved",
"deleted": "Deleted", "deleted": "Deleted",
"deletedFromServer": "Pad deleted from the server", "deletedFromServer": "Document destroyed",
"mustLogin": "You must be logged in to access this page", "mustLogin": "You must be logged in to access this page",
"disabledApp": "This application has been disabled. Contact the administrator of this CryptPad for more information.", "disabledApp": "This application has been disabled. Contact the administrator of this CryptPad for more information.",
"realtime_unrecoverableError": "An unrecoverable error has occured. Click OK to reload.", "realtime_unrecoverableError": "An unrecoverable error has occured. Click OK to reload.",
@ -571,7 +571,7 @@
"upload_notEnoughSpace": "There is not enough space for this file in your CryptDrive.", "upload_notEnoughSpace": "There is not enough space for this file in your CryptDrive.",
"upload_notEnoughSpaceBrief": "Not enough space", "upload_notEnoughSpaceBrief": "Not enough space",
"upload_tooLarge": "This file exceeds the maximum upload size allowed for your account.", "upload_tooLarge": "This file exceeds the maximum upload size allowed for your account.",
"upload_tooLargeBrief": "File too large", "upload_tooLargeBrief": "File exceeds the {0}MB limit",
"upload_choose": "Choose a file", "upload_choose": "Choose a file",
"upload_pending": "Pending", "upload_pending": "Pending",
"upload_cancelled": "Cancelled", "upload_cancelled": "Cancelled",
@ -1468,5 +1468,33 @@
"loading_state_5": "Reconstruct document", "loading_state_5": "Reconstruct document",
"tag_add": "Add", "tag_add": "Add",
"tag_edit": "Edit", "tag_edit": "Edit",
"error_unhelpfulScriptError": "Script Error: See browser console for details" "error_unhelpfulScriptError": "Script Error: See browser console for details",
"documentID": "Document identifier",
"unableToDisplay": "Unable to display the document. Please press Esc to reload the page. If the problem persists, please contact support.",
"errorPopupBlocked": "CryptPad needs to be able to open new tabs to operate. Please allow popup windows in your browser's address bar. These windows will never be used to show you advertising.",
"admin_archiveTitle": "Archive documents",
"admin_archiveHint": "Make a document unavailable without deleting it permanently. It will be placed in an 'archive' directory and deleted after a few days (configurable in the server configuration file).",
"admin_archiveButton": "Archive",
"admin_unarchiveTitle": "Restore documents",
"admin_unarchiveHint": "Restore a document that had previously been archived",
"admin_unarchiveButton": "Restore",
"admin_archiveInput": "Document URL",
"admin_archiveInput2": "Document password",
"admin_archiveInval": "Invalid document",
"restoredFromServer": "Document restored",
"archivedFromServer": "Document archived",
"allowNotifications": "Allow notifications",
"fileTableHeader": "Downloads and uploads",
"download_zip": "Building ZIP file...",
"download_zip_file": "File {0}/{1}",
"Offline": "Offline",
"mediatag_saveButton": "Save",
"pad_mediatagShare": "Share file",
"pad_mediatagOpen": "Open file",
"mediatag_notReady": "Please complete the download",
"settings_mediatagSizeTitle": "Automatic download limit",
"settings_mediatagSizeHint": "Maximum size in megabytes (MB) for automatically loading media elements (images, videos, pdf) embedded into documents. Elements bigger than the specified size can be loaded manually. Use \"-1\" to always load the media elements automatically.",
"mediatag_loadButton": "Load attachment",
"history_trimPrompt": "This document has accumulated {0} of history that may slow down loading time. Consider deleting the history if it is not needed.",
"contacts_confirmCancel": "Are you sure you want to cancel your contact request with <b>{0}</b>?"
} }

@ -28,6 +28,8 @@ define([
// Loaded in load #2 // Loaded in load #2
nThen(function (waitFor) { nThen(function (waitFor) {
$(waitFor()); $(waitFor());
}).nThen(function (waitFor) {
SFCommonO.initIframe(waitFor);
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
var req = { var req = {
cfg: requireConfig, cfg: requireConfig,

@ -82,6 +82,8 @@ define([
var readOnly = !secret.keys.editKeyStr; var readOnly = !secret.keys.editKeyStr;
if (!manager || !manager.folders[fId]) { return; } if (!manager || !manager.folders[fId]) { return; }
manager.folders[fId].userObject.setReadOnly(readOnly, secret.keys.secondaryKey); manager.folders[fId].userObject.setReadOnly(readOnly, secret.keys.secondaryKey);
manager.folders[fId].offline = newObj.offline;
})); }));
}); });
// Remove from memory folders that have been deleted from the drive remotely // Remove from memory folders that have been deleted from the drive remotely
@ -310,6 +312,10 @@ define([
onReconnect(); onReconnect();
}); });
common.onLogout(function () { setEditable(false); }); common.onLogout(function () { setEditable(false); });
// Check if our drive history needs to be trimmed
common.checkTrimHistory(null, true);
}); });
}; };
main(); main();

@ -54,8 +54,14 @@ define([
if (Utils.LocalStore.isLoggedIn()) { return; } if (Utils.LocalStore.isLoggedIn()) { return; }
Utils.LocalStore.setFSHash(''); Utils.LocalStore.setFSHash('');
Utils.LocalStore.clearThumbnail(); Utils.LocalStore.clearThumbnail();
try {
Utils.Cache.clear(function () {
window.location.reload(); window.location.reload();
}); });
} catch (e) {
window.location.reload();
}
});
sframeChan.on('Q_DRIVE_USEROBJECT', function (data, cb) { sframeChan.on('Q_DRIVE_USEROBJECT', function (data, cb) {
Cryptpad.userObjectCommand(data, cb); Cryptpad.userObjectCommand(data, cb);
}); });

@ -1,5 +1,6 @@
@import (reference) '../../customize/src/less2/include/tokenfield.less'; @import (reference) '../../customize/src/less2/include/tokenfield.less';
@import (reference) '../../customize/src/less2/include/framework.less'; @import (reference) '../../customize/src/less2/include/framework.less';
@import (reference) '../../customize/src/less2/include/markdown.less';
&.cp-app-file { &.cp-app-file {
@ -45,6 +46,7 @@
z-index: -1; z-index: -1;
} }
.mediatag_cryptpad();
media-tag { media-tag {
img { img {
max-width: 100%; max-width: 100%;
@ -62,7 +64,49 @@
} }
} }
#cp-app-file-upload-form, #cp-app-file-download-form { #cp-app-file-download-form {
padding: 0px;
margin: 0px;
position: relative;
display: block;
max-width: 90vw;
height: 150px;
width: ~"min(90vw, 600px)";
.cp-app-file-progress-container {
margin-top: 5px;
height: 40px;
font-size: 20px;
border: 1px solid @colortheme_logo-2;
background: white;
color: @cryptpad_text_col;
display: flex;
justify-content: space-between;
position: relative;
.cp-app-file-progress-dl {
border-right: 1px solid @cryptpad_text_col;
}
.cp-app-file-progress-dl, .cp-app-file-progress-dc {
width: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.cp-app-file-progress {
z-index: 1;
position: absolute;
top: 0;
left: 0;
bottom: 0;
background: @colortheme_logo-2;
}
}
.cp-app-file-progress-txt {
margin-left: 30px;
}
}
#cp-app-file-upload-form {
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
@ -154,6 +198,9 @@
max-height: 100%; max-height: 100%;
max-width: 100%; max-width: 100%;
} }
&:empty {
display: none !important;
}
} }
} }

@ -48,69 +48,6 @@ define([
return new Blob(chunks); return new Blob(chunks);
}; };
var concatBuffer = function (a, b) { // TODO make this not so ugly
return new Uint8Array(slice(a).concat(slice(b)));
};
var fetchMetadata = function (src, cb) {
var done = false;
var CB = function (err, res) {
if (done) { return; }
done = true;
cb(err, res);
};
var xhr = new XMLHttpRequest();
xhr.open("GET", src, true);
xhr.setRequestHeader('Range', 'bytes=0-1');
xhr.responseType = 'arraybuffer';
xhr.onerror= function () { return CB('XHR_ERROR'); };
xhr.onload = function () {
if (/^4/.test('' + this.status)) { return CB('XHR_ERROR'); }
var res = new Uint8Array(xhr.response);
var size = decodePrefix(res);
var xhr2 = new XMLHttpRequest();
xhr2.open("GET", src, true);
xhr2.setRequestHeader('Range', 'bytes=2-' + (size + 2));
xhr2.responseType = 'arraybuffer';
xhr2.onload = function () {
if (/^4/.test('' + this.status)) { return CB('XHR_ERROR'); }
var res2 = new Uint8Array(xhr2.response);
var all = concatBuffer(res, res2);
CB(void 0, all);
};
xhr2.send(null);
};
xhr.send(null);
};
var decryptMetadata = function (u8, key) {
var prefix = u8.subarray(0, 2);
var metadataLength = decodePrefix(prefix);
var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength));
var metaChunk = Nacl.secretbox.open(metaBox, createNonce(), key);
try {
return JSON.parse(Nacl.util.encodeUTF8(metaChunk));
}
catch (e) { return null; }
};
var fetchDecryptedMetadata = function (src, key, cb) {
if (typeof(src) !== 'string') {
return window.setTimeout(function () {
cb('NO_SOURCE');
});
}
fetchMetadata(src, function (e, buffer) {
if (e) { return cb(e); }
cb(void 0, decryptMetadata(buffer, key));
});
};
var decrypt = function (u8, key, done, progress) { var decrypt = function (u8, key, done, progress) {
var MAX = u8.length; var MAX = u8.length;
var _progress = function (offset) { var _progress = function (offset) {
@ -128,6 +65,11 @@ define([
metadata: undefined, metadata: undefined,
}; };
var cancelled = false;
var cancel = function () {
cancelled = true;
};
var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength)); var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength));
var metaChunk = Nacl.secretbox.open(metaBox, nonce, key); var metaChunk = Nacl.secretbox.open(metaBox, nonce, key);
@ -168,6 +110,7 @@ define([
var chunks = []; var chunks = [];
var again = function () { var again = function () {
if (cancelled) { return; }
takeChunk(function (e, plaintext) { takeChunk(function (e, plaintext) {
if (e) { if (e) {
return setTimeout(function () { return setTimeout(function () {
@ -188,6 +131,10 @@ define([
}; };
again(); again();
return {
cancel: cancel
};
}; };
// metadata // metadata
@ -258,8 +205,5 @@ define([
encrypt: encrypt, encrypt: encrypt,
joinChunks: joinChunks, joinChunks: joinChunks,
computeEncryptedSize: computeEncryptedSize, computeEncryptedSize: computeEncryptedSize,
decryptMetadata: decryptMetadata,
fetchMetadata: fetchMetadata,
fetchDecryptedMetadata: fetchDecryptedMetadata,
}; };
}); });

@ -16,11 +16,6 @@
<label for="cp-app-file-upfile" class="btn btn-primary cp-app-file-block unselectable" data-localization-title="upload_choose" <label for="cp-app-file-upfile" class="btn btn-primary cp-app-file-block unselectable" data-localization-title="upload_choose"
data-localization="upload_choose"></label> data-localization="upload_choose"></label>
</div> </div>
<div id="cp-app-file-download-form" style="display: none;">
<input type="button" name="dl" id="cp-app-file-dlfile" class="cp-app-file-input" />
<label for="cp-app-file-dlfile" class="btn btn-success cp-app-file-block unselectable" data-localization-title="download_button"><span data-localization="download_button"></span></label>
<span class="cp-app-file-block" id="cp-app-file-dlprogress"></span>
</div>
<div id="cp-app-file-download-view" style="display: none;"> <div id="cp-app-file-download-view" style="display: none;">
<media-tag id="cp-app-file-view"></media-tag> <media-tag id="cp-app-file-view"></media-tag>
</div> </div>

@ -8,6 +8,7 @@ define([
'/common/common-util.js', '/common/common-util.js',
'/common/common-hash.js', '/common/common-hash.js',
'/common/common-interface.js', '/common/common-interface.js',
'/common/hyperscript.js',
'/customize/messages.js', '/customize/messages.js',
'/file/file-crypto.js', '/file/file-crypto.js',
@ -29,6 +30,7 @@ define([
Util, Util,
Hash, Hash,
UI, UI,
h,
Messages, Messages,
FileCrypto, FileCrypto,
MediaTag) MediaTag)
@ -37,18 +39,12 @@ define([
var Nacl = window.nacl; var Nacl = window.nacl;
var APP = window.APP = {}; var APP = window.APP = {};
MediaTag.setDefaultConfig('download', {
text: Messages.download_mt_button
});
var andThen = function (common) { var andThen = function (common) {
var $appContainer = $('#cp-app-file-content'); var $appContainer = $('#cp-app-file-content');
var $form = $('#cp-app-file-upload-form'); var $form = $('#cp-app-file-upload-form');
var $dlform = $('#cp-app-file-download-form');
var $dlview = $('#cp-app-file-download-view'); var $dlview = $('#cp-app-file-download-view');
var $label = $form.find('label'); var $label = $form.find('label');
var $dllabel = $dlform.find('label span');
var $progress = $('#cp-app-file-dlprogress');
var $bar = $('.cp-toolbar-container'); var $bar = $('.cp-toolbar-container');
var $body = $('body'); var $body = $('body');
@ -88,21 +84,45 @@ define([
var toolbar = APP.toolbar = Toolbar.create(configTb); var toolbar = APP.toolbar = Toolbar.create(configTb);
if (!uploadMode) { if (!uploadMode) {
(function () {
var hexFileName = secret.channel; var hexFileName = secret.channel;
var src = fileHost + Hash.getBlobPathFromHex(hexFileName); var src = fileHost + Hash.getBlobPathFromHex(hexFileName);
var key = secret.keys && secret.keys.cryptKey; var key = secret.keys && secret.keys.cryptKey;
var cryptKey = Nacl.util.encodeBase64(key); var cryptKey = Nacl.util.encodeBase64(key);
FileCrypto.fetchDecryptedMetadata(src, key, function (e, metadata) { var $mt = $dlview.find('media-tag');
if (e) { $mt.attr('src', src);
if (e === 'XHR_ERROR') { $mt.attr('data-crypto-key', 'cryptpad:'+cryptKey);
return void UI.errorLoadingScreen(Messages.download_resourceNotAvailable, false, function () { $mt.css('transform', 'scale(2)');
common.gotoURL('/file/');
}); var rightsideDisplayed = false;
} var metadataReceived = false;
return void console.error(e); UI.removeLoadingScreen();
$dlview.show();
MediaTag($mt[0]).on('complete', function (decrypted) {
$mt.css('transform', '');
if (!rightsideDisplayed) {
toolbar.$drawer
.append(common.createButton('export', true, {}, function () {
saveAs(decrypted.content, decrypted.metadata.name);
}));
rightsideDisplayed = true;
} }
// make pdfs big
var toolbarHeight = $('#cp-toolbar').height();
$('media-tag iframe').css({
'height': 'calc(100vh - ' + toolbarHeight + 'px)',
'width': '100vw',
'position': 'absolute',
'bottom': 0,
'left': 0,
'border': 0
});
}).on('metadata', function (metadata) {
if (metadataReceived) { return; }
metadataReceived = true;
// Add pad attributes when the file is saved in the drive // Add pad attributes when the file is saved in the drive
Title.onTitleChange(function () { Title.onTitleChange(function () {
var owners = metadata.owners; var owners = metadata.owners;
@ -137,93 +157,13 @@ define([
toolbar.$drawer.append(common.createButton('hashtag', true)); toolbar.$drawer.append(common.createButton('hashtag', true));
} }
toolbar.$file.show(); toolbar.$file.show();
var displayFile = function (ev, sizeMb, CB) {
var called_back;
var cb = function (e) {
if (called_back) { return; }
called_back = true;
if (CB) { CB(e); }
};
var $mt = $dlview.find('media-tag');
$mt.attr('src', src);
$mt.attr('data-crypto-key', 'cryptpad:'+cryptKey);
var rightsideDisplayed = false;
MediaTag($mt[0]).on('complete', function (decrypted) {
$dlview.show();
$dlform.hide();
var $dlButton = $dlview.find('media-tag button');
if (ev) { $dlButton.click(); }
if (!rightsideDisplayed) {
toolbar.$drawer
.append(common.createButton('export', true, {}, function () {
saveAs(decrypted.content, decrypted.metadata.name);
}));
rightsideDisplayed = true;
}
// make pdfs big
var toolbarHeight = $('#cp-toolbar').height();
var $another_iframe = $('media-tag iframe').css({
'height': 'calc(100vh - ' + toolbarHeight + 'px)',
'width': '100vw',
'position': 'absolute',
'bottom': 0,
'left': 0,
'border': 0
});
if ($another_iframe.length) {
$another_iframe.load(function () {
cb();
});
} else {
cb();
}
}).on('progress', function (data) {
var p = data.progress +'%';
$progress.width(p);
}).on('error', function (err) { }).on('error', function (err) {
console.error(err);
});
};
var todoBigFile = function (sizeMb) {
$dlform.show();
UI.removeLoadingScreen();
$dllabel.append($('<br>'));
$dllabel.append(Util.fixHTML(metadata.name));
// don't display the size if you don't know it.
if (typeof(sizeM) === 'number') {
$dllabel.append($('<br>'));
$dllabel.append(Messages._getKey('formattedMB', [sizeMb]));
}
var decrypting = false;
var onClick = function (ev) {
if (decrypting) { return; }
decrypting = true;
displayFile(ev, sizeMb, function (err) {
$appContainer.css('background-color', $appContainer.css('background-color',
common.getAppConfig().appBackgroundColor); common.getAppConfig().appBackgroundColor);
if (err) { UI.alert(err); } UI.warn(Messages.error);
}); console.error(err);
};
if (typeof(sizeMb) === 'number' && sizeMb < 5) { return void onClick(); }
$dlform.find('#cp-app-file-dlfile, #cp-app-file-dlprogress').click(onClick);
};
common.getFileSize(hexFileName, function (e, data) {
if (e) {
return void UI.errorLoadingScreen(e);
}
var size = Util.bytesToMegabytes(data);
return void todoBigFile(size);
});
}); });
})();
return; return;
} }

@ -9,9 +9,11 @@
</head> </head>
<body class="cp-app-oodoc"> <body class="cp-app-oodoc">
<div id="cp-toolbar" class="cp-toolbar-container"></div> <div id="cp-toolbar" class="cp-toolbar-container"></div>
<div id="cp-app-oo-container">
<div id="cp-app-oo-editor"> <div id="cp-app-oo-editor">
<div id="cp-app-oo-placeholder"></div> <div id="cp-app-oo-placeholder-a"></div>
<script type="text/javascript" src="/common/onlyoffice/web-apps/apps/api/documents/api.js"></script> <!--<script type="text/javascript" src="/common/onlyoffice/web-apps/apps/api/documents/api.js"></script>-->
</div>
</div> </div>
</body> </body>

@ -9,9 +9,11 @@
</head> </head>
<body class="cp-app-ooslide"> <body class="cp-app-ooslide">
<div id="cp-toolbar" class="cp-toolbar-container"></div> <div id="cp-toolbar" class="cp-toolbar-container"></div>
<div id="cp-app-oo-container">
<div id="cp-app-oo-editor"> <div id="cp-app-oo-editor">
<div id="cp-app-oo-placeholder"></div> <div id="cp-app-oo-placeholder-a"></div>
<script type="text/javascript" src="/common/onlyoffice/web-apps/apps/api/documents/api.js"></script> <!--<script type="text/javascript" src="/common/onlyoffice/web-apps/apps/api/documents/api.js"></script>-->
</div>
</div> </div>
</body> </body>

@ -46,6 +46,7 @@ define([
'/common/test.js', '/common/test.js',
'/bower_components/diff-dom/diffDOM.js', '/bower_components/diff-dom/diffDOM.js',
'/bower_components/file-saver/FileSaver.min.js',
'css!/customize/src/print.css', 'css!/customize/src/print.css',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
@ -462,7 +463,9 @@ define([
setTimeout(function() { // Just in case setTimeout(function() { // Just in case
var tags = dom.querySelectorAll('media-tag:empty'); var tags = dom.querySelectorAll('media-tag:empty');
Array.prototype.slice.call(tags).forEach(function(el) { Array.prototype.slice.call(tags).forEach(function(el) {
MediaTag(el); var mediaObject = MediaTag(el, {
body: dom
});
$(el).on('keydown', function(e) { $(el).on('keydown', function(e) {
if ([8, 46].indexOf(e.which) !== -1) { if ([8, 46].indexOf(e.which) !== -1) {
$(el).remove(); $(el).remove();
@ -472,13 +475,17 @@ define([
var observer = new MutationObserver(function(mutations) { var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) { mutations.forEach(function(mutation) {
if (mutation.type === 'childList') { if (mutation.type === 'childList') {
var list_values = [].slice.call(el.children); var list_values = slice(el.children)
.map(function (el) { return el.outerHTML; })
.join('');
mediaTagMap[el.getAttribute('src')] = list_values; mediaTagMap[el.getAttribute('src')] = list_values;
if (mediaObject.complete) { observer.disconnect(); }
} }
}); });
}); });
observer.observe(el, { observer.observe(el, {
attributes: false, attributes: false,
subtree: true,
childList: true, childList: true,
characterData: false characterData: false
}); });
@ -491,9 +498,10 @@ define([
Array.prototype.slice.call(tags).forEach(function(tag) { Array.prototype.slice.call(tags).forEach(function(tag) {
var src = tag.getAttribute('src'); var src = tag.getAttribute('src');
if (mediaTagMap[src]) { if (mediaTagMap[src]) {
mediaTagMap[src].forEach(function(n) { tag.innerHTML = mediaTagMap[src];
tag.appendChild(n.cloneNode()); /*mediaTagMap[src].forEach(function(n) {
}); tag.appendChild(n.cloneNode(true));
});*/
} }
}); });
}; };
@ -1084,6 +1092,9 @@ define([
border: Messages.pad_mediatagBorder, border: Messages.pad_mediatagBorder,
preview: Messages.pad_mediatagPreview, preview: Messages.pad_mediatagPreview,
'import': Messages.pad_mediatagImport, 'import': Messages.pad_mediatagImport,
download: Messages.download_mt_button,
share: Messages.pad_mediatagShare,
open: Messages.pad_mediatagOpen,
options: Messages.pad_mediatagOptions options: Messages.pad_mediatagOptions
}; };
Ckeditor._commentsTranslations = { Ckeditor._commentsTranslations = {
@ -1164,6 +1175,28 @@ define([
editor.plugins.mediatag.import = function($mt) { editor.plugins.mediatag.import = function($mt) {
framework._.sfCommon.importMediaTag($mt); framework._.sfCommon.importMediaTag($mt);
}; };
editor.plugins.mediatag.download = function($mt) {
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);
};
editor.plugins.mediatag.open = function($mt) {
var hash = framework._.sfCommon.getHashFromMediaTag($mt);
framework._.sfCommon.openURL(Hash.hashToHref(hash, 'file'));
};
editor.plugins.mediatag.share = function($mt) {
var data = {
file: true,
pathname: '/file/',
hashes: {
fileHash: framework._.sfCommon.getHashFromMediaTag($mt)
},
title: Util.find($mt[0], ['_mediaObject', 'name']) || ''
};
framework._.sfCommon.getSframeChannel().event('EV_SHARE_OPEN', data);
};
Links.init(Ckeditor, editor); Links.init(Ckeditor, editor);
}).nThen(function() { }).nThen(function() {
// Move ckeditor parts to have a structure like the other apps // Move ckeditor parts to have a structure like the other apps

@ -53,15 +53,57 @@
editor.plugins.mediatag.import($mt); editor.plugins.mediatag.import($mt);
} }
}); });
editor.addCommand('downloadMT', {
exec: function (editor) {
var w = targetWidget;
targetWidget = undefined;
var $mt = $(w.$).find('media-tag');
editor.plugins.mediatag.download($mt);
}
});
editor.addCommand('openMT', {
exec: function (editor) {
var w = targetWidget;
targetWidget = undefined;
var $mt = $(w.$).find('media-tag');
editor.plugins.mediatag.open($mt);
}
});
editor.addCommand('shareMT', {
exec: function (editor) {
var w = targetWidget;
targetWidget = undefined;
var $mt = $(w.$).find('media-tag');
editor.plugins.mediatag.share($mt);
}
});
if (editor.addMenuItems) { if (editor.addMenuItems) {
editor.addMenuGroup('mediatag'); editor.addMenuGroup('mediatag');
editor.addMenuItem('open', {
label: Messages.open,
icon: 'iframe',
command: 'openMT',
group: 'mediatag'
});
editor.addMenuItem('share', {
label: Messages.share,
icon: 'link',
command: 'shareMT',
group: 'mediatag'
});
editor.addMenuItem('importMediatag', { editor.addMenuItem('importMediatag', {
label: Messages.import, label: Messages.import,
icon: 'save', icon: 'save',
command: 'importMediatag', command: 'importMediatag',
group: 'mediatag' group: 'mediatag'
}); });
editor.addMenuItem('download', {
label: Messages.download,
icon: 'save',
command: 'downloadMT',
group: 'mediatag'
});
editor.addMenuItem('mediatag', { editor.addMenuItem('mediatag', {
label: Messages.options, label: Messages.options,
icon: 'image', icon: 'image',
@ -76,6 +118,9 @@
targetWidget = element; targetWidget = element;
return { return {
mediatag: CKEDITOR.TRISTATE_OFF, mediatag: CKEDITOR.TRISTATE_OFF,
open: CKEDITOR.TRISTATE_OFF,
share: CKEDITOR.TRISTATE_OFF,
download: CKEDITOR.TRISTATE_OFF,
importMediatag: CKEDITOR.TRISTATE_OFF, importMediatag: CKEDITOR.TRISTATE_OFF,
}; };
} }

@ -1087,6 +1087,8 @@ define([
common.openPadChat(function () {}); common.openPadChat(function () {});
UI.removeLoadingScreen(); UI.removeLoadingScreen();
common.checkTrimHistory();
}; };
var onError = function (info) { var onError = function (info) {

@ -101,13 +101,13 @@ define([
var url = APP.origin + '/profile/#' + hash; var url = APP.origin + '/profile/#' + hash;
$('<button>', { $('<button>', {
'class': 'btn btn-success '+VIEW_PROFILE_BUTTON, 'class': 'btn '+VIEW_PROFILE_BUTTON,
}).text(Messages.profile_viewMyProfile).click(function () { }).text(Messages.profile_viewMyProfile).click(function () {
window.open(url, '_blank'); window.open(url, '_blank');
}).appendTo($container); }).appendTo($container);
$('<button>', { $('<button>', {
'class': 'btn btn-success '+VIEW_PROFILE_BUTTON, 'class': 'btn btn-primary '+VIEW_PROFILE_BUTTON,
}).append(h('i.fa.fa-shhare-alt')) }).append(h('i.fa.fa-shhare-alt'))
.append(h('span', Messages.shareButton)) .append(h('span', Messages.shareButton))
.click(function () { .click(function () {
@ -136,7 +136,7 @@ define([
APP.$linkEdit = $(); APP.$linkEdit = $();
if (APP.readOnly) { return; } if (APP.readOnly) { return; }
var button = h('button.btn.btn-primary', { var button = h('button.btn', {
title: Messages.clickToEdit title: Messages.clickToEdit
}, Messages.profile_addLink); }, Messages.profile_addLink);
APP.$linkEdit = $(button); APP.$linkEdit = $(button);
@ -243,10 +243,34 @@ define([
return; return;
} }
var addCancel = function () {
var cancelButton = h('button.btn.btn-danger.cp-app-profile-friend-request', [
h('i.fa.fa-user-times'),
Messages.cancel
]);
$(cancelButton).click(function () {
// Unfriend confirm
var content = h('div', [
UI.setHTML(h('p'), Messages._getKey('contacts_confirmCancel', [name]))
]);
UI.confirm(content, function (yes) {
if (!yes) { return; }
module.execCommand('CANCEL_FRIEND', {
curvePublic: data.curvePublic,
notifications: data.notifications
}, function (e) {
refreshFriendRequest(data);
if (e) { UI.warn(Messages.error); return void console.error(e); }
});
});
}).appendTo(APP.$friend);
};
// Pending friend (we've sent a friend request) // Pending friend (we've sent a friend request)
var pendingFriends = APP.common.getPendingFriends(); // Friend requests sent var pendingFriends = APP.common.getPendingFriends(); // Friend requests sent
if (pendingFriends[data.curvePublic]) { if (pendingFriends[data.curvePublic]) {
$button.attr('disabled', 'disabled').append(Messages.profile_friendRequestSent); $button.attr('disabled', 'disabled').append(Messages.profile_friendRequestSent);
addCancel();
return; return;
} }
// This is not a friend yet: we can send a friend request // This is not a friend yet: we can send a friend request
@ -255,8 +279,9 @@ define([
APP.common.sendFriendRequest({ APP.common.sendFriendRequest({
curvePublic: data.curvePublic, curvePublic: data.curvePublic,
notifications: data.notifications notifications: data.notifications
}, function () { }, function (err, obj) {
$button.attr('disabled', 'disabled').append(Messages.profile_friendRequestSent); if (obj && obj.error) { return void UI.warn(Messages.error); }
//$button.attr('disabled', 'disabled').append(Messages.profile_friendRequestSent);
}); });
}); });
}; };
@ -468,7 +493,7 @@ define([
var $div = $(h('div.cp-sidebarlayout-element')).appendTo($container); var $div = $(h('div.cp-sidebarlayout-element')).appendTo($container);
APP.$edPublic = $('<button>', { APP.$edPublic = $('<button>', {
'class': 'btn btn-success', 'class': 'btn',
}).append(h('i.fa.fa-key')) }).append(h('i.fa.fa-key'))
.append(h('span', Messages.profile_copyKey)) .append(h('span', Messages.profile_copyKey))
.click(function () { .click(function () {

@ -50,7 +50,7 @@ define([
cb(null, secret); cb(null, secret);
}); });
}; };
var addData = function (meta, Cryptad, user) { var addData = function (meta, Cryptpad, user) {
meta.isOwnProfile = !window.location.hash || meta.isOwnProfile = !window.location.hash ||
window.location.hash.slice(1) === user.profile; window.location.hash.slice(1) === user.profile;
}; };

@ -52,10 +52,10 @@ define([
: Share.getShareModal; : Share.getShareModal;
f(common, { f(common, {
origin: priv.origin, origin: priv.origin,
pathname: priv.pathname, pathname: data.pathname || priv.pathname,
password: priv.password, password: data.hashes ? '' : priv.password,
isTemplate: priv.isTemplate, isTemplate: data.hashes ? false : priv.isTemplate,
hashes: priv.hashes, hashes: data.hashes || priv.hashes,
common: common, common: common,
title: data.title, title: data.title,
versionHash: data.versionHash, versionHash: data.versionHash,
@ -64,8 +64,8 @@ define([
hideIframe(); hideIframe();
}, },
fileData: { fileData: {
hash: priv.hashes.fileHash, hash: (data.hashes && data.hashes.fileHash) || priv.hashes.fileHash,
password: priv.password password: data.hashes ? '' : priv.password
} }
}, function (e, modal) { }, function (e, modal) {
if (e) { console.error(e); } if (e) { console.error(e); }

@ -33,7 +33,7 @@ define([
// loading screen setup. // loading screen setup.
var done = waitFor(); var done = waitFor();
var onMsg = function (msg) { var onMsg = function (msg) {
var data = JSON.parse(msg.data); var data = typeof(msg.data) === "object" ? msg.data : JSON.parse(msg.data);
if (data.q !== 'READY') { return; } if (data.q !== 'READY') { return; }
window.removeEventListener('message', onMsg); window.removeEventListener('message', onMsg);
var _done = done; var _done = done;

@ -70,6 +70,10 @@
margin-right: 100%; margin-right: 100%;
} }
} }
& > .fa {
align-self: center;
margin-right: -16px;
}
} }
.cp-settings-info-block { .cp-settings-info-block {
[type="text"] { [type="text"] {

@ -51,7 +51,7 @@ define([
'cp-settings-info-block', 'cp-settings-info-block',
'cp-settings-displayname', 'cp-settings-displayname',
'cp-settings-language-selector', 'cp-settings-language-selector',
'cp-settings-resettips', 'cp-settings-mediatag-size',
'cp-settings-change-password', 'cp-settings-change-password',
'cp-settings-delete' 'cp-settings-delete'
], ],
@ -62,6 +62,7 @@ define([
'cp-settings-userfeedback', 'cp-settings-userfeedback',
], ],
'drive': [ 'drive': [
'cp-settings-resettips',
'cp-settings-drive-duplicate', 'cp-settings-drive-duplicate',
'cp-settings-thumbnails', 'cp-settings-thumbnails',
'cp-settings-drive-backup', 'cp-settings-drive-backup',
@ -576,6 +577,58 @@ define([
cb(form); cb(form);
}, true); }, true);
makeBlock('mediatag-size', function(cb) {
var $inputBlock = $('<div>', {
'class': 'cp-sidebarlayout-input-block',
});
var spinner;
var $input = $('<input>', {
'min': -1,
'max': 1000,
type: 'number',
}).appendTo($inputBlock);
var oldVal;
var todo = function () {
var val = parseInt($input.val());
if (typeof(val) !== 'number' || isNaN(val)) { return UI.warn(Messages.error); }
if (val === oldVal) { return; }
spinner.spin();
common.setAttribute(['general', 'mediatag-size'], val, function (err) {
if (err) {
spinner.hide();
console.error(err);
return UI.warn(Messages.error);
}
oldVal = val;
spinner.done();
UI.log(Messages.saved);
});
};
var $save = $(h('button.btn.btn-primary', Messages.settings_save)).appendTo($inputBlock);
spinner = UI.makeSpinner($inputBlock);
$save.click(todo);
$input.on('keyup', function(e) {
if (e.which === 13) { todo(); }
});
common.getAttribute(['general', 'mediatag-size'], function(e, val) {
if (e) { return void console.error(e); }
if (typeof(val) !== 'number' || isNaN(val)) {
oldVal = 5;
$input.val(5);
} else {
oldVal = val;
$input.val(val);
}
});
cb($inputBlock);
}, true);
// Security // Security
makeBlock('safe-links', function(cb) { makeBlock('safe-links', function(cb) {
@ -777,7 +830,7 @@ define([
Feedback.send('FULL_DRIVE_EXPORT_COMPLETE'); Feedback.send('FULL_DRIVE_EXPORT_COMPLETE');
saveAs(blob, filename); saveAs(blob, filename);
}, errors); }, errors);
}, ui.update); }, ui.update, common.getCache());
ui.onCancel(function() { ui.onCancel(function() {
ui.close(); ui.close();
bu.stop(); bu.stop();
@ -903,7 +956,7 @@ define([
cb(content); cb(content);
}; };
makeBlock('trim-history', function(cb, $div) { makeBlock('trim-history', function(cb, $div) {
if (!common.isLoggedIn()) { return; } if (!common.isLoggedIn()) { return void cb(false); }
redrawTrimHistory(cb, $div); redrawTrimHistory(cb, $div);
}, true); }, true);

@ -230,7 +230,7 @@ define([
var form = h('div.cp-support-form-container', content); var form = h('div.cp-support-form-container', content);
$(cancel).click(function () { $(cancel).click(function () {
$(form).closest('.cp-support-list-ticket').find('.cp-support-list-actions').show(); $(form).closest('.cp-support-list-ticket').find('.cp-support-list-actions').css('display', '');
$(form).remove(); $(form).remove();
}); });
@ -257,8 +257,9 @@ define([
var url; var url;
if (ctx.isAdmin) { if (ctx.isAdmin) {
ticketCategory = Messages['support_cat_'+(content.category || 'all')] + ' - '; ticketCategory = Messages['support_cat_'+(content.category || 'all')] + ' - ';
url = h('button.btn.btn-primary.fa.fa-clipboard'); url = h('button.btn.fa.fa-clipboard');
$(url).click(function () { $(url).click(function (e) {
e.stopPropagation();
var link = privateData.origin + privateData.pathname + '#' + 'support-' + content.id; var link = privateData.origin + privateData.pathname + '#' + 'support-' + content.id;
var success = Clipboard.copy(link); var success = Clipboard.copy(link);
if (success) { UI.log(Messages.shareSuccess); } if (success) { UI.log(Messages.shareSuccess); }
@ -269,7 +270,10 @@ define([
'data-cat': content.category, 'data-cat': content.category,
'data-id': content.id 'data-id': content.id
}, [ }, [
h('h2', [ticketCategory, ticketTitle, url]), h('h2', [
h('span', [ticketCategory, ticketTitle]),
h('span.cp-support-title-buttons',url)
]),
actions actions
])); ]));
@ -303,7 +307,7 @@ define([
var form = makeForm(ctx, function () { var form = makeForm(ctx, function () {
var sent = sendForm(ctx, content.id, form, content.sender); var sent = sendForm(ctx, content.id, form, content.sender);
if (sent) { if (sent) {
$(actions).show(); $(actions).css('display', '');
$(form).remove(); $(form).remove();
} }
}, content.title); }, content.title);
@ -397,6 +401,7 @@ define([
var fmConfig = { var fmConfig = {
body: $('body'), body: $('body'),
noStore: true, // Don't store attachments into our drive
onUploaded: function (ev, data) { onUploaded: function (ev, data) {
if (ev.callback) { if (ev.callback) {
ev.callback(data); ev.callback(data);

@ -46,7 +46,9 @@ define([
Backup, Backup,
Messages) Messages)
{ {
var APP = {}; var APP = {
teams: {}
};
var driveAPP = {}; var driveAPP = {};
var saveAs = window.saveAs; var saveAs = window.saveAs;
//var SHARED_FOLDER_NAME = Messages.fm_sharedFolderName; //var SHARED_FOLDER_NAME = Messages.fm_sharedFolderName;
@ -91,6 +93,8 @@ define([
var readOnly = !secret.keys.editKeyStr; var readOnly = !secret.keys.editKeyStr;
if (!manager || !manager.folders[fId]) { return; } if (!manager || !manager.folders[fId]) { return; }
manager.folders[fId].userObject.setReadOnly(readOnly, secret.keys.secondaryKey); manager.folders[fId].userObject.setReadOnly(readOnly, secret.keys.secondaryKey);
manager.folders[fId].offline = newObj.offline;
})); }));
}); });
// Remove from memory folders that have been deleted from the drive remotely // Remove from memory folders that have been deleted from the drive remotely
@ -211,6 +215,11 @@ define([
if (obj && obj.error) { if (obj && obj.error) {
return void UI.warn(Messages.error); return void UI.warn(Messages.error);
} }
// Refresh offline state
APP.teams[APP.team] = APP.teams[APP.team] || {};
APP.teams[APP.team].offline = obj.offline;
common.displayAvatar($avatar, obj.avatar, obj.name); common.displayAvatar($avatar, obj.avatar, obj.name);
$category.append($avatar); $category.append($avatar);
$avatar.append(h('span.cp-sidebarlayout-category-name', obj.name)); $avatar.append(h('span.cp-sidebarlayout-category-name', obj.name));
@ -333,6 +342,11 @@ define([
}); });
APP.drive = drive; APP.drive = drive;
driveAPP.refresh = drive.refresh; driveAPP.refresh = drive.refresh;
if (APP.teams[id] && APP.teams[id].offline) {
setEditable(false);
drive.refresh();
}
}); });
}; };
@ -406,7 +420,15 @@ define([
content.push(h('h3', Messages.team_listTitle + ' ' + slots)); content.push(h('h3', Messages.team_listTitle + ' ' + slots));
APP.teams = {};
keys.forEach(function (id) { keys.forEach(function (id) {
if (!obj[id].empty) {
APP.teams[id] = {
offline: obj[id] && obj[id].offline
};
}
var team = obj[id]; var team = obj[id];
if (team.empty) { if (team.empty) {
list.push(h('div.cp-team-list-team.empty', [ list.push(h('div.cp-team-list-team.empty', [
@ -454,7 +476,7 @@ define([
var getWarningBox = function () { var getWarningBox = function () {
return h('div.alert.alert-warning', { return h('div.alert.alert-warning', {
role:'alert' role:'alert'
}, isOwner ? Messages.team_maxOwner : Messages._getKey('team_maxTeams', [MAX_TEAMS_SLOTS])); }, Messages._getKey('team_maxTeams', [MAX_TEAMS_SLOTS]));
}; };
if (Object.keys(privateData.teams || {}).length >= Constants.MAX_TEAMS_SLOTS || isOwner) { if (Object.keys(privateData.teams || {}).length >= Constants.MAX_TEAMS_SLOTS || isOwner) {
@ -1049,7 +1071,7 @@ define([
Feedback.send('FULL_TEAMDRIVE_EXPORT_COMPLETE'); Feedback.send('FULL_TEAMDRIVE_EXPORT_COMPLETE');
saveAs(blob, filename); saveAs(blob, filename);
}, errors); }, errors);
}, ui.update); }, ui.update, common.getCache);
ui.onCancel(function() { ui.onCancel(function() {
ui.close(); ui.close();
bu.stop(); bu.stop();
@ -1433,13 +1455,15 @@ define([
} }
}); });
var onDisconnect = function (noAlert) { var onDisconnect = function (teamId) {
if (APP.team && teamId && APP.team !== teamId) { return; }
setEditable(false); setEditable(false);
if (APP.team && driveAPP.refresh) { driveAPP.refresh(); } if (APP.team && driveAPP.refresh) { driveAPP.refresh(); }
toolbar.failed(); toolbar.failed();
if (!noAlert) { UIElements.disconnectAlert(); } UIElements.disconnectAlert();
}; };
var onReconnect = function () { var onReconnect = function (teamId) {
if (APP.team && teamId && APP.team !== teamId) { return; }
setEditable(true); setEditable(true);
if (APP.team && driveAPP.refresh) { driveAPP.refresh(); } if (APP.team && driveAPP.refresh) { driveAPP.refresh(); }
toolbar.reconnecting(); toolbar.reconnecting();
@ -1449,11 +1473,17 @@ define([
sframeChan.on('EV_DRIVE_LOG', function (msg) { sframeChan.on('EV_DRIVE_LOG', function (msg) {
UI.log(msg); UI.log(msg);
}); });
sframeChan.on('EV_NETWORK_DISCONNECT', function () { sframeChan.on('EV_NETWORK_DISCONNECT', function (teamId) {
onDisconnect(); onDisconnect(teamId);
if (teamId && APP.teams[teamId]) {
APP.teams[teamId].offline = true;
}
}); });
sframeChan.on('EV_NETWORK_RECONNECT', function () { sframeChan.on('EV_NETWORK_RECONNECT', function (teamId) {
onReconnect(); onReconnect(teamId);
if (teamId && APP.teams[teamId]) {
APP.teams[teamId].offline = false;
}
}); });
common.onLogout(function () { setEditable(false); }); common.onLogout(function () { setEditable(false); });
}); });

@ -55,10 +55,10 @@ define([
sframeChan.event('EV_'+obj.data.ev, obj.data.data); sframeChan.event('EV_'+obj.data.ev, obj.data.data);
} }
if (obj.data.ev === 'NETWORK_RECONNECT') { if (obj.data.ev === 'NETWORK_RECONNECT') {
sframeChan.event('EV_NETWORK_RECONNECT'); sframeChan.event('EV_NETWORK_RECONNECT', obj.data.data);
} }
if (obj.data.ev === 'NETWORK_DISCONNECT') { if (obj.data.ev === 'NETWORK_DISCONNECT') {
sframeChan.event('EV_NETWORK_DISCONNECT'); sframeChan.event('EV_NETWORK_DISCONNECT', obj.data.data);
} }
}); });

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

Loading…
Cancel
Save