fix merge conflict

pull/1/head
ansuz 5 years ago
commit bf1332451f

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

@ -224,6 +224,12 @@ module.exports = {
* STORAGE * 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 /* 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). * after a configurable number of days of inactivity (default 90 days).
* The value can be changed or set to false to remove expiration. * 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="&#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="&#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="&#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> </font></defs></svg>

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

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

@ -169,6 +169,28 @@ define([], function () {
height: 100%; height: 100%;
background: #5cb85c; 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); */}).toString().slice(14, -3);
var urlArgs = window.location.href.replace(/^.*\?([^\?]*)$/, function (all, x) { return x; }); var urlArgs = window.location.href.replace(/^.*\?([^\?]*)$/, function (all, x) { return x; });
var elem = document.createElement('div'); var elem = document.createElement('div');
@ -182,7 +204,7 @@ define([], function () {
'</div>', '</div>',
'<div class="cp-loading-container">', '<div class="cp-loading-container">',
'<div class="cp-loading-spinner-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>', '</div>',
'<p id="cp-loading-message"></p>', '<p id="cp-loading-message"></p>',
'</div>' '</div>'

@ -26,7 +26,8 @@ var getLanguage = messages._getLanguage = function () {
var l = getBrowserLanguage(); var l = getBrowserLanguage();
// Edge returns 'fr-FR' --> transform it to 'fr' and check again // Edge returns 'fr-FR' --> transform it to 'fr' and check again
return map[l] ? l : 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(); 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: '/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: '/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: '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: '/privacy.html'}, Msg.privacy),
//h('a.nav-item.nav-link', { href: '/contact.html'}, Msg.contact), //h('a.nav-item.nav-link', { href: '/contact.html'}, Msg.contact),
//h('a.nav-item.nav-link', { href: '/about.html'}, Msg.about), //h('a.nav-item.nav-link', { href: '/about.html'}, Msg.about),

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

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

@ -1,3 +1,4 @@
@import (reference) "./browser.less";
@import (reference) './colortheme-all.less'; @import (reference) './colortheme-all.less';
@import (reference) './modal.less'; @import (reference) './modal.less';
@ -10,24 +11,35 @@
#cp-fileupload { #cp-fileupload {
.modal_base(); .modal_base();
position: absolute; position: absolute;
left: 10vw; right: 10vw; right: 10vw;
bottom: 10vh; bottom: 10vh;
opacity: 0.9;
box-sizing: border-box; box-sizing: border-box;
z-index: 1000000; //Z file upload table container z-index: 1000000; //Z file upload table container
display: none; display: none;
#cp-fileupload-table { color: darken(@colortheme_drive-bg, 10%);
width: 80vw;
tr:nth-child(1) { @media screen and (max-width: @browser_media-medium-screen) {
background-color: darken(@colortheme_modal-bg, 20%); left: 5vw; right: 5vw; bottom: 5vw;
td { }
font-weight: bold;
padding: 0.25em; .cp-fileupload-header {
&:nth-child(4), &:nth-child(5) { display: flex;
text-align: center; 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_h: 0.25em;
@upload_pad_v: 0.5em; @upload_pad_v: 0.5em;
@ -35,27 +47,55 @@
padding: @upload_pad_h @upload_pad_v; padding: @upload_pad_h @upload_pad_v;
} }
.cp-fileupload-table-link { .cp-fileupload-table-link {
display: flex;
align-items: center;
white-space: nowrap;
max-width: 30vw;
margin: 0px @upload_pad_v;
.fa { .fa {
margin-top: 4px;
margin-right: 5px; 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 { .cp-fileupload-table-progress {
width: 25%; min-width: 12em;
max-width: 16em;
position: relative; position: relative;
text-align: center; text-align: center;
box-sizing: border-box; box-sizing: border-box;
} }
.cp-fileupload-table-progress-container { .cp-fileupload-table-progress-container {
position: relative;
}
.cp-fileupload-table-progressbar {
position: absolute; position: absolute;
width: 0px; width: 0px;
left: @upload_pad_v; height: 100%;
top: @upload_pad_h; bottom: @upload_pad_h; background-color: #dddddd;
background-color: rgba(0,0,255,0.3);
z-index: -1; //Z file upload progress container z-index: -1; //Z file upload progress container
} }
.cp-fileupload-table-cancel { text-align: center; } .cp-fileupload-table-cancel {
.fa.cancel { text-align: center;
color: rgb(255, 0, 115); 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; text-overflow: ellipsis;
} }
} }
div.plain-text-reader {
background: #f3f3f3;
padding: 10px;
color: black;
text-align: left;
}
} }
.markdown_preformatted-code (@color: #333) { .markdown_preformatted-code (@color: #333) {

@ -1,4 +1,5 @@
@import (reference) "./colortheme-all.less"; @import (reference) "./colortheme-all.less";
@import (reference) "./avatar.less";
.notifications_main() { .notifications_main() {
--LessLoader_require: LessLoader_currentFile(); --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 { h3 {
margin: 0; margin: 0;
} }
&.cp-pricing {
div {
font-size: 1.2em;
color: @cryptpad_color_blue;
&:first-child {
font-weight: bold;
}
&:last-child {
font-size: 1em;
}
}
}
} }
} }
h3 { h3 {

@ -5,10 +5,24 @@
const nThen = require('nthen'); const nThen = require('nthen');
const Nacl = require('tweetnacl'); const Nacl = require('tweetnacl');
const Crypto = require('crypto'); const Crypto = require('crypto');
const Once = require("./lib/once");
const Meta = require("./lib/metadata");
let Log; let Log;
const now = function () { return (new Date()).getTime(); }; 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) { const getHash = function (msg) {
if (typeof(msg) !== 'string') { if (typeof(msg) !== 'string') {
Log.warn('HK_GET_HASH', 'getHash() called on ' + typeof(msg) + ': ' + msg); 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) { const sliceCpIndex = function (cpIndex, line) {
// Remove "old" checkpoints (cp sent before 100 messages ago) // Remove "old" checkpoints (cp sent before 100 messages ago)
const minLine = Math.max(0, (line - 100)); const minLine = Math.max(0, (line - 100));
@ -36,6 +62,20 @@ const sliceCpIndex = function (cpIndex, line) {
return start.concat(end); 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) { module.exports.create = function (cfg) {
const rpc = cfg.rpc; const rpc = cfg.rpc;
const tasks = cfg.tasks; const tasks = cfg.tasks;
@ -44,7 +84,7 @@ module.exports.create = function (cfg) {
Log.silly('HK_LOADING', 'LOADING HISTORY_KEEPER MODULE'); Log.silly('HK_LOADING', 'LOADING HISTORY_KEEPER MODULE');
const historyKeeperKeys = {}; const metadata_cache = {};
const HISTORY_KEEPER_ID = Crypto.randomBytes(8).toString('hex'); const HISTORY_KEEPER_ID = Crypto.randomBytes(8).toString('hex');
Log.verbose('HK_ID', 'History keeper ID: ' + HISTORY_KEEPER_ID); 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; let STANDARD_CHANNEL_LENGTH, EPHEMERAL_CHANNEL_LENGTH;
const setConfig = function (config) { const setConfig = function (config) {
STANDARD_CHANNEL_LENGTH = config.STANDARD_CHANNEL_LENGTH; STANDARD_CHANNEL_LENGTH = config.STANDARD_CHANNEL_LENGTH;
EPHEMERAL_CHANNEL_LENGTH = config.EPHEMERAL_CHANNEl_LENGTH; EPHEMERAL_CHANNEL_LENGTH = config.EPHEMERAL_CHANNEL_LENGTH;
sendMsg = config.sendMsg; 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 computeIndex = function (channelName, cb) {
const cpIndex = []; const cpIndex = [];
let messageBuf = []; let messageBuf = [];
let validateKey;
let metadata; let metadata;
let i = 0; let i = 0;
store.readMessagesBin(channelName, 0, (msgObj, rmcb) => {
let msg; const ref = {};
i++;
if (!validateKey && msgObj.buff.indexOf('validateKey') > -1) { const CB = Once(cb);
metadata = msg = tryParse(msgObj.buff.toString('utf8'));
if (typeof msg === "undefined") { return rmcb(); } const offsetByHash = {};
if (msg.validateKey) { let size = 0;
validateKey = historyKeeperKeys[channelName] = msg; nThen(function (w) {
return rmcb(); // 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();
}
} }
} i++;
if (msgObj.buff.indexOf('cp|') > -1) { if (msgObj.buff.indexOf('cp|') > -1) {
msg = msg || tryParse(msgObj.buff.toString('utf8')); msg = msg || tryParse(msgObj.buff.toString('utf8'));
if (typeof msg === "undefined") { return rmcb(); } if (typeof msg === "undefined") { return readMore(); }
if (msg[2] === 'MSG' && msg[4].indexOf('cp|') === 0) { // cache the offsets of checkpoints if they can be parsed
cpIndex.push({ if (msg[2] === 'MSG' && msg[4].indexOf('cp|') === 0) {
offset: msgObj.offset, cpIndex.push({
line: i offset: msgObj.offset,
}); line: i
messageBuf = []; });
// we only want to store messages since the latest checkpoint
// so clear the buffer every time you see a new one
messageBuf = [];
}
} }
} // if it's not metadata or a checkpoint then it should be a regular message
messageBuf.push(msgObj); // store it in the buffer
return rmcb(); messageBuf.push(msgObj);
}, (err) => { return readMore();
if (err && err.code !== 'ENOENT') { return void cb(err); } }, w((err) => {
const offsetByHash = {}; if (err && err.code !== 'ENOENT') {
let size = 0; w.abort();
messageBuf.forEach((msgObj) => { return void CB(err);
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;
} }
// There is a trailing \n at the end of the file
size = msgObj.offset + msgObj.buff.length + 1; // 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
cb(null, { 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 // Only keep the checkpoints included in the last 100 messages
cpIndex: sliceCpIndex(cpIndex, i), cpIndex: sliceCpIndex(cpIndex, i),
offsetByHash: offsetByHash, 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 getIndex = (ctx, channelName, cb) => {
const chan = ctx.channels[channelName]; 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) => { 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; } 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 the message first, and update the index only once it's stored.
// store.messageBin can be async so updating the index first may // store.messageBin can be async so updating the index first may
// result in a wrong cpIndex // result in a wrong cpIndex
nThen((waitFor) => { nThen((waitFor) => {
store.messageBin(channel.id, msgBin, waitFor(function (err) { store.messageBin(id, msgBin, waitFor(function (err) {
if (err) { if (err) {
waitFor.abort(); 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) => { }).nThen((waitFor) => {
getIndex(ctx, channel.id, waitFor((err, index) => { getIndex(ctx, id, waitFor((err, index) => {
if (err) { if (err) {
Log.warn("HK_STORE_MESSAGE_INDEX", err.stack); Log.warn("HK_STORE_MESSAGE_INDEX", err.stack);
// non-critical, we'll be able to get the channel index later // 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; return;
} }
if (typeof (index.line) === "number") { index.line++; } if (typeof (index.line) === "number") { index.line++; }
@ -161,60 +358,177 @@ module.exports.create = function (cfg) {
line: ((index.line || 0) + 1) line: ((index.line || 0) + 1)
} /*:cp_index_item*/)); } /*:cp_index_item*/));
} }
if (maybeMsgHash) { index.offsetByHash[maybeMsgHash] = index.size; } if (optionalMessageHash) { index.offsetByHash[optionalMessageHash] = index.size; }
index.size += msgBin.length; 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) { const onChannelMessage = function (ctx, channel, msgStruct) {
// don't store messages if the channel id indicates that it's an ephemeral message // 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; } if (!channel.id || channel.id.length === EPHEMERAL_CHANNEL_LENGTH) { return; }
const isCp = /^cp\|/.test(msgStruct[4]); 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; let id;
if (isCp) { if (isCp) {
/*::if (typeof(msgStruct[4]) !== 'string') { throw new Error(); }*/ // id becomes either null or an array or results...
id = /cp\|(([A-Za-z0-9+\/=]+)\|)?/.exec(msgStruct[4]); id = CHECKPOINT_PATTERN.exec(msgStruct[4]);
if (Array.isArray(id) && id[2] && id[2] === channel.lastSavedCp) { if (Array.isArray(id) && id[2] && id[2] === channel.lastSavedCp) {
// Reject duplicate checkpoints // Reject duplicate checkpoints
return; return;
} }
} }
if (historyKeeperKeys[channel.id] && historyKeeperKeys[channel.id].validateKey) {
/*::if (typeof(msgStruct[4]) !== 'string') { throw new Error(); }*/ let metadata;
let signedMsg = (isCp) ? msgStruct[4].replace(/^cp\|(([A-Za-z0-9+\/=]+)\|)?/, '') : msgStruct[4]; nThen(function (w) {
signedMsg = Nacl.util.decodeBase64(signedMsg); // getIndex (and therefore the latest metadata)
const validateKey = Nacl.util.decodeBase64(historyKeeperKeys[channel.id].validateKey); getIndex(ctx, channel.id, w(function (err, index) {
const validated = Nacl.sign.open(signedMsg, validateKey); if (err) {
if (!validated) { w.abort();
Log.info("HK_SIGNED_MESSAGE_REJECTED", 'Channel '+channel.id); return void Log.error('CHANNEL_MESSAGE_ERROR', err);
return; }
}
} if (!index.metadata) {
if (isCp) { // if there's no channel metadata then it can't be an expiring channel
// WARNING: the fact that we only check the most recent checkpoints // nor can we possibly validate it
// is a potential source of bugs if one editor has high latency and return;
// 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]) { metadata = index.metadata;
// Store new checkpoint hash
channel.lastSavedCp = id[2]; 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()); // add the time to the message
storeMessage(ctx, channel, JSON.stringify(msgStruct), isCp, getHash(msgStruct[4])); 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) { 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*/) => { const getHistoryOffset = (ctx, channelName, lastKnownHash, cb /*:(e:?Error, os:?number)=>void*/) => {
// lastKnownhash === -1 means we want the complete history // lastKnownhash === -1 means we want the complete history
if (lastKnownHash === -1) { return void cb(null, 0); } if (lastKnownHash === -1) { return void cb(null, 0); }
@ -223,8 +537,17 @@ module.exports.create = function (cfg) {
getIndex(ctx, channelName, waitFor((err, index) => { getIndex(ctx, channelName, waitFor((err, index) => {
if (err) { waitFor.abort(); return void cb(err); } 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]; 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") { if (lastKnownHash && typeof(lkh) !== "number") {
waitFor.abort(); waitFor.abort();
return void cb(new Error('EINVAL')); return void cb(new Error('EINVAL'));
@ -250,12 +573,20 @@ module.exports.create = function (cfg) {
offset = lkh; offset = lkh;
})); }));
}).nThen((waitFor) => { }).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; } 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')); 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])) { if (typeof(msg[4]) !== 'string' || lastKnownHash !== getHash(msg[4])) {
return void rmcb(); return void readMore();
} }
offset = msgObj.offset; offset = msgObj.offset;
abort(); 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) => { const getHistoryAsync = (ctx, channelName, lastKnownHash, beforeHash, handler, cb) => {
let offset = -1; let offset = -1;
nThen((waitFor) => { nThen((waitFor) => {
@ -280,15 +620,24 @@ module.exports.create = function (cfg) {
}).nThen((waitFor) => { }).nThen((waitFor) => {
if (offset === -1) { return void cb(new Error("could not find offset")); } if (offset === -1) { return void cb(new Error("could not find offset")); }
const start = (beforeHash) ? 0 : 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(); } 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) { }, waitFor(function (err) {
return void cb(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) { const getOlderHistory = function (channelName, oldestKnownHash, cb) {
var messageBuffer = []; var messageBuffer = [];
var found = false; var found = false;
@ -298,10 +647,11 @@ module.exports.create = function (cfg) {
let parsed = tryParse(msgStr); let parsed = tryParse(msgStr);
if (typeof parsed === "undefined") { return; } if (typeof parsed === "undefined") { return; }
if (parsed.validateKey) { // identify classic metadata messages by their inclusion of a channel.
historyKeeperKeys[channelName] = parsed; // and don't send metadata, since:
return; // 1. the user won't be interested in it
} // 2. this metadata is potentially incomplete/incorrect
if (isMetadataMessage(parsed)) { return; }
var content = parsed[4]; var content = parsed[4];
if (typeof(content) !== 'string') { return; } 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) { const historyKeeperBroadcast = function (ctx, channel, msg) {
let chan = ctx.channels[channel] || (([] /*:any*/) /*:Chan_t*/); let chan = ctx.channels[channel] || (([] /*:any*/) /*:Chan_t*/);
chan.forEach(function (user) { chan.forEach(function (user) {
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(msg)]); 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) { const onChannelCleared = function (ctx, channel) {
historyKeeperBroadcast(ctx, channel, { historyKeeperBroadcast(ctx, channel, {
error: 'ECLEARED', error: 'ECLEARED',
@ -351,13 +708,29 @@ module.exports.create = function (cfg) {
}); });
}); });
delete ctx.channels[channel]; delete ctx.channels[channel];
delete historyKeeperKeys[channel]; delete metadata_cache[channel];
}; };
// Check if the selected channel is expired // Check if the selected channel is expired
// If it is, remove it from memory and broadcast a message to its members // 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) { const checkExpired = function (ctx, channel) {
if (channel && channel.length === STANDARD_CHANNEL_LENGTH && historyKeeperKeys[channel] && if (channel && channel.length === STANDARD_CHANNEL_LENGTH && metadata_cache[channel] &&
historyKeeperKeys[channel].expire && historyKeeperKeys[channel].expire < +new Date()) { metadata_cache[channel].expire && metadata_cache[channel].expire < +new Date()) {
store.closeChannel(channel, function () { store.closeChannel(channel, function () {
historyKeeperBroadcast(ctx, channel, { historyKeeperBroadcast(ctx, channel, {
error: 'EEXPIRED', error: 'EEXPIRED',
@ -365,12 +738,25 @@ module.exports.create = function (cfg) {
}); });
}); });
delete ctx.channels[channel]; delete ctx.channels[channel];
delete historyKeeperKeys[channel]; delete metadata_cache[channel];
return true; return true;
} }
return; 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) { const onDirectMessage = function (ctx, seq, user, json) {
let parsed; let parsed;
let channelName; let channelName;
@ -386,7 +772,7 @@ module.exports.create = function (cfg) {
} }
// If the requested history is for an expired channel, abort // 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) // have to abort later (once we know the expiration time)
if (checkExpired(ctx, parsed[1])) { return; } if (checkExpired(ctx, parsed[1])) { return; }
@ -396,35 +782,31 @@ module.exports.create = function (cfg) {
// parsed[3] is the last known hash (optionnal) // parsed[3] is the last known hash (optionnal)
sendMsg(ctx, user, [seq, 'ACK']); sendMsg(ctx, user, [seq, 'ACK']);
channelName = parsed[1]; channelName = parsed[1];
var validateKey = parsed[2]; var config = parsed[2];
var lastKnownHash = parsed[3]; var metadata = {};
var owners; var lastKnownHash;
var expire;
if (parsed[2] && typeof parsed[2] === "object") { // clients can optionally pass a map of attributes
validateKey = parsed[2].validateKey; // if the channel already exists this map will be ignored
lastKnownHash = parsed[2].lastKnownHash; // otherwise it will be stored as the initial metadata state for the channel
owners = parsed[2].owners; if (config && typeof config === "object" && !Array.isArray(parsed[2])) {
if (parsed[2].expire) { lastKnownHash = config.lastKnownHash;
expire = +parsed[2].expire * 1000 + (+new Date()); 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) { 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(); var w = waitFor();
/* unless this is a young channel, we will serve all messages from an offset /* 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... so, let's just fall through...
*/ */
if (err) { return w(); } 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(); } 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 // 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(); } if (checkExpired(ctx, channelName)) { return void waitFor.abort(); }
// Send the metadata to the user // always send metadata with GET_HISTORY requests
if (!lastKnownHash && index.cpIndex.length > 1) { sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(index.metadata)], w);
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(index.metadata)], w);
return;
}
w();
})); }));
}).nThen(() => { }).nThen(() => {
let msgCount = 0; 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) { 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++; msgCount++;
// avoid sending the metadata message a second time
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(msg)], cb); if (isMetadataMessage(msg) && metadata_cache[channelName]) { return readMore(); }
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(msg)], readMore);
}, (err) => { }, (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 && err.code !== 'ENOENT') {
if (err.message !== 'EINVAL') { Log.error("HK_GET_HISTORY", err); } if (err.message !== 'EINVAL') { Log.error("HK_GET_HISTORY", err); }
const parsedMsg = {error:err.message, channel: channelName}; const parsedMsg = {error:err.message, channel: channelName};
@ -478,24 +850,46 @@ module.exports.create = function (cfg) {
return; 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]; const chan = ctx.channels[channelName];
if (msgCount === 0 && !historyKeeperKeys[channelName] && chan && chan.indexOf(user) > -1) {
var key = {}; if (msgCount === 0 && !metadata_cache[channelName] && chan && chan.indexOf(user) > -1) {
key.channel = channelName; metadata_cache[channelName] = metadata;
if (validateKey) {
key.validateKey = validateKey; // 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
if (owners) { // this means that the cache starts off as invalid, so we have to correct it
key.owners = owners; if (chan && chan.index) { chan.index.metadata = metadata; }
}
if (expire) { // new channels will always have their metadata written to a dedicated metadata log
key.expire = expire; // 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; sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(metadata)]);
storeMessage(ctx, chan, JSON.stringify(key), false, undefined);
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(key)]);
} }
// End of history message: // End of history message:
@ -551,9 +945,12 @@ module.exports.create = function (cfg) {
// parsed[2] is a validation key (optionnal) // parsed[2] is a validation key (optionnal)
// parsed[3] is the last known hash (optionnal) // parsed[3] is the last known hash (optionnal)
sendMsg(ctx, user, [seq, 'ACK']); 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; } 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) => { }, (err) => {
let parsedMsg = ['FULL_HISTORY_END', parsed[1]]; let parsedMsg = ['FULL_HISTORY_END', parsed[1]];
if (err) { if (err) {
@ -581,6 +978,15 @@ module.exports.create = function (cfg) {
if (msg[3] === 'CLEAR_OWNED_CHANNEL') { if (msg[3] === 'CLEAR_OWNED_CHANNEL') {
onChannelCleared(ctx, msg[4]); 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))]); sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify([parsed[0]].concat(output))]);
}); });
} catch (e) { } 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", "name": "cryptpad",
"version": "2.25.0", "version": "3.0.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -56,9 +56,9 @@
"optional": true "optional": true
}, },
"async-limiter": { "async-limiter": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
}, },
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
@ -99,9 +99,9 @@
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
}, },
"chainpad-server": { "chainpad-server": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/chainpad-server/-/chainpad-server-3.0.2.tgz", "resolved": "https://registry.npmjs.org/chainpad-server/-/chainpad-server-3.0.3.tgz",
"integrity": "sha512-c5aEljVAapDKKs0+Rt2jymKAszm8X4ZeLFNJj1yxflwBqoh0jr8OANYvbfjtNaYFe2Wdflp/1i4gibYX4IMc+g==", "integrity": "sha512-NRfV7FFBEYy4ZVX7h0P5znu55X8v5K4iGWeMGihkfWZLKu70GmCPUTwpBCP79dUvnCToKEa4/e8aoSPcvZC8pA==",
"requires": { "requires": {
"nthen": "^0.1.8", "nthen": "^0.1.8",
"pull-stream": "^3.6.9", "pull-stream": "^3.6.9",
@ -227,19 +227,25 @@
} }
}, },
"dom-serializer": { "dom-serializer": {
"version": "0.1.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.1.tgz",
"integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", "integrity": "sha512-sK3ujri04WyjwQXVoK4PU3y8ula1stq10GJZpqHIUgoGZdsGzAGu65BnU3d08aTVSvO7mGPZUc0wTEDL+qGE0Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"domelementtype": "^1.3.0", "domelementtype": "^2.0.1",
"entities": "^1.1.1" "entities": "^2.0.0"
}, },
"dependencies": { "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": { "entities": {
"version": "1.1.2", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==",
"dev": true "dev": true
} }
} }
@ -458,9 +464,9 @@
} }
}, },
"graceful-fs": { "graceful-fs": {
"version": "4.1.15", "version": "4.2.2",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz",
"integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q=="
}, },
"has-ansi": { "has-ansi": {
"version": "2.0.0", "version": "2.0.0",
@ -597,9 +603,9 @@
} }
}, },
"jszip": { "jszip": {
"version": "3.2.1", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.1.tgz", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.2.tgz",
"integrity": "sha512-iCMBbo4eE5rb1VCpm5qXOAaUiRKRUKiItn8ah2YQQx9qymmSAY98eyQfioChEYcVQLh0zxJ3wS4A0mh90AVPvw==", "integrity": "sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA==",
"dev": true, "dev": true,
"requires": { "requires": {
"lie": "~3.3.0", "lie": "~3.3.0",
@ -697,9 +703,9 @@
} }
}, },
"lodash": { "lodash": {
"version": "4.17.14", "version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"dev": true "dev": true
}, },
"lodash.clonedeep": { "lodash.clonedeep": {
@ -711,7 +717,8 @@
"lodash.merge": { "lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "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": { "lodash.sortby": {
"version": "4.7.0", "version": "4.7.0",
@ -965,9 +972,9 @@
} }
}, },
"process-nextick-args": { "process-nextick-args": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true "dev": true
}, },
"promise": { "promise": {
@ -997,9 +1004,9 @@
"optional": true "optional": true
}, },
"pull-stream": { "pull-stream": {
"version": "3.6.12", "version": "3.6.14",
"resolved": "https://registry.npmjs.org/pull-stream/-/pull-stream-3.6.12.tgz", "resolved": "https://registry.npmjs.org/pull-stream/-/pull-stream-3.6.14.tgz",
"integrity": "sha512-+LO1XIVyTMmeoH26UHznpgrgX2npTVYccTkMpgk/EyiQjFt1FmoNm+w+/zMLuz9U3bpvT5sSUicMKEe/2JjgEA==" "integrity": "sha512-KIqdvpqHHaTUA2mCYcLG1ibEbu/LCKoJZsBWyv9lSYtPkJPBq8m3Hxa103xHi6D2thj5YXa0TqK3L3GUkwgnew=="
}, },
"qs": { "qs": {
"version": "6.5.2", "version": "6.5.2",
@ -1049,9 +1056,9 @@
"integrity": "sha1-lAFm0gfRDphhT+SSU60vCsAZ9+E=" "integrity": "sha1-lAFm0gfRDphhT+SSU60vCsAZ9+E="
}, },
"rimraf": { "rimraf": {
"version": "2.6.3", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true, "dev": true,
"requires": { "requires": {
"glob": "^7.1.3" "glob": "^7.1.3"

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

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

@ -105,6 +105,18 @@ app.head(/^\/common\/feedback\.html/, function (req, res, next) {
}()); }());
app.use(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); setHeaders(req, res);
if (/[\?\&]ver=[^\/]+$/.test(req.url)) { res.setHeader("Cache-Control", "max-age=31536000"); } if (/[\?\&]ver=[^\/]+$/.test(req.url)) { res.setHeader("Cache-Control", "max-age=31536000"); }
next(); next();
@ -247,7 +259,7 @@ var historyKeeper;
var log; 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) { var nt = nThen(function (w) {
// set up logger // set up logger
var Logger = require("./lib/log"); var Logger = require("./lib/log");
@ -261,13 +273,13 @@ var nt = nThen(function (w) {
config.store = _store; config.store = _store;
})); }));
}).nThen(function (w) { }).nThen(function (w) {
if (!config.enableTaskScheduling) { return; }
var Tasks = require("./storage/tasks"); var Tasks = require("./storage/tasks");
Tasks.create(config, w(function (e, tasks) { Tasks.create(config, w(function (e, tasks) {
if (e) { if (e) {
throw e; throw e;
} }
config.tasks = tasks; config.tasks = tasks;
if (config.disableIntegratedTasks) { return; }
setInterval(function () { setInterval(function () {
tasks.runAll(function (err) { tasks.runAll(function (err) {
if (err) { if (err) {

@ -6,6 +6,7 @@ var Fse = require("fs-extra");
var Path = require("path"); var Path = require("path");
var nThen = require("nthen"); var nThen = require("nthen");
var Semaphore = require("saferphore"); var Semaphore = require("saferphore");
var Once = require("../lib/once");
const ToPull = require('stream-to-pull-stream'); const ToPull = require('stream-to-pull-stream');
const Pull = require('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'; 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 getMetadataAtPath = function (Env, path, cb) {
var remainder = ''; var remainder = '';
var stream = Fs.createReadStream(path, { encoding: 'utf8' }); var stream = Fs.createReadStream(path, { encoding: 'utf8' });
@ -60,11 +85,6 @@ var getMetadataAtPath = function (Env, path, cb) {
stream.on('error', function (e) { complete(e); }); 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) { var closeChannel = function (env, channelName, cb) {
if (!env.channels[channelName]) { return void cb(); } if (!env.channels[channelName]) { return void cb(); }
try { 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 clearChannel = function (env, channelId, cb) {
var path = mkPath(env, channelId); var path = mkPath(env, channelId);
getMetadataAtPath(env, path, function (e, metadata) { 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 readMessages = function (path, msgHandler, cb) {
var remainder = ''; var remainder = '';
var stream = Fs.createReadStream(path, { encoding: 'utf8' }); var stream = Fs.createReadStream(path, { encoding: 'utf8' });
@ -127,6 +151,104 @@ var readMessages = function (path, msgHandler, cb) {
stream.on('error', function (e) { complete(e); }); 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 NEWLINE_CHR = ('\n').charCodeAt(0);
const mkBufferSplit = () => { const mkBufferSplit = () => {
let remainder = null; let remainder = null;
@ -160,6 +282,8 @@ const mkBufferSplit = () => {
}, Pull.flatten()); }, 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 = () => { const mkOffsetCounter = () => {
let offset = 0; let offset = 0;
return Pull.map((buff) => { 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 readMessagesBin = (env, id, start, msgHandler, cb) => {
const stream = Fs.createReadStream(mkPath(env, id), { start: start }); const stream = Fs.createReadStream(mkPath(env, id), { start: start });
// TODO get the channel and add the atime
let keepReading = true; let keepReading = true;
Pull( Pull(
ToPull.read(stream), 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) { var checkPath = function (path, callback) {
// TODO check if we actually need to use stat at all
Fs.stat(path, function (err) { Fs.stat(path, function (err) {
if (!err) { if (!err) {
callback(undefined, true); callback(undefined, true);
@ -208,31 +336,79 @@ var checkPath = function (path, callback) {
}); });
}; };
var removeChannel = function (env, channelName, cb) { var labelError = function (label, err) {
var filename = mkPath(env, channelName); return label + (err.code ? "_" + err.code: '');
Fs.unlink(filename, cb);
}; };
// pass in the path so we can reuse the same function for archived files /* removeChannel
var channelExists = function (filepath, channelName, cb) { fully deletes a channel log and any associated metadata
Fs.stat(filepath, function (err, stat) { */
if (err) { var removeChannel = function (env, channelName, cb) {
if (err.code === 'ENOENT') { var channelPath = mkPath(env, channelName);
// no, the file doesn't exist var metadataPath = mkMetadataPath(env, channelName);
return void cb(void 0, false);
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 removeArchivedChannel = function (env, channelName, cb) {
var filename = mkArchivePath(env, channelName); var channelPath = mkArchivePath(env, channelName);
Fs.unlink(filename, cb); 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) { var listChannels = function (root, handler, cb) {
// do twenty things at a time // do twenty things at a time
var sema = Semaphore.create(20); var sema = Semaphore.create(20);
@ -255,15 +431,31 @@ var listChannels = function (root, handler, cb) {
var wait = w(); var wait = w();
dirList.forEach(function (dir) { dirList.forEach(function (dir) {
sema.take(function (give) { 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); var nestedDirPath = Path.join(root, dir);
Fs.readdir(nestedDirPath, w(give(function (err, list) { Fs.readdir(nestedDirPath, w(give(function (err, list) {
if (err) { return void handler(err); } // Is this correct? if (err) { return void handler(err); } // Is this correct?
list.forEach(function (item) { list.forEach(function (item) {
// ignore things that don't match the naming pattern // ignore hidden files
if (/^\./.test(item) || !/[0-9a-fA-F]{32,}\.ndjson$/.test(item)) { return; } 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 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; } if ([32, 34].indexOf(channel.length) === -1) { return; }
// otherwise throw it on the pile // 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 // move a channel's log file from its current location
// to an equivalent location in the cold storage directory // to an equivalent location in the cold storage directory
var archiveChannel = function (env, channelName, cb) { var archiveChannel = function (env, channelName, cb) {
// TODO close channels before archiving them?
if (!env.retainData) { if (!env.retainData) {
return void cb("ARCHIVES_DISABLED"); 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. // 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 // 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) { var unarchiveChannel = function (env, channelName, cb) {
// very much like 'archiveChannel' but in the opposite direction // very much like 'archiveChannel' but in the opposite direction
// the file is currently archived // the file is currently archived
var currentPath = mkArchivePath(env, channelName); var channelPath = mkPath(env, channelName);
var unarchivedPath = 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 // if a file exists in the unarchived path, you probably don't want to clobber its data
// so unlike 'archiveChannel' we won't overwrite. // so unlike 'archiveChannel' we won't overwrite.
// Fse.move will call back with EEXIST in such a situation // 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) { var flushUnusedChannels = function (env, cb, frame) {
@ -352,11 +631,34 @@ var flushUnusedChannels = function (env, cb, frame) {
cb(); 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 channelBytes = function (env, chanName, cb) {
var path = mkPath(env, chanName); var channelPath = mkPath(env, chanName);
Fs.stat(path, function (err, stats) { var dataPath = mkMetadataPath(env, chanName);
if (err) { return void cb(err); }
cb(undefined, stats.size); 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) => { const messageBin = (env, chanName, msgBin, cb) => {
getChannel(env, chanName, function (err, chan) { getChannel(env, chanName, function (err, chan) {
if (!chan) { if (!chan) {
@ -466,18 +769,19 @@ const messageBin = (env, chanName, msgBin, cb) => {
chan.writeStream.write(msgBin, function () { chan.writeStream.write(msgBin, function () {
/*::if (!chan) { throw new Error("Flow unreachable"); }*/ /*::if (!chan) { throw new Error("Flow unreachable"); }*/
chan.onError.splice(chan.onError.indexOf(complete), 1); chan.onError.splice(chan.onError.indexOf(complete), 1);
chan.atime = +new Date();
if (!cb) { return; } 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(); complete();
}); });
}); });
}; };
// append a string to a channel's log as a new line
var message = function (env, chanName, msg, cb) { var message = function (env, chanName, msg, cb) {
messageBin(env, chanName, new Buffer(msg + '\n', 'utf8'), cb); messageBin(env, chanName, new Buffer(msg + '\n', 'utf8'), cb);
}; };
// stream messages from a channel log
var getMessages = function (env, chanName, handler, cb) { var getMessages = function (env, chanName, handler, cb) {
getChannel(env, chanName, function (err, chan) { getChannel(env, chanName, function (err, chan) {
if (!chan) { if (!chan) {
@ -499,6 +803,9 @@ var getMessages = function (env, chanName, handler, cb) {
errorState = true; errorState = true;
return void cb(err); 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"); } if (!chan) { throw new Error("impossible, flow checking"); }
chan.atime = +new Date(); chan.atime = +new Date();
cb(); cb();
@ -563,80 +870,124 @@ module.exports.create = function (
})); }));
}).nThen(function () { }).nThen(function () {
cb({ cb({
readMessagesBin: (channelName, start, asyncMsgHandler, cb) => { // OLDER METHODS
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } // write a new message to a log
readMessagesBin(env, channelName, start, asyncMsgHandler, cb);
},
message: function (channelName, content, cb) { message: function (channelName, content, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
message(env, channelName, content, cb); 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) => { messageBin: (channelName, content, cb) => {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
messageBin(env, channelName, content, cb); 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')); } 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) { removeChannel: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
removeChannel(env, channelName, function (err) { removeChannel(env, channelName, function (err) {
cb(err); cb(err);
}); });
}, },
// remove a channel and its associated metadata log from the archive directory
removeArchivedChannel: function (channelName, cb) { removeArchivedChannel: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
removeArchivedChannel(env, channelName, cb); removeArchivedChannel(env, channelName, cb);
}, },
closeChannel: function (channelName, cb) { // clear all data for a channel but preserve its metadata
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);
},
clearChannel: function (channelName, cb) { clearChannel: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
clearChannel(env, channelName, cb); 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) { isChannelAvailable: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
// construct the path // construct the path
var filepath = mkPath(env, channelName); var filepath = mkPath(env, channelName);
channelExists(filepath, channelName, cb); channelExists(filepath, cb);
}, },
// check if a channel exists in the archive
isChannelArchived: function (channelName, cb) { isChannelArchived: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
// construct the path // construct the path
var filepath = mkArchivePath(env, channelName); var filepath = mkArchivePath(env, channelName);
channelExists(filepath, channelName, cb); channelExists(filepath, cb);
},
listArchivedChannels: function (handler, cb) {
listChannels(Path.join(env.archiveRoot, 'datastore'), handler, cb);
}, },
// move a channel from the database to the archive, along with its metadata
archiveChannel: function (channelName, cb) { archiveChannel: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
archiveChannel(env, channelName, cb); archiveChannel(env, channelName, cb);
}, },
// restore a channel from the archive to the database, along with its metadata
restoreArchivedChannel: function (channelName, cb) { restoreArchivedChannel: function (channelName, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
unarchiveChannel(env, channelName, cb); 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) { log: function (channelName, content, cb) {
message(env, channelName, content, cb); message(env, channelName, content, cb);
}, },
// shut down the database
shutdown: function () { shutdown: function () {
clearInterval(it); clearInterval(it);
} }

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

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

@ -272,6 +272,7 @@ define([
var andThen2 = function (editor, CodeMirror, framework, isPresentMode) { var andThen2 = function (editor, CodeMirror, framework, isPresentMode) {
var common = framework._.sfCommon; var common = framework._.sfCommon;
var privateData = common.getMetadataMgr().getPrivateData();
var previewPane = mkPreviewPane(editor, CodeMirror, framework, isPresentMode); var previewPane = mkPreviewPane(editor, CodeMirror, framework, isPresentMode);
var markdownTb = mkMarkdownTb(editor, framework); var markdownTb = mkMarkdownTb(editor, framework);
@ -349,7 +350,8 @@ define([
onUploaded: function (ev, data) { onUploaded: function (ev, data) {
var parsed = Hash.parsePadUrl(data.url); var parsed = Hash.parsePadUrl(data.url);
var secret = Hash.getSecrets('file', parsed.hash, data.password); 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 key = Hash.encodeBase64(secret.keys.cryptKey);
var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + key + '"></media-tag>'; var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + key + '"></media-tag>';
editor.replaceSelection(mt); editor.replaceSelection(mt);
@ -363,7 +365,15 @@ define([
}); });
framework.setFileExporter(CodeMirror.getContentExtension, CodeMirror.fileExporter); 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) { framework.setNormalizer(function (c) {
return { return {

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

@ -7,6 +7,7 @@ define(function () {
fileHashKey: 'FS_hash', fileHashKey: 'FS_hash',
// sessionStorage // sessionStorage
newPadPathKey: "newPadPath", newPadPathKey: "newPadPath",
newPadFileData: "newPadFileData",
// Store // Store
displayNameKey: 'cryptpad.username', displayNameKey: 'cryptpad.username',
oldStorageKey: 'CryptPad_RECENTPADS', 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 * spinner
@ -773,6 +783,7 @@ define([
var icon = AppConfig.applicationsIcon[type]; var icon = AppConfig.applicationsIcon[type];
var font = icon.indexOf('cptools') === 0 ? 'cptools' : 'fa'; var font = icon.indexOf('cptools') === 0 ? 'cptools' : 'fa';
if (type === 'fileupload') { type = 'file'; } if (type === 'fileupload') { type = 'file'; }
if (type === 'folderupload') { type = 'file'; }
var appClass = ' cp-icon cp-icon-color-'+type; var appClass = ' cp-icon cp-icon-color-'+type;
$icon = $('<span>', {'class': font + ' ' + icon + appClass}); $icon = $('<span>', {'class': font + ' ' + icon + appClass});
} }

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

@ -15,6 +15,7 @@ define([
}; };
var supportedTypes = [ var supportedTypes = [
'text/plain',
'image/png', 'image/png',
'image/jpeg', 'image/jpeg',
'image/jpg', 'image/jpg',
@ -23,7 +24,12 @@ define([
'application/pdf' '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 supportedTypes.some(function (t) {
return type.indexOf(t) !== -1; 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) { Thumb.fromBlob = function (blob, cb) {
if (blob.type.indexOf('video/') !== -1) { if (blob.type.indexOf('video/') !== -1) {
return void Thumb.fromVideoBlob(blob, cb); return void Thumb.fromVideoBlob(blob, cb);
@ -171,6 +197,9 @@ define([
if (blob.type.indexOf('application/pdf') !== -1) { if (blob.type.indexOf('application/pdf') !== -1) {
return void Thumb.fromPdfBlob(blob, cb); return void Thumb.fromPdfBlob(blob, cb);
} }
if (Util.isPlainTextFile(blob.type, blob.name)) {
return void Thumb.fromPlainTextBlob(blob, cb);
}
Thumb.fromImageBlob(blob, cb); Thumb.fromImageBlob(blob, cb);
}; };
@ -230,9 +259,15 @@ define([
if (!Visible.currently()) { to = window.setTimeout(interval, Thumb.UPDATE_FIRST); } if (!Visible.currently()) { to = window.setTimeout(interval, Thumb.UPDATE_FIRST); }
}; };
var addThumbnail = function (err, thumb, $span, cb) { 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(); 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.find('.cp-icon').hide();
$span.prepend(img); $span.prepend(img);
cb($(img)); cb($(img));
@ -254,9 +289,11 @@ define([
var parsed = Hash.parsePadUrl(href); var parsed = Hash.parsePadUrl(href);
var k = getKey(parsed.type, channel); var k = getKey(parsed.type, channel);
var whenNewThumb = function () { var whenNewThumb = function () {
var privateData = common.getMetadataMgr().getPrivateData();
var fileHost = privateData.fileHost || privateData.origin;
var secret = Hash.getSecrets('file', parsed.hash, password); var secret = Hash.getSecrets('file', parsed.hash, password);
var hexFileName = secret.channel; var hexFileName = secret.channel;
var src = Hash.getBlobPathFromHex(hexFileName); var src = fileHost + Hash.getBlobPathFromHex(hexFileName);
var key = secret.keys && secret.keys.cryptKey; var key = secret.keys && secret.keys.cryptKey;
FileCrypto.fetchDecryptedMetadata(src, key, function (e, metadata) { FileCrypto.fetchDecryptedMetadata(src, key, function (e, metadata) {
if (e) { if (e) {

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

@ -319,6 +319,36 @@ define([], function () {
return window.innerHeight < 800 || window.innerWidth < 800; 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; return Util;
}); });
}(self)); }(self));

@ -16,10 +16,10 @@ define([
var disconnect = Util.find(S, ['network', 'disconnect']); var disconnect = Util.find(S, ['network', 'disconnect']);
if (typeof(disconnect) === 'function') { disconnect(); } if (typeof(disconnect) === 'function') { disconnect(); }
} }
if (S.leave) { if (S.realtime && S.realtime.stop) {
try { try {
S.leave(); S.realtime.stop();
} catch (e) { console.log(e); } } catch (e) { console.error(e); }
} }
var abort = Util.find(S, ['session', 'realtime', 'abort']); var abort = Util.find(S, ['session', 'realtime', 'abort']);
if (typeof(abort) === 'function') { if (typeof(abort) === 'function') {
@ -52,11 +52,12 @@ define([
Object.keys(b).forEach(function (k) { a[k] = b[k]; }); 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') { if (typeof(cb) !== 'function') {
throw new Error('Cryptget expects a callback'); throw new Error('Cryptget expects a callback');
} }
opt = opt || {}; opt = opt || {};
progress = progress || function () {};
var config = makeConfig(hash, opt); var config = makeConfig(hash, opt);
var Session = { cb: cb, hasNetwork: Boolean(opt.network) }; var Session = { cb: cb, hasNetwork: Boolean(opt.network) };
@ -64,7 +65,7 @@ define([
config.onReady = function (info) { config.onReady = function (info) {
var rt = Session.session = info.realtime; var rt = Session.session = info.realtime;
Session.network = info.network; Session.network = info.network;
Session.leave = info.leave; progress(1);
finish(Session, void 0, rt.getUserDoc()); finish(Session, void 0, rt.getUserDoc());
}; };
@ -72,6 +73,16 @@ define([
finish(Session, info.error); 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); overwrite(config, opt);
Session.realtime = CPNetflux.start(config); Session.realtime = CPNetflux.start(config);

@ -254,8 +254,12 @@ define([
common.clearOwnedChannel = function (channel, cb) { common.clearOwnedChannel = function (channel, cb) {
postMessage("CLEAR_OWNED_CHANNEL", channel, cb); postMessage("CLEAR_OWNED_CHANNEL", channel, cb);
}; };
common.removeOwnedChannel = function (channel, cb) { // "force" allows you to delete your drive ID
postMessage("REMOVE_OWNED_CHANNEL", channel, cb); common.removeOwnedChannel = function (channel, cb, force) {
postMessage("REMOVE_OWNED_CHANNEL", {
channel: channel,
force: force
}, cb);
}; };
common.getDeletedPads = function (data, 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 // Forget button
common.moveToTrash = function (cb, href) { common.moveToTrash = function (cb, href) {
href = href || window.location.href; href = href || window.location.href;
@ -693,6 +758,17 @@ define([
pad.onConnectEvent = Util.mkEvent(); pad.onConnectEvent = Util.mkEvent();
pad.onErrorEvent = 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) { common.changePadPassword = function (Crypt, href, newPassword, edPublic, cb) {
if (!href) { return void cb({ error: 'EINVAL_HREF' }); } if (!href) { return void cb({ error: 'EINVAL_HREF' }); }
var parsed = Hash.parsePadUrl(href); var parsed = Hash.parsePadUrl(href);
@ -943,7 +1019,7 @@ define([
common.logoutFromAll(waitFor(function () { common.logoutFromAll(waitFor(function () {
postMessage("DISCONNECT"); postMessage("DISCONNECT");
})); }));
})); }), true);
} }
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
if (!oldIsOwned) { if (!oldIsOwned) {
@ -1263,6 +1339,12 @@ define([
messenger: rdyCfg.messenger, // Boolean messenger: rdyCfg.messenger, // Boolean
driveEvents: rdyCfg.driveEvents // 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]) { if (sessionStorage[Constants.newPadPathKey]) {
common.initialPath = sessionStorage[Constants.newPadPathKey]; common.initialPath = sessionStorage[Constants.newPadPathKey];
delete sessionStorage[Constants.newPadPathKey]; delete sessionStorage[Constants.newPadPathKey];
@ -1287,10 +1369,12 @@ define([
errEv.preventDefault(); errEv.preventDefault();
errEv.stopPropagation(); errEv.stopPropagation();
noWorker = true; noWorker = true;
worker.terminate();
w(); w();
}; };
worker.onmessage = function (ev) { worker.onmessage = function (ev) {
if (ev.data === "OK") { if (ev.data === "OK") {
worker.terminate();
w(); 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([ define([
'jquery', 'jquery',
'/api/config',
'/bower_components/marked/marked.min.js', '/bower_components/marked/marked.min.js',
'/common/common-hash.js', '/common/common-hash.js',
'/common/common-util.js', '/common/common-util.js',
@ -10,11 +11,12 @@ define([
'/bower_components/diff-dom/diffDOM.js', '/bower_components/diff-dom/diffDOM.js',
'/bower_components/tweetnacl/nacl-fast.min.js', '/bower_components/tweetnacl/nacl-fast.min.js',
'css!/common/highlight/styles/github.css' '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 DiffMd = {};
var DiffDOM = window.diffDOM; var DiffDOM = window.diffDOM;
var renderer = new Marked.Renderer(); var renderer = new Marked.Renderer();
var restrictedRenderer = new Marked.Renderer();
var Mermaid = { var Mermaid = {
init: function () {} init: function () {}
@ -61,13 +63,18 @@ define([
return h('div.cp-md-toc', content).outerHTML; 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, { var r = Marked(md, {
sanitize: sanitize sanitize: sanitize
}); });
// Add Table of Content // 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 = []; toc = [];
return r; return r;
@ -83,12 +90,7 @@ define([
return defaultCode.apply(renderer, arguments); return defaultCode.apply(renderer, arguments);
} }
}; };
restrictedRenderer.code = renderer.code;
var stripTags = function (text) {
var div = document.createElement("div");
div.innerHTML = text;
return div.innerText;
};
renderer.heading = function (text, level) { renderer.heading = function (text, level) {
var i = 0; var i = 0;
@ -105,10 +107,13 @@ define([
toc.push({ toc.push({
level: level, level: level,
id: id, id: id,
title: stripTags(text) title: Util.stripTags(text)
}); });
return "<h" + level + " id=\"" + id + "\"><a href=\"#" + id + "\" class=\"anchor\"></a>" + text + "</h" + level + ">"; return "<h" + level + " id=\"" + id + "\"><a href=\"#" + id + "\" class=\"anchor\"></a>" + text + "</h" + level + ">";
}; };
restrictedRenderer.heading = function (text) {
return text;
};
// Tasks list // Tasks list
var checkedTaskItemPtn = /^\s*(<p>)?\[[xX]\](<\/p>)?\s*/; var checkedTaskItemPtn = /^\s*(<p>)?\[[xX]\](<\/p>)?\s*/;
@ -138,6 +143,13 @@ define([
var cls = (isCheckedTaskItem || isUncheckedTaskItem || hasBogusInput) ? ' class="todo-list-item"' : ''; var cls = (isCheckedTaskItem || isUncheckedTaskItem || hasBogusInput) ? ' class="todo-list-item"' : '';
return '<li'+ cls + '>' + text + '</li>\n'; 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) { renderer.image = function (href, title, text) {
if (href.slice(0,6) === '/file/') { if (href.slice(0,6) === '/file/') {
// DEPRECATED // DEPRECATED
@ -146,7 +158,7 @@ define([
console.log('DEPRECATED: mediatag using markdown syntax!'); console.log('DEPRECATED: mediatag using markdown syntax!');
var parsed = Hash.parsePadUrl(href); var parsed = Hash.parsePadUrl(href);
var secret = Hash.getSecrets('file', parsed.hash); 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 key = Hash.encodeBase64(secret.keys.cryptKey);
var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + key + '"></media-tag>'; var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + key + '"></media-tag>';
if (mediaMap[src]) { if (mediaMap[src]) {
@ -162,12 +174,19 @@ define([
out += this.options.xhtml ? '/>' : '>'; out += this.options.xhtml ? '/>' : '>';
return out; 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) { renderer.paragraph = function (p) {
if (p === '[TOC]') { if (p === '[TOC]') {
return '<p><div class="cp-md-toc"></div></p>'; 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; var MutationObserver = window.MutationObserver;

@ -1,11 +1,13 @@
define([ define([
'/common/cryptget.js', '/common/cryptget.js',
'/file/file-crypto.js',
'/common/common-hash.js', '/common/common-hash.js',
'/common/sframe-common-file.js', '/common/common-util.js',
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
'/bower_components/saferphore/index.js', '/bower_components/saferphore/index.js',
'/bower_components/jszip/dist/jszip.min.js', '/bower_components/jszip/dist/jszip.min.js',
], function (Crypt, Hash, SFCFile, nThen, Saferphore, JsZip) { ], function (Crypt, FileCrypto, Hash, Util, nThen, Saferphore, JsZip) {
var saveAs = window.saveAs;
var sanitize = function (str) { var sanitize = function (str) {
return str.replace(/[\\/?%*:|"<>]/gi, '_')/*.toLowerCase()*/; return str.replace(/[\\/?%*:|"<>]/gi, '_')/*.toLowerCase()*/;
@ -34,7 +36,7 @@ define([
var path = '/' + type + '/export.js'; var path = '/' + type + '/export.js';
require([path], function (Exporter) { require([path], function (Exporter) {
Exporter.main(json, function (data) { Exporter.main(json, function (data) {
result.ext = '.' + Exporter.type; result.ext = Exporter.ext || '';
result.data = data; result.data = data;
cb(result); 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 // 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. // or fetch&decrypt it if it's a file.
var addFile = function (ctx, zip, fData, existingNames) { var addFile = function (ctx, zip, fData, existingNames) {
@ -126,7 +209,7 @@ define([
// Files (mediatags...) // Files (mediatags...)
var todoFile = function () { var todoFile = function () {
var it; var it;
var dl = SFCFile.downloadFile(fData, function (err, res) { var dl = _downloadFile(ctx, fData, function (err, res) {
if (it) { clearInterval(it); } if (it) { clearInterval(it); }
if (err) { return void error(err); } if (err) { return void error(err); }
var opts = { var opts = {
@ -163,12 +246,12 @@ define([
var existingNames = []; var existingNames = [];
Object.keys(root).forEach(function (k) { Object.keys(root).forEach(function (k) {
var el = root[k]; var el = root[k];
if (typeof el === "object") { if (typeof el === "object" && el.metadata !== true) { // if folder
var fName = getUnique(sanitize(k), '', existingNames); var fName = getUnique(sanitize(k), '', existingNames);
existingNames.push(fName.toLowerCase()); existingNames.push(fName.toLowerCase());
return void makeFolder(ctx, el, zip.folder(fName), fd); 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 sfData = ctx.sf[el].metadata;
var sfName = getUnique(sanitize(sfData.title || 'Folder'), '', existingNames); var sfName = getUnique(sanitize(sfData.title || 'Folder'), '', existingNames);
existingNames.push(sfName.toLowerCase()); existingNames.push(sfName.toLowerCase());
@ -183,12 +266,14 @@ define([
}; };
// Main function. Create the empty zip and fill it starting from drive.root // Main function. Create the empty zip and fill it starting from drive.root
var create = function (data, getPad, cb, progress) { var create = function (data, getPad, fileHost, cb, progress) {
if (!data || !data.uo || !data.uo.drive) { return void cb('EEMPTY'); } if (!data || !data.uo || !data.uo.drive) { return void cb('EEMPTY'); }
var sem = Saferphore.create(5); var sem = Saferphore.create(5);
var ctx = { var ctx = {
fileHost: fileHost,
get: getPad, get: getPad,
data: data.uo.drive, data: data.uo.drive,
folder: data.folder || ctx.data.root,
sf: data.sf, sf: data.sf,
zip: new JsZip(), zip: new JsZip(),
errors: [], errors: [],
@ -197,11 +282,12 @@ define([
max: 0, max: 0,
done: 0 done: 0
}; };
var filesData = data.sharedFolderId && ctx.sf[data.sharedFolderId] ? ctx.sf[data.sharedFolderId].filesData : ctx.data.filesData;
progress('reading', -1); progress('reading', -1);
nThen(function (waitFor) { nThen(function (waitFor) {
ctx.waitFor = waitFor; ctx.waitFor = waitFor;
var zipRoot = ctx.zip.folder('Root'); var zipRoot = ctx.zip.folder('Root');
makeFolder(ctx, ctx.data.root, zipRoot, ctx.data.filesData); makeFolder(ctx, ctx.folder, zipRoot, filesData);
progress('download', {}); progress('download', {});
}).nThen(function () { }).nThen(function () {
console.log(ctx.zip); 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 { 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 // Default config, can be overriden per media-tag call
var config = { var config = {
allowed: [ allowed: [
'text/plain',
'image/png', 'image/png',
'image/jpeg', 'image/jpeg',
'image/jpg', 'image/jpg',
@ -53,6 +66,24 @@
text: "Download" text: "Download"
}, },
Plugins: { 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) { image: function (metadata, url, content, cfg, cb) {
var img = document.createElement('img'); var img = document.createElement('img');
img.setAttribute('src', url); img.setAttribute('src', url);
@ -271,6 +302,9 @@
var blob = decrypted.content; var blob = decrypted.content;
var mediaType = getType(mediaObject, metadata, cfg); var mediaType = getType(mediaObject, metadata, cfg);
if (isplainTextFile(metadata)) {
mediaType = "text";
}
if (mediaType === 'application') { if (mediaType === 'application') {
mediaType = mediaObject.extension; mediaType = mediaObject.extension;

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

@ -151,7 +151,7 @@ define([
}); });
try { try {
var $d = $(d); 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"); $d.addClass("cp-app-contacts-content");
// override link clicking, because we're in an iframe // override link clicking, because we're in an iframe

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

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

@ -2,9 +2,13 @@ define([
'jquery', 'jquery',
'/common/hyperscript.js', '/common/hyperscript.js',
'/common/common-hash.js', '/common/common-hash.js',
'/common/common-interface.js',
'/common/common-ui-elements.js', '/common/common-ui-elements.js',
'/common/common-util.js',
'/common/common-constants.js',
'/customize/messages.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 = {}; var handlers = {};
@ -25,10 +29,11 @@ define([
handlers['FRIEND_REQUEST'] = function (common, data) { handlers['FRIEND_REQUEST'] = function (common, data) {
var content = data.content; var content = data.content;
var msg = content.msg; var msg = content.msg;
var name = Util.fixHTML(msg.content.displayName) || Messages.anonymous;
// Display the notification // Display the notification
content.getFormatText = function () { content.getFormatText = function () {
return Messages._getKey('friendRequest_notification', [msg.content.displayName || Messages.anonymous]); return Messages._getKey('friendRequest_notification', [name]);
}; };
// Check authenticity // Check authenticity
@ -46,8 +51,9 @@ define([
handlers['FRIEND_REQUEST_ACCEPTED'] = function (common, data) { handlers['FRIEND_REQUEST_ACCEPTED'] = function (common, data) {
var content = data.content; var content = data.content;
var msg = content.msg; var msg = content.msg;
var name = Util.fixHTML(msg.content.name) || Messages.anonymous;
content.getFormatText = function () { content.getFormatText = function () {
return Messages._getKey('friendRequest_accepted', [msg.content.name || Messages.anonymous]); return Messages._getKey('friendRequest_accepted', [name]);
}; };
if (!content.archived) { if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data); content.dismissHandler = defaultDismiss(common, data);
@ -57,8 +63,9 @@ define([
handlers['FRIEND_REQUEST_DECLINED'] = function (common, data) { handlers['FRIEND_REQUEST_DECLINED'] = function (common, data) {
var content = data.content; var content = data.content;
var msg = content.msg; var msg = content.msg;
var name = Util.fixHTML(msg.content.name) || Messages.anonymous;
content.getFormatText = function () { content.getFormatText = function () {
return Messages._getKey('friendRequest_declined', [msg.content.name || Messages.anonymous]); return Messages._getKey('friendRequest_declined', [name]);
}; };
if (!content.archived) { if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data); content.dismissHandler = defaultDismiss(common, data);
@ -74,22 +81,137 @@ define([
var key = type === 'drive' ? 'notification_folderShared' : var key = type === 'drive' ? 'notification_folderShared' :
(type === 'file' ? 'notification_fileShared' : (type === 'file' ? 'notification_fileShared' :
'notification_padShared'); 'notification_padShared');
var name = Util.fixHTML(msg.content.name) || Messages.anonymous;
var title = Util.fixHTML(msg.content.title);
content.getFormatText = function () { content.getFormatText = function () {
return Messages._getKey(key, [msg.content.name || Messages.anonymous, msg.content.title]); return Messages._getKey(key, [name, title]);
}; };
content.handler = function () { content.handler = function () {
var todo = function () { common.openURL(msg.content.href); }; var todo = function () {
if (!msg.content.password) { return void todo(); } common.openURL(msg.content.href);
common.getSframeChannel().query('Q_SESSIONSTORAGE_PUT', { defaultDismiss(common, data)();
key: 'newPadPassword', };
value: msg.content.password nThen(function (waitFor) {
}, todo); 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) { if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data); 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 { return {
add: function (common, data) { add: function (common, data) {
var type = data.content.msg.type; var type = data.content.msg.type;

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

@ -34,7 +34,7 @@ define([
var sendDriveEvent = function () {}; var sendDriveEvent = function () {};
var registerProxyEvents = function () {}; var registerProxyEvents = function () {};
var storeHash; var storeHash, storeChannel;
var store = window.CryptPad_AsyncStore = { var store = window.CryptPad_AsyncStore = {
modules: {} modules: {}
@ -239,6 +239,20 @@ define([
Store.removeOwnedChannel = function (clientId, data, cb) { Store.removeOwnedChannel = function (clientId, data, cb) {
if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } 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) { store.rpc.removeOwnedChannel(data, function (err) {
cb({error:err}); cb({error:err});
}); });
@ -573,7 +587,10 @@ define([
})); }));
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
// Delete Drive // Delete Drive
Store.removeOwnedChannel(clientId, secret.channel, waitFor()); Store.removeOwnedChannel(clientId, {
channel: secret.channel,
force: true
}, waitFor());
}).nThen(function () { }).nThen(function () {
store.network.disconnect(); store.network.disconnect();
cb({ cb({
@ -721,7 +738,10 @@ define([
var object = getAttributeObject(data.attr); var object = getAttributeObject(data.attr);
object.obj[object.key] = data.value; object.obj[object.key] = data.value;
} catch (e) { return void cb({error: e}); } } catch (e) { return void cb({error: e}); }
onSync(cb); onSync(function () {
cb();
broadcast([], "UPDATE_METADATA");
});
}; };
Store.getAttribute = function (clientId, data, cb) { Store.getAttribute = function (clientId, data, cb) {
var object; var object;
@ -786,6 +806,7 @@ define([
var h = p.hashData; var h = p.hashData;
if (AppConfig.disableAnonymousStore && !store.loggedIn) { return void cb(); } if (AppConfig.disableAnonymousStore && !store.loggedIn) { return void cb(); }
if (p.type === "debug") { return void cb(); }
var channelData = Store.channels && Store.channels[channel]; var channelData = Store.channels && Store.channels[channel];
@ -1191,10 +1212,7 @@ define([
}, },
noChainPad: true, noChainPad: true,
channel: data.channel, channel: data.channel,
validateKey: data.validateKey, metadata: data.metadata,
owners: data.owners,
password: data.password,
expire: data.expire,
network: store.network, network: store.network,
//readOnly: data.readOnly, //readOnly: data.readOnly,
onConnect: function (wc, sendMessage) { onConnect: function (wc, sendMessage) {
@ -1244,6 +1262,128 @@ define([
channel.sendMessage(msg, clientId, cb); 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 // GET_FULL_HISTORY from sframe-common-outer
Store.getFullHistory = function (clientId, data, cb) { Store.getFullHistory = function (clientId, data, cb) {
var network = store.network; var network = store.network;
@ -1350,14 +1490,16 @@ define([
websocketURL: NetConfig.getWebsocketURL(), websocketURL: NetConfig.getWebsocketURL(),
channel: secret.channel, channel: secret.channel,
readOnly: false, readOnly: false,
validateKey: secret.keys.validateKey || undefined,
crypto: Crypto.createEncryptor(secret.keys), crypto: Crypto.createEncryptor(secret.keys),
userName: 'sharedFolder', userName: 'sharedFolder',
logLevel: 1, logLevel: 1,
ChainPad: ChainPad, ChainPad: ChainPad,
classic: true, classic: true,
network: store.network, network: store.network,
owners: owners metadata: {
validateKey: secret.keys.validateKey || undefined,
owners: owners
}
}; };
var rt = Listmap.create(listmapConfig); var rt = Listmap.create(listmapConfig);
store.sharedFolders[id] = rt; store.sharedFolders[id] = rt;
@ -1824,6 +1966,7 @@ define([
} }
// No password for drive // No password for drive
var secret = Hash.getSecrets('drive', hash); var secret = Hash.getSecrets('drive', hash);
storeChannel = secret.channel;
var listmapConfig = { var listmapConfig = {
data: {}, data: {},
websocketURL: NetConfig.getWebsocketURL(), 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 { return {
add: function (ctx, box, data, cb) { add: function (ctx, box, data, cb) {
/** /**

@ -92,9 +92,11 @@ define([
var hk = network.historyKeeper; var hk = network.historyKeeper;
var cfg = { var cfg = {
validateKey: obj.validateKey, validateKey: obj.validateKey,
lastKnownHash: chan.lastKnownHash || chan.lastCpHash, metadata: {
owners: obj.owners, lastKnownHash: chan.lastKnownHash || chan.lastCpHash,
expire: obj.expire owners: obj.owners,
expire: obj.expire
}
}; };
var msg = ['GET_HISTORY', wc.id, cfg]; var msg = ['GET_HISTORY', wc.id, cfg];
// Add the validateKey if we are the channel creator and we have a validateKey // 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_FULL_HISTORY: Store.getFullHistory,
GET_HISTORY_RANGE: Store.getHistoryRange, GET_HISTORY_RANGE: Store.getHistoryRange,
IS_NEW_CHANNEL: Store.isNewChannel, IS_NEW_CHANNEL: Store.isNewChannel,
REQUEST_PAD_ACCESS: Store.requestPadAccess,
GIVE_PAD_ACCESS: Store.givePadAccess,
GET_PAD_METADATA: Store.getPadMetadata,
// Drive // Drive
DRIVE_USEROBJECT: Store.userObjectCommand, DRIVE_USEROBJECT: Store.userObjectCommand,
// Settings, // Settings,

@ -65,6 +65,7 @@ define([
if (box) { if (box) {
actual += box.length; actual += box.length;
var progressValue = (actual / estimate * 100); var progressValue = (actual / estimate * 100);
progressValue = Math.min(progressValue, 100);
updateProgress(progressValue); updateProgress(progressValue);
return void sendChunk(box, function (e) { 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 // If the type is a query, your handler will be invoked with a reply function that takes
// one argument (the content to reply with). // one argument (the content to reply with).
chan.on = function (queryType, handler, quiet) { chan.on = function (queryType, handler, quiet) {
(handlers[queryType] = handlers[queryType] || []).push(function (data, msg) { var h = function (data, msg) {
handler(data.content, function (replyContent) { handler(data.content, function (replyContent) {
postMsg(JSON.stringify({ postMsg(JSON.stringify({
txid: data.txid, txid: data.txid,
content: replyContent content: replyContent
})); }));
}, msg); }, msg);
}); };
(handlers[queryType] = handlers[queryType] || []).push(h);
if (!quiet) { if (!quiet) {
event('EV_REGISTER_HANDLER', queryType); 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 // If a particular handler is registered, call the callback immediately, otherwise it will be called

@ -461,6 +461,102 @@ define([
cb(id); 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 // Delete permanently some pads or folders
var _delete = function (Env, data, cb) { var _delete = function (Env, data, cb) {
data = data || {}; data = data || {};
@ -598,6 +694,8 @@ define([
_addFolder(Env, data, cb); break; _addFolder(Env, data, cb); break;
case 'addSharedFolder': case 'addSharedFolder':
_addSharedFolder(Env, data, cb); break; _addSharedFolder(Env, data, cb); break;
case 'convertFolderToSharedFolder':
_convertFolderToSharedFolder(Env, data, cb); break;
case 'delete': case 'delete':
_delete(Env, data, cb); break; _delete(Env, data, cb); break;
case 'emptyTrash': case 'emptyTrash':
@ -914,6 +1012,16 @@ define([
} }
}, cb); }, 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) { var deleteInner = function (Env, paths, cb) {
return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", { return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", {
cmd: "delete", cmd: "delete",
@ -1074,6 +1182,9 @@ define([
} }
return Env.user.userObject.hasSubfolder(el, trashRoot); return Env.user.userObject.hasSubfolder(el, trashRoot);
}; };
var hasSubSharedFolder = function (Env, el) {
return Env.user.userObject.hasSubSharedFolder(el);
};
var hasFile = function (Env, el, trashRoot) { var hasFile = function (Env, el, trashRoot) {
if (Env.folders[el]) { if (Env.folders[el]) {
var uo = Env.folders[el].userObject; var uo = Env.folders[el].userObject;
@ -1113,6 +1224,7 @@ define([
emptyTrash: callWithEnv(emptyTrashInner), emptyTrash: callWithEnv(emptyTrashInner),
addFolder: callWithEnv(addFolderInner), addFolder: callWithEnv(addFolderInner),
addSharedFolder: callWithEnv(addSharedFolderInner), addSharedFolder: callWithEnv(addSharedFolderInner),
convertFolderToSharedFolder: callWithEnv(convertFolderToSharedFolderInner),
delete: callWithEnv(deleteInner), delete: callWithEnv(deleteInner),
restore: callWithEnv(restoreInner), restore: callWithEnv(restoreInner),
setFolderData: callWithEnv(setFolderDataInner), setFolderData: callWithEnv(setFolderDataInner),
@ -1144,6 +1256,7 @@ define([
isInTrashRoot: callWithEnv(isInTrashRoot), isInTrashRoot: callWithEnv(isInTrashRoot),
comparePath: callWithEnv(comparePath), comparePath: callWithEnv(comparePath),
hasSubfolder: callWithEnv(hasSubfolder), hasSubfolder: callWithEnv(hasSubfolder),
hasSubSharedFolder: callWithEnv(hasSubSharedFolder),
hasFile: callWithEnv(hasFile), hasFile: callWithEnv(hasFile),
// Data // Data
user: Env.user, user: Env.user,

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

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

@ -39,7 +39,8 @@ define([
}; };
module.getContentExtension = function (mode) { 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) { module.fileExporter = function (content) {
return new Blob([ content ], { type: 'text/plain;charset=utf-8' }); return new Blob([ content ], { type: 'text/plain;charset=utf-8' });
@ -61,6 +62,7 @@ define([
}); });
editor._noCursorUpdate = false; editor._noCursorUpdate = false;
editor.state.focused = true;
if(selects[0] === selects[1]) { if(selects[0] === selects[1]) {
editor.setCursor(posToCursor(selects[0], remoteDoc)); editor.setCursor(posToCursor(selects[0], remoteDoc));
} }
@ -98,9 +100,17 @@ define([
// lines beginning with a hash are potentially valuable // lines beginning with a hash are potentially valuable
// works for markdown, python, bash, etc. // works for markdown, python, bash, etc.
var hash = /^#+(.*?)$/; var hash = /^#+(.*?)$/;
var hashAndLink = /^#+\s*\[(.*?)\]\(.*\)\s*$/;
if (hash.test(line)) { 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) { line.replace(hash, function (a, one) {
text = one; text = Util.stripTags(one);
}); });
return true; return true;
} }
@ -135,6 +145,7 @@ define([
}; };
var editor = exp.editor = CMeditor.fromTextArea($textarea[0], { var editor = exp.editor = CMeditor.fromTextArea($textarea[0], {
allowDropFileTypes: [],
lineNumbers: true, lineNumbers: true,
lineWrapping: true, lineWrapping: true,
autoCloseBrackets: true, autoCloseBrackets: true,
@ -323,7 +334,7 @@ define([
var mode; var mode;
if (!mime) { if (!mime) {
var ext = /.+\.([^.]+)$/.exec(file.name); var ext = /.+\.([^.]+)$/.exec(file.name);
if (ext[1]) { if (ext && ext[1]) {
mode = CMeditor.findModeByExtension(ext[1]); mode = CMeditor.findModeByExtension(ext[1]);
mode = mode && mode.mode || null; mode = mode && mode.mode || null;
} }
@ -339,7 +350,8 @@ define([
exp.setMode('text'); exp.setMode('text');
$toolbarContainer.find('#language-mode').val('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) { exp.setValueAndCursor = function (oldDoc, remoteDoc) {
@ -366,40 +378,39 @@ define([
return { content: canonicalize(editor.getValue()) }; 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) { exp.mkIndentSettings = function (metadataMgr) {
var setIndentation = function (units, useTabs, fontSize, spellcheck) { var setIndentation = function (units, useTabs, fontSize, spellcheck) {
if (typeof(units) !== 'number') { return; } if (typeof(units) !== 'number') { return; }
var doc = editor.getDoc();
editor.setOption('indentUnit', units); editor.setOption('indentUnit', units);
editor.setOption('tabSize', units); editor.setOption('tabSize', units);
editor.setOption('indentWithTabs', useTabs); editor.setOption('indentWithTabs', useTabs);
editor.setOption('spellcheck', spellcheck); editor.setOption('spellcheck', spellcheck);
if (!useTabs) { editor.setOption("extraKeys", {
editor.setOption("extraKeys", { Tab: function() {
Tab: function() { if (doc.somethingSelected()) {
editor.replaceSelection(Array(units + 1).join(" ")); editor.execCommand("indentMore");
} }
}); else {
} else { if (!useTabs) { editor.execCommand("insertSoftTab"); }
editor.setOption("extraKeys", { else { editor.execCommand("insertTab"); }
Tab: undefined, }
}); },
} "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'); $('.CodeMirror').css('font-size', fontSize+'px');
}; };

@ -1,6 +1,7 @@
define([ define([
'jquery', 'jquery',
'/file/file-crypto.js', '/file/file-crypto.js',
'/common/make-backup.js',
'/common/common-thumbnail.js', '/common/common-thumbnail.js',
'/common/common-interface.js', '/common/common-interface.js',
'/common/common-ui-elements.js', '/common/common-ui-elements.js',
@ -11,9 +12,8 @@ define([
'/bower_components/file-saver/FileSaver.min.js', '/bower_components/file-saver/FileSaver.min.js',
'/bower_components/tweetnacl/nacl-fast.min.js', '/bower_components/tweetnacl/nacl-fast.min.js',
], function ($, FileCrypto, Thumb, UI, UIElements, Util, Hash, h, Messages) { ], function ($, FileCrypto, MakeBackup, Thumb, UI, UIElements, Util, Hash, h, Messages) {
var Nacl = window.nacl; var Nacl = window.nacl;
var saveAs = window.saveAs;
var module = {}; var module = {};
var blobToArrayBuffer = function (blob, cb) { var blobToArrayBuffer = function (blob, cb) {
@ -42,16 +42,19 @@ define([
return 'cp-fileupload-element-' + String(Math.random()).substring(2); 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 $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) { 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; return File.$container;
}; };
@ -100,16 +103,19 @@ define([
var $row = $table.find('tr[id="'+id+'"]'); 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 $pv = $row.find('.cp-fileupload-table-progress-value');
var $pb = $row.find('.cp-fileupload-table-progress-container'); var $pb = $row.find('.cp-fileupload-table-progressbar');
var $pc = $row.find('.cp-fileupload-table-progress');
var $link = $row.find('.cp-fileupload-table-link'); 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) { updateProgress = function (progressValue) {
$pv.text(Math.round(progressValue*100)/100 + '%'); $pv.text(Math.round(progressValue * 100) / 100 + '%');
$pb.css({ $pb.css({
width: (progressValue/100)*$pc.width()+'px' width: progressValue + '%'
}); });
}; };
@ -179,8 +185,14 @@ define([
clearTimeout(queue.to); clearTimeout(queue.to);
queue.to = window.setTimeout(function () { queue.to = window.setTimeout(function () {
if (config.keepTable) { return; } if (config.keepTable) { return; }
File.$container.fadeOut(); // don't hide panel if mouse over
}, 3000); if (File.$container.is(":hover")) {
File.$container.one("mouseleave", function () { File.$container.fadeOut(); });
}
else {
File.$container.fadeOut();
}
}, 60000);
return; return;
} }
if (queue.inProgress) { return; } if (queue.inProgress) { return; }
@ -199,8 +211,9 @@ define([
window.setTimeout(function () { $table.show(); }); window.setTimeout(function () { $table.show(); });
var estimate = obj.dl ? obj.size : FileCrypto.computeEncryptedSize(obj.blob.byteLength, obj.metadata); var estimate = obj.dl ? obj.size : FileCrypto.computeEncryptedSize(obj.blob.byteLength, obj.metadata);
var $progressBar = $('<div>', {'class':'cp-fileupload-table-progress-container'}); var $progressContainer = $('<div>', {'class':'cp-fileupload-table-progress-container'});
var $progressValue = $('<span>', {'class':'cp-fileupload-table-progress-value'}).text(Messages.upload_pending); $('<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 $tr = $('<tr>', {id: id}).appendTo($table);
var $lines = $table.find('tr[id]'); 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 () { 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; }); queue.queue = queue.queue.filter(function (el) { return el.id !== id; });
$cancel.remove(); $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); $tr.find('.cp-fileupload-table-progress-value').text(Messages.upload_cancelled);
}); });
var $link = $('<a>', { var $link = $('<a>', {
'class': 'cp-fileupload-table-link', 'class': 'cp-fileupload-table-link',
'rel': 'noopener noreferrer' '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); $('<td>').append($link).appendTo($tr);
// size
$('<td>').text(prettySize(estimate)).appendTo($tr); $('<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); $('<td>', {'class': 'cp-fileupload-table-cancel'}).append($cancel).appendTo($tr);
queue.next(); queue.next();
@ -234,37 +256,32 @@ define([
owned: true, owned: true,
store: true store: true
}; };
var fileUploadModal = function (file, cb) { var createHelper = function (href, text) {
var extIdx = file.name.lastIndexOf('.'); return UI.createHelper(origin + href, text);
var name = extIdx !== -1 ? file.name.slice(0,extIdx) : file.name; };
var ext = extIdx !== -1 ? file.name.slice(extIdx) : ""; var createManualStore = function (isFolderUpload) {
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 privateData = common.getMetadataMgr().getPrivateData(); var privateData = common.getMetadataMgr().getPrivateData();
var autoStore = Util.find(privateData, ['settings', 'general', 'autostore']) || 0; var autoStore = Util.find(privateData, ['settings', 'general', 'autostore']) || 0;
var initialState = modalState.owned || modalState.store; var initialState = modalState.owned || modalState.store;
var initialDisabled = modalState.owned ? { disabled: true } : {}; var initialDisabled = modalState.owned ? { disabled: true } : {};
var manualStore = autoStore === 1 ? undefined : var manualStore = autoStore === 1 ? undefined :
UI.createCheckbox('cp-upload-store', Messages.autostore_forceSave, initialState, { UI.createCheckbox('cp-upload-store', isFolderUpload ? (Messages.uploadFolder_modal_forceSave) : Messages.autostore_forceSave, initialState, {
input: initialDisabled 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 // Ask for name, password and owner
var content = h('div', [ var content = h('div', [
h('h4', Messages.upload_modal_title), h('h4', Messages.upload_modal_title),
UIElements.setHTML(h('label', {for: 'cp-upload-name'}), UIElements.setHTML(h('label', {for: 'cp-upload-name'}),
Messages._getKey('upload_modal_filename', [ext])), 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), h('label', {for: 'cp-upload-password'}, Messages.creation_passwordValue),
UI.passwordInput({id: 'cp-upload-password'}), UI.passwordInput({id: 'cp-upload-password'}),
h('span', { h('span', {
@ -277,7 +294,7 @@ define([
]); ]);
$(content).find('#cp-upload-owned').on('change', function () { $(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) { if (val) {
$(content).find('#cp-upload-store').prop('checked', true).prop('disabled', true); $(content).find('#cp-upload-store').prop('checked', true).prop('disabled', true);
} else { } else {
@ -291,14 +308,14 @@ define([
// Get the values // Get the values
var newName = $(content).find('#cp-upload-name').val(); var newName = $(content).find('#cp-upload-name').val();
var password = $(content).find('#cp-upload-password').val() || undefined; var password = $(content).find('#cp-upload-password').val() || undefined;
var owned = $(content).find('#cp-upload-owned').is(':checked'); var owned = Util.isChecked($(content).find('#cp-upload-owned'));
var forceSave = owned || $(content).find('#cp-upload-store').is(':checked'); var forceSave = owned || Util.isChecked($(content).find('#cp-upload-store'));
modalState.owned = owned; modalState.owned = owned;
modalState.store = forceSave; modalState.store = forceSave;
// Add extension to the name if needed // 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 newExtIdx = newName.lastIndexOf('.');
var newExt = newExtIdx !== -1 ? newName.slice(newExtIdx) : ""; var newExt = newExtIdx !== -1 ? newName.slice(newExtIdx) : "";
if (newExt !== ext) { newName += ext; } 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 = { var handleFileState = {
queue: [], queue: [],
inProgress: false inProgress: false
}; };
var handleFile = File.handleFile = function (file, e) { /* if defaultOptions is passed, the function does not show the upload options modal, and directly save the file with the specified options */
if (handleFileState.inProgress) { return void handleFileState.queue.push([file, e]); } var handleFile = File.handleFile = function (file, e, defaultOptions) {
if (handleFileState.inProgress) { return void handleFileState.queue.push([file, e, defaultOptions]); }
handleFileState.inProgress = true; handleFileState.inProgress = true;
var thumb; var thumb;
@ -345,7 +414,7 @@ define([
handleFileState.inProgress = false; handleFileState.inProgress = false;
if (handleFileState.queue.length) { if (handleFileState.queue.length) {
var next = handleFileState.queue.shift(); var next = handleFileState.queue.shift();
handleFile(next[0], next[1]); handleFile(next[0], next[1], next[2]);
} }
}; };
var getName = function () { var getName = function () {
@ -354,20 +423,31 @@ define([
if (config.noStore) { return void finish(); } if (config.noStore) { return void finish(); }
// Otherwise, ask for password, name and ownership // Otherwise, ask for password, name and ownership
fileUploadModal(file, function (obj) { // if default options were passed, upload file immediately
if (!obj) { return void finish(true); } if (defaultOptions && typeof defaultOptions === "object") {
name = obj.name; name = defaultOptions.name || file.name;
password = obj.password; password = defaultOptions.password || undefined;
owned = obj.owned; owned = !!defaultOptions.owned;
forceSave = obj.forceSave; forceSave = !!defaultOptions.forceSave;
finish(); 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) { blobToArrayBuffer(file, function (e, buffer) {
if (e) { console.error(e); } if (e) { console.error(e); }
file_arraybuffer = buffer; file_arraybuffer = buffer;
if (!Thumb.isSupportedType(file.type)) { return getName(); } if (!Thumb.isSupportedType(file)) { return getName(); }
// make a resized thumbnail from the image.. // make a resized thumbnail from the image..
Thumb.fromBlob(file, function (e, thumb64) { Thumb.fromBlob(file, function (e, thumb64) {
if (e) { console.error(e); } if (e) { console.error(e); }
@ -446,124 +526,135 @@ define([
createUploader(config.dropArea, config.hoverArea, config.body); createUploader(config.dropArea, config.hoverArea, config.body);
File.downloadFile = function (fData, cb) { // TODO implement the ability to cancel downloads :D
var parsed = Hash.parsePadUrl(fData.href || fData.roHref); var updateProgressbar = function (file, data, downloadFunction, cb) {
var hash = parsed.hash; if (queue.inProgress) { return; }
var name = fData.filename || fData.title; queue.inProgress = true;
var secret = Hash.getSecrets('file', hash, fData.password); var id = file.id;
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();
};
var updateDLProgress = function (progressValue) { var $row = $table.find('tr[id="'+id+'"]');
var text = Math.round(progressValue*100) + '%'; var $pv = $row.find('.cp-fileupload-table-progress-value');
text += ' ('+ Messages.download_step1 +'...)'; var $pb = $row.find('.cp-fileupload-table-progressbar');
$pv.text(text); var $link = $row.find('.cp-fileupload-table-link');
$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 dl = module.downloadFile(fData, function (err, obj) { var done = function () {
$link.prepend($('<span>', {'class': 'fa fa-external-link'})) $row.find('.cp-fileupload-table-cancel').addClass('success').html('').append(h('span.fa.fa-check'));
.attr('href', '#') queue.inProgress = false;
.click(function (e) { queue.next();
e.preventDefault(); };
obj.download();
});
done();
if (obj) { obj.download(); }
cb(err, obj);
}, {
src: src,
key: key,
name: name,
progress: updateDLProgress,
progress2: updateProgress,
});
var $cancel = $('<span>', {'class': 'cp-fileupload-table-cancel-button fa fa-times'}).click(function () { /*
dl.cancel(); var cancelled = function () {
$cancel.remove(); $row.find('.cp-fileupload-table-cancel').addClass('cancelled').html('').append(h('span.fa.fa-minus'));
$row.find('.cp-fileupload-table-progress-value').text(Messages.upload_cancelled); queue.inProgress = false;
done(); queue.next();
}); };*/
$row.find('.cp-fileupload-table-cancel').html('').append($cancel);
}; /**
* 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({ queue.push({
dl: todo, dl: function (file) { updateProgressbar(file, fData, MakeBackup.downloadFile, cb); },
size: data, size: data,
name: name name: name
}); });
}); });
}; };
return File; File.downloadPad = function (pData, cb) {
}; queue.push({
dl: function (file) { updateProgressbar(file, pData, MakeBackup.downloadPad, cb); },
module.downloadFile = function (fData, cb, obj) { size: 0,
var cancelled = false; name: pData.title,
var cancel = function () { });
cancelled = true;
}; };
var src, key, name;
if (obj && obj.src && obj.key && obj.name) { File.downloadFolder = function (data, cb) {
src = obj.src; queue.push({
key = obj.key; dl: function (file) { updateProgressbar(file, data, MakeBackup.downloadFolder, cb); },
name = obj.name; size: 0,
} else { name: data.folderName,
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
}; };
return File;
}; };
return module; return module;
}); });

@ -53,7 +53,7 @@ define([
'data-hash': data.content.hash 'data-hash': data.content.hash
}, [h('div.cp-notification-content', h('p', formatData(data)))]); }, [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()); $(notif).find('.cp-notification-content p').html(data.content.getFormatText());
} }

@ -272,23 +272,27 @@ define([
var parsed = Utils.Hash.parsePadUrl(window.location.href); var parsed = Utils.Hash.parsePadUrl(window.location.href);
if (!parsed.type) { throw new Error(); } if (!parsed.type) { throw new Error(); }
var defaultTitle = Utils.Hash.getDefaultName(parsed); var defaultTitle = Utils.Hash.getDefaultName(parsed);
var edPublic; var edPublic, curvePublic, notifications, isTemplate;
var forceCreationScreen = cfg.useCreationScreen && var forceCreationScreen = cfg.useCreationScreen &&
sessionStorage[Utils.Constants.displayPadCreationScreen]; sessionStorage[Utils.Constants.displayPadCreationScreen];
delete sessionStorage[Utils.Constants.displayPadCreationScreen]; delete sessionStorage[Utils.Constants.displayPadCreationScreen];
var updateMeta = function () { var updateMeta = function () {
//console.log('EV_METADATA_UPDATE'); //console.log('EV_METADATA_UPDATE');
var metaObj, isTemplate; var metaObj;
nThen(function (waitFor) { nThen(function (waitFor) {
Cryptpad.getMetadata(waitFor(function (err, m) { Cryptpad.getMetadata(waitFor(function (err, m) {
if (err) { console.log(err); } if (err) { console.log(err); }
metaObj = m; metaObj = m;
edPublic = metaObj.priv.edPublic; // needed to create an owned pad 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 (typeof(isTemplate) === "undefined") {
if (err) { console.log(err); } Cryptpad.isTemplate(window.location.href, waitFor(function (err, t) {
isTemplate = t; if (err) { console.log(err); }
})); isTemplate = t;
}));
}
}).nThen(function (/*waitFor*/) { }).nThen(function (/*waitFor*/) {
metaObj.doc = { metaObj.doc = {
defaultTitle: defaultTitle, defaultTitle: defaultTitle,
@ -317,6 +321,9 @@ define([
channel: secret.channel, channel: secret.channel,
enableSF: localStorage.CryptPad_SF === "1", // TODO to remove when enabled by default enableSF: localStorage.CryptPad_SF === "1", // TODO to remove when enabled by default
devMode: localStorage.CryptPad_dev === "1", devMode: localStorage.CryptPad_dev === "1",
fromFileData: Cryptpad.fromFileData ? {
title: Cryptpad.fromFileData.title
} : undefined,
}; };
if (window.CryptPad_newSharedFolder) { if (window.CryptPad_newSharedFolder) {
additionalPriv.newSharedFolder = window.CryptPad_newSharedFolder; additionalPriv.newSharedFolder = window.CryptPad_newSharedFolder;
@ -357,6 +364,8 @@ define([
sframeChan.event("EV_NEW_VERSION"); sframeChan.event("EV_NEW_VERSION");
}); });
// Put in the following function the RPC queries that should also work in filepicker // Put in the following function the RPC queries that should also work in filepicker
var addCommonRpc = function (sframeChan) { var addCommonRpc = function (sframeChan) {
sframeChan.on('Q_ANON_RPC_MESSAGE', function (data, cb) { sframeChan.on('Q_ANON_RPC_MESSAGE', function (data, cb) {
@ -735,6 +744,7 @@ define([
var initShareModal = function (cfg) { var initShareModal = function (cfg) {
cfg.hashes = hashes; cfg.hashes = hashes;
cfg.password = password; cfg.password = password;
cfg.isTemplate = isTemplate;
// cfg.hidden means pre-loading the filepicker while keeping it hidden. // cfg.hidden means pre-loading the filepicker while keeping it hidden.
// if cfg.hidden is true and the iframe already exists, do nothing // if cfg.hidden is true and the iframe already exists, do nothing
if (!ShareModal.$iframe) { 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) { sframeChan.on('EV_GOTO_URL', function (url) {
if (url) { if (url) {
window.location.href = url; window.location.href = url;
@ -835,13 +861,6 @@ define([
Cryptpad.setLanguage(data, cb); 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) { sframeChan.on('Q_GET_ALL_TAGS', function (data, cb) {
Cryptpad.listAllTags(function (err, tags) { Cryptpad.listAllTags(function (err, tags) {
cb({ cb({
@ -868,6 +887,9 @@ define([
Cryptpad.removeLoginBlock(data, cb); 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 cgNetwork;
var whenCGReady = function (cb) { var whenCGReady = function (cb) {
if (cgNetwork && cgNetwork !== true) { console.log(cgNetwork); return void cb(); } if (cgNetwork && cgNetwork !== true) { console.log(cgNetwork); return void cb(); }
@ -884,7 +906,12 @@ define([
error: err, error: err,
data: val data: val
}); });
}, data.opts); }, data.opts, function (progress) {
sframeChan.event("EV_CRYPTGET_PROGRESS", {
hash: data.hash,
progress: progress,
});
});
}; };
//return void todo(); //return void todo();
if (i > 30) { if (i > 30) {
@ -941,6 +968,42 @@ define([
sframeChan.event('EV_WORKER_TIMEOUT'); 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) { if (cfg.messaging) {
Notifier.getPermission(); Notifier.getPermission();
@ -1061,13 +1124,21 @@ define([
readOnly = false; readOnly = false;
updateMeta(); updateMeta();
var rtConfig = {}; var rtConfig = {
metadata: {}
};
if (data.owned) { 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) { if (data.expire) {
rtConfig.expire = data.expire; rtConfig.metadata.expire = data.expire;
} }
rtConfig.metadata.validateKey = (secret.keys && secret.keys.validateKey) || undefined;
Utils.rtConfig = rtConfig; Utils.rtConfig = rtConfig;
nThen(function(waitFor) { nThen(function(waitFor) {
if (data.templateId) { if (data.templateId) {
@ -1080,11 +1151,11 @@ define([
})); }));
} }
}).nThen(function () { }).nThen(function () {
var cryptputCfg = $.extend(true, {}, rtConfig, {password: password});
if (data.template) { if (data.template) {
// Pass rtConfig to useTemplate because Cryptput will create the file and // 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 // we need to have the owners and expiration time in the first line on the
// server // server
var cryptputCfg = $.extend(true, {}, rtConfig, {password: password});
Cryptpad.useTemplate({ Cryptpad.useTemplate({
href: data.template href: data.template
}, Cryptget, function () { }, Cryptget, function () {
@ -1093,6 +1164,14 @@ define([
}, cryptputCfg); }, cryptputCfg);
return; 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 // Start realtime outside the iframe and callback
startRealtime(rtConfig); startRealtime(rtConfig);
cb(); 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.gotoURL = function (url) { ctx.sframeChan.event('EV_GOTO_URL', url); };
funcs.openURL = function (url) { ctx.sframeChan.event('EV_OPEN_URL', url); }; funcs.openURL = function (url) { ctx.sframeChan.event('EV_OPEN_URL', url); };
funcs.openUnsafeURL = function (url) { funcs.openUnsafeURL = function (url) {
@ -497,7 +505,7 @@ define([
}; };
var shortcuts = []; var shortcuts = [];
funcs.addShortcuts = function (w) { funcs.addShortcuts = function (w, isApp) {
w = w || window; w = w || window;
if (shortcuts.indexOf(w) !== -1) { return; } if (shortcuts.indexOf(w) !== -1) { return; }
shortcuts.push(w); shortcuts.push(w);
@ -505,7 +513,7 @@ define([
// Ctrl || Meta (mac) // Ctrl || Meta (mac)
if (e.ctrlKey || (navigator.platform === "MacIntel" && e.metaKey)) { if (e.ctrlKey || (navigator.platform === "MacIntel" && e.metaKey)) {
// Ctrl+E: New pad modal // Ctrl+E: New pad modal
if (e.which === 69) { if (e.which === 69 && isApp) {
e.preventDefault(); e.preventDefault();
return void funcs.createNewPadModal(); return void funcs.createNewPadModal();
} }
@ -611,22 +619,24 @@ define([
ctx.metadataMgr.onReady(waitFor()); ctx.metadataMgr.onReady(waitFor());
funcs.addShortcuts();
}).nThen(function () { }).nThen(function () {
var privateData = ctx.metadataMgr.getPrivateData();
funcs.addShortcuts(window, Boolean(privateData.app));
try { try {
var feedback = ctx.metadataMgr.getPrivateData().feedbackAllowed; var feedback = privateData.feedbackAllowed;
Feedback.init(feedback); Feedback.init(feedback);
} catch (e) { Feedback.init(false); } } catch (e) { Feedback.init(false); }
try { try {
var forbidden = ctx.metadataMgr.getPrivateData().disabledApp; var forbidden = privateData.disabledApp;
if (forbidden) { if (forbidden) {
UI.alert(Messages.disabledApp, function () { UI.alert(Messages.disabledApp, function () {
funcs.gotoURL('/drive/'); funcs.gotoURL('/drive/');
}, {forefront: true}); }, {forefront: true});
return; return;
} }
var mustLogin = ctx.metadataMgr.getPrivateData().registeredOnly; var mustLogin = privateData.registeredOnly;
if (mustLogin) { if (mustLogin) {
UI.alert(Messages.mustLogin, function () { UI.alert(Messages.mustLogin, function () {
funcs.setLoginRedirect(function () { funcs.setLoginRedirect(function () {
@ -640,7 +650,7 @@ define([
} }
try { try {
window.CP_DEV_MODE = ctx.metadataMgr.getPrivateData().devMode; window.CP_DEV_MODE = privateData.devMode;
} catch (e) {} } catch (e) {}
ctx.sframeChan.on('EV_LOGOUT', function () { ctx.sframeChan.on('EV_LOGOUT', function () {
@ -650,7 +660,7 @@ define([
} }
}); });
UI.addLoadingScreen({hideTips: true}); UI.addLoadingScreen({hideTips: true});
var origin = ctx.metadataMgr.getPrivateData().origin; var origin = privateData.origin;
var href = origin + "/login/"; var href = origin + "/login/";
var onLogoutMsg = Messages._getKey('onLogout', ['<a href="' + href + '" target="_blank">', '</a>']); var onLogoutMsg = Messages._getKey('onLogout', ['<a href="' + href + '" target="_blank">', '</a>']);
UI.errorLoadingScreen(onLogoutMsg, true); UI.errorLoadingScreen(onLogoutMsg, true);

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

@ -1111,5 +1111,32 @@
"notifications_cat_friends": "Freundschaftsanfragen", "notifications_cat_friends": "Freundschaftsanfragen",
"notifications_cat_pads": "Mit mir geteilt", "notifications_cat_pads": "Mit mir geteilt",
"notifications_cat_archived": "Verlauf", "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_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_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_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", "newButton": "Nouveau",
"newButtonTitle": "Créer un nouveau pad", "newButtonTitle": "Créer un nouveau pad",
"uploadButton": "Importer des fichiers", "uploadButton": "Importer des fichiers",
"uploadFolderButton": "Importer un dossier",
"uploadButtonTitle": "Importer un nouveau fichier dans le dossier actuel", "uploadButtonTitle": "Importer un nouveau fichier dans le dossier actuel",
"saveTemplateButton": "Sauver en tant que modèle", "saveTemplateButton": "Sauver en tant que modèle",
"saveTemplatePrompt": "Choisir un titre pour ce 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_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.", "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_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", "newButton": "New",
"newButtonTitle": "Create a new pad", "newButtonTitle": "Create a new pad",
"uploadButton": "Upload files", "uploadButton": "Upload files",
"uploadFolderButton": "Upload folder",
"uploadButtonTitle": "Upload a new file to the current folder", "uploadButtonTitle": "Upload a new file to the current folder",
"saveTemplateButton": "Save as template", "saveTemplateButton": "Save as template",
"saveTemplatePrompt": "Choose a title for the 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_newButtonTitle": "Create a new pad or folder, import a file in the current folder",
"fm_newFolder": "New folder", "fm_newFolder": "New folder",
"fm_newFile": "New pad", "fm_newFile": "New pad",
"fm_morePads": "More",
"fm_folder": "Folder", "fm_folder": "Folder",
"fm_sharedFolder": "Shared folder", "fm_sharedFolder": "Shared folder",
"fm_folderName": "Folder name", "fm_folderName": "Folder name",
@ -383,6 +385,7 @@
"fc_color": "Change color", "fc_color": "Change color",
"fc_open": "Open", "fc_open": "Open",
"fc_open_ro": "Open (read-only)", "fc_open_ro": "Open (read-only)",
"fc_openInCode": "Open in Code editor",
"fc_expandAll": "Expand All", "fc_expandAll": "Expand All",
"fc_collapseAll": "Collapse All", "fc_collapseAll": "Collapse All",
"fc_delete": "Move to trash", "fc_delete": "Move to trash",
@ -554,6 +557,10 @@
"upload_modal_title": "File upload options", "upload_modal_title": "File upload options",
"upload_modal_filename": "File name (extension <em>{0}</em> added automatically)", "upload_modal_filename": "File name (extension <em>{0}</em> added automatically)",
"upload_modal_owner": "Owned file", "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_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_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.", "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_owned": "Owned folder",
"sharedFolders_create_password": "Folder password", "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.", "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.", "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_file": "file",
"autostore_sf": "folder", "autostore_sf": "folder",
@ -1111,5 +1121,22 @@
"notifications_cat_friends": "Friend requests", "notifications_cat_friends": "Friend requests",
"notifications_cat_pads": "Shared with me", "notifications_cat_pads": "Shared with me",
"notifications_cat_archived": "History", "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", "drive": "Drive",
"whiteboard": "Whiteboard", "whiteboard": "Whiteboard",
"file": "File", "file": "File",
"media": "Media" "media": "Media",
"kanban": "Kanban",
"todo": "A Fazer",
"contacts": "Contactos",
"sheet": "SpreadSheet (Beta)"
}, },
"button_newpad": "Novo bloco RTF", "button_newpad": "Novo bloco RTF",
"button_newcode": "Novo bloco de código", "button_newcode": "Novo bloco de código",

@ -102,7 +102,7 @@
"forgetPrompt": "Нажав ОК, вы удалите документ в корзину. Уверены?", "forgetPrompt": "Нажав ОК, вы удалите документ в корзину. Уверены?",
"movedToTrash": "Документ был удалён в корзину.<br><a href=\"/drive/\">Доступ к диску</a>", "movedToTrash": "Документ был удалён в корзину.<br><a href=\"/drive/\">Доступ к диску</a>",
"shareButton": "Поделиться", "shareButton": "Поделиться",
"shareSuccess": "Ссылка скопирована в буфер обмена.", "shareSuccess": "Ссылка скопирована в буфер обмена",
"userListButton": "Список пользователей", "userListButton": "Список пользователей",
"chatButton": "Чат", "chatButton": "Чат",
"userAccountButton": "Ваш профиль", "userAccountButton": "Ваш профиль",
@ -139,36 +139,36 @@
"or": "или", "or": "или",
"tags_title": "Теги (только для вас)", "tags_title": "Теги (только для вас)",
"tags_add": "Обновить теги страницы", "tags_add": "Обновить теги страницы",
"tags_searchHint": "Начните поиск в вашем CryptDrive при помощи # чтобы найти пэды с тегами", "tags_searchHint": "Начните поиск в вашем CryptDrive при помощи # чтобы найти пэды с тегами.",
"tags_notShared": "Ваши теги не разделяются с другими пользователями", "tags_notShared": "Ваши теги не разделяются с другими пользователями",
"button_newsheet": "Новый Лист", "button_newsheet": "Новый Лист",
"newButtonTitle": "Создать новый блокнот", "newButtonTitle": "Создать новый документ",
"useTemplateCancel": "Начать заново (Esc)", "useTemplateCancel": "Начать заново (Esc)",
"previewButtonTitle": "Отобразить или скрыть режим предпросмотра разметки", "previewButtonTitle": "Показать или скрыть просмотр Маркдаун разметки",
"printOptions": "Опции расположения", "printOptions": "Настройки размещения",
"printBackgroundValue": "<b>Текущий фон:</b> <em>{0}</em>", "printBackgroundValue": "<b>Текущий фон:</b> <em>{0}</em>",
"printBackgroundNoValue": "<em>Нет отображаемого фонового изображения</em>", "printBackgroundNoValue": "<em>Фоновое изображение не показано</em>",
"tags_duplicate": "Скопировать тег: {0}", "tags_duplicate": "Скопировать метку: {0}",
"tags_noentry": "Вы не можете присвоить тег удалённому блокноту!", "tags_noentry": "Вы не можете присвоить метку удалённому документу!",
"slideOptionsText": "Опции", "slideOptionsText": "Настройки",
"slideOptionsTitle": "Настроить ваши слайды", "slideOptionsTitle": "Настройте ваши слайды",
"slideOptionsButton": "Сохранить (Enter)", "slideOptionsButton": "Сохранить (Ввод)",
"slide_invalidLess": "Неверный настраиваемый стиль", "slide_invalidLess": "Неверный пользовательский стиль",
"languageButton": "Язык", "languageButton": "Язык",
"languageButtonTitle": "Выберите язык, используемый для подсветки синтаксиса", "languageButtonTitle": "Выберите язык для использования подсветки слов",
"themeButton": "Тема", "themeButton": "Тема",
"themeButtonTitle": "Выберите цветовую тему, используемую в редакторе кода и слайдов", "themeButtonTitle": "Выберите цветовую тему для использования в редакторе кода и слайдов",
"editShare": "Редактирование ссылки", "editShare": "Редактируемая ссылка",
"editShareTitle": "Скопировать редактируемую ссылку", "editShareTitle": "Скопировать редактируемую ссылку в буфер обмена",
"editOpen": "Открыть редактируемую ссылку в новой вкладке", "editOpen": "Открыть редактируемую ссылку в новой вкладке",
"editOpenTitle": "Открыть блокнот в режиме редактирования в новой вкладке", "editOpenTitle": "Открыть данный документ для редактирования в новой вкладке",
"viewShare": "Ссылка только для чтения", "viewShare": "Ссылка только для чтения",
"viewShareTitle": "Скопировать ссылку для чтения в буфер обмена", "viewShareTitle": "Скопировать ссылку только для чтения в буфер обмена",
"viewOpen": "Открыть ссылку в режиме чтения в новой вкладке", "viewOpen": "Открыть ссылку только для чтения в новой вкладке",
"viewOpenTitle": "Открыть блокнот в режиме чтения в новой вкладке", "viewOpenTitle": "Открыть данный документ для чтения в новой вкладке",
"fileShare": "Скопировать ссылку", "fileShare": "Скопировать ссылку",
"getEmbedCode": "Получить код для встраивания", "getEmbedCode": "Получить код для встраивания",
"viewEmbedTitle": "Встроить блокнот на внешнюю страницу", "viewEmbedTitle": "Встроить документ во внешнюю страницу",
"notifyJoined": "{0} присоединился к совместной сессии", "notifyJoined": "{0} присоединился к совместной сессии",
"notifyRenamed": "{0} теперь известен как {1}", "notifyRenamed": "{0} теперь известен как {1}",
"notifyLeft": "{0} покинул совместную сессию", "notifyLeft": "{0} покинул совместную сессию",
@ -258,7 +258,7 @@
"profile_fieldSaved": "Сохранено новое значение: {0}", "profile_fieldSaved": "Сохранено новое значение: {0}",
"profile_viewMyProfile": "Посмотреть мой профиль", "profile_viewMyProfile": "Посмотреть мой профиль",
"contacts_title": "Контакты", "contacts_title": "Контакты",
"contacts_added": "Приглашение принято контактом", "contacts_added": "Приглашение принято контактом.",
"contacts_rejected": "Контакт не принял приглашение", "contacts_rejected": "Контакт не принял приглашение",
"contacts_send": "Отправить", "contacts_send": "Отправить",
"contacts_remove": "Убрать этот контакт", "contacts_remove": "Убрать этот контакт",
@ -302,22 +302,22 @@
"crowdfunding_popup_no": "Не сейчас", "crowdfunding_popup_no": "Не сейчас",
"crowdfunding_popup_never": "Не спрашивать меня снова", "crowdfunding_popup_never": "Не спрашивать меня снова",
"markdown_toc": "Содержимое", "markdown_toc": "Содержимое",
"fm_expirablePad": "Этот блокнот удалится через {0}", "fm_expirablePad": "Этот блокнот истечет {0}",
"fileEmbedTitle": "Вставить файл во внешнюю страницу", "fileEmbedTitle": "Встроить файл во внешнюю страницу",
"kanban_removeItemConfirm": "Вы уверенны, что хотите удалить этот пункт?", "kanban_removeItemConfirm": "Вы уверенны, что хотите удалить этот пункт?",
"settings_backup2": "Скачать мой CryptDrive", "settings_backup2": "Скачать мой CryptDrive",
"settings_backup2Confirm": "Это позволит скачать все пэды и файлы с вашего CryptDrive. Если вы хотите продолжить, выберите имя и нажмите OK", "settings_backup2Confirm": "Это позволит скачать все пэды и файлы с вашего CryptDrive. Если вы хотите продолжить, выберите имя и нажмите OK",
"settings_exportTitle": "Экспортировать Ваш CryptDrive", "settings_exportTitle": "Экспортировать Ваш CryptDrive",
"fileEmbedScript": "Чтобы вставить этот файл, включите этот скрипт один раз на своей странице, чтобы загрузить медиатег:", "fileEmbedScript": "Чтобы вставить этот файл, включите этот скрипт один раз на своей странице, чтобы загрузить медиатег:",
"fileEmbedTag": "Затем поместите медиатег в любое место на странице, куда вы хотите его вставить:", "fileEmbedTag": "Затем поместите медиатег в любое место на странице,в которое вы хотите его вставить:",
"pad_mediatagRatio": "Оставить соотношение", "pad_mediatagRatio": "Оставить соотношение",
"kanban_item": "Элемент {0}", "kanban_item": "Элемент {0}",
"poll_p_encryption": "Все ваши данные зашифрованы, доступ к ним имеют только пользователи, имеющие доступ к этой ссылке. Даже сервер не видит, что вы меняете.", "poll_p_encryption": "Все ваши данные зашифрованы, доступ к ним имеют только пользователи, имеющие доступ к этой ссылке. Даже сервер не видит, что вы меняете.",
"wizardLog": "Нажмите кнопку в левом верхнем углу, чтобы вернуться к опросу", "wizardLog": "Нажмите кнопку в левом верхнем углу, чтобы вернуться к опросу",
"poll_bookmark_col": "Добавить этот столбец в закладку, чтобы он всегда был разблокирован и отображался для вас в начале", "poll_bookmark_col": "Добавить этот столбец в закладку, чтобы он всегда был разблокирован и отображался для вас в начале",
"poll_bookmarked_col": "Это твоя колонка закладок. Она всегда будет разблокирована и отображаться для вас в начале.", "poll_bookmarked_col": "Это твоя колонка закладок. Она всегда будет разблокирована и отображаться для вас в начале.",
"poll_wizardDescription": "Автоматическое создавайте несколько опций путем ввода произвольного количества дат и временных сегментов", "poll_wizardDescription": "Автоматически создавайте несколько опций путем ввода произвольного количества дат и временных сегментов",
"poll_comment_disabled": "Опубликуйте этот опрос с помощью кнопки ✓ для включения комментариев", "poll_comment_disabled": "Опубликуйте этот опрос с помощью кнопки ✓ для включения комментариев.",
"oo_cantUpload": "Загрузка запрещена, если присутствуют другие пользователи.", "oo_cantUpload": "Загрузка запрещена, если присутствуют другие пользователи.",
"oo_uploaded": "Ваша загрузка завершена. Нажмите OK, чтобы перезагрузить страницу или отменить, чтобы остаться в режиме чтения.", "oo_uploaded": "Ваша загрузка завершена. Нажмите OK, чтобы перезагрузить страницу или отменить, чтобы остаться в режиме чтения.",
"canvas_imageEmbed": "Вставьте изображение с вашего компьютера", "canvas_imageEmbed": "Вставьте изображение с вашего компьютера",
@ -367,13 +367,13 @@
"fm_padIsOwnedOther": "Этот пэд принадлежит другому пользователю", "fm_padIsOwnedOther": "Этот пэд принадлежит другому пользователю",
"fm_deletedPads": "Эти пэды больше не существуют на сервере, они были удалены с вашего CryptDrive: {0}", "fm_deletedPads": "Эти пэды больше не существуют на сервере, они были удалены с вашего CryptDrive: {0}",
"fm_tags_name": "Имя тэга", "fm_tags_name": "Имя тэга",
"printCSS": "Пользовательские настройки вида (CSS)", "printCSS": "Пользовательские настройки вида (CSS):",
"viewEmbedTag": "Чтобы встроить данный документ вставьте iframe в нужную страницу. Вы можете настроить внешний вид используя CSS и HTML атрибуты. ", "viewEmbedTag": "Чтобы встроить данный документ, вставьте iframe в нужную страницу. Вы можете настроить внешний вид используя CSS и HTML атрибуты.",
"debug_getGraphText": "Это код DOT для генерации графика истории этого документа:", "debug_getGraphText": "Это код DOT для генерации графика истории этого документа:",
"fm_ownedPadsName": "Собственный", "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_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_backup_title": "Резервная ссылка",
"fm_burnThisDriveButton": "Удалить всю информацию, хранящуюся на CryptPad в браузере.", "fm_burnThisDriveButton": "Удалить всю информацию, хранящуюся от CryptPad в браузере",
"fm_tags_used": "Количество использований", "fm_tags_used": "Количество использований",
"fm_restoreDrive": "Восстановление прежнего состояния диска. Для достижения наилучших результатов не вносите изменения в диск, пока этот процесс не будет завершен.", "fm_restoreDrive": "Восстановление прежнего состояния диска. Для достижения наилучших результатов не вносите изменения в диск, пока этот процесс не будет завершен.",
"fm_passwordProtected": "Этот документ защищен паролем", "fm_passwordProtected": "Этот документ защищен паролем",
@ -389,5 +389,118 @@
"fc_empty": "Удалить корзину", "fc_empty": "Удалить корзину",
"fc_prop": "Свойства", "fc_prop": "Свойства",
"fc_hashtag": "Теги", "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) { exp.isFolderEmpty = function (element) {
if (!isFolder(element)) { return false; } 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) { 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) // Get data from AllFiles (Cryptpad_RECENTPADS)
var getFileData = exp.getFileData = function (file) { var getFileData = exp.getFileData = function (file) {
if (!file) { return; } if (!file) { return; }

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

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

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

@ -83,6 +83,7 @@ define([
}).nThen(function (/*waitFor*/) { }).nThen(function (/*waitFor*/) {
metaObj.doc = {}; metaObj.doc = {};
var additionalPriv = { var additionalPriv = {
fileHost: ApiConfig.fileHost,
accountName: Utils.LocalStore.getAccountName(), accountName: Utils.LocalStore.getAccountName(),
origin: window.location.origin, origin: window.location.origin,
pathname: window.location.pathname, 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. // Pads from the code app will be exported using this format instead of plain text.
define([ define([
], function () { ], function () {
var module = {}; var module = {
ext: '.json'
};
module.main = function (userDoc, cb) { module.main = function (userDoc, cb) {
var content = userDoc.content; 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)], { return new Blob([JSON.stringify(kanban.getBoardsJSON(), 0, 2)], {
type: 'application/json', 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 = {}; var create = {};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save