Merge pull request #2 from xwiki-labs/staging

Staging
pull/1/head
Adrian Nöthlich 6 years ago committed by GitHub
commit 52df6234f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,6 +4,8 @@ www/common/pdfjs/
www/common/tippy/
www/common/highlight/
www/common/jquery-ui/
www/common/onlyoffice/sdkjs
www/common/onlyoffice/web-apps
server.js
www/common/old-media-tag.js
@ -23,3 +25,4 @@ www/kanban/jscolor.js
www/common/media-tag-nacl.min.js
customize/

@ -1,3 +1,130 @@
# Quokka release (v2.16.0)
## Goals
We set aside an additional week for this release in order to deploy _encrypted spreadsheets_, which we've been working toward for a long time.
This feature combines our usual focus on privacy with OnlyOffice's spreadsheet editor.
At least for this first release we're still considering this functionality to be **highly experimental**.
We've done our best to make this new application fun and easy to use, however, it will still require a lot of work before it supports all the features that you can expect from our other editors.
We welcome you to try it out and report any difficulties you encounter, though you may want to wait before you start using it for all your financial documents.
## Update notes
* OnlyOffice requires more lax Content Security Policy headers than the rest of the platform. Compare your configuration against `config.example.js`.
* If you are running a customized `application_config.js`, you may need to update `availablePadTypes` and `registeredOnlyTypes`. See [the wiki](https://github.com/xwiki-labs/cryptpad/wiki/Application-config) for more details.
* In addition to a few serverside changes for the new spreadsheet editor, this release fixes a bug that affected system administrators who had set custom limits for some users and disabled communication with our payment server. Restart your server after updating for these changes to take effect.
## Features
* We've implemented a feature we call _ephemeral channels_, which we use for displaying other users' cursors in our rich text, code, and slide editors. Ephemeral channels behave exactly like our regular server messaging infrastructure except that no history is stored.
* We've added additional highlighting modes in our code editor for C, C++, Java, and Objective-C
* We've imposed a limit of five items for the table which displays upload progress, in order to keep it from taking up too much space on the screen when users upload many files in one session.
## Bugfixes
* [@3n2pS3P5kG23S96yxRbUHAZajuH2F](https://github.com/3n2pS3P5kG23S96yxRbUHAZajuH2F) reported an issue shortly after our last release which threw an error if our feedback API was disabled. The fix was on our master branch, but now it will be properly tagged.
* We noticed an issue in our code editor where imported .md files were interpreted as text, instead of markdown. This caused the preview pane to stop working.
* We also discovered an issue which had broken our CryptDrive import function, but as far as we know it did not affect any users. It should be working as intended now.
* Unfortunately, we don't do a lot of testing on Internet Explorer 11, but one of our users was kind enough to report an error. We tracked down a few uses of APIs which do not exist on IE11, and replaced them with compatible functions, so now users of IE11 will be able to enjoy CryptPad once more.
# Pademelon release (v2.15.0)
## Goals
For this release we planned to improve upon last release's introduction of the display of other users' cursors in our code and slide editors by adding the same functionality to our rich text editor.
Beyond just producing software, the CryptPad team has also begun to produce peer-reviewed papers.
We have previously published [Private Document Editing with Some Trust](https://dl.acm.org/citation.cfm?doid=3209280.3209535) as a part of the 2018 proceedings of the ACM Symposium on Document Engineering.
We have recently been accepted for publication as a part of [HCI-CPT](http://2019.hci.international/hci-cpt): the first international conference on HCI (Human Computer Interaction) for cybersecurity, privacy and trust.
In preparation for this publication we've begun to collect additional usage data in order to inform the wider community of our findings regarding usability of cryptography-based collaboration systems.
## Update notes
* Updating to version 2.15.0 from 2.14.0 should only require that update to the latest clientside code via git, and update any cache-busting parameters you've set.
* Several of our third-party clientside dependencies have been updated, and you may optionally run `bower update` to receive their latest versions.
* As explained above, we have added a number of new keys to our existing feedback system. The new keys are detailed below
* HOME_SUPPORT_CRYPTPAD informs us when users discover our opencollective campaign from the CryptPad home page
* UPGRADE_ACCOUNT informs us when someone clicks the upgrade account button from their CryptDrive or settings page
* SUPPORT_CRYPTPAD is not active on our CryptPad instance, since this key is only sent when clicking the _donate button_ which is shown when upgraded accounts are disabled
* DELETE_ACCOUNT_AUTOMATIC informs us when somebody deletes their account automatically from the settings page. Automatic account deletion is only available for accounts created since version 1.29.0
* DELETE_ACCOUNT_MANUAL informs us when a user generates the proof of their account ownership which is required for manual account deletion. This feature is available only for accounts predating version 1.29.0
* OWNED_DRIVE_MIGRATION informs us when a user migrates their CryptDrive from our legacy format (which does not support automatic deletion) to our newer format (which does) via the settings page
* PASSWORD_CHANGED informs us when a user changes their password from the settings page
* NO_WEBRTC informs us when a users browser does not support WebRTC at all via a crude test which never actually runs any WebRTC-based code
* SUBSCRIPTION_BUTTON informs us when a user navigates to our paid account administration panel from their settings page
* LOGOUT_EVERYWHERE informs us when a user executes the command to log out of their account on all remote devices from the settings page
* We've implemented the ability to configure which applications are available on a particular CryptPad instance via `cryptpad/customize/application_config.js`. Two arrays (`config.availablePadTypes` and `config.registeredOnlyTypes`) define which applications are available to everyone, and which applications are available to registered users. Due to a bug which was discovered, this behaviour is incorrect for our encrypted file viewer, and as a result encrypted files cannot currently be disabled. This will be addressed in our next release.
## Features
* Our rich text editor now displays other users' cursors when editing with a group. Preferences for this behaviour can be defined via the settings page.
* Links in our rich text editor can now be clicked more easily, as a small tooltip with a clickable link will be displayed above the editable link in the document.
* Users who wish to be notified of spelling errors in their rich text pads can enable spellcheck via the settings page.
* As noted above, various pad types can be disabled by instance administrators via `customize/application_config.js`.
* We've enabled a feature in the settings page which will migrate users' CryptDrive from our legacy format to our latest format (which supports automatic deletion). Only users with accounts dating back to version 1.29.0 will notice any difference.
* We've worked to improve some usability issues presented by the interaction of _owned files_ and _shared folders_. Since only the owner of an owned document can delete it the owner must keep a record of that document in their CryptDrive even if they place it in a shared folder (where someone else could delete it while they are offline). As such, owned documents were always copied to shared folders instead of being moved, and this proliferation of copies made it more difficult for users to organize their CryptDrives. Duplicated owned documents which are kept in your CryptDrive can now be hidden via the settings page. If those files are removed from a shared folder by another user, the hidden duplicate will be revealed in the root of your CryptDrive's tree.
* Finally, we've implemented the ability to copy documents to multiple shared folders via an entry in the right-click menu for any such document.
## Bugfixes
* We've improved the styles for displaying other users' cursors in the code and slide editors to avoid moving your view of the text when someone else highlights it.
* We've also changed some of the logic for how often other users' cursors are updated and displayed, so as to maximize the accuracy of their position and not show incorrect placements while you are typing.
* We fixed a bug which caused errors while loading your CryptDrive after a shared folder had been deleted.
# Opossum release (v2.14.0)
## Goals
For this release we chose to focus on our in-pad chat functionality and the ability to show your cursor's position to other users in the same pad.
## Update notes
* We've released an updated version of a serverside dependency: `chainpad-server`
* this addresses a recently introduced bug which is capable of sending more history than clients require under certain circumstances
* to use this updated dependency, run `npm update` and restart your server
## Features
* Our code editor is now capable of displaying other user's cursors within your view of the document.
* this is enabled by default, but you can choose not to share your own cursor, and to disable the display of other users' cursors in your document
* your initial color is chosen randomly, but you can choose any color you like within the settings page alongside the other configuration options for cursors
* After some consideration, we have chosen to change the permissions around the chat functionality embedded within every pad.
* previously we had allowed viewers to participate in chat, even though they could not change the document.
* we decided that this was counter-intuitive
* in the event of an XSS vulnerability it could be used as a vector for privilege escalation
* as such, we have modified our embedded chat functionality to only allow editors to participate
* this change is not backwards-compatible, and so the embedded chat boxes will have dropped their older history
* our assumption is that this will be an improvement for the majority of our users, and that it's fairly safe to drop older history given that chat is a relatively new feature
* if this has affected you in an adverse way, the information is still accessible, and you can contact us if you need a way to recover that information
* Finally, it is now possible to print the rendered markdown content in our code editor, thanks to a contribution from [@joldie](https://github.com/joldie)
# Numbat release (v2.13.0)
## Goals
This release features long-awaited improvements to our Rich Text Pad.
This work was done over a short period, and we're releasing it now so that users can take advantage of the improvements as soon as possible.
## Update notes
* We've fixed a bug related to chat via an update to our messaging server. To install the update, run `npm update`. This server improvement is backwards compatible, so you can update your clientside or serverside dependencies in either order. Restart your server for the changes to take effect.
* You can run `bower update` in order to take advantage of the latest clientside dependencies. Depending on when you last updated you may benefit from updates to Codemirror or some other clientside libraries.
## Features
* We've refactored a great deal of CryptPad's Remote Procedure Call mechanisms related to chat. This should simplify CryptPad and make potential bugs less likely to occur.
## Bugfixes
* The behaviour of the cursor in our rich text editor has been greatly improved. Your experience when collaboratively editing should be noticeably better.
* Characters inserted into rich text pads were sometimes dropped due to a race condition between CKEditor and ChainPad, but this asynchronous behaviour has been resolved. As such the editor should be much more reliable.
* Deleting chat history from the server now removes it from your chat interface and that of remote messengers, where it previously would require a reload of the interface to see the correct chat history.
* We now correctly set owners of a shared chat channel such that either chat participant in a one-to-one room can delete the history.
* If you request history with a `lastKnownHash` which is not in the history, the server informs you that it is not there via a direct message. Clients fall back to a classic full retreival of the history. Previously this would fail, and print a message to the server's stdout.
* Firefox users may have noticed that when they clicked the dropdown menus for styles in the CKEditor toolbar, their scrollbar would jump to the top of the document. Their scroll position is now preserved in cases where it would previously have been disrupted.
# Manatee release (v2.12.0)
## Goals

@ -1,20 +1,49 @@
FROM node:6-alpine
# 6-stretch is the ONLY node 6 release supported by arm32v7, arm64v8 and x86-64 docker hub labels
FROM node:6-stretch
COPY . /cryptpad
WORKDIR /cryptpad
RUN apk add --no-cache git tini \
&& npm install --production \
&& npm install -g bower \
&& bower install --allow-root
EXPOSE 3000 3001
# You want USE_SSL=true if not putting cryptpad behind a proxy
ENV USE_SSL=false
ENV STORAGE=\'./storage/file\'
ENV LOG_TO_STDOUT=true
# Persistent storage needs
VOLUME /cryptpad/datastore
VOLUME /cryptpad/customize
VOLUME /cryptpad/blobstage
VOLUME /cryptpad/pins
VOLUME /cryptpad/tasks
VOLUME /cryptpad/block
ENV USE_SSL=false
ENV STORAGE=\'./storage/file\'
ENV LOG_TO_STDOUT=true
# Required packages
# jq is a build only dependency, removed in cleanup stage
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git jq python
# Install tini for faux init
# sleep 1 is to ensure overlay2 can catch up with the copy prior to running chmod
COPY ./docker-install-tini.sh /
RUN chmod a+x /docker-install-tini.sh \
&& sleep 1 \
&& /docker-install-tini.sh \
&& rm /docker-install-tini.sh
# Cleanup apt
RUN apt-get remove -y --purge jq python \
&& apt-get auto-remove -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install cryptpad
COPY . /cryptpad
WORKDIR /cryptpad
RUN npm install --production \
&& npm install -g bower \
&& bower install --allow-root
# Unsafe / Safe ports
EXPOSE 3000 3001
# Run cryptpad on startup
CMD ["/sbin/tini", "--", "/cryptpad/container-start.sh"]

@ -1,25 +0,0 @@
FROM arm64v8/node:6
COPY . /cryptpad
WORKDIR /cryptpad
RUN npm config set unsafe-perm true
ADD https://github.com/krallin/tini/releases/download/v0.18.0/tini-static-arm64 /sbin/tini
RUN chmod a+x /sbin/tini
RUN apt install -y git \
&& npm install --production \
&& npm install -g bower \
&& bower install --allow-root
EXPOSE 3000 3001
VOLUME /cryptpad/datastore
VOLUME /cryptpad/customize
ENV USE_SSL=false
ENV STORAGE=\'./storage/file\'
ENV LOG_TO_STDOUT=true
CMD ["/sbin/tini", "--", "/cryptpad/container-start.sh"]

@ -36,7 +36,7 @@
"alertifyjs": "1.0.11",
"scrypt-async": "1.2.0",
"require-css": "0.1.10",
"less": "^3.7.1",
"less": "3.7.1",
"bootstrap": "^v4.0.0",
"diff-dom": "2.1.1",
"nthen": "^0.1.5",

@ -88,6 +88,28 @@ module.exports = {
"img-src * blob:",
].join('; '),
// OnlyOffice requires even more lax content security policy in order to function.
ooContentSecurity: [
"default-src 'none'",
"style-src 'unsafe-inline' 'self'" + domain,
// Unsafe inline, unsafe-eval are needed for ckeditor :(
"script-src 'self' 'unsafe-eval' 'unsafe-inline'" + domain,
"font-src 'self'" + domain,
/* See above under 'contentSecurity' as to how these values should be
* configured for best effect.
*/
"child-src *",
// IE/Edge
"frame-src *",
// see the comment above in the 'contentSecurity' section
"connect-src 'self' blob: ws: wss:" + domain,
// (insecure remote) images are included by users of the wysiwyg who embed photos in their pads
"img-src * blob: data:",
].join('; '),
httpPort: 3000,
// This is for allowing the cross-domain iframe to function when developing

@ -28,7 +28,6 @@ CKEDITOR.editorConfig = function( config ) {
config.font_defaultLabel = 'Arial';
config.fontSize_defaultLabel = '16';
config.contentsCss = '/customize/ckeditor-contents.css?' + CKEDITOR.CRYPTPAD_URLARGS;
config.keystrokes = [
[ CKEDITOR.ALT + 121 /*F10*/, 'toolbarFocus' ],

@ -149,3 +149,59 @@ a > img {
border: none;
outline: 1px solid #0782C1;
}
.cp-cursor-position {
cursor: default;
background-color: red;
background-clip: padding-box;
padding: 0 1px;
border: 2px solid red;
border-right-color: transparent !important;
border-left-color: transparent !important;
margin-left: -3px;
margin-right: -3px;
}
.cp-cursor-position[data-type="start"] {
border-left: none;
border-right-width: 4px;
margin-right: -5px;
margin-left: -1px;
}
.cp-cursor-position[data-type="end"] {
border-right: none;
border-left-width: 4px;
margin-left: -5px;
margin-right: -1px;
}
.cp-cursor-avatar {
display: flex;
align-items: center;
}
.cp-cursor-avatar media-tag {
min-height: 32px;
max-height: 32px;
min-width: 32px;
max-width: 32px;
margin-right: 10px;
}
.cp-cursor-avatar media-tag img {
border-radius: 4px;
max-height: 100%;
max-width: 100%;
}
.cp-link-clicked {
position: absolute;
background: white;
border: 1px solid #333;
border-radius: 5px;
padding: 3px 8px;
display: inline-block;
max-width: 200px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.cp-link-clicked a {
cursor: pointer;
}

@ -91,7 +91,7 @@ define([
])
])
]),
h('div.cp-version-footer', "CryptPad v2.12.0 (Manatee)")
h('div.cp-version-footer', "CryptPad v2.16.0 (Quokka)")
]);
};

@ -2,44 +2,51 @@ define([
'jquery',
'/api/config',
'/common/hyperscript.js',
'/common/common-feedback.js',
'/customize/messages.js',
'/customize/application_config.js',
'/common/outer/local-store.js',
'/customize/pages.js'
], function ($, Config, h, Msg, AppConfig, LocalStore, Pages) {
], function ($, Config, h, Feedback, Msg, AppConfig, LocalStore, Pages) {
var urlArgs = Config.requireConf.urlArgs;
var isAvailableType = function (x) {
if (!Array.isArray(AppConfig.availablePadTypes)) { return true; }
return AppConfig.availablePadTypes.some(function (type) {
return x.indexOf(type) > -1;
});
return AppConfig.availablePadTypes.indexOf(x) !== -1;
};
var checkRegisteredType = function (x) {
// Return true if we're registered or if the app is not registeredOnly
if (LocalStore.isLoggedIn()) { return true; }
if (!Array.isArray(AppConfig.registeredOnlyTypes)) { return true; }
return AppConfig.registeredOnlyTypes.indexOf(x) === -1;
};
return function () {
var showingMore = false;
var icons = [
[ 'pad', '/pad/', Msg.main_richTextPad, 'pad' ],
[ 'code', '/code/', Msg.main_codePad, 'code' ],
[ 'slide', '/slide/', Msg.main_slidePad, 'slide' ],
[ 'poll', '/poll/', Msg.main_pollPad, 'poll' ],
[ 'kanban', '/kanban/', Msg.main_kanbanPad, 'kanban' ],
[ 'whiteboard', '/whiteboard/', Msg.main_whiteboardPad, 'whiteboard' ],
[ 'recent', '/drive/', LocalStore.isLoggedIn() ? Msg.main_yourCryptDrive : Msg.main_localPads, 'drive' ]
[ 'pad', Msg.main_richTextPad],
[ 'code', Msg.main_codePad],
[ 'slide', Msg.main_slidePad],
[ 'sheet', Msg.main_sheetPad],
[ 'poll', Msg.main_pollPad],
[ 'kanban', Msg.main_kanbanPad],
[ 'whiteboard', Msg.main_whiteboardPad],
[ 'drive', LocalStore.isLoggedIn() ? Msg.main_yourCryptDrive : Msg.main_localPads]
].filter(function (x) {
return isAvailableType(x[1]);
return isAvailableType(x[0]) && checkRegisteredType(x[0]);
})
.map(function (x, i) {
var s = 'div.bs-callout.cp-callout-' + x[0];
if (i > 2) { s += '.cp-more.cp-hidden'; }
var icon = AppConfig.applicationsIcon[x[3]];
var icon = AppConfig.applicationsIcon[x[0]];
var font = icon.indexOf('cptools') === 0 ? 'cptools' : 'fa';
return h('a', [
{ href: x[1] },
{ href: '/'+ x[0] +'/' },
h(s, [
h('i.' + font + '.' + icon),
h('div.pad-button-text', [ h('h4', x[2]) ])
h('div.pad-button-text', [ h('h4', x[1]) ])
])
]);
});
@ -79,6 +86,7 @@ define([
$(crowdFunding).click(function () {
_link.click();
Feedback.send('HOME_SUPPORT_CRYPTPAD');
});
var blocks = h('div.container',[

@ -30,7 +30,8 @@ define([
UI.createCheckbox('import-recent', Msg.register_importRecent),
]),
h('div.extra', [
h('button.login.first.btn', Msg.login_login)
h('button.login.first.btn', Msg.login_login),
h('button#register.first.btn', Msg.login_register)
])
])
]),

@ -46,6 +46,38 @@
display: block;
}
}
// Code app
body.cp-app-code {
display: block;
* {
visibility: hidden;
height: auto;
max-height: none;
}
#cme_toolbox {
display: none;
}
#cp-app-code-editor {
display: block;
#cp-app-code-container {
display: none;
}
#cp-app-code-preview {
display: block;
#cp-app-code-print {
display: block;
overflow: visible !important;
width: 100%;
visibility: visible;
* { visibility: visible; }
pre { border: none; }
}
#cp-app-code-preview-content {
display: none !important;
}
}
}
}
}
}
}

@ -115,6 +115,18 @@
@colortheme_todo-color: #000;
@colortheme_todo-warn: #cd2532;
@colortheme_oodoc-bg: #5170B5;
@colortheme_oodoc-color: #FFF;
@colortheme_oodoc-warn: #cd2532;
@colortheme_ooslide-bg: #c65d27;
@colortheme_ooslide-color: #FFF;
@colortheme_ooslide-warn: #cd2532;
@colortheme_oocell-bg: #7e983f;
@colortheme_oocell-color: #FFF;
@colortheme_oocell-warn: #cd2532;
@colortheme_kanban-bg: #8C4;
@colortheme_kanban-color: #000;
@colortheme_kanban-warn: #e6385d;

@ -0,0 +1,36 @@
.cursor_main() {
// CodeMirror
.cp-codemirror-cursor {
cursor: default;
background-color: red;
background-clip: padding-box;
padding: 0 1px;
border: 2px solid red;
border-right-color: transparent !important;
border-left-color: transparent !important;
margin-left: -3px;
margin-right: -3px;
}
.cp-codemirror-selection {
background-color: rgba(255,0,0,0.3);
}
// Tippy
.cp-cursor-avatar {
@size: 32px;
display: flex;
align-items: center;
media-tag {
min-height: @size;
max-height: @size;
min-width: @size;
max-width: @size;
margin-right: 10px;
img {
border-radius: 4px;
max-height: 100%;
max-width: 100%;
}
}
}
}

@ -13,6 +13,7 @@
@import (reference) "./app-print.less";
@import (reference) "./app-noscroll.less";
@import (reference) "./messenger.less";
@import (reference) "./cursor.less";
.framework_main(@bg-color, @warn-color, @color) {
--LessLoader_require: LessLoader_currentFile();
@ -38,6 +39,7 @@
.checkmark_main(20px);
.password_main();
.messenger_main();
.cursor_main();
.creation_main(
@bg-color: @bg-color,
@color: @color

@ -16,6 +16,9 @@
.cp-icon-color-profile { color: @colortheme_settings-bg; }
.cp-icon-color-default { color: @colortheme_default-bg; }
.cp-icon-color-todo { color: @colortheme_todo-bg; }
.cp-icon-color-oodoc { color: @colortheme_oodoc-bg; }
.cp-icon-color-ooslide { color: @colortheme_ooslide-bg; }
.cp-icon-color-sheet { color: @colortheme_oocell-bg; }
.cp-icon-color-kanban { color: @colortheme_kanban-bg; }
.cp-border-color-pad { border-color: @colortheme_pad-bg !important; }
@ -30,6 +33,9 @@
.cp-border-color-profile { border-color: @colortheme_settings-bg !important; }
.cp-border-color-default { border-color: @colortheme_default-bg !important; }
.cp-border-color-todo { border-color: @colortheme_todo-bg !important; }
.cp-border-color-oodoc { border-color: @colortheme_oodoc-bg !important; }
.cp-border-color-ooslide { border-color: @colortheme_ooslide-bg !important; }
.cp-border-color-sheet { border-color: @colortheme_oocell-bg !important; }
.cp-border-color-kanban { border-color: @colortheme_kanban-bg !important; }
}

@ -1,6 +1,7 @@
.markdown_main() {
blockquote {
background: #e5e5e5;
background: rgba(128,128,128,0.5);
padding: 10px;
border-left: 3px solid #999;
padding-right: 0;

@ -235,6 +235,7 @@
padding: 5px;
margin: 2px 0;
background: rgba(0,0,0,0.1);
border-right: 3px solid transparent;
.avatar_main(30px);
.cp-avatar-default, media-tag {
margin-right: 5px;

@ -166,7 +166,8 @@
.cp-callout-poll .cptools { background-color: @colortheme_poll-bg; }
.cp-callout-kanban .cptools { background-color: @colortheme_kanban-bg; }
.cp-callout-whiteboard .cptools { background-color: @colortheme_whiteboard-bg; }
.cp-callout-recent .fa { background-color: @colortheme_drive-bg; }
.cp-callout-drive .fa { background-color: @colortheme_drive-bg; }
.cp-callout-sheet .fa { background-color: @colortheme_oocell-bg; }
.cp-hidden { display: none !important; }
.cp-callout-more {
display: inline-block;

@ -61,6 +61,16 @@
transform: scale(1.05);
}
}
#register {
border-color: @cryptpad_color_blue;
background: #fff;
color: @cryptpad_color_blue;
padding: 10px;
border-radius: 0;
&:hover {
transform: scale(1.05);
}
}
}
}
.cp-container {

@ -21,3 +21,8 @@ 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

@ -0,0 +1,25 @@
#!/bin/sh
# Figure out latest release via GitHub API
release=$(curl --silent "https://api.github.com/repos/krallin/tini/releases/latest" | jq -r .tag_name)
# _Reliable_ way to get which arch for tini download
arch=$(python <<EOF
from __future__ import print_function
import platform
processor = platform.machine()
if processor == 'aarch64':
print('arm64', end='')
elif processor == 'x86 64' or processor == 'x86_64':
print('amd64', end='')
elif processor == 'armv7l':
print('armhf', end='')
EOF
)
# Download/install tini
curl -L https://github.com/krallin/tini/releases/download/$release/tini-static-$arch \
-o /sbin/tini
chmod a+x /sbin/tini

@ -1,5 +1,11 @@
# Cryptpad Docker Image
Cryptpad includes support for building a Docker image and running it to provide a Cryptpad instance. You can manage the container manually, or let Docker Compose manage it for you.
A full tutorial is available [on the Cryptpad Github wiki](https://github.com/xwiki-labs/cryptpad/wiki/Docker-(with-Nginx-and-Traefik)). This document provides a brief overview.
## Features
- Configuration via .env file
- Ready for use with traffic
- Using github master for now, release 0.3.0 too old
@ -9,14 +15,22 @@
## Run
Run from the cryptpad source directory:
Run from the cryptpad source directory, keeping instance state in `/var/cryptpad`:
```
docker build -t xwiki/cryptpad .
docker run --restart=always -d --name cryptpad -p 3000:3000 -v /var/cryptpad:/cryptpad/datastore xwiki/cryptpad
docker run --restart=always -d --name cryptpad -p 3000:3000 \
-v /var/cryptpad/files:/cryptpad/datastore \
-v /var/cryptpad/customize:/cryptpad/customize
-v /var/cryptpad/blob:/cryptpad/blob \
-v /var/cryptpad/blobstage:/cryptpad/blobstage \
-v /var/cryptpad/pins:/cryptpad/pins \
-v /var/cryptpad/tasks:/cryptpad/tasks \
-v /var/cryptpad/block:/cryptpad/block \
xwiki/cryptpad
```
Or, using docker-compose
Or, using docker-compose and the included `docker-compose.yml`, keeping instance state in the current directory under `./data`:
```
docker-compose up -d
@ -39,10 +53,15 @@ On runtime, in `bin/container-start.sh` the settings are written to the `config.
The docker-compose file is preconfigured to persist folders
- cryptpad/datastore --> ./data/customize
- cryptpad/datastore --> ./data/files
- cryptpad/customize --> ./data/customize
- cryptpad/pins --> ./data/pins
- cryptpad/blob --> ./data/blob
- cryptpad/blobstage --> ./data/blobstage
- cryptpad/tasks --> ./data/tasks
- cryptpad/block --> ./data/block
In customize included find your configuration in `config.js`.
Your configuration file will be in `./data/customize/config.js`.
The data folder is ignored by git, so if you want to add your customizations to git versioning change the volume:

@ -34,15 +34,15 @@ server {
# 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 https://api.your-main-domain.com wss://your-main-domain.com your-main-domain.com blob: your-main-domain.com";
set $fontSrc "'self' data: your-main-domain.com";
set $imgSrc "data: * blob:";
set $frameSrc "'self' your-sandbox-domain.com blob:";
set $mediaSrc "* blob:";
set $childSrc "https://your-main-domain.com";
set $workerSrc "https://your-main-domain.com";
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";
set $unsafe 0;
if ($uri = "/pad/inner.html") { set $unsafe 1; }

@ -1,7 +1,7 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
"version": "2.12.0",
"version": "2.16.0",
"license": "AGPL-3.0+",
"repository": {
"type": "git",

@ -457,10 +457,40 @@ var getHash = function (Env, publicKey, cb) {
});
};
var applyCustomLimits = function (Env, config) {
var isLimit = function (o) {
var valid = o && typeof(o) === 'object' &&
typeof(o.limit) === 'number' &&
typeof(o.plan) === 'string' &&
typeof(o.note) === 'string';
return valid;
};
// read custom limits from the config
var customLimits = (function (custom) {
var limits = {};
Object.keys(custom).forEach(function (k) {
k.replace(/\/([^\/]+)$/, function (all, safeKey) {
var id = unescapeKeyCharacters(safeKey || '');
limits[id] = custom[k];
return '';
});
});
return limits;
}(config.customLimits || {}));
Object.keys(customLimits).forEach(function (k) {
if (!isLimit(customLimits[k])) { return; }
Env.limits[k] = customLimits[k];
});
};
// The limits object contains storage limits for all the publicKey that have paid
// To each key is associated an object containing the 'limit' value and a 'note' explaining that limit
var updateLimits = function (Env, config, publicKey, cb /*:(?string, ?any[])=>void*/) {
if (config.adminEmail === false) {
applyCustomLimits(Env, config);
if (config.allowSubscriptions === false) { return; }
throw new Error("allowSubscriptions must be false if adminEmail is false");
}
@ -490,27 +520,6 @@ var updateLimits = function (Env, config, publicKey, cb /*:(?string, ?any[])=>vo
}
};
// read custom limits from the config
var customLimits = (function (custom) {
var limits = {};
Object.keys(custom).forEach(function (k) {
k.replace(/\/([^\/]+)$/, function (all, safeKey) {
var id = unescapeKeyCharacters(safeKey || '');
limits[id] = custom[k];
return '';
});
});
return limits;
}(config.customLimits || {}));
var isLimit = function (o) {
var valid = o && typeof(o) === 'object' &&
typeof(o.limit) === 'number' &&
typeof(o.plan) === 'string' &&
typeof(o.note) === 'string';
return valid;
};
var req = Https.request(options, function (response) {
if (!('' + response.statusCode).match(/^2\d\d$/)) {
return void cb('SERVER ERROR ' + response.statusCode);
@ -525,10 +534,7 @@ var updateLimits = function (Env, config, publicKey, cb /*:(?string, ?any[])=>vo
try {
var json = JSON.parse(str);
Env.limits = json;
Object.keys(customLimits).forEach(function (k) {
if (!isLimit(customLimits[k])) { return; }
Env.limits[k] = customLimits[k];
});
applyCustomLimits(Env, config);
var l;
if (userId) {
@ -544,6 +550,7 @@ var updateLimits = function (Env, config, publicKey, cb /*:(?string, ?any[])=>vo
});
req.on('error', function (e) {
applyCustomLimits(Env, config);
if (!config.domain) { return cb(); }
cb(e);
});

@ -75,9 +75,20 @@ var setHeaders = (function () {
if (config.padContentSecurity) {
padHeaders['Content-Security-Policy'] = clone(config.padContentSecurity);
}
const ooHeaders = clone(headers);
if (config.ooContentSecurity) {
ooHeaders['Content-Security-Policy'] = clone(config.ooContentSecurity);
}
if (Object.keys(headers).length) {
return function (req, res) {
const h = /^\/pad(2)?\/inner\.html.*/.test(req.url) ? padHeaders : headers;
const h = [/^\/pad(2)?\/inner\.html.*/].some((regex) => {
return regex.test(req.url)
}) ? padHeaders : ([
/^\/sheet\/inner\.html.*/,
/^\/common\/onlyoffice\/.*\/index\.html.*/
].some((regex) => {
return regex.test(req.url)
}) ? ooHeaders : headers);
for (let header in h) { res.setHeader(header, h[header]); }
};
}

@ -162,6 +162,7 @@ const mkOffsetCounter = () => {
const readMessagesBin = (env, id, start, msgHandler, cb) => {
const stream = Fs.createReadStream(mkPath(env, id), { start: start });
// TODO get the channel and add the atime
let keepReading = true;
Pull(
ToPull.read(stream),

@ -63,10 +63,31 @@
height: 100%;
border-left: 1px solid black;
box-sizing: border-box;
font-family: Calibri,Ubuntu,sans-serif;
//font-family: Calibri,Ubuntu,sans-serif;
font: @colortheme_app-font;
word-wrap: break-word;
position: relative;
flex: 1;
h1, h2, h3, h4, h5, h6 {
font-weight: bold;
padding-bottom: 0.3em;
border-bottom: 1px solid #eee;
}
li {
min-height: 22px;
}
.todo-list-item {
list-style: none;
.fa {
position: absolute;
margin-left: -17px;
margin-top: 4px;
&.fa-check-square {
font-size: 15px;
margin-top: 5px;
}
}
}
media-tag {
* {
max-width:100%;
@ -124,5 +145,12 @@
display: none !important;
}
}
#cp-app-code-print {
position: relative;
display: none;
margin: 1em auto;
.markdown_preformatted-code;
.markdown_gfm-table(black);
}
}

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html class="cp-app-noscroll">
<html class="cp-app-noscroll cp-app-print">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script async data-bootload="/code/inner.js" data-main="/common/sframe-boot.js?ver=1.6" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
@ -17,6 +17,7 @@
</div>
<div id="cp-app-code-preview">
<div id="cp-app-code-preview-content"></div>
<div id="cp-app-code-print"></div>
</div>
</div>
</body>

@ -70,6 +70,16 @@ define([
'xml',
]);
var mkPrintButton = function (framework, $content, $print) {
var $printButton = framework._.sfCommon.createButton('print', true);
$printButton.click(function () {
$print.html($content.html());
window.focus();
window.print();
framework.feedback('PRINT_CODE');
});
framework._.toolbar.$drawer.append($printButton);
};
var mkMarkdownTb = function (editor, framework) {
var $codeMirrorContainer = $('#cp-app-code-container');
var markdownTb = framework._.sfCommon.createMarkdownToolbar(editor);
@ -265,6 +275,11 @@ define([
var previewPane = mkPreviewPane(editor, CodeMirror, framework, isPresentMode);
var markdownTb = mkMarkdownTb(editor, framework);
var $print = $('#cp-app-code-print');
var $content = $('#cp-app-code-preview-content');
mkPrintButton(framework, $content, $print);
mkHelpMenu(framework);
var evModeChange = Util.mkEvent();
@ -295,12 +310,25 @@ define([
});
framework.setContentGetter(function () {
CodeMirror.removeCursors();
var content = CodeMirror.getContent();
content.highlightMode = CodeMirror.highlightMode;
previewPane.draw();
return content;
});
var cursorTo;
var updateCursor = function () {
if (cursorTo) { clearTimeout(cursorTo); }
if (editor._noCursorUpdate) { return; }
cursorTo = setTimeout(function () {
framework.updateCursor();
}, 500); // 500ms to make sure it is sent after chainpad sync
};
framework.onCursorUpdate(CodeMirror.setRemoteCursor);
framework.setCursorGetter(CodeMirror.getCursor);
editor.on('cursorActivity', updateCursor);
framework.onEditableChange(function () {
editor.setOption('readOnly', framework.isLocked() || framework.isReadOnly());
});

@ -6,11 +6,21 @@
define(function() {
var config = {};
/* Select the buttons displayed on the main page to create new collaborative sessions
* Existing types : pad, code, poll, slide
/* Select the buttons displayed on the main page to create new collaborative sessions.
* Removing apps from the list will prevent users from accessing them. They will instead be
* redirected to the drive.
* You should never remove the drive from this list.
*/
config.availablePadTypes = ['drive', 'pad', 'code', 'slide', 'poll', 'kanban', 'whiteboard', 'file', 'todo', 'contacts'];
config.registeredOnlyTypes = ['file', 'contacts'];
config.availablePadTypes = ['drive', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard',
/*'oodoc', 'ooslide',*/ 'file', 'todo', 'contacts'];
/* The registered only types are apps restricted to registered users.
* You should never remove apps from this list unless you know what you're doing. The apps
* listed here by default can't work without a user account.
* You can however add apps to this list. The new apps won't be visible for unregistered
* 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'];
/* Cryptpad apps use a common API to display notifications to users
* by default, notifications are hidden after 5 seconds
@ -83,6 +93,9 @@ define(function() {
todo: 'cptools-todo',
contacts: 'cptools-contacts',
kanban: 'cptools-kanban',
oodoc: 'fa-file-word-o',
ooslide: 'fa-file-powerpoint-o',
sheet: 'fa-file-excel-o',
drive: 'fa-hdd-o',
};

@ -15,6 +15,8 @@ define(function () {
displayPadCreationScreen: 'displayPadCreationScreen',
deprecatedKey: 'deprecated',
// Sub
plan: 'CryptPad_plan'
plan: 'CryptPad_plan',
// Apps
criticalApps: ['profile', 'settings', 'debug']
};
});

@ -21,15 +21,16 @@ define([
};
http.send();
};
Feedback.send = function (action, force) {
if (AppConfig.disableFeedback) { return; }
if (!action) { return; }
Feedback.send = function (action, force, cb) {
if (typeof(cb) !== 'function') { cb = function () {}; }
if (AppConfig.disableFeedback) { return void cb(); }
if (!action) { return void cb(); }
if (force !== true) {
if (!Feedback.state) { return; }
if (!Feedback.state) { return void cb(); }
}
var href = '/common/feedback.html?' + action + '=' + randomToken();
ajax(href);
ajax(href, cb);
};
Feedback.reportAppUsage = function () {

@ -12,6 +12,7 @@ define([
var hexToBase64 = Util.hexToBase64;
var base64ToHex = Util.base64ToHex;
Hash.encodeBase64 = Nacl.util.encodeBase64;
Hash.decodeBase64 = Nacl.util.decodeBase64;
// This implementation must match that on the server
// it's used for a checksum
@ -75,9 +76,10 @@ define([
return s.replace(/\/+/g, '/');
};
Hash.createChannelId = function () {
var id = uint8ArrayToHex(Crypto.Nacl.randomBytes(16));
if (id.length !== 32 || /[^a-f0-9]/.test(id)) {
Hash.ephemeralChannelLength = 34;
Hash.createChannelId = function (ephemeral) {
var id = uint8ArrayToHex(Crypto.Nacl.randomBytes(ephemeral? 17: 16));
if ([32, 34].indexOf(id.length) === -1 || /[^a-f0-9]/.test(id)) {
throw new Error('channel ids must consist of 32 hex characters');
}
return id;

@ -26,6 +26,15 @@ define([
return JSON.parse(JSON.stringify(o));
};
var convertToUint8 = function (obj) {
var l = Object.keys(obj).length;
var u = new Uint8Array(l);
for (var i = 0; i<l; i++) {
u[i] = obj[i];
}
return u;
};
// TODO
// - mute a channel (hide notifications or don't open it?)
var createData = Msg.createData = function (proxy, hash) {
@ -62,12 +71,6 @@ define([
Msg.messenger = function (store) {
var messenger = {
handlers: {
message: [],
join: [],
leave: [],
update: [],
friend: [],
unfriend: [],
event: []
},
range_requests: {},
@ -136,12 +139,12 @@ define([
delete messenger.range_requests[txid];
};
messenger.getMoreHistory = function (chanId, hash, count, cb) {
var getMoreHistory = function (chanId, hash, count, cb) {
if (typeof(cb) !== 'function') { return; }
if (typeof(hash) !== 'string') {
// Channel is empty!
return void cb(void 0, []);
return void cb([]);
}
var chan = getChannel(chanId);
@ -189,17 +192,17 @@ define([
}
};*/
messenger.setChannelHead = function (id, hash, cb) {
var setChannelHead = function (id, hash, cb) {
var channel = getChannel(id);
if (channel.isFriendChat) {
var friend = getFriendFromChannel(id);
if (!friend) { return void cb('NO_SUCH_FRIEND'); }
if (!friend) { return void cb({error: 'NO_SUCH_FRIEND'}); }
friend.lastKnownHash = hash;
} else if (channel.isPadChat) {
// Nothing to do
} else {
// TODO room
return void cb('NOT_IMPLEMENTED');
return void cb({error: 'NOT_IMPLEMENTED'});
}
cb();
};
@ -217,8 +220,10 @@ define([
}
});
eachHandler('update', function (f) {
f(clone(data), types, channel);
emit('UPDATE_DATA', {
info: clone(data),
types: types,
channel: channel
});
};
@ -238,7 +243,7 @@ define([
return;
}
var decryptedMsg = channel.encryptor.decrypt(parsed0.msg);
var decryptedMsg = channel.decrypt(parsed0.msg);
if (decryptedMsg === null) {
return void console.error("Failed to decrypt message");
@ -263,17 +268,19 @@ define([
if (parsed[2] !== sender || !parsed[1]) { return; }
channel.mapId[sender] = parsed[1];
checkFriendData(parsed[1].curvePublic, parsed[1], channel.id);
eachHandler('join', function (f) {
f(parsed[1], channel.id);
emit('JOIN', {
info: parsed[1],
id: channel.id
});
if (channel.readOnly) { return; }
if (parsed[0] !== Types.mapId) { return; } // Don't send your key if it's already an ACK
// Answer with your own key
var myData = createData(proxy);
delete myData.channel;
var rMsg = [Types.mapIdAck, myData, channel.wc.myID];
var rMsgStr = JSON.stringify(rMsg);
var cryptMsg = channel.encryptor.encrypt(rMsgStr);
var cryptMsg = channel.encrypt(rMsgStr);
var data = {
channel: channel.id,
msg: cryptMsg
@ -300,7 +307,7 @@ define([
var pushMsg = function (channel, cryptMsg) {
var sig = cryptMsg.slice(0, 64);
if (msgAlreadyKnown(channel, sig)) { return; }
var msg = channel.encryptor.decrypt(cryptMsg);
var msg = channel.decrypt(cryptMsg);
var parsedMsg = JSON.parse(msg);
var curvePublic;
@ -321,9 +328,7 @@ define([
channel.messages.push(res);
if (!joining[channel.id]) {
// Channel is ready
eachHandler('message', function (f) {
f(res);
});
emit('MESSAGE', res);
}
return true;
@ -342,8 +347,9 @@ define([
removeFromFriendList(curvePublic, function () {
channel.wc.leave(Types.unfriend);
delete channels[channel.id];
eachHandler('unfriend', function (f) {
f(curvePublic, false);
emit('UNFRIEND', {
curvePublic: curvePublic,
fromMe: false
});
});
return;
@ -368,11 +374,11 @@ define([
if (!channel) {
return void console.error('NO_SUCH_CHANNEL');
}
if (channel.readOnly) { return; }
var msg = [Types.update, myData.curvePublic, +new Date(), myData];
var msgStr = JSON.stringify(msg);
var cryptMsg = channel.encryptor.encrypt(msgStr);
var cryptMsg = channel.encrypt(msgStr);
channel.wc.bcast(cryptMsg).then(function () {
// TODO send event
//channel.refresh();
@ -380,13 +386,48 @@ define([
console.error(err);
});
});
eachHandler('update', function (f) {
f(myData, ['displayName', 'profile', 'avatar']);
emit('UPDATE', {
info: myData,
types: ['displayName', 'profile', 'avatar'],
});
friends.me = myData;
}
};
var getChannelMessagesSince = function (chan, data, keys) {
console.log('Fetching [%s] messages since [%s]', chan.id, data.lastKnownHash || '');
if (chan.isPadChat) {
// We need to use GET_HISTORY_RANGE to make sure we won't get the full history
var txid = Util.uid();
initRangeRequest(txid, chan.id, undefined);
var msg0 = ['GET_HISTORY_RANGE', chan.id, {
//from: hash,
count: 10,
txid: txid,
}
];
network.sendto(network.historyKeeper, JSON.stringify(msg0)).then(function () {
}, function (err) {
throw new Error(err);
});
return;
}
var friend = getFriendFromChannel(chan.id) || {};
var cfg = {
validateKey: keys ? keys.validateKey : undefined,
owners: [proxy.edPublic, friend.edPublic],
lastKnownHash: data.lastKnownHash
};
var msg = ['GET_HISTORY', chan.id, cfg];
network.sendto(network.historyKeeper, JSON.stringify(msg))
.then(function () {}, function (err) {
throw new Error(err);
});
};
var onChannelReady = function (chanId) {
var cb = joining[chanId];
if (typeof(cb) !== 'function') {
@ -434,7 +475,7 @@ define([
if (msg[2] !== 'MSG') { return; }
try {
return {
d: JSON.parse(channel.encryptor.decrypt(msg[4])),
d: JSON.parse(channel.decrypt(msg[4])),
sig: msg[4].slice(0, 64),
};
} catch (e) {
@ -458,7 +499,7 @@ define([
});
orderMessages(channel, decrypted);
req.cb(void 0, decrypted);
req.cb(decrypted);
return deleteRangeRequest(txid);
} else {
console.log(parsed);
@ -469,15 +510,31 @@ define([
if ((parsed.validateKey || parsed.owners) && parsed.channel) {
return;
}
// End of initial history
if (parsed.state && parsed.state === 1 && parsed.channel) {
if (channels[parsed.channel]) {
if (parsed.channel && channels[parsed.channel]) {
// Error in initial history
// History cleared while we're in the channel
if (parsed.error === 'ECLEARED') {
setChannelHead(parsed.channel, '', function () {});
emit('CLEAR_CHANNEL', parsed.channel);
return;
}
// History cleared while we were offline
// ==> we asked for an invalid last known hash
if (parsed.error && parsed.error === "EINVAL") {
setChannelHead(parsed.channel, '', function () {
getChannelMessagesSince(getChannel(parsed.channel), {}, {});
});
return;
}
// End of initial history
if (parsed.state && parsed.state === 1 && parsed.channel) {
// parsed.channel is Ready
// channel[parsed.channel].ready();
channels[parsed.channel].ready = true;
onChannelReady(parsed.channel);
return;
}
return;
}
// Initial history message
var chan = parsed[3];
@ -495,7 +552,6 @@ define([
//channels[chan.id].notify();
}
//channels[chan.id].refresh();
// TODO emit message event
}
};
@ -504,19 +560,19 @@ define([
onDirectMessage(msg, sender);
});
messenger.removeFriend = function (curvePublic, cb) {
var removeFriend = function (curvePublic, cb) {
if (typeof(cb) !== 'function') { throw new Error('NO_CALLBACK'); }
var data = getFriend(proxy, curvePublic);
if (!data) {
// friend is not valid
console.error('friend is not valid');
return void cb('INVALID_FRIEND');
return void cb({error: 'INVALID_FRIEND'});
}
var channel = channels[data.channel];
if (!channel) {
return void cb("NO_SUCH_CHANNEL");
return void cb({error: "NO_SUCH_CHANNEL"});
}
if (!network.webChannels.some(function (wc) {
@ -527,58 +583,27 @@ define([
var msg = [Types.unfriend, proxy.curvePublic, +new Date()];
var msgStr = JSON.stringify(msg);
var cryptMsg = channel.encryptor.encrypt(msgStr);
var cryptMsg = channel.encrypt(msgStr);
try {
channel.wc.bcast(cryptMsg).then(function () {
removeFromFriendList(curvePublic, function () {
delete channels[channel.id];
eachHandler('unfriend', function (f) {
f(curvePublic, true);
emit('UNFRIEND', {
curvePublic: curvePublic,
fromMe: true
});
cb();
});
}, function (err) {
console.error(err);
cb(err);
cb({error: err});
});
} catch (e) {
cb(e);
cb({error: e});
}
};
var getChannelMessagesSince = function (chan, data, keys) {
console.log('Fetching [%s] messages since [%s]', chan.id, data.lastKnownHash || '');
if (chan.isPadChat) {
// We need to use GET_HISTORY_RANGE to make sure we won't get the full history
var txid = Util.uid();
initRangeRequest(txid, chan.id, undefined);
var msg0 = ['GET_HISTORY_RANGE', chan.id, {
//from: hash,
count: 10,
txid: txid,
}
];
network.sendto(network.historyKeeper, JSON.stringify(msg0)).then(function () {
}, function (err) {
throw new Error(err);
});
return;
}
var cfg = {
validateKey: keys ? keys.validateKey : undefined,
owners: [proxy.edPublic, data.edPublic],
lastKnownHash: data.lastKnownHash
};
var msg = ['GET_HISTORY', chan.id, cfg];
network.sendto(network.historyKeeper, JSON.stringify(msg))
.then(function () {}, function (err) {
throw new Error(err);
});
};
var openChannel = function (data) {
var keys = data.keys;
var encryptor = data.encryptor || Curve.createEncryptor(keys);
@ -587,24 +612,33 @@ define([
isFriendChat: data.isFriendChat,
isPadChat: data.isPadChat,
padChan: data.padChan,
readOnly: data.readOnly,
sending: false,
encryptor: encryptor,
messages: [],
userList: [],
mapId: {},
};
channel.encrypt = function (msg) {
if (channel.readOnly) { return; }
return encryptor.encrypt(msg);
};
channel.decrypt = data.decrypt || function (msg) {
return encryptor.decrypt(msg);
};
var onJoining = function (peer) {
if (peer === Msg.hk) { return; }
if (channel.userList.indexOf(peer) !== -1) { return; }
channel.userList.push(peer);
if (channel.readOnly) { return; }
// Join event will be sent once we are able to ID this peer
var myData = createData(proxy);
delete myData.channel;
var msg = [Types.mapId, myData, channel.wc.myID];
var msgStr = JSON.stringify(msg);
var cryptMsg = channel.encryptor.encrypt(msgStr);
var cryptMsg = channel.encrypt(msgStr);
var data = {
channel: channel.id,
msg: cryptMsg
@ -629,8 +663,9 @@ define([
})) { return; }
// Send the notification
eachHandler('leave', function (f) {
f(otherData, channel.id);
emit('LEAVE', {
info: otherData,
id: channel.id
});
};
@ -674,27 +709,14 @@ define([
}));
};
/*messenger.openFriendChannel = function (curvePublic, cb) {
if (typeof(curvePublic) !== 'string') { return void cb('INVALID_ID'); }
if (typeof(cb) !== 'function') { throw new Error('expected callback'); }
var friend = clone(friends[curvePublic]);
if (typeof(friend) !== 'object') {
return void cb('NO_FRIEND_DATA');
}
var channel = friend.channel;
if (!channel) { return void cb('E_NO_CHANNEL'); }
joining[channel] = cb;
openFriendChannel(friend, curvePublic);
};*/
messenger.sendMessage = function (id, payload, cb) {
var sendMessage = function (id, payload, cb) {
var channel = getChannel(id);
if (!channel) { return void cb('NO_CHANNEL'); }
if (!channel) { return void cb({error: 'NO_CHANNEL'}); }
if (channel.readOnly) { return void cb({error: 'FORBIDDEN'}); }
if (!network.webChannels.some(function (wc) {
if (wc.id === channel.wc.id) { return true; }
})) {
return void cb('NO_SUCH_CHANNEL');
return void cb({error: 'NO_SUCH_CHANNEL'});
}
var msg = [Types.message, proxy.curvePublic, +new Date(), payload];
@ -704,17 +726,17 @@ define([
msg.push(name);
}
var msgStr = JSON.stringify(msg);
var cryptMsg = channel.encryptor.encrypt(msgStr);
var cryptMsg = channel.encrypt(msgStr);
channel.wc.bcast(cryptMsg).then(function () {
pushMsg(channel, cryptMsg);
cb();
}, function (err) {
cb(err);
cb({error: err});
});
};
messenger.getStatus = function (chanId, cb) {
var getStatus = function (chanId, cb) {
// Display green status if one member is not me
var channel = getChannel(chanId);
if (!channel) { return void cb('NO_SUCH_CHANNEL'); }
@ -723,26 +745,11 @@ define([
if (!data) { return false; }
return data.curvePublic !== proxy.curvePublic;
});
cb(void 0, online);
cb(online);
};
messenger.getFriendInfo = function (channel, cb) {
setTimeout(function () {
var friend;
for (var k in friends) {
if (friends[k].channel === channel) {
friend = friends[k];
break;
}
}
if (!friend) { return void cb('NO_SUCH_FRIEND'); }
// this clone will be redundant when ui uses postmessage
cb(void 0, clone(friend));
});
};
messenger.getMyInfo = function (cb) {
cb(void 0, {
var getMyInfo = function (cb) {
cb({
curvePublic: proxy.curvePublic,
displayName: proxy[Constants.displayNameKey]
});
@ -777,8 +784,8 @@ define([
var channel = friend.channel;
if (!channel) { return; }
loadFriend(friend, function () {
eachHandler('friend', function (f) {
f(curvePublic);
emit('FRIEND', {
curvePublic: curvePublic,
});
});
return;
@ -795,8 +802,9 @@ define([
var channel = channels[o];
channel.wc.leave(Types.unfriend);
delete channels[channel.id];
eachHandler('unfriend', function (f) {
f(curvePublic, true);
emit('UNFRIEND', {
curvePublic: curvePublic,
fromMe: true
});
});
@ -807,8 +815,8 @@ define([
var channel = friend.channel;
if (!channel) { return; }
loadFriend(friend, function () {
eachHandler('friend', function (f) {
f(friend.curvePublic);
emit('FRIEND', {
curvePublic: friend.curvePublic,
});
});
};
@ -905,20 +913,32 @@ define([
}
};
var validateKeys = {};
messenger.storeValidateKey = function (chan, key) {
validateKeys[chan] = key;
};
var openPadChat = function (data, cb) {
var channel = data.channel;
if (getChannel(channel)) {
emit('PADCHAT_READY', channel);
return void cb();
}
var keys = data.secret && data.secret.keys;
var cryptKey = keys.viewKeyStr ? Crypto.b64AddSlashes(keys.viewKeyStr) : data.secret.key;
var encryptor = Crypto.createEncryptor(cryptKey);
var secret = data.secret;
if (secret.keys.cryptKey) {
secret.keys.cryptKey = convertToUint8(secret.keys.cryptKey);
}
var encryptor = Crypto.createEncryptor(secret.keys);
var vKey = (secret.keys && secret.keys.validateKey) || validateKeys[secret.channel];
var chanData = {
padChan: data.secret && data.secret.channel,
readOnly: typeof(secret.keys) === "object" && !secret.keys.validateKey,
encryptor: encryptor,
channel: data.channel,
isPadChat: true,
decrypt: function (msg) {
return encryptor.decrypt(msg, vKey);
},
//lastKnownHash: friend.lastKnownHash,
//owners: [proxy.edPublic, friend.edPublic],
//isFriendChat: true
@ -939,6 +959,7 @@ define([
messenger.leavePad = function (padChan) {
// Leave chat and prevent reconnect when we leave a pad
delete validateKeys[padChan];
Object.keys(channels).some(function (chatChan) {
var channel = channels[chatChan];
if (channel.padChan !== padChan) { return; }
@ -967,6 +988,24 @@ define([
if (cmd === 'OPEN_PAD_CHAT') {
return void openPadChat(data, cb);
}
if (cmd === 'GET_MY_INFO') {
return void getMyInfo(cb);
}
if (cmd === 'REMOVE_FRIEND') {
return void removeFriend(data, cb);
}
if (cmd === 'GET_STATUS') {
return void getStatus(data, cb);
}
if (cmd === 'GET_MORE_HISTORY') {
return void getMoreHistory(data.id, data.sig, data.count, cb);
}
if (cmd === 'SEND_MESSAGE') {
return void sendMessage(data.id, data.content, cb);
}
if (cmd === 'SET_CHANNEL_HEAD') {
return void setChannelHead(data.id, data.sig, cb);
}
};
Object.freeze(messenger);

@ -768,7 +768,7 @@ define([
break;
case 'print':
button = $('<button>', {
title: Messages.printButtonTitle,
title: Messages.printButtonTitle2,
'class': "fa fa-print cp-toolbar-icon-print",
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.printText));
break;
@ -873,6 +873,15 @@ define([
});
});
break;
case 'save': // OnlyOffice save
button = $('<button>', {
'class': 'fa fa-save',
title: Messages.settings_save,
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'})
.text(Messages.settings_save))
.click(common.prepareFeedback(type));
if (callback) { button.click(callback); }
break;
default:
data = data || {};
var icon = data.icon || "fa-question";
@ -880,6 +889,10 @@ define([
'class': "fa " + icon,
})
.click(common.prepareFeedback(data.name || 'DEFAULT'));
//.click(common.prepareFeedback(type));
if (callback) {
button.click(callback);
}
if (data.title) { button.attr('title', data.title); }
if (data.style) { button.attr('style', data.style); }
if (data.id) { button.attr('id', data.id); }
@ -1211,6 +1224,13 @@ define([
var emojis = emojiStringToArray(str);
return isEmoji(emojis[0])? emojis[0]: str[0];
};
var avatars = {};
UIElements.setAvatar = function (hash, data) {
avatars[hash] = data;
};
UIElements.getAvatar = function (hash) {
return avatars[hash];
};
UIElements.displayAvatar = function (Common, $container, href, name, cb) {
var displayDefault = function () {
var text = getFirstEmojiOrCharacter(name);
@ -1304,21 +1324,27 @@ define([
var urls = common.getMetadataMgr().getPrivateData().accounts;
var makeDonateButton = function () {
$('<a>', {
var $a = $('<a>', {
'class': 'cp-limit-upgrade btn btn-success',
href: urls.donateURL,
rel: "noreferrer noopener",
target: "_blank",
}).text(Messages.supportCryptpad).appendTo($container);
$a.click(function () {
Feedback.send('SUPPORT_CRYPTPAD');
});
};
var makeUpgradeButton = function () {
$('<a>', {
var $a = $('<a>', {
'class': 'cp-limit-upgrade btn btn-success',
href: urls.upgradeURL,
rel: "noreferrer noopener",
target: "_blank",
}).text(Messages.upgradeAccount).appendTo($container);
$a.click(function () {
Feedback.send('UPGRADE_ACCOUNT');
});
};
if (!Config.removeDonateButton) {
@ -1909,7 +1935,13 @@ define([
onSelect: function (data) {
if (data.type === type && first) {
UI.addLoadingScreen({hideTips: true});
sframeChan.query('Q_TEMPLATE_USE', data.href, function () {
var chatChan = common.getPadChat();
var cursorChan = common.getCursorChannel();
sframeChan.query('Q_TEMPLATE_USE', {
href: data.href,
chat: chatChan,
cursor: cursorChan
}, function () {
first = false;
UI.removeLoadingScreen();
Feedback.send('TEMPLATE_USED');

@ -297,6 +297,15 @@ define([], function () {
return false;
};
Util.hexToRGB = function (hex) {
var h = hex.replace(/^#/, '');
return [
parseInt(h.slice(0,2), 16),
parseInt(h.slice(2,4), 16),
parseInt(h.slice(4,6), 16),
];
};
return Util;
});
}(self));

@ -118,7 +118,14 @@ define([
};
// Settings
common.deleteAccount = function (cb) {
postMessage("DELETE_ACCOUNT", null, cb);
postMessage("DELETE_ACCOUNT", null, function (obj) {
if (obj.state) {
Feedback.send('DELETE_ACCOUNT_AUTOMATIC');
} else {
Feedback.send('DELETE_ACCOUNT_MANUAL');
}
cb(obj);
});
};
// Drive
common.userObjectCommand = function (data, cb) {
@ -493,9 +500,10 @@ define([
});
};
common.useTemplate = function (href, Crypt, cb, optsPut) {
common.useTemplate = function (data, Crypt, cb, optsPut) {
// opts is used to overrides options for chainpad-netflux in cryptput
// it allows us to add owners and expiration time if it is a new file
var href = data.href;
var parsed = Hash.parsePadUrl(href);
var parsed2 = Hash.parsePadUrl(window.location.href);
@ -531,8 +539,13 @@ define([
}
if (typeof(meta) === "object") {
meta.defaultTitle = meta.title || meta.defaultTitle;
delete meta.users;
meta.title = "";
delete meta.users;
delete meta.chat2;
delete meta.chat;
delete meta.cursor;
if (data.chat) { meta.chat2 = data.chat; }
if (data.cursor) { meta.cursor = data.cursor; }
}
val = JSON.stringify(parsed);
} catch (e) {
@ -617,47 +630,27 @@ define([
});
};
// Messenger
var messenger = common.messenger = {};
messenger.getFriendList = function (cb) {
postMessage("CONTACTS_GET_FRIEND_LIST", null, cb);
};
messenger.getMyInfo = function (cb) {
postMessage("CONTACTS_GET_MY_INFO", null, cb);
};
messenger.getFriendInfo = function (curvePublic, cb) {
postMessage("CONTACTS_GET_FRIEND_INFO", curvePublic, cb);
};
messenger.removeFriend = function (curvePublic, cb) {
postMessage("CONTACTS_REMOVE_FRIEND", curvePublic, cb);
};
messenger.openFriendChannel = function (curvePublic, cb) {
postMessage("CONTACTS_OPEN_FRIEND_CHANNEL", curvePublic, cb);
};
messenger.getFriendStatus = function (curvePublic, cb) {
postMessage("CONTACTS_GET_FRIEND_STATUS", curvePublic, cb);
};
messenger.getMoreHistory = function (data, cb) {
postMessage("CONTACTS_GET_MORE_HISTORY", data, cb);
};
messenger.sendMessage = function (data, cb) {
postMessage("CONTACTS_SEND_MESSAGE", data, cb);
};
messenger.setChannelHead = function (data, cb) {
postMessage("CONTACTS_SET_CHANNEL_HEAD", data, cb);
// Onlyoffice
var onlyoffice = common.onlyoffice = {};
onlyoffice.execCommand = function (data, cb) {
postMessage("OO_COMMAND", data, cb);
};
onlyoffice.onEvent = Util.mkEvent();
// Messenger
var messenger = common.messenger = {};
messenger.execCommand = function (data, cb) {
postMessage("CHAT_COMMAND", data, cb);
};
messenger.onEvent = Util.mkEvent();
messenger.onMessageEvent = Util.mkEvent();
messenger.onJoinEvent = Util.mkEvent();
messenger.onLeaveEvent = Util.mkEvent();
messenger.onUpdateEvent = Util.mkEvent();
messenger.onFriendEvent = Util.mkEvent();
messenger.onUnfriendEvent = Util.mkEvent();
// Cursor
var cursor = common.cursor = {};
cursor.execCommand = function (data, cb) {
postMessage("CURSOR_COMMAND", data, cb);
};
cursor.onEvent = Util.mkEvent();
// Pad RPC
var pad = common.padRpc = {};
@ -947,7 +940,12 @@ define([
}
}).nThen(function () {
// We have the new drive, with the new login block
window.location.reload();
var feedbackKey = (password === newPassword)?
'OWNED_DRIVE_MIGRATION': 'PASSWORD_CHANGED';
Feedback.send(feedbackKey, undefined, function () {
window.location.reload();
});
});
};
@ -1082,15 +1080,12 @@ define([
common.onNetworkReconnect.fire(data);
});
},
// Messenger
CONTACTS_MESSAGE: common.messenger.onMessageEvent.fire,
CONTACTS_JOIN: common.messenger.onJoinEvent.fire,
CONTACTS_LEAVE: common.messenger.onLeaveEvent.fire,
CONTACTS_UPDATE: common.messenger.onUpdateEvent.fire,
CONTACTS_FRIEND: common.messenger.onFriendEvent.fire,
CONTACTS_UNFRIEND: common.messenger.onUnfriendEvent.fire,
// OnlyOffice
OO_EVENT: common.onlyoffice.onEvent.fire,
// Chat
CHAT_EVENT: common.messenger.onEvent.fire,
// Cursor
CURSOR_EVENT: common.cursor.onEvent.fire,
// Pad
PAD_READY: common.padRpc.onReadyEvent.fire,
PAD_MESSAGE: common.padRpc.onMessageEvent.fire,
@ -1125,6 +1120,14 @@ define([
return doesSupport;
};
common.isWebRTCSupported = function () {
return Boolean(navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia ||
window.RTCPeerConnection);
};
common.ready = (function () {
var env = {};
var initialized = false;
@ -1140,6 +1143,10 @@ define([
Feedback.send("NO_PROXIES");
}
if (!common.isWebRTCSupported()) {
Feedback.send("NO_WEBRTC");
}
var shimPattern = /CRYPTPAD_SHIM/;
if (shimPattern.test(Array.isArray.toString())) {
Feedback.send("NO_ISARRAY");

@ -8,6 +8,253 @@ define([
var Cursor = function (inner) {
var cursor = {};
var getTextNodeValue = function (el) {
if (!el.data) { return; }
// We want to transform html entities into their code (non-breaking spaces into $&nbsp;)
var div = document.createElement('div');
div.innerText = el.data;
return div.innerHTML;
};
// Store the cursor position as an offset from the beginning of the text HTML content
var offsetRange = cursor.offsetRange = {
start: 0,
end: 0
};
// Get the length of the opening tag of an node (<body class="cp"> ==> 17)
var getOpeningTagLength = function (node) {
if (node.nodeType === node.TEXT_NODE) { return 0; }
var html = node.outerHTML;
var tagRegex = /^(<\s*[a-zA-Z-]*[^>]*>)(.+)/;
var match = tagRegex.exec(html);
var res = match && match.length > 1 ? match[1].length : 0;
return res;
};
// Get the offset recursively. We start with <body> and continue following the
// path to the range
var offsetInNode = function (element, offset, path, range) {
if (path.length === 0) {
offset += getOpeningTagLength(range.el);
if (range.el.nodeType === range.el.TEXT_NODE) {
var div = document.createElement('div');
div.innerText = range.el.data.slice(0, range.offset);
return offset + div.innerHTML.length;
}
return offset + range.offset;
}
offset += getOpeningTagLength(element);
for (var i = 0; i < element.childNodes.length; i++) {
if (element.childNodes[i] === path[0]) {
return offsetInNode(path.shift(), offset, path, range);
}
// It is not yet our path, add the length of the text node or tag's outerHTML
offset += (getTextNodeValue(element.childNodes[i]) || element.childNodes[i].outerHTML).length;
}
};
// Get the cursor position as a range and transform it into
// an offset from the beginning of the outer HTML
var getOffsetFromRange = function (element) {
var doc = element.ownerDocument || element.document;
var win = doc.defaultView || doc.parentWindow;
var o = {
start: 0,
end: 0
};
if (typeof win.getSelection !== "undefined") {
var sel = win.getSelection();
if (sel.rangeCount > 0) {
var range = win.getSelection().getRangeAt(0);
// Do it for both start and end
['start', 'end'].forEach(function (t) {
var inNode = {
el: range[t + 'Container'],
offset: range[t + 'Offset']
};
while (inNode.el.nodeType !== Node.TEXT_NODE && inNode.el.childNodes.length > inNode.offset) {
inNode.el = inNode.el.childNodes[inNode.offset];
inNode.offset = 0;
}
var current = inNode.el;
var path = [];
while (current !== element) {
path.unshift(current);
current = current.parentNode;
}
if (current === element) { // Should always be the case
o[t] = offsetInNode(current, 0, path, inNode);
} else {
console.error('???');
}
});
}
}
return o;
};
// Update the value of the offset
// This should be called before applying changes to the document
cursor.offsetUpdate = function () {
try {
var range = getOffsetFromRange(inner);
offsetRange.start = range.start;
offsetRange.end = range.end;
} catch (e) {
console.error(e);
}
};
// Transform the offset value using the operations from the diff
// between the old and the new states of the document.
var offsetTransformRange = function (offset, ops) {
var transformCursor = function (cursor, op) {
if (!op) { return cursor; }
var pos = op.offset;
var remove = op.toRemove;
var insert = op.toInsert.length;
if (typeof cursor === 'undefined') { return; }
if (typeof remove === 'number' && pos < cursor) {
cursor -= Math.min(remove, cursor - pos);
}
if (typeof insert === 'number' && pos < cursor) {
cursor += insert;
}
return cursor;
};
var c = offset;
if (Array.isArray(ops)) {
for (var i = ops.length - 1; i >= 0; i--) {
c = transformCursor(c, ops[i]);
}
offset = c;
}
return offset;
};
// Get the range starting from <body> and the offset value.
// We substract length of HTML content to the offset until we reach a text node or 0.
// If we reach a text node, it means we're in the final possible child and the
// current valu of the offset is the range one.
// If we reach 0 or a negative value, it means the range in is the current tag
// and we should use offset 0.
var getFinalRange = function (el, offset) {
if (el.nodeType === el.TEXT_NODE) {
// This should be the final text node
var txt = document.createElement("textarea");
txt.appendChild(el.cloneNode());
txt.innerHTML = txt.innerHTML.slice(0, offset);
return {
el: el,
offset: txt.value.length
};
}
if (el.tagName === 'BR') {
// If the range is in a <br>, we have a brFix that will make it better later
return {
el: el,
offset: 0
};
}
// Remove the current tag opening length
offset = offset - getOpeningTagLength(el);
if (offset <= 0) {
// Return the current node...
return {
el: el,
offset: 0
};
}
// For each child, if they length is greater than the current offset, they are
// containing the range element we're looking for.
// Otherwise, our range element is in a later sibling and we can just substract
// their length.
var newOffset = offset;
for (var i = 0; i < el.childNodes.length; i++) {
try {
newOffset -= (getTextNodeValue(el.childNodes[i]) || el.childNodes[i].outerHTML).length;
} catch (e) {
console.log(el);
console.log(el.childNodes[i]);
}
if (newOffset <= 0) {
return getFinalRange(el.childNodes[i], offset);
}
offset = newOffset;
}
// New offset ends up in the closing tag
// ==> return the last child...
if (el.childNodes.length) {
return getFinalRange(el.childNodes[el.childNodes.length - 1], offset);
} else {
return {
el: el,
offset: 0
};
}
};
// Transform an offset into a range that we can use to restore the cursor
var getRangeFromOffset = function (element) {
var range = {
start: {
el: null,
offset: 0
},
end: {
el: null,
offset: 0
}
};
['start', 'end'].forEach(function (t) {
var offset = offsetRange[t];
var res = getFinalRange(element, offset);
range[t].el = res.el;
range[t].offset = res.offset;
});
return range;
};
cursor.getNewOffset = function (ops) {
return {
selectionStart: offsetTransformRange(offsetRange.start, ops),
selectionEnd: offsetTransformRange(offsetRange.end, ops)
};
};
cursor.getNewRange = function (data, ops) {
offsetRange.start = offsetTransformRange(data.start, ops);
offsetRange.end = offsetTransformRange(data.end, ops);
var range = getRangeFromOffset(inner);
return range;
};
// Restore the cursor position after applying the changes.
cursor.restoreOffset = function (ops) {
try {
offsetRange.start = offsetTransformRange(offsetRange.start, ops);
offsetRange.end = offsetTransformRange(offsetRange.end, ops);
var range = getRangeFromOffset(inner);
var sel = cursor.makeSelection();
var r = cursor.makeRange();
cursor.fixStart(range.start.el, range.start.offset);
cursor.fixEnd(range.end.el, range.end.offset);
cursor.fixSelection(sel, r);
cursor.brFix();
} catch (e) {
console.error(e);
}
};
// there ought to only be one cursor at a time, so let's just
// keep it internally
var Range = cursor.Range = {

@ -53,22 +53,22 @@ define([
var hasBogusInput = bogusCheckPtn.test(text);
if (isCheckedTaskItem) {
text = text.replace(checkedTaskItemPtn,
'<i class="fa fa-check-square" aria-hidden="true"></i>&nbsp;') + '\n';
'<i class="fa fa-check-square" aria-hidden="true"></i>') + '\n';
}
if (isUncheckedTaskItem) {
text = text.replace(uncheckedTaskItemPtn,
'<i class="fa fa-square-o" aria-hidden="true"></i>&nbsp;') + '\n';
'<i class="fa fa-square-o" aria-hidden="true"></i>') + '\n';
}
if (!isCheckedTaskItem && !isUncheckedTaskItem && hasBogusInput) {
if (/checked/.test(text)) {
text = text.replace(bogusCheckPtn,
'<i class="fa fa-check-square" aria-hidden="true"></i>&nbsp;') + '\n';
'<i class="fa fa-check-square" aria-hidden="true"></i>') + '\n';
} else if (/disabled/.test(text)) {
text = text.replace(bogusCheckPtn,
'<i class="fa fa-square-o" aria-hidden="true"></i>&nbsp;') + '\n';
'<i class="fa fa-square-o" aria-hidden="true"></i>') + '\n';
}
}
var cls = (isCheckedTaskItem || isUncheckedTaskItem) ? ' class="todo-list-item"' : '';
var cls = (isCheckedTaskItem || isUncheckedTaskItem || hasBogusInput) ? ' class="todo-list-item"' : '';
return '<li'+ cls + '>' + text + '</li>\n';
};
renderer.image = function (href, title, text) {

@ -160,6 +160,7 @@ define(['json.sortify'], function (Sortify) {
var list = members.slice().filter(function (m) { return m.length === 32; });
return list.length - Object.keys(metadataObj.users).length;
},
getChannelMembers: function () { return members.slice(); },
getPrivateData : function () {
return priv;
},

@ -10,6 +10,8 @@ define([
"ASN.1 asn.1",
"Asterisk asterisk",
"Brainfuck brainfuck .b",
"C text/x-csrc .c",
"C text/x-c++src .cpp",
"C-like clike",
"Clojure clojure",
"CMake cmake",
@ -48,6 +50,7 @@ define([
"HTTP http",
"IDL idl",
"JADE jade",
"Java text/x-java .java",
"JavaScript javascript .js",
"Jinja2 jinja2",
"JSX jsx .jsx",
@ -65,6 +68,7 @@ define([
"Nginx nginx",
"NSIS nsis",
"N-Triples ntriples",
"Objective-C text/x-objectivec .m",
"Octave octave",
"Org-mode orgmode .org",
"Oz oz",

File diff suppressed because one or more lines are too long

@ -0,0 +1,62 @@
@import (reference) "../../customize/src/less2/include/framework.less";
// body
body.cp-app-sheet, body.cp-app-oodoc, body.cp-app-ooslide {
display: flex;
flex-flow: column;
&.cp-app-sheet {
.framework_main(
@bg-color: @colortheme_oocell-bg,
@warn-color: @colortheme_oocell-warn,
@color: @colortheme_oocell-color
);
}
&.cp-app-oodoc {
.framework_main(
@bg-color: @colortheme_oodoc-bg,
@warn-color: @colortheme_oodoc-warn,
@color: @colortheme_oodoc-color
);
}
&.cp-app-ooslide {
.framework_main(
@bg-color: @colortheme_ooslide-bg,
@warn-color: @colortheme_ooslide-warn,
@color: @colortheme_ooslide-color
);
}
#cp-fileupload {
display: none !important;
}
#cp-toolbar {
display: flex; // We need this to remove a 3px border at the bottom of the toolbar
}
.cp-cryptpad-toolbar {
padding: 0px;
display: inline-block;
}
#cp-app-oo-container {
flex: 1;
height: 100%;
background-color: lightgrey;
display: flex;
}
#cp-app-oo-editor {
flex: 1;
height: 100%;
background-color: lightgrey;
display: flex;
flex-flow: column;
}
#ooframe {
flex: 1;
border:none;
margin:0;
padding:0;
}
}

@ -0,0 +1,10 @@
git clone https://github.com/ldubost/web-apps.git
git clone https://github.com/ldubost/sdkjs.git
cd sdkjs
make
cd ..
rm -rf ../web-apps
cp -r web-apps/deploy/web-apps ..
rm -rf ../sdkjs
cp -r web-apps/deploy/sdkjs ..

@ -0,0 +1,5 @@
cd web-apps
git pull
cd ../sdkjs
git pull
make clean

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save