diff --git a/CHANGELOG.md b/CHANGELOG.md index ea5909dab..b249e10c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Dockerfile b/Dockerfile index ae312e3f9..6cac1605c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/config/config.example.js b/config/config.example.js index 779843f44..9981c0626 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -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/.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) */ diff --git a/container-start.sh b/container-start.sh index 81338503a..9f28d0127 100755 --- a/container-start.sh +++ b/container-start.sh @@ -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 diff --git a/customize.dist/loading.js b/customize.dist/loading.js index 600f3e8d4..e20e79438 100644 --- a/customize.dist/loading.js +++ b/customize.dist/loading.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'); diff --git a/customize.dist/messages.js b/customize.dist/messages.js index 829cc4f23..45dd2fac2 100755 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -5,6 +5,7 @@ var map = { 'de': 'Deutsch', 'el': 'Ελληνικά', 'es': 'Español', + 'fi': 'Suomalainen', 'fr': 'Français', 'it': 'Italiano', 'nb': 'Norwegian Bokmål', diff --git a/customize.dist/pages.js b/customize.dist/pages.js index 281f213f6..a7fcd9441 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -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)") ]); }; diff --git a/customize.dist/src/less2/include/alertify.less b/customize.dist/src/less2/include/alertify.less index a2b787e1c..83c9068eb 100644 --- a/customize.dist/src/less2/include/alertify.less +++ b/customize.dist/src/less2/include/alertify.less @@ -168,6 +168,9 @@ margin-bottom: 0; } } + .cp-alertify-type-container { + overflow: visible !important; + } .alertify-tabs { max-height: 100%; display: flex; diff --git a/customize.dist/src/less2/include/buttons.less b/customize.dist/src/less2/include/buttons.less index f3cb50e17..ad6aaf9cc 100644 --- a/customize.dist/src/less2/include/buttons.less +++ b/customize.dist/src/less2/include/buttons.less @@ -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; } } diff --git a/customize.dist/src/less2/include/dropdown.less b/customize.dist/src/less2/include/dropdown.less index 10044528e..271603216 100644 --- a/customize.dist/src/less2/include/dropdown.less +++ b/customize.dist/src/less2/include/dropdown.less @@ -17,8 +17,7 @@ button { .fa-caret-down { - margin-right: 0px; - margin-left: 5px; + margin-right: 1em !important; } * { .tools_unselectable(); diff --git a/customize.dist/src/less2/include/modals-ui-elements.less b/customize.dist/src/less2/include/modals-ui-elements.less index 289cc4e69..cee774a98 100644 --- a/customize.dist/src/less2/include/modals-ui-elements.less +++ b/customize.dist/src/less2/include/modals-ui-elements.less @@ -14,6 +14,9 @@ .radio-group { display: flex; flex-direction: row; + &:not(:last-child){ + margin-bottom: 8px; + } .cp-radio { margin-right: 30px; } diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index 0df5823a0..1d1ca0c47 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -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; diff --git a/docker-compose.yml b/docker-compose.yml index 73673f7dd..e0d977866 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf index a54687560..02f233735 100644 --- a/docs/example.nginx.conf +++ b/docs/example.nginx.conf @@ -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; } diff --git a/historyKeeper.js b/historyKeeper.js index 53ce46807..59a694e8c 100644 --- a/historyKeeper.js +++ b/historyKeeper.js @@ -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. diff --git a/lib/load-config.js b/lib/load-config.js index 80f4706dc..0756c2df4 100644 --- a/lib/load-config.js +++ b/lib/load-config.js @@ -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; diff --git a/package-lock.json b/package-lock.json index 08328fcb1..de127e96c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 835641d6b..1d676323d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/rpc.js b/rpc.js index 8c7fe6c70..fcd85a390 100644 --- a/rpc.js +++ b/rpc.js @@ -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 /*:(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'); diff --git a/scripts/check-account-deletion.js b/scripts/check-account-deletion.js index cf271a1da..0532e69ed 100644 --- a/scripts/check-account-deletion.js +++ b/scripts/check-account-deletion.js @@ -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'] diff --git a/server.js b/server.js index d1382480f..7b5a7fb9f 100644 --- a/server.js +++ b/server.js @@ -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 }); -} diff --git a/storage/file.js b/storage/file.js index 402982f6b..bb65cff43 100644 --- a/storage/file.js +++ b/storage/file.js @@ -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 diff --git a/www/assert/main.js b/www/assert/main.js index 6ad05a7a2..c29b3bfa3 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -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 diff --git a/www/code/inner.js b/www/code/inner.js index e2ed086de..e933bd4bd 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -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); diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index d4c1954e4..900a9ec28 100644 --- a/www/common/application_config_internal.js +++ b/www/common/application_config_internal.js @@ -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 diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 4ed1193e3..902a91793 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -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/'; } diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 4528b7a1b..d4b3059b4 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -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: ''; diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 13250a7d3..ed60032e3 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -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 = $('