Merge branch '5.2-candidate' into soon

This commit is contained in:
ansuz 2022-12-14 14:57:13 +05:30
commit 4aaa2bd71c
56 changed files with 6248 additions and 919 deletions

View File

@ -1,3 +1,56 @@
# 5.2.0
## Goals
This release is focused on addressing long-standing user feedback with new features. The most requested are improvements to Forms—multiple submissions and the ability to delete responses—as well as recurring events in Calendar.
## Update notes
To update from `5.1.0` to `5.2.0`:
1. Update your reverse proxy configuration to match the settings in our current `./docs/example.nginx.conf` and reload its configuration
2. Stop your API server
3. Fetch the latest code with git
4. Install the latest dependencies with `bower update` and `npm i`
5. Run `npm run build` to generate the new static pages
5. Restart your server
6. Review your instance's checkup page to ensure that you are passing all tests
<!-- XXX Notes about changes to Nginx conf -->
## Features
- Forms
- New setting to allow participants (including Guests) to submit a form multiple times and/or delete their responses
- Notifications for form owners when new responses are submitted
- New option for form authors to delete all responses
- New option for form authors/auditors to export responses as JSON (in addition to existing CSV and CryptPad Sheet)
- Settings have been refactored in a modal with a summary in the main editor view
- Display fixes for long questions/options in some question types
- Calendar
- New event settings to repeat periodically
- quick default patterns (e.g. weekly on Mondays, yearly on December 14th, etc), and custom intervals
- modify one, future, or all events
- easily stop repetition from event preview
- Drive
- filter by doc type
- Teams
- Improved onboarding with the ability to use the same invitation link for a set number of people. Previously each link was limited to one use
- Initial role can now be set for invitation links, the recipient is assigned the role directly when joining, previously all new members joined as "Viewers"
- Code
- Asciidoc syntax support AND asciidoc rendering
- handle jade language
- don't suggest c-language highlighting twice
- /checkup/
- [new test to confirm that public instances are open for registration](https://github.com/xwiki-labs/cryptpad/commit/174d97c442d5400d512dfccc478fd9fbd6fa075c)
# 5.1.0
## Goals

View File

@ -95,7 +95,7 @@ define([
return h('a', attrs, [icon, text]);
};
Pages.versionString = "5.1.0";
Pages.versionString = "5.2.0";
var customURLs = Pages.customURLs = {};
(function () {

View File

@ -183,6 +183,11 @@
}
}
}
.cp-dropdown-content {
a {
text-decoration: none;
}
}
}
.cp-alertify-type-container {
overflow: visible !important;

View File

@ -11,10 +11,12 @@
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
background-color: @cp_scrollbar-fg;
width: 6px;
height: 6px;
}
height: 100%;
width: 100%;

View File

@ -120,6 +120,7 @@
border-width: 0 @checkmark-width @checkmark-width 0;
border-width: 0 var(--checkmark-width) var(--checkmark-width) 0;
position: absolute;
box-sizing: border-box;
}
&:focus {
box-shadow: 0px 0px 5px @cp_checkmark-back1;

View File

@ -48,7 +48,7 @@
button {
.fa-caret-down {
margin-right: 1em !important;
margin-right: 0.5em !important;
}
* {
.tools_unselectable();

View File

@ -91,7 +91,10 @@
height: 100%;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
background-color: @cp_buttons-primary;
background-color: @cp_buttons-default-color;
&.btn-primary, &.btn-secondary {
background-color: @cp_buttons-primary;
}
&.danger, &.btn-danger, &.danger-alt, &.btn-danger-alt {
background-color: @cp_buttons-red;
}
@ -327,6 +330,9 @@
fill: @cryptpad_text_col;
}
}
.flatpickr-monthDropdown-month {
background: @cp_flatpickr-bg;
}
}
.flatpickr-current-month {
span.cur-month:hover {

View File

@ -5,6 +5,7 @@
& {
each(@colortheme_apps, {
button .cp-icon-color-@{key},
.cp-icon-color-@{key} { color: @value; }
});

View File

@ -123,6 +123,11 @@
display: flex;
align-items: center;
margin-bottom: 5px;
&.cp-teams-invite-role {
.cp-radio:not(:first-child) {
margin-left: 10px;
}
}
}
.cp-teams-invite-message {
resize: none;

View File

@ -27,14 +27,16 @@
color: @cryptpad_color_red;
}
}
.cp-avatar {
.avatar_main(30px);
padding: 0 5px;
.cp-reminder, .cp-avatar {
cursor: pointer;
&:hover {
background-color: @cp_dropdown-bg-hover;
}
}
.cp-avatar {
.avatar_main(30px);
padding: 0 5px;
}
.cp-notification-content {
flex: 1;
align-items: stretch;

View File

@ -967,6 +967,9 @@
}
}
}
.cp-toolbar-dropdown-nowrap {
white-space: nowrap;
}
.cp-toolbar-bottom {
color: @cp_toolbar-bottom-fg;
display: inline-flex;
@ -998,11 +1001,15 @@
.fa, .cptools {
margin-right: 5px;
}
.cp-dropdown-button-title .cp-icon {
margin-left: 5px;
}
&:hover {
background-color: fade(@cp_toolbar-bottom-bg, 70%);
}
}
.cp-toolbar-bottom-left > button,
.cp-toolbar-bottom-left > span > button,
.cp-toolbar-bottom-mid > button,
.cp-toolbar-bottom-right > button,
.cp-toolbar-bottom-right > span > button {
@ -1070,7 +1077,7 @@
.cp-toolbar-name, .cp-button-name {
display: none;
}
i {
i, span {
margin-right: 0;
}
}

View File

@ -6,6 +6,10 @@
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
# Let's Encrypt webroot
include letsencrypt-webroot;
# CryptPad serves static assets over these two domains.
# `main_domain` is what users will enter in their address bar.
@ -46,26 +50,35 @@ server {
# IMPORTANT: this config is intended to serve assets for at least two domains
# (your main domain and your sandbox domain). As such, you'll need to generate a single SSL certificate
# that includes both domains in order for things to work as expected.
ssl_certificate /home/cryptpad/.acme.sh/your-main-domain.com/fullchain.cer;
ssl_certificate_key /home/cryptpad/.acme.sh/your-main-domain.com/your-main-domain.com.key;
ssl_trusted_certificate /home/cryptpad/.acme.sh/your-main-domain.com/ca.cer;
ssl_certificate /etc/ssl/lets-encrypt/your-main-domain.com/cert;
ssl_certificate_key /etc/ssl/lets-encrypt/your-main-domain.com/key;
# diffie-hellman parameters are used to negotiate keys for your session
# generate strong parameters using the following command
ssl_dhparam /etc/nginx/dhparam.pem; # openssl dhparam -out /etc/nginx/dhparam.pem 4096
# Speeds things up a little bit when resuming a session
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:5m;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_session_tickets off;
# You'll need nginx 1.13.0 or better to support TLSv1.3
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# https://cipherli.st/
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
# verify chain of trust of OCSP response using Root CA and Intermediate certs
ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
# replace with the IP address of your resolver
resolver 8.8.8.8 8.8.4.4 1.1.1.1 1.0.0.1 9.9.9.9 149.112.112.112 208.67.222.222 208.67.220.220;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options nosniff;
add_header Access-Control-Allow-Origin "${allowed_origins}";

View File

@ -6,6 +6,7 @@ const nThen = require("nthen");
const Core = require("./core");
const Metadata = require("./metadata");
const HK = require("../hk-util");
const Nacl = require("tweetnacl/nacl-fast");
Channel.disconnectChannelMembers = function (Env, Server, channelId, code, cb) {
var done = Util.once(Util.mkAsync(cb));
@ -207,6 +208,40 @@ Channel.trimHistory = function (Env, safeKey, data, cb) {
});
};
// Delete a signed mailbox message. This is used when users want
// to delete their form reponses.
Channel.deleteMailboxMessage = function (Env, data, cb) {
const channelId = data.channel;
const hash = data.hash;
const proof = data.proof;
let nonce, proofBytes;
try {
nonce = Nacl.util.decodeBase64(proof.split('|')[0]);
proofBytes = Nacl.util.decodeBase64(proof.split('|')[1]);
} catch (e) {
return void cb('EINVAL');
}
Env.msgStore.deleteChannelLine(channelId, hash, function (msg) {
// Check if you're allowed to delete this hash
try {
const mySecret = new Uint8Array(32);
const msgBytes = Nacl.util.decodeBase64(msg).subarray(64); // Remove signature
const theirPublic = msgBytes.subarray(24,56); // 0-24 = nonce; 24-56=publickey (32 bytes)
const hashBytes = Nacl.box.open(proofBytes, nonce, theirPublic, mySecret);
return Nacl.util.encodeUTF8(hashBytes) === hash;
} catch (e) {
return false;
}
}, function (err) {
if (err) { return void cb(err); }
// clear historyKeeper's cache for this channel
Env.historyKeeper.channelClose(channelId);
cb();
delete Env.channel_cache[channelId];
delete Env.metadata_cache[channelId];
});
};
var ARRAY_LINE = /^\[/;
/* Files can contain metadata but not content
@ -320,10 +355,11 @@ Channel.writePrivateMessage = function (Env, args, _cb, Server, netfluxId) {
Server.getChannelUserList(channelId).forEach(function (userId) {
Server.send(userId, fullMessage);
});
cb();
});
cb();
});
};

View File

@ -31,7 +31,8 @@ the owners field is guaranteed to exist.
* mailbox <STRING|MAP>
* ADD_MAILBOX
* RM_MAILBOX
* deleteLines <BOOLEAN>
* ALLOW_LINE_DELETION
*/
var commands = {};
@ -71,6 +72,24 @@ commands.RESTRICT_ACCESS = function (meta, args) {
return true;
};
// ["ALLOW_LINE_DELETION", [true], 1561623438989]
// ["ALLOW_LINE_DELETION", [false], 1561623438989]
commands.ALLOW_LINE_DELETION = function (meta, args) {
if (!Array.isArray(args) || typeof(args[0]) !== 'boolean') {
throw new Error('INVALID_STATE');
}
var bool = args[0];
// reject the proposed command if there is no change in state
if (meta.deleteLines === bool) { return false; }
// apply the new state
meta.deleteLines = args[0];
return true;
};
// ["ADD_ALLOWED", ["7eEqelGso3EBr5jHlei6av4r9w2B9XZiGGwA1EgZ-5I=", ...], 1561623438989]
commands.ADD_ALLOWED = function (meta, args) {
if (!Array.isArray(args)) {

View File

@ -20,6 +20,7 @@ const UNAUTHENTICATED_CALLS = {
IS_CHANNEL_PINNED: Pinning.isChannelPinned, // FIXME drop this RPC
IS_NEW_CHANNEL: Channel.isNewChannel,
WRITE_PRIVATE_MESSAGE: Channel.writePrivateMessage,
DELETE_MAILBOX_MESSAGE: Channel.deleteMailboxMessage,
GET_METADATA: Metadata.getMetadata,
};

View File

@ -928,7 +928,7 @@ var getMessages = function (env, chanName, handler, cb) {
});
};
var trimChannel = function (env, channelName, hash, _cb) {
var filterMessages = function (env, channelName, check, filterHandler, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
// this function is queued as a blocking action for the relevant channel
@ -985,6 +985,13 @@ var trimChannel = function (env, channelName, hash, _cb) {
}
// if there were no errors just fall through to the next block
}));
}).nThen(function (w) {
// If we want to delete a signle line of the file, make sure this pad allows it
if (typeof(check) !== "function") { return; }
if (!metadataReference.meta || !check(metadataReference.meta)) {
w.abort();
return void cb("EFORBIDDEN");
}
}).nThen(function (w) {
// create temp buffer writeStream
tempStream = Fs.createWriteStream(tempChannelPath, {
@ -1023,14 +1030,15 @@ var trimChannel = function (env, channelName, hash, _cb) {
if (!msg) { return void readMore(); }
var msgHash = Extras.getHash(msg[4]);
if (msgHash === hash) {
// everything from this point on should be retained
retain = true;
return void tempStream.write(s_msg + '\n', function () {
var remove = function () { readMore(); };
var preserve = function () {
tempStream.write(s_msg + '\n', function () {
readMore();
});
}
readMore();
};
var preserveRemaining = function () { retain = true; };
filterHandler(msg, msgHash, abort, remove, preserve, preserveRemaining);
};
readMessagesBin(env, channelName, 0, handler, w(function (err) {
@ -1102,6 +1110,35 @@ var trimChannel = function (env, channelName, hash, _cb) {
});
});
};
var deleteChannelLine = function (env, channelName, hash, checkRights, _cb) {
var check = function (meta) { return Boolean(meta.deleteLines); };
var handler = function (msg, msgHash, abort, remove, preserve, preserveRemaining) {
if (msgHash === hash) {
if (typeof(checkRights) === "function" && !checkRights(msg[4])) {
// Not allowed: abort
return void abort();
}
// Line found: remove it and preserve all remaining lines
preserveRemaining();
return void remove();
}
// Continue until we find the correct hash
preserve();
};
filterMessages(env, channelName, check, handler, _cb);
};
var trimChannel = function (env, channelName, hash, _cb) {
var handler = function (msg, msgHash, abort, remove, preserve, preserveRemaining) {
if (msgHash === hash) {
// Everything from this point on should be retained
preserveRemaining();
return void preserve();
}
// Remove until we find our hash
remove();
};
filterMessages(env, channelName, null, handler, _cb);
};
module.exports.create = function (conf, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
@ -1236,6 +1273,12 @@ module.exports.create = function (conf, _cb) {
trimChannel(env, channelName, hash, Util.both(cb, next));
});
},
deleteChannelLine: function (channelName, hash, checkRights, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
schedule.blocking(channelName, function (next) {
deleteChannelLine(env, channelName, hash, checkRights, Util.both(cb, next));
});
},
// check if a channel exists in the database
isChannelAvailable: function (channelName, cb) {

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "cryptpad",
"version": "5.1.0",
"version": "5.2.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cryptpad",
"version": "5.1.0",
"version": "5.2.0",
"license": "AGPL-3.0+",
"dependencies": {
"@mcrowe/minibloom": "^0.2.0",

View File

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

View File

@ -17,6 +17,9 @@
.cp-small { display: none; }
}
.flatpickr-calendar {
z-index: 100001 !important; // Alertify is 100000
}
#cp-sidebarlayout-container #cp-sidebarlayout-rightside {
padding: 0;
& > div {
@ -101,6 +104,21 @@
color: @cryptpad_text_col !important;
}
}
.tui-full-calendar-floating-layer.cp-calendar-popup-flex {
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
justify-content: center !important;
align-items: center !important;
.tui-full-calendar-popup {
width: 540px !important;
}
}
#tui-full-calendar-popup-arrow {
display: none !important;
}
}
.tui-full-calendar-timegrid-timezone {
background-color: @cp_sidebar-right-bg !important;
@ -116,13 +134,30 @@
border-color: @cp_calendar-border !important;
}
.tui-full-calendar-popup {
border-radius: @variables_radius_L;
}
.tui-full-calendar-popup-container {
background: @cp_flatpickr-bg;
color: @cryptpad_text_col;
border-radius: @variables_radius;
border-radius: @variables_radius_L;
font-weight: normal;
.tui-full-calendar-icon:not(.tui-full-calendar-calendar-dot):not(.tui-full-calendar-dropdown-arrow):not(.tui-full-calendar-ic-checkbox) {
display: none;
}
.tui-full-calendar-popup-detail-item {
a {
color: @cryptpad_color_link;
text-decoration: underline;
}
}
.tui-full-calendar-section-button-save {
height: 40px;
.btn-primary { // Update button
margin-right: 0px;
}
}
}
li.tui-full-calendar-popup-section-item {
padding: 0 6px;
@ -186,6 +221,9 @@
width: 100%;
height: 32px;
border-radius: @variables_radius;
input[type="checkbox"].tui-full-calendar-checkbox-square:checked + span {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAABbmlDQ1BpY2MAACiRdZHNK0RRGMZ/Zoh8NIqFNItZDFmMEiVLxsJmkgZlsLn3zpe6c93uvZNkq2wspizExtfCf8BW2VJKkZKs/AG+Npqu97hqJM7t3PfXc87zds5zIJQyjZJbPwAly3PSE8nYXGY+1vhEMy20EyaqGa49NjWV4t/xfkOdqtf9qtf/+/4cLdmca0Bdk/CwYTue8KhwasWzFW8KdxpFLSu8L5xw5IDCF0rXA35UXAj4VbEzkx6HkOoZK/xg/QcbRack3CccL5ll4/s86iatOWt2Wmq3zCguaSZIEkOnzBImHv1SLcnsb9/Al2+SZfEY8rdZxRFHgaJ4E6KWpWtOal70nHwmqyr333m6+aHBoHtrEhoefP+lBxq3oFrx/Y8D368eQvgezqyaf1lyGnkTvVLT4nsQWYeT85qmb8PpBnTd2ZqjfUlhmaF8Hp6PoS0DHVfQvBBk9b3O0S3MrMkTXcLOLvTK/sjiJ6CLZ94KREMsAAAACXBIWXMAAAsSAAALEgHS3X78AAABHUlEQVQoFWNkaP//n4EMwESGHrAW6mtkZvz3G2g03BsBagz/nRUQ7sNp49//TJ+Byv6BlMbrMjCsC2Jg3BbKwOAgB9GMolGAg4GBESIOIrmA+A9I03xvFHGwCrhGkDOe5TAw9LvAFbEn6jEwwTT9BtodvJ7h/4FHYH0MLBCKgaHTgYGBE8jLN2FgYAJae+4FA8NcLwZWkAtAmoLWMTBsuYNwECMsHrVEGBj2RzEwiIEciATANgE1bb6DJAgMNLhTr71hYHBcxsDw+htCAQ5NIAU/4RpBPKjmf2++MfwHaQpZD7cJyEMB3+BORRL+rybE8FOEk4Hj2FO4KMgdrFAMitun2DTCVSMxYAkBFFYg9j94qCIpwsZEil5wyDIDAAXIUsnSKmq7AAAAAElFTkSuQmCC);
}
.tui-full-calendar-ic-checkbox {
margin-left: 5px;
border-radius: 2px;
@ -196,6 +234,7 @@
.tui-full-calendar-popup-detail {
font: @colortheme_app-font;
color: @cryptpad_text_col;
box-shadow: @cryptpad_ui_shadow;
.tui-full-calendar-popup-container {
padding-bottom: 17px;
}
@ -203,28 +242,44 @@
font-size: 14px;
}
.tui-full-calendar-section-button {
margin-top: 10px;
border: 0;
display: flex;
align-items: center;
align-items: start;
button {
flex: 1;
margin: 0;
}
}
.tui-full-calendar-popup-top-line {
border-radius: 10px 10px 0px 0px;
height: 10px;
}
.tui-full-calendar-popup-vertical-line {
visibility: hidden;
width: 10px;
}
}
.cp-recurrence-label, .cp-notif-label {
color: @cryptpad_text_col;
margin-right: 1rem;
i {
margin-right: 0.5rem;
}
}
.cp-calendar-recurrence-container {
margin-top: 1rem;
.cp-calendar-rec-translated-str {
margin-top: 0.5rem;
}
}
.cp-calendar-add-notif {
flex-flow: column;
align-items: baseline !important;
margin: 10px 0;
.cp-notif-label {
color: @cp_sidebar-hint;
margin-right: 20px;
}
margin: 1rem 0;
* {
font-size: @colortheme_app-font-size;
font-weight: normal;
@ -234,34 +289,58 @@
}
.cp-calendar-notif-list-container {
margin-bottom: 10px;
.cp-notif-label {
margin-top: 0.5em;
}
}
.cp-calendar-notif-list {
display: flex;
flex-flow: column;
.cp-notif-entry {
margin-bottom: 2px;
border-radius: @variables_radius;
background-color: fade(@cryptpad_text_col, 10%);
padding: 0.25rem;
.cp-notif-value {
width: 170px;
display: inline-flex;
line-height: 30px;
vertical-align: middle;
.cp-before {
flex: 1;
min-width: 0;
}
}
span:not(:last-child) {
margin-right: 5px;
margin: 0px 5px;
}
.btn-danger-outline {
margin-right: 0px !important;
background-color: transparent;
color: @cryptpad_text_col;
border-color: @cryptpad_text_col;
&:hover {
color: @cp_buttons-red-color;
background-color: @cp_buttons-red;
border-color: @cp_buttons-red;
}
}
}
}
.cp-notif-empty {
display: none;
margin-bottom: 2px;
border-radius: @variables_radius;
background-color: fade(@cryptpad_text_col, 10%);
padding: 0.25rem 0.5rem;
line-height: 30px;
}
.cp-calendar-notif-list:empty ~ .cp-notif-empty {
display: block;
}
.cp-calendar-notif-form {
align-items: center;
margin-bottom: 20px;
// margin-bottom: 20px;
input {
width: 80px;
margin-right: 5px;
@ -270,15 +349,120 @@
}
.cp-calendar-close {
top: 17px;
right: 17px;
height: auto;
margin-right: 0px;
line-height: initial;
border: 1px solid;
&:not(:hover) {
background: transparent;
}
}
}
.cp-calendar-rec-inline, .cp-calendar-rec-block {
&:not(:last-child) {
margin-bottom: 10px;
}
}
.cp-calendar-rec-inline {
display: flex;
flex-flow: row;
align-items: center;
& > *:not(:first-child) { margin-left: 5px; }
.cp-dropdown-container {
position: unset;
}
input[type="number"] {
width: 80px !important;
margin-bottom: 0 !important;
}
.cp-checkmark {
margin-right: 0.5rem;
}
}
.cp-calendar-rec-block {
.cp-calendar-rec-block-title {
margin-bottom: 0.5rem !important;
}
.cp-radio {
margin-bottom: 0.5rem;
}
input[type="radio"]:not(:checked) ~ .cp-checkmark-label {
input {
filter: grayscale(1);
}
}
.cp-checkmark-label {
& > *:not(:first-child) { margin-left: 5px; }
width: 100%;
//height: 26px;
display: flex;
align-items: center;
& > input {
margin-bottom: 0 !important;
}
input {
display: inline;
height: 24px !important;
padding: 0 5px !important;
}
input[type="text"] {
width: 200px !important;
}
input[type="number"] {
width: 80px !important;
margin-bottom: 0 !important;
}
}
}
#cp-calendar-rec-monthly-pick ~ .cp-checkmark-label {
display: flex;
align-items: center;
& > span {
margin-right: 20px;
}
}
button.cp-calendar-pick-el {
display: flex;
align-items: center;
justify-content: center;
&:not(:last-child) {
margin-right: 5px;
}
}
div.cp-calendar-weekly-pick {
button {
width: 50px;
}
}
div.cp-calendar-monthly-pick {
display: flex;
flex-flow: column;
& > div {
display: flex;
&:not(:last-child) {
margin-bottom: 5px;
}
button {
height: 25px;
width: 25px;
&.lastday {
width: 115px;
}
}
}
}
.tui-full-calendar-ic-repeat-b {
display: none;
& ~ * {
display: none;
}
}
#cp-toolbar .cp-calendar-browse {
display: flex;
align-items: center;
@ -395,6 +579,7 @@
align-items: center;
justify-content: center;
border-radius: @variables_radius;
flex-shrink: 0;
}
&.cp-active {
background-color: @cp_sidebar-left-item-bg;

View File

@ -2,7 +2,9 @@
// Calendars will be exported using this format instead of plain text.
define([
'/customize/pages.js',
], function (Pages) {
'/common/common-util.js',
'/calendar/recurrence.js'
], function (Pages, Util, Rec) {
var module = {};
var getICSDate = function (str) {
@ -57,60 +59,197 @@ define([
var data = content[uid];
// DTSTAMP: now...
// UID: uid
var start, end;
if (data.isAllDay && data.startDay && data.endDay) {
start = "DTSTART;VALUE=DATE:" + getDate(data.startDay);
end = "DTEND;VALUE=DATE:" + getDate(data.endDay, true);
} else {
start = "DTSTART:"+getICSDate(data.start);
end = "DTEND:"+getICSDate(data.end);
}
var getDT = function (data) {
var start, end;
if (data.isAllDay) {
var startDate = new Date(data.start);
var endDate = new Date(data.end);
data.startDay = data.startDay || (startDate.getFullYear() + '-' + (startDate.getMonth()+1) + '-' + startDate.getDate());
data.endDay = data.endDay || (endDate.getFullYear() + '-' + (endDate.getMonth()+1) + '-' + endDate.getDate());
start = "DTSTART;VALUE=DATE:" + getDate(data.startDay);
end = "DTEND;VALUE=DATE:" + getDate(data.endDay, true);
} else {
start = "DTSTART:"+getICSDate(data.start);
end = "DTEND:"+getICSDate(data.end);
}
return {
start: start,
end: end
};
};
Array.prototype.push.apply(ICS, [
'BEGIN:VEVENT',
'DTSTAMP:'+getICSDate(+new Date()),
'UID:'+uid,
start,
end,
'SUMMARY:'+ data.title,
'LOCATION:'+ data.location,
]);
if (Array.isArray(data.reminders)) {
data.reminders.forEach(function (valueMin) {
var time = valueMin * 60;
var days = Math.floor(time / DAY);
time -= days * DAY;
var hours = Math.floor(time / HOUR);
time -= hours * HOUR;
var minutes = Math.floor(time / MINUTE);
time -= minutes * MINUTE;
var seconds = time;
var str = "-P" + days + "D";
if (hours || minutes || seconds) {
str += "T" + hours + "H" + minutes + "M" + seconds + "S";
var getRRule = function (data) {
if (!data.recurrenceRule || !data.recurrenceRule.freq) { return; }
var r = data.recurrenceRule;
var rrule = "RRULE:";
rrule += "FREQ="+r.freq.toUpperCase();
Object.keys(r).forEach(function (k) {
if (k === "freq") { return; }
if (k === "by") {
Object.keys(r.by).forEach(function (_k) {
rrule += ";BY"+_k.toUpperCase()+"="+r.by[_k];
});
return;
}
rrule += ";"+k.toUpperCase()+"="+r[k];
});
return rrule;
};
var addEvent = function (arr, data, recId) {
var uid = data.id;
var dt = getDT(data);
var start = dt.start;
var end = dt.end;
var rrule = getRRule(data);
Array.prototype.push.apply(arr, [
'BEGIN:VEVENT',
'DTSTAMP:'+getICSDate(+new Date()),
'UID:'+uid,
start,
end,
recId,
rrule,
'SUMMARY:'+ data.title,
'LOCATION:'+ data.location,
].filter(Boolean));
if (Array.isArray(data.reminders)) {
data.reminders.forEach(function (valueMin) {
var time = valueMin * 60;
var days = Math.floor(time / DAY);
time -= days * DAY;
var hours = Math.floor(time / HOUR);
time -= hours * HOUR;
var minutes = Math.floor(time / MINUTE);
time -= minutes * MINUTE;
var seconds = time;
var str = "-P" + days + "D";
if (hours || minutes || seconds) {
str += "T" + hours + "H" + minutes + "M" + seconds + "S";
}
Array.prototype.push.apply(arr, [
'BEGIN:VALARM',
'ACTION:DISPLAY',
'DESCRIPTION:This is an event reminder',
'TRIGGER:'+str,
'END:VALARM'
]);
});
}
if (Array.isArray(data.cp_hidden)) {
Array.prototype.push.apply(arr, data.cp_hidden);
}
arr.push('END:VEVENT');
};
var applyChanges = function (base, changes) {
var applyDiff = function (obj, k) {
var diff = obj[k]; // Diff is always compared to origin start/end
var d = new Date(base[k]);
d.setDate(d.getDate() + diff.d);
d.setHours(d.getHours() + diff.h);
d.setMinutes(d.getMinutes() + diff.m);
base[k] = +d;
};
Object.keys(changes || {}).forEach(function (k) {
if (k === "start" || k === "end") {
return applyDiff(changes, k);
}
base[k] = changes[k];
});
};
var prev = data;
// Check if we have "one-time" or "from date" updates.
// "One-time" updates will be added accordingly to the ICS specs
// "From date" updates will be added as new events and will add
// an "until" value to the initial event's RRULE
var toAdd = [];
if (data.recurrenceRule && data.recurrenceRule.freq && data.recUpdate) {
var ru = data.recUpdate;
var _all = {};
var duration = data.end - data.start;
var all = Rec.getAllOccurrences(data); // "false" if infinite
Object.keys(ru.from || {}).forEach(function (d) {
if (!Object.keys(ru.from[d] || {}).length) { return; }
_all[d] = _all[d] || {};
_all[d].from = ru.from[d];
});
Object.keys(ru.one || {}).forEach(function (d) {
if (!Object.keys(ru.one[d] || {}).length) { return; }
_all[d] = _all[d] || {};
_all[d].one = ru.one[d];
});
Object.keys(_all).sort(function (a, b) {
return Number(a) - Number(b);
}).forEach(function (d) {
d = Number(d);
var r = prev.recurrenceRule;
// This rule won't apply if we've reached "until" or "count"
var idx = all && all.indexOf(d);
if (all && idx === -1) {
// Make sure we don't have both count and until
if (all.length === r.count) { delete r.until; }
else { delete r.count; }
return;
}
var ud = _all[d];
if (ud.from) { // "From" updates are not supported by ICS: make a new event
var _new = Util.clone(prev);
r.until = getICSDate(d - 1); // Stop previous recursion
delete r.count;
addEvent(ICS, prev, null); // Add previous event
Array.prototype.push.apply(ICS, toAdd); // Add individual updates
toAdd = [];
prev = _new;
if (all) { all = all.slice(idx); }
// if we updated the recurrence rule, count is reset, nothing to do
// if we didn't update the recurrence, we need to fix the count
var _r = _new.recurrenceRule;
if (all && !ud.from.recurrenceRule && _r && _r.count) {
_r.count -= idx;
}
prev.start = d;
prev.end = d + duration;
prev.id = Util.uid();
applyChanges(prev, ud.from);
duration = prev.end - prev.start;
}
if (ud.one) { // Add update
var _one = Util.clone(prev);
_one.start = d;
_one.end = d + duration;
applyChanges(_one, ud.one);
var recId = "RECURRENCE-ID:"+getICSDate(+d);
delete _one.recurrenceRule;
addEvent(toAdd, _one, recId); // Add updated event
}
Array.prototype.push.apply(ICS, [
'BEGIN:VALARM',
'ACTION:DISPLAY',
'DESCRIPTION:This is an event reminder',
'TRIGGER:'+str,
'END:VALARM'
]);
});
}
if (Array.isArray(data.cp_hidden)) {
Array.prototype.push.apply(ICS, data.cp_hidden);
}
ICS.push('END:VEVENT');
addEvent(ICS, prev);
Array.prototype.push.apply(ICS, toAdd); // Add individual updates
});
ICS.push('END:VCALENDAR');
return new Blob([ ICS.join('\n') ], { type: 'text/calendar;charset=utf-8' });
return new Blob([ ICS.join('\r\n') ], { type: 'text/calendar;charset=utf-8' });
};
module.import = function (content, id, cb) {
@ -171,7 +310,7 @@ define([
}
// Store other properties
var used = ['dtstart', 'dtend', 'uid', 'summary', 'location', 'dtstamp'];
var used = ['dtstart', 'dtend', 'uid', 'summary', 'location', 'dtstamp', 'rrule', 'recurrence-id'];
var hidden = [];
ev.getAllProperties().forEach(function (p) {
if (used.indexOf(p.name) !== -1) { return; }
@ -192,8 +331,25 @@ define([
if (reminders.indexOf(minutes) === -1) { reminders.push(minutes); }
});
// Get recurrence rule
var rrule = ev.getFirstPropertyValue('rrule');
var rec;
if (rrule && rrule.freq) {
rec = {};
rec.freq = rrule.freq.toLowerCase();
if (rrule.interval) { rec.interval = rrule.interval; }
if (rrule.count) { rec.count = rrule.count; }
if (Object.keys(rrule).includes('wkst')) { rec.wkst = (rrule.wkst + 6) % 7; }
if (rrule.until) { rec.until = +new Date(rrule.until); }
Object.keys(rrule.parts || {}).forEach(function (k) {
rec.by = rec.by || {};
var _k = k.toLowerCase().slice(2); // "BYDAY" ==> "day"
rec.by[_k] = rrule.parts[k];
});
}
// Create event
res[uid] = {
var obj = {
calendarId: id,
id: uid,
category: 'time',
@ -203,15 +359,48 @@ define([
start: start,
end: end,
reminders: reminders,
cp_hidden: hidden
cp_hidden: hidden,
};
if (rec) { obj.recurrenceRule = rec; }
if (!hidden.length) { delete res[uid].cp_hidden; }
if (!reminders.length) { delete res[uid].reminders; }
if (!hidden.length) { delete obj.cp_hidden; }
if (!reminders.length) { delete obj.reminders; }
var recId = ev.getFirstPropertyValue('recurrence-id');
if (recId) {
setTimeout(function () {
if (!res[uid]) { return; }
var old = res[uid];
var time = +new Date(recId);
var diff = {};
var from = {};
Object.keys(obj).forEach(function (k) {
if (JSON.stringify(old[k]) === JSON.stringify(obj[k])) { return; }
if (['start','end'].includes(k)) {
diff[k] = Rec.diffDate(old[k], obj[k]);
return;
}
if (k === "recurrenceRule") {
from[k] = obj[k];
return;
}
diff[k] = obj[k];
});
old.recUpdate = old.recUpdate || {one:{},from:{}};
if (Object.keys(from).length) { old.recUpdate.from[time] = from; }
if (Object.keys(diff).length) { old.recUpdate.one[time] = diff; }
});
return;
}
res[uid] = obj;
});
cb(null, res);
// setTimeout to make sure we call back after the "recurrence-id" setTimeout
// are called
setTimeout(function () {
cb(null, res);
});
});
};

File diff suppressed because it is too large Load Diff

View File

@ -6,17 +6,27 @@ define([
'/common/sframe-common-outer.js',
], function (nThen, ApiConfig, DomReady, SFCommonO) {
// Loaded in load #2
var hash, href;
nThen(function (waitFor) {
DomReady.onReady(waitFor());
}).nThen(function (waitFor) {
SFCommonO.initIframe(waitFor);
var obj = SFCommonO.initIframe(waitFor, true);
href = obj.href;
hash = obj.hash;
}).nThen(function (/*waitFor*/) {
var addData = function (meta) {
meta.calendarHash = Boolean(window.location.hash);
var addData = function (meta, Cryptpad, user, Utils) {
if (hash) {
var parsed = Utils.Hash.parsePadUrl(href);
if (parsed.hashData && parsed.hashData.newPadOpts) {
meta.calendarOpts = Utils.Hash.decodeDataOptions(parsed.hashData.newPadOpts);
}
}
meta.calendarHash = hash;
};
SFCommonO.start({
addData: addData,
hash: hash,
href: href,
noRealtime: true,
cache: true,
});

869
www/calendar/recurrence.js Normal file
View File

@ -0,0 +1,869 @@
define([
'/common/common-util.js',
], function (Util) {
var Rec = {};
var debug = function () {};
// Get week number with any "WKST" (firts day of the week)
// Week 1 is the first week of the year containing at least 4 days in this year
// It depends on which day is considered the first day of the week (default Monday)
// In our case, wkst is a number matching the JS rule: 0 == Sunday
var getWeekNo = Rec.getWeekNo = function (date, wkst) {
if (typeof(wkst) !== "number") { wkst = 1; } // Default monday
var newYear = new Date(date.getFullYear(),0,1);
var day = newYear.getDay() - wkst; //the day of week the year begins on
day = (day >= 0 ? day : day + 7);
var daynum = Math.floor((date.getTime() - newYear.getTime())/86400000) + 1;
var weeknum;
// Week 1 / week 53
if (day < 4) {
weeknum = Math.floor((daynum+day-1)/7) + 1;
if (weeknum > 52) {
var nYear = new Date(date.getFullYear() + 1,0,1);
var nday = nYear.getDay() - wkst;
nday = nday >= 0 ? nday : nday + 7;
weeknum = nday < 4 ? 1 : 53;
}
}
else {
weeknum = Math.floor((daynum+day-1)/7);
}
return weeknum;
};
var getYearDay = function (date) {
var start = new Date(date.getFullYear(), 0, 0);
var diff = (date - start) +
((start.getTimezoneOffset() - date.getTimezoneOffset()) * 60 * 1000);
var oneDay = 1000 * 60 * 60 * 24;
return Math.floor(diff / oneDay);
};
var setYearDay = function (date, day) {
if (typeof(day) !== "number" || Math.abs(day) < 1 || Math.abs(day) > 366) { return; }
if (day < 0) {
var max = getYearDay(new Date(date.getFullYear(), 11, 31));
day = max + day + 1;
}
date.setMonth(0);
date.setDate(day);
return true;
};
var getEndData = function (s, e) {
if (s > e) { return void console.error("Wrong data"); }
var days;
if (e.getFullYear() === s.getFullYear()) {
days = getYearDay(e) - getYearDay(s);
} else { // eYear < sYear
var tmp = new Date(s.getFullYear(), 11, 31);
var d1 = getYearDay(tmp) - getYearDay(s); // Number of days before December 31st
var de = getYearDay(e);
days = d1 + de;
while ((tmp.getFullYear()+1) < e.getFullYear()) {
tmp.setFullYear(tmp.getFullYear()+1);
days += getYearDay(tmp);
}
}
return {
h: e.getHours(),
m: e.getMinutes(),
days: days
};
};
var setEndData = function (s, e, data) {
e.setTime(+s);
if (!data) { return; }
e.setHours(data.h);
e.setMinutes(data.m);
e.setSeconds(0);
e.setDate(s.getDate() + data.days);
};
var DAYORDER = Rec.DAYORDER = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
var getDayData = function (str) {
var pos = Number(str.slice(0,-2));
var day = DAYORDER.indexOf(str.slice(-2));
return pos ? [pos, day] : day;
};
var goToFirstWeekDay = function (date, wkst) {
var d = date.getDay();
wkst = typeof(wkst) === "number" ? wkst : 1;
if (d >= wkst) {
date.setDate(date.getDate() - (d-wkst));
} else {
date.setDate(date.getDate() - (7+d-wkst));
}
};
var getDateStr = function (date) {
return date.getFullYear() + '-' + (date.getMonth()+1) + '-' + date.getDate();
};
var FREQ = {};
FREQ['daily'] = function (s, i) {
s.setDate(s.getDate()+i);
};
FREQ['weekly'] = function (s,i) {
s.setDate(s.getDate()+(i*7));
};
FREQ['monthly'] = function (s,i) {
s.setMonth(s.getMonth()+i);
};
FREQ['yearly'] = function (s,i) {
s.setFullYear(s.getFullYear()+i);
};
// EXPAND is used to create iterations added from a BYxxx rule
// dateA is the start date and b is the number or id of the BYxxx rule item
var EXPAND = {};
EXPAND['month'] = function (dateS, origin, b) {
var oS = new Date(origin.start);
var a = dateS.getMonth() + 1;
var toAdd = (b-a+12)%12;
var m = dateS.getMonth() + toAdd;
dateS.setMonth(m);
dateS.setDate(oS.getDate());
if (dateS.getMonth() !== m) { return; } // Day 31 may move us to the next month
return true;
};
EXPAND['weekno'] = function (dateS, origin, week, rule) {
var wkst = rule && rule.wkst;
if (typeof(wkst) !== "number") { wkst = 1; } // Default monday
var oS = new Date(origin.start);
var lastD = new Date(dateS.getFullYear(), 11, 31); // December 31st
var lastW = getWeekNo(lastD, wkst); // Last week of the year is either 52 or 53
var doubleOne = lastW === 1;
if (lastW === 1) { lastW = 52; }
var a = getWeekNo(dateS, wkst);
if (!week || week > lastW) { return false; } // Week 53 may not exist this year
if (week < 0) { week = lastW + week + 1; } // Turn negative week number into positive
var toAdd = week - a;
var weekS = new Date(+dateS);
// Go to the selected week
weekS.setDate(weekS.getDate() + (toAdd * 7));
goToFirstWeekDay(weekS, wkst);
// Then make sure we are in the correct start day
var all = 'aaaaaaa'.split('').map(function (o, i) {
var date = new Date(+weekS);
date.setDate(date.getDate() + i);
if (date.getFullYear() !== dateS.getFullYear()) { return; }
return date.toLocaleDateString() !== oS.toLocaleDateString() && date;
}).filter(Boolean);
// If we're looking for week 1 and the last week is a week 1, add the days
if (week === 1 && doubleOne) {
goToFirstWeekDay(lastD, wkst);
'aaaaaaa'.split('').some(function (o, i) {
var date = new Date(+lastD);
date.setDate(date.getDate() + i);
if (date.toLocaleDateString() === oS.toLocaleDateString()) { return; }
if (date.getFullYear() > dateS.getFullYear()) { return true; }
all.push(date);
});
}
return all.length ? all : undefined;
};
EXPAND['yearday'] = function (dateS, origin, b) {
var y = dateS.getFullYear();
var state = setYearDay(dateS, b);
if (!state) { return; } // Invalid day "b"
if (dateS.getFullYear() !== y) { return; } // Day 366 make move us to the next year
return true;
};
EXPAND['monthday'] = function (dateS, origin, b, rule) {
if (typeof(b) !== "number" || Math.abs(b) < 1 || Math.abs(b) > 31) { return false; }
var setMonthDay = function (date, day) {
var m = date.getMonth();
if (day < 0) {
var tmp = new Date(date.getFullYear(), date.getMonth()+1, 0); // Last day
day = tmp.getDate() + day + 1;
}
date.setDate(day);
return date.getMonth() === m; // Don't push if day 31 moved us to the next month
};
// Monthly events
if (rule.freq === 'monthly') {
return setMonthDay(dateS, b);
}
var all = 'aaaaaaaaaaaa'.split('').map(function (o, i) {
var date = new Date(dateS.getFullYear(), i, 1);
var ok = setMonthDay(date, b);
return ok ? date : undefined;
}).filter(Boolean);
return all.length ? all : undefined;
};
EXPAND['day'] = function (dateS, origin, b, rule) {
// Here "b" can be a single day ("TU") or a position and a day ("1MO")
var day = getDayData(b);
var pos;
if (Array.isArray(day)) {
pos = day[0];
day = day[1];
}
var all = [];
if (![0,1,2,3,4,5,6].includes(day)) { return false; }
var filterPos = function (m) {
if (!pos) { return; }
var _all = [];
'aaaaaaaaaaaa'.split('').some(function (a, i) {
if (typeof(m) !== "undefined" && i !== m) { return; }
var _pos;
var tmp = all.filter(function (d) {
return d.getMonth() === i;
});
if (pos < 0) {
_pos = tmp.length + pos;
} else {
_pos = pos - 1; // An array starts at 0 but the recurrence rule starts at 1
}
_all.push(tmp[_pos]);
return typeof(m) !== "undefined" && i === m;
});
all = _all.filter(Boolean); // The "5th" {day} won't always exist
};
var tmp;
if (rule.freq === 'yearly') {
tmp = new Date(+dateS);
var y = dateS.getFullYear();
while (tmp.getDay() !== day) { tmp.setDate(tmp.getDate()+1); }
while (tmp.getFullYear() === y) {
all.push(new Date(+tmp));
tmp.setDate(tmp.getDate()+7);
}
filterPos();
return all;
}
if (rule.freq === 'monthly') {
tmp = new Date(+dateS);
var m = dateS.getMonth();
while (tmp.getDay() !== day) { tmp.setDate(tmp.getDate()+1); }
while (tmp.getMonth() === m) {
all.push(new Date(+tmp));
tmp.setDate(tmp.getDate()+7);
}
filterPos(m);
return all;
}
if (rule.freq === 'weekly') {
while (dateS.getDay() !== day) { dateS.setDate(dateS.getDate()+1); }
}
return true;
};
var LIMIT = {};
LIMIT['month'] = function (events, rule) {
return events.filter(function (s) {
return rule.includes(s.getMonth()+1);
});
};
LIMIT['weekno'] = function (events, weeks, rules) {
return events.filter(function (s) {
var wkst = rules && rules.wkst;
if (typeof(wkst) !== "number") { wkst = 1; } // Default monday
var lastD = new Date(s.getFullYear(), 11, 31); // December 31st
var lastW = getWeekNo(lastD, wkst); // Last week of the year is either 52 or 53
if (lastW === 1) { lastW = 52; }
var w = getWeekNo(s, wkst);
return weeks.some(function (week) {
if (week > 0) { return week === w; }
return w === (lastW + week + 1);
});
});
};
LIMIT['yearday'] = function (events, days) {
return events.filter(function (s) {
var d = getYearDay(s);
var max = getYearDay(new Date(s.getFullYear(), 11, 31));
return days.some(function (day) {
if (day > 0) { return day === d; }
return d === (max + day + 1);
});
});
};
LIMIT['monthday'] = function (events, rule) {
return events.filter(function (s) {
var r = Util.clone(rule);
// Transform the negative monthdays into positive for this specific month
r = r.map(function (b) {
if (b < 0) {
var tmp = new Date(s.getFullYear(), s.getMonth()+1, 0); // Last day
b = tmp.getDate() + b + 1;
}
return b;
});
return r.includes(s.getDate());
});
};
LIMIT['day'] = function (events, days, rules) {
return events.filter(function (s) {
var dayStr = s.toLocaleDateString();
// Check how to handle position in BYDAY rules (last day of the month or the year?)
var type = 'yearly';
if (rules.freq === 'monthly' ||
(rules.freq === 'yearly' && rules.by && rules.by.month)) {
type = 'monthly';
}
// Check if this event matches one of the allowed days
return days.some(function (r) {
// rule elements are strings with pos and day
var day = getDayData(r);
var pos;
if (Array.isArray(day)) {
pos = day[0];
day = day[1];
}
if (!pos) {
return s.getDay() === day;
}
// If we have a position, we can use EXPAND.day to get the nth {day} of the
// year/month and compare if it matches with
var d = new Date(s.getFullYear(), s.getMonth(), 1);
if (type === 'yearly') { d.setMonth(0); }
var res = EXPAND["day"](d, {}, r, {freq: type});
return res.some(function (date) {
return date.toLocaleDateString() === dayStr;
});
});
});
};
LIMIT['setpos'] = function (events, rule) {
var init = events.slice();
var rules = Util.deduplicateString(rule.slice().map(function (n) {
if (n > 0) { return (n-1); }
if (n === 0) { return; }
return init.length + n;
}));
return events.filter(function (ev) {
var idx = init.indexOf(ev);
return rules.includes(idx);
});
};
var BYORDER = ['month','weekno','yearday','monthday','day'];
var BYDAYORDER = ['month','monthday','day'];
Rec.getMonthId = function (d) {
return d.getFullYear() + '-' + d.getMonth();
};
var cache = window.CP_calendar_cache = {};
var recurringAcross = {};
Rec.resetCache = function () {
cache = window.CP_calendar_cache = {};
recurringAcross = {};
};
var iterate = function (rule, _origin, s) {
// "origin" is the original event to detect the start of BYxxx
var origin = Util.clone(_origin);
var oS = new Date(origin.start);
var id = origin.id.split('|')[0]; // Use same cache when updating recurrence rule
// "uid" is used for the cache
var uid = s.toLocaleDateString();
cache[id] = cache[id] || {};
var inter = rule.interval || 1;
var freq = rule.freq;
var all = [];
var limit = function (byrule, n) {
all = LIMIT[byrule](all, n, rule);
};
var expand = function (byrule) {
return function (n) {
// Set the start date at the beginning of the current FREQ
var _s = new Date(+s);
if (rule.freq === 'yearly') {
// January 1st
_s.setMonth(0);
_s.setDate(1);
} else if (rule.freq === 'monthly') {
_s.setDate(1);
} else if (rule.freq === 'weekly') {
goToFirstWeekDay(_s, rule.wkst);
} else if (rule.freq === 'daily') {
// We don't have < byday rules so we can't expand daily rules
}
var add = EXPAND[byrule](_s, origin, n, rule);
if (!add) { return; }
if (Array.isArray(add)) {
add = add.filter(function (dateS) {
return dateS.toLocaleDateString() !== oS.toLocaleDateString();
});
Array.prototype.push.apply(all, add);
} else {
if (_s.toLocaleDateString() === oS.toLocaleDateString()) { return; }
all.push(_s);
}
};
};
// Manage interval for the next iteration
var it = Util.once(function () {
FREQ[freq](s, inter);
});
var addDefault = function () {
if (freq === "monthly") {
s.setDate(15);
} else if (freq === "yearly" && oS.getMonth() === 1 && oS.getDate() === 29) {
s.setDate(28);
}
it();
var _s = new Date(+s);
if (freq === "monthly" || freq === "yearly") {
_s.setDate(oS.getDate());
if (_s.getDate() !== oS.getDate()) { return; } // If 31st or Feb 29th doesn't exist
if (freq === "yearly" && _s.getMonth() !== oS.getMonth()) { return; }
// FIXME if there is a recUpdate that moves the 31st to the 30th, the event
// will still only be displayed on months with 31 days
}
all.push(_s);
};
if (Array.isArray(cache[id][uid])) {
debug('Get cache', id, uid);
if (freq === "monthly") {
s.setDate(15);
} else if (freq === "yearly" && oS.getMonth() === 1 && oS.getDate() === 29) {
s.setDate(28);
}
it();
return cache[id][uid];
}
if (rule.by && freq === 'yearly') {
var order = BYORDER.slice();
var monthLimit = false;
if (rule.by.weekno || rule.by.yearday || rule.by.monthday || rule.by.day) {
order.shift();
monthLimit = true;
}
var first = true;
order.forEach(function (_order) {
var r = rule.by[_order];
if (!r) { return; }
if (first) {
r.forEach(expand(_order));
first = false;
} else if (_order === "day") {
if (rule.by.yearday || rule.by.monthday || rule.by.weekno) {
limit('day', rule.by.day);
} else {
rule.by.day.forEach(expand('day'));
}
} else {
limit(_order, r);
}
});
if (rule.by.month && monthLimit) {
limit('month', rule.by.month);
}
}
if (rule.by && freq === 'monthly') {
// We're going to compute all the entries for the coming month
if (!rule.by.monthday && !rule.by.day) {
addDefault();
} else if (rule.by.monthday) {
rule.by.monthday.forEach(expand('monthday'));
} else if (rule.by.day) {
rule.by.day.forEach(expand('day'));
}
if (rule.by.month) {
limit('month', rule.by.month);
}
if (rule.by.day && rule.by.monthday) {
limit('day', rule.by.day);
}
}
if (rule.by && freq === 'weekly') {
// We're going to compute all the entries for the coming week
if (!rule.by.day) {
addDefault();
} else {
rule.by.day.forEach(expand('day'));
}
if (rule.by.month) {
limit('month', rule.by.month);
}
}
if (rule.by && freq === 'daily') {
addDefault();
BYDAYORDER.forEach(function (_order) {
var r = rule.by[_order];
if (!r) { return; }
limit(_order, r);
});
}
all.sort(function (a, b) {
return a-b;
});
if (rule.by && rule.by.setpos) {
limit('setpos', rule.by.setpos);
}
if (!rule.by || !Object.keys(rule.by).length) {
addDefault();
} else {
it();
}
var done = [];
all = all.filter(function (newS) {
var start = new Date(+newS).toLocaleDateString();
if (done.includes(start)) { return false; }
done.push(start);
return true;
});
debug('Set cache', id, uid);
cache[id][uid] = all;
return all;
};
var getNextRules = function (obj) {
if (!obj.recUpdate) { return []; }
var _allRules = {};
var _obj = obj.recUpdate.from;
Object.keys(_obj || {}).forEach(function (d) {
var u = _obj[d];
if (u.recurrenceRule) { _allRules[d] = u.recurrenceRule; }
});
return Object.keys(_allRules).sort(function (a, b) { return Number(a)-Number(b); })
.map(function (k) {
var r = Util.clone(_allRules[k]);
if (!FREQ[r.freq]) { return; }
if (r.interval && r.interval < 1) { return; }
r._start = Number(k);
return r;
}).filter(Boolean);
};
Rec.getRecurring = function (months, events) {
if (window.CP_DEV_MODE) { debug = console.warn; }
var toAdd = [];
months.forEach(function (monthId) {
// from 1st day of the month at 00:00 to last day at 23:59:59:999
var ms = monthId.split('-');
var _startMonth = new Date(ms[0], ms[1]);
var _endMonth = new Date(+_startMonth);
_endMonth.setMonth(_endMonth.getMonth() + 1);
_endMonth.setMilliseconds(-1);
debug('Compute month', _startMonth.toLocaleDateString());
var rec = events || [];
rec.forEach(function (obj) {
var _start = new Date(obj.start);
var _end = new Date(obj.end);
var _origin = obj;
var rule = obj.recurrenceRule;
if (!rule) { return; }
var nextRules = getNextRules(obj);
var nextRule = nextRules.shift();
if (_start >= _endMonth) { return; }
// Check the "until" date of the latest rule we can use and stop now
// if the recurrence ends before the current month
var until = rule.until;
var _nextRules = nextRules.slice();
var _nextRule = nextRule;
while (_nextRule && _nextRule._start && _nextRule._start < _startMonth) {
until = nextRule.until;
_nextRule = _nextRules.shift();
}
if (until < _startMonth) { return; }
var endData = getEndData(_start, _end);
if (rule.interval && rule.interval < 1) { return; }
if (!FREQ[rule.freq]) { return; }
/*
// Rule examples
rule.by = {
//month: [1, 4, 5, 8, 12],
//weekno: [1, 2, 4, 5, 32, 34, 35, 50],
//yearday: [1, 2, 29, 30, -2, -1, 250],
//monthday: [1, 2, 3, -3, -2, -1],
//day: ["MO", "WE", "FR"],
//setpos: [1, 2, -1, -2]
};
rule.wkst = 0;
rule.interval = 2;
rule.freq = 'yearly';
rule.count = 10;
*/
debug('Iterate over', obj.title, obj);
debug('Use rule', rule);
var count = rule.count;
var c = 1;
var next = function (start) {
var evS = new Date(+start);
if (count && c >= count) { return; }
debug('Start iteration', evS.toLocaleDateString());
var _toAdd = iterate(rule, obj, evS);
debug('Iteration results', JSON.stringify(_toAdd.map(function (o) { return new Date(o).toLocaleDateString();})));
// Make sure to continue if the current year doesn't provide any result
if (!_toAdd.length) {
if (evS.getFullYear() < _startMonth.getFullYear() ||
evS < _endMonth) {
return void next(evS);
}
return;
}
var stop = false;
var newrule = false;
_toAdd.some(function (_newS) {
// Make event with correct start and end time
var _ev = Util.clone(obj);
_ev.id = _origin.id + '|' + (+_newS);
var _evS = new Date(+_newS);
var _evE = new Date(+_newS);
setEndData(_evS, _evE, endData);
_ev.start = +_evS;
_ev.end = +_evE;
_ev._count = c;
if (_ev.isAllDay && _ev.startDay) { _ev.startDay = getDateStr(_evS); }
if (_ev.isAllDay && _ev.endDay) { _ev.endDay = getDateStr(_evE); }
if (nextRule && _ev.start === nextRule._start) {
newrule = true;
}
var useNewRule = function () {
if (!newrule) { return; }
debug('Use new rule', nextRule);
_ev._count = c;
count = nextRule.count;
c = 1;
evS = +_evS;
obj = _ev;
rule = nextRule;
nextRule = nextRules.shift();
};
if (c >= count) { // Limit reached
debug(_evS.toLocaleDateString(), 'count');
stop = true;
return true;
}
if (_evS >= _endMonth) { // Won't affect us anymore
debug(_evS.toLocaleDateString(), 'endMonth');
stop = true;
return true;
}
if (rule.until && _evS > rule.until) {
debug(_evS.toLocaleDateString(), 'until');
stop = true;
return true;
}
if (_evS < _start) { // "Expand" rules may create events before the _start
debug(_evS.toLocaleDateString(), 'start');
return;
}
c++;
if (_evE < _startMonth) { // Ended before the current month
// Nothing to display but continue the recurrence
debug(_evS.toLocaleDateString(), 'startMonth');
if (newrule) { useNewRule(); }
return;
}
// If a recurring event start and end in different months, make sure
// it is only added once
if ((_evS < _endMonth && _evE >= _endMonth) ||
(_evS < _startMonth && _evE >= _startMonth)) {
if (recurringAcross[_ev.id] && recurringAcross[_ev.id].includes(_ev.start)) {
return;
} else {
recurringAcross[_ev.id] = recurringAcross[_ev.id] || [];
recurringAcross[_ev.id].push(_ev.start);
}
}
// Add this event
toAdd.push(_ev);
if (newrule) {
useNewRule();
return true;
}
});
if (!stop) { next(evS); }
};
next(_start);
debug('Added this month (all events)', toAdd.map(function (ev) {
return new Date(ev.start).toLocaleDateString();
}));
});
});
return toAdd;
};
Rec.getAllOccurrences = function (ev) {
if (!ev.recurrenceRule) { return [ev.start]; }
var r = ev.recurrenceRule;
// In case of infinite recursion, we can't get all
if (!r.until && !r.count) { return false; }
var all = [ev.start];
var d = new Date(ev.start);
d.setDate(15); // Make sure we won't skip a month if the event starts on day > 28
var toAdd = [];
var i = 0;
var check = function () {
return r.count ? (all.length < r.count) : (+d <= r.until);
};
while ((toAdd = Rec.getRecurring([Rec.getMonthId(d)], [ev])) && check() && i < (r.count*12)) {
Array.prototype.push.apply(all, toAdd.map(function (_ev) { return _ev.start; }));
d.setMonth(d.getMonth() + 1);
i++;
}
return all;
};
Rec.diffDate = function (oldTime, newTime) {
var n = new Date(newTime);
var o = new Date(oldTime);
// Diff Days
var d = 0;
var mult = n < o ? -1 : 1;
while (n.toLocaleDateString() !== o.toLocaleDateString() || mult >= 10000) {
n.setDate(n.getDate() - mult);
d++;
}
d = mult * d;
// Diff hours
n = new Date(newTime);
var h = n.getHours() - o.getHours();
// Diff minutes
var m = n.getMinutes() - o.getMinutes();
return {
d: d,
h: h,
m: m
};
};
var sortUpdate = function (obj) {
return Object.keys(obj).sort(function (d1, d2) {
return Number(d1) - Number(d2);
});
};
Rec.applyUpdates = function (events) {
events.forEach(function (ev) {
ev.raw = {
start: ev.start,
end: ev.end,
};
if (!ev.recUpdate) { return; }
var from = ev.recUpdate.from || {};
var one = ev.recUpdate.one || {};
var s = ev.start;
// Add "until" date to our recurrenceRule if it has been modified in future occurences
var nextRules = getNextRules(ev).filter(function (r) {
return r._start > s;
});
var nextRule = nextRules.shift();
var applyDiff = function (obj, k) {
var diff = obj[k]; // Diff is always compared to origin start/end
var d = new Date(ev.raw[k]);
d.setDate(d.getDate() + diff.d);
d.setHours(d.getHours() + diff.h);
d.setMinutes(d.getMinutes() + diff.m);
ev[k] = +d;
};
sortUpdate(from).forEach(function (d) {
if (s < Number(d)) { return; }
Object.keys(from[d]).forEach(function (k) {
if (k === 'start' || k === 'end') { return void applyDiff(from[d], k); }
if (k === "recurrenceRule" && !from[d][k]) { return; }
ev[k] = from[d][k];
});
});
Object.keys(one[s] || {}).forEach(function (k) {
if (k === 'start' || k === 'end') { return void applyDiff(one[s], k); }
if (k === "recurrenceRule" && !one[s][k]) { return; }
ev[k] = one[s][k];
});
if (ev.deleted) {
Object.keys(ev).forEach(function (k) {
delete ev[k];
});
}
if (nextRule && ev.recurrenceRule) {
ev.recurrenceRule._next = nextRule._start - 1;
}
if (ev.reminders) {
ev.raw.reminders = ev.reminders;
}
});
return events;
};
return Rec;
});

View File

@ -68,6 +68,7 @@ define([
'markdown',
'gfm',
'html',
'asciidoc',
'htmlembedded',
'htmlmixed',
'index.html',
@ -143,6 +144,31 @@ define([
previews['htmlmixed'] = function (val, $div, common) {
DiffMd.apply(val, $div, common);
};
previews['asciidoc'] = function (val, $div, common) {
require([
'asciidoctor',
'/lib/highlight/highlight.pack.js',
'css!/lib/highlight/styles/' + (window.CryptPad_theme === 'dark' ? 'dark.css' : 'github.css')
], function (asciidoctor) {
var reg = asciidoctor.Extensions.create();
var Highlight = window.hljs;
reg.inlineMacro('media-tag', function () {
var t = this;
t.process(function (parent, target) {
var d = target.split('|');
return t.createInline(parent, 'quoted', `<media-tag src="${d[0]}" data-crypto-key="${d[1]}"></media-tag>`).convert();
});
});
var html = asciidoctor.convert(val, { attributes: 'showtitle', extension_registry: reg });
DiffMd.apply(html, $div, common);
$div.find('pre code').each(function (i, el) {
Highlight.highlightBlock(el);
});
});
};
var mkPreviewPane = function (editor, CodeMirror, framework, isPresentMode) {
var $previewContainer = $('#cp-app-code-preview');
@ -370,9 +396,17 @@ define([
evModeChange.reg(function (mode) {
if (MEDIA_TAG_MODES.indexOf(mode) !== -1) {
// Embedding is enabled
framework.setMediaTagEmbedder(function (mt) {
framework.setMediaTagEmbedder(function (mt, d) {
editor.focus();
editor.replaceSelection($(mt)[0].outerHTML);
var txt = $(mt)[0].outerHTML;
if (editor.getMode().name === "asciidoc") {
if (d.static) {
txt = d.href + `[${d.name}]`;
} else {
txt = `media-tag:${d.src}|${d.key}[]`;
}
}
editor.replaceSelection(txt);
});
} else {
// Embedding is disabled

View File

@ -352,10 +352,21 @@ define([
});
var linkName, linkPassword, linkMessage, linkError, linkSpinText;
var linkForm, linkSpin, linkResult;
var linkForm, linkSpin, linkResult, linkUses, linkRole;
var linkWarning;
// Invite from link
var dismissButton = h('span.fa.fa-times');
var roleViewer = UI.createRadio('cp-team-role', 'cp-team-role-viewer',
Messages.team_viewers, true, {
input: { value: 'VIEWER' },
});
var roleMember = UI.createRadio('cp-team-role', 'cp-team-role-member',
Messages.team_members, false, {
input: { value: 'MEMBER' },
});
var linkContent = h('div.cp-share-modal', [
h('p', Messages.team_inviteLinkTitle ),
linkError = h('div.alert.alert-danger.cp-teams-invite-alert', {style : 'display: none;'}),
@ -372,7 +383,7 @@ define([
h('div.cp-teams-invite-block', [
h('span', Messages.team_inviteLinkSetPassword),
h('a.cp-teams-help.fa.fa-question-circle', {
href: origin + Pages.localizeDocsLink('https://docs.cryptpad.org/en/user_guide/security.html#passwords-for-documents-and-folders'),
href: Pages.localizeDocsLink('https://docs.cryptpad.org/en/user_guide/security.html#passwords-for-documents-and-folders'),
target: "_blank",
'data-tippy-placement': "right"
})
@ -387,7 +398,21 @@ define([
linkMessage = h('textarea.cp-teams-invite-message', {
placeholder: Messages.team_inviteLinkNoteMsg,
rows: 3
})
}),
linkRole = h('div.cp-teams-invite-block.cp-teams-invite-role',
h('span', Messages.team_inviteRole),
roleViewer,
roleMember
),
h('div.cp-teams-invite-block.cp-teams-invite-uses',
linkUses = h('input', {
type: 'number',
min: 0,
max: 999,
value: 1
}),
h('span', Messages.team_inviteUses)
),
]),
linkSpin = h('div.cp-teams-invite-spinner', {
style: 'display: none;'
@ -407,10 +432,11 @@ define([
dismissButton
])
]);
$(linkUses).on('change keyup', function(e) {
if (e.target.value === '') { e.target.value = 0; }
});
$(linkMessage).keydown(function (e) {
if (e.which === 13) {
e.stopPropagation();
}
if (e.which === 13) { e.stopPropagation(); }
});
var localStore = window.cryptpadStore;
localStore.get('hide-alert-teamInvite', function (val) {
@ -428,6 +454,12 @@ define([
var $nav = $linkContent.closest('.alertify').find('nav');
$(linkError).text('').hide();
var name = $(linkName).val();
var uses = Number($(linkUses).val());
if (isNaN(uses) || !uses) { uses = -1; }
var role = $(linkRole).find("input[name='cp-team-role']:checked").val() || 'VIEWER';
var pw = $(linkPassword).find('input').val();
var msg = $(linkMessage).val();
var hash = Hash.createRandomHash('invite', pw);
@ -461,6 +493,8 @@ define([
hash: hash,
teamId: config.teamId,
seeds: seeds,
role: role,
uses: uses
}, waitFor(function (obj) {
if (obj && obj.error) {
waitFor.abort();
@ -1417,7 +1451,7 @@ define([
return /HTML/.test(Object.prototype.toString.call(o)) &&
typeof(o.tagName) === 'string';
};
var allowedTags = ['a', 'p', 'hr', 'div'];
var allowedTags = ['a', 'li', 'p', 'hr', 'div'];
var isValidOption = function (o) {
if (typeof o !== "object") { return false; }
if (isElement(o)) { return true; }
@ -1541,6 +1575,7 @@ define([
$innerblock.find('.cp-dropdown-element-active').removeClass('cp-dropdown-element-active');
if (config.isSelect && value) {
// We use JSON.stringify here to escape quotes
if (typeof(value) === "object") { value = JSON.stringify(value); }
var $val = $innerblock.find('[data-value='+JSON.stringify(value)+']');
setActive($val);
try {

View File

@ -125,6 +125,12 @@ define([
formSeed = obj;
}));
}).nThen(function () {
if (!formSeed) { // no drive mode
formSeed = localStorage.CP_formSeed || Hash.createChannelId();
localStorage.CP_formSeed = formSeed;
} else {
delete localStorage.CP_formSeed;
}
cb({
curvePrivate: curvePrivate,
curvePublic: curvePrivate && Hash.getCurvePublicFromPrivate(curvePrivate),
@ -135,28 +141,162 @@ define([
common.getFormAnswer = function (data, cb) {
postMessage("GET", {
key: ['forms', data.channel],
}, cb);
};
common.storeFormAnswer = function (data) {
postMessage("SET", {
key: ['forms', data.channel],
value: {
hash: data.hash,
curvePrivate: data.curvePrivate,
anonymous: data.anonymous
}
}, function (obj) {
if (obj && obj.error) {
if (obj.error === "ENODRIVE") {
var answered = JSON.parse(localStorage.CP_formAnswered || "[]");
if (answered.indexOf(data.channel) === -1) { answered.push(data.channel); }
localStorage.CP_formAnswered = JSON.stringify(answered);
return;
}
console.error(obj.error);
if (obj && obj.error === "ENODRIVE") {
var all = Util.tryParse(localStorage.CP_formAnswers || "{}");
return void cb(all[data.channel]);
}
});
if (obj && obj.error) { return void cb(obj); }
if (obj) {
if (!Array.isArray(obj)) { obj = [obj]; }
return void cb(obj);
}
// We have a drive and no answer but maybe we had
// previous "nodrive" answers: migrate
var old = Util.tryParse(localStorage.CP_formAnswers || "{}");
if (Array.isArray(old[data.channel])) {
var d = old[data.channel];
return void postMessage("SET", {
key: ['forms', data.channel],
value: d
}, function (obj) {
// Delete old data if it was correctly stored in the drive
if (obj && obj.error) { return void cb(d); }
delete old[data.channel];
localStorage.CP_formAnswers = JSON.stringify(old);
cb(d);
});
}
cb();
});
};
common.storeFormAnswer = function (data, cb) {
var answer = {
uid: data.uid,
hash: data.hash,
curvePrivate: data.curvePrivate,
anonymous: data.anonymous
};
var answers = [];
Nthen(function (waitFor) {
common.getFormAnswer(data, waitFor(function (obj) {
if (!obj || obj.error) { return; }
answers = obj;
}));
}).nThen(function () {
answers.push(answer);
postMessage("SET", {
key: ['forms', data.channel],
value: answers
}, function (obj) {
if (obj && obj.error) {
if (obj.error === "ENODRIVE") {
var all = Util.tryParse(localStorage.CP_formAnswers || "{}");
all[data.channel] = answers;
localStorage.CP_formAnswers = JSON.stringify(all);
/*
var answered = JSON.parse(localStorage.CP_formAnswered || "[]");
if (answered.indexOf(data.channel) === -1) { answered.push(data.channel); }
localStorage.CP_formAnswered = JSON.stringify(answered);
*/
return void cb();
}
console.error(obj.error);
}
cb();
});
});
};
common.deleteFormAnswers = function (data, _cb) {
var cb = Util.once(_cb);
common.getFormAnswer(data, function (obj) {
if (!obj || obj.error) { return void cb(); }
if (!obj.length) { return void cb(); }
var n = Nthen;
var nacl, theirs;
n = n(function (waitFor) {
require(['/bower_components/tweetnacl/nacl-fast.min.js'], waitFor(function () {
nacl = window.nacl;
var s = new Uint8Array(32);
theirs = nacl.box.keyPair.fromSecretKey(s);
}));
}).nThen;
var toDelete = [];
obj.forEach(function (answer) {
if (answer.uid !== data.uid) { return; }
n = n(function (waitFor) {
var hash = answer.hash;
var h = nacl.util.decodeUTF8(hash);
// Make proof
var curve = answer.curvePrivate;
var mySecret = nacl.util.decodeBase64(curve);
var nonce = nacl.randomBytes(24);
var proofBytes = nacl.box(h, nonce, theirs.publicKey, mySecret);
var proof = nacl.util.encodeBase64(nonce) +'|'+ nacl.util.encodeBase64(proofBytes);
var lineData = {
channel: data.channel,
hash: hash,
proof: proof
};
postMessage("DELETE_MAILBOX_MESSAGE", lineData, waitFor(function (obj) {
if (obj && obj.error && obj.error !== 'HASH_NOT_FOUND') {
// If HASH_NOT_FOUND, the message is already deleted
// so we can delete it locally
waitFor.abort();
return void cb(obj);
}
toDelete.push(hash);
}));
}).nThen;
});
n(function () {
obj = obj.filter(function (answer) { return !toDelete.includes(answer.hash); });
if (!obj.length) { obj = undefined; }
postMessage("SET", {
key: ['forms', data.channel],
value: obj
}, function (_obj) {
if (_obj && _obj.error === "ENODRIVE") {
var all = Util.tryParse(localStorage.CP_formAnswers || "{}");
if (obj) { all[data.channel] = obj; }
else { delete all[data.channel]; }
localStorage.CP_formAnswers = JSON.stringify(all);
return void cb();
}
return void cb(_obj);
});
});
});
};
common.muteChannel = function (channel, state, cb) {
var mutedChannels = [];
Nthen(function (waitFor) {
postMessage("GET", {
key: ['mutedChannels'],
}, waitFor(function (obj) {
if (obj && obj.error) { waitFor.abort(); return void cb(obj); }
mutedChannels = obj || [];
}));
}).nThen(function () {
if (state) {
if (!mutedChannels.includes(channel)) {
mutedChannels.push(channel);
}
} else {
mutedChannels = mutedChannels.filter(function (chan) {
return chan !== channel;
});
}
postMessage("SET", {
key: ['mutedChannels'],
value: mutedChannels
}, cb);
});
};
common.makeNetwork = function (cb) {
@ -1154,8 +1294,8 @@ define([
pad.onMetadataEvent = Util.mkEvent();
pad.onChannelDeleted = Util.mkEvent();
pad.requestAccess = function (data, cb) {
postMessage("REQUEST_PAD_ACCESS", data, cb);
pad.contactOwner = function (data, cb) {
postMessage("CONTACT_PAD_OWNER", data, cb);
};
pad.giveAccess = function (data, cb) {
postMessage("GIVE_PAD_ACCESS", data, cb);
@ -2385,6 +2525,7 @@ define([
anonHash: LocalStore.getFSHash(),
localToken: tryParsing(localStorage.getItem(Constants.tokenKey)), // TODO move this to LocalStore ?
language: common.getLanguage(),
form_seed: localStorage.CP_formSeed,
cache: rdyCfg.cache,
noDrive: rdyCfg.noDrive,
disableCache: localStorage['CRYPTPAD_STORE|disableCache'],

View File

@ -78,6 +78,7 @@ define([
var TAGS_NAME = Messages.fm_tagsName;
var SHARED_FOLDER = 'sf';
var SHARED_FOLDER_NAME = Messages.fm_sharedFolderName;
var FILTER = "filter";
// Icons
var faFolder = 'cptools-folder';
@ -1149,8 +1150,12 @@ define([
common.getMediaTagPreview(mts, idx);
};
var refresh = APP.refresh = function () {
APP.displayDirectory(currentPath);
var FILTER_BY = "filterBy";
var refresh = APP.refresh = function (cb) {
var type = APP.store[FILTER_BY];
var path = type ? [FILTER, type, currentPath] : currentPath;
APP.displayDirectory(path, undefined, cb);
};
// `app`: true (force open wiht the app), false (force open in preview),
@ -2940,72 +2945,147 @@ define([
$block.find('a.cp-app-drive-new-link, li.cp-app-drive-new-link').click(showLinkModal);
}
$block.find('a.cp-app-drive-new-doc, li.cp-app-drive-new-doc')
.click(function () {
.on('click auxclick', function (e) {
e.preventDefault();
var type = $(this).attr('data-type') || 'pad';
var path = manager.isPathIn(currentPath, [TRASH]) ? '' : currentPath;
openIn(type, path, APP.team);
});
};
var getNewPadOptions = function (isInRoot) {
var options = [];
if (isInRoot) {
options.push({
class: 'cp-app-drive-new-folder',
icon: $folderIcon.clone()[0],
name: Messages.fm_folder,
});
if (!APP.disableSF && !manager.isInSharedFolder(currentPath)) {
options.push({
class: 'cp-app-drive-new-shared-folder',
icon: $sharedFolderIcon.clone()[0],
name: Messages.fm_sharedFolder,
});
}
options.push({ separator: true });
options.push({
class: 'cp-app-drive-new-fileupload',
icon: getIcon('fileupload')[0],
name: Messages.uploadButton,
});
if (APP.allowFolderUpload) {
options.push({
class: 'cp-app-drive-new-folderupload',
icon: getIcon('folderupload')[0],
name: Messages.uploadFolderButton,
});
}
options.push({ separator: true });
options.push({
class: 'cp-app-drive-new-link',
icon: getIcon('link')[0],
name: Messages.fm_link_new,
});
options.push({ separator: true });
}
getNewPadTypes().forEach(function (type) {
var typeClass = 'cp-app-drive-new-doc';
var premium = common.checkRestrictedApp(type);
if (premium < 0) {
typeClass += ' cp-app-hidden cp-app-disabled';
} else if (premium === 0) {
typeClass += ' cp-app-disabled';
}
options.push({
class: typeClass,
type: type,
icon: getIcon(type)[0],
name: Messages.type[type],
});
});
if (APP.store[FILTER_BY]) {
var typeFilter = APP.store[FILTER_BY];
options = options.filter((obj) => {
if (obj.separator) { return false; }
if (typeFilter === 'link') {
return obj.class.includes('cp-app-drive-new-link');
}
if (typeFilter === 'file') {
return obj.class.includes('cp-app-drive-new-fileupload');
}
if (getNewPadTypes().indexOf(typeFilter) !== -1) {
return typeFilter === obj.type;
}
});
}
return options;
};
var createNewButton = function (isInRoot, $container) {
if (!APP.editable) { return; }
if (!APP.loggedIn) { return; } // Anonymous users can use the + menu in the toolbar
if (!manager.isPathIn(currentPath, [ROOT, 'hrefArray'])) { return; }
// Create dropdown
var options = getNewPadOptions(isInRoot).map(function (obj) {
if (obj.separator) { return { tag: 'hr' }; }
var newObj = {
tag: 'a',
attributes: { 'class': obj.class },
content: [ obj.icon, obj.name ]
};
if (obj.type) {
newObj.attributes['data-type'] = obj.type;
newObj.attributes['href'] = APP.origin + Hash.hashToHref('', obj.type);
}
return newObj;
});
var dropdownConfig = {
buttonContent: [
h('i.fa.fa-plus'),
h('span.cp-button-name', Messages.fm_newButton),
],
buttonCls: 'cp-toolbar-dropdown-nowrap',
options: options,
feedback: 'DRIVE_NEWPAD_LOCALFOLDER',
common: common
};
var $block = UIElements.createDropdown(dropdownConfig);
// Custom style:
$block.find('button').addClass('cp-app-drive-toolbar-new');
addNewPadHandlers($block, isInRoot);
$container.append($block);
};
var createFilterButton = function (isTemplate, $container) {
if (!APP.loggedIn) { return; }
// Create dropdown
var options = [];
if (isInRoot) {
if (APP.store[FILTER_BY]) {
options.push({
tag: 'a',
attributes: {'class': 'cp-app-drive-new-folder pewpew'},
attributes: {
'class': 'cp-app-drive-rm-filter',
},
content: [
$folderIcon.clone()[0],
Messages.fm_folder,
],
});
if (!APP.disableSF && !manager.isInSharedFolder(currentPath)) {
options.push({
tag: 'a',
attributes: {'class': 'cp-app-drive-new-shared-folder'},
content: [
$sharedFolderIcon.clone()[0],
Messages.fm_sharedFolder,
],
});
}
options.push({tag: 'hr'});
options.push({
tag: 'a',
attributes: {'class': 'cp-app-drive-new-fileupload'},
content: [
getIcon('fileupload')[0],
Messages.uploadButton,
],
});
if (APP.allowFolderUpload) {
options.push({
tag: 'a',
attributes: {'class': 'cp-app-drive-new-folderupload'},
content: [
getIcon('folderupload')[0],
Messages.uploadFolderButton,
],
});
}
options.push({tag: 'hr'});
options.push({
tag: 'a',
attributes: {'class': 'cp-app-drive-new-link'},
content: [
getIcon('link')[0],
Messages.fm_link_new,
h('i.fa.fa-times'),
Messages.fm_rmFilter,
],
});
options.push({tag: 'hr'});
}
getNewPadTypes().forEach(function (type) {
var attributes = {
'class': 'cp-app-drive-new-doc',
'class': 'cp-app-drive-filter-doc',
'data-type': type,
'href': '#'
};
@ -3026,21 +3106,72 @@ define([
],
});
});
if (!isTemplate) {
options.push({tag: 'hr'});
options.push({
tag: 'a',
attributes: {
'class': 'cp-app-drive-filter-doc',
'data-type': 'link'
},
content: [
getIcon('link')[0],
Messages.fm_link_type,
],
});
options.push({
tag: 'a',
attributes: {
'class': 'cp-app-drive-filter-doc',
'data-type': 'file',
'href': '#'
},
content: [
getIcon('file')[0],
Messages.type['file'],
],
});
}
var dropdownConfig = {
buttonContent: [
h('span.fa.fa-plus'),
h('span', Messages.fm_newButton),
h('i.fa.fa-filter'),
h('span.cp-button-name', Messages.fm_filterBy),
],
buttonCls: 'cp-toolbar-dropdown-nowrap',
options: options,
feedback: 'DRIVE_NEWPAD_LOCALFOLDER',
feedback: 'DRIVE_FILTERBY',
common: common
};
if (APP.store[FILTER_BY]) {
var type = APP.store[FILTER_BY];
var message = type === 'link' ? Messages.fm_link_type : Messages.type[type];
dropdownConfig.buttonContent.push(
h('span.cp-button-name', ':'),
getIcon(type)[0],
h('span.cp-button-name', message)
);
}
var $block = UIElements.createDropdown(dropdownConfig);
// Custom style:
$block.find('button').addClass('cp-app-drive-toolbar-new');
// Add style
if (APP.store[FILTER_BY]) {
$block.find('button').addClass('cp-toolbar-button-active');
}
addNewPadHandlers($block, isInRoot);
// Add handlers
if (APP.store[FILTER_BY]) {
$block.find('a.cp-app-drive-rm-filter')
.click(function () {
APP.store[FILTER_BY] = undefined;
APP.displayDirectory(currentPath);
});
}
$block.find('a.cp-app-drive-filter-doc')
.click(function () {
var type = $(this).attr('data-type') || 'invalid-filter';
APP.store[FILTER_BY] = type;
APP.displayDirectory([FILTER, type, currentPath]);
});
$container.append($block);
};
@ -3302,65 +3433,38 @@ define([
return keys;
};
var filterPads = function (files, type, path, useId) {
var root = path && manager.find(path);
return files
.filter(function (e) {
return useId ? manager.isFile(e) : (path && manager.isFile(root[e]));
})
.filter(function (e) {
var id = useId ? e : root[e];
var data = manager.getFileData(id);
if (type === 'link') { return data.static; }
var href = data.href || data.roHref;
return href ? (href.split('/')[1] === type) : true;
// if types are unreachable, display files to avoid misleading the user
});
};
// Create the ghost icon to add pads/folders
var createNewPadIcons = function ($block, isInRoot) {
var $container = $('<div>');
if (isInRoot) {
// Folder
var $element1 = $('<li>', {
'class': 'cp-app-drive-new-folder cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend($folderIcon.clone()).appendTo($container);
$element1.append($('<span>', { 'class': 'cp-app-drive-new-name' })
.text(Messages.fm_folder));
// Shared Folder
if (!APP.disableSF && !manager.isInSharedFolder(currentPath)) {
var $element3 = $('<li>', {
'class': 'cp-app-drive-new-shared-folder cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend($sharedFolderIcon.clone()).appendTo($container);
$element3.append($('<span>', { 'class': 'cp-app-drive-new-name' })
.text(Messages.fm_sharedFolder));
}
// Upload file
var $elementFileUpload = $('<li>', {
'class': 'cp-app-drive-new-fileupload cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend(getIcon('fileupload')).appendTo($container);
$elementFileUpload.append($('<span>', {'class': 'cp-app-drive-new-name'})
.text(Messages.uploadButton));
// Upload folder
if (APP.allowFolderUpload) {
var $elementFolderUpload = $('<li>', {
'class': 'cp-app-drive-new-folderupload cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend(getIcon('folderupload')).appendTo($container);
$elementFolderUpload.append($('<span>', {'class': 'cp-app-drive-new-name'})
.text(Messages.uploadFolderButton));
}
// Link
var $elementLink = $('<li>', {
'class': 'cp-app-drive-new-link cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend(getIcon('link')).appendTo($container);
$elementLink.append($('<span>', {'class': 'cp-app-drive-new-name'})
.text(Messages.fm_link_type));
}
// Pads
getNewPadTypes().forEach(function (type) {
var $element = $('<li>', {
'class': 'cp-app-drive-new-doc cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend(getIcon(type)).appendTo($container);
$element.append($('<span>', {'class': 'cp-app-drive-new-name'})
.text(Messages.type[type]));
$element.attr('data-type', type);
getNewPadOptions(isInRoot).forEach(function (obj) {
if (obj.separator) { return; }
var premium = common.checkRestrictedApp(type);
if (premium < 0) {
$element.addClass('cp-app-hidden cp-app-disabled');
} else if (premium === 0) {
$element.addClass('cp-app-disabled');
var $element = $('<li>', {
'class': obj.class + ' cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend(obj.icon).appendTo($container);
$element.append($('<span>', { 'class': 'cp-app-drive-new-name' })
.text(obj.name));
if (obj.type) {
$element.attr('data-type', obj.type);
}
});
@ -3435,7 +3539,7 @@ define([
// Unsorted element are represented by "href" in an array: they don't have a filename
// and they don't hav a hierarchical structure (folder/subfolders)
var displayHrefArray = function ($container, rootName, draggable) {
var displayHrefArray = function ($container, rootName, draggable, typeFilter) {
var unsorted = files[rootName];
if (unsorted.length) {
var $fileHeader = getFileListHeader(true);
@ -3445,6 +3549,7 @@ define([
var sortBy = APP.store[SORT_FILE_BY];
sortBy = sortBy === "" ? sortBy = 'name' : sortBy;
var sortedFiles = sortElements(false, [rootName], keys, sortBy, !getSortFileDesc(), true);
sortedFiles = typeFilter ? filterPads(sortedFiles, typeFilter, false, true) : sortedFiles;
sortedFiles.forEach(function (id) {
var file = manager.getFileData(id);
if (!file) {
@ -3526,7 +3631,7 @@ define([
createGhostIcon($container);
};
var displayTrashRoot = function ($list, $folderHeader, $fileHeader) {
var displayTrashRoot = function ($list, $folderHeader, $fileHeader, typeFilter) {
var filesList = [];
var root = files[TRASH];
var isEmpty = true;
@ -3549,14 +3654,25 @@ define([
isEmpty = false;
});
var sortedFolders = typeFilter ? [] : sortTrashElements(true, filesList, null, !getSortFolderDesc());
var sortedFiles = sortTrashElements(false, filesList, APP.store[SORT_FILE_BY], !getSortFileDesc);
if (typeFilter) {
var ids = sortedFiles.map(function (obj) { return obj.element; });
var idsFilter = filterPads(ids, typeFilter, false, true);
sortedFiles = sortedFiles.filter(function (obj) {
return (idsFilter.indexOf(obj.element) !== -1);
});
// prevent trash emptying while filter is active
isEmpty = true;
}
if (!isEmpty) {
var $empty = createEmptyTrashButton();
$content.append($empty);
}
var sortedFolders = sortTrashElements(true, filesList, null, !getSortFolderDesc());
var sortedFiles = sortTrashElements(false, filesList, APP.store[SORT_FILE_BY], !getSortFileDesc());
if (manager.hasSubfolder(root, true)) { $list.append($folderHeader); }
if (!typeFilter && manager.hasSubfolder(root, true)) { $list.append($folderHeader); }
sortedFolders.forEach(function (f) {
var $element = createElement([TRASH], f.spath, root, true);
$list.append($element);
@ -3728,7 +3844,7 @@ define([
});
};
var displayRecent = function ($list) {
var displayRecent = function ($list, typeFilter) {
var filesList = manager.getRecentPads();
var limit = 20;
@ -3744,6 +3860,14 @@ define([
var i = 0;
var channels = [];
if (typeFilter) {
var ids = filesList.map(function (arr) { return arr[0]; });
var idsFilter = filterPads(ids, typeFilter, false, true);
filesList = filesList.filter(function (arr) {
return (idsFilter.indexOf(arr[0]) !== -1);
});
}
$list.append(h('li.cp-app-drive-element-separator', h('span', Messages.drive_active1Day)));
filesList.some(function (arr) {
var id = arr[0];
@ -3932,6 +4056,17 @@ define([
$content.html("");
sel.$selectBox = $('<div>', {'class': 'cp-app-drive-content-select-box'})
.appendTo($content);
var typeFilter;
var isFilter = path[0] === FILTER;
if (isFilter) {
if (path.length < 3) { return; }
typeFilter = path[1];
path = path[2];
currentPath = path;
} else {
APP.store[FILTER_BY] = undefined;
}
var isInRoot = manager.isPathIn(path, [ROOT]);
var inTrash = manager.isPathIn(path, [TRASH]);
var isTrashRoot = manager.comparePath(path, [TRASH]);
@ -3939,6 +4074,8 @@ define([
var isAllFiles = manager.comparePath(path, [FILES_DATA]);
var isVirtual = virtualCategories.indexOf(path[0]) !== -1;
var isSearch = path[0] === SEARCH;
var isRecent = path[0] === RECENT;
var isOwned = path[0] === OWNED;
var isTags = path[0] === TAGS;
// ANON_SHARED_FOLDER
var isSharedFolder = path[0] === SHARED_FOLDER && APP.newSharedFolder;
@ -4040,6 +4177,9 @@ define([
if (!readOnlyFolder) {
createNewButton(isInRoot, APP.toolbar.$bottomL);
}
if (!isTags && !isSearch) {
createFilterButton(isTemplate, APP.toolbar.$bottomL);
}
if (APP.mobile()) {
var $context = $('<button>', {
@ -4075,16 +4215,16 @@ define([
var $fileHeader = getFileListHeader(true);
if (isTemplate) {
displayHrefArray($list, path[0], true);
displayHrefArray($list, path[0], true, typeFilter);
} else if (isAllFiles) {
displayAllFiles($list);
} else if (isTrashRoot) {
displayTrashRoot($list, $folderHeader, $fileHeader);
displayTrashRoot($list, $folderHeader, $fileHeader, typeFilter);
} else if (isSearch) {
displaySearch($list, path[1]);
} else if (path[0] === RECENT) {
displayRecent($list);
} else if (path[0] === OWNED) {
} else if (isRecent) {
displayRecent($list, typeFilter);
} else if (isOwned) {
displayOwned($list);
} else if (isTags) {
displayTags($list);
@ -4093,11 +4233,12 @@ define([
displaySharedFolder($list);
} else {
if (!inTrash) { $dirContent.contextmenu(openContextMenu('content')); }
if (manager.hasSubfolder(root)) { $list.append($folderHeader); }
if (!isFilter && manager.hasSubfolder(root)) { $list.append($folderHeader); }
// display sub directories
var keys = Object.keys(root);
var sortedFolders = sortElements(true, path, keys, null, !getSortFolderDesc());
var sortedFolders = isFilter ? [] : sortElements(true, path, keys, null, !getSortFolderDesc());
var sortedFiles = sortElements(false, path, keys, APP.store[SORT_FILE_BY], !getSortFileDesc());
sortedFiles = isFilter ? filterPads(sortedFiles, typeFilter, path) : sortedFiles;
sortedFolders.forEach(function (key) {
if (manager.isFile(root[key])) { return; }
var $element = createElement(path, key, root, true);
@ -4149,10 +4290,12 @@ define([
appStatus.ready(true);
};
var displayDirectory = APP.displayDirectory = function (path, force) {
var displayDirectory = APP.displayDirectory = function (path, force, cb) {
cb = cb || function () {};
if (APP.closed || (APP.$content && !$.contains(document.documentElement, APP.$content[0]))) { return; }
if (history.isHistoryMode) {
return void _displayDirectory(path, force);
_displayDirectory(path, force);
return void cb();
}
if (!manager.comparePath(currentPath, path)) {
removeSelected();
@ -4161,6 +4304,7 @@ define([
copyObjectValue(files, proxy.drive);
updateSharedFolders(sframeChan, manager, files, folders, function () {
_displayDirectory(path, force);
cb();
});
});
};
@ -5206,8 +5350,9 @@ define([
};
APP.FM = common.createFileManager(fmConfig);
refresh();
UI.removeLoadingScreen();
refresh(function () {
UI.removeLoadingScreen();
});
/*
if (!APP.team) {

View File

@ -102,6 +102,7 @@ define([
// Send the command
sframeChan.query('Q_SET_PAD_METADATA', {
channel: channel,
channels: otherChan,
command: pending ? 'RM_PENDING_OWNERS' : 'RM_OWNERS',
value: [ed],
teamId: teamOwner
@ -335,11 +336,6 @@ define([
}
}).nThen(function (waitFor) {
var href = data.href;
var hashes = priv.hashes || {};
var bestHash = hashes.editHash || hashes.viewHash || hashes.fileHash;
if (data.fakeHref) {
href = Hash.hashToHref(bestHash, priv.app);
}
sel.forEach(function (el) {
var curve = $(el).attr('data-curve');
if (curve === user.curvePublic) { return; }
@ -928,12 +924,7 @@ define([
}
var href = data.href;
var hashes = priv.hashes || {};
var bestHash = hashes.editHash || hashes.viewHash || hashes.fileHash;
if (data.fakeHref) {
href = Hash.hashToHref(bestHash, priv.app);
}
var isNotStored = Boolean(data.fakeHref);
var isNotStored = Boolean(data.isNotStored);
sframeChan.query(q, {
teamId: typeof(owned) !== "boolean" ? owned : undefined,
href: href,
@ -1055,13 +1046,13 @@ define([
var owned = Modal.isOwned(Env, data);
// Request edit access
if (common.isLoggedIn() && ((data.roHref && !data.href) || data.fakeHref) && !owned && !opts.calendar && priv.app !== 'form') {
if (common.isLoggedIn() && data.roHref && !owned && !opts.calendar && priv.app !== 'form') {
var requestButton = h('button.btn.btn-secondary.no-margin.cp-access-margin-right',
Messages.requestEdit_button);
var requestBlock = h('p', requestButton);
var $requestBlock = $(requestBlock).hide();
content.push(requestBlock);
sframeChan.query('Q_REQUEST_ACCESS', {
sframeChan.query('Q_CONTACT_OWNER', {
send: false,
metadata: data
}, function (err, obj) {
@ -1072,9 +1063,10 @@ define([
$requestBlock.show().find('button').click(function () {
if (spinner.getState()) { return; }
spinner.spin();
sframeChan.query('Q_REQUEST_ACCESS', {
sframeChan.query('Q_CONTACT_OWNER', {
send: true,
metadata: data
metadata: data,
query: "REQUEST_PAD_ACCESS"
}, function (err, obj) {
if (obj && obj.state) {
UI.log(Messages.requestEdit_sent);

View File

@ -42,11 +42,13 @@ define([
if (err || !val) {
if (opts.access) {
data.password = priv.password;
// Access modal and the pad is not stored: we're not an owner
// so we don't need the correct href, just the type
var h = Hash.createRandomHash(priv.app, priv.password);
data.fakeHref = true;
data.href = base + priv.pathname + '#' + h;
// Access modal and the pad is not stored: get the hashes from outer
var hashes = priv.hashes || {};
data.href = Hash.hashToHref(hashes.editHash || hashes.fileHash, priv.app);
if (hashes.viewHash) {
data.roHref = Hash.hashToHref(hashes.viewHash, priv.app);
}
data.isNotStored = true;
} else {
waitFor.abort();
return void cb(err || 'EEMPTY');

View File

@ -37,20 +37,18 @@ define([
}));
}
if (!data.fakeHref) {
if (data.href) {
$('<label>', {'for': 'cp-app-prop-link'}).text(Messages.editShare).appendTo($d);
$d.append(UI.dialog.selectable(data.href, {
id: 'cp-app-prop-link',
}));
}
if (data.href) {
$('<label>', {'for': 'cp-app-prop-link'}).text(Messages.editShare).appendTo($d);
$d.append(UI.dialog.selectable(data.href, {
id: 'cp-app-prop-link',
}));
}
if (data.roHref && !opts.noReadOnly) {
$('<label>', {'for': 'cp-app-prop-rolink'}).text(Messages.viewShare).appendTo($d);
$d.append(UI.dialog.selectable(data.roHref, {
id: 'cp-app-prop-rolink',
}));
}
if (data.roHref && !opts.noReadOnly) {
$('<label>', {'for': 'cp-app-prop-rolink'}).text(Messages.viewShare).appendTo($d);
$d.append(UI.dialog.selectable(data.roHref, {
id: 'cp-app-prop-rolink',
}));
}
if (data.tags && Array.isArray(data.tags)) {

View File

@ -12,7 +12,7 @@ define([
"Asterisk asterisk",
"Brainfuck brainfuck .b",
"C text/x-csrc .c",
"C text/x-c++src .cpp",
"C++ text/x-c++src .cpp",
"C-like clike .c",
"Clojure clojure .clj",
"CMake cmake _", /* no extension */
@ -50,7 +50,6 @@ define([
"HTML htmlmixed .html",
"HTTP http _", /* no extension */
"IDL idl .idl",
"JADE jade .jade",
"Java text/x-java .java",
"JavaScript javascript .js",
"Jinja2 jinja2 .j2",

View File

@ -335,6 +335,28 @@ define([
}
};
handlers['FORM_RESPONSE'] = function(common, data) {
var content = data.content;
var msg = content.msg;
// Display the notification
var title = Util.fixHTML(msg.content.title || Messages.unknownPad);
var href = msg.content.href;
content.getFormatText = function() {
return Messages._getKey('form_responseNotification', [title]);
};
if (href) {
content.handler = function() {
common.openURL(href);
defaultDismiss(common, data)();
};
}
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
handlers['COMMENT_REPLY'] = function(common, data) {
var content = data.content;
var msg = content.msg;
@ -481,6 +503,18 @@ define([
var missed = content.msg.missed;
var start = msg.start;
var title = Util.fixHTML(msg.title);
content.handler = function () {
var priv = common.getMetadataMgr().getPrivateData();
var time = Util.find(data, ['content', 'msg', 'content', 'start']);
if (priv.app === "calendar" && window.APP && window.APP.moveToDate) {
return void window.APP.moveToDate(time);
}
var url = Hash.hashToHref('', 'calendar');
var optsUrl = Hash.getNewPadURL(url, {
time: time
});
common.openURL(optsUrl);
};
content.getFormatText = function () {
var now = +new Date();

View File

@ -659,6 +659,7 @@ define([
offline: store.proxy && store.offline,
teams: teams,
plan: store.ready ? (account.plan || '') : undefined,
mutedChannels: proxy.mutedChannels
}
};
cb(JSON.parse(JSON.stringify(metadata)));
@ -1947,42 +1948,23 @@ define([
}).nThen(cb);
};
// requestPadAccess is used to check if we have a way to contact the owner
// of the pad AND to send the request if we want
// data.send === false ==> check if we can contact them
// data.send === true ==> send the request
Store.requestPadAccess = function (clientId, data, cb) {
// contactPadOwner is used to send "REQUEST_ACCESS" messages
// and to notify form owners when sending a response
Store.contactPadOwner = function (clientId, data, cb) {
var owner = data.owner;
// If the owner was not is the pad metadata, check if it is a friend.
// We'll contact the first owner for whom we know the mailbox
/* // TODO decide whether we want to re-enable this feature for our own contacts
// communicate the exception to users that 'muting' won't apply to friends
check mailbox in our contacts is not compatible with the new "mute pad" feature
var owners = data.owners;
if (!owner && Array.isArray(owners)) {
var friends = store.proxy.friends || {};
// If we have friends, check if an owner is one of them (with a mailbox)
if (Object.keys(friends).filter(function (curve) { return curve !== 'me'; }).length) {
owners.some(function (edPublic) {
return Object.keys(friends).some(function (curve) {
if (curve === "me") { return; }
if (edPublic === friends[curve].edPublic &&
friends[curve].notifications) {
owner = friends[curve];
return true;
}
});
});
}
}
*/
// If send is true, send the request to the owner.
if (owner) {
if (data.send) {
store.mailbox.sendTo('REQUEST_PAD_ACCESS', {
channel: data.channel
var sendTo = function (query, msg, user, _cb) {
if (store.mailbox) {
return store.mailbox.sendTo(query, msg, user, _cb);
}
Mailbox.sendToAnon(store.anon_rpc, query, msg, user, _cb);
};
sendTo(data.query, {
channel: data.channel,
data: data.msgData
}, {
channel: owner.notifications,
curvePublic: owner.curvePublic
@ -2179,6 +2161,13 @@ define([
}
};
Store.deleteMailboxMessage = function (clientId, data, cb) {
if (!store.anon_rpc) { return void cb({error: 'RPC_NOT_READY'}); }
store.anon_rpc.send('DELETE_MAILBOX_MESSAGE', data, function (e) {
cb({error:e});
});
};
// GET_FULL_HISTORY from sframe-common-outer
Store.getFullHistory = function (clientId, data, cb) {
var network = store.network;
@ -2973,6 +2962,9 @@ define([
if (!rt.proxy.uid && store.noDriveUid) {
rt.proxy.uid = store.noDriveUid;
}
if (!rt.proxy.form_seed && data.form_seed) {
rt.proxy.form_seed = data.form_seed;
}
/*
// deprecating localStorage migration as of 4.2.0
var drive = rt.proxy.drive;

View File

@ -4,12 +4,13 @@ define([
'/common/common-constants.js',
'/common/common-realtime.js',
'/common/outer/cache-store.js',
'/calendar/recurrence.js',
'/customize/messages.js',
'/bower_components/nthen/index.js',
'chainpad-listmap',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad/chainpad.dist.js',
], function (Util, Hash, Constants, Realtime, Cache, Messages, nThen, Listmap, Crypto, ChainPad) {
], function (Util, Hash, Constants, Realtime, Cache, Rec, Messages, nThen, Listmap, Crypto, ChainPad) {
var Calendar = {};
var getStore = function (ctx, id) {
@ -90,7 +91,29 @@ define([
});
};
var updateEventReminders = function (ctx, reminders, _ev, useLastVisit) {
var getRecurring = function (ev) {
var mid = new Date();
var start = new Date(mid.getFullYear(), mid.getMonth()-1, 15);
var end = new Date(mid.getFullYear(), mid.getMonth()+1, 15);
var startId = Rec.getMonthId(start);
var midId = Rec.getMonthId(mid);
var endId = Rec.getMonthId(end);
var toAdd = Rec.getRecurring([startId, midId, endId], [ev]);
var all = [ev];
Array.prototype.push.apply(all, toAdd);
return Rec.applyUpdates(all);
};
var clearDismissed = function (ctx, uid) {
var h = Util.find(ctx, ['store', 'proxy', 'hideReminders']) || {};
Object.keys(h).filter(function (id) {
return id.indexOf(uid) === 0;
}).forEach(function (id) {
delete h[id];
});
};
var _updateEventReminders = function (ctx, reminders, _ev, useLastVisit) {
var now = +new Date();
var ev = Util.clone(_ev);
var uid = ev.id;
@ -101,6 +124,10 @@ define([
}
reminders[uid] = [];
if (_ev.deleted) { return; }
var d = Util.find(ctx, ['store', 'proxy', 'hideReminders', uid]) || []; // dismissed
var last = ctx.store.data.lastVisit;
if (ev.isAllDay) {
@ -119,10 +146,11 @@ define([
if (ev.end <= now && !missed) {
// No reminder for past events
delete reminders[uid];
clearDismissed(ctx, uid);
return;
}
var send = function () {
var send = function (d) {
var hide = Util.find(ctx, ['store', 'proxy', 'settings', 'general', 'calendar', 'hideNotif']);
if (hide) { return; }
var ctime = ev.start <= now ? ev.start : +new Date(); // Correct order for past events
@ -133,11 +161,18 @@ define([
missed: Boolean(missed),
content: ev
},
hash: 'REMINDER|'+uid
hash: 'REMINDER|'+uid+'-'+d
}, null, function () {
});
};
var sendNotif = function () { ctx.Store.onReadyEvt.reg(send); };
var sent = false;
var sendNotif = function (delay) {
sent = true;
ctx.Store.onReadyEvt.reg(function () {
send(delay);
});
};
var notifs = ev.reminders || [];
notifs.sort(function (a, b) {
@ -148,6 +183,10 @@ define([
var delay = delayMinutes * 60000;
var time = now + delay;
if (d.some(function (minutes) {
return delayMinutes >= minutes;
})) { return; }
// setTimeout only work with 32bit timeout values. If the event is too far away,
// ignore this event for now
// FIXME: call this function again in xxx days to reload these missing timeout?
@ -156,18 +195,35 @@ define([
// If we're too late to send a notification, send it instantly and ignore
// all notifications that were supposed to be sent even earlier
if (ev.start <= time) {
sendNotif();
sendNotif(delayMinutes);
return true;
}
// It starts in more than "delay": prepare the notification
reminders[uid].push(setTimeout(function () {
sendNotif();
sendNotif(delayMinutes);
}, (ev.start - time)));
});
if (!sent) {
// Remone any existing notification from the UI
ctx.Store.onReadyEvt.reg(function () {
ctx.store.mailbox.hideMessage('reminders', {
hash: 'REMINDER|'+uid
}, null, function () {
});
});
}
};
var updateEventReminders = function (ctx, reminders, ev, useLastVisit) {
var all = getRecurring(Util.clone(ev));
all.forEach(function (_ev) {
_updateEventReminders(ctx, reminders, _ev, useLastVisit);
});
};
var addReminders = function (ctx, id, ev) {
var calendar = ctx.calendars[id];
if (!ev) { return; } // XXX deleted event remote: delete reminders
if (!calendar || !calendar.reminders) { return; }
if (calendar.stores.length === 1 && calendar.stores[0] === 0) { return; }
@ -352,10 +408,20 @@ define([
c.lm = lm;
var proxy = c.proxy = lm.proxy;
var _updateCalled = false;
var _update = function () {
if (_updateCalled) { return; }
_updateCalled = true;
setTimeout(function () {
_updateCalled = false;
update();
});
};
lm.proxy.on('cacheready', function () {
if (!proxy.metadata) { return; }
c.cacheready = true;
setTimeout(update);
_update();
if (cb) { cb(null, lm.proxy); }
addInitialReminders(ctx, channel, cfg.lastVisitNotif);
}).on('ready', function (info) {
@ -372,24 +438,39 @@ define([
title: data.title
};
}
setTimeout(update);
_update();
if (cb) { cb(null, lm.proxy); }
addInitialReminders(ctx, channel, cfg.lastVisitNotif);
}).on('change', [], function () {
if (!c.ready) { return; }
setTimeout(update);
_update();
}).on('change', ['content'], function (o, n, p) {
if (p.length === 2 && n && !o) { // New event
addReminders(ctx, channel, n);
return void addReminders(ctx, channel, n);
}
if (p.length === 2 && !n && o) { // Deleted event
addReminders(ctx, channel, {
return void addReminders(ctx, channel, {
id: p[1],
start: 0
});
}
if (p.length === 3 && n && o && p[2] === 'start') { // Update event start
setTimeout(function () {
if (p.length >= 3 && ['start','reminders','isAllDay'].includes(p[2])) {
// Updated event
return void setTimeout(function () {
addReminders(ctx, channel, proxy.content[p[1]]);
});
}
if (p.length >= 6 && ['start','reminders','isAllDay'].includes(p[5])) {
// Updated recurring event
return void setTimeout(function () {
addReminders(ctx, channel, proxy.content[p[1]]);
});
}
}).on('remove', ['content'], function (x, p) {
_update();
if ((p.length >= 3 && p[2] === 'reminders') ||
(p.length >= 6 && p[5] === 'reminders')) {
return void setTimeout(function () {
addReminders(ctx, channel, proxy.content[p[1]]);
});
}
@ -400,10 +481,10 @@ define([
updateLocalCalendars(ctx, c, md);
}).on('disconnect', function () {
c.offline = true;
setTimeout(update);
_update();
}).on('reconnect', function () {
c.offline = false;
setTimeout(update);
_update();
}).on('error', function (info) {
if (!info || !info.error) { return; }
if (info.error === "EDELETED" ) {
@ -411,7 +492,7 @@ define([
}
if (info.error === "ERESTRICTED" ) {
c.restricted = true;
setTimeout(update);
_update();
}
cb(info);
});
@ -760,8 +841,11 @@ define([
var ev = c.proxy.content[data.ev.id];
if (!ev) { return void cb({error: "EINVAL"}); }
data.rawData = data.rawData || {};
// update the event
var changes = data.changes || {};
var type = data.type || {};
var newC;
if (changes.calendarId) {
@ -770,7 +854,122 @@ define([
newC.proxy.content = newC.proxy.content || {};
}
var RECUPDATE = {
one: {},
from: {}
};
if (['one','from','all'].includes(type.which)) {
ev.recUpdate = ev.recUpdate || RECUPDATE;
if (!ev.recUpdate.one) { ev.recUpdate.one = {}; }
if (!ev.recUpdate.from) { ev.recUpdate.from = {}; }
}
var update = ev.recUpdate;
var alwaysAll = ['calendarId'];
var keys = Object.keys(changes).filter(function (s) {
// we can only change the calendar or recurrence rule on the origin
return !alwaysAll.includes(s);
});
// Delete (future) affected keys
var cleanAfter = function (time) {
[update.from, update.one].forEach(function (obj) {
Object.keys(obj).forEach(function (d) {
if (Number(d) < time) { return; }
delete obj[d];
});
});
};
var cleanKeys = function (obj, when) {
Object.keys(obj).forEach(function (d) {
if (when && Number(d) < when) { return; }
keys.forEach(function (k) {
delete obj[d][k];
});
});
};
// Update recurrence rule. We may create a new event here
var dontSendUpdate = false;
if (typeof(changes.recurrenceRule) !== "undefined") {
if (['one','from'].includes(type.which) && !data.rawData.isOrigin) {
cleanAfter(type.when);
} else {
update = ev.recUpdate = RECUPDATE;
}
}
if (type.which === "one") {
update.one[type.when] = update.one[type.when] || {};
// Nothing to delete
} else if (type.which === "from") {
update.from[type.when] = update.from[type.when] || {};
// Delete all "single/from" updates (affected keys only) after this "from" date
cleanKeys(update.from, type.when);
cleanKeys(update.one, type.when);
} else if (type.which === "all") {
// Delete all "single/from" updates (affected keys only) after
cleanKeys(update.from);
cleanKeys(update.one);
}
if (changes.start && update && (!type.which || type.which === "all")) {
var diff = changes.start - ev.start;
var newOne = {};
var newFrom = {};
Object.keys(update.one || {}).forEach(function (time) {
newOne[Number(time)+diff] = update.one[time];
});
Object.keys(update.from || {}).forEach(function (time) {
newFrom[Number(time)+diff] = update.from[time];
});
update.one = newOne;
update.from = newFrom;
}
// Clear the "dismissed" reminders when the user is updating reminders
var h = Util.find(ctx, ['store', 'proxy', 'hideReminders']) || {};
if (changes.reminders) {
if (type.which === 'one') {
if (!type.when || type.when === ev.start) { delete h[data.ev.id]; }
else { delete h[data.ev.id +'|'+ type.when]; }
} else if (type.which === "from") {
Object.keys(h).filter(function (id) {
return id.indexOf(data.ev.id) === 0;
}).forEach(function (id) {
var time = Number(id.split('|')[1]);
if (!time) { return; }
if (time < type.when) { return; }
delete h[id];
});
} else {
Object.keys(h).filter(function (id) {
return id.indexOf(data.ev.id) === 0;
}).forEach(function (id) {
delete h[id];
});
}
}
// Apply the changes
Object.keys(changes).forEach(function (key) {
if (!alwaysAll.includes(key) && type.which === "one") {
if (key === "recurrenceRule") {
if (data.rawData && data.rawData.isOrigin) {
return (ev[key] = changes[key]);
}
// Always "from", never "one" for recurrence rules
update.from[type.when] = update.from[type.when] || {};
return (update.from[type.when][key] = changes[key]);
}
update.one[type.when][key] = changes[key];
return;
}
if (!alwaysAll.includes(key) && type.which === "from") {
update.from[type.when][key] = changes[key];
return;
}
ev[key] = changes[key];
});
@ -790,6 +989,7 @@ define([
delete c.proxy.content[data.ev.id];
}
nThen(function (waitFor) {
Realtime.whenRealtimeSyncs(c.lm.realtime, waitFor());
if (newC) { Realtime.whenRealtimeSyncs(newC.lm.realtime, waitFor()); }
@ -806,8 +1006,8 @@ define([
addReminders(ctx, id, ev);
}
sendUpdate(ctx, c);
if (newC) { sendUpdate(ctx, newC); }
if (!dontSendUpdate || newC) { sendUpdate(ctx, c); }
if (newC && !dontSendUpdate) { sendUpdate(ctx, newC); }
cb();
});
};
@ -816,7 +1016,22 @@ define([
var c = ctx.calendars[id];
if (!c) { return void cb({error: "ENOENT"}); }
c.proxy.content = c.proxy.content || {};
delete c.proxy.content[data.id];
var evId = data.id.split('|')[0];
if (data.id === evId) {
delete c.proxy.content[data.id];
} else {
var ev = c.proxy.content[evId];
var s = data.raw && data.raw.start;
if (s) {
ev.recUpdate = ev.recUpdate || {
one: {},
from: {}
};
ev.recUpdate.one[s] = {
deleted: true
};
}
}
Realtime.whenRealtimeSyncs(c.lm.realtime, function () {
addReminders(ctx, id, {
id: data.id,
@ -867,6 +1082,20 @@ define([
openChannels(ctx);
}));
ctx.store.proxy.on('change', ['hideReminders'], function (o,n,p) {
var uid = p[1].split('|')[0];
Object.keys(ctx.calendars).some(function (calId) {
var c = ctx.calendars[calId];
if (!c || !c.proxy || !c.proxy.content) { return; }
if (c.proxy.content[uid]) {
setTimeout(function () {
addReminders(ctx, calId, c.proxy.content[uid]);
});
return true;
}
});
});
calendar.closeTeam = function (teamId) {
Object.keys(ctx.calendars).forEach(function (id) {
var ctxCal = ctx.calendars[id];

View File

@ -1,5 +1,5 @@
(function () {
var factory = function (Util, Cred, Nacl) {
var factory = function (Util, Cred, Nacl, Crypto) {
var Invite = {};
var encode64 = Nacl.util.encodeBase64;
@ -49,6 +49,24 @@ var factory = function (Util, Cred, Nacl) {
roster.invite(toInvite, cb);
};
// Invite links should only be visible to members or above, so
// we store them in the roster encrypted with a string only available
// to users with edit rights
var decodeUTF8 = Nacl.util.decodeUTF8;
Invite.encryptHash = function (data, seedStr) {
var array = decodeUTF8(seedStr);
var bytes = Nacl.hash(array);
var cryptKey = bytes.subarray(0, 32);
return Crypto.encrypt(data, cryptKey);
};
Invite.decryptHash = function (encryptedStr, seedStr) {
var array = decodeUTF8(seedStr);
var bytes = Nacl.hash(array);
var cryptKey = bytes.subarray(0, 32);
return Crypto.decrypt(encryptedStr, cryptKey);
};
/* INPUTS
* password (for scrypt)
@ -84,16 +102,17 @@ var factory = function (Util, Cred, Nacl) {
module.exports = factory(
require("../common-util"),
require("../common-credential.js"),
require("nthen"),
require("tweetnacl/nacl-fast")
require("tweetnacl/nacl-fast"),
require("chainpad-crypto/crypto")
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-util.js',
'/common/common-credential.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
], function (Util, Cred) {
return factory(Util, Cred, window.nacl);
], function (Util, Cred, Crypto) {
return factory(Util, Cred, window.nacl, Crypto);
});
}
}());

View File

@ -20,6 +20,10 @@ define([
if (!curvePublic) { return false; }
return Boolean(muted[curvePublic]);
};
var isChannelMuted = function (ctx, channel) {
var muted = ctx.store.proxy.mutedChannels || [];
return muted.includes(channel);
};
// Store the friend request displayed to avoid duplicates
var friendRequest = {};
@ -575,6 +579,61 @@ define([
cb();
};
// Hide duplicates when receiving a form notification:
// Keep only one notification per channel
var formNotifs = {};
handlers['FORM_RESPONSE'] = function (ctx, box, data, cb) {
var msg = data.msg;
var hash = data.hash;
var content = msg.content;
var channel = content.channel;
if (!channel) { return void cb(true); }
if (isChannelMuted(ctx, channel)) { return void cb(true); }
var title, href;
ctx.Store.getAllStores().some(function (s) {
var res = s.manager.findChannel(channel);
// Check if the pad is in our drive
return res.some(function (obj) {
if (!obj.data) { return; }
if (href && !obj.data.href) { return; } // We already have the VIEW url, we need EDIT
href = obj.data.href || obj.data.roHref;
title = obj.data.filename || obj.data.title;
if (obj.data.href) { return true; } // Abort only if we have the EDIT url
});
});
// If we don't have the edit url, ignore this notification
if (!href) { return void cb(true); }
// Add the title
content.href = href;
content.title = title;
// Remove duplicates
var old = formNotifs[channel];
var toRemove = old ? old.data : undefined;
// Update the data
formNotifs[channel] = {
data: {
type: box.type,
hash: hash
}
};
cb(false, toRemove);
};
removeHandlers['FORM_RESPONSE'] = function (ctx, box, data, hash) {
var content = data.content;
var channel = content.channel;
var old = formNotifs[channel];
if (old && old.data && old.data.hash === hash) {
delete formNotifs[channel];
}
};
// Hide duplicates when receiving a SHARE_PAD notification:
// Keep only one notification per channel: the stronger and more recent one
var comments = {};

View File

@ -164,6 +164,22 @@ proxy.mailboxes = {
});
});
};
Mailbox.sendToAnon = function (anonRpc, type, msg, user, cb) {
var Nacl = Crypto.Nacl;
var curveSeed = Nacl.randomBytes(32);
var curvePair = Nacl.box.keyPair.fromSecretKey(new Uint8Array(curveSeed));
var curvePrivate = Nacl.util.encodeBase64(curvePair.secretKey);
var curvePublic = Nacl.util.encodeBase64(curvePair.publicKey);
sendTo({
store: {
anon_rpc: anonRpc,
proxy: {
curvePrivate: curvePrivate,
curvePublic: curvePublic
}
}
}, type, msg, user, cb);
};
// Mark a message as read
var dismiss = function (ctx, data, cId, cb) {
@ -177,6 +193,15 @@ proxy.mailboxes = {
hideMessage(ctx, type, hash, ctx.clients.filter(function (clientId) {
return clientId !== cId;
}));
var uid = hash.slice(9).split('-')[0];
var d = Util.find(ctx, ['store', 'proxy', 'hideReminders', uid]);
if (!d) {
var h = ctx.store.proxy.hideReminders = ctx.store.proxy.hideReminders || {};
d = h[uid] = h[uid] || [];
}
var delay = hash.split('-')[1];
if (delay && !d.includes(delay)) { d.push(Number(delay)); }
return;
}
@ -590,6 +615,9 @@ proxy.mailboxes = {
});
};
mailbox.hideMessage = function (type, msg) {
hideMessage(ctx, type, msg.hash, ctx.clients);
};
mailbox.showMessage = function (type, msg, cId, cb) {
if (type === "reminders" && msg) {
ctx.boxes.reminders.content[msg.hash] = msg.msg;

View File

@ -463,9 +463,22 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto, Feedback)
if (typeof(members[curve]) !== 'undefined') { throw new Error("MEMBER_ALREADY_PRESENT"); }
// copy the new profile from the old one
members[curve] = Util.clone(members[author]);
// and erase the old one
delete members[author];
var clone = Util.clone(members[author]);
delete clone.remaining;
delete clone.totalUses;
delete clone.inviteChannel;
delete clone.previewChannel;
members[curve] = clone;
// XXX
var remaining = members[author].remaining || 1;
if (remaining === -1) { return true; } // Infinite uses, keep the link
if (remaining > 1) { // Remove 1 use
members[author].remaining = remaining - 1;
} else { // Disable link
delete members[author];
}
return true;
};

View File

@ -79,7 +79,7 @@ define([
GET_HISTORY: Store.getHistory,
GET_HISTORY_RANGE: Store.getHistoryRange,
IS_NEW_CHANNEL: Store.isNewChannel,
REQUEST_PAD_ACCESS: Store.requestPadAccess,
CONTACT_PAD_OWNER: Store.contactPadOwner,
GIVE_PAD_ACCESS: Store.givePadAccess,
BURN_PAD: Store.burnPad,
GET_PAD_METADATA: Store.getPadMetadata,
@ -88,6 +88,7 @@ define([
GET_LAST_HASH: Store.getLastHash,
GET_SNAPSHOT: Store.getSnapshot,
CORRUPTED_CACHE: Store.corruptedCache,
DELETE_MAILBOX_MESSAGE: Store.deleteMailboxMessage,
// Drive
DRIVE_USEROBJECT: Store.userObjectCommand,
// Settings,

View File

@ -1038,6 +1038,17 @@ define([
});
}
// Decrypt hash for invite links
Object.keys(members).forEach(function (curve) {
var member = members[curve];
if (!member.inviteChannel) { return; }
if (!member.hash) { return; }
if (!teamData.hash) { delete member.hash; return; }
try {
member.hash = Invite.decryptHash(member.hash, teamData.hash);
} catch (e) { console.error(e); }
});
cb(members);
});
};
@ -1580,10 +1591,12 @@ define([
var message = data.message;
var name = data.name;
/*
var password = data.password;
//var password = data.password;
var hash = data.hash;
*/
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
try {
var encryptedHash = Invite.encryptHash(hash, teamData.hash);
} catch (e) { console.error(e); }
// derive { channel, cryptKey} for the preview content channel
var previewKeys = Invite.derivePreviewKeys(seeds.preview);
@ -1595,6 +1608,10 @@ define([
// and a placeholder in the roster
var ephemeralKeys = Invite.generateKeys();
// Initial role of the invited users
var role = data.role || "VIEWER";
var uses = data.uses || 1;
nThen(function (w) {
@ -1652,9 +1669,12 @@ define([
};
putOpts.metadata.validateKey = sign.validateKey;
// available only with the link and the content
var inviteContent = {
teamData: getInviteData(ctx, teamId, false),
teamData: getInviteData(ctx, teamId, role === "MEMBER"),
ephemeral: {
edPublic: ephemeralKeys.edPublic,
edPrivate: ephemeralKeys.edPrivate,
@ -1692,6 +1712,10 @@ define([
curvePublic: ephemeralKeys.curvePublic,
displayName: data.name,
pending: true,
remaining: uses,
totalUses: uses,
role: role,
hash: encryptedHash,
inviteChannel: inviteKeys.channel,
previewChannel: previewKeys.channel,
}
@ -1821,20 +1845,22 @@ define([
}));
}).nThen(function () {
var tempRpc = {};
initRpc(ctx, tempRpc, inviteContent.ephemeral, function (err) {
if (err) { return; }
var rpc = tempRpc.rpc;
if (rosterState.inviteChannel) {
rpc.removeOwnedChannel(rosterState.inviteChannel, function (err) {
if (err) { console.error(err); }
});
}
if (rosterState.previewChannel) {
rpc.removeOwnedChannel(rosterState.previewChannel, function (err) {
if (err) { console.error(err); }
});
}
});
if (!rosterState.remaining || rosterState.remaining === 1) {
initRpc(ctx, tempRpc, inviteContent.ephemeral, function (err) {
if (err) { return; }
var rpc = tempRpc.rpc;
if (rosterState.inviteChannel) {
rpc.removeOwnedChannel(rosterState.inviteChannel, function (err) {
if (err) { console.error(err); }
});
}
if (rosterState.previewChannel) {
rpc.removeOwnedChannel(rosterState.previewChannel, function (err) {
if (err) { console.error(err); }
});
}
});
}
// Add the team to our list and join...
joinTeam(ctx, {
team: inviteContent.teamData

View File

@ -249,7 +249,6 @@ var factory = function (Util, Rpc) {
}, cb);
};
cb(e, exp);
});
};

View File

@ -20,7 +20,9 @@ define([
'netflux-client': '/bower_components/netflux-websocket/netflux-client',
'chainpad-netflux': '/bower_components/chainpad-netflux/chainpad-netflux',
'chainpad-listmap': '/bower_components/chainpad-listmap/chainpad-listmap',
'cm-extra': '/lib/codemirror-extra-modes'
'cm-extra': '/lib/codemirror-extra-modes',
// asciidoctor same
'asciidoctor': '/lib/asciidoctor/asciidoctor.min'
},
map: {
'*': {

View File

@ -80,7 +80,7 @@ define([
try {
var val = JSON.parse(states[idx].getContent().doc);
var md = config.extractMetadata(val);
var users = Object.keys(md.users).sort();
var users = Object.keys(md.users || {}).sort();
return users.join();
} catch (e) {
console.error(e);

View File

@ -13,6 +13,7 @@ define([
Mailbox.create = function (Common) {
var mailbox = Common.mailbox;
var sframeChan = Common.getSframeChannel();
var priv = Common.getMetadataMgr().getPrivateData();
var execCommand = function (cmd, data, cb) {
sframeChan.query('Q_MAILBOX_COMMAND', {
@ -67,6 +68,14 @@ define([
}
} else if (data.type === 'reminders') {
avatar = h('i.fa.fa-calendar.cp-broadcast.preview');
if (priv.app !== 'calendar') { avatar.classList.add('cp-reminder'); }
$(avatar).click(function (e) {
e.stopPropagation();
if (data.content && data.content.handler) {
return void data.content.handler();
}
Common.openURL(Hash.hashToHref('', 'calendar'));
});
} else if (userData && typeof(userData) === "object" && userData.profile) {
avatar = h('span.cp-avatar');
Common.displayAvatar($(avatar), userData.avatar, userData.displayName || userData.name);
@ -120,7 +129,8 @@ define([
onViewedHandlers.push(function (data) {
var hash = data.hash.replace(/"/g, '\\\"');
var $notif = $('.cp-notification[data-hash="'+hash+'"]:not(.cp-app-notification-archived)');
if (/^REMINDER\|/.test(hash)) { hash = hash.split('-')[0]; }
var $notif = $('.cp-notification[data-hash^="'+hash+'"]:not(.cp-app-notification-archived)');
if ($notif.length) {
$notif.remove();
}

View File

@ -710,6 +710,12 @@ define([
additionalPriv.registeredOnly = true;
}
if (metaObj.priv && Array.isArray(metaObj.priv.mutedChannels)
&& metaObj.priv.mutedChannels.includes(secret.channel)) {
delete metaObj.priv.mutedChannes;
additionalPriv.isChannelMuted = true;
}
var priv = metaObj.priv;
var _plan = typeof(priv.plan) === "undefined" ? Utils.LocalStore.getPremium() : priv.plan;
var p = Utils.Util.checkRestrictedApp(parsed.type, AppConfig,
@ -942,7 +948,7 @@ define([
var metadata = data.metadata;
var add = data.add;
var _secret = secret;
if (metadata && (metadata.href || metadata.roHref) && !metadata.fakeHref) {
if (metadata && (metadata.href || metadata.roHref)) {
var _parsed = Utils.Hash.parsePadUrl(metadata.href || metadata.roHref);
_secret = Utils.Hash.getSecrets(_parsed.type, _parsed.hash, metadata.password);
}
@ -1010,9 +1016,9 @@ define([
});
});
// REQUEST_ACCESS is used both to check IF we can contact an owner (send === false)
// CONTACT_OWNER is used both to check IF we can contact an owner (send === false)
// AND also to send the request if we want (send === true)
sframeChan.on('Q_REQUEST_ACCESS', function (data, cb) {
sframeChan.on('Q_CONTACT_OWNER', function (data, cb) {
if (readOnly && hashes.editHash) {
return void cb({error: 'ALREADYKNOWN'});
}
@ -1030,8 +1036,6 @@ define([
var crypto = Crypto.createEncryptor(_secret.keys);
nThen(function (waitFor) {
// Try to get the owner's mailbox from the pad metadata first.
// If it's is an older owned pad, check if the owner is a friend
// or an acquaintance (from async-store directly in requestAccess)
var todo = function (obj) {
owners = obj.owners;
@ -1065,11 +1069,12 @@ define([
}));
}).nThen(function () {
// If we are just checking (send === false) and there is a mailbox field, cb state true
// If there is no mailbox, we'll have to check if an owner is a friend in the worker
if (!send) { return void cb({state: Boolean(owner)}); }
Cryptpad.padRpc.requestAccess({
Cryptpad.padRpc.contactOwner({
send: send,
query: data.query,
msgData: data.msgData,
channel: _secret.channel,
owner: owner,
owners: owners

View File

@ -663,49 +663,6 @@ MessengerUI, Messages, Pages) {
return $shareBlock;
};
/*
var createRequest = function (toolbar, config) {
if (!config.metadataMgr) {
throw new Error("You must provide a `metadataMgr` to display the request access button");
}
// We can only requets more access if we're in read-only mode
if (config.readOnly !== 1) { return; }
var $requestBlock = $('<button>', {
'class': 'fa fa-lock cp-toolbar-share-button',
title: Messages.requestEdit_button
}).hide();
// If we have access to the owner's mailbox, display the button and enable it
// false => check if we can contact the owner
// true ==> send the request
Common.getSframeChannel().query('Q_REQUEST_ACCESS', {send:false}, function (err, obj) {
if (obj && obj.state) {
var locked = false;
$requestBlock.show().click(function () {
if (locked) { return; }
locked = true;
Common.getSframeChannel().query('Q_REQUEST_ACCESS', {send:true}, function (err, obj) {
if (obj && obj.state) {
UI.log(Messages.requestEdit_sent);
$requestBlock.hide();
} else {
locked = false;
}
});
});
}
});
toolbar.$leftside.append($requestBlock);
toolbar.request = $requestBlock;
return $requestBlock;
};
*/
var createTitle = function (toolbar, config) {
var $titleContainer = $('<span>', {
'class': TITLE_CLS

View File

@ -293,7 +293,7 @@ define([
};
Messages.convertPage = "Convert"; // XXX 4.11.0
Messages.convert_hint = "Pick the file you want to convert. The list of output format will be visible afterward."; // XXX 4.11.0
Messages.convert_hint = "Pick the file you want to convert. The list of output format will be visible afterwards."; // XXX 4.11.0
var createToolbar = function () {
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications'];

View File

@ -193,28 +193,20 @@
display: flex;
justify-content: space-between;
flex-wrap: wrap;
.cp-form-settings-preview {
min-width: 260px;
margin-right: 20px;
}
.cp-form-color-theme-container {
max-width: 300px;
}
& > div {
flex-basis: 33.333333%;
flex: 1;
padding-right: 20px;
}
}
}
}
@media screen and (max-width: 600px) and (min-width: 400px) {
.cp-form-creator-settings {
& > div {
flex-basis: 50% !important;
}
}
}
@media screen and (max-width: 400px) {
.cp-form-creator-settings {
& > div {
flex-basis: 100% !important;
}
}
}
.cp-form-creator-settings {
.cp-form-actions {
margin-top: 5px;
@ -230,6 +222,18 @@
}
}
div.cp-form-settings-preview {
background: @cp_form-bg1;
padding: 10px;
border-radius: @variables_radius_L;
& > *:not(:last-child) {
margin-bottom: 10px;
}
.cp-form-setting-title {
color: @cryptpad_color_link;
}
}
.cp-form-color-container {
& > div {
display: flex;
@ -251,12 +255,6 @@
}
}
}
.cp-form-response-msg-container button {
white-space: initial;
line-height: 25px;
padding: 5.5px 6px;
}
}
div.cp-form-filler-container {
width: 300px;
@ -276,6 +274,7 @@
}
}
div.cp-form-creator-content {
position: relative;
.cp-form-block-type {
margin-top: -35px;
&.editable {
@ -404,7 +403,7 @@
display: flex;
flex-flow: column;
margin: 20px 0px 0px 0px;
padding-bottom: 100px;
padding-bottom: 75px;
&> div:first-child {
display: flex;
height: 100%;
@ -449,6 +448,17 @@
}
}
}
.cp-form-response-msg-container {
padding-bottom: 75px;
button {
white-space: initial;
line-height: 25px;
padding: 5.5px 6px;
}
.cp-form-response-msg-hint {
color: @cryptpad_color_link;
}
}
.cp-form-send-container {
text-align: center;
@ -666,7 +676,7 @@
}
}
&.editable {
cursor: grab;
&:not(.nodrag) { cursor: grab; }
.cp-form-edit-save {
margin-top: 20px;
button {
@ -764,8 +774,13 @@
justify-content: center;
flex: 1;
flex-flow: column;
div.cp-form-submit-table {
display: grid;
grid-gap: 10px;
margin: 10px 0;
}
.cp-form-submit-actions {
button:not(:last-child) {
span:not(:last-child) {
margin-right: 10px;
}
}
@ -833,6 +848,10 @@
background: @cp_form-bg2;
}
.cp-form-results-delete {
vertical-align: top;
}
.cp-form-results-type-multiradio-data {
.cp-mr-q {
font-weight: bold;
@ -915,11 +934,27 @@
display: flex;
flex-flow: column;
align-items: baseline;
.cp-radio {
.cp-radio, .cp-checkmark {
display: inline-flex;
max-width: 100%;
}
.cp-checkmark-label {
word-break: break-word;
}
}
.cp-form-multiradio-container {
overflow: auto;
}
.cp-form-type-multiradio {
.cp-form-multiradio-header {
white-space: nowrap;
span {
text-align: center;
max-width: 30ch;
overflow: hidden;
text-overflow: ellipsis;
}
}
display: table;
& > * {
display: table-row;
@ -928,9 +963,25 @@
padding: 5px 20px;
vertical-align: middle;
&:first-child {
min-width: 200px;
//overflow: auto;
max-width: 300px;
min-width: 300px;
width: 300px;
position: absolute;
left: 20px;
word-break: break-word;
background: @cp_form-bg1;
z-index: 2;
}
.cp-radio-mark {
&:nth-child(2) {
visibility: hidden;
//overflow: auto;
max-width: 300px;
min-width: 300px;
width: 300px;
word-break: break-word;
}
.cp-radio-mark, .cp-checkmark-mark {
margin: auto;
}
}
@ -942,8 +993,11 @@
.cp-form-type-sort {
cursor: grab;
padding: 5px;
display: flex;
align-items: center;
word-break: break-word;
.cp-form-handle {
margin-right: 5px;
min-width: 12px;
}
.cp-form-sort-order {
border: 1px solid @cryptpad_text_col;
@ -962,22 +1016,24 @@
.cp-form-type-poll-container {
overflow: auto;
.cp-form-poll-hint {
margin-bottom: 10px;
position: absolute;
}
}
.cp-form-type-poll {
margin-top: 32px; // cp-form-poll-hint is "absolute"
display: inline-flex;
flex-flow: column;
width: 100%;
& > div {
display: flex;
}
.cp-poll-total {
display: flex;
width: 100%;
}
.cp-form-poll-body {
flex-flow: column;
max-height: 225px;
overflow: auto;
& > div {
display: flex;
@ -985,15 +1041,15 @@
}
.cp-poll-cell {
width: 100px;
height: 35px;
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-top:5px;
margin-left:5px;
&:first-child {
width: 200px;
}
flex-shrink: 0;
flex-grow: 1;
word-break: break-word;
button {
width: 100%;
border-top: 0px;
@ -1039,6 +1095,27 @@
.cp-poll-time-day {
flex-basis: 100px;
border-bottom: 1px solid @cryptpad_text_col;
&:first-child {
border-color: @cp_form-bg1 !important;
}
}
.cp-form-poll-option, .cp-poll-time-day {
span {
text-overflow: ellipsis;
max-width: 100%;
overflow: hidden;
line-height: 1.2;
hyphens: auto;
text-align: center;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
.cp-poll-switch {
button.btn {
border-radius: 0px;
}
}
&:not(.cp-form-poll-switch) {
& > div {
@ -1046,13 +1123,25 @@
margin-bottom: 5px;
}
}
.cp-form-poll-body {
&::-webkit-scrollbar {
display: none;
.cp-poll-cell {
&:first-child {
min-width: 200px;
background: #424242;
position: absolute;
margin-left: 0;
background: @cp_form-bg1;
}
&:nth-child(2) {
margin-left: 205px;
}
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.cp-form-poll-option, .cp-poll-time-day, .cp-form-poll-choice {
flex-grow: 1;
flex-shrink: 0;
}
.cp-form-poll-option, .cp-poll-time-day {
flex-flow: column;
text-align: center;
@ -1074,9 +1163,6 @@
flex-flow: column;
&.cp-form-poll-body {
flex-flow: row;
max-width: 550px;
max-height: unset;
scroll-snap-type: x mandatory;
& > div {
flex-flow: column;
}
@ -1091,12 +1177,18 @@
}
}
.cp-form-poll-option, .cp-poll-switch {
span {
max-height: 100%;
}
width: 200px;
.cp-form-weekday-separator {
margin-right: 5px;
margin-left: 5px;
}
}
div.cp-poll-time-day-container {
width: auto !important;
}
.cp-poll-time-day {
flex-basis: 40px;
border-right: none;
@ -1145,6 +1237,34 @@
}
}
#cp-form-settings {
.cp-modal {
text-align: left;
width: 500px;
padding: 24px;
h2 {
font-size: 20px;
line-height: 40px;
margin-top: -12px;
i {
margin-right: 5px;
}
}
& > *:not(h2) {
color: @cryptpad_text_col;
}
& > div:not(:last-child) {
margin-bottom: 10px;
}
.cp-form-setting-title {
color: @cryptpad_color_link;
}
}
}
& > .flatpickr-calendar {
z-index: 100001;
}
.charts_main();
}

View File

@ -13,8 +13,74 @@ define([
value += '"' + vv + '"';
return value;
};
Export.results = function (content, answers, TYPES, order, isArray) {
var exportJSON = function (content, answers, TYPES, order) {
var form = content.form;
var res = {
questions: {},
responses: []
};
var q = res.questions;
var r = res.responses;
// Add questions
var i = 1;
order.forEach(function (key) {
var obj = form[key];
if (!obj) { return; }
var type = obj.type;
if (!TYPES[type]) { return; } // Ignore static types
var id = `q${i++}`;
if (TYPES[type] && TYPES[type].exportCSV) {
var _obj = Util.clone(obj);
_obj.q = "tmp";
q[id] = {
question: obj.q,
items: TYPES[type].exportCSV(false, _obj).map(function (str) {
return str.slice(6); // Remove "tmp | "
})
};
} else {
q[id] = obj.q || Messages.form_default;
}
});
Object.keys(answers || {}).forEach(function (key) {
var userObj = answers[key];
Object.keys(userObj).forEach(function (k) {
var obj = userObj[k];
var time = new Date(obj.time).toISOString();
var msg = obj.msg || {};
var user = msg._userdata || {};
var data = {
'_time': time,
'_name': user.name || Messages.anonymous
};
var i = 1;
order.forEach(function (key) {
if (!form[key]) { return; }
var type = form[key].type;
if (!TYPES[type]) { return; } // Ignore static types
var id = `q${i++}`;
if (TYPES[type].exportCSV) {
data[id] = TYPES[type].exportCSV(msg[key], form[key]);
return;
}
data[id] = msg[key];
});
r.push(data);
});
});
return JSON.stringify(res, 0, 2);
};
Export.results = function (content, answers, TYPES, order, format) {
if (!content || !content.form) { return; }
if (format === "json") { return exportJSON(content, answers, TYPES, order); }
var isArray = format === "array";
var csv = "";
var array = [];
var form = content.form;
@ -39,29 +105,32 @@ define([
array.push(questions);
Object.keys(answers || {}).forEach(function (key) {
var obj = answers[key];
csv += '\n';
var time = new Date(obj.time).toISOString();
var msg = obj.msg || {};
var user = msg._userdata || {};
var line = [];
line.push(time);
line.push(user.name || Messages.anonymous);
order.forEach(function (key) {
var type = form[key].type;
if (!TYPES[type]) { return; } // Ignore static types
if (TYPES[type].exportCSV) {
var res = TYPES[type].exportCSV(msg[key], form[key]);
Array.prototype.push.apply(line, res);
return;
}
line.push(String(msg[key] || ''));
var _obj = answers[key];
Object.keys(_obj).forEach(function (uid) {
var obj = _obj[uid];
csv += '\n';
var time = new Date(obj.time).toISOString();
var msg = obj.msg || {};
var user = msg._userdata || {};
var line = [];
line.push(time);
line.push(user.name || Messages.anonymous);
order.forEach(function (key) {
var type = form[key].type;
if (!TYPES[type]) { return; } // Ignore static types
if (TYPES[type].exportCSV) {
var res = TYPES[type].exportCSV(msg[key], form[key]);
Array.prototype.push.apply(line, res);
return;
}
line.push(String(msg[key] || ''));
});
line.forEach(function (v, i) {
if (i) { csv += ','; }
csv += escapeCSV(v);
});
array.push(line);
});
line.forEach(function (v, i) {
if (i) { csv += ','; }
csv += escapeCSV(v);
});
array.push(line);
});
if (isArray) { return array; }
return csv;

File diff suppressed because it is too large Load Diff

View File

@ -44,6 +44,7 @@ define([
var addRpc = function (sframeChan, Cryptpad, Utils) {
sframeChan.on('EV_FORM_PIN', function (data) {
channels.answersChannel = data.channel;
Cryptpad.changeMetadata();
Cryptpad.getPadAttribute('answersChannel', function (err, res) {
// If already stored, don't pin it again
if (res && res === data.channel) { return; }
@ -122,6 +123,8 @@ define([
return false;
}
};
var deleteLines = false; // "false" to support old forms
sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, _cb) {
var cb = Utils.Util.once(_cb);
var myKeys = {};
@ -160,6 +163,9 @@ define([
Cryptpad.makeNetwork(w(function (err, nw) {
network = nw;
}));
Cryptpad.getPadMetadata({channel: data.channel}, w(function (md) {
if (md && md.deleteLines) { deleteLines = true; }
}));
}).nThen(function () {
if (!network) { return void cb({error: "E_CONNECT"}); }
@ -183,6 +189,9 @@ define([
validateKey: keys.secondaryValidateKey,
owners: [myKeys.edPublic],
crypto: crypto,
metadata: {
deleteLines: true
}
//Cache: Utils.Cache // TODO enable cache for form responses when the cache stops evicting old answers
};
var results = {};
@ -198,7 +207,6 @@ define([
nThen(function (waitFor) {
accessKeys.forEach(function (obj) {
Pinpad.create(network, obj, waitFor(function (e) {
console.log('done', obj);
if (e) { console.error(e); }
}));
});
@ -222,14 +230,26 @@ define([
config.onMessage = function (msg, peer, vKey, isCp, hash, senderCurve, cfg) {
var parsed = Utils.Util.tryParse(msg);
if (!parsed) { return; }
var uid = parsed._uid || '000';
// If we have a "non-anonymous" answer, it may be the edition of a
// previous anonymous answer. Check if a previous anonymous answer exists
// with the same uid and delete it.
if (parsed._proof) {
var check = checkAnonProof(parsed._proof, data.channel, curvePrivate);
if (check) {
delete results[parsed._proof.key];
var theirAnonKey = parsed._proof.key;
if (check && results[theirAnonKey] && results[theirAnonKey][uid]) {
delete results[theirAnonKey][uid];
}
}
if (data.cantEdit && results[senderCurve]) { return; }
results[senderCurve] = {
parsed._time = cfg && cfg.time;
if (deleteLines) { parsed._hash = hash; }
if (data.cantEdit && results[senderCurve]
&& results[senderCurve][uid]) { return; }
results[senderCurve] = results[senderCurve] || {};
results[senderCurve][uid] = {
msg: parsed,
hash: hash,
time: cfg && cfg.time
@ -239,7 +259,7 @@ define([
});
});
sframeChan.on("Q_FETCH_MY_ANSWERS", function (data, cb) {
var answer;
var answers = [];
var myKeys;
nThen(function (w) {
Cryptpad.getFormKeys(w(function (keys) {
@ -259,38 +279,68 @@ define([
w.abort();
return void cb(obj);
}
answer = obj;
// Get the latest edit per uid
var temp = {};
obj.forEach(function (ans) {
var uid = ans.uid || '000';
temp[uid] = ans;
});
answers = Object.values(temp);
}));
Cryptpad.getPadMetadata({channel: data.channel}, w(function (md) {
if (md && md.deleteLines) { deleteLines = true; }
}));
}).nThen(function () {
if (answer.anonymous) {
if (!myKeys.formSeed) { return void cb({ error: "ANONYMOUS_ERROR" }); }
myKeys = getAnonymousKeys(myKeys.formSeed, data.channel);
}
Cryptpad.getHistoryRange({
channel: data.channel,
lastKnownHash: answer.hash,
toHash: answer.hash,
}, function (obj) {
if (obj && obj.error) { return void cb(obj); }
var messages = obj.messages;
if (!messages.length) { return void cb(); }
if (obj.lastKnownHash !== answer.hash) { return void cb(); }
try {
var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, {
validateKey: data.validateKey,
ephemeral_private: Nacl.util.decodeBase64(answer.curvePrivate),
my_private: Nacl.util.decodeBase64(myKeys.curvePrivate),
their_public: Nacl.util.decodeBase64(data.publicKey)
});
var parsed = JSON.parse(res.content);
parsed._isAnon = answer.anonymous;
parsed._time = messages[0].time;
cb(parsed);
} catch (e) {
cb({error: e});
}
var n = nThen;
var err;
var all = {};
answers.forEach(function (answer) {
n = n(function(waitFor) {
var finalKeys = myKeys;
if (answer.anonymous) {
if (!myKeys.formSeed) {
err = 'ANONYMOUS_ERROR';
console.error('ANONYMOUS_ERROR', answer);
return;
}
finalKeys = getAnonymousKeys(myKeys.formSeed, data.channel);
}
Cryptpad.getHistoryRange({
channel: data.channel,
lastKnownHash: answer.hash,
toHash: answer.hash,
}, waitFor(function (obj) {
if (obj && obj.error) { err = obj.error; return; }
var messages = obj.messages;
if (!messages.length) {
// XXX TODO delete from drive.forms
return;
}
if (obj.lastKnownHash !== answer.hash) { return; }
try {
var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, {
validateKey: data.validateKey,
ephemeral_private: Nacl.util.decodeBase64(answer.curvePrivate),
my_private: Nacl.util.decodeBase64(finalKeys.curvePrivate),
their_public: Nacl.util.decodeBase64(data.publicKey)
});
var parsed = JSON.parse(res.content);
parsed._isAnon = answer.anonymous;
parsed._time = messages[0].time;
if (deleteLines) { parsed._hash = answer.hash; }
var uid = parsed._uid || '000';
if (all[uid] && !all[uid]._isAnon) { parsed._isAnon = false; }
all[uid] = parsed;
} catch (e) {
err = e;
}
}));
}).nThen;
});
n(function () {
if (err) { return void cb({error: err}); }
cb(all);
});
});
});
@ -304,11 +354,6 @@ define([
// We can create a seed in localStorage.
if (!keys.formSeed) {
// No drive mode
var answered = JSON.parse(localStorage.CP_formAnswered || "[]");
if(answered.indexOf(data.channel) !== -1) {
// Already answered: abort
return void cb({ error: "EANSWERED" });
}
keys = { formSeed: noDriveSeed };
}
myKeys = keys;
@ -334,6 +379,8 @@ define([
}
var crypto = Utils.Crypto.Mailbox.createEncryptor(myKeys);
var uid = data.results._uid || Utils.Util.uid();
data.results._uid = uid;
var text = JSON.stringify(data.results);
var ciphertext = crypto.encrypt(text, box.publicKey);
@ -343,15 +390,39 @@ define([
ciphertext
], function (err, response) {
Cryptpad.storeFormAnswer({
uid: uid,
channel: box.channel,
hash: hash,
curvePrivate: ephemeral_private,
anonymous: Boolean(data.anonymous)
}, function () {
var res = data.results;
res._isAnon = data.anonymous;
res._time = +new Date();
if (deleteLines) { res._hash = hash; }
cb({
error: err,
response: response,
results: res
});
});
cb({error: err, response: response, hash: hash});
});
});
});
sframeChan.on("Q_FORM_DELETE_ALL_ANSWERS", function (data, cb) {
if (!data || !data.channel) { return void cb({error: 'EINVAL'}); }
Cryptpad.clearOwnedChannel(data.channel, cb);
});
sframeChan.on("Q_FORM_DELETE_ANSWER", function (data, cb) {
if (!deleteLines) {
return void cb({error: 'EFORBIDDEN'});
}
Cryptpad.deleteFormAnswers(data, cb);
});
sframeChan.on("Q_FORM_MUTE", function (data, cb) {
if (!Utils.secret) { return void cb({error: 'EINVAL'}); }
Cryptpad.muteChannel(Utils.secret.channel, data.muted, cb);
});
};
SFCommonO.start({
addData: addData,

1476
www/lib/asciidoctor/asciidoctor.min.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -43,7 +43,14 @@ define([
};
var getEndDate = function () {
setTimeout(function () { $(endPickr.calendarContainer).remove(); });
return endPickr.parseDate(e.value);
var d = endPickr.parseDate(e.value);
if (endPickr.config.dateFormat === "Y-m-d") { // All day event
// Tui-calendar will remove 1s (1000ms) to the date for an unknown reason...
d.setMilliseconds(1000);
}
return d;
};
return {

View File

@ -12,4 +12,5 @@ This file is intended to be used as a log of what third-party source we have ven
* [Fabricjs 4.6.0](https://github.com/fabricjs/fabric.js) and [Fabric-history](https://github.com/lyzerk/fabric-history) for the whiteboard app
* [Requirejs optional module plugin](https://stackoverflow.com/a/27422370)
* [asciidoc.js 2.0.0](https://github.com/asciidoctor/codemirror-asciidoc/releases/tag/2.0.0) with slight changes to match the format of other codemirror modes
* [Asciidoctor.js 2.2.6](https://github.com/asciidoctor/asciidoctor.js/releases/tag/v2.2.6) for AsciiDoc rendering

View File

@ -285,6 +285,22 @@
}
}
.cp-teams-invite-uses { // XXX
input {
margin-bottom: 0px !important;
margin-right: 10px;
width: 75px !important;
}
}
.cp-teams-invite-role {
margin-bottom: 15px;
span:first-child {
margin-right: 10px;
}
}
#cp-teams-roster-dialog {
table {
width: 100%;

View File

@ -17,6 +17,7 @@ define([
'/customize/application_config.js',
'/common/messenger-ui.js',
'/common/inner/invitation.js',
'/common/clipboard.js',
'/common/make-backup.js',
'/customize/messages.js',
@ -43,6 +44,7 @@ define([
AppConfig,
MessengerUI,
InviteInner,
Clipboard,
Backup,
Messages)
{
@ -714,6 +716,18 @@ define([
title: Messages.team_pendingOwnerTitle
}, ' ' + Messages.team_pendingOwner));
}
if (data.pending && data.inviteChannel && data.remaining === -1) { // Invite link
$(name).append(h('em', ' ' + Messages.team_linkUsesInfinite));
} else if (data.pending && data.inviteChannel) {
$(name).append(h('em', ' ' + Messages._getKey('team_linkUses', [
data.remaining || 1,
data.totalUses || 1
])));
}
if (data.pending && data.inviteChannel) {
var r = data.role === "MEMBER" ? Messages.team_members : Messages.team_viewers;
$(name).append(h('em', ' (' + r + ')'));
}
// Status
var status = h('span.cp-team-member-status'+(data.online ? '.online' : ''));
// Actions
@ -817,6 +831,23 @@ define([
actions,
status,
];
if (data.inviteChannel) {
if (data.hash) {
var copy = h('span.fa.fa-copy');
$(copy).click(function () {
var privateData = common.getMetadataMgr().getPrivateData();
var origin = privateData.origin;
var href = origin + Hash.hashToHref(data.hash, 'teams');
var success = Clipboard.copy(href);
if (success) { UI.log(Messages.shareSuccess); }
}).prependTo(actions);
}
content = [
avatar,
name,
actions
];
}
var div = h('div.cp-team-roster-member', content);
if (data.profile) {
$(div).dblclick(function (e) {
@ -872,7 +903,7 @@ define([
if (!roster[k].pending) { return; }
if (!roster[k].inviteChannel) { return; }
roster[k].curvePublic = k;
return roster[k].role === "VIEWER" || !roster[k].role;
return roster[k].role === "MEMBER" || roster[k].role === "VIEWER" || !roster[k].role;
}).map(function (k) {
return makeMember(common, roster[k], me);
});
@ -1349,7 +1380,7 @@ define([
Messages._getKey('team_inviteFromMsg',
[Util.fixHTML(getDisplayName(json.author.displayName)),
Util.fixHTML(json.teamName)])));
if (typeof(json.message) === 'string') {
if (typeof(json.message) === 'string' && json.message) {
var message = h('div.cp-teams-invite-message');
json.message.split('\n').forEach(line => {
if (line.trim()) {
@ -1524,12 +1555,12 @@ define([
$div.empty().append(content);
});
}
var $divLink = $('div.cp-team-link').empty();
/*var $divLink = $('div.cp-team-link').empty();
if ($divLink.length) {
refreshLink(common, function (content) {
$divLink.append(content);
});
}
}*/
var $divCreate = $('div.cp-team-create');
if ($divCreate.length) {
refreshCreate(common, function (content) {