Merge branch 'staging' into communities-oo

pull/1/head
yflory 5 years ago
commit 47768112b4

@ -1,3 +1,66 @@
# JamaicanMonkey release (3.9.0)
## Goals
Over time we've added many small configuration values to CryptPad's `config/config.js`.
As the number of possible variations grew it became increasingly difficult to test the platform and to provide clear documentation.
Ultimately this has made the platform more difficult to understand and consequently to host.
This release features relatively few bug fixes or features.
Instead, we took the calm period of the northern winter holidays to simplify the process of running a server and to begin working on some comprehensive documentation.
## Update notes
We have chosen to drop support for a number of parameters which we believe are not widely used.
Read the following list carefully before updating, as you could be relying on behaviour which no longer exists.
* Due to reasons of security and performance we have long advised that administrators make their instance available only over HTTPS provided by a reverse proxy such as nginx instead of loading TLS certificates via the node process itself. We have removed the option of serving HTTPS traffic directly from node by removing all support for HTTPS in this process.
* Over the years many administrators have had to migrate their instance from one machine to another and have had difficulty identifying which directories were responsible for storing user data. We are beginning to migrate all user-generated data from the repository's root into the `data` directory as a new default, allowing for admins to migrate content by copying this single directory.
* for the time being we have not moved anything which is exposed directly over HTTPS since that complicates the upgrade process by requiring all configuration changes to be made simultaneously.
* the modifications we've made only affect the _default configuration_ provided by `config/config.example.js`, existing instances which have copied this file to `config/config.js` will not be affected.
* only the following values have been modified:
* `pinPath`
* `taskPath`
* `blobStagingPath`
* We have modified the Dockerfile volume list to reflect the changes to these default paths. If you are using docker you will have to either:
* revert their removal or
* move the affected directories into the `data` directory and update your live config file to reflect their new location
* Please note that we do our team does not use docker, that it was included in the main repository as a community contribution, and that we are not committed to supporting its configuration since we do not test it.
* Our official policy is to provide an up-to-date set of configuration files reflecting the state of our production installation on [CryptPad.fr](https://cryptpad.fr) using Debian, nginx, and systemd.
* we are actively working on improving our documentation for this particular configuration and we plan to close issues for other configurations as being outside of the project's scope.
* We've updated our example nginx configuration file, located at `cryptpad/docs/example.nginx.conf`.
* in addition to a great number of comments, it now makes use of variables configure the domains referenced by the CSP headers which are required to take advantage of all of CryptPad's security features.
* Prompted by warnings from recent nodejs versions we are updating our recommended version to v12.14.0 which is at the time of this writing the latest Long Term Support version.
* you may need to update to successfully launch your server.
* as always, we recommend using nvm to manage nodejs installation.
* We have dropped support for a number of experimental features:
* replify (which allowed admins to modify their server at runtime using a REPL connected via a named socket)
* heapdump (which provided snapshots of the server's memory if it crashed)
* configurable RPC files as a configuration parameter
* Finally, we've replaced a number of websocket configuration values (`websocketURL`, `websocketPath`, `useExternalWebsockets`, and `useSecureWebsockets`) with one optional value (`externalWebsocketURL`) in config.js
* if your instance is configured in the default manner you shouldn't actually need this value, as it will default to using `/cryptpad_websocket`.
* if you have configured your instance to serve all static assets over one domain and to host your API server on another, set `externalWebsocketURL` to `wss://your-domain.tld/cryptpad_websocket` or whatever URL will be correctly forwarded to your API server.
Once you have reviewed your configuration files and ensured that they are correct, update to 3.9.0 with the following steps:
1. take your server down
2. get the latest code with `git pull origin master`
3. install some required serverside dependency with `npm update`
4. (optionally) update clientside dependencies with `bower update`
5. bring your server back up
## Features
* We made some minor improvements to the process of redeeming invitation links for teams.
* invitation links can only be used once, so we remove the hash from the URL bar once you've landed on the redemption page so that reloading after redeeming doesn't indicate that you've used an expired link.
* [One of our Finnish-speaking contributors](https://weblate.cryptpad.fr/user/ilo/) has translated a very large amount of the platform's text in the last few weeks, making Finnish our fifth most thoroughly translated language!
## Bug fixes
* We noticed and fixed a style regression which incorrectly removed the scrollbar from some textareas
* We also found that it was possible to corrupt the href of an item in a team's drive if you first shared a pad with your team then transferred ownership, the link stored in the team's drive would have its domain concatenated together twice.
* The type value of read-only pads displayed as search results in user and team drives was incorrect but is now correctly inferred.
# IsolobodonPortoricensis release (3.8.0)
We had some trouble finding an extinct animal whose name started with "I", and we had to resort to using a scientific name.

@ -28,11 +28,8 @@ VOLUME /cryptpad/cfg
VOLUME /cryptpad/datastore
VOLUME /cryptpad/customize
VOLUME /cryptpad/blobstage
VOLUME /cryptpad/pins
VOLUME /cryptpad/tasks
VOLUME /cryptpad/block
VOLUME /cryptpad/blob
VOLUME /cryptpad/blobstage
VOLUME /cryptpad/data
# Copy cryptpad and tini from the build container

@ -97,15 +97,17 @@ module.exports = {
httpUnsafeOrigin: domain,
/* your server's websocket url is configurable
* (default: '/cryptpad_websocket')
/* Your CryptPad server will share this value with clients
* via its /api/config endpoint.
*
* websocketPath can be relative, of the form '/path/to/websocket'
* or absolute, specifying a particular URL
* If you want to host your API and asset servers on different hosts
* specify a URL for your API server websocket endpoint, like so:
* wss://api.yourdomain.com/cryptpad_websocket
*
* 'wss://cryptpad.fr:3000/cryptpad_websocket'
* Otherwise, leave this commented and your clients will use the default
* websocket (wss://yourdomain.com/cryptpad_websocket)
*/
websocketPath: '/cryptpad_websocket',
//externalWebsocketURL: 'wss://api.yourdomain.com/cryptpad_websocket
/* CryptPad can be configured to send customized HTTP Headers
* These settings may vary widely depending on your needs
@ -124,15 +126,6 @@ module.exports = {
padContentSecurity: baseCSP.join('; ') +
"script-src 'self' 'unsafe-eval' 'unsafe-inline'" + domain,
/* it is recommended that you serve CryptPad over https
* the filepaths below are used to configure your certificates
*/
//privKeyAndCertFiles: [
// '/etc/apache2/ssl/my_secret.key',
// '/etc/apache2/ssl/my_public_cert.crt',
// '/etc/apache2/ssl/my_certificate_authorities_cert_chain.ca'
//],
/* Main pages
* add exceptions to the router so that we can access /privacy.html
* and other odd pages
@ -281,7 +274,6 @@ module.exports = {
*/
openFileLimit: 2048,
/* =====================
* DATABASE VOLUMES
* ===================== */
@ -307,12 +299,12 @@ module.exports = {
* Pin requests are stored in a pin-store. The location of this store is
* defined here.
*/
pinPath: './pins',
pinPath: './data/pins',
/* if you would like the list of scheduled tasks to be stored in
a custom location, change the path below:
*/
taskPath: './tasks',
taskPath: './data/tasks',
/* if you would like users' authenticated blocks to be stored in
a custom location, change the path below:
@ -327,7 +319,7 @@ module.exports = {
/* CryptPad stores incomplete blobs in a 'staging' area until they are
* fully uploaded. Set its location here.
*/
blobStagingPath: './blobstage',
blobStagingPath: './data/blobstage',
/* CryptPad supports logging events directly to the disk in a 'logs' directory
* Set its location here, or set it to false (or nothing) if you'd rather not log
@ -368,42 +360,6 @@ module.exports = {
*/
logFeedback: false,
/* You can get a repl for debugging the server if you want it.
* to enable this, specify the debugReplName and then you can
* connect to it with `nc -U /tmp/repl/<your name>.sock`
* If you run multiple cryptpad servers, you need to use different
* repl names.
*/
//debugReplName: "cryptpad"
/* =====================
* DEPRECATED
* ===================== */
/*
You have the option of specifying an alternative storage adaptor.
These status of these alternatives are specified in their READMEs,
which are available at the following URLs:
mongodb: a noSQL database
https://github.com/xwiki-labs/cryptpad-mongo-store
amnesiadb: in memory storage
https://github.com/xwiki-labs/cryptpad-amnesia-store
leveldb: a simple, fast, key-value store
https://github.com/xwiki-labs/cryptpad-level-store
sql: an adaptor for a variety of sql databases via knexjs
https://github.com/xwiki-labs/cryptpad-sql-store
For the most up to date solution, use the default storage adaptor.
*/
storage: './storage/file',
/* CryptPad's socket server can be extended to respond to RPC calls
* you can configure it to respond to custom RPC calls if you like.
* provide the path to your RPC module here, or `false` if you would
* like to disable the RPC interface completely
*/
rpc: './rpc.js',
/* CryptPad supports verbose logging
* (false by default)
*/

@ -17,9 +17,6 @@ sedeasy() {
}
# Configure
[ -n "$USE_SSL" ] && echo "Using secure websockets: $USE_SSL" \
&& sedeasy "useSecureWebsockets: [^,]*," "useSecureWebsockets: ${USE_SSL}," cfg/config.js
[ -n "$STORAGE" ] && echo "Using storage adapter: $STORAGE" \
&& sedeasy "storage: [^,]*," "storage: ${STORAGE}," cfg/config.js

@ -13,7 +13,8 @@ define([], function () {
right: 0px;
background: linear-gradient(to right, #326599 0%, #326599 50%, #4591c4 50%, #4591c4 100%);
color: #fafafa;
font-size: 1.5em;
font-size: 1.3em;
line-height: 120%;
opacity: 1;
display: flex;
flex-flow: column;
@ -77,14 +78,12 @@ define([], function () {
background: #FFF;
padding: 20px;
width: 100%;
color: #000;
text-align: center;
color: #3F4141;
text-align: left;
display: none;
}
#cp-loading-password-prompt {
font-size: 18px;
}
#cp-loading-password-prompt .cp-password-error {
#cp-loading-password-prompt p.cp-password-error {
color: white;
background: #9e0000;
padding: 5px;
@ -94,24 +93,53 @@ define([], function () {
text-align: left;
margin-bottom: 15px;
}
#cp-loading-burn-after-reading .cp-password-info {
margin-bottom: 15px;
}
p.cp-password-info{
text-align: left;
}
#cp-loading-password-prompt .cp-password-form {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
}
#cp-loading-password-prompt .cp-password-form button,
#cp-loading-password-prompt .cp-password-form .cp-password-input {
#cp-loading-password-prompt .cp-password-form button{
background-color: #4591c4;
color: white;
border: 1px solid #4591c4;
}
.cp-password-input{
font-size:16px;
border: 1px solid #4591c4;
background-color: white;
border-radius 0;
}
.cp-password-form button{
padding: 8px 12px;
font-weight: bold;
text-transform: uppercase;
}
#cp-loading-password-prompt .cp-password-form{
width: 100%;
}
#cp-loading-password-prompt .cp-password-form .cp-password-container {
flex-shrink: 1;
min-width: 0;
}
#cp-loading-password-prompt .cp-password-form .cp-password-container .cp-password-reveal{
color: #4591c4;
padding: 0px 24px;
}
#cp-loading-password-prompt .cp-password-form input {
flex: 1;
padding: 0 5px;
padding: 12px;
min-width: 0;
text-overflow: ellipsis;
}
@ -119,7 +147,7 @@ define([], function () {
background-color: #326599;
}
#cp-loading-password-prompt ::placeholder {
color: #d9d9d9;
color: #999999;
opacity: 1;
}
#cp-loading-password-prompt :-ms-input-placeholder {
@ -154,7 +182,7 @@ define([], function () {
background: #222;
color: #fafafa;
text-align: center;
font-size: 1.5em;
font-size: 1.3em;
opacity: 0.7;
font-family: 'Open Sans', 'Helvetica Neue', sans-serif;
padding: 15px;
@ -201,6 +229,19 @@ define([], function () {
animation-timing-function: cubic-bezier(.6,0.15,0.4,0.85);
}
button.primary{
border: 1px solid #4591c4;
padding: 8px 12px;
text-transform: uppercase;
background-color: #4591c4;
color: white;
font-weight: bold;
}
button.primary:hover{
background-color: rgb(52, 118, 162);
}
*/}).toString().slice(14, -3);
var urlArgs = window.location.href.replace(/^.*\?([^\?]*)$/, function (all, x) { return x; });
var elem = document.createElement('div');

@ -5,6 +5,7 @@ var map = {
'de': 'Deutsch',
'el': 'Ελληνικά',
'es': 'Español',
'fi': 'Suomalainen',
'fr': 'Français',
'it': 'Italiano',
'nb': 'Norwegian Bokmål',

@ -103,7 +103,7 @@ define([
])*/
])
]),
h('div.cp-version-footer', "CryptPad v3.8.0 (IsolobodonPortoricensis)")
h('div.cp-version-footer', "CryptPad v3.9.0 (JamaicanMonkey)")
]);
};

@ -168,6 +168,9 @@
margin-bottom: 0;
}
}
.cp-alertify-type-container {
overflow: visible !important;
}
.alertify-tabs {
max-height: 100%;
display: flex;

@ -23,10 +23,27 @@
}
}
div.cp-alertify-type {
display: flex;
input {
margin: 0;
flex: 1;
min-width: 0;
}
span {
button {
margin: 0;
height: 100%;
margin-left: -1px;
text-transform: unset !important;
}
}
}
textarea {
overflow: hidden;
padding: 8px;
&[readonly] {
overflow: hidden;
resize: none;
}
}

@ -17,8 +17,7 @@
button {
.fa-caret-down {
margin-right: 0px;
margin-left: 5px;
margin-right: 1em !important;
}
* {
.tools_unselectable();

@ -14,6 +14,9 @@
.radio-group {
display: flex;
flex-direction: row;
&:not(:last-child){
margin-bottom: 8px;
}
.cp-radio {
margin-right: 30px;
}

@ -97,6 +97,12 @@
.ckeditor_fix();
.cp-burn-after-reading {
text-align: center;
font-size: @colortheme_app-font-size !important;
margin: 0 !important;
}
.cp-markdown-toolbar {
height: @toolbar_line-height;
background-color: @toolbar-bg-color-l20;

@ -25,10 +25,7 @@ services:
volumes:
- ./data/files:/cryptpad/datastore:rw
- ./data/customize:/cryptpad/customize:rw
- ./data/pins:/cryptpad/pins:rw
- ./data/blob:/cryptpad/blob:rw
- ./data/blobstage:/cryptpad/blobstage:rw
- ./data/tasks:/cryptpad/tasks:rw
- ./data/block:/cryptpad/block:rw
- ./data/config:/cryptpad/cfg:rw
- ./data/data:/cryptpad/data:rw

@ -6,55 +6,119 @@
server {
listen 443 ssl http2;
# CryptPad serves static assets over these two domains.
# `main_domain` is what users will enter in their address bar.
# Privileged computation such as key management is handled in this scope
# UI content is loaded via the `sandbox_domain`.
# "Content Security Policy" headers prevent content loaded via the sandbox
# from accessing privileged information.
# These variables must be different to take advantage of CryptPad's sandboxing techniques.
# In the event of an XSS vulnerability in CryptPad's front-end code
# this will limit the amount of information accessible to attackers.
set $main_domain "your-main-domain.com";
set $sandbox_domain "your-sandbox-domain.com";
# CryptPad's dynamic content (websocket traffic and encrypted blobs)
# can be served over separate domains. Using dedicated domains (or subdomains)
# for these purposes allows you to move them to a separate machine at a later date
# if you find that a single machine cannot handle all of your users.
# If you don't use dedicated domains, this can be the same as $main_domain
# If you do, they'll be added as exceptions to any rules which block connections to remote domains.
set $api_domain "api.your-main-domain.com";
set $files_domain "files.your-main-domain.com";
# nginx doesn't let you set server_name via variables, so you need to hardcode your domains here
server_name your-main-domain.com your-sandbox-domain.com;
# You'll need to Set the path to your certificates and keys here
ssl_certificate /home/cryptpad/.acme.sh/your-main-domain.com/fullchain.cer;
ssl_certificate_key /home/cryptpad/.acme.sh/your-main-domain.com/your-main-domain.com.key;
ssl_trusted_certificate /home/cryptpad/.acme.sh/your-main-domain.com/ca.cer;
ssl_dhparam /etc/nginx/dhparam.pem;
# diffie-hellman parameters are used to negotiate keys for your session
# generate strong parameters using the following command
ssl_dhparam /etc/nginx/dhparam.pem; # openssl dhparam -out /etc/nginx/dhparam.pem 4096
# Speeds things up a little bit when resuming a session
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # omit SSLv3 because of POODLE
# ECDHE better than DHE (faster) ECDHE & DHE GCM better than CBC (attacks on AES) Everything better than SHA1 (deprecated)
ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:5m;
# You'll need nginx 1.13.0 or better to support TLSv1.3
ssl_protocols TLSv1.2 TLSv1.3;
# https://cipherli.st/
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options nosniff;
# add_header X-Frame-Options "SAMEORIGIN";
# Insert the path to your CryptPad repository root here
root /home/cryptpad/cryptpad;
index index.html;
error_page 404 /customize.dist/404.html;
# any static assets loaded with "ver=" in their URL will be cached for a year
if ($args ~ ver=) {
set $cacheControl max-age=31536000;
}
# Will not set any header if it is emptystring
add_header Cache-Control $cacheControl;
set $styleSrc "'unsafe-inline' 'self' your-main-domain.com";
set $scriptSrc "'self' your-main-domain.com";
set $connectSrc "'self' https://your-main-domain.com wss://your-main-domain.com your-main-domain.com https://api.your-main-domain.com blob: your-main-domain.com";
set $fontSrc "'self' data: your-main-domain.com";
set $imgSrc "'self' data: * blob: your-main-domain.com";
set $frameSrc "'self' your-sandbox-domain.com blob: your-sandbox-domain.com";
set $mediaSrc "'self' data: * blob: your-main-domain.com";
set $childSrc "https://your-main-domain.com";
set $workerSrc "https://your-main-domain.com";
# CSS can be dynamically set inline, loaded from the same domain, or from $main_domain
set $styleSrc "'unsafe-inline' 'self' ${main_domain}";
# connect-src restricts URLs which can be loaded using script interfaces
set $connectSrc "'self' https://${main_domain} $main_domain https://${api_domain} blob:";
# fonts can be loaded from data-URLs or the main domain
set $fontSrc "'self' data: ${main_domain}";
# images can be loaded from anywhere, though we'd like to deprecate this as it allows the use of images for tracking
set $imgSrc "'self' data: * blob: ${main_domain}";
# frame-src specifies valid sources for nested browsing contexts.
# this prevents loading any iframes from anywhere other than the sandbox domain
set $frameSrc "'self' ${sandbox_domain} blob:";
# specifies valid sources for loading media using video or audio
set $mediaSrc "'self' data: * blob: ${main_domain}";
# defines valid sources for webworkers and nested browser contexts
# deprecated in favour of worker-src and frame-src
set $childSrc "https://${main_domain}";
# specifies valid sources for Worker, SharedWorker, or ServiceWorker scripts.
# supercedes child-src but is unfortunately not yet universally supported.
set $workerSrc "https://${main_domain}";
# script-src specifies valid sources for javascript, including inline handlers
set $scriptSrc "'self' ${main_domain}";
set $unsafe 0;
# the following assets are loaded via the sandbox domain
# they unfortunately still require exceptions to the sandboxing to work correctly.
if ($uri = "/pad/inner.html") { set $unsafe 1; }
if ($uri = "/sheet/inner.html") { set $unsafe 1; }
if ($uri = "/common/onlyoffice/web-apps/apps/spreadsheeteditor/main/index.html") { set $unsafe 1; }
# everything except the sandbox domain is a privileged scope, as they might be used to handle keys
if ($host != sandbox.cryptpad.info) { set $unsafe 0; }
# privileged contexts allow a few more rights than unprivileged contexts, though limits are still applied
if ($unsafe) {
set $scriptSrc "'self' 'unsafe-eval' 'unsafe-inline' new2.cryptpad.fr cryptpad.fr";
set $scriptSrc "'self' 'unsafe-eval' 'unsafe-inline' ${main_domain}";
}
add_header Content-Security-Policy "default-src 'none'; child-src $childSrc; worker-src $workerSrc; media-src $mediaSrc; style-src $styleSrc; script-src $scriptSrc; connect-src $connectSrc; font-src $fontSrc; img-src $imgSrc; frame-src $frameSrc;";
# Finally, set all the rules you composed above.
add_header Content-Security-Policy "default-src 'none'; child-src $childSrc; worker-src $workerSrc; media-src $mediaSrc; style-src $styleSrc; script-src $scriptSrc; connect-src $connectSrc; font-src $fontSrc; img-src $imgSrc; frame-src $frameSrc;";
# The nodejs process can handle all traffic whether accessed over websocket or as static assets
# We prefer to serve static content from nginx directly and to leave the API server to handle
# the dynamic content that only it can manage. This is primarily an optimization
location ^~ /cryptpad_websocket {
proxy_pass http://localhost:3000;
proxy_set_header X-Real-IP $remote_addr;
@ -67,33 +131,20 @@ server {
proxy_set_header Connection upgrade;
}
location ^~ /datastore/ {
alias /home/cryptpad/office.cryptpad/data/datastore;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Max-Age' 0;
add_header 'Content-Type' 'application/octet-stream; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
add_header Cache-Control max-age=0;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
try_files $uri =404;
}
location ^~ /customize.dist/ {
# This is needed in order to prevent infinite recursion between /customize/ and the root
}
# try to load customizeable content via /customize/ and fall back to the default content
# located at /customize.dist/
# This is what allows you to override behaviour.
location ^~ /customize/ {
rewrite ^/customize/(.*)$ $1 break;
try_files /customize/$uri /customize.dist/$uri;
}
# /api/config is loaded once per page load and is used to retrieve
# the caching variable which is applied to every other resource
# which is loaded during that session.
location = /api/config {
proxy_pass http://localhost:3000;
proxy_set_header X-Real-IP $remote_addr;
@ -101,24 +152,48 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# encrypted blobs are immutable and are thus cached for a year
location ^~ /blob/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'application/octet-stream; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
add_header Cache-Control max-age=31536000;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
try_files $uri =404;
}
# the "block-store" serves encrypted payloads containing users' drive keys
# these payloads are unlocked via login credentials. They are mutable
# and are thus never cached. They're small enough that it doesn't matter, in any case.
location ^~ /block/ {
add_header Cache-Control max-age=0;
try_files $uri =404;
}
location ^~ /datastore/ {
add_header Cache-Control max-age=0;
try_files $uri =404;
}
# This block provides an alternative means of loading content
# otherwise only served via websocket. This is solely for debugging purposes,
# and is thus not allowed by default.
#location ^~ /datastore/ {
#add_header Cache-Control max-age=0;
#try_files $uri =404;
#}
# The nodejs server has some built-in forwarding rules to prevent
# URLs like /pad from resulting in a 404. This simply adds a trailing slash
# to a variety of applications.
location ~ ^/(register|login|settings|user|pad|drive|poll|slide|code|whiteboard|file|media|profile|contacts|todo|filepicker|debug|kanban|sheet|support|admin|notifications|teams)$ {
rewrite ^(.*)$ $1/ redirect;
}
# Finally, serve anything the above exceptions don't govern.
try_files /www/$uri /www/$uri/index.html /customize/$uri;
}

@ -284,7 +284,7 @@ module.exports.create = function (cfg) {
const storeMessage = function (ctx, channel, msg, isCp, optionalMessageHash) {
const id = channel.id;
const msgBin = new Buffer(msg + '\n', 'utf8');
const msgBin = Buffer.from(msg + '\n', 'utf8');
queueStorage(id, function (next) {
// Store the message first, and update the index only once it's stored.

@ -8,7 +8,14 @@ try {
console.log("You can configure the administrator email (adminEmail) in your config/config.js file");
}
} catch (e) {
console.log("Config not found, loading the example config. You can customize the configuration by copying config/config.example.js to " + configPath);
if (e instanceof SyntaxError) {
console.error("config/config.js is faulty. See stacktrace below for more information. Terminating. \n");
console.error(e.name + ": " + e.message);
console.error(e.stack.split("\n\n")[0]);
process.exit(1);
} else {
console.log("Config not found, loading the example config. You can customize the configuration by copying config/config.example.js to " + configPath);
}
config = require("../config/config.example");
}
module.exports = config;

7
package-lock.json generated

@ -1,6 +1,6 @@
{
"name": "cryptpad",
"version": "3.8.0",
"version": "3.9.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -1069,11 +1069,6 @@
"string_decoder": "~0.10.x"
}
},
"replify": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/replify/-/replify-1.2.0.tgz",
"integrity": "sha1-lAFm0gfRDphhT+SSU60vCsAZ9+E="
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",

@ -1,7 +1,7 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
"version": "3.8.0",
"version": "3.9.0",
"license": "AGPL-3.0+",
"repository": {
"type": "git",
@ -20,7 +20,6 @@
"netflux-websocket": "^0.1.20",
"nthen": "0.1.8",
"pull-stream": "^3.6.1",
"replify": "^1.2.0",
"saferphore": "0.0.1",
"sortify": "^1.0.4",
"stream-to-pull-stream": "^1.7.2",

@ -1062,7 +1062,7 @@ var writeLoginBlock = function (Env, msg, cb) { // FIXME BLOCKS
// flow is dumb and I need to guard against this which will never happen
/*:: if (typeof(validatedBlock) === 'undefined') { throw new Error('should never happen'); } */
/*:: if (typeof(path) === 'undefined') { throw new Error('should never happen'); } */
Fs.writeFile(path, new Buffer(validatedBlock), { encoding: "binary", }, function (err) {
Fs.writeFile(path, Buffer.from(validatedBlock), { encoding: "binary", }, function (err) {
if (err) { return void cb(err); }
cb();
});
@ -1369,7 +1369,6 @@ type NetfluxWebsocketSrvContext_t = {
*/
RPC.create = function (
config /*:Config_t*/,
debuggable /*:<T>(string, T)=>T*/,
cb /*:(?Error, ?Function)=>void*/
) {
Log = config.log;
@ -1405,8 +1404,6 @@ RPC.create = function (
console.error("Can't parse admin keys. Please update or fix your config.js file!");
}
debuggable('rpc_env', Env);
var Sessions = Env.Sessions;
var paths = Env.paths;
var pinPath = paths.pin = keyOrDefaultString('pinPath', './pins');

@ -41,7 +41,7 @@ nThen((waitFor) => {
pinned = Pins.calculateFromLog(content.toString('utf8'), f);
}));
}).nThen((waitFor) => {
Pinned.load(waitFor((d) => {
Pinned.load(waitFor((err, d) => {
data = Object.keys(d);
}), {
exclude: [edPublic + '.ndjson']

@ -3,40 +3,19 @@
*/
var Express = require('express');
var Http = require('http');
var Https = require('https');
var Fs = require('fs');
var WebSocketServer = require('ws').Server;
var NetfluxSrv = require('./node_modules/chainpad-server/NetfluxWebsocketSrv');
var NetfluxSrv = require('chainpad-server/NetfluxWebsocketSrv');
var Package = require('./package.json');
var Path = require("path");
var nThen = require("nthen");
var config = require("./lib/load-config");
var websocketPort = config.websocketPort || config.httpPort;
var useSecureWebsockets = config.useSecureWebsockets || false;
// This is stuff which will become available to replify
const debuggableStore = new WeakMap();
const debuggable = function (name, x) {
if (name in debuggableStore) {
try { throw new Error(); } catch (e) {
console.error('cannot add ' + name + ' more than once [' + e.stack + ']');
}
} else {
debuggableStore[name] = x;
}
return x;
};
debuggable('global', global);
debuggable('config', config);
// support multiple storage back ends
var Storage = require(config.storage||'./storage/file');
var app = debuggable('app', Express());
var Storage = require('./storage/file');
var httpsOpts;
var app = Express();
// mode can be FRESH (default), DEV, or PACKAGE
@ -166,29 +145,6 @@ app.use("/customize.dist", Express.static(__dirname + '/customize.dist'));
app.use(/^\/[^\/]*$/, Express.static('customize'));
app.use(/^\/[^\/]*$/, Express.static('customize.dist'));
if (config.privKeyAndCertFiles) {
var privKeyAndCerts = '';
config.privKeyAndCertFiles.forEach(function (file) {
privKeyAndCerts = privKeyAndCerts + Fs.readFileSync(file);
});
var array = privKeyAndCerts.split('\n-----BEGIN ');
for (var i = 1; i < array.length; i++) { array[i] = '-----BEGIN ' + array[i]; }
var privKey;
for (var i = 0; i < array.length; i++) {
if (array[i].indexOf('PRIVATE KEY-----\n') !== -1) {
privKey = array[i];
array.splice(i, 1);
break;
}
}
if (!privKey) { throw new Error("cannot find private key"); }
httpsOpts = {
cert: array.shift(),
key: privKey,
ca: array
};
}
var admins = [];
try {
admins = (config.adminKeys || []).map(function (k) {
@ -211,10 +167,7 @@ app.get('/api/config', function(req, res){
},
removeDonateButton: (config.removeDonateButton === true),
allowSubscriptions: (config.allowSubscriptions === true),
websocketPath: config.useExternalWebsocket ? undefined : config.websocketPath,
// FIXME don't send websocketURL if websocketPath is provided. deprecated.
websocketURL:'ws' + ((useSecureWebsockets) ? 's' : '') + '://' + host + ':' +
websocketPort + '/cryptpad_websocket',
websocketPath: config.externalWebsocketURL,
httpUnsafeOrigin: config.httpUnsafeOrigin.replace(/^\s*/, ''),
adminEmail: config.adminEmail,
adminKeys: admins,
@ -251,7 +204,7 @@ app.use(function (req, res, next) {
send404(res, custom_four04_path);
});
var httpServer = httpsOpts ? Https.createServer(httpsOpts, app) : Http.createServer(app);
var httpServer = Http.createServer(app);
httpServer.listen(config.httpPort,config.httpAddress,function(){
var host = config.httpAddress;
@ -282,7 +235,13 @@ var nt = nThen(function (w) {
log = config.log = _log;
}));
}).nThen(function (w) {
if (config.useExternalWebsocket) { return; }
if (config.externalWebsocketURL) {
// if you plan to use an external websocket server
// then you don't need to load any API services other than the logger.
// Just abort.
w.abort();
return;
}
Storage.create(config, w(function (_store) {
config.store = _store;
}));
@ -304,11 +263,7 @@ var nt = nThen(function (w) {
}, 1000 * 60 * 5); // run every five minutes
}));
}).nThen(function (w) {
config.rpc = typeof(config.rpc) === 'undefined'? './rpc.js' : config.rpc;
if (typeof(config.rpc) !== 'string') { return; }
// load pin store...
var Rpc = require(config.rpc);
Rpc.create(config, debuggable, w(function (e, _rpc) {
require("./rpc").create(config, w(function (e, _rpc) {
if (e) {
w.abort();
throw e;
@ -316,7 +271,6 @@ var nt = nThen(function (w) {
rpc = _rpc;
}));
}).nThen(function () {
if (config.useExternalWebsocket) { return; }
var HK = require('./historyKeeper.js');
var hkConfig = {
tasks: config.tasks,
@ -327,15 +281,6 @@ var nt = nThen(function (w) {
};
historyKeeper = HK.create(hkConfig);
}).nThen(function () {
if (config.useExternalWebsocket) { return; }
if (websocketPort !== config.httpPort) {
log.debug("setting up a new websocket server");
wsConfig = { port: websocketPort};
}
var wsSrv = new WebSocketServer(wsConfig);
NetfluxSrv.run(wsSrv, config, historyKeeper);
});
if (config.debugReplName) {
require('replify')({ name: config.debugReplName, app: debuggableStore });
}

@ -832,7 +832,7 @@ const messageBin = (env, chanName, msgBin, cb) => {
// append a string to a channel's log as a new line
var message = function (env, chanName, msg, cb) {
messageBin(env, chanName, new Buffer(msg + '\n', 'utf8'), cb);
messageBin(env, chanName, Buffer.from(msg + '\n', 'utf8'), cb);
};
// stream messages from a channel log

@ -254,12 +254,46 @@ define([
!secret.hashData.present);
}, "test support for trailing slashes in version 1 hash failed to parse");
// test support for ownerKey
assert(function (cb) {
var secret = Hash.parsePadUrl('/invite/#/1/ilrOtygzDVoUSRpOOJrUuQ/e8jvf36S3chzkkcaMrLSW7PPrz7VDp85lIFNI26dTmr=/');
var secret = Hash.parsePadUrl('/pad/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI/present/uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA/embed');
return cb(secret.hashData.version === 1 &&
secret.hashData.mode === "edit" &&
secret.hashData.channel === "3Ujt4F2Sjnjbis6CoYWpoQ" &&
secret.hashData.key === "usn4+9CqVja8Q7RZOGTfRgqI" &&
secret.hashData.ownerKey === "uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA" &&
secret.hashData.embed &&
secret.hashData.present);
}, "test support for owner key in version 1 hash failed to parse");
assert(function (cb) {
var parsed = Hash.parsePadUrl('/pad/#/2/pad/edit/oRE0oLCtEXusRDyin7GyLGcS/p/uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA/embed');
var secret = Hash.getSecrets('pad', parsed.hash);
return cb(parsed.hashData.version === 2 &&
parsed.hashData.mode === "edit" &&
parsed.hashData.type === "pad" &&
parsed.hashData.key === "oRE0oLCtEXusRDyin7GyLGcS" &&
secret.channel === "d8d51b4aea863f3f050f47f8ad261753" &&
window.nacl.util.encodeBase64(secret.keys.cryptKey) === "0Ts1M6VVEozErV2Nx/LTv6Im5SCD7io2LlhasyyBPQo=" &&
secret.keys.validateKey === "f5A1FM9Gp55tnOcM75RyHD1oxBG9ZPh9WDA7qe2Fvps=" &&
parsed.hashData.ownerKey === "uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA" &&
parsed.hashData.embed &&
parsed.hashData.password);
}, "test support for owner key in version 2 hash failed to parse");
assert(function (cb) {
var secret = Hash.parsePadUrl('/file/#/1/TRplGM-WsVkXR+LkJ0tD3D45A1YFZ-Cy/eO4RJwh8yHEEDhl1aHfuwQ2IzosPBZx-HDaWc1lW+hY=/uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA/');
return cb(secret.hashData.version === 1 &&
secret.hashData.channel === "TRplGM/WsVkXR+LkJ0tD3D45A1YFZ/Cy" &&
secret.hashData.key === "eO4RJwh8yHEEDhl1aHfuwQ2IzosPBZx/HDaWc1lW+hY=" &&
secret.hashData.ownerKey === "uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA" &&
!secret.hashData.present);
}, "test support for owner key in version 1 file hash failed to parse");
assert(function (cb) {
var secret = Hash.parsePadUrl('/invite/#/2/invite/edit/oRE0oLCtEXusRDyin7GyLGcS/p/');
var hd = secret.hashData;
cb(hd.channel === "ilrOtygzDVoUSRpOOJrUuQ" &&
hd.pubkey === "e8jvf36S3chzkkcaMrLSW7PPrz7VDp85lIFNI26dTmr=" &&
hd.type === 'invite');
cb(hd.key === "oRE0oLCtEXusRDyin7GyLGcS" &&
hd.password &&
hd.app === 'invite');
}, "test support for invite urls");
// test support for V2

@ -98,6 +98,7 @@ define([
};
var mkHelpMenu = function (framework) {
var $codeMirrorContainer = $('#cp-app-code-container');
$codeMirrorContainer.prepend(framework._.sfCommon.getBurnAfterReadingWarning());
var helpMenu = framework._.sfCommon.createHelpMenu(['text', 'code']);
$codeMirrorContainer.prepend(helpMenu.menu);

@ -20,7 +20,7 @@ define(function() {
* users and these users will be redirected to the login page if they still try to access
* the app
*/
config.registeredOnlyTypes = ['file', 'contacts', 'oodoc', 'ooslide', 'sheet', 'notifications'];
config.registeredOnlyTypes = ['file', 'contacts', 'oodoc', 'ooslide', 'notifications'];
/* CryptPad is available is multiple languages, but only English and French are maintained
* by the developers. The other languages may be outdated, and any missing string for a langauge

@ -15,6 +15,20 @@ var factory = function (Util, Crypto, Nacl) {
.decodeUTF8(JSON.stringify(list))));
};
// XXX move this code?
Hash.generateSignPair = function () {
var ed = Nacl.sign.keyPair();
var makeSafe = function (key) {
return Crypto.b64RemoveSlashes(key).replace(/=+$/g, '');
};
return {
validateKey: Hash.encodeBase64(ed.publicKey),
signKey: Hash.encodeBase64(ed.secretKey),
safeValidateKey: makeSafe(Hash.encodeBase64(ed.publicKey)),
safeSignKey: makeSafe(Hash.encodeBase64(ed.secretKey)),
};
};
var getEditHashFromKeys = Hash.getEditHashFromKeys = function (secret) {
var version = secret.version;
var data = secret.keys;
@ -134,6 +148,17 @@ Version 1
/code/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI
*/
var getOwnerKey = function (hashArr) {
var k;
// Check if we have a ownerKey for this pad
hashArr.some(function (data) {
if (data.length === 86) { // XXX 88 characters - 2 trailing "="...
k = data;
return true;
}
});
return k;
};
var parseTypeHash = Hash.parseTypeHash = function (type, hash) {
if (!hash) { return; }
var options;
@ -158,9 +183,12 @@ Version 1
options = hashArr.slice(5);
parsed.present = options.indexOf('present') !== -1;
parsed.embed = options.indexOf('embed') !== -1;
parsed.ownerKey = getOwnerKey(options);
parsed.getHash = function (opts) {
var hash = hashArr.slice(0, 5).join('/') + '/';
var owner = typeof(opts.ownerKey) !== "undefined" ? opts.ownerKey : parsed.ownerKey;
if (owner) { hash += owner + '/'; }
if (opts.embed) { hash += 'embed/'; }
if (opts.present) { hash += 'present/'; }
return hash;
@ -177,9 +205,12 @@ Version 1
parsed.password = options.indexOf('p') !== -1;
parsed.present = options.indexOf('present') !== -1;
parsed.embed = options.indexOf('embed') !== -1;
parsed.ownerKey = getOwnerKey(options);
parsed.getHash = function (opts) {
var hash = hashArr.slice(0, 5).join('/') + '/';
var owner = typeof(opts.ownerKey) !== "undefined" ? opts.ownerKey : parsed.ownerKey;
if (owner) { hash += owner + '/'; }
if (parsed.password) { hash += 'p/'; }
if (opts.embed) { hash += 'embed/'; }
if (opts.present) { hash += 'present/'; }
@ -196,6 +227,8 @@ Version 1
parsed.version = 1;
parsed.channel = hashArr[2].replace(/-/g, '/');
parsed.key = hashArr[3].replace(/-/g, '/');
options = hashArr.slice(4);
parsed.ownerKey = getOwnerKey(options);
return parsed;
}
if (hashArr[1] && hashArr[1] === '2') { // Version 2
@ -207,9 +240,12 @@ Version 1
parsed.password = options.indexOf('p') !== -1;
parsed.present = options.indexOf('present') !== -1;
parsed.embed = options.indexOf('embed') !== -1;
parsed.ownerKey = getOwnerKey(options);
parsed.getHash = function (opts) {
var hash = hashArr.slice(0, 4).join('/') + '/';
var owner = typeof(opts.ownerKey) !== "undefined" ? opts.ownerKey : parsed.ownerKey;
if (owner) { hash += owner + '/'; }
if (parsed.password) { hash += 'p/'; }
if (opts.embed) { hash += 'embed/'; }
if (opts.present) { hash += 'present/'; }

@ -161,6 +161,17 @@ define([
return h('p.msg', h('input', attrs));
};
dialog.textTypeInput = function (dropdown) {
var attrs = {
type: 'text',
'class': 'cp-text-type-input',
};
return h('p.msg.cp-alertify-type-container', h('div.cp-alertify-type', [
h('input', attrs),
dropdown // must be a "span"
]));
};
dialog.nav = function (content) {
return h('nav', content || [
dialog.cancelButton(),
@ -186,6 +197,7 @@ define([
});
};
return $frame.click(function (e) {
$frame.find('.cp-dropdown-content').hide();
e.stopPropagation();
})[0];
};
@ -209,10 +221,16 @@ define([
$(title).prepend(' ').prepend(icon);
}
$(title).click(function () {
var old = tabs[active];
if (old.onHide) { old.onHide(); }
titles.forEach(function (t) { $(t).removeClass('alertify-tabs-active'); });
contents.forEach(function (c) { $(c).removeClass('alertify-tabs-content-active'); });
if (tab.onShow) {
tab.onShow();
}
$(title).addClass('alertify-tabs-active');
$(content).addClass('alertify-tabs-content-active');
active = i;
});
titles.push(title);
contents.push(content);
@ -474,7 +492,8 @@ define([
cb = cb || function () {};
opt = opt || {};
var inputBlock = opt.password ? UI.passwordInput() : dialog.textInput();
var inputBlock = opt.password ? UI.passwordInput() :
(opt.typeInput ? dialog.textTypeInput(opt.typeInput) : dialog.textInput());
var input = $(inputBlock).is('input') ? inputBlock : $(inputBlock).find('input')[0];
input.value = typeof(def) === 'string'? def: '';

@ -921,60 +921,79 @@ define([
className: 'primary cp-share-with-friends',
name: Messages.share_withFriends,
onClick: function () {
var href = Hash.getRelativeHref(linkGetter());
var $friends = $div.find('.cp-usergrid-user.cp-selected');
$friends.each(function (i, el) {
var curve = $(el).attr('data-curve');
// Check if the selected element is a friend or a team
if (curve) { // Friend
if (!curve || !friends[curve]) { return; }
var friend = friends[curve];
if (!friend.notifications || !friend.curvePublic) { return; }
common.mailbox.sendTo("SHARE_PAD", {
var href;
NThen(function (waitFor) {
var w = waitFor();
// linkGetter can be async if this is a burn after reading URL
var res = linkGetter({}, function (url) {
if (!url) {
waitFor.abort();
return;
}
console.warn('BAR');
href = url;
setTimeout(w);
});
if (res && /^http/.test(res)) {
href = Hash.getRelativeHref(res);
setTimeout(w);
return;
}
}).nThen(function () {
var $friends = $div.find('.cp-usergrid-user.cp-selected');
$friends.each(function (i, el) {
var curve = $(el).attr('data-curve');
// Check if the selected element is a friend or a team
if (curve) { // Friend
if (!curve || !friends[curve]) { return; }
var friend = friends[curve];
if (!friend.notifications || !friend.curvePublic) { return; }
common.mailbox.sendTo("SHARE_PAD", {
href: href,
password: config.password,
isTemplate: config.isTemplate,
name: myName,
title: title
}, {
channel: friend.notifications,
curvePublic: friend.curvePublic
});
return;
}
// Team
var ed = $(el).attr('data-ed');
var team = teams[ed];
if (!team) { return; }
sframeChan.query('Q_STORE_IN_TEAM', {
href: href,
password: config.password,
isTemplate: config.isTemplate,
name: myName,
title: title
}, {
channel: friend.notifications,
curvePublic: friend.curvePublic
path: config.isTemplate ? ['template'] : undefined,
title: title,
teamId: team.id
}, function (err) {
if (err) { return void console.error(err); }
});
return;
}
// Team
var ed = $(el).attr('data-ed');
var team = teams[ed];
if (!team) { return; }
sframeChan.query('Q_STORE_IN_TEAM', {
href: href,
password: config.password,
path: config.isTemplate ? ['template'] : undefined,
title: title,
teamId: team.id
}, function (err) {
if (err) { return void console.error(err); }
});
});
UI.findCancelButton().click();
// Update the "recently shared with" array:
// Get the selected curves
var curves = $friends.toArray().map(function (el) {
return ($(el).attr('data-curve') || '').slice(0,8);
}).filter(function (x) { return x; });
// Prepend them to the "order" array
Array.prototype.unshift.apply(order, curves);
order = Util.deduplicateString(order);
// Make sure we don't have "old" friends and save
order = order.filter(function (curve) {
return smallCurves.indexOf(curve) !== -1;
UI.findCancelButton().click();
// Update the "recently shared with" array:
// Get the selected curves
var curves = $friends.toArray().map(function (el) {
return ($(el).attr('data-curve') || '').slice(0,8);
}).filter(function (x) { return x; });
// Prepend them to the "order" array
Array.prototype.unshift.apply(order, curves);
order = Util.deduplicateString(order);
// Make sure we don't have "old" friends and save
order = order.filter(function (curve) {
return smallCurves.indexOf(curve) !== -1;
});
common.setAttribute(['general', 'share-friends'], order);
if (onShare) {
onShare.fire();
}
});
common.setAttribute(['general', 'share-friends'], order);
if (onShare) {
onShare.fire();
}
},
keys: [13]
};
@ -1053,6 +1072,29 @@ define([
}
};
var makeBurnAfterReadingUrl = function (common, href, channel, cb) {
var keyPair = Hash.generateSignPair();
var parsed = Hash.parsePadUrl(href);
console.error(href, parsed);
var newHref = parsed.getUrl({
ownerKey: keyPair.safeSignKey
});
var sframeChan = common.getSframeChannel();
NThen(function (waitFor) {
sframeChan.query('Q_SET_PAD_METADATA', {
channel: channel,
command: 'ADD_OWNERS',
value: [keyPair.validateKey]
}, waitFor(function (err) {
if (err) {
waitFor.abort();
UI.warn(Messages.error);
}
}));
}).nThen(function () {
cb(newHref);
});
};
UIElements.createShareModal = function (config) {
var origin = config.origin;
var pathname = config.pathname;
@ -1082,6 +1124,7 @@ define([
var parsed = Hash.parsePadUrl(pathname);
var canPresent = ['code', 'slide'].indexOf(parsed.type) !== -1;
var burnAfterReading;
var rights = h('div.msg.cp-inline-radio-group', [
h('label', Messages.share_linkAccess),
h('div.radio-group',[
@ -1090,9 +1133,33 @@ define([
canPresent ? UI.createRadio('accessRights', 'cp-share-present',
Messages.share_linkPresent, false, { mark: {tabindex:1} }) : undefined,
UI.createRadio('accessRights', 'cp-share-editable-true',
Messages.share_linkEdit, false, { mark: {tabindex:1} })])
Messages.share_linkEdit, false, { mark: {tabindex:1} })]),
burnAfterReading = hashes.viewHash ? UI.createRadio('accessRights', 'cp-share-bar', Messages.burnAfterReading_linkBurnAfterReading ||
'View once and self-destruct', false, { mark: {tabindex:1}, label: {style: "display: none;"} }) : undefined // XXX temp KEY
]);
// Burn after reading
// Check if we are an owner of this pad. If we are, we can show the burn after reading option.
// When BAR is selected, display a red message indicating the consequence and add
// the options to generate the BAR url
var barAlert = h('div.alert.alert-danger.cp-alertify-bar-selected', {
style: 'display: none;'
}, Messages.burnAfterReading_warningLink || " You have set this pad to self-destruct. Once a recipient opens this pad, it will be permanently deleted from the server."); // XXX temp KEY
var channel = Hash.getSecrets('pad', hash, config.password).channel;
common.getPadMetadata({
channel: channel
}, function (obj) {
if (!obj || obj.error) { return; }
var priv = common.getMetadataMgr().getPrivateData();
// Not an owner: don't display the burn after reading option
if (!Array.isArray(obj.owners) || obj.owners.indexOf(priv.edPublic) === -1) {
$(burnAfterReading).remove();
return;
}
// When the burn after reading option is selected, transform the modal buttons
$(burnAfterReading).show();
});
var $rights = $(rights);
var saveValue = function () {
@ -1104,13 +1171,25 @@ define([
});
};
var getLinkValue = function (initValue) {
var burnAfterReadingUrl;
var getLinkValue = function (initValue, cb) {
var val = initValue || {};
var edit = val.edit !== undefined ? val.edit : Util.isChecked($rights.find('#cp-share-editable-true'));
var embed = val.embed;
var present = val.present !== undefined ? val.present : Util.isChecked($rights.find('#cp-share-present'));
var burnAfterReading = Util.isChecked($rights.find('#cp-share-bar'));
if (burnAfterReading && !burnAfterReadingUrl) {
if (cb) { // Called from the contacts tab, "share" button
var barHref = origin + pathname + '#' + (hashes.viewHash || hashes.editHash);
return makeBurnAfterReadingUrl(common, barHref, channel, function (url) {
cb(url);
});
}
return Messages.burnAfterReading_generateLink || 'Click on the button below to generate a link'; // XXX temp KEY
}
var hash = (!hashes.viewHash || (edit && hashes.editHash)) ? hashes.editHash : hashes.viewHash;
var href = origin + pathname + '#' + hash;
var href = burnAfterReading ? burnAfterReadingUrl : (origin + pathname + '#' + hash);
var parsed = Hash.parsePadUrl(href);
return origin + parsed.getUrl({embed: embed, present: present});
};
@ -1164,8 +1243,8 @@ define([
});
});
linkContent.push($(barAlert).clone()[0]); // Burn after reading
var link = h('div.cp-share-modal', linkContent);
var $link = $(link);
@ -1173,7 +1252,7 @@ define([
var linkButtons = [
makeCancelButton(),
!config.sharedFolder && {
className: 'secondary',
className: 'secondary cp-nobar',
name: Messages.share_linkOpen,
onClick: function () {
saveValue();
@ -1184,9 +1263,8 @@ define([
return true;
},
keys: [[13, 'ctrl']]
},
{
className: 'primary',
}, {
className: 'primary cp-nobar',
name: Messages.share_linkCopy,
onClick: function () {
saveValue();
@ -1197,26 +1275,26 @@ define([
if (success) { UI.log(Messages.shareSuccess); }
},
keys: [13]
}, {
className: 'primary cp-bar',
name: 'GENERATE LINK',
onClick: function () {
var barHref = origin + pathname + '#' + (hashes.viewHash || hashes.editHash);
makeBurnAfterReadingUrl(common, barHref, channel, function (url) {
burnAfterReadingUrl = url;
$rights.find('input[type="radio"]').trigger('change');
});
return true;
},
keys: []
}
];
// update values for link preview when radio btns change
$link.find('#cp-share-link-preview').val(getLinkValue());
$rights.find('input[type="radio"]').on('change', function () {
$link.find('#cp-share-link-preview').val(getLinkValue({
embed: Util.isChecked($link.find('#cp-share-embed'))
}));
});
$link.find('input[type="checkbox"]').on('change', function () {
$link.find('#cp-share-link-preview').val(getLinkValue({
embed: Util.isChecked($link.find('#cp-share-embed'))
}));
});
var frameLink = UI.dialog.customModal(link, {
buttons: linkButtons,
onClose: config.onClose,
});
$(frameLink).find('.cp-bar').hide();
// Share with contacts tab
@ -1244,10 +1322,17 @@ define([
]));
}
$(contactsContent).append($(barAlert).clone()); // Burn after reading
var contactButtons = friendsObject.buttons;
contactButtons.unshift(makeCancelButton());
var onShowContacts = function () {
if (!hasFriends) {
$rights.hide();
}
};
var frameContacts = UI.dialog.customModal(contactsContent, {
buttons: contactButtons,
onClose: config.onClose,
@ -1286,26 +1371,60 @@ define([
keys: [13]
}];
var onShowEmbed = function () {
$rights.find('#cp-share-bar').closest('label').hide();
$rights.find('input[type="radio"]:enabled').first().prop('checked', 'checked');
$rights.find('input[type="radio"]').trigger('change');
};
var embed = h('div.cp-share-modal', embedContent);
var $embed = $(embed);
// update values for link preview when radio btns change
var frameEmbed = UI.dialog.customModal(embed, {
buttons: embedButtons,
onClose: config.onClose,
});
// update values for link and embed preview when radio btns change
$embed.find('#cp-embed-link-preview').val(getEmbedValue());
$link.find('#cp-share-link-preview').val(getLinkValue());
$rights.find('input[type="radio"]').on('change', function () {
$link.find('#cp-share-link-preview').val(getLinkValue({
embed: Util.isChecked($link.find('#cp-share-embed'))
}));
// Hide or show the burn after reading alert
if (Util.isChecked($rights.find('#cp-share-bar')) && !burnAfterReadingUrl) {
$('.cp-alertify-bar-selected').show();
// Show burn after reading button
$('.alertify').find('.cp-bar').show();
$('.alertify').find('.cp-nobar').hide();
return;
}
$embed.find('#cp-embed-link-preview').val(getEmbedValue());
// Hide burn after reading button
$('.alertify').find('.cp-nobar').show();
$('.alertify').find('.cp-bar').hide();
$('.cp-alertify-bar-selected').hide();
});
var frameEmbed = UI.dialog.customModal(embed, {
buttons: embedButtons,
onClose: config.onClose,
$link.find('input[type="checkbox"]').on('change', function () {
$link.find('#cp-share-link-preview').val(getLinkValue({
embed: Util.isChecked($link.find('#cp-share-embed'))
}));
});
// Create modal
var resetTab = function () {
$rights.show();
$rights.find('label.cp-radio').show();
};
var tabs = [{
title: Messages.share_contactCategory,
icon: "fa fa-address-book",
content: frameContacts,
active: hasFriends
active: hasFriends,
onShow: onShowContacts,
onHide: resetTab
}, {
title: Messages.share_linkCategory,
icon: "fa fa-link",
@ -1314,7 +1433,9 @@ define([
}, {
title: Messages.share_embedCategory,
icon: "fa fa-code",
content: frameEmbed
content: frameEmbed,
onShow: onShowEmbed,
onHide: resetTab
}];
if (typeof(AppConfig.customizeShareOptions) === 'function') {
AppConfig.customizeShareOptions(hashes, tabs, {
@ -1643,6 +1764,11 @@ define([
h('p', Messages.team_inviteLinkTitle ),
linkError = h('div.alert.alert-danger.cp-teams-invite-alert', {style : 'display: none;'}),
linkForm = h('div.cp-teams-invite-form', [
// autofill: 'off' was insufficient
// adding these two fake inputs confuses firefox and prevents unwanted form autofill
h('input', { type: 'text', style: 'display: none'}),
h('input', { type: 'password', style: 'display: none'}),
linkName = h('input', {
placeholder: Messages.team_inviteLinkTempName
}),
@ -2774,9 +2900,11 @@ define([
var $button = $('<button>', {
'class': ''
}).append($('<span>', {'class': 'cp-dropdown-button-title'}).html(config.text || ""));
/*$('<span>', {
'class': 'fa fa-caret-down',
}).appendTo($button);*/
if (config.caretDown) {
$('<span>', {
'class': 'fa fa-caret-down',
}).prependTo($button);
}
// Menu
var $innerblock = $('<div>', {'class': 'cp-dropdown-content'});
@ -2843,6 +2971,9 @@ define([
if (config.isSelect) {
var pressed = '';
var to;
$container.on('click', 'a', function () {
value = $(this).data('value');
});
$container.keydown(function (e) {
var $value = $innerblock.find('[data-value].cp-dropdown-element-active:visible');
if (e.which === 38) { // Up
@ -3865,6 +3996,7 @@ define([
UIElements.onServerError = function (common, err, toolbar, cb) {
if (["EDELETED", "EEXPIRED"].indexOf(err.type) === -1) { return; }
var priv = common.getMetadataMgr().getPrivateData();
var msg = err.type;
if (err.type === 'EEXPIRED') {
msg = Messages.expiredError;
@ -3872,11 +4004,14 @@ define([
msg += Messages.errorCopy;
}
} else if (err.type === 'EDELETED') {
if (priv.burnAfterReading) { return void cb(); }
msg = Messages.deletedError;
if (err.loaded) {
msg += Messages.errorCopy;
}
}
var sframeChan = common.getSframeChannel();
sframeChan.event('EV_SHARE_OPEN', {hidden: true});
if (toolbar && typeof toolbar.deleted === "function") { toolbar.deleted(); }
UI.errorLoadingScreen(msg, true, true);
(cb || function () {})();
@ -3921,6 +4056,26 @@ define([
$password.find('.cp-password-input').focus();
};
UIElements.displayBurnAfterReadingPage = function (common, cb) {
var info = h('p.cp-password-info', Messages.burnAfterReading_warning || 'This document will self-destruct as soon as you open it. It will be removed form the server, once you close this window you will not be able to access it again. If you are not ready to proceed you can close this window and come back later. '); // XXX temp KEY
var button = h('button.primary', Messages.burnAfterReading_proceed || 'view and delete'); // XXX temp KEY
$(button).on('click', function () {
cb();
});
var block = h('div#cp-loading-burn-after-reading', [
info,
button
]);
UI.errorLoadingScreen(block);
};
UIElements.getBurnAfterReadingWarning = function (common) {
var priv = common.getMetadataMgr().getPrivateData();
if (!priv.burnAfterReading) { return; }
return h('div.alert.alert-danger.cp-burn-after-reading', Messages.burnAfterReading_warningDeleted || 'This pad has been deleted from the server, once you close this window you will not be able to access it again.'); // XXX temp KEY
};
var crowdfundingState = false;
UIElements.displayCrowdfunding = function (common) {
if (crowdfundingState) { return; }
@ -3978,6 +4133,9 @@ define([
if (data && data.stored) { return; } // We won't display the popup for dropped files
var priv = common.getMetadataMgr().getPrivateData();
// This pad will be deleted automatically, it shouldn't be stored
if (priv.burnAfterReading) { return; }
var typeMsg = priv.pathname.indexOf('/file/') !== -1 ? Messages.autostore_file :
priv.pathname.indexOf('/drive/') !== -1 ? Messages.autostore_sf :
Messages.autostore_pad;

@ -3,7 +3,7 @@
// polyfill for atob in case you're using this from node...
window.atob = window.atob || function (str) { return Buffer.from(str, 'base64').toString('binary'); }; // jshint ignore:line
window.btoa = window.btoa || function (str) { return new Buffer(str, 'binary').toString('base64'); }; // jshint ignore:line
window.btoa = window.btoa || function (str) { return Buffer.from(str, 'binary').toString('base64'); }; // jshint ignore:line
Util.slice = function (A, start, end) {
return Array.prototype.slice.call(A, start, end);

@ -728,7 +728,7 @@ define([
}).nThen(function () {
postMessage("SET_PAD_TITLE", {
teamId: data.teamId,
href: data.href,
href: Hash.getRelativeHref(data.href),
title: data.title,
password: data.password,
channel: secret.channel,
@ -847,6 +847,10 @@ define([
postMessage('GET_PAD_METADATA', data, cb);
};
common.burnPad = function (data) {
postMessage('BURN_PAD', data);
};
common.changePadPassword = function (Crypt, Crypto, data, cb) {
var href = data.href;
var newPassword = data.password;

@ -3008,7 +3008,7 @@ define([
r.paths.forEach(function (path) {
if (!r.inSharedFolder &&
APP.hideDuplicateOwned && manager.isDuplicateOwned(path)) { return; }
var href = r.data.href;
var href = r.data.href || r.data.roHref;
var parsed = Hash.parsePadUrl(href);
var $table = $('<table>');
var $icon = $('<td>', {'rowspan': '3', 'class': 'cp-app-drive-search-icon'});

@ -597,7 +597,9 @@ define([
ooChannel.cpIndex++;
ooChannel.lastHash = hash;
// Check if a checkpoint is needed
if (ooChannel.cpIndex % CHECKPOINT_INTERVAL === 0) {
var lastCp = getLastCp();
if (common.isLoggedIn() && (ooChannel.cpIndex % CHECKPOINT_INTERVAL === 0 ||
(ooChannel.cpIndex - lastCp.index) > CHECKPOINT_INTERVAL)) {
makeCheckpoint();
}
// Remove my lock
@ -686,7 +688,7 @@ define([
var startOO = function (blob, file) {
if (APP.ooconfig) { return void console.error('already started'); }
var url = URL.createObjectURL(blob);
var lock = readOnly || !common.isLoggedIn();
var lock = readOnly;// || !common.isLoggedIn();
// Config
APP.ooconfig = {
@ -1276,6 +1278,7 @@ define([
$rightside.append($forget);
var helpMenu = common.createHelpMenu(['beta', 'oo']);
$('#cp-app-oo-editor').prepend(common.getBurnAfterReadingWarning());
$('#cp-app-oo-editor').prepend(helpMenu.menu);
toolbar.$drawer.append(helpMenu.button);

@ -9,6 +9,7 @@ define([
'/common/common-feedback.js',
'/common/common-realtime.js',
'/common/common-messaging.js',
'/common/pinpad.js',
'/common/outer/sharedfolder.js',
'/common/outer/cursor.js',
'/common/outer/onlyoffice.js',
@ -26,7 +27,7 @@ define([
'/bower_components/nthen/index.js',
'/bower_components/saferphore/index.js',
], function (Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback,
Realtime, Messaging,
Realtime, Messaging, Pinpad,
SF, Cursor, OnlyOffice, Mailbox, Profile, Team, Messenger,
NetConfig, AppConfig,
Crypto, ChainPad, CpNetflux, Listmap, nThen, Saferphore) {
@ -409,19 +410,17 @@ define([
var initRpc = function (clientId, data, cb) {
if (!store.loggedIn) { return cb(); }
if (store.rpc) { return void cb(account); }
require(['/common/pinpad.js'], function (Pinpad) {
Pinpad.create(store.network, store.proxy, function (e, call) {
if (e) { return void cb({error: e}); }
Pinpad.create(store.network, store.proxy, function (e, call) {
if (e) { return void cb({error: e}); }
store.rpc = call;
store.rpc = call;
Store.getPinLimit(null, null, function (obj) {
if (obj.error) { console.error(obj.error); }
account.limit = obj.limit;
account.plan = obj.plan;
account.note = obj.note;
cb(obj);
});
Store.getPinLimit(null, null, function (obj) {
if (obj.error) { console.error(obj.error); }
account.limit = obj.limit;
account.plan = obj.plan;
account.note = obj.note;
cb(obj);
});
});
};
@ -1653,6 +1652,73 @@ define([
cb();
};
// Delete a pad received with a burn after reading URL
var notifyOwnerPadRemoved = function (data, obj) {
var channel = data.channel;
var href = data.href;
var parsed = Hash.parsePadUrl(href);
var secret = Hash.getSecrets(parsed.type, parsed.hash, data.password);
if (obj && obj.error) { return; }
if (!obj.mailbox) { return; }
// Decrypt the mailbox
var crypto = Crypto.createEncryptor(secret.keys);
var m = [];
try {
if (typeof (obj.mailbox) === "string") {
m.push(crypto.decrypt(obj.mailbox, true, true));
} else {
Object.keys(obj.mailbox).forEach(function (k) {
m.push(crypto.decrypt(obj.mailbox[k], true, true));
});
}
} catch (e) {
console.error(e);
}
// Tell all the owners that the pad was deleted from the server
var curvePublic = store.proxy.curvePublic;
var myData = Messaging.createData(store.proxy, false);
m.forEach(function (obj) {
var mb = JSON.parse(obj);
if (mb.curvePublic === curvePublic) { return; }
store.mailbox.sendTo('OWNED_PAD_REMOVED', {
channel: channel,
user: myData
}, {
channel: mb.notifications,
curvePublic: mb.curvePublic
}, function () {});
});
};
Store.burnPad = function (clientId, data) {
var channel = data.channel;
var ownerKey = Crypto.b64AddSlashes(data.ownerKey || '');
if (!channel || !ownerKey) { return void console.error("Can't delete BAR pad"); }
try {
var signKey = Hash.decodeBase64(ownerKey);
var pair = Crypto.Nacl.sign.keyPair.fromSecretKey(signKey);
Pinpad.create(store.network, {
edPublic: Hash.encodeBase64(pair.publicKey),
edPrivate: Hash.encodeBase64(pair.secretKey)
}, function (e, rpc) {
if (e) { return void console.error(e); }
Store.getPadMetadata(null, {
channel: channel
}, function (md) {
rpc.removeOwnedChannel(channel, function (err) {
if (err) { return void console.error(err); }
// Notify owners that the pad was removed
notifyOwnerPadRemoved(data, md);
});
});
});
} catch (e) {
console.error(e);
}
};
// Fetch the latest version of the metadata on the server and return it.
// If the pad is stored in our drive, update the local values of "owners" and "expire"
Store.getPadMetadata = function (clientId, data, cb) {
@ -2109,6 +2175,11 @@ define([
updateMetadata: function () {
broadcast([], "UPDATE_METADATA");
},
updateDrive: function () {
sendDriveEvent('DRIVE_CHANGE', {
path: ['drive', 'filesData']
});
},
pinPads: function (data, cb) { Store.pinPads(null, data, cb); },
}, waitFor, function (ev, data, clients, _cb) {
var cb = Util.once(_cb || function () {});

@ -17,6 +17,7 @@ var factory = function (Util, Cred, Nacl) {
};
};
// XXX move this function?
Invite.generateSignPair = function () {
var ed = Nacl.sign.keyPair();
return {

@ -482,6 +482,31 @@ define([
cb(true);
};
handlers['OWNED_PAD_REMOVED'] = function (ctx, box, data, cb) {
var msg = data.msg;
var content = msg.content;
if (msg.author !== content.user.curvePublic) { return void cb(true); }
if (!content.channel) {
console.log('Remove invalid notification');
return void cb(true);
}
var channel = content.channel;
var res = ctx.store.manager.findChannel(channel);
res.forEach(function (obj) {
var paths = ctx.store.manager.findFile(obj.id);
ctx.store.manager.delete({
paths: paths
}, function () {
ctx.updateDrive();
});
});
cb(true);
};
return {

@ -422,6 +422,7 @@ proxy.mailboxes = {
store: store,
pinPads: cfg.pinPads,
updateMetadata: cfg.updateMetadata,
updateDrive: cfg.updateDrive,
emit: emit,
clients: [],
boxes: {},

@ -4,8 +4,7 @@ define([
var Config = {};
Config.getWebsocketURL = function (origin) {
if (!ApiConfig.websocketPath) { return ApiConfig.websocketURL; }
var path = ApiConfig.websocketPath;
var path = ApiConfig.websocketPath || '/cryptpad_websocket';
if (/^ws{1,2}:\/\//.test(path)) { return path; }
var l = window.location;

@ -80,6 +80,7 @@ define([
IS_NEW_CHANNEL: Store.isNewChannel,
REQUEST_PAD_ACCESS: Store.requestPadAccess,
GIVE_PAD_ACCESS: Store.givePadAccess,
BURN_PAD: Store.burnPad,
GET_PAD_METADATA: Store.getPadMetadata,
SET_PAD_METADATA: Store.setPadMetadata,
CHANGE_PAD_PASSWORD_PIN: Store.changePadPasswordPin,

@ -1035,6 +1035,7 @@ define([
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return true; }
var team = ctx.teams[teamId];
if (!team) { return true; }
var secret = Hash.getSecrets('team', hash || teamData.roHash, teamData.password);
// Upgrade the listmap if we can
@ -1440,7 +1441,6 @@ define([
var json = Util.tryParse(val);
if (!json) { return void cb({ error: "parseError" }); }
console.error("JSON", json);
cb(json);
}, { // cryptget opts
network: ctx.store.network,

@ -119,6 +119,7 @@ define([
// If it's not a shared folder, check the pads
if (!data) { data = Env.user.userObject.getFileData(id, editable); }
ret.push({
id: id,
data: data,
userObject: Env.user.userObject
});
@ -126,6 +127,7 @@ define([
Object.keys(Env.folders).forEach(function (fId) {
Env.folders[fId].userObject.findChannels([channel]).forEach(function (id) {
ret.push({
id: id,
fId: fId,
data: Env.folders[fId].userObject.getFileData(id, editable),
userObject: Env.folders[fId].userObject
@ -1095,9 +1097,11 @@ define([
// Store
getChannelsList: callWithEnv(getChannelsList),
addPad: callWithEnv(addPad),
delete: callWithEnv(_delete),
// Tools
findChannel: callWithEnv(findChannel),
findHref: callWithEnv(findHref),
findFile: callWithEnv(findFile),
getEditHash: callWithEnv(getEditHash),
user: Env.user,
folders: Env.folders

@ -6,9 +6,11 @@ define([
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/customize/messages.js',
'/common/hyperscript.js',
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-interface.js',
'/common/common-ui-elements.js',
'/common/common-thumbnail.js',
'/common/common-feedback.js',
'/customize/application_config.js',
@ -26,9 +28,11 @@ define([
nThen,
SFCommon,
Messages,
h,
Util,
Hash,
UI,
UIElements,
Thumb,
Feedback,
AppConfig,
@ -408,10 +412,36 @@ define([
var $export = common.createButton('export', true, {}, function () {
var ext = (typeof(extension) === 'function') ? extension() : extension;
var suggestion = title.suggestTitle('cryptpad-document');
ext = ext || '.txt';
var types = [{
tag: 'a',
attributes: {
'data-value': ext,
'href': '#'
},
content: ext
}, {
tag: 'a',
attributes: {
'data-value': '',
'href': '#'
},
content: '&nbsp;'
}];
var dropdownConfig = {
text: ext, // Button initial text
caretDown: true,
options: types, // Entries displayed in the menu
isSelect: true,
initialValue: ext,
common: common
};
var $select = UIElements.createDropdown(dropdownConfig);
UI.prompt(Messages.exportPrompt,
Util.fixFileName(suggestion) + ext, function (filename)
Util.fixFileName(suggestion), function (filename)
{
if (!(typeof(filename) === 'string' && filename)) { return; }
filename = filename + $select.getValue();
if (async) {
fe(function (blob) {
SaveAs(blob, filename);
@ -420,6 +450,8 @@ define([
}
var blob = fe();
SaveAs(blob, filename);
}, {
typeInput: $select[0]
});
});
toolbar.$drawer.append($export);

@ -81,8 +81,8 @@ define([
});
localStorage.CRYPTPAD_URLARGS = ApiConfig.requireConf.urlArgs;
}
var cache = {};
var localStore = {};
var cache = window.cpCache = {};
var localStore = window.localStore = {};
Object.keys(localStorage).forEach(function (k) {
if (k.indexOf('CRYPTPAD_CACHE|') === 0) {
cache[k.slice(('CRYPTPAD_CACHE|').length)] = localStorage[k];
@ -323,6 +323,7 @@ define([
}
Utils.crypto = Utils.Crypto.createEncryptor(Utils.secret.keys);
var parsed = Utils.Hash.parsePadUrl(window.location.href);
var burnAfterReading = parsed && parsed.hashData && parsed.hashData.ownerKey;
if (!parsed.type) { throw new Error(); }
var defaultTitle = Utils.UserObject.getDefaultName(parsed);
var edPublic, curvePublic, notifications, isTemplate;
@ -376,6 +377,7 @@ define([
fromFileData: Cryptpad.fromFileData ? {
title: Cryptpad.fromFileData.title
} : undefined,
burnAfterReading: burnAfterReading,
storeInTeam: Cryptpad.initialTeam || (Cryptpad.initialPath ? -1 : undefined)
};
if (window.CryptPad_newSharedFolder) {
@ -507,6 +509,17 @@ define([
}
});
sframeChan.on('Q_GET_PAD_METADATA', function (data, cb) {
if (!data || !data.channel) {
data = {
channel: secret.channel
};
}
Cryptpad.getPadMetadata(data, cb);
});
sframeChan.on('Q_SET_PAD_METADATA', function (data, cb) {
Cryptpad.setPadMetadata(data, cb);
});
};
addCommonRpc(sframeChan);
@ -1170,18 +1183,6 @@ define([
});
});
sframeChan.on('Q_GET_PAD_METADATA', function (data, cb) {
if (!data || !data.channel) {
data = {
channel: secret.channel
};
}
Cryptpad.getPadMetadata(data, cb);
});
sframeChan.on('Q_SET_PAD_METADATA', function (data, cb) {
Cryptpad.setPadMetadata(data, cb);
});
if (cfg.messaging) {
Notifier.getPermission();
@ -1236,6 +1237,16 @@ define([
window.location.hash = hash;
};
if (burnAfterReading) {
Cryptpad.padRpc.onReadyEvent.reg(function () {
Cryptpad.burnPad({
password: password,
href: window.location.href,
channel: secret.channel,
ownerKey: burnAfterReading
});
});
}
var cpNfCfg = {
sframeChan: sframeChan,
channel: secret.channel,
@ -1359,12 +1370,17 @@ define([
});
});
sframeChan.on('EV_BURN_AFTER_READING', function () {
startRealtime();
});
sframeChan.ready();
Utils.Feedback.reportAppUsage();
if (!realtime && !Test.testing) { return; }
if (isNewFile && cfg.useCreationScreen && !Test.testing) { return; }
if (burnAfterReading) { return; }
//if (isNewFile && Utils.LocalStore.isLoggedIn()
// && AppConfig.displayCreationScreen && cfg.useCreationScreen) { return; }

@ -96,6 +96,7 @@ define([
funcs.createMarkdownToolbar = callWithCommon(UIElements.createMarkdownToolbar);
funcs.createHelpMenu = callWithCommon(UIElements.createHelpMenu);
funcs.getPadCreationScreen = callWithCommon(UIElements.getPadCreationScreen);
funcs.getBurnAfterReadingWarning = callWithCommon(UIElements.getBurnAfterReadingWarning);
funcs.createNewPadModal = callWithCommon(UIElements.createNewPadModal);
funcs.onServerError = callWithCommon(UIElements.onServerError);
funcs.importMediaTagMenu = callWithCommon(UIElements.importMediaTagMenu);
@ -300,6 +301,13 @@ define([
}
// If we display the pad creation screen, it will handle deleted pads directly
funcs.getPadCreationScreen(c, config, waitFor());
return;
}
if (priv.burnAfterReading) {
UIElements.displayBurnAfterReadingPage(funcs, waitFor(function () {
UI.addLoadingScreen();
ctx.sframeChan.event('EV_BURN_AFTER_READING');
}));
}
};
funcs.createPad = function (cfg, cb) {

@ -377,5 +377,377 @@
"fm_padIsOwnedOther": "Tämän padin omistaa toinen käyttäjä",
"fm_deletedPads": "Nämä padit eivät ole enää saatavilla palvelimella, ne on poistettu CryptDrivestasi: {0}",
"fm_tags_name": "Tunnisteen nimi",
"fm_tags_used": "Käyttökertojen määrä"
"fm_tags_used": "Käyttökertojen määrä",
"fm_restoreDrive": "Palautetaan Drivesi aikaisempaan tilaan. Saadaksesi parhaimman lopputuloksen vältä muutosten tekemistä Driveesi, kunnes toimenpide on valmis.",
"fm_moveNestedSF": "Et voi siirtää jaettua kansiota toisen jaetun kansion sisään. Kansiota {0} ei siirretty.",
"fm_passwordProtected": "Salasanasuojattu",
"fc_newfolder": "Uusi kansio",
"fc_newsharedfolder": "Uusi jaettu kansio",
"fc_rename": "Nimeä uudelleen",
"fc_color": "Vaihda väriä",
"fc_open": "Avaa",
"fc_open_ro": "Avaa (vain luku)",
"fc_openInCode": "Avaa Koodi-editorissa",
"fc_expandAll": "Laajenna kaikki",
"fc_collapseAll": "Tiivistä kaikki",
"fc_delete": "Siirrä roskakoriin",
"fc_delete_owned": "Poista palvelimelta",
"fc_restore": "Palauta",
"fc_remove": "Poista CryptDrivesta",
"fc_remove_sharedfolder": "Poista",
"fc_empty": "Tyhjennä roskakori",
"fc_prop": "Ominaisuudet",
"fc_hashtag": "Tunnisteet",
"fc_sizeInKilobytes": "Koko kilotavuissa",
"fo_moveUnsortedError": "Et voi siirtää kansiota mallipohjalistaan",
"fo_existingNameError": "Valitsemasi nimi on jo käytössä kansiossa. Ole hyvä ja valitse toinen nimi.",
"fo_moveFolderToChildError": "Et voi siirtää kansiota sen alikansioon",
"fo_unableToRestore": "Tiedoston palauttaminen sen alkuperäiseen sijaintiin ei onnistunut. Voit yrittää siirtää sen toiseen sijaintiin.",
"fo_unavailableName": "Valitsemassasi sijainnissa on jo samanniminen tiedosto tai kansio. Ole hyvä ja anna elementille uusi nimi ja yritä uudelleen.",
"fs_migration": "CryptDrivesi päivitetään uuteen versioon. Nykyinen sivu täytyy ladata uudelleen. <br><strong>Ole hyvä ja lataa sivu uudelleen jatkaaksesi käyttöä.</strong>",
"login_login": "Kirjaudu sisään",
"login_makeAPad": "Luo padi anonyyminä",
"login_nologin": "Selaa paikallisia padeja",
"login_register": "Rekisteröidy",
"logoutButton": "Kirjaudu ulos",
"settingsButton": "Asetukset",
"login_username": "Käyttäjänimi",
"login_password": "Salasana",
"login_confirm": "Vahvista salasanasi",
"login_remember": "Muista minut",
"login_hashing": "Salasanaasi hajautetaan, tämä saattaa kestää jonkin aikaa.",
"login_hello": "Hei {0},",
"login_helloNoName": "Hei,",
"login_accessDrive": "Käytä Driveasi",
"login_orNoLogin": "tai",
"login_noSuchUser": "VIrheellinen käyttäjätunnus tai salasana. Yritä uudelleen tai rekisteröidy",
"login_invalUser": "Käyttäjänimi vaaditaan",
"login_invalPass": "Salasana vaaditaan",
"login_unhandledError": "Tapahtui odottamaton virhe :(",
"register_importRecent": "Tuo padeja anonyymistä sessiosta",
"register_acceptTerms": "Hyväksyn <a href='/terms.html' tabindex='-1'>käyttöehdot</a>",
"register_passwordsDontMatch": "Salasanat eivät täsmää!",
"register_passwordTooShort": "Salasanan täytyy olla vähintään {0} merkkiä pitkä.",
"register_mustAcceptTerms": "Sinun täytyy hyväksyä käyttöehdot.",
"register_mustRememberPass": "Emme voi nollata salasanaasi, jos unohdat sen. On erittäin tärkeää, että muistat sen! Ole hyvä ja laita rasti valintaruutuun vahvistaaksesi.",
"register_whyRegister": "Miksi rekisteröityminen kannattaa?",
"register_header": "Tervetuloa CryptPadiin",
"register_explanation": "<h3>Käydään läpi muutama perusasia:</h3><ul class='list-unstyled'><li><i class='fa fa-info-circle'></i>Salasanasi on myös salausavain, jolla kaikki luomasi padit salataan. Jos kadotat salasanasi, emme voi millään tavalla palauttaa tietojasi.</li><li><i class='fa fa-info-circle'></i>Voit tuoda viimeksi katselemasi padit selaimestasi käyttäjätilillesi.</li><li><i class='fa fa-info-circle'> </i> Jos käytät jaettua tietokonetta, täytyy sinun kirjautua ulos lopettaessasi työskentelyn - välilehden sulkeminen ei riitä.</li></ul>",
"register_writtenPassword": "Olen kirjoittanut ylös käyttäjätunnukseni ja salasanani, jatka",
"register_cancel": "Takaisin",
"register_warning": "Nollatietoperiaate tarkoittaa, ettemme voi palauttaa tietojasi, jos hävität salasanasi.",
"register_alreadyRegistered": "Tämä käyttäjä on jo olemassa, haluatko kirjautua sisään?",
"register_emailWarning0": "Näyttää siltä, että lähetit sähköpostiosoitteesi käyttäjätunnuksenasi.",
"register_emailWarning1": "Voit halutessasi tehdä niin, mutta sitä ei lähetetä palvelimellemme.",
"register_emailWarning2": "Toisin kuin monissa muissa palveluissa, emme voi palauttaa salasanaasi sähköpostin avulla.",
"register_emailWarning3": "Jos ymmärrät tämän ja haluat silti käyttää sähköpostiosoitettasi käyttäjätunnuksenasi, klikkaa OK.",
"settings_cat_account": "Käyttäjätili",
"settings_cat_drive": "CryptDrive",
"settings_cat_cursor": "Kursori",
"settings_cat_code": "Koodi",
"settings_cat_pad": "Teksti",
"settings_cat_creation": "Uusi padi",
"settings_cat_subscription": "Tilaus",
"settings_title": "Asetukset",
"settings_save": "Tallenna",
"settings_backupCategory": "Varmuuskopiointi",
"settings_backupHint": "Varmuuskopioi tai palauta CryptDrivesi sisältö kokonaisuudessaan. Varmuuskopio ei sisällä padiesi sisältöä, ainoastaan niiden käyttöön tarvittavat avaimet.",
"settings_backup": "Varmuuskopioi",
"settings_restore": "Palauta",
"settings_backupHint2": "Lataa kaikkien padiesi nykyinen sisältö. Padit ladataan luettavassa tiedostomuodossa, jos sellainen on saatavilla.",
"settings_backup2": "Lataa oma CryptDrive tietokoneellesi",
"settings_backup2Confirm": "Tämä lataa kaikki CryptDrivesi padit ja tiedostot tietokoneellesi. Jos haluat jatkaa, valitse nimi ja paina OK",
"settings_exportTitle": "Vie oma CryptDrive",
"settings_exportDescription": "Odota hetki, dokumenttejasi puretaan ja ladataan. Tämä voi viedä muutaman minuutin. Välilehden sulkeminen keskeyttää toimenpiteen.",
"settings_exportFailed": "Jos padin lataamiseen menee enemmän kuin yksi (1) minuutti, sitä ei oteta mukaan vientiin. Viennistä pois jääneiden padien linkit näytetään lopuksi.",
"settings_exportWarning": "Huomautus: tämä työkalu on beta-versiossa, ja siinä saattaa olla skaalautuvuusongelmia. Suosittelemme välilehden jättämistä aktiiviseksi paremman suorituskyvyn takaamiseksi.",
"settings_exportCancel": "Haluatko varmasti keskeyttää viennin? Seuraavalla kerralla toimenpide täytyy aloittaa alusta.",
"settings_export_reading": "Luetaan CryptDrivea...",
"settings_export_download": "Ladataan ja puretaan dokumentteja...",
"settings_export_compressing": "Pakataan tiedostoja...",
"settings_export_done": "Latauksesi on valmis!",
"settings_exportError": "Tarkastele virheitä",
"settings_exportErrorDescription": "Emme onnistuneet lisäämään seuraavia dokumentteja vientipakettiin:",
"settings_exportErrorEmpty": "Tätä dokumenttia ei voi viedä (tyhjä tai virheellinen sisältö).",
"settings_exportErrorMissing": "Tätä dokumenttia ei löydy palvelimeltamme (vanhentunut tai omistajansa poistama)",
"settings_exportErrorOther": "Dokumenttia viedessä tapahtui virhe: {0}",
"settings_resetNewTitle": "Tyhjennä CryptDrive",
"settings_resetButton": "Poista",
"settings_reset": "Poista kaikki tiedostot ja kansiot CryptDrivestasi",
"settings_resetPrompt": "Tämä toiminto poistaa kaikki padit CryptDrivestasi.<br>Haluatko varmasti jatkaa?<br>Kirjoita \"<em>I love CryptPad</em>\" vahvistaaksesi.",
"settings_resetDone": "Drivesi on nyt tyhjennetty!",
"settings_resetError": "Virheellinen vahvistusteksti. CryptDriveasi ei ole muutettu.",
"settings_resetTipsAction": "Nollaa",
"settings_resetTips": "Vinkit",
"settings_resetTipsButton": "Nollaa CryptDriven vinkit",
"settings_resetTipsDone": "Kaikki vinkit tulevat jälleen näkyviin.",
"settings_thumbnails": "Pienoiskuvat",
"settings_disableThumbnailsAction": "Ota pienoiskuvien luonti pois käytöstä CryptDrivessa",
"settings_disableThumbnailsDescription": "Pienoiskuvat luodaan automaattisesti uuden padin käytön yhteydessä, ja ne säilytetään selaimessasi. Voit ottaa tämän ominaisuuden pois käytöstä.",
"settings_resetThumbnailsAction": "Tyhjennä",
"settings_resetThumbnailsDescription": "Tyhjennä kaikki padien pienoiskuvat selaimesi välimuistista.",
"settings_resetThumbnailsDone": "Kaikki pienoiskuvat on poistettu.",
"settings_importTitle": "Tuo tässä selaimessa viimeksi käytetyt padit CryptDriveesi",
"settings_import": "Tuo",
"settings_importConfirm": "Haluatko varmasti tuoda tässä selaimessa viimeksi käytetyt padit käyttäjätilisi CryptDriveen?",
"settings_importDone": "Tuonti valmis",
"settings_autostoreTitle": "Padien tallennus CryptDrivessa",
"settings_autostoreHint": "<b>Automaattinen</b>Kaikki käyttämäsi padit tallennetaan CryptDriveesi.<br><b>Manuaalinen (kysy aina)</b> Jos et ole vielä tallentanut padia, kysytään sinulta, haluatko tallentaa sen CryptDriveesi.<br><b>Manuaalinen (älä kysy)</b>Padeja ei tallenneta automaattisesti CryptDriveesi. Padien tallennusmahdollisuus piilotetaan.",
"settings_autostoreYes": "Automaattinen",
"settings_autostoreNo": "Manuaalinen (älä kysy)",
"settings_autostoreMaybe": "Manuaalinen (kysy aina)",
"settings_userFeedbackTitle": "Palaute",
"settings_userFeedbackHint1": "CryptPad tarjoaa palvelimelle hyvin yksinkertaista palautetta kertoakseen meille, miten voimme kehittää käyttäjäkokemustasi. ",
"settings_userFeedbackHint2": "Padisi sisältöä ei koskaan jaeta palvelimen kanssa.",
"settings_userFeedback": "Salli käyttäjäpalaute",
"settings_deleteTitle": "Käyttäjätilin poisto",
"settings_deleteHint": "Käyttäjätilin poisto on pysyvä toimenpide. CryptDrivesi ja lista padeistasi poistetaan palvelimelta. Loput padeistasi poistetaan 90 päivän kuluttua, jos kukaan muu ei ole tallentanut niitä omaan CryptDriveensa.",
"settings_deleteButton": "Poista käyttäjätilisi",
"settings_deleteModal": "Jaa seuraavat tiedot CryptPad-instanssisi ylläpitäjän kanssa poistaaksesi tietosi palvelimelta.",
"settings_deleteConfirm": "Klikkaamalla OK käyttäjätilisi poistetaan pysyvästi. Oletko varma?",
"settings_deleted": "Käyttäjätilisi on nyt poistettu. Klikkaa OK siirtyäksesi kotisivulle.",
"settings_anonymous": "Et ole kirjautunut sisään. Nämä asetukset koskevat vain tätä selainta.",
"settings_publicSigningKey": "Julkinen salausavain",
"settings_usage": "Käyttö",
"settings_usageTitle": "Näytä kiinnitettyjen padien koko megatavuissa",
"settings_pinningNotAvailable": "Kiinnitetyt padit ovat saatavilla ainoastaan kirjautuneille käyttäjille.",
"settings_pinningError": "Jokin meni pieleen",
"settings_usageAmount": "Kiinnitetyt padisi käyttävät {0}Mt",
"settings_logoutEverywhereButton": "Kirjaudu ulos",
"settings_logoutEverywhereTitle": "Kirjaudu ulos kaikkialta",
"settings_logoutEverywhere": "Pakota uloskirjautuminen kaikista web-sessioista",
"settings_logoutEverywhereConfirm": "Oletko varma? Joudut kirjautumaan kaikilla laitteillasi uudelleen sisään.",
"settings_driveDuplicateTitle": "Omistettujen padien kaksoiskappaleet",
"settings_driveDuplicateHint": "Siirtäessäsi omistettuja padeja jaettuun kansioon omassa CryptDrivessasi säilytetään niistä kopio, jotta tiedosto pysyy hallinnassasi. Voit piilottaa tiedostojen kaksoiskappaleet. Vain jaettu versio jää näkyville, ellei sitä poisteta. Tässä tapauksessa alkuperäinen padi näytetään sen edellisessä sijainnissa.",
"settings_driveDuplicateLabel": "Piilota kaksoiskappaleet",
"settings_codeIndentation": "Koodieditorin sisennys (välilyönnit)",
"settings_codeUseTabs": "Sisennä sarkainmerkkejä käyttäen (välilyöntien sijaan)",
"settings_codeFontSize": "Koodieditorin fonttikoko",
"settings_padWidth": "Editorin maksimileveys",
"settings_padWidthHint": "Teksti-tyyppiset padit käyttävät oletusleveytenä näyttölaitteesi maksimileveyttä, mikä voi tehdä lukemisesta vaikeaa. Tästä voit pienentää editorin leveyttä.",
"settings_padWidthLabel": "Pienennä editorin leveyttä",
"settings_padSpellcheckTitle": "Oikeinkirjoituksen tarkistus",
"settings_padSpellcheckHint": "Tämä vaihtoehto ottaa käyttöön oikeinkirjoituksen tarkastuksen Teksti-tyyppisissä padeissa. Oikeinkirjoitusvirheet alleviivataan punaisella. Klikkaa virheellistä sanaa hiiren oikealla painikkeella painaessasi Ctrl- tai Meta-näppäintä pohjaan nähdäksesi korjausehdotukset.",
"settings_padSpellcheckLabel": "Ota oikeinkirjoituksen tarkastus käyttöön Teksti-muotoisissa padeissa",
"settings_creationSkip": "Ohita padin luontiruutu",
"settings_creationSkipHint": "Padien luontisivu esittää vaihtoehtoja padin luomiseen auttaakseen sinua hallitsemaan ja suojaamaan tietojasi. Jos koet sen hidastavan työskentelyäsi, voit tällä asetuksella ohittaa luontisivun ja käyttää sen sijaan yläpuolella määrittelemiäsi oletusasetuksia.",
"settings_creationSkipTrue": "Ohita",
"settings_creationSkipFalse": "Näytä",
"settings_templateSkip": "Ohita mallipohjan valinta-dialogi",
"settings_templateSkipHint": "Luodessasi uutta padia sinulta kysytään, haluatko käyttää mallipohjaa, jos sinulla on tallennettuja mallipohjia tälle padityypille. Tällä asetuksella voit valita, ettei mallipohjan valinta-dialogia näytetä ja siten mallipohjia ei koskaan käytetä.",
"settings_ownDriveTitle": "Ota käyttöön viimeisimmät tiliominaisuudet",
"settings_ownDriveHint": "Teknisistä syistä vanhemmilla käyttäjätileillä ei suoraan ole pääsyä CryptPadin uusimpiin ominaisuuksiin. Ilmainen päivitys uuteen käyttäjätiliin valmistelee CryptDrivesi tulevia ominaisuuksia varten häiritsemättä tavanomaista toimintaasi.",
"settings_ownDriveButton": "Päivitä käyttäjätilisi",
"settings_ownDriveConfirm": "Käyttäjätilin päivitykseen voi mennä jonkin aikaa. Joudut kirjautumaan uudelleen sisään kaikilla laitteillasi. Oletko varma, että haluat aloittaa päivityksen?",
"settings_ownDrivePending": "Käyttäjätiliäsi päivitetään. Ole hyvä, äläkä sulje tai lataa tätä sivua uudelleen, ennen kuin toimenpide on valmis.",
"settings_changePasswordTitle": "Vaihda salasanasi",
"settings_changePasswordHint": "Vaihda käyttäjätilisi salasana. Syötä nykyinen salasanasi ja vahvista uusi salasana kirjoittamalla se kahdesti. <br><b>Emme voi nollata unohtuneita salasanoja, joten olethan varovainen!</b>",
"settings_changePasswordButton": "Vaihda salasana",
"settings_changePasswordCurrent": "Nykyinen salasana",
"settings_changePasswordNew": "Uusi salasana",
"settings_changePasswordNewConfirm": "Vahvista uusi salasana",
"settings_changePasswordConfirm": "Oletko varma, että haluat vaihtaa salasanasi? Joudut kirjautumaan uudelleen sisään kaikilla laitteillasi.",
"settings_changePasswordError": "Tapahtui odottamaton virhe. Jos et pääse kirjautumaan sisään tai vaihtamaan salasanaasi, ota yhteyttä CryptPad-instanssisi ylläpitäjiin.",
"settings_changePasswordPending": "Salasanaasi päivitetään. Ole hyvä äläkä sulje tai lataa tätä sivua uudelleen, ennen kuin toimenpide on valmis.",
"settings_changePasswordNewPasswordSameAsOld": "Uuden salasanasi on oltava erilainen kuin nykyinen salasana.",
"settings_cursorColorTitle": "Kursorin väri",
"settings_cursorColorHint": "Vaihda kollaboratiivisissa dokumenteissa käytettävää käyttäjääsi yhdistettyä väriä.",
"settings_cursorShareTitle": "Jaa oma kursorisijainti",
"settings_cursorShareHint": "Voit päättää, haluatko kursorisijaintisi näkyvän muille kollaboratiivisissa dokumenteissa.",
"settings_cursorShareLabel": "Jaa sijainti",
"settings_cursorShowTitle": "Näytä muiden käyttäjien kursorisijainti",
"settings_cursorShowHint": "Voit valita, haluatko nähdä muiden käyttäjien kursorit kollaboratiivisissa dokumenteissa.",
"settings_cursorShowLabel": "Näytä kursorit",
"upload_title": "Tiedostojen lataus",
"upload_type": "Tyyppi",
"upload_modal_title": "Tiedostojen latauksen asetukset",
"upload_modal_filename": "Tiedostonimi (pääte <em>{0}</em> lisätty automaattisesti)",
"upload_modal_owner": "Omistettu tiedosto",
"uploadFolder_modal_title": "Kansion latauksen asetukset",
"uploadFolder_modal_filesPassword": "Tiedostojen salasana",
"uploadFolder_modal_owner": "Omistetut tiedostot",
"uploadFolder_modal_forceSave": "Tallenna tiedostot CryptDriveesi",
"upload_serverError": "Palvelinvirhe: tiedoston lataus palvelimelle epäonnistui.",
"upload_uploadPending": "Edellinen latauksesi on kesken. Haluatko keskeyttää sen ja ladata uuden tiedoston?",
"upload_success": "Tiedosto ({0}) on ladattu onnistuneesti palvelimelle ja lisätty CryptDriveesi.",
"upload_notEnoughSpace": "CryptDrivessasi ei ole tarpeeksi vapaata tallennustilaa tälle tiedostolle.",
"upload_notEnoughSpaceBrief": "Tallennustila ei riitä",
"upload_tooLarge": "Tiedoston koko ylittää suurimman sallitun latauskoon.",
"upload_tooLargeBrief": "Liian suuri tiedosto",
"upload_choose": "Valitse tiedosto",
"upload_pending": "Odottaa",
"upload_cancelled": "Keskeytetty",
"upload_name": "Tiedostonimi",
"upload_size": "Koko",
"upload_progress": "Edistyminen",
"upload_mustLogin": "Kirjaudu sisään ladataksesi tiedostoja",
"upload_up": "Lataa",
"download_button": "Pura ja lataa",
"download_mt_button": "Lataa tietokoneelle",
"download_resourceNotAvailable": "Pyydetty resurssi ei ollut saatavilla... Paina Esc jatkaaksesi.",
"download_dl": "Lataa tietokoneelle",
"download_step1": "Ladataan",
"download_step2": "Puretaan",
"todo_title": "CryptTodo",
"todo_newTodoNamePlaceholder": "Kuvaile tehtävääsi...",
"todo_newTodoNameTitle": "Lisää tämä tehtävä Tehtävät-listaasi",
"todo_markAsCompleteTitle": "Merkitse tehtävä valmiiksi",
"todo_markAsIncompleteTitle": "Merkitse tehtävä keskeneräiseksi",
"todo_removeTaskTitle": "Poista tehtävä Tehtävät-listaltasi",
"pad_showToolbar": "Näytä työkalurivi",
"pad_hideToolbar": "Piilota työkalurivi",
"pad_base64": "Tämä padi sisältää tehottomasti tallennettuja kuvia. Kuvat kasvattavat padin kokoa merkittävästi ja hidastavat padin lataamista. Voit siirtää nämä tiedostot uuteen tiedostomuotoon, joka säilytetään erillisenä CryptDrivessasi. Haluatko siirtää kuvat nyt?",
"mdToolbar_button": "Näytä tai piilota Markdown-työkalupalkki",
"mdToolbar_defaultText": "Lisää tekstisi tähän",
"mdToolbar_help": "Ohje",
"mdToolbar_tutorial": "http://www.markdowntutorial.com/",
"mdToolbar_bold": "Lihavoitu",
"mdToolbar_italic": "Kursiivi",
"mdToolbar_strikethrough": "Yliviivaus",
"mdToolbar_heading": "Otsikko",
"mdToolbar_link": "Linkki",
"mdToolbar_quote": "Lainaus",
"mdToolbar_nlist": "Järjestetty lista",
"mdToolbar_list": "Luettelomerkkilista",
"mdToolbar_check": "Tehtävälista",
"mdToolbar_code": "Koodi",
"mdToolbar_toc": "Sisällysluettelo",
"home_product": "CryptPad on sisäänrakennetun tietosuojan periaatteen mukainen vaihtoehto suosituille toimisto-ohjelmistoille ja pilvipalveluille. Kaikki CryptPadiin tallennettavat tiedot salataan ennen palvelimelle lähettämistä. Kukaan (emme edes me) ei voi päästä tietoihisi ilman avaimiasi.",
"home_host": "Tämä on itsenäinen yhteisön ylläpitämä Cryptpad-instanssi. Sen lähdekoodi on saatavilla <a href=\"https://github.com/xwiki-labs/cryptpad\" target=\"_blank\" rel=\"noreferrer noopener\">GitHubissa</a>.",
"home_host_agpl": "Cryptpad-ohjelmisto jaellaan AGPL3-ohjelmistolisenssin ehtojen mukaisesti",
"home_ngi": "NGI Award-palkinnon voittaja",
"about_intro": "CryptPadia kehittää Pariisissa, Ranskassa ja Iasissa, Romaniassa toimiva<a href=\"http://xwiki.com\">XWiki SAS</a>-pienyrityksen tutkimusryhmä. CryptPadin parissa työskentelee kolme ryhmän ydinjäsentä ja lisäksi joitakin avustajia XWiki SAS:n sisältä ja ulkopuolelta.",
"about_core": "Ydinkehittäjät",
"about_contributors": "Tärkeät avustajat",
"main_info": "<h2>Luottamuksellista yhteistyötä</h2>Jaetut dokumentit mahdollistavat ideoiden jakamisen samalla kun <strong>nollatietoperiaate</strong>-teknologia suojaa yksityisyytesi - <strong>jopa meiltä</strong>.",
"main_catch_phrase": "Pilvipalvelu nollatietoperiaatteella",
"main_footerText": "CryptPadin avulla voit nopeasti luoda kollaboratiivisia dokumentteja muistiinpanoja ja yhteistä ideointia varten.",
"footer_applications": "Sovellukset",
"footer_contact": "Ota yhteyttä",
"footer_aboutUs": "Tietoa meistä",
"about": "Tietoa meistä",
"privacy": "Yksityisyys",
"contact": "Ota yhteyttä",
"terms": "Käyttöehdot",
"blog": "Blogi",
"topbar_whatIsCryptpad": "Mikä on CryptPad",
"whatis_title": "Mikä on CryptPad",
"whatis_collaboration": "Nopeaa ja helppoa yhteistyötä",
"whatis_collaboration_p1": "CryptPadin avulla voit nopeasti luoda kollaboratiivisia dokumentteja muistiinpanoja ja yhteistä ideointia varten. Rekisteröitymällä ja kirjautumalla sisään saat mahdollisuuden ladata tiedostoja palvelimelle ja oman CryptDriven, jossa voit säilyttää kaikki padisi. Rekisteröityneet käyttäjät saavat ilmaiseksi 50 Mt tallennustilaa.",
"whatis_collaboration_p2": "Voit helposti antaa käyttöoikeuden CryptPad-dokumenttiin jakamalla sen linkin. Voit myös jakaa dokumentin linkin <em>vain luku</em>-tilassa, jolloin voit julkistaa yhteistyön tulokset ja muokata niitä edelleen.",
"team_inviteLinkError": "Linkin luomisessa tapahtui virhe.",
"whatis_collaboration_p3": "<a href=\"http://ckeditor.com/\">CKEditor</a>:illa voit luoda yksinkertaisia muotoiltavia tekstitiedostoja sekä Markdown-tiedostoja, jotka muunnetaan tekstiä muokatessasi reaaliaikaisesti esitysmuotoon. Voit myös käyttää Kysely-sovellusta tapahtumien ajoittamiseen useiden osallistujien kanssa.",
"whatis_zeroknowledge": "Nollatietoperiaate",
"whatis_zeroknowledge_p1": "Emme halua tietää, mitä kirjoitat. Modernin kryptografian avulla voit olla varma, ettemme todellakaan tiedä siitä mitään. CryptPad käyttää <strong>100-prosenttisesti asiakasohjelmassa tapahtuvaa salausta</strong> suojatakseen tuottamaasi sisältöä meiltä palvelimen ylläpitäjiltä.",
"whatis_zeroknowledge_p2": "Rekisteröityessäsi ja kirjautuessasi sisään käyttäjätunnuksesi ja salasanasi lasketaan salaiseksi avaimeksi <a href=\"https://en.wikipedia.org/wiki/Scrypt\">scrypt-avaintenmuodostusfunktiolla</a>. Tätä avainta, käyttäjätunnustasi ja salasanaasi ei koskaan lähetetä palvelimelle. Sen sijaan niitä käytetään asiakasohjelmassa CryptDrivesi sisällön purkamiseen. CryptDrivesi puolestaan sisältää avaimet kaikkiin padeihin, joihin sinulla on käyttöoikeus.",
"whatis_zeroknowledge_p3": "Kun jaat linkin dokumenttiin, jaat itse asiassa dokumentin käyttöön tarvittavan salausavaimen, mutta koska salausavain sisällytetään <a href=\"https://en.wikipedia.org/wiki/Fragment_identifier\">katkelmatunnisteeseen (fragment identifier)</a>, sitä ei koskaan lähetetä palvelimelle suoraan. Tutustu <a href=\"https://blog.cryptpad.fr/2017/07/07/cryptpad-analytics-what-we-cant-know-what-we-must-know-what-we-want-to-know/\">yksityisyydestä kertovaan blogikirjoitukseemme</a> saadaksesi selville, mihin metadataan meillä on pääsy ja mihin taas ei.",
"whatis_drive": "CryptDriven järjestely",
"whatis_drive_p1": "Kun käytät padia CryptPadissa, lisätään se automaattisesti CryptDrivesi pääkansioon. Voit halutessasi myöhemmin järjestellä padit kansioihin tai viedä ne roskakoriin. CryptDrive antaa sinun hakea padejasi ja järjestellä niitä milloin ja miten haluat.",
"whatis_drive_p2": "Intuitiivinen raahaa ja pudota-käyttöliittymä mahdollistaa padien siirtelemisen CryptDrivessa niin, etteivät niiden linkit muutu, eivätkä padien osallistujat siten koskaan menetä käyttöoikeuttaan niihin.",
"whatis_drive_p3": "Voit myös ladata CryptDriveesi tiedostoja ja jakaa niitä kollegoidesi kanssa. Ladattuja tiedostoja voidaan järjestellä samaan tapaan kuin kollaboratiivisia padeja.",
"whatis_business": "CryptPad yrityksille",
"admin_activeSessionsTitle": "Aktiiviset yhteydet",
"admin_activeSessionsHint": "Aktiivisten WebSocket-yhteyksien määrä (ja yhdistetyt uniikit IP-osoitteet)",
"admin_activePadsTitle": "Aktiiviset padit",
"admin_activePadsHint": "Tällä hetkellä katseltavien tai muokattavien uniikkien dokumenttien määrä",
"admin_registeredTitle": "Rekisteröityneet käyttäjät",
"admin_registeredHint": "CryptPad-instanssiisi rekisteröityneiden käyttäjien määrä",
"admin_updateLimitTitle": "Päivitä käyttäjien tallennuskiintiöt",
"admin_updateLimitHint": "Käyttäjien tallennuskiintiöiden pakotettu päivitys voidaan tehdä milloin tahansa, mutta se on tarpeen ainoastaan virhetilanteissa",
"admin_updateLimitButton": "Päivitä tallennuskiintiöt",
"admin_updateLimitDone": "Päivitys onnistui",
"admin_flushCacheTitle": "Tyhjennä HTTP-välimuisti",
"notifications_cat_friends": "Kaveripyynnöt",
"notifications_cat_pads": "Kanssani jaetut",
"notifications_cat_archived": "Historia",
"notifications_dismissAll": "Hylkää kaikki",
"support_notification": "Ylläpitäjä on vastannut tukipyyntöösi",
"requestEdit_button": "Pyydä muokkausoikeutta",
"requestEdit_dialog": "Haluatko varmasti pyytää padin omistajalta muokkausoikeutta?",
"requestEdit_confirm": "{1} on pyytänyt oikeutta muokata padia <b>{0}</b>. Haluatko myöntää muokkausoikeuden?",
"requestEdit_fromFriend": "Olet kaveri käyttäjän {0} kanssa",
"requestEdit_fromStranger": "<b>Et ole</b> käyttäjän {0} kaveri",
"requestEdit_viewPad": "Avaa padi uudessa välilehdessä",
"later": "Päätä myöhemmin",
"requestEdit_request": "{1} haluaa muokata padia <b>{0}</b>",
"requestEdit_accepted": "{1} on myöntänyt sinulle muokkausoikeuden padiin <b>{0}</b>",
"requestEdit_sent": "Kaveripyyntö lähetetty",
"properties_unknownUser": "{0} tuntematon(ta) käyttäjä(ä)",
"pricing": "Hinnoittelu",
"homePage": "Kotisivu",
"features_noData": "Henkilötietoja ei tarvita",
"features_pricing": "{0}-{2}€/kk",
"features_emailRequired": "Sähköpostiosoite vaaditaan",
"owner_removeText": "Poista olemassaoleva omistaja",
"owner_removePendingText": "Peru odottava tarjous",
"owner_addText": "Tarjoa yhteisomistajuutta kaverille",
"owner_unknownUser": "Tuntematon käyttäjä",
"owner_removeButton": "Poista valitut omistajat",
"owner_removePendingButton": "Peru valitut tarjoukset",
"owner_addButton": "Tarjoa omistajuutta",
"owner_removeConfirm": "Haluatko varmasti poistaa omistajuuden valituilta käyttäjiltä? Tästä toimenpiteestä lähetetään heille ilmoitus.",
"owner_removeMeConfirm": "Olet luopumassa omistajuusoikeuksistasi. Tätä toimintoa ei voi perua. Oletko varma?",
"owner_addConfirm": "Yhteisomistajat voivat muokata sisältöä ja poistaa omistajuutesi. Oletko varma?",
"owner_openModalButton": "Hallinnoi omistajia",
"owner_add": "{0} haluaa sinut padin <b>{1}</b> omistajaksi. Hyväksytkö tämän?",
"owner_request": "{0} haluaa sinut <b>{1}</b> omistajaksi",
"owner_request_accepted": "{0} on hyväksynyt tarjouksesi <b>{1}</b> omistajuudesta",
"share_linkTeam": "Lisää tiimin CryptDriveen",
"team_pickFriends": "Valitse tiimiin kutsuttavat kaverit",
"team_inviteModalButton": "Kutsu",
"team_noFriend": "Sinulla ei ole vielä kavereita CryptPadissa.",
"drive_sfPassword": "Jaettu kansiosi {0} ei ole enää saatavilla. Se on joko poistettu omistajansa toimesta tai sille on asetettu uusi salasana. Voit poistaa tämän kansion CryptDrivestasi tai palauttaa käyttöoikeuden käyttämällä uutta salasanaa.",
"drive_sfPasswordError": "Väärä salasana",
"password_error_seed": "Padia ei löytynyt!<br>Tämä virhe voi johtua kahdesta syystä: joko padiin on lisätty tai vaihdettu salasana, tai padi on poistettu palvelimelta.",
"properties_confirmChangeFile": "Oletko varma? Käyttäjät, joilla ei ole uutta salasanaa eivät pääse enää käyttämään tiedostoa.",
"properties_confirmNewFile": "Oletko varma? Salasanan lisääminen muuttaa tämän tiedoston URL-osoitetta. Käyttäjät, joilla ei ole salasanaa eivät pääse enää käyttämään tiedostoa.",
"properties_passwordWarningFile": "Salasanan vaihto onnistui, mutta emme onnistuneet päivittämään uusia tietoja CryptDriveesi. Tiedoston vanha versio täytyy ehkä poistaa manuaalisesti.",
"properties_passwordSuccessFile": "Salasanan vaihto onnistui.",
"driveOfflineError": "Yhteytesi CryptPadiin on katkennut. Tähän padiin tehdyt muutokset eivät tallennu CryptDriveesi. Ole hyvä ja sulje kaikki CryptPad-välilehdet, ja yritä uudelleen uudessa selainikkunassa. ",
"teams_table": "Roolit",
"teams_table_generic": "Roolit ja käyttöoikeudet",
"teams_table_generic_view": "Tarkastele: käytä kansioita ja padeja vain luku-tilassa.",
"teams_table_generic_edit": "Muokkaa: luo, muokkaa ja poista kansioita ja padeja.",
"teams_table_generic_admin": "Hallitse jäseniä: kutsu ja poista jäseniä, vaihda jäsenten rooleja Ylläpitäjä-rooliin asti.",
"teams_table_generic_own": "Hallitse tiimiä: vaihda tiimin nimeä ja avatar-kuvaa, lisää tai poista Omistajia, muuta tiimin tilausta, poista tiimi.",
"teams_table_specific": "Poikkeukset",
"teams_table_specificHint": "Nämä ovat vanhempia jaettuja kansioita, joissa katselijoilla on edelleen oikeus muokata olemassaolevia padeja. Näihin kansioihin luodut tai kopioidut padit saavat oletuskäyttöoikeudet.",
"teams_table_admins": "Hallitse jäseniä",
"teams_table_owners": "Hallitse tiimiä",
"teams_table_role": "Rooli",
"pad_wordCount": "Sanamäärä: {0}",
"share_linkWarning": "Tämä linkki sisältää dokumenttisi avaimet. Linkin vastaanottajat saavat dokumenttiisi käyttöoikeudet, joita ei voi poistaa jälkikäteen.",
"share_linkPasswordAlert": "Tämä elementti on salasanasuojattu. Kun lähetät linkin, täytyy vastaanottajan syöttää salasana.",
"share_contactPasswordAlert": "Tämä elementti on salasanasuojattu. Koska jaat sen CryptPad-yhteyshenkilön kanssa, ei vastaanottajan tarvitse syöttää salasanaa.",
"share_embedPasswordAlert": "Tämä elementti on salasanasuojattu. Kun upotat tämän padin, katselijoita pyydetään syöttämään salasana.",
"passwordFaqLink": "Lue lisää salasanoista",
"share_noContactsLoggedIn": "Sinulla ei ole vielä yhtään CryptPad-yhteyshenkilöä. Jaa linkki käyttäjäprofiiliisi, jotta muut käyttäjät voivat lähettää sinulle yhteyspyyntöjä.",
"share_copyProfileLink": "Kopioi käyttäjäprofiilin linkki",
"share_noContactsNotLoggedIn": "Kirjaudu sisään tai rekisteröidy nähdäksesi olemassaolevat yhteystietosi ja lisätäksesi uusia yhteystietoja.",
"contacts_mute": "Mykistä",
"contacts_unmute": "Poista mykistys",
"contacts_manageMuted": "Hallinnoi mykistyksiä",
"contacts_mutedUsers": "Mykistetyt käyttäjätilit",
"contacts_muteInfo": "Mykistetyt käyttäjät eivät voi lähettää sinulle viestejä tai ilmoituksia.<br>Mykistetyt käyttäjät eivät saa tietää, että olet mykistänyt heidät. ",
"team_inviteLinkTitle": "Luo yksilöity kutsu tähän tiimiin",
"team_inviteLinkTempName": "Väliaikainen nimi (näkyvissä Odottavat kutsut-listassa)",
"team_inviteLinkSetPassword": "Suojaa linkki salasanalla (suositeltavaa)",
"team_inviteLinkNote": "Lisää henkilökohtainen viesti",
"team_inviteLinkNoteMsg": "Tämä viesti näytetään ennen vastaanottajan päätöstä tiimiin liittymisestä.",
"team_inviteLinkLoading": "Luodaan linkkiä",
"team_inviteLinkWarning": "Ensimmäinen tämän linkin avaava henkilö voi liittyä tiimiin ja katsella sen sisältöä. Ole varovainen, kun jaat sen.",
"team_inviteLinkErrorName": "Ole hyvä ja lisää kutsuttavalle henkilölle nimi. Hän voi muuttaa sitä myöhemmin. ",
"team_inviteLinkCreate": "Luo linkki",
"team_inviteLinkCopy": "Kopioi linkki",
"team_inviteFrom": "Lähettäjä:",
"team_inviteFromMsg": "{0} on kutsunut sinut liittymään tiimiin <b>{1}</b>",
"team_invitePleaseLogin": "Ole hyvä ja kirjaudu sisään tai rekisteröidy hyväksyäksesi kutsun.",
"team_inviteEnterPassword": "Ole hyvä ja syötä kutsun salasana jatkaaksesi.",
"team_invitePasswordLoading": "Puretaan kutsua",
"team_inviteJoin": "Liity tiimiin",
"team_inviteTitle": "Kutsu tiimiin",
"team_inviteGetData": "Haetaan tiimitietoja",
"team_cat_link": "Kutsulinkki",
"team_links": "Kutsulinkit",
"team_inviteInvalidLinkError": "Tämä kutsulinkki ei ole kelvollinen."
}

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<p>What can be built on top of CryptPad?</p>
<ul>
<li><a href="/examples/form/">forms</a></li>
<li><a href="/examples/text/">text</a></li>
<li><a href="/examples/board/">kanban board</a></li>
<!-- <li><a href="/examples/json/">json objects</a></li> -->
<li><a href="/examples/read/">ajax-like get/put behaviour</a></li>
<li><a href="/examples/render/">render markdown content as html</a></li>
<li><a href="/examples/style/">edit a page's style tag</a></li>
</ul>

@ -1,125 +0,0 @@
# Realtime Lists and Maps
Our realtime list/map API has some limitations.
## Datatype Serialization
Only datatypes which can be serialized via `JSON.parse(JSON.stringify(yourObject))` will be preserved.
This means the following types can be serialized:
1. strings
2. objects
3. arrays
4. booleans
5. numbers
6. null
While these cannot be serialized:
1. undefined
2. symbol
## Object Interaction
Only 'get' and 'set' methods are supported.
This is because we need to limit the operations we support to those supported by all browsers we might use.
Currently that means we can't rely on `in`, `delete`, or anything other than a `get`/`set` operation to behave as expected.
Treat all other features as `Undefined Behaviour`.
> Your mileage may vary
`set` methods include all of the [assignment operators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Assignment_Operators#Exponentiation_assignment).
```
// where 'x' is the realtime object `{}`
// assignment
x.n = 5;
x.n += 3;
x.n++;
++x.n;
x.a = 5;
x.b = 3;
x.a *= x.b++;
x // {a: 15, b: 4, n: 10}
```
Instead of `delete`, assign `undefined`.
`delete` will remove an attribute locally, but the deletion will not propogate to other clients until your next serialization.
This is potentially problematic, as it can result in poorly formed patches.
### Object and array methods
methods which do not directly use setters and getters can be problematic:
`Array.push` behaves correctly, however, `Array.pop` does not.
## Deep Equality
Normally in Javascript objects are passed by reference.
That means you can do things like this:
```
var a = {x: 5};
var b = a;
// true
console.log(a === b);
```
Using the realtime list/map API, objects are serialized, and are therefore copied by value.
Since objects are deserialized and created on each client, you will not be able to rely on this kind of equality across objects, despite their having been created in this fashion.
Object equality _might_ work if the comparison is performed on the same client that initially created the object, but relying on this kind of behaviour is not advisable.
## Listeners
You can add a listener to an attribute (via its path relative to the root realtime object).
There are various types of listeners
* change
* remove
* disconnect
* ready
### Semantics
Suppose you have a realtime object `A` containing nested structures.
```
{
a: {
b: {
c: 5
}
},
d: {
e: [
1,
4,
9
]
}
}
```
If you want to be alerted whenever the second element in the array `e` within `d` changes, you can attach a listener like so:
```
A.on('change', ['d', 'e', 1], function (oldval, newval, path, rootObject) {
/* do something with these values */
console.log("value changes from %s to %s", oldval, newval);
});
```
## Known Bugs
there is currently an issue with popping the last element of an array.

@ -1,50 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
<style>
html, body{
padding: 0px;
margin: 0px;
overflow: hidden;
box-sizing: border-box;
}
form {
border: 3px solid black;
border-radius: 5px;
padding: 15px;
font-weight: bold !important;
font-size: 18px !important;
}
input[type="text"]
{
margin-top: 5px;
margin-bottom: 5px;
width: 80%;
height: 3em;
font-weight: bold;
font-size: 18px;
}
textarea {
width: 80%;
height: 40vh;
}
div#content {
width: 80%;
margin: auto;
}
</style>
</head>
<body>
<div id="content">
<p>The field below behaves like a <a target="_blank" href="https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop">REPL</a>, with the realtime object created by this page exposed as the value <code>x</code></p>
<p>Open your browser's console to see the output.</p>
<input type="text" name="repl" placeholder="Value" autofocus><br>
</div>
</body>
</html>

@ -1,76 +0,0 @@
define([
'jquery',
'/api/config',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js',
'/common/cryptpad-common.js'
], function ($, Config, RtListMap, Crypto, Common) {
var secret = Common.getSecrets();
var config = {
websocketURL: Config.websocketURL,
channel: secret.channel,
//cryptKey: secret.key,
data: {},
crypto: Crypto.createEncryptor(secret.key)
};
var module = window.APP = {};
var $repl = $('[name="repl"]');
var setEditable = module.setEditable = function (bool) {
[$repl].forEach(function ($el) {
$el.attr('disabled', !bool);
});
};
setEditable(false);
var rt = module.rt = RtListMap.create(config);
rt.proxy.on('create', function (info) {
console.log("initializing...");
window.location.hash = info.channel + secret.key;
}).on('ready', function () {
console.log("...your realtime object is ready");
rt.proxy
// on(event, path, cb)
.on('change', [], function (o, n, p) {
console.log("root change event firing for path [%s]: %s => %s", p.join(','), o, n);
})
.on('remove', [], function (o, p) {
console.log("Removal of value [%s] at path [%s]", o, p.join(','));
})
.on('change', ['a', 'b', 'c'], function (o, n, p) {
console.log("Deeper change event at [%s]: %s => %s", p.join(','), o, n);
console.log("preventing propogation...");
return false;
})
// on(event, cb)
.on('disconnect', function () {
setEditable(false);
window.alert("Network connection lost");
});
// set up user interface hooks
$repl.on('keyup', function (e) {
if (e.which === 13 /* enter keycode */) {
var value = $repl.val();
if (!value.trim()) { return; }
console.log("evaluating `%s`", value);
var x = rt.proxy;
x = x; // LOL jshint says this is unused otherwise <3
console.log('> ', eval(value)); // jshint ignore:line
console.log();
$repl.val('');
}
});
setEditable(true);
});
});

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html class="cp slide">
<head>
<title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
</head>
<body>
<div id="iframe-container">
<iframe id="pad-iframe"></iframe><script src="/common/noscriptfix.js"></script>
</div>
</body>
</html>

@ -1,8 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<h1>PEWPEW

@ -1,50 +0,0 @@
define([
'jquery',
'/common/cryptpad-common.js',
'/common/pinpad.js'
], function ($, Cryptpad, Pinpad) {
window.APP = {
Cryptpad: Cryptpad,
};
var synchronize = function (call) {
// provide a sorted list of unique channels
var list = Cryptpad.getCanonicalChannelList();
var localHash = call.hashChannelList(list);
var serverHash;
call.getFileListSize(function (e, bytes) {
if (e) { return void console.error(e); }
console.log("total %sK bytes used", bytes / 1000);
});
call.getServerHash(function (e, hash) {
if (e) { return void console.error(e); }
serverHash = hash;
if (serverHash === localHash) {
return console.log("all your pads are pinned. There is nothing to do");
}
call.reset(list, function (e, response) {
if (e) { return console.error(e); }
else {
return console.log('reset pin list. new hash is [%s]', response);
}
});
});
};
$(function () {
Cryptpad.ready(function () {
var network = Cryptpad.getNetwork();
var proxy = Cryptpad.getStore().getProxy().proxy;
Pinpad.create(network, proxy, function (e, call) {
if (e) { return void console.error(e); }
synchronize(call);
});
});
});
});

@ -1,33 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
<link rel="icon" type="image/png"
href="/customize/main-favicon.png"
data-main-favicon="/customize/main-favicon.png"
data-alt-favicon="/customize/alt-favicon.png"
id="favicon" />
<style>
input {
width: 50vw;
padding: 15px;
}
pre {
max-width: 90vw;
overflow: auto;
}
</style>
</head>
<body>
<input id="target" type="text" value="/1/edit/xvhI6k6n7qYEtNL8cAv5zw/a4KKGGDY0S8GDj6m9iumX5E4"></input>
<button id="get">get</button>
<hr />
<textarea id="putter" type="text"></textarea>
<button id="put">put</button>
<button id="open">open</button>

@ -1,32 +0,0 @@
define([
'jquery',
'/common/cryptget.js'
], function ($, Crypt) {
var $target = $('#target');
var useDoc = function (err, doc) {
if (err) { return console.error(err); }
//console.log(doc);
$('#putter').val(doc);
};
$('#get').click(function () {
var val = $target.val();
if (!val.trim()) { return; }
Crypt.get(val, useDoc);
});
$('#put').click(function () {
var hash = $target.val().trim();
Crypt.put(hash, $('#putter').val(), function (e) {
if (e) { console.error(e); }
$('#get').click();
});
});
$('#open').click(function () {
window.open('/code/#' + $target.val());
});
if (window.location.hash) { Crypt.get(void 0, useDoc); }
});

@ -1,42 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="render-sd.css" />
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
<style>
html, body {
padding: 0;
margin: 0;
width: 100%;
min-height: 100%;
}
body { overflow-y: auto; }
#inner {
display: fixed;
width: 95%;
height: 100%;
top: 0px;
left: 0px;
margin: 0px auto;
padding: 0px;
}
img { max-width: 100%; }
code { font-family: monospace; }
blockquote, p, pre, code, li { font-size: 20px; }
table, thead, tbody, th, tr, td{
border: 1pt solid #586e75;
background-color: #002b36;
padding: 15px;
}
</style>
</head>
<body>
<div id="target">
<div id="inner"></div>
</div>

@ -1,104 +0,0 @@
define([
'jquery',
'/api/config',
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/marked/marked.min.js',
'/bower_components/hyperjson/hyperjson.js',
'/common/cryptpad-common.js',
'/bower_components/diff-dom/diffDOM.js',
], function ($, Config, Realtime, Crypto, Marked, Hyperjson, Cryptpad) {
var DiffDom = window.diffDOM;
var secret = Cryptpad.getSecrets();
// set markdown rendering options :: strip html to prevent XSS
Marked.setOptions({
sanitize: true
});
var module = window.APP = { };
var $target = module.$target = $('#target');
var config = {
websocketURL: Config.websocketURL,
channel: secret.channel,
crypto: Crypto.createEncryptor(secret.key)
};
var draw = window.draw = (function () {
var target = $target[0],
inner = $target.find('#inner')[0];
if (!target) { throw new Error(); }
var DD = new DiffDom({});
return function (md) {
var rendered = Marked(md||"");
// make a dom
var New = $('<div id="inner">'+rendered+'</div>')[0];
var patches = (DD).diff(inner, New);
DD.apply(inner, patches);
return patches;
};
}());
var redrawTimeout;
var lazyDraw = function (md) {
if (redrawTimeout) { clearTimeout(redrawTimeout); }
redrawTimeout = setTimeout(function () {
draw(md);
}, 450);
};
var initializing = true;
config.onInit = function (info) {
window.location.hash = info.channel + secret.key;
module.realtime = info.realtime;
};
var getContent = function (userDoc) {
try {
var parsed = JSON.parse(userDoc);
if (typeof(parsed.content) !== 'string') {
throw new Error();
}
return parsed.content;
} catch (err) {
return userDoc;
}
};
// when your editor is ready
config.onReady = function () {
console.log("Realtime is ready!");
var userDoc = module.realtime.getUserDoc();
lazyDraw(getContent(userDoc));
initializing = false;
};
// when remote editors do things...
config.onRemote = function () {
if (initializing) { return; }
var userDoc = module.realtime.getUserDoc();
lazyDraw(getContent(userDoc));
};
config.onLocal = function () {
// we're not really expecting any local events for this editor...
/* but we might add a second pane in the future so that you don't need
a second window to edit your markdown */
if (initializing) { return; }
var userDoc = module.realtime.getUserDoc();
lazyDraw(userDoc);
};
config.onAbort = function () {
window.alert("Network Connection Lost");
};
Realtime.start(config);
});

@ -1,116 +0,0 @@
html {
font-family: sans-serif;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
/*background-color: #073642;*/
color: #839496;
font-family: 'PT Sans', sans-serif;
}
body {
background-color: #002b36;
}
a:focus {
outline: thin dotted;
}
a:active,
a:hover {
outline: 0;
}
h1 {
font-size: 2em;
}
b,
strong {
font-weight: bold;
}
code,
pre {
font-family: monospace, serif;
font-size: 1em;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
q {
quotes: "\201C" "\201D" "\2018" "\2019";
}
img {
border: 0;
}
svg:not(:root) {
overflow: hidden;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
pre,
code {
font-family: 'Inconsolata', sans-serif;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: 'PT Sans Narrow', sans-serif;
font-weight: 700;
}
code {
background-color: #073642;
padding: 2px;
}
a {
color: #b58900;
}
a:visited {
color: #cb4b16;
}
a:hover {
color: #cb4b16;
}
h1 {
color: #d33682;
}
h2,
h3,
h4,
h5,
h6 {
color: #859900;
}
pre {
background-color: #002b36;
color: #839496;
border: 1pt solid #586e75;
box-shadow: 5pt 5pt 8pt #073642;
}
pre code {
background-color: #002b36;
}
h1 {
font-size: 2.8em;
}
h2 {
font-size: 2.4em;
}
h3 {
font-size: 1.8em;
}
h4 {
font-size: 1.4em;
}
h5 {
font-size: 1.3em;
}
h6 {
font-size: 1.15em;
}

@ -58,7 +58,7 @@ define([
// Remove the listener once we've received the READY message
window.removeEventListener('message', whenReady);
// Answer with the requested data
postMsg(JSON.stringify({ txid: data.txid, language: Cryptpad.getLanguage() }));
postMsg(JSON.stringify({ txid: data.txid, language: Cryptpad.getLanguage(), localStore: window.localStore, cache: window.cpCache }));
// Then start the channel
window.addEventListener('message', function (msg) {

@ -350,6 +350,8 @@ define([
var mkHelpMenu = function (framework) {
var $toolbarContainer = $('#cp-app-kanban-container');
$toolbarContainer.prepend(framework._.sfCommon.getBurnAfterReadingWarning());
var helpMenu = framework._.sfCommon.createHelpMenu(['kanban']);
$toolbarContainer.prepend(helpMenu.menu);

@ -190,6 +190,7 @@ define([
var mkHelpMenu = function (framework) {
var $toolbarContainer = $('.cke_toolbox_main');
$toolbarContainer.before(framework._.sfCommon.getBurnAfterReadingWarning());
var helpMenu = framework._.sfCommon.createHelpMenu(['text', 'pad']);
$toolbarContainer.before(helpMenu.menu);

@ -1187,6 +1187,7 @@ define([
$drawer.append($export);
var helpMenu = common.createHelpMenu(['poll']);
$('#cp-app-poll-form').prepend(common.getBurnAfterReadingWarning());
$('#cp-app-poll-form').prepend(helpMenu.menu);
$drawer.append(helpMenu.button);

@ -60,7 +60,7 @@ define([
// Remove the listener once we've received the READY message
window.removeEventListener('message', whenReady);
// Answer with the requested data
postMsg(JSON.stringify({ txid: data.txid, language: Cryptpad.getLanguage() }));
postMsg(JSON.stringify({ txid: data.txid, language: Cryptpad.getLanguage(), localStore: window.localStore, cache: window.cpCache }));
// Then start the channel
window.addEventListener('message', function (msg) {
@ -105,6 +105,21 @@ define([
config.addCommonRpc(sframeChan);
sframeChan.on('EV_CACHE_PUT', function (x) {
Object.keys(x).forEach(function (k) {
localStorage['CRYPTPAD_CACHE|' + k] = x[k];
});
});
sframeChan.on('EV_LOCALSTORE_PUT', function (x) {
Object.keys(x).forEach(function (k) {
if (typeof(x[k]) === "undefined") {
delete localStorage['CRYPTPAD_STORE|' + k];
return;
}
localStorage['CRYPTPAD_STORE|' + k] = x[k];
});
});
sframeChan.on('Q_GET_FILES_LIST', function (types, cb) {
Cryptpad.getSecureFilesList(types, function (err, data) {
cb({

@ -410,6 +410,7 @@ define([
var mkHelpMenu = function (framework) {
var $codeMirrorContainer = $('#cp-app-slide-editor-container');
$codeMirrorContainer.prepend(framework._.sfCommon.getBurnAfterReadingWarning());
var helpMenu = framework._.sfCommon.createHelpMenu(['text', 'slide']);
$codeMirrorContainer.prepend(helpMenu.menu);

@ -1059,6 +1059,7 @@ define([
var hashData = Hash.parseTypeHash('invite', hash);
var password = hashData.password;
var seeds = InviteInner.deriveSeeds(hashData.key);
var sframeChan = common.getSframeChannel();
if (Object.keys(privateData.teams || {}).length >= Constants.MAX_TEAMS_SLOTS) {
return void cb([
@ -1146,6 +1147,7 @@ define([
return;
}
// No error: join successful!
sframeChan.event('EV_SET_HASH', '');
var $div = $('div.cp-team-list').empty();
refreshList(common, function (content) {
$div.append(content);
@ -1170,7 +1172,6 @@ define([
nThen(function (waitFor) {
// Get preview content.
var sframeChan = common.getSframeChannel();
sframeChan.query('Q_ANON_GET_PREVIEW_CONTENT', { seeds: seeds }, waitFor(function (err, json) {
if (json && (json.error || !Object.keys(json).length)) {
$(errorBlock).text(Messages.team_inviteInvalidLinkError).show();

@ -270,6 +270,7 @@ define([
var mkHelpMenu = function (framework) {
var $appContainer = $('#cp-app-whiteboard-container');
$appContainer.prepend(framework._.sfCommon.getBurnAfterReadingWarning());
var helpMenu = framework._.sfCommon.createHelpMenu(['whiteboard']);
$appContainer.prepend(helpMenu.menu);
framework._.toolbar.$drawer.append(helpMenu.button);

Loading…
Cancel
Save