diff --git a/CHANGELOG.md b/CHANGELOG.md index d227edc7d..a3a77352f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,111 @@ +# Zebra release (v2.25.0) + +## Goals + +This release coincided with XWiki's yearly seminar, so our regular schedule was interrupted a bit. We spent the time we had working towards implementing components of "editable metadata", which will allow pad owners to add new owners or transfer ownership to friends, among other things. + +Otherwise we wanted to deploy a built-in support system to improve our ability to debug issues as well as to make it easier for users to report problems. Along the way we did our best to improve usability and fix small annoying bugs. + +As this is the last release in our 2.0 cycle, we're going to take some extra time to prepare some big features for our 3.0.0 release, which we expect to deploy on August 20th, 2019. + +## Update notes + +* We've updated some dependencies that are used to lint the CryptPad codebase to detect errors. Run `npm install` if you plan to develop for CryptPad and you want to use the linter +* This release introduces a _support_ tab within the admin panel. If you generate an asymmetric keypair and add it to your server-side configuration file then users will have the option of opening support tickets if they encounter errors. Their support tickets will include some basic information about their account which might help you to solve their issues. To set up your _"encrypted support mailbox"_: + 1. run `node ./scripts/generate-admin-keys.js` + 2. copy the "public key" and add it to your config.js file like so: + * `supportMailboxPublicKey: "BL3kgYBM0HNw5ms8ULWU1wMTb5ePBbxAPjDZKamkuB8=", + 3. copy the private key and store it in a safe place + 4. navigate to the "support" tab in the admin panel and enter the private key + 5. share the private key with any other administrators who should be able to read the support tickets + 6. restart so that your users receive the public key stored in your configuration file + * this will allow them to submit tickets via the support page + * if you don't know how to fix the issue and want to open a ticket on our public tracker, include the information submitted along with their ticket + +## Features + +* The feature added in the previous release which displayed a preview of the theme and highlighting mode chosen for the code and slide editors has been improved to also display previews when navigating through the dropdowns using keyboard arrow keys. +* We've followed up on our initial work on notifications by adding a full notifications page which offers the ability to review older notifications that you might have accidentally dismissed. +* When you right-click on an element in the CryptDrive the resulting menu now includes icons to make it easier to find the action for which you are looking +* We now include folders in search results which used to only include files +* You can right-click to add colors to folders, in case that helps you organize your content more effectively + +# Yak release (v2.24.0) + +## Goals + +We've recently had an intern join our team, so this release and those until the end of summer are likely to feature a lot of small usability fixes. +Otherwise, we've continued to develop team-centric features, particularly the way that registered users share pads with friends. +Finally, we prioritized the ability to archive files for a period instead of deleting them, which we've been planning for a while. + +## Update notes + +* There are some important steps in this release: + * **make sure you read the full update notes before proceeding!** +* [@zimbatm](https://github.com/zimbatm) added the ability to configure the location of your configuration file via environment variables when launching the server: + * `CRYPTPAD_CONFIG=/home/cryptpad/cryptpad/cryptpad-config/config.js /home/cryptpad/cryptpad/server.js` +* We discovered a bug in our Xenops release which resulted in the server's list of pads stored for each user to be incorrect. + * if you're running CryptPad 2.23.0, we recommend that you disable any scripts configured to delete inactive pads + * updating to 2.24.0 will fix the issue in the client, but each user's list of "pinned pads" won't be corrected until they visit your instance and run the latest code +* This release introduces the ability to archive some data instead of deleting it, since it can be scary to remove user data when you can't easily inspect it to see what it is + * to take advantage of this new functionality you'll need to update your configuration file with three new configuration points: + * set `retainData` to `true` if you want to archive channels instead of deleting them + * either by user command or due to inactivity + * the server will fall back to its default deletion behaviour if this value is `false` or not set at all + * set `archiveRetentionTime` to the number of days that an archived pad should be stored in the archive directory before being deleted permanently + * set `archivePath` to the path where you'd like archives to be stored + * it should not be publicly accessible in order to respect the users' wishes +* We've introduced some new scripts to work with the database, some of which were needed to diagnose problems stemming from the pinning bug + * `evict-inactive.js` identifies channels which are unpinned and inactive and archives them + * unlike `delete-inactive.js` it only handles channels, not files or any other kind of data + * ...but it's much safer, since nothing is removed permanently + * in the coming releases we'll implement archival for other types of data so that we can fully remove unsafe scripts + * `diagnose-archive-conflicts.js` checks all the files in your archive and identifies whether they can be restored safely or if they conflict with newer files in the production database + * `restore-archived.js` restores any channels archived by the server or evict-inactive.js, excluding those which would conflict with the database +* This release depends on updates to some serverside dependencies. Run `npm update`: + * `ws` addresses a potential vulnerability, so if possible anyone running earlier versions of CryptPad should update + * `chainpad-server` handles users' websocket connections and we needed to make a few changes to deal with changes in the `ws` API + * `heapdump` is no longer a default dependency, though you can install it if you want its functionality +* This release also features a **Clientside migration** which modifies users' CryptDrives. Any clients which are running both the latest code after the update as well as an older version in another browser or device risk creating conflicts in their account data. To prevent this, update in the following manner: + 1. ensure that you've added the configuration values listed above + 2. shut down the server and ensure that it doesn't restart until you've completed the following steps + 3. pull the latest clientside and serverside code via git + 4. `npm update` to get the latest serverside dependencies + 5. update the cache-busting string if you are handling the cache manually, otherwise allow the server to handle this as per its default + 5. restart the server: clients with open tabs should be prompted to reload instead of reconnecting because the server's version has changed +* We recommend that you test a local version of CryptPad before deploying this latest code, as aspects of the above-mentioned migrations are not backwards-compatible. + * you can roll back, but users' CryptDrives might have errors coping with data introduced by newer features. + +## Features + +* As mentioned above, CryptPad instances can be configured to temporarily archive files instead of deleting them permanently. + * as a user this means if you accidentally delete a file you have the option of contacting your administrator and asking them to help + * if they're really nice and have the spare time to help you, they might actually recover your data! +* A contributor is working on translating CryptPad into the Catalan language. + * if your preferred language isn't supported, you can do the same on https://weblate.cryptpad.fr +* We added the ability to add colors to folders in users CryptDrives, along with support for arbitrary folder metadata which we aren't using yet. +* Users with existing friends on the platform will run a migration to allow them to share pads with friends directly instead of sending them a link. + * they'll receive a notification indicating the title of the pad and who shared it + * if you've already added friends on the platform, you can send them pads from the usual "sharing menu" +* Our code editor already offered the ability to set their color theme and highlighting mode, but now those values will be previewed when mousing over the the option in the dropdown. + * Our slide editor now offers the same theme selection as the code editor +* It's now possible to view the history of a shared folder by clicking the history button while viewing the shared folder's contents. + +## Bug fixes + +* The CryptDrive received a number of usability fixes this time around: + * better styles when hovering over interactive elements in the drive (cursors, shading, etc) + * clicking the history button in the drive a second time will exit history mode + * after being resized, the tree pane now correctly responds to mobile layout styles + * the path indicator also adapts to very narrow layouts + * the user's current location is preserved when renaming the current folder or its ancestors + * you can right-click on elements in the tree and expand or collapse all of their children +* A user noticed that one-on-one chats did not seem to be deleted, as their messages were still available after a reload. + * they were deleted but our usage of the sharedWorker API incorrectly preserved a local cache of those message until you closed all of your browser tabs +* We've also fixed some elements of the chat UI, notably the position of the chat's scrollbar when first loading older messages and how the interface scrolls to keep up with new messages. +* We've noticed some cases of tooltips getting stuck in the UI and implemented some measures to prevent this from happening. +* After "unfriending" another user it was possible that they would be automatically re-added as friends. + # Xenops release (v2.23.0) ## Goals diff --git a/Dockerfile b/Dockerfile index 7378b14ed..0093f414c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,6 +33,7 @@ VOLUME /cryptpad/tasks VOLUME /cryptpad/block VOLUME /cryptpad/blob VOLUME /cryptpad/blobstage +VOLUME /cryptpad/data # Copy cryptpad and tini from the build container COPY --from=build /sbin/tini /sbin/tini @@ -44,4 +45,4 @@ WORKDIR /cryptpad EXPOSE 3000 3001 # Run cryptpad on startup -CMD ["/sbin/tini", "--", "/cryptpad/container-start.sh"] \ No newline at end of file +CMD ["/sbin/tini", "--", "/cryptpad/container-start.sh"] diff --git a/config/config.example.js b/config/config.example.js index eb134591c..5f4473b99 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -64,6 +64,19 @@ module.exports = { //"https://my.awesome.website/user/#/1/cryptpad-user1/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=", ], + /* CryptPad's administration panel includes a "support" tab + * wherein administrators with a secret key can view messages + * sent from users via the encrypted forms on the /support/ page + * + * To enable this functionality: + * run `node ./scripts/generate-admin-keys.js` + * save the public key in your config in the value below + * add the private key via the admin panel + * and back it up in a secure manner + * + */ + // supportMailboxPublicKey: "", + /* ===================== * Infra setup * ===================== */ diff --git a/customize.dist/fonts/cptools/fonts/cptools.svg b/customize.dist/fonts/cptools/fonts/cptools.svg index f7fe0879f..93eef8d38 100644 --- a/customize.dist/fonts/cptools/fonts/cptools.svg +++ b/customize.dist/fonts/cptools/fonts/cptools.svg @@ -25,4 +25,5 @@ + \ No newline at end of file diff --git a/customize.dist/fonts/cptools/fonts/cptools.ttf b/customize.dist/fonts/cptools/fonts/cptools.ttf index 1dac2ff87..18338a9ee 100644 Binary files a/customize.dist/fonts/cptools/fonts/cptools.ttf and b/customize.dist/fonts/cptools/fonts/cptools.ttf differ diff --git a/customize.dist/fonts/cptools/fonts/cptools.woff b/customize.dist/fonts/cptools/fonts/cptools.woff index 4f01d5d15..d8f56ba86 100644 Binary files a/customize.dist/fonts/cptools/fonts/cptools.woff and b/customize.dist/fonts/cptools/fonts/cptools.woff differ diff --git a/customize.dist/fonts/cptools/style.css b/customize.dist/fonts/cptools/style.css index 952207f15..349b62f2b 100644 --- a/customize.dist/fonts/cptools/style.css +++ b/customize.dist/fonts/cptools/style.css @@ -1,9 +1,9 @@ @font-face { font-family: 'cptools'; src: - url('fonts/cptools.ttf?yr9e7c') format('truetype'), - url('fonts/cptools.woff?yr9e7c') format('woff'), - url('fonts/cptools.svg?yr9e7c#cptools') format('svg'); + url('fonts/cptools.ttf?cljhos') format('truetype'), + url('fonts/cptools.woff?cljhos') format('woff'), + url('fonts/cptools.svg?cljhos#cptools') format('svg'); font-weight: normal; font-style: normal; } @@ -24,6 +24,9 @@ -moz-osx-font-smoothing: grayscale; } +.cptools-folder-upload:before { + content: "\e912"; +} .cptools-folder-no-color:before { content: "\e900"; } diff --git a/customize.dist/loading.js b/customize.dist/loading.js index e8f8a9453..765ec56f9 100644 --- a/customize.dist/loading.js +++ b/customize.dist/loading.js @@ -169,6 +169,28 @@ define([], function () { height: 100%; background: #5cb85c; } + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(1800deg); + } +} + +.cp-spinner { + display: inline-block; + box-sizing: border-box; + width: 80px; + height: 80px; + border: 11px solid lightgrey; + border-radius: 50%; + border-top-color: transparent; + animation: spin infinite 3s; + animation-timing-function: cubic-bezier(.6,0.15,0.4,0.85); +} + */}).toString().slice(14, -3); var urlArgs = window.location.href.replace(/^.*\?([^\?]*)$/, function (all, x) { return x; }); var elem = document.createElement('div'); @@ -182,7 +204,7 @@ define([], function () { '', '
', '
', - '', + '', '
', '

', '
' diff --git a/customize.dist/messages.js b/customize.dist/messages.js index 80fffddd0..7fa088c43 100755 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -3,8 +3,8 @@ var map = { 'ca': 'Català', 'de': 'Deutsch', - 'es': 'Español', 'el': 'Ελληνικά', + 'es': 'Español', 'fr': 'Français', 'it': 'Italiano', 'nb': 'Norwegian Bokmål', @@ -12,8 +12,8 @@ var map = { 'pt-br': 'Português do Brasil', 'ro': 'Română', 'ru': 'Русский', + //'te': 'తెలుగు', 'zh': '繁體中文', - 'te': 'తెలుగు', }; var messages = {}; diff --git a/customize.dist/pages.js b/customize.dist/pages.js index 10e712a30..3fd5e63bb 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -103,7 +103,7 @@ define([ ])*/ ]) ]), - h('div.cp-version-footer', "CryptPad v2.23.0 (Xenops)") + h('div.cp-version-footer', "CryptPad v2.25.0 (Zebra)") ]); }; diff --git a/customize.dist/pages/about.js b/customize.dist/pages/about.js index 6410debbf..e683da183 100644 --- a/customize.dist/pages/about.js +++ b/customize.dist/pages/about.js @@ -21,22 +21,6 @@ define([ h('h2.text-center', Msg.about_core) ]), ]), - h('div.row.align-items-center', [ - h('div.col-12.col-sm-12.col-md-12.col-lg-6.cp-bio-avatar', [ - h('img.img-fluid', {'src': '/customize/images/CalebJames.jpg'}) - ]), - h('div.col-12.col-sm-12.col-md-12.col-lg-6.cp-profile-det', [ - h('h3', "Caleb James Delisle"), - h('hr'), - Pages.setHTML(h('div#bioCaleb'), '

Caleb is a cryptography developer, Machine Technology graduate of the Franklin County Technical School and lifelong tinkerer.
In 2011, he started the cjdns Open Source project to show that secure networking could be invisible and easily deployed.
After joining XWiki SAS in 2014, he started the CryptPad project with the intent of bringing the same transparent security to collaborative editing.
He\'s always trying to learn from more experienced colleagues and when someone passes through the Research Team office, his favorite words are "Pull up a chair!".

'), - h('a.cp-soc-media', { href : 'https://twitter.com/cjdelisle'}, [ - h('i.fa.fa-twitter') - ]), - h('a.cp-soc-media', { href : 'https://github.com/cjdelisle'}, [ - h('i.fa.fa-github') - ]) - ]), - ]), h('div.row.align-items-center',[ h('div.col-12.col-sm-12.col-md-12.col-lg-6.order-lg-2.cp-bio-avatar.cp-bio-avatar-right', [ h('img.img-fluid', {'src': '/customize/images/AaronMacSween.jpg'}) @@ -74,16 +58,16 @@ define([ ]), h('div.row.align-items-center', [ h('div.col-12.col-sm-12.col-md-12.col-lg-6.cp-bio-avatar', [ - h('img.img-fluid', {'src': '/customize/images/Pierre-new.jpg'}) + h('img.img-fluid', {'src': '/customize/images/CalebJames.jpg'}) ]), h('div.col-12.col-sm-12.col-md-12.col-lg-6.cp-profile-det', [ - h('h3', "Pierre Bondoerffer"), + h('h3', "Caleb James Delisle"), h('hr'), - Pages.setHTML(h('div#bioPierre'), '

Resident CSS wizard and emoji extraordinaire, Pierre is passionate about anything related to technology. He loves to hack around computers and put parts together.
He is currently studying at 42, where he learns about algorithms, networking, kernel programming and graphics.
As a part of an internship, he joined XWiki SAS and worked on CryptPad to improve user experience. He also maintains the Spanish translation.

'), - h('a.cp-soc-media', { href : 'https://twitter.com/pbondoer'}, [ + Pages.setHTML(h('div#bioCaleb'), '

Caleb is a cryptography developer, Machine Technology graduate of the Franklin County Technical School and lifelong tinkerer.
In 2011, he started the cjdns Open Source project to show that secure networking could be invisible and easily deployed.
After joining XWiki SAS in 2014, he started the CryptPad project with the intent of bringing the same transparent security to collaborative editing.
He\'s always trying to learn from more experienced colleagues and when someone passes through the Research Team office, his favorite words are "Pull up a chair!".

'), + h('a.cp-soc-media', { href : 'https://twitter.com/cjdelisle'}, [ h('i.fa.fa-twitter') ]), - h('a.cp-soc-media', { href : 'https://github.com/pbondoer'}, [ + h('a.cp-soc-media', { href : 'https://github.com/cjdelisle'}, [ h('i.fa.fa-github') ]) ]), diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less index b2076034c..8aa99e403 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -137,6 +137,15 @@ @colortheme_admin-color: #FFF; @colortheme_admin-warn: #ffae00; +@colortheme_notifications-bg: #4ae397; +@colortheme_notifications-color: #000; +@colortheme_notifications-warn: #e34a85; + +@colortheme_support-bg: #42d1f4; +@colortheme_support-color: #000; +@colortheme_support-warn: #9A37F7; + + // Sidebar layout (profile / settings) @colortheme_sidebar-active: #fff; @colortheme_sidebar-left-bg: #eee; diff --git a/customize.dist/src/less2/include/contextmenu.less b/customize.dist/src/less2/include/contextmenu.less index 4d81cb9aa..e4d1318ec 100644 --- a/customize.dist/src/less2/include/contextmenu.less +++ b/customize.dist/src/less2/include/contextmenu.less @@ -23,7 +23,7 @@ } .dropdown-toggle { transform: rotate(270deg); - margin-left: 10px; + margin-left: 1rem; float: right; } .dropdown-menu { @@ -40,6 +40,7 @@ .fa, .cptools { margin-right: 1rem; color: @colortheme_context-menu-icon-color; + width: 16px; } } } diff --git a/customize.dist/src/less2/include/notifications.less b/customize.dist/src/less2/include/notifications.less index 4b37c14b4..f27d6ed60 100644 --- a/customize.dist/src/less2/include/notifications.less +++ b/customize.dist/src/less2/include/notifications.less @@ -1,4 +1,5 @@ @import (reference) "./colortheme-all.less"; +@import (reference) "./avatar.less"; .notifications_main() { --LessLoader_require: LessLoader_currentFile(); @@ -14,6 +15,7 @@ display: flex; .cp-notification-content { flex: 1; + align-items: stretch; min-width: 0; p { word-break: break-word; @@ -28,8 +30,7 @@ .cp-notification-dismiss { color: black; width: 25px; - height: 100%; - display: none; + display: flex; align-items: center; justify-content: center; cursor: pointer; @@ -39,6 +40,33 @@ } } } + hr { + margin: 0px !important; + } + .cp-notifications-gotoapp { + p { + padding: 10px 0 !important; + text-align: center !important; + font-weight: bold; + cursor: pointer; + &:hover { + background-color: rgba(0,0,0,0.1); + } + } + } + .cp-notifications-requestedit-verified { + display: flex; + align-items: center; + &> span.cp-avatar { + .avatar_main(30px); + } + &> span { + margin-right: 10px; + } + &> p { + margin: 0; + } + } } diff --git a/customize.dist/src/less2/include/sidebar-layout.less b/customize.dist/src/less2/include/sidebar-layout.less index 07471183a..b25cbf43c 100644 --- a/customize.dist/src/less2/include/sidebar-layout.less +++ b/customize.dist/src/less2/include/sidebar-layout.less @@ -90,12 +90,21 @@ button.btn { @button-bg: @colortheme_sidebar-button-bg; @button-red-bg: @colortheme_sidebar-button-red-bg; + @button-alt-bg: @colortheme_sidebar-button-alt-bg; background-color: @button-bg; border-color: darken(@button-bg, 10%); color: white; &:hover { background-color: darken(@button-bg, 10%); } + &.btn-secondary { + background-color: @button-alt-bg; + border-color: darken(@button-alt-bg, 10%); + color: black; + &:hover { + background-color: darken(@button-alt-bg, 10%); + } + } &.btn-danger { background-color: @button-red-bg; border-color: darken(@button-red-bg, 10%); diff --git a/customize.dist/src/less2/include/support.less b/customize.dist/src/less2/include/support.less new file mode 100644 index 000000000..0a352ec32 --- /dev/null +++ b/customize.dist/src/less2/include/support.less @@ -0,0 +1,86 @@ +@import (reference) "./colortheme-all.less"; +.support_main () { + @ticket-bg: #F7F7F7; + @msg-bg: #eee; + @fromme-bg: #ddd; + .cp-support-form-container { + [type="text"] { + width: @sidebar_button-width; + margin-bottom: 10px; + } + textarea { + width: 2*@sidebar_button-width; + max-width: 90%; + padding: 10px 15px; + height: 300px; + } + } + .cp-support-container { + .cp-support-list-ticket { + display: flex; + flex-flow: column; + background-color: @ticket-bg; + padding: 10px; + width: 1200px; + max-width: 90%; + margin: 5px auto; + .cp-support-list-message { + background-color: @msg-bg; + margin: 2px; + padding: 2px 5px; + .cp-support-fromme { + background-color: @fromme-bg; + } + .cp-support-showdata { + cursor: pointer; + background-color: @fromme-bg; + .cp-support-message-data { + display: none; + cursor: default; + } + } + .cp-support-message-time { + float: right; + } + pre { + margin-bottom: 0; + white-space: pre-wrap; + &.cp-support-message-content { + margin-top: 10px; + margin-bottom: 10px; + } + } + } + .cp-support-list-actions { + order: 3; + .cp-support-hide { + display: none; + } + } + .cp-support-form-container { + order: 2; + } + &.cp-support-list-closed { + .cp-support-list-actions { + display: block !important; + .cp-support-answer, .cp-support-close { + display: none; + } + .cp-support-hide { + display: inline; + } + } + .cp-support-form-container { + display: none !important; + } + } + button { + margin-left: 2px; + margin-right: 5px; + } + } + } +} + + + diff --git a/docker-compose.yml b/docker-compose.yml index 8f5f2528d..73673f7dd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,3 +31,4 @@ services: - ./data/tasks:/cryptpad/tasks:rw - ./data/block:/cryptpad/block:rw - ./data/config:/cryptpad/cfg:rw + - ./data/data:/cryptpad/data:rw diff --git a/package-lock.json b/package-lock.json index 30b43f03e..7a91644fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cryptpad", - "version": "2.23.0", + "version": "2.25.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -573,16 +573,16 @@ "dev": true }, "jshint": { - "version": "2.9.7", - "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.9.7.tgz", - "integrity": "sha512-Q8XN38hGsVQhdlM+4gd1Xl7OB1VieSuCJf+fEJjpo59JH99bVJhXRXAh26qQ15wfdd1VPMuDWNeSWoNl53T4YA==", + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.10.2.tgz", + "integrity": "sha512-e7KZgCSXMJxznE/4WULzybCMNXNAd/bf5TSrvVEq78Q/K8ZwFpmBqQeDtNiHc3l49nV4E/+YeHU/JZjSUIrLAA==", "dev": true, "requires": { "cli": "~1.0.0", "console-browserify": "1.1.x", "exit": "0.1.x", "htmlparser2": "3.8.x", - "lodash": "~4.17.10", + "lodash": "~4.17.11", "minimatch": "~3.0.2", "shelljs": "0.3.x", "strip-json-comments": "1.0.x" @@ -697,9 +697,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", + "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==", "dev": true }, "lodash.clonedeep": { @@ -709,10 +709,9 @@ "dev": true }, "lodash.merge": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", - "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==", - "dev": true + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, "lodash.sortby": { "version": "4.7.0", diff --git a/package.json b/package.json index 06c187f11..585e8b8fc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cryptpad", "description": "realtime collaborative visual editor with zero knowlege server", - "version": "2.23.0", + "version": "2.25.0", "license": "AGPL-3.0+", "repository": { "type": "git", @@ -23,7 +23,7 @@ }, "devDependencies": { "flow-bin": "^0.59.0", - "jshint": "~2.9.1", + "jshint": "^2.10.2", "less": "2.7.1", "lesshint": "^4.5.0", "selenium-webdriver": "^3.6.0" diff --git a/scripts/generate-admin-keys.js b/scripts/generate-admin-keys.js new file mode 100644 index 000000000..26d26537e --- /dev/null +++ b/scripts/generate-admin-keys.js @@ -0,0 +1,25 @@ +/* jshint esversion: 6, node: true */ + +const Nacl = require('tweetnacl'); + +const keyPair = Nacl.box.keyPair(); +console.log("You've just generated a new key pair for your support mailbox."); + +console.log("The public key should first be added to your config.js file ('supportMailboxPublicKey'), then save and restart the server."); +console.log("Once restarted, administrators (specified with 'adminKeys' in config.js too) will be able to add the private key into their account. This can be done using the administration panel."); +console.log("You will have to send the private key to each administrator manually so that they can add it to their account."); +console.log(); +console.log("WARNING: the public and private keys must come from the same key pair to have a working encrypted support mailbox."); +console.log(); +console.log("NOTE: You can change the key pair at any time if you want to revoke access to the support mailbox. You just have to generate a new key pair using this file, and replace the value in config.js, and then send the new private key to the administrators of your choice."); + + +console.log(); +console.log(); +console.log("Your public key (add it to config.js):"); +console.log(Nacl.util.encodeBase64(keyPair.publicKey)); + +console.log(); +console.log(); +console.log("Your private key (store it in a safe place and send it to your instance's admins):"); +console.log(Nacl.util.encodeBase64(keyPair.secretKey)); diff --git a/server.js b/server.js index b10b32da8..b8c2b8163 100644 --- a/server.js +++ b/server.js @@ -193,6 +193,7 @@ app.get('/api/config', function(req, res){ httpUnsafeOrigin: config.httpUnsafeOrigin, adminEmail: config.adminEmail, adminKeys: admins, + supportMailbox: config.supportMailboxPublicKey }, null, '\t'), 'obj.httpSafeOrigin = ' + (function () { if (config.httpSafeOrigin) { return '"' + config.httpSafeOrigin + '"'; } diff --git a/www/admin/app-admin.less b/www/admin/app-admin.less index c3b8a31de..a1c1ee779 100644 --- a/www/admin/app-admin.less +++ b/www/admin/app-admin.less @@ -1,5 +1,6 @@ @import (reference) '../../customize/src/less2/include/framework.less'; @import (reference) '../../customize/src/less2/include/sidebar-layout.less'; +@import (reference) '../../customize/src/less2/include/support.less'; &.cp-app-admin { @@ -9,6 +10,11 @@ @color: @colortheme_admin-color ); .sidebar-layout_main(); + .support_main(); + + .cp-hidden { + display: none !important; + } display: flex; flex-flow: column; diff --git a/www/admin/inner.js b/www/admin/inner.js index 83dec1415..4c335dd12 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -9,6 +9,8 @@ define([ '/customize/messages.js', '/common/common-interface.js', '/common/common-util.js', + '/common/common-hash.js', + '/support/ui.js', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', @@ -23,7 +25,9 @@ define([ h, Messages, UI, - Util + Util, + Hash, + Support ) { var APP = {}; @@ -41,6 +45,10 @@ define([ 'cp-admin-active-pads', 'cp-admin-registered', 'cp-admin-disk-usage', + ], + 'support': [ + 'cp-admin-support-list', + 'cp-admin-support-init' ] }; @@ -94,7 +102,6 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ACTIVE_SESSIONS', }, function (e, data) { - console.log(e, data); var total = data[0]; var ips = data[1]; $div.append(h('pre', total + ' (' + ips + ')')); @@ -160,6 +167,108 @@ define([ return $div; }; + var supportKey = ApiConfig.supportMailbox; + create['support-list'] = function () { + if (!supportKey || !APP.privateKey) { return; } + var $div = makeBlock('support-list'); + $div.addClass('cp-support-container'); + var hashesById = {}; + + // Register to the "support" mailbox + common.mailbox.subscribe(['supportadmin'], { + onMessage: function (data) { + /* + Get ID of the ticket + If we already have a div for this ID + Push the message to the end of the ticket + If it's a new ticket ID + Make a new div for this ID + */ + var msg = data.content.msg; + var hash = data.content.hash; + var content = msg.content; + var id = content.id; + var $ticket = $div.find('.cp-support-list-ticket[data-id="'+id+'"]'); + + hashesById[id] = hashesById[id] || []; + if (hashesById[id].indexOf(hash) === -1) { + hashesById[id].push(data); + } + + if (msg.type === 'CLOSE') { + // A ticket has been closed by the admins... + if (!$ticket.length) { return; } + $ticket.addClass('cp-support-list-closed'); + $ticket.append(APP.support.makeCloseMessage(content, hash)); + return; + } + if (msg.type !== 'TICKET') { return; } + + if (!$ticket.length) { + $ticket = APP.support.makeTicket($div, content, function () { + var error = false; + hashesById[id].forEach(function (d) { + common.mailbox.dismiss(d, function (err) { + if (err) { + error = true; + console.error(err); + } + }); + }); + if (!error) { $ticket.remove(); } + }); + } + $ticket.append(APP.support.makeMessage(content, hash)); + } + }); + return $div; + }; + + var checkAdminKey = function (priv) { + if (!supportKey) { return; } + return Hash.checkBoxKeyPair(priv, supportKey); + }; + + create['support-init'] = function () { + var $div = makeBlock('support-init'); + if (!supportKey) { + $div.append(h('p', Messages.admin_supportInitHelp)); + return $div; + } + if (!APP.privateKey || !checkAdminKey(APP.privateKey)) { + $div.append(h('p', Messages.admin_supportInitPrivate)); + + var error = h('div.cp-admin-support-error'); + var input = h('input.cp-admin-add-private-key'); + var button = h('button.btn.btn-primary', Messages.admin_supportAddKey); + + if (APP.privateKey && !checkAdminKey(APP.privateKey)) { + $(error).text(Messages.admin_supportAddError); + } + + $div.append(h('div', [ + error, + input, + button + ])); + + $(button).click(function () { + var key = $(input).val(); + if (!checkAdminKey(key)) { + $(input).val(''); + return void $(error).text(Messages.admin_supportAddError); + } + sFrameChan.query("Q_ADMIN_MAILBOX", key, function () { + APP.privateKey = key; + $('.cp-admin-support-init').hide(); + APP.$rightside.append(create['support-list']()); + }); + }); + return $div; + } + return; + }; + var hideCategories = function () { APP.$rightside.find('> div').hide(); }; @@ -180,6 +289,7 @@ define([ var $category = $('
', {'class': 'cp-sidebarlayout-category'}).appendTo($categories); if (key === 'general') { $category.append($('', {'class': 'fa fa-user-o'})); } if (key === 'stats') { $category.append($('', {'class': 'fa fa-hdd-o'})); } + if (key === 'support') { $category.append($('', {'class': 'fa fa-life-ring'})); } if (key === active) { $category.addClass('cp-leftside-active'); @@ -236,8 +346,10 @@ define([ return void UI.errorLoadingScreen(Messages.admin_authError || '403 Forbidden'); } + APP.privateKey = privateData.supportPrivateKey; APP.origin = privateData.origin; APP.readOnly = privateData.readOnly; + APP.support = Support.create(common, true); // Content var $rightside = APP.$rightside; diff --git a/www/admin/main.js b/www/admin/main.js index cdfaf115c..817d2bd2e 100644 --- a/www/admin/main.js +++ b/www/admin/main.js @@ -38,6 +38,9 @@ define([ }).nThen(function (/*waitFor*/) { var addRpc = function (sframeChan, Cryptpad/*, Utils*/) { // Adding a new avatar from the profile: pin it and store it in the object + sframeChan.on('Q_ADMIN_MAILBOX', function (data, cb) { + Cryptpad.addAdminMailbox(data, cb); + }); sframeChan.on('Q_ADMIN_RPC', function (data, cb) { Cryptpad.adminRpc(data, cb); }); diff --git a/www/assert/main.js b/www/assert/main.js index 17769868d..6ad05a7a2 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -371,6 +371,20 @@ define([ return cb(true); }, "version 2 hash failed to parse correctly"); + assert(function (cb) { + var x; + var set_x = function (v) { + x = v; + }; + + Util.mkAsync(set_x)(7); + set_x(5); + + Util.mkAsync(function (expected) { + cb(x === expected); + })(7); + }, "test mkAsync"); + assert(function (cb) { Wire.create({ constructor: function (cb) { diff --git a/www/code/export.js b/www/code/export.js index 23d689361..04616a192 100644 --- a/www/code/export.js +++ b/www/code/export.js @@ -8,7 +8,7 @@ define([ module.main = function (userDoc, cb) { var mode = userDoc.highlightMode || 'gfm'; var content = userDoc.content; - module.type = SFCodeMirror.getContentExtension(mode); + module.ext = SFCodeMirror.getContentExtension(mode); cb(SFCodeMirror.fileExporter(content)); }; diff --git a/www/code/inner.js b/www/code/inner.js index 9e4d0a207..b061e36fa 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -363,7 +363,15 @@ define([ }); framework.setFileExporter(CodeMirror.getContentExtension, CodeMirror.fileExporter); - framework.setFileImporter({}, CodeMirror.fileImporter); + framework.setFileImporter({}, function () { + /* setFileImporter currently takes a function with the following signature: + (content, file) => {} + I used 'apply' with 'arguments' to avoid breaking things if this API ever changes. + */ + var ret = CodeMirror.fileImporter.apply(null, Array.prototype.slice.call(arguments)); + previewPane.modeChange(ret.mode); + return ret; + }); framework.setNormalizer(function (c) { return { diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index 49be02723..e4d5aa4f9 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']; + config.registeredOnlyTypes = ['file', 'contacts', 'oodoc', 'ooslide', 'sheet', '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-constants.js b/www/common/common-constants.js index 9eb3c4075..986e115e2 100644 --- a/www/common/common-constants.js +++ b/www/common/common-constants.js @@ -17,6 +17,6 @@ define(function () { // Sub plan: 'CryptPad_plan', // Apps - criticalApps: ['profile', 'settings', 'debug', 'admin'] + criticalApps: ['profile', 'settings', 'debug', 'admin', 'support', 'notifications'] }; }); diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 3ef802681..e49b0e217 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -85,6 +85,34 @@ define([ return id; }; + /* Given a base64-encoded public key, deterministically derive a channel id + Used for support mailboxes + */ + Hash.getChannelIdFromKey = function (publicKey) { + if (!publicKey) { return; } + return uint8ArrayToHex(Hash.decodeBase64(publicKey).subarray(0,16)); + }; + + /* Given a base64-encoded asymmetric private key + derive the corresponding public key + */ + Hash.getBoxPublicFromSecret = function (priv) { + if (!priv) { return; } + var u8_priv = Hash.decodeBase64(priv); + var pair = Nacl.box.keyPair.fromSecretKey(u8_priv); + return Hash.encodeBase64(pair.publicKey); + }; + + /* Given a base64-encoded private key and public key + check that the keys are part of a valid keypair + */ + Hash.checkBoxKeyPair = function (priv, pub) { + if (!pub || !priv) { return false; } + var u8_priv = Hash.decodeBase64(priv); + var pair = Nacl.box.keyPair.fromSecretKey(u8_priv); + return pub === Hash.encodeBase64(pair.publicKey); + }; + Hash.createRandomHash = function (type, password) { var cryptor; if (type === 'file') { diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 5391636ab..e44a4ecff 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -799,6 +799,11 @@ define([ // forever, this is a solution which just searches for tooltips which have no corrisponding element and removes // them. $('.tippy-popper').each(function (i, el) { + if (el._tippy && el._tippy.reference && document.body.contains(el._tippy.reference)) { + el._tippy.destroy(); + el.remove(); + return; + } if ($('[aria-describedby=' + el.getAttribute('id') + ']').length === 0) { el.remove(); } @@ -849,6 +854,9 @@ define([ mutations.forEach(function(mutation) { if (mutation.type === "childList") { for (var i = 0; i < mutation.addedNodes.length; i++) { + if ($(mutation.addedNodes[i]).attr('title')) { + addTippy(0, mutation.addedNodes[i]); + } $(mutation.addedNodes[i]).find('[title]').each(addTippy); } diff --git a/www/common/common-messenger.js b/www/common/common-messenger.js index 9868e0266..1102f94a2 100644 --- a/www/common/common-messenger.js +++ b/www/common/common-messenger.js @@ -451,9 +451,7 @@ define([ var txid = parsed[1]; var req = getRangeRequest(txid); var type = parsed[0]; - if (!req) { - return void console.error("received response to unknown request"); - } + if (!req) { return; } if (!req.cb) { // This is the initial history for a pad chat diff --git a/www/common/common-thumbnail.js b/www/common/common-thumbnail.js index 2a03715cd..a6e5d5128 100644 --- a/www/common/common-thumbnail.js +++ b/www/common/common-thumbnail.js @@ -196,8 +196,8 @@ define([ }; Thumb.initPadThumbnails = function (common, opts) { - if (!opts.href || !opts.getContent) { - throw new Error("href and getContent are needed for thumbnails"); + if (!opts.type || !opts.getContent) { + throw new Error("type and getContent are needed for thumbnails"); } var oldThumbnailState; var mkThumbnail = function () { @@ -230,9 +230,15 @@ define([ if (!Visible.currently()) { to = window.setTimeout(interval, Thumb.UPDATE_FIRST); } }; + var addThumbnail = function (err, thumb, $span, cb) { + var u8 = Nacl.util.decodeBase64(thumb.split(',')[1]); + var blob = new Blob([u8], { + type: 'image/png' + }); + var url = URL.createObjectURL(blob); var img = new Image(); - img.src = thumb.slice(0,5) === 'data:' ? thumb : 'data:image/png;base64,'+thumb; + img.src = url; $span.find('.cp-icon').hide(); $span.prepend(img); cb($(img)); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index d4a07c5a7..9aa313779 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -119,15 +119,32 @@ define([ $('