From c702a23c77fac3e604da7732bb59a279e626ff09 Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Tue, 5 Dec 2017 17:48:30 +0100 Subject: [PATCH] Initial work on the 'pad will expire' logic --- .../src/less2/include/colortheme.less | 12 ++ customize.dist/src/less2/include/toolbar.less | 16 ++- customize.dist/translations/messages.js | 1 + pinned.js | 74 +++++++++++ rpc.js | 118 +++++++++++++++--- server.js | 1 + www/code/app-code.less | 2 + www/common/sframe-app-framework.js | 11 +- www/common/toolbar3.js | 21 ++++ www/contacts/app-contacts.less | 1 + www/drive/app-drive.less | 2 + www/file/app-file.less | 2 + www/pad/app-pad.less | 2 + www/poll/app-poll.less | 2 + www/settings/app-settings.less | 4 +- www/slide/app-slide.less | 2 + www/whiteboard/app-whiteboard.less | 2 + www/whiteboard/inner.js | 11 +- 18 files changed, 263 insertions(+), 21 deletions(-) create mode 100644 pinned.js diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less index 4893f5fa2..8cebdaf20 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -42,42 +42,54 @@ @colortheme_pad-bg: #1c4fa0; @colortheme_pad-color: #fff; @colortheme_pad-toolbar-bg: #c1e7ff; +@colortheme_pad-warn: #F83A3A; @colortheme_slide-bg: #e57614; @colortheme_slide-color: #fff; +@colortheme_slide-warn: #7bccd1; @colortheme_code-bg: #ffae00; @colortheme_code-color: #000; +@colortheme_code-warn: #005bef; @colortheme_poll-bg: #006304; @colortheme_poll-color: #fff; @colortheme_poll-help-bg: #bbffbb; @colortheme_poll-th-bg: #005bef; @colortheme_poll-th-fg: #fff; +@colortheme_poll-warn: #cd2532; @colortheme_whiteboard-bg: #800080; @colortheme_whiteboard-color: #fff; +@colortheme_whiteboard-warn: #ffae00; @colortheme_drive-bg: #0087ff; @colortheme_drive-color: #fff; +@colortheme_drive-warn: #cd2532; @colortheme_file-bg: #cd2532; @colortheme_file-color: #fff; +@colortheme_file-warn: #ffae00; @colortheme_friends-bg: #607b8d; @colortheme_friends-color: #fff; +@colortheme_friends-warn: #cd2532; @colortheme_default-bg: #ddd; @colortheme_default-color: #000; +@colortheme_default-warn: #cd2532; @colortheme_settings-bg: #0087ff; @colortheme_settings-color: #fff; +@colortheme_settings-warn: #cd2532; @colortheme_profile-bg: #0087ff; @colortheme_profile-color: #fff; +@colortheme_profile-warn: #cd2532; @colortheme_todo-bg: #7bccd1; @colortheme_todo-color: #000; +@colortheme_todo-warn: #cd2532; // Sidebar layout (profile / settings) @colortheme_sidebar-active: #fff; diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index 5143e4872..4e955af6c 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -7,6 +7,7 @@ @import (once) "./icon-colors.less"; @import (once) "./tools.less"; +@_cp-toolbar-color-warn: black; .toolbar_main () { @@ -224,6 +225,7 @@ } .cp-toolbar-limit { text-shadow: -1px 0 @color, 0 1px @color, 1px 0 @color, 0 -1px @color; + color: @_cp-toolbar-color-warn; } .cp-toolbar-leftside, .cp-toolbar-rightside { background-color: lighten(@bgcolor, 8%); @@ -238,6 +240,19 @@ width: 100%; } } + .cp-pad-not-pinned { + padding-left: 20px; + font-size: @colortheme_app-font-size; + color: @_cp-toolbar-color-warn; + a { + font-size: @colortheme_app-font-size; + font-weight: bold; + color: @_cp-toolbar-color-warn; + &:hover { + text-decoration: underline; + } + } + } .cp-toolbar-title-hoverable:hover { .cp-toolbar-title-editable, .cp-toolbar-title-edit { cursor: text; @@ -383,7 +398,6 @@ vertical-align: middle; line-height: @toolbar_top-height; span { - color: red; cursor: pointer; margin: auto; font-size: 20px; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 511629de8..36a94307a 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -31,6 +31,7 @@ define(function () { out.typeError = "This pad is not compatible with the selected application"; out.onLogout = 'You are logged out, click here to log in
or press Escape to access your pad in read-only mode.'; out.wrongApp = "Unable to display the content of that realtime session in your browser. Please try to reload that page."; + out.padNotPinned = 'This pad will expire in 3 months, login or register to preserve it.'; out.loading = "Loading..."; out.error = "Error"; diff --git a/pinned.js b/pinned.js new file mode 100644 index 000000000..372ed2d10 --- /dev/null +++ b/pinned.js @@ -0,0 +1,74 @@ +/* jshint esversion: 6, node: true */ +const Fs = require('fs'); +const Semaphore = require('saferphore'); +const nThen = require('nthen'); + +const sema = Semaphore.create(20); + +let dirList; +const fileList = []; +const pinned = {}; + +const hashesFromPinFile = (pinFile, fileName) => { + var pins = {}; + pinFile.split('\n').filter((x)=>(x)).map((l) => JSON.parse(l)).forEach((l) => { + switch (l[0]) { + case 'RESET': { + pins = {}; + //jshint -W086 + // fallthrough + } + case 'PIN': { + l[1].forEach((x) => { pins[x] = 1; }); + break; + } + case 'UNPIN': { + l[1].forEach((x) => { delete pins[x]; }); + break; + } + default: throw new Error(JSON.stringify(l) + ' ' + fileName); + } + }); + return Object.keys(pins); +}; + +module.exports.load = function (cb) { + nThen((waitFor) => { + Fs.readdir('./pins', waitFor((err, list) => { + if (err) { throw err; } + dirList = list; + })); + }).nThen((waitFor) => { + fileList.splice(0, fileList.length); + dirList.forEach((f) => { + sema.take((returnAfter) => { + Fs.readdir('./pins/' + f, waitFor(returnAfter((err, list2) => { + if (err) { throw err; } + list2.forEach((ff) => { fileList.push('./pins/' + f + '/' + ff); }); + }))); + }); + }); + }).nThen((waitFor) => { + fileList.forEach((f) => { + sema.take((returnAfter) => { + Fs.readFile(f, waitFor(returnAfter((err, content) => { + if (err) { throw err; } + const hashes = hashesFromPinFile(content.toString('utf8'), f); + hashes.forEach((x) => { + (pinned[x] = pinned[x] || {})[f.replace(/.*\/([^/]*).ndjson$/, (x, y)=>y)] = 1; + }); + }))); + }); + }); + }).nThen(() => { + cb(pinned); + }); +}; + +if (!module.parent) { + module.exports.load(function (data) { + Object.keys(data).forEach(function (x) { + console.log(x + ' ' + JSON.stringify(data[x])); + }); + }); +} \ No newline at end of file diff --git a/rpc.js b/rpc.js index b0e3a0e6d..fc87bde58 100644 --- a/rpc.js +++ b/rpc.js @@ -10,6 +10,7 @@ var Fs = require("fs"); var Path = require("path"); var Https = require("https"); const Package = require('./package.json'); +const Pinned = require('./pinned'); var RPC = module.exports; @@ -212,7 +213,6 @@ var checkSignature = function (signedMsg, signature, publicKey) { }; var loadUserPins = function (Env, publicKey, cb) { - var pinStore = Env.pinStore; var session = beginSession(Env.Sessions, publicKey); if (session.channels) { @@ -230,7 +230,7 @@ var loadUserPins = function (Env, publicKey, cb) { pins[channel] = false; }; - pinStore.getMessages(publicKey, function (msg) { + Env.pinStore.getMessages(publicKey, function (msg) { // handle messages... var parsed; try { @@ -325,9 +325,8 @@ var getFileSize = function (Env, channel, cb) { }; var getMultipleFileSize = function (Env, channels, cb) { - var msgStore = Env.msgStore; if (!Array.isArray(channels)) { return cb('INVALID_PIN_LIST'); } - if (typeof(msgStore.getChannelSize) !== 'function') { + if (typeof(Env.msgStore.getChannelSize) !== 'function') { return cb('GET_CHANNEL_SIZE_UNSUPPORTED'); } @@ -499,6 +498,54 @@ var sumChannelSizes = function (sizes) { .reduce(function (a, b) { return a + b; }, 0); }; +// inform that the +var loadChannelPins = function (Env) { + Pinned.load(function (data) { + Env.pinnedPads = data; + Env.evPinnedPadsReady.fire(); + }); +}; +var addPinned = function ( + Env, + publicKey /*:string*/, + channelList /*Array*/, + cb /*:()=>void*/) +{ + Env.evPinnedPadsReady.reg(() => { + channelList.forEach((c) => { + const x = Env.pinnedPads[c]; + if (!x) { return; } + delete x[publicKey]; + }); + cb(); + }); +}; +var removePinned = function ( + Env, + publicKey /*:string*/, + channelList /*Array*/, + cb /*:()=>void*/) +{ + Env.evPinnedPadsReady.reg(() => { + channelList.forEach((c) => { + const x = Env.pinnedPads[c]; + if (!x) { return; } + delete x[publicKey]; + }); + cb(); + }); +}; +var isChannelPinned = function (Env, channel, cb) { + Env.evPinnedPadsReady.reg(() => { + if (Env.pinnedPads[channel] && Object.keys(Env.pinnedPads[channel]).length) { + cb(true); + } else { + delete Env.pinnedPads[channel]; + cb(false); + } + }); +}; + var pinChannel = function (Env, publicKey, channels, cb) { if (!channels && channels.filter) { return void cb('INVALID_PIN_LIST'); @@ -534,6 +581,7 @@ var pinChannel = function (Env, publicKey, channels, cb) { toStore.forEach(function (channel) { session.channels[channel] = true; }); + addPinned(Env, publicKey, toStore, () => {}); getHash(Env, publicKey, cb); }); }); @@ -542,7 +590,6 @@ var pinChannel = function (Env, publicKey, channels, cb) { }; var unpinChannel = function (Env, publicKey, channels, cb) { - var pinStore = Env.pinStore; if (!channels && channels.filter) { // expected array return void cb('INVALID_PIN_LIST'); @@ -560,13 +607,13 @@ var unpinChannel = function (Env, publicKey, channels, cb) { return void getHash(Env, publicKey, cb); } - pinStore.message(publicKey, JSON.stringify(['UNPIN', toStore]), + Env.pinStore.message(publicKey, JSON.stringify(['UNPIN', toStore]), function (e) { if (e) { return void cb(e); } toStore.forEach(function (channel) { delete session.channels[channel]; }); - + removePinned(Env, publicKey, toStore, () => {}); getHash(Env, publicKey, cb); }); }); @@ -574,7 +621,6 @@ var unpinChannel = function (Env, publicKey, channels, cb) { var resetUserPins = function (Env, publicKey, channelList, cb) { if (!Array.isArray(channelList)) { return void cb('INVALID_PIN_LIST'); } - var pinStore = Env.pinStore; var session = beginSession(Env.Sessions, publicKey); if (!channelList.length) { @@ -605,13 +651,18 @@ var resetUserPins = function (Env, publicKey, channelList, cb) { They will not be able to pin additional pads until they upgrade or delete enough files to go back under their limit. */ if (pinSize > limit[0] && session.hasPinned) { return void(cb('E_OVER_LIMIT')); } - pinStore.message(publicKey, JSON.stringify(['RESET', channelList]), + Env.pinStore.message(publicKey, JSON.stringify(['RESET', channelList]), function (e) { if (e) { return void cb(e); } channelList.forEach(function (channel) { pins[channel] = true; }); + var oldChannels = Object.keys(session.channels); + removePinned(Env, publicKey, oldChannels, () => { + addPinned(Env, publicKey, channelList, ()=>{}); + }); + // update in-memory cache IFF the reset was allowed. session.channels = pins; getHash(Env, publicKey, function (e, hash) { @@ -879,6 +930,7 @@ var isUnauthenticatedCall = function (call) { return [ 'GET_FILE_SIZE', 'GET_MULTIPLE_FILE_SIZE', + 'IS_CHANNEL_PINNED', ].indexOf(call) !== -1; }; @@ -894,10 +946,31 @@ var isAuthenticatedCall = function (call) { 'GET_LIMIT', 'UPLOAD_COMPLETE', 'UPLOAD_CANCEL', - 'EXPIRE_SESSION', + 'EXPIRE_SESSION' ].indexOf(call) !== -1; }; +const mkEvent = function (once) { + var handlers = []; + var fired = false; + return { + reg: function (cb) { + if (once && fired) { return void setTimeout(cb); } + handlers.push(cb); + }, + unreg: function (cb) { + if (handlers.indexOf(cb) === -1) { throw new Error("Not registered"); } + handlers.splice(handlers.indexOf(cb), 1); + }, + fire: function () { + if (once && fired) { return; } + fired = true; + var args = Array.prototype.slice.call(arguments); + handlers.forEach(function (h) { h.apply(null, args); }); + } + }; +}; + /*::const ConfigType = require('./config.example.js');*/ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function)=>void*/) { // load pin-store... @@ -909,14 +982,19 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) return typeof(config[key]) === 'string'? config[key]: def; }; - var Env = {}; - Env.defaultStorageLimit = config.defaultStorageLimit; - - Env.maxUploadSize = config.maxUploadSize || (20 * 1024 * 1024); - - var Sessions = Env.Sessions = {}; + var Env = { + defaultStorageLimit: config.defaultStorageLimit, + maxUploadSize: config.maxUploadSize || (20 * 1024 * 1024), + Sessions: {}, + paths: {}, + msgStore: (undefined /*:any*/), + pinStore: (undefined /*:any*/), + pinnedPads: {}, + evPinnedPadsReady: mkEvent(true) + }; - var paths = Env.paths = {}; + var Sessions = Env.Sessions; + var paths = Env.paths; var pinPath = paths.pin = keyOrDefaultString('pinPath', './pins'); var blobPath = paths.blob = keyOrDefaultString('blobPath', './blob'); var blobStagingPath = paths.staging = keyOrDefaultString('blobStagingPath', './blobstage'); @@ -943,6 +1021,10 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) } respond(e, [null, dict, null]); }); + case 'IS_CHANNEL_PINNED': + return void isChannelPinned(Env, msg[1], function (isPinned) { + respond(null, [null, isPinned, null]); + }); default: console.error("unsupported!"); return respond('UNSUPPORTED_RPC_CALL', msg); @@ -1174,6 +1256,8 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) updateLimitDaily(); setInterval(updateLimitDaily, 24*3600*1000); + loadChannelPins(Env); + Store.create({ filePath: pinPath, }, function (s) { diff --git a/server.js b/server.js index baf9b2e99..da847ac31 100644 --- a/server.js +++ b/server.js @@ -102,6 +102,7 @@ app.use("/blob", Express.static(Path.join(__dirname, (config.blobPath || './blob app.use("/customize", Express.static(__dirname + '/customize')); app.use("/customize", Express.static(__dirname + '/customize.dist')); +app.use("/customize.dist", Express.static(__dirname + '/customize.dist')); app.use(/^\/[^\/]*$/, Express.static('customize')); app.use(/^\/[^\/]*$/, Express.static('customize.dist')); diff --git a/www/code/app-code.less b/www/code/app-code.less index 2541431ea..5025d4b3c 100644 --- a/www/code/app-code.less +++ b/www/code/app-code.less @@ -5,6 +5,8 @@ @import (once) '../../customize/src/less2/include/alertify.less'; @import (once) '../../customize/src/less2/include/tokenfield.less'; +@_cp-toolbar-color-warn: @colortheme_code-warn; + .toolbar_main(); .fileupload_main(); .alertify_main(); diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index 1f7723cf3..b5620ca6f 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -461,7 +461,16 @@ define([ getHeadingText: function () { return titleRecommender(); } }, onLocal); var configTb = { - displayed: ['userlist', 'title', 'useradmin', 'spinner', 'newpad', 'share', 'limit'], + displayed: [ + 'userlist', + 'title', + 'useradmin', + 'spinner', + 'newpad', + 'share', + 'limit', + 'unpinnedWarning' + ], title: title.getTitleConfig(), metadataMgr: cpNfInner.metadataMgr, readOnly: readOnly, diff --git a/www/common/toolbar3.js b/www/common/toolbar3.js index b06d358ba..1227a1dbc 100644 --- a/www/common/toolbar3.js +++ b/www/common/toolbar3.js @@ -719,6 +719,26 @@ define([ return $titleContainer; }; + var createUnpinnedWarning = function (toolbar, config) { + if (Common.isLoggedIn()) { return; } + var pd = config.metadataMgr.getPrivateData(); + var o = pd.origin; + var hashes = pd.availableHashes; + var url = pd.origin + pd.pathname + '#' + (hashes.editHash || hashes.viewHash); + var cid = Hash.hrefToHexChannelId(url); + Common.sendAnonRpcMsg('IS_CHANNEL_PINNED', cid, function (x) { + if (x.error || !Array.isArray(x.response)) { return void console.log(x); } + if (x.response[0] === true) { return; } + var msg = $('', { + 'class': 'cp-pad-not-pinned', + }).append( + Messages._getKey('padNotPinned', [o + '/login', o + '/register']) + ); + $('.cp-toolbar-title').append(msg); + console.log("This pad is not pinned"); + }); + }; + var createPageTitle = function (toolbar, config) { if (config.title || !config.pageTitle) { return; } var $titleContainer = $('', { @@ -1087,6 +1107,7 @@ define([ tb['upgrade'] = $.noop; tb['newpad'] = createNewPad; tb['useradmin'] = createUserAdmin; + tb['unpinnedWarning'] = createUnpinnedWarning; var addElement = toolbar.addElement = function (arr, additionnalCfg, init) { if (typeof additionnalCfg === "object") { $.extend(true, config, additionnalCfg); } diff --git a/www/contacts/app-contacts.less b/www/contacts/app-contacts.less index 6ae7a62da..26fba2c90 100644 --- a/www/contacts/app-contacts.less +++ b/www/contacts/app-contacts.less @@ -8,6 +8,7 @@ @import (once) '../../customize/src/less2/include/avatar.less'; +@_cp-toolbar-color-warn: @colortheme_friends-warn;; .toolbar_main(); .fileupload_main(); diff --git a/www/drive/app-drive.less b/www/drive/app-drive.less index cad82bc2f..17ae35fcb 100644 --- a/www/drive/app-drive.less +++ b/www/drive/app-drive.less @@ -8,6 +8,8 @@ @import (once) "../../customize/src/less2/include/limit-bar.less"; @import (once) "../../customize/src/less2/include/tokenfield.less"; +@_cp-toolbar-color-warn: @colortheme_drive-warn; + .toolbar_main(); .fileupload_main(); .alertify_main(); diff --git a/www/file/app-file.less b/www/file/app-file.less index e2065d00f..7a990cb03 100644 --- a/www/file/app-file.less +++ b/www/file/app-file.less @@ -5,6 +5,8 @@ @import (once) '../../customize/src/less2/include/alertify.less'; @import (once) '../../customize/src/less2/include/tokenfield.less'; +@_cp-toolbar-color-warn: @colortheme_file-warn; + .toolbar_main(); .fileupload_main(); .alertify_main(); diff --git a/www/pad/app-pad.less b/www/pad/app-pad.less index 4bc6cac9d..6e34f473d 100644 --- a/www/pad/app-pad.less +++ b/www/pad/app-pad.less @@ -6,6 +6,8 @@ @import (once) '../../customize/src/less2/include/alertify.less'; @import (once) '../../customize/src/less2/include/tokenfield.less'; +@_cp-toolbar-color-warn: @colortheme_pad-warn; + .toolbar_main(); .alertify_main(); diff --git a/www/poll/app-poll.less b/www/poll/app-poll.less index 08acd2b6b..f717a6f56 100644 --- a/www/poll/app-poll.less +++ b/www/poll/app-poll.less @@ -13,6 +13,8 @@ @import (once) '../../customize/src/less2/include/tools.less'; @import (once) '../../customize/src/less2/include/avatar.less'; +@_cp-toolbar-color-warn: @colortheme_poll-warn; + .toolbar_main(); .fileupload_main(); .alertify_main(); diff --git a/www/settings/app-settings.less b/www/settings/app-settings.less index b3afcc257..7af0a0ff1 100644 --- a/www/settings/app-settings.less +++ b/www/settings/app-settings.less @@ -4,7 +4,9 @@ @import (once) "../../customize/src/less2/include/markdown.less"; @import (once) '../../customize/src/less2/include/alertify.less'; @import (once) '../../customize/src/less2/include/sidebar-layout.less'; -@import (once) "../../customize/src/less2/include/limit-bar.less"; +@import (once) "../../customize/src/less2/include/limit-bar.less"; + +@_cp-toolbar-color-warn: @colortheme_settings-warn; .toolbar_main(); .alertify_main(); diff --git a/www/slide/app-slide.less b/www/slide/app-slide.less index 20c5a1830..32088eb64 100644 --- a/www/slide/app-slide.less +++ b/www/slide/app-slide.less @@ -6,6 +6,8 @@ @import (once) "../../customize/src/less2/include/mediatag.less"; @import (once) '../../customize/src/less2/include/tokenfield.less'; +@_cp-toolbar-color-warn: @colortheme_slide-warn; + .mediatag_base(); .toolbar_main(); .fileupload_main(); diff --git a/www/whiteboard/app-whiteboard.less b/www/whiteboard/app-whiteboard.less index 9ec88d743..95fe016e9 100644 --- a/www/whiteboard/app-whiteboard.less +++ b/www/whiteboard/app-whiteboard.less @@ -6,6 +6,8 @@ @import (once) '../../customize/src/less2/include/tools.less'; @import (once) '../../customize/src/less2/include/tokenfield.less'; +@_cp-toolbar-color-warn: @colortheme_whiteboard-warn; + .toolbar_main(); .fileupload_main(); .alertify_main(); diff --git a/www/whiteboard/inner.js b/www/whiteboard/inner.js index d65089a41..d19514b02 100644 --- a/www/whiteboard/inner.js +++ b/www/whiteboard/inner.js @@ -393,7 +393,16 @@ define([ Title = common.createTitle({}); var configTb = { - displayed: ['title', 'useradmin', 'spinner', 'share', 'userlist', 'newpad', 'limit'], + displayed: [ + 'userlist', + 'title', + 'useradmin', + 'spinner', + 'newpad', + 'share', + 'limit', + 'unpinnedWarning' + ], title: Title.getTitleConfig(), metadataMgr: metadataMgr, readOnly: readOnly,