From 8a7f5556c44b8c00610c398229e1f8f3190ab1b4 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 15 Apr 2021 11:10:11 +0200 Subject: [PATCH 1/2] Calendar export --- www/calendar/export.js | 121 +++++++++++++++++++++++++++++++++++++++++ www/calendar/inner.js | 63 +++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 www/calendar/export.js diff --git a/www/calendar/export.js b/www/calendar/export.js new file mode 100644 index 000000000..0d5921e73 --- /dev/null +++ b/www/calendar/export.js @@ -0,0 +1,121 @@ +// This file is used when a user tries to export the entire CryptDrive. +// Calendars will be exported using this format instead of plain text. +define([ + '/customize/pages.js', +], function (Pages) { + var module = {}; + + var getICSDate = function (str) { + var date = new Date(str); + + var m = date.getUTCMonth() + 1; + var d = date.getUTCDate(); + var h = date.getUTCHours(); + var min = date.getUTCMinutes(); + + var year = date.getUTCFullYear().toString(); + var month = m < 10 ? "0" + m : m.toString(); + var day = d < 10 ? "0" + d : d.toString(); + var hours = h < 10 ? "0" + h : h.toString(); + var minutes = min < 10 ? "0" + min : min.toString(); + + return year + month + day + "T" + hours + minutes + "00Z"; + } + + + var getDate = function (str, end) { + var date = new Date(str); + if (end) { + date.setDate(date.getDate() + 1); + } + var m = date.getUTCMonth() + 1; + var d = date.getUTCDate(); + + var year = date.getUTCFullYear().toString(); + var month = m < 10 ? "0" + m : m.toString(); + var day = d < 10 ? "0" + d : d.toString(); + + return year+month+day; + }; + + var MINUTE = 60; + var HOUR = MINUTE * 60; + var DAY = HOUR * 24; + + + module.main = function (userDoc) { + var content = userDoc.content; + var md = userDoc.metadata; + + var ICS = [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//CryptPad//CryptPad Calendar '+Pages.versionString+'//EN', + 'METHOD:PUBLISH', + ]; + + Object.keys(content).forEach(function (uid) { + 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); + } + + 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"; + } + Array.prototype.push.apply(ICS, [ + 'BEGIN:VALARM', + 'ACTION:DISPLAY', + 'DESCRIPTION:This is an event reminder', + 'TRIGGER:'+str, + 'END:VALARM' + ]); + // XXX ACTION:EMAIL + // XXX ATTENDEE:mailto:xxx@xxx.xxx + // XXX SUMMARY:Alarm notification + }); + } + + // XXX add hidden data (from imports) + + ICS.push('END:VEVENT'); + }); + + ICS.push('END:VCALENDAR'); + + return new Blob([ ICS.join('\n') ], { type: 'text/calendar;charset=utf-8' }); + }; + + return module; +}); + + diff --git a/www/calendar/inner.js b/www/calendar/inner.js index f74f3cfaf..9986206f0 100644 --- a/www/calendar/inner.js +++ b/www/calendar/inner.js @@ -15,12 +15,14 @@ define([ '/customize/messages.js', '/customize/application_config.js', '/lib/calendar/tui-calendar.min.js', + '/calendar/export.js', '/common/inner/share.js', '/common/inner/access.js', '/common/inner/properties.js', '/common/jscolor.js', + '/bower_components/file-saver/FileSaver.min.js', 'css!/lib/calendar/tui-calendar.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/calendar/app-calendar.less', @@ -41,9 +43,11 @@ define([ Messages, AppConfig, Calendar, + Export, Share, Access, Properties ) { + var SaveAs = window.saveAs; var APP = window.APP = { calendars: {} }; @@ -453,6 +457,65 @@ Messages.calendar_allDay = "All day"; return true; } }); + + if (!data.readOnly) { + options.push({ + tag: 'a', + attributes: { + 'class': 'fa fa-upload', + }, + content: h('span', Messages.importButton), + action: function (e) { + e.stopPropagation(); + return true; + } + }); + } + options.push({ + tag: 'a', + attributes: { + 'class': 'fa fa-download', + }, + content: h('span', Messages.exportButton), + action: function (e) { + e.stopPropagation(); + var cal = APP.calendars[id]; + var suggestion = Util.find(cal, ['content', 'metadata', 'title']); + var types = []; + types.push({ + tag: 'a', + attributes: { + 'data-value': '.ics', + 'href': '#' + }, + content: '.ics' + }); + var dropdownConfig = { + text: '.ics', // Button initial text + caretDown: true, + options: types, // Entries displayed in the menu + isSelect: true, + initialValue: '.ics', + common: common + }; + var $select = UIElements.createDropdown(dropdownConfig); + UI.prompt(Messages.exportPrompt, + Util.fixFileName(suggestion), function (filename) + { + if (!(typeof(filename) === 'string' && filename)) { return; } + var ext = $select.getValue(); + filename = filename + ext; + var blob = Export.main(cal.content); + SaveAs(blob, filename); + }, { + typeInput: $select[0] + }); + return true; + } + }); + + + options.push({ tag: 'a', attributes: { From 6ced7316002cb304db2cb9f22836054d237d1406 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 15 Apr 2021 17:45:38 +0200 Subject: [PATCH 2/2] Calendar import --- www/calendar/export.js | 87 ++++++++++++++++++++++++++++++-- www/calendar/inner.js | 22 +++++++- www/common/common-ui-elements.js | 2 +- www/common/outer/calendar.js | 20 ++++++++ 4 files changed, 125 insertions(+), 6 deletions(-) diff --git a/www/calendar/export.js b/www/calendar/export.js index 0d5921e73..e470c5d33 100644 --- a/www/calendar/export.js +++ b/www/calendar/export.js @@ -99,13 +99,12 @@ define([ 'TRIGGER:'+str, 'END:VALARM' ]); - // XXX ACTION:EMAIL - // XXX ATTENDEE:mailto:xxx@xxx.xxx - // XXX SUMMARY:Alarm notification }); } - // XXX add hidden data (from imports) + if (Array.isArray(data.cp_hidden)) { + Array.prototype.push.apply(ICS, data.cp_hidden); + } ICS.push('END:VEVENT'); }); @@ -115,6 +114,86 @@ define([ return new Blob([ ICS.join('\n') ], { type: 'text/calendar;charset=utf-8' }); }; + module.import = function (content, id, cb) { + require(['/lib/ical.min.js'], function () { + var ICAL = window.ICAL; + var res = {}; + + try { + var jcalData = ICAL.parse(content); + var vcalendar = new ICAL.Component(jcalData); + } catch (e) { + return void cb(e); + } + + var method = vcalendar.getFirstPropertyValue('method'); + if (method !== "PUBLISH") { return void cb('NOT_SUPPORTED'); } + + var events = vcalendar.getAllSubcomponents('vevent'); + events.forEach(function (ev) { + var uid = ev.getFirstPropertyValue('uid'); + if (!uid) { return; } + + // Get start and end time + var isAllDay = false; + var start = ev.getFirstPropertyValue('dtstart'); + var end = ev.getFirstPropertyValue('dtend'); + if (start.isDate && end.isDate) { + isAllDay = true; + start = String(start); + end.adjust(-1); // Substract one day + end = String(end); + } else { + start = +start.toJSDate(); + end = +end.toJSDate(); + } + + // Store other properties + var used = ['dtstart', 'dtend', 'uid', 'summary', 'location', 'dtstamp']; + var hidden = []; + ev.getAllProperties().forEach(function (p) { + if (used.indexOf(p.name) !== -1) { return; } + // This is an unused property + hidden.push(p.toICALString()); + }); + + // Get reminders + var reminders = []; + ev.getAllSubcomponents('valarm').forEach(function (al) { + var action = al.getFirstPropertyValue('action'); + if (action !== 'DISPLAY') { + // XXX email: maybe keep a notification in CryptPad? + hidden.push(al.toString()); + return; + } + var trigger = al.getFirstPropertyValue('trigger'); + var minutes = -trigger.toSeconds() / 60; + if (reminders.indexOf(minutes) === -1) { reminders.push(minutes); } + }); + + // Create event + res[uid] = { + calendarId: id, + id: uid, + category: 'time', + title: ev.getFirstPropertyValue('summary'), + location: ev.getFirstPropertyValue('location'), + isAllDay: isAllDay, + start: start, + end: end, + reminders: reminders, + cp_hidden: hidden + }; + + if (!hidden.length) { delete res[uid].cp_hidden; } + if (!reminders.length) { delete res[uid].reminders; } + + }); + + cb(null, res); + }); + }; + return module; }); diff --git a/www/calendar/inner.js b/www/calendar/inner.js index 9986206f0..a33bff39c 100644 --- a/www/calendar/inner.js +++ b/www/calendar/inner.js @@ -107,6 +107,12 @@ Messages.calendar_allDay = "All day"; cb(null, obj); }); }; + var importICSCalendar = function (data, cb) { + APP.module.execCommand('IMPORT_ICS', data, function (obj) { + if (obj && obj.error) { return void cb(obj.error); } + cb(null, obj); + }); + }; var newEvent = function (data, cb) { APP.module.execCommand('CREATE_EVENT', data, function (obj) { if (obj && obj.error) { return void cb(obj.error); } @@ -466,7 +472,21 @@ Messages.calendar_allDay = "All day"; }, content: h('span', Messages.importButton), action: function (e) { - e.stopPropagation(); + UIElements.importContent('text/calendar', function (res) { + Export.import(res, id, function (err, json) { + if (err) { return void UI.warn(Messages.importError); } + importICSCalendar({ + id: id, + json: json + }, function (err) { + if (err) { return void UI.warn(Messages.error); } + UI.log(Messages.saved); + }); + + }); + }, { + accept: ['.ics'] + })(); return true; } }); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 6ec1cf420..578e716e3 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -127,7 +127,7 @@ define([ dcAlert = undefined; }; - var importContent = function (type, f, cfg) { + var importContent = UIElements.importContent = function (type, f, cfg) { return function () { var $files = $('', {type:"file"}); if (cfg && cfg.accept) { diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index b9f25462c..bd17b37f8 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -434,6 +434,22 @@ ctx.calendars[channel] = { }); }; + var importICSCalendar = function (ctx, data, cId, cb) { + var id = data.id; + var c = ctx.calendars[id]; + if (!c || !c.proxy) { return void cb({error: "ENOENT"}); } + var json = data.json; + c.proxy.content = c.proxy.content || {}; + Object.keys(json).forEach(function (uid) { + c.proxy.content[uid] = json[uid]; + }); + + Realtime.whenRealtimeSyncs(c.lm.realtime, function () { + sendUpdate(ctx, c); + cb(); + }); + }; + var openCalendar = function (ctx, data, cId, cb) { var secret = Hash.getSecrets('calendar', data.hash, data.password); var hash = Hash.getEditHashFromKeys(secret); @@ -785,6 +801,10 @@ ctx.calendars[channel] = { if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); } return void importCalendar(ctx, data, clientId, cb); } + if (cmd === 'IMPORT_ICS') { + if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); } + return void importICSCalendar(ctx, data, clientId, cb); + } if (cmd === 'ADD') { if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); } return void addCalendar(ctx, data, clientId, cb);