From e5ee3ee22adb60e8b3748d452d2a55ec7996d20e Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 7 May 2021 18:03:39 +0200 Subject: [PATCH 001/424] Fix type error in sheets --- www/common/onlyoffice/v4/sdkjs/cell/sdk-all.js | 14 +++++++------- www/common/onlyoffice/v4/sdkjs/slide/sdk-all.js | 14 +++++++------- www/common/onlyoffice/v4/sdkjs/word/sdk-all.js | 14 +++++++------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/www/common/onlyoffice/v4/sdkjs/cell/sdk-all.js b/www/common/onlyoffice/v4/sdkjs/cell/sdk-all.js index b8d012784..cdefc6082 100644 --- a/www/common/onlyoffice/v4/sdkjs/cell/sdk-all.js +++ b/www/common/onlyoffice/v4/sdkjs/cell/sdk-all.js @@ -12867,13 +12867,13 @@ deleted){self.dependencyFormulas.delColumnTable(tableName,deleted);var wsActive= wsActive.getHidden())wsActive.setHidden(false);if(!bNoBuildDep)this.dependencyFormulas.initOpen();if(bSnapshot)this.snapshot=this._getSnapshot()};Workbook.prototype.initPostOpenZip=function(pivotCaches){var t=this;this.forEach(function(ws){ws.initPostOpenZip(pivotCaches,t.oNumFmtsOpen)})};Workbook.prototype.setCommonIndexObjectsFrom=function(wb){this.oStyleManager=wb.oStyleManager;this.sharedStrings=wb.sharedStrings;this.workbookFormulas=wb.workbookFormulas};Workbook.prototype.forEach=function(callback, isCopyPaste){if(isCopyPaste||isCopyPaste===false)callback(this.getActiveWs(),this.getActive());else for(var i=0,l=this.aWorksheets.length;i=0&&index=0&&index=0&&indexBefore=0&&index=0&&insertBefore=0&&index=0&&indexBefore=0&&index=0&&insertBefore=0&&index0&&index=0&&indexFrom=0&&indexTo=0&&index=0&&index=0&&indexBefore=0&&index=0&&insertBefore=0&&index=0&&indexBefore=0&&index=0&&insertBefore=0&&index0&&index=0&&indexFrom=0&&indexTo=0&&index=0&&index=0&&indexBefore=0&&index=0&&insertBefore=0&&index=0&&indexBefore=0&&index=0&&insertBefore=0&&index0&&index=0&&indexFrom=0&&indexTo Date: Fri, 7 May 2021 18:03:39 +0200 Subject: [PATCH 002/424] Fix type error in sheets --- www/common/onlyoffice/v4/sdkjs/cell/sdk-all.js | 14 +++++++------- www/common/onlyoffice/v4/sdkjs/slide/sdk-all.js | 14 +++++++------- www/common/onlyoffice/v4/sdkjs/word/sdk-all.js | 14 +++++++------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/www/common/onlyoffice/v4/sdkjs/cell/sdk-all.js b/www/common/onlyoffice/v4/sdkjs/cell/sdk-all.js index b8d012784..cdefc6082 100644 --- a/www/common/onlyoffice/v4/sdkjs/cell/sdk-all.js +++ b/www/common/onlyoffice/v4/sdkjs/cell/sdk-all.js @@ -12867,13 +12867,13 @@ deleted){self.dependencyFormulas.delColumnTable(tableName,deleted);var wsActive= wsActive.getHidden())wsActive.setHidden(false);if(!bNoBuildDep)this.dependencyFormulas.initOpen();if(bSnapshot)this.snapshot=this._getSnapshot()};Workbook.prototype.initPostOpenZip=function(pivotCaches){var t=this;this.forEach(function(ws){ws.initPostOpenZip(pivotCaches,t.oNumFmtsOpen)})};Workbook.prototype.setCommonIndexObjectsFrom=function(wb){this.oStyleManager=wb.oStyleManager;this.sharedStrings=wb.sharedStrings;this.workbookFormulas=wb.workbookFormulas};Workbook.prototype.forEach=function(callback, isCopyPaste){if(isCopyPaste||isCopyPaste===false)callback(this.getActiveWs(),this.getActive());else for(var i=0,l=this.aWorksheets.length;i=0&&index=0&&index=0&&indexBefore=0&&index=0&&insertBefore=0&&index=0&&indexBefore=0&&index=0&&insertBefore=0&&index0&&index=0&&indexFrom=0&&indexTo=0&&index=0&&index=0&&indexBefore=0&&index=0&&insertBefore=0&&index=0&&indexBefore=0&&index=0&&insertBefore=0&&index0&&index=0&&indexFrom=0&&indexTo=0&&index=0&&index=0&&indexBefore=0&&index=0&&insertBefore=0&&index=0&&indexBefore=0&&index=0&&insertBefore=0&&index0&&index=0&&indexFrom=0&&indexTo Date: Tue, 11 May 2021 16:17:00 +0530 Subject: [PATCH 003/424] handle admin decree errors on the admin panel addresses #642 --- www/admin/inner.js | 58 ++++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/www/admin/inner.js b/www/admin/inner.js index ffc9a561c..8e3ed16d2 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -267,8 +267,11 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['RESTRICT_REGISTRATION', [val]] - }, function (e) { - if (e) { UI.warn(Messages.error); console.error(e); } + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); + } APP.updateStatus(function () { spinner.done(); state = APP.instanceStatus.restrictRegistration; @@ -316,8 +319,11 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['UPDATE_DEFAULT_STORAGE', data] - }, function (e) { - if (e) { UI.warn(Messages.error); return void console.error(e); } + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + return void console.error(e, response); + } var limit = getPrettySize(l); $div.find('.cp-admin-defaultlimit-value').text(Messages._getKey('admin_limit', [limit])); }); @@ -448,8 +454,12 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['RM_QUOTA', data] - }, function (e) { - if (e) { UI.warn(Messages.error); console.error(e); } + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); + return; + } APP.refreshLimits(); $key.val(''); }); @@ -462,8 +472,12 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['SET_QUOTA', data] - }, function (e) { - if (e) { UI.warn(Messages.error); console.error(e); } + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); + return; + } APP.refreshLimits(); $key.val(''); }); @@ -1030,9 +1044,10 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['SET_LAST_BROADCAST_HASH', [lastHash]] - }, function (e) { - if (e) { - console.error(e); + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); return; } console.log('lastBroadcastHash updated'); @@ -1336,9 +1351,10 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['SET_MAINTENANCE', [data]] - }, function (e) { - if (e) { - UI.warn(Messages.error); console.error(e); + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); $button.prop('disabled', ''); return; } @@ -1430,10 +1446,11 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['SET_SURVEY_URL', [data]] - }, function (e) { - if (e) { + }, function (e, response) { + if (e || response.error) { $button.prop('disabled', ''); - UI.warn(Messages.error); console.error(e); + UI.warn(Messages.error); + console.error(e, response); return; } // Maintenance applied, send notification @@ -1529,11 +1546,12 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'GET_WORKER_PROFILES', }, function (e, data) { - if (e) { return void console.error(e); } + if (e || data.error) { + UI.warn(Messages.error); + return void console.error(e, data); + } //console.info(data); $div.find("table").remove(); - - process(data); $div.append(table); }); From 6eaee92ac3cd159b36f2c4a42ae3afb5773ce1ef Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 12 May 2021 10:12:00 +0530 Subject: [PATCH 004/424] rename variables in AppConfig to minimize copy-paste problems --- www/common/application_config_internal.js | 70 +++++++++++------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index c7b66c1e5..791ecf367 100644 --- a/www/common/application_config_internal.js +++ b/www/common/application_config_internal.js @@ -4,14 +4,14 @@ * file (make a copy from /customize.dist/application_config.js) */ define(function() { - var config = {}; + var AppConfig = {}; /* Select the buttons displayed on the main page to create new collaborative sessions. * Removing apps from the list will prevent users from accessing them. They will instead be * redirected to the drive. * You should never remove the drive from this list. */ - config.availablePadTypes = ['drive', 'teams', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard', + AppConfig.availablePadTypes = ['drive', 'teams', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard', /*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts' /*, 'calendar' */]; /* The registered only types are apps restricted to registered users. * You should never remove apps from this list unless you know what you're doing. The apps @@ -20,7 +20,7 @@ define(function() { * users and these users will be redirected to the login page if they still try to access * the app */ - config.registeredOnlyTypes = ['file', 'contacts', 'notifications', 'support']; + AppConfig.registeredOnlyTypes = ['file', 'contacts', 'notifications', 'support']; /* CryptPad is available is multiple languages, but only English and French are maintained * by the developers. The other languages may be outdated, and any missing string for a langauge @@ -30,37 +30,37 @@ define(function() { * can be found at the top of the file `/customize.dist/messages.js`. The list should only * contain languages code ('en', 'fr', 'de', 'pt-br', etc.), not their full name. */ - //config.availableLanguages = ['en', 'fr', 'de']; + //AppConfig.availableLanguages = ['en', 'fr', 'de']; /* You can display a link to the imprint (legal notice) of your website in the static pages * footer. To do so, you can either set the following value to `true` and create an imprint.html page * in the `customize` directory. You can also set it to an absolute URL if your imprint page already exists. */ - config.imprint = false; - // config.imprint = true; - // config.imprint = 'https://xwiki.com/en/company/legal-notice'; + AppConfig.imprint = false; + // AppConfig.imprint = true; + // AppConfig.imprint = 'https://xwiki.com/en/company/legal-notice'; /* You can display a link to your own privacy policy in the static pages footer. * To do so, set the following value to the absolute URL of your privacy policy. */ - // config.privacy = 'https://xwiki.com/en/company/PrivacyPolicy'; + // AppConfig.privacy = 'https://xwiki.com/en/company/PrivacyPolicy'; /* We (the project's developers) include the ability to display a 'Roadmap' in static pages footer. * This is disabled by default. * We use this to publish the project's development roadmap, but you can use it however you like. * To do so, set the following value to an absolute URL. */ - //config.roadmap = 'https://cryptpad.fr/kanban/#/2/kanban/view/PLM0C3tFWvYhd+EPzXrbT+NxB76Z5DtZhAA5W5hG9wo/'; + //AppConfig.roadmap = 'https://cryptpad.fr/kanban/#/2/kanban/view/PLM0C3tFWvYhd+EPzXrbT+NxB76Z5DtZhAA5W5hG9wo/'; /* Cryptpad apps use a common API to display notifications to users * by default, notifications are hidden after 5 seconds * You can change their duration here (measured in milliseconds) */ - config.notificationTimeout = 5000; - config.disableUserlistNotifications = false; + AppConfig.notificationTimeout = 5000; + AppConfig.disableUserlistNotifications = false; // Update the default colors available in the whiteboard application - config.whiteboardPalette = [ + AppConfig.whiteboardPalette = [ '#000000', // black '#FFFFFF', // white '#848484', // grey @@ -82,14 +82,14 @@ define(function() { // Background color in the apps with centered content: // - file app in view mode // - rich text app when editor's width reduced in settings - config.appBackgroundColor = '#666'; + AppConfig.appBackgroundColor = '#666'; // Set enableTemplates to false to remove the button allowing users to save a pad as a template // and remove the template category in CryptDrive - config.enableTemplates = true; + AppConfig.enableTemplates = true; // Set enableHistory to false to remove the "History" button in all the apps. - config.enableHistory = true; + AppConfig.enableHistory = true; /* user passwords are hashed with scrypt, and salted with their username. this value will be appended to the username, causing the resulting hash @@ -101,15 +101,15 @@ define(function() { created. Changing it at a later time will break logins for all existing users. */ - config.loginSalt = ''; - config.minimumPasswordLength = 8; + AppConfig.loginSalt = ''; + AppConfig.minimumPasswordLength = 8; // Amount of time (ms) before aborting the session when the algorithm cannot synchronize the pad - config.badStateTimeout = 30000; + AppConfig.badStateTimeout = 30000; // Customize the icon used for each application. // You can update the colors by making a copy of /customize.dist/src/less2/include/colortheme.less - config.applicationsIcon = { + AppConfig.applicationsIcon = { file: 'cptools-file', fileupload: 'cptools-file-upload', folderupload: 'cptools-folder-upload', @@ -130,50 +130,50 @@ define(function() { // Ability to create owned pads and expiring pads through a new pad creation screen. // The new screen can be disabled by the users in their settings page - config.displayCreationScreen = true; + AppConfig.displayCreationScreen = true; // Prevent anonymous users from storing pads in their drive - config.disableAnonymousStore = false; + AppConfig.disableAnonymousStore = false; // Hide the usage bar in settings and drive - //config.hideUsageBar = true; + //AppConfig.hideUsageBar = true; // Disable feedback for all the users and hide the settings part about feedback - //config.disableFeedback = true; + //AppConfig.disableFeedback = true; // Add new options in the share modal (extend an existing tab or add a new tab). // More info about how to use it on the wiki: - // https://github.com/xwiki-labs/cryptpad/wiki/Application-config#configcustomizeshareoptions - //config.customizeShareOptions = function (hashes, tabs, config) {}; + // https://github.com/xwiki-labs/cryptpad/wiki/Application-config#configcustomizeshareoptions XXX page is a 404 + //AppConfig.customizeShareOptions = function (hashes, tabs, config) {}; // Add code to be executed on every page before loading the user object. `isLoggedIn` (bool) is // indicating if the user is registered or anonymous. Here you can change the way anonymous users // work in CryptPad, use an external SSO or even force registration // *NOTE*: You have to call the `callback` function to continue the loading process - //config.beforeLogin = function(isLoggedIn, callback) {}; + //AppConfig.beforeLogin = function(isLoggedIn, callback) {}; // Add code to be executed on every page after the user object is loaded (also work for // unregistered users). This allows you to interact with your users' drive // *NOTE*: You have to call the `callback` function to continue the loading process - //config.afterLogin = function(api, callback) {}; + //AppConfig.afterLogin = function(api, callback) {}; // Disabling the profile app allows you to import the profile informations (display name, avatar) // from an external source and make sure the users can't change them from CryptPad. - // You can use config.afterLogin to import these values in the users' drive. - //config.disableProfile = true; + // You can use AppConfig.afterLogin to import these values in the users' drive. + //AppConfig.disableProfile = true; // Disable the use of webworkers and sharedworkers in CryptPad. // Workers allow us to run the websockets connection and open the user drive in a separate thread. // SharedWorkers allow us to load only one websocket and one user drive for all the browser tabs, // making it much faster to open new tabs. - config.disableWorkers = false; + AppConfig.disableWorkers = false; // Teams are always loaded during the initial loading screen (for the first tab only if // SharedWorkers are available). Allowing users to be members of multiple teams can // make them have a very slow loading time. To avoid impacting the user experience // significantly, we're limiting the number of teams per user to 3 by default. // You can change this value here. - //config.maxTeamsSlots = 5; + //AppConfig.maxTeamsSlots = 5; // Each team is considered as a registered user by the server. Users and teams are indistinguishable // in the database so teams will offer the same storage limits as users by default. @@ -181,7 +181,7 @@ define(function() { // We're limiting the number of teams each user is able to own to 1 in order to make sure // users don't use "fake" teams (1 member) just to increase their storage limit. // You can change the value here. - // config.maxOwnedTeams = 5; + // AppConfig.maxOwnedTeams = 5; // The userlist displayed in collaborative documents is stored alongside the document data. // Everytime someone with edit rights joins a document or modify their user data (display @@ -192,14 +192,14 @@ define(function() { // position of other users' cursor. You can configure the number of user from which the session // will enter into degraded mode. A big number may result in collaborative edition being broken, // but this number depends on the network and CPU performances of each user's device. - config.degradedLimit = 8; + AppConfig.degradedLimit = 8; // In "legacy" mode, one-time users were always creating an "anonymous" drive when visiting CryptPad // in which they could store their pads. The new "driveless" mode allow users to open an existing // pad without creating a drive in the background. The drive will only be created if they visit // a different page (Drive, Settings, etc.) or try to create a new pad themselves. You can disable // the driveless mode by changing the following value to "false" - config.allowDrivelessMode = true; + AppConfig.allowDrivelessMode = true; - return config; + return AppConfig; }); From 32494fca0c154dfd44d48bc5ecadc60c9bd101ea Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 12 May 2021 14:18:26 +0530 Subject: [PATCH 005/424] let NGINX handle its own headers --- docs/example.nginx.conf | 7 +++++++ server.js | 17 +++++------------ www/checkup/app-checkup.less | 4 ++-- www/checkup/main.js | 6 +++--- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf index 78e4f30ce..a51a1ecaa 100644 --- a/docs/example.nginx.conf +++ b/docs/example.nginx.conf @@ -167,6 +167,13 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # These settings prevent both NGINX and the API server + # from setting the same headers and creating duplicates + proxy_hide_header Cross-Origin-Resource-Policy; + add_header Cross-Origin-Resource-Policy cross-origin; + proxy_hide_header Cross-Origin-Embedder-Policy; + add_header Cross-Origin-Embedder-Policy require-corp; } # encrypted blobs are immutable and are thus cached for a year diff --git a/server.js b/server.js index 3a71f83b8..1824cf59c 100644 --- a/server.js +++ b/server.js @@ -108,28 +108,21 @@ var setHeaders = (function () { // apply a bunch of cross-origin headers for XLSX export in FF and printing elsewhere applyHeaderMap(res, { "Cross-Origin-Opener-Policy": /^\/sheet\//.test(req.url)? 'same-origin': '', - "Cross-Origin-Embedder-Policy": 'require-corp', }); if (Env.NO_SANDBOX) { // handles correct configuration for local development // https://stackoverflow.com/questions/11531121/add-duplicate-http-response-headers-in-nodejs applyHeaderMap(res, { "Cross-Origin-Resource-Policy": 'cross-origin', + "Cross-Origin-Embedder-Policy": 'require-corp', }); } - // Don't set CSP headers on /api/config because they aren't necessary and they cause problems + // Don't set CSP headers on /api/ endpoints + // because they aren't necessary and they cause problems // when duplicated by NGINX in production environments - if (/^\/api\/(broadcast|config)/.test(req.url)) { - /* - if (Env.NO_SANDBOX) { - applyHeaderMap(res, { - "Cross-Origin-Resource-Policy": 'cross-origin', - }); - } - */ - return; - } + if (/^\/api\/(broadcast|config)/.test(req.url)) { return; } + applyHeaderMap(res, { "Cross-Origin-Resource-Policy": 'cross-origin', }); diff --git a/www/checkup/app-checkup.less b/www/checkup/app-checkup.less index 40e6a1add..a475d5983 100644 --- a/www/checkup/app-checkup.less +++ b/www/checkup/app-checkup.less @@ -20,7 +20,7 @@ html, body { } .pending { - border: 1px solid white; + border: 1px solid @cryptpad_text_col; .fa { margin-right: 20px; } @@ -45,7 +45,7 @@ html, body { table { td { padding: 5px; - border: 1px solid white; + border: 1px solid @cryptpad_text_col; } } diff --git a/www/checkup/main.js b/www/checkup/main.js index 87e310e32..6983e461c 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -237,7 +237,7 @@ define([ var blockUrl = Login.Block.getBlockUrl(opt.blockKeys); var blockRequest = Login.Block.serialize("{}", opt.blockKeys); var removeRequest = Login.Block.remove(opt.blockKeys); - console.log('Test block URL:', blockUrl); + console.warn('Testing block URL (%s). One 404 is normal.', blockUrl); var userHash = '/2/drive/edit/000000000000000000000000'; var secret = Hash.getSecrets('drive', userHash); @@ -375,7 +375,7 @@ define([ }); assert(function (cb, msg) { - msg = msg; + msg.innerText = "This test is incorrect."; return void cb(true); /* msg.appendChild(h('span', [ @@ -419,7 +419,6 @@ define([ $.ajax('/api/broadcast', { dataType: 'text', complete: function (xhr) { - console.log(xhr); cb(xhr.status === 200); }, }); @@ -445,6 +444,7 @@ define([ var expect = { 'cross-origin-resource-policy': 'cross-origin', + 'cross-origin-embedder-policy': 'require-corp', }; var incorrect = Object.keys(expect).some(function (k) { var response = xhr.getResponseHeader(k); From d9b9ca680fee7739256317c39c309d3d2f91cf9a Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 17 May 2021 11:00:25 +0200 Subject: [PATCH 006/424] Fix calendar day offset --- www/calendar/inner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/calendar/inner.js b/www/calendar/inner.js index 58262f956..58ad22ac1 100644 --- a/www/calendar/inner.js +++ b/www/calendar/inner.js @@ -119,7 +119,7 @@ define([ }; var getWeekDays = function (large) { - var baseDate = new Date(Date.UTC(2017, 0, 1)); // just a Sunday + var baseDate = new Date(2017, 0, 1); // just a Sunday var weekDays = []; for(var i = 0; i < 7; i++) { weekDays.push(baseDate.toLocaleDateString(undefined, { weekday: 'long' })); From e18dd626707ae0b2c6a7d15f3988180a0b259b0f Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 17 May 2021 11:00:25 +0200 Subject: [PATCH 007/424] Fix calendar day offset --- www/calendar/inner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/calendar/inner.js b/www/calendar/inner.js index 58262f956..58ad22ac1 100644 --- a/www/calendar/inner.js +++ b/www/calendar/inner.js @@ -119,7 +119,7 @@ define([ }; var getWeekDays = function (large) { - var baseDate = new Date(Date.UTC(2017, 0, 1)); // just a Sunday + var baseDate = new Date(2017, 0, 1); // just a Sunday var weekDays = []; for(var i = 0; i < 7; i++) { weekDays.push(baseDate.toLocaleDateString(undefined, { weekday: 'long' })); From eeb2638182e8f186d49afa6f70c5c99f9a074801 Mon Sep 17 00:00:00 2001 From: Weblate Date: Wed, 12 May 2021 01:56:26 +0200 Subject: [PATCH 008/424] Translated using Weblate (German) Currently translated at 100.0% (1230 of 1230 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/de/ --- www/common/translations/messages.de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/translations/messages.de.json b/www/common/translations/messages.de.json index 3177b4c75..9ed337bea 100644 --- a/www/common/translations/messages.de.json +++ b/www/common/translations/messages.de.json @@ -383,7 +383,7 @@ "settings_resetThumbnailsDone": "Alle Vorschaubilder wurden entfernt.", "settings_importTitle": "Importiere die kürzlich besuchten Dokumente in dein CryptDrive", "settings_import": "Importieren", - "settings_importConfirm": "Bist du sicher, dass du die kürzlich besuchten Pads in das CryptDrive deines Accounts importieren möchtest??", + "settings_importConfirm": "Bist du sicher, dass du die kürzlich besuchten Pads in das CryptDrive deines Accounts importieren möchtest?", "settings_importDone": "Import abgeschlossen", "settings_autostoreTitle": "Speichern von Pads im CryptDrive", "settings_autostoreHint": "Automatisch: Alle Pads werden in deinem CryptDrive gespeichert.
Manuell (immer nachfragen): Wenn du ein Pad noch nicht gespeichert hast, wirst du gefragt, ob du es im CryptDrive speichern willst.
Manuell (nie nachfragen): Pads werden nicht automatisch im CryptDrive gespeichert. Die Option zum Speichern wird versteckt.", From 2ed25c38fb6aed00397e645fb9dd8d17aa32eec4 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 18 May 2021 12:25:53 +0530 Subject: [PATCH 009/424] display more information about incorrect headers on checkup page --- www/checkup/main.js | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/www/checkup/main.js b/www/checkup/main.js index 6983e461c..b17709a26 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -424,7 +424,11 @@ define([ }); }); - var checkAPIHeaders = function (url, cb) { + var code = function (content) { + return h('code', content); + }; + + var checkAPIHeaders = function (url, msg, cb) { $.ajax(url, { dataType: 'text', complete: function (xhr) { @@ -446,14 +450,29 @@ define([ 'cross-origin-resource-policy': 'cross-origin', 'cross-origin-embedder-policy': 'require-corp', }; - var incorrect = Object.keys(expect).some(function (k) { + var incorrect = false; + + Object.keys(expect).forEach(function (k) { var response = xhr.getResponseHeader(k); - if (response !== expect[k]) { - return true; + var expected = expect[k]; + if (response !== expected) { + incorrect = true; + msg.appendChild(h('p', [ + 'The ', + code(k), + ' header for ', + code(url), + " is '", + code(response), + "' instead of '", + code(expected), + "' as expected.", + ])); + } }); - if (duplicated || incorrect) { console.error(allHeaders); } + if (duplicated || incorrect) { console.debug(allHeaders); } cb(!duplicated && !incorrect); }, }); @@ -464,13 +483,13 @@ define([ assert(function (cb, msg) { var url = '/api/config'; msg.innerText = url + INCORRECT_HEADER_TEXT; - checkAPIHeaders(url, cb); + checkAPIHeaders(url, msg, cb); }); assert(function (cb, msg) { var url = '/api/broadcast'; msg.innerText = url + INCORRECT_HEADER_TEXT; - checkAPIHeaders(url, cb); + checkAPIHeaders(url, msg, cb); }); var setWarningClass = function (msg) { From f5c029e937de0bf02079e2f1e7ebca2bb09f7558 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 18 May 2021 10:47:11 +0200 Subject: [PATCH 010/424] Add config options to prevent anonymous users from creating pads #704 --- www/common/application_config_internal.js | 2 ++ www/common/sframe-common.js | 3 +++ 2 files changed, 5 insertions(+) diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index 791ecf367..904fe7e45 100644 --- a/www/common/application_config_internal.js +++ b/www/common/application_config_internal.js @@ -134,6 +134,8 @@ define(function() { // Prevent anonymous users from storing pads in their drive AppConfig.disableAnonymousStore = false; + // Prevent anonymous users from creating new pads (they can still access and edit existing ones) + AppConfig.disableAnonymousPadCreation = false; // Hide the usage bar in settings and drive //AppConfig.hideUsageBar = true; diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 70c239d67..227656040 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -448,6 +448,9 @@ define([ } }; funcs.createPad = function (cfg, cb) { + if (AppConfig.disableAnonymousPadCreation && !funcs.isLoggedIn()) { + return void UI.errorLoadingScreen(Messages.mustLogin); + } ctx.sframeChan.query("Q_CREATE_PAD", { owned: cfg.owned, expire: cfg.expire, From d819ff093d8386f39cc9e3c85ff4a2bcfa9af331 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 18 May 2021 10:59:48 +0200 Subject: [PATCH 011/424] Fix date rendering with 12h format in flatpickr inputs --- www/admin/inner.js | 4 ++++ www/lib/calendar/date-picker.js | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/www/admin/inner.js b/www/admin/inner.js index 8e3ed16d2..527b081c5 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -1314,19 +1314,23 @@ define([ var $start = $(start); var $end = $(end); var is24h = false; + var dateFormat = "Y-m-d H:i"; try { is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); } catch (e) {} + if (!is24h) { dateFormat = "Y-m-d h:i K"; } var endPickr = Flatpickr(end, { enableTime: true, time_24hr: is24h, + dateFormat: dateFormat, minDate: new Date() }); Flatpickr(start, { enableTime: true, time_24hr: is24h, minDate: new Date(), + dateFormat: dateFormat, onChange: function () { endPickr.set('minDate', new Date($start.val())); } diff --git a/www/lib/calendar/date-picker.js b/www/lib/calendar/date-picker.js index bc42dbbeb..d66e98c3d 100644 --- a/www/lib/calendar/date-picker.js +++ b/www/lib/calendar/date-picker.js @@ -9,14 +9,17 @@ define([ var end = cfg.endpicker; var is24h = false + var dateFormat = "Y-m-d H:i"; try { is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); } catch (e) {} + if (!is24h) { dateFormat = "Y-m-d h:i K"; } var e = $(end.input)[0]; var endPickr = Flatpickr(e, { enableTime: true, time_24hr: is24h, + dateFormat: dateFormat, minDate: start.date }); endPickr.setDate(end.date); @@ -25,6 +28,7 @@ define([ var startPickr = Flatpickr(s, { enableTime: true, time_24hr: is24h, + dateFormat: dateFormat, onChange: function () { endPickr.set('minDate', startPickr.parseDate(s.value)); } From 755300d742ea4d27da4d449b02b8c701d27fc78d Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 18 May 2021 12:31:31 +0200 Subject: [PATCH 012/424] Add email and support config in the admin UI --- www/admin/inner.js | 117 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 7 deletions(-) diff --git a/www/admin/inner.js b/www/admin/inner.js index 527b081c5..5bc76d011 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -16,6 +16,7 @@ define([ '/support/ui.js', '/lib/datepicker/flatpickr.js', + '/bower_components/tweetnacl/nacl-fast.min.js', 'css!/lib/datepicker/flatpickr.min.css', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', @@ -44,6 +45,7 @@ define([ 'instanceStatus': {} }; + var Nacl = window.nacl; var common; var sFrameChan; @@ -54,6 +56,7 @@ define([ 'cp-admin-archive', 'cp-admin-unarchive', 'cp-admin-registration', + 'cp-admin-email' ], 'quota': [ // Msg.admin_cat_quota 'cp-admin-defaultlimit', @@ -71,7 +74,8 @@ define([ ], 'support': [ // Msg.admin_cat_support 'cp-admin-support-list', - 'cp-admin-support-init' + 'cp-admin-support-init', + 'cp-admin-support-priv', ], 'broadcast': [ // Msg.admin_cat_broadcast 'cp-admin-maintenance', @@ -285,6 +289,48 @@ define([ return $div; }; + Messages.admin_emailTitle = "Admin contact email"; // XXX + Messages.admin_emailHint = "Set the contact email for your instance here"; // XXX + Messages.admin_emailButton = "Update"; + create['email'] = function () { + var key = 'email'; + var $div = makeBlock(key, true); // Msg.admin_emailHint, Msg.admin_emailTitle, Msg.admin_emailButton + var $button = $div.find('button'); + + var input = h('input', { + type: 'email', + value: ApiConfig.adminEmail || '' + }); + var $input = $(input); + var innerDiv = h('div.cp-admin-setlimit-form', input); + var spinner = UI.makeSpinner($(innerDiv)); + + $button.click(function () { + if (!$input.val()) { return; } + spinner.spin(); + $button.attr('disabled', 'disabled'); + sFrameChan.query('Q_ADMIN_RPC', { + cmd: 'ADMIN_DECREE', + data: ['SET_ADMIN_EMAIL', [$input.val()]] + }, function (e, response) { + $button.removeAttr('disabled'); + if (e || response.error) { + UI.warn(Messages.error); + $input.val(''); + console.error(e, response); + spinner.hide(); + return; + } + spinner.done(); + UI.log(Messages.saved); + }); + }); + + $button.before(innerDiv); + + return $div; + }; + var getPrettySize = UIElements.prettySize; create['defaultlimit'] = function () { @@ -651,8 +697,13 @@ define([ }; var supportKey = ApiConfig.supportMailbox; + var checkAdminKey = function (priv) { + if (!supportKey) { return; } + return Hash.checkBoxKeyPair(priv, supportKey); + }; + create['support-list'] = function () { - if (!supportKey || !APP.privateKey) { return; } + if (!supportKey || !APP.privateKey || !checkAdminKey(APP.privateKey)) { return; } var $container = makeBlock('support-list'); // Msg.admin_supportListHint, .admin_supportListTitle var $div = $(h('div.cp-support-container')).appendTo($container); @@ -913,15 +964,66 @@ define([ }; - var checkAdminKey = function (priv) { - if (!supportKey) { return; } - return Hash.checkBoxKeyPair(priv, supportKey); - }; + Messages.admin_supportPrivTitle = "Support admin key"; // XXX + Messages.admin_supportPrivHint = "Display the private key allowing other admins to access the support. A form to enter this key will be displayed in their admin panel."; + Messages.admin_supportPrivButton = "Show key"; + create['support-priv'] = function () { + if (!supportKey || !APP.privateKey || !checkAdminKey(APP.privateKey)) { return; } + var $div = makeBlock('support-priv', true); // Msg.admin_supportPrivHint, .admin_supportPrivTitle, .admin_supportPrivButton + var $button = $div.find('button').click(function () { + $button.remove(); + $div.append(h('pre', APP.privateKey)); + }); + return $div; + }; + Messages.admin_supportInitGenerate = "Generate support keys"; // XXX create['support-init'] = function () { var $div = makeBlock('support-init'); // Msg.admin_supportInitHint, .admin_supportInitTitle if (!supportKey) { - $div.append(h('p', Messages.admin_supportInitHelp)); + (function () { + $div.append(h('p', Messages.admin_supportInitHelp)); // XXX Update text for this key + var button = h('button.btn.btn-primary', Messages.admin_supportInitGenerate); + var $button = $(button).appendTo($div); + $div.append($button); + var spinner = UI.makeSpinner($div); + $button.click(function () { + spinner.spin(); + $button.attr('disabled', 'disabled'); + var keyPair = Nacl.box.keyPair(); + var pub = Nacl.util.encodeBase64(keyPair.publicKey); + var priv = Nacl.util.encodeBase64(keyPair.secretKey); + // Store the private key first. It won't be used until the decree is accepted. + sFrameChan.query("Q_ADMIN_MAILBOX", priv, function (err, obj) { + if (err || (obj && obj.error)) { + console.error(err || obj.error); + UI.warn(Messages.error); + spinner.hide(); + return; + } + // Then send the decree + sFrameChan.query('Q_ADMIN_RPC', { + cmd: 'ADMIN_DECREE', + data: ['SET_SUPPORT_MAILBOX', [pub]] + }, function (e, response) { + $button.removeAttr('disabled'); + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); + spinner.hide(); + return; + } + spinner.done(); + UI.log(Messages.saved); + supportKey = pub; + APP.privateKey = priv; + $('.cp-admin-support-init').hide(); + APP.$rightside.append(create['support-list']()); + APP.$rightside.append(create['support-priv']()); + }); + }); + }); + })(); return $div; } if (!APP.privateKey || !checkAdminKey(APP.privateKey)) { @@ -951,6 +1053,7 @@ define([ APP.privateKey = key; $('.cp-admin-support-init').hide(); APP.$rightside.append(create['support-list']()); + APP.$rightside.append(create['support-priv']()); }); }); return $div; From 7ce2b67fd62e1d486768adb240ed906f187a9822 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 19 May 2021 13:46:52 +0530 Subject: [PATCH 013/424] replace 'pricing' link text with 'features' when subscriptions are disabled addresses #683 --- customize.dist/pages.js | 13 ++++++++++++- customize.dist/pages/features.js | 14 ++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/customize.dist/pages.js b/customize.dist/pages.js index e3ae2b83e..26b0e7a4e 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -43,6 +43,17 @@ define([ return Pages.externalLink(el, Pages.localizeDocsLink(href)); }; + var accounts = Pages.accounts = { + donateURL: AppConfig.donateURL || "https://opencollective.com/cryptpad/", + upgradeURL: AppConfig.upgradeURL + }; + + Pages.areSubscriptionsAllowed = function () { + try { + return ApiConfig.allowSubscriptions && accounts.upgradeURL && !ApiConfig.restrictRegistration; + } catch (err) { return void console.error(err); } + }; + var languageSelector = function () { var options = []; var languages = Msg._languages; @@ -133,7 +144,7 @@ define([ footerCol('footer_product', [ footLink('/what-is-cryptpad.html', 'topbar_whatIsCryptpad'), Pages.docsLink, - footLink('/features.html', 'pricing'), + footLink('/features.html', Pages.areSubscriptionsAllowed()? 'pricing': 'features'), // Messages.pricing, Messages.features Pages.githubLink, footLink('https://opencollective.com/cryptpad/contribute/', 'footer_donate'), ]), diff --git a/customize.dist/pages/features.js b/customize.dist/pages/features.js index ee82d5d67..1dfd2417f 100644 --- a/customize.dist/pages/features.js +++ b/customize.dist/pages/features.js @@ -8,10 +8,7 @@ define([ '/api/config', '/common/common-ui-elements.js', ], function ($, h, Msg, AppConfig, LocalStore, Pages, Config, UIElements) { - var accounts = { - donateURL: AppConfig.donateURL || "https://opencollective.com/cryptpad/", - upgradeURL: AppConfig.upgradeURL - }; + var accounts = Pages.accounts; return function () { Msg.features_f_apps_note = AppConfig.availablePadTypes.map(function (app) { @@ -145,10 +142,11 @@ define([ ]), ]), ]); - var availableFeatures = - (Config.allowSubscriptions && accounts.upgradeURL && !Config.restrictRegistration) ? - [anonymousFeatures, registeredFeatures, premiumFeatures] : - [anonymousFeatures, registeredFeatures]; + var availableFeatures = [ + anonymousFeatures, + registeredFeatures, + Pages.areSubscriptionsAllowed() ? premiumFeatures: undefined, + ]; return h('div#cp-main', [ Pages.infopageTopbar(), From 10f52230a46909551b4f41f02208c048bd445ece Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 20 May 2021 10:43:29 +0200 Subject: [PATCH 014/424] Form app prototype --- .../src/less2/include/colortheme-dark.less | 1 + .../src/less2/include/colortheme.less | 1 + www/common/application_config_internal.js | 3 +- www/common/common-ui-elements.js | 1 + www/form/app-form.less | 92 +++++ www/form/index.html | 12 + www/form/inner.html | 20 + www/form/inner.js | 355 ++++++++++++++++++ www/form/main.js | 90 +++++ 9 files changed, 574 insertions(+), 1 deletion(-) create mode 100644 www/form/app-form.less create mode 100644 www/form/index.html create mode 100644 www/form/inner.html create mode 100644 www/form/inner.js create mode 100644 www/form/main.js diff --git a/customize.dist/src/less2/include/colortheme-dark.less b/customize.dist/src/less2/include/colortheme-dark.less index 588d1888e..3443aaeb0 100644 --- a/customize.dist/src/less2/include/colortheme-dark.less +++ b/customize.dist/src/less2/include/colortheme-dark.less @@ -10,6 +10,7 @@ code: #EAA000; slide: #e57614; poll: #2c9e98; + form: #2c9e98; whiteboard: #a72ba7; kanban: #8C4; sheet: #40865c; diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less index 3ab249f9f..e56676213 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -10,6 +10,7 @@ code: #EAA000; slide: #e57614; poll: #2c9e98; + form: #2c9e98; whiteboard: #a72ba7; kanban: #8C4; sheet: #40865c; diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index 904fe7e45..267316541 100644 --- a/www/common/application_config_internal.js +++ b/www/common/application_config_internal.js @@ -12,7 +12,7 @@ define(function() { * You should never remove the drive from this list. */ AppConfig.availablePadTypes = ['drive', 'teams', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard', - /*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts' /*, 'calendar' */]; + /*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts', 'form']; /* The registered only types are apps restricted to registered users. * You should never remove apps from this list unless you know what you're doing. The apps * listed here by default can't work without a user account. @@ -117,6 +117,7 @@ define(function() { code: 'cptools-code', slide: 'cptools-slide', poll: 'cptools-poll', + form: 'cptools-poll', whiteboard: 'cptools-whiteboard', todo: 'cptools-todo', contacts: 'fa-address-book', diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 887eb147a..e8c0cce4c 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -2050,6 +2050,7 @@ define([ AppConfig.registeredOnlyTypes.indexOf(p) !== -1) { return; } return true; }); + Messages.type.form = "Form"; // XXX types.forEach(function (p) { var $element = $('
  • ', { 'class': 'cp-icons-element', diff --git a/www/form/app-form.less b/www/form/app-form.less new file mode 100644 index 000000000..996de4ad7 --- /dev/null +++ b/www/form/app-form.less @@ -0,0 +1,92 @@ +@import (reference) '../../customize/src/less2/include/framework.less'; +@import (reference) '../../customize/src/less2/include/tools.less'; +@import (reference) '../../customize/src/less2/include/avatar.less'; + +&.cp-app-form { + @form_input-width: 400px; + + .framework_main( + @bg-color: @colortheme_apps[form] + ); + + display: flex; + flex-flow: column; + + #cp-app-form-editor { + flex: 1; + display: flex; + flex-flow: row; + height: 100%; + overflow: hidden; + } + + #cp-app-form-container { + display: flex; + flex: 1; + justify-content: center; + + div.cp-form-creator-container { + display: flex; + flex: 1; + max-width: 1300px; + div.cp-form-creator-control { + padding: 10px; + display: flex; + flex-flow: column; + width: 300px; + } + div.cp-form-creator-content { + padding: 10px; + display: flex; + flex-flow: column; + flex: 1; + .cp-form-block { + &:not(:last-child) { + margin-bottom: 20px; + } + .cp-form-input-block { + display: flex; + //width: @form_input-width; + &:not(:focus-within) { + input { + background: transparent; + border: none; + & ~ button:not(:disabled) { + .cp-form-edit { display: inline; } + .cp-form-save { display: none; } + } + } + } + input { + flex: 1; + min-width: 100px; + } + button { + .cp-form-edit { + display: none; + margin: 0 !important; + } + .cp-form-save { display: inline; } + } + } + } + .cp-form-edit-block { + .cp-form-edit-block-input { + display: flex; + width: 400px; + input { + flex: 1; + min-width: 100px; + } + button { + i { margin: 0 !important; } + } + + } + } + } + } + } + +} + diff --git a/www/form/index.html b/www/form/index.html new file mode 100644 index 000000000..96a3cce86 --- /dev/null +++ b/www/form/index.html @@ -0,0 +1,12 @@ + + + + CryptPad + + + + + + + + diff --git a/www/form/inner.html b/www/form/inner.html new file mode 100644 index 000000000..de37af4f6 --- /dev/null +++ b/www/form/inner.html @@ -0,0 +1,20 @@ + + + + + + + + +
    +
    +
    +
    + + + diff --git a/www/form/inner.js b/www/form/inner.js new file mode 100644 index 000000000..d9393faf1 --- /dev/null +++ b/www/form/inner.js @@ -0,0 +1,355 @@ +define([ + 'jquery', + 'json.sortify', + '/bower_components/chainpad-crypto/crypto.js', + '/common/sframe-app-framework.js', + '/common/toolbar.js', + '/bower_components/nthen/index.js', + '/common/sframe-common.js', + '/common/common-util.js', + '/common/common-hash.js', + '/common/common-interface.js', + '/common/common-ui-elements.js', + '/common/clipboard.js', + '/common/inner/common-mediatag.js', + '/common/hyperscript.js', + '/customize/messages.js', + '/customize/application_config.js', + + '/common/inner/share.js', + '/common/inner/access.js', + '/common/inner/properties.js', + + '/bower_components/file-saver/FileSaver.min.js', + 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', + 'less!/form/app-form.less', +], function ( + $, + JSONSortify, + Crypto, + Framework, + Toolbar, + nThen, + SFCommon, + Util, + Hash, + UI, + UIElements, + Clipboard, + MT, + h, + Messages, + AppConfig, + Share, Access, Properties + ) +{ + var SaveAs = window.saveAs; + var APP = window.APP = { + }; + + Messages.button_newform = "New Form"; // XXX + Messages.form_invalid = "Invalid form"; + Messages.form_editBlock = "Edit options"; + + Messages.form_newOption = "New option"; + + Messages.form_default = "Your question here?"; + Messages.form_type_input = "Text"; // XXX + Messages.form_type_radio = "Radio"; // XXX + + Messages.form_duplicates = "Duplicate entries have been removed"; + + var makeFormSettings = function (framework) { + // XXX + // Button to set results as public + // Checkbox to allow anonymous answers + // Button to clear all answers? + }; + + var TYPES = { + input: { + get: function () { + var tag = h('input'); + var $tag = $(tag); + return { + tag: tag, + getValue: function () { return $tag.val(); }, + //setValue: function (val) { $tag.val(val); } + }; + }, + icon: h('i.fa.fa-font') + }, + radio: { + defaultOpts: { + values: ["Option 1", "Option 2"] // XXX? + }, + get: function (opts) { + if (!opts) { opts = TYPES.radio.defaultOpts; } + var name = Util.uid(); + var els = opts.values.map(function (data, i) { + return UI.createRadio(name, 'cp-form-'+name+'-'+i, + data, false, { mark: {tabindex:1} }); + }); + var tag = h('div.radio-group', els); + return { + tag: tag, + getValue: function () { + var res; + els.some(function (el, i) { + if (Util.isChecked($(el).find('input'))) { + res = opts.values[i]; + } + }); + return res; + }, + edit: function (cb) { + var v = opts.values.slice(); + + var add = h('button.btn.btn-secondary', [ + h('i.fa.fa-plus'), + h('span', Messages.tag_add) + ]); + + // Show existing options + var getOption = function (val) { + var input = h('input', {value:val}); + var del = h('button.btn.btn-danger', h('i.fa.fa-times')); + var el = h('div.cp-form-edit-block-input', [ input, del ]); + $(del).click(function () { $(el).remove(); }); + return el; + }; + var inputs = v.map(getOption); + inputs.push(add); + var container = h('div.cp-form-edit-block', inputs); + + // Add option + var $add = $(add).click(function () { + $add.before(getOption(Messages.form_newOption)); + }); + + // Cancel changes + var cancelBlock = h('button.btn.btn-secondary', Messages.cancel); + $(cancelBlock).click(function () { cb(); }); + + // Save changes + var saveBlock = h('button.btn.btn-primary', [ + h('i.fa.fa-floppy-o'), + h('span', Messages.settings_save) + ]); + $(saveBlock).click(function () { + $(saveBlock).attr('disabled', 'disabled'); + var values = []; + var duplicates = false; + $(container).find('input').each(function (i, el) { + var val = $(el).val().trim(); + if (values.indexOf(val) === -1) { values.push(val); } + else { duplicates = true; } + }); + if (duplicates) { + UI.warn(Messages.form_duplicates); + } + cb({values: values}); + }); + + return [ + container, + h('div', [cancelBlock, saveBlock]) + ]; + } + //setValue: function (val) {} + }; + + }, + icon: h('i.fa.fa-list-ul') + } + }; + + var renderForm = function (content, editable) { + + }; + var updateForm = function (framework, content, editable) { + var $container = $('div.cp-form-creator-content'); + + var form = content.form; + + // XXX order array later + var elements = Object.keys(form).map(function (uid) { + var block = form[uid]; + var type = block.type; + var model = TYPES[type]; + if (!model) { return; } + var data = model.get(block.opts); + var q = h('div.cp-form-block-question', block.q || Messages.form_default); + var edit, editContainer; + if (editable) { + // Question + + var inputQ = h('input', { + value: block.q || Messages.form_default + }); + var $inputQ = $(inputQ); + var saveQ = h('button.btn.btn-primary', [ + h('i.fa.fa-pencil.cp-form-edit'), + h('span.cp-form-save', Messages.settings_save) + ]); + var $saveQ = $(saveQ).click(function () { + var v = $inputQ.val(); + if (!v || !v.trim() || v === block.q) { return; } + block.q = v.trim(); + framework.localChange(); + $saveQ.attr('disabled', 'disabled'); + framework._.cpNfInner.chainpad.onSettle(function () { + $saveQ.removeAttr('disabled'); + $saveQ.blur(); + UI.log(Messages.saved); + }); + }); + var onBlur = function (e) { + if (e && e.relatedTarget && e.relatedTarget === saveQ) { return; } + $inputQ.val(block.q); + }; + $inputQ.keydown(function (e) { + if (e.which === 13) { return void $saveQ.click(); } + if (e.which === 27) { return void $inputQ.blur(); } + }); + $inputQ.blur(onBlur); + q = h('div.cp-form-input-block', [inputQ, saveQ]); + + // Values + if (data.edit) { + edit = h('button.btn.btn-primary.cp-form-edit-button', [ + h('i.fa.fa-pencil'), + h('span', Messages.form_editBlock) + ]); + editContainer = h('div'); + var onSave = function (newOpts) { + if (!newOpts) { // Cancel edit + $(editContainer).empty(); + $edit.show(); + $(data.tag).show(); + return; + } + $(editContainer).empty(); + block.opts = newOpts; + var $oldTag = $(data.tag); + framework._.cpNfInner.chainpad.onSettle(function () { + $edit.show(); + UI.log(Messages.saved); + data = model.get(newOpts); + $oldTag.before(data.tag).remove(); + }); + }; + var $edit = $(edit).click(function () { + $(data.tag).hide(); + $(editContainer).append(data.edit(onSave)); + $edit.hide(); + }); + } + } + return h('div.cp-form-block', [ + q, + h('div.cp-form-block-content', [ + data.tag, + edit + ]), + editContainer + ]); + }); + + $container.empty().append(elements); + }; + + var andThen = function (framework) { + framework.start(); + var content = {}; + + var $container = $('#cp-app-form-container'); + + var makeFormCreator = function () { + var controls = Object.keys(TYPES).map(function (type) { + + var btn = h('button.btn', [ + TYPES[type].icon.cloneNode(), + h('span', Messages['form_type_'+type]) + ]); + $(btn).click(function () { + var uid = Util.uid(); + content.form[uid] = { + //q: Messages.form_default, + //opts: opts + type: type, + }; + framework.localChange(); + updateForm(framework, content, true); + }); + return btn; + }); + var controlContainer = h('div.cp-form-creator-control', controls); + + var contentContainer = h('div.cp-form-creator-content'); + var div = h('div.cp-form-creator-container', [ + controlContainer, + contentContainer, + ]); + return div; + }; + + $container.append(makeFormCreator()); + + var sframeChan = framework._.sfCommon.getSframeChannel(); + var metadataMgr = framework._.cpNfInner.metadataMgr; + var priv = metadataMgr.getPrivateData(); + APP.isEditor = Boolean(priv.form_public); + + framework.onReady(function (isNew) { + var priv = metadataMgr.getPrivateData(); + if (APP.isEditor) { + if (!content.form) { + content.form = {}; + framework.localChange(); + } + if (!content.answers || !content.answers.channel || !content.answers.publicKey) { + content.answers = { + channel: Hash.createChannelId(), + publicKey: priv.form_public + }; + framework.localChange(); + } + } + + if (!content.answers || !content.answers.channel || !content.answers.publicKey) { + return void UI.errorLoadingScreen(Messages.form_invalid); + } + // XXX fetch answers and + // * viewers ==> check if you've already answered and show form (new or edit) + // * editors ==> show schema and warn users if existing questions already have answers + if (APP.isEditor) { + sframeChan.query("Q_FORM_FETCH_ANSWERS", { + channel: content.answers.channel, + publicKey: content.answers.publicKey + }, function () { + updateForm(framework, content, true); + + }); + return; + } + updateForm(framework, content, false); + + }); + + framework.onContentUpdate(function (newContent) { + console.log(newContent); + content = newContent; + }); + + framework.setContentGetter(function () { + return content; + }); + + }; + + Framework.create({ + toolbarContainer: '#cp-toolbar', + contentContainer: '#cp-app-form-editor', + }, andThen); +}); diff --git a/www/form/main.js b/www/form/main.js new file mode 100644 index 000000000..be47466e3 --- /dev/null +++ b/www/form/main.js @@ -0,0 +1,90 @@ +// Load #1, load as little as possible because we are in a race to get the loading screen up. +define([ + '/bower_components/nthen/index.js', + '/api/config', + '/common/dom-ready.js', + '/common/sframe-common-outer.js', + '/bower_components/tweetnacl/nacl-fast.min.js', +], function (nThen, ApiConfig, DomReady, SFCommonO) { + var Nacl = window.nacl; + + // Loaded in load #2 + nThen(function (waitFor) { + DomReady.onReady(waitFor()); + }).nThen(function (waitFor) { + var obj = SFCommonO.initIframe(waitFor, true); + href = obj.href; + hash = obj.hash; + }).nThen(function (/*waitFor*/) { + var privateKey, publicKey; + var addData = function (meta, CryptPad, user, Utils) { + var keys = Utils.secret && Utils.secret.keys; + var secondary = keys && keys.secondaryKey; + if (!secondary) { return; } + var curvePair = Nacl.box.keyPair.fromSecretKey(Nacl.util.decodeUTF8(secondary).slice(0,32)); + publicKey = meta.form_public = Nacl.util.encodeBase64(curvePair.publicKey); + privateKey = Nacl.util.encodeBase64(curvePair.secretKey); + }; + var addRpc = function (sframeChan, Cryptpad, Utils) { + sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, cb) { + var keys; + var CPNetflux; + var network; + nThen(function (w) { + require([ + '/bower_components/chainpad-netflux/chainpad-netflux.js', + ], w(function (_CPNetflux, _Crypto) { + CPNetflux = _CPNetflux; + })); + Cryptpad.getAccessKeys(w(function (_keys) { + keys = _keys; + })); + Cryptpad.makeNetwork(w(function (err, nw) { + network = nw; + })); + }).nThen(function (w) { + if (!network) { return void cb({error: "E_CONNECT"}); } + + var keys = Utils.secret && Utils.secret.keys; + + var crypto = Utils.Crypto.Mailbox.createEncryptor({ + curvePrivate: privateKey, + curvePublic: publicKey || data.publicKey + }); + var config = { + network: network, + channel: data.channel, + noChainPad: true, + validateKey: keys.secondaryValidateKey, + owners: [], // XXX add pad owner + crypto: crypto, + // XXX Cache + }; + config.onReady = function () { + cb(); + // XXX + }; + config.onMessage = function () { + // XXX + }; + CPNetflux.start(config); + }); + }); + sframeChan.on('EV_FORM_MAILBOX', function (data) { + var curvePair = Nacl.box.keyPair(); + publicKey = Nacl.util.encodeBase64(curvePair.publicKey); + privateKey = Nacl.util.encodeBase64(curvePair.secretKey); + }); + }; + SFCommonO.start({ + addData: addData, + addRpc: addRpc, + cache: true, + noDrive: true, + hash: hash, + href: href, + useCreationScreen: true, + messaging: true + }); + }); +}); From f2422483a5bd2bbed9ac20cf407e3f1df9e777b7 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 20 May 2021 14:37:04 +0530 Subject: [PATCH 015/424] note that some configurable restrictions are only enforced clientside and remove comments about an unsupported API addresses #704 --- www/common/application_config_internal.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index 904fe7e45..dcfb3872b 100644 --- a/www/common/application_config_internal.js +++ b/www/common/application_config_internal.js @@ -133,8 +133,10 @@ define(function() { AppConfig.displayCreationScreen = true; // Prevent anonymous users from storing pads in their drive + // NOTE: this is only enforced client-side as the server does not distinguish between users drives and pads AppConfig.disableAnonymousStore = false; // Prevent anonymous users from creating new pads (they can still access and edit existing ones) + // NOTE: this is only enforced client-side and will not prevent malicious clients from storing data AppConfig.disableAnonymousPadCreation = false; // Hide the usage bar in settings and drive @@ -143,11 +145,6 @@ define(function() { // Disable feedback for all the users and hide the settings part about feedback //AppConfig.disableFeedback = true; - // Add new options in the share modal (extend an existing tab or add a new tab). - // More info about how to use it on the wiki: - // https://github.com/xwiki-labs/cryptpad/wiki/Application-config#configcustomizeshareoptions XXX page is a 404 - //AppConfig.customizeShareOptions = function (hashes, tabs, config) {}; - // Add code to be executed on every page before loading the user object. `isLoggedIn` (bool) is // indicating if the user is registered or anonymous. Here you can change the way anonymous users // work in CryptPad, use an external SSO or even force registration From 87c6e3270dc2dd1fa887cf95f81365e9d3bb05df Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 20 May 2021 16:13:28 +0530 Subject: [PATCH 016/424] make a note about restricting channel creation to registered users --- lib/hk-util.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/hk-util.js b/lib/hk-util.js index 7c244c174..5a96970d0 100644 --- a/lib/hk-util.js +++ b/lib/hk-util.js @@ -701,6 +701,7 @@ const handleGetHistory = function (Env, Server, seq, userId, parsed) { } if (msgCount === 0 && !metadata_cache[channelName] && Server.channelContainsUser(channelName, userId)) { + // TODO this might be a good place to reject channel creation by anonymous users handleFirstMessage(Env, channelName, metadata); Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(metadata)]); } From 0c7f77f5ed7d8bd18fbb8daa7a17d46d3e5be6b5 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 20 May 2021 16:16:07 +0530 Subject: [PATCH 017/424] sketch out some more sandbox tests and note down some improvements --- www/checkup/app-checkup.less | 6 ++ www/checkup/main.js | 30 ++++-- www/checkup/sandbox/index.html | 11 +++ www/checkup/sandbox/main.js | 175 +++++++++++++++++++++++++++++++++ 4 files changed, 214 insertions(+), 8 deletions(-) create mode 100644 www/checkup/sandbox/index.html create mode 100644 www/checkup/sandbox/main.js diff --git a/www/checkup/app-checkup.less b/www/checkup/app-checkup.less index a475d5983..99999e2bb 100644 --- a/www/checkup/app-checkup.less +++ b/www/checkup/app-checkup.less @@ -12,6 +12,12 @@ html, body { color: @cryptpad_text_col; font-family: "IBM Plex Mono"; + iframe.sandbox-test { + display: block; + width: 100%; + height: 100%; + } + .report { font-size: 30px; max-width: 50%; diff --git a/www/checkup/main.js b/www/checkup/main.js index b17709a26..896156fc6 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -20,6 +20,7 @@ define([ ], function ($, ApiConfig, Assertions, h, Messages, DomReady, nThen, SFCommonO, Login, Hash, Util, Pinpad, NetConfig, Pages) { + var Assert = Assertions(); var trimSlashes = function (s) { if (typeof(s) !== 'string') { return s; } @@ -52,6 +53,13 @@ define([ var trimmedSafe = trimSlashes(ApiConfig.httpSafeOrigin); var trimmedUnsafe = trimSlashes(ApiConfig.httpUnsafeOrigin); +/* +// XXX display results from this iframe on this page + document.body.appendChild(h('iframe', { + class: 'sandbox-test', + src: trimmedSafe + '/checkup/sandbox/index.html', + })); +*/ assert(function (cb, msg) { msg.appendChild(h('span', [ @@ -117,7 +125,7 @@ define([ var checkAvailability = function (url, cb) { $.ajax({ - url: url, + url: url, // XXX bust cache data: {}, complete: function (xhr) { cb(xhr.status === 200); @@ -158,6 +166,7 @@ define([ ])); var to; + var obj; nThen(function (waitFor) { DomReady.onReady(waitFor()); }).nThen(function (waitFor) { @@ -165,8 +174,13 @@ define([ console.error('TIMEOUT loading iframe on the safe domain'); cb(false); }, 5000); - SFCommonO.initIframe(waitFor); + obj = SFCommonO.initIframe(waitFor); + }).nThen(function () { + SFCommonO.start({ + href: obj.href, + }); }).nThen(function () { + console.error("DONE?"); // Iframe is loaded clearTimeout(to); cb(true); @@ -344,7 +358,7 @@ define([ //'cross-origin-opener-policy': 'same-origin', // FIXME this is in our nginx config but not server.js }; - $.ajax(url, { + $.ajax(url, { // XXX bust cache complete: function (xhr) { cb(!Object.keys(expect).some(function (k) { var response = xhr.getResponseHeader(k); @@ -390,7 +404,7 @@ define([ RESTART_WARNING(), ])); - $.ajax(sheetURL, { + $.ajax(sheetURL, { // FIXME bust cache complete: function (xhr) { var csp = xhr.getResponseHeader('Content-Security-Policy'); if (!/unsafe\-eval/.test(csp)) { @@ -416,7 +430,7 @@ define([ "Your browser console may provide more details as to why this resource could not be loaded. ", ])); - $.ajax('/api/broadcast', { + $.ajax('/api/broadcast', { // XXX bust cache dataType: 'text', complete: function (xhr) { cb(xhr.status === 200); @@ -429,7 +443,7 @@ define([ }; var checkAPIHeaders = function (url, msg, cb) { - $.ajax(url, { + $.ajax(url, { // XXX bust cache dataType: 'text', complete: function (xhr) { var allHeaders = xhr.getAllResponseHeaders(); @@ -481,13 +495,13 @@ define([ var INCORRECT_HEADER_TEXT = ' was served with duplicated or incorrect headers. Compare your reverse-proxy configuration against the provided example.'; assert(function (cb, msg) { - var url = '/api/config'; + var url = '/api/config'; // XXX bust cache msg.innerText = url + INCORRECT_HEADER_TEXT; checkAPIHeaders(url, msg, cb); }); assert(function (cb, msg) { - var url = '/api/broadcast'; + var url = '/api/broadcast'; // XXX bust cache msg.innerText = url + INCORRECT_HEADER_TEXT; checkAPIHeaders(url, msg, cb); }); diff --git a/www/checkup/sandbox/index.html b/www/checkup/sandbox/index.html new file mode 100644 index 000000000..04a9502d3 --- /dev/null +++ b/www/checkup/sandbox/index.html @@ -0,0 +1,11 @@ + + + + + + + + +
    + + diff --git a/www/checkup/sandbox/main.js b/www/checkup/sandbox/main.js new file mode 100644 index 000000000..8066dc1b3 --- /dev/null +++ b/www/checkup/sandbox/main.js @@ -0,0 +1,175 @@ +define([ + 'jquery', + '/api/config', + '/assert/assertions.js', + '/common/hyperscript.js', + '/customize/messages.js', + '/common/dom-ready.js', + '/bower_components/nthen/index.js', + '/common/sframe-common-outer.js', + '/customize/login.js', + '/common/common-hash.js', + '/common/common-util.js', + '/common/pinpad.js', + '/common/outer/network-config.js', + '/customize/pages.js', + + '/bower_components/tweetnacl/nacl-fast.min.js', + 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', + 'less!/checkup/app-checkup.less', +], function ($, ApiConfig, Assertions, h, Messages, DomReady, + nThen, SFCommonO, Login, Hash, Util, Pinpad, + NetConfig, Pages) { + var Assert = Assertions(); + var assert = function (f, msg) { + Assert(f, msg || h('span.advisory-text.cp-danger')); + }; + + var code = function (content) { + return h('code', content); + }; + + var getHeaders = function (url, cb) { + $.ajax(url + "?test=" + (+new Date()), { + dataType: 'text', + complete: function (xhr) { + var allHeaders = xhr.getAllResponseHeaders(); + return void cb(void 0, allHeaders, xhr); + }, + }); + }; + var parseCSP = function (CSP) { + //console.error(CSP); + var CSP_headers = {}; + CSP.split(";") + .forEach(function (rule) { + rule = (rule || "").trim(); + if (!rule) { return; } + var parts = rule.split(/\s/); + var first = parts[0]; + var rest = rule.slice(first.length + 1); + CSP_headers[first] = rest; + //console.error(rule.trim()); + console.info("[%s] '%s'", first, rest); + }); + return CSP_headers; + }; + + var hasUnsafeEval = function (CSP_headers) { + return /unsafe\-eval/.test(CSP_headers['script-src']); + }; + + var hasUnsafeInline = function (CSP_headers) { + return /unsafe\-inline/.test(CSP_headers['script-src']); + }; + + var hasOnlyOfficeHeaders = function (CSP_headers) { + if (!hasUnsafeEval(CSP_headers)) { + console.error("NO_UNSAFE_EVAL"); + console.log(CSP_headers); + return false; + } + if (!hasUnsafeInline(CSP_headers)) { + console.error("NO_UNSAFE_INLINE"); + return void false; + } + return true; + }; + + // XXX run these from /checkup/inner.js and report to /checkup/main.js + assert(function (cb, msg) { + var url = '/sheet/inner.html'; + msg.appendChild(h('span', [ + code(url), + ' has the wrong headers.', + ])); + getHeaders(url, function (err, headers, xhr) { + var CSP_headers = parseCSP(xhr.getResponseHeader('content-security-policy')); + cb(hasOnlyOfficeHeaders(CSP_headers)); + }); + }); + + assert(function (cb, msg) { + var url = '/common/onlyoffice/v4/web-apps/apps/spreadsheeteditor/main/index.html'; + msg.appendChild(h('span', [ + code(url), + ' has the wrong headers.', + ])); + getHeaders(url, function (err, headers, xhr) { + var CSP_headers = parseCSP(xhr.getResponseHeader('content-security-policy')); + cb(hasOnlyOfficeHeaders(CSP_headers)); + }); + }); + + var row = function (cells) { + return h('tr', cells.map(function (cell) { + return h('td', cell); + })); + }; + + var failureReport = function (obj) { + return h('div.error', [ + h('h5', obj.message), + h('table', [ + row(["Failed test number", obj.test + 1]), + row(["Returned value", obj.output]), + ]), + ]); + }; + + var completed = 0; + var $progress = $('#cp-progress'); + + var versionStatement = function () { + return h('p', [ + "This instance is running ", + h('span.cp-app-checkup-version',[ + "CryptPad", + ' ', + Pages.versionString, + ]), + '.', + ]); + }; + + Assert.run(function (state) { + var errors = state.errors; + var failed = errors.length; + + Messages.assert_numberOfTestsPassed = "{0} / {1} tests passed."; + + var statusClass = failed? 'failure': 'success'; + + var failedDetails = "Details found below"; + var successDetails = "This checkup only tests the most common configuration issues. You may still experience errors or incorrect behaviour."; + var details = h('p', failed? failedDetails: successDetails); + + var summary = h('div.summary.' + statusClass, [ + versionStatement(), + h('p', Messages._getKey('assert_numberOfTestsPassed', [ + state.passed, + state.total + ])), + details, + ]); + + var report = h('div.report', [ + summary, + h('div.failures', errors.map(failureReport)), + ]); + + $progress.remove(); + $('body').prepend(report); + }, function (i, total) { + console.log('test '+ i +' completed'); + completed++; + Messages.assert_numberOfTestsCompleted = "{0} / {1} tests completed."; + $progress.html('').append(h('div.report.pending.summary', [ + versionStatement(), + h('p', [ + h('i.fa.fa-spinner.fa-pulse'), + h('span', Messages._getKey('assert_numberOfTestsCompleted', [completed, total])) + ]) + ])); + }); +}); From 3184ad419ea9aa9be0adf1daaf8a0bce7f65806d Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 20 May 2021 13:54:20 +0200 Subject: [PATCH 018/424] Ability to submit form answers --- www/common/common-hash.js | 6 + www/common/cryptpad-common.js | 11 ++ www/form/inner.js | 207 ++++++++++++++++++++++------------ www/form/main.js | 26 +++++ 4 files changed, 175 insertions(+), 75 deletions(-) diff --git a/www/common/common-hash.js b/www/common/common-hash.js index cf3ca76a4..c59f5babe 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -34,6 +34,12 @@ var factory = function (Util, Crypto, Keys, Nacl) { var keyPair = Nacl.sign.keyPair.fromSecretKey(privateKey); return Nacl.util.encodeBase64(keyPair.publicKey); }; + Hash.getCurvePublicFromPrivate = function (curvePrivateSafeStr) { + var curvePrivateStr = Crypto.b64AddSlashes(curvePrivateSafeStr); + var privateKey = Nacl.util.decodeBase64(curvePrivateStr); + var keyPair = Nacl.box.keyPair.fromSecretKey(privateKey); + return Nacl.util.encodeBase64(keyPair.publicKey); + }; var getEditHashFromKeys = Hash.getEditHashFromKeys = function (secret) { var version = secret.version; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index e9635d5b5..ba0624a08 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -100,6 +100,17 @@ define([ cb(keys); }); }; + common.getFormKeys = function (cb) { + postMessage("GET", { + key: ['curvePrivate'], + }, function (obj) { + if (obj.error) { return void cb(); } + cb({ + curvePrivate: obj, + curvePublic: Hash.getCurvePublicFromPrivate(obj) + }); + }); + }; common.makeNetwork = function (cb) { require([ diff --git a/www/form/inner.js b/www/form/inner.js index d9393faf1..f50583e37 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -59,6 +59,13 @@ define([ Messages.form_duplicates = "Duplicate entries have been removed"; + Messages.form_reset = "Reset"; + Messages.form_sent = "Sent"; + + // XXX to update our own answers, we need to store the server hash of the message + // and we'll be able to use getHistoryRange to fetch this message when we come back + + var makeFormSettings = function (framework) { // XXX // Button to set results as public @@ -66,6 +73,59 @@ define([ // Button to clear all answers? }; + var editOptions = function (v, cb) { + var add = h('button.btn.btn-secondary', [ + h('i.fa.fa-plus'), + h('span', Messages.tag_add) + ]); + + // Show existing options + var getOption = function (val) { + var input = h('input', {value:val}); + var del = h('button.btn.btn-danger', h('i.fa.fa-times')); + var el = h('div.cp-form-edit-block-input', [ input, del ]); + $(del).click(function () { $(el).remove(); }); + return el; + }; + var inputs = v.map(getOption); + inputs.push(add); + var container = h('div.cp-form-edit-block', inputs); + + // Add option + var $add = $(add).click(function () { + $add.before(getOption(Messages.form_newOption)); + }); + + // Cancel changes + var cancelBlock = h('button.btn.btn-secondary', Messages.cancel); + $(cancelBlock).click(function () { cb(); }); + + // Save changes + var saveBlock = h('button.btn.btn-primary', [ + h('i.fa.fa-floppy-o'), + h('span', Messages.settings_save) + ]); + $(saveBlock).click(function () { + $(saveBlock).attr('disabled', 'disabled'); + var values = []; + var duplicates = false; + $(container).find('input').each(function (i, el) { + var val = $(el).val().trim(); + if (values.indexOf(val) === -1) { values.push(val); } + else { duplicates = true; } + }); + if (duplicates) { + UI.warn(Messages.form_duplicates); + } + cb({values: values}); + }); + + return [ + container, + h('div', [cancelBlock, saveBlock]) + ]; + }; + var TYPES = { input: { get: function () { @@ -75,6 +135,7 @@ define([ tag: tag, getValue: function () { return $tag.val(); }, //setValue: function (val) { $tag.val(val); } + reset: function () { $tag.val(''); } }; }, icon: h('i.fa.fa-font') @@ -102,59 +163,10 @@ define([ }); return res; }, + reset: function () { $(tag).find('input').removeAttr('checked'); }, edit: function (cb) { var v = opts.values.slice(); - - var add = h('button.btn.btn-secondary', [ - h('i.fa.fa-plus'), - h('span', Messages.tag_add) - ]); - - // Show existing options - var getOption = function (val) { - var input = h('input', {value:val}); - var del = h('button.btn.btn-danger', h('i.fa.fa-times')); - var el = h('div.cp-form-edit-block-input', [ input, del ]); - $(del).click(function () { $(el).remove(); }); - return el; - }; - var inputs = v.map(getOption); - inputs.push(add); - var container = h('div.cp-form-edit-block', inputs); - - // Add option - var $add = $(add).click(function () { - $add.before(getOption(Messages.form_newOption)); - }); - - // Cancel changes - var cancelBlock = h('button.btn.btn-secondary', Messages.cancel); - $(cancelBlock).click(function () { cb(); }); - - // Save changes - var saveBlock = h('button.btn.btn-primary', [ - h('i.fa.fa-floppy-o'), - h('span', Messages.settings_save) - ]); - $(saveBlock).click(function () { - $(saveBlock).attr('disabled', 'disabled'); - var values = []; - var duplicates = false; - $(container).find('input').each(function (i, el) { - var val = $(el).val().trim(); - if (values.indexOf(val) === -1) { values.push(val); } - else { duplicates = true; } - }); - if (duplicates) { - UI.warn(Messages.form_duplicates); - } - cb({values: values}); - }); - - return [ - container, - h('div', [cancelBlock, saveBlock]) - ]; + return editOptions(v, cb); } //setValue: function (val) {} }; @@ -164,23 +176,60 @@ define([ } }; - var renderForm = function (content, editable) { + var makeFormControls = function (framework, content) { + var send = h('button.btn.btn-primary', Messages.poll_commit); + var reset = h('button.btn.btn-danger-alt', Messages.form_reset); + $(reset).click(function () { + if (!Array.isArray(APP.formBlocks)) { return; } + APP.formBlocks.forEach(function (data) { + if (typeof(data.reset) === "function") { data.reset(); } + }); + }); + var $send = $(send).click(function () { + $send.attr('disabled', 'disabled'); + if (!Array.isArray(APP.formBlocks)) { return; } + var results = {}; + APP.formBlocks.forEach(function (data) { + results[data.uid] = data.getValue(); + }); + var sframeChan = framework._.sfCommon.getSframeChannel(); + sframeChan.query('Q_FORM_SUBMIT', { + mailbox: content.answers, + results: results + }, function (err, data) { + console.error(data); + if (err || (data && data.error)) { + console.error(err || data.error); + return void UI.warn(Messages.error); + } + UI.alert(Messages.form_sent); + }); + }); + return h('div.cp-form-send-container', [send, reset]); }; var updateForm = function (framework, content, editable) { var $container = $('div.cp-form-creator-content'); var form = content.form; + APP.formBlocks = []; + // XXX order array later var elements = Object.keys(form).map(function (uid) { var block = form[uid]; var type = block.type; var model = TYPES[type]; if (!model) { return; } + var data = model.get(block.opts); + data.uid = uid; + var q = h('div.cp-form-block-question', block.q || Messages.form_default); var edit, editContainer; + + APP.formBlocks.push(data); + if (editable) { // Question @@ -231,6 +280,7 @@ define([ } $(editContainer).empty(); block.opts = newOpts; + framework.localChange(); var $oldTag = $(data.tag); framework._.cpNfInner.chainpad.onSettle(function () { $edit.show(); @@ -257,34 +307,43 @@ define([ }); $container.empty().append(elements); + $container.append(makeFormControls(framework, content)); }; var andThen = function (framework) { framework.start(); var content = {}; - var $container = $('#cp-app-form-container'); + var sframeChan = framework._.sfCommon.getSframeChannel(); + var metadataMgr = framework._.cpNfInner.metadataMgr; + + var priv = metadataMgr.getPrivateData(); + APP.isEditor = Boolean(priv.form_public); var makeFormCreator = function () { - var controls = Object.keys(TYPES).map(function (type) { - var btn = h('button.btn', [ - TYPES[type].icon.cloneNode(), - h('span', Messages['form_type_'+type]) - ]); - $(btn).click(function () { - var uid = Util.uid(); - content.form[uid] = { - //q: Messages.form_default, - //opts: opts - type: type, - }; - framework.localChange(); - updateForm(framework, content, true); + var controlContainer; + if (APP.isEditor) { + var controls = Object.keys(TYPES).map(function (type) { + + var btn = h('button.btn', [ + TYPES[type].icon.cloneNode(), + h('span', Messages['form_type_'+type]) + ]); + $(btn).click(function () { + var uid = Util.uid(); + content.form[uid] = { + //q: Messages.form_default, + //opts: opts + type: type, + }; + framework.localChange(); + updateForm(framework, content, true); + }); + return btn; }); - return btn; - }); - var controlContainer = h('div.cp-form-creator-control', controls); + controlContainer = h('div.cp-form-creator-control', controls); + } var contentContainer = h('div.cp-form-creator-content'); var div = h('div.cp-form-creator-container', [ @@ -294,12 +353,9 @@ define([ return div; }; + var $container = $('#cp-app-form-container'); $container.append(makeFormCreator()); - - var sframeChan = framework._.sfCommon.getSframeChannel(); - var metadataMgr = framework._.cpNfInner.metadataMgr; - var priv = metadataMgr.getPrivateData(); - APP.isEditor = Boolean(priv.form_public); + if (!APP.isEditor) { makeFormControls(); } framework.onReady(function (isNew) { var priv = metadataMgr.getPrivateData(); @@ -340,6 +396,7 @@ define([ framework.onContentUpdate(function (newContent) { console.log(newContent); content = newContent; + updateForm(framework, content, APP.isEditor); }); framework.setContentGetter(function () { diff --git a/www/form/main.js b/www/form/main.js index be47466e3..d9eb3e660 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -63,6 +63,7 @@ define([ config.onReady = function () { cb(); // XXX + network.disconnect(); }; config.onMessage = function () { // XXX @@ -70,6 +71,31 @@ define([ CPNetflux.start(config); }); }); + sframeChan.on("Q_FORM_SUBMIT", function (data, cb) { + var box = data.mailbox; + var myKeys; + nThen(function (w) { + Cryptpad.getFormKeys(w(function (keys) { + myKeys = keys; + })); + }).nThen(function (w) { + + var keys = Utils.secret && Utils.secret.keys; + myKeys.signingKey = keys.secondarySignKey; + + var crypto = Utils.Crypto.Mailbox.createEncryptor(myKeys); + var text = JSON.stringify(data.results); + var ciphertext = crypto.encrypt(text, box.publicKey); + + var hash = ciphertext.slice(0,64); // XXX use this to recover our previous answers + Cryptpad.anonRpcMsg("WRITE_PRIVATE_MESSAGE", [ + box.channel, + ciphertext + ], function (err, response) { + cb({error: err, response: response, hash: hash}); + }); + }); + }); sframeChan.on('EV_FORM_MAILBOX', function (data) { var curvePair = Nacl.box.keyPair(); publicKey = Nacl.util.encodeBase64(curvePair.publicKey); From 3b30cfcc555c9a4de1e3a6216839657b0a51fc74 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 20 May 2021 16:20:15 +0200 Subject: [PATCH 019/424] Recover previous answers --- www/common/cryptpad-common.js | 18 ++++++++++++ www/common/outer/async-store.js | 1 + www/form/inner.js | 45 +++++++++++++++++++++++------ www/form/main.js | 50 +++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 9 deletions(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index ba0624a08..173aa9c9f 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -100,6 +100,7 @@ define([ cb(keys); }); }; + common.getFormKeys = function (cb) { postMessage("GET", { key: ['curvePrivate'], @@ -111,6 +112,23 @@ 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 + } + }, function (obj) { + if (obj && obj.error) { console.error(obj.error); } + }); + + }; common.makeNetwork = function (cb) { require([ diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 21bc77798..0e1e9684e 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -2696,6 +2696,7 @@ define([ nThen(function (waitFor) { if (!proxy.settings) { proxy.settings = NEW_USER_SETTINGS; } + if (!proxy.forms) { proxy.forms = {}; } if (!proxy.friends_pending) { proxy.friends_pending = {}; } // Call onCacheReady if the manager is not yet defined diff --git a/www/form/inner.js b/www/form/inner.js index f50583e37..b62c4e0da 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -62,6 +62,8 @@ define([ Messages.form_reset = "Reset"; Messages.form_sent = "Sent"; + Messages.form_cantFindAnswers = "Unable to retrieve your existing answers for this form."; + // XXX to update our own answers, we need to store the server hash of the message // and we'll be able to use getHistoryRange to fetch this message when we come back @@ -134,7 +136,7 @@ define([ return { tag: tag, getValue: function () { return $tag.val(); }, - //setValue: function (val) { $tag.val(val); } + setValue: function (val) { $tag.val(val); }, reset: function () { $tag.val(''); } }; }, @@ -148,8 +150,10 @@ define([ if (!opts) { opts = TYPES.radio.defaultOpts; } var name = Util.uid(); var els = opts.values.map(function (data, i) { - return UI.createRadio(name, 'cp-form-'+name+'-'+i, - data, false, { mark: {tabindex:1} }); + var radio = UI.createRadio(name, 'cp-form-'+name+'-'+i, + data, false, { mark: { tabindex:1 } }); + $(radio).find('input').data('val', data); + return radio; }); var tag = h('div.radio-group', els); return { @@ -167,8 +171,17 @@ define([ edit: function (cb) { var v = opts.values.slice(); return editOptions(v, cb); + }, + setValue: function (val) { + this.reset(); + els.some(function (el) { + var $el = $(el).find('input'); + if ($el.data('val') === val) { + $el.prop('checked', true); + return true; + } + }); } - //setValue: function (val) {} }; }, @@ -208,7 +221,7 @@ define([ }); return h('div.cp-form-send-container', [send, reset]); }; - var updateForm = function (framework, content, editable) { + var updateForm = function (framework, content, editable, answers) { var $container = $('div.cp-form-creator-content'); var form = content.form; @@ -224,6 +237,7 @@ define([ var data = model.get(block.opts); data.uid = uid; + if (answers && answers[uid]) { data.setValue(answers[uid]); } var q = h('div.cp-form-block-question', block.q || Messages.form_default); var edit, editContainer; @@ -364,16 +378,17 @@ define([ content.form = {}; framework.localChange(); } - if (!content.answers || !content.answers.channel || !content.answers.publicKey) { + if (!content.answers || !content.answers.channel || !content.answers.publicKey || !content.answers.validateKey) { content.answers = { channel: Hash.createChannelId(), - publicKey: priv.form_public + publicKey: priv.form_public, + validateKey: priv.form_answerValidateKey }; framework.localChange(); } } - if (!content.answers || !content.answers.channel || !content.answers.publicKey) { + if (!content.answers || !content.answers.channel || !content.answers.publicKey || !content.answers.validateKey) { return void UI.errorLoadingScreen(Messages.form_invalid); } // XXX fetch answers and @@ -389,7 +404,19 @@ define([ }); return; } - updateForm(framework, content, false); + + sframeChan.query("Q_FETCH_MY_ANSWERS", { + channel: content.answers.channel, + validateKey: content.answers.validateKey, + publicKey: content.answers.publicKey + }, function (err, obj) { + if (obj && obj.error) { + UI.warn(Messages.form_cantFindAnswers); + } + var answers; + if (obj && !obj.error) { answers = obj; } + updateForm(framework, content, false, answers); + }); }); diff --git a/www/form/main.js b/www/form/main.js index d9eb3e660..089c3433e 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -22,6 +22,9 @@ define([ var secondary = keys && keys.secondaryKey; if (!secondary) { return; } var curvePair = Nacl.box.keyPair.fromSecretKey(Nacl.util.decodeUTF8(secondary).slice(0,32)); + var validateKey = keys.secondaryValidateKey; + meta.form_answerValidateKey = validateKey; + publicKey = meta.form_public = Nacl.util.encodeBase64(curvePair.publicKey); privateKey = Nacl.util.encodeBase64(curvePair.secretKey); }; @@ -71,6 +74,44 @@ define([ CPNetflux.start(config); }); }); + sframeChan.on("Q_FETCH_MY_ANSWERS", function (data, cb) { + var keys; + var CPNetflux; + var network; + var answer; + var myKeys; + nThen(function (w) { + Cryptpad.getFormKeys(w(function (keys) { + myKeys = keys; + })); + Cryptpad.getFormAnswer({channel: data.channel}, w(function (obj) { + if (!obj || obj.error) { + w.abort(); + return void cb(obj); + } + answer = obj; + })); + }).nThen(function (w) { + 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; + var ephemeral_priv = answer.curvePrivate; + 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) + }); + cb(JSON.parse(res.content)); + }); + + }); + + }); sframeChan.on("Q_FORM_SUBMIT", function (data, cb) { var box = data.mailbox; var myKeys; @@ -83,6 +124,10 @@ define([ var keys = Utils.secret && Utils.secret.keys; myKeys.signingKey = keys.secondarySignKey; + var ephemeral_keypair = Nacl.box.keyPair(); + var ephemeral_private = Nacl.util.encodeBase64(ephemeral_keypair.secretKey); + myKeys.ephemeral_keypair = ephemeral_keypair; + var crypto = Utils.Crypto.Mailbox.createEncryptor(myKeys); var text = JSON.stringify(data.results); var ciphertext = crypto.encrypt(text, box.publicKey); @@ -92,6 +137,11 @@ define([ box.channel, ciphertext ], function (err, response) { + Cryptpad.storeFormAnswer({ + channel: box.channel, + hash: hash, + curvePrivate: ephemeral_private + }); cb({error: err, response: response, hash: hash}); }); }); From 07c90b6a94edea51430cbab4ee4a87d9924e13ac Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 21 May 2021 13:39:33 +0200 Subject: [PATCH 020/424] View responses --- .../src/less2/include/colortheme-dark.less | 4 + .../src/less2/include/colortheme.less | 5 + www/common/cryptpad-common.js | 4 +- www/form/app-form.less | 66 ++++++++- www/form/inner.js | 133 ++++++++++++++++-- www/form/main.js | 31 ++-- 6 files changed, 218 insertions(+), 25 deletions(-) diff --git a/customize.dist/src/less2/include/colortheme-dark.less b/customize.dist/src/less2/include/colortheme-dark.less index 3443aaeb0..875ad004b 100644 --- a/customize.dist/src/less2/include/colortheme-dark.less +++ b/customize.dist/src/less2/include/colortheme-dark.less @@ -427,3 +427,7 @@ @cp_calendar-now: @cryptpad_color_brand_300; @cp_calendar-now-fg: @cryptpad_color_grey_800; +// Forms +@cp_forms-bg1: @cryptpad_color_grey_800; +@cp_forms-bg2: @cryptpad_color_grey_900; +@cp_forms-border: @cryptpad_color_grey_800; diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less index e56676213..012a66239 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -426,3 +426,8 @@ @cp_calendar-border: @cryptpad_color_grey_300; @cp_calendar-now: @cryptpad_color_brand; @cp_calendar-now-fg: @cryptpad_color_grey_200; + +// Forms +@cp_forms-bg1: @cryptpad_color_grey_200; +@cp_forms-bg2: @cryptpad_color_grey_100; +@cp_forms-border: @cryptpad_color_grey_200; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 173aa9c9f..8419db9ba 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -69,7 +69,7 @@ define([ }, cb); }; - common.getAccessKeys = function (cb) { + common.getAccessKeys = function (cb, opts) { var keys = []; Nthen(function (waitFor) { // Push account keys @@ -84,6 +84,7 @@ define([ }); } catch (e) { console.error(e); } })); + // Push teams keys postMessage("GET", { key: ['teams'], @@ -92,6 +93,7 @@ define([ Object.keys(obj || {}).forEach(function (id) { var t = obj[id]; var _keys = t.keys.drive || {}; + _keys.id = id; if (!_keys.edPrivate) { return; } keys.push(t.keys.drive); }); diff --git a/www/form/app-form.less b/www/form/app-form.less index 996de4ad7..ee62e2c89 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -20,6 +20,17 @@ overflow: hidden; } + &.cp-app-form-results { + div.cp-form-creator-content, .cp-app-form-button-results { + display: none !important; + } + } + &:not(.cp-app-form-results) { + div.cp-form-creator-results, .cp-app-form-button-creator { + display: none !important; + } + } + #cp-app-form-container { display: flex; flex: 1; @@ -34,8 +45,13 @@ display: flex; flex-flow: column; width: 300px; + .cp-form-creator-types { + margin-top: 20px; + display: flex; + flex-flow: column; + } } - div.cp-form-creator-content { + div.cp-form-creator-content, div.cp-form-creator-results { padding: 10px; display: flex; flex-flow: column; @@ -86,6 +102,54 @@ } } } + div.cp-form-creator-results { + display: flex; + flex-flow: column; + position: relative; + & > div { + background: @cp_forms-bg1; + padding: 10px; + &:not(:last-child) { + margin-bottom: 20px; + } + } + .cp-form-block-question { + margin-bottom: 5px; + } + .cp-form-block-type { + float: right; + padding: 5px; + margin-top: -10px; + margin-right: -10px; + i { margin-right: 5px; } + background: @cp_forms-bg2; + } + .cp-form-results-type-text { + max-height: 300px; + overflow: auto; + .cp-form-results-type-text-data { + padding: 5px 10px; + background: @cp_forms-bg2; + &:not(:last-child) { margin-bottom: 1px; } + } + } + .cp-form-results-type-radio { + display: table; + .cp-form-results-type-radio-data { + display: table-row; + border: 1px solid @cp_forms-border; + & > span { + border: 1px solid @cp_forms-border; + display: table-cell; + padding: 5px 10px; + background: @cp_forms-bg2; + &.cp-value { + min-width: 200px; + } + } + } + } + } } } diff --git a/www/form/inner.js b/www/form/inner.js index b62c4e0da..246d8149f 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -59,21 +59,17 @@ define([ Messages.form_duplicates = "Duplicate entries have been removed"; + Messages.form_submit = "Submit"; + Messages.form_update = "Update"; Messages.form_reset = "Reset"; Messages.form_sent = "Sent"; Messages.form_cantFindAnswers = "Unable to retrieve your existing answers for this form."; - // XXX to update our own answers, we need to store the server hash of the message - // and we'll be able to use getHistoryRange to fetch this message when we come back + Messages.form_viewResults = "Go to responses"; + Messages.form_viewCreator = "Go to form creator"; - - var makeFormSettings = function (framework) { - // XXX - // Button to set results as public - // Checkbox to allow anonymous answers - // Button to clear all answers? - }; + Messages.form_notAnswered = "And {0} empty answers"; var editOptions = function (v, cb) { var add = h('button.btn.btn-secondary', [ @@ -128,6 +124,12 @@ define([ ]; }; + var getEmpty = function (empty) { + if (empty) { + return UI.setHTML(h('div.cp-form-results-type-text-empty'), Messages._getKey('form_notAnswered', [empty])); + } + }; + var TYPES = { input: { get: function () { @@ -140,6 +142,19 @@ define([ reset: function () { $tag.val(''); } }; }, + printResults: function (answers, uid) { + var results = []; + var empty = 0; + Object.keys(answers).forEach(function (author) { + var obj = answers[author]; + var answer = obj.msg[uid]; + if (!answer || !answer.trim()) { return empty++; } + results.push(h('div.cp-form-results-type-text-data', answer)); + }); + results.push(getEmpty(empty)); + + return h('div.cp-form-results-type-text', results); + }, icon: h('i.fa.fa-font') }, radio: { @@ -185,12 +200,33 @@ define([ }; }, + printResults: function (answers, uid) { + var results = []; + var empty = 0; + var count = {}; + Object.keys(answers).forEach(function (author) { + var obj = answers[author]; + var answer = obj.msg[uid]; + if (!answer || !answer.trim()) { return empty++; } + count[answer] = count[answer] || 0; + count[answer]++; + }); + Object.keys(count).forEach(function (value) { + results.push(h('div.cp-form-results-type-radio-data', [ + h('span.cp-value', value), + h('span.cp-count', count[value]) + ])); + }); + results.push(getEmpty(empty)); + + return h('div.cp-form-results-type-radio', results); + }, icon: h('i.fa.fa-list-ul') } }; - var makeFormControls = function (framework, content) { - var send = h('button.btn.btn-primary', Messages.poll_commit); + var makeFormControls = function (framework, content, update) { + var send = h('button.btn.btn-primary', update ? Messages.form_update : Messages.form_submit); var reset = h('button.btn.btn-danger-alt', Messages.form_reset); $(reset).click(function () { if (!Array.isArray(APP.formBlocks)) { return; } @@ -211,12 +247,13 @@ define([ mailbox: content.answers, results: results }, function (err, data) { - console.error(data); + $send.attr('disabled', 'disabled'); if (err || (data && data.error)) { console.error(err || data.error); return void UI.warn(Messages.error); } UI.alert(Messages.form_sent); + $send.text(Messages.form_update); }); }); return h('div.cp-form-send-container', [send, reset]); @@ -321,7 +358,30 @@ define([ }); $container.empty().append(elements); - $container.append(makeFormControls(framework, content)); + $container.append(makeFormControls(framework, content, Boolean(answers))); + }; + + var renderResults = function (content, answers) { + var $container = $('div.cp-form-creator-results').empty(); + var form = content.form; + var elements = Object.keys(form).map(function (uid) { + var block = form[uid]; + var type = block.type; + var model = TYPES[type]; + if (!model || !model.printResults) { return; } + var print = model.printResults(answers, uid); + + var q = h('div.cp-form-block-question', block.q || Messages.form_default); + return h('div.cp-form-block', [ + h('div.cp-form-block-type', [ + TYPES[type].icon.cloneNode(), + h('span', Messages['form_type_'+type]) + ]), + q, + h('div.cp-form-block-content', print), + ]); + }); + $container.append(elements); }; var andThen = function (framework) { @@ -333,6 +393,39 @@ define([ var priv = metadataMgr.getPrivateData(); APP.isEditor = Boolean(priv.form_public); + var $body = $('body'); + + var makeFormSettings = function () { + var viewResults = h('button.btn.btn-primary', [ + h('span.cp-app-form-button-results', Messages.form_viewResults), + h('span.cp-app-form-button-creator', Messages.form_viewCreator), + ]); + var $v = $(viewResults).click(function () { + if ($body.hasClass('cp-app-form-results')) { + $body.removeClass('cp-app-form-results'); + return; + } + $v.attr('disabled', 'disabled'); + sframeChan.query("Q_FORM_FETCH_ANSWERS", { + channel: content.answers.channel, + validateKey: content.answers.validateKey, + publicKey: content.answers.publicKey + }, function (err, answers) { + $v.removeAttr('disabled'); + $body.addClass('cp-app-form-results'); + renderResults(content, answers); + }); + + }); + return [ + viewResults + ]; + + // XXX + // Button to set results as public + // Checkbox to allow anonymous answers + // Button to clear all answers? + }; var makeFormCreator = function () { @@ -356,13 +449,21 @@ define([ }); return btn; }); - controlContainer = h('div.cp-form-creator-control', controls); + + var settings = makeFormSettings(); + + controlContainer = h('div.cp-form-creator-control', [ + h('div.cp-form-creator-settings', settings), + h('div.cp-form-creator-types', controls) + ]); } var contentContainer = h('div.cp-form-creator-content'); + var resultsContainer = h('div.cp-form-creator-results'); var div = h('div.cp-form-creator-container', [ controlContainer, contentContainer, + resultsContainer ]); return div; }; @@ -397,8 +498,10 @@ define([ if (APP.isEditor) { sframeChan.query("Q_FORM_FETCH_ANSWERS", { channel: content.answers.channel, + validateKey: content.answers.validateKey, publicKey: content.answers.publicKey - }, function () { + }, function (err, obj) { + if (obj) { APP.answers = obj; } updateForm(framework, content, true); }); diff --git a/www/form/main.js b/www/form/main.js index 089c3433e..95f162864 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -30,7 +30,7 @@ define([ }; var addRpc = function (sframeChan, Cryptpad, Utils) { sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, cb) { - var keys; + var myKeys; var CPNetflux; var network; nThen(function (w) { @@ -40,7 +40,15 @@ define([ CPNetflux = _CPNetflux; })); Cryptpad.getAccessKeys(w(function (_keys) { - keys = _keys; + if (!Array.isArray(_keys)) { return; } + + _keys.some(function (_k) { + if ((!Cryptpad.initialTeam && !_k.id) || Cryptpad.initialTeam === _k.id) { + myKeys = _k; + return true; + } + }); + console.error(myKeys); })); Cryptpad.makeNetwork(w(function (err, nw) { network = nw; @@ -52,24 +60,31 @@ define([ var crypto = Utils.Crypto.Mailbox.createEncryptor({ curvePrivate: privateKey, - curvePublic: publicKey || data.publicKey + curvePublic: publicKey || data.publicKey, + validateKey: data.validateKey }); var config = { network: network, channel: data.channel, noChainPad: true, validateKey: keys.secondaryValidateKey, - owners: [], // XXX add pad owner + owners: [myKeys.edPublic], // XXX add pad owner crypto: crypto, // XXX Cache }; + var results = {}; config.onReady = function () { - cb(); - // XXX + cb(results); network.disconnect(); }; - config.onMessage = function () { - // XXX + config.onMessage = function (msg, peer, vKey, isCp, hash, senderCurve, cfg) { + var parsed = Utils.Util.tryParse(msg); + if (!parsed) { return; } + results[senderCurve] = { + msg: parsed, + hash: hash, + time: cfg.time + }; }; CPNetflux.start(config); }); From 37cbb51b92db979c9322db8dd5ce8e178a1e9dad Mon Sep 17 00:00:00 2001 From: Weblate Date: Fri, 21 May 2021 14:53:52 +0200 Subject: [PATCH 021/424] Translated using Weblate (English) Currently translated at 100.0% (1236 of 1236 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ Translated using Weblate (English) Currently translated at 100.0% (1235 of 1235 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ Translated using Weblate (English) Currently translated at 100.0% (1234 of 1234 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ Translated using Weblate (English) Currently translated at 100.0% (1233 of 1233 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ Translated using Weblate (English) Currently translated at 100.0% (1232 of 1232 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ Translated using Weblate (English) Currently translated at 100.0% (1231 of 1231 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ --- www/common/translations/messages.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/www/common/translations/messages.json b/www/common/translations/messages.json index 5ada6865a..8e3729d12 100644 --- a/www/common/translations/messages.json +++ b/www/common/translations/messages.json @@ -1230,5 +1230,11 @@ "mediatag_defaultImageName": "image", "register_registrationIsClosed": "Registration is closed.", "oo_conversionSupport": "Your browser cannot handle conversion to and from Microsoft Office formats. We suggest using a recent version of Firefox or Chrome.", - "oo_importBin": "Click OK to import CryptPad's internal .bin format." + "oo_importBin": "Click OK to import CryptPad's internal .bin format.", + "admin_emailTitle": "Admin contact email", + "admin_emailHint": "Set the contact email for your instance here", + "admin_supportPrivTitle": "Support mailbox private key", + "admin_supportInitGenerate": "Generate support keys", + "admin_supportPrivHint": "Display the private key that other admins will need to view support tickets. A form to enter this key will be displayed on their admin panel.", + "admin_supportPrivButton": "Show key" } From 158c37a2f28239f04c47f1c678ec2330883abe41 Mon Sep 17 00:00:00 2001 From: Weblate Date: Fri, 21 May 2021 14:58:04 +0200 Subject: [PATCH 022/424] Translated using Weblate (English) Currently translated at 100.0% (1237 of 1237 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ --- www/common/translations/messages.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/common/translations/messages.json b/www/common/translations/messages.json index 8e3729d12..cdc01cddf 100644 --- a/www/common/translations/messages.json +++ b/www/common/translations/messages.json @@ -1236,5 +1236,6 @@ "admin_supportPrivTitle": "Support mailbox private key", "admin_supportInitGenerate": "Generate support keys", "admin_supportPrivHint": "Display the private key that other admins will need to view support tickets. A form to enter this key will be displayed on their admin panel.", - "admin_supportPrivButton": "Show key" + "admin_supportPrivButton": "Show key", + "admin_emailButton": "Update" } From 2262929c090695646368c9b77627ca650afee6d8 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 21 May 2021 18:38:33 +0530 Subject: [PATCH 023/424] remove hardcoded translations for new admin components --- www/admin/inner.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/www/admin/inner.js b/www/admin/inner.js index 5bc76d011..03e2270a9 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -289,9 +289,6 @@ define([ return $div; }; - Messages.admin_emailTitle = "Admin contact email"; // XXX - Messages.admin_emailHint = "Set the contact email for your instance here"; // XXX - Messages.admin_emailButton = "Update"; create['email'] = function () { var key = 'email'; var $div = makeBlock(key, true); // Msg.admin_emailHint, Msg.admin_emailTitle, Msg.admin_emailButton @@ -963,26 +960,22 @@ define([ return $container; }; - - Messages.admin_supportPrivTitle = "Support admin key"; // XXX - Messages.admin_supportPrivHint = "Display the private key allowing other admins to access the support. A form to enter this key will be displayed in their admin panel."; - Messages.admin_supportPrivButton = "Show key"; create['support-priv'] = function () { if (!supportKey || !APP.privateKey || !checkAdminKey(APP.privateKey)) { return; } var $div = makeBlock('support-priv', true); // Msg.admin_supportPrivHint, .admin_supportPrivTitle, .admin_supportPrivButton var $button = $div.find('button').click(function () { $button.remove(); - $div.append(h('pre', APP.privateKey)); + var $selectable = $(UI.dialog.selectable(APP.privateKey)).css({ 'max-width': '28em' }); + $div.append($selectable); }); return $div; }; - Messages.admin_supportInitGenerate = "Generate support keys"; // XXX create['support-init'] = function () { var $div = makeBlock('support-init'); // Msg.admin_supportInitHint, .admin_supportInitTitle if (!supportKey) { (function () { - $div.append(h('p', Messages.admin_supportInitHelp)); // XXX Update text for this key + $div.append(h('p', Messages.admin_supportInitHelp)); var button = h('button.btn.btn-primary', Messages.admin_supportInitGenerate); var $button = $(button).appendTo($div); $div.append($button); @@ -1419,7 +1412,7 @@ define([ var is24h = false; var dateFormat = "Y-m-d H:i"; try { - is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); + is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); // XXX } catch (e) {} if (!is24h) { dateFormat = "Y-m-d h:i K"; } From 7000abf3c017ec3b367250fb5f4602c69bcef6ed Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 21 May 2021 18:47:53 +0530 Subject: [PATCH 024/424] widen markdown code blocks --- customize.dist/src/less2/include/markdown.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/customize.dist/src/less2/include/markdown.less b/customize.dist/src/less2/include/markdown.less index 717b49990..3943f128d 100644 --- a/customize.dist/src/less2/include/markdown.less +++ b/customize.dist/src/less2/include/markdown.less @@ -194,7 +194,7 @@ pre > code { display: block; position: relative; - width: 90%; + width: 100%; margin: auto; padding: 5px; overflow-x: auto; From 506c78f121cd88b372c5de01f15018aead65e02e Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 21 May 2021 18:48:26 +0530 Subject: [PATCH 025/424] increment version string to 4.6.0 --- customize.dist/pages.js | 2 +- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/customize.dist/pages.js b/customize.dist/pages.js index 26b0e7a4e..aaf7c60b3 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -105,7 +105,7 @@ define([ var imprintUrl = AppConfig.imprint && (typeof(AppConfig.imprint) === "boolean" ? '/imprint.html' : AppConfig.imprint); - Pages.versionString = "v4.5.0"; + Pages.versionString = "v4.6.0"; // used for the about menu diff --git a/package-lock.json b/package-lock.json index 75b03cfe1..b1d794abe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cryptpad", - "version": "4.5.0", + "version": "4.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3314ffd17..8526afc8a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cryptpad", "description": "realtime collaborative visual editor with zero knowlege server", - "version": "4.5.0", + "version": "4.6.0", "license": "AGPL-3.0+", "repository": { "type": "git", From cc5674585899e24339aa7b4d7899269d483cbe7c Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 21 May 2021 20:35:48 +0530 Subject: [PATCH 026/424] add more thorough tests for sandbox configuration on the checkup page --- www/checkup/app-checkup.less | 6 -- www/checkup/main.js | 194 +++++++++++++++++++++++++++++++---- www/checkup/sandbox/main.js | 183 ++++++--------------------------- 3 files changed, 202 insertions(+), 181 deletions(-) diff --git a/www/checkup/app-checkup.less b/www/checkup/app-checkup.less index 99999e2bb..a475d5983 100644 --- a/www/checkup/app-checkup.less +++ b/www/checkup/app-checkup.less @@ -12,12 +12,6 @@ html, body { color: @cryptpad_text_col; font-family: "IBM Plex Mono"; - iframe.sandbox-test { - display: block; - width: 100%; - height: 100%; - } - .report { font-size: 30px; max-width: 50%; diff --git a/www/checkup/main.js b/www/checkup/main.js index 896156fc6..3ac3b190a 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -20,7 +20,6 @@ define([ ], function ($, ApiConfig, Assertions, h, Messages, DomReady, nThen, SFCommonO, Login, Hash, Util, Pinpad, NetConfig, Pages) { - var Assert = Assertions(); var trimSlashes = function (s) { if (typeof(s) !== 'string') { return s; } @@ -51,15 +50,12 @@ define([ ]); }; + var cacheBuster = function (url) { + return url + '?test=' + (+new Date()); + }; + var trimmedSafe = trimSlashes(ApiConfig.httpSafeOrigin); var trimmedUnsafe = trimSlashes(ApiConfig.httpUnsafeOrigin); -/* -// XXX display results from this iframe on this page - document.body.appendChild(h('iframe', { - class: 'sandbox-test', - src: trimmedSafe + '/checkup/sandbox/index.html', - })); -*/ assert(function (cb, msg) { msg.appendChild(h('span', [ @@ -125,7 +121,7 @@ define([ var checkAvailability = function (url, cb) { $.ajax({ - url: url, // XXX bust cache + url: cacheBuster(url), data: {}, complete: function (xhr) { cb(xhr.status === 200); @@ -166,7 +162,6 @@ define([ ])); var to; - var obj; nThen(function (waitFor) { DomReady.onReady(waitFor()); }).nThen(function (waitFor) { @@ -174,13 +169,8 @@ define([ console.error('TIMEOUT loading iframe on the safe domain'); cb(false); }, 5000); - obj = SFCommonO.initIframe(waitFor); + SFCommonO.initIframe(waitFor); }).nThen(function () { - SFCommonO.start({ - href: obj.href, - }); - }).nThen(function () { - console.error("DONE?"); // Iframe is loaded clearTimeout(to); cb(true); @@ -351,14 +341,14 @@ define([ assert(function (cb, msg) { msg.innerText = "Missing HTTP headers required for .xlsx export from sheets. "; - var url = sheetURL; + var url = cacheBuster(sheetURL); var expect = { 'cross-origin-resource-policy': 'cross-origin', 'cross-origin-embedder-policy': 'require-corp', //'cross-origin-opener-policy': 'same-origin', // FIXME this is in our nginx config but not server.js }; - $.ajax(url, { // XXX bust cache + $.ajax(url, { complete: function (xhr) { cb(!Object.keys(expect).some(function (k) { var response = xhr.getResponseHeader(k); @@ -430,7 +420,7 @@ define([ "Your browser console may provide more details as to why this resource could not be loaded. ", ])); - $.ajax('/api/broadcast', { // XXX bust cache + $.ajax(cacheBuster('/api/broadcast'), { dataType: 'text', complete: function (xhr) { cb(xhr.status === 200); @@ -443,7 +433,7 @@ define([ }; var checkAPIHeaders = function (url, msg, cb) { - $.ajax(url, { // XXX bust cache + $.ajax(cacheBuster(url), { dataType: 'text', complete: function (xhr) { var allHeaders = xhr.getAllResponseHeaders(); @@ -495,13 +485,13 @@ define([ var INCORRECT_HEADER_TEXT = ' was served with duplicated or incorrect headers. Compare your reverse-proxy configuration against the provided example.'; assert(function (cb, msg) { - var url = '/api/config'; // XXX bust cache + var url = '/api/config'; msg.innerText = url + INCORRECT_HEADER_TEXT; checkAPIHeaders(url, msg, cb); }); assert(function (cb, msg) { - var url = '/api/broadcast'; // XXX bust cache + var url = '/api/broadcast'; msg.innerText = url + INCORRECT_HEADER_TEXT; checkAPIHeaders(url, msg, cb); }); @@ -559,6 +549,166 @@ define([ cb(false); }); + var response = Util.response(function (err) { + console.error('SANDBOX_ERROR', err); + }); + + var sandboxIframe = h('iframe', { + class: 'sandbox-test', + src: cacheBuster(trimmedSafe + '/checkup/sandbox/index.html'), + }); + document.body.appendChild(sandboxIframe); + + var sandboxIframeReady = Util.mkEvent(true); + setTimeout(function () { + sandboxIframeReady.fire("TIMEOUT"); + }, 10 * 1000); + + var postMessage = function (content, cb) { + try { + var txid = Util.uid(); + content.txid = txid; + response.expect(txid, cb, 15000); + sandboxIframe.contentWindow.postMessage(JSON.stringify(content), '*'); + } catch (err) { + console.error(err); + } + }; + + window.addEventListener('message', function (event) { + try { + var msg = JSON.parse(event.data); + if (msg.command === 'READY') { return void sandboxIframeReady.fire(); } + var txid = msg.txid; + if (!txid) { return console.log("no handler for ", txid); } + response.handle(txid, msg.content); + } catch (err) { + console.error(event); + console.error(err); + } + }); + + var parseCSP = function (CSP) { + //console.error(CSP); + var CSP_headers = {}; + CSP.split(";") + .forEach(function (rule) { + rule = (rule || "").trim(); + if (!rule) { return; } + var parts = rule.split(/\s/); + var first = parts[0]; + var rest = rule.slice(first.length + 1); + CSP_headers[first] = rest; + //console.error(rule.trim()); + //console.info("[%s] '%s'", first, rest); + }); + return CSP_headers; + }; + + var hasUnsafeEval = function (CSP_headers) { + return /unsafe\-eval/.test(CSP_headers['script-src']); + }; + + var hasUnsafeInline = function (CSP_headers) { + return /unsafe\-inline/.test(CSP_headers['script-src']); + }; + + var hasOnlyOfficeHeaders = function (CSP_headers) { + if (!hasUnsafeEval(CSP_headers)) { + console.error("NO_UNSAFE_EVAL"); + console.log(CSP_headers); + return false; + } + if (!hasUnsafeInline(CSP_headers)) { + console.error("NO_UNSAFE_INLINE"); + return void false; + } + return true; + }; + + var CSP_WARNING = function (url) { + return h('span', [ + code(url), + ' does not have the required ', + code("'content-security-policy'"), + ' headers set. This is most often related to incorrectly configured sandbox domains or reverse proxies.', + ]); + }; + + assert(function (_cb, msg) { + var url = '/sheet/inner.html'; + var cb = Util.once(Util.mkAsync(_cb)); + msg.appendChild(CSP_WARNING(url)); + nThen(function (w) { + sandboxIframeReady.reg(w(function (err) { + if (!err) { return; } + w.abort(); + cb(err); + })); + }).nThen(function () { + postMessage({ + command: 'GET_HEADER', + content: { + url: url, + header: 'content-security-policy', + }, + }, function (content) { + var CSP_headers = parseCSP(content); + cb(hasOnlyOfficeHeaders(CSP_headers)); + }); + }); + }); + + assert(function (cb, msg) { + var url = '/common/onlyoffice/v4/web-apps/apps/spreadsheeteditor/main/index.html'; + msg.appendChild(CSP_WARNING(url)); + nThen(function (w) { + sandboxIframeReady.reg(w(function (err) { + if (!err) { return; } + w.abort(); + cb(err); + })); + }).nThen(function () { + postMessage({ + command: 'GET_HEADER', + content: { + url: url, + header: 'content-security-policy', + }, + }, function (content) { + var CSP_headers = parseCSP(content); + cb(hasOnlyOfficeHeaders(CSP_headers)); + }); + }); + }); + + assert(function (cb, msg) { + var url = '/sheet/inner.html'; + msg.appendChild(h('span', [ + code(url), + ' does not have the required ', + code("'cross-origin-opener-policy'"), + ' headers set.', + ])); + nThen(function (w) { + sandboxIframeReady.reg(w(function (err) { + if (!err) { return; } + w.abort(); + cb(err); + })); + }).nThen(function () { + postMessage({ + command: 'GET_HEADER', + content: { + url: url, + header: 'cross-origin-opener-policy', + }, + }, function (content) { + cb(content === 'same-origin'); + }); + }); + }); + if (false) { assert(function (cb, msg) { msg.innerText = 'fake test to simulate failure'; diff --git a/www/checkup/sandbox/main.js b/www/checkup/sandbox/main.js index 8066dc1b3..7450ed0e3 100644 --- a/www/checkup/sandbox/main.js +++ b/www/checkup/sandbox/main.js @@ -1,34 +1,16 @@ define([ 'jquery', - '/api/config', - '/assert/assertions.js', - '/common/hyperscript.js', - '/customize/messages.js', - '/common/dom-ready.js', - '/bower_components/nthen/index.js', - '/common/sframe-common-outer.js', - '/customize/login.js', - '/common/common-hash.js', - '/common/common-util.js', - '/common/pinpad.js', - '/common/outer/network-config.js', - '/customize/pages.js', + //'/bower_components/nthen/index.js', + //'/common/common-util.js', '/bower_components/tweetnacl/nacl-fast.min.js', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/checkup/app-checkup.less', -], function ($, ApiConfig, Assertions, h, Messages, DomReady, - nThen, SFCommonO, Login, Hash, Util, Pinpad, - NetConfig, Pages) { - var Assert = Assertions(); - var assert = function (f, msg) { - Assert(f, msg || h('span.advisory-text.cp-danger')); +], function ($ /*, nThen, Util */) { + var postMessage = function (content) { + window.parent.postMessage(JSON.stringify(content), '*'); }; - - var code = function (content) { - return h('code', content); - }; - + postMessage({ command: "READY", }); var getHeaders = function (url, cb) { $.ajax(url + "?test=" + (+new Date()), { dataType: 'text', @@ -38,138 +20,33 @@ define([ }, }); }; - var parseCSP = function (CSP) { - //console.error(CSP); - var CSP_headers = {}; - CSP.split(";") - .forEach(function (rule) { - rule = (rule || "").trim(); - if (!rule) { return; } - var parts = rule.split(/\s/); - var first = parts[0]; - var rest = rule.slice(first.length + 1); - CSP_headers[first] = rest; - //console.error(rule.trim()); - console.info("[%s] '%s'", first, rest); - }); - return CSP_headers; - }; - - var hasUnsafeEval = function (CSP_headers) { - return /unsafe\-eval/.test(CSP_headers['script-src']); - }; - - var hasUnsafeInline = function (CSP_headers) { - return /unsafe\-inline/.test(CSP_headers['script-src']); - }; - - var hasOnlyOfficeHeaders = function (CSP_headers) { - if (!hasUnsafeEval(CSP_headers)) { - console.error("NO_UNSAFE_EVAL"); - console.log(CSP_headers); - return false; - } - if (!hasUnsafeInline(CSP_headers)) { - console.error("NO_UNSAFE_INLINE"); - return void false; - } - return true; - }; - - // XXX run these from /checkup/inner.js and report to /checkup/main.js - assert(function (cb, msg) { - var url = '/sheet/inner.html'; - msg.appendChild(h('span', [ - code(url), - ' has the wrong headers.', - ])); + var COMMANDS = {}; + COMMANDS.GET_HEADER = function (content, cb) { + var url = content.url; getHeaders(url, function (err, headers, xhr) { - var CSP_headers = parseCSP(xhr.getResponseHeader('content-security-policy')); - cb(hasOnlyOfficeHeaders(CSP_headers)); + cb(xhr.getResponseHeader(content.header)); }); - }); - - assert(function (cb, msg) { - var url = '/common/onlyoffice/v4/web-apps/apps/spreadsheeteditor/main/index.html'; - msg.appendChild(h('span', [ - code(url), - ' has the wrong headers.', - ])); - getHeaders(url, function (err, headers, xhr) { - var CSP_headers = parseCSP(xhr.getResponseHeader('content-security-policy')); - cb(hasOnlyOfficeHeaders(CSP_headers)); - }); - }); - - var row = function (cells) { - return h('tr', cells.map(function (cell) { - return h('td', cell); - })); - }; - - var failureReport = function (obj) { - return h('div.error', [ - h('h5', obj.message), - h('table', [ - row(["Failed test number", obj.test + 1]), - row(["Returned value", obj.output]), - ]), - ]); - }; - - var completed = 0; - var $progress = $('#cp-progress'); - - var versionStatement = function () { - return h('p', [ - "This instance is running ", - h('span.cp-app-checkup-version',[ - "CryptPad", - ' ', - Pages.versionString, - ]), - '.', - ]); }; - Assert.run(function (state) { - var errors = state.errors; - var failed = errors.length; - - Messages.assert_numberOfTestsPassed = "{0} / {1} tests passed."; - - var statusClass = failed? 'failure': 'success'; - - var failedDetails = "Details found below"; - var successDetails = "This checkup only tests the most common configuration issues. You may still experience errors or incorrect behaviour."; - var details = h('p', failed? failedDetails: successDetails); - - var summary = h('div.summary.' + statusClass, [ - versionStatement(), - h('p', Messages._getKey('assert_numberOfTestsPassed', [ - state.passed, - state.total - ])), - details, - ]); - - var report = h('div.report', [ - summary, - h('div.failures', errors.map(failureReport)), - ]); - - $progress.remove(); - $('body').prepend(report); - }, function (i, total) { - console.log('test '+ i +' completed'); - completed++; - Messages.assert_numberOfTestsCompleted = "{0} / {1} tests completed."; - $progress.html('').append(h('div.report.pending.summary', [ - versionStatement(), - h('p', [ - h('i.fa.fa-spinner.fa-pulse'), - h('span', Messages._getKey('assert_numberOfTestsCompleted', [completed, total])) - ]) - ])); + window.addEventListener("message", function (event) { + if (event && event.data) { + try { + //console.log(JSON.parse(event.data)); + var msg = JSON.parse(event.data); + var command = msg.command; + var txid = msg.txid; + COMMANDS[command](msg.content, function (response) { + // postMessage with same txid + postMessage({ + txid: txid, + content: response, + }); + }); + } catch (err) { + console.error(err); + } + } else { + console.error(event); + } }); }); From 1f86578920d7f9ff849e473ec78e564d24e7f29a Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 21 May 2021 20:38:47 +0530 Subject: [PATCH 027/424] update instructions for adminEmail configuration --- config/config.example.js | 5 ----- www/checkup/main.js | 3 +-- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/config/config.example.js b/config/config.example.js index f53f3bb5b..ef54f3413 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -147,11 +147,6 @@ module.exports = { */ //removeDonateButton: false, - /* CryptPad will display a point of contact for your instance on its contact page - * (/contact.html) if you provide it below. - */ - adminEmail: 'i.did.not.read.my.config@cryptpad.fr', - /* * By default, CryptPad contacts one of our servers once a day. * This check-in will also send some very basic information about your instance including its diff --git a/www/checkup/main.js b/www/checkup/main.js index 3ac3b190a..2b1c68ffa 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -511,8 +511,7 @@ define([ 'This instance does not provide a valid ', h('code', 'adminEmail'), ' which can make it difficult to contact its adminstrator to report vulnerabilities or abusive content.', - ' This can be configured in ', CONFIG_PATH(), '. ', - RESTART_WARNING(), + " This can be configured on your instance's admin panel. Use the provided 'Flush cache' button for this change to take effect for all users." ])); cb(email); }); From d06cba0b5ebb2d24fda12a50864bd12d983c2369 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 21 May 2021 17:40:41 +0200 Subject: [PATCH 028/424] Sort and delete form questions --- .../src/less2/include/colortheme-dark.less | 6 +- .../src/less2/include/colortheme.less | 6 +- customize.dist/src/less2/include/forms.less | 8 +- www/common/common-interface.js | 6 +- www/form/app-form.less | 45 ++++-- www/form/inner.js | 128 +++++++++++++++--- 6 files changed, 163 insertions(+), 36 deletions(-) diff --git a/customize.dist/src/less2/include/colortheme-dark.less b/customize.dist/src/less2/include/colortheme-dark.less index 875ad004b..68a456bcc 100644 --- a/customize.dist/src/less2/include/colortheme-dark.less +++ b/customize.dist/src/less2/include/colortheme-dark.less @@ -428,6 +428,6 @@ @cp_calendar-now-fg: @cryptpad_color_grey_800; // Forms -@cp_forms-bg1: @cryptpad_color_grey_800; -@cp_forms-bg2: @cryptpad_color_grey_900; -@cp_forms-border: @cryptpad_color_grey_800; +@cp_form-bg1: @cryptpad_color_grey_800; +@cp_form-bg2: @cryptpad_color_grey_900; +@cp_form-border: @cryptpad_color_grey_800; diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less index 012a66239..c385cd83c 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -428,6 +428,6 @@ @cp_calendar-now-fg: @cryptpad_color_grey_200; // Forms -@cp_forms-bg1: @cryptpad_color_grey_200; -@cp_forms-bg2: @cryptpad_color_grey_100; -@cp_forms-border: @cryptpad_color_grey_200; +@cp_form-bg1: @cryptpad_color_grey_200; +@cp_form-bg2: @cryptpad_color_grey_100; +@cp_form-border: @cryptpad_color_grey_200; diff --git a/customize.dist/src/less2/include/forms.less b/customize.dist/src/less2/include/forms.less index ffe061fa3..4fb799e13 100644 --- a/customize.dist/src/less2/include/forms.less +++ b/customize.dist/src/less2/include/forms.less @@ -71,6 +71,12 @@ div.cp-button-confirm { display: inline-block; + &.new { + vertical-align: top; + button { + height: 35px; + } + } button { margin: 0 !important; } @@ -85,7 +91,7 @@ } } } - button.cp-button-confirm-placeholder { + button.cp-button-confirm-placeholder:not(.new) { margin-bottom: 3px !important; } diff --git a/www/common/common-interface.js b/www/common/common-interface.js index dbcb089f3..bba514dbf 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -747,6 +747,7 @@ define([ cb = Util.once(cb); } var classes = 'btn ' + (config.classes || 'btn-primary'); + var newCls = config.new ? '.new' : ''; var button = h('button', { "class": classes, @@ -759,7 +760,7 @@ define([ }); var timer = h('div.cp-button-timer', div); - var content = h('div.cp-button-confirm', [ + var content = h('div.cp-button-confirm'+newCls, [ button, timer ]); @@ -795,7 +796,8 @@ define([ to = setTimeout(todo, INTERVAL); }; - $(originalBtn).addClass('cp-button-confirm-placeholder').click(function (e) { + var newCls2 = config.new ? 'new' : ''; + $(originalBtn).addClass('cp-button-confirm-placeholder').addClass(newCls2).click(function (e) { e.stopPropagation(); // If we have a validation function, continue only if it's true if (config.validate && !config.validate()) { return; } diff --git a/www/form/app-form.less b/www/form/app-form.less index ee62e2c89..4ea9d3cf6 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -11,6 +11,8 @@ display: flex; flex-flow: column; + font: @colortheme_app-font; + color: @cryptpad_text_col; #cp-app-form-editor { flex: 1; @@ -56,17 +58,26 @@ display: flex; flex-flow: column; flex: 1; + overflow: auto; + .cp-form-block { + .tools_unselectable(); + background: @cp_form-bg1; + padding: 10px; &:not(:last-child) { margin-bottom: 20px; } + .cp-form-block-question { + margin-bottom: 5px; + } .cp-form-input-block { display: flex; //width: @form_input-width; - &:not(:focus-within) { + &:not(.editing) { input { background: transparent; border: none; + padding: 0 !important; & ~ button:not(:disabled) { .cp-form-edit { display: inline; } .cp-form-save { display: none; } @@ -76,15 +87,24 @@ input { flex: 1; min-width: 100px; + padding: 0 10px !important; + height: auto; } button { .cp-form-edit { display: none; - margin: 0 !important; } .cp-form-save { display: inline; } } + .cp-form-block-drag { + font-size: 22px; + width: 20px; + margin-left: 5px; + text-align: center; + line-height: 31px; + } } + &.editable { cursor: grab; } } .cp-form-edit-block { .cp-form-edit-block-input { @@ -107,7 +127,7 @@ flex-flow: column; position: relative; & > div { - background: @cp_forms-bg1; + background: @cp_form-bg1; padding: 10px; &:not(:last-child) { margin-bottom: 20px; @@ -122,14 +142,14 @@ margin-top: -10px; margin-right: -10px; i { margin-right: 5px; } - background: @cp_forms-bg2; + background: @cp_form-bg2; } .cp-form-results-type-text { max-height: 300px; overflow: auto; .cp-form-results-type-text-data { padding: 5px 10px; - background: @cp_forms-bg2; + background: @cp_form-bg2; &:not(:last-child) { margin-bottom: 1px; } } } @@ -137,12 +157,12 @@ display: table; .cp-form-results-type-radio-data { display: table-row; - border: 1px solid @cp_forms-border; + border: 1px solid @cp_form-border; & > span { - border: 1px solid @cp_forms-border; + border: 1px solid @cp_form-border; display: table-cell; padding: 5px 10px; - background: @cp_forms-bg2; + background: @cp_form-bg2; &.cp-value { min-width: 200px; } @@ -152,5 +172,14 @@ } } + .cp-form-type-radio { + display: flex; + flex-flow: column; + align-items: baseline; + .cp-radio { + display: inline-flex; + } + } + } diff --git a/www/form/inner.js b/www/form/inner.js index 246d8149f..b65e30278 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -20,6 +20,8 @@ define([ '/common/inner/access.js', '/common/inner/properties.js', + '/bower_components/sortablejs/Sortable.min.js', + '/bower_components/file-saver/FileSaver.min.js', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/form/app-form.less', @@ -40,7 +42,8 @@ define([ h, Messages, AppConfig, - Share, Access, Properties + Share, Access, Properties, + Sortable ) { var SaveAs = window.saveAs; @@ -50,6 +53,7 @@ define([ Messages.button_newform = "New Form"; // XXX Messages.form_invalid = "Invalid form"; Messages.form_editBlock = "Edit options"; + Messages.form_editQuestion = "Edit question"; Messages.form_newOption = "New option"; @@ -63,6 +67,7 @@ define([ Messages.form_update = "Update"; Messages.form_reset = "Reset"; Messages.form_sent = "Sent"; + Messages.form_delete = "Delete block"; Messages.form_cantFindAnswers = "Unable to retrieve your existing answers for this form."; @@ -170,7 +175,7 @@ define([ $(radio).find('input').data('val', data); return radio; }); - var tag = h('div.radio-group', els); + var tag = h('div.radio-group.cp-form-type-radio', els); return { tag: tag, getValue: function () { @@ -266,7 +271,7 @@ define([ APP.formBlocks = []; // XXX order array later - var elements = Object.keys(form).map(function (uid) { + var elements = content.order.map(function (uid) { var block = form[uid]; var type = block.type; var model = TYPES[type]; @@ -277,7 +282,7 @@ define([ if (answers && answers[uid]) { data.setValue(answers[uid]); } var q = h('div.cp-form-block-question', block.q || Messages.form_default); - var edit, editContainer; + var editButtons, editContainer; APP.formBlocks.push(data); @@ -288,32 +293,64 @@ define([ value: block.q || Messages.form_default }); var $inputQ = $(inputQ); - var saveQ = h('button.btn.btn-primary', [ + var saveQ = h('button.btn.btn-primary.small', [ h('i.fa.fa-pencil.cp-form-edit'), + h('span.cp-form-edit', Messages.form_editQuestion), + h('i.fa.fa-floppy-o.cp-form-save'), h('span.cp-form-save', Messages.settings_save) ]); + var dragHandle = h('i.fa.fa-arrows-v.cp-form-block-drag'); + var $saveQ = $(saveQ).click(function () { + if (!$(q).hasClass('editing')) { + $(q).addClass('editing'); + $inputQ.focus(); + return; + } var v = $inputQ.val(); - if (!v || !v.trim() || v === block.q) { return; } + if (!v || !v.trim()) { return void UI.warn(Messages.error); } block.q = v.trim(); framework.localChange(); $saveQ.attr('disabled', 'disabled'); framework._.cpNfInner.chainpad.onSettle(function () { + $(q).removeClass('editing'); $saveQ.removeAttr('disabled'); - $saveQ.blur(); + $inputQ.blur(); UI.log(Messages.saved); }); }); - var onBlur = function (e) { + var onCancelQ = function (e) { if (e && e.relatedTarget && e.relatedTarget === saveQ) { return; } - $inputQ.val(block.q); + $inputQ.val(block.q || Messages.form_default); + if (!e) { $inputQ.blur(); } + $(q).removeClass('editing'); }; $inputQ.keydown(function (e) { if (e.which === 13) { return void $saveQ.click(); } - if (e.which === 27) { return void $inputQ.blur(); } + if (e.which === 27) { return void onCancelQ(); } + }); + $inputQ.focus(function () { + $(q).addClass('editing'); + }); + $inputQ.blur(onCancelQ); + q = h('div.cp-form-input-block', [inputQ, saveQ, dragHandle]); + + // Delete question + var edit; + var del = h('button.btn.btn-danger', [ + h('i.fa.fa-trash-o'), + h('span', Messages.form_delete) + ]); + UI.confirmButton(del, { + classes: 'btn-danger', + new: true + }, function () { + delete content.form[uid]; + var idx = content.order.indexOf(uid); + content.order.splice(idx, 1); + $('.cp-form-block[data-id="'+uid+'"]').remove(); + framework.localChange(); }); - $inputQ.blur(onBlur); - q = h('div.cp-form-input-block', [inputQ, saveQ]); // Values if (data.edit) { @@ -325,7 +362,7 @@ define([ var onSave = function (newOpts) { if (!newOpts) { // Cancel edit $(editContainer).empty(); - $edit.show(); + $(editButtons).show(); $(data.tag).show(); return; } @@ -334,37 +371,62 @@ define([ framework.localChange(); var $oldTag = $(data.tag); framework._.cpNfInner.chainpad.onSettle(function () { - $edit.show(); + $(editButtons).show(); UI.log(Messages.saved); data = model.get(newOpts); $oldTag.before(data.tag).remove(); }); }; - var $edit = $(edit).click(function () { + $(edit).click(function () { $(data.tag).hide(); $(editContainer).append(data.edit(onSave)); - $edit.hide(); + $(editButtons).hide(); }); } + + editButtons = h('div.cp-form-edit-buttons-container', [ + edit, del + ]); } - return h('div.cp-form-block', [ + var editableCls = editable ? ".editable" : ""; + return h('div.cp-form-block'+editableCls, { + 'data-id':uid + }, [ q, h('div.cp-form-block-content', [ data.tag, - edit + editButtons ]), editContainer ]); }); $container.empty().append(elements); + + if (editable) { + Sortable.create($container[0], { + direction: "vertical", + filter: "input, button", + preventOnFilter: false, + store: { + set: function (s) { + content.order = s.toArray(); + framework.localChange(); + } + } + }); + + return; + } + + // In view mode, add "Submit" and "reset" buttons $container.append(makeFormControls(framework, content, Boolean(answers))); }; var renderResults = function (content, answers) { var $container = $('div.cp-form-creator-results').empty(); var form = content.form; - var elements = Object.keys(form).map(function (uid) { + var elements = content.order.map(function (uid) { var block = form[uid]; var type = block.type; var model = TYPES[type]; @@ -427,6 +489,26 @@ define([ // Button to clear all answers? }; + var checkIntegrity = function (getter) { + var changed = false; + content.order.forEach(function (uid) { + if (!content.form[uid]) { + var idx = content.order.indexOf(uid); + content.order.splice(idx, 1); + changed = true; + } + }); + Object.keys(content.form).forEach(function (uid) { + var idx = content.order.indexOf(uid); + if (idx === -1) { + changed = true; + content.order.push(uid); + } + }); + + if (!getter && changed) { framework.localChange(); } + }; + var makeFormCreator = function () { var controlContainer; @@ -444,6 +526,7 @@ define([ //opts: opts type: type, }; + content.order.push(uid); framework.localChange(); updateForm(framework, content, true); }); @@ -479,6 +562,10 @@ define([ content.form = {}; framework.localChange(); } + if (!content.order) { + content.order = []; + framework.localChange(); + } if (!content.answers || !content.answers.channel || !content.answers.publicKey || !content.answers.validateKey) { content.answers = { channel: Hash.createChannelId(), @@ -502,6 +589,7 @@ define([ publicKey: content.answers.publicKey }, function (err, obj) { if (obj) { APP.answers = obj; } + checkIntegrity(false); updateForm(framework, content, true); }); @@ -518,6 +606,7 @@ define([ } var answers; if (obj && !obj.error) { answers = obj; } + checkIntegrity(false); updateForm(framework, content, false, answers); }); @@ -530,6 +619,7 @@ define([ }); framework.setContentGetter(function () { + checkIntegrity(true); return content; }); From e81d5e4be42d511448db6e9e186415639b653320 Mon Sep 17 00:00:00 2001 From: Weblate Date: Sun, 23 May 2021 15:34:58 +0200 Subject: [PATCH 029/424] Translated using Weblate (German) Currently translated at 100.0% (1237 of 1237 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/de/ --- www/common/translations/messages.de.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/www/common/translations/messages.de.json b/www/common/translations/messages.de.json index 9ed337bea..649d6595d 100644 --- a/www/common/translations/messages.de.json +++ b/www/common/translations/messages.de.json @@ -1230,5 +1230,12 @@ "register_registrationIsClosed": "Die Registrierung ist geschlossen.", "mediatag_defaultImageName": "Bild", "oo_importBin": "Klicke auf OK, um CryptPads interne Format .bin zu importieren.", - "oo_conversionSupport": "Dein Browser unterstützt die Konvertierung von und in Microsoft-Office-Formaten nicht. Wir empfehlen die Verwendung einer aktuellen Version von Firefox oder Chrome." + "oo_conversionSupport": "Dein Browser unterstützt die Konvertierung von und in Microsoft-Office-Formaten nicht. Wir empfehlen die Verwendung einer aktuellen Version von Firefox oder Chrome.", + "admin_emailTitle": "E-Mail-Adresse der Administratoren", + "admin_emailHint": "Lege hier die Kontaktadresse für deine Instanz fest", + "admin_supportPrivHint": "Lasse dir den privaten Schlüssel anzeigen, den andere Administratoren zum Zugriff auf die Support-Tickets benötigen. Ein Formular zur Eingabe dieses Schlüssels wird in ihrem Administrationsbereich angezeigt.", + "admin_supportPrivTitle": "Privater Schlüssel für das Support-Postfach", + "admin_emailButton": "Aktualisieren", + "admin_supportPrivButton": "Schlüssel anzeigen", + "admin_supportInitGenerate": "Support-Schlüssel generieren" } From ba5284c116bdc171c9727d0ae6ce62d6c2cd608a Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 25 May 2021 11:48:40 +0200 Subject: [PATCH 030/424] Fix getSharedFolderData --- www/common/proxy-manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 9371b9e8b..51fda924f 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -237,7 +237,7 @@ define([ var getSharedFolderData = function (Env, id) { if (!Env.folders[id]) { return {}; } - var obj = Env.folders[id].proxy.metadata || {}; + var obj = Util.clone(Env.folders[id].proxy.metadata || {}); for (var k in Env.user.proxy[UserObject.SHARED_FOLDERS][id] || {}) { if (typeof(Env.user.proxy[UserObject.SHARED_FOLDERS][id][k]) === "undefined") { // XXX "deleted folder" for restricted shared folders when viewer in a team continue; From 5c402a00a3937ebea622b57bdba953b02e527f2e Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 25 May 2021 13:51:03 +0200 Subject: [PATCH 031/424] Make results public and view results as participant --- www/form/inner.js | 100 ++++++++++++++++++++++++++++++++-------------- www/form/main.js | 4 +- 2 files changed, 73 insertions(+), 31 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index b65e30278..d748c5462 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -76,6 +76,11 @@ define([ Messages.form_notAnswered = "And {0} empty answers"; + Messages.form_makePublic = "Make public"; + Messages.form_makePublicWarning = "Are you sure you want to make the results of this form public? This can't be undone."; + Messages.form_isPublic = "Results are public"; + Messages.form_isPrivate = "Results are private"; + var editOptions = function (v, cb) { var add = h('button.btn.btn-secondary', [ h('i.fa.fa-plus'), @@ -230,6 +235,29 @@ define([ } }; + var renderResults = function (content, answers) { + var $container = $('div.cp-form-creator-results').empty(); + var form = content.form; + var elements = content.order.map(function (uid) { + var block = form[uid]; + var type = block.type; + var model = TYPES[type]; + if (!model || !model.printResults) { return; } + var print = model.printResults(answers, uid); + + var q = h('div.cp-form-block-question', block.q || Messages.form_default); + return h('div.cp-form-block', [ + h('div.cp-form-block-type', [ + TYPES[type].icon.cloneNode(), + h('span', Messages['form_type_'+type]) + ]), + q, + h('div.cp-form-block-content', print), + ]); + }); + $container.append(elements); + }; + var makeFormControls = function (framework, content, update) { var send = h('button.btn.btn-primary', update ? Messages.form_update : Messages.form_submit); var reset = h('button.btn.btn-danger-alt', Messages.form_reset); @@ -261,7 +289,22 @@ define([ $send.text(Messages.form_update); }); }); - return h('div.cp-form-send-container', [send, reset]); + + if (content.answers.privateKey) { + var viewResults = h('button.btn.btn-primary', [ + h('span.cp-app-form-button-results', Messages.form_viewResults), + ]); + var sframeChan = framework._.sfCommon.getSframeChannel(); + var $v = $(viewResults).click(function () { + $v.attr('disabled', 'disabled'); + sframeChan.query("Q_FORM_FETCH_ANSWERS", content.answers, function (err, answers) { + $v.removeAttr('disabled'); + $('body').addClass('cp-app-form-results'); + renderResults(content, answers); + }); + }); + } + return h('div.cp-form-send-container', [send, reset, viewResults]); }; var updateForm = function (framework, content, editable, answers) { var $container = $('div.cp-form-creator-content'); @@ -423,29 +466,6 @@ define([ $container.append(makeFormControls(framework, content, Boolean(answers))); }; - var renderResults = function (content, answers) { - var $container = $('div.cp-form-creator-results').empty(); - var form = content.form; - var elements = content.order.map(function (uid) { - var block = form[uid]; - var type = block.type; - var model = TYPES[type]; - if (!model || !model.printResults) { return; } - var print = model.printResults(answers, uid); - - var q = h('div.cp-form-block-question', block.q || Messages.form_default); - return h('div.cp-form-block', [ - h('div.cp-form-block-type', [ - TYPES[type].icon.cloneNode(), - h('span', Messages['form_type_'+type]) - ]), - q, - h('div.cp-form-block-content', print), - ]); - }); - $container.append(elements); - }; - var andThen = function (framework) { framework.start(); var content = {}; @@ -458,6 +478,27 @@ define([ var $body = $('body'); var makeFormSettings = function () { + var makePublic = h('button.btn.btn-primary', Messages.form_makePublic); + if (content.answers.privateKey) { makePublic = undefined; } + var publicText = content.answers.privateKey ? Messages.form_isPublic : Messages.form_isPrivate; + var resultsType = h('div.cp-form-results-type-container', [ + h('span.cp-form-results-type', publicText), + makePublic + ]); + var $makePublic = $(makePublic).click(function () { + UI.confirm(Messages.form_makePublicWarning, function (yes) { + if (!yes) { return; } + content.answers.privateKey = priv.form_private; + framework.localChange(); + framework._.cpNfInner.chainpad.onSettle(function () { + UI.log(Messages.saved); + $makePublic.remove(); + $(resultsType).find('.cp-form-results-type').text(Messages.form_isPublic); + }); + }); + }); + + var viewResults = h('button.btn.btn-primary', [ h('span.cp-app-form-button-results', Messages.form_viewResults), h('span.cp-app-form-button-creator', Messages.form_viewCreator), @@ -480,7 +521,8 @@ define([ }); return [ - viewResults + resultsType, + viewResults, ]; // XXX @@ -551,12 +593,12 @@ define([ return div; }; - var $container = $('#cp-app-form-container'); - $container.append(makeFormCreator()); - if (!APP.isEditor) { makeFormControls(); } - framework.onReady(function (isNew) { var priv = metadataMgr.getPrivateData(); + + var $container = $('#cp-app-form-container'); + $container.append(makeFormCreator()); + if (APP.isEditor) { if (!content.form) { content.form = {}; diff --git a/www/form/main.js b/www/form/main.js index 95f162864..32810bd1c 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -26,7 +26,7 @@ define([ meta.form_answerValidateKey = validateKey; publicKey = meta.form_public = Nacl.util.encodeBase64(curvePair.publicKey); - privateKey = Nacl.util.encodeBase64(curvePair.secretKey); + privateKey = meta.form_private = Nacl.util.encodeBase64(curvePair.secretKey); }; var addRpc = function (sframeChan, Cryptpad, Utils) { sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, cb) { @@ -59,7 +59,7 @@ define([ var keys = Utils.secret && Utils.secret.keys; var crypto = Utils.Crypto.Mailbox.createEncryptor({ - curvePrivate: privateKey, + curvePrivate: privateKey || data.privateKey, curvePublic: publicKey || data.publicKey, validateKey: data.validateKey }); From 84ec461542c5e17e2717e002fc589e18d24c1042 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 25 May 2021 14:37:58 +0200 Subject: [PATCH 032/424] Clean deprecated values in shared folders metadata --- www/common/proxy-manager.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 51fda924f..0b19e9020 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -237,7 +237,15 @@ define([ var getSharedFolderData = function (Env, id) { if (!Env.folders[id]) { return {}; } - var obj = Util.clone(Env.folders[id].proxy.metadata || {}); + var proxy = Env.folders[id].proxy; + + // Clean deprecated values + if (Object.keys(proxy.metadata || {}).length > 1) { + proxy.metadata = { title: proxy.metadata.title; } + } + + var obj = Util.clone(proxy.metadata || {}); + for (var k in Env.user.proxy[UserObject.SHARED_FOLDERS][id] || {}) { if (typeof(Env.user.proxy[UserObject.SHARED_FOLDERS][id][k]) === "undefined") { // XXX "deleted folder" for restricted shared folders when viewer in a team continue; From 6f64d62698c16e60a89a4c7e2b1665533c65febb Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 25 May 2021 15:15:51 +0200 Subject: [PATCH 033/424] Realtime changes --- www/form/inner.js | 98 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 83 insertions(+), 15 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index d748c5462..d82aeae90 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -27,7 +27,7 @@ define([ 'less!/form/app-form.less', ], function ( $, - JSONSortify, + Sortify, Crypto, Framework, Toolbar, @@ -81,15 +81,29 @@ define([ Messages.form_isPublic = "Results are public"; Messages.form_isPrivate = "Results are private"; - var editOptions = function (v, cb) { + var editOptions = function (v, setCursorGetter, cb, tmp) { var add = h('button.btn.btn-secondary', [ h('i.fa.fa-plus'), h('span', Messages.tag_add) ]); + var cursor; + if (tmp && tmp.content && Sortify(v) === Sortify(tmp.old)) { + v = tmp.content.values; + cursor = tmp.cursor; + } + // Show existing options var getOption = function (val) { var input = h('input', {value:val}); + + // if this element was active before the remote change, restore cursor + if (cursor && cursor.el === val) { + input.selectionStart = cursor.start || 0; + input.selectionEnd = cursor.end || 0; + setTimeout(function () { input.focus(); }); + } + var del = h('button.btn.btn-danger', h('i.fa.fa-times')); var el = h('div.cp-form-edit-block-input', [ input, del ]); $(del).click(function () { $(el).remove(); }); @@ -108,6 +122,26 @@ define([ var cancelBlock = h('button.btn.btn-secondary', Messages.cancel); $(cancelBlock).click(function () { cb(); }); + // Set cursor getter (to handle remote changes to the form) + setCursorGetter(function () { + var values = []; + var active = document.activeElement; + var cursor = {}; + $(container).find('input').each(function (i, el) { + if (el === active) { + cursor.el= $(el).val(); + cursor.start = el.selectionStart; + cursor.end = el.selectionEnd; + } + values.push($(el).val()); + }); + return { + old: v, + content: {values: values}, + cursor: cursor + }; + }); + // Save changes var saveBlock = h('button.btn.btn-primary', [ h('i.fa.fa-floppy-o'), @@ -181,6 +215,8 @@ define([ return radio; }); var tag = h('div.radio-group.cp-form-type-radio', els); + var cursorGetter; + var setCursorGetter = function (f) { cursorGetter = f; }; return { tag: tag, getValue: function () { @@ -193,10 +229,11 @@ define([ return res; }, reset: function () { $(tag).find('input').removeAttr('checked'); }, - edit: function (cb) { + edit: function (cb, tmp) { var v = opts.values.slice(); - return editOptions(v, cb); + return editOptions(v, setCursorGetter, cb, tmp); }, + getCursor: function () { return cursorGetter(); }, setValue: function (val) { this.reset(); els.some(function (el) { @@ -258,6 +295,14 @@ define([ $container.append(elements); }; + var getFormResults = function () { + if (!Array.isArray(APP.formBlocks)) { return; } + var results = {}; + APP.formBlocks.forEach(function (data) { + results[data.uid] = data.getValue(); + }); + return results; + }; var makeFormControls = function (framework, content, update) { var send = h('button.btn.btn-primary', update ? Messages.form_update : Messages.form_submit); var reset = h('button.btn.btn-danger-alt', Messages.form_reset); @@ -269,12 +314,8 @@ define([ }); var $send = $(send).click(function () { $send.attr('disabled', 'disabled'); - if (!Array.isArray(APP.formBlocks)) { return; } - var results = {}; - APP.formBlocks.forEach(function (data) { - results[data.uid] = data.getValue(); - }); - + var results = getFormResults(); + if (!results) { return; } var sframeChan = framework._.sfCommon.getSframeChannel(); sframeChan.query('Q_FORM_SUBMIT', { mailbox: content.answers, @@ -306,8 +347,9 @@ define([ } return h('div.cp-form-send-container', [send, reset, viewResults]); }; - var updateForm = function (framework, content, editable, answers) { + var updateForm = function (framework, content, editable, answers, temp) { var $container = $('div.cp-form-creator-content'); + if (!$container.length) { return; } // Not ready var form = content.form; @@ -403,6 +445,7 @@ define([ ]); editContainer = h('div'); var onSave = function (newOpts) { + data.editing = false; if (!newOpts) { // Cancel edit $(editContainer).empty(); $(editButtons).show(); @@ -420,11 +463,22 @@ define([ $oldTag.before(data.tag).remove(); }); }; - $(edit).click(function () { + var onEdit = function (tmp) { + data.editing = true; $(data.tag).hide(); - $(editContainer).append(data.edit(onSave)); + $(editContainer).append(data.edit(onSave, tmp)); $(editButtons).hide(); + }; + $(edit).click(function () { + onEdit(); }); + + // If we were editing this field, recover our unsaved changes + if (temp && temp[uid]) { + setTimeout(function () { + onEdit(temp[uid]); + }); + } } editButtons = h('div.cp-form-edit-buttons-container', [ @@ -458,7 +512,6 @@ define([ } } }); - return; } @@ -466,6 +519,18 @@ define([ $container.append(makeFormControls(framework, content, Boolean(answers))); }; + var getTempFields = function () { + if (!Array.isArray(APP.formBlocks)) { return; } + var temp = {}; + APP.formBlocks.forEach(function (data) { + if (data.editing) { + var cursor = data.getCursor && data.getCursor(); + temp[data.uid] = cursor; + } + }); + return temp; + }; + var andThen = function (framework) { framework.start(); var content = {}; @@ -657,7 +722,10 @@ define([ framework.onContentUpdate(function (newContent) { console.log(newContent); content = newContent; - updateForm(framework, content, APP.isEditor); + var answers, temp; + if (!APP.isEditor) { answers = getFormResults(); } + else { temp = getTempFields(); } + updateForm(framework, content, APP.isEditor, answers, temp); }); framework.setContentGetter(function () { From 396885f959a34a8ada19bb809911ad280bc1a6d0 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 26 May 2021 14:59:56 +0530 Subject: [PATCH 034/424] add Czech and Indonesian translation files --- www/common/translations/messages.cs.json | 1 + www/common/translations/messages.id.json | 1 + 2 files changed, 2 insertions(+) create mode 100644 www/common/translations/messages.cs.json create mode 100644 www/common/translations/messages.id.json diff --git a/www/common/translations/messages.cs.json b/www/common/translations/messages.cs.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/www/common/translations/messages.cs.json @@ -0,0 +1 @@ +{} diff --git a/www/common/translations/messages.id.json b/www/common/translations/messages.id.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/www/common/translations/messages.id.json @@ -0,0 +1 @@ +{} From 4a1de329946e545bf9adc1393d8d890f84ab5d45 Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 26 May 2021 11:57:14 +0200 Subject: [PATCH 035/424] Dedicated form share modal and auditor role --- www/common/common-hash.js | 17 +++++++++++++++++ www/common/inner/share.js | 32 ++++++++++++++++++++++++++++---- www/common/toolbar.js | 4 +++- www/form/inner.js | 14 ++++++++++++++ www/form/main.js | 19 +++++++++++++++++-- www/secureiframe/inner.js | 1 + 6 files changed, 80 insertions(+), 7 deletions(-) diff --git a/www/common/common-hash.js b/www/common/common-hash.js index c59f5babe..b9cdfa699 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -215,6 +215,17 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app) }); return k ? Crypto.b64AddSlashes(k) : ''; }; + var getAuditorKey = function (hashArr) { + var k; + // Check if we have a ownerKey for this pad + hashArr.some(function (data) { + if (/^auditor=/.test(data)) { + k = data.slice(8); + return true; + } + }); + return k ? Crypto.b64AddSlashes(k) : ''; + }; var getOwnerKey = function (hashArr) { var k; // Check if we have a ownerKey for this pad @@ -237,6 +248,7 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app) parsed.present = options.indexOf('present') !== -1; parsed.embed = options.indexOf('embed') !== -1; parsed.versionHash = getVersionHash(options); + parsed.auditorKey = getAuditorKey(options); parsed.newPadOpts = getNewPadOpts(options); parsed.loginOpts = getLoginOpts(options); parsed.ownerKey = getOwnerKey(options); @@ -278,6 +290,7 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app) present: parsed.present, ownerKey: parsed.ownerKey, versionHash: parsed.versionHash, + auditorKey: parsed.auditorKey, newPadOpts: parsed.newPadOpts, loginOpts: parsed.loginOpts, password: parsed.password @@ -304,6 +317,10 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app) if (versionHash) { hash += 'hash=' + Crypto.b64RemoveSlashes(versionHash) + '/'; } + var auditorKey = typeof(opts.auditorKey) !== "undefined" ? opts.auditorKey : parsed.auditorKey; + if (auditorKey) { + hash += 'auditor=' + Crypto.b64RemoveSlashes(auditorKey) + '/'; + } if (opts.newPadOpts) { hash += 'newpad=' + opts.newPadOpts + '/'; } if (opts.loginOpts) { hash += 'login=' + opts.loginOpts + '/'; } return hash; diff --git a/www/common/inner/share.js b/www/common/inner/share.js index f2eb2e953..5db88b672 100644 --- a/www/common/inner/share.js +++ b/www/common/inner/share.js @@ -494,7 +494,23 @@ define([ var parsed = Hash.parsePadUrl(pathname); var canPresent = ['code', 'slide'].indexOf(parsed.type) !== -1; var versionHash = hashes.viewHash && opts.versionHash; - var canBAR = parsed.type !== 'drive' && !versionHash; + var isForm = parsed.type === "form"; // && opts.auditorHash; + var canBAR = parsed.type !== 'drive' && !versionHash && !isForm; + + var labelEdit = Messages.share_linkEdit; + var labelView = Messages.share_linkView; + + var auditor; + if (isForm) { + Messages.share_formEdit = "Author"; // XXX + Messages.share_formView = "Participant"; // XXX + Messages.share_formAuditor = "Auditor"; // XXX + labelEdit = Messages.share_formEdit; + labelView = Messages.share_formView; + auditor = UI.createRadio('accessRights', 'cp-share-form', Messages.share_formAuditor, false, { + mark: {tabindex:1}, + }); + } var burnAfterReading = (hashes.viewHash && canBAR) ? UI.createRadio('accessRights', 'cp-share-bar', Messages.burnAfterReading_linkBurnAfterReading, false, { @@ -505,12 +521,13 @@ define([ h('label', Messages.share_linkAccess), h('div.radio-group',[ UI.createRadio('accessRights', 'cp-share-editable-false', - Messages.share_linkView, true, { mark: {tabindex:1} }), + labelView, true, { mark: {tabindex:1} }), canPresent ? UI.createRadio('accessRights', 'cp-share-present', Messages.share_linkPresent, false, { mark: {tabindex:1} }) : undefined, UI.createRadio('accessRights', 'cp-share-editable-true', - Messages.share_linkEdit, false, { mark: {tabindex:1} })]), - burnAfterReading + labelEdit, false, { mark: {tabindex:1} }), + auditor]), + burnAfterReading, ]); // Burn after reading @@ -553,6 +570,7 @@ define([ var embed = val.embed; var present = val.present !== undefined ? val.present : Util.isChecked($rights.find('#cp-share-present')); var burnAfterReading = Util.isChecked($rights.find('#cp-share-bar')); + var formAuditor = Util.isChecked($rights.find('#cp-share-form')); if (versionHash) { edit = false; present = false; @@ -569,6 +587,9 @@ define([ } var hash = (!hashes.viewHash || (edit && hashes.editHash)) ? hashes.editHash : hashes.viewHash; + if (formAuditor && opts.auditorHash) { + hash = opts.auditorHash; + } var href = burnAfterReading ? opts.burnAfterReadingUrl : (origin + pathname + '#' + hash); var parsed = Hash.parsePadUrl(href); @@ -594,6 +615,9 @@ define([ $rights.find('#cp-share-present').removeAttr('checked').attr('disabled', true); $rights.find('#cp-share-editable-true').attr('checked', true); } + if (isForm && !opts.auditorHash) { + $rights.find('#cp-share-form').removeAttr('checked').attr('disabled', true); + } var getLink = function () { return $rights.parent().find('#cp-share-link-preview'); diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 119d7fb3d..26ff870f6 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -553,11 +553,13 @@ MessengerUI, Messages, Pages) { if (toolbar.isDeleted) { return void UI.warn(Messages.deletedFromServer); } + var privateData = config.metadataMgr.getPrivateData(); var title = (config.title && config.title.getTitle && config.title.getTitle()) || (config.title && config.title.defaultName) || ""; Common.getSframeChannel().event('EV_SHARE_OPEN', { - title: title + title: title, + auditorHash: privateData.form_auditorHash }); }); diff --git a/www/form/inner.js b/www/form/inner.js index d82aeae90..f37b3f3e4 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -689,6 +689,20 @@ define([ // XXX fetch answers and // * viewers ==> check if you've already answered and show form (new or edit) // * editors ==> show schema and warn users if existing questions already have answers + + if (priv.form_auditorKey) { + sframeChan.query("Q_FORM_FETCH_ANSWERS", { + channel: content.answers.channel, + validateKey: content.answers.validateKey, + publicKey: content.answers.publicKey, + privateKey: priv.form_auditorKey + }, function (err, obj) { + $body.addClass('cp-app-form-results'); + renderResults(content, obj); + }); + return; + } + if (APP.isEditor) { sframeChan.query("Q_FORM_FETCH_ANSWERS", { channel: content.answers.channel, diff --git a/www/form/main.js b/www/form/main.js index 32810bd1c..4ebe99295 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -19,6 +19,13 @@ define([ var privateKey, publicKey; var addData = function (meta, CryptPad, user, Utils) { var keys = Utils.secret && Utils.secret.keys; + + var parsed = Utils.Hash.parseTypeHash('pad', hash.slice(1)); + if (parsed.auditorKey) { + meta.form_auditorKey = parsed.auditorKey; + meta.form_auditorHash = hash; + } + var secondary = keys && keys.secondaryKey; if (!secondary) { return; } var curvePair = Nacl.box.keyPair.fromSecretKey(Nacl.util.decodeUTF8(secondary).slice(0,32)); @@ -27,10 +34,19 @@ define([ publicKey = meta.form_public = Nacl.util.encodeBase64(curvePair.publicKey); privateKey = meta.form_private = Nacl.util.encodeBase64(curvePair.secretKey); + + var auditorHash = Utils.Hash.getViewHashFromKeys({ + version: 1, + channel: Utils.secret.channel, + keys: { viewKeyStr: Nacl.util.encodeBase64(keys.cryptKey) } + }); + var parsed = Utils.Hash.parseTypeHash('pad', auditorHash); + meta.form_auditorHash = parsed.getHash({auditorKey: privateKey}); + }; var addRpc = function (sframeChan, Cryptpad, Utils) { sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, cb) { - var myKeys; + var myKeys = {}; var CPNetflux; var network; nThen(function (w) { @@ -48,7 +64,6 @@ define([ return true; } }); - console.error(myKeys); })); Cryptpad.makeNetwork(w(function (err, nw) { network = nw; diff --git a/www/secureiframe/inner.js b/www/secureiframe/inner.js index 1c5af4a57..4226c7f82 100644 --- a/www/secureiframe/inner.js +++ b/www/secureiframe/inner.js @@ -58,6 +58,7 @@ define([ hashes: data.hashes || priv.hashes, common: common, title: data.title, + auditorHash: data.auditorHash, versionHash: data.versionHash, friends: friends, onClose: function () { From c28cf2046442b2e114dd5afa4292f1409798d182 Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 26 May 2021 12:03:27 +0200 Subject: [PATCH 036/424] Fix form template --- www/common/cryptpad-common.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 8419db9ba..973a27d6b 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -743,6 +743,10 @@ define([ delete meta.chat2; delete meta.chat; delete meta.cursor; + + if (meta.type === "form") { + delete parsed.answers; + } } }; From 9a1a1830fc5e000cf8c8fc5a571f7da9a53c3829 Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 26 May 2021 12:03:32 +0200 Subject: [PATCH 037/424] Fix type error --- www/form/inner.js | 6 +++--- www/form/main.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index f37b3f3e4..bfc7dac3a 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -661,9 +661,6 @@ define([ framework.onReady(function (isNew) { var priv = metadataMgr.getPrivateData(); - var $container = $('#cp-app-form-container'); - $container.append(makeFormCreator()); - if (APP.isEditor) { if (!content.form) { content.form = {}; @@ -683,6 +680,9 @@ define([ } } + var $container = $('#cp-app-form-container'); + $container.append(makeFormCreator()); + if (!content.answers || !content.answers.channel || !content.answers.publicKey || !content.answers.validateKey) { return void UI.errorLoadingScreen(Messages.form_invalid); } diff --git a/www/form/main.js b/www/form/main.js index 4ebe99295..ca4aea7e6 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -21,7 +21,7 @@ define([ var keys = Utils.secret && Utils.secret.keys; var parsed = Utils.Hash.parseTypeHash('pad', hash.slice(1)); - if (parsed.auditorKey) { + if (parsed && parsed.auditorKey) { meta.form_auditorKey = parsed.auditorKey; meta.form_auditorHash = hash; } From 378cd38fbdf5755bc7092faa320f6c9b4a7e16fe Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 26 May 2021 14:54:56 +0200 Subject: [PATCH 038/424] Access and Properties modals for forms --- www/common/common-ui-elements.js | 3 +++ www/common/inner/access.js | 32 ++++++++++++++++++++++++++++++-- www/common/inner/properties.js | 2 ++ www/common/outer/async-store.js | 12 ++++++++++++ www/common/proxy-manager.js | 5 +++++ www/form/inner.js | 3 +++ www/form/main.js | 22 +++++++++++++++++++--- 7 files changed, 74 insertions(+), 5 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index e8c0cce4c..4e33b83d0 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -3013,6 +3013,7 @@ define([ // ACCEPT sframeChan.query('Q_SET_PAD_METADATA', { channel: msg.content.channel, + channels: msg.content.channels, command: 'ADD_OWNERS', value: [priv.edPublic] }, function (err, res) { @@ -3062,6 +3063,7 @@ define([ // Remove yourself from the pending owners sframeChan.query('Q_SET_PAD_METADATA', { channel: msg.content.channel, + channels: msg.content.channels, command: 'RM_PENDING_OWNERS', value: [priv.edPublic] }, function (err, res) { @@ -3078,6 +3080,7 @@ define([ // Remove yourself from the pending owners sframeChan.query('Q_SET_PAD_METADATA', { channel: msg.content.channel, + channels: msg.content.channels, command: 'RM_PENDING_OWNERS', value: [priv.edPublic] }, function (err, res) { diff --git a/www/common/inner/access.js b/www/common/inner/access.js index 693b8d236..a1d2c37a1 100644 --- a/www/common/inner/access.js +++ b/www/common/inner/access.js @@ -32,6 +32,12 @@ define([ var teamOwner = data.teamId; var title = opts.title; + var p = priv.propChannels; + var otherChan; + if (p && p.answersChannel) { + otherChan = [p.answersChannel]; + } + opts = opts || {}; var redrawAll = function () {}; @@ -255,6 +261,7 @@ define([ // Send the command sframeChan.query('Q_SET_PAD_METADATA', { channel: channel, + channels: otherChan, command: 'ADD_OWNERS', value: toAddTeams.map(function (obj) { return obj.edPublic; }), teamId: teamOwner @@ -290,6 +297,7 @@ define([ // Send the command sframeChan.query('Q_SET_PAD_METADATA', { channel: channel, + channels: otherChan, command: 'ADD_PENDING_OWNERS', value: toAdd, teamId: teamOwner @@ -310,6 +318,7 @@ define([ // Send the command sframeChan.query('Q_SET_PAD_METADATA', { channel: channel, + channels: otherChan, command: 'ADD_OWNERS', value: [priv.edPublic], teamId: teamOwner @@ -338,6 +347,7 @@ define([ if (!friend) { return; } common.mailbox.sendTo("ADD_OWNER", { channel: channel, + channels: otherChan, href: href, calendar: opts.calendar, password: data.password || priv.password, @@ -417,6 +427,12 @@ define([ var allowed = data.allowed || []; var teamOwner = data.teamId; + var p = priv.propChannels; + var otherChan; + if (p && p.answersChannel) { + otherChan = [p.answersChannel]; + } + var redrawAll = function () {}; var addBtn = h('button.btn.btn-primary.cp-access-add', [h('i.fa.fa-arrow-left'), h('i.fa.fa-arrow-up')]); @@ -495,6 +511,7 @@ define([ // Send the command sframeChan.query('Q_SET_PAD_METADATA', { channel: channel, + channels: otherChan, command: 'RM_ALLOWED', value: [ed], teamId: teamOwner @@ -524,6 +541,7 @@ define([ var val = $checkbox.is(':checked'); sframeChan.query('Q_SET_PAD_METADATA', { channel: channel, + channels: otherChan, command: 'RESTRICT_ACCESS', value: [Boolean(val)], teamId: teamOwner @@ -659,6 +677,7 @@ define([ // Send the command sframeChan.query('Q_SET_PAD_METADATA', { channel: channel, + channels: otherChan, command: 'ADD_ALLOWED', value: toAdd, teamId: teamOwner @@ -987,6 +1006,15 @@ define([ UI.findCancelButton().click(); if (err || (obj && obj.error)) { UI.warn(Messages.error); } }); + + // If this is a form wiht a answer channel, delete it too + var p = priv.propChannels; + if (p.answersChannel) { + sframeChan.query('Q_DELETE_OWNED', { + teamId: typeof(owned) !== "boolean" ? owned : undefined, + channel: p.answersChannel + }, function () {}); + } }); if (!opts.noEditPassword) { $d.append(h('br')); } $d.append(h('div', [ @@ -1020,7 +1048,7 @@ define([ var owned = Modal.isOwned(Env, data); // Request edit access - if (common.isLoggedIn() && ((data.roHref && !data.href) || data.fakeHref) && !owned && !opts.calendar) { + if (common.isLoggedIn() && ((data.roHref && !data.href) || data.fakeHref) && !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); @@ -1058,7 +1086,7 @@ define([ var canMute = data.mailbox && owned === true && ( (typeof (data.mailbox) === "string" && data.owners[0] === edPublic) || data.mailbox[edPublic]); - if (owned === true && !opts.calendar) { + if (owned === true && !opts.calendar && priv.app !== 'form') { var cbox = UI.createCheckbox('cp-access-mute', Messages.access_muteRequests, !canMute); var $cbox = $(cbox); var spinner = UI.makeSpinner($cbox); diff --git a/www/common/inner/properties.js b/www/common/inner/properties.js index ddbf017fc..c946ed208 100644 --- a/www/common/inner/properties.js +++ b/www/common/inner/properties.js @@ -24,6 +24,7 @@ define([ if (privateData.propChannels) { var p = privateData.propChannels; data.channel = data.channel || p.channel; + data.answersChannel = data.answersChannel || p.answersChannel; data.rtChannel = data.rtChannel || p.rtChannel; data.lastVersion = data.lastVersion || p.lastVersion; data.lastCpHash = data.lastCpHash || p.lastCpHash; @@ -75,6 +76,7 @@ define([ var bytes = 0; var historyBytes; var chan = [data.channel]; + if (data.answersChannel) { chan.push(data.answersChannel); } if (data.rtChannel) { chan.push(data.rtChannel); } if (data.lastVersion) { chan.push(Hash.hrefToHexChannelId(data.lastVersion)); } diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 0e1e9684e..2d08b52d4 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -2139,11 +2139,23 @@ define([ if (!data.channel) { return void cb({ error: 'ENOTFOUND'}); } if (!data.command) { return void cb({ error: 'EINVAL' }); } var s = getStore(data.teamId); + var otherChannels = data.channels; + delete data.channels; s.rpc.setMetadata(data, function (err, res) { if (err) { return void cb({ error: err }); } if (!Array.isArray(res) || !res.length) { return void cb({}); } cb(res[0]); }); + // If we have other related channels, send the command for them too + if (Array.isArray(otherChannels)) { + otherChannels.forEach(function (chan) { + var _d = Util.clone(data); + _d.channel = chan; + Store.setPadMetadata(clientId, _d, function () { + + }); + }); + } }; // GET_FULL_HISTORY from sframe-common-outer diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 9371b9e8b..526677d4b 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -810,6 +810,7 @@ define([ _findChannels(Env, toUnpin).forEach(function (id) { var data = _getFileData(Env, id); var arr = [data.channel]; + if (data.answersChannel) { arr.push(data.answersChannel); } if (data.rtChannel) { arr.push(data.rtChannel); } if (data.lastVersion) { arr.push(Hash.hrefToHexChannelId(data.lastVersion)); } Array.prototype.push.apply(toKeep, arr); @@ -1176,6 +1177,10 @@ define([ result.push(otherChan); } } + // Pin form answers channels + if (data.answersChannel && result.indexOf(data.answersChannel) === -1) { + result.push(data.answersChannel); + } // Pin onlyoffice realtime patches if (data.rtChannel && result.indexOf(data.rtChannel) === -1) { result.push(data.rtChannel); diff --git a/www/form/inner.js b/www/form/inner.js index bfc7dac3a..f53141842 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -597,6 +597,7 @@ define([ }; var checkIntegrity = function (getter) { + if (!content.order || !content.form) { return; } var changed = false; content.order.forEach(function (uid) { if (!content.form[uid]) { @@ -680,6 +681,8 @@ define([ } } + sframeChan.event('EV_FORM_PIN', {channel: content.answers.channel}); + var $container = $('#cp-app-form-container'); $container.append(makeFormCreator()); diff --git a/www/form/main.js b/www/form/main.js index ca4aea7e6..c02921887 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -17,6 +17,10 @@ define([ hash = obj.hash; }).nThen(function (/*waitFor*/) { var privateKey, publicKey; + var channels = {}; + var getPropChannels = function () { + return channels; + }; var addData = function (meta, CryptPad, user, Utils) { var keys = Utils.secret && Utils.secret.keys; @@ -45,6 +49,17 @@ define([ }; var addRpc = function (sframeChan, Cryptpad, Utils) { + sframeChan.on('EV_FORM_PIN', function (data) { + channels.answersChannel = data.channel; + Cryptpad.getPadAttribute('answersChannel', function (err, res) { + // If already stored, don't pin it again + if (res && res === data.channel) { return; } + Cryptpad.pinPads([data.channel], function () { + Cryptpad.setPadAttribute('answersChannel', data.channel, function () {}); + }); + }); + + }); sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, cb) { var myKeys = {}; var CPNetflux; @@ -83,7 +98,7 @@ define([ channel: data.channel, noChainPad: true, validateKey: keys.secondaryValidateKey, - owners: [myKeys.edPublic], // XXX add pad owner + owners: [myKeys.edPublic], crypto: crypto, // XXX Cache }; @@ -162,7 +177,7 @@ define([ var text = JSON.stringify(data.results); var ciphertext = crypto.encrypt(text, box.publicKey); - var hash = ciphertext.slice(0,64); // XXX use this to recover our previous answers + var hash = ciphertext.slice(0,64); Cryptpad.anonRpcMsg("WRITE_PRIVATE_MESSAGE", [ box.channel, ciphertext @@ -190,7 +205,8 @@ define([ hash: hash, href: href, useCreationScreen: true, - messaging: true + messaging: true, + getPropChannels: getPropChannels }); }); }); From 9c3dc7aa9c46f943c83ea863cecde5252f018ba4 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 26 May 2021 19:05:19 +0530 Subject: [PATCH 039/424] simplify some tests on the checkup page --- www/checkup/main.js | 89 ++++++++++++++++--------------------- www/checkup/sandbox/main.js | 4 +- 2 files changed, 40 insertions(+), 53 deletions(-) diff --git a/www/checkup/main.js b/www/checkup/main.js index 2b1c68ffa..2ab2cbbb0 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -574,6 +574,19 @@ define([ } }; + var deferredPostMessage = function (content, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); + nThen(function (w) { + sandboxIframeReady.reg(w(function (err) { + if (!err) { return; } + w.abort(); + cb(err); + })); + }).nThen(function () { + postMessage(content, cb); + }); + }; + window.addEventListener('message', function (event) { try { var msg = JSON.parse(event.data); @@ -638,46 +651,30 @@ define([ var url = '/sheet/inner.html'; var cb = Util.once(Util.mkAsync(_cb)); msg.appendChild(CSP_WARNING(url)); - nThen(function (w) { - sandboxIframeReady.reg(w(function (err) { - if (!err) { return; } - w.abort(); - cb(err); - })); - }).nThen(function () { - postMessage({ - command: 'GET_HEADER', - content: { - url: url, - header: 'content-security-policy', - }, - }, function (content) { - var CSP_headers = parseCSP(content); - cb(hasOnlyOfficeHeaders(CSP_headers)); - }); + deferredPostMessage({ + command: 'GET_HEADER', + content: { + url: url, + header: 'content-security-policy', + }, + }, function (content) { + var CSP_headers = parseCSP(content); + cb(hasOnlyOfficeHeaders(CSP_headers)); }); }); assert(function (cb, msg) { var url = '/common/onlyoffice/v4/web-apps/apps/spreadsheeteditor/main/index.html'; msg.appendChild(CSP_WARNING(url)); - nThen(function (w) { - sandboxIframeReady.reg(w(function (err) { - if (!err) { return; } - w.abort(); - cb(err); - })); - }).nThen(function () { - postMessage({ - command: 'GET_HEADER', - content: { - url: url, - header: 'content-security-policy', - }, - }, function (content) { - var CSP_headers = parseCSP(content); - cb(hasOnlyOfficeHeaders(CSP_headers)); - }); + deferredPostMessage({ + command: 'GET_HEADER', + content: { + url: url, + header: 'content-security-policy', + }, + }, function (content) { + var CSP_headers = parseCSP(content); + cb(hasOnlyOfficeHeaders(CSP_headers)); }); }); @@ -689,22 +686,14 @@ define([ code("'cross-origin-opener-policy'"), ' headers set.', ])); - nThen(function (w) { - sandboxIframeReady.reg(w(function (err) { - if (!err) { return; } - w.abort(); - cb(err); - })); - }).nThen(function () { - postMessage({ - command: 'GET_HEADER', - content: { - url: url, - header: 'cross-origin-opener-policy', - }, - }, function (content) { - cb(content === 'same-origin'); - }); + deferredPostMessage({ + command: 'GET_HEADER', + content: { + url: url, + header: 'cross-origin-opener-policy', + }, + }, function (content) { + cb(content === 'same-origin'); }); }); diff --git a/www/checkup/sandbox/main.js b/www/checkup/sandbox/main.js index 7450ed0e3..7ddfb07f8 100644 --- a/www/checkup/sandbox/main.js +++ b/www/checkup/sandbox/main.js @@ -1,12 +1,10 @@ define([ 'jquery', - //'/bower_components/nthen/index.js', - //'/common/common-util.js', '/bower_components/tweetnacl/nacl-fast.min.js', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/checkup/app-checkup.less', -], function ($ /*, nThen, Util */) { +], function ($) { var postMessage = function (content) { window.parent.postMessage(JSON.stringify(content), '*'); }; From a5245fba207eaa13aa21743f651163cf7cee3874 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 26 May 2021 19:07:21 +0530 Subject: [PATCH 040/424] disable attempted execution of scripts in PDFs it triggered a CSP error which prevented teh rest of the document from rendering --- www/common/pdfjs/build/pdf.worker.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/www/common/pdfjs/build/pdf.worker.js b/www/common/pdfjs/build/pdf.worker.js index 753d5c5d2..2e1bb365e 100644 --- a/www/common/pdfjs/build/pdf.worker.js +++ b/www/common/pdfjs/build/pdf.worker.js @@ -5355,10 +5355,12 @@ var PDFFunction = function PDFFunctionClosure() { var domain = IR[1]; var range = IR[2]; var code = IR[3]; +/* var compiled = new PostScriptCompiler().compile(code, domain, range); if (compiled) { return new Function('src', 'srcOffset', 'dest', 'destOffset', compiled); } +*/ (0, _util.info)('Unable to compile PS function'); var numOutputs = range.length >> 1; var numInputs = domain.length >> 1; @@ -38545,4 +38547,4 @@ if (typeof PDFJS === 'undefined' || !PDFJS.compatibilityChecked) { /***/ }) /******/ ]); }); -//# sourceMappingURL=pdf.worker.js.map \ No newline at end of file +//# sourceMappingURL=pdf.worker.js.map From 4577a3baf0ed5953059d370d6030bdf6d83463a3 Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 26 May 2021 17:02:34 +0200 Subject: [PATCH 041/424] Open and close a form --- www/common/common-ui-elements.js | 1 + www/form/app-form.less | 5 +- www/form/inner.js | 126 ++++++++++++++++++++++++++++++- 3 files changed, 129 insertions(+), 3 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 4e33b83d0..41b5dfe62 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1133,6 +1133,7 @@ define([ sheet: 'sheets', poll: 'poll', kanban: 'kanban', + form: 'form', whiteboard: 'whiteboard', }; diff --git a/www/form/app-form.less b/www/form/app-form.less index 4ea9d3cf6..cc67ff4f1 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -38,6 +38,10 @@ flex: 1; justify-content: center; + .cp-form-input-block { + display: flex; + } + div.cp-form-creator-container { display: flex; flex: 1; @@ -71,7 +75,6 @@ margin-bottom: 5px; } .cp-form-input-block { - display: flex; //width: @form_input-width; &:not(.editing) { input { diff --git a/www/form/inner.js b/www/form/inner.js index f53141842..01cb9275b 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -20,9 +20,11 @@ define([ '/common/inner/access.js', '/common/inner/properties.js', + '/lib/datepicker/flatpickr.js', '/bower_components/sortablejs/Sortable.min.js', '/bower_components/file-saver/FileSaver.min.js', + 'css!/lib/datepicker/flatpickr.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/form/app-form.less', ], function ( @@ -43,6 +45,7 @@ define([ Messages, AppConfig, Share, Access, Properties, + Flatpickr, Sortable ) { @@ -81,6 +84,13 @@ define([ Messages.form_isPublic = "Results are public"; Messages.form_isPrivate = "Results are private"; + Messages.form_open = "Open"; + Messages.form_setEnd = "Set closing date"; + Messages.form_removeEnd = "Remove closing date"; + Messages.form_isOpen = "This form is open"; + Messages.form_isClosed = "This form was closed on {0}"; + Messages.form_willClose = "This form will close on {0}"; + var editOptions = function (v, setCursorGetter, cb, tmp) { var add = h('button.btn.btn-secondary', [ h('i.fa.fa-plus'), @@ -304,8 +314,8 @@ define([ return results; }; var makeFormControls = function (framework, content, update) { - var send = h('button.btn.btn-primary', update ? Messages.form_update : Messages.form_submit); - var reset = h('button.btn.btn-danger-alt', Messages.form_reset); + var send = h('button.cp-open.btn.btn-primary', update ? Messages.form_update : Messages.form_submit); + var reset = h('button.cp-open.btn.btn-danger-alt', Messages.form_reset); $(reset).click(function () { if (!Array.isArray(APP.formBlocks)) { return; } APP.formBlocks.forEach(function (data) { @@ -345,6 +355,12 @@ define([ }); }); } + + if (APP.isClosed) { + send = undefined; + reset = undefined; + } + return h('div.cp-form-send-container', [send, reset, viewResults]); }; var updateForm = function (framework, content, editable, answers, temp) { @@ -542,6 +558,11 @@ define([ APP.isEditor = Boolean(priv.form_public); var $body = $('body'); + var $toolbarContainer = $('#cp-toolbar'); + var helpMenu = framework._.sfCommon.createHelpMenu(['text', 'pad']); + $toolbarContainer.after(helpMenu.menu); + + var makeFormSettings = function () { var makePublic = h('button.btn.btn-primary', Messages.form_makePublic); if (content.answers.privateKey) { makePublic = undefined; } @@ -563,6 +584,73 @@ define([ }); }); + // End date / Closed state + var endDateContainer = h('div.cp-form-status-container'); + var $endDate = $(endDateContainer); + var refreshEndDate = function () { + $endDate.empty(); + + var endDate = content.answers.endDate; + var date = new Date(endDate).toLocaleString(); + var now = +new Date(); + var text = Messages.form_isOpen; + var buttonTxt = Messages.form_setEnd; + if (endDate <= now) { + text = Messages._getKey('form_isClosed', [date]); + buttonTxt = Messages.form_open; + action = function () { + }; + } else if (endDate > now) { + text = Messages._getKey('form_willClose', [date]); + buttonTxt = Messages.form_removeEnd; + } + + var button = h('button.btn.btn-secondary', buttonTxt); + + var $button = $(button).click(function () { + $button.attr('disabled', 'disabled'); + // If there is an end date, remove it + if (endDate) { + delete content.answers.endDate; + framework.localChange(); + refreshEndDate(); + return; + } + // Otherwise add it + var datePicker = h('input'); + var is24h = false; + var dateFormat = "Y-m-d H:i"; + try { + is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); + } catch (e) {} + if (!is24h) { dateFormat = "Y-m-d h:i K"; } + var picker = Flatpickr(datePicker, { + enableTime: true, + time_24hr: is24h, + dateFormat: dateFormat, + minDate: new Date() + }); + var save = h('button.btn.btn-primary', Messages.settings_save); + $(save).click(function () { + var d = picker.parseDate(datePicker.value); + content.answers.endDate = +d; + framework.localChange(); + refreshEndDate(); + }); + var confirmContent = h('div', [ + h('div', Messages.form_setEnd), + h('div.cp-form-input-block', [datePicker, save]), + ]); + $button.after(confirmContent); + $button.remove(); + }); + + $endDate.append(h('div.cp-form-status', text)); + $endDate.append(h('div.cp-form-actions', button)); + + }; + refreshEndDate(); + var viewResults = h('button.btn.btn-primary', [ h('span.cp-app-form-button-results', Messages.form_viewResults), @@ -586,6 +674,7 @@ define([ }); return [ + endDateContainer, resultsType, viewResults, ]; @@ -659,6 +748,35 @@ define([ return div; }; + var endDateEl = h('div.alert.alert-warning.cp-burn-after-reading'); + var endDate; + var endDateTo; + var refreshEndDateBanner = function (force) { + var _endDate = content.answers.endDate; + if (_endDate === endDate && !force) { return; } + endDate = _endDate; + var date = new Date(endDate).toLocaleString(); + var text = Messages._getKey('form_isClosed', [date]); + if (endDate > +new Date()) { + text = Messages._getKey('form_willClose', [date]); + } + if ($('.cp-help-container').length && endDate) { + $(endDateEl).text(text); + $('.cp-help-container').before(endDateEl); + } else { + $(endDateEl).remove(); + } + + APP.isClosed = endDate && endDate < (+new Date()); + clearTimeout(endDateTo); + if (!APP.isClosed && endDate) { + setTimeout(function () { + refreshEndDateBanner(true); + $('.cp-form-send-container').find('.cp-open').remove(); + },(endDate - +new Date() + 100)); + } + }; + framework.onReady(function (isNew) { var priv = metadataMgr.getPrivateData(); @@ -720,6 +838,9 @@ define([ return; } + refreshEndDateBanner(); + + sframeChan.query("Q_FETCH_MY_ANSWERS", { channel: content.answers.channel, validateKey: content.answers.validateKey, @@ -739,6 +860,7 @@ define([ framework.onContentUpdate(function (newContent) { console.log(newContent); content = newContent; + refreshEndDateBanner(); var answers, temp; if (!APP.isEditor) { answers = getFormResults(); } else { temp = getTempFields(); } From d52dc7ce5b97b7577448bedddd1d4b0c817d9980 Mon Sep 17 00:00:00 2001 From: Weblate Date: Wed, 26 May 2021 17:45:21 +0200 Subject: [PATCH 042/424] Translated using Weblate (French) Currently translated at 100.0% (1237 of 1237 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/ --- www/common/translations/messages.fr.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/www/common/translations/messages.fr.json b/www/common/translations/messages.fr.json index 1e29c418a..afa6a12d0 100644 --- a/www/common/translations/messages.fr.json +++ b/www/common/translations/messages.fr.json @@ -1230,5 +1230,12 @@ "mediatag_defaultImageName": "image", "register_registrationIsClosed": "Les inscriptions sont fermées.", "oo_conversionSupport": "Votre navigateur ne gère pas la conversion vers et depuis les formats Microsoft Office. Il est recommandé d'utiliser une version récente de Firefox ou Chrome.", - "oo_importBin": "Cliquez sur OK pour importer au format .bin interne à CryptPad." + "oo_importBin": "Cliquez sur OK pour importer au format .bin interne à CryptPad.", + "admin_emailButton": "Valider", + "admin_supportPrivButton": "Afficher la clé", + "admin_supportPrivHint": "Afficher la clé privée et la transmettre aux autres administrateurs pour leur permettre de consulter les tickets de support. Un formulaire permettant de saisir cette clé est affiché dans leur espace d'administration.", + "admin_supportInitGenerate": "Créer les clés", + "admin_supportPrivTitle": "Clé privée de la messagerie de support", + "admin_emailHint": "Entrez ici l'adresse email de contact pour votre instance", + "admin_emailTitle": "Email de l'administrateur" } From edb839099460bb77c1bcdf07c04dae64d2f26cd4 Mon Sep 17 00:00:00 2001 From: Weblate Date: Wed, 26 May 2021 17:45:22 +0200 Subject: [PATCH 043/424] Translated using Weblate (Czech) Currently translated at 4.6% (57 of 1237 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/cs/ Translated using Weblate (Czech) Currently translated at 3.7% (47 of 1237 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/cs/ Translated using Weblate (Czech) Currently translated at 1.1% (14 of 1237 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/cs/ --- www/common/translations/messages.cs.json | 62 +++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/www/common/translations/messages.cs.json b/www/common/translations/messages.cs.json index 0967ef424..5a1dd3bbd 100644 --- a/www/common/translations/messages.cs.json +++ b/www/common/translations/messages.cs.json @@ -1 +1,61 @@ -{} +{ + "register_whyRegister": "Proč se registrovat?", + "register_mustAcceptTerms": "Musíte souhlasit s podmínkami služby.", + "register_passwordTooShort": "Heslo musí být alespoň {0} znaků dlouhé.", + "register_passwordsDontMatch": "Hesla nejsou stejná!", + "register_acceptTerms": "Potvrzuji podmínky služby", + "register_importRecent": "Importovat dokumenty z Vašeho neregistrovaného sezení", + "login_unhandledError": "Stala se neočekávaná chyba :(", + "login_invalPass": "Heslo je povinné", + "login_invalUser": "Uživatelské jméno je povinné", + "login_noSuchUser": "Chybné uživatelské jméno nebo heslo. Opakujte nebo se registrujte", + "login_hashing": "Hashuji Vaše heslo, to může chvíli trvat.", + "login_confirm": "Opakujte heslo", + "login_password": "Heslo", + "login_username": "Uživatelské jméno", + "padNotPinned": "Tento dokument expiruje za 3 měsíce neaktivity, {0}přihlašte se{1} nebo {2}se registrujte{3} pro jeho uchování.", + "onLogout": "Jste odhlášení, {0}klikněte zde{1} pro přihlášení
    nebo zmáčkněte Escape pro čtení dokumentu.", + "typeError": "Tento dokument není kompatibilní s vybranou aplikací", + "common_connectionLost": "Spojení se serverem ztraceno
    Nyní jste v režimu čtení, dokud nebude spojení obnoveno.", + "button_newsheet": "Nová tabulka", + "button_newwhiteboard": "Nová tabule", + "button_newslide": "Nová prezentace", + "button_newpoll": "Nové hlasování", + "button_newcode": "Nový kód", + "button_newpad": "Nový formátovaný dokument", + "type": { + "teams": "Týmy", + "sheet": "Tabulka", + "contacts": "Kontakty", + "todo": "Úkol", + "media": "Média", + "file": "Soubor", + "whiteboard": "Tabule", + "drive": "CryptDrive", + "slide": "Prezentace", + "poll": "Hlasování", + "code": "Kód", + "pad": "Formátovaný text" + }, + "logoutEverywhere": "Odhlásit se všude", + "login_login": "Přihlásit se", + "login_register": "Registrovat", + "logoutButton": "Odhlásit se", + "settingsButton": "Nastavení", + "register_emailWarning0": "Vypadá to, že byl vyplněn email jako uživatelské jméno.", + "register_alreadyRegistered": "Tento uživatel již existuje, chcete se přihlásit?", + "register_warning": "Varování", + "register_cancel": "Zrušit", + "register_writtenPassword": "Své uživatelské jméno a heslo jsem si zapsal, pokračovat", + "register_header": "Registrovat", + "deletedFromServer": "Dokument zničen", + "deleted": "Smazáno", + "saved": "Uloženo", + "error": "Chyba", + "loading": "Načítání...", + "invalidHashError": "Požadovaný dokument má neplatné URL.", + "inactiveError": "Tento dokument byl smazán kvůli neaktivitě. Zmáčkněte Escape pro vytvoření nového dokumentu.", + "deletedError": "Tento dokument byl smazán a není již dostupný.", + "expiredError": "Tento dokument expiroval a není již dostupný.", + "padNotPinnedVariable": "Tento dokument expiruje za {4} dny neaktivity, {0}přihlašte se{1} nebo {2}se registrujte{3} pro jeho uchování." +} From af7027e5edb4cc2b00bc2bf034225cb97fef3d25 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 27 May 2021 12:37:14 +0530 Subject: [PATCH 044/424] remove XXX that is actually O.K. --- www/admin/inner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/admin/inner.js b/www/admin/inner.js index 03e2270a9..3956d7e50 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -1412,7 +1412,7 @@ define([ var is24h = false; var dateFormat = "Y-m-d H:i"; try { - is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); // XXX + is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); } catch (e) {} if (!is24h) { dateFormat = "Y-m-d h:i K"; } From b8796d47513dd49721214a381445ad33920c9279 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 27 May 2021 12:37:50 +0530 Subject: [PATCH 045/424] WIP changelog --- CHANGELOG.md | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcb7e9230..e667c55b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,38 @@ -# WIP +# 4.6.0 -* fix opening links from temporary shared folders on iphone or other contexts that do not support shared workers -* add checkup test for disabling google FLoC +## Goals + +* work on polls/surveys +* stabilize and implement tests + +## Update notes + +* checkup/server/config + * test for anti-FLoC header + * add anti-FloC header to server so the default dev server passes all tests + * update NGINX example to avoid duplicated headers + * simplify dev server headers + * adjust table borders for dark/light mode + * say what headers are wrong +* rename exported object in `application_config_internal.js` to avoid copypasta errors +* `AppConfig.disableAnonymousPadCreation = false;` + * note that it's only enforced client-side * update lodash devDependency +## Features + +* generate `supportMailbox` keys via the admin panel + * document this +* markdown preview + * code blocks are full width + +## Bug fixes + +* fix opening links from temporary shared folders on iphone or other contexts that do not support shared workers +* show "features" instead of "pricing" in static pages' footer when premium subscriptions are not available +* use preferred 12/24h time format in date picker +* add error handling to admin panel decree RPCs + # 4.5.0 ## Goals From cba66d5db39444448e2027d2dba32871115abbdd Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 27 May 2021 14:17:32 +0530 Subject: [PATCH 046/424] close websockets when the checkup is complete --- www/checkup/main.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/www/checkup/main.js b/www/checkup/main.js index 2ab2cbbb0..66d2ccaaa 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -173,10 +173,13 @@ define([ }).nThen(function () { // Iframe is loaded clearTimeout(to); + console.log("removing sandbox iframe"); + $('iframe#sbox-iframe').remove(); cb(true); }); }); + var shared_websocket; // Test Websocket var evWSError = Util.mkEvent(true); assert(function (_cb, msg) { @@ -188,7 +191,7 @@ define([ ])); })); - var ws = new WebSocket(NetConfig.getWebsocketURL()); + var ws = shared_websocket = new WebSocket(NetConfig.getWebsocketURL()); var to = setTimeout(function () { console.error('Websocket TIMEOUT'); evWSError.fire(); @@ -207,6 +210,7 @@ define([ }); // Test login block + var shared_realtime; assert(function (_cb, msg) { var websocketErr = "No WebSocket available"; var cb = Util.once(Util.both(_cb, function (status) { @@ -268,7 +272,7 @@ define([ console.error("Can't create new channel. This may also be a websocket issue."); return void cb(false); } - RT = rt; + shared_realtime = RT = rt; var proxy = rt.proxy; proxy.edPublic = opt.edPublic; proxy.edPrivate = opt.edPrivate; @@ -334,7 +338,6 @@ define([ }).nThen(function () { cb(true); }); - }); var sheetURL = '/common/onlyoffice/v4/web-apps/apps/spreadsheeteditor/main/index.html'; @@ -511,7 +514,9 @@ define([ 'This instance does not provide a valid ', h('code', 'adminEmail'), ' which can make it difficult to contact its adminstrator to report vulnerabilities or abusive content.', - " This can be configured on your instance's admin panel. Use the provided 'Flush cache' button for this change to take effect for all users." + " This can be configured on your instance's admin panel. Use the provided ", + code("Flush cache'"), + " button for this change to take effect for all users.", ])); cb(email); }); @@ -591,6 +596,7 @@ define([ try { var msg = JSON.parse(event.data); if (msg.command === 'READY') { return void sandboxIframeReady.fire(); } + if (msg.q === "READY") { return; } // ignore messages from the usual sandboxed iframe var txid = msg.txid; if (!txid) { return console.log("no handler for ", txid); } response.handle(txid, msg.content); @@ -763,6 +769,14 @@ define([ $progress.remove(); $('body').prepend(report); + try { + console.log('closing shared websocket'); + shared_websocket.close(); + } catch (err) { console.error(err); } + try { + console.log('closing shared realtime'); + shared_realtime.network.disconnect(); + } catch (err) { console.error(err); } }, function (i, total) { console.log('test '+ i +' completed'); completed++; From ae2e7818035455994069dda41aaf4af6a2eecada Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 27 May 2021 14:18:06 +0530 Subject: [PATCH 047/424] remove mention of the supportMailboxPublicKey from the example config --- config/config.example.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/config/config.example.js b/config/config.example.js index ef54f3413..d05b985af 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -122,19 +122,6 @@ module.exports = { ], */ - /* CryptPad's administration panel includes a "support" tab - * wherein administrators with a secret key can view messages - * sent from users via the encrypted forms on the /support/ page - * - * To enable this functionality: - * run `node ./scripts/generate-admin-keys.js` - * save the public key in your config in the value below - * add the private key via the admin panel - * and back it up in a secure manner - * - */ - // supportMailboxPublicKey: "", - /* We're very proud that CryptPad is available to the public as free software! * We do, however, still need to pay our bills as we develop the platform. * From 8ecf7a70c441b8b4bfd44a09945828d48b0ef29c Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 27 May 2021 14:33:03 +0530 Subject: [PATCH 048/424] lint compliance and dead code removal --- www/checkup/main.js | 37 ++----------------------------------- 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/www/checkup/main.js b/www/checkup/main.js index 66d2ccaaa..2beee1229 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -191,7 +191,8 @@ define([ ])); })); - var ws = shared_websocket = new WebSocket(NetConfig.getWebsocketURL()); + var ws = new WebSocket(NetConfig.getWebsocketURL()); + shared_websocket = ws; var to = setTimeout(function () { console.error('Websocket TIMEOUT'); evWSError.fire(); @@ -381,40 +382,6 @@ define([ }); }); - assert(function (cb, msg) { - msg.innerText = "This test is incorrect."; - return void cb(true); - /* - msg.appendChild(h('span', [ - "The spreadsheet editor's code was not served with the required Content-Security Policy headers. ", - "This is most often caused by incorrectly configured sandbox parameters (", - h('code', 'httpUnsafeOrigin'), - ' and ', - h('code', 'httpSafeOrigin'), - ' in ', - CONFIG_PATH, - "), or settings in your reverse proxy's configuration which don't match your application server's config. ", - RESTART_WARNING(), - ])); - - $.ajax(sheetURL, { // FIXME bust cache - complete: function (xhr) { - var csp = xhr.getResponseHeader('Content-Security-Policy'); - if (!/unsafe\-eval/.test(csp)) { - // OnlyOffice requires unsafe-eval - console.error('CSP', csp); - return cb("expected 'unsafe-eval'"); - } - if (!/unsafe\-inline/.test(csp)) { - // OnlyOffice also requires unsafe-inline - console.error('CSP', csp); - return cb("expected 'unsafe-inline'"); - } - cb(true); - }, - }); */ - }); - assert(function (cb, msg) { msg.appendChild(h('span', [ h('code', '/api/broadcast'), From c92bbeb89c3b4981463d4e50e611dcb4540a24d7 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 27 May 2021 15:15:09 +0530 Subject: [PATCH 049/424] fix syntax error --- www/common/proxy-manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 0b19e9020..ccb6eabdf 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -241,7 +241,7 @@ define([ // Clean deprecated values if (Object.keys(proxy.metadata || {}).length > 1) { - proxy.metadata = { title: proxy.metadata.title; } + proxy.metadata = { title: proxy.metadata.title }; } var obj = Util.clone(proxy.metadata || {}); From 3ce5d477a17cb903c30e0631ebae9e25e9281753 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 27 May 2021 12:44:50 +0200 Subject: [PATCH 050/424] Fix issue allowing users to select disabled checkboxes and radio using the spacebar shortcut --- www/common/common-interface.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/www/common/common-interface.js b/www/common/common-interface.js index bba514dbf..4166c0902 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -1177,6 +1177,7 @@ define([ var label = h('span.cp-checkmark-label', labelTxt); $mark.keydown(function (e) { + if ($input.is(':disabled')) { return; } if (e.which === 32) { e.stopPropagation(); e.preventDefault(); @@ -1226,6 +1227,7 @@ define([ var label = h('span.cp-checkmark-label', labelTxt); $(mark).keydown(function (e) { + if ($input.is(':disabled')) { return; } if (e.which === 32) { e.stopPropagation(); e.preventDefault(); From 3a30f6e16fc8d5c29eb9e0997787ece00fe14140 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 27 May 2021 17:49:56 +0530 Subject: [PATCH 051/424] update changelog for 4.6.0 --- CHANGELOG.md | 53 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e667c55b3..81880bd7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,36 +2,47 @@ ## Goals -* work on polls/surveys -* stabilize and implement tests +Our main goal for this release cycle was to get a strong start on our upcoming _forms_ app. This is a big job which we didn't expect to finish in the course of a few weeks, so in the meantime we've taken the opportunity to address many minor issues, stabilize the codebase, and implement a number of new tests. ## Update notes -* checkup/server/config - * test for anti-FLoC header - * add anti-FloC header to server so the default dev server passes all tests - * update NGINX example to avoid duplicated headers - * simplify dev server headers - * adjust table borders for dark/light mode - * say what headers are wrong -* rename exported object in `application_config_internal.js` to avoid copypasta errors -* `AppConfig.disableAnonymousPadCreation = false;` - * note that it's only enforced client-side -* update lodash devDependency +Over the years the example configuration file has grown to include a large number of parameters. We've seen that this can make it hard to pick out which configuration parameters are important for a newly installed or migrated instance. We're trying to address this by moving more configuration options to the admin panel. + +4.6.0 introduces the ability to generate credentials for your instance's support ticket mailbox and publish the corresponding public key with the push of a button. Previously it was necessary to run a script, copy its value, update the config file, restart the server, and enter the private component of the keypair into an input on the admin panel. The relevant button can be found in the admin panel's _Support_ tab. + +We've also introduced the ability to update your _adminEmail_ settings via a field on the _General_ tab of the admin panel. This value is used by the contact page so that your users can contact you (instead of us) in case they encounter any problems when using your instance. Both the `supportMailbox` and `adminEmail` values are distributed by the `/api/config` endpoint which is typically cached by clients. You probably need to use the _Flush cache_ button to ensure that everyone loads the latest value. This button can also found on the _General_ tab. + +One admin reported difficulty customizing their instance because they copy-pasted code from `cryptpad/www/common/application_config_internal.js` directly into `cryptpad/customize/application_config.js`. Unfortunately the internal variable name for the configuration object in the former did not match the value in the latter, so this led to a reference error. We've updated the variable name in the internal configuration file which provides the default options to match the customizable one, making it easier to copy-paste code examples without understanding what it's really doing. + +We also introduced a new configuration option in `application_config_internal.js` which prevents unregistered users from creating new pads. Add `AppConfig.disableAnonymousPadCreation = true;` to your `customize/application_config.js` to disable anonymous pad creation. If you read the adjacent comment above the default example you'll see that this barrier is only enforced on the client, so it will keep out honest users but won't stop malicious ones from messaging the server directly. + +This release also includes a number of new tests on the `/checkup/` page. Most notably it now checks for headers on certain assets which can only be checked from within the sandboxed iframe. These new tests automate the manual checks we were performing when admins reported that everything was working except for sheets, and go a little bit further to report which particular headers are incorrect. We also fixed some bugs that were checking headers on resources which could be cached, added a test for the recently added anti-FLoC header, fixed the styles on the page to respond to both light and dark mode, and made sure that websocket connections that were opened by tests were closed when they finished. + +Some of the tests we implemented checked the headers on resources that were particularly prone to misconfiguration because its headers were set by both NGINX and the NodeJS application server (see [#694](https://github.com/xwiki-labs/cryptpad/issues/694)). We tested in a variety of configurations and ultimately decided that the most resilient solution was to give up on using heuristics in the application server and just update the example NGINX config to use a patch proposed by another admin which fully overrides the settings of the application server. You can find this patch in the `/api/(config|broadcast)` section of the example config. + +Finally, we've made some minor changes to the provided `package-lock.json` file because `npm` reported some "Regular Expression Denial of Service" vulnerabilities. One of these was easy to fix, but another two were reported shortly thereafter. These "vulnerabilities" only affect some developer dependencies and will have no effect on regular usage of our software. The "risk" is essentially that malicious modifications to our source code can be tailored to make our style linting software run particularly slowly. This can only be triggered by integrating such malicious changes into your local repository and running `npm run lint:less`, so maybe don't do that. + +To update from 4.5.0 to 4.6.0: + +1. Apply the documented NGINX configuration +2. Stop your server +3. Get the latest code with git +4. Install the latest dependencies with `bower update` and `npm i` +5. Restart your server ## Features -* generate `supportMailbox` keys via the admin panel - * document this -* markdown preview - * code blocks are full width +This release includes very few new features aside from those already mentioned in the _Update notes_ section. One very minor improvement is that formatted code blocks in the code editor's markdown preview use the full width of their parent container instead of being indented. ## Bug fixes -* fix opening links from temporary shared folders on iphone or other contexts that do not support shared workers -* show "features" instead of "pricing" in static pages' footer when premium subscriptions are not available -* use preferred 12/24h time format in date picker -* add error handling to admin panel decree RPCs +* Once again we fixed a bug that only occurs on Safari because Apple refuses to implement APIs that make the web a viable competitor to their app store. This one was triggered by opening a shared folder from its link as an unregistered user, then trying to open a pad stored only in that folder and not elsewhere in your drive. Literally every other browser supports _SharedWorkers_, which allow tabs on the same domain to share a background process, reducing consumption of CPU, RAM, and electricity, as well as allowing the newly opened tab to read the document's credentials from the temporarily loaded shared folder. On Safari the new tab failed to load. We fixed it by checking whether the shared folder would be accessible from newly opened tabs, and choosing to use the document's "unsafe link" instead of its "safe link". +* We updated the "Features" page to be displayed as "Pricing" in the footer when some prospective clients reported that they couldn't find a mention of what they would get by creating a premium subscription. [#683](https://github.com/xwiki-labs/cryptpad/issues/683) had the opposite problem, that they didn't support payment and they wanted to only show features. Now the footer displays the appropriate string depending on your instance's configuration. +* We fixed some inconsistent UI in our recently introduced date picker. The time formats displayed in the text field and date picker interface should now match the localization settings provided to your browser by your OS. Previously it was possible for one of these elements to appear in 24 hour time while the other appeared in 12 hour time. +* Another time-related issue appeared in the calendar for users in Hawai'i, who reported that some events were displayed on the wrong day due to the incorrect initialization of a reference date. +* We've applied a minor optimization which should reduce the size of shared folders. +* Some functionality on the admin panel has been improved with some better error handling. +* Finally, one user reported that one of their PDFs was displaying only blank pages. After a short investigation we found that the problematic PDF was trying to run some scripts which were being blocked by our strict Content-Security-Policy headers. We've updated our PDF renderer to avoid compiling and running such scripts. As a result, such PDFs should not be prevented from rendering, though they may lack some dynamic functionality that you might be expecting. We'd welcome an example of such a PDF so we can assess if there is a safe way to load their embedded scripts and how much work would be required to do so. # 4.5.0 From c87c122c3e93065706f7e91d90b019851a2b5ae1 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 27 May 2021 18:05:00 +0530 Subject: [PATCH 052/424] capitalize a thing --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81880bd7d..7799523df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Goals -Our main goal for this release cycle was to get a strong start on our upcoming _forms_ app. This is a big job which we didn't expect to finish in the course of a few weeks, so in the meantime we've taken the opportunity to address many minor issues, stabilize the codebase, and implement a number of new tests. +Our main goal for this release cycle was to get a strong start on our upcoming _Forms_ app. This is a big job which we didn't expect to finish in the course of a few weeks, so in the meantime we've taken the opportunity to address many minor issues, stabilize the codebase, and implement a number of new tests. ## Update notes From 29dc1a5b3ba2f5c368b374f1dbd4f6108d203e2d Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 27 May 2021 17:46:46 +0200 Subject: [PATCH 053/424] Add more block types in form --- www/form/app-form.less | 35 ++- www/form/inner.js | 503 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 517 insertions(+), 21 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index cc67ff4f1..f0182d9a7 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -109,6 +109,18 @@ } &.editable { cursor: grab; } } + .cp-form-edit-max-options { + display: flex; + align-items: center; + input { + width: 100px; + margin-left: 10px; + } + } + .cp-form-edit-options-block { + display: flex; + flex-wrap: wrap; + } .cp-form-edit-block { .cp-form-edit-block-input { display: flex; @@ -158,6 +170,10 @@ } .cp-form-results-type-radio { display: table; + .cp-form-results-type-multiradio-data { + display: flex; + flex-flow: column; + } .cp-form-results-type-radio-data { display: table-row; border: 1px solid @cp_form-border; @@ -175,7 +191,7 @@ } } - .cp-form-type-radio { + .cp-form-type-radio, .cp-form-type-checkbox { display: flex; flex-flow: column; align-items: baseline; @@ -183,6 +199,23 @@ display: inline-flex; } } + .cp-form-type-multiradio { + display: table; + & > * { + display: table-row; + & > * { + display: table-cell; + padding: 5px 20px; + vertical-align: middle; + &:first-child { + min-width: 200px; + } + .cp-radio-mark { + margin: auto; + } + } + } + } } diff --git a/www/form/inner.js b/www/form/inner.js index 01cb9275b..02f052888 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -57,14 +57,17 @@ define([ Messages.form_invalid = "Invalid form"; Messages.form_editBlock = "Edit options"; Messages.form_editQuestion = "Edit question"; - - Messages.form_newOption = "New option"; + Messages.form_editMax = "Max selectable options"; Messages.form_default = "Your question here?"; Messages.form_type_input = "Text"; // XXX Messages.form_type_radio = "Radio"; // XXX + Messages.form_type_multiradio = "Multiline Radio"; // XXX + Messages.form_type_checkbox = "Checkbox"; // XXX + Messages.form_type_multicheck = "Multiline Checkbox"; // XXX Messages.form_duplicates = "Duplicate entries have been removed"; + Messages.form_maxOptions = "{0} answer(s) max"; Messages.form_submit = "Submit"; Messages.form_update = "Update"; @@ -91,42 +94,109 @@ define([ Messages.form_isClosed = "This form was closed on {0}"; Messages.form_willClose = "This form will close on {0}"; + Messages.form_defaultOption = "Option {0}"; + Messages.form_defaultItem = "Item {0}"; + Messages.form_newOption = "New option"; + Messages.form_newItem = "New item"; + Messages.form_add_option = "Add option"; + Messages.form_add_item = "Add item"; + + + var MAX_OPTIONS = 10; // XXX + var MAX_ITEMS = 10; // XXX + var editOptions = function (v, setCursorGetter, cb, tmp) { var add = h('button.btn.btn-secondary', [ h('i.fa.fa-plus'), - h('span', Messages.tag_add) + h('span', Messages.form_add_option) + ]); + var addItem = h('button.btn.btn-secondary', [ + h('i.fa.fa-plus'), + h('span', Messages.form_add_item) ]); var cursor; if (tmp && tmp.content && Sortify(v) === Sortify(tmp.old)) { - v = tmp.content.values; + v = tmp.content; cursor = tmp.cursor; } + var maxOptions, maxInput; + if (typeof(v.max) === "number") { + maxInput = h('input', { + type:"number", + value: v.max, + min: 1, + max: v.values.length + }) + maxOptions = h('div.cp-form-edit-max-options', [ + h('span', Messages.form_editMax), + maxInput + ]); + } + // Show existing options - var getOption = function (val) { + var $add; + var getOption = function (val, isItem, uid) { var input = h('input', {value:val}); + if (uid) { $(input).data('uid', uid); } // if this element was active before the remote change, restore cursor if (cursor && cursor.el === val) { + console.log(isItem); + console.log(cursor.item); + console.log(Boolean(isItem)); + console.log(Boolean(cursor.item)); + } + + var setCursor = function () { input.selectionStart = cursor.start || 0; input.selectionEnd = cursor.end || 0; setTimeout(function () { input.focus(); }); + }; + if (isItem) { + if (cursor && cursor.uid === uid && cursor.item) { setCursor(); } + } else { + if (cursor && cursor.el === val && !cursor.item) { setCursor(); } } var del = h('button.btn.btn-danger', h('i.fa.fa-times')); var el = h('div.cp-form-edit-block-input', [ input, del ]); - $(del).click(function () { $(el).remove(); }); + $(del).click(function () { + $(el).remove(); + // We've just deleted an item/option so we should be under the MAX limit and + // we can show the "add" button again + if (isItem && $addItem) { $addItem.show(); } + if (!isItem && $add) { $add.show(); } + }); return el; }; - var inputs = v.map(getOption); + var inputs = v.values.map(function (val) { return getOption(val, false); }); inputs.push(add); + var container = h('div.cp-form-edit-block', inputs); - // Add option - var $add = $(add).click(function () { - $add.before(getOption(Messages.form_newOption)); + var containerItems; + if (v.items) { + var inputsItems = v.items.map(function (itemData) { + return getOption(itemData.v, true, itemData.uid); + }); + inputsItems.push(addItem); + containerItems = h('div.cp-form-edit-block', inputsItems); + } + + // "Add option" button handler + $add = $(add).click(function () { + $add.before(getOption(Messages.form_newOption, false)); + if ($(container).find('input').length >= MAX_OPTIONS) { $add.hide(); } + }); + // If multiline block, handle "Add item" button + $addItem = $(addItem).click(function () { + $addItem.before(getOption(Messages.form_newItem, true, Util.uid())); + if ($(containerItems).find('input').length >= MAX_ITEMS) { $addItem.hide(); } }); + if ($(container).find('input').length >= MAX_OPTIONS) { $add.hide(); } + if ($(containerItems).find('input').length >= MAX_ITEMS) { $addItem.hide(); } // Cancel changes var cancelBlock = h('button.btn.btn-secondary', Messages.cancel); @@ -145,9 +215,31 @@ define([ } values.push($(el).val()); }); + var _content = {values: values}; + + if (maxInput) { + _content.max = Number($(maxInput).val()) || 1; + } + + if (v.items) { + var items = []; + $(containerItems).find('input').each(function (i, el) { + if (el === active) { + cursor.item = true; + cursor.uid= $(el).data('uid'); + cursor.start = el.selectionStart; + cursor.end = el.selectionEnd; + } + items.push({ + uid: $(el).data('uid'), + v: $(el).val() + }); + }); + _content.items = items; + } return { - old: v, - content: {values: values}, + old: (tmp && tmp.old) || v, + content: _content, cursor: cursor }; }); @@ -159,6 +251,8 @@ define([ ]); $(saveBlock).click(function () { $(saveBlock).attr('disabled', 'disabled'); + + // Get values var values = []; var duplicates = false; $(container).find('input').each(function (i, el) { @@ -166,14 +260,43 @@ define([ if (values.indexOf(val) === -1) { values.push(val); } else { duplicates = true; } }); + var res = { values: values }; + + // If multiline block, get items + if (v.items) { + var items = []; + $(containerItems).find('input').each(function (i, el) { + var val = $(el).val().trim(); + var uid = $(el).data('uid'); + if (!items.some(function (i) { return i.uid === uid; })) { + items.push({ + uid: $(el).data('uid'), + v: val + }); + } + else { duplicates = true; } + }); + res.items = items; + } + + // Show duplicates warning if (duplicates) { UI.warn(Messages.form_duplicates); } - cb({values: values}); + + // If checkboxes, get the maximum number of values the users can select + if (maxInput) { + var maxVal = Number($(maxInput).val()); + if (isNaN(maxVal)) { maxVal = values.length; } + res.max = maxVal; + } + + cb(res); }); return [ - container, + maxOptions, + h('div.cp-form-edit-options-block', [containerItems, container]), h('div', [cancelBlock, saveBlock]) ]; }; @@ -184,6 +307,18 @@ define([ } }; + var findItem = function (items, uid) { + if (!Array.isArray(items)) { return; } + var res; + items.some(function (item) { + if (item.uid === uid) { + res = item.v; + return true; + } + }); + return res; + }; + var TYPES = { input: { get: function () { @@ -213,10 +348,13 @@ define([ }, radio: { defaultOpts: { - values: ["Option 1", "Option 2"] // XXX? + values: [1,2].map(function (i) { + return Messages._getKey('form_defaultOption', [i]); + }) }, get: function (opts) { if (!opts) { opts = TYPES.radio.defaultOpts; } + if (!Array.isArray(opts.values)) { return; } var name = Util.uid(); var els = opts.values.map(function (data, i) { var radio = UI.createRadio(name, 'cp-form-'+name+'-'+i, @@ -232,15 +370,17 @@ define([ getValue: function () { var res; els.some(function (el, i) { - if (Util.isChecked($(el).find('input'))) { - res = opts.values[i]; + var $i = $(el).find('input'); + if (Util.isChecked($i)) { + res = $i.data('val'); + return true; } }); return res; }, reset: function () { $(tag).find('input').removeAttr('checked'); }, edit: function (cb, tmp) { - var v = opts.values.slice(); + var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); }, getCursor: function () { return cursorGetter(); }, @@ -279,7 +419,326 @@ define([ return h('div.cp-form-results-type-radio', results); }, icon: h('i.fa.fa-list-ul') - } + }, + multiradio: { + defaultOpts: { + items: [1,2].map(function (i) { + return { + uid: Util.uid(), + v: Messages._getKey('form_defaultItem', [i]) + }; + }), + values: [1,2].map(function (i) { + return Messages._getKey('form_defaultOption', [i]); + }) + }, + get: function (opts) { + if (!opts) { opts = TYPES.multiradio.defaultOpts; } + if (!Array.isArray(opts.items) || !Array.isArray(opts.values)) { return; } + var lines = opts.items.map(function (itemData) { + var name = itemData.uid; + var item = itemData.v; + var els = opts.values.map(function (data, i) { + var radio = UI.createRadio(name, 'cp-form-'+name+'-'+i, + '', false, { mark: { tabindex:1 } }); + $(radio).find('input').data('uid', name); + $(radio).find('input').data('val', data); + return radio; + }); + els.unshift(h('div.cp-form-multiradio-item', item)); + return h('div.radio-group', {'data-uid':name}, els); + }); + var header = opts.values.map(function (v) { return h('span', v); }); + header.unshift(h('span')); + lines.unshift(h('div.cp-form-multiradio-header', header)); + + var tag = h('div.radio-group.cp-form-type-multiradio', lines); + var cursorGetter; + var setCursorGetter = function (f) { cursorGetter = f; }; + return { + tag: tag, + getValue: function () { + var res = {}; + var l = lines.slice(1); + l.forEach(function (el, i) { + var $el = $(el); + var uid = $el.attr('data-uid'); + var $l = $el.find('input').each(function (i, input) { + var $i = $(input); + if (res[uid]) { return; } + if (Util.isChecked($i)) { res[uid] = $i.data('val'); } + }); + }); + return res; + }, + reset: function () { $(tag).find('input').removeAttr('checked'); }, + edit: function (cb, tmp) { + var v = Util.clone(opts); + return editOptions(v, setCursorGetter, cb, tmp); + }, + getCursor: function () { return cursorGetter(); }, + setValue: function (val) { + this.reset(); + Object.keys(val || {}).forEach(function (uid) { + $(tag).find('[name="'+uid+'"]').each(function (i, el) { + if ($(el).data('val') !== val[uid]) { return; } + $(el).prop('checked', true); + }); + }); + } + }; + + }, + printResults: function (answers, uid, form) { + var structure = form[uid]; + if (!structure) { return; } + var results = []; + var empty = 0; + var count = {}; + Object.keys(answers).forEach(function (author) { + var obj = answers[author]; + var answer = obj.msg[uid]; + if (!answer || !Object.keys(answer).length) { return empty++; } + //count[answer] = count[answer] || {}; + Object.keys(answer).forEach(function (q_uid) { + var c = count[q_uid] = count[q_uid] || {}; + var res = answer[q_uid]; + if (!res || !res.trim()) { return; } + c[res] = c[res] || 0; + c[res]++; + }); + }); + Object.keys(count).forEach(function (q_uid) { + var q = findItem(structure.opts.items, q_uid); + var c = count[q_uid]; + var values = Object.keys(c).map(function (res) { + return h('div.cp-form-results-type-radio-data', [ + h('span.cp-value', res), + h('span.cp-count', c[res]) + ]); + }); + results.push(h('div.cp-form-results-type-multiradio-data', [ + h('span.cp-mr-q', q), + h('span.cp-mr-value', values) + ])); + }); + results.push(getEmpty(empty)); + + return h('div.cp-form-results-type-radio', results); + }, + icon: h('i.fa.fa-list-ul') + }, + checkbox: { + defaultOpts: { + max: 3, + values: [1, 2, 3].map(function (i) { + return Messages._getKey('form_defaultOption', [i]); + }) + }, + get: function (opts) { + if (!opts) { opts = TYPES.checkbox.defaultOpts; } + if (!Array.isArray(opts.values)) { return; } + var name = Util.uid(); + var els = opts.values.map(function (data, i) { + var cbox = UI.createCheckbox('cp-form-'+name+'-'+i, + data, false, { mark: { tabindex:1 } }); + $(cbox).find('input').data('val', data); + return cbox; + }); + var tag = h('div', [ + h('div.cp-form-max-options', Messages._getKey('form_maxOptions', [opts.max])), + h('div.radio-group.cp-form-type-checkbox', els) + ]); + var $tag = $(tag); + $tag.find('input').on('change', function () { + var selected = $tag.find('input:checked').length; + if (selected >= opts.max) { + $tag.find('input:not(:checked)').attr('disabled', 'disabled'); + } else { + $tag.find('input').removeAttr('disabled'); + } + }); + var cursorGetter; + var setCursorGetter = function (f) { cursorGetter = f; }; + return { + tag: tag, + getValue: function () { + var res = []; + els.forEach(function (el, i) { + var $i = $(el).find('input'); + if (Util.isChecked($i)) { + res.push($i.data('val')); + } + }); + return res; + }, + reset: function () { $(tag).find('input').removeAttr('checked'); }, + edit: function (cb, tmp) { + var v = Util.clone(opts); + return editOptions(v, setCursorGetter, cb, tmp); + }, + getCursor: function () { return cursorGetter(); }, + setValue: function (val) { + this.reset(); + if (!Array.isArray(val)) { return; } + els.forEach(function (el) { + var $el = $(el).find('input'); + if (val.indexOf($el.data('val')) !== -1) { + $el.prop('checked', true); + } + }); + } + }; + + }, + printResults: function (answers, uid) { + var results = []; + var empty = 0; + var count = {}; + Object.keys(answers).forEach(function (author) { + var obj = answers[author]; + var answer = obj.msg[uid]; + if (!Array.isArray(answer) || !answer.length) { return empty++; } + answer.forEach(function (val) { + count[val] = count[val] || 0; + count[val]++; + }); + }); + Object.keys(count).forEach(function (value) { + results.push(h('div.cp-form-results-type-radio-data', [ + h('span.cp-value', value), + h('span.cp-count', count[value]) + ])); + }); + results.push(getEmpty(empty)); + + return h('div.cp-form-results-type-radio', results); + }, + icon: h('i.fa.fa-check-square-o') + }, + multicheck: { + defaultOpts: { + max: 3, + items: [1,2].map(function (i) { + return { + uid: Util.uid(), + v: Messages._getKey('form_defaultItem', [i]) + }; + }), + values: [1,2,3].map(function (i) { + return Messages._getKey('form_defaultOption', [i]); + }) + }, + get: function (opts) { + if (!opts) { opts = TYPES.multicheck.defaultOpts; } + if (!Array.isArray(opts.items) || !Array.isArray(opts.values)) { return; } + var lines = opts.items.map(function (itemData) { + var name = itemData.uid; + var item = itemData.v; + var els = opts.values.map(function (data, i) { + var cbox = UI.createCheckbox('cp-form-'+name+'-'+i, + '', false, { mark: { tabindex:1 } }); + $(cbox).find('input').data('uid', name); + $(cbox).find('input').data('val', data); + return cbox; + }); + els.unshift(h('div.cp-form-multiradio-item', item)); + return h('div.radio-group', {'data-uid':name}, els); + }); + + lines.forEach(function (l) { + $(l).find('input').on('change', function () { + var selected = $(l).find('input:checked').length; + if (selected >= opts.max) { + $(l).find('input:not(:checked)').attr('disabled', 'disabled'); + } else { + $(l).find('input').removeAttr('disabled'); + } + }); + }); + + var header = opts.values.map(function (v) { return h('span', v); }); + header.unshift(h('span')); + lines.unshift(h('div.cp-form-multiradio-header', header)); + + var tag = h('div.radio-group.cp-form-type-multiradio', lines); + var cursorGetter; + var setCursorGetter = function (f) { cursorGetter = f; }; + return { + tag: tag, + getValue: function () { + var res = {}; + var l = lines.slice(1); + l.forEach(function (el, i) { + var $el = $(el); + var uid = $el.attr('data-uid'); + res[uid] = []; + var $l = $el.find('input').each(function (i, input) { + var $i = $(input); + if (Util.isChecked($i)) { res[uid].push($i.data('val')); } + }); + }); + return res; + }, + reset: function () { $(tag).find('input').removeAttr('checked'); }, + edit: function (cb, tmp) { + var v = Util.clone(opts); + return editOptions(v, setCursorGetter, cb, tmp); + }, + getCursor: function () { return cursorGetter(); }, + setValue: function (val) { + this.reset(); + Object.keys(val || {}).forEach(function (uid) { + if (!Array.isArray(val[uid])) { return; } + $(tag).find('[data-uid="'+uid+'"] input').each(function (i, el) { + if (val[uid].indexOf($(el).data('val')) === -1) { return; } + $(el).prop('checked', true); + }); + }); + } + }; + + }, + printResults: function (answers, uid, form) { + var structure = form[uid]; + if (!structure) { return; } + var results = []; + var empty = 0; + var count = {}; + Object.keys(answers).forEach(function (author) { + var obj = answers[author]; + var answer = obj.msg[uid]; + if (!answer || !Object.keys(answer).length) { return empty++; } + Object.keys(answer).forEach(function (q_uid) { + var c = count[q_uid] = count[q_uid] || {}; + var res = answer[q_uid]; + if (!Array.isArray(res) || !res.length) { return; } + res.forEach(function (v) { + c[v] = c[v] || 0; + c[v]++; + }); + }); + }); + Object.keys(count).forEach(function (q_uid) { + var q = findItem(structure.opts.items, q_uid); + var c = count[q_uid]; + var values = Object.keys(c).map(function (res) { + return h('div.cp-form-results-type-radio-data', [ + h('span.cp-value', res), + h('span.cp-count', c[res]) + ]); + }); + results.push(h('div.cp-form-results-type-multiradio-data', [ + h('span.cp-mr-q', q), + h('span.cp-mr-value', values) + ])); + }); + results.push(getEmpty(empty)); + + return h('div.cp-form-results-type-radio', results); + }, + icon: h('i.fa.fa-list-ul') + }, }; var renderResults = function (content, answers) { @@ -290,7 +749,7 @@ define([ var type = block.type; var model = TYPES[type]; if (!model || !model.printResults) { return; } - var print = model.printResults(answers, uid); + var print = model.printResults(answers, uid, form); var q = h('div.cp-form-block-question', block.q || Messages.form_default); return h('div.cp-form-block', [ @@ -336,6 +795,7 @@ define([ console.error(err || data.error); return void UI.warn(Messages.error); } + $send.removeAttr('disabled'); UI.alert(Messages.form_sent); $send.text(Messages.form_update); }); @@ -379,6 +839,7 @@ define([ if (!model) { return; } var data = model.get(block.opts); + if (!data) { return; } data.uid = uid; if (answers && answers[uid]) { data.setValue(answers[uid]); } @@ -476,6 +937,7 @@ define([ $(editButtons).show(); UI.log(Messages.saved); data = model.get(newOpts); + if (!data) { data = {}; } $oldTag.before(data.tag).remove(); }); }; @@ -752,6 +1214,7 @@ define([ var endDate; var endDateTo; var refreshEndDateBanner = function (force) { + if (APP.isEditor) { return; } var _endDate = content.answers.endDate; if (_endDate === endDate && !force) { return; } endDate = _endDate; From 0068eaa5503ae2744a59bd1a3518d3eaa2064fb7 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 28 May 2021 14:55:33 +0530 Subject: [PATCH 054/424] fix pricing link in top bar --- customize.dist/pages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/customize.dist/pages.js b/customize.dist/pages.js index aaf7c60b3..c7e142a7f 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -215,7 +215,7 @@ define([ h('div.collapse.navbar-collapse.justify-content-end#menuCollapse', [ h('a.nav-item.nav-link', { href: '/what-is-cryptpad.html'}, Msg.about), h('a.nav-item.nav-link', { href: 'https://docs.cryptpad.fr'}, Msg.docs_link), - h('a.nav-item.nav-link', { href: '/features.html'}, Msg.pricing), + h('a.nav-item.nav-link', { href: '/features.html'}, Pages.areSubscriptionsAllowed()? Msg.pricing: Msg.features), ].concat(rightLinks)) ); }; From 24e181ab9adcfb6f3ea825ce29fc911833348ee2 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 28 May 2021 15:34:27 +0530 Subject: [PATCH 055/424] elaborate on some messages in the checkup page --- www/checkup/main.js | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/www/checkup/main.js b/www/checkup/main.js index 2beee1229..054280b84 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -373,8 +373,28 @@ define([ }); }); + var link = function (href, text) { + return h('a', { + href: href, + rel: 'noopener noreferrer', + target: '_blank', + }, text); + }; + assert(function (cb, msg) { - msg.innerText = "Missing HTTP header required to disable Google's Floc."; + setWarningClass(msg); + msg.appendChild(h('span', [ + "You haven't opted out of participation in Google's ", + code('FLoC'), + " targeted advertizing network. This can be done by adding a ", + code('permissions-policy'), + " HTTP header with a value of ", + code('interest-cohort=()'), + " in your reverse proxy's configuration. See the provided NGINX configuration file for an example. ", + h('p', [ + link("https://www.eff.org/deeplinks/2021/04/am-i-floced-launch", 'Learn more'), + ]), + ])); $.ajax('/?'+ (+new Date()), { complete: function (xhr) { cb(xhr.getResponseHeader('permissions-policy') === 'interest-cohort=()'); @@ -493,12 +513,9 @@ define([ setWarningClass(msg); msg.appendChild(h('span', [ "This instance's encrypted support ticket functionality has not been enabled. This can make it difficult for its users to safely report issues that concern sensitive information. ", - "This can be configured via the ", - h('code', 'supportMailbox'), - " attribute in ", - CONFIG_PATH(), - ". ", - RESTART_WARNING(), + "This can be configured via the admin panel's ", + h('code', 'Support'), + " tab.", ])); cb(support && typeof(support) === 'string' && support.length === 44); }); From 75a1d7617d9dcc54d005979461c6912fbe45902d Mon Sep 17 00:00:00 2001 From: Weblate Date: Fri, 28 May 2021 12:08:12 +0200 Subject: [PATCH 056/424] Translated using Weblate (Indonesian) Currently translated at 1.7% (22 of 1237 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/id/ --- www/common/translations/messages.id.json | 28 +++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/www/common/translations/messages.id.json b/www/common/translations/messages.id.json index 0967ef424..a66724a75 100644 --- a/www/common/translations/messages.id.json +++ b/www/common/translations/messages.id.json @@ -1 +1,27 @@ -{} +{ + "type": { + "pad": "Teks kaya", + "sheet": "Lembar Kerja", + "teams": "Tim", + "contacts": "Kontak", + "todo": "Tugas", + "media": "Media", + "file": "Berkas", + "whiteboard": "Papan tulis", + "drive": "CryptDrive", + "slide": "Presentasi", + "kanban": "Kanban", + "poll": "Pemilihan", + "code": "Kode" + }, + "typeError": "Papan ini tidak sesuai dengan aplikasi yang dipilih", + "common_connectionLost": "Koneksi Server Terputus
    Kau sekarang di mode baca hingga koneksi kembali.", + "button_newsheet": "Lembar Kerja Baru", + "button_newkanban": "Kanban Baru", + "button_newwhiteboard": "Papan Tulis Baru", + "button_newslide": "Presentasi Baru", + "button_newpoll": "Pemilihan Baru", + "button_newcode": "papan Kode Baru", + "button_newpad": "papan Teks Kaya Baru", + "main_title": "Cryptpad: Informasi Aman, Kolaborasi Waktu Nyata" +} From a948237043209254ef9c3996f2c9b051be1520f6 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 May 2021 14:04:24 +0200 Subject: [PATCH 057/424] Add Poll block type --- .../src/less2/include/colortheme-dark.less | 5 + .../src/less2/include/colortheme.less | 5 + www/common/common-ui-elements.js | 2 + www/form/app-form.less | 61 ++++ www/form/inner.js | 300 ++++++++++++++++-- 5 files changed, 354 insertions(+), 19 deletions(-) diff --git a/customize.dist/src/less2/include/colortheme-dark.less b/customize.dist/src/less2/include/colortheme-dark.less index 68a456bcc..8dbe77271 100644 --- a/customize.dist/src/less2/include/colortheme-dark.less +++ b/customize.dist/src/less2/include/colortheme-dark.less @@ -431,3 +431,8 @@ @cp_form-bg1: @cryptpad_color_grey_800; @cp_form-bg2: @cryptpad_color_grey_900; @cp_form-border: @cryptpad_color_grey_800; +@cp_form-poll-color: @cryptpad_color_grey_800; +@cp_form-poll-no: @cryptpad_color_light_red; +@cp_form-poll-yes: @cryptpad_color_light_green; +@cp_form-poll-maybe: @cryptpad_color_light_yellow; +@cp_form_poll-yes-color: @cryptpad_color_green; diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less index c385cd83c..e68da8835 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -431,3 +431,8 @@ @cp_form-bg1: @cryptpad_color_grey_200; @cp_form-bg2: @cryptpad_color_grey_100; @cp_form-border: @cryptpad_color_grey_200; +@cp_form-poll-color: @cryptpad_color_grey_800; +@cp_form-poll-no: @cryptpad_color_light_red; +@cp_form-poll-yes: @cryptpad_color_light_green; +@cp_form-poll-maybe: @cryptpad_color_light_yellow; +@cp_form_poll-yes-color: @cryptpad_color_green; diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 41b5dfe62..6485e9c64 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1473,11 +1473,13 @@ define([ if (config.isSelect) { var pressed = ''; var to; + $container.onChange = Util.mkEvent(); $container.on('click', 'a', function () { value = $(this).data('value'); var $val = $(this); var textValue = $val.html() || value; $button.find('.cp-dropdown-button-title').html(textValue); + $container.onChange.fire(textValue, value); }); $container.keydown(function (e) { var $value = $innerblock.find('[data-value].cp-dropdown-element-active:visible'); diff --git a/www/form/app-form.less b/www/form/app-form.less index f0182d9a7..8795b71aa 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -217,5 +217,66 @@ } } + .cp-form-type-poll { + display: flex; + flex-flow: column; + & > div { + display: flex; + } + .cp-poll-cell { + width: 100px; + height: 40px; + display: inline-flex; + align-items: center; + justify-content: center; + &:first-child { + width: 200px; + } + button { + width: 100%; + } + } + &.cp-form-poll-switch { + flex-flow: row; + & > div { + flex-flow: column; + } + .cp-poll-cell:not(.cp-poll-switch) { + &:first-child { + width: 100px; + } + } + .cp-form-poll-option, .cp-poll-switch { + width: 200px; + } + } + .cp-form-poll-choice, .cp-form-poll-answer { + .fa { + display: none; + } + color: @cp_form-poll-color; + &[data-value="0"] { + background: @cp_form-poll-no; + .cp-no { display: inline; } + } + &[data-value="1"] { + background: @cp_form-poll-yes; + .cp-yes { display: inline; } + } + &[data-value="2"] { + background: @cp_form-poll-maybe; + .cp-maybe { display: inline; } + } + } + div.cp-form-poll-choice { + cursor: pointer; + padding: 5px; + border: 5px double @cp_form-bg1; + } + div.cp-form-poll-answer { + color: @cp_form_poll-yes-color; + } + } + } diff --git a/www/form/inner.js b/www/form/inner.js index 02f052888..4dbe64df8 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -53,11 +53,24 @@ define([ var APP = window.APP = { }; + var is24h = false; + var dayFormat = "Y-m-d"; + var dateFormat = "Y-m-d H:i"; + try { + is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); + } catch (e) {} + if (!is24h) { dateFormat = "Y-m-d h:i K"; } + Messages.button_newform = "New Form"; // XXX Messages.form_invalid = "Invalid form"; Messages.form_editBlock = "Edit options"; Messages.form_editQuestion = "Edit question"; Messages.form_editMax = "Max selectable options"; + Messages.form_editType = "Options type"; + + Messages.form_poll_text = "Text"; + Messages.form_poll_day = "Day"; + Messages.form_poll_time = "Time"; Messages.form_default = "Your question here?"; Messages.form_type_input = "Text"; // XXX @@ -65,6 +78,7 @@ define([ Messages.form_type_multiradio = "Multiline Radio"; // XXX Messages.form_type_checkbox = "Checkbox"; // XXX Messages.form_type_multicheck = "Multiline Checkbox"; // XXX + Messages.form_type_poll = "Poll"; // XXX Messages.form_duplicates = "Duplicate entries have been removed"; Messages.form_maxOptions = "{0} answer(s) max"; @@ -135,21 +149,57 @@ define([ ]); } + var type, typeSelect; + if (v.type) { + var options = ['text', 'day', 'time'].map(function (t) { + return { + tag: 'a', + attributes: { + 'class': 'cp-form-type-value', + 'data-value': t, + 'href': '#', + }, + content: Messages['form_poll_'+t] + }; + }); + var dropdownConfig = { + text: '', // Button initial text + options: options, // Entries displayed in the menu + //left: true, // Open to the left of the button + //container: $(type), + isSelect: true, + caretDown: true, + buttonCls: 'btn btn-secondary' + }; + typeSelect = UIElements.createDropdown(dropdownConfig); + typeSelect.setValue(v.type); + + type = h('div.cp-form-edit-type', [ + h('span', Messages.form_editType), + typeSelect[0] + ]); + } + // Show existing options var $add; var getOption = function (val, isItem, uid) { var input = h('input', {value:val}); if (uid) { $(input).data('uid', uid); } - // if this element was active before the remote change, restore cursor - if (cursor && cursor.el === val) { - console.log(isItem); - console.log(cursor.item); - console.log(Boolean(isItem)); - console.log(Boolean(cursor.item)); + // If the input is a date, initialize flatpickr + if (v.type && v.type !== 'text') { + if (v.type === 'time') { + Flatpickr(input, { + enableTime: true, + time_24hr: is24h, + dateFormat: dateFormat, + }); + } else if (v.type === 'day') { Flatpickr(input); } } + // if this element was active before the remote change, restore cursor var setCursor = function () { + if (v.type !== 'text') { return; } input.selectionStart = cursor.start || 0; input.selectionEnd = cursor.end || 0; setTimeout(function () { input.focus(); }); @@ -175,6 +225,7 @@ define([ inputs.push(add); var container = h('div.cp-form-edit-block', inputs); + var $container = $(container); var containerItems; if (v.items) { @@ -185,17 +236,47 @@ define([ containerItems = h('div.cp-form-edit-block', inputsItems); } + // Doodle type change: empty current values and change input types? + if (typeSelect) { + typeSelect.onChange.reg(function (prettyVal, val) { + $container.find('input').each(function (i, input) { + if (!input._flatpickr && val !== 'text') { + input.value = ""; + } + + if (input._flatpickr) { + input._flatpickr.destroy(); + delete input._flatpickr; + } + if (val === 'time') { + Flatpickr(input, { + enableTime: true, + time_24hr: is24h, + dateFormat: dateFormat, + }); + } + if (val === 'day') { + Flatpickr(input, { + time_24hr: is24h, + }); + } + }); + }); + } + // "Add option" button handler $add = $(add).click(function () { $add.before(getOption(Messages.form_newOption, false)); - if ($(container).find('input').length >= MAX_OPTIONS) { $add.hide(); } + var l = $container.find('input').length; + $(maxInput).attr('max', l); + if (l >= MAX_OPTIONS) { $add.hide(); } }); // If multiline block, handle "Add item" button $addItem = $(addItem).click(function () { $addItem.before(getOption(Messages.form_newItem, true, Util.uid())); if ($(containerItems).find('input').length >= MAX_ITEMS) { $addItem.hide(); } }); - if ($(container).find('input').length >= MAX_OPTIONS) { $add.hide(); } + if ($container.find('input').length >= MAX_OPTIONS) { $add.hide(); } if ($(containerItems).find('input').length >= MAX_ITEMS) { $addItem.hide(); } // Cancel changes @@ -207,8 +288,8 @@ define([ var values = []; var active = document.activeElement; var cursor = {}; - $(container).find('input').each(function (i, el) { - if (el === active) { + $container.find('input').each(function (i, el) { + if (el === active && !el._flatpickr) { cursor.el= $(el).val(); cursor.start = el.selectionStart; cursor.end = el.selectionEnd; @@ -221,6 +302,10 @@ define([ _content.max = Number($(maxInput).val()) || 1; } + if (typeSelect) { + _content.type = typeSelect.getValue(); + } + if (v.items) { var items = []; $(containerItems).find('input').each(function (i, el) { @@ -255,7 +340,7 @@ define([ // Get values var values = []; var duplicates = false; - $(container).find('input').each(function (i, el) { + $container.find('input').each(function (i, el) { var val = $(el).val().trim(); if (values.indexOf(val) === -1) { values.push(val); } else { duplicates = true; } @@ -291,16 +376,63 @@ define([ res.max = maxVal; } + if (typeSelect) { + res.type = typeSelect.getValue(); + } + cb(res); }); return [ + type, maxOptions, h('div.cp-form-edit-options-block', [containerItems, container]), h('div', [cancelBlock, saveBlock]) ]; }; + var makePollTable = function (answers, opts) { + // Create first line with options + var els = opts.values.map(function (data, i) { + if (opts.type === "day") { + var _date = new Date(data); + data = _date.toLocaleDateString(); + } + return h('div.cp-poll-cell.cp-form-poll-option', data); + }); + // Insert axis switch button + var switchAxis = h('button.btn', [ + h('i.fa.fa-exchange'), + ]); + els.unshift(h('div.cp-poll-cell.cp-poll-switch', switchAxis)); + var lines = [h('div', els)]; + + // Add answers + if (Array.isArray(answers)) { + answers.forEach(function (answer) { + if (!answer.name || !answer.values) { return; } + var _name = answer.name; + var values = answer.values || {}; + var els = opts.values.map(function (data) { + var res = values[data] || 0; + var v = (Number(res) === 1) ? h('i.fa.fa-check.cp-yes') : undefined; + var cell = h('div.cp-poll-cell.cp-form-poll-answer', { + 'data-value': res + }, v); + return cell; + }); + els.unshift(h('div.cp-poll-cell.cp-poll-answer-name', _name)); + lines.push(h('div', els)); + }); + } + + var $s = $(switchAxis).click(function () { + $s.closest('.cp-form-type-poll').toggleClass('cp-form-poll-switch'); + }); + + return lines; + }; + var getEmpty = function (empty) { if (empty) { return UI.setHTML(h('div.cp-form-results-type-text-empty'), Messages._getKey('form_notAnswered', [empty])); @@ -739,6 +871,99 @@ define([ }, icon: h('i.fa.fa-list-ul') }, + poll: { + defaultOpts: { + type: 'text', // Text or Days or Time + values: [1, 2, 3].map(function (i) { + return Messages._getKey('form_defaultOption', [i]); + }) + }, + get: function (opts, answers, username) { + if (!opts) { opts = TYPES.poll.defaultOpts; } + if (!Array.isArray(opts.values)) { return; } + var name = Util.uid(); + + var lines = makePollTable(answers, opts); + + // Add form + // XXX only if not already answered! + var addLine = opts.values.map(function (data, i) { + var cell = h('div.cp-poll-cell.cp-form-poll-choice', [ + h('i.fa.fa-times.cp-no'), + h('i.fa.fa-check.cp-yes'), + h('i.fa.fa-question.cp-maybe'), + ]); + var $c = $(cell); + $c.data('option', data); + var val = 0; + $c.attr('data-value', val); + $c.click(function () { + val = (val+1)%3; + $c.attr('data-value', val); + }); + cell._setValue = function (v) { + val = v; + $c.attr('data-value', val); + }; + return cell; + }); + // Name input + var nameInput = h('input', { value: username }); + addLine.unshift(h('div.cp-poll-cell', nameInput)); + // XXX Submit button here? + lines.push(h('div', addLine)); + + + + var tag = h('div.cp-form-type-poll', lines); + var $tag = $(tag); + + var cursorGetter; + var setCursorGetter = function (f) { cursorGetter = f; }; + return { + tag: tag, + getValue: function () { + var res = {}; + var name = $(nameInput).val().trim() || Messages.anonymous; + $tag.find('.cp-form-poll-choice').each(function (i, el) { + var $el = $(el); + res[$el.data('option')] = $el.attr('data-value'); + }); + return { + name: name, + values: res + }; + }, + reset: function () { + $tag.find('.cp-form-poll-choice').attr('data-value', 0); + }, + edit: function (cb, tmp) { + var v = Util.clone(opts); + return editOptions(v, setCursorGetter, cb, tmp); + }, + getCursor: function () { return cursorGetter(); }, + setValue: function (res) { + this.reset(); + if (!res || !res.values || !res.name) { return; } + var val = res.values; + $(nameInput).val(res.name); + $tag.find('.cp-form-poll-choice').each(function (i, el) { + if (!el._setValue) { return; } + var $el = $(el); + console.log(el, $el.data('option'), val); + el._setValue(val[$el.data('option')] || 0); + }); + } + }; + + }, + printResults: function (answers, uid, form) { + var _answers = getBlockAnswers(answers, uid); + var lines = makePollTable(_answers, form[uid].opts); + return h('div.cp-form-type-poll', lines); + }, + icon: h('i.fa.fa-check-square-o') + }, }; var renderResults = function (content, answers) { @@ -764,6 +989,14 @@ define([ $container.append(elements); }; + var getBlockAnswers = function (answers, uid, filterCurve) { + return Object.keys(answers || {}).map(function (user) { + if (filterCurve && user === filterCurve) { return; } + try { + return answers[user].msg[uid]; + } catch (e) { console.error(e); } + }).filter(Boolean); + }; var getFormResults = function () { if (!Array.isArray(APP.formBlocks)) { return; } var results = {}; @@ -809,6 +1042,7 @@ define([ var $v = $(viewResults).click(function () { $v.attr('disabled', 'disabled'); sframeChan.query("Q_FORM_FETCH_ANSWERS", content.answers, function (err, answers) { + if (answers) { APP.answers = answers; } $v.removeAttr('disabled'); $('body').addClass('cp-app-form-results'); renderResults(content, answers); @@ -838,7 +1072,17 @@ define([ var model = TYPES[type]; if (!model) { return; } - var data = model.get(block.opts); + var _answers, name; + if (type === 'poll') { + var metadataMgr = framework._.cpNfInner.metadataMgr; + var user = metadataMgr.getUserData(); + // If we are a participant, our results shouldn't be in the table but in the + // editable part: remove them from _answers + _answers = getBlockAnswers(APP.answers, uid, !editable && user.curvePublic); + name = user.name; + } + + var data = model.get(block.opts, _answers, name); if (!data) { return; } data.uid = uid; if (answers && answers[uid]) { data.setValue(answers[uid]); } @@ -936,7 +1180,8 @@ define([ framework._.cpNfInner.chainpad.onSettle(function () { $(editButtons).show(); UI.log(Messages.saved); - data = model.get(newOpts); + var _answers = getBlockAnswers(APP.answers, uid); + data = model.get(newOpts, answers); if (!data) { data = {}; } $oldTag.before(data.tag).remove(); }); @@ -1015,6 +1260,7 @@ define([ var sframeChan = framework._.sfCommon.getSframeChannel(); var metadataMgr = framework._.cpNfInner.metadataMgr; + var user = metadataMgr.getUserData(); var priv = metadataMgr.getPrivateData(); APP.isEditor = Boolean(priv.form_public); @@ -1080,12 +1326,6 @@ define([ } // Otherwise add it var datePicker = h('input'); - var is24h = false; - var dateFormat = "Y-m-d H:i"; - try { - is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); - } catch (e) {} - if (!is24h) { dateFormat = "Y-m-d h:i K"; } var picker = Flatpickr(datePicker, { enableTime: true, time_24hr: is24h, @@ -1129,6 +1369,7 @@ define([ validateKey: content.answers.validateKey, publicKey: content.answers.publicKey }, function (err, answers) { + if (answers) { APP.answers = answers; } $v.removeAttr('disabled'); $body.addClass('cp-app-form-results'); renderResults(content, answers); @@ -1281,6 +1522,7 @@ define([ publicKey: content.answers.publicKey, privateKey: priv.form_auditorKey }, function (err, obj) { + if (obj) { APP.answers = obj; } $body.addClass('cp-app-form-results'); renderResults(content, obj); }); @@ -1303,6 +1545,26 @@ define([ refreshEndDateBanner(); + // If the results are public and there is at least one doodle, fetch the results now + if (content.answers.privateKey && Object.keys(content.form).some(function (uid) { + return content.form[uid].type === "poll"; + })) { + sframeChan.query("Q_FORM_FETCH_ANSWERS", { + channel: content.answers.channel, + validateKey: content.answers.validateKey, + publicKey: content.answers.publicKey, + privateKey: content.answers.privateKey, + }, function (err, obj) { + if (obj) { APP.answers = obj; } + checkIntegrity(false); + var myAnswers; + if (user.curvePublic && obj && obj[user.curvePublic]) { // XXX ANONYMOUS + myAnswers = obj[user.curvePublic].msg; + } + updateForm(framework, content, false, myAnswers); + }); + return; + } sframeChan.query("Q_FETCH_MY_ANSWERS", { channel: content.answers.channel, From 6694d9df03d49c23b7e41442b8e6e0bdf1c1bcdb Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 May 2021 14:13:16 +0200 Subject: [PATCH 058/424] lint compliance --- www/common/common-interface.js | 9 +++--- www/common/cryptpad-common.js | 2 +- www/form/inner.js | 50 ++++++++++++++++------------------ www/form/main.js | 22 +++++---------- 4 files changed, 36 insertions(+), 47 deletions(-) diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 4166c0902..9bdf9ec7a 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -1223,6 +1223,7 @@ define([ $.extend(markOpts, opts.mark || {}); var input = h('input', inputOpts); + var $input = $(input); var mark = h('span.cp-radio-mark', markOpts); var label = h('span.cp-checkmark-label', labelTxt); @@ -1231,13 +1232,13 @@ define([ if (e.which === 32) { e.stopPropagation(); e.preventDefault(); - if ($(input).is(':checked')) { return; } - $(input).prop('checked', !$(input).is(':checked')); - $(input).change(); + if ($input.is(':checked')) { return; } + $input.prop('checked', !$input.is(':checked')); + $input.change(); } }); - $(input).change(function () { $(mark).focus(); }); + $input.change(function () { $(mark).focus(); }); var radio = h('label', labelOpts, [ input, diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 973a27d6b..69ed47587 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -69,7 +69,7 @@ define([ }, cb); }; - common.getAccessKeys = function (cb, opts) { + common.getAccessKeys = function (cb) { var keys = []; Nthen(function (waitFor) { // Push account keys diff --git a/www/form/inner.js b/www/form/inner.js index 4dbe64df8..48a4bae93 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -23,7 +23,6 @@ define([ '/lib/datepicker/flatpickr.js', '/bower_components/sortablejs/Sortable.min.js', - '/bower_components/file-saver/FileSaver.min.js', 'css!/lib/datepicker/flatpickr.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/form/app-form.less', @@ -49,12 +48,10 @@ define([ Sortable ) { - var SaveAs = window.saveAs; var APP = window.APP = { }; var is24h = false; - var dayFormat = "Y-m-d"; var dateFormat = "Y-m-d H:i"; try { is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); @@ -142,7 +139,7 @@ define([ value: v.max, min: 1, max: v.values.length - }) + }); maxOptions = h('div.cp-form-edit-max-options', [ h('span', Messages.form_editMax), maxInput @@ -181,7 +178,7 @@ define([ } // Show existing options - var $add; + var $add, $addItem; var getOption = function (val, isItem, uid) { var input = h('input', {value:val}); if (uid) { $(input).data('uid', uid); } @@ -393,7 +390,7 @@ define([ var makePollTable = function (answers, opts) { // Create first line with options - var els = opts.values.map(function (data, i) { + var els = opts.values.map(function (data) { if (opts.type === "day") { var _date = new Date(data); data = _date.toLocaleDateString(); @@ -451,6 +448,15 @@ define([ return res; }; + var getBlockAnswers = function (answers, uid, filterCurve) { + return Object.keys(answers || {}).map(function (user) { + if (filterCurve && user === filterCurve) { return; } + try { + return answers[user].msg[uid]; + } catch (e) { console.error(e); } + }).filter(Boolean); + }; + var TYPES = { input: { get: function () { @@ -501,7 +507,7 @@ define([ tag: tag, getValue: function () { var res; - els.some(function (el, i) { + els.some(function (el) { var $i = $(el).find('input'); if (Util.isChecked($i)) { res = $i.data('val'); @@ -592,10 +598,10 @@ define([ getValue: function () { var res = {}; var l = lines.slice(1); - l.forEach(function (el, i) { + l.forEach(function (el) { var $el = $(el); var uid = $el.attr('data-uid'); - var $l = $el.find('input').each(function (i, input) { + $el.find('input').each(function (i, input) { var $i = $(input); if (res[uid]) { return; } if (Util.isChecked($i)) { res[uid] = $i.data('val'); } @@ -696,7 +702,7 @@ define([ tag: tag, getValue: function () { var res = []; - els.forEach(function (el, i) { + els.forEach(function (el) { var $i = $(el).find('input'); if (Util.isChecked($i)) { res.push($i.data('val')); @@ -801,11 +807,11 @@ define([ getValue: function () { var res = {}; var l = lines.slice(1); - l.forEach(function (el, i) { + l.forEach(function (el) { var $el = $(el); var uid = $el.attr('data-uid'); res[uid] = []; - var $l = $el.find('input').each(function (i, input) { + $el.find('input').each(function (i, input) { var $i = $(input); if (Util.isChecked($i)) { res[uid].push($i.data('val')); } }); @@ -881,13 +887,12 @@ define([ get: function (opts, answers, username) { if (!opts) { opts = TYPES.poll.defaultOpts; } if (!Array.isArray(opts.values)) { return; } - var name = Util.uid(); var lines = makePollTable(answers, opts); // Add form // XXX only if not already answered! - var addLine = opts.values.map(function (data, i) { + var addLine = opts.values.map(function (data) { var cell = h('div.cp-poll-cell.cp-form-poll-choice', [ h('i.fa.fa-times.cp-no'), h('i.fa.fa-check.cp-yes'), @@ -989,14 +994,6 @@ define([ $container.append(elements); }; - var getBlockAnswers = function (answers, uid, filterCurve) { - return Object.keys(answers || {}).map(function (user) { - if (filterCurve && user === filterCurve) { return; } - try { - return answers[user].msg[uid]; - } catch (e) { console.error(e); } - }).filter(Boolean); - }; var getFormResults = function () { if (!Array.isArray(APP.formBlocks)) { return; } var results = {}; @@ -1034,8 +1031,9 @@ define([ }); }); + var viewResults; if (content.answers.privateKey) { - var viewResults = h('button.btn.btn-primary', [ + viewResults = h('button.btn.btn-primary', [ h('span.cp-app-form-button-results', Messages.form_viewResults), ]); var sframeChan = framework._.sfCommon.getSframeChannel(); @@ -1181,7 +1179,7 @@ define([ $(editButtons).show(); UI.log(Messages.saved); var _answers = getBlockAnswers(APP.answers, uid); - data = model.get(newOpts, answers); + data = model.get(newOpts, _answers); if (!data) { data = {}; } $oldTag.before(data.tag).remove(); }); @@ -1306,8 +1304,6 @@ define([ if (endDate <= now) { text = Messages._getKey('form_isClosed', [date]); buttonTxt = Messages.form_open; - action = function () { - }; } else if (endDate > now) { text = Messages._getKey('form_willClose', [date]); buttonTxt = Messages.form_removeEnd; @@ -1481,7 +1477,7 @@ define([ } }; - framework.onReady(function (isNew) { + framework.onReady(function () { var priv = metadataMgr.getPrivateData(); if (APP.isEditor) { diff --git a/www/form/main.js b/www/form/main.js index c02921887..27e6552de 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -8,6 +8,7 @@ define([ ], function (nThen, ApiConfig, DomReady, SFCommonO) { var Nacl = window.nacl; + var href, hash; // Loaded in load #2 nThen(function (waitFor) { DomReady.onReady(waitFor()); @@ -44,8 +45,8 @@ define([ channel: Utils.secret.channel, keys: { viewKeyStr: Nacl.util.encodeBase64(keys.cryptKey) } }); - var parsed = Utils.Hash.parseTypeHash('pad', auditorHash); - meta.form_auditorHash = parsed.getHash({auditorKey: privateKey}); + var _parsed = Utils.Hash.parseTypeHash('pad', auditorHash); + meta.form_auditorHash = _parsed.getHash({auditorKey: privateKey}); }; var addRpc = function (sframeChan, Cryptpad, Utils) { @@ -67,7 +68,7 @@ define([ nThen(function (w) { require([ '/bower_components/chainpad-netflux/chainpad-netflux.js', - ], w(function (_CPNetflux, _Crypto) { + ], w(function (_CPNetflux) { CPNetflux = _CPNetflux; })); Cryptpad.getAccessKeys(w(function (_keys) { @@ -83,7 +84,7 @@ define([ Cryptpad.makeNetwork(w(function (err, nw) { network = nw; })); - }).nThen(function (w) { + }).nThen(function () { if (!network) { return void cb({error: "E_CONNECT"}); } var keys = Utils.secret && Utils.secret.keys; @@ -120,9 +121,6 @@ define([ }); }); sframeChan.on("Q_FETCH_MY_ANSWERS", function (data, cb) { - var keys; - var CPNetflux; - var network; var answer; var myKeys; nThen(function (w) { @@ -136,7 +134,7 @@ define([ } answer = obj; })); - }).nThen(function (w) { + }).nThen(function () { Cryptpad.getHistoryRange({ channel: data.channel, lastKnownHash: answer.hash, @@ -144,7 +142,6 @@ define([ }, function (obj) { if (obj && obj.error) { return void cb(obj); } var messages = obj.messages; - var ephemeral_priv = answer.curvePrivate; var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, { validateKey: data.validateKey, ephemeral_private: Nacl.util.decodeBase64(answer.curvePrivate), @@ -164,7 +161,7 @@ define([ Cryptpad.getFormKeys(w(function (keys) { myKeys = keys; })); - }).nThen(function (w) { + }).nThen(function () { var keys = Utils.secret && Utils.secret.keys; myKeys.signingKey = keys.secondarySignKey; @@ -191,11 +188,6 @@ define([ }); }); }); - sframeChan.on('EV_FORM_MAILBOX', function (data) { - var curvePair = Nacl.box.keyPair(); - publicKey = Nacl.util.encodeBase64(curvePair.publicKey); - privateKey = Nacl.util.encodeBase64(curvePair.secretKey); - }); }; SFCommonO.start({ addData: addData, From ee670350150143b7907a4f092208965949928022 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 May 2021 14:22:06 +0200 Subject: [PATCH 059/424] Fix poll forms issues --- www/form/inner.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 48a4bae93..844e27dc0 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -241,6 +241,8 @@ define([ input.value = ""; } + v.type = val; + if (input._flatpickr) { input._flatpickr.destroy(); delete input._flatpickr; @@ -263,6 +265,7 @@ define([ // "Add option" button handler $add = $(add).click(function () { + var txt = v.type ? '' : Messages.form_newOption; $add.before(getOption(Messages.form_newOption, false)); var l = $container.find('input').length; $(maxInput).attr('max', l); @@ -891,7 +894,6 @@ define([ var lines = makePollTable(answers, opts); // Add form - // XXX only if not already answered! var addLine = opts.values.map(function (data) { var cell = h('div.cp-poll-cell.cp-form-poll-choice', [ h('i.fa.fa-times.cp-no'), @@ -913,7 +915,7 @@ define([ return cell; }); // Name input - var nameInput = h('input', { value: username }); + var nameInput = h('input', { value: username || '' }); addLine.unshift(h('div.cp-poll-cell', nameInput)); // XXX Submit button here? lines.push(h('div', addLine)); From ed5cc5158bd8b5743fb1ae0c23a679f262f4d421 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 May 2021 14:23:02 +0200 Subject: [PATCH 060/424] Fix poll forms issues... --- www/form/inner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/form/inner.js b/www/form/inner.js index 844e27dc0..4e9eee66c 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -266,7 +266,7 @@ define([ // "Add option" button handler $add = $(add).click(function () { var txt = v.type ? '' : Messages.form_newOption; - $add.before(getOption(Messages.form_newOption, false)); + $add.before(getOption(txt, false)); var l = $container.find('input').length; $(maxInput).attr('max', l); if (l >= MAX_OPTIONS) { $add.hide(); } From e68fccc863fe59e3cd26fd8529505f2d932d43b9 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 May 2021 15:35:53 +0200 Subject: [PATCH 061/424] Fix cursor in forms --- www/form/inner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/form/inner.js b/www/form/inner.js index 4e9eee66c..71fe9de9b 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -196,7 +196,7 @@ define([ // if this element was active before the remote change, restore cursor var setCursor = function () { - if (v.type !== 'text') { return; } + if (v.type && v.type !== 'text') { return; } input.selectionStart = cursor.start || 0; input.selectionEnd = cursor.end || 0; setTimeout(function () { input.focus(); }); From 8871f41bfef2cf22de8663f7589ae3c45b838d0d Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 May 2021 16:58:22 +0200 Subject: [PATCH 062/424] Improve rendering of form polls with time values --- www/form/app-form.less | 18 +++++++++++++++- www/form/inner.js | 48 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 8795b71aa..efe97ed86 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -74,6 +74,9 @@ .cp-form-block-question { margin-bottom: 5px; } + .cp-form-block-content { + overflow-x: auto; + } .cp-form-input-block { //width: @form_input-width; &:not(.editing) { @@ -218,7 +221,7 @@ } .cp-form-type-poll { - display: flex; + display: inline-flex; flex-flow: column; & > div { display: flex; @@ -236,6 +239,12 @@ width: 100%; } } + .cp-poll-time-day { + flex-basis: 100px; + border-right: 1px solid @cryptpad_text_col; + border-left: 1px solid @cryptpad_text_col; + border-top: 1px solid @cryptpad_text_col; + } &.cp-form-poll-switch { flex-flow: row; & > div { @@ -249,6 +258,13 @@ .cp-form-poll-option, .cp-poll-switch { width: 200px; } + .cp-poll-time-day { + flex-basis: 40px; + border-right: none; + border-bottom: 1px solid @cryptpad_text_col; + border-left: 1px solid @cryptpad_text_col; + border-top: 1px solid @cryptpad_text_col; + } } .cp-form-poll-choice, .cp-form-poll-answer { .fa { diff --git a/www/form/inner.js b/www/form/inner.js index 71fe9de9b..a0a52e673 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -53,10 +53,15 @@ define([ var is24h = false; var dateFormat = "Y-m-d H:i"; + var timeFormat = "H:i"; try { is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); } catch (e) {} - if (!is24h) { dateFormat = "Y-m-d h:i K"; } + is24h = false; + if (!is24h) { + dateFormat = "Y-m-d h:i K"; + timeFormat = "h:i K"; + } Messages.button_newform = "New Form"; // XXX Messages.form_invalid = "Invalid form"; @@ -190,8 +195,13 @@ define([ enableTime: true, time_24hr: is24h, dateFormat: dateFormat, + defaultDate: val ? new Date(val) : undefined }); - } else if (v.type === 'day') { Flatpickr(input); } + } else if (v.type === 'day') { + Flatpickr(input, { + defaultDate: val ? new Date(val) : undefined + }); + } } // if this element was active before the remote change, restore cursor @@ -342,6 +352,7 @@ define([ var duplicates = false; $container.find('input').each(function (i, el) { var val = $(el).val().trim(); + if (v.type === "day" || v.type === "time") { val = +new Date(val); } if (values.indexOf(val) === -1) { values.push(val); } else { duplicates = true; } }); @@ -392,12 +403,22 @@ define([ }; var makePollTable = function (answers, opts) { + // Sort date values + if (opts.type !== "text") { + opts.values.sort(function (a, b) { + return +new Date(a) - +new Date(b); + }); + } // Create first line with options var els = opts.values.map(function (data) { if (opts.type === "day") { var _date = new Date(data); data = _date.toLocaleDateString(); } + if (opts.type === "time") { + var _dateT = new Date(data); + data = Flatpickr.formatDate(_dateT, timeFormat); + } return h('div.cp-poll-cell.cp-form-poll-option', data); }); // Insert axis switch button @@ -407,6 +428,28 @@ define([ els.unshift(h('div.cp-poll-cell.cp-poll-switch', switchAxis)); var lines = [h('div', els)]; + // Add an initial row to "time" values containing the days + if (opts.type === "time") { + var days = [h('div.cp-poll-cell')]; + var _days = {}; + opts.values.forEach(function (d) { + var date = new Date(d); + var day = date.toLocaleDateString(); + _days[day] = _days[day] || 0; + _days[day]++; + }); + var dayValues = Object.keys(_days).map(function (d) { return _days[d]; }); + var minDay = Math.min.apply(null, dayValues); + console.log(_days, minDay); + Object.keys(_days).forEach(function (day) { + days.push(h('div.cp-poll-cell.cp-poll-time-day', { + style: 'flex-grow:'+(_days[day]-1)+';' + }, day)); + }); + var width = (opts.values.length + 2)*100; + lines.unshift(h('div', days)); + } + // Add answers if (Array.isArray(answers)) { answers.forEach(function (answer) { @@ -957,7 +1000,6 @@ define([ $tag.find('.cp-form-poll-choice').each(function (i, el) { if (!el._setValue) { return; } var $el = $(el); - console.log(el, $el.data('option'), val); el._setValue(val[$el.data('option')] || 0); }); } From 5d7ab79935142401c4c923e2f9997d7828e121d5 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 May 2021 18:23:19 +0200 Subject: [PATCH 063/424] Improve form polls creation (time and day types) --- www/form/inner.js | 143 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 114 insertions(+), 29 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index a0a52e673..37d3d50af 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -116,9 +116,11 @@ define([ Messages.form_newItem = "New item"; Messages.form_add_option = "Add option"; Messages.form_add_item = "Add item"; + Messages.form_addMultiple = "Add all"; + Messages.form_clear = "Clear"; - var MAX_OPTIONS = 10; // XXX + var MAX_OPTIONS = 15; // XXX var MAX_ITEMS = 10; // XXX var editOptions = function (v, setCursorGetter, cb, tmp) { @@ -184,6 +186,7 @@ define([ // Show existing options var $add, $addItem; + var addMultiple; var getOption = function (val, isItem, uid) { var input = h('input', {value:val}); if (uid) { $(input).data('uid', uid); } @@ -198,9 +201,9 @@ define([ defaultDate: val ? new Date(val) : undefined }); } else if (v.type === 'day') { - Flatpickr(input, { + /*Flatpickr(input, { defaultDate: val ? new Date(val) : undefined - }); + });*/ } } @@ -224,7 +227,10 @@ define([ // We've just deleted an item/option so we should be under the MAX limit and // we can show the "add" button again if (isItem && $addItem) { $addItem.show(); } - if (!isItem && $add) { $add.show(); } + if (!isItem && $add) { + $add.show(); + if (v.type === "time") { $(addMultiple).show(); } + } }); return el; }; @@ -243,32 +249,93 @@ define([ containerItems = h('div.cp-form-edit-block', inputsItems); } + // Calendar... + var calendarView; + if (v.type) { + var calendarInput = h('input'); + calendarView = h('div', calendarInput); + var calendarDefault = v.type === "day" ? v.values.map(function (time) { + if (!time) { return; } + var d = new Date(time); + if (!isNaN(d)) { return d; } + }).filter(Boolean) : undefined; + Flatpickr(calendarInput, { + mode: 'multiple', + inline: true, + defaultDate: calendarDefault, + appendTo: calendarView + }); + } + + // Calendar time // XXX + if (v.type) { + var multipleInput = h('input'); + var multipleClearButton = h('button.btn', Messages.form_clear); + var addMultipleButton = h('button.btn', [ + h('i.fa.fa-plus'), + h('span', Messages.form_addMultiple) + ]); + addMultiple = h('div', { style: "display: none;" }, [ + multipleInput, + addMultipleButton, + multipleClearButton + ]); + var multiplePickr = Flatpickr(multipleInput, { + mode: 'multiple', + enableTime: true, + dateFormat: dateFormat, + }); + $(multipleClearButton).click(function () { + multiplePickr.clear(); + }); + $(addMultipleButton).click(function () { + multiplePickr.selectedDates.some(function (date) { + $add.before(getOption(date, false)); + var l = $container.find('input').length; + $(maxInput).attr('max', l); + if (l >= MAX_OPTIONS) { + $add.hide(); + $(addMultiple).hide(); + return true; + } + }); + multiplePickr.clear(); + }); + } + + var refreshView = function () { + if (!v.type) { return; } + var $calendar = $(calendarView); + if (v.type !== "day") { + $calendar.hide(); + $container.show(); + var l = $container.find('input').length; + if (v.type === "time" && l < MAX_OPTIONS) { + $(addMultiple).show(); + } else { + $(addMultiple).hide(); + } + } else { + $calendar.show(); + $container.hide(); + } + }; + refreshView(); + // Doodle type change: empty current values and change input types? if (typeSelect) { typeSelect.onChange.reg(function (prettyVal, val) { + v.type = val; + refreshView(); + if (val !== "text") { + $container.find('.cp-form-edit-block-input').remove(); + return; + } $container.find('input').each(function (i, input) { - if (!input._flatpickr && val !== 'text') { - input.value = ""; - } - - v.type = val; - if (input._flatpickr) { input._flatpickr.destroy(); delete input._flatpickr; } - if (val === 'time') { - Flatpickr(input, { - enableTime: true, - time_24hr: is24h, - dateFormat: dateFormat, - }); - } - if (val === 'day') { - Flatpickr(input, { - time_24hr: is24h, - }); - } }); }); } @@ -281,6 +348,7 @@ define([ $(maxInput).attr('max', l); if (l >= MAX_OPTIONS) { $add.hide(); } }); + // If multiline block, handle "Add item" button $addItem = $(addItem).click(function () { $addItem.before(getOption(Messages.form_newItem, true, Util.uid())); @@ -306,6 +374,12 @@ define([ } values.push($(el).val()); }); + if (v.type === "day") { + var dayPickr = $(calendarView).find('input')[0]._flatpickr; + values = dayPickr.selectedDates.map(function (date) { + return +date; + }); + } var _content = {values: values}; if (maxInput) { @@ -350,12 +424,22 @@ define([ // Get values var values = []; var duplicates = false; - $container.find('input').each(function (i, el) { - var val = $(el).val().trim(); - if (v.type === "day" || v.type === "time") { val = +new Date(val); } - if (values.indexOf(val) === -1) { values.push(val); } - else { duplicates = true; } - }); + if (v.type === "day") { + var dayPickr = $(calendarView).find('input')[0]._flatpickr; + values = dayPickr.selectedDates.map(function (date) { + return +date; + }); + } else { + $container.find('input').each(function (i, el) { + var val = $(el).val().trim(); + if (v.type === "day" || v.type === "time") { val = +new Date(val); } + if (values.indexOf(val) === -1) { values.push(val); } + else { duplicates = true; } + }); + } + if (!values.length) { + return void UI.warn(Messages.error); // XXX error message: no values + } var res = { values: values }; // If multiline block, get items @@ -397,7 +481,9 @@ define([ return [ type, maxOptions, + calendarView, h('div.cp-form-edit-options-block', [containerItems, container]), + addMultiple, h('div', [cancelBlock, saveBlock]) ]; }; @@ -446,7 +532,6 @@ define([ style: 'flex-grow:'+(_days[day]-1)+';' }, day)); }); - var width = (opts.values.length + 2)*100; lines.unshift(h('div', days)); } From 8745042205fc58c06349caab7c6630f32ec96c4a Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 31 May 2021 15:43:11 +0530 Subject: [PATCH 064/424] create Ukrainian translation file --- www/common/translations/messages.uk.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 www/common/translations/messages.uk.json diff --git a/www/common/translations/messages.uk.json b/www/common/translations/messages.uk.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/www/common/translations/messages.uk.json @@ -0,0 +1 @@ +{} From eea0688879dcc12250be27eec5c1612d4744390a Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 31 May 2021 15:53:55 +0530 Subject: [PATCH 065/424] log messages which fail signature validation --- lib/hk-util.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/hk-util.js b/lib/hk-util.js index 5a96970d0..276ac3e6f 100644 --- a/lib/hk-util.js +++ b/lib/hk-util.js @@ -963,7 +963,11 @@ HK.onChannelMessage = function (Env, Server, channel, msgStruct, cb) { // validation can fail in multiple ways if (err === 'FAILED') { // we log this case, but not others for some reason - Log.info("HK_SIGNED_MESSAGE_REJECTED", 'Channel '+channel.id); + Log.info("HK_SIGNED_MESSAGE_REJECTED", { + channel: channel.id, + validateKey: metadata.validayKey, + message: signedMsg, + }); } // always abort if there was an error... cb('FAILED_VALIDATION'); From 1fe57c7e0336b4289fba81d627123988e6ef9fbc Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 31 May 2021 16:30:47 +0530 Subject: [PATCH 066/424] lint compliance and minor refactor --- www/checkup/main.js | 72 ++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/www/checkup/main.js b/www/checkup/main.js index 054280b84..f6b3ef4c7 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -30,8 +30,12 @@ define([ Assert(f, msg || h('span.advisory-text.cp-danger')); }; + var code = function (content) { + return h('code', content); + }; + var CONFIG_PATH = function () { - return h('code', 'cryptpad/config/config.js'); + return code('cryptpad/config/config.js'); }; var API_CONFIG_LINK = function () { return h('a', { @@ -50,6 +54,18 @@ define([ ]); }; + var link = function (href, text) { + return h('a', { + href: href, + rel: 'noopener noreferrer', + target: '_blank', + }, text); + }; + + var setWarningClass = function (msg) { + $(msg).removeClass('cp-danger').addClass('cp-warning'); + }; + var cacheBuster = function (url) { return url + '?test=' + (+new Date()); }; @@ -60,9 +76,9 @@ define([ assert(function (cb, msg) { msg.appendChild(h('span', [ "CryptPad's sandbox requires that both ", - h('code', 'httpUnsafeOrigin'), + code('httpUnsafeOrigin'), ' and ', - h('code', 'httpSafeOrigin'), + code('httpSafeOrigin'), " be configured in ", CONFIG_PATH(), '. ', @@ -75,9 +91,9 @@ define([ assert(function (cb, msg) { msg.appendChild(h('span', [ - h('code', 'httpUnsafeOrigin'), + code('httpUnsafeOrigin'), ' and ', - h('code', 'httpSafeOrigin'), + code('httpSafeOrigin'), ' are equivalent. ', "In order for CryptPad's security features to be as effective as intended they must be different. ", "See ", @@ -91,9 +107,9 @@ define([ assert(function (cb, msg) { msg.appendChild(h('span', [ - h('code', 'httpUnsafeOrigin'), + code('httpUnsafeOrigin'), ' and ', - h('code', 'httpSafeOrigin'), + code('httpSafeOrigin'), ' must not contain trailing slashes. This can be configured in ', CONFIG_PATH(), '. ', @@ -105,10 +121,10 @@ define([ assert(function (cb, msg) { msg.appendChild(h("span", [ "It appears that you are trying to load this page via an origin other than its main domain (", - h('code', ApiConfig.httpUnsafeOrigin), + code(ApiConfig.httpUnsafeOrigin), "). See the ", - h('code', 'httpUnsafeOrigin'), + code('httpUnsafeOrigin'), " option in ", CONFIG_PATH(), " which is exposed via ", @@ -132,7 +148,7 @@ define([ assert(function (cb, msg) { msg.appendChild(h('span', [ "The main domain (configured via ", - h('code', 'httpUnsafeOrigin'), + code('httpUnsafeOrigin'), ' as ', ApiConfig.httpUnsafeOrigin, ' in ', @@ -149,13 +165,13 @@ define([ assert(function (cb, msg) { msg.appendChild(h('span', [ "Your browser was not able to load an iframe using the origin specified as ", - h('code', "httpSafeOrigin"), + code("httpSafeOrigin"), " (", ApiConfig.httpSafeOrigin, ") in ", CONFIG_PATH(), ". This can be caused by an invalid ", - h('code', 'httpUnsafeDomain'), + code('httpUnsafeDomain'), ', invalid CSP configuration in your reverse proxy, invalid SSL certificates, and many other factors. ', 'More information about your particular error may be found in your browser console. ', RESTART_WARNING(), @@ -230,7 +246,7 @@ define([ msg.appendChild(h('span', [ "Unable to create, retrieve, or remove encrypted credentials from the server. ", "This is most commonly caused by a mismatch between the value of the ", - h('code', 'blockPath'), + code('blockPath'), ' value configured in ', CONFIG_PATH(), " and the corresponding settings in your reverse proxy's configuration file,", @@ -359,11 +375,11 @@ define([ if (response !== expect[k]) { msg.appendChild(h('span', [ 'A value of ', - h('code', expect[k]), + code(expect[k]), ' was expected for the ', - h('code', k), + code(k), ' HTTP header, but instead a value of "', - h('code', response), + code(response), '" was received.', ])); return true; // returning true indicates that a value is incorrect @@ -373,14 +389,6 @@ define([ }); }); - var link = function (href, text) { - return h('a', { - href: href, - rel: 'noopener noreferrer', - target: '_blank', - }, text); - }; - assert(function (cb, msg) { setWarningClass(msg); msg.appendChild(h('span', [ @@ -404,7 +412,7 @@ define([ assert(function (cb, msg) { msg.appendChild(h('span', [ - h('code', '/api/broadcast'), + code('/api/broadcast'), " could not be loaded. This can be caused by an outdated application server or an incorrectly configured reverse proxy. ", "Even if the most recent code has been downloaded it's possible the application server has not been restarted. ", "Your browser console may provide more details as to why this resource could not be loaded. ", @@ -418,10 +426,6 @@ define([ }); }); - var code = function (content) { - return h('code', content); - }; - var checkAPIHeaders = function (url, msg, cb) { $.ajax(cacheBuster(url), { dataType: 'text', @@ -486,10 +490,6 @@ define([ checkAPIHeaders(url, msg, cb); }); - var setWarningClass = function (msg) { - $(msg).removeClass('cp-danger').addClass('cp-warning'); - }; - assert(function (cb, msg) { var email = ApiConfig.adminEmail; if (typeof(email) === 'string' && email && email !== 'i.did.not.read.my.config@cryptpad.fr') { @@ -499,7 +499,7 @@ define([ setWarningClass(msg); msg.appendChild(h('span', [ 'This instance does not provide a valid ', - h('code', 'adminEmail'), + code('adminEmail'), ' which can make it difficult to contact its adminstrator to report vulnerabilities or abusive content.', " This can be configured on your instance's admin panel. Use the provided ", code("Flush cache'"), @@ -514,7 +514,7 @@ define([ msg.appendChild(h('span', [ "This instance's encrypted support ticket functionality has not been enabled. This can make it difficult for its users to safely report issues that concern sensitive information. ", "This can be configured via the admin panel's ", - h('code', 'Support'), + code('Support'), " tab.", ])); cb(support && typeof(support) === 'string' && support.length === 44); @@ -528,7 +528,7 @@ define([ setWarningClass(msg); msg.appendChild(h('span', [ "This instance has not been configured to support web administration. This can be enabled by adding a registered user's public signing key to the ", - h('code', 'adminKeys'), + code('adminKeys'), ' array in ', CONFIG_PATH(), '. ', From 833bcc93cc15939f44468deb7eb48ef575dc441a Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 31 May 2021 15:32:04 +0200 Subject: [PATCH 067/424] Sort options and items in form editor --- www/form/app-form.less | 6 ++++++ www/form/inner.js | 20 +++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index efe97ed86..3636144ee 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -125,6 +125,12 @@ flex-wrap: wrap; } .cp-form-edit-block { + .cp-form-handle { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + } .cp-form-edit-block-input { display: flex; width: 400px; diff --git a/www/form/inner.js b/www/form/inner.js index 37d3d50af..b2406fc93 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -221,7 +221,14 @@ define([ } var del = h('button.btn.btn-danger', h('i.fa.fa-times')); - var el = h('div.cp-form-edit-block-input', [ input, del ]); + var el = h('div.cp-form-edit-block-input', [ + h('span.cp-form-handle', [ + h('i.fa.fa-ellipsis-v'), + h('i.fa.fa-ellipsis-v'), + ]), + input, + del + ]); $(del).click(function () { $(el).remove(); // We've just deleted an item/option so we should be under the MAX limit and @@ -240,6 +247,12 @@ define([ var container = h('div.cp-form-edit-block', inputs); var $container = $(container); + Sortable.create(container, { + direction: "vertical", + handle: ".cp-form-handle", + draggable: ".cp-form-edit-block-input", + }); + var containerItems; if (v.items) { var inputsItems = v.items.map(function (itemData) { @@ -247,6 +260,11 @@ define([ }); inputsItems.push(addItem); containerItems = h('div.cp-form-edit-block', inputsItems); + Sortable.create(containerItems, { + direction: "vertical", + handle: ".cp-form-handle", + draggable: ".cp-form-edit-block-input", + }); } // Calendar... From 3fbb771b9c2e3f6b6f37d2d909689fc5108a38c3 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 31 May 2021 20:10:47 +0530 Subject: [PATCH 068/424] add a setting to preserve redirect-to-drive behaviour from the home page disable it by default --- customize.dist/main.js | 2 +- www/common/common-constants.js | 1 + www/common/cryptpad-common.js | 10 +++++++ www/common/outer/async-store.js | 4 +++ www/common/outer/local-store.js | 10 +++++++ www/common/sframe-common-outer.js | 1 + www/settings/inner.js | 50 +++++++++++++++++++++++++++++-- www/settings/main.js | 3 ++ 8 files changed, 78 insertions(+), 3 deletions(-) diff --git a/customize.dist/main.js b/customize.dist/main.js index 44ef51d0d..5969c6c71 100644 --- a/customize.dist/main.js +++ b/customize.dist/main.js @@ -12,7 +12,7 @@ define([ // Make sure we don't display non-translated content (empty button) $main.find('#data').removeClass('hidden'); - if (LocalStore.isLoggedIn()) { + if (LocalStore.isLoggedIn() && LocalStore.getDriveRedirectPreference()) { if (window.location.pathname === '/') { window.location = '/drive/'; return; diff --git a/www/common/common-constants.js b/www/common/common-constants.js index 0a1c1d7f6..fd4d1474b 100644 --- a/www/common/common-constants.js +++ b/www/common/common-constants.js @@ -10,6 +10,7 @@ define(['/customize/application_config.js'], function (AppConfig) { oldStorageKey: 'CryptPad_RECENTPADS', storageKey: 'filesData', tokenKey: 'loginToken', + prefersDriveRedirectKey: 'prefersDriveRedirect', displayPadCreationScreen: 'displayPadCreationScreen', deprecatedKey: 'deprecated', MAX_TEAMS_SLOTS: AppConfig.maxTeamsSlots || 5, diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index e9635d5b5..fd2d29cd2 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -1103,6 +1103,11 @@ define([ postMessage('BURN_PAD', data); }; + common.setDriveRedirectPreference = function (data, cb) { + LocalStore.setDriveRedirectPreference(data && data.value); + cb(); + }; + common.changePadPassword = function (Crypt, Crypto, data, cb) { var href = data.href; var oldPassword = data.oldPassword; @@ -2506,6 +2511,11 @@ define([ } if (data.anonHash && !cfg.userHash) { LocalStore.setFSHash(data.anonHash); } + var prefersDriveRedirect = data[Constants.prefersDriveRedirectKey]; + if (typeof(prefersDriveRedirect) === 'boolean') { + LocalStore.setDriveRedirectPreference(prefersDriveRedirect); + } + initialized = true; channelIsReady(); }); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 21bc77798..cec0d7d73 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -3167,6 +3167,10 @@ define([ initialized = false; } + var redirect = Constants.prefersDriveRedirectKey; + var redirectPreference = Util.find(store, [ 'proxy', 'settings', 'general', redirect, ]); + ret[redirect] = redirectPreference; + callback(ret); }); diff --git a/www/common/outer/local-store.js b/www/common/outer/local-store.js index 9b62fbbde..fb49d084d 100644 --- a/www/common/outer/local-store.js +++ b/www/common/outer/local-store.js @@ -76,6 +76,16 @@ define([ return window.CP_logged_in || typeof getUserHash() === "string"; }; + LocalStore.getDriveRedirectPreference = function () { + try { + return JSON.parse(localStorage[Constants.redirectToDriveKey]); + } catch (err) { return; } + }; + + LocalStore.setDriveRedirectPreference = function (bool) { + localStorage.setItem(Constants.redirectToDriveKey, Boolean(bool)); + }; + LocalStore.login = function (hash, name, cb) { if (!hash) { throw new Error('expected a user hash'); } if (!name) { throw new Error('expected a user name'); } diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 198d17099..cc1ddf2f5 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -615,6 +615,7 @@ define([ newTemplate: Array.isArray(Cryptpad.initialPath) && Cryptpad.initialPath[0] === "template", feedbackAllowed: Utils.Feedback.state, + prefersDriveRedirect: Utils.LocalStore.getDriveRedirectPreference(), isPresent: parsed.hashData && parsed.hashData.present, isEmbed: parsed.hashData && parsed.hashData.embed, oldVersionHash: parsed.hashData && parsed.hashData.version < 2, // password diff --git a/www/settings/inner.js b/www/settings/inner.js index 24e03a4cf..f979379d7 100644 --- a/www/settings/inner.js +++ b/www/settings/inner.js @@ -14,6 +14,7 @@ define([ '/api/config', '/common/make-backup.js', '/common/common-feedback.js', + '/common/common-constants.js', '/common/jscolor.js', '/bower_components/file-saver/FileSaver.min.js', @@ -35,7 +36,8 @@ define([ AppConfig, ApiConfig, Backup, - Feedback + Feedback, + Constants ) { var saveAs = window.saveAs; var APP = window.APP = {}; @@ -72,7 +74,8 @@ define([ 'cp-settings-thumbnails', 'cp-settings-drive-backup', 'cp-settings-drive-import-local', - 'cp-settings-trim-history' + 'cp-settings-trim-history', + 'cp-settings-redirect', //'cp-settings-drive-reset' ], 'cursor': [ // Msg.settings_cat_cursor @@ -841,6 +844,49 @@ define([ return $div; }; + Messages.settings_driveRedirectTitle = "DRIVE REDIRECT TITLE"; // XXX redirect + Messages.settings_driveRedirectHint = "DRIVE REDIRECT HINT"; // XXX redirect + Messages.settings_driveRedirect = "DRIVE REDIRECT"; // XXX redirect + + create['redirect'] = function () { + if (!common.isLoggedIn()) { return; } + var $div = $('
    ', { 'class': 'cp-settings-redirect cp-sidebarlayout-element' }); + + $('', { 'class': 'label' }).text(Messages.settings_driveRedirectTitle).appendTo($div); + + $('', { 'class': 'cp-sidebarlayout-description' }) + .append(Messages.settings_driveRedirectHint) + .appendTo($div); + + var $ok = $('', { 'class': 'fa fa-check', title: Messages.saved }); + var $spinner = $('', { 'class': 'fa fa-spinner fa-pulse' }); + + var $cbox = $(UI.createCheckbox('cp-settings-redirect', + Messages.settings_driveRedirect, + false, { label: { class: 'noTitle' } })); + var $checkbox = $cbox.find('input').on('change', function() { + $spinner.show(); + $ok.hide(); + var val = $checkbox.is(':checked') || false; + common.setAttribute(['general', Constants.prefersDriveRedirectKey, val, function() { + $spinner.hide(); + $ok.show(); + sframeChan.query("Q_SET_DRIVE_REDIRECT_PREFERENCE", { + value: val, + }, console.log); + }); + }); + + $cbox.appendTo($div); + + $ok.hide().appendTo($cbox); + $spinner.hide().appendTo($cbox); + + if (privateData.prefersDriveRedirect === true) { + $checkbox[0].checked = true; + } + return $div; + }; create['resettips'] = function() { var $div = $('
    ', { 'class': 'cp-settings-resettips cp-sidebarlayout-element' }); diff --git a/www/settings/main.js b/www/settings/main.js index 3cfd22fcc..8101203b1 100644 --- a/www/settings/main.js +++ b/www/settings/main.js @@ -78,6 +78,9 @@ define([ } cb(); }); + sframeChan.on('Q_SET_DRIVE_REDIRECT_PREFERENCE', function (data, cb) { + Cryptpad.setDriveRedirectPreference(data, cb); + }); }; var category; if (window.location.hash) { From fc8ce9cb0e0e668a349afa2aa635421e735929bc Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 31 May 2021 17:19:14 +0200 Subject: [PATCH 069/424] Allow anonymous answers --- www/common/cryptpad-common.js | 32 +++++--- www/common/outer/async-store.js | 7 +- www/form/inner.js | 134 ++++++++++++++++++++++++-------- www/form/main.js | 45 ++++++++++- 4 files changed, 174 insertions(+), 44 deletions(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 69ed47587..dbcbcf8eb 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -76,7 +76,7 @@ define([ postMessage("GET", { key: ['edPrivate'], }, waitFor(function (obj) { - if (obj.error) { return; } + if (!obj || obj.error) { return; } try { keys.push({ edPrivate: obj, @@ -89,7 +89,7 @@ define([ postMessage("GET", { key: ['teams'], }, waitFor(function (obj) { - if (obj.error) { return; } + if (!obj || obj.error) { return; } Object.keys(obj || {}).forEach(function (id) { var t = obj[id]; var _keys = t.keys.drive || {}; @@ -104,13 +104,26 @@ define([ }; common.getFormKeys = function (cb) { - postMessage("GET", { - key: ['curvePrivate'], - }, function (obj) { - if (obj.error) { return void cb(); } + var curvePrivate; + var formSeed; + Nthen(function (waitFor) { + postMessage("GET", { + key: ['curvePrivate'], + }, waitFor(function (obj) { + if (!obj || obj.error) { return; } + curvePrivate = obj; + })); + postMessage("GET", { + key: ['form_seed'], + }, waitFor(function (obj) { + if (!obj || obj.error) { return; } + formSeed = obj; + })); + }).nThen(function () { cb({ - curvePrivate: obj, - curvePublic: Hash.getCurvePublicFromPrivate(obj) + curvePrivate: curvePrivate, + curvePublic: curvePrivate && Hash.getCurvePublicFromPrivate(curvePrivate), + formSeed: formSeed }); }); }; @@ -124,7 +137,8 @@ define([ key: ['forms', data.channel], value: { hash: data.hash, - curvePrivate: data.curvePrivate + curvePrivate: data.curvePrivate, + anonymous: data.anonymous } }, function (obj) { if (obj && obj.error) { console.error(obj.error); } diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 2d08b52d4..1af15a9ed 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -629,6 +629,7 @@ define([ if (!proxy.uid) { store.noDriveUid = store.noDriveUid || Hash.createChannelId(); } + var metadata = { // "user" is shared with everybody via the userlist user: { @@ -655,7 +656,7 @@ define([ accountName: proxy.login_name || '', offline: store.proxy && store.offline, teams: teams, - plan: account.plan + plan: account.plan, } }; cb(JSON.parse(JSON.stringify(metadata))); @@ -2710,6 +2711,10 @@ define([ if (!proxy.settings) { proxy.settings = NEW_USER_SETTINGS; } if (!proxy.forms) { proxy.forms = {}; } if (!proxy.friends_pending) { proxy.friends_pending = {}; } + // Form seed is used to generate a box encryption keypair when + // answering a form anonymously + if (!proxy.form_seed) { proxy.form_seed = Hash.createChannelId(); } + // Call onCacheReady if the manager is not yet defined if (!manager) { diff --git a/www/form/inner.js b/www/form/inner.js index b2406fc93..33ddff44e 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -110,6 +110,12 @@ define([ Messages.form_isClosed = "This form was closed on {0}"; Messages.form_willClose = "This form will close on {0}"; + Messages.form_anonymous_on = "Anonymous answers are allowed"; + Messages.form_anonymous_off = "Anonymous answers are blocked"; + Messages.form_anonymous_button_on = "Block anonymous answers"; + Messages.form_anonymous_button_off = "Allow anonymous answers"; + Messages.form_anonymous_blocked = "Anonymous responses are blocked for this form. You must log in or register to submit answers."; + Messages.form_defaultOption = "Option {0}"; Messages.form_defaultItem = "Item {0}"; Messages.form_newOption = "New option"; @@ -119,6 +125,7 @@ define([ Messages.form_addMultiple = "Add all"; Messages.form_clear = "Clear"; + Messages.form_anonymousBox = "Answer anonymously"; var MAX_OPTIONS = 15; // XXX var MAX_ITEMS = 10; // XXX @@ -544,7 +551,6 @@ define([ }); var dayValues = Object.keys(_days).map(function (d) { return _days[d]; }); var minDay = Math.min.apply(null, dayValues); - console.log(_days, minDay); Object.keys(_days).forEach(function (day) { days.push(h('div.cp-poll-cell.cp-poll-time-day', { style: 'flex-grow:'+(_days[day]-1)+';' @@ -1061,13 +1067,10 @@ define([ return cell; }); // Name input - var nameInput = h('input', { value: username || '' }); + var nameInput = h('input', { value: username || Messages.anonymous }); addLine.unshift(h('div.cp-poll-cell', nameInput)); - // XXX Submit button here? lines.push(h('div', addLine)); - - var tag = h('div.cp-form-type-poll', lines); var $tag = $(tag); @@ -1150,6 +1153,20 @@ define([ return results; }; var makeFormControls = function (framework, content, update) { + var loggedIn = framework._.sfCommon.isLoggedIn(); + + if (!loggedIn && !content.answers.anonymous) { return; } + + var cbox; + if (loggedIn) { + cbox = UI.createCheckbox('cp-form-anonymous', + Messages.form_anonymousBox, false, { mark: { tabindex:1 } }); + if (!content.answers.anonymous) { + $(cbox).find('input').attr('disabled', 'disabled'); + } + + } + var send = h('button.cp-open.btn.btn-primary', update ? Messages.form_update : Messages.form_submit); var reset = h('button.cp-open.btn.btn-danger-alt', Messages.form_reset); $(reset).click(function () { @@ -1162,10 +1179,12 @@ define([ $send.attr('disabled', 'disabled'); var results = getFormResults(); if (!results) { return; } + var sframeChan = framework._.sfCommon.getSframeChannel(); sframeChan.query('Q_FORM_SUBMIT', { mailbox: content.answers, - results: results + results: results, + anonymous: !loggedIn || Util.isChecked($(cbox).find('input')) }, function (err, data) { $send.attr('disabled', 'disabled'); if (err || (data && data.error)) { @@ -1186,7 +1205,8 @@ define([ var sframeChan = framework._.sfCommon.getSframeChannel(); var $v = $(viewResults).click(function () { $v.attr('disabled', 'disabled'); - sframeChan.query("Q_FORM_FETCH_ANSWERS", content.answers, function (err, answers) { + sframeChan.query("Q_FORM_FETCH_ANSWERS", content.answers, function (err, obj) { + var answers = obj && obj.results; if (answers) { APP.answers = answers; } $v.removeAttr('disabled'); $('body').addClass('cp-app-form-results'); @@ -1200,7 +1220,10 @@ define([ reset = undefined; } - return h('div.cp-form-send-container', [send, reset, viewResults]); + return h('div.cp-form-send-container', [ + cbox ? h('div', cbox) : undefined, + send, reset, viewResults + ]); }; var updateForm = function (framework, content, editable, answers, temp) { var $container = $('div.cp-form-creator-content'); @@ -1401,6 +1424,7 @@ define([ var andThen = function (framework) { framework.start(); + var evOnChange = Util.mkEvent(); var content = {}; var sframeChan = framework._.sfCommon.getSframeChannel(); @@ -1416,26 +1440,54 @@ define([ $toolbarContainer.after(helpMenu.menu); + // XXX refresh form settings on remote change var makeFormSettings = function () { - var makePublic = h('button.btn.btn-primary', Messages.form_makePublic); - if (content.answers.privateKey) { makePublic = undefined; } - var publicText = content.answers.privateKey ? Messages.form_isPublic : Messages.form_isPrivate; - var resultsType = h('div.cp-form-results-type-container', [ - h('span.cp-form-results-type', publicText), - makePublic - ]); - var $makePublic = $(makePublic).click(function () { - UI.confirm(Messages.form_makePublicWarning, function (yes) { - if (!yes) { return; } - content.answers.privateKey = priv.form_private; + // Private / public status + var resultsType = h('div.cp-form-results-type-container'); + var $results = $(resultsType); + var refreshPublic = function () { + $results.empty(); + var makePublic = h('button.btn.btn-primary', Messages.form_makePublic); + if (content.answers.privateKey) { makePublic = undefined; } + var publicText = content.answers.privateKey ? Messages.form_isPublic : Messages.form_isPrivate; + $results.append(h('span.cp-form-results-type', publicText)); + $results.append(makePublic); + var $makePublic = $(makePublic).click(function () { + UI.confirm(Messages.form_makePublicWarning, function (yes) { + if (!yes) { return; } + $makePublic.attr('disabled', 'disabled'); + content.answers.privateKey = priv.form_private; + framework.localChange(); + framework._.cpNfInner.chainpad.onSettle(function () { + UI.log(Messages.saved); + refreshPublic(); + }); + }); + }); + }; + refreshPublic(); + + // Allow anonymous answers + var privacyContainer = h('div.cp-form-privacy-container'); + var $privacy = $(privacyContainer); + var refreshPrivacy = function () { + $privacy.empty(); + var anonymous = content.answers.anonymous; + var key = anonymous ? 'on' : 'off'; + var button = h('button.btn.btn-secondary', Messages['form_anonymous_button_'+key]); + var $b = $(button).click(function () { + $b.attr('disabled', 'disabled'); + content.answers.anonymous = !anonymous; framework.localChange(); framework._.cpNfInner.chainpad.onSettle(function () { UI.log(Messages.saved); - $makePublic.remove(); - $(resultsType).find('.cp-form-results-type').text(Messages.form_isPublic); + refreshPrivacy(); }); }); - }); + $privacy.append(h('div.cp-form-status', Messages['form_anonymous_'+key])); + $privacy.append(h('div.cp-form-actions', button)); + }; + refreshPrivacy(); // End date / Closed state var endDateContainer = h('div.cp-form-status-container'); @@ -1511,7 +1563,8 @@ define([ channel: content.answers.channel, validateKey: content.answers.validateKey, publicKey: content.answers.publicKey - }, function (err, answers) { + }, function (err, obj) { + var answers = obj && obj.results; if (answers) { APP.answers = answers; } $v.removeAttr('disabled'); $body.addClass('cp-app-form-results'); @@ -1519,8 +1572,14 @@ define([ }); }); + + evOnChange.reg(refreshPublic); + evOnChange.reg(refreshPrivacy); + evOnChange.reg(refreshEndDate); + return [ endDateContainer, + privacyContainer, resultsType, viewResults, ]; @@ -1665,9 +1724,10 @@ define([ publicKey: content.answers.publicKey, privateKey: priv.form_auditorKey }, function (err, obj) { - if (obj) { APP.answers = obj; } + var answers = obj && obj.results; + if (answers) { APP.answers = answers; } $body.addClass('cp-app-form-results'); - renderResults(content, obj); + renderResults(content, answers); }); return; } @@ -1678,16 +1738,21 @@ define([ validateKey: content.answers.validateKey, publicKey: content.answers.publicKey }, function (err, obj) { - if (obj) { APP.answers = obj; } + var answers = obj && obj.results; + if (answers) { APP.answers = answers; } checkIntegrity(false); updateForm(framework, content, true); - }); return; } refreshEndDateBanner(); + var loggedIn = framework._.sfCommon.isLoggedIn(); + if (!loggedIn && !content.answers.anonymous) { + UI.alert(Messages.form_anonymous_blocked); + } + // If the results are public and there is at least one doodle, fetch the results now if (content.answers.privateKey && Object.keys(content.form).some(function (uid) { return content.form[uid].type === "poll"; @@ -1698,12 +1763,19 @@ define([ publicKey: content.answers.publicKey, privateKey: content.answers.privateKey, }, function (err, obj) { - if (obj) { APP.answers = obj; } + var answers = obj && obj.results; + if (answers) { APP.answers = answers; } checkIntegrity(false); var myAnswers; - if (user.curvePublic && obj && obj[user.curvePublic]) { // XXX ANONYMOUS - myAnswers = obj[user.curvePublic].msg; + var curve1 = user.curvePublic + var curve2 = obj && obj.myKey; // Anonymous answer key + if (answers) { + var myAnswersObj = answers[curve1] || answers[curve2] || undefined; + if (myAnswersObj) { + myAnswers = myAnswersObj.msg; + } } + console.warn(obj); updateForm(framework, content, false, myAnswers); }); return; @@ -1726,8 +1798,8 @@ define([ }); framework.onContentUpdate(function (newContent) { - console.log(newContent); content = newContent; + evOnChange.fire(); refreshEndDateBanner(); var answers, temp; if (!APP.isEditor) { answers = getFormResults(); } diff --git a/www/form/main.js b/www/form/main.js index 27e6552de..f51aae4cf 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -63,6 +63,7 @@ define([ }); sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, cb) { var myKeys = {}; + var myFormKeys; var CPNetflux; var network; nThen(function (w) { @@ -81,12 +82,19 @@ define([ } }); })); + Cryptpad.getFormKeys(w(function (keys) { + myFormKeys = keys; + })); Cryptpad.makeNetwork(w(function (err, nw) { network = nw; })); }).nThen(function () { if (!network) { return void cb({error: "E_CONNECT"}); } + if (myFormKeys.formSeed) { + myFormKeys = getAnonymousKeys(myFormKeys.formSeed, data.channel); + } + var keys = Utils.secret && Utils.secret.keys; var crypto = Utils.Crypto.Mailbox.createEncryptor({ @@ -105,7 +113,15 @@ define([ }; var results = {}; config.onReady = function () { - cb(results); + var myKey; + // If we have submitted an anonymous answer, retrieve it + if (myFormKeys.curvePublic && results[myFormKeys.curvePublic]) { + myKey = myFormKeys.curvePublic; + } + cb({ + myKey: myKey, + results: results + }); network.disconnect(); }; config.onMessage = function (msg, peer, vKey, isCp, hash, senderCurve, cfg) { @@ -135,6 +151,10 @@ define([ answer = obj; })); }).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, @@ -154,6 +174,16 @@ define([ }); }); + var getAnonymousKeys = function (formSeed, channel) { + var array = Nacl.util.decodeBase64(formSeed + channel); + var hash = Nacl.hash(array); + var secretKey = Nacl.util.encodeBase64(hash.subarray(32)); + var publicKey = Utils.Hash.getCurvePublicFromPrivate(secretKey); + return { + curvePrivate: secretKey, + curvePublic: publicKey, + }; + }; sframeChan.on("Q_FORM_SUBMIT", function (data, cb) { var box = data.mailbox; var myKeys; @@ -162,7 +192,15 @@ define([ myKeys = keys; })); }).nThen(function () { - + // XXX if we are a registered user (myKeys.curvePrivate exists), we may + // have already answered anonymously. We should send a "proof" to show + // that the existing anonymous answer are ours (using myKeys.formSeed). + // Even if we never answered anonymously, the keyPair would be unique to + // the current channel so it wouldn't leak anything. + if (data.anonymous) { + if (!myKeys.formSeed) { return void cb({ error: "ANONYMOUS_ERROR" }); } + myKeys = getAnonymousKeys(myKeys.formSeed, box.channel); + } var keys = Utils.secret && Utils.secret.keys; myKeys.signingKey = keys.secondarySignKey; @@ -182,7 +220,8 @@ define([ Cryptpad.storeFormAnswer({ channel: box.channel, hash: hash, - curvePrivate: ephemeral_private + curvePrivate: ephemeral_private, + anonymous: Boolean(data.anonymous) }); cb({error: err, response: response, hash: hash}); }); From 6021b152136c514308e0f31009ce5b22dae27f6a Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 31 May 2021 18:23:25 +0200 Subject: [PATCH 070/424] Add description block to forms --- www/form/app-form.less | 13 +++++ www/form/inner.js | 112 ++++++++++++++++++++++++++++++++++++----- www/form/main.js | 20 ++++---- 3 files changed, 123 insertions(+), 22 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 3636144ee..9edc90522 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -123,6 +123,19 @@ .cp-form-edit-options-block { display: flex; flex-wrap: wrap; + .CodeMirror { + cursor: default; + flex: 1; + margin: auto; + min-width: 80%; + width: 80%; + min-height: 200px; + height: 200px; + border: 1px solid @cp_forms-border; + .CodeMirror-placeholder { + color: #777; + } + } } .cp-form-edit-block { .cp-form-handle { diff --git a/www/form/inner.js b/www/form/inner.js index 33ddff44e..80bd3d2d3 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -15,6 +15,9 @@ define([ '/common/hyperscript.js', '/customize/messages.js', '/customize/application_config.js', + '/common/diffMarked.js', + '/common/sframe-common-codemirror.js', + 'cm/lib/codemirror', '/common/inner/share.js', '/common/inner/access.js', @@ -23,6 +26,13 @@ define([ '/lib/datepicker/flatpickr.js', '/bower_components/sortablejs/Sortable.min.js', + 'cm/addon/display/placeholder', + 'cm/mode/markdown/markdown', + 'css!cm/lib/codemirror.css', + + 'css!/bower_components/codemirror/lib/codemirror.css', + 'css!/bower_components/codemirror/addon/dialog/dialog.css', + 'css!/bower_components/codemirror/addon/fold/foldgutter.css', 'css!/lib/datepicker/flatpickr.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/form/app-form.less', @@ -43,6 +53,9 @@ define([ h, Messages, AppConfig, + DiffMd, + SFCodeMirror, + CMeditor, Share, Access, Properties, Flatpickr, Sortable @@ -82,6 +95,8 @@ define([ Messages.form_type_multicheck = "Multiline Checkbox"; // XXX Messages.form_type_poll = "Poll"; // XXX + Messages.form_type_md = "Description"; // XXX + Messages.form_duplicates = "Duplicate entries have been removed"; Messages.form_maxOptions = "{0} answer(s) max"; @@ -550,7 +565,6 @@ define([ _days[day]++; }); var dayValues = Object.keys(_days).map(function (d) { return _days[d]; }); - var minDay = Math.min.apply(null, dayValues); Object.keys(_days).forEach(function (day) { days.push(h('div.cp-poll-cell.cp-poll-time-day', { style: 'flex-grow:'+(_days[day]-1)+';' @@ -612,6 +626,75 @@ define([ }).filter(Boolean); }; + var STATIC_TYPES = { + md: { + defaultOpts: { + text: "Your text here" // XXX + }, + get: function (opts) { + if (!opts) { opts = STATIC_TYPES.md.defaultOpts; } + var tag = h('div', { + id: 'form'+Util.uid() + }, opts.text); + var $tag = $(tag); + DiffMd.apply(DiffMd.render(opts.text || ''), $tag, APP.common); + return { + tag: tag, + edit: function (cb, tmp) { + // XXX use tmp and cursor getter + var t = h('textarea'); + var block = h('div.cp-form-edit-options-block', [t]); + var cm = SFCodeMirror.create("gfm", CMeditor, t); + var editor = cm.editor; + editor.setOption('lineNumbers', true); + editor.setOption('lineWrapping', true); + editor.setOption('styleActiveLine', true); + editor.setOption('readOnly', false); + console.warn(APP.common); + setTimeout(function () { + editor.setValue(opts.text); + editor.refresh(); + editor.save(); + editor.focus(); + }); + if (APP.common) { + var markdownTb = APP.common.createMarkdownToolbar(editor); + $(block).prepend(markdownTb.toolbar); + $(markdownTb.toolbar).show(); + cm.configureTheme(APP.common, function () {}); + } + // Cancel changes + var cancelBlock = h('button.btn.btn-secondary', Messages.cancel); + $(cancelBlock).click(function () { cb(); }); + // Save changes + var saveBlock = h('button.btn.btn-primary', [ + h('i.fa.fa-floppy-o'), + h('span', Messages.settings_save) + ]); + $(saveBlock).click(function () { + $(saveBlock).attr('disabled', 'disabled'); + cb({ + text: editor.getValue() + }); + }); + return [ + block, + h('div', [cancelBlock, saveBlock]) + ]; + }, + getCursor: function () { return cursorGetter(); }, + //getValue: function () { return $tag.val(); }, + //setValue: function (val) { $tag.val(val); }, + //reset: function () { $tag.val(''); } + }; + }, + printResults: function () { + var results = []; + return h('div.cp-form-results-type-text', results); + }, + icon: h('i.fa.fa-info') + }, + }; var TYPES = { input: { get: function () { @@ -1117,7 +1200,7 @@ define([ var lines = makePollTable(_answers, form[uid].opts); return h('div.cp-form-type-poll', lines); }, - icon: h('i.fa.fa-check-square-o') + icon: h('i.cptools.cptools-poll') }, }; @@ -1148,6 +1231,7 @@ define([ if (!Array.isArray(APP.formBlocks)) { return; } var results = {}; APP.formBlocks.forEach(function (data) { + if (!data.getValue) { return; } results[data.uid] = data.getValue(); }); return results; @@ -1237,7 +1321,8 @@ define([ var elements = content.order.map(function (uid) { var block = form[uid]; var type = block.type; - var model = TYPES[type]; + var model = TYPES[type] || STATIC_TYPES[type]; + var isStatic = Boolean(STATIC_TYPES[type]); if (!model) { return; } var _answers, name; @@ -1253,7 +1338,7 @@ define([ var data = model.get(block.opts, _answers, name); if (!data) { return; } data.uid = uid; - if (answers && answers[uid]) { data.setValue(answers[uid]); } + if (answers && answers[uid] && data.setValue) { data.setValue(answers[uid]); } var q = h('div.cp-form-block-question', block.q || Messages.form_default); var editButtons, editContainer; @@ -1380,7 +1465,7 @@ define([ return h('div.cp-form-block'+editableCls, { 'data-id':uid }, [ - q, + isStatic ? undefined : q, h('div.cp-form-block-content', [ data.tag, editButtons @@ -1394,7 +1479,7 @@ define([ if (editable) { Sortable.create($container[0], { direction: "vertical", - filter: "input, button", + filter: "input, button, .CodeMirror", preventOnFilter: false, store: { set: function (s) { @@ -1427,6 +1512,7 @@ define([ var evOnChange = Util.mkEvent(); var content = {}; + APP.common = framework._.sfCommon; var sframeChan = framework._.sfCommon.getSframeChannel(); var metadataMgr = framework._.cpNfInner.metadataMgr; var user = metadataMgr.getUserData(); @@ -1615,10 +1701,9 @@ define([ var controlContainer; if (APP.isEditor) { - var controls = Object.keys(TYPES).map(function (type) { - + var addControl = function (type) { var btn = h('button.btn', [ - TYPES[type].icon.cloneNode(), + (TYPES[type] || STATIC_TYPES[type]).icon.cloneNode(), h('span', Messages['form_type_'+type]) ]); $(btn).click(function () { @@ -1633,13 +1718,16 @@ define([ updateForm(framework, content, true); }); return btn; - }); + }; + var controls = Object.keys(TYPES).map(addControl); + var staticControls = Object.keys(STATIC_TYPES).map(addControl); var settings = makeFormSettings(); controlContainer = h('div.cp-form-creator-control', [ h('div.cp-form-creator-settings', settings), - h('div.cp-form-creator-types', controls) + h('div.cp-form-creator-types', controls), + h('div.cp-form-creator-types', staticControls) ]); } @@ -1767,7 +1855,7 @@ define([ if (answers) { APP.answers = answers; } checkIntegrity(false); var myAnswers; - var curve1 = user.curvePublic + var curve1 = user.curvePublic; var curve2 = obj && obj.myKey; // Anonymous answer key if (answers) { var myAnswersObj = answers[curve1] || answers[curve2] || undefined; diff --git a/www/form/main.js b/www/form/main.js index f51aae4cf..cd96a3367 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -61,6 +61,16 @@ define([ }); }); + var getAnonymousKeys = function (formSeed, channel) { + var array = Nacl.util.decodeBase64(formSeed + channel); + var hash = Nacl.hash(array); + var secretKey = Nacl.util.encodeBase64(hash.subarray(32)); + var publicKey = Utils.Hash.getCurvePublicFromPrivate(secretKey); + return { + curvePrivate: secretKey, + curvePublic: publicKey, + }; + }; sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, cb) { var myKeys = {}; var myFormKeys; @@ -174,16 +184,6 @@ define([ }); }); - var getAnonymousKeys = function (formSeed, channel) { - var array = Nacl.util.decodeBase64(formSeed + channel); - var hash = Nacl.hash(array); - var secretKey = Nacl.util.encodeBase64(hash.subarray(32)); - var publicKey = Utils.Hash.getCurvePublicFromPrivate(secretKey); - return { - curvePrivate: secretKey, - curvePublic: publicKey, - }; - }; sframeChan.on("Q_FORM_SUBMIT", function (data, cb) { var box = data.mailbox; var myKeys; From 657011a7c76a51fa8c4bb1d02324541e91f5ef85 Mon Sep 17 00:00:00 2001 From: Weblate Date: Tue, 1 Jun 2021 11:45:58 +0200 Subject: [PATCH 071/424] Translated using Weblate (English) Currently translated at 100.0% (1238 of 1238 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ --- www/common/translations/messages.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/common/translations/messages.json b/www/common/translations/messages.json index cdc01cddf..24b393bf6 100644 --- a/www/common/translations/messages.json +++ b/www/common/translations/messages.json @@ -13,7 +13,8 @@ "todo": "Todo", "contacts": "Contacts", "sheet": "Sheet", - "teams": "Teams" + "teams": "Teams", + "form": "Form" }, "button_newpad": "New Rich Text pad", "button_newcode": "New Code pad", From ce6879fd68727b878d6cfd88a2d453cafb17f93b Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 1 Jun 2021 13:54:04 +0200 Subject: [PATCH 072/424] Fix date parsing in firefox --- www/form/inner.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/www/form/inner.js b/www/form/inner.js index 80bd3d2d3..893e5eddb 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -472,7 +472,12 @@ define([ } else { $container.find('input').each(function (i, el) { var val = $(el).val().trim(); - if (v.type === "day" || v.type === "time") { val = +new Date(val); } + if (v.type === "day" || v.type === "time") { + var f = el._flatpickr; + if (f && f.selectedDates && f.selectedDates.length) { + val = +f.selectedDates[0]; + } + } if (values.indexOf(val) === -1) { values.push(val); } else { duplicates = true; } }); From 21c47f5e57fe38ca44420f00f88fa1dc176e1a26 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 1 Jun 2021 14:20:20 +0200 Subject: [PATCH 073/424] Realtime part of the description block in forms --- www/form/inner.js | 44 ++++++++++++++++++++++++++++++++++++++------ www/form/main.js | 31 ++++++++++++++++++++++++++++--- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 893e5eddb..941cdfd4c 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -646,7 +646,6 @@ define([ return { tag: tag, edit: function (cb, tmp) { - // XXX use tmp and cursor getter var t = h('textarea'); var block = h('div.cp-form-edit-options-block', [t]); var cm = SFCodeMirror.create("gfm", CMeditor, t); @@ -655,9 +654,23 @@ define([ editor.setOption('lineWrapping', true); editor.setOption('styleActiveLine', true); editor.setOption('readOnly', false); - console.warn(APP.common); + + var text = opts.text; + var cursor; + if (tmp && tmp.content && tmp.old.text === text) { + text = tmp.content.text; + cursor = tmp.cursor; + } + setTimeout(function () { - editor.setValue(opts.text); + editor.setValue(text); + if (cursor) { + if (Sortify(cursor.start) === Sortify(cursor.end)) { + editor.setCursor(cursor.start); + } else { + editor.setSelection(cursor.start, cursor.end); + } + } editor.refresh(); editor.save(); editor.focus(); @@ -676,12 +689,31 @@ define([ h('i.fa.fa-floppy-o'), h('span', Messages.settings_save) ]); + + var getContent = function () { + return { + text: editor.getValue() + }; + }; $(saveBlock).click(function () { $(saveBlock).attr('disabled', 'disabled'); - cb({ - text: editor.getValue() - }); + cb(getContent()); }); + + cursorGetter = function () { + if (document.activeElement && block.contains(document.activeElement)) { + cursor = { + start: editor.getCursor('from'), + end: editor.getCursor('to') + } + } + return { + old: opts, + content: getContent(), + cursor: cursor + }; + }; + return [ block, h('div', [cancelBlock, saveBlock]) diff --git a/www/form/main.js b/www/form/main.js index cd96a3367..dd6bc0009 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -71,19 +71,24 @@ define([ curvePublic: publicKey, }; }; - sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, cb) { + sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, _cb) { + var cb = Utils.Util.once(_cb); var myKeys = {}; var myFormKeys; - var CPNetflux; + var accessKeys; + var CPNetflux, Pinpad; var network; nThen(function (w) { require([ '/bower_components/chainpad-netflux/chainpad-netflux.js', - ], w(function (_CPNetflux) { + '/common/pinpad.js', + ], w(function (_CPNetflux, _Pinpad) { CPNetflux = _CPNetflux; + Pinpad = _Pinpad; })); Cryptpad.getAccessKeys(w(function (_keys) { if (!Array.isArray(_keys)) { return; } + accessKeys = _keys; _keys.some(function (_k) { if ((!Cryptpad.initialTeam && !_k.id) || Cryptpad.initialTeam === _k.id) { @@ -122,6 +127,26 @@ define([ // XXX Cache }; var results = {}; + config.onError = function (info) { + cb({ error: info.type }); + }; + config.onRejected = function (data, cb) { + if (!Array.isArray(data) || !data.length || data[0].length !== 16) { + return void cb(true); + } + if (!Array.isArray(accessKeys)) { return void cb(true); } + network.historyKeeper = data[0]; + nThen(function (waitFor) { + accessKeys.forEach(function (obj) { + Pinpad.create(network, obj, waitFor(function (e) { + console.log('done', obj); + if (e) { console.error(e); } + })); + }); + }).nThen(function () { + cb(); + }); + }; config.onReady = function () { var myKey; // If we have submitted an anonymous answer, retrieve it From b88963cec2695d7154c7de3d3a41412f19c72817 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 1 Jun 2021 17:53:57 +0530 Subject: [PATCH 074/424] conditionally log some information about the checkup page when launching the server --- server.js | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/server.js b/server.js index 1824cf59c..d92540399 100644 --- a/server.js +++ b/server.js @@ -20,6 +20,14 @@ var canonicalizeOrigin = function (s) { return (s || '').trim().replace(/\/+$/, ''); }; +var fancyURL = function (domain, path) { + try { + if (domain && path) { return new URL(path, domain).href; } + return new URL(domain); + } catch (err) {} + return false; +}; + (function () { // you absolutely must provide an 'httpUnsafeOrigin' if (typeof(config.httpUnsafeOrigin) !== 'string') { @@ -47,21 +55,6 @@ var canonicalizeOrigin = function (s) { if (typeof(config.httpSafePort) !== 'number') { config.httpSafePort = config.httpPort + 1; } - - if (Env.DEV_MODE) { return; } - console.log(` - m m mm mmmmm mm m mmmmm mm m mmm m - # # # ## # "# #"m # # #"m # m" " # - " #"# # # # #mmmm" # #m # # # #m # # mm # - ## ##" #mm# # "m # # # # # # # # # - # # # # # " # ## mm#mm # ## "mmm" # -`); - - console.log("\nNo 'httpSafeOrigin' provided."); - console.log("Your configuration probably isn't taking advantage of all of CryptPad's security features!"); - console.log("This is acceptable for development, otherwise your users may be at risk.\n"); - - console.log("Serving sandboxed content via port %s.\nThis is probably not what you want for a production instance!\n", config.httpSafePort); } }()); @@ -338,7 +331,19 @@ nThen(function (w) { var port = config.httpPort; var ps = port === 80? '': ':' + port; - console.log('[%s] server available http://%s%s', new Date().toISOString(), hostName, ps); + var roughAddress = 'http://' + hostName + ps; + var betterAddress = fancyURL(config.httpUnsafeOrigin); + + if (betterAddress) { + console.log('Serving content for %s via %s.\n', betterAddress, roughAddress); + } else { + console.log('Serving content via %s.\n', roughAddress); + } + if (!Array.isArray(config.adminKeys)) { + console.log("Your instance is not correctly configured for safe use in production.\nSee %s for more information.\n", + fancyURL(config.httpUnsafeOrigin, '/checkup/') || 'https://your-domain.com/checkup/' + ); + } }); if (config.httpSafePort) { From 1d4b4081dbbdd297509b3a9ed234295c7764b346 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 1 Jun 2021 18:13:59 +0530 Subject: [PATCH 075/424] fix minor issues with home page redirect preference --- www/settings/inner.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/www/settings/inner.js b/www/settings/inner.js index f979379d7..64ae1880e 100644 --- a/www/settings/inner.js +++ b/www/settings/inner.js @@ -69,13 +69,13 @@ define([ 'cp-settings-custom-theme', ], 'drive': [ + 'cp-settings-redirect', 'cp-settings-resettips', 'cp-settings-drive-duplicate', 'cp-settings-thumbnails', 'cp-settings-drive-backup', 'cp-settings-drive-import-local', 'cp-settings-trim-history', - 'cp-settings-redirect', //'cp-settings-drive-reset' ], 'cursor': [ // Msg.settings_cat_cursor @@ -844,9 +844,9 @@ define([ return $div; }; - Messages.settings_driveRedirectTitle = "DRIVE REDIRECT TITLE"; // XXX redirect - Messages.settings_driveRedirectHint = "DRIVE REDIRECT HINT"; // XXX redirect - Messages.settings_driveRedirect = "DRIVE REDIRECT"; // XXX redirect + Messages.settings_driveRedirectTitle = "Home page redirection"; // XXX redirect + Messages.settings_driveRedirectHint = "Automatic redirection from the home page to the drive when logged in is no longer enabled by default. Legacy behaviour can be enabled below."; // XXX redirect + Messages.settings_driveRedirect = "Automatically redirect me"; // XXX redirect create['redirect'] = function () { if (!common.isLoggedIn()) { return; } @@ -868,7 +868,7 @@ define([ $spinner.show(); $ok.hide(); var val = $checkbox.is(':checked') || false; - common.setAttribute(['general', Constants.prefersDriveRedirectKey, val, function() { + common.setAttribute(['general', Constants.prefersDriveRedirectKey], val, function() { $spinner.hide(); $ok.show(); sframeChan.query("Q_SET_DRIVE_REDIRECT_PREFERENCE", { From 774a65af2847efab5885aa431fcfde7b8bb879e7 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 1 Jun 2021 18:16:54 +0530 Subject: [PATCH 076/424] update changelog --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7799523df..b02ebf004 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +# WIP + +## Goals + +## Update notes + +* `bower update` for `chainpad-crypto#0.2.6` ([signed mailbox extensions](https://github.com/xwiki-labs/chainpad-crypto/releases/tag/0.2.6)) +* log some information about the checkup page when launching the server + +## Features + +* more detailed messages for some tests on the checkup page +* log messages which fail signature validation +* make drive-redirect configurable via the settings page (disabled by default) + +## Bug fixes + +* variably display "Features" or "Pricing" in the _top bar_ + # 4.6.0 ## Goals From ec9a32c15a7a1626a345bf9f1d643ce12197baa0 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 1 Jun 2021 19:27:24 +0530 Subject: [PATCH 077/424] minor UI improvements for report page --- CHANGELOG.md | 1 + .../src/less2/pages/page-report.less | 12 ++++++ www/common/clipboard.js | 18 +++++++-- www/report/main.js | 38 ++++++++++++++++++- 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b02ebf004..384814692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * more detailed messages for some tests on the checkup page * log messages which fail signature validation * make drive-redirect configurable via the settings page (disabled by default) +* minor UI improvements for report page ## Bug fixes diff --git a/customize.dist/src/less2/pages/page-report.less b/customize.dist/src/less2/pages/page-report.less index bd2cd6c60..220240237 100644 --- a/customize.dist/src/less2/pages/page-report.less +++ b/customize.dist/src/less2/pages/page-report.less @@ -12,6 +12,18 @@ html, body { background-color: @cp_static-bg !important; color: @cryptpad_text_col; font-family: "IBM Plex Mono"; + + #cp-report, #cp-report-ui { + max-width: 900px; + margin: auto; + padding: 15px; + } + #cp-report { + border: 1px solid @cryptpad_text_col; + } + #cp-report-ui { + text-align: right; + } } diff --git a/www/common/clipboard.js b/www/common/clipboard.js index 191895dfd..7d93e15f3 100644 --- a/www/common/clipboard.js +++ b/www/common/clipboard.js @@ -1,13 +1,15 @@ define(['jquery'], function ($) { var Clipboard = {}; - // copy arbitrary text to the clipboard - // return boolean indicating success - Clipboard.copy = function (text) { + var copy = function (text, multiline) { var $ta = $('', { type: 'text', }).val(text); + if (multiline) { + $ta = $('