fix merge conflict

pull/1/head
ansuz 5 years ago
commit bf1332451f

@ -30,7 +30,7 @@
"secure-fabric.js": "secure-v1.7.9",
"hyperjson": "~1.4.0",
"chainpad-crypto": "^0.2.0",
"chainpad-listmap": "^0.5.0",
"chainpad-listmap": "^0.7.0",
"chainpad": "^5.1.0",
"file-saver": "1.3.1",
"alertifyjs": "1.0.11",
@ -39,7 +39,7 @@
"less": "3.7.1",
"bootstrap": "^v4.0.0",
"diff-dom": "2.1.1",
"nthen": "^0.1.5",
"nthen": "0.1.7",
"open-sans-fontface": "^1.4.2",
"bootstrap-tokenfield": "^0.12.1",
"localforage": "^1.5.2",

@ -224,6 +224,12 @@ module.exports = {
* STORAGE
* ===================== */
/* By default the CryptPad server will run scheduled tasks every five minutes
* If you want to run scheduled tasks in a separate process (like a crontab)
* you can disable this behaviour by setting the following value to true
*/
disableIntegratedTasks: false,
/* Pads that are not 'pinned' by any registered user can be set to expire
* after a configurable number of days of inactivity (default 90 days).
* The value can be changed or set to false to remove expiration.

@ -25,4 +25,5 @@
<glyph unicode="&#xe90f;" glyph-name="code" d="M839.56 905.788h-655.119c-35.33 0-63.97-28.64-63.97-63.97v-787.637c0-35.33 28.64-63.97 63.97-63.97v0h655.119c35.33 0 63.97 28.64 63.97 63.97v0 787.637c0 35.33-28.64 63.97-63.97 63.97v0zM843.294 54.182c0-2.063-1.672-3.735-3.735-3.735v0h-655.119c-2.063 0-3.735 1.672-3.735 3.735v0 787.637c0 2.063 1.672 3.735 3.735 3.735v0h655.119c2.063 0 3.735-1.672 3.735-3.735v0zM445.741 514.259c0 0.036 0 0.078 0 0.121 0 5.928-2.817 11.198-7.185 14.545l-0.044 0.032c-3.983 2.691-8.892 4.295-14.175 4.295-4.094 0-7.962-0.963-11.392-2.675l0.148 0.067-184.2-86.618c-8.222-3.631-13.857-11.713-13.857-21.111 0-0.117 0.001-0.234 0.003-0.351v0.018c-0.012-0.283-0.019-0.616-0.019-0.95 0-9.595 5.609-17.881 13.727-21.756l0.145-0.062 182.874-86.618c3.48-1.929 7.624-3.084 12.033-3.132h0.015c0.048 0 0.104-0.001 0.16-0.001 5.069 0 9.747 1.674 13.511 4.5l-0.058-0.042c4.41 3.332 7.23 8.565 7.23 14.458 0 0.084-0.001 0.168-0.002 0.252v-0.013c-0.071 7.753-4.57 14.439-11.087 17.657l-0.116 0.052-159.021 75.174 159.503 75.174c6.763 2.889 11.483 9.349 11.805 16.947l0.001 0.039zM593.92 607.503c0.002 0.105 0.003 0.229 0.003 0.352 0 5.748-2.249 10.971-5.915 14.836l0.009-0.010c-3.681 4.147-9.026 6.747-14.977 6.747-0.029 0-0.057 0-0.086 0h0.004c-9.851-0.020-18.105-6.831-20.33-16l-0.029-0.143-101.798-317.32c-0.832-2.21-1.355-4.765-1.445-7.429l-0.001-0.040c0-0.049-0.001-0.106-0.001-0.164 0-5.808 2.246-11.091 5.916-15.028l-0.012 0.013c3.716-4.077 9.049-6.626 14.977-6.626 0.029 0 0.058 0 0.086 0h-0.004c4.647 0.168 8.845 1.966 12.066 4.836l-0.019-0.017c3.378 2.964 5.926 6.796 7.301 11.149l0.048 0.175 102.28 317.199c0.984 2.183 1.668 4.715 1.92 7.372l0.007 0.097zM795.106 444.024l-183.236 86.618c-3.456 2.029-7.611 3.228-12.047 3.228-5.294 0-10.189-1.707-14.164-4.601l0.069 0.048c-4.553-3.371-7.472-8.724-7.472-14.758 0-0.106 0.001-0.211 0.003-0.316v0.016c0.315-7.802 5.137-14.404 11.919-17.299l0.128-0.049 158.901-74.812-159.985-75.535c-6.524-3.171-10.945-9.741-10.963-17.345v-0.002c-0.001-0.071-0.002-0.155-0.002-0.24 0-5.892 2.82-11.126 7.184-14.425l0.045-0.033c3.706-2.784 8.384-4.458 13.453-4.458 0.056 0 0.113 0 0.169 0.001h-0.009c0.189-0.005 0.412-0.008 0.636-0.008 4.169 0 8.098 1.028 11.547 2.844l-0.136-0.065 183.959 86.98c8.264 3.938 13.873 12.223 13.873 21.819 0 0.334-0.007 0.667-0.020 0.998l0.002-0.047c0.002 0.099 0.002 0.216 0.002 0.333 0 9.398-5.634 17.48-13.71 21.053l-0.147 0.058z" />
<glyph unicode="&#xe910;" glyph-name="new-template" d="M840.764 886.152h-655.119c-35.33 0-63.97-28.64-63.97-63.97v0-787.637c0-35.33 28.64-63.97 63.97-63.97v0h655.119c35.33 0 63.97 28.64 63.97 63.97v0 787.637c0 35.33-28.64 63.97-63.97 63.97v0zM844.499 34.545c0-2.063-1.672-3.735-3.735-3.735v0h-655.119c-2.063 0-3.735 1.672-3.735 3.735v0 787.637c0 2.063 1.672 3.735 3.735 3.735h655.119c2.063 0 3.735-1.672 3.735-3.735v0zM643.915 466.071h-93.365v93.003c0 10.313-8.36 18.673-18.673 18.673v0h-37.346c-0.036 0-0.078 0-0.121 0-10.246 0-18.552-8.306-18.552-18.552 0-0.042 0-0.085 0-0.127v0.006-93.003h-93.365c-0.036 0-0.078 0-0.121 0-10.246 0-18.552-8.306-18.552-18.552 0-0.042 0-0.085 0-0.127v0.006-37.346c0-10.313 8.36-18.673 18.673-18.673v0h93.365v-93.967c0-0.036 0-0.078 0-0.121 0-10.246 8.306-18.552 18.552-18.552 0.042 0 0.085 0 0.127 0h37.34c10.313 0 18.673 8.36 18.673 18.673v0 93.606h93.365c10.285 0.068 18.605 8.388 18.673 18.666v37.352c0.002 0.108 0.003 0.234 0.003 0.361 0 10.313-8.36 18.673-18.673 18.673-0.001 0-0.002 0-0.004 0v0z" />
<glyph unicode="&#xe911;" glyph-name="palette" d="M408.6 950c-198.8-38.8-359-198.6-398.2-396.8-74-374 263.4-652.8 517.6-613.4 82.4 12.8 122.8 109.2 85 183.4-46.2 90.8 19.8 196.8 121.8 196.8h159.4c71.6 0 129.6 59.2 129.8 130.6-1 315.2-287.8 563.2-615.4 499.4zM192 320c-35.4 0-64 28.6-64 64s28.6 64 64 64 64-28.6 64-64-28.6-64-64-64zM256 576c-35.4 0-64 28.6-64 64s28.6 64 64 64 64-28.6 64-64-28.6-64-64-64zM512 704c-35.4 0-64 28.6-64 64s28.6 64 64 64 64-28.6 64-64-28.6-64-64-64zM768 576c-35.4 0-64 28.6-64 64s28.6 64 64 64 64-28.6 64-64-28.6-64-64-64z" />
<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" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

@ -1,9 +1,9 @@
@font-face {
font-family: 'cptools';
src:
url('fonts/cptools.ttf?yr9e7c') format('truetype'),
url('fonts/cptools.woff?yr9e7c') format('woff'),
url('fonts/cptools.svg?yr9e7c#cptools') format('svg');
url('fonts/cptools.ttf?cljhos') format('truetype'),
url('fonts/cptools.woff?cljhos') format('woff'),
url('fonts/cptools.svg?cljhos#cptools') format('svg');
font-weight: normal;
font-style: normal;
}
@ -24,6 +24,9 @@
-moz-osx-font-smoothing: grayscale;
}
.cptools-folder-upload:before {
content: "\e912";
}
.cptools-folder-no-color:before {
content: "\e900";
}

@ -169,6 +169,28 @@ define([], function () {
height: 100%;
background: #5cb85c;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(1800deg);
}
}
.cp-spinner {
display: inline-block;
box-sizing: border-box;
width: 80px;
height: 80px;
border: 11px solid white;
border-radius: 50%;
border-top-color: transparent;
animation: spin infinite 3s;
animation-timing-function: cubic-bezier(.6,0.15,0.4,0.85);
}
*/}).toString().slice(14, -3);
var urlArgs = window.location.href.replace(/^.*\?([^\?]*)$/, function (all, x) { return x; });
var elem = document.createElement('div');
@ -182,7 +204,7 @@ define([], function () {
'</div>',
'<div class="cp-loading-container">',
'<div class="cp-loading-spinner-container">',
'<span class="fa fa-spinner fa-pulse fa-4x fa-fw"></span>',
'<span class="cp-spinner"></span>',
'</div>',
'<p id="cp-loading-message"></p>',
'</div>'

@ -26,7 +26,8 @@ var getLanguage = messages._getLanguage = function () {
var l = getBrowserLanguage();
// Edge returns 'fr-FR' --> transform it to 'fr' and check again
return map[l] ? l :
(map[l.split('-')[0]] ? l.split('-')[0] : 'en');
(map[l.split('-')[0]] ? l.split('-')[0] :
(map[l.split('_')[0]] ? l.split('_')[0] : 'en'));
};
var language = getLanguage();

@ -103,7 +103,7 @@ define([
])*/
])
]),
h('div.cp-version-footer', "CryptPad v2.25.0 (Zebra)")
h('div.cp-version-footer', "CryptPad v3.0.0 (Aurochs)")
]);
};
@ -146,7 +146,7 @@ define([
//h('a.nav-item.nav-link', { href: '/what-is-cryptpad.html'}, Msg.topbar_whatIsCryptpad), // Moved the FAQ
//h('a.nav-item.nav-link', { href: '/faq.html'}, Msg.faq_link),
h('a.nav-item.nav-link', { href: 'https://blog.cryptpad.fr/'}, Msg.blog),
h('a.nav-item.nav-link', { href: '/features.html'}, Msg.features),
h('a.nav-item.nav-link', { href: '/features.html'}, Msg.pricing),
h('a.nav-item.nav-link', { href: '/privacy.html'}, Msg.privacy),
//h('a.nav-item.nav-link', { href: '/contact.html'}, Msg.contact),
//h('a.nav-item.nav-link', { href: '/about.html'}, Msg.about),

@ -21,14 +21,14 @@ define([
target: '_blank',
rel: 'noopener noreferrer'
}, h('button.cp-features-register-button', Msg.features_f_subscribe));
$(premiumButton).click(function (e) {
/*$(premiumButton).click(function (e) {
if (LocalStore.isLoggedIn()) { return; }
// Not logged in: go to /login with a redirect to this page
e.preventDefault();
e.stopPropagation();
sessionStorage.redirectTo = '/features.html';
window.location.href = '/login/';
});
});*/
return h('div#cp-main', [
Pages.infopageTopbar(),
h('div.container-fluid.cp_cont_features',[
@ -43,6 +43,10 @@ define([
h('div.card-body',[
h('h3.text-center',Msg.features_anon)
]),
h('div.card-body.cp-pricing',[
h('div.text-center', '0€'),
h('div.text-center', Msg.features_noData),
]),
h('ul.list-group.list-group-flush',
['apps', 'core', 'file0', 'cryptdrive0', 'storage0'].map(function (f) {
return h('li.list-group-item', [
@ -61,6 +65,10 @@ define([
h('div.card-body',[
h('h3.text-center',Msg.features_registered)
]),
h('div.card-body.cp-pricing',[
h('div.text-center', '0€'),
h('div.text-center', Msg.features_noData),
]),
h('ul.list-group.list-group-flush', [
['anon', 'social', 'file1', 'cryptdrive1', 'devices', 'storage1'].map(function (f) {
return h('li.list-group-item', [
@ -87,6 +95,13 @@ define([
h('div.card-body',[
h('h3.text-center',Msg.features_premium)
]),
h('div.card-body.cp-pricing',[
h('div.text-center', h('a', {
href: accounts.upgradeURL,
target: '_blank'
}, Msg._getKey('features_pricing', ['5', '10', '15']))),
h('div.text-center', Msg.features_emailRequired),
]),
h('ul.list-group.list-group-flush', [
['reg', 'storage2', 'support', 'supporter'].map(function (f) {
return h('li.list-group-item', [

@ -22,14 +22,13 @@
}
}
.dropdown-toggle {
transform: rotate(270deg);
margin-left: 1rem;
float: right;
}
.dropdown-menu {
top: -0.7rem;
left: 100%;
&.left {
left: -10rem;
}
}
}
a {

@ -1,3 +1,4 @@
@import (reference) "./browser.less";
@import (reference) './colortheme-all.less';
@import (reference) './modal.less';
@ -10,24 +11,35 @@
#cp-fileupload {
.modal_base();
position: absolute;
left: 10vw; right: 10vw;
right: 10vw;
bottom: 10vh;
opacity: 0.9;
box-sizing: border-box;
z-index: 1000000; //Z file upload table container
display: none;
#cp-fileupload-table {
width: 80vw;
tr:nth-child(1) {
background-color: darken(@colortheme_modal-bg, 20%);
td {
font-weight: bold;
padding: 0.25em;
&:nth-child(4), &:nth-child(5) {
text-align: center;
}
color: darken(@colortheme_drive-bg, 10%);
@media screen and (max-width: @browser_media-medium-screen) {
left: 5vw; right: 5vw; bottom: 5vw;
}
.cp-fileupload-header {
display: flex;
background-color: darken(@colortheme_modal-bg, 10%);
font-weight: bold;
.cp-fileupload-header-title {
padding: 0.25em 0.5em;
flex-grow: 1;
}
.cp-fileupload-header-close {
padding: 0.25em 0.5em;
cursor: pointer;
&:hover {
background-color: rgba(0,0,0,0.1);
}
}
}
#cp-fileupload-table {
width: 100%;
@upload_pad_h: 0.25em;
@upload_pad_v: 0.5em;
@ -35,27 +47,55 @@
padding: @upload_pad_h @upload_pad_v;
}
.cp-fileupload-table-link {
display: flex;
align-items: center;
white-space: nowrap;
max-width: 30vw;
margin: 0px @upload_pad_v;
.fa {
margin-top: 4px;
margin-right: 5px;
}
.cp-fileupload-table-name {
overflow: hidden;
text-overflow: ellipsis;
}
&[href]:hover {
text-decoration: none;
.cp-fileupload-table-name {
text-decoration: underline;
}
}
}
.cp-fileupload-table-progress {
width: 25%;
min-width: 12em;
max-width: 16em;
position: relative;
text-align: center;
box-sizing: border-box;
}
.cp-fileupload-table-progress-container {
position: relative;
}
.cp-fileupload-table-progressbar {
position: absolute;
width: 0px;
left: @upload_pad_v;
top: @upload_pad_h; bottom: @upload_pad_h;
background-color: rgba(0,0,255,0.3);
height: 100%;
background-color: #dddddd;
z-index: -1; //Z file upload progress container
}
.cp-fileupload-table-cancel { text-align: center; }
.fa.cancel {
color: rgb(255, 0, 115);
.cp-fileupload-table-cancel {
text-align: center;
padding: 0px;
&:not(.success):not(.cancelled):hover {
background-color: rgba(0,0,0,0.1);
}
.fa {
padding: @upload_pad_h @upload_pad_v;
&.fa-times {
cursor: pointer;
}
}
}
}
}

@ -44,6 +44,13 @@
text-overflow: ellipsis;
}
}
div.plain-text-reader {
background: #f3f3f3;
padding: 10px;
color: black;
text-align: left;
}
}
.markdown_preformatted-code (@color: #333) {

@ -1,4 +1,5 @@
@import (reference) "./colortheme-all.less";
@import (reference) "./avatar.less";
.notifications_main() {
--LessLoader_require: LessLoader_currentFile();
@ -53,6 +54,19 @@
}
}
}
.cp-notifications-requestedit-verified {
display: flex;
align-items: center;
&> span.cp-avatar {
.avatar_main(30px);
}
&> span {
margin-right: 10px;
}
&> p {
margin: 0;
}
}
}

@ -47,6 +47,18 @@
h3 {
margin: 0;
}
&.cp-pricing {
div {
font-size: 1.2em;
color: @cryptpad_color_blue;
&:first-child {
font-weight: bold;
}
&:last-child {
font-size: 1em;
}
}
}
}
}
h3 {

@ -5,10 +5,24 @@
const nThen = require('nthen');
const Nacl = require('tweetnacl');
const Crypto = require('crypto');
const Once = require("./lib/once");
const Meta = require("./lib/metadata");
let Log;
const now = function () { return (new Date()).getTime(); };
/* getHash
* this function slices off the leading portion of a message which is
most likely unique
* these "hashes" are used to identify particular messages in a channel's history
* clients store "hashes" either in memory or in their drive to query for new messages:
* when reconnecting to a pad
* when connecting to chat or a mailbox
* thus, we can't change this function without invalidating client data which:
* is encrypted clientside
* can't be easily migrated
* don't break it!
*/
const getHash = function (msg) {
if (typeof(msg) !== 'string') {
Log.warn('HK_GET_HASH', 'getHash() called on ' + typeof(msg) + ': ' + msg);
@ -25,6 +39,18 @@ const tryParse = function (str) {
}
};
/* sliceCpIndex
returns a list of all checkpoints which might be relevant for a client connecting to a session
* if there are two or fewer checkpoints, return everything you have
* if there are more than two
* return at least two
* plus any more which were received within the last 100 messages
This is important because the additional history is what prevents
clients from forking on checkpoints and dropping forked history.
*/
const sliceCpIndex = function (cpIndex, line) {
// Remove "old" checkpoints (cp sent before 100 messages ago)
const minLine = Math.max(0, (line - 100));
@ -36,6 +62,20 @@ const sliceCpIndex = function (cpIndex, line) {
return start.concat(end);
};
const isMetadataMessage = function (parsed) {
return Boolean(parsed && parsed.channel);
};
// validateKeyStrings supplied by clients must decode to 32-byte Uint8Arrays
const isValidValidateKeyString = function (key) {
try {
return typeof(key) === 'string' &&
Nacl.util.decodeBase64(key).length === Nacl.sign.publicKeyLength;
} catch (e) {
return false;
}
};
module.exports.create = function (cfg) {
const rpc = cfg.rpc;
const tasks = cfg.tasks;
@ -44,7 +84,7 @@ module.exports.create = function (cfg) {
Log.silly('HK_LOADING', 'LOADING HISTORY_KEEPER MODULE');
const historyKeeperKeys = {};
const metadata_cache = {};
const HISTORY_KEEPER_ID = Crypto.randomBytes(8).toString('hex');
Log.verbose('HK_ID', 'History keeper ID: ' + HISTORY_KEEPER_ID);
@ -53,54 +93,122 @@ module.exports.create = function (cfg) {
let STANDARD_CHANNEL_LENGTH, EPHEMERAL_CHANNEL_LENGTH;
const setConfig = function (config) {
STANDARD_CHANNEL_LENGTH = config.STANDARD_CHANNEL_LENGTH;
EPHEMERAL_CHANNEL_LENGTH = config.EPHEMERAL_CHANNEl_LENGTH;
EPHEMERAL_CHANNEL_LENGTH = config.EPHEMERAL_CHANNEL_LENGTH;
sendMsg = config.sendMsg;
};
/* computeIndex
can call back with an error or a computed index which includes:
* cpIndex:
* array including any checkpoints pushed within the last 100 messages
* processed by 'sliceCpIndex(cpIndex, line)'
* offsetByHash:
* a map containing message offsets by their hash
* this is for every message in history, so it could be very large...
* except we remove offsets from the map if they occur before the oldest relevant checkpoint
* size: in bytes
* metadata:
* validationKey
* expiration time
* owners
* ??? (anything else we might add in the future)
* line
* the number of messages in history
* including the initial metadata line, if it exists
*/
const computeIndex = function (channelName, cb) {
const cpIndex = [];
let messageBuf = [];
let validateKey;
let metadata;
let i = 0;
store.readMessagesBin(channelName, 0, (msgObj, rmcb) => {
let msg;
i++;
if (!validateKey && msgObj.buff.indexOf('validateKey') > -1) {
metadata = msg = tryParse(msgObj.buff.toString('utf8'));
if (typeof msg === "undefined") { return rmcb(); }
if (msg.validateKey) {
validateKey = historyKeeperKeys[channelName] = msg;
return rmcb();
const ref = {};
const CB = Once(cb);
const offsetByHash = {};
let size = 0;
nThen(function (w) {
// iterate over all messages in the channel log
// old channels can contain metadata as the first message of the log
// remember metadata the first time you encounter it
// otherwise index important messages in the log
store.readMessagesBin(channelName, 0, (msgObj, readMore) => {
let msg;
// keep an eye out for the metadata line if you haven't already seen it
// but only check for metadata on the first line
if (!i && !metadata && msgObj.buff.indexOf('{') === 0) {
i++; // always increment the message counter
msg = tryParse(msgObj.buff.toString('utf8'));
if (typeof msg === "undefined") { return readMore(); }
// validate that the current line really is metadata before storing it as such
if (isMetadataMessage(msg)) {
metadata = msg;
return readMore();
}
}
}
if (msgObj.buff.indexOf('cp|') > -1) {
msg = msg || tryParse(msgObj.buff.toString('utf8'));
if (typeof msg === "undefined") { return rmcb(); }
if (msg[2] === 'MSG' && msg[4].indexOf('cp|') === 0) {
cpIndex.push({
offset: msgObj.offset,
line: i
});
messageBuf = [];
i++;
if (msgObj.buff.indexOf('cp|') > -1) {
msg = msg || tryParse(msgObj.buff.toString('utf8'));
if (typeof msg === "undefined") { return readMore(); }
// cache the offsets of checkpoints if they can be parsed
if (msg[2] === 'MSG' && msg[4].indexOf('cp|') === 0) {
cpIndex.push({
offset: msgObj.offset,
line: i
});
// we only want to store messages since the latest checkpoint
// so clear the buffer every time you see a new one
messageBuf = [];
}
}
}
messageBuf.push(msgObj);
return rmcb();
}, (err) => {
if (err && err.code !== 'ENOENT') { return void cb(err); }
const offsetByHash = {};
let size = 0;
messageBuf.forEach((msgObj) => {
const msg = tryParse(msgObj.buff.toString('utf8'));
if (typeof msg === "undefined") { return; }
if (msg[0] === 0 && msg[2] === 'MSG' && typeof(msg[4]) === 'string') {
offsetByHash[getHash(msg[4])] = msgObj.offset;
// if it's not metadata or a checkpoint then it should be a regular message
// store it in the buffer
messageBuf.push(msgObj);
return readMore();
}, w((err) => {
if (err && err.code !== 'ENOENT') {
w.abort();
return void CB(err);
}
// There is a trailing \n at the end of the file
size = msgObj.offset + msgObj.buff.length + 1;
});
cb(null, {
// once indexing is complete you should have a buffer of messages since the latest checkpoint
// map the 'hash' of each message to its byte offset in the log, to be used for reconnecting clients
messageBuf.forEach((msgObj) => {
const msg = tryParse(msgObj.buff.toString('utf8'));
if (typeof msg === "undefined") { return; }
if (msg[0] === 0 && msg[2] === 'MSG' && typeof(msg[4]) === 'string') {
// msgObj.offset is API guaranteed by our storage module
// it should always be a valid positive integer
offsetByHash[getHash(msg[4])] = msgObj.offset;
}
// There is a trailing \n at the end of the file
size = msgObj.offset + msgObj.buff.length + 1;
});
}));
}).nThen(function (w) {
// create a function which will iterate over amendments to the metadata
const handler = Meta.createLineHandler(ref, Log.error);
// initialize the accumulator in case there was a foundational metadata line in the log content
if (metadata) { handler(void 0, metadata); }
// iterate over the dedicated metadata log (if it exists)
// proceed even in the event of a stream error on the metadata log
store.readDedicatedMetadata(channelName, handler, w(function (err) {
if (err) {
return void Log.error("DEDICATED_METADATA_ERROR", err);
}
}));
}).nThen(function () {
// when all is done, cache the metadata in memory
if (ref.index) { // but don't bother if no metadata was found...
metadata = metadata_cache[channelName] = ref.meta;
}
// and return the computed index
CB(null, {
// Only keep the checkpoints included in the last 100 messages
cpIndex: sliceCpIndex(cpIndex, i),
offsetByHash: offsetByHash,
@ -111,13 +219,61 @@ module.exports.create = function (cfg) {
});
};
/* getIndex
calls back with an error if anything goes wrong
or with a cached index for a channel if it exists
(along with metadata)
otherwise it calls back with the index computed by 'computeIndex'
as an added bonus:
if the channel exists but its index does not then it caches the index
*/
const indexQueues = {};
const getIndex = (ctx, channelName, cb) => {
const chan = ctx.channels[channelName];
if (chan && chan.index) { return void cb(undefined, chan.index); }
// if there is a channel in memory and it has an index cached, return it
if (chan && chan.index) {
// enforce async behaviour
return void setTimeout(function () {
cb(undefined, chan.index);
});
}
// if a call to computeIndex is already in progress for this channel
// then add the callback for the latest invocation to the queue
// and wait for it to complete
if (Array.isArray(indexQueues[channelName])) {
indexQueues[channelName].push(cb);
return;
}
// otherwise, make a queue for any 'getIndex' calls made before the following 'computeIndex' call completes
var queue = indexQueues[channelName] = (indexQueues[channelName] || [cb]);
computeIndex(channelName, (err, ret) => {
if (err) { return void cb(err); }
if (!Array.isArray(queue)) {
// something is very wrong if there's no callback array
return void Log.error("E_INDEX_NO_CALLBACK", channelName);
}
// clean up the queue that you're about to handle, but keep a local copy
delete indexQueues[channelName];
// this is most likely an unrecoverable filesystem error
if (err) {
// call back every pending function with the error
return void queue.forEach(function (_cb) {
_cb(err);
});
}
// cache the computed result if possible
if (chan) { chan.index = ret; }
cb(undefined, ret);
// call back every pending function with the result
queue.forEach(function (_cb) {
_cb(void 0, ret);
});
});
};
@ -128,24 +284,65 @@ module.exports.create = function (cfg) {
}
*/
/* storeMessage
* ctx
* channel id
* the message to store
* whether the message is a checkpoint
* optionally the hash of the message
* it's not always used, but we guard against it
* async but doesn't have a callback
* source of a race condition whereby:
* two messaages can be inserted
* two offsets can be computed using the total size of all the messages
* but the offsets don't correspond to the actual location of the newlines
* because the two actions were performed like ABba...
* the fix is to use callbacks and implement queueing for writes
* to guarantee that offset computation is always atomic with writes
*/
const storageQueues = {};
const storeQueuedMessage = function (ctx, queue, id) {
if (queue.length === 0) {
delete storageQueues[id];
return;
}
const first = queue.shift();
const msgBin = first.msg;
const optionalMessageHash = first.hash;
const isCp = first.isCp;
const storeMessage = function (ctx, channel, msg, isCp, maybeMsgHash) {
const msgBin = new Buffer(msg + '\n', 'utf8');
// Store the message first, and update the index only once it's stored.
// store.messageBin can be async so updating the index first may
// result in a wrong cpIndex
nThen((waitFor) => {
store.messageBin(channel.id, msgBin, waitFor(function (err) {
store.messageBin(id, msgBin, waitFor(function (err) {
if (err) {
waitFor.abort();
return void Log.error("HK_STORE_MESSAGE_ERROR", err.message);
Log.error("HK_STORE_MESSAGE_ERROR", err.message);
// this error is critical, but there's not much we can do at the moment
// proceed with more messages, but they'll probably fail too
// at least you won't have a memory leak
// TODO make it possible to respond to clients with errors so they know
// their message wasn't stored
storeQueuedMessage(ctx, queue, id);
return;
}
}));
}).nThen((waitFor) => {
getIndex(ctx, channel.id, waitFor((err, index) => {
getIndex(ctx, id, waitFor((err, index) => {
if (err) {
Log.warn("HK_STORE_MESSAGE_INDEX", err.stack);
// non-critical, we'll be able to get the channel index later
// proceed to the next message in the queue
storeQueuedMessage(ctx, queue, id);
return;
}
if (typeof (index.line) === "number") { index.line++; }
@ -161,60 +358,177 @@ module.exports.create = function (cfg) {
line: ((index.line || 0) + 1)
} /*:cp_index_item*/));
}
if (maybeMsgHash) { index.offsetByHash[maybeMsgHash] = index.size; }
if (optionalMessageHash) { index.offsetByHash[optionalMessageHash] = index.size; }
index.size += msgBin.length;
// handle the next element in the queue
storeQueuedMessage(ctx, queue, id);
}));
});
};
// Determine what we should store when a message a broadcasted to a channel
const storeMessage = function (ctx, channel, msg, isCp, optionalMessageHash) {
const id = channel.id;
const msgBin = new Buffer(msg + '\n', 'utf8');
if (Array.isArray(storageQueues[id])) {
return void storageQueues[id].push({
msg: msgBin,
hash: optionalMessageHash,
isCp: isCp,
});
}
const queue = storageQueues[id] = (storageQueues[id] || [{
msg: msgBin,
hash: optionalMessageHash,
}]);
storeQueuedMessage(ctx, queue, id);
};
var CHECKPOINT_PATTERN = /^cp\|(([A-Za-z0-9+\/=]+)\|)?/;
/* onChannelMessage
Determine what we should store when a message a broadcasted to a channel"
* ignores ephemeral channels
* ignores messages sent to expired channels
* rejects duplicated checkpoints
* validates messages to channels that have validation keys
* caches the id of the last saved checkpoint
* adds timestamps to incoming messages
* writes messages to the store
*/
const onChannelMessage = function (ctx, channel, msgStruct) {
// don't store messages if the channel id indicates that it's an ephemeral message
if (!channel.id || channel.id.length === EPHEMERAL_CHANNEL_LENGTH) { return; }
const isCp = /^cp\|/.test(msgStruct[4]);
if (historyKeeperKeys[channel.id] && historyKeeperKeys[channel.id].expire &&
historyKeeperKeys[channel.id].expire < +new Date()) {
return; // Don't store messages on expired channel
}
let id;
if (isCp) {
/*::if (typeof(msgStruct[4]) !== 'string') { throw new Error(); }*/
id = /cp\|(([A-Za-z0-9+\/=]+)\|)?/.exec(msgStruct[4]);
// id becomes either null or an array or results...
id = CHECKPOINT_PATTERN.exec(msgStruct[4]);
if (Array.isArray(id) && id[2] && id[2] === channel.lastSavedCp) {
// Reject duplicate checkpoints
return;
}
}
if (historyKeeperKeys[channel.id] && historyKeeperKeys[channel.id].validateKey) {
/*::if (typeof(msgStruct[4]) !== 'string') { throw new Error(); }*/
let signedMsg = (isCp) ? msgStruct[4].replace(/^cp\|(([A-Za-z0-9+\/=]+)\|)?/, '') : msgStruct[4];
signedMsg = Nacl.util.decodeBase64(signedMsg);
const validateKey = Nacl.util.decodeBase64(historyKeeperKeys[channel.id].validateKey);
const validated = Nacl.sign.open(signedMsg, validateKey);
if (!validated) {
Log.info("HK_SIGNED_MESSAGE_REJECTED", 'Channel '+channel.id);
return;
}
}
if (isCp) {
// WARNING: the fact that we only check the most recent checkpoints
// is a potential source of bugs if one editor has high latency and
// pushes a duplicate of an earlier checkpoint than the latest which
// has been pushed by editors with low latency
if (Array.isArray(id) && id[2]) {
// Store new checkpoint hash
channel.lastSavedCp = id[2];
let metadata;
nThen(function (w) {
// getIndex (and therefore the latest metadata)
getIndex(ctx, channel.id, w(function (err, index) {
if (err) {
w.abort();
return void Log.error('CHANNEL_MESSAGE_ERROR', err);
}
if (!index.metadata) {
// if there's no channel metadata then it can't be an expiring channel
// nor can we possibly validate it
return;
}
metadata = index.metadata;
if (metadata.expire && metadata.expire < +new Date()) {
// don't store message sent to expired channels
w.abort();
return;
// TODO if a channel expired a long time ago but it's still here, remove it
}
// if there's no validateKey present skip to the next block
if (!metadata.validateKey) { return; }
// trim the checkpoint indicator off the message if it's present
let signedMsg = (isCp) ? msgStruct[4].replace(CHECKPOINT_PATTERN, '') : msgStruct[4];
// convert the message from a base64 string into a Uint8Array
// FIXME this can fail and the client won't notice
signedMsg = Nacl.util.decodeBase64(signedMsg);
// FIXME this can blow up
// TODO check that that won't cause any problems other than not being able to append...
const validateKey = Nacl.util.decodeBase64(metadata.validateKey);
// validate the message
const validated = Nacl.sign.open(signedMsg, validateKey);
if (!validated) {
// don't go any further if the message fails validation
w.abort();
Log.info("HK_SIGNED_MESSAGE_REJECTED", 'Channel '+channel.id);
return;
}
}));
}).nThen(function () {
// do checkpoint stuff...
// 1. get the checkpoint id
// 2. reject duplicate checkpoints
if (isCp) {
// if the message is a checkpoint we will have already validated
// that it isn't a duplicate. remember its id so that we can
// repeat this process for the next incoming checkpoint
// WARNING: the fact that we only check the most recent checkpoints
// is a potential source of bugs if one editor has high latency and
// pushes a duplicate of an earlier checkpoint than the latest which
// has been pushed by editors with low latency
// FIXME
if (Array.isArray(id) && id[2]) {
// Store new checkpoint hash
channel.lastSavedCp = id[2];
}
}
}
msgStruct.push(now());
storeMessage(ctx, channel, JSON.stringify(msgStruct), isCp, getHash(msgStruct[4]));
// add the time to the message
msgStruct.push(now());
// storeMessage
storeMessage(ctx, channel, JSON.stringify(msgStruct), isCp, getHash(msgStruct[4]));
});
};
/* dropChannel
* exported as API
* used by chainpad-server/NetfluxWebsocketSrv.js
* cleans up memory structures which are managed entirely by the historyKeeper
* the netflux server manages other memory in ctx.channels
*/
const dropChannel = function (chanName) {
delete historyKeeperKeys[chanName];
delete metadata_cache[chanName];
};
/* getHistoryOffset
returns a number representing the byte offset from the start of the log
for whatever history you're seeking.
query by providing a 'lastKnownHash',
which is really just a string of the first 64 characters of an encrypted message.
OR by -1 which indicates that we want the full history (byte offset 0)
OR nothing, which indicates that you want whatever messages the historyKeeper deems relevant
(typically the last few checkpoints)
this function embeds a lot of the history keeper's logic:
0. if you passed -1 as the lastKnownHash it means you want the complete history
* I'm not sure why you'd need to call this function if you know it will return 0 in this case...
* it has a side-effect of filling the index cache if it's empty
1. if you provided a lastKnownHash and that message does not exist in the history:
* either the client has made a mistake or the history they knew about no longer exists
* call back with EINVAL
2. if you did not provide a lastKnownHash
* and there are fewer than two checkpoints:
* return 0 (read from the start of the file)
* and there are two or more checkpoints:
* return the offset of the earliest checkpoint which 'sliceCpIndex' considers relevant
3. if you did provide a lastKnownHash
* read through the log until you find the hash that you're looking for
* call back with either the byte offset of the message that you found OR
* -1 if you didn't find it
*/
const getHistoryOffset = (ctx, channelName, lastKnownHash, cb /*:(e:?Error, os:?number)=>void*/) => {
// lastKnownhash === -1 means we want the complete history
if (lastKnownHash === -1) { return void cb(null, 0); }
@ -223,8 +537,17 @@ module.exports.create = function (cfg) {
getIndex(ctx, channelName, waitFor((err, index) => {
if (err) { waitFor.abort(); return void cb(err); }
// Check last known hash
// check if the "hash" the client is requesting exists in the index
const lkh = index.offsetByHash[lastKnownHash];
// we evict old hashes from the index as new checkpoints are discovered.
// if someone connects and asks for a hash that is no longer relevant,
// we tell them it's an invalid request. This is because of the semantics of "GET_HISTORY"
// which is only ever used when connecting or reconnecting in typical uses of history...
// this assumption should hold for uses by chainpad, but perhaps not for other uses cases.
// EXCEPT: other cases don't use checkpoints!
// clients that are told that their request is invalid should just make another request
// without specifying the hash, and just trust the server to give them the relevant data.
// QUESTION: does this mean mailboxes are causing the server to store too much stuff in memory?
if (lastKnownHash && typeof(lkh) !== "number") {
waitFor.abort();
return void cb(new Error('EINVAL'));
@ -250,12 +573,20 @@ module.exports.create = function (cfg) {
offset = lkh;
}));
}).nThen((waitFor) => {
// if offset is less than zero then presumably the channel has no messages
// returning falls through to the next block and therefore returns -1
if (offset !== -1) { return; }
store.readMessagesBin(channelName, 0, (msgObj, rmcb, abort) => {
// do a lookup from the index
// FIXME maybe we don't need this anymore?
// otherwise we have a non-negative offset and we can start to read from there
store.readMessagesBin(channelName, 0, (msgObj, readMore, abort) => {
// tryParse return a parsed message or undefined
const msg = tryParse(msgObj.buff.toString('utf8'));
if (typeof msg === "undefined") { return rmcb(); }
// if it was undefined then go onto the next message
if (typeof msg === "undefined") { return readMore(); }
if (typeof(msg[4]) !== 'string' || lastKnownHash !== getHash(msg[4])) {
return void rmcb();
return void readMore();
}
offset = msgObj.offset;
abort();
@ -267,6 +598,15 @@ module.exports.create = function (cfg) {
});
};
/* getHistoryAsync
* finds the appropriate byte offset from which to begin reading using 'getHistoryOffset'
* streams through the rest of the messages, safely parsing them and returning the parsed content to the handler
* calls back when it has reached the end of the log
Used by:
* GET_HISTORY
*/
const getHistoryAsync = (ctx, channelName, lastKnownHash, beforeHash, handler, cb) => {
let offset = -1;
nThen((waitFor) => {
@ -280,15 +620,24 @@ module.exports.create = function (cfg) {
}).nThen((waitFor) => {
if (offset === -1) { return void cb(new Error("could not find offset")); }
const start = (beforeHash) ? 0 : offset;
store.readMessagesBin(channelName, start, (msgObj, rmcb, abort) => {
store.readMessagesBin(channelName, start, (msgObj, readMore, abort) => {
if (beforeHash && msgObj.offset >= offset) { return void abort(); }
handler(tryParse(msgObj.buff.toString('utf8')), rmcb);
handler(tryParse(msgObj.buff.toString('utf8')), readMore);
}, waitFor(function (err) {
return void cb(err);
}));
});
};
/* getOlderHistory
* allows clients to query for all messages until a known hash is read
* stores all messages in history as they are read
* can therefore be very expensive for memory
* should probably be converted to a streaming interface
Used by:
* GET_HISTORY_RANGE
*/
const getOlderHistory = function (channelName, oldestKnownHash, cb) {
var messageBuffer = [];
var found = false;
@ -298,10 +647,11 @@ module.exports.create = function (cfg) {
let parsed = tryParse(msgStr);
if (typeof parsed === "undefined") { return; }
if (parsed.validateKey) {
historyKeeperKeys[channelName] = parsed;
return;
}
// identify classic metadata messages by their inclusion of a channel.
// and don't send metadata, since:
// 1. the user won't be interested in it
// 2. this metadata is potentially incomplete/incorrect
if (isMetadataMessage(parsed)) { return; }
var content = parsed[4];
if (typeof(content) !== 'string') { return; }
@ -329,13 +679,20 @@ module.exports.create = function (cfg) {
};
*/
/* historyKeeperBroadcast
* uses API from the netflux server to send messages to every member of a channel
* sendMsg runs in a try-catch and drops users if sending a message fails
*/
const historyKeeperBroadcast = function (ctx, channel, msg) {
let chan = ctx.channels[channel] || (([] /*:any*/) /*:Chan_t*/);
chan.forEach(function (user) {
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(msg)]);
});
};
/* onChannelCleared
* broadcasts to all clients in a channel if that channel is deleted
*/
const onChannelCleared = function (ctx, channel) {
historyKeeperBroadcast(ctx, channel, {
error: 'ECLEARED',
@ -351,13 +708,29 @@ module.exports.create = function (cfg) {
});
});
delete ctx.channels[channel];
delete historyKeeperKeys[channel];
delete metadata_cache[channel];
};
// Check if the selected channel is expired
// If it is, remove it from memory and broadcast a message to its members
const onChannelMetadataChanged = function (ctx, channel) {
channel = channel;
};
/* checkExpired
* synchronously returns true or undefined to indicate whether the channel is expired
* according to its metadata
* has some side effects:
* closes the channel via the store.closeChannel API
* and then broadcasts to all channel members that the channel has expired
* removes the channel from the netflux-server's in-memory cache
* removes the channel metadata from history keeper's in-memory cache
FIXME the boolean nature of this API should be separated from its side effects
*/
const checkExpired = function (ctx, channel) {
if (channel && channel.length === STANDARD_CHANNEL_LENGTH && historyKeeperKeys[channel] &&
historyKeeperKeys[channel].expire && historyKeeperKeys[channel].expire < +new Date()) {
if (channel && channel.length === STANDARD_CHANNEL_LENGTH && metadata_cache[channel] &&
metadata_cache[channel].expire && metadata_cache[channel].expire < +new Date()) {
store.closeChannel(channel, function () {
historyKeeperBroadcast(ctx, channel, {
error: 'EEXPIRED',
@ -365,12 +738,25 @@ module.exports.create = function (cfg) {
});
});
delete ctx.channels[channel];
delete historyKeeperKeys[channel];
delete metadata_cache[channel];
return true;
}
return;
};
/* onDirectMessage
* exported for use by the netflux-server
* parses and handles all direct messages directed to the history keeper
* check if it's expired and execute all the associated side-effects
* routes queries to the appropriate handlers
* GET_HISTORY
* GET_HISTORY_RANGE
* GET_FULL_HISTORY
* RPC
* if the rpc has special hooks that the history keeper needs to be aware of...
* execute them here...
*/
const onDirectMessage = function (ctx, seq, user, json) {
let parsed;
let channelName;
@ -386,7 +772,7 @@ module.exports.create = function (cfg) {
}
// If the requested history is for an expired channel, abort
// Note the if we don't have the keys for that channel in historyKeeperKeys, we'll
// Note the if we don't have the keys for that channel in metadata_cache, we'll
// have to abort later (once we know the expiration time)
if (checkExpired(ctx, parsed[1])) { return; }
@ -396,35 +782,31 @@ module.exports.create = function (cfg) {
// parsed[3] is the last known hash (optionnal)
sendMsg(ctx, user, [seq, 'ACK']);
channelName = parsed[1];
var validateKey = parsed[2];
var lastKnownHash = parsed[3];
var owners;
var expire;
if (parsed[2] && typeof parsed[2] === "object") {
validateKey = parsed[2].validateKey;
lastKnownHash = parsed[2].lastKnownHash;
owners = parsed[2].owners;
if (parsed[2].expire) {
expire = +parsed[2].expire * 1000 + (+new Date());
var config = parsed[2];
var metadata = {};
var lastKnownHash;
// clients can optionally pass a map of attributes
// if the channel already exists this map will be ignored
// otherwise it will be stored as the initial metadata state for the channel
if (config && typeof config === "object" && !Array.isArray(parsed[2])) {
lastKnownHash = config.lastKnownHash;
metadata = config.metadata || {};
if (metadata.expire) {
metadata.expire = +metadata.expire * 1000 + (+new Date());
}
}
metadata.channel = channelName;
// if the user sends us an invalid key, we won't be able to validate their messages
// so they'll never get written to the log anyway. Let's just drop their message
// on the floor instead of doing a bunch of extra work
// TODO send them an error message so they know something is wrong
if (metadata.validateKey && !isValidValidateKeyString(metadata.validateKey)) {
return void Log.error('HK_INVALID_KEY', metadata.validateKey);
}
nThen(function (waitFor) {
if (!tasks) { return; } // tasks are not supported
if (typeof(expire) !== 'number' || !expire) { return; }
// the fun part...
// the user has said they want this pad to expire at some point
tasks.write(expire, "EXPIRE", [ channelName ], waitFor(function (err) {
if (err) {
// if there is an error, we don't want to crash the whole server...
// just log it, and if there's a problem you'll be able to fix it
// at a later date with the provided information
Log.error('HK_CREATE_EXPIRE_TASK', err);
Log.info('HK_INVALID_EXPIRE_TASK', JSON.stringify([expire, 'EXPIRE', channelName]));
}
}));
}).nThen(function (waitFor) {
var w = waitFor();
/* unless this is a young channel, we will serve all messages from an offset
@ -438,39 +820,29 @@ module.exports.create = function (cfg) {
so, let's just fall through...
*/
if (err) { return w(); }
// it's possible that the channel doesn't have metadata
// but in that case there's no point in checking if the channel expired
// or in trying to send metadata, so just skip this block
if (!index || !index.metadata) { return void w(); }
// Store the metadata if we don't have it in memory
if (!historyKeeperKeys[channelName]) {
historyKeeperKeys[channelName] = index.metadata;
}
// And then check if the channel is expired. If it is, send the error and abort
// FIXME this is hard to read because 'checkExpired' has side effects
if (checkExpired(ctx, channelName)) { return void waitFor.abort(); }
// Send the metadata to the user
if (!lastKnownHash && index.cpIndex.length > 1) {
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(index.metadata)], w);
return;
}
w();
// always send metadata with GET_HISTORY requests
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(index.metadata)], w);
}));
}).nThen(() => {
let msgCount = 0;
let expired = false;
getHistoryAsync(ctx, channelName, lastKnownHash, false, (msg, cb) => {
// TODO compute lastKnownHash in a manner such that it will always skip past the metadata line?
getHistoryAsync(ctx, channelName, lastKnownHash, false, (msg, readMore) => {
if (!msg) { return; }
if (msg.validateKey) {
// If it is a young channel, this is the part where we get the metadata
// Check if the channel is expired and abort if it is.
if (!historyKeeperKeys[channelName]) { historyKeeperKeys[channelName] = msg; }
expired = checkExpired(ctx, channelName);
}
if (expired) { return void cb(); }
msgCount++;
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(msg)], cb);
// avoid sending the metadata message a second time
if (isMetadataMessage(msg) && metadata_cache[channelName]) { return readMore(); }
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(msg)], readMore);
}, (err) => {
// If the pad is expired, stop here, we've already sent the error message
if (expired) { return; }
if (err && err.code !== 'ENOENT') {
if (err.message !== 'EINVAL') { Log.error("HK_GET_HISTORY", err); }
const parsedMsg = {error:err.message, channel: channelName};
@ -478,24 +850,46 @@ module.exports.create = function (cfg) {
return;
}
// If this is a new channel, we need to store the metadata as
// the first message in the file
const chan = ctx.channels[channelName];
if (msgCount === 0 && !historyKeeperKeys[channelName] && chan && chan.indexOf(user) > -1) {
var key = {};
key.channel = channelName;
if (validateKey) {
key.validateKey = validateKey;
}
if (owners) {
key.owners = owners;
}
if (expire) {
key.expire = expire;
if (msgCount === 0 && !metadata_cache[channelName] && chan && chan.indexOf(user) > -1) {
metadata_cache[channelName] = metadata;
// the index will have already been constructed and cached at this point
// but it will not have detected any metadata because it hasn't been written yet
// this means that the cache starts off as invalid, so we have to correct it
if (chan && chan.index) { chan.index.metadata = metadata; }
// new channels will always have their metadata written to a dedicated metadata log
// but any lines after the first which are not amendments in a particular format will be ignored.
// Thus we should be safe from race conditions here if just write metadata to the log as below...
// TODO validate this logic
// otherwise maybe we need to check that the metadata log is empty as well
store.writeMetadata(channelName, JSON.stringify(metadata), function (err) {
if (err) {
// FIXME tell the user that there was a channel error?
return void Log.error('HK_WRITE_METADATA', {
channel: channelName,
error: err,
});
}
});
// write tasks
if(tasks && metadata.expire && typeof(metadata.expire) === 'number') {
// the fun part...
// the user has said they want this pad to expire at some point
tasks.write(metadata.expire, "EXPIRE", [ channelName ], function (err) {
if (err) {
// if there is an error, we don't want to crash the whole server...
// just log it, and if there's a problem you'll be able to fix it
// at a later date with the provided information
Log.error('HK_CREATE_EXPIRE_TASK', err);
Log.info('HK_INVALID_EXPIRE_TASK', JSON.stringify([metadata.expire, 'EXPIRE', channelName]));
}
});
}
historyKeeperKeys[channelName] = key;
storeMessage(ctx, chan, JSON.stringify(key), false, undefined);
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(key)]);
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(metadata)]);
}
// End of history message:
@ -551,9 +945,12 @@ module.exports.create = function (cfg) {
// parsed[2] is a validation key (optionnal)
// parsed[3] is the last known hash (optionnal)
sendMsg(ctx, user, [seq, 'ACK']);
getHistoryAsync(ctx, parsed[1], -1, false, (msg, cb) => {
// FIXME should we send metadata here too?
// none of the clientside code which uses this API needs metadata, but it won't hurt to send it (2019-08-22)
getHistoryAsync(ctx, parsed[1], -1, false, (msg, readMore) => {
if (!msg) { return; }
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(['FULL_HISTORY', msg])], cb);
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(['FULL_HISTORY', msg])], readMore);
}, (err) => {
let parsedMsg = ['FULL_HISTORY_END', parsed[1]];
if (err) {
@ -581,6 +978,15 @@ module.exports.create = function (cfg) {
if (msg[3] === 'CLEAR_OWNED_CHANNEL') {
onChannelCleared(ctx, msg[4]);
}
// FIXME METADATA CHANGE
if (msg[3] === 'SET_METADATA') { // or whatever we call the RPC????
// make sure we update our cache of metadata
// or at least invalidate it and force other mechanisms to recompute its state
// 'output' could be the new state as computed by rpc
onChannelMetadataChanged(ctx, msg[4]);
}
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify([parsed[0]].concat(output))]);
});
} catch (e) {

@ -0,0 +1,11 @@
// remove duplicate elements in an array
module.exports = function (O) {
// make a copy of the original array
var A = O.slice();
for (var i = 0; i < A.length; i++) {
for (var j = i + 1; j < A.length; j++) {
if (A[i] === A[j]) { A.splice(j--, 1); }
}
}
return A;
};

@ -0,0 +1,126 @@
var Meta = module.exports;
var deduplicate = require("./deduplicate");
/* Metadata fields:
* channel <STRING>
* validateKey <STRING>
* owners <ARRAY>
* ADD_OWNERS
* RM_OWNERS
* expire <NUMBER>
*/
var commands = {};
// ["ADD_OWNERS", ["7eEqelGso3EBr5jHlei6av4r9w2B9XZiGGwA1EgZ-5I="], 1561623438989]
commands.ADD_OWNERS = function (meta, args) {
// bail out if args isn't an array
if (!Array.isArray(args)) {
throw new Error('METADATA_INVALID_OWNERS');
}
// you shouldn't be able to get here if there are no owners
// because only an owner should be able to change the owners
if (!Array.isArray(meta.owners)) {
throw new Error("METADATA_NONSENSE_OWNERS");
}
args.forEach(function (owner) {
if (meta.owners.indexOf(owner) >= 0) { return; }
meta.owners.push(owner);
});
};
// ["RM_OWNERS", ["CrufexqXcY-z+eKJlEbNELVy5Sb7E-EAAEFI8GnEtZ0="], 1561623439989]
commands.RM_OWNERS = function (meta, args) {
// what are you doing if you don't have owners to remove?
if (!Array.isArray(args)) {
throw new Error('METADATA_INVALID_OWNERS');
}
// if there aren't any owners to start, this is also pointless
if (!Array.isArray(meta.owners)) {
throw new Error("METADATA_NONSENSE_OWNERS");
}
// remove owners one by one
// we assume there are no duplicates
args.forEach(function (owner) {
var index = meta.owners.indexOf(owner);
if (index < 0) { return; }
meta.owners.splice(index, 1);
});
};
// ["RESET_OWNERS", ["7eEqelGso3EBr5jHlei6av4r9w2B9XZiGGwA1EgZ-5I="], 1561623439989]
commands.RESET_OWNERS = function (meta, args) {
// expect a new array, even if it's empty
if (!Array.isArray(args)) {
throw new Error('METADATA_INVALID_OWNERS');
}
// assume there are owners to start
if (!Array.isArray(meta.owners)) {
throw new Error("METADATA_NONSENSE_OWNERS");
}
// overwrite the existing owners with the new one
meta.owners = deduplicate(args);
};
commands.UPDATE_EXPIRATION = function () {
throw new Error("E_NOT_IMPLEMENTED");
};
var handleCommand = function (meta, line) {
var command = line[0];
var args = line[1];
//var time = line[2];
if (typeof(commands[command]) !== 'function') {
throw new Error("METADATA_UNSUPPORTED_COMMAND");
}
commands[command](meta, args);
};
Meta.createLineHandler = function (ref, errorHandler) {
ref.meta = {};
ref.index = 0;
return function (err, line) {
if (err) {
return void errorHandler('METADATA_HANDLER_LINE_ERR', {
error: err,
index: ref.index,
line: JSON.stringify(line),
});
}
if (Array.isArray(line)) {
try {
handleCommand(ref.meta, line);
ref.index++;
} catch (err2) {
errorHandler("METADATA_COMMAND_ERR", {
error: err2.stack,
line: line,
});
}
return;
}
if (ref.index === 0 && typeof(line) === 'object') {
ref.index++;
// special case!
ref.meta = line;
return;
}
errorHandler("METADATA_HANDLER_WEIRDLINE", {
line: line,
index: ref.index++,
});
};
};

75
package-lock.json generated

@ -1,6 +1,6 @@
{
"name": "cryptpad",
"version": "2.25.0",
"version": "3.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -56,9 +56,9 @@
"optional": true
},
"async-limiter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
},
"balanced-match": {
"version": "1.0.0",
@ -99,9 +99,9 @@
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
},
"chainpad-server": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/chainpad-server/-/chainpad-server-3.0.2.tgz",
"integrity": "sha512-c5aEljVAapDKKs0+Rt2jymKAszm8X4ZeLFNJj1yxflwBqoh0jr8OANYvbfjtNaYFe2Wdflp/1i4gibYX4IMc+g==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/chainpad-server/-/chainpad-server-3.0.3.tgz",
"integrity": "sha512-NRfV7FFBEYy4ZVX7h0P5znu55X8v5K4iGWeMGihkfWZLKu70GmCPUTwpBCP79dUvnCToKEa4/e8aoSPcvZC8pA==",
"requires": {
"nthen": "^0.1.8",
"pull-stream": "^3.6.9",
@ -227,19 +227,25 @@
}
},
"dom-serializer": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz",
"integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==",
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.1.tgz",
"integrity": "sha512-sK3ujri04WyjwQXVoK4PU3y8ula1stq10GJZpqHIUgoGZdsGzAGu65BnU3d08aTVSvO7mGPZUc0wTEDL+qGE0Q==",
"dev": true,
"requires": {
"domelementtype": "^1.3.0",
"entities": "^1.1.1"
"domelementtype": "^2.0.1",
"entities": "^2.0.0"
},
"dependencies": {
"domelementtype": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
"integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==",
"dev": true
},
"entities": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
"integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==",
"dev": true
}
}
@ -458,9 +464,9 @@
}
},
"graceful-fs": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
"integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA=="
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz",
"integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q=="
},
"has-ansi": {
"version": "2.0.0",
@ -597,9 +603,9 @@
}
},
"jszip": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.1.tgz",
"integrity": "sha512-iCMBbo4eE5rb1VCpm5qXOAaUiRKRUKiItn8ah2YQQx9qymmSAY98eyQfioChEYcVQLh0zxJ3wS4A0mh90AVPvw==",
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.2.tgz",
"integrity": "sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA==",
"dev": true,
"requires": {
"lie": "~3.3.0",
@ -697,9 +703,9 @@
}
},
"lodash": {
"version": "4.17.14",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz",
"integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==",
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"dev": true
},
"lodash.clonedeep": {
@ -711,7 +717,8 @@
"lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"lodash.sortby": {
"version": "4.7.0",
@ -965,9 +972,9 @@
}
},
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
},
"promise": {
@ -997,9 +1004,9 @@
"optional": true
},
"pull-stream": {
"version": "3.6.12",
"resolved": "https://registry.npmjs.org/pull-stream/-/pull-stream-3.6.12.tgz",
"integrity": "sha512-+LO1XIVyTMmeoH26UHznpgrgX2npTVYccTkMpgk/EyiQjFt1FmoNm+w+/zMLuz9U3bpvT5sSUicMKEe/2JjgEA=="
"version": "3.6.14",
"resolved": "https://registry.npmjs.org/pull-stream/-/pull-stream-3.6.14.tgz",
"integrity": "sha512-KIqdvpqHHaTUA2mCYcLG1ibEbu/LCKoJZsBWyv9lSYtPkJPBq8m3Hxa103xHi6D2thj5YXa0TqK3L3GUkwgnew=="
},
"qs": {
"version": "6.5.2",
@ -1049,9 +1056,9 @@
"integrity": "sha1-lAFm0gfRDphhT+SSU60vCsAZ9+E="
},
"rimraf": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
"glob": "^7.1.3"

@ -1,7 +1,7 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
"version": "2.25.0",
"version": "3.0.0",
"license": "AGPL-3.0+",
"repository": {
"type": "git",
@ -11,7 +11,7 @@
"chainpad-server": "~3.0.2",
"express": "~4.16.0",
"fs-extra": "^7.0.0",
"nthen": "~0.1.0",
"nthen": "0.1.8",
"pull-stream": "^3.6.1",
"replify": "^1.2.0",
"saferphore": "0.0.1",

@ -17,6 +17,7 @@ const Saferphore = require("saferphore");
const nThen = require("nthen");
const getFolderSize = require("get-folder-size");
const Pins = require("./lib/pins");
const Meta = require("./lib/metadata");
var RPC = module.exports;
@ -313,22 +314,22 @@ var getFileSize = function (Env, channel, cb) {
});
};
var getMetadata = function (Env, channel, cb) {
if (!isValidId(channel)) { return void cb('INVALID_CHAN'); }
if (channel.length === 32) {
if (typeof(Env.msgStore.getChannelMetadata) !== 'function') {
return cb('GET_CHANNEL_METADATA_UNSUPPORTED');
}
if (channel.length !== 32) { return cb("INVALID_CHAN"); }
return void Env.msgStore.getChannelMetadata(channel, function (e, data) {
if (e) {
if (e.code === 'INVALID_METADATA') { return void cb(void 0, {}); }
return void cb(e.code);
}
cb(void 0, data);
});
}
var ref = {};
var lineHandler = Meta.createLineHandler(ref, Log.error);
return void Env.msgStore.readChannelMetadata(channel, lineHandler, function (err) {
if (err) {
// stream errors?
return void cb(err);
}
cb(void 0, ref.meta);
});
};
var getMultipleFileSize = function (Env, channels, cb) {
@ -802,18 +803,13 @@ var clearOwnedChannel = function (Env, channelId, unsafeKey, cb) {
return cb('INVALID_ARGUMENTS');
}
if (!(Env.msgStore && Env.msgStore.getChannelMetadata)) {
return cb('E_NOT_IMPLEMENTED');
}
Env.msgStore.getChannelMetadata(channelId, function (e, metadata) {
if (e) { return cb(e); }
getMetadata(Env, channelId, function (err, metadata) {
if (err) { return void cb(err); }
if (!(metadata && Array.isArray(metadata.owners))) { return void cb('E_NO_OWNERS'); }
// Confirm that the channel is owned by the user in question
if (metadata.owners.indexOf(unsafeKey) === -1) {
return void cb('INSUFFICIENT_PERMISSIONS');
}
// FIXME COLDSTORAGE
return void Env.msgStore.clearChannel(channelId, function (e) {
cb(e);
@ -822,6 +818,7 @@ var clearOwnedChannel = function (Env, channelId, unsafeKey, cb) {
};
var removeOwnedBlob = function (Env, blobId, unsafeKey, cb) {
// FIXME METADATA
var safeKey = escapeKeyCharacters(unsafeKey);
var safeKeyPrefix = safeKey.slice(0,3);
var blobPrefix = blobId.slice(0,2);
@ -891,17 +888,12 @@ var removeOwnedChannel = function (Env, channelId, unsafeKey, cb) {
return void removeOwnedBlob(Env, channelId, unsafeKey, cb);
}
if (!(Env.msgStore && Env.msgStore.removeChannel && Env.msgStore.getChannelMetadata)) {
return cb("E_NOT_IMPLEMENTED");
}
Env.msgStore.getChannelMetadata(channelId, function (e, metadata) {
if (e) { return cb(e); }
getMetadata(Env, channelId, function (err, metadata) {
if (err) { return void cb(err); }
if (!(metadata && Array.isArray(metadata.owners))) { return void cb('E_NO_OWNERS'); }
if (metadata.owners.indexOf(unsafeKey) === -1) {
return void cb('INSUFFICIENT_PERMISSIONS');
}
// if the admin has configured data retention...
// temporarily archive the file instead of removing it
if (Env.retainData) {
@ -1459,21 +1451,23 @@ var removeLoginBlock = function (Env, msg, cb) {
});
};
var ARRAY_LINE = /^\[/;
/* Files can contain metadata but not content
call back with true if the channel log has no content other than metadata
otherwise false
*/
var isNewChannel = function (Env, channel, cb) {
if (!isValidId(channel)) { return void cb('INVALID_CHAN'); }
if (channel.length !== 32) { return void cb('INVALID_CHAN'); }
var count = 0;
var done = false;
Env.msgStore.getMessages(channel, function (msg) {
if (done) { return; }
var parsed;
try {
parsed = JSON.parse(msg);
if (parsed && typeof(parsed) === 'object') { count++; }
if (count >= 2) {
if (typeof(msg) === 'string' && ARRAY_LINE.test(msg)) {
done = true;
cb(void 0, false); // it is not a new file
return void cb(void 0, false);
}
} catch (e) {
WARN('invalid message read from store', e);
@ -1722,7 +1716,7 @@ RPC.create = function (
respond(e, [null, size, null]);
});
case 'GET_METADATA':
return void getMetadata(Env, msg[1], function (e, data) {
return void getMetadata(Env, msg[1], function (e, data) { // FIXME METADATA
WARN(e, msg[1]);
respond(e, [null, data, null]);
});

@ -105,6 +105,18 @@ app.head(/^\/common\/feedback\.html/, function (req, res, next) {
}());
app.use(function (req, res, next) {
if (req.method === 'OPTIONS' && /\/blob\//.test(req.url)) {
console.log(req.url);
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range');
res.setHeader('Access-Control-Max-Age', 1728000);
res.setHeader('Content-Type', 'application/octet-stream; charset=utf-8');
res.setHeader('Content-Length', 0);
res.statusCode = 204;
return void res.end();
}
setHeaders(req, res);
if (/[\?\&]ver=[^\/]+$/.test(req.url)) { res.setHeader("Cache-Control", "max-age=31536000"); }
next();
@ -247,7 +259,7 @@ var historyKeeper;
var log;
// Initialize tasks, then rpc, then store, then history keeper and then start the server
// Initialize logging, the the store, then tasks, then rpc, then history keeper and then start the server
var nt = nThen(function (w) {
// set up logger
var Logger = require("./lib/log");
@ -261,13 +273,13 @@ var nt = nThen(function (w) {
config.store = _store;
}));
}).nThen(function (w) {
if (!config.enableTaskScheduling) { return; }
var Tasks = require("./storage/tasks");
Tasks.create(config, w(function (e, tasks) {
if (e) {
throw e;
}
config.tasks = tasks;
if (config.disableIntegratedTasks) { return; }
setInterval(function () {
tasks.runAll(function (err) {
if (err) {

@ -6,6 +6,7 @@ var Fse = require("fs-extra");
var Path = require("path");
var nThen = require("nthen");
var Semaphore = require("saferphore");
var Once = require("../lib/once");
const ToPull = require('stream-to-pull-stream');
const Pull = require('pull-stream');
@ -27,6 +28,30 @@ var mkArchivePath = function (env, channelId) {
return Path.join(env.archiveRoot, 'datastore', channelId.slice(0, 2), channelId) + '.ndjson';
};
var mkMetadataPath = function (env, channelId) {
return Path.join(env.root, channelId.slice(0, 2), channelId) + '.metadata.ndjson';
};
var mkArchiveMetadataPath = function (env, channelId) {
return Path.join(env.archiveRoot, 'datastore', channelId.slice(0, 2), channelId) + '.metadata.ndjson';
};
// pass in the path so we can reuse the same function for archived files
var channelExists = function (filepath, cb) {
Fs.stat(filepath, function (err, stat) {
if (err) {
if (err.code === 'ENOENT') {
// no, the file doesn't exist
return void cb(void 0, false);
}
return void cb(err);
}
if (!stat.isFile()) { return void cb("E_NOT_FILE"); }
return void cb(void 0, true);
});
};
// reads classic metadata from a channel log and aborts
var getMetadataAtPath = function (Env, path, cb) {
var remainder = '';
var stream = Fs.createReadStream(path, { encoding: 'utf8' });
@ -60,11 +85,6 @@ var getMetadataAtPath = function (Env, path, cb) {
stream.on('error', function (e) { complete(e); });
};
var getChannelMetadata = function (Env, channelId, cb) {
var path = mkPath(Env, channelId);
getMetadataAtPath(Env, path, cb);
};
var closeChannel = function (env, channelName, cb) {
if (!env.channels[channelName]) { return void cb(); }
try {
@ -77,6 +97,7 @@ var closeChannel = function (env, channelName, cb) {
}
};
// truncates a file to the end of its metadata line
var clearChannel = function (env, channelId, cb) {
var path = mkPath(env, channelId);
getMetadataAtPath(env, path, function (e, metadata) {
@ -106,6 +127,9 @@ var clearChannel = function (env, channelId, cb) {
});
};
/* readMessages is our classic method of reading messages from the disk
notably doesn't provide a means of aborting if you finish early
*/
var readMessages = function (path, msgHandler, cb) {
var remainder = '';
var stream = Fs.createReadStream(path, { encoding: 'utf8' });
@ -127,6 +151,104 @@ var readMessages = function (path, msgHandler, cb) {
stream.on('error', function (e) { complete(e); });
};
/* getChannelMetadata
reads only the metadata embedded in the first line of a channel log.
does not necessarily provide the most up to date metadata, as it
could have been amended
*/
var getChannelMetadata = function (Env, channelId, cb) {
var path = mkPath(Env, channelId);
// gets metadata embedded in a file
getMetadataAtPath(Env, path, cb);
};
// low level method for getting just the dedicated metadata channel
var getDedicatedMetadata = function (env, channelId, handler, cb) {
var metadataPath = mkMetadataPath(env, channelId);
readMessages(metadataPath, function (line) {
if (!line) { return; }
try {
var parsed = JSON.parse(line);
handler(null, parsed);
} catch (e) {
handler(e, line);
}
}, function (err) {
if (err) {
// ENOENT => there is no metadata log
if (err.code === 'ENOENT') { return void cb(); }
// otherwise stream errors?
return void cb(err);
}
cb();
});
};
/* readMetadata
fetches the classic format of the metadata from the channel log
if it is present, otherwise load the log of metadata amendments.
Requires a handler to process successive lines.
*/
var readMetadata = function (env, channelId, handler, cb) {
/*
Possibilities
1. there is no metadata because it's an old channel
2. there is metadata in the first line of the channel, but nowhere else
3. there is metadata in the first line of the channel as well as in a dedicated log
4. there is no metadata in the first line of the channel. Everything is in the dedicated log
How to proceed
1. load the first line of the channel and treat it as a metadata message if applicable
2. load the dedicated log and treat it as an update
*/
nThen(function (w) {
// returns the first line of a channel, parsed...
getChannelMetadata(env, channelId, w(function (err, data) {
if (err) {
// 'INVALID_METADATA' if it can't parse
// stream errors if anything goes wrong at a lower level
// ENOENT (no channel here)
return void handler(err);
}
// disregard anything that isn't a map
if (!data || typeof(data) !== 'object' || Array.isArray(data)) { return; }
// otherwise it's good.
handler(null, data);
}));
}).nThen(function () {
getDedicatedMetadata(env, channelId, handler, function (err) {
if (err) {
// stream errors?
return void cb(err);
}
cb();
});
});
};
// writeMetadata appends to the dedicated log of metadata amendments
var writeMetadata = function (env, channelId, data, cb) {
var path = mkMetadataPath(env, channelId);
Fse.mkdirp(Path.dirname(path), PERMISSIVE, function (err) {
if (err && err.code !== 'EEXIST') { return void cb(err); }
// TODO see if we can make this any faster by using something other than appendFile
Fs.appendFile(path, data + '\n', cb);
});
};
// transform a stream of arbitrarily divided data
// into a stream of buffers divided by newlines in the source stream
// TODO see if we could improve performance by using libnewline
const NEWLINE_CHR = ('\n').charCodeAt(0);
const mkBufferSplit = () => {
let remainder = null;
@ -160,6 +282,8 @@ const mkBufferSplit = () => {
}, Pull.flatten());
};
// return a streaming function which transforms buffers into objects
// containing the buffer and the offset from the start of the stream
const mkOffsetCounter = () => {
let offset = 0;
return Pull.map((buff) => {
@ -170,9 +294,13 @@ const mkOffsetCounter = () => {
});
};
// readMessagesBin asynchronously iterates over the messages in a channel log
// the handler for each message must call back to read more, which should mean
// that this function has a lower memory profile than our classic method
// of reading logs line by line.
// it also allows the handler to abort reading at any time
const readMessagesBin = (env, id, start, msgHandler, cb) => {
const stream = Fs.createReadStream(mkPath(env, id), { start: start });
// TODO get the channel and add the atime
let keepReading = true;
Pull(
ToPull.read(stream),
@ -187,8 +315,8 @@ const readMessagesBin = (env, id, start, msgHandler, cb) => {
);
};
// check if a file exists at $path
var checkPath = function (path, callback) {
// TODO check if we actually need to use stat at all
Fs.stat(path, function (err) {
if (!err) {
callback(undefined, true);
@ -208,31 +336,79 @@ var checkPath = function (path, callback) {
});
};
var removeChannel = function (env, channelName, cb) {
var filename = mkPath(env, channelName);
Fs.unlink(filename, cb);
var labelError = function (label, err) {
return label + (err.code ? "_" + err.code: '');
};
// pass in the path so we can reuse the same function for archived files
var channelExists = function (filepath, channelName, cb) {
Fs.stat(filepath, function (err, stat) {
if (err) {
if (err.code === 'ENOENT') {
// no, the file doesn't exist
return void cb(void 0, false);
/* removeChannel
fully deletes a channel log and any associated metadata
*/
var removeChannel = function (env, channelName, cb) {
var channelPath = mkPath(env, channelName);
var metadataPath = mkMetadataPath(env, channelName);
var CB = Once(cb);
var errors = 0;
nThen(function (w) {
Fs.unlink(channelPath, w(function (err) {
if (err) {
if (err.code === 'ENOENT') {
errors++;
return;
}
w.abort();
CB(labelError("E_CHANNEL_REMOVAL", err));
}
return void cb(err);
}));
Fs.unlink(metadataPath, w(function (err) {
if (err) {
if (err.code === 'ENOENT') {
errors++;
return;
} // proceed if there's no metadata to delete
w.abort();
CB(labelError("E_METADATA_REMOVAL", err));
}
}));
}).nThen(function () {
if (errors === 2) {
return void CB(labelError('E_REMOVE_CHANNEL', new Error("ENOENT")));
}
if (!stat.isFile()) { return void cb("E_NOT_FILE"); }
return void cb(void 0, true);
CB();
});
};
/* removeArchivedChannel
fully removes an archived channel log and any associated metadata
*/
var removeArchivedChannel = function (env, channelName, cb) {
var filename = mkArchivePath(env, channelName);
Fs.unlink(filename, cb);
var channelPath = mkArchivePath(env, channelName);
var metadataPath = mkArchiveMetadataPath(env, channelName);
var CB = Once(cb);
nThen(function (w) {
Fs.unlink(channelPath, w(function (err) {
if (err) {
w.abort();
CB(labelError("E_ARCHIVED_CHANNEL_REMOVAL", err));
}
}));
Fs.unlink(metadataPath, w(function (err) {
if (err) {
if (err.code === "ENOENT") { return; }
w.abort();
CB(labelError("E_ARCHIVED_METADATA_REMOVAL", err));
}
}));
}).nThen(function () {
CB();
});
};
// TODO implement a method of removing metadata that doesn't have a corresponding channel
var listChannels = function (root, handler, cb) {
// do twenty things at a time
var sema = Semaphore.create(20);
@ -255,15 +431,31 @@ var listChannels = function (root, handler, cb) {
var wait = w();
dirList.forEach(function (dir) {
sema.take(function (give) {
// TODO modify the asynchronous bits here to keep less in memory at any given time
// list a directory -> process its contents with semaphores until less than N jobs are running
// then list the next directory...
var nestedDirPath = Path.join(root, dir);
Fs.readdir(nestedDirPath, w(give(function (err, list) {
if (err) { return void handler(err); } // Is this correct?
list.forEach(function (item) {
// ignore things that don't match the naming pattern
if (/^\./.test(item) || !/[0-9a-fA-F]{32,}\.ndjson$/.test(item)) { return; }
// ignore hidden files
if (/^\./.test(item)) { return; }
// ignore anything that isn't channel or metadata
if (!/^[0-9a-fA-F]{32}(\.metadata?)*\.ndjson$/.test(item)) {
return;
}
if (!/^[0-9a-fA-F]{32}\.ndjson$/.test(item)) {
// this will catch metadata, which we want to ignore if
// the corresponding channel is present
if (list.indexOf(item.replace(/\.metadata/, '')) !== -1) { return; }
// otherwise fall through
}
var filepath = Path.join(nestedDirPath, item);
var channel = filepath.replace(/\.ndjson$/, '').replace(/.*\//, '');
var channel = filepath
.replace(/\.ndjson$/, '')
.replace(/\.metadata/, '')
.replace(/.*\//, '');
if ([32, 34].indexOf(channel.length) === -1) { return; }
// otherwise throw it on the pile
@ -296,6 +488,7 @@ var listChannels = function (root, handler, cb) {
// move a channel's log file from its current location
// to an equivalent location in the cold storage directory
var archiveChannel = function (env, channelName, cb) {
// TODO close channels before archiving them?
if (!env.retainData) {
return void cb("ARCHIVES_DISABLED");
}
@ -314,20 +507,106 @@ var archiveChannel = function (env, channelName, cb) {
// use Fse.move to move it, Fse makes paths to the directory when you use it.
// https://github.com/jprichardson/node-fs-extra/blob/HEAD/docs/move.md
Fse.move(currentPath, archivePath, { overwrite: true }, cb);
nThen(function (w) {
// move the channel log and abort if anything goes wrong
Fse.move(currentPath, archivePath, { overwrite: true }, w(function (err) {
if (err) {
// proceed to the next block to remove metadata even if there's no channel
if (err.code === 'ENOENT') { return; }
// abort and callback for other types of errors
w.abort();
return void cb(err);
}
}));
}).nThen(function (w) {
// archive the dedicated metadata channel
var metadataPath = mkMetadataPath(env, channelName);
var archiveMetadataPath = mkArchiveMetadataPath(env, channelName);
Fse.move(metadataPath, archiveMetadataPath, { overwrite: true, }, w(function (err) {
// there's no metadata to archive, so you're done!
if (err && err.code === "ENOENT") {
return void cb();
}
// there was an error archiving the metadata
if (err) {
return void cb(labelError("E_METADATA_ARCHIVAL", err));
}
// it was archived successfully
cb();
}));
});
};
// restore a channel and its metadata from the archive
// to the appropriate location in the live database
var unarchiveChannel = function (env, channelName, cb) {
// very much like 'archiveChannel' but in the opposite direction
// the file is currently archived
var currentPath = mkArchivePath(env, channelName);
var unarchivedPath = mkPath(env, channelName);
var channelPath = mkPath(env, channelName);
var metadataPath = mkMetadataPath(env, channelName);
// don't call the callback multiple times
var CB = Once(cb);
// if a file exists in the unarchived path, you probably don't want to clobber its data
// so unlike 'archiveChannel' we won't overwrite.
// Fse.move will call back with EEXIST in such a situation
Fse.move(currentPath, unarchivedPath, cb);
nThen(function (w) {
// if either metadata or a file exist in prod, abort
channelExists(channelPath, w(function (err, exists) {
if (err) {
w.abort();
return void CB(err);
}
if (exists) {
w.abort();
return CB('UNARCHIVE_CHANNEL_CONFLICT');
}
}));
channelExists(metadataPath, w(function (err, exists) {
if (err) {
w.abort();
return void CB(err);
}
if (exists) {
w.abort();
return CB("UNARCHIVE_METADATA_CONFLICT");
}
}));
}).nThen(function (w) {
// construct archive paths
var archiveChannelPath = mkArchivePath(env, channelName);
// restore the archived channel
Fse.move(archiveChannelPath, channelPath, w(function (err) {
if (err) {
w.abort();
return void CB(err);
}
}));
}).nThen(function (w) {
var archiveMetadataPath = mkArchiveMetadataPath(env, channelName);
// TODO validate that it's ok to move metadata non-atomically
// restore the metadata log
Fse.move(archiveMetadataPath, metadataPath, w(function (err) {
// if there's nothing to move, you're done.
if (err && err.code === 'ENOENT') {
return CB();
}
// call back with an error if something goes wrong
if (err) {
w.abort();
return void CB(labelError("E_METADATA_RESTORATION", err));
}
// otherwise it was moved successfully
CB();
}));
});
};
var flushUnusedChannels = function (env, cb, frame) {
@ -352,11 +631,34 @@ var flushUnusedChannels = function (env, cb, frame) {
cb();
};
/* channelBytes
calls back with an error or the size (in bytes) of a channel and its metadata
*/
var channelBytes = function (env, chanName, cb) {
var path = mkPath(env, chanName);
Fs.stat(path, function (err, stats) {
if (err) { return void cb(err); }
cb(undefined, stats.size);
var channelPath = mkPath(env, chanName);
var dataPath = mkMetadataPath(env, chanName);
var CB = Once(cb);
var channelSize = 0;
var dataSize = 0;
nThen(function (w) {
Fs.stat(channelPath, w(function (err, stats) {
if (err) {
if (err.code === 'ENOENT') { return; }
return void CB(err);
}
channelSize = stats.size;
}));
Fs.stat(dataPath, w(function (err, stats) {
if (err) {
if (err.code === 'ENOENT') { return; }
return void CB(err);
}
dataSize = stats.size;
}));
}).nThen(function () {
CB(void 0, channelSize + dataSize);
});
};
@ -450,6 +752,7 @@ var getChannel = function (
});
};
// write a message to the disk as raw bytes
const messageBin = (env, chanName, msgBin, cb) => {
getChannel(env, chanName, function (err, chan) {
if (!chan) {
@ -466,18 +769,19 @@ const messageBin = (env, chanName, msgBin, cb) => {
chan.writeStream.write(msgBin, function () {
/*::if (!chan) { throw new Error("Flow unreachable"); }*/
chan.onError.splice(chan.onError.indexOf(complete), 1);
chan.atime = +new Date();
if (!cb) { return; }
//chan.messages.push(msg);
chan.atime = +new Date(); // FIXME seems like odd behaviour that not passing a callback would result in not updating atime...
complete();
});
});
};
// append a string to a channel's log as a new line
var message = function (env, chanName, msg, cb) {
messageBin(env, chanName, new Buffer(msg + '\n', 'utf8'), cb);
};
// stream messages from a channel log
var getMessages = function (env, chanName, handler, cb) {
getChannel(env, chanName, function (err, chan) {
if (!chan) {
@ -499,6 +803,9 @@ var getMessages = function (env, chanName, handler, cb) {
errorState = true;
return void cb(err);
}
// is it really, though? what if we hit the limit of open channels
// and 'clean up' in the middle of reading a massive file?
// certainly unlikely
if (!chan) { throw new Error("impossible, flow checking"); }
chan.atime = +new Date();
cb();
@ -563,80 +870,124 @@ module.exports.create = function (
}));
}).nThen(function () {
cb({
readMessagesBin: (channelName, start, asyncMsgHandler, cb) => {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
readMessagesBin(env, channelName, start, asyncMsgHandler, cb);
},
// OLDER METHODS
// write a new message to a log
message: function (channelName, content, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
message(env, channelName, content, cb);
},
// iterate over all the messages in a log
getMessages: function (channelName, msgHandler, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
getMessages(env, channelName, msgHandler, cb);
},
// NEWER IMPLEMENTATIONS OF THE SAME THING
// write a new message to a log
messageBin: (channelName, content, cb) => {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
messageBin(env, channelName, content, cb);
},
getMessages: function (channelName, msgHandler, cb) {
// iterate over the messages in a log
readMessagesBin: (channelName, start, asyncMsgHandler, cb) => {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
getMessages(env, channelName, msgHandler, cb);
readMessagesBin(env, channelName, start, asyncMsgHandler, cb);
},
// METHODS for deleting data
// remove a channel and its associated metadata log if present
removeChannel: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
removeChannel(env, channelName, function (err) {
cb(err);
});
},
// remove a channel and its associated metadata log from the archive directory
removeArchivedChannel: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
removeArchivedChannel(env, channelName, cb);
},
closeChannel: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
closeChannel(env, channelName, cb);
},
flushUnusedChannels: function (cb) {
flushUnusedChannels(env, cb);
},
getChannelSize: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
channelBytes(env, channelName, cb);
},
getChannelMetadata: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
getChannelMetadata(env, channelName, cb);
},
// clear all data for a channel but preserve its metadata
clearChannel: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
clearChannel(env, channelName, cb);
},
listChannels: function (handler, cb) {
listChannels(env.root, handler, cb);
},
// check if a channel exists in the database
isChannelAvailable: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
// construct the path
var filepath = mkPath(env, channelName);
channelExists(filepath, channelName, cb);
channelExists(filepath, cb);
},
// check if a channel exists in the archive
isChannelArchived: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
// construct the path
var filepath = mkArchivePath(env, channelName);
channelExists(filepath, channelName, cb);
},
listArchivedChannels: function (handler, cb) {
listChannels(Path.join(env.archiveRoot, 'datastore'), handler, cb);
channelExists(filepath, cb);
},
// move a channel from the database to the archive, along with its metadata
archiveChannel: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
archiveChannel(env, channelName, cb);
},
// restore a channel from the archive to the database, along with its metadata
restoreArchivedChannel: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
unarchiveChannel(env, channelName, cb);
},
// METADATA METHODS
// fetch the metadata for a channel
getChannelMetadata: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
getChannelMetadata(env, channelName, cb);
},
// iterate over lines of metadata changes from a dedicated log
readDedicatedMetadata: function (channelName, handler, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
getDedicatedMetadata(env, channelName, handler, cb);
},
// iterate over multiple lines of metadata changes
readChannelMetadata: function (channelName, handler, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
readMetadata(env, channelName, handler, cb);
},
// write a new line to a metadata log
writeMetadata: function (channelName, data, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
writeMetadata(env, channelName, data, cb);
},
// CHANNEL ITERATION
listChannels: function (handler, cb) {
listChannels(env.root, handler, cb);
},
listArchivedChannels: function (handler, cb) {
listChannels(Path.join(env.archiveRoot, 'datastore'), handler, cb);
},
getChannelSize: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
channelBytes(env, channelName, cb);
},
// OTHER DATABASE FUNCTIONALITY
// remove a particular channel from the cache
closeChannel: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
closeChannel(env, channelName, cb);
},
// iterate over open channels and close any that are not active
flushUnusedChannels: function (cb) {
flushUnusedChannels(env, cb);
},
// write to a log file
log: function (channelName, content, cb) {
message(env, channelName, content, cb);
},
// shut down the database
shutdown: function () {
clearInterval(it);
}

@ -199,13 +199,13 @@ define([
// A ticket has been closed by the admins...
if (!$ticket.length) { return; }
$ticket.addClass('cp-support-list-closed');
$ticket.append(Support.makeCloseMessage(common, content, hash));
$ticket.append(APP.support.makeCloseMessage(content, hash));
return;
}
if (msg.type !== 'TICKET') { return; }
if (!$ticket.length) {
$ticket = Support.makeTicket($div, common, content, function () {
$ticket = APP.support.makeTicket($div, content, function () {
var error = false;
hashesById[id].forEach(function (d) {
common.mailbox.dismiss(d, function (err) {
@ -218,7 +218,7 @@ define([
if (!error) { $ticket.remove(); }
});
}
$ticket.append(Support.makeMessage(common, content, hash, true));
$ticket.append(APP.support.makeMessage(content, hash));
}
});
return $div;
@ -349,6 +349,7 @@ define([
APP.privateKey = privateData.supportPrivateKey;
APP.origin = privateData.origin;
APP.readOnly = privateData.readOnly;
APP.support = Support.create(common, true);
// Content
var $rightside = APP.$rightside;

@ -8,7 +8,7 @@ define([
module.main = function (userDoc, cb) {
var mode = userDoc.highlightMode || 'gfm';
var content = userDoc.content;
module.type = SFCodeMirror.getContentExtension(mode);
module.ext = SFCodeMirror.getContentExtension(mode);
cb(SFCodeMirror.fileExporter(content));
};

@ -272,6 +272,7 @@ define([
var andThen2 = function (editor, CodeMirror, framework, isPresentMode) {
var common = framework._.sfCommon;
var privateData = common.getMetadataMgr().getPrivateData();
var previewPane = mkPreviewPane(editor, CodeMirror, framework, isPresentMode);
var markdownTb = mkMarkdownTb(editor, framework);
@ -349,7 +350,8 @@ define([
onUploaded: function (ev, data) {
var parsed = Hash.parsePadUrl(data.url);
var secret = Hash.getSecrets('file', parsed.hash, data.password);
var src = Hash.getBlobPathFromHex(secret.channel);
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>';
editor.replaceSelection(mt);
@ -363,7 +365,15 @@ define([
});
framework.setFileExporter(CodeMirror.getContentExtension, CodeMirror.fileExporter);
framework.setFileImporter({}, CodeMirror.fileImporter);
framework.setFileImporter({}, function () {
/* setFileImporter currently takes a function with the following signature:
(content, file) => {}
I used 'apply' with 'arguments' to avoid breaking things if this API ever changes.
*/
var ret = CodeMirror.fileImporter.apply(null, Array.prototype.slice.call(arguments));
previewPane.modeChange(ret.mode);
return ret;
});
framework.setNormalizer(function (c) {
return {

@ -93,6 +93,7 @@ define(function() {
config.applicationsIcon = {
file: 'cptools-file',
fileupload: 'cptools-file-upload',
folderupload: 'cptools-folder-upload',
pad: 'cptools-pad',
code: 'cptools-code',
slide: 'cptools-slide',

@ -7,6 +7,7 @@ define(function () {
fileHashKey: 'FS_hash',
// sessionStorage
newPadPathKey: "newPadPath",
newPadFileData: "newPadFileData",
// Store
displayNameKey: 'cryptpad.username',
oldStorageKey: 'CryptPad_RECENTPADS',

@ -592,6 +592,16 @@ define([
]);
};
UI.createHelper = function (href, text) {
var q = h('a.fa.fa-question-circle', {
style: 'text-decoration: none !important;',
title: text,
href: href,
target: "_blank",
'data-tippy-placement': "right"
});
return q;
};
/*
* spinner
@ -773,6 +783,7 @@ define([
var icon = AppConfig.applicationsIcon[type];
var font = icon.indexOf('cptools') === 0 ? 'cptools' : 'fa';
if (type === 'fileupload') { type = 'file'; }
if (type === 'folderupload') { type = 'file'; }
var appClass = ' cp-icon cp-icon-color-'+type;
$icon = $('<span>', {'class': font + ' ' + icon + appClass});
}

@ -422,8 +422,10 @@ define([
var friend = getFriendFromChannel(chan.id) || {};
var cfg = {
validateKey: keys ? keys.validateKey : undefined,
owners: [proxy.edPublic, friend.edPublic],
metadata: {
validateKey: keys ? keys.validateKey : undefined,
owners: [proxy.edPublic, friend.edPublic],
},
lastKnownHash: data.lastKnownHash
};
var msg = ['GET_HISTORY', chan.id, cfg];

@ -15,6 +15,7 @@ define([
};
var supportedTypes = [
'text/plain',
'image/png',
'image/jpeg',
'image/jpg',
@ -23,7 +24,12 @@ define([
'application/pdf'
];
Thumb.isSupportedType = function (type) {
Thumb.isSupportedType = function (file) {
if (!file) { return false; }
var type = file.type;
if (Util.isPlainTextFile(file.type, file.name)) {
type = "text/plain";
}
return supportedTypes.some(function (t) {
return type.indexOf(t) !== -1;
});
@ -164,6 +170,26 @@ define([
});
});
};
Thumb.fromPlainTextBlob = function (blob, cb) {
var canvas = document.createElement("canvas");
canvas.width = canvas.height = Thumb.dimension;
var reader = new FileReader();
reader.addEventListener('loadend', function (e) {
var content = e.srcElement.result;
var lines = content.split("\n");
var canvasContext = canvas.getContext("2d");
var fontSize = 4;
canvas.height = (lines.length) * (fontSize + 1);
canvasContext.font = fontSize + 'px monospace';
lines.forEach(function (text, i) {
canvasContext.fillText(text, 5, i * (fontSize + 1));
});
var D = getResizedDimensions(canvas, "txt");
Thumb.fromCanvas(canvas, D, cb);
});
reader.readAsText(blob);
};
Thumb.fromBlob = function (blob, cb) {
if (blob.type.indexOf('video/') !== -1) {
return void Thumb.fromVideoBlob(blob, cb);
@ -171,6 +197,9 @@ define([
if (blob.type.indexOf('application/pdf') !== -1) {
return void Thumb.fromPdfBlob(blob, cb);
}
if (Util.isPlainTextFile(blob.type, blob.name)) {
return void Thumb.fromPlainTextBlob(blob, cb);
}
Thumb.fromImageBlob(blob, cb);
};
@ -230,9 +259,15 @@ define([
if (!Visible.currently()) { to = window.setTimeout(interval, Thumb.UPDATE_FIRST); }
};
var addThumbnail = function (err, thumb, $span, cb) {
var u8 = Nacl.util.decodeBase64(thumb.split(',')[1]);
var blob = new Blob([u8], {
type: 'image/png'
});
var url = URL.createObjectURL(blob);
var img = new Image();
img.src = thumb.slice(0,5) === 'data:' ? thumb : 'data:image/png;base64,'+thumb;
img.src = url;
$span.find('.cp-icon').hide();
$span.prepend(img);
cb($(img));
@ -254,9 +289,11 @@ define([
var parsed = Hash.parsePadUrl(href);
var k = getKey(parsed.type, channel);
var whenNewThumb = function () {
var privateData = common.getMetadataMgr().getPrivateData();
var fileHost = privateData.fileHost || privateData.origin;
var secret = Hash.getSecrets('file', parsed.hash, password);
var hexFileName = secret.channel;
var src = Hash.getBlobPathFromHex(hexFileName);
var src = fileHost + Hash.getBlobPathFromHex(hexFileName);
var key = secret.keys && secret.keys.cryptKey;
FileCrypto.fetchDecryptedMetadata(src, key, function (e, metadata) {
if (e) {

@ -119,15 +119,35 @@ define([
$('<label>', {'for': 'cp-app-prop-owners'}).text(Messages.creation_owners)
.appendTo($d);
var owners = Messages.creation_noOwner;
var edPublic = common.getMetadataMgr().getPrivateData().edPublic;
var priv = common.getMetadataMgr().getPrivateData();
var edPublic = priv.edPublic;
var owned = false;
if (data.owners && data.owners.length) {
if (data.owners.indexOf(edPublic) !== -1) {
owners = Messages.yourself;
owned = true;
} else {
owners = Messages.creation_ownedByOther;
}
var names = [];
var strangers = 0;
data.owners.forEach(function (ed) {
// If a friend is an owner, add their name to the list
// otherwise, increment the list of strangers
if (ed === edPublic) {
names.push(Messages.yourself);
return;
}
if (!Object.keys(priv.friends || {}).some(function (c) {
var friend = priv.friends[c] || {};
if (friend.edPublic !== ed || c === 'me') { return; }
names.push(friend.displayName);
return true;
})) {
strangers++;
}
});
if (strangers) {
names.push(Messages._getKey('properties_unknownUser', [strangers]));
}
owners = names.join(', ');
}
$d.append(UI.dialog.selectable(owners, {
id: 'cp-app-prop-owners',
@ -325,7 +345,7 @@ define([
});
};
var getFriendsList = function (config) {
var getFriendsList = function (config, onShare) {
var common = config.common;
var title = config.title;
var friends = config.friends;
@ -337,17 +357,18 @@ define([
if (curve.length <= 40) { return; }
var data = friends[curve];
if (!data.notifications) { return; }
var name = data.displayName || Messages.anonymous;
var avatar = h('span.cp-share-friend-avatar.cp-avatar');
UIElements.displayAvatar(common, $(avatar), data.avatar, data.displayName);
UIElements.displayAvatar(common, $(avatar), data.avatar, name);
return h('div.cp-share-friend', {
'data-curve': data.curvePublic,
'data-name': data.displayName,
'data-name': name,
'data-order': i,
title: data.displayName,
title: name,
style: 'order:'+i+';'
},[
avatar,
h('span.cp-share-friend-name', data.displayName)
h('span.cp-share-friend-name', name)
]);
}).filter(function (x) { return x; });
var smallCurves = Object.keys(friends).map(function (c) {
@ -413,6 +434,7 @@ define([
common.mailbox.sendTo("SHARE_PAD", {
href: href,
password: config.password,
isTemplate: config.isTemplate,
name: myName,
title: title
}, {
@ -436,6 +458,9 @@ define([
return smallCurves.indexOf(curve) !== -1;
});
common.setAttribute(['general', 'share-friends'], order);
if (onShare) {
onShare.fire();
}
});
$nav.append(button);
}
@ -512,8 +537,10 @@ define([
// Share link tab
var hasFriends = Object.keys(config.friends || {}).length !== 0;
var friendsList = hasFriends ? getFriendsList(config) : undefined;
var onFriendShare = Util.mkEvent();
var friendsList = hasFriends ? getFriendsList(config, onFriendShare) : undefined;
var friendsUIClass = hasFriends ? '.cp-share-columns' : '';
var link = h('div.cp-share-modal' + friendsUIClass, [
h('div.cp-share-column', [
hasFriends ? h('p', Messages.share_description) : undefined,
@ -547,11 +574,12 @@ define([
present: present
});
};
onFriendShare.reg(saveValue);
var getLinkValue = function (initValue) {
var val = initValue || {};
var edit = initValue ? val.edit : Util.isChecked($(link).find('#cp-share-editable-true'));
var embed = initValue ? val.embed : Util.isChecked($(link).find('#cp-share-embed'));
var present = initValue ? val.present : Util.isChecked($(link).find('#cp-share-present'));
var edit = val.edit !== undefined ? val.edit : Util.isChecked($(link).find('#cp-share-editable-true'));
var embed = val.embed !== undefined ? val.embed : Util.isChecked($(link).find('#cp-share-embed'));
var present = val.present !== undefined ? val.present : Util.isChecked($(link).find('#cp-share-present'));
var hash = (!hashes.viewHash || (edit && hashes.editHash)) ? hashes.editHash : hashes.viewHash;
var href = origin + pathname + '#' + hash;
@ -1474,7 +1502,7 @@ define([
UIElements.getAvatar = function (hash) {
return avatars[hash];
};
UIElements.displayAvatar = function (Common, $container, href, name, cb) {
UIElements.displayAvatar = function (common, $container, href, name, cb) {
var displayDefault = function () {
var text = getFirstEmojiOrCharacter(name);
var $avatar = $('<span>', {'class': 'cp-avatar-default'}).text(text);
@ -1510,12 +1538,14 @@ define([
return;
}
// No password for avatars
var privateData = common.getMetadataMgr().getPrivateData();
var origin = privateData.fileHost || privateData.origin;
var secret = Hash.getSecrets('file', parsed.hash);
if (secret.keys && secret.channel) {
var hexFileName = secret.channel;
var cryptKey = Hash.encodeBase64(secret.keys && secret.keys.cryptKey);
var src = Hash.getBlobPathFromHex(hexFileName);
Common.getFileSize(hexFileName, function (e, data) {
var src = origin + Hash.getBlobPathFromHex(hexFileName);
common.getFileSize(hexFileName, function (e, data) {
if (e || !data) {
displayDefault();
return void console.error(e || "404 avatar");
@ -1525,7 +1555,7 @@ define([
var $img = $('<media-tag>').appendTo($container);
$img.attr('src', src);
$img.attr('data-crypto-key', 'cryptpad:' + cryptKey);
UIElements.displayMediatagImage(Common, $img, function (err, $image, img) {
UIElements.displayMediatagImage(common, $img, function (err, $image, img) {
if (err) { return void console.error(err); }
centerImage($img, $image, img);
});
@ -1832,6 +1862,15 @@ define([
content: $userAdminContent.html()
});
}
options.push({
tag: 'a',
attributes: {
'target': '_blank',
'href': origin+'/index.html',
'class': 'fa fa-home'
},
content: h('span', Messages.homePage)
});
if (padType !== 'drive' || (!accountName && priv.newSharedFolder)) {
options.push({
tag: 'a',
@ -1843,6 +1882,7 @@ define([
content: h('span', Messages.login_accessDrive)
});
}
options.push({ tag: 'hr' });
// Add the change display name button if not in read only mode
if (config.changeNameButtonCls && config.displayChangeName && !AppConfig.disableProfile) {
options.push({
@ -1865,6 +1905,7 @@ define([
content: h('span', Messages.settingsButton)
});
}
options.push({ tag: 'hr' });
// Add administration panel link if the user is an admin
if (priv.edPublic && Array.isArray(Config.adminKeys) && Config.adminKeys.indexOf(priv.edPublic) !== -1) {
options.push({
@ -1880,6 +1921,16 @@ define([
content: h('span', Messages.supportPage || 'Support')
});
}
options.push({
tag: 'a',
attributes: {
'target': '_blank',
'href': origin+'/features.html',
'class': 'fa fa-star-o'
},
content: h('span', priv.plan ? Messages.settings_cat_subscription : Messages.pricing)
});
options.push({ tag: 'hr' });
// Add login or logout button depending on the current status
if (accountName) {
options.push({
@ -2080,6 +2131,9 @@ define([
};
UIElements.createNewPadModal = function (common) {
// if in drive, show new pad modal instead
if ($("body.cp-app-drive").length !== 0) { return void $(".cp-app-drive-element-row.cp-app-drive-new-ghost").click(); }
var $modal = UIElements.createModal({
id: 'cp-app-toolbar-creation-dialog',
$body: $('body')
@ -2274,7 +2328,10 @@ define([
if (!common.isLoggedIn()) { return void cb(); }
var sframeChan = common.getSframeChannel();
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var type = metadataMgr.getMetadataLazy().type;
var fromFileData = privateData.fromFileData;
var $body = $('body');
var $creationContainer = $('<div>', { id: 'cp-creation-container' }).appendTo($body);
@ -2286,7 +2343,8 @@ define([
// Title
//var colorClass = 'cp-icon-color-'+type;
//$creation.append(h('h2.cp-creation-title', Messages.newButtonTitle));
$creation.append(h('h3.cp-creation-title', Messages['button_new'+type]));
var newPadH3Title = Messages['button_new' + type];
$creation.append(h('h3.cp-creation-title', newPadH3Title));
//$creation.append(h('h2.cp-creation-title.'+colorClass, Messages.newButtonTitle));
// Deleted pad warning
@ -2296,7 +2354,7 @@ define([
));
}
var origin = common.getMetadataMgr().getPrivateData().origin;
var origin = privateData.origin;
var createHelper = function (href, text) {
var q = h('a.cp-creation-help.fa.fa-question-circle', {
title: text,
@ -2453,7 +2511,26 @@ define([
});
if (i < TEMPLATES_DISPLAYED) { $(left).addClass('hidden'); }
};
redraw(0);
if (fromFileData) {
var todo = function (thumbnail) {
allData = [{
name: fromFileData.title,
id: 0,
thumbnail: thumbnail,
icon: h('span.cptools.cptools-file'),
}];
redraw(0);
};
todo();
sframeChan.query("Q_GET_FILE_THUMBNAIL", null, function (err, res) {
if (err || (res && res.error)) { return; }
todo(res.data);
});
}
else {
redraw(0);
}
// Change template selection when Tab is pressed
next = function (revert) {
@ -2742,8 +2819,12 @@ define([
UIElements.displayCrowdfunding(common);
modal.delete();
});
var waitingForStoringCb = false;
$(store).click(function () {
if (waitingForStoringCb) { return; }
waitingForStoringCb = true;
common.getSframeChannel().query("Q_AUTOSTORE_STORE", null, function (err, obj) {
waitingForStoringCb = false;
var error = err || (obj && obj.error);
if (error) {
if (error === 'E_OVER_LIMIT') {
@ -2830,11 +2911,27 @@ define([
'aria-labelledBy': 'dropdownMenu',
'style': 'display:block;position:static;margin-bottom:5px;'
}, [
h('li', h('a.dropdown-item', {
h('li', h('a.cp-app-code-context-saveindrive.dropdown-item', {
'tabindex': '-1',
}, Messages.pad_mediatagImport))
'data-icon': "fa-cloud-upload",
}, Messages.pad_mediatagImport)),
h('li', h('a.cp-app-code-context-download.dropdown-item', {
'tabindex': '-1',
'data-icon': "fa-download",
}, Messages.download_mt_button)),
])
]);
// create the icon for each contextmenu option
$(menu).find("li a.dropdown-item").each(function (i, el) {
var $icon = $("<span>");
if ($(el).attr('data-icon')) {
var font = $(el).attr('data-icon').indexOf('cptools') === 0 ? 'cptools' : 'fa';
$icon.addClass(font).addClass($(el).attr('data-icon'));
} else {
$icon.text($(el).text());
}
$(el).prepend($icon);
});
var m = createContextMenu(menu);
mediatagContextMenu = m;
@ -2844,7 +2941,13 @@ define([
e.stopPropagation();
m.hide();
var $mt = $menu.data('mediatag');
common.importMediaTag($mt);
if ($(this).hasClass("cp-app-code-context-saveindrive")) {
common.importMediaTag($mt);
}
else if ($(this).hasClass("cp-app-code-context-download")) {
var media = $mt[0]._mediaObject;
window.saveAs(media._blob.content, media.name);
}
});
return m;

@ -319,6 +319,36 @@ define([], function () {
return window.innerHeight < 800 || window.innerWidth < 800;
};
Util.stripTags = function (text) {
var div = document.createElement("div");
div.innerHTML = text;
return div.innerText;
};
// return an object containing {name, ext}
// or {} if the name could not be parsed
Util.parseFilename = function (filename) {
if (!filename || !filename.trim()) { return {}; }
var parsedName = /^(\.?.+?)(\.[^.]+)?$/.exec(filename) || [];
return {
name: parsedName[1],
ext: parsedName[2],
};
};
// Tell if a file is plain text from its metadata={title, fileType}
Util.isPlainTextFile = function (type, name) {
// does its type begins with "text/"
if (type && type.indexOf("text/") === 0) { return true; }
// no type and no file extension -> let's guess it's plain text
var parsedName = Util.parseFilename(name);
if (!type && name && !parsedName.ext) { return true; }
// other exceptions
if (type === 'application/x-javascript') { return true; }
if (type === 'application/xml') { return true; }
return false;
};
return Util;
});
}(self));

@ -16,10 +16,10 @@ define([
var disconnect = Util.find(S, ['network', 'disconnect']);
if (typeof(disconnect) === 'function') { disconnect(); }
}
if (S.leave) {
if (S.realtime && S.realtime.stop) {
try {
S.leave();
} catch (e) { console.log(e); }
S.realtime.stop();
} catch (e) { console.error(e); }
}
var abort = Util.find(S, ['session', 'realtime', 'abort']);
if (typeof(abort) === 'function') {
@ -52,11 +52,12 @@ define([
Object.keys(b).forEach(function (k) { a[k] = b[k]; });
};
var get = function (hash, cb, opt) {
var get = function (hash, cb, opt, progress) {
if (typeof(cb) !== 'function') {
throw new Error('Cryptget expects a callback');
}
opt = opt || {};
progress = progress || function () {};
var config = makeConfig(hash, opt);
var Session = { cb: cb, hasNetwork: Boolean(opt.network) };
@ -64,7 +65,7 @@ define([
config.onReady = function (info) {
var rt = Session.session = info.realtime;
Session.network = info.network;
Session.leave = info.leave;
progress(1);
finish(Session, void 0, rt.getUserDoc());
};
@ -72,6 +73,16 @@ define([
finish(Session, info.error);
};
// We use the new onMessage handler to compute the progress:
// we should receive 2 checkpoints max, so 100 messages max
// We're going to consider that 1 message = 1%, and we'll send 100%
// at the end
var i = 0;
config.onMessage = function () {
i++;
progress(Math.min(0.99, i/100));
};
overwrite(config, opt);
Session.realtime = CPNetflux.start(config);

@ -254,8 +254,12 @@ define([
common.clearOwnedChannel = function (channel, cb) {
postMessage("CLEAR_OWNED_CHANNEL", channel, cb);
};
common.removeOwnedChannel = function (channel, cb) {
postMessage("REMOVE_OWNED_CHANNEL", channel, cb);
// "force" allows you to delete your drive ID
common.removeOwnedChannel = function (channel, cb, force) {
postMessage("REMOVE_OWNED_CHANNEL", {
channel: channel,
force: force
}, cb);
};
common.getDeletedPads = function (data, cb) {
@ -567,6 +571,67 @@ define([
});
};
common.useFile = function (Crypt, cb, optsPut) {
var fileHost = Config.fileHost || window.location.origin;
var data = common.fromFileData;
var parsed = Hash.parsePadUrl(data.href);
var parsed2 = Hash.parsePadUrl(window.location.href);
var hash = parsed.hash;
var name = data.title;
var secret = Hash.getSecrets('file', hash, data.password);
var src = fileHost + Hash.getBlobPathFromHex(secret.channel);
var key = secret.keys && secret.keys.cryptKey;
var u8;
var res;
var mode;
var val;
Nthen(function(waitFor) {
Util.fetch(src, waitFor(function (err, _u8) {
if (err) { return void waitFor.abort(); }
u8 = _u8;
}));
}).nThen(function (waitFor) {
require(["/file/file-crypto.js"], waitFor(function (FileCrypto) {
FileCrypto.decrypt(u8, key, waitFor(function (err, _res) {
if (err || !_res.content) { return void waitFor.abort(); }
res = _res;
}));
}));
}).nThen(function (waitFor) {
var ext = Util.parseFilename(data.title).ext;
if (!ext) {
mode = "text";
return;
}
require(["/common/modes.js"], waitFor(function (Modes) {
Modes.list.some(function (fType) {
if (fType.ext === ext) {
mode = fType.mode;
return true;
}
});
}));
}).nThen(function (waitFor) {
var reader = new FileReader();
reader.addEventListener('loadend', waitFor(function (e) {
val = {
content: e.srcElement.result,
highlightMode: mode,
metadata: {
defaultTitle: name,
title: name,
type: "code",
},
};
}));
reader.readAsText(res.content);
}).nThen(function () {
Crypt.put(parsed2.hash, JSON.stringify(val), cb, optsPut);
});
};
// Forget button
common.moveToTrash = function (cb, href) {
href = href || window.location.href;
@ -693,6 +758,17 @@ define([
pad.onConnectEvent = Util.mkEvent();
pad.onErrorEvent = Util.mkEvent();
pad.requestAccess = function (data, cb) {
postMessage("REQUEST_PAD_ACCESS", data, cb);
};
pad.giveAccess = function (data, cb) {
postMessage("GIVE_PAD_ACCESS", data, cb);
};
common.getPadMetadata = function (data, cb) {
postMessage('GET_PAD_METADATA', data, cb);
};
common.changePadPassword = function (Crypt, href, newPassword, edPublic, cb) {
if (!href) { return void cb({ error: 'EINVAL_HREF' }); }
var parsed = Hash.parsePadUrl(href);
@ -943,7 +1019,7 @@ define([
common.logoutFromAll(waitFor(function () {
postMessage("DISCONNECT");
}));
}));
}), true);
}
}).nThen(function (waitFor) {
if (!oldIsOwned) {
@ -1263,6 +1339,12 @@ define([
messenger: rdyCfg.messenger, // Boolean
driveEvents: rdyCfg.driveEvents // Boolean
};
// if a pad is created from a file
if (sessionStorage[Constants.newPadFileData]) {
common.fromFileData = JSON.parse(sessionStorage[Constants.newPadFileData]);
delete sessionStorage[Constants.newPadFileData];
}
if (sessionStorage[Constants.newPadPathKey]) {
common.initialPath = sessionStorage[Constants.newPadPathKey];
delete sessionStorage[Constants.newPadPathKey];
@ -1287,10 +1369,12 @@ define([
errEv.preventDefault();
errEv.stopPropagation();
noWorker = true;
worker.terminate();
w();
};
worker.onmessage = function (ev) {
if (ev.data === "OK") {
worker.terminate();
w();
}
};

@ -1,51 +0,0 @@
define([
'/common/curve.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
], function (Curve, Listmap) {
var Edit = {};
Edit.create = function (config, cb) { //network, channel, theirs, mine, cb) {
var network = config.network;
var channel = config.channel;
var keys = config.keys;
try {
var encryptor = Curve.createEncryptor(keys);
var lm = Listmap.create({
network: network,
data: {},
channel: channel,
readOnly: false,
validateKey: keys.validateKey || undefined,
crypto: encryptor,
userName: 'lol',
logLevel: 1,
});
var done = function () {
// TODO make this abort and disconnect the session after the
// user has finished making changes to the object, and they
// have propagated.
};
lm.proxy
.on('create', function () {
console.log('created');
})
.on('ready', function () {
console.log('ready');
cb(lm, done);
})
.on('disconnect', function () {
console.log('disconnected');
})
.on('change', [], function (o, n, p) {
console.log(o, n, p);
});
} catch (e) {
console.error(e);
}
};
return Edit;
});

@ -1,5 +1,6 @@
define([
'jquery',
'/api/config',
'/bower_components/marked/marked.min.js',
'/common/common-hash.js',
'/common/common-util.js',
@ -10,11 +11,12 @@ define([
'/bower_components/diff-dom/diffDOM.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
'css!/common/highlight/styles/github.css'
],function ($, Marked, Hash, Util, h, MediaTag, Highlight, Messages) {
],function ($, ApiConfig, Marked, Hash, Util, h, MediaTag, Highlight, Messages) {
var DiffMd = {};
var DiffDOM = window.diffDOM;
var renderer = new Marked.Renderer();
var restrictedRenderer = new Marked.Renderer();
var Mermaid = {
init: function () {}
@ -61,13 +63,18 @@ define([
return h('div.cp-md-toc', content).outerHTML;
};
DiffMd.render = function (md, sanitize) {
DiffMd.render = function (md, sanitize, restrictedMd) {
Marked.setOptions({
renderer: restrictedMd ? restrictedRenderer : renderer,
});
var r = Marked(md, {
sanitize: sanitize
});
// Add Table of Content
r = r.replace(/<div class="cp-md-toc"><\/div>/g, getTOC());
if (!restrictedMd) {
r = r.replace(/<div class="cp-md-toc"><\/div>/g, getTOC());
}
toc = [];
return r;
@ -83,12 +90,7 @@ define([
return defaultCode.apply(renderer, arguments);
}
};
var stripTags = function (text) {
var div = document.createElement("div");
div.innerHTML = text;
return div.innerText;
};
restrictedRenderer.code = renderer.code;
renderer.heading = function (text, level) {
var i = 0;
@ -105,10 +107,13 @@ define([
toc.push({
level: level,
id: id,
title: stripTags(text)
title: Util.stripTags(text)
});
return "<h" + level + " id=\"" + id + "\"><a href=\"#" + id + "\" class=\"anchor\"></a>" + text + "</h" + level + ">";
};
restrictedRenderer.heading = function (text) {
return text;
};
// Tasks list
var checkedTaskItemPtn = /^\s*(<p>)?\[[xX]\](<\/p>)?\s*/;
@ -138,6 +143,13 @@ define([
var cls = (isCheckedTaskItem || isUncheckedTaskItem || hasBogusInput) ? ' class="todo-list-item"' : '';
return '<li'+ cls + '>' + text + '</li>\n';
};
restrictedRenderer.listitem = function (text) {
if (bogusCheckPtn.test(text)) {
text = text.replace(bogusCheckPtn, '');
}
return '<li>' + text + '</li>\n';
};
renderer.image = function (href, title, text) {
if (href.slice(0,6) === '/file/') {
// DEPRECATED
@ -146,7 +158,7 @@ define([
console.log('DEPRECATED: mediatag using markdown syntax!');
var parsed = Hash.parsePadUrl(href);
var secret = Hash.getSecrets('file', parsed.hash);
var src = Hash.getBlobPathFromHex(secret.channel);
var src = (ApiConfig.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>';
if (mediaMap[src]) {
@ -162,12 +174,19 @@ define([
out += this.options.xhtml ? '/>' : '>';
return out;
};
restrictedRenderer.image = renderer.image;
var renderParagraph = function (p) {
return /<media\-tag[\s\S]*>/i.test(p)? p + '\n': '<p>' + p + '</p>\n';
};
renderer.paragraph = function (p) {
if (p === '[TOC]') {
return '<p><div class="cp-md-toc"></div></p>';
}
return /<media\-tag[\s\S]*>/i.test(p)? p + '\n': '<p>' + p + '</p>\n';
return renderParagraph(p);
};
restrictedRenderer.paragraph = function (p) {
return renderParagraph(p);
};
var MutationObserver = window.MutationObserver;

@ -1,11 +1,13 @@
define([
'/common/cryptget.js',
'/file/file-crypto.js',
'/common/common-hash.js',
'/common/sframe-common-file.js',
'/common/common-util.js',
'/bower_components/nthen/index.js',
'/bower_components/saferphore/index.js',
'/bower_components/jszip/dist/jszip.min.js',
], function (Crypt, Hash, SFCFile, nThen, Saferphore, JsZip) {
], function (Crypt, FileCrypto, Hash, Util, nThen, Saferphore, JsZip) {
var saveAs = window.saveAs;
var sanitize = function (str) {
return str.replace(/[\\/?%*:|"<>]/gi, '_')/*.toLowerCase()*/;
@ -34,7 +36,7 @@ define([
var path = '/' + type + '/export.js';
require([path], function (Exporter) {
Exporter.main(json, function (data) {
result.ext = '.' + Exporter.type;
result.ext = Exporter.ext || '';
result.data = data;
cb(result);
});
@ -43,6 +45,87 @@ define([
});
};
var _downloadFile = function (ctx, fData, cb, updateProgress) {
var cancelled = false;
var cancel = function () {
cancelled = true;
};
var parsed = Hash.parsePadUrl(fData.href || fData.roHref);
var hash = parsed.hash;
var name = fData.filename || fData.title;
var secret = Hash.getSecrets('file', hash, fData.password);
var src = (ctx.fileHost || '') + Hash.getBlobPathFromHex(secret.channel);
var key = secret.keys && secret.keys.cryptKey;
Util.fetch(src, function (err, u8) {
if (cancelled) { return; }
if (err) { return void cb('E404'); }
FileCrypto.decrypt(u8, key, function (err, res) {
if (cancelled) { return; }
if (err) { return void cb(err); }
if (!res.content) { return void cb('EEMPTY'); }
var dl = function () {
saveAs(res.content, name || res.metadata.name);
};
cb(null, {
metadata: res.metadata,
content: res.content,
download: dl
});
}, updateProgress && updateProgress.progress2);
}, updateProgress && updateProgress.progress);
return {
cancel: cancel
};
};
var _downloadPad = function (ctx, pData, cb, updateProgress) {
var cancelled = false;
var cancel = function () {
cancelled = true;
};
var parsed = Hash.parsePadUrl(pData.href || pData.roHref);
var name = pData.filename || pData.title;
var opts = {
password: pData.password
};
var handler = ctx.sframeChan.on("EV_CRYPTGET_PROGRESS", function (data) {
if (data.hash !== parsed.hash) { return; }
updateProgress.progress(data.progress);
if (data.progress === 1) {
handler.stop();
updateProgress.progress2(1);
}
});
ctx.get({
hash: parsed.hash,
opts: opts
}, function (err, val) {
if (cancelled) { return; }
if (err) { return; }
if (!val) { return; }
transform(ctx, parsed.type, val, function (res) {
if (cancelled) { return; }
if (!res.data) { return; }
var dl = function () {
saveAs(res.data, Util.fixFileName(name));
};
cb(null, {
metadata: res.metadata,
content: res.data,
download: dl
});
});
});
return {
cancel: cancel
};
};
// Add a file to the zip. We have to cryptget&transform it if it's a pad
// or fetch&decrypt it if it's a file.
var addFile = function (ctx, zip, fData, existingNames) {
@ -126,7 +209,7 @@ define([
// Files (mediatags...)
var todoFile = function () {
var it;
var dl = SFCFile.downloadFile(fData, function (err, res) {
var dl = _downloadFile(ctx, fData, function (err, res) {
if (it) { clearInterval(it); }
if (err) { return void error(err); }
var opts = {
@ -163,12 +246,12 @@ define([
var existingNames = [];
Object.keys(root).forEach(function (k) {
var el = root[k];
if (typeof el === "object") {
if (typeof el === "object" && el.metadata !== true) { // if folder
var fName = getUnique(sanitize(k), '', existingNames);
existingNames.push(fName.toLowerCase());
return void makeFolder(ctx, el, zip.folder(fName), fd);
}
if (ctx.data.sharedFolders[el]) {
if (ctx.data.sharedFolders[el]) { // if shared folder
var sfData = ctx.sf[el].metadata;
var sfName = getUnique(sanitize(sfData.title || 'Folder'), '', existingNames);
existingNames.push(sfName.toLowerCase());
@ -183,12 +266,14 @@ define([
};
// Main function. Create the empty zip and fill it starting from drive.root
var create = function (data, getPad, cb, progress) {
var create = function (data, getPad, fileHost, cb, progress) {
if (!data || !data.uo || !data.uo.drive) { return void cb('EEMPTY'); }
var sem = Saferphore.create(5);
var ctx = {
fileHost: fileHost,
get: getPad,
data: data.uo.drive,
folder: data.folder || ctx.data.root,
sf: data.sf,
zip: new JsZip(),
errors: [],
@ -197,11 +282,12 @@ define([
max: 0,
done: 0
};
var filesData = data.sharedFolderId && ctx.sf[data.sharedFolderId] ? ctx.sf[data.sharedFolderId].filesData : ctx.data.filesData;
progress('reading', -1);
nThen(function (waitFor) {
ctx.waitFor = waitFor;
var zipRoot = ctx.zip.folder('Root');
makeFolder(ctx, ctx.data.root, zipRoot, ctx.data.filesData);
makeFolder(ctx, ctx.folder, zipRoot, filesData);
progress('download', {});
}).nThen(function () {
console.log(ctx.zip);
@ -222,7 +308,33 @@ define([
};
};
var _downloadFolder = function (ctx, data, cb, updateProgress) {
create(data, ctx.get, ctx.fileHost, function (blob, errors) {
console.error(errors); // TODO show user errors
var dl = function () {
saveAs(blob, data.folderName);
};
cb(null, {download: dl});
}, function (state, progress) {
if (state === "reading") {
updateProgress.folderProgress(0);
}
if (state === "download") {
if (typeof progress.current !== "number") { return; }
updateProgress.folderProgress(progress.current / progress.max);
}
else if (state === "done") {
updateProgress.folderProgress(1);
}
});
};
return {
create: create
create: create,
downloadFile: _downloadFile,
downloadPad: _downloadPad,
downloadFolder: _downloadFolder,
};
});

@ -30,9 +30,22 @@
};
var isplainTextFile = function (metadata) {
// does its type begins with "text/"
if (metadata.type.indexOf("text/") === 0) { return true; }
// no type and no file extension -> let's guess it's plain text
var parsedName = /^(\.?.+?)(\.[^.]+)?$/.exec(metadata.name) || [];
if (!metadata.type && !parsedName[2]) { return true; }
// other exceptions
if (metadata.type === 'application/x-javascript') { return true; }
if (metadata.type === 'application/xml') { return true; }
return false;
};
// Default config, can be overriden per media-tag call
var config = {
allowed: [
'text/plain',
'image/png',
'image/jpeg',
'image/jpg',
@ -53,6 +66,24 @@
text: "Download"
},
Plugins: {
/**
* @param {object} metadataObject {name, metadatatype, owners} containing metadata of the file
* @param {strint} url Url of the blob object
* @param {Blob} content Blob object containing the data of the file
* @param {object} cfg Object {Plugins, allowed, download, pdf} containing infos about plugins
* @param {function} cb Callback function: (err, pluginElement) => {}
*/
text: function (metadata, url, content, cfg, cb) {
var plainText = document.createElement('div');
plainText.className = "plain-text-reader";
plainText.setAttribute('style', 'white-space: pre-wrap;');
var reader = new FileReader();
reader.addEventListener('loadend', function (e) {
plainText.innerText = e.srcElement.result;
cb(void 0, plainText);
});
reader.readAsText(content);
},
image: function (metadata, url, content, cfg, cb) {
var img = document.createElement('img');
img.setAttribute('src', url);
@ -271,6 +302,9 @@
var blob = decrypted.content;
var mediaType = getType(mediaObject, metadata, cfg);
if (isplainTextFile(metadata)) {
mediaType = "text";
}
if (mediaType === 'application') {
mediaType = mediaObject.extension;

@ -49,7 +49,7 @@ define([
// We want to merge an edit pad: check if we have the same channel
// but read-only and upgrade it in that case
datas.forEach(function (pad) {
if (!pad.href) { data.href = pad.href; }
if (pad.data && !pad.data.href) { pad.data.href = data.href; }
});
return;
}

@ -151,7 +151,7 @@ define([
});
try {
var $d = $(d);
DiffMd.apply(DiffMd.render(md || '', true), $d, common);
DiffMd.apply(DiffMd.render(md || '', true, true), $d, common);
$d.addClass("cp-app-contacts-content");
// override link clicking, because we're in an iframe

@ -99,6 +99,7 @@ define(['json.sortify'], function (Sortify) {
var addAuthor = function () {
if (!meta.user || !meta.user.netfluxId || !priv || !priv.edPublic) { return; }
var authors = metadataObj.authors || {};
var old = Sortify(authors);
if (!authors[priv.edPublic]) {
authors[priv.edPublic] = {
nId: [meta.user.netfluxId],
@ -110,9 +111,11 @@ define(['json.sortify'], function (Sortify) {
authors[priv.edPublic].nId.push(meta.user.netfluxId);
}
}
metadataObj.authors = authors;
metadataLazyObj.authors = JSON.parse(JSON.stringify(authors));
change();
if (Sortify(authors) !== old) {
metadataObj.authors = authors;
metadataLazyObj.authors = JSON.parse(JSON.stringify(authors));
change();
}
};
var netfluxId;
@ -191,6 +194,15 @@ define(['json.sortify'], function (Sortify) {
onChange: function (f) { changeHandlers.push(f); },
onChangeLazy: function (f) { lazyChangeHandlers.push(f); },
onRequestSync: function (f) { syncHandlers.push(f); },
off: function (name, f) {
var h = [];
if (name === 'change') { h = changeHandlers; }
else if (name === 'lazy') { h = lazyChangeHandlers; }
else if (name === 'title') { h = titleChangeHandlers; }
else if (name === 'sync') { h = syncHandlers; }
var idx = h.indexOf(f);
if (idx !== -1) { h.splice(idx, 1); }
},
isConnected : function () {
return members.indexOf(meta.user.netfluxId) !== -1;
},

@ -6,141 +6,141 @@ define([
// mode language (extension)
var list = Modes.list = [
"APL apl .apl",
"ASCII-Armor asciiarmor",
"ASN.1 asn.1",
"ASCII-Armor asciiarmor .asc",
"ASN.1 asn.1 .asn1",
"Asterisk asterisk",
"Brainfuck brainfuck .b",
"C text/x-csrc .c",
"C text/x-c++src .cpp",
"C-like clike",
"Clojure clojure",
"CMake cmake",
"COBOL cobol",
"CoffeeScript coffeescript",
"Common_Lisp commonlisp",
"Crystal crystal",
"C-like clike .c",
"Clojure clojure .clj",
"CMake cmake _", /* no extension */
"COBOL cobol .cbl",
"CoffeeScript coffeescript .coffee",
"Common_Lisp commonlisp .lisp",
"Crystal crystal .cr",
"CSS css .css",
"Cypher cypher",
"D d",
"Dart dart",
"Diff diff",
"Django django",
"Dockerfile dockerfile",
"DTD dtd",
"Dylan dylan",
"EBNF ebnf",
"ECL ecl",
"Eiffel eiffel",
"Cypher cypher .cypher",
"D d .d",
"Dart dart .dart",
"Diff diff .diff",
"Django django .py",
"Dockerfile dockerfile _", /* no extension */
"DTD dtd .dtd",
"Dylan dylan .dylan",
"EBNF ebnf .ebnf",
"ECL ecl .ecl",
"Eiffel eiffel .e",
"Elm elm .elm",
"Erlang erlang",
"Factor factor",
"FCL fcl",
"Forth forth",
"Fortran fortran",
"GAS gas",
"Gherkin gherkin",
"Go go",
"Groovy groovy",
"Haml haml",
"Handlebars handlebars",
"Erlang erlang .erl",
"Factor factor .factor",
"FCL fcl .fcl",
"Forth forth .fs",
"Fortran fortran .f90",
"GAS gas .gas",
"Gherkin gherkin .feature",
"Go go .go",
"Groovy groovy .groovy",
"Haml haml .haml",
"Handlebars handlebars .hbs",
"Haskell haskell .hs",
"Haskell-Literate haskell-literate",
"Haxe haxe",
"Haskell-Literate haskell-literate .lhs",
"Haxe haxe .hx",
"HTML htmlmixed .html",
"HTTP http",
"IDL idl",
"JADE jade",
"HTTP http _", /* no extension */
"IDL idl .idl",
"JADE jade .jade",
"Java text/x-java .java",
"JavaScript javascript .js",
"Jinja2 jinja2",
"Jinja2 jinja2 .j2",
"JSX jsx .jsx",
"Julia julia",
"LiveScript livescript",
"Lua lua",
"Julia julia .jl",
"LiveScript livescript .ls",
"Lua lua .lua",
"Markdown gfm .md",
//"markdown markdown .md",
"Mathematica mathematica",
"mIRC mirc",
"ML mllike",
"Modelica modelica",
"MscGen mscgen",
"MUMPS mumps",
"Nginx nginx",
"NSIS nsis",
"N-Triples ntriples",
"Mathematica mathematica .nb",
"mIRC mirc .irc",
"ML mllike _", /* no extension */
"Modelica modelica .mo",
"MscGen mscgen .mscgen",
"MUMPS mumps .m",
"Nginx nginx .conf",
"NSIS nsis .nsi",
"N-Triples ntriples .nq",
"Objective-C text/x-objectivec .m",
"Octave octave",
"Octave octave .m",
"Org-mode orgmode .org",
"Oz oz",
"Pascal pascal",
"PEG.js pegjs",
"Perl perl",
"PHP php",
"Pig pig",
"PowerShell powershell",
"Properties properties",
"Protocol_Buffers protobuf",
"Puppet puppet",
"Oz oz .oz",
"Pascal pascal .pas",
"PEG.js pegjs .pegjs",
"Perl perl .pl",
"PHP php .php",
"Pig pig .pig",
"PowerShell powershell .ps1",
"Properties properties .properties",
"Protocol_Buffers protobuf .proto",
"Puppet puppet .pp",
"Python python .py",
"Q q",
"R r",
"RPM rpm",
"RST rst",
"Ruby ruby",
"Rust rust",
"Sass sass",
"Q q .q",
"R r .r",
"RPM rpm .rpm",
"RST rst .rst",
"Ruby ruby .rb",
"Rust rust .rs",
"Sass sass .sass",
"Scheme scheme .scm",
"Shell shell .sh",
"Sieve sieve",
"Slim slim",
"Smalltalk smalltalk",
"Smarty smarty",
"Solr solr",
"Soy soy",
"SPARQL sparql",
"Spreadsheet spreadsheet",
"SQL sql",
"sTeX stex",
"Stylus stylus",
"Swift swift",
"Tcl tcl",
"Sieve sieve .sieve",
"Slim slim .slim",
"Smalltalk smalltalk _", /* no extension */
"Smarty smarty _", /* no extension */
"Solr solr _", /* no extension */
"Soy soy .soy",
"SPARQL sparql .rq",
"Spreadsheet spreadsheet .xls",
"SQL sql .sql",
"sTeX stex .stex",
"Stylus stylus .styl",
"Swift swift .swift",
"Tcl tcl .tcl",
"Text text .txt",
"Textile textile",
"TiddlyWiki tiddlywiki",
"Tiki tiki",
"TOML toml",
"Tornado tornado",
"troff troff",
"Textile textile .textile",
"TiddlyWiki tiddlywiki .tw",
"Tiki tiki _", /* no extension */
"TOML toml .toml",
"Tornado tornado .tornado",
"troff troff .troff",
"TTCN ttcn",
"TTCN-cfg ttcn-cfg",
"Turtle turtle",
"Twig twig",
"Visual_Basic vb",
"VBScript vbscript",
"Velocity velocity",
"Verilog verilog",
"VHDL vhdl",
"Vue vue",
"XML xml",
"Turtle turtle .ttl",
"Twig twig .twig",
"Visual_Basic vb .vb",
"VBScript vbscript .vbs",
"Velocity velocity .vm",
"Verilog verilog .v",
"VHDL vhdl .vhdl",
"Vue vue .vue",
"XML xml .xml",
//"xwiki xwiki21",
"XQuery xquery",
"XQuery xquery .xquery",
"YAML yaml .yaml",
"YAML_Frontmatter yaml-frontmatter",
"Z80 z80"
"YAML_Frontmatter yaml-frontmatter _", /* no extension */
"Z80 z80 .z80"
].map(function (line) {
var kv = line.split(/\s/);
return {
language: kv[0].replace(/_/g, ' '),
mode: kv[1],
ext: kv[2],
ext: kv[2] === '_' ? '' : kv[2],
};
});
Modes.extensionOf = function (mode) {
var ext = '';
var ext;
list.some(function (o) {
if (o.mode !== mode) { return; }
ext = o.ext || '';
ext = o.ext;
return true;
});
return ext;

@ -2,9 +2,13 @@ define([
'jquery',
'/common/hyperscript.js',
'/common/common-hash.js',
'/common/common-interface.js',
'/common/common-ui-elements.js',
'/common/common-util.js',
'/common/common-constants.js',
'/customize/messages.js',
], function ($, h, Hash, UIElements, Messages) {
'/bower_components/nthen/index.js'
], function ($, h, Hash, UI, UIElements, Util, Constants, Messages, nThen) {
var handlers = {};
@ -25,10 +29,11 @@ define([
handlers['FRIEND_REQUEST'] = function (common, data) {
var content = data.content;
var msg = content.msg;
var name = Util.fixHTML(msg.content.displayName) || Messages.anonymous;
// Display the notification
content.getFormatText = function () {
return Messages._getKey('friendRequest_notification', [msg.content.displayName || Messages.anonymous]);
return Messages._getKey('friendRequest_notification', [name]);
};
// Check authenticity
@ -46,8 +51,9 @@ define([
handlers['FRIEND_REQUEST_ACCEPTED'] = function (common, data) {
var content = data.content;
var msg = content.msg;
var name = Util.fixHTML(msg.content.name) || Messages.anonymous;
content.getFormatText = function () {
return Messages._getKey('friendRequest_accepted', [msg.content.name || Messages.anonymous]);
return Messages._getKey('friendRequest_accepted', [name]);
};
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
@ -57,8 +63,9 @@ define([
handlers['FRIEND_REQUEST_DECLINED'] = function (common, data) {
var content = data.content;
var msg = content.msg;
var name = Util.fixHTML(msg.content.name) || Messages.anonymous;
content.getFormatText = function () {
return Messages._getKey('friendRequest_declined', [msg.content.name || Messages.anonymous]);
return Messages._getKey('friendRequest_declined', [name]);
};
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
@ -74,22 +81,137 @@ define([
var key = type === 'drive' ? 'notification_folderShared' :
(type === 'file' ? 'notification_fileShared' :
'notification_padShared');
var name = Util.fixHTML(msg.content.name) || Messages.anonymous;
var title = Util.fixHTML(msg.content.title);
content.getFormatText = function () {
return Messages._getKey(key, [msg.content.name || Messages.anonymous, msg.content.title]);
return Messages._getKey(key, [name, title]);
};
content.handler = function () {
var todo = function () { common.openURL(msg.content.href); };
if (!msg.content.password) { return void todo(); }
common.getSframeChannel().query('Q_SESSIONSTORAGE_PUT', {
key: 'newPadPassword',
value: msg.content.password
}, todo);
var todo = function () {
common.openURL(msg.content.href);
defaultDismiss(common, data)();
};
nThen(function (waitFor) {
if (msg.content.isTemplate) {
common.sessionStorage.put(Constants.newPadPathKey, ['template'], waitFor());
}
if (msg.content.password) {
common.sessionStorage.put('newPadPassword', msg.content.password, waitFor());
}
}).nThen(function () {
todo();
});
};
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
// New support message from the admins
handlers['SUPPORT_MESSAGE'] = function (common, data) {
var content = data.content;
content.getFormatText = function () {
return Messages.support_notification;
};
content.handler = function () {
common.openURL('/support/');
defaultDismiss(common, data)();
};
};
handlers['REQUEST_PAD_ACCESS'] = function (common, data) {
var content = data.content;
var msg = content.msg;
// Check authenticity
if (msg.author !== msg.content.user.curvePublic) { return; }
// Display the notification
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var title = Util.fixHTML(msg.content.title);
content.getFormatText = function () {
return Messages._getKey('requestEdit_request', [title, name]);
};
// if not archived, add handlers
content.handler = function () {
var metadataMgr = common.getMetadataMgr();
var priv = metadataMgr.getPrivateData();
var link = h('a', {
href: '#'
}, Messages.requestEdit_viewPad);
var verified = h('p');
var $verified = $(verified);
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var title = Util.fixHTML(msg.content.title);
if (priv.friends && priv.friends[msg.author]) {
$verified.addClass('cp-notifications-requestedit-verified');
var f = priv.friends[msg.author];
$verified.append(h('span.fa.fa-certificate'));
var $avatar = $(h('span.cp-avatar')).appendTo($verified);
$verified.append(h('p', Messages._getKey('requestEdit_fromFriend', [f.displayName])));
common.displayAvatar($avatar, f.avatar, f.displayName);
} else {
$verified.append(Messages._getKey('requestEdit_fromStranger', [name]));
}
var div = h('div', [
UI.setHTML(h('p'), Messages._getKey('requestEdit_confirm', [title, name])),
verified,
link
]);
$(link).click(function (e) {
e.preventDefault();
e.stopPropagation();
common.openURL(msg.content.href);
});
UI.confirm(div, function (yes) {
if (!yes) { return; }
common.getSframeChannel().event('EV_GIVE_ACCESS', {
channel: msg.content.channel,
user: msg.content.user
});
defaultDismiss(common, data)();
}, {
ok: Messages.friendRequest_accept,
cancel: Messages.later
});
};
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
handlers['GIVE_PAD_ACCESS'] = function (common, data) {
var content = data.content;
var msg = content.msg;
// Check authenticity
if (msg.author !== msg.content.user.curvePublic) { return; }
if (!msg.content.href) { return; }
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var title = Util.fixHTML(msg.content.title);
// Display the notification
content.getFormatText = function () {
return Messages._getKey('requestEdit_accepted', [title, name]);
};
// if not archived, add handlers
content.handler = function () {
common.openURL(msg.content.href);
defaultDismiss(common, data)();
};
};
// NOTE: don't forget to fixHTML everything returned by "getFormatText"
return {
add: function (common, data) {
var type = data.content.msg.type;

@ -748,7 +748,8 @@ define([
var secret = Hash.getSecrets('file', parsed.hash);
if (!secret || !secret.channel) { return; }
var hexFileName = secret.channel;
var src = Hash.getBlobPathFromHex(hexFileName);
var fileHost = privateData.fileHost || privateData.origin;
var src = fileHost + Hash.getBlobPathFromHex(hexFileName);
var key = secret.keys && secret.keys.cryptKey;
var xhr = new XMLHttpRequest();
xhr.open('GET', src, true);

@ -34,7 +34,7 @@ define([
var sendDriveEvent = function () {};
var registerProxyEvents = function () {};
var storeHash;
var storeHash, storeChannel;
var store = window.CryptPad_AsyncStore = {
modules: {}
@ -239,6 +239,20 @@ define([
Store.removeOwnedChannel = function (clientId, data, cb) {
if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
// "data" used to be a string (channelID), now it can also be an object
// data.force tells us we can safely remove the drive ID
var channel = data;
var force = false;
if (data && typeof(data) === "object") {
channel = data.channel;
force = data.force;
}
if (channel === storeChannel && !force) {
return void cb({error: 'User drive removal blocked!'});
}
store.rpc.removeOwnedChannel(data, function (err) {
cb({error:err});
});
@ -573,7 +587,10 @@ define([
}));
}).nThen(function (waitFor) {
// Delete Drive
Store.removeOwnedChannel(clientId, secret.channel, waitFor());
Store.removeOwnedChannel(clientId, {
channel: secret.channel,
force: true
}, waitFor());
}).nThen(function () {
store.network.disconnect();
cb({
@ -721,7 +738,10 @@ define([
var object = getAttributeObject(data.attr);
object.obj[object.key] = data.value;
} catch (e) { return void cb({error: e}); }
onSync(cb);
onSync(function () {
cb();
broadcast([], "UPDATE_METADATA");
});
};
Store.getAttribute = function (clientId, data, cb) {
var object;
@ -786,6 +806,7 @@ define([
var h = p.hashData;
if (AppConfig.disableAnonymousStore && !store.loggedIn) { return void cb(); }
if (p.type === "debug") { return void cb(); }
var channelData = Store.channels && Store.channels[channel];
@ -1191,10 +1212,7 @@ define([
},
noChainPad: true,
channel: data.channel,
validateKey: data.validateKey,
owners: data.owners,
password: data.password,
expire: data.expire,
metadata: data.metadata,
network: store.network,
//readOnly: data.readOnly,
onConnect: function (wc, sendMessage) {
@ -1244,6 +1262,128 @@ define([
channel.sendMessage(msg, clientId, cb);
};
// requestPadAccess is used to check if we have a way to contact the owner
// of the pad AND to send the request if we want
// data.send === false ==> check if we can contact them
// data.send === true ==> send the request
Store.requestPadAccess = function (clientId, data, cb) {
var owner = data.owner;
var channel = channels[data.channel];
if (!channel) { return void cb({error: 'ENOTFOUND'}); }
if (!data.send && channel && (!channel.data || !channel.data.channel)) {
var i = 0;
var it = setInterval(function () {
if (channel.data && channel.data.channel) {
clearInterval(it);
Store.requestPadAccess(clientId, data, cb);
return;
}
if (i >= 300) { // One minute timeout
clearInterval(it);
return void cb({error: 'ETIMEOUT'});
}
i++;
}, 200);
return;
}
// If the owner was not is the pad metadata, check if it is a friend.
// We'll contact the first owner for whom we know the mailbox
var fData = channel.data || {};
if (!owner && fData.owners) {
var friends = store.proxy.friends || {};
if (Object.keys(friends).length > 1) {
fData.owners.some(function (edPublic) {
return Object.keys(friends).some(function (curve) {
if (curve === "me") { return; }
if (edPublic === friends[curve].edPublic &&
friends[curve].notifications) {
owner = friends[curve];
return true;
}
});
});
}
}
// If send is true, send the request to the owner.
if (owner) {
if (data.send) {
var myData = Messaging.createData(store.proxy);
delete myData.channel;
store.mailbox.sendTo('REQUEST_PAD_ACCESS', {
channel: data.channel,
user: myData
}, {
channel: owner.notifications,
curvePublic: owner.curvePublic
}, function () {
cb({state: true});
});
return;
}
return void cb({state: true});
}
cb({state: false});
};
Store.givePadAccess = function (clientId, data, cb) {
var edPublic = store.proxy.edPublic;
var channel = data.channel;
var res = store.manager.findChannel(channel);
if (!data.user || !data.user.notifications || !data.user.curvePublic) {
return void cb({error: 'EINVAL'});
}
var href, title;
if (!res.some(function (obj) {
if (obj.data &&
Array.isArray(obj.data.owners) && obj.data.owners.indexOf(edPublic) !== -1 &&
obj.data.href) {
href = obj.data.href;
title = obj.data.title;
return true;
}
})) { return void cb({error: 'ENOTFOUND'}); }
var myData = Messaging.createData(store.proxy);
delete myData.channel;
store.mailbox.sendTo("GIVE_PAD_ACCESS", {
channel: channel,
href: href,
title: title,
user: myData
}, {
channel: data.user.notifications,
curvePublic: data.user.curvePublic
});
cb();
};
Store.getPadMetadata = function (clientId, data, cb) {
if (!data.channel) { return void cb({ error: 'ENOTFOUND'}); }
var channel = channels[data.channel];
if (!channel) { return void cb({ error: 'ENOTFOUND' }); }
if (!channel.data || !channel.data.channel) {
var i = 0;
var it = setInterval(function () {
if (channel.data && channel.data.channel) {
clearInterval(it);
Store.getPadMetadata(clientId, data, cb);
return;
}
if (i >= 300) { // One minute timeout
clearInterval(it);
return void cb({error: 'ETIMEOUT'});
}
i++;
}, 200);
return;
}
cb(channel.data || {});
};
// GET_FULL_HISTORY from sframe-common-outer
Store.getFullHistory = function (clientId, data, cb) {
var network = store.network;
@ -1350,14 +1490,16 @@ define([
websocketURL: NetConfig.getWebsocketURL(),
channel: secret.channel,
readOnly: false,
validateKey: secret.keys.validateKey || undefined,
crypto: Crypto.createEncryptor(secret.keys),
userName: 'sharedFolder',
logLevel: 1,
ChainPad: ChainPad,
classic: true,
network: store.network,
owners: owners
metadata: {
validateKey: secret.keys.validateKey || undefined,
owners: owners
}
};
var rt = Listmap.create(listmapConfig);
store.sharedFolders[id] = rt;
@ -1824,6 +1966,7 @@ define([
}
// No password for drive
var secret = Hash.getSecrets('drive', hash);
storeChannel = secret.channel;
var listmapConfig = {
data: {},
websocketURL: NetConfig.getWebsocketURL(),

@ -201,6 +201,64 @@ define([
}
};
// Hide duplicates when receiving a SUPPORT_MESSAGE notification
var supportMessage = false;
handlers['SUPPORT_MESSAGE'] = function (ctx, box, data, cb) {
if (supportMessage) { return void cb(true); }
supportMessage = true;
cb();
};
// Incoming edit rights request: add data before sending it to inner
handlers['REQUEST_PAD_ACCESS'] = function (ctx, box, data, cb) {
var msg = data.msg;
var content = msg.content;
if (msg.author !== content.user.curvePublic) { return void cb(true); }
var channel = content.channel;
var res = ctx.store.manager.findChannel(channel);
if (!res.length) { return void cb(true); }
var edPublic = ctx.store.proxy.edPublic;
var title, href;
if (!res.some(function (obj) {
if (obj.data &&
Array.isArray(obj.data.owners) && obj.data.owners.indexOf(edPublic) !== -1 &&
obj.data.href) {
href = obj.data.href;
title = obj.data.filename || obj.data.title;
return true;
}
})) { return void cb(true); }
content.title = title;
content.href = href;
cb(false);
};
handlers['GIVE_PAD_ACCESS'] = function (ctx, box, data, cb) {
var msg = data.msg;
var content = msg.content;
if (msg.author !== content.user.curvePublic) { return void cb(true); }
var channel = content.channel;
var res = ctx.store.manager.findChannel(channel);
var title;
res.forEach(function (obj) {
if (obj.data && !obj.data.href) {
if (!title) { title = obj.data.filename || obj.data.title; }
obj.data.href = content.href;
}
});
content.title = title || content.title;
cb(false);
};
return {
add: function (ctx, box, data, cb) {
/**

@ -92,9 +92,11 @@ define([
var hk = network.historyKeeper;
var cfg = {
validateKey: obj.validateKey,
lastKnownHash: chan.lastKnownHash || chan.lastCpHash,
owners: obj.owners,
expire: obj.expire
metadata: {
lastKnownHash: chan.lastKnownHash || chan.lastCpHash,
owners: obj.owners,
expire: obj.expire
}
};
var msg = ['GET_HISTORY', wc.id, cfg];
// Add the validateKey if we are the channel creator and we have a validateKey

@ -78,6 +78,9 @@ define([
GET_FULL_HISTORY: Store.getFullHistory,
GET_HISTORY_RANGE: Store.getHistoryRange,
IS_NEW_CHANNEL: Store.isNewChannel,
REQUEST_PAD_ACCESS: Store.requestPadAccess,
GIVE_PAD_ACCESS: Store.givePadAccess,
GET_PAD_METADATA: Store.getPadMetadata,
// Drive
DRIVE_USEROBJECT: Store.userObjectCommand,
// Settings,

@ -65,6 +65,7 @@ define([
if (box) {
actual += box.length;
var progressValue = (actual / estimate * 100);
progressValue = Math.min(progressValue, 100);
updateProgress(progressValue);
return void sendChunk(box, function (e) {

@ -81,17 +81,25 @@ define([
// If the type is a query, your handler will be invoked with a reply function that takes
// one argument (the content to reply with).
chan.on = function (queryType, handler, quiet) {
(handlers[queryType] = handlers[queryType] || []).push(function (data, msg) {
var h = function (data, msg) {
handler(data.content, function (replyContent) {
postMsg(JSON.stringify({
txid: data.txid,
content: replyContent
}));
}, msg);
});
};
(handlers[queryType] = handlers[queryType] || []).push(h);
if (!quiet) {
event('EV_REGISTER_HANDLER', queryType);
}
return {
stop: function () {
var idx = handlers[queryType].indexOf(h);
if (idx === -1) { return; }
handlers[queryType].splice(idx, 1);
}
};
};
// If a particular handler is registered, call the callback immediately, otherwise it will be called

@ -461,6 +461,102 @@ define([
cb(id);
});
};
// convert a folder to a Shared Folder
var _convertFolderToSharedFolder = function (Env, data, cb) {
var path = data.path;
var folderElement = Env.user.userObject.find(path);
// don't try to convert top-level elements (trash, root, etc) to shared-folders
// TODO also validate that you're in root (not templates, etc)
if (data.path.length <= 1) {
return void cb({
error: 'E_INVAL_PATH',
});
}
if (_isInSharedFolder(Env, path)) {
return void cb({
error: 'E_INVAL_NESTING',
});
}
if (Env.user.userObject.hasSubSharedFolder(folderElement)) {
return void cb({
error: 'E_INVAL_NESTING',
});
}
var parentPath = path.slice(0, -1);
var parentFolder = Env.user.userObject.find(parentPath);
var folderName = path[path.length - 1];
var SFId;
nThen(function (waitFor) {
// create shared folder
_addSharedFolder(Env, {
path: parentPath,
name: folderName,
owned: data.owned, // XXX FIXME hardcoded preference
password: data.password || '', // XXX FIXME hardcoded preference
}, waitFor(function (id) {
// _addSharedFolder can be an id or an error
if (typeof(id) === 'object' && id && id.error) {
waitFor.abort();
return void cb(id);
} else {
SFId = id;
}
}));
}).nThen(function (waitFor) {
// move everything from folder to SF
if (!SFId) {
waitFor.abort();
return void cb({
error: 'E_NO_ID'
});
}
var paths = [];
for (var el in folderElement) {
if (Env.user.userObject.isFolder(folderElement[el]) || Env.user.userObject.isFile(folderElement[el])) {
paths.push(path.concat(el));
}
}
var SFKey;
// this is basically Array.find, except it works in IE
Object.keys(parentFolder).some(function (el) {
if (parentFolder[el] === SFId) {
SFKey = el;
return true;
}
});
if (!SFKey) {
waitFor.abort();
return void cb({
error: 'E_NO_KEY'
});
}
var newPath = parentPath.concat(SFKey).concat(UserObject.ROOT);
_move(Env, {
paths: paths,
newPath: newPath,
copy: false,
}, waitFor());
}).nThen(function () {
// migrate metadata
var sharedFolderElement = Env.user.proxy[UserObject.SHARED_FOLDERS][SFId];
var metadata = Env.user.userObject.getFolderData(folderElement);
for (var key in metadata) {
// it shouldn't be possible to have nested metadata
// but this is a reasonable sanity check
if (key === "metadata") { continue; }
// copy the metadata from the original folder to the new shared folder
sharedFolderElement[key] = metadata[key];
}
// remove folder
Env.user.userObject.delete([path], function () {
cb();
});
});
};
// Delete permanently some pads or folders
var _delete = function (Env, data, cb) {
data = data || {};
@ -598,6 +694,8 @@ define([
_addFolder(Env, data, cb); break;
case 'addSharedFolder':
_addSharedFolder(Env, data, cb); break;
case 'convertFolderToSharedFolder':
_convertFolderToSharedFolder(Env, data, cb); break;
case 'delete':
_delete(Env, data, cb); break;
case 'emptyTrash':
@ -914,6 +1012,16 @@ define([
}
}, cb);
};
var convertFolderToSharedFolderInner = function (Env, path, owned, password, cb) {
return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", {
cmd: "convertFolderToSharedFolder",
data: {
path: path,
owned: owned,
password: password
}
}, cb);
};
var deleteInner = function (Env, paths, cb) {
return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", {
cmd: "delete",
@ -1074,6 +1182,9 @@ define([
}
return Env.user.userObject.hasSubfolder(el, trashRoot);
};
var hasSubSharedFolder = function (Env, el) {
return Env.user.userObject.hasSubSharedFolder(el);
};
var hasFile = function (Env, el, trashRoot) {
if (Env.folders[el]) {
var uo = Env.folders[el].userObject;
@ -1113,6 +1224,7 @@ define([
emptyTrash: callWithEnv(emptyTrashInner),
addFolder: callWithEnv(addFolderInner),
addSharedFolder: callWithEnv(addSharedFolderInner),
convertFolderToSharedFolder: callWithEnv(convertFolderToSharedFolderInner),
delete: callWithEnv(deleteInner),
restore: callWithEnv(restoreInner),
setFolderData: callWithEnv(setFolderDataInner),
@ -1144,6 +1256,7 @@ define([
isInTrashRoot: callWithEnv(isInTrashRoot),
comparePath: callWithEnv(comparePath),
hasSubfolder: callWithEnv(hasSubfolder),
hasSubSharedFolder: callWithEnv(hasSubSharedFolder),
hasFile: callWithEnv(hasFile),
// Data
user: Env.user,

@ -314,11 +314,21 @@ define([
var newPad = false;
if (newContentStr === '') { newPad = true; }
var privateDat = cpNfInner.metadataMgr.getPrivateData();
var type = privateDat.app;
// contentUpdate may be async so we need an nthen here
nThen(function (waitFor) {
if (!newPad) {
var newContent = JSON.parse(newContentStr);
cpNfInner.metadataMgr.updateMetadata(extractMetadata(newContent));
var metadata = extractMetadata(newContent);
if (metadata && typeof(metadata.type) !== 'undefined' && metadata.type !== type) {
var errorText = Messages.typeError;
UI.errorLoadingScreen(errorText);
waitFor.abort();
return;
}
cpNfInner.metadataMgr.updateMetadata(metadata);
newContent = normalize(newContent);
contentUpdate(newContent, waitFor);
} else {
@ -356,8 +366,6 @@ define([
UI.removeLoadingScreen(emitResize);
var privateDat = cpNfInner.metadataMgr.getPrivateData();
var type = privateDat.app;
if (AppConfig.textAnalyzer && textContentGetter) {
AppConfig.textAnalyzer(textContentGetter, privateDat.channel);
}
@ -401,7 +409,7 @@ define([
var ext = (typeof(extension) === 'function') ? extension() : extension;
var suggestion = title.suggestTitle('cryptpad-document');
UI.prompt(Messages.exportPrompt,
Util.fixFileName(suggestion) + '.' + ext, function (filename)
Util.fixFileName(suggestion) + ext, function (filename)
{
if (!(typeof(filename) === 'string' && filename)) { return; }
if (async) {
@ -457,7 +465,7 @@ define([
if (data.type !== 'file') { console.log('unhandled embed type ' + data.type); return; }
var privateDat = cpNfInner.metadataMgr.getPrivateData();
var origin = privateDat.fileHost || privateDat.origin;
var src = data.src = origin + data.src;
var src = data.src = data.src.slice(0,1) === '/' ? origin + data.src : data.src;
mediaTagEmbedder($('<media-tag src="' + src +
'" data-crypto-key="cryptpad:' + data.key + '"></media-tag>'), data);
}
@ -603,6 +611,7 @@ define([
'newpad',
'share',
'limit',
'request',
'unpinnedWarning',
'notifications'
],

@ -23,14 +23,12 @@ define([], function () {
var start = function (conf) {
var channel = conf.channel;
var Crypto = conf.crypto;
var validateKey = conf.validateKey;
var isNewHash = conf.isNewHash;
var readOnly = conf.readOnly || false;
var padRpc = conf.padRpc;
var sframeChan = conf.sframeChan;
var password = conf.password;
var owners = conf.owners;
var expire = conf.expire;
var metadata= conf.metadata || {};
var validateKey = metadata.validateKey;
var onConnect = conf.onConnect || function () { };
conf = undefined;
@ -127,11 +125,8 @@ define([], function () {
// join the netflux network, promise to handle opening of the channel
padRpc.joinPad({
channel: channel || null,
validateKey: validateKey,
readOnly: readOnly,
owners: owners,
password: password,
expire: expire
metadata: metadata
});
};

@ -39,7 +39,8 @@ define([
};
module.getContentExtension = function (mode) {
return (Modes.extensionOf(mode) || '.txt').slice(1);
var ext = Modes.extensionOf(mode);
return ext !== undefined ? ext : '.txt';
};
module.fileExporter = function (content) {
return new Blob([ content ], { type: 'text/plain;charset=utf-8' });
@ -61,6 +62,7 @@ define([
});
editor._noCursorUpdate = false;
editor.state.focused = true;
if(selects[0] === selects[1]) {
editor.setCursor(posToCursor(selects[0], remoteDoc));
}
@ -98,9 +100,17 @@ define([
// lines beginning with a hash are potentially valuable
// works for markdown, python, bash, etc.
var hash = /^#+(.*?)$/;
var hashAndLink = /^#+\s*\[(.*?)\]\(.*\)\s*$/;
if (hash.test(line)) {
// test for link inside the title, and set text just to the name of the link
if (hashAndLink.test(line)) {
line.replace(hashAndLink, function (a, one) {
text = Util.stripTags(one);
});
return true;
}
line.replace(hash, function (a, one) {
text = one;
text = Util.stripTags(one);
});
return true;
}
@ -135,6 +145,7 @@ define([
};
var editor = exp.editor = CMeditor.fromTextArea($textarea[0], {
allowDropFileTypes: [],
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
@ -323,7 +334,7 @@ define([
var mode;
if (!mime) {
var ext = /.+\.([^.]+)$/.exec(file.name);
if (ext[1]) {
if (ext && ext[1]) {
mode = CMeditor.findModeByExtension(ext[1]);
mode = mode && mode.mode || null;
}
@ -339,7 +350,8 @@ define([
exp.setMode('text');
$toolbarContainer.find('#language-mode').val('text');
}
return { content: content };
// return the mode so that the code editor can decide how to display the new content
return { content: content, mode: mode };
};
exp.setValueAndCursor = function (oldDoc, remoteDoc) {
@ -366,40 +378,39 @@ define([
return { content: canonicalize(editor.getValue()) };
};
exp.mkFileManager = function (framework) {
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 src = Hash.getBlobPathFromHex(secret.channel);
var key = Hash.encodeBase64(secret.keys.cryptKey);
var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + key + '"></media-tag>';
editor.replaceSelection(mt);
}
};
framework._.sfCommon.createFileManager(fmConfig);
};
exp.mkIndentSettings = function (metadataMgr) {
var setIndentation = function (units, useTabs, fontSize, spellcheck) {
if (typeof(units) !== 'number') { return; }
var doc = editor.getDoc();
editor.setOption('indentUnit', units);
editor.setOption('tabSize', units);
editor.setOption('indentWithTabs', useTabs);
editor.setOption('spellcheck', spellcheck);
if (!useTabs) {
editor.setOption("extraKeys", {
Tab: function() {
editor.replaceSelection(Array(units + 1).join(" "));
editor.setOption("extraKeys", {
Tab: function() {
if (doc.somethingSelected()) {
editor.execCommand("indentMore");
}
});
} else {
editor.setOption("extraKeys", {
Tab: undefined,
});
}
else {
if (!useTabs) { editor.execCommand("insertSoftTab"); }
else { editor.execCommand("insertTab"); }
}
},
"Shift-Tab": function () {
editor.execCommand("indentLess");
},
"Backspace": function () {
var cursor = doc.getCursor();
var line = doc.getLine(cursor.line);
var beforeCursor = line.substring(0, cursor.ch);
if (beforeCursor && beforeCursor.trim() === "") {
editor.execCommand("indentLess");
} else {
editor.execCommand("delCharBefore");
}
},
});
$('.CodeMirror').css('font-size', fontSize+'px');
};

@ -1,6 +1,7 @@
define([
'jquery',
'/file/file-crypto.js',
'/common/make-backup.js',
'/common/common-thumbnail.js',
'/common/common-interface.js',
'/common/common-ui-elements.js',
@ -11,9 +12,8 @@ define([
'/bower_components/file-saver/FileSaver.min.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
], function ($, FileCrypto, Thumb, UI, UIElements, Util, Hash, h, Messages) {
], function ($, FileCrypto, MakeBackup, Thumb, UI, UIElements, Util, Hash, h, Messages) {
var Nacl = window.nacl;
var saveAs = window.saveAs;
var module = {};
var blobToArrayBuffer = function (blob, cb) {
@ -42,16 +42,19 @@ define([
return 'cp-fileupload-element-' + String(Math.random()).substring(2);
};
var tableHeader = h('div.cp-fileupload-header', [
h('div.cp-fileupload-header-title', h('span', Messages.fileuploadHeader || 'Uploaded files')),
h('div.cp-fileupload-header-close', h('span.fa.fa-times')),
]);
var $table = File.$table = $('<table>', { id: 'cp-fileupload-table' });
var $thead = $('<tr>').appendTo($table);
$('<td>').text(Messages.upload_type).appendTo($thead);
$('<td>').text(Messages.upload_name).appendTo($thead);
$('<td>').text(Messages.upload_size).appendTo($thead);
$('<td>').text(Messages.upload_progress).appendTo($thead);
$('<td>').text(Messages.cancel).appendTo($thead);
var createTableContainer = function ($body) {
File.$container = $('<div>', { id: 'cp-fileupload' }).append($table).appendTo($body);
File.$container = $('<div>', { id: 'cp-fileupload' }).append(tableHeader).append($table).appendTo($body);
$('.cp-fileupload-header-close').click(function () {
File.$container.fadeOut();
});
return File.$container;
};
@ -100,16 +103,19 @@ define([
var $row = $table.find('tr[id="'+id+'"]');
$row.find('.cp-fileupload-table-cancel').html('-');
$row.find('.cp-fileupload-table-cancel').addClass('success').html('').append(h('span.fa.fa-minus'));
var $pv = $row.find('.cp-fileupload-table-progress-value');
var $pb = $row.find('.cp-fileupload-table-progress-container');
var $pc = $row.find('.cp-fileupload-table-progress');
var $pb = $row.find('.cp-fileupload-table-progressbar');
var $link = $row.find('.cp-fileupload-table-link');
/**
* Update progress in the download panel, for uploading a file
* @param {number} progressValue Progression of download, between 0 and 100
*/
updateProgress = function (progressValue) {
$pv.text(Math.round(progressValue*100)/100 + '%');
$pv.text(Math.round(progressValue * 100) / 100 + '%');
$pb.css({
width: (progressValue/100)*$pc.width()+'px'
width: progressValue + '%'
});
};
@ -179,8 +185,14 @@ define([
clearTimeout(queue.to);
queue.to = window.setTimeout(function () {
if (config.keepTable) { return; }
File.$container.fadeOut();
}, 3000);
// don't hide panel if mouse over
if (File.$container.is(":hover")) {
File.$container.one("mouseleave", function () { File.$container.fadeOut(); });
}
else {
File.$container.fadeOut();
}
}, 60000);
return;
}
if (queue.inProgress) { return; }
@ -199,8 +211,9 @@ define([
window.setTimeout(function () { $table.show(); });
var estimate = obj.dl ? obj.size : FileCrypto.computeEncryptedSize(obj.blob.byteLength, obj.metadata);
var $progressBar = $('<div>', {'class':'cp-fileupload-table-progress-container'});
var $progressValue = $('<span>', {'class':'cp-fileupload-table-progress-value'}).text(Messages.upload_pending);
var $progressContainer = $('<div>', {'class':'cp-fileupload-table-progress-container'});
$('<div>', {'class':'cp-fileupload-table-progressbar'}).appendTo($progressContainer);
$('<span>', {'class':'cp-fileupload-table-progress-value'}).text(Messages.upload_pending).appendTo($progressContainer);
var $tr = $('<tr>', {id: id}).appendTo($table);
var $lines = $table.find('tr[id]');
@ -211,19 +224,28 @@ define([
var $cancel = $('<span>', {'class': 'cp-fileupload-table-cancel-button fa fa-times'}).click(function () {
queue.queue = queue.queue.filter(function (el) { return el.id !== id; });
$cancel.remove();
$tr.find('.cp-fileupload-table-cancel').text('-');
$tr.find('.cp-fileupload-table-cancel').addClass('cancelled').html('').append(h('span.fa.fa-minus'));
$tr.find('.cp-fileupload-table-progress-value').text(Messages.upload_cancelled);
});
var $link = $('<a>', {
'class': 'cp-fileupload-table-link',
'rel': 'noopener noreferrer'
}).text(obj.dl ? obj.name : obj.metadata.name);
}).append(h('span.cp-fileupload-table-name', obj.dl ? obj.name : obj.metadata.name));
var typeIcon;
if (obj.dl) { typeIcon = h('span.fa.fa-arrow-down', { title: Messages.download_dl }); }
else { typeIcon = h('span.fa.fa-arrow-up', { title: Messages.upload_up }); }
$('<td>').text(obj.dl ? Messages.download_dl : Messages.upload_up).appendTo($tr);
// type (download / upload)
$('<td>', {'class': 'cp-fileupload-table-type'}).append(typeIcon).appendTo($tr);
// name
$('<td>').append($link).appendTo($tr);
// size
$('<td>').text(prettySize(estimate)).appendTo($tr);
$('<td>', {'class': 'cp-fileupload-table-progress'}).append($progressBar).append($progressValue).appendTo($tr);
// progress
$('<td>', {'class': 'cp-fileupload-table-progress'}).append($progressContainer).appendTo($tr);
// cancel
$('<td>', {'class': 'cp-fileupload-table-cancel'}).append($cancel).appendTo($tr);
queue.next();
@ -234,37 +256,32 @@ define([
owned: true,
store: true
};
var fileUploadModal = function (file, cb) {
var extIdx = file.name.lastIndexOf('.');
var name = extIdx !== -1 ? file.name.slice(0,extIdx) : file.name;
var ext = extIdx !== -1 ? file.name.slice(extIdx) : "";
var createHelper = function (href, text) {
var q = h('a.fa.fa-question-circle', {
style: 'text-decoration: none !important;',
title: text,
href: origin + href,
target: "_blank",
'data-tippy-placement': "right"
});
return q;
};
var createHelper = function (href, text) {
return UI.createHelper(origin + href, text);
};
var createManualStore = function (isFolderUpload) {
var privateData = common.getMetadataMgr().getPrivateData();
var autoStore = Util.find(privateData, ['settings', 'general', 'autostore']) || 0;
var initialState = modalState.owned || modalState.store;
var initialDisabled = modalState.owned ? { disabled: true } : {};
var manualStore = autoStore === 1 ? undefined :
UI.createCheckbox('cp-upload-store', Messages.autostore_forceSave, initialState, {
input: initialDisabled
});
UI.createCheckbox('cp-upload-store', isFolderUpload ? (Messages.uploadFolder_modal_forceSave) : Messages.autostore_forceSave, initialState, {
input: initialDisabled
});
return manualStore;
};
var fileUploadModal = function (defaultFileName, cb) {
var parsedName = /^(\.?.+?)(\.[^.]+)?$/.exec(defaultFileName) || [];
var ext = parsedName[2] || "";
var manualStore = createManualStore();
// Ask for name, password and owner
var content = h('div', [
h('h4', Messages.upload_modal_title),
UIElements.setHTML(h('label', {for: 'cp-upload-name'}),
Messages._getKey('upload_modal_filename', [ext])),
h('input#cp-upload-name', {type: 'text', placeholder: name}),
h('input#cp-upload-name', {type: 'text', placeholder: defaultFileName, value: defaultFileName}),
h('label', {for: 'cp-upload-password'}, Messages.creation_passwordValue),
UI.passwordInput({id: 'cp-upload-password'}),
h('span', {
@ -277,7 +294,7 @@ define([
]);
$(content).find('#cp-upload-owned').on('change', function () {
var val = $(content).find('#cp-upload-owned').is(':checked');
var val = Util.isChecked($(content).find('#cp-upload-owned'));
if (val) {
$(content).find('#cp-upload-store').prop('checked', true).prop('disabled', true);
} else {
@ -291,14 +308,14 @@ define([
// Get the values
var newName = $(content).find('#cp-upload-name').val();
var password = $(content).find('#cp-upload-password').val() || undefined;
var owned = $(content).find('#cp-upload-owned').is(':checked');
var forceSave = owned || $(content).find('#cp-upload-store').is(':checked');
var owned = Util.isChecked($(content).find('#cp-upload-owned'));
var forceSave = owned || Util.isChecked($(content).find('#cp-upload-store'));
modalState.owned = owned;
modalState.store = forceSave;
// Add extension to the name if needed
if (!newName || !newName.trim()) { newName = file.name; }
if (!newName || !newName.trim()) { newName = defaultFileName; }
var newExtIdx = newName.lastIndexOf('.');
var newExt = newExtIdx !== -1 ? newName.slice(newExtIdx) : "";
if (newExt !== ext) { newName += ext; }
@ -312,12 +329,64 @@ define([
});
};
File.showFolderUploadModal = function (foldername, cb) {
var manualStore = createManualStore(true);
// Ask for name, password and owner
var content = h('div', [
h('h4', Messages.uploadFolder_modal_title),
UIElements.setHTML(h('label', {for: 'cp-upload-name'}), Messages.fm_folderName),
h('input#cp-upload-foldername', {type: 'text', placeholder: foldername, value: foldername}),
h('label', {for: 'cp-upload-password'}, Messages.uploadFolder_modal_filesPassword),
UI.passwordInput({id: 'cp-upload-password'}),
h('span', {
style: 'display:flex;align-items:center;justify-content:space-between'
}, [
UI.createCheckbox('cp-upload-owned', Messages.uploadFolder_modal_owner, modalState.owned),
createHelper('/faq.html#keywords-owned', Messages.creation_owned1)
]),
manualStore
]);
$(content).find('#cp-upload-owned').on('change', function () {
var val = Util.isChecked($(content).find('#cp-upload-owned'));
if (val) {
$(content).find('#cp-upload-store').prop('checked', true).prop('disabled', true);
} else {
$(content).find('#cp-upload-store').prop('disabled', false);
}
});
UI.confirm(content, function (yes) {
if (!yes) { return void cb(); }
// Get the values
var newName = $(content).find('#cp-upload-foldername').val();
var password = $(content).find('#cp-upload-password').val() || undefined;
var owned = Util.isChecked($(content).find('#cp-upload-owned'));
var forceSave = owned || Util.isChecked($(content).find('#cp-upload-store'));
modalState.owned = owned;
modalState.store = forceSave;
if (!newName || !newName.trim()) { newName = foldername; }
cb({
folderName: newName,
password: password,
owned: owned,
forceSave: forceSave
});
});
};
var handleFileState = {
queue: [],
inProgress: false
};
var handleFile = File.handleFile = function (file, e) {
if (handleFileState.inProgress) { return void handleFileState.queue.push([file, e]); }
/* if defaultOptions is passed, the function does not show the upload options modal, and directly save the file with the specified options */
var handleFile = File.handleFile = function (file, e, defaultOptions) {
if (handleFileState.inProgress) { return void handleFileState.queue.push([file, e, defaultOptions]); }
handleFileState.inProgress = true;
var thumb;
@ -345,7 +414,7 @@ define([
handleFileState.inProgress = false;
if (handleFileState.queue.length) {
var next = handleFileState.queue.shift();
handleFile(next[0], next[1]);
handleFile(next[0], next[1], next[2]);
}
};
var getName = function () {
@ -354,20 +423,31 @@ define([
if (config.noStore) { return void finish(); }
// Otherwise, ask for password, name and ownership
fileUploadModal(file, function (obj) {
if (!obj) { return void finish(true); }
name = obj.name;
password = obj.password;
owned = obj.owned;
forceSave = obj.forceSave;
finish();
});
// if default options were passed, upload file immediately
if (defaultOptions && typeof defaultOptions === "object") {
name = defaultOptions.name || file.name;
password = defaultOptions.password || undefined;
owned = !!defaultOptions.owned;
forceSave = !!defaultOptions.forceSave;
return void finish();
}
// if no default options were passed, ask the user
else {
fileUploadModal(file.name, function (obj) {
if (!obj) { return void finish(true); }
name = obj.name;
password = obj.password;
owned = obj.owned;
forceSave = obj.forceSave;
finish();
});
}
};
blobToArrayBuffer(file, function (e, buffer) {
if (e) { console.error(e); }
file_arraybuffer = buffer;
if (!Thumb.isSupportedType(file.type)) { return getName(); }
if (!Thumb.isSupportedType(file)) { return getName(); }
// make a resized thumbnail from the image..
Thumb.fromBlob(file, function (e, thumb64) {
if (e) { console.error(e); }
@ -446,124 +526,135 @@ define([
createUploader(config.dropArea, config.hoverArea, config.body);
File.downloadFile = function (fData, cb) {
var parsed = Hash.parsePadUrl(fData.href || fData.roHref);
var hash = parsed.hash;
var name = fData.filename || fData.title;
var secret = Hash.getSecrets('file', hash, fData.password);
var src = Hash.getBlobPathFromHex(secret.channel);
var key = secret.keys && secret.keys.cryptKey;
common.getFileSize(secret.channel, function (e, data) {
var todo = function (file) {
if (queue.inProgress) { return; }
queue.inProgress = true;
var id = file.id;
var $row = $table.find('tr[id="'+id+'"]');
var $pv = $row.find('.cp-fileupload-table-progress-value');
var $pb = $row.find('.cp-fileupload-table-progress-container');
var $pc = $row.find('.cp-fileupload-table-progress');
var $link = $row.find('.cp-fileupload-table-link');
var done = function () {
$row.find('.cp-fileupload-table-cancel').text('-');
queue.inProgress = false;
queue.next();
};
// TODO implement the ability to cancel downloads :D
var updateProgressbar = function (file, data, downloadFunction, cb) {
if (queue.inProgress) { return; }
queue.inProgress = true;
var id = file.id;
var updateDLProgress = function (progressValue) {
var text = Math.round(progressValue*100) + '%';
text += ' ('+ Messages.download_step1 +'...)';
$pv.text(text);
$pb.css({
width: progressValue * $pc.width()+'px'
});
};
var updateProgress = function (progressValue) {
var text = Math.round(progressValue*100) + '%';
text += progressValue === 1 ? '' : ' ('+ Messages.download_step2 +'...)';
$pv.text(text);
$pb.css({
width: progressValue * $pc.width()+'px'
});
};
var $row = $table.find('tr[id="'+id+'"]');
var $pv = $row.find('.cp-fileupload-table-progress-value');
var $pb = $row.find('.cp-fileupload-table-progressbar');
var $link = $row.find('.cp-fileupload-table-link');
var dl = module.downloadFile(fData, function (err, obj) {
$link.prepend($('<span>', {'class': 'fa fa-external-link'}))
.attr('href', '#')
.click(function (e) {
e.preventDefault();
obj.download();
});
done();
if (obj) { obj.download(); }
cb(err, obj);
}, {
src: src,
key: key,
name: name,
progress: updateDLProgress,
progress2: updateProgress,
});
var done = function () {
$row.find('.cp-fileupload-table-cancel').addClass('success').html('').append(h('span.fa.fa-check'));
queue.inProgress = false;
queue.next();
};
var $cancel = $('<span>', {'class': 'cp-fileupload-table-cancel-button fa fa-times'}).click(function () {
dl.cancel();
$cancel.remove();
$row.find('.cp-fileupload-table-progress-value').text(Messages.upload_cancelled);
done();
});
$row.find('.cp-fileupload-table-cancel').html('').append($cancel);
};
/*
var cancelled = function () {
$row.find('.cp-fileupload-table-cancel').addClass('cancelled').html('').append(h('span.fa.fa-minus'));
queue.inProgress = false;
queue.next();
};*/
/**
* Update progress in the download panel, for downloading a file
* @param {number} progressValue Progression of download, between 0 and 1
*/
var updateDLProgress = function (progressValue) {
var text = Math.round(progressValue * 100) + '%';
text += ' ('+ Messages.download_step1 + '...)';
$pv.text(text);
$pb.css({
width: (progressValue * 100) + '%'
});
};
/**
* Update progress in the download panel, for decrypting a file (after downloading it)
* @param {number} progressValue Progression of download, between 0 and 1
*/
var updateDecryptProgress = function (progressValue) {
var text = Math.round(progressValue * 100) + '%';
text += progressValue === 1 ? '' : ' (' + Messages.download_step2 + '...)';
$pv.text(text);
$pb.css({
width: (progressValue * 100) + '%'
});
};
/**
* As updateDLProgress but for folders
* @param {number} progressValue Progression of download, between 0 and 1
*/
var updateProgress = function (progressValue) {
var text = Math.round(progressValue*100) + '%';
$pv.text(text);
$pb.css({
width: (progressValue * 100) + '%'
});
};
var privateData = common.getMetadataMgr().getPrivateData();
var ctx = {
fileHost: privateData.fileHost,
get: common.getPad,
sframeChan: sframeChan,
};
downloadFunction(ctx, data, function (err, obj) {
$link.prepend($('<span>', {'class': 'fa fa-external-link'}))
.attr('href', '#')
.click(function (e) {
e.preventDefault();
obj.download();
});
done();
if (obj) { obj.download(); }
cb(err, obj);
}, {
progress: updateDLProgress,
progress2: updateDecryptProgress,
folderProgress: updateProgress,
});
/*
var $cancel = $('<span>', {'class': 'cp-fileupload-table-cancel-button fa fa-times'}).click(function () {
dl.cancel();
$cancel.remove();
$row.find('.cp-fileupload-table-progress-value').text(Messages.upload_cancelled);
cancelled();
});
*/
$row.find('.cp-fileupload-table-cancel')
.html('')
.append(h('span.fa.fa-minus'));
//.append($cancel);
};
File.downloadFile = function (fData, cb) {
var name = fData.filename || fData.title;
common.getFileSize(fData.channel, function (e, data) {
queue.push({
dl: todo,
dl: function (file) { updateProgressbar(file, fData, MakeBackup.downloadFile, cb); },
size: data,
name: name
});
});
};
return File;
};
module.downloadFile = function (fData, cb, obj) {
var cancelled = false;
var cancel = function () {
cancelled = true;
File.downloadPad = function (pData, cb) {
queue.push({
dl: function (file) { updateProgressbar(file, pData, MakeBackup.downloadPad, cb); },
size: 0,
name: pData.title,
});
};
var src, key, name;
if (obj && obj.src && obj.key && obj.name) {
src = obj.src;
key = obj.key;
name = obj.name;
} else {
var parsed = Hash.parsePadUrl(fData.href || fData.roHref);
var hash = parsed.hash;
name = fData.filename || fData.title;
var secret = Hash.getSecrets('file', hash, fData.password);
src = Hash.getBlobPathFromHex(secret.channel);
key = secret.keys && secret.keys.cryptKey;
}
Util.fetch(src, function (err, u8) {
if (cancelled) { return; }
if (err) { return void cb('E404'); }
FileCrypto.decrypt(u8, key, function (err, res) {
if (cancelled) { return; }
if (err) { return void cb(err); }
if (!res.content) { return void cb('EEMPTY'); }
var dl = function () {
saveAs(res.content, name || res.metadata.name);
};
cb(null, {
metadata: res.metadata,
content: res.content,
download: dl
});
}, obj && obj.progress2);
}, obj && obj.progress);
return {
cancel: cancel
File.downloadFolder = function (data, cb) {
queue.push({
dl: function (file) { updateProgressbar(file, data, MakeBackup.downloadFolder, cb); },
size: 0,
name: data.folderName,
});
};
return File;
};
return module;
});

@ -53,7 +53,7 @@ define([
'data-hash': data.content.hash
}, [h('div.cp-notification-content', h('p', formatData(data)))]);
if (data.content.getFormatText) {
if (typeof(data.content.getFormatText) === "function") {
$(notif).find('.cp-notification-content p').html(data.content.getFormatText());
}

@ -272,23 +272,27 @@ define([
var parsed = Utils.Hash.parsePadUrl(window.location.href);
if (!parsed.type) { throw new Error(); }
var defaultTitle = Utils.Hash.getDefaultName(parsed);
var edPublic;
var edPublic, curvePublic, notifications, isTemplate;
var forceCreationScreen = cfg.useCreationScreen &&
sessionStorage[Utils.Constants.displayPadCreationScreen];
delete sessionStorage[Utils.Constants.displayPadCreationScreen];
var updateMeta = function () {
//console.log('EV_METADATA_UPDATE');
var metaObj, isTemplate;
var metaObj;
nThen(function (waitFor) {
Cryptpad.getMetadata(waitFor(function (err, m) {
if (err) { console.log(err); }
metaObj = m;
edPublic = metaObj.priv.edPublic; // needed to create an owned pad
curvePublic = metaObj.user.curvePublic;
notifications = metaObj.user.notifications;
}));
Cryptpad.isTemplate(window.location.href, waitFor(function (err, t) {
if (err) { console.log(err); }
isTemplate = t;
}));
if (typeof(isTemplate) === "undefined") {
Cryptpad.isTemplate(window.location.href, waitFor(function (err, t) {
if (err) { console.log(err); }
isTemplate = t;
}));
}
}).nThen(function (/*waitFor*/) {
metaObj.doc = {
defaultTitle: defaultTitle,
@ -317,6 +321,9 @@ define([
channel: secret.channel,
enableSF: localStorage.CryptPad_SF === "1", // TODO to remove when enabled by default
devMode: localStorage.CryptPad_dev === "1",
fromFileData: Cryptpad.fromFileData ? {
title: Cryptpad.fromFileData.title
} : undefined,
};
if (window.CryptPad_newSharedFolder) {
additionalPriv.newSharedFolder = window.CryptPad_newSharedFolder;
@ -357,6 +364,8 @@ define([
sframeChan.event("EV_NEW_VERSION");
});
// Put in the following function the RPC queries that should also work in filepicker
var addCommonRpc = function (sframeChan) {
sframeChan.on('Q_ANON_RPC_MESSAGE', function (data, cb) {
@ -735,6 +744,7 @@ define([
var initShareModal = function (cfg) {
cfg.hashes = hashes;
cfg.password = password;
cfg.isTemplate = isTemplate;
// cfg.hidden means pre-loading the filepicker while keeping it hidden.
// if cfg.hidden is true and the iframe already exists, do nothing
if (!ShareModal.$iframe) {
@ -808,6 +818,22 @@ define([
});
});
sframeChan.on('Q_GET_FILE_THUMBNAIL', function (data, cb) {
if (!Cryptpad.fromFileData || !Cryptpad.fromFileData.href) {
return void cb({
error: "EINVAL",
});
}
var key = getKey(Cryptpad.fromFileData.href, Cryptpad.fromFileData.channel);
Utils.LocalStore.getThumbnail(key, function (e, data) {
if (data === "EMPTY") { data = null; }
cb({
error: e,
data: data
});
});
});
sframeChan.on('EV_GOTO_URL', function (url) {
if (url) {
window.location.href = url;
@ -835,13 +861,6 @@ define([
Cryptpad.setLanguage(data, cb);
});
sframeChan.on('Q_CLEAR_OWNED_CHANNEL', function (channel, cb) {
Cryptpad.clearOwnedChannel(channel, cb);
});
sframeChan.on('Q_REMOVE_OWNED_CHANNEL', function (channel, cb) {
Cryptpad.removeOwnedChannel(channel, cb);
});
sframeChan.on('Q_GET_ALL_TAGS', function (data, cb) {
Cryptpad.listAllTags(function (err, tags) {
cb({
@ -868,6 +887,9 @@ define([
Cryptpad.removeLoginBlock(data, cb);
});
// It seems we have performance issues when we open and close a lot of channels over
// the same network, maybe a memory leak. To fix this, we kill and create a new
// network every 30 cryptget calls (1 call = 1 channel)
var cgNetwork;
var whenCGReady = function (cb) {
if (cgNetwork && cgNetwork !== true) { console.log(cgNetwork); return void cb(); }
@ -884,7 +906,12 @@ define([
error: err,
data: val
});
}, data.opts);
}, data.opts, function (progress) {
sframeChan.event("EV_CRYPTGET_PROGRESS", {
hash: data.hash,
progress: progress,
});
});
};
//return void todo();
if (i > 30) {
@ -941,6 +968,42 @@ define([
sframeChan.event('EV_WORKER_TIMEOUT');
});
sframeChan.on('EV_GIVE_ACCESS', function (data, cb) {
Cryptpad.padRpc.giveAccess(data, cb);
});
sframeChan.on('Q_REQUEST_ACCESS', function (data, cb) {
if (readOnly && hashes.editHash) {
return void cb({error: 'ALREADYKNOWN'});
}
var owner;
var crypto = Crypto.createEncryptor(secret.keys);
nThen(function (waitFor) {
// Try to get the owner's mailbox from the pad metadata first.
// If it's is an older owned pad, check if the owner is a friend
// or an acquaintance (from async-store directly in requestAccess)
Cryptpad.getPadMetadata({
channel: secret.channel
}, waitFor(function (obj) {
obj = obj || {};
if (obj.error) { return; }
if (obj.mailbox) {
try {
var dataStr = crypto.decrypt(obj.mailbox, true, true);
var data = JSON.parse(dataStr);
if (!data.notifications || !data.curvePublic) { return; }
owner = data;
} catch (e) { console.error(e); }
}
}));
}).nThen(function () {
Cryptpad.padRpc.requestAccess({
send: data,
channel: secret.channel,
owner: owner
}, cb);
});
});
if (cfg.messaging) {
Notifier.getPermission();
@ -1061,13 +1124,21 @@ define([
readOnly = false;
updateMeta();
var rtConfig = {};
var rtConfig = {
metadata: {}
};
if (data.owned) {
rtConfig.owners = [edPublic];
rtConfig.metadata.owners = [edPublic];
rtConfig.metadata.mailbox = Utils.crypto.encrypt(JSON.stringify({
notifications: notifications,
curvePublic: curvePublic
}));
}
if (data.expire) {
rtConfig.expire = data.expire;
rtConfig.metadata.expire = data.expire;
}
rtConfig.metadata.validateKey = (secret.keys && secret.keys.validateKey) || undefined;
Utils.rtConfig = rtConfig;
nThen(function(waitFor) {
if (data.templateId) {
@ -1080,11 +1151,11 @@ define([
}));
}
}).nThen(function () {
var cryptputCfg = $.extend(true, {}, rtConfig, {password: password});
if (data.template) {
// Pass rtConfig to useTemplate because Cryptput will create the file and
// we need to have the owners and expiration time in the first line on the
// server
var cryptputCfg = $.extend(true, {}, rtConfig, {password: password});
Cryptpad.useTemplate({
href: data.template
}, Cryptget, function () {
@ -1093,6 +1164,14 @@ define([
}, cryptputCfg);
return;
}
// if we open a new code from a file
if (Cryptpad.fromFileData) {
Cryptpad.useFile(Cryptget, function () {
startRealtime();
cb();
}, cryptputCfg);
return;
}
// Start realtime outside the iframe and callback
startRealtime(rtConfig);
cb();

@ -459,6 +459,14 @@ define([
});
}; */
funcs.getPad = function (data, cb) {
ctx.sframeChan.query("Q_CRYPTGET", data, function (err, obj) {
if (err) { return void cb(err); }
if (obj.error) { return void cb(obj.error); }
cb(null, obj.data);
}, { timeout: 60000 });
};
funcs.gotoURL = function (url) { ctx.sframeChan.event('EV_GOTO_URL', url); };
funcs.openURL = function (url) { ctx.sframeChan.event('EV_OPEN_URL', url); };
funcs.openUnsafeURL = function (url) {
@ -497,7 +505,7 @@ define([
};
var shortcuts = [];
funcs.addShortcuts = function (w) {
funcs.addShortcuts = function (w, isApp) {
w = w || window;
if (shortcuts.indexOf(w) !== -1) { return; }
shortcuts.push(w);
@ -505,7 +513,7 @@ define([
// Ctrl || Meta (mac)
if (e.ctrlKey || (navigator.platform === "MacIntel" && e.metaKey)) {
// Ctrl+E: New pad modal
if (e.which === 69) {
if (e.which === 69 && isApp) {
e.preventDefault();
return void funcs.createNewPadModal();
}
@ -611,22 +619,24 @@ define([
ctx.metadataMgr.onReady(waitFor());
funcs.addShortcuts();
}).nThen(function () {
var privateData = ctx.metadataMgr.getPrivateData();
funcs.addShortcuts(window, Boolean(privateData.app));
try {
var feedback = ctx.metadataMgr.getPrivateData().feedbackAllowed;
var feedback = privateData.feedbackAllowed;
Feedback.init(feedback);
} catch (e) { Feedback.init(false); }
try {
var forbidden = ctx.metadataMgr.getPrivateData().disabledApp;
var forbidden = privateData.disabledApp;
if (forbidden) {
UI.alert(Messages.disabledApp, function () {
funcs.gotoURL('/drive/');
}, {forefront: true});
return;
}
var mustLogin = ctx.metadataMgr.getPrivateData().registeredOnly;
var mustLogin = privateData.registeredOnly;
if (mustLogin) {
UI.alert(Messages.mustLogin, function () {
funcs.setLoginRedirect(function () {
@ -640,7 +650,7 @@ define([
}
try {
window.CP_DEV_MODE = ctx.metadataMgr.getPrivateData().devMode;
window.CP_DEV_MODE = privateData.devMode;
} catch (e) {}
ctx.sframeChan.on('EV_LOGOUT', function () {
@ -650,7 +660,7 @@ define([
}
});
UI.addLoadingScreen({hideTips: true});
var origin = ctx.metadataMgr.getPrivateData().origin;
var origin = privateData.origin;
var href = origin + "/login/";
var onLogoutMsg = Messages._getKey('onLogout', ['<a href="' + href + '" target="_blank">', '</a>']);
UI.errorLoadingScreen(onLogoutMsg, true);

@ -615,8 +615,6 @@ MessengerUI, Messages) {
return $requestBlock;
};
createRequest = createRequest;
var createTitle = function (toolbar, config) {
var $titleContainer = $('<span>', {
'class': TITLE_CLS
@ -896,7 +894,7 @@ MessengerUI, Messages) {
if (e) { return void console.error("Unable to get the pinned usage", e); }
if (overLimit) {
var key = 'pinLimitReachedAlert';
if (ApiConfig.noSubscriptionButton === true) {
if (!ApiConfig.allowSubscriptions) {
key = 'pinLimitReachedAlertNoAccounts';
}
$limit.show().click(function () {
@ -1221,6 +1219,7 @@ MessengerUI, Messages) {
tb['fileshare'] = createFileShare;
tb['title'] = createTitle;
tb['pageTitle'] = createPageTitle;
tb['request'] = createRequest;
tb['lag'] = $.noop;
tb['spinner'] = createSpinner;
tb['state'] = $.noop;

@ -1111,5 +1111,32 @@
"notifications_cat_friends": "Freundschaftsanfragen",
"notifications_cat_pads": "Mit mir geteilt",
"notifications_cat_archived": "Verlauf",
"notifications_dismissAll": "Alle verbergen"
"notifications_dismissAll": "Alle verbergen",
"support_notification": "Ein Administrator hat dein Support-Ticket beantwortet",
"requestEdit_button": "Bearbeitungsrechte anfragen",
"requestEdit_dialog": "Bist du sicher, dass du den Eigentümer um Bearbeitungsrechte für das Pad bitten möchtest?",
"requestEdit_confirm": "{1} hat Bearbeitungsrechte für das Pad <b>{0}</b> angefragt. Möchtest du die Rechte vergeben?",
"requestEdit_fromFriend": "Du bist mit {0} befreundet",
"requestEdit_fromStranger": "Du bist <b>nicht</b> mit {0} befreundet",
"requestEdit_viewPad": "Pad in neuem Tab öffnen",
"later": "Später entscheiden",
"requestEdit_request": "{1} möchte das Pad <b>{0}</b> bearbeiten",
"requestEdit_accepted": "{1} hat dir Bearbeitungsrechte für das Pad <b>{0}</b> gegeben",
"requestEdit_sent": "Anfrage gesendet",
"uploadFolderButton": "Ordner hochladen",
"properties_unknownUser": "{0} unbekannte(r) Benutzer",
"fm_morePads": "Mehr",
"fc_openInCode": "Im Code-Editor öffnen",
"uploadFolder_modal_title": "Optionen für Ordnerupload",
"uploadFolder_modal_filesPassword": "Passwort für Dateien",
"uploadFolder_modal_owner": "Eigene Dateien",
"uploadFolder_modal_forceSave": "Dateien im CryptDrive speichern",
"convertFolderToSF_SFParent": "Dieser Ordner kann an seinem aktuellen Ort nicht einen geteilten Ordner umgewandelt werden. Verschiebe ihn zunächst aus dem übergeordneten geteilten Ordner heraus.",
"convertFolderToSF_SFChildren": "Dieser Ordner kann nicht in einen geteilten Ordner umgewandelt werden, weil er bereits geteilte Ordner enthält. Verschiebe diese geteilten Ordner zunächst an einen anderen Ort.",
"convertFolderToSF_confirm": "Dieser Ordner muss in einen geteilten Ordner umgewandelt werden, damit ihn andere sehen können. Fortfahren?",
"pricing": "Preise und Konditionen",
"homePage": "Hauptseite",
"features_noData": "Keine persönlichen Informationen benötigt",
"features_pricing": "Zwischen {0} und {2} € pro Monat",
"features_emailRequired": "E-Mail-Adresse benötigt"
}

@ -595,5 +595,21 @@
"settings_deleteButton": "Borrar su cuenta",
"settings_deleteModal": "Compartir la siguiente información con el administrado de su CryptDrive a fin de que sus datos sean removidos de su servidor.",
"settings_deleteConfirm": "Presionar OK borrará su cuanta de manera permanente. Está seguro?",
"settings_deleted": "Tu cuenta de usuario ha sido borrada. Presione OK para ir a la página principal."
"settings_deleted": "Tu cuenta de usuario ha sido borrada. Presione OK para ir a la página principal.",
"uploadFolderButton": "Subir carpeta",
"fm_morePads": "Más",
"fc_color": "Cambiar color",
"fc_openInCode": "Abrir en el editor de código",
"fc_expandAll": "Expandir todo",
"fc_collapseAll": "Colapsar todo",
"settings_driveDuplicateLabel": "Ocultar duplicados",
"settings_codeFontSize": "Tamaño de la fuente en el editor de código",
"settings_padWidth": "Ancho máximo del editor",
"settings_padWidthLabel": "Reducir el ancho del editor",
"settings_padSpellcheckTitle": "Corrector ortográfico",
"settings_creationSkipTrue": "Omitir",
"settings_creationSkipFalse": "Monitor",
"settings_ownDriveTitle": "Habilitar las últimas características de la cuenta",
"settings_ownDriveHint": "Por razones técnicas, las cuentas más antiguas no tienen acceso a todas nuestras funciones más recientes. Una actualización gratuita a una nueva cuenta preparará su CryptDrive para las próximas funciones sin interrumpir sus actividades habituales.",
"settings_ownDriveButton": "Actualiza tu cuenta"
}

@ -109,6 +109,7 @@
"newButton": "Nouveau",
"newButtonTitle": "Créer un nouveau pad",
"uploadButton": "Importer des fichiers",
"uploadFolderButton": "Importer un dossier",
"uploadButtonTitle": "Importer un nouveau fichier dans le dossier actuel",
"saveTemplateButton": "Sauver en tant que modèle",
"saveTemplatePrompt": "Choisir un titre pour ce modèle",
@ -1111,5 +1112,31 @@
"admin_supportInitHint": "Vous pouvez configurer une messagerie de support afin de fournir aux utilisateurs de votre instance CryptPad un moyen de vous contacter de manière sécurisée en cas de problème avec leur compte.",
"admin_supportListHint": "Voici la liste des tickets envoyés par les utilisateurs au support. Tous les administrateurs peuvent voir les tickets et leurs réponses. Un ticket fermé ne peut pas être ré-ouvert. Vous ne pouvez supprimer (ou cacher) que les tickets fermés, et les tickets supprimés restent visible par les autres administrateurs.",
"support_formHint": "Ce formulaire peut être utilisé pour créer un nouveau ticket de support. Utilisez-le pour contacter les administrateurs de manière sécurisée afin de résoudre un problème ou d'obtenir des renseignements. Merci de ne pas créer de nouveau ticket si vous avez déjà un ticket ouvert concernant le même problème, vous pouvez utiliser le bouton \"Répondre\" dans ce cas.",
"support_listHint": "Voici la liste des tickets envoyés au support, ainsi que les réponses. Un ticket fermé ne peut pas être ré-ouvert, mais il est possible d'en créer un nouveau. Vous pouvez cacher les tickets qui ont été fermés."
"support_listHint": "Voici la liste des tickets envoyés au support, ainsi que les réponses. Un ticket fermé ne peut pas être ré-ouvert, mais il est possible d'en créer un nouveau. Vous pouvez cacher les tickets qui ont été fermés.",
"support_notification": "Un administrateur a répondu à votre ticket de support",
"requestEdit_button": "Demander les droits d'édition",
"requestEdit_dialog": "Êtes-vous sûr de vouloir demander les droits d'édition de ce pad au propriétaire ?",
"requestEdit_confirm": "{1} a demandé les droits d'édition pour le pad <b>{0}</b>. Souhaitez-vous leur accorder les droits ?",
"requestEdit_fromFriend": "Vous êtes amis avec {0}",
"requestEdit_fromStranger": "Vous n'êtes <b>pas</b> amis avec {0}",
"requestEdit_viewPad": "Ouvrir le pad dans un nouvel onglet",
"later": "Décider plus tard",
"requestEdit_request": "{1} souhaite éditer le pad <b>{0}</b>",
"requestEdit_accepted": "{1} vous a accordé les droits d'édition du pad <b>{0}</b>",
"requestEdit_sent": "Demande envoyée",
"properties_unknownUser": "{0} utilisateur(s) inconnu(s)",
"fm_morePads": "Plus",
"fc_openInCode": "Ouvrir dans l'application Code",
"uploadFolder_modal_title": "Options d'importation du dossier",
"uploadFolder_modal_filesPassword": "Mot de passe des fichiers",
"uploadFolder_modal_owner": "Être propriétaire des fichiers",
"uploadFolder_modal_forceSave": "Stocker les fichiers dans votre CryptDrive",
"convertFolderToSF_SFParent": "Impossible de convertir ce dossier en dossier partagé car il se situe à l'intérieur d'un autre dossier partagé. Veuillez le déplacer à l'extérieur afin de continuer.",
"convertFolderToSF_SFChildren": "Impossible de convertir ce dossier en dossier partagé car il contient déjà d'autres dossiers partagés. Veuillez déplacer ces dossiers à l'extérieur afin de continuer.",
"convertFolderToSF_confirm": "Ce dossier va être converti en dossier partagé afin de pouvoir être accessible par d'autres utilisateurs. Continuer ?",
"pricing": "Tarification",
"homePage": "Page d'accueil",
"features_noData": "Aucune donnée personnelle requise",
"features_pricing": "Entre {0} et {2}€ par mois",
"features_emailRequired": "Adresse email requise"
}

@ -110,6 +110,7 @@
"newButton": "New",
"newButtonTitle": "Create a new pad",
"uploadButton": "Upload files",
"uploadFolderButton": "Upload folder",
"uploadButtonTitle": "Upload a new file to the current folder",
"saveTemplateButton": "Save as template",
"saveTemplatePrompt": "Choose a title for the template",
@ -321,6 +322,7 @@
"fm_newButtonTitle": "Create a new pad or folder, import a file in the current folder",
"fm_newFolder": "New folder",
"fm_newFile": "New pad",
"fm_morePads": "More",
"fm_folder": "Folder",
"fm_sharedFolder": "Shared folder",
"fm_folderName": "Folder name",
@ -383,6 +385,7 @@
"fc_color": "Change color",
"fc_open": "Open",
"fc_open_ro": "Open (read-only)",
"fc_openInCode": "Open in Code editor",
"fc_expandAll": "Expand All",
"fc_collapseAll": "Collapse All",
"fc_delete": "Move to trash",
@ -554,6 +557,10 @@
"upload_modal_title": "File upload options",
"upload_modal_filename": "File name (extension <em>{0}</em> added automatically)",
"upload_modal_owner": "Owned file",
"uploadFolder_modal_title": "Folder upload options",
"uploadFolder_modal_filesPassword": "Files password",
"uploadFolder_modal_owner": "Owned files",
"uploadFolder_modal_forceSave": "Store files in your CryptDrive",
"upload_serverError": "Server Error: unable to upload your file at this time.",
"upload_uploadPending": "You already have an upload in progress. Cancel it and upload your new file?",
"upload_success": "Your file ({0}) has been successfully uploaded and added to your drive.",
@ -989,6 +996,9 @@
"sharedFolders_create_owned": "Owned folder",
"sharedFolders_create_password": "Folder password",
"sharedFolders_share": "Share this URL with other registered users to give them access to the shared folder. Once they open this URL, the shared folder will be added to the root directory of their CryptDrive.",
"convertFolderToSF_SFParent": "This folder cannot be converted to a shared folder in its current location. Move it outside of the containing shared folder to continue.",
"convertFolderToSF_SFChildren": "This folder cannot be converted to a shared folder because it already contains shared folders. Move those Shared folders elsewhere to continue.",
"convertFolderToSF_confirm": "This folder must be converted to a Shared folder for others to view it. Continue?",
"chrome68": "It seems that you're using the browser Chrome or Chromium version 68. It contains a bug resulting in the page turning completely white after a few seconds or the page being unresponsive to clicks. To fix this issue, you can switch to another tab and come back, or try to scroll in the page. This bug should be fixed in the next version of your browser.",
"autostore_file": "file",
"autostore_sf": "folder",
@ -1111,5 +1121,22 @@
"notifications_cat_friends": "Friend requests",
"notifications_cat_pads": "Shared with me",
"notifications_cat_archived": "History",
"notifications_dismissAll": "Dismiss all"
"notifications_dismissAll": "Dismiss all",
"support_notification": "An administrator has responded to your support ticket",
"requestEdit_button": "Request edit rights",
"requestEdit_dialog": "Are you sure you'd like to ask the owner of this pad for the ability to edit?",
"requestEdit_confirm": "{1} has asked for the ability to edit the pad <b>{0}</b>. Would you like to grant them access?",
"requestEdit_fromFriend": "You are friends with {0}",
"requestEdit_fromStranger": "You are <b>not</b> friends with {0}",
"requestEdit_viewPad": "Open the pad in a new tab",
"later": "Decide later",
"requestEdit_request": "{1} wants to edit the pad <b>{0}</b>",
"requestEdit_accepted": "{1} granted you edit rights for the pad <b>{0}</b>",
"requestEdit_sent": "Request sent",
"properties_unknownUser": "{0} unknown user(s)",
"pricing": "Pricing",
"homePage": "Home page",
"features_noData": "No personal information required",
"features_pricing": "Between {0} and {2}€ per month",
"features_emailRequired": "Email address required"
}

@ -8,7 +8,11 @@
"drive": "Drive",
"whiteboard": "Whiteboard",
"file": "File",
"media": "Media"
"media": "Media",
"kanban": "Kanban",
"todo": "A Fazer",
"contacts": "Contactos",
"sheet": "SpreadSheet (Beta)"
},
"button_newpad": "Novo bloco RTF",
"button_newcode": "Novo bloco de código",

@ -102,7 +102,7 @@
"forgetPrompt": "Нажав ОК, вы удалите документ в корзину. Уверены?",
"movedToTrash": "Документ был удалён в корзину.<br><a href=\"/drive/\">Доступ к диску</a>",
"shareButton": "Поделиться",
"shareSuccess": "Ссылка скопирована в буфер обмена.",
"shareSuccess": "Ссылка скопирована в буфер обмена",
"userListButton": "Список пользователей",
"chatButton": "Чат",
"userAccountButton": "Ваш профиль",
@ -139,36 +139,36 @@
"or": "или",
"tags_title": "Теги (только для вас)",
"tags_add": "Обновить теги страницы",
"tags_searchHint": "Начните поиск в вашем CryptDrive при помощи # чтобы найти пэды с тегами",
"tags_searchHint": "Начните поиск в вашем CryptDrive при помощи # чтобы найти пэды с тегами.",
"tags_notShared": "Ваши теги не разделяются с другими пользователями",
"button_newsheet": "Новый Лист",
"newButtonTitle": "Создать новый блокнот",
"newButtonTitle": "Создать новый документ",
"useTemplateCancel": "Начать заново (Esc)",
"previewButtonTitle": "Отобразить или скрыть режим предпросмотра разметки",
"printOptions": "Опции расположения",
"previewButtonTitle": "Показать или скрыть просмотр Маркдаун разметки",
"printOptions": "Настройки размещения",
"printBackgroundValue": "<b>Текущий фон:</b> <em>{0}</em>",
"printBackgroundNoValue": "<em>Нет отображаемого фонового изображения</em>",
"tags_duplicate": "Скопировать тег: {0}",
"tags_noentry": "Вы не можете присвоить тег удалённому блокноту!",
"slideOptionsText": "Опции",
"slideOptionsTitle": "Настроить ваши слайды",
"slideOptionsButton": "Сохранить (Enter)",
"slide_invalidLess": "Неверный настраиваемый стиль",
"printBackgroundNoValue": "<em>Фоновое изображение не показано</em>",
"tags_duplicate": "Скопировать метку: {0}",
"tags_noentry": "Вы не можете присвоить метку удалённому документу!",
"slideOptionsText": "Настройки",
"slideOptionsTitle": "Настройте ваши слайды",
"slideOptionsButton": "Сохранить (Ввод)",
"slide_invalidLess": "Неверный пользовательский стиль",
"languageButton": "Язык",
"languageButtonTitle": "Выберите язык, используемый для подсветки синтаксиса",
"languageButtonTitle": "Выберите язык для использования подсветки слов",
"themeButton": "Тема",
"themeButtonTitle": "Выберите цветовую тему, используемую в редакторе кода и слайдов",
"editShare": "Редактирование ссылки",
"editShareTitle": "Скопировать редактируемую ссылку",
"themeButtonTitle": "Выберите цветовую тему для использования в редакторе кода и слайдов",
"editShare": "Редактируемая ссылка",
"editShareTitle": "Скопировать редактируемую ссылку в буфер обмена",
"editOpen": "Открыть редактируемую ссылку в новой вкладке",
"editOpenTitle": "Открыть блокнот в режиме редактирования в новой вкладке",
"editOpenTitle": "Открыть данный документ для редактирования в новой вкладке",
"viewShare": "Ссылка только для чтения",
"viewShareTitle": "Скопировать ссылку для чтения в буфер обмена",
"viewOpen": "Открыть ссылку в режиме чтения в новой вкладке",
"viewOpenTitle": "Открыть блокнот в режиме чтения в новой вкладке",
"viewShareTitle": "Скопировать ссылку только для чтения в буфер обмена",
"viewOpen": "Открыть ссылку только для чтения в новой вкладке",
"viewOpenTitle": "Открыть данный документ для чтения в новой вкладке",
"fileShare": "Скопировать ссылку",
"getEmbedCode": "Получить код для встраивания",
"viewEmbedTitle": "Встроить блокнот на внешнюю страницу",
"viewEmbedTitle": "Встроить документ во внешнюю страницу",
"notifyJoined": "{0} присоединился к совместной сессии",
"notifyRenamed": "{0} теперь известен как {1}",
"notifyLeft": "{0} покинул совместную сессию",
@ -258,7 +258,7 @@
"profile_fieldSaved": "Сохранено новое значение: {0}",
"profile_viewMyProfile": "Посмотреть мой профиль",
"contacts_title": "Контакты",
"contacts_added": "Приглашение принято контактом",
"contacts_added": "Приглашение принято контактом.",
"contacts_rejected": "Контакт не принял приглашение",
"contacts_send": "Отправить",
"contacts_remove": "Убрать этот контакт",
@ -302,22 +302,22 @@
"crowdfunding_popup_no": "Не сейчас",
"crowdfunding_popup_never": "Не спрашивать меня снова",
"markdown_toc": "Содержимое",
"fm_expirablePad": "Этот блокнот удалится через {0}",
"fileEmbedTitle": "Вставить файл во внешнюю страницу",
"fm_expirablePad": "Этот блокнот истечет {0}",
"fileEmbedTitle": "Встроить файл во внешнюю страницу",
"kanban_removeItemConfirm": "Вы уверенны, что хотите удалить этот пункт?",
"settings_backup2": "Скачать мой CryptDrive",
"settings_backup2Confirm": "Это позволит скачать все пэды и файлы с вашего CryptDrive. Если вы хотите продолжить, выберите имя и нажмите OK",
"settings_exportTitle": "Экспортировать Ваш CryptDrive",
"fileEmbedScript": "Чтобы вставить этот файл, включите этот скрипт один раз на своей странице, чтобы загрузить медиатег:",
"fileEmbedTag": "Затем поместите медиатег в любое место на странице, куда вы хотите его вставить:",
"fileEmbedTag": "Затем поместите медиатег в любое место на странице,в которое вы хотите его вставить:",
"pad_mediatagRatio": "Оставить соотношение",
"kanban_item": "Элемент {0}",
"poll_p_encryption": "Все ваши данные зашифрованы, доступ к ним имеют только пользователи, имеющие доступ к этой ссылке. Даже сервер не видит, что вы меняете.",
"wizardLog": "Нажмите кнопку в левом верхнем углу, чтобы вернуться к опросу",
"poll_bookmark_col": "Добавить этот столбец в закладку, чтобы он всегда был разблокирован и отображался для вас в начале",
"poll_bookmarked_col": "Это твоя колонка закладок. Она всегда будет разблокирована и отображаться для вас в начале.",
"poll_wizardDescription": "Автоматическое создавайте несколько опций путем ввода произвольного количества дат и временных сегментов",
"poll_comment_disabled": "Опубликуйте этот опрос с помощью кнопки ✓ для включения комментариев",
"poll_wizardDescription": "Автоматически создавайте несколько опций путем ввода произвольного количества дат и временных сегментов",
"poll_comment_disabled": "Опубликуйте этот опрос с помощью кнопки ✓ для включения комментариев.",
"oo_cantUpload": "Загрузка запрещена, если присутствуют другие пользователи.",
"oo_uploaded": "Ваша загрузка завершена. Нажмите OK, чтобы перезагрузить страницу или отменить, чтобы остаться в режиме чтения.",
"canvas_imageEmbed": "Вставьте изображение с вашего компьютера",
@ -367,13 +367,13 @@
"fm_padIsOwnedOther": "Этот пэд принадлежит другому пользователю",
"fm_deletedPads": "Эти пэды больше не существуют на сервере, они были удалены с вашего CryptDrive: {0}",
"fm_tags_name": "Имя тэга",
"printCSS": "Пользовательские настройки вида (CSS)",
"viewEmbedTag": "Чтобы встроить данный документ вставьте iframe в нужную страницу. Вы можете настроить внешний вид используя CSS и HTML атрибуты. ",
"printCSS": "Пользовательские настройки вида (CSS):",
"viewEmbedTag": "Чтобы встроить данный документ, вставьте iframe в нужную страницу. Вы можете настроить внешний вид используя CSS и HTML атрибуты.",
"debug_getGraphText": "Это код DOT для генерации графика истории этого документа:",
"fm_ownedPadsName": "Собственный",
"fm_info_anonymous": "Вы не вошли в учетную запись, поэтому срок действия ваших пэдов истечет через 3 месяца (<a href=\"https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/\" target=\"_blank\">find out more</a>). Они хранятся в вашем браузере, поэтому очистка истории может привести к их исчезновению..<br><a href=\"/register/\">Sign up</a> or <a href=\"/login/\">Log in</a> to keep them alive.<br>",
"fm_backup_title": "Резервная ссылка",
"fm_burnThisDriveButton": "Удалить всю информацию, хранящуюся на CryptPad в браузере.",
"fm_burnThisDriveButton": "Удалить всю информацию, хранящуюся от CryptPad в браузере",
"fm_tags_used": "Количество использований",
"fm_restoreDrive": "Восстановление прежнего состояния диска. Для достижения наилучших результатов не вносите изменения в диск, пока этот процесс не будет завершен.",
"fm_passwordProtected": "Этот документ защищен паролем",
@ -389,5 +389,118 @@
"fc_empty": "Удалить корзину",
"fc_prop": "Свойства",
"fc_hashtag": "Теги",
"fc_sizeInKilobytes": "Размер в килобайтах"
"fc_sizeInKilobytes": "Размер в килобайтах",
"poll_title": "Приватный выбор даты",
"fm_moveNestedSF": "Нельзя помещать одну общую папку в другую. Папка {0} не была перемещена.",
"fc_color": "Изменить цвет",
"fc_expandAll": "Расширить все",
"fc_collapseAll": "Скрыть все",
"fc_remove": "Удалить из вашего CryptDrive",
"fo_moveUnsortedError": "Вы не можете переместить папку в список черновиков",
"fo_existingNameError": "Это имя уже используется в данной директории. Пожалуйста выберите другое.",
"fo_unableToRestore": "Невозможно восстановить этот файл в исходное местоположение. Вы можете попытаться переместить его в другое место.",
"login_login": "Войти",
"login_makeAPad": "Создать анонимный пэд",
"login_nologin": "Просмотреть локальные пэды",
"login_register": "Зарегистрироваться",
"logoutButton": "Выйти",
"settingsButton": "Настройки",
"login_username": "Имя пользователя",
"login_password": "Пароль",
"login_confirm": "Подтвердите ваш пароль",
"login_remember": "Запомнить меня",
"login_hashing": "Ваш пароль хэшируется, это может занять некое время.",
"login_hello": "Привет {0},",
"login_helloNoName": "Привет,",
"fm_info_sharedFolder": "Это общая папка. Вы не вошли в систему, поэтому можете получить к ней доступ только в режиме только для чтения.<br><a href=\"/register/\">Sign up</a> или <a href=\"/login/\">Log in</a> для импорта на CryptDrive и его изменения.",
"fo_moveFolderToChildError": "Вы не можете переместить папку в одну из нее следующую",
"fo_unavailableName": "Файл или папка с таким же именем уже существуют в новом месте. Переименуйте элемент и повторите попытку.",
"fs_migration": "Ваш CryptDrive обновляется до новой версии. В результате, текущая страница должна быть перезагружена.<br><strong> перезагрузите эту страницу, чтобы продолжить ей пользоваться.</strong>",
"login_accessDrive": "Доступ к хранилищу",
"login_orNoLogin": "или",
"login_noSuchUser": "Неверный логин или пароль. Попробуйте еще раз или зарегистрируйтесь",
"login_invalUser": "Неоьходимо имя пользователя",
"login_invalPass": "Необходим пароль",
"login_unhandledError": "Произошла неожиданная ошибка :(",
"register_importRecent": "Импортировать пэды из вашей анонимной сессии",
"register_passwordsDontMatch": "Пароли не совпадают!",
"register_passwordTooShort": "Длина пароля должна составлять не менее {0} символов.",
"register_mustAcceptTerms": "Вы должны принять условия пользования.",
"register_mustRememberPass": "Мы не сможем сбросить ваш пароль, если вы его забудете. Очень важно, чтобы вы его запомнили! Пожалуйста, отметьте флажок для подтверждения.",
"register_whyRegister": "Почему стоит зарегистрироваться?",
"register_header": "Добро пожаловать в CryptPad",
"register_writtenPassword": "Я записал свое имя пользователя и пароль, продолжить",
"register_cancel": "Назад",
"register_alreadyRegistered": "Этот пользователь уже существует, вы хотите войти?",
"settings_cat_account": "Учетная запись",
"settings_cat_drive": "КриптДрайв",
"settings_cat_cursor": "курсор",
"settings_cat_code": "Код",
"settings_cat_pad": "Текст с форматированием",
"settings_cat_creation": "новый пэд",
"settings_cat_subscription": "Подписка",
"settings_title": "Настройки",
"settings_save": "Сохранить",
"settings_backupHint": "Резервное копирование или восстановление всего содержимого CryptDrive. Он не будет содержать содержимое ваших пэдов, только ключи для доступа к ним.",
"settings_restore": "Восстановить",
"settings_exportDescription": "Пожалуйста, подождите, пока мы загружаем и расшифровываем ваши документы. Это может занять несколько минут. Закрытие вкладки прервет процесс.",
"settings_exportFailed": "Если загрузка пэда занимает более 1 минуты, он не будет включен в экспорт. Отображается ссылка на любой блокнот, который не был экспортирован.",
"settings_exportWarning": "Примечание: этот инструмент все еще находится в бета-версии и может иметь проблемы со масштабируемостью. Для повышения производительности рекомендуется оставить данную вкладку сфокусированной.",
"settings_exportCancel": "Вы уверены, что хотите отменить экспорт? В следующий раз вам придется начинать все сначала.",
"settings_export_reading": "Читаем ваше хранилище...",
"settings_export_download": "Скачиваем и расшифровываем ваши документы...",
"contacts_request": "<em>{0}</em> хотел бы добавить вас в список контактов. <b>Принять <b>?",
"contacts_confirmRemove": "Вы уверены, что хотите удалить <em>1{0}</em>2 из ваших контактов?",
"register_acceptTerms": "Я принимаю <a href='/terms.html' tabindex='-1'>1 условия пользования</a>",
"register_warning": "Мы не сможем восстановить ваши данные, если вы потеряете пароль, так как мы не имеем доступа к ним.",
"settings_backupCategory": "Резервное копирование",
"settings_backup": "Резервная копия",
"settings_backupHint2": "Загрузите текущее содержимое всех ваших пэдов. Пэды будут загружены в читаемом формате, если такой формат доступен.",
"settings_export_compressing": "Данные сжимаются..",
"settings_export_done": "Ваше скачивание завершено!",
"settings_exportError": "Посмотреть ошибки",
"settings_exportErrorDescription": "Мы не смогли добавить в экспорт следующие документы:",
"settings_exportErrorEmpty": "Этот документ не может быть экспортирован (пустое или недостоверное содержимое).",
"settings_exportErrorMissing": "Этот документ отсутствует на наших серверах (истек или удален владельцем)",
"settings_exportErrorOther": "При попытке экспорта данного документа возникла ошибка: {0}",
"settings_resetNewTitle": "Очистить хриналище",
"settings_resetButton": "Удалить",
"settings_reset": "Удалите все файлы и папки с CryptDrive",
"settings_resetDone": "Ваше хранилище теперь пустое!",
"settings_resetError": "Неправильный текст верификации. Ваш CryptDrive не был изменен.",
"settings_resetTipsAction": "Сброс",
"settings_resetTips": "Подсказки",
"settings_resetTipsButton": "Сброс доступных подсказок в CryptDrive",
"settings_resetTipsDone": "Теперь все подсказки снова видны.",
"settings_thumbnails": "Иконки",
"settings_disableThumbnailsAction": "Отключить создание иконок в вашем хранилище",
"settings_disableThumbnailsDescription": "Иконки автоматически создаются и сохраняются в браузере при посещении нового документа. Вы можете отключить эту функцию здесь.",
"settings_resetThumbnailsAction": "Очистить",
"settings_resetThumbnailsDescription": "Очистить все иконки документов, хранящиеся в вашем браузере.",
"settings_resetThumbnailsDone": "Все иконки были удалены.",
"settings_import": "Импортировать",
"settings_importDone": "Импортирование завершено",
"settings_autostoreTitle": "Хранилище документов в CryptDrive",
"settings_autostoreYes": "Автоматически",
"settings_autostoreNo": "Вручную (никогда не спрашивать)",
"settings_autostoreMaybe": "Вручную (всегда спрашивать)",
"settings_userFeedbackTitle": "Обратная связь",
"settings_userFeedbackHint2": "Содержимое вашего документа никогда не будет передаваться на сервер.",
"fm_alert_anonymous": "Здравствуйте, в настоящее время вы используете CryptPad анонимно, это нормально, но ваши пэды могут быть удалены после периода бездействия. Мы отключили расширенные возможности хранилища для анонимных пользователей, потому что хотим быть уверенными, что это небезопасное место для хранения вещей. Вы можете <a href=\"https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/\" target=\"_blank\">1читать далее</a>2 о том, почему мы это делаем и почему вам стоит <a href=\"/register/\">зарегистрироваться</a>4 and <a href=\"/login/\">5Log in</a>6.",
"settings_resetPrompt": "Это действие удалит все документы с диска.<br>Вы уверены, что хотите продолжить?<br>Напишите \"<em>Я люблю CryptPad</em>\" для подтверждения.",
"settings_importTitle": "Импортируйте последние документы данного браузера в ваше хранилище",
"settings_importConfirm": "Вы уверены, что хотите импортировать последние документы из этого браузера в хранилище вашего пользователя?",
"settings_userFeedbackHint1": "CryptPad держит очень простую обратную связь с сервером, чтобы мы знали, как улучшить ваше пользование.\n",
"settings_userFeedback": "Включить телеметрию",
"settings_deleteTitle": "Удаление аккаунта",
"settings_deleteHint": "Удаление аккаунта является постоянным. Ваш CryptDrive и список пэдов будут удалены с сервера. Остальные ваши пэды будут удалены через 90 дней, если никто другой не сохранил их в CryptDrive.",
"settings_deleteButton": "Удалить ваш аккаунт",
"settings_deleteModal": "Обменивайтесь следующей информацией с администратором CryptPad, чтобы удалить ваши данные с сервера.",
"settings_deleteConfirm": "Нажмите OK, чтобы удалить ваш аккаунт навсегда. Вы уверены?",
"settings_deleted": "Ваша учетная запись пользователя удалена. Нажмите OK, чтобы перейти на главную страницу.",
"settings_publicSigningKey": "Публичный ключ подписи",
"settings_usage": "Использование",
"settings_usageTitle": "Смотрите общий размер ваших прикрепленных документов в мегабайтах",
"settings_pinningNotAvailable": "Прикрепленные документы доступны только зарегистрированным пользователям.",
"settings_pinningError": "Что-то пошло не так"
}

@ -123,7 +123,11 @@ define([
};
exp.isFolderEmpty = function (element) {
if (!isFolder(element)) { return false; }
return Object.keys(element).length === 0;
// if the folder contains nothing, it's empty
if (Object.keys(element).length === 0) { return true; }
// or if it contains one thing and that thing is metadata
if (Object.keys(element).length === 1 && isFolderData(element[Object.keys(element)[0]])) { return true; }
return false;
};
exp.hasSubfolder = function (element, trashRoot) {
@ -170,6 +174,20 @@ define([
}
};
var hasSubSharedFolder = exp.hasSubSharedFolder = function (folder) {
for (var el in folder) {
if (isSharedFolder(folder[el])) {
return true;
}
else if (isFolder(folder[el])) {
if (hasSubSharedFolder(folder[el])) {
return true;
}
}
}
return false;
};
// Get data from AllFiles (Cryptpad_RECENTPADS)
var getFileData = exp.getFileData = function (file) {
if (!file) { return; }

@ -30,6 +30,7 @@
@drive_content-bg-ro: darken(@drive_content-bg, 10%);
@drive_selected-bg: #888;
@drive_droppable-bg: #FE9A2E;
/* PAGE */
@ -107,7 +108,7 @@
.cp-app-drive-container {
flex: 1;
overflow: auto;
overflow-x: auto;
width: 100%;
display: flex;
flex-flow: row;
@ -121,6 +122,7 @@
#cp-app-drive-tree {
resize: none;
width: 100% !important;
min-width: unset;
max-width: unset;
max-height: unset;
border-bottom: 1px solid @drive_mobile-tree-border-col;
@ -156,7 +158,7 @@
}
.cp-app-drive-element-droppable {
background-color: #FE9A2E;
background-color: @drive_droppable-bg;
color: #222;
}
@ -239,7 +241,6 @@
max-height: 100%;
.cp-app-drive-tree-categories-container {
flex: 1;
max-width: 500px;
overflow: auto;
}
img.cp-app-drive-icon {
@ -438,13 +439,13 @@
flex: 1;
// Needed to avoid the folder's path to overflows
// https://stackoverflow.com/questions/38223879/white-space-nowrap-breaks-flexbox-layout
min-width: 0;
// min-width: 0;
}
#cp-app-drive-content {
box-sizing: border-box;
background: @drive_content-bg;
color: @drive_content-fg;
overflow: auto;
overflow-y: auto;
flex: 1;
display: flex;
flex-flow: column;
@ -939,6 +940,7 @@
overflow: hidden;
text-overflow: ellipsis;
transition: all 0.15s;
cursor: pointer;
&:first-child {
flex-shrink: 1;
@ -946,17 +948,20 @@
&.cp-app-drive-path-separator {
color: #ccc;
cursor: default;
}
&.cp-app-drive-path-collapse {
position: relative;
}
&:hover {
&.cp-app-drive-element-droppable {
background-color: @drive_droppable-bg;
}
&:not(.cp-app-drive-element-droppable):hover {
&:not(.cp-app-drive-path-separator) {
background-color: darken(@colortheme_drive-bg, 15%);
text-decoration: underline;
cursor: pointer;
}
& ~ .cp-app-drive-path-element {
background-color: darken(@colortheme_drive-bg, 15%);

File diff suppressed because it is too large Load Diff

@ -52,6 +52,16 @@
max-width: 100%;
max-height: ~"calc(100vh - 96px)";
}
.plain-text-reader {
align-self: flex-start;
width: 90vw;
height: 100%;
padding: 2em;
background-color: white;
overflow-y: auto;
word-wrap: break-word;
white-space: pre-wrap;
}
}
#cp-app-file-upload-form, #cp-app-file-download-form {

@ -59,6 +59,7 @@ define([
var secret;
var metadataMgr = common.getMetadataMgr();
var priv = metadataMgr.getPrivateData();
var fileHost = priv.fileHost || priv.origin || '';
if (!priv.filehash) {
uploadMode = true;
@ -88,7 +89,7 @@ define([
if (!uploadMode) {
var hexFileName = secret.channel;
var src = Hash.getBlobPathFromHex(hexFileName);
var src = fileHost + Hash.getBlobPathFromHex(hexFileName);
var key = secret.keys && secret.keys.cryptKey;
var cryptKey = Nacl.util.encodeBase64(key);

@ -29,6 +29,7 @@ define([
var andThen = function (common) {
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var $body = $('body');
var sframeChan = common.getSframeChannel();
var filters = metadataMgr.getPrivateData().types;
@ -41,7 +42,8 @@ define([
hideFileDialog();
if (parsed.type === 'file') {
var secret = Hash.getSecrets('file', parsed.hash, data.password);
var src = Hash.getBlobPathFromHex(secret.channel);
var fileHost = privateData.fileHost || privateData.origin;
var src = fileHost + Hash.getBlobPathFromHex(secret.channel);
var key = Hash.encodeBase64(secret.keys.cryptKey);
sframeChan.event("EV_FILE_PICKED", {
type: parsed.type,

@ -83,6 +83,7 @@ define([
}).nThen(function (/*waitFor*/) {
metaObj.doc = {};
var additionalPriv = {
fileHost: ApiConfig.fileHost,
accountName: Utils.LocalStore.getAccountName(),
origin: window.location.origin,
pathname: window.location.pathname,

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html class="cp">
<!-- If this file is not called customize.dist/src/template.html, it is generated -->
<head>
<title data-localization="main_title">CryptPad: Zero Knowledge, Collaborative Real Time Editing</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<link rel="stylesheet" href="/bower_components/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="/bower_components/codemirror/addon/dialog/dialog.css">
<link rel="stylesheet" href="/bower_components/codemirror/addon/fold/foldgutter.css" />
</head>
<body class="html">
<noscript>
<p><strong>OOPS</strong> In order to do encryption in your browser, Javascript is really <strong>really</strong> required.</p>
<p><strong>OUPS</strong> Afin de pouvoir réaliser le chiffrement dans votre navigateur, Javascript est <strong>vraiment</strong> nécessaire.</p>
</noscript>
</html>

@ -1,88 +0,0 @@
define([
'jquery',
'/common/cryptpad-common.js',
'/common/common-interface.js',
//'/common/common-hash.js',
//'/bower_components/chainpad-listmap/chainpad-listmap.js',
//'/common/curve.js',
'less!/invite/main.less',
], function ($, Cryptpad, UI/*, Hash , Listmap, Curve*/) {
var Messages = Cryptpad.Messages;
var comingSoon = function () {
return $('<div>', {
'class': 'coming-soon',
})
.text(Messages.comingSoon)
.append('<br>');
};
$(function () {
UI.removeLoadingScreen();
console.log("wut");
$('body #mainBlock').append(comingSoon());
});
return;
/* jshint ignore:start */
var APP = window.APP = {};
//var Messages = Cryptpad.Messages;
var onInit = function () {};
var onDisconnect = function () {};
var onChange = function () {};
var andThen = function () {
var hash = window.location.hash.slice(1);
var info = Hash.parseTypeHash('invite', hash);
console.log(info);
if (!info.pubkey) {
UI.removeLoadingScreen();
UI.alert('invalid invite');
return;
}
var proxy = Cryptpad.getProxy();
var mySecret = proxy.curvePrivate;
var keys = Curve.deriveKeys(info.pubkey, mySecret);
var encryptor = Curve.createEncryptor(keys);
UI.removeLoadingScreen();
var listmapConfig = {
data: {},
network: Cryptpad.getNetwork(),
channel: info.channel,
readOnly: false,
validateKey: keys.validateKey,
crypto: encryptor,
userName: 'profile',
logLevel: 1,
};
var lm = APP.lm = Listmap.create(listmapConfig);
lm.proxy.on('create', onInit)
.on('ready', function () {
APP.initialized = true;
console.log(JSON.stringify(lm.proxy));
})
.on('disconnect', onDisconnect)
.on('change', [], onChange);
};
$(function () {
var $main = $('#mainBlock');
// main block is hidden in case javascript is disabled
$main.removeClass('hidden');
APP.$container = $('#container');
Cryptpad.ready(function () {
andThen();
});
});
/* jshint ignore:end */
});

@ -1,150 +0,0 @@
/*
.cp {
#mainBlock {
z-index: 1;
width: 1000px;
max-width: 90%;
margin: auto;
#container {
font-size: 25px;
width: 100%;
}
}
#header {
display: flex;
#rightside {
flex: 1;
display: flex;
flex-flow: column;
}
}
#avatar {
width: 300px;
//height: 350px;
margin: 10px;
margin-right: 20px;
text-align: center;
&> span {
display: inline-block;
text-align: center;
height: 300px;
width: 300px;
border: 1px solid black;
border-radius: 10px;
overflow: hidden;
position: relative;
.delete {
right: 0;
position: absolute;
opacity: 0.7;
&:hover {
opacity: 1;
}
}
}
img {
max-width: 100%;
max-height: 100%;
vertical-align: top;
}
media-tag {
height: 100%;
width: 100%;
display: inline-flex;
justify-content: center;
align-items: center;
img {
min-width: 100%;
min-height: 100%;
max-width: none;
max-height: none;
flex-shrink: 0;
}
}
button {
height: 40px;
margin: 5px;
}
}
#displayName, #link {
width: 100%;
height: 40px;
margin: 10px 0;
input {
width: 100%;
font-size: 20px;
box-sizing: border-box;
padding-right: 30px;
}
input:focus ~ .edit {
display: none;
}
.edit {
position: absolute;
margin-left: -25px;
margin-top: 8px;
}
.temp {
font-weight: 400;
font-family: sans-serif;
}
.displayName {
font-weight: bold;
font-size: 30px;
}
.displayName, .link {
line-height: 40px;
}
}
#description {
position: relative;
font-size: 16px;
border: 1px solid #DDD;
margin-bottom: 20px;
.rendered {
padding: 0 15px;
}
.ok, .spin {
position: absolute;
top: 2px;
right: 2px;
display: none;
z-index: 1000;
}
textarea {
width: 100%;
height: 300px;
}
.CodeMirror {
border: 1px solid #DDD;
font-family: monospace;
font-size: 16px;
line-height: initial;
pre {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
}
}
#createProfile {
height: 100%;
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
}
}
*/
.coming-soon {
text-align: center;
font-size: 25px;
height: 100%;
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
}

@ -2,7 +2,9 @@
// Pads from the code app will be exported using this format instead of plain text.
define([
], function () {
var module = {};
var module = {
ext: '.json'
};
module.main = function (userDoc, cb) {
var content = userDoc.content;

@ -367,7 +367,7 @@ define([
});
}
framework.setFileExporter('json', function () {
framework.setFileExporter('.json', function () {
return new Blob([JSON.stringify(kanban.getBoardsJSON(), 0, 2)], {
type: 'application/json',
});

@ -44,7 +44,7 @@ define([
],
};
var notifsAllowedTypes = ["FRIEND_REQUEST", "FRIEND_REQUEST_ACCEPTED", "FRIEND_REQUEST_DECLINED", "SHARE_PAD"];
var notifsAllowedTypes = ["FRIEND_REQUEST", "FRIEND_REQUEST_ACCEPTED", "FRIEND_REQUEST_DECLINED", "SHARE_PAD", "REQUEST_PAD_ACCESS"];
var create = {};

@ -5,7 +5,7 @@ define([
'/bower_components/nthen/index.js',
], function ($, Util, Hyperjson, nThen) {
var module = {
type: 'html'
ext: '.html'
};
var exportMediaTags = function (inner, cb) {

@ -457,6 +457,8 @@ define([
framework._.sfCommon.addShortcuts(ifrWindow);
var privateData = framework._.sfCommon.getMetadataMgr().getPrivateData();
var documentBody = ifrWindow.document.body;
var observer = new MutationObserver(function (muts) {
@ -702,7 +704,8 @@ define([
onUploaded: function (ev, data) {
var parsed = Hash.parsePadUrl(data.url);
var secret = Hash.getSecrets('file', parsed.hash, data.password);
var src = Hash.getBlobPathFromHex(secret.channel);
var fileHost = privateData.fileHost || privateData.origin;
var src = fileHost + Hash.getBlobPathFromHex(secret.channel);
var key = Hash.encodeBase64(secret.keys.cryptKey);
var mt = '<media-tag contenteditable="false" src="' + src + '" data-crypto-key="cryptpad:' + key + '"></media-tag>';
// MEDIATAG
@ -786,7 +789,7 @@ define([
});
}, true);
framework.setFileExporter(Exporter.type, function (cb) {
framework.setFileExporter(Exporter.ext, function (cb) {
Exporter.main(inner, cb);
}, true);

@ -3,7 +3,9 @@
define([
'/customize/messages.js',
], function (Messages) {
var module = {};
var module = {
ext: '.csv'
};
var copyObject = function (obj) {
return JSON.parse(JSON.stringify(obj));

@ -122,9 +122,13 @@
#cp-app-profile-invite-button {
float: right;
}
#cp-app-profile-viewprofile-button {
.cp-app-profile-viewprofile-button {
margin-bottom: 20px;
float: right;
margin-left: 5px;
&> span {
margin-left: 10px;
}
}
#cp-app-profile-description {
position: relative;

@ -9,6 +9,7 @@ define([
'/common/common-interface.js',
'/common/common-ui-elements.js',
'/common/common-realtime.js',
'/common/clipboard.js',
'/common/hyperscript.js',
'/customize/messages.js',
'/customize/application_config.js',
@ -36,6 +37,7 @@ define([
UI,
UIElements,
Realtime,
Clipboard,
h,
Messages,
AppConfig,
@ -96,15 +98,20 @@ define([
var hash = common.getMetadataMgr().getPrivateData().hashes.viewHash;
var url = APP.origin + '/profile/#' + hash;
var $button = $('<button>', {
'class': 'btn btn-success',
id: VIEW_PROFILE_BUTTON,
})
.text(Messages.profile_viewMyProfile)
.click(function () {
$('<button>', {
'class': 'btn btn-success '+VIEW_PROFILE_BUTTON,
}).text(Messages.profile_viewMyProfile).click(function () {
window.open(url, '_blank');
});
$container.append($button);
}).appendTo($container);
$('<button>', {
'class': 'btn btn-success '+VIEW_PROFILE_BUTTON,
}).append(h('i.fa.fa-shhare-alt'))
.append(h('span', Messages.shareButton))
.click(function () {
var success = Clipboard.copy(url);
if (success) { UI.log(Messages.shareSuccess); }
}).appendTo($container);
};
var addDisplayName = function ($container) {

@ -111,8 +111,14 @@
vertical-align: middle;
margin-right: 5px;
}
input[type="color"] {
width: 100px;
.cp-settings-cursor-color-picker {
display: inline-block;
vertical-align: middle;
height: 25px;
width: 70px;
margin-right: 10px;
cursor: pointer;
border: 1px solid black;
}
.cp-settings-language-selector {
button.btn {

@ -12,9 +12,10 @@ define([
'/customize/credential.js',
'/customize/application_config.js',
'/api/config',
'/settings/make-backup.js',
'/common/make-backup.js',
'/common/common-feedback.js',
'/common/jscolor.js',
'/bower_components/file-saver/FileSaver.min.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
@ -1082,18 +1083,9 @@ define([
var exportDrive = function () {
Feedback.send('FULL_DRIVE_EXPORT_START');
var todo = function (data, filename) {
var getPad = function (data, cb) {
sframeChan.query("Q_CRYPTGET", data, function (err, obj) {
if (err) { return void cb(err); }
if (obj.error) { return void cb(obj.error); }
cb(null, obj.data);
}, { timeout: 60000 });
};
var ui = createExportUI();
var bu = Backup.create(data, getPad, function (blob, errors) {
console.log(blob);
var bu = Backup.create(data, common.getPad, privateData.fileHost, function (blob, errors) {
saveAs(blob, filename);
sframeChan.event('EV_CRYPTGET_DISCONNECT');
ui.complete(function () {
@ -1191,29 +1183,47 @@ define([
var $inputBlock = $('<div>').appendTo($div);
var $colorPicker = $("<div>", { class: "cp-settings-cursor-color-picker"});
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved});
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'});
var $input = $('<input>', {
type: 'color',
}).on('change', function () {
var val = $input.val();
// when jscolor picker value change
var _onchange = function (colorL) {
var val = "#" + colorL.toString();
if (!/^#[0-9a-fA-F]{6}$/.test(val)) { return; }
$spinner.show();
$ok.hide();
common.setAttribute(['general', 'cursor', 'color'], val, function () {
$spinner.hide();
$ok.show();
});
}).appendTo($inputBlock);
};
var to;
var onchange = function (colorL) {
$spinner.show();
$ok.hide();
$ok.hide().appendTo($inputBlock);
$spinner.hide().appendTo($inputBlock);
if (to) { clearTimeout(to); }
to = setTimeout(function () {
_onchange(colorL);
}, 300);
};
// jscolor picker
var jscolorL = new window.jscolor($colorPicker[0],{showOnClick: false, onFineChange: onchange, valueElement:undefined});
$colorPicker.click(function () {
jscolorL.show();
});
// set default color
common.getAttribute(['general', 'cursor', 'color'], function (e, val) {
if (e) { return void console.error(e); }
$input.val(val || '');
val = val || "#000";
jscolorL.fromString(val);
});
$colorPicker.appendTo($inputBlock);
$ok.hide().appendTo($inputBlock);
$spinner.hide().appendTo($inputBlock);
return $div;
};

@ -43,6 +43,7 @@ define([
origin: origin,
pathname: pathname,
password: priv.password,
isTemplate: priv.isTemplate,
hashes: hashes,
common: common,
title: data.title,

@ -85,12 +85,14 @@ define([
}).nThen(function (/*waitFor*/) {
metaObj.doc = {};
var additionalPriv = {
fileHost: ApiConfig.fileHost,
accountName: Utils.LocalStore.getAccountName(),
origin: window.location.origin,
pathname: window.location.pathname,
feedbackAllowed: Utils.Feedback.state,
hashes: config.data.hashes,
password: config.data.password,
isTemplate: config.data.isTemplate,
file: config.data.file,
};
for (var k in additionalPriv) { metaObj.priv[k] = additionalPriv[k]; }

@ -4,7 +4,7 @@ define([
'/common/sframe-common-codemirror.js',
], function (SFCodeMirror) {
var module = {
type: 'md'
ext: '.md'
};
module.main = function (userDoc, cb) {

@ -438,6 +438,7 @@ define([
var andThen2 = function (editor, CodeMirror, framework, isPresentMode) {
var common = framework._.sfCommon;
var privateData = common.getMetadataMgr().getPrivateData();
var $contentContainer = $('#cp-app-slide-editor');
var $modal = $('#cp-app-slide-modal');
@ -515,7 +516,8 @@ define([
onUploaded: function (ev, data) {
var parsed = Hash.parsePadUrl(data.url);
var secret = Hash.getSecrets('file', parsed.hash, data.password);
var src = Hash.getBlobPathFromHex(secret.channel);
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>';
editor.replaceSelection(mt);
@ -538,7 +540,7 @@ define([
editor.on('change', framework.localChange);
framework.setFileExporter(CodeMirror.getContentExtension, CodeMirror.fileExporter);
framework.setFileExporter(".md", CodeMirror.fileExporter);
framework.setFileImporter({}, CodeMirror.fileImporter);
framework.start();

@ -107,13 +107,13 @@ define([
// A ticket has been closed by the admins...
if (!$ticket.length) { return; }
$ticket.addClass('cp-support-list-closed');
$ticket.append(Support.makeCloseMessage(common, content, hash));
$ticket.append(APP.support.makeCloseMessage(content, hash));
return;
}
if (msg.type !== 'TICKET') { return; }
if (!$ticket.length) {
$ticket = Support.makeTicket($div, common, content, function () {
$ticket = APP.support.makeTicket($div, content, function () {
var error = false;
hashesById[id].forEach(function (d) {
common.mailbox.dismiss(d, function (err) {
@ -126,7 +126,7 @@ define([
if (!error) { $ticket.remove(); }
});
}
$ticket.append(Support.makeMessage(common, content, hash, false));
$ticket.append(APP.support.makeMessage(content, hash));
}
});
return $div;
@ -137,7 +137,7 @@ define([
var key = 'form';
var $div = makeBlock(key, true);
var form = Support.makeForm();
var form = APP.support.makeForm();
$div.find('button').before(form);
@ -147,7 +147,7 @@ define([
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var user = metadataMgr.getUserData();
var sent = Support.sendForm(common, id, form, {
var sent = APP.support.sendForm(id, form, {
channel: privateData.support,
curvePublic: user.curvePublic
});
@ -244,6 +244,7 @@ define([
APP.origin = privateData.origin;
APP.readOnly = privateData.readOnly;
APP.support = Support.create(common, false);
// Content
var $rightside = APP.$rightside;

@ -8,7 +8,8 @@ define([
'/customize/messages.js',
], function ($, ApiConfig, h, UI, Hash, Util, Messages) {
var send = function (common, id, type, data, dest) {
var send = function (ctx, id, type, data, dest) {
var common = ctx.common;
var supportKey = ApiConfig.supportMailbox;
var supportChannel = Hash.getChannelIdFromKey(supportKey);
var metadataMgr = common.getMetadataMgr();
@ -27,6 +28,10 @@ define([
data.id = id;
data.time = +new Date();
if (!ctx.isAdmin) {
data.sender.userAgent = window.navigator && window.navigator.userAgent;
}
// Send the message to the admin mailbox and to the user mailbox
common.mailbox.sendTo(type, data, {
channel: supportChannel,
@ -36,9 +41,16 @@ define([
channel: dest.channel,
curvePublic: dest.curvePublic
});
if (ctx.isAdmin) {
common.mailbox.sendTo('SUPPORT_MESSAGE', {}, {
channel: dest.notifications,
curvePublic: dest.curvePublic
});
}
};
var sendForm = function (common, id, form, dest) {
var sendForm = function (ctx, id, form, dest) {
var $title = $(form).find('.cp-support-form-title');
var $content = $(form).find('.cp-support-form-msg');
@ -53,7 +65,7 @@ define([
$content.val('');
$title.val('');
send(common, id, 'TICKET', {
send(ctx, id, 'TICKET', {
title: title,
message: content,
}, dest);
@ -97,7 +109,7 @@ define([
return form;
};
var makeTicket = function ($div, common, content, onHide) {
var makeTicket = function (ctx, $div, content, onHide) {
var ticketTitle = content.title + ' (#' + content.id + ')';
var answer = h('button.btn.btn-primary.cp-support-answer', Messages.support_answer);
var close = h('button.btn.btn-danger.cp-support-close', Messages.support_close);
@ -117,7 +129,7 @@ define([
]));
$(close).click(function () {
send(common, content.id, 'CLOSE', {}, content.sender);
send(ctx, content.id, 'CLOSE', {}, content.sender);
});
$(hide).click(function () {
@ -129,7 +141,7 @@ define([
$ticket.find('.cp-support-form-container').remove();
$(actions).hide();
var form = makeForm(function () {
var sent = sendForm(common, content.id, form, content.sender);
var sent = sendForm(ctx, content.id, form, content.sender);
if (sent) {
$(actions).show();
$(form).remove();
@ -142,7 +154,9 @@ define([
return $ticket;
};
var makeMessage = function (common, content, hash, isAdmin) {
var makeMessage = function (ctx, content, hash) {
var common = ctx.common;
var isAdmin = ctx.isAdmin;
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
@ -157,11 +171,12 @@ define([
$(userData).find('pre').toggle();
});
var name = Util.fixHTML(content.sender.name) || Messages.anonymous;
return h('div.cp-support-list-message', {
'data-hash': hash
}, [
h('div.cp-support-message-from' + (fromMe ? '.cp-support-fromme' : ''), [
UI.setHTML(h('span'), Messages._getKey('support_from', [content.sender.name])),
UI.setHTML(h('span'), Messages._getKey('support_from', [name])),
h('span.cp-support-message-time', content.time ? new Date(content.time).toLocaleString() : '')
]),
h('pre.cp-support-message-content', content.message),
@ -169,27 +184,48 @@ define([
]);
};
var makeCloseMessage = function (common, content, hash) {
var makeCloseMessage = function (ctx, content, hash) {
var common = ctx.common;
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var fromMe = content.sender && content.sender.edPublic === privateData.edPublic;
var name = Util.fixHTML(content.sender.name) || Messages.anonymous;
return h('div.cp-support-list-message', {
'data-hash': hash
}, [
h('div.cp-support-message-from' + (fromMe ? '.cp-support-fromme' : ''), [
UI.setHTML(h('span'), Messages._getKey('support_from', [content.sender.name])),
UI.setHTML(h('span'), Messages._getKey('support_from', [name])),
h('span.cp-support-message-time', content.time ? new Date(content.time).toLocaleString() : '')
]),
h('pre.cp-support-message-content', Messages.support_closed)
]);
};
var create = function (common, isAdmin) {
var ui = {};
var ctx = {
common: common,
isAdmin: isAdmin
};
ui.sendForm = function (id, form, dest) {
return sendForm(ctx, id, form, dest);
};
ui.makeForm = makeForm;
ui.makeTicket = function ($div, content, onHide) {
return makeTicket(ctx, $div, content, onHide);
};
ui.makeMessage = function (content, hash) {
return makeMessage(ctx, content, hash);
};
ui.makeCloseMessage = function (content, hash) {
return makeCloseMessage(ctx, content, hash);
};
return ui;
};
return {
sendForm: sendForm,
makeForm: makeForm,
makeTicket: makeTicket,
makeMessage: makeMessage,
makeCloseMessage: makeCloseMessage
create: create
};
});

@ -14,7 +14,7 @@ define([
var canvas = new Fabric.Canvas(canvas_node);
var content = userDoc.content;
canvas.loadFromJSON(content, function () {
module.type = 'svg';
module.ext = '.svg';
cb(canvas.toSVG());
});
};

@ -257,7 +257,7 @@ define([
metadataMgr.onChange(function () {
var md = metadataMgr.getMetadata();
if (md.palette) {
updateLocalPalette(md.palette);
updatePalette(md.palette);
}
});
@ -415,11 +415,11 @@ define([
setEditable(!locked);
});
framework.setFileExporter('png', function (cb) {
framework.setFileExporter('.png', function (cb) {
$canvas[0].toBlob(function (blob) {
cb(blob);
});
});
}, true);
framework.setNormalizer(function (c) {
return {

Loading…
Cancel
Save