Merge branch 'staging' into ooFix

pull/1/head
yflory 4 years ago
commit 363757676b

@ -15,6 +15,7 @@ www/common/onlyoffice/v2*
server.js
www/common/old-media-tag.js
www/scratch
www/lib
www/common/toolbar.js
www/common/hyperscript.js

@ -1,3 +1,62 @@
# UplandMoa (3.20.0)
## Goals
We've held off on deploying any major features while we work towards deploying some documentation we've been busy organizing. This release features a wide range of minor features intended to address a number of github issues and frequent causes of support tickets.
## Update notes
This release features a modification to the recommended Content Security Policy headers as demonstrated in `./cryptpad/docs/example.nginx.conf`. CryptPad will work without making this change, however, we highly recommend updating your instance's nginx.conf as it will mitigate a variety of potential security vulnerabilities.
Otherwise, we've introduced a new client-side dependency (_Mathjax_) and changed some server-side code that will require a server restart.
To update from 3.19.1 to 3.20.0:
1. Apply the recommended changes to your `nginx.conf`
2. Stop your server
3. Get the latest platform code with git
4. Install client-side dependencies with `bower update`
5. Reload nginx to apply the updated CSP headers
6. Restart the CryptPad API server
## Features
* As noted above, this release features a change to the Content Security Policy headers which define the types of code that can be loaded in a given context. More specifically, we've addressed a number of CKEditor's quirks which required us to set a more lax security policy for the rich text editor. With these changes in place the only remaining exceptions to our general policy are applied for the sake of our OnlyOffice integration, though we hope to address its quirks soon as well.
* On the topic of the rich text editor, we also moved the _print_ action from the CKEditor toolbar to the _File_ menu to be more consistent with our other apps.
* The Kanban board that we use to organize our own team has become rather large and complex due to a wealth of long-term ideas and a large number of tags. We started to notice some performance issues as a result, and have begun looking into some optimizations to improve its scalability. As a start, we avoid applying changes whenever the Kanban's tab is not visible.
* We finally decided to file off one of the platform's rough edges which had been confusing curious users for some time. Every registered user is identified by a randomly-generated cryptographic key (the _Public Signing Key_ found on your settings page). These identifiers are used to allocate additional storage space via our premium accounts, and we occasionally require them for other support issues like deleting accounts or debugging server issues. Unfortunately, because we occasionally receive emails asking for help with _other administrators instances_ these keys were formatted along with the host domain in the form of a URL. As such, it was very tempting to open them in the browser even though there was no functionality corresponding to the URL. We've updated all the code that parses these keys and introduced a new format which is clearly _not a URL_, so hopefully we'll get fewer messages asking us why they _don't work_.
* We've made a number of small improvements to the common functionality in our code and slide editors:
* We've merged and built upon a pull request which implemented two new extensions to our markdown renderer for _Mathjax_ and _Markmap_. This introduces support for embedding formatted equations and markdown-based mind maps. Since these depend on new client-side code which would otherwise increase page loading time we've also implemented support for lazily loading extensions on demand, so you'll only load the extra code if the current document requires it.
* The _slide_ editor now throttles slide redraws so that updates are only applied after 400ms of inactivity rather than on every character update.
* We've made a number of small style tweaks for blockquotes, tables, and embedded media in rendered markdown.
* Lastly, we've made a large number of improvements to user and team drives:
* Search results now include shared folders with matching names and have been made _sortable_ like the rest of the drive.
* Inserting media in a document via the _Insert_ menu now updates its access time, which causes it to show up in the _Recent pads_ category of your drive.
* Shared folders now support access lists. To apply an access list to a shared folder that you own you may right-click the shared folder in your drive, choose _Access_, then click the _List_ tab of the resulting dialog. Enabling its access list will restrict access to its owners and any other contacts that you or other owners add to its list. Note, this access applies to the folder itself (who can view it or add to its directory), its access list will not be applied recursively to all the elements contained within which might be contained in other shared folders or other users drives.
* In the interest of removing jargon from the platform we've started to change text from "Delete from the server" to "Destroy". We plan to make more changes like this on an ongoing basis as we notice them.
* We've made a significant change to the way that _owned files_ are treated in the user and team drives. Previously, files that you owned were implicitly deleted from the server whenever you removed them from your drive. This seemed sensible when we first introduced the concept of ownership, however, now that a variety of assets can have multiple owners it is clearly less appropriate. Rather than require users to first remove themselves as a co-owner before removing an asset from their drive in order to allow other owners to continue accessing it we now offer two distinct _Remove_ and _Destroy_ actions. _Remove_ will simply take it out of your drive so that it will no longer count against your storage limit, while _Destroy_ will cause it to stop existing _for everyone_. To clarify the two actions we've associated them with a _trash bin_ and _paper shredder_ icon, respectively.
## Bug fixes
* Remote changes in the Kanban app removed pending text in new cards, effectively making it impossible (and very frustrating) to create new cards while anyone else was editing existing content or submitting their own new cards.
* Dropping an image directly into a spreadsheet no longer puts the UI into an unrecoverable state, though we still don't support image drop. To insert images, use the "Insert" menu. This was actually fixed in our 3.19.1 release, but it wasn't documented in the release notes.
* When a user attempted to open an automatically expiring document which had passed its expiration date they were shown a general message indicating that the document had been deleted even when they had sufficient information to know that it had been marked for expiration. We now display a message indicating the more likely cause of its deletion.
* We've spent some time working on the usability of comments in our rich text app:
* When a user started adding a first comment to a document then canceled their action it was possible for the document to get stuck in an odd layout. This extra space allocated towards comments now correctly collapses as intended when there are no comments, pending or otherwise.
* The comments UI is now completely disabled whenever the document is in read-only mode, whether due to disconnection or insufficient permissions.
* The _comment_ button in the app toolbar now toggles on and off to indicate the eligibility of the current selection as a new comment.
* We've fixed a number of issues with teams:
* Users no longer send themselves a notification when they remove themself as an owner of a pad from within the _Teams_ UI.
* The _worker_ process which is responsible for managing account rights now correctly upgrades and downgrades its internal state when its role within a team is changed by a remote user instead of requiring a complete worker reload.
* The worker does not delete credentials to access a team when it finds that its id is not in the team's roster, since this could be triggered accidentally by some unrelated server bugs that responded incorrectly to a request for the team roster's history.
* We've fixed a number of issues in our code and slide editors:
* The "Language" dropdown selectors in the "Theme" menu used to show "Language (Markdown)" when the page was first loaded, however, changing the setting to another language would drop the annotation and instead show only "Markdown". Now the annotation is preserved as intended.
* A recent update to our stylesheets introduced a regression in the buttons of our "print options" dialog.
* While polishing up the PRs which introduced the _Mathjax_ and _Markmap_ support we noticed that the client-side cache which is used to prevent unnecessary redraws of embedded media was causing only one instance of an element to be rendered when the same source was embedded in multiple sections of a document.
* The "File export" dialog featured a similar regression in the style of its buttons which has been addressed.
* We fixed a minor bug in our 3.19.0 release in which unregistered users (who do not have a "mailbox") tried to send a notification to themselves.
* We've added an additional check to the process for changing your account password in which we make sure that we are not overwriting another account with the same username and password.
# Thylacine's revenge (3.19.1)
Our upcoming 3.20.0 release is planned for July 7th, 2020, but we are once again releasing a minor version featuring some nice bug fixes and usability improvements which are ready to be deployed now. In case you missed [our announcement](https://social.weho.st/@cryptpad/104360490068671089) we are phasing out our usage of the `master` and basing our releases on the `main` branch. For best results we recommend explicitly checking out code by its tag.

@ -49,7 +49,8 @@
"saferphore": "^0.0.1",
"jszip": "Stuk/jszip#^3.1.5",
"requirejs-plugins": "^1.0.3",
"dragula.js": "3.7.2"
"dragula.js": "3.7.2",
"MathJax": "3.0.5"
},
"resolutions": {
"bootstrap": "^v4.0.0",

@ -28,4 +28,5 @@
<glyph unicode="&#xe912;" glyph-name="folder-upload" d="M829.44 727.251h-296.84l-47.104 62.886c-25.923 34.066-66.406 55.898-111.999 56.139h-178.938c-77.457-0.137-140.211-62.891-140.348-140.335v-515.868c0.137-77.457 62.891-140.211 140.335-140.348h634.893c77.457 0.137 140.211 62.891 140.348 140.335v396.482c0 0.036 0 0.078 0 0.121 0 77.561-62.807 140.452-140.335 140.589h-0.013zM911.119 190.072c-0.068-45.083-36.597-81.611-81.673-81.679h-634.887c-45.083 0.068-81.611 36.597-81.679 81.673v515.862c0.068 45.083 36.597 81.611 81.673 81.679h178.906c26.48-0.030 50.004-12.656 64.908-32.207l0.146-0.199 47.104-62.765 17.709-24.094h326.114c45.125-0.069 81.679-36.665 81.679-81.799 0 0 0 0 0 0v0zM562.838 166.883h-102.039v203.957h-72.523l123.723 214.076 123.723-214.076h-72.885v-203.957z" />
<glyph unicode="&#xe913;" glyph-name="add-bottom" d="M108.793 271.501c-15.312 0-27.996 12.684-27.996 27.996v55.992c0 15.312 12.684 28.006 27.996 28.006h225.414v-111.993zM108.793 495.478c-15.312 0-27.996 12.694-27.996 28.006v55.992c0 15.312 12.684 27.996 27.996 27.996h403.491v-111.993zM108.793 719.465c-15.312 0-27.996 12.694-27.996 28.006v55.992c0 15.312 12.684 27.996 27.996 27.996h615.968c15.312 0 27.996-12.684 27.996-27.996v-55.992c0-15.312-12.684-28.006-27.996-28.006zM943.202 287.839c0-19.465-15.792-35.257-35.258-35.257h-152.782v-152.782c0-19.465-15.792-35.257-35.258-35.257h-70.515c-19.465 0-35.257 15.792-35.257 35.257v152.782h-152.782c-19.465 0-35.257 15.792-35.257 35.257v70.515c0 19.465 15.792 35.258 35.257 35.258h152.782v152.782c0 19.465 15.792 35.258 35.257 35.258h70.515c19.465 0 35.258-15.792 35.258-35.258v-152.782h152.782c19.465 0 35.258-15.792 35.258-35.258z" />
<glyph unicode="&#xe914;" glyph-name="add-top" d="M108.793 624.499c-15.312 0-27.996-12.684-27.996-27.996v-55.992c0-15.312 12.684-28.006 27.996-28.006h225.414v111.993zM108.793 400.522c-15.312 0-27.996-12.694-27.996-28.006v-55.992c0-15.312 12.684-27.996 27.996-27.996h403.491v111.993zM108.793 176.535c-15.312 0-27.996-12.694-27.996-28.006v-55.992c0-15.312 12.684-27.996 27.996-27.996h615.968c15.312 0 27.996 12.684 27.996 27.996v55.992c0 15.312-12.684 28.006-27.996 28.006zM943.202 608.161c0 19.465-15.792 35.257-35.258 35.257h-152.782v152.782c0 19.465-15.792 35.257-35.258 35.257h-70.515c-19.465 0-35.257-15.792-35.257-35.257v-152.782h-152.782c-19.465 0-35.257-15.792-35.257-35.257v-70.515c0-19.465 15.792-35.258 35.257-35.258h152.782v-152.782c0-19.465 15.792-35.258 35.257-35.258h70.515c19.465 0 35.258 15.792 35.258 35.258v152.782h152.782c19.465 0 35.258 15.792 35.258 35.258z" />
<glyph unicode="&#xe915;" glyph-name="destroy" d="M193.049 938.213c-25.264 0-45.745-20.492-45.745-45.757v-443.97h-67.453c-26.649-0.003-48.252-21.606-48.255-48.255 0.023-26.635 21.62-48.216 48.255-48.219h157.56l-80.102-86.481 86.481-94.352-86.396-94.23 109.353-119.164 39.535 49.965-63.439 69.247 86.347 94.181-86.262 94.085 80.344 86.748h171.688l-80.102-86.481 86.481-94.352-86.347-94.23 109.305-119.164 39.535 49.965-63.439 69.247 86.347 94.181-86.262 94.085 80.344 86.748h171.688l-80.102-86.481 86.529-94.352-86.396-94.23 109.341-119.164 39.499 49.965-63.451 69.199 86.396 94.23-86.299 94.133 80.344 86.699h105.763c26.64-0.004 48.244 21.579 48.267 48.219-0.003 26.654-21.613 48.259-48.267 48.255h-64.663v230.418c0 25.264-14.779 60.536-32.417 78.174l-148.718 148.719c-17.637 17.637-52.909 32.417-78.173 32.417zM208.317 877.2h366.078v-198.296c0-25.264 20.505-45.769 45.769-45.769h198.296v-184.65h-610.143zM635.409 873.392c8.104-2.86 16.213-7.154 19.549-10.49l149.204-149.204c3.337-3.336 7.63-11.446 10.49-19.549h-179.243z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 25 KiB

@ -1,9 +1,9 @@
@font-face {
font-family: 'cptools';
src:
url('fonts/cptools.ttf?da4x1y') format('truetype'),
url('fonts/cptools.woff?da4x1y') format('woff'),
url('fonts/cptools.svg?da4x1y#cptools') format('svg');
url('fonts/cptools.ttf?5ntnhs') format('truetype'),
url('fonts/cptools.woff?5ntnhs') format('woff'),
url('fonts/cptools.svg?5ntnhs#cptools') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
@ -25,6 +25,9 @@
-moz-osx-font-smoothing: grayscale;
}
.cptools-destroy:before {
content: "\e915";
}
.cptools-add-bottom:before {
content: "\e913";
}

@ -62,7 +62,7 @@ define([
var imprintUrl = AppConfig.imprint && (typeof(AppConfig.imprint) === "boolean" ?
'/imprint.html' : AppConfig.imprint);
Pages.versionString = "CryptPad v3.19.1 (Thylacine's revenge)";
Pages.versionString = "CryptPad v3.20.0 (UplandMoa)";
// used for the about menu
Pages.imprintLink = AppConfig.imprint ? footLink(imprintUrl, 'imprint') : undefined;

@ -227,16 +227,7 @@
}
}
::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: @cryptpad_color_grey;
opacity: 1; /* Firefox */
}
:-ms-input-placeholder { /* Internet Explorer 10-11 */
color: @cryptpad_color_grey;
}
::-ms-input-placeholder { /* Microsoft Edge */
color: @cryptpad_color_grey;
}
.tools_placeholder-color(@cryptpad_color_grey);
span.cp-password-container {
display: flex;

@ -159,6 +159,10 @@
user-select: none;
}
.cp-app-drive-element-restricted {
color: #939393;
}
.cp-app-drive-element-droppable {
background-color: @drive_droppable-bg;
color: #222;
@ -218,7 +222,7 @@
#cp-app-drive-search {
display: flex;
display: inline-flex;
align-items: center;
max-width: 400px;
font-size: 30px;
@ -226,7 +230,7 @@
input {
background: transparent;
color: @colortheme_drive-color;
.tools_placeholder-color(@colortheme_drive-color);
.tools_placeholder-color(@cryptpad_color_grey);
outline-width: 0px;
border-radius: 0;
width: 100%;
@ -252,7 +256,19 @@
color: @colortheme_drive-color;
}
}
.cp-app-drive-search-spinner {
display: inline-flex;
color: @colortheme_drive-color;
font-size: 40px;
align-items: center;
justify-content: center;
}
.cp-app-drive-search-noresult {
font-size: 30px;
padding: 15px;
font-style: italic;
color: @cryptpad_color_grey;
}
/* TREE */
@ -313,6 +329,7 @@
padding: 0 10px;
border: 0;
color: lighten(@colortheme_sidebar-left-fg, 40%);
height: auto;
}
& > span.cp-app-drive-element-row {
overflow: hidden;
@ -528,7 +545,7 @@
&.cp-app-drive-tags-list {
width: 100%;
table {
margin: 10px 50px;
margin: 10px;
width: ~"calc(100% - 100px)";
table-layout: fixed;
td, th {
@ -548,6 +565,21 @@
.cp-app-drive-element {
.cp-app-drive-element-truncated { display: none; }
}
.cp-app-drive-new-ghost {
cursor: pointer;
opacity: 0.5;
padding: 0;
align-items: center;
justify-content: center;
display: inline-flex;
&:hover {
opacity: 0.7;
}
.fa, .cptools {
cursor: pointer;
}
}
div.cp-app-drive-content-grid {
padding: 1em;
ul {
@ -571,6 +603,7 @@
border-radius: 0;
border: 1px solid #ddd;
font-size: 14px;
height: auto;
}
.cp-app-drive-element-state {
position: absolute;
@ -594,27 +627,17 @@
}
}
}
.cp-app-drive-element-list {
display: none;
}
.cp-app-drive-new-ghost {
cursor: pointer;
opacity: 0.5;
padding: 0;
flex-flow: column;
align-items: center;
justify-content: center;
display: inline-flex;
&:hover {
opacity: 0.7;
}
.fa, .cptools {
cursor: pointer;
font-size: 90px;
margin-top: 5px;
margin-bottom: 0;
}
}
.cp-app-drive-element-list {
display: none;
}
}
.cp-app-drive-content-list {
@ -622,6 +645,10 @@
display: none;
}
// Make it act as a table!
.cp-app-drive-new-ghost {
padding: 0 5px;
margin-top: 20px;
}
padding-left: 10px;
ul {
width: 100%;
@ -634,6 +661,7 @@
padding: 0 4px;
flex: 1;
min-width: 0;
height: auto;
}
&> span {
@ -780,6 +808,7 @@
flex-flow: row-reverse;
flex-grow: 1;
justify-content: flex-end;
padding-left: 10px;
.cp-app-drive-path-element {
.tools_unselectable();
display: inline-block;

@ -99,9 +99,12 @@
cursor: pointer;
border-radius: 0;
.fa {
.fa, .cptools {
margin-right: 0.2em;
}
.cptools {
vertical-align: middle;
}
color: @alertify-btn-fg;
border: 1px solid @alertify-btn-fg;

@ -1,23 +1,32 @@
@import (reference) "./tools.less";
.markdown_main() {
@nice-grey: #f3f3f3;
@accent-grey: rgba(0, 0, 0, 0.2);
hr {
border-top: 1px solid @accent-grey;
}
blockquote {
background: #e5e5e5;
background: rgba(128,128,128,0.5);
background: @nice-grey;
background: rgba(144, 144, 144, 0.2);
padding: 10px;
border-left: 3px solid #999;
border-left: 2px solid @accent-grey;
padding-right: 0;
p { margin: 0; }
blockquote { margin: 0; }
& > *:not(:last-child) {
margin-bottom: 10px !important;
}
}
// todo ul, ol
// TOC
div.cp-md-toc {
background: #f3f3f3;
background: @nice-grey;
padding: 20px;
//float: right;
* {
margin: 5px;
margin-right: 0;
}
max-width: 100%;
min-width: 200px;
white-space: nowrap;
@ -93,7 +102,13 @@
border: 1px solid #BBB;
}
pre.mermaid {
pre.markmap {
border: 1px solid #ddd;
svg {
height: 400px;
}
}
pre[data-plugin] {
svg {
max-width: 100%;
cursor: pointer;

@ -150,6 +150,16 @@
overflow: unset;
margin-bottom: 0;
}
pre.markmap {
margin-bottom: 0;
max-height: 100%;
overflow: hidden;
display: flex;
flex-flow: column;
svg {
flex: 1;
}
}
.cp-spinner {
border-color: @colortheme_logo-1;
border-top-color: transparent;

@ -2,13 +2,9 @@
&::-webkit-input-placeholder { /* WebKit, Blink, Edge */
color: @color;;
}
&:-moz-placeholder { /* Mozilla Firefox 4 to 18 */
&::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: @color;
opacity: 1;
}
&::-moz-placeholder { /* Mozilla Firefox 19+ */
color: @color;
opacity: 1;
opacity: 1; /* Firefox */
}
&:-ms-input-placeholder { /* Internet Explorer 10-11 */
color: @color;

2
package-lock.json generated

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

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

@ -105,6 +105,12 @@
.markdown_preformatted-code;
.markdown_gfm-table(black);
table {
margin-bottom: 1rem;
}
media-tag > * {
margin-bottom: 1rem;
}
}
.cp-splitter {

@ -297,6 +297,8 @@ define([
}
});
DiffMd.onPluginLoaded(drawPreview);
return {
forceDraw: forceDrawPreview,
draw: drawPreview,

@ -28,6 +28,13 @@ var factory = function (Util, Crypto, Keys, Nacl) {
};
};
Hash.getSignPublicFromPrivate = function (edPrivateSafeStr) {
var edPrivateStr = Crypto.b64AddSlashes(edPrivateSafeStr);
var privateKey = Nacl.util.decodeBase64(edPrivateStr);
var keyPair = Nacl.sign.keyPair.fromSecretKey(privateKey);
return Nacl.util.encodeBase64(keyPair.publicKey);
};
var getEditHashFromKeys = Hash.getEditHashFromKeys = function (secret) {
var version = secret.version;
var data = secret.keys;

@ -395,7 +395,10 @@ define([
var navs = [];
buttons.forEach(function (b) {
if (!b.name || !b.onClick) { return; }
var button = h('button', { tabindex: '1', 'class': b.className || '' }, b.name);
var button = h('button', { tabindex: '1', 'class': b.className || '' }, [
b.iconClass ? h('i' + b.iconClass) : undefined,
b.name
]);
button.classList.add('btn');
var todo = function () {
var noClose = b.onClick();

@ -76,6 +76,9 @@ define([
finish(Session, void 0, rt.getUserDoc());
};
config.onError = function (info) {
finish(Session, info.error);
};
config.onChannelError = function (info) {
finish(Session, info.error);
};

@ -632,7 +632,12 @@ define([
}
}).nThen(function () {
Crypt.get(parsed.hash, function (err, val) {
if (err) { throw new Error(err); }
if (err) {
return void cb(err);
}
if (!val) {
return void cb('ENOENT');
}
try {
// Try to fix the title before importing the template
var parsed = JSON.parse(val);
@ -665,14 +670,14 @@ define([
Crypt.get(parsed.hash, _waitFor(function (err, _val) {
if (err) {
_waitFor.abort();
return void cb();
return void cb(err);
}
try {
val = JSON.parse(_val);
fixPadMetadata(val, true);
} catch (e) {
_waitFor.abort();
return void cb();
return void cb(e.message);
}
}), optsGet);
return;
@ -691,7 +696,7 @@ define([
Util.fetch(src, waitFor(function (err, _u8) {
if (err) {
_waitFor.abort();
return void waitFor.abort();
return void cb(err);
}
u8 = _u8;
}));
@ -700,7 +705,7 @@ define([
FileCrypto.decrypt(u8, key, waitFor(function (err, _res) {
if (err || !_res.content) {
_waitFor.abort();
return void waitFor.abort();
return void cb(err);
}
res = _res;
}));

@ -19,22 +19,119 @@ define([
var renderer = new Marked.Renderer();
var restrictedRenderer = new Marked.Renderer();
var Mermaid = {
init: function () {}
};
var pluginLoaded = Util.mkEvent();
DiffMd.onPluginLoaded = pluginLoaded.reg;
var mermaidThemeCSS = //".node rect { fill: #DDD; stroke: #AAA; } " +
"rect.task, rect.task0, rect.task2 { stroke-width: 1 !important; rx: 0 !important; } " +
"g.grid g.tick line { opacity: 0.25; }" +
"g.today line { stroke: red; stroke-width: 1; stroke-dasharray: 3; opacity: 0.5; }";
require(['mermaid', 'css!/code/mermaid-new.css'], function (_Mermaid) {
var Mermaid = {
__stubbed: true,
init: function () {
require([
'mermaid',
'css!/code/mermaid-new.css'
], function (_Mermaid) {
console.debug("loaded mermaid");
if (Mermaid.__stubbed) {
Mermaid = _Mermaid;
Mermaid.initialize({
gantt: { axisFormat: '%m-%d', },
"themeCSS": mermaidThemeCSS,
});
}
pluginLoaded.fire();
});
}
};
var Mathjax = {
__stubbed: true,
tex2svg: function (a, b) {
require([
'/bower_components/MathJax/es5/tex-svg.js',
], function () {
console.debug("Loaded mathjax");
if (Mathjax.__stubbed) {
Mathjax = window.MathJax;
}
Mathjax.tex2svg(a, b);
pluginLoaded.fire();
});
}
};
var drawMarkmap;
var MarkMapTransform;
var Markmap;
var markmapLoaded = false;
var loadMarkmap = function ($el) {
require([
'/lib/markmap/transform.min.js',
'/lib/markmap/view.min.js',
], function (_Transform, _View) {
if (!markmapLoaded) {
console.debug("Loaded markmap");
MarkMapTransform = _Transform;
Markmap = _View;
markmapLoaded = true;
}
drawMarkmap($el);
pluginLoaded.fire();
});
};
var sfCommon;
var fixMarkmapClickables = function ($svg) {
// find all links in the tree and do the following for each one
var onClick = function (e) {
e.preventDefault();
e.stopImmediatePropagation();
var $el = $(e.target);
// Open links only from the preview modal
if (!sfCommon) { return void console.error('No sfCommon'); }
var href = $el.attr('href');
if (!href || !/^(https?:\/\/|\/)/.test(href)) { return; }
if (/^http/.test(href)) {
sfCommon.openUnsafeURL(href);
return;
}
sfCommon.openURL(href);
};
$svg.find('a').click(onClick);
// make sure the links added later by collapsing/expading the map are also safe
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
var n;
for (var i = 0; i < mutation.addedNodes.length; i++) {
n = mutation.addedNodes[i];
if (n.nodeName === "A") { return void n.addEventListener('click', onClick); }
$(n).find('a').click(onClick);
}
}
});
});
observer.observe($svg[0], {
childList: true,
subtree: true
});
};
drawMarkmap = function ($el) {
if (!markmapLoaded) { return void loadMarkmap($el); }
if (!$el) { return console.error("no element provided"); }
var data = MarkMapTransform.transform($el[0].getAttribute("markmap-source"));
$el[0].innerHTML = "<svg width='100%' height='600'/>";
Markmap.markmap($el[0].firstChild, data);
fixMarkmapClickables($el);
};
var highlighter = function () {
return function(code, lang) {
@ -92,9 +189,15 @@ define([
var mediaMap = {};
var defaultCode = renderer.code;
renderer.code = function (code, language) {
if (!code || typeof(code) !== 'string' || !code.trim()) { return defaultCode.apply(renderer, arguments); }
if (language === 'mermaid' && code.match(/^(graph|pie|gantt|sequenceDiagram|classDiagram|gitGraph)/)) {
return '<pre class="mermaid">'+Util.fixHTML(code)+'</pre>';
return '<pre class="mermaid" data-plugin="mermaid">'+Util.fixHTML(code)+'</pre>';
} else if (language === 'markmap') {
return '<pre class="markmap" data-plugin="markmap">'+Util.fixHTML(code)+'</pre>';
} else if (language === 'mathjax') {
return '<pre class="mathjax" data-plugin="mathjax">'+Util.fixHTML(code)+'</pre>';
} else {
return defaultCode.apply(renderer, arguments);
}
@ -199,7 +302,6 @@ define([
return renderParagraph(p);
};
var MutationObserver = window.MutationObserver;
var forbiddenTags = [
'SCRIPT',
'IFRAME',
@ -297,6 +399,8 @@ define([
return patch;
};
var plugins = {};
var removeMermaidClickables = function ($el) {
// find all links in the tree and do the following for each one
$el.find('a').each(function (index, a) {
@ -313,17 +417,83 @@ define([
// finally, find all 'clickable' items and remove the class
$el.find('.clickable').removeClass('clickable');
};
var renderMermaid = function ($el) {
plugins.mermaid = {
name: 'mermaid',
attr: 'mermaid-source',
render: function ($el) {
Mermaid.init(undefined, $el);
// clickable elements in mermaid don't work well with our sandboxing setup
// the function below strips clickable elements but still leaves behind some artifacts
// tippy tooltips might still be useful, so they're not removed. It would be
// preferable to just support links, but this covers up a rough edge in the meantime
removeMermaidClickables($el);
}
};
plugins.markmap = {
name: 'markmap',
attr: 'markmap-source',
render: function ($el) {
drawMarkmap($el);
}
};
plugins.mathjax = {
name: 'mathjax',
attr: 'mathjax-source',
render: function renderMathjax ($el) {
var el = $el[0];
if (!el) { return; }
var code = el.getAttribute("mathjax-source");
var svg = Mathjax.tex2svg(code, {display: true});
if (!svg) { return; }
svg.innerHTML = svg.innerHTML.replace(/xlink:href/g, "href");
var wrapper = document.createElement('span');
wrapper.innerHTML = svg.innerHTML;
el.innerHTML = wrapper.outerHTML;
}
};
var getAvailableCachedElement = function ($content, cache, src) {
var cached = cache[src];
if (!Array.isArray(cached)) { return; }
var root = $content[0];
var l = cached.length;
for (var i = 0; i < l; i++) {
if (!root.contains(cached[i])) {
return cached[i];
}
}
};
var cacheRenderedElement = function (cache, src, el) {
if (Array.isArray(cache[src])) {
cache[src].push(el);
} else {
cache[src] = [ el ];
}
};
// remove elements from the cache that are not embedded in the dom
var clearUnusedCacheEntries = function ($content, plugins) {
var root = $content[0];
Object.keys(plugins).forEach(function (name) {
var plugin = plugins[name];
var cache = plugin.cache;
Object.keys(cache).forEach(function (key) {
var list = cache[key];
if (!Array.isArray(list)) { return; }
cache[key] = list.filter(function (el) {
return root.contains(el);
});
});
});
};
DiffMd.apply = function (newHtml, $content, common) {
if (!sfCommon) { sfCommon = common; }
var contextMenu = common.importMediaTagMenu();
var id = $content.attr('id');
if (!id) { throw new Error("The element must have a valid id"); }
@ -343,8 +513,10 @@ define([
var Dom = domFromHTML($('<div>').append($div).html());
$content[0].normalize();
var mermaid_source = [];
var mermaid_cache = {};
Object.keys(plugins).forEach(function (id) {
plugins[id].source = [];
plugins[id].cache = {};
});
var canonicalizeMermaidSource = function (src) {
// ignore changes to empty lines, since that won't affect
@ -352,12 +524,15 @@ define([
return src.replace(/\n[ \t]*\n*[ \t]*\n/g, '\n');
};
// iterate over the unrendered mermaid inputs, caching their source as you go
$(newDomFixed).find('pre.mermaid').each(function (index, el) {
// iterate over the unrendered mermaid and markmap inputs,
// caching their source as you go
$(newDomFixed).find('pre[data-plugin]').each(function (index, el) {
if (el.childNodes.length === 1 && el.childNodes[0].nodeType === 3) {
var plugin = plugins[el.getAttribute('data-plugin')];
if (!plugin) { return; }
var src = canonicalizeMermaidSource(el.childNodes[0].wholeText);
el.setAttribute('mermaid-source', src);
mermaid_source[index] = src;
el.setAttribute(plugin.attr, src);
plugin.source[index] = src;
}
});
@ -365,18 +540,21 @@ define([
var $parent = $content.parent();
var scrollTop = $parent.scrollTop();
// iterate over rendered mermaid charts
$content.find('pre.mermaid:not([processed="true"])').each(function (index, el) {
$content.find('pre[data-plugin]:not([processed="true"])').each(function (index, el) {
var plugin = plugins[el.getAttribute('data-plugin')];
if (!plugin) { return; }
// retrieve the attached source code which it was drawn
var src = el.getAttribute('mermaid-source');
var src = el.getAttribute(plugin.attr);
/* The new source might have syntax errors that will prevent rendering.
It might be preferable to keep the existing state instead of removing it
if you don't have something better to display. Ideally we should display
the cause of the syntax error so that the user knows what to correct. */
//if (!Mermaid.parse(src)) { } // TODO
//if (plugin.name === "mermaid" && !Mermaid.parse(src)) { } // TODO
// check if that source exists in the set of charts which are about to be rendered
if (mermaid_source.indexOf(src) === -1) {
if (plugin.source.indexOf(src) === -1) {
// if it's not, then you can remove it
if (el.parentNode && el.parentNode.children.length) {
el.parentNode.removeChild(el);
@ -384,26 +562,30 @@ define([
} else if (el.childNodes.length === 1 && el.childNodes[0].nodeType !== 3) {
// otherwise, confirm that the content of the rendered chart is not a text node
// and keep a copy of it
mermaid_cache[src] = el.childNodes[0];
cacheRenderedElement(plugin.cache, src, el.childNodes[0]);
}
});
var oldDom = domFromHTML($content[0].outerHTML);
var MutationObserver = window.MutationObserver;
var onPreview = function ($mt) {
return function () {
var isSvg = $mt.is('pre.mermaid');
var mts = [];
$content.find('media-tag, pre.mermaid').each(function (i, el) {
// Get all previewable elements from the doc
$content.find('media-tag, pre[data-plugin]').each(function (i, el) {
if (el.nodeName.toLowerCase() === "pre") {
var clone = el.cloneNode();
var plugin = plugins[el.getAttribute('data-plugin')];
if (!plugin) { return; }
return void mts.push({
svg: clone,
render: function () {
var $el = $(clone);
$el.text(clone.getAttribute('mermaid-source'));
$el.text(clone.getAttribute(plugin.attr));
$el.attr('data-processed', '');
renderMermaid($el);
plugin.render($el);
}
});
}
@ -415,9 +597,16 @@ define([
});
// Find initial position
// If the element is supported by one of our plugin types
// (mermaid, mathjax, or markmap) get the corresponding attribute
var isSvg = $mt.is('pre[data-plugin]');
var plugin = isSvg && plugins[$mt.attr('data-plugin')];
// Get initial idx
var idx = -1;
mts.some(function (obj, i) {
if (isSvg && $mt.attr('mermaid-source') === $(obj.svg).attr('mermaid-source')) {
if (isSvg && $mt.attr(plugin.attr) === $(obj.svg).attr(plugin.attr)) {
idx = i;
return true;
}
@ -426,16 +615,17 @@ define([
return true;
}
});
// Not found, re-render
if (idx === -1) {
if (isSvg) {
if (isSvg && $mt.attr(plugin.attr)) {
var clone = $mt[0].cloneNode();
mts.unshift({
svg: clone,
render: function () {
var $el = $(clone);
$el.text(clone.getAttribute('mermaid-source'));
$el.text(clone.getAttribute(plugin.attr));
$el.attr('data-processed', '');
renderMermaid($el);
plugin.render($el);
}
});
} else {
@ -511,8 +701,10 @@ define([
if (target) { target.scrollIntoView(); }
});
// loop over mermaid elements in the rendered content
$content.find('pre.mermaid').each(function (index, el) {
// loop over plugin elements in the rendered content
$content.find('pre[data-plugin]').each(function (index, el) {
var plugin = plugins[el.getAttribute('data-plugin')];
if (!plugin) { return; }
var $el = $(el);
$el.off('contextmenu').on('contextmenu', function (e) {
e.preventDefault();
@ -529,14 +721,14 @@ define([
// since you've simply drawn the content that was supplied via markdown
// you can assume that the index of your rendered charts matches that
// of those in the markdown source.
var src = mermaid_source[index];
el.setAttribute('mermaid-source', src);
var cached = mermaid_cache[src];
var src = plugin.source[index];
el.setAttribute(plugin.attr, src);
var cached = getAvailableCachedElement($content, plugin.cache, src);
// check if you had cached a pre-rendered instance of the supplied source
if (typeof(cached) !== 'object') {
try {
renderMermaid($el);
plugin.render($el);
} catch (e) { console.error(e); }
return;
}
@ -551,6 +743,8 @@ define([
el.setAttribute('data-processed', true);
});
}
clearUnusedCacheEntries($content, plugins);
// recover the previous scroll position to avoid jank
$parent.scrollTop(scrollTop);
};

@ -86,7 +86,7 @@ define([
var faColor = 'cptools-palette';
var faTrash = 'fa-trash';
var faCopy = 'fa-clone';
var faDelete = 'fa-eraser';
var faDelete = 'cptools-destroy';
var faAccess = 'fa-unlock-alt';
var faProperties = 'fa-info-circle';
var faTags = 'fa-hashtag';
@ -129,6 +129,7 @@ define([
//var $ownerIcon = $('<span>', {"class": "fa fa-id-card"});
var $tagsIcon = $('<span>', {"class": "fa " + faTags});
var $passwordIcon = $('<span>', {"class": "fa fa-lock"});
var $restrictedIcon = $('<span>', {"class": "fa fa-ban"});
var $expirableIcon = $('<span>', {"class": "fa fa-clock-o"});
var $separator = $('<div>', {"class": "dropdown-divider"});
@ -233,9 +234,9 @@ define([
return LS;
};
var getViewModeClass = function () {
var getViewModeClass = function (forceList) {
var mode = APP.store[LS_VIEWMODE];
if (mode === 'list') { return 'cp-app-drive-content-list'; }
if (mode === 'list' || forceList) { return 'cp-app-drive-content-list'; }
return 'cp-app-drive-content-grid';
};
var getViewMode = function () {
@ -450,15 +451,15 @@ define([
h('li', h('a.cp-app-drive-context-deleteowned.dropdown-item.cp-app-drive-context-editable', {
'tabindex': '-1',
'data-icon': faDelete,
}, Messages.fc_delete_owned)), // XXX update key? "Delete from the server"
}, Messages.fc_delete_owned)),
h('li', h('a.cp-app-drive-context-remove.dropdown-item.cp-app-drive-context-editable', {
'tabindex': '-1',
'data-icon': faTrash,
}, Messages.fc_remove)), // XXX update key? "Remove from your CryptDrive"
}, Messages.fc_remove)),
h('li', h('a.cp-app-drive-context-removesf.dropdown-item.cp-app-drive-context-editable', {
'tabindex': '-1',
'data-icon': faTrash,
}, Messages.fc_remove_sharedfolder)), // XXX update key? "Remove"
}, Messages.fc_remove_sharedfolder)),
$separator.clone()[0],
h('li', h('a.cp-app-drive-context-properties.dropdown-item', {
'tabindex': '-1',
@ -1211,6 +1212,9 @@ define([
if (!$element.is('.cp-app-drive-element-owned')) {
hide.push('deleteowned');
}
if ($element.is('.cp-app-drive-element-restricted')) {
hide.push('rename', 'download', 'share', 'access', 'color');
}
if ($element.is('.cp-app-drive-element-notrash')) {
// We can't delete elements in virtual categories
hide.push('delete');
@ -1726,7 +1730,9 @@ define([
&& $target.parents('#cp-app-drive-content')) {
newPath = currentPath;
}
if (newPath[0] !== ROOT) { newPath = [ROOT]; }
// XXX Why did we add the following line?
// https://github.com/xwiki-labs/cryptpad/commit/f103a0fb08d137901174ef7e15dbc2c9a2ec3ca1
//if (newPath[0] !== ROOT) { newPath = [ROOT]; }
return newPath;
};
var onFileDrop = APP.onFileDrop = function (file, e) {
@ -1949,7 +1955,8 @@ define([
var $ro;
if (manager.isSharedFolder(element)) {
var data = manager.getSharedFolderData(element);
key = data && data.title ? data.title : key;
var fId = element;
key = data.title || data.lastTitle;
element = manager.folders[element].proxy[manager.user.userObject.ROOT];
$span.addClass('cp-app-drive-element-sharedf');
_addOwnership($span, $state, data);
@ -1964,6 +1971,11 @@ define([
$ro.attr('title', Messages.readonly);
}
if (files.restrictedFolders[fId]) {
var $restricted = $restrictedIcon.clone().appendTo($state);
$restricted.attr('title', Messages.fm_restricted);
}
var $shared = $sharedIcon.clone().appendTo($state);
$shared.attr('title', Messages.fm_canBeShared);
} else if ($content.data('readOnlyFolder') || APP.readOnly) {
@ -1972,14 +1984,14 @@ define([
}
var sf = manager.hasSubfolder(element);
var files = manager.hasFile(element);
var hasFiles = manager.hasFile(element);
var $name = $('<span>', {'class': 'cp-app-drive-element-name'}).text(key);
var $subfolders = $('<span>', {
'class': 'cp-app-drive-element-folders cp-app-drive-element-list'
}).text(sf);
var $files = $('<span>', {
'class': 'cp-app-drive-element-files cp-app-drive-element-list'
}).text(files);
}).text(hasFiles);
var $filler = $('<span>', {
'class': 'cp-app-drive-element-filler cp-app-drive-element-list'
});
@ -2051,32 +2063,38 @@ define([
element = root[key];
}
var restricted = files.restrictedFolders[element];
var isSharedFolder = manager.isSharedFolder(element);
var $icon = !isFolder ? getFileIcon(element) : undefined;
var ro = manager.isReadOnlyFile(element);
// ro undefined means it's an old hash which doesn't support read-only
var roClass = typeof(ro) === 'undefined' ?' cp-app-drive-element-noreadonly' :
ro ? ' cp-app-drive-element-readonly' : '';
var liClass = 'cp-app-drive-element-file cp-app-drive-element' + roClass;
var roClass = typeof(ro) === 'undefined' ? '.cp-app-drive-element-noreadonly' :
ro ? '.cp-app-drive-element-readonly' : '';
var liClass = '.cp-app-drive-element-file';
var restrictedClass = restricted ? '.cp-app-drive-element-restricted' : '';
if (isSharedFolder) {
liClass = 'cp-app-drive-element-folder cp-app-drive-element';
liClass = '.cp-app-drive-element-folder';
$icon = $sharedFolderIcon.clone();
$icon.css("color", getFolderColor(path.concat(elPath)));
} else if (isFolder) {
liClass = 'cp-app-drive-element-folder cp-app-drive-element';
liClass = '.cp-app-drive-element-folder';
$icon = manager.isFolderEmpty(root[key]) ? $folderEmptyIcon.clone() : $folderIcon.clone();
$icon.css("color", getFolderColor(path.concat(elPath)));
}
var $element = $('<li>', {
draggable: true,
'class': 'cp-app-drive-element-row'
});
var classes = restrictedClass + roClass + liClass;
var $element = $(h('li.cp-app-drive-element.cp-app-drive-element-row' + classes, {
draggable: true
}));
$element.data('path', newPath);
if (isElementSelected($element)) {
selectElement($element);
}
$element.prepend($icon).dblclick(function () {
if (restricted) {
UI.warn(Messages.fm_restricted);
return;
}
if (isFolder) {
APP.displayDirectory(newPath);
return;
@ -2100,8 +2118,7 @@ define([
}
e.stopPropagation();
});
$element.addClass(liClass);
var droppable = !isTrash && !APP.$content.data('readOnlyFolder');
var droppable = !isTrash && !APP.$content.data('readOnlyFolder') && !restricted;
addDragAndDropHandlers($element, newPath, isFolder, droppable);
$element.click(function(e) {
e.stopPropagation();
@ -2382,11 +2399,10 @@ define([
var emptyTrashModal = function () {
var ownedInTrash = manager.ownedInTrash();
var hasOwned = Array.isArray(ownedInTrash) && ownedInTrash.length;
Messages.fm_emptyTrashOwned = "Your trash contains documents you own. You can remove them for everyone or only from your drive"; // XXX
var content = h('p', [
Messages.fm_emptyTrashDialog,
hasOwned ? h('br') : undefined,
hasOwned ? Messages.fm_emptyTrashOwned : undefined // XXX update UI?
hasOwned ? UI.setHTML(h('span'), Messages.fm_emptyTrashOwned) : undefined
]);
var buttons = [{
className: 'cancel',
@ -2396,7 +2412,8 @@ define([
}];
if (hasOwned) {
buttons.push({
className: 'secondary',
className: 'danger',
iconClass: '.cptools.cptools-destroy',
name: Messages.fc_delete_owned,
onClick: function () {
manager.emptyTrash(true, refresh);
@ -2406,8 +2423,8 @@ define([
}
buttons.push({
className: 'primary',
// XXX fc_remove: Remove from your CryptDrive
// We may want to use a new key here
iconClass: '.fa.fa-trash',
name: hasOwned ? Messages.fc_remove : Messages.okButton,
onClick: function () {
manager.emptyTrash(false, refresh);
@ -2887,10 +2904,11 @@ define([
var el = useId ? _el : root[_el];
var sfId = (el && el.root && el.key) ? el.root[el.key] : el;
if (folder && el && manager.isSharedFolder(sfId)) {
var title = manager.getSharedFolderData(sfId).title || el;
var sfData = manager.getSharedFolderData(sfId);
var title = sfData.title || sfData.lastTitle || el;
return String(title).toLowerCase();
} else if (folder) {
return String((el && el.key) || el).toLowerCase();
return String((el && el.key) || _el).toLowerCase();
}
var data = manager.getFileData(el);
if (!data) { return ''; }
@ -3014,7 +3032,7 @@ define([
if (APP.$content.data('readOnlyFolder') || !APP.editable) { return; }
var isInRoot = currentPath[0] === ROOT;
var $element = $('<li>', {
'class': 'cp-app-drive-element-row cp-app-drive-element-grid cp-app-drive-new-ghost'
'class': 'cp-app-drive-element-row cp-app-drive-new-ghost'
}).prepend($addIcon.clone()).appendTo($list);
$element.append($('<span>', {'class': 'cp-app-drive-element-name'})
.text(Messages.fm_newFile));
@ -3131,7 +3149,10 @@ define([
return;
}
var allfiles = files[FILES_DATA];
if (allfiles.length === 0) { return; }
if (Object.keys(allfiles || {}).length === 0) {
createGhostIcon($container);
return;
}
var $fileHeader = getFileListHeader(true);
$container.append($fileHeader);
var keys = manager.getFiles([FILES_DATA]);
@ -3211,18 +3232,26 @@ define([
$searchIcon.clone().appendTo($div);
var $spinnerContainer = $(h('div.cp-app-drive-search-spinner'));
var spinner = UI.makeSpinner($spinnerContainer);
var $input = APP.Search.$input = $('<input>', {
id: 'cp-app-drive-search-input',
placeholder: Messages.fm_searchName,
type: 'text',
draggable: false,
tabindex: 1,
}).keyup(function (e) {
var lastValue = search.value;
search.value = $input.val().trim();
if (lastValue === search.value) { return; }
if (search.to) { window.clearTimeout(search.to); }
if ($input.val().trim() === "") {
if (search.value === "") {
search.cursor = 0;
APP.displayDirectory([SEARCH]);
return;
}
spinner.spin();
if (e.which === 13) {
var newLocation = [SEARCH, $input.val()];
search.cursor = $input[0].selectionStart;
@ -3267,12 +3296,26 @@ define([
$div.append(cancel);
$list.append($div);
$spinnerContainer.appendTo($list);
setTimeout(function () {
$input.focus();
});
$list.closest('#cp-app-drive-content-folder').addClass('cp-app-drive-content-list');
if (typeof(value) === "string" && value.trim()) {
spinner.spin();
} else {
return;
}
setTimeout(function () {
//$list.closest('#cp-app-drive-content-folder').addClass('cp-app-drive-content-list');
var filesList = manager.search(value);
if (!filesList.length) {
Messages.fm_noResult = "No result found"; // XXX
$list.append(h('div.cp-app-drive-search-noresult', Messages.fm_noResult));
spinner.hide();
return;
}
var sortable = {};
var sortableFolders = [];
filesList.forEach(function (r) {
@ -3334,6 +3377,8 @@ define([
addEl(obj, false);
});
setTimeout(collapseDrivePath);
spinner.hide();
});
};
var displayRecent = function ($list) {
@ -3589,17 +3634,23 @@ define([
var $dirContent = $('<div>', {id: FOLDER_CONTENT_ID});
$dirContent.data('path', path);
if (!isSearch && !isTags) {
var mode = getViewMode();
if (mode) {
$dirContent.addClass(getViewModeClass());
}
if (!isTags) {
$dirContent.addClass(getViewModeClass(isSearch));
if (!isSearch) {
createViewModeButton(APP.toolbar.$bottomR);
}
}
var $list = $('<ul>').appendTo($dirContent);
var sfId = manager.isInSharedFolder(currentPath);
// Restricted folder? display ROOT instead
if (sfId && files.restrictedFolders[sfId]) {
_displayDirectory([ROOT], true);
return;
}
var readOnlyFolder = false;
if (APP.readOnly) {
// Read-only drive (team?)
@ -3753,8 +3804,15 @@ define([
}
var $elementRow = $('<span>', {'class': 'cp-app-drive-element-row'}).append($collapse).append($icon).append($name).click(function (e) {
e.stopPropagation();
if (files.restrictedFolders[isSharedFolder]) {
UI.warn(Messages.fm_restricted);
return;
}
APP.displayDirectory(path);
});
if (files.restrictedFolders[isSharedFolder]) {
$elementRow.addClass('cp-app-drive-element-restricted');
}
if (isSharedFolder) {
var sfData = manager.getSharedFolderData(isSharedFolder);
_addOwnership($elementRow, $(), sfData);
@ -3837,7 +3895,7 @@ define([
if (!manager.isFolder(root[key])) { return; }
var newPath = path.slice();
newPath.push(key);
var isSharedFolder = manager.isSharedFolder(root[key]);
var isSharedFolder = manager.isSharedFolder(root[key]) && root[key];
var sfId = manager.isInSharedFolder(newPath) || (isSharedFolder && root[key]);
var $icon, isCurrentFolder, subfolder;
if (isSharedFolder) {
@ -4130,6 +4188,10 @@ define([
else if ($this.hasClass('cp-app-drive-context-open')) {
paths.forEach(function (p) {
var el = manager.find(p.path);
if (files.restrictedFolders[el]) {
UI.warn(Messages.fm_restricted);
return;
}
openFile(el, false, true);
});
}
@ -4676,7 +4738,10 @@ define([
var ok = manager.isValidDrive(obj.drive);
if (!ok) { return; }
var restricted = files.restrictedFolders;
copyObjectValue(files, obj.drive);
files.restrictedFolders = restricted;
appStatus.isReady = true;
refresh();
};
@ -4715,7 +4780,6 @@ define([
});
}
*/
var deprecated = files.sharedFoldersTemp;
var nt = nThen;
var passwordModal = function (fId, data, cb) {
var content = [];
@ -4771,6 +4835,7 @@ define([
onClose: cb
});
};
var deprecated = files.sharedFoldersTemp;
if (typeof (deprecated) === "object" && APP.editable && Object.keys(deprecated).length) {
Object.keys(deprecated).forEach(function (fId) {
var data = deprecated[fId];
@ -4787,7 +4852,6 @@ define([
});
}
return {
refresh: refresh,
close: function () {

@ -372,8 +372,7 @@ define([
var parsed = Hash.parsePadUrl(data.href || data.roHref);
var owned = Modal.isOwned(Env, data);
var disabled = !owned || !parsed.hashData || parsed.hashData.type !== 'pad';
var allowDisabled = parsed.type === 'drive';
if (disabled || allowDisabled) { return void cb(); }
if (disabled) { return void cb(); }
opts = opts || {};
@ -899,7 +898,7 @@ define([
}
}
if (owned) {
var deleteOwned = h('button.btn.btn-danger-alt', Messages.fc_delete_owned);
var deleteOwned = h('button.btn.btn-danger-alt', [h('i.cptools.cptools-destroy'), Messages.fc_delete_owned]);
var spinner = UI.makeSpinner();
UI.confirmButton(deleteOwned, {
classes: 'btn-danger'
@ -910,6 +909,7 @@ define([
channel: data.channel
}, function (err, obj) {
spinner.done();
UI.findCancelButton().click();
if (err || (obj && obj.error)) { UI.warn(Messages.error); }
});
});

@ -256,7 +256,7 @@ define([
}
// Reset modal
$inner.find('media-tag, pre.mermaid').detach();
$inner.find('media-tag, pre[data-plugin]').detach();
$spinner.show();
// Check src and cryptkey

@ -1439,6 +1439,7 @@ define([
}, {
typeInput: $select[0]
}, true);
$select.find('button').addClass('btn');
};
var x2tImportImagesInternal = function(x2t, images, i, callback) {

@ -2054,6 +2054,10 @@ define([
var addSharedFolderHandler = function () {
store.sharedFolders = {};
store.handleSharedFolder = function (id, rt) {
if (!rt) {
delete store.sharedFolders[id];
return;
}
store.sharedFolders[id] = rt;
if (store.driveEvents) {
registerProxyEvents(rt.proxy, id);
@ -2083,6 +2087,9 @@ define([
Store.addSharedFolder = function (clientId, data, cb) {
var s = getStore(data.teamId);
s.manager.addSharedFolder(data, function (id) {
if (id && typeof(id) === "object" && id.error) {
return void cb(id);
}
var send = data.teamId ? s.sendEvent : sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
@ -2188,6 +2195,8 @@ define([
});
};
registerProxyEvents = function (proxy, fId) {
if (!proxy) { return; }
if (proxy.deprecated || proxy.restricted) { return; }
if (!fId) {
// Listen for shared folder password change
proxy.on('change', ['drive', UserObject.SHARED_FOLDERS], function (o, n, p) {

@ -211,6 +211,9 @@ define([
// We can only hide it
sf.teams.forEach(function (obj) {
obj.store.manager.deprecateProxy(obj.id, secret.channel);
if (obj.store.handleSharedFolder) {
obj.store.handleSharedFolder(obj.id, null);
}
});
} catch (e) {}
delete allSharedFolders[secret.channel];
@ -225,6 +228,7 @@ define([
sf.teams.forEach(function (obj) {
obj.store.manager.restrictedProxy(obj.id, secret.channel);
});
delete allSharedFolders[secret.channel];
return void cb();
}
}
@ -251,9 +255,14 @@ define([
if (!sf) { return; }
var clients = sf.teams;
if (!Array.isArray(clients)) { return; }
// Remove the shared folder from the client's store and
// remove the client/team from our list
var idx;
clients.some(function (obj, i) {
if (obj.store.id === teamId) {
if (obj.store.handleSharedFolder) {
obj.store.handleSharedFolder(obj.id, null);
}
idx = i;
return true;
}

@ -164,6 +164,10 @@ define([
var handleSharedFolder = function (ctx, id, sfId, rt) {
var t = ctx.teams[id];
if (!t) { return; }
if (!rt) {
delete t.sharedFolders[sfId];
return;
}
t.sharedFolders[sfId] = rt;
registerChangeEvents(ctx, t, rt.proxy, sfId);
};

@ -52,6 +52,10 @@ define([
// Password may have changed
var deprecateProxy = function (Env, id, channel) {
if (Env.folders[id] && Env.folders[id].deleting) {
// Folder is being deleted by its owner, don't deprecate it
return;
}
if (Env.user.userObject.readOnly) {
// In a read-only team, we can't deprecate a shared folder
// Use a empty object with a deprecated flag...
@ -69,7 +73,7 @@ define([
};
var restrictedProxy = function (Env, id) {
var lm = { proxy: { deprecated: true } };
var lm = { proxy: { restricted: true, root: {}, filesData: {} } };
removeProxy(Env, id);
addProxy(Env, id, lm, function () {});
return void Env.Store.refreshDriveUI();
@ -215,7 +219,7 @@ define([
if (!Env.folders[id]) { return {}; }
var obj = Env.folders[id].proxy.metadata || {};
for (var k in Env.user.proxy[UserObject.SHARED_FOLDERS][id] || {}) {
var data = JSON.parse(JSON.stringify(Env.user.proxy[UserObject.SHARED_FOLDERS][id][k]));
var data = Util.clone(Env.user.proxy[UserObject.SHARED_FOLDERS][id][k] || {});
if (k === "href" && data.indexOf('#') === -1) {
try {
data = Env.user.userObject.cryptor.decrypt(data);
@ -491,6 +495,17 @@ define([
};
if (data.password) { folderData.password = data.password; }
if (data.owned) { folderData.owners = [Env.edPublic]; }
}).nThen(function (waitFor) {
Env.Store.getPadMetadata(null, {
channel: folderData.channel
}, waitFor(function (obj) {
if (obj && (obj.error || obj.rejected)) {
waitFor.abort();
return void cb({
error: obj.error || 'ERESTRICTED'
});
}
}));
}).nThen(function (waitFor) {
Env.pinPads([folderData.channel], waitFor());
}).nThen(function (waitFor) {
@ -812,19 +827,38 @@ define([
var data = uo.isFile(el) ? uo.getFileData(el) : getSharedFolderData(Env, el);
chan = data.channel;
}
// If the pad was a shared folder, delete it too and leave it
var fId;
Object.keys(Env.user.proxy[UserObject.SHARED_FOLDERS] || {}).some(function (id) {
var sfData = Env.user.proxy[UserObject.SHARED_FOLDERS][id] || {};
if (sfData.channel === chan) {
fId = Number(id);
Env.folders[id].deleting = true;
return true;
}
});
Env.removeOwnedChannel(chan, function (obj) {
// If the error is that the file is already removed, nothing to
// report, it's a normal behavior (pad expired probably)
if (obj && obj.error && obj.error.code !== "ENOENT") {
// RPC may not be responding
// Send a report that can be handled manually
if (fId && Env.folders[fId] && Env.folders[fId].deleting) {
delete Env.folders[fId].deleting;
}
console.error(obj.error, chan);
Feedback.send('ERROR_DELETING_OWNED_PAD=' + chan + '|' + obj.error, true);
return void cb();
}
// No error: delete the pads and all its copies from our drive and shared folders
// No error: delete the pad and all its copies from our drive and shared folders
var ids = _findChannels(Env, [chan]);
// If the pad was a shared folder, delete it too and leave it
if (fId) {
ids.push(fId);
}
ids.forEach(function (id) {
var paths = findFile(Env, id);
var _resolved = _resolvePaths(Env, paths);
@ -858,6 +892,10 @@ define([
}).nThen(function () {
// Remove deleted pads from the drive
_delete(Env, { resolved: toDelete }, cb);
// If we were using the access modal, send a refresh command
if (data.channel) {
Env.Store.refreshDriveUI();
}
});
};
@ -915,9 +953,8 @@ define([
if (!resolved.id) {
var el = Env.user.userObject.find(resolved.path);
if (Env.user.userObject.isSharedFolder(el) && Env.folders[el]) {
var oldName = Env.folders[el].proxy.metadata.title;
Env.folders[el].proxy.metadata.title = data.newName;
Env.user.proxy[UserObject.SHARED_FOLDERS][el].lastTitle = oldName;
Env.user.proxy[UserObject.SHARED_FOLDERS][el].lastTitle = data.newName;
return void cb();
}
}

@ -75,6 +75,21 @@ define([
}
};
module.handleImagePaste = function (editor) {
// Don't paste file path in the users wants to paste a file
editor.on('paste', function (editor, ev) {
try {
if (!ev.clipboardData.items) { return; }
var items = Array.prototype.slice.apply(ev.clipboardData.items);
var hasFile = items.some(function (el) {
return el.kind === "file";
});
if (!hasFile) { return; }
ev.preventDefault();
} catch (e) { console.error(e); }
});
};
module.getHeadingText = function (editor) {
var lines = editor.getValue().split(/\n/);
@ -234,6 +249,8 @@ define([
editor.scrollIntoView(cursor);
});
module.handleImagePaste(editor);
var setMode = exp.setMode = function (mode, cb) {
exp.highlightMode = mode;
if (mode === 'markdown') { mode = 'gfm'; }

@ -528,6 +528,26 @@ define([
$hoverArea.removeClass('cp-fileupload-hovering');
onFileDrop(dropped, e);
});
// Upload files on paste
$area.on('paste', function (e) {
try {
var ev = e.originalEvent;
if (!ev.clipboardData.items) { return; }
var items = Array.prototype.slice.apply(ev.clipboardData.items);
var hasFile = items.some(function (el) {
return el.kind === "file";
});
if (!hasFile) { return; }
ev.preventDefault();
items.forEach(function (el) {
if (el.kind !== "file") { return; }
var file = el.getAsFile();
handleFile(file, e);
});
} catch (err) { console.error(err); }
});
};
var createCkeditorDropHandler = function () {
var editor = config.ckeditor;

@ -28,6 +28,7 @@ define([
var Test;
var password;
var initialPathInDrive;
var burnAfterReading;
var currentPad = window.CryptPad_location = {
app: '',
@ -171,6 +172,8 @@ define([
});
var parsed = Utils.Hash.parsePadUrl(currentPad.href);
burnAfterReading = parsed && parsed.hashData && parsed.hashData.ownerKey;
currentPad.app = parsed.type;
if (cfg.getSecrets) {
var w = waitFor();
@ -376,9 +379,27 @@ define([
}));
}).nThen(done);
}
}).nThen(function (waitFor) {
if (!burnAfterReading) { return; }
// This is a burn after reading URL: make sure our owner key is still valid
try {
var publicKey = Utils.Hash.getSignPublicFromPrivate(burnAfterReading);
Cryptpad.getPadMetadata({
channel: secret.channel
}, waitFor(function (md) {
if (md && md.error) { return console.error(md.error); }
// If our key is not valid anymore, don't show BAR warning
if (!(md && Array.isArray(md.owners)) || md.owners.indexOf(publicKey) === -1) {
burnAfterReading = null;
}
}));
} catch (e) {
console.error(e);
}
}).nThen(function (waitFor) {
if (cfg.afterSecrets) {
cfg.afterSecrets(Cryptpad, Utils, secret, waitFor());
cfg.afterSecrets(Cryptpad, Utils, secret, waitFor(), sframeChan);
}
}).nThen(function (waitFor) {
// Check if the pad exists on server
@ -402,7 +423,6 @@ define([
}
Utils.crypto = Utils.Crypto.createEncryptor(Utils.secret.keys);
var parsed = Utils.Hash.parsePadUrl(currentPad.href);
var burnAfterReading = parsed && parsed.hashData && parsed.hashData.ownerKey;
if (!parsed.type) { throw new Error(); }
var defaultTitle = Utils.UserObject.getDefaultName(parsed);
var edPublic, curvePublic, notifications, isTemplate;
@ -1600,7 +1620,16 @@ define([
// server
Cryptpad.useTemplate({
href: data.template
}, Cryptget, function () {
}, Cryptget, function (err) {
if (err) {
// TODO: better messages in case of expired, deleted, etc.?
if (err === 'ERESTRICTED') {
sframeChan.event('EV_RESTRICTED_ERROR');
} else {
sframeChan.query("EV_LOADING_ERROR", "DELETED");
}
return;
}
startRealtime();
cb();
}, cryptputCfg);
@ -1608,7 +1637,16 @@ define([
}
// if we open a new code from a file
if (Cryptpad.fromFileData) {
Cryptpad.useFile(Cryptget, function () {
Cryptpad.useFile(Cryptget, function (err) {
if (err) {
// TODO: better messages in case of expired, deleted, etc.?
if (err === 'ERESTRICTED') {
sframeChan.event('EV_RESTRICTED_ERROR');
} else {
sframeChan.query("EV_LOADING_ERROR", "DELETED");
}
return;
}
startRealtime();
cb();
}, cryptputCfg);

@ -671,6 +671,10 @@ define([
UIElements.displayPasswordPrompt(funcs, cfg);
});
ctx.sframeChan.on("EV_RESTRICTED_ERROR", function () {
UI.errorLoadingScreen(Messages.restrictedError);
});
ctx.sframeChan.on("EV_PAD_PASSWORD_ERROR", function () {
UI.errorLoadingScreen(Messages.password_error_seed);
});

@ -233,7 +233,7 @@
"poll_removeOption": "Bist du sicher, dass du diese Option entfernen möchtest?",
"poll_removeUser": "Bist du sicher, dass du diesen Nutzer entfernen möchtest?",
"poll_titleHint": "Titel",
"poll_descriptionHint": "Beschreibe deine Abstimmung und publiziere sie mit der Schaltfläche ✓ (Veröffentlichen), wenn du fertig bist. Bei der Beschreibung kann Markdown-Syntax verwendet werden und du kannst Medien-Elemente von deinem CryptDrive einbetten. Jeder, der den Link kennt, kann die Beschreibung ändern. Dies wird aber nicht empfohlen.",
"poll_descriptionHint": "Beschreibe deine Abstimmung und veröffentliche sie mit der Schaltfläche ✓ (Veröffentlichen).\nBei der Beschreibung kann Markdown-Syntax verwendet werden und du kannst Medien-Elemente von deinem CryptDrive einbetten.\nJeder, der den Link kennt, kann die Beschreibung ändern. Dies wird aber nicht empfohlen.",
"poll_remove": "Entfernen",
"poll_edit": "Bearbeiten",
"poll_locked": "Gesperrt",
@ -327,9 +327,9 @@
"fm_openParent": "Im Ordner zeigen",
"fm_noname": "Dokument ohne Titel",
"fm_emptyTrashDialog": "Soll der Papierkorb wirklich geleert werden?",
"fm_removeSeveralPermanentlyDialog": "Bist du sicher, dass du diese {0} Elemente endgültig aus deinem CryptDrive entfernen möchtest?",
"fm_removeSeveralPermanentlyDialog": "Bist du sicher, dass du diese {0} Elemente aus deinem CryptDrive entfernen möchtest? Sie verbleiben in den CryptDrives anderer Benutzer, die sie gespeichert haben.",
"fm_removePermanentlyNote": "Wenn du fortfährst, werden deine eigenen Pads vom Server entfernt.",
"fm_removePermanentlyDialog": "Bist du sicher, dass du dieses Element endgültig aus deinem CryptDrive entfernen möchtest?",
"fm_removePermanentlyDialog": "Bist du sicher, dass du dieses Element aus deinem CryptDrive entfernen möchtest? Es verbleibt in den CryptDrives anderer Benutzer, die es gespeichert haben.",
"fm_removeSeveralDialog": "Bist du sicher, dass du diese {0} Elemente in den Papierkorb verschieben möchtest?",
"fm_removeDialog": "Bist du sicher, dass du {0} in den Papierkorb verschieben möchtest?",
"fm_deleteOwnedPad": "Bist du sicher, dass du dieses Pad endgültig vom Server löschen möchtest?",
@ -373,9 +373,9 @@
"fc_open": "Öffnen",
"fc_open_ro": "Öffnen (schreibgeschützt)",
"fc_delete": "In den Papierkorb verschieben",
"fc_delete_owned": "Vom Server löschen",
"fc_delete_owned": "Zerstören",
"fc_restore": "Wiederherstellen",
"fc_remove": "Aus deinem CryptDrive entfernen",
"fc_remove": "Entfernen",
"fc_remove_sharedfolder": "Entfernen",
"fc_empty": "Den Papierkorb leeren",
"fc_prop": "Eigenschaften",
@ -605,7 +605,7 @@
"blog": "Blog",
"topbar_whatIsCryptpad": "Was ist CryptPad",
"whatis_title": "Was ist CryptPad",
"whatis_collaboration": "Effektive und und leichte Zusammenarbeit",
"whatis_collaboration": "Effektive und leichte Zusammenarbeit",
"whatis_collaboration_p1": "Mit CryptPad kannst du kollaborative Dokumente erstellen, um Notizen und Ideen gemeinsam zu bearbeiten. Wenn du dich registrierst und einloggst, bekommst du die Möglichkeit, Dateien hochzuladen und Ordner einzurichten, um alle deine Dokumente zu organisieren. Als registrierter Nutzer erhältst du kostenlos 50 MB Speicherplatz.",
"whatis_collaboration_p2": "Du kannst den Zugang zu einem CryptPad-Dokument teilen, indem du einfach den entsprechenden Link teilst. Du kannst auch einen <em>schreibgeschützten</em> Zugang erstellen, um die Ergebnisse deiner Arbeit zu teilen, während du sie noch bearbeitest.",
"whatis_collaboration_p3": "Du kannst Rich-Text Dokumente mit dem <a href=\"http://ckeditor.com/\">CKEditor</a> erstellen. Außerdem kannst du Markdown-Dokumente erstellen, die in Echtzeit formatiert angezeigt werden, während du tippst. Du kannst auch die Umfrage-Anwendung verwenden, um Termine unter mehrere Teilnehmern zu abzustimmen.",
@ -720,7 +720,7 @@
},
"register": {
"q": "Weisst der Server mehr über mich, wenn ich registriere?",
"a": "Wir verlangen nicht Deine Emailadresse und der Server kennt Benutzername und Passwort auch dann nicht, wenn du dich registrierst. Statt dessen generiert das Registrierungs- und Anmeldeformular ein Schlüsselpaar mit deiner Eingabe. Nur der öffentliche Schlüssel dieses Schlüsselpaars wird zum Server geschickt. Mit diesem öffentlichen Schlüssel könenn wir z.B. die Menge der Daten, die du benutzt, kontrollieren, denn jeder Benutzer hat eine beschränkte Quota.<br><br> Wir benutzen die <em>Rückmeldung</em>s-Funktion, um den Server zu informieren, dass jemand mit deiner IP ein Konto registriert hat. Damit können wir messen, wie viele Benutzer CryptPad Konten registrieren und aus welchen Regionen. Somit können wir erfahren, welche Sprache besseren Support braucht.<br><br> Wenn Du registrierst, erstellst Du einen öffentlichen Schlüssel, der benutzt wird, um den Server zu informieren, dass er Dokumente auch dann nicht löschen sollte, wenn sie nicht aktiv benutzt werden. Diese Information zeigt dem Server, wie Du CryptPad benutzt und dieses System erlaubt uns, die Dokumente zu löschen, wofür sich keiner mehr interessiert."
"a": "Wir verlangen deine E-Mail-Adresse nicht, und der Server kennt Benutzername und Passwort auch dann nicht, wenn du dich registrierst. Statt dessen generiert das Registrierungs- und Anmeldeformular ein Schlüsselpaar aus deiner Eingabe. Nur der öffentliche Schlüssel dieses Schlüsselpaars wird zum Server geschickt. Mit diesem öffentlichen Schlüssel können wir z.B. die Menge der Daten, die du benutzt, kontrollieren, denn jeder Benutzer hat eine beschränkte Quota.<br><br>Wir benutzen die <em>Rückmeldung</em>s-Funktion, um den Server zu informieren, dass jemand mit deiner IP ein Konto registriert hat. Damit können wir messen, wie viele Benutzer CryptPad Konten registrieren und aus welchen Regionen. Somit können wir erfahren, welche Sprache besser unterstützt werden sollte.<br><br>Registrierte Benutzer informieren den Server, dass er Dokumente im CryptDrive auch dann nicht wegen Inaktivität löschen sollte, wenn sie nicht aktiv benutzt werden."
},
"other": {
"q": "Was können andere Benutzer über mich erfahren?",
@ -770,7 +770,7 @@
},
"remove": {
"q": "Ich habe ein Dokument aus meinem CryptDrive gelöscht, aber der Inhalt ist noch verfügbar. Wie kann ich es entfernen?",
"a": "Nur <em>eigene Pads</em>, die im Februar 2018 eingeführt wurden, können gelöscht werden und zwar nur von deren Eigentümer (der Benutzer, der das Dokument ursprünglich erstellt hat). Wenn du nicht der Eigentümer des Pads bist, musst du den Eigentümer bitten, dass er dieses für dich löscht. Bei Pads, deren Eigentümer du bist, kannst du <strong>auf das Pad in deinem CryptDrive rechtsklicken</em> und <strong>Vom Server löschen</strong> wählen."
"a": "Nur <em>eigene Pads</em>, die im Februar 2018 eingeführt wurden, können gelöscht werden und zwar nur von deren <em>Eigentümer</em> (der Benutzer, der das Dokument ursprünglich erstellt hat). Wenn du nicht der Eigentümer des Pads bist, musst du den Eigentümer bitten, dass er dieses für dich löscht. Bei Pads, deren Eigentümer du bist, kannst du <strong>auf das Pad in deinem CryptDrive rechtsklicken</strong> und <strong>Vom Server löschen</strong> wählen."
},
"forget": {
"q": "Was passiert, wenn ich mein Passwort vergesse?",
@ -797,7 +797,7 @@
"title": "Andere Fragen",
"pay": {
"q": "Wieso soll ich zahlen, wenn so viele Funktionen sowieso kostenfrei sind?",
"a": "Wir geben Unterstützern zusätzlichen Speicherplatz sowie die Möglichkeit, die Speicherplatzbegrenzung ihrer Kontakte zu erhöhen (<a href='https://accounts.cryptpad.fr/#/faq' target='_blank'>erfahre mehr</a>).<br><br>Über diese diese kurzfristigen Vorteile hinaus kannst du, wenn du ein Premiumangebot annimmst, die aktive Weiterentwicklung von CryptPad fördern. Das beinhaltet, Fehler zu beseitigen, neue Funktionen zu umzusetzen und Installationen von CryptPad auf eigenen Servern zu erleichtern. Zusätzlich hilfst du, anderen Anbietern zu beweisen, dass Leute datenschutzfreundliche Technologien unterstützen. Wir hoffen, dass Geschäftsmodelle, die auf dem Verkauf von Benutzerdaten basieren, letztendlich der Vergangenheit angehören werden.<br><br>Außerdem glauben wir, dass es gut ist, die Funktionen von CryptPad kostenfrei anzubieten. Denn jeder verdient persönlichen Datenschutz und nicht nur Personen mit hohem Einkommen. Durch deine Unterstützung hilfst du uns, zu ermöglichen, dass auch Menschen mit geringerem Einkommen diese grundlegenden Funktionen genießen können, ohne dass ein Preisetikett daran klebt."
"a": "Wir geben Unterstützern zusätzlichen Speicherplatz sowie die Möglichkeit, die Speicherplatzbegrenzung ihrer Kontakte zu erhöhen (<a href='https://accounts.cryptpad.fr/#/faq' target='_blank'>erfahre mehr</a>).<br><br>Über diese kurzfristigen Vorteile hinaus kannst du, wenn du ein Premiumangebot annimmst, die aktive Weiterentwicklung von CryptPad fördern. Das beinhaltet, Fehler zu beseitigen, neue Funktionen umzusetzen und Installationen von CryptPad auf eigenen Servern zu erleichtern. Zusätzlich hilfst du, anderen Anbietern zu beweisen, dass Benutzer datenschutzfreundliche Technologien unterstützen. Wir hoffen, dass Geschäftsmodelle, die auf dem Verkauf von Benutzerdaten basieren, letztendlich der Vergangenheit angehören werden.<br><br>Außerdem glauben wir, dass es gut ist, die Funktionen von CryptPad kostenfrei anzubieten. Denn jeder verdient persönlichen Datenschutz und nicht nur Personen mit hohem Einkommen. Durch deine Unterstützung hilfst du uns, zu ermöglichen, dass auch Menschen mit geringerem Einkommen diese grundlegenden Funktionen genießen können, ohne dass ein Preisetikett daran klebt."
},
"goal": {
"q": "Was ist euer Ziel?",
@ -906,7 +906,7 @@
"shortcuts": "Mit den Tastenkürzeln `Strg+b`, `Strg+i` und `Strg+u` formatierst du Text fett, kursiv oder unterstrichen.",
"indent": "In nummerierten Listen oder Aufzählungen kannst du mit Tab und Umschalt+Tab den Einzug erhöhen oder reduzieren."
},
"feedback_about": "Wenn du das liest, fragst du dich wahrscheinlich, weshalb dein Browser bei der der Ausführung mancher Aktionen Anfragen an Webseiten sendet",
"feedback_about": "Wenn du das liest, fragst du dich wahrscheinlich, weshalb dein Browser bei der Ausführung mancher Aktionen Anfragen an Webseiten sendet",
"feedback_privacy": "Wir respektieren deine Datenschutz, aber gleichzeitig wollen wir, dass die Benutzung von CryptPad sehr leicht ist. Deshalb wollen wir erfahren, welche Funktion am wichtigsten für unsere Benutzer ist, indem wir diese mit einer genauen Parameterbeschreibung anfordern.",
"feedback_optout": "Wenn du das nicht möchtest, kannst du es in <a href='/settings/'>deinen Einstellungen</a> deaktivieren",
"creation_404": "Dieses Pad existiert nicht mehr. Benutze das folgende Formular, um ein neues Pad zu erstellen.",
@ -1395,5 +1395,7 @@
"oo_refresh": "Neu laden",
"support_category": "Wähle eine Kategorie",
"oo_refreshText": "Dieses Dokument wurde aktualisiert",
"support_formCategoryError": "Fehler: Kategorie ist leer"
"support_formCategoryError": "Fehler: Kategorie ist leer",
"fm_restricted": "Du kannst auf dieses Element nicht zugreifen",
"fm_emptyTrashOwned": "In deinem Papierkorb sind Dokumente gespeichert, deren Eigentümer du bist. Du kannst sie aus deinem CryptDrive <b>entfernen</b> oder für alle Benutzer <b>zerstören</b>."
}

@ -331,9 +331,9 @@
"fm_openParent": "Montrer dans le dossier",
"fm_noname": "Document sans titre",
"fm_emptyTrashDialog": "Êtes-vous sûr de vouloir vider la corbeille ?",
"fm_removeSeveralPermanentlyDialog": "Êtes-vous sûr de vouloir supprimer ces {0} éléments de votre CryptDrive de manière permanente ?",
"fm_removeSeveralPermanentlyDialog": "Êtes vous sûr de vouloir supprimer ces {0} éléments ? Ils resteront dans le drive des autres utilisateurs qui les ont stockés.",
"fm_removePermanentlyNote": "Les pads dont vous êtes le propriétaire seront supprimés du serveur.",
"fm_removePermanentlyDialog": "Êtes-vous sûr de vouloir supprimer cet élément de votre CryptDrive de manière permanente ?",
"fm_removePermanentlyDialog": "Êtes-vous sûr de vouloir supprimer cet élément ? Il restera dans le drive des autres utilisateurs qui l'ont stocké.",
"fm_deleteOwnedPad": "Êtes-vous sûr de vouloir supprimer définitivement ce pad du serveur ?",
"fm_deleteOwnedPads": "Êtes-vous sûr de vouloir supprimer définitivement ces pads du serveur ?",
"fm_restoreDialog": "Êtes-vous sûr de vouloir restaurer {0} à son emplacement précédent ?",
@ -380,9 +380,9 @@
"fc_expandAll": "Développer tout",
"fc_collapseAll": "Réduire tout",
"fc_delete": "Déplacer vers la corbeille",
"fc_delete_owned": "Supprimer du serveur",
"fc_delete_owned": "Détruire",
"fc_restore": "Restaurer",
"fc_remove": "Supprimer de votre CryptDrive",
"fc_remove": "Supprimer",
"fc_remove_sharedfolder": "Supprimer",
"fc_empty": "Vider la corbeille",
"fc_prop": "Propriétés",
@ -1394,5 +1394,7 @@
"support_attachments": "Pièces jointes",
"oo_refreshText": "Ce document a été mis à jour",
"oo_refresh": "Recharger",
"support_category": "Choisir une catégorie"
"support_category": "Choisir une catégorie",
"fm_restricted": "Vous n'avez pas accès à cet élément",
"fm_emptyTrashOwned": "Votre corbeille contient des documents dont vous êtes propriétaire. Vous pouvez les <b>supprimer</b> de votre drive uniquement, ou les <b>détruire</b> pour tous les utilisateurs."
}

@ -338,9 +338,9 @@
"fm_openParent": "Show in folder",
"fm_noname": "Untitled Document",
"fm_emptyTrashDialog": "Are you sure you want to empty the trash?",
"fm_removeSeveralPermanentlyDialog": "Are you sure you want to permanently remove these {0} elements from your CryptDrive?",
"fm_removeSeveralPermanentlyDialog": "Are you sure you want to remove these {0} items from your drive? They will remain in the drives of other users who have stored them.",
"fm_removePermanentlyNote": "Owned pads will be removed from the server if you continue.",
"fm_removePermanentlyDialog": "Are you sure you want to permanently remove that element from your CryptDrive?",
"fm_removePermanentlyDialog": "Are you sure you want to remove this item from your drive? It will remain in the drives of other users who have stored it.",
"fm_removeSeveralDialog": "Are you sure you want to move these {0} elements to the trash?",
"fm_removeDialog": "Are you sure you want to move {0} to the trash?",
"fm_deleteOwnedPad": "Are you sure you want to permanently remove this pad from the server?",
@ -389,9 +389,9 @@
"fc_expandAll": "Expand All",
"fc_collapseAll": "Collapse All",
"fc_delete": "Move to trash",
"fc_delete_owned": "Delete from the server",
"fc_delete_owned": "Destroy",
"fc_restore": "Restore",
"fc_remove": "Remove from your CryptDrive",
"fc_remove": "Remove",
"fc_remove_sharedfolder": "Remove",
"fc_empty": "Empty the trash",
"fc_prop": "Properties",
@ -1395,5 +1395,7 @@
"oo_refresh": "Refresh",
"oo_refreshText": "This document has been updated",
"support_category": "Choose a category",
"support_formCategoryError": "Error: category is empty"
"support_formCategoryError": "Error: category is empty",
"fm_emptyTrashOwned": "Your trash contains documents you own. You can <b>remove</b> them from your drive only, or <b>destroy</b> them for all users.",
"fm_restricted": "You do not have access"
}

@ -42,6 +42,7 @@ define([
if (!drive || !drive.sharedFolders) {
return void cb();
}
var r = drive.restrictedFolders = drive.restrictedFolders || {};
var oldIds = Object.keys(folders);
nThen(function (waitFor) {
Object.keys(drive.sharedFolders).forEach(function (fId) {
@ -60,7 +61,11 @@ define([
APP.newSharedFolder = null;
}
}
if (newObj && newObj.deprecated) {
if (newObj && newObj.restricted) {
r[fId] = drive.sharedFolders[fId];
if (!r[fId].title) { r[fId].title = r[fId].lastTitle; }
}
if (newObj && (newObj.deprecated /*|| newObj.restricted*/)) {
delete folders[fId];
delete drive.sharedFolders[fId];
if (manager && manager.folders) {

@ -45,11 +45,16 @@ define([
};
window.addEventListener('message', onMsg);
}).nThen(function (/*waitFor*/) {
var afterSecrets = function (Cryptpad, Utils, secret, cb) {
var afterSecrets = function (Cryptpad, Utils, secret, cb, sframeChan) {
var _hash = hash.slice(1);
if (_hash && Utils.LocalStore.isLoggedIn()) {
// Add a shared folder!
Cryptpad.addSharedFolder(null, secret, function (id) {
if (id && typeof(id) === "object" && id.error) {
sframeChan.event("EV_RESTRICTED_ERROR");
return;
}
window.CryptPad_newSharedFolder = id;
// Clear the hash now that the secrets have been generated

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -725,6 +725,7 @@ define([
var fmConfig = {
ckeditor: editor,
dropArea: $inner,
body: $('body'),
onUploaded: function(ev, data) {
var parsed = Hash.parsePadUrl(data.url);
@ -743,7 +744,24 @@ define([
editor.widgets.initOn(element, 'mediatag');
}
};
window.APP.FM = framework._.sfCommon.createFileManager(fmConfig);
var FM = window.APP.FM = framework._.sfCommon.createFileManager(fmConfig);
editor.on('paste', function (ev) {
try {
var files = ev.data.dataTransfer._.files;
files.forEach(function (f) {
FM.handleFile(f);
});
// If the paste data contains files, don't use the ckeditor default handlers
// ==> they would try to include either a remote image URL or a base64 image
if (files.length) {
ev.cancel();
ev.preventDefault();
}
} catch (e) {
console.error(e);
}
});
framework._.sfCommon.getAttribute(['pad', 'spellcheck'], function(err, data) {
if (framework.isReadOnly()) { return; }

@ -2,6 +2,7 @@ define([
'jquery',
'/common/toolbar.js',
'/common/common-util.js',
'/common/common-hash.js',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/common/common-realtime.js',
@ -32,6 +33,7 @@ define([
$,
Toolbar,
Util,
Hash,
nThen,
SFCommon,
CommonRealtime,
@ -940,6 +942,25 @@ define([
$('.CodeMirror').parent().prepend(markdownTb.toolbar);
APP.toolbar.$bottomL.append(markdownTb.button);
// Add drop and paste handlers
var privateData = metadataMgr.getPrivateData();
var fmConfig = {
dropArea: $('.CodeMirror'),
body: $('body'),
onUploaded: function (ev, data) {
var parsed = Hash.parsePadUrl(data.url);
var secret = Hash.getSecrets('file', parsed.hash, data.password);
var fileHost = privateData.fileHost || privateData.origin;
var src = fileHost + Hash.getBlobPathFromHex(secret.channel);
var key = Hash.encodeBase64(secret.keys.cryptKey);
var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + key + '"></media-tag>';
APP.editor.replaceSelection(mt);
}
};
common.createFileManager(fmConfig);
SframeCM.handleImagePaste(APP.editor);
// Initialize author name for comments.
// Disable name modification for logged in users
var $cName = APP.$addComment.find('.cp-app-poll-comments-add-name')

@ -203,7 +203,7 @@ define([
$p.append($('<br>'));
$('<button>', {
title: Messages.filePickerButton,
'class': '',
'class': 'btn',
style: 'font-size: 17px',
id: 'cp-app-slide-options-bg'
}).click(function () {
@ -221,16 +221,18 @@ define([
}
});
}).text(Messages.printBackgroundButton).appendTo($p);
$p.append($('<br>'));
}
$p.append($('<br>'));
var $bgValue = $('<div>').appendTo($p);
var $bgValue = $('<div class="cp-background-selected">').appendTo($p);
var refreshValue = function () {
$bgValue.html('');
if (slideOptionsTmp.background && slideOptionsTmp.background.name) {
$bgValue.append(Messages._getKey("printBackgroundValue", [slideOptionsTmp.background.name]));
$('<button>', {
$('<span>', {
'class': 'fa fa-times',
title: Messages.printBackgroundRemove
title: Messages.printBackgroundRemove,
style: 'margin-left: 5px'
}).click(function () {
slideOptionsTmp.background = false;
refreshValue();
@ -247,7 +249,6 @@ define([
};
}
$p.append($('<br>'));
$p.append($('<br>'));
// CSS
$('<label>', {'for': 'cp-app-slide-options-css'}).text(Messages.printCSS).appendTo($p);
$p.append($('<br>'));
@ -307,8 +308,8 @@ define([
h = UI.listenForKeys(todo, todoCancel);
var $nav = $('<nav>').appendTo($div);
$('<button>', {'class': 'cancel'}).text(Messages.cancelButton).appendTo($nav).click(todoCancel);
$('<button>', {'class': 'ok'}).text(Messages.settings_save).appendTo($nav).click(todo);
$('<button>', {'class': 'btn cancel'}).text(Messages.cancelButton).appendTo($nav).click(todoCancel);
$('<button>', {'class': 'btn ok'}).text(Messages.settings_save).appendTo($nav).click(todo);
return $container;
};
@ -484,15 +485,19 @@ define([
CodeMirror.init(framework.localChange, framework._.title, framework._.toolbar);
CodeMirror.configureTheme(common);
var drawSlides = Util.throttle(function (content) {
Slide.update(content);
}, 400);
framework.onContentUpdate(function (newContent) {
CodeMirror.contentUpdate(newContent);
Slide.update(newContent.content);
drawSlides(newContent.content);
});
framework.setContentGetter(function () {
CodeMirror.removeCursors();
var content = CodeMirror.getContent();
Slide.update(content.content);
drawSlides(content.content);
return content;
});

@ -154,18 +154,22 @@ define([
updateFontSize();
};
Slide.update = function (content) {
Slide.update = function (content, force) {
updateFontSize();
if (!content) { content = ''; }
var old = Slide.content;
Slide.content = content.replace(/\n\s*\-\-\-\s*\n/g, '\n\n'+separator+'\n\n');
if (old !== Slide.content) {
if (force || old !== Slide.content) {
draw(Slide.index);
return;
}
change(Slide.lastIndex, Slide.index);
};
DiffMd.onPluginLoaded(function () {
Slide.update(Slide.content, true);
});
Slide.left = function () {
console.log('left');
Slide.lastIndex = Slide.index;

@ -55,6 +55,7 @@ define([
if (!drive || !drive.sharedFolders) {
return void cb();
}
var r = drive.restrictedFolders = drive.restrictedFolders || {};
var oldIds = Object.keys(folders);
nThen(function (waitFor) {
Object.keys(drive.sharedFolders).forEach(function (fId) {
@ -65,7 +66,11 @@ define([
sframeChan.query('Q_DRIVE_GETOBJECT', {
sharedFolder: fId
}, waitFor(function (err, newObj) {
if (newObj && newObj.deprecated) {
if (newObj && newObj.restricted) {
r[fId] = drive.sharedFolders[fId];
if (!r[fId].title) { r[fId].title = r[fId].lastTitle; }
}
if (newObj && (newObj.deprecated || newObj.restricted)) {
delete folders[fId];
delete drive.sharedFolders[fId];
if (manager && manager.folders) {
@ -252,10 +257,7 @@ define([
});
if (active === 'drive') {
APP.$rightside.addClass('cp-rightside-drive');
APP.$leftside.on('mouseover', function() {
APP.$leftside.addClass('cp-leftside-narrow');
APP.$leftside.off('mouseover');
});
} else {
APP.$rightside.removeClass('cp-rightside-drive');
APP.$leftside.removeClass('cp-leftside-narrow');

@ -491,10 +491,10 @@ define([
}, [
h('button#cp-app-whiteboard-clear.btn.btn-danger', Messages.canvas_clear), ' ',
h('div.cp-whiteboard-type', [
h('button.brush.fa.fa-paint-brush.btn-primary', {title: Messages.canvas_brush}),
h('button.move.fa.fa-arrows', {title: Messages.canvas_select}),
h('button.btn.brush.fa.fa-paint-brush.btn-primary', {title: Messages.canvas_brush}),
h('button.btn.move.fa.fa-arrows', {title: Messages.canvas_select}),
]),
h('button.fa.fa-trash#cp-app-whiteboard-delete', {
h('button.btn.fa.fa-trash#cp-app-whiteboard-delete', {
disabled: 'disabled',
title: Messages.canvas_delete
}),

Loading…
Cancel
Save