diff --git a/customize.dist/messages.js b/customize.dist/messages.js index 469279baf..abf96dbc0 100755 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -117,7 +117,6 @@ define(req, function(AppConfig, Default, Language) { Messages._languages = map; Messages._languageUsed = language; - // Get keys with parameters Messages._getKey = function (key, argArray) { if (!Messages[key]) { return '?'; } @@ -135,6 +134,14 @@ define(req, function(AppConfig, Default, Language) { } }; + Messages.addItemBottom = 'Add item to bottom of the board'; // XXX or 'Add card..' + Messages.addItemTop = 'Add item to top of the board'; // XXX or 'Add card..' + + Messages.admin_logoSize_error = "The logo size is too large"; // XXX + Messages.limit_error = "Please enter a valid number"; // XXX + Messages.positiveNumber_error = "Please enter a positive number"; // XXX + Messages.answerType_error = "Please select how to answer the form"; // XXX + // XXX Messages.admin_cat_admins = "Administrators"; Messages.admin_admin = "Admin"; @@ -144,6 +151,8 @@ define(req, function(AppConfig, Default, Language) { Messages.admin_addAdminsHint = "Add administrators from their public key or from your contacts list"; Messages.admin_addAdminsAdd = "Promote a contact to admin"; Messages.admin_addKeyLabel = "Add an admin using their public key"; + Messages.admin_errorAddKeyLabel = "Add a valid public key"; + Messages.admin_errorAddAdmins = "Pick a contact to promote to admin"; Messages.admin_listName = "Admin name"; Messages.admin_listKey = "Admin key"; @@ -152,6 +161,9 @@ define(req, function(AppConfig, Default, Language) { Messages.admin_listHardcoded = "Admin added into config.js. Can only be removed by editing the config file."; Messages.admin_listConfirm = "Are you sure you want to remove the admin rights of this user?"; + Messages.fm_link_invalid = "Please provide a valid URL"; // XXX + Messages.skipLink = "Skip to main content"; // XXX + return Messages; }); diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index e61d9fbda..8e7879fce 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -445,6 +445,23 @@ display: none !important; } + .cp-toolbar-skip-link { + position: absolute; + top: -100px; + left: 45%; + background-color: @cp_buttons-primary; + color: @cp_buttons-primary-text; + padding: 0.3rem 0.5rem; + font-size: 1rem; + text-decoration: none; + border-radius: @variables_radius; + z-index: 1000; + transition: top 0.3s ease; + } + .cp-toolbar-skip-link:focus { + top: 10px; + } + @media screen and (max-width: @browser_media-medium-screen), screen and (max-height: 500px) { flex-wrap: wrap; diff --git a/eslint.config.js b/eslint.config.js index 9b449c36e..64f7373ab 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -70,6 +70,7 @@ module.exports = [{ "linebreak-style": ["off", "unix"], quotes: ["off", "single"], semi: ["error", "always"], + eqeqeq: ["error", "always"], "no-irregular-whitespace": ["off"], "no-self-assign": ["off"], "no-empty": ["off"], diff --git a/install-onlyoffice.sh b/install-onlyoffice.sh index 6f9981c34..da42811af 100755 --- a/install-onlyoffice.sh +++ b/install-onlyoffice.sh @@ -37,7 +37,17 @@ main() { install_x2t v7.3+1 ab0c05b0e4c81071acea83f0c6a8e75f5870c360ec4abc4af09105dd9b52264af9711ec0b7020e87095193ac9b6e20305e446f2321a541f743626a598e5318c1 rm -rf "$BUILDS_DIR" - if command -v rdfind &>/dev/null; then + + if [ "${RDFIND+x}" != "x" ]; then + if command -v rdfind &>/dev/null; then + RDFIND="1" + else + RDFIND="0" + fi + fi + + if [ "$RDFIND" = "1" ]; then + ensure_command_available rdfind rdfind -makehardlinks true -makeresultsfile false $OO_DIR/v* fi } @@ -73,6 +83,18 @@ parse_arguments() { TRUST_REPOSITORY="1" shift ;; + --check) + CHECK="1" + shift + ;; + --rdfind) + RDFIND="1" + shift + ;; + --no-rdfind) + RDFIND="0" + shift + ;; *) show_help shift @@ -103,9 +125,6 @@ show_help() { cat <x2t.zip.sha512 diff --git a/lib/env.js b/lib/env.js index bb5235551..715dde3e0 100644 --- a/lib/env.js +++ b/lib/env.js @@ -242,9 +242,14 @@ module.exports.create = function (config) { sso: plugins?.SSO?.config || {}, enforceMFA: config.enforceMFA, - onlyOffice: { - availableVersions: getInstalledOOVersions() - }, + ...(getInstalledOOVersions().length > 0 + ? { + onlyOffice: { + availableVersions: getInstalledOOVersions(), + }, + } + : {}), + // initialized as undefined bearerSecret: void 0, diff --git a/package-lock.json b/package-lock.json index 027c522ea..f0b3e83d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6404,9 +6404,10 @@ } }, "node_modules/xml-crypto": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-3.2.0.tgz", - "integrity": "sha512-qVurBUOQrmvlgmZqIVBqmb06TD2a/PpEUfFPgD7BuBfjmoH4zgkqaWSIJrnymlCvM2GGt9x+XtJFA+ttoAufqg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-3.2.1.tgz", + "integrity": "sha512-0GUNbPtQt+PLMsC5HoZRONX+K6NBJEqpXe/lsvrFj0EqfpGPpVfJKGE7a5jCg8s2+Wkrf/2U1G41kIH+zC9eyQ==", + "license": "MIT", "dependencies": { "@xmldom/xmldom": "^0.8.8", "xpath": "0.0.32" diff --git a/src/worker/async-store.js b/src/worker/async-store.js index 40355d419..8cccfe526 100644 --- a/src/worker/async-store.js +++ b/src/worker/async-store.js @@ -513,6 +513,7 @@ const factory = (ApiConfig = {}, Sortify, UserObject, ProxyManager, if (e) { return void cb({error: e}); } store.rpc = call; + store.onRpcReadyEvt.fire(); Store.getPinLimit(null, null, function (obj) { diff --git a/www/admin/inner.js b/www/admin/inner.js index 7c0a157d0..2da209bd5 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -342,27 +342,6 @@ define([ } } }); - const $keyBtn = $(keyButton); - Util.onClickEnter($keyBtn, () => { - let val = $keyInput.val().trim(); - let key = Keys.canonicalize(val); - if (!key) { return; } - // We have a valid key - let name = Messages.admin_admin; - try { - let parsed = Keys.parseUser(val); - name = parsed.user; - } catch (e) {} - $keyBtn.prop('disabled', 'disabled'); - addAdmin({ ed:key, name }, (err) => { - $keyBtn.prop('disabled', false); - if (!err) { $keyInput.val(''); } - // refresh - APP.updateStatus(function () { - evRefreshAdmins.fire(); - }); - }); - }); const drawContacts = () => { $div.empty(); @@ -388,6 +367,9 @@ define([ let addBtn = blocks.button('primary', 'fa-plus', Messages.tag_add); Util.onClickEnter($(addBtn), () => { var $sel = $(contactsGrid.div).find('.cp-usergrid-user.cp-selected'); + if (!$sel.length) { + return UI.warn(Messages.admin_errorAddAdmins); + } nThen((waitFor) => { $sel.each((i, el) => { const $el = $(el); @@ -402,7 +384,6 @@ define([ }).nThen(() => { APP.updateStatus(function () { evRefreshAdmins.fire(); - drawContacts(); }); }); }); @@ -410,6 +391,30 @@ define([ drawContacts(); }); + const $keyBtn = $(keyButton); + Util.onClickEnter($keyBtn, () => { + let val = $keyInput.val().trim(); + let key = Keys.canonicalize(val); + if (!key) { + return UI.warn(Messages.admin_errorAddKeyLabel); + } + // We have a valid key + let name = Messages.admin_admin; + try { + let parsed = Keys.parseUser(val); + name = parsed.user; + } catch (e) {} + $keyBtn.prop('disabled', 'disabled'); + addAdmin({ ed:key, name }, (err) => { + $keyBtn.prop('disabled', false); + if (!err) { $keyInput.val(''); } + // refresh + APP.updateStatus(function () { + evRefreshAdmins.fire(); + }); + }); + }); + const list = blocks.form([ //currentList.div, contactsGrid.div, @@ -1086,7 +1091,12 @@ define([ sframeCommand('UPLOAD_LOGO', {dataURL}, (err, response) => { $button.removeAttr('disabled'); if (err) { - UI.warn(Messages.error); + if(err === 'E_TOO_LARGE') { + UI.warn(Messages.admin_logoSize_error); + } + else{ + UI.warn(Messages.error); + } $(input).val(''); console.error(err, response); spinner.hide(); @@ -1828,7 +1838,9 @@ define([ multiple: true, validate: function () { var l = parseInt($(newLimit).val()); - if (isNaN(l)) { return false; } + if (isNaN(l)) { + return UI.warn(Messages.limit_error); + } return true; } }, function () { @@ -1845,6 +1857,7 @@ define([ } var limit = getPrettySize(l); $(text).text(Messages._getKey('admin_limit', [limit])); + UI.log(Messages.saved); }); }); @@ -3902,12 +3915,16 @@ define([ multiple: true, validate: function () { var l = parseInt($(newDuration).val()); - if (isNaN(l)) { return false; } + if (isNaN(l)) { + return void UI.warn(Messages.limit_error); + } return true; } }, function () { var d = parseInt($(newDuration).val()); - if (!isPositiveInteger(d)) { return void UI.warn(Messages.error); } + if (!isPositiveInteger(d)) { + return void UI.warn(Messages.positiveNumber_error); + } var data = [d]; sFrameChan.query('Q_ADMIN_RPC', { @@ -3919,6 +3936,7 @@ define([ return void console.error(e, response); } $(form).find('.cp-admin-bytes-written-duration').text(Messages._getKey('admin_bytesWrittenDuration', [d])); + UI.log(Messages.saved); }); }); cb(form); @@ -4171,6 +4189,7 @@ define([ $container: APP.$toolbar, pageTitle: Messages.adminPage || 'Admin', metadataMgr: common.getMetadataMgr(), + skipLink: '#cp-sidebarlayout-container' }; APP.toolbar = Toolbar.create(configTb); APP.toolbar.$rightside.hide(); diff --git a/www/calendar/app-calendar.less b/www/calendar/app-calendar.less index 65424d200..245d08d6f 100644 --- a/www/calendar/app-calendar.less +++ b/www/calendar/app-calendar.less @@ -176,6 +176,7 @@ } .tui-full-calendar-icon { text-align:center; + flex-shrink: 0; } .tui-full-calendar-popup-detail-item { a { @@ -193,7 +194,7 @@ } li.tui-full-calendar-popup-section-item { padding: 0 6px; - height: 32px; + min-height: 32px; } .tui-full-calendar-popup-section-item { height: auto; @@ -215,6 +216,8 @@ } .tui-full-calendar-content { text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; font: @colortheme_app-font; padding: 0 10px; &:focus{ diff --git a/www/calendar/inner.js b/www/calendar/inner.js index 0994dbb6a..82482d4ba 100644 --- a/www/calendar/inner.js +++ b/www/calendar/inner.js @@ -670,7 +670,6 @@ define([ tag: 'a', attributes: { 'data-value': '.ics', - 'href': '#' }, content: '.ics' }); @@ -1303,7 +1302,6 @@ ICS ==> create a new event with the same UID and a RECURRENCE-ID field (with a v attributes: { 'class': 'cp-calendar-view', 'data-value': k, - 'href': '#', }, content: Messages['calendar_'+k] // Messages.calendar_day @@ -1564,7 +1562,6 @@ APP.recurrenceRule = { attributes: { 'class': 'cp-calendar-recurrence', 'data-value': '', - 'href': '#', }, content: Messages.calendar_rec_no }]; @@ -1577,7 +1574,6 @@ APP.recurrenceRule = { attributes: { 'class': 'cp-calendar-recurrence', 'data-value': basicStr[rec], - 'href': '#', }, content: Messages._getKey('calendar_rec_' + rec, [ getWeekDays(true)[date.getDay()], @@ -1598,7 +1594,6 @@ APP.recurrenceRule = { attributes: { 'class': 'cp-calendar-recurrence', 'data-value': basicStr.days, - 'href': '#', }, content: Messages['calendar_rec_' + (isWeekend ? 'weekend' : 'weekdays')] }); @@ -1608,7 +1603,6 @@ APP.recurrenceRule = { attributes: { 'class': 'cp-calendar-recurrence', 'data-value': 'custom', - 'href': '#', }, content: Messages.calendar_rec_custom }); @@ -1690,7 +1684,6 @@ APP.recurrenceRule = { attributes: { 'class': 'cp-calendar-recurrence-freq', 'data-value': rec, - 'href': '#', }, content: Messages['calendar_rec_freq_' + rec] }); @@ -2000,7 +1993,6 @@ APP.recurrenceRule = { attributes: { 'class': 'cp-calendar-reminder', 'data-value': k, - 'href': '#', }, content: Messages['calendar_'+k] // Messages.calendar_minutes @@ -2145,6 +2137,7 @@ APP.recurrenceRule = { $container: APP.$toolbar, pageTitle: Messages.calendar, metadataMgr: common.getMetadataMgr(), + skipLink: '#cp-sidebarlayout-container' }; APP.toolbar = Toolbar.create(configTb); APP.toolbar.$rightside.hide(); @@ -2228,6 +2221,7 @@ APP.recurrenceRule = { $el.find('.tui-full-calendar-dropdown-menu li').each(function (i, li) { var $li = $(li); var id = $li.attr('data-calendar-id'); + $li.attr('tabindex', 0); var c = calendars[id]; if (!c || c.readOnly) { return void $li.remove(); @@ -2235,6 +2229,86 @@ APP.recurrenceRule = { // If at least one calendar is editable, show the popup show = true; }); + let calendarDropdown = function ($el) { + let $dropdownButton = $el.find('.tui-full-calendar-dropdown-button'); + let $dropdownMenu = $el.find('.tui-full-calendar-dropdown-menu'); + + let toggleAriaExpanded = function (isOpen) { + $dropdownButton.attr('aria-expanded', isOpen ? 'true' : 'false'); + isOpen ? $dropdownMenu.show() : $dropdownMenu.hide(); + }; + + let calendarDropdownNavigation = function (event) { + let $focusedItem = $dropdownMenu.find('li:focus'); + switch (event.key) { + case ' ': + case 'Enter': + event.preventDefault(); + $focusedItem.click(); + toggleAriaExpanded(false); + $el.find('#tui-full-calendar-schedule-title').focus(); + break; + case 'Tab': + event.preventDefault(); + toggleAriaExpanded(false); + $el.find('.tui-full-calendar-popup-section').removeClass('tui-full-calendar-open'); + if (event.shiftKey) { + $el.find('.tui-full-calendar-popup-save').focus(); + } else { + $el.find('#tui-full-calendar-schedule-title').focus(); + } + break; + case 'ArrowDown': + event.preventDefault(); + var $next = $focusedItem.next('li'); + if ($next.length) { + $next.focus(); + } else { + $dropdownMenu.find('li').first().focus(); + } + break; + case 'ArrowUp': + event.preventDefault(); + var $prev = $focusedItem.prev('li'); + if ($prev.length) { + $prev.focus(); + } else { + $dropdownMenu.find('li').last().focus(); + } + break; + case 'Escape': + event.preventDefault(); + event.stopPropagation(); + toggleAriaExpanded(false); + $dropdownButton.focus(); + break; + } + }; + + $dropdownButton.on('click keydown', function (event) { + if (event.type !== 'click' && event.key !== 'Enter' && event.key !== ' ' && event.key !== 'ArrowDown' && event.key !== 'ArrowUp') { + return; + } + let isOpen = $el.find('.tui-full-calendar-open').length > 0; + toggleAriaExpanded(!isOpen); + if (!isOpen && event.key !== 'ArrowUp') { + $dropdownMenu.find('li').first().focus(); + } + else if(!isOpen){ + $dropdownMenu.find('li').last().focus(); + } + }); + // click outside the dropdown button => closes the dropdown => aria-expanded is false + $(document).on('click', function (event) { + if (!$(event.target).closest($dropdownButton).length) { + toggleAriaExpanded(false); + } + }); + + $dropdownMenu.on('keydown', calendarDropdownNavigation); + }; + calendarDropdown($el); + if ($el.find('.tui-full-calendar-hide.tui-full-calendar-dropdown').length || !show) { $el.hide(); UI.warn(Messages.calendar_errorNoCalendar); diff --git a/www/checkup/main.js b/www/checkup/main.js index 3c42bc016..0f2f8cf6c 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -423,34 +423,39 @@ define([ }); }); + const ooEnabled = ApiConfig.onlyOffice && ApiConfig.onlyOffice.availableVersions.includes( + OOCurrentVersion.currentVersion, + ); var sheetURL = `/common/onlyoffice/dist/${OOCurrentVersion.currentVersion}/web-apps/apps/spreadsheeteditor/main/index.html`; - assert(function (cb, msg) { - msg.innerText = "Missing HTTP headers required for .xlsx export from sheets. "; - var expect = { - 'cross-origin-resource-policy': 'cross-origin', - 'cross-origin-embedder-policy': 'require-corp', - }; - - Tools.common_xhr(sheetURL, function (xhr) { - var result = !Object.keys(expect).some(function (k) { - var response = xhr.getResponseHeader(k); - if (response !== expect[k]) { - msg.appendChild(h('span', [ - 'A value of ', - code(expect[k]), - ' was expected for the ', - code(k), - ' HTTP header, but instead a value of "', - code(response), - '" was received.', - ])); - return true; // returning true indicates that a value is incorrect - } + if (ooEnabled) { + assert(function (cb, msg) { + msg.innerText = "Missing HTTP headers required for .xlsx export from sheets. "; + var expect = { + 'cross-origin-resource-policy': 'cross-origin', + 'cross-origin-embedder-policy': 'require-corp', + }; + + Tools.common_xhr(sheetURL, function (xhr) { + var result = !Object.keys(expect).some(function (k) { + var response = xhr.getResponseHeader(k); + if (response !== expect[k]) { + msg.appendChild(h('span', [ + 'A value of ', + code(expect[k]), + ' was expected for the ', + code(k), + ' HTTP header, but instead a value of "', + code(response), + '" was received.', + ])); + return true; // returning true indicates that a value is incorrect + } + }); + cb(result || xhr.getAllResponseHeaders()); }); - cb(result || xhr.getAllResponseHeaders()); }); - }); + } assert(function (cb, msg) { setWarningClass(msg); @@ -719,20 +724,21 @@ define([ }); }); - assert(function (cb, msg) { // FIXME possibly superseded by more advanced CSP tests? - var url = `/common/onlyoffice/dist/${OOCurrentVersion.currentVersion}/web-apps/apps/spreadsheeteditor/main/index.html`; - msg.appendChild(CSP_WARNING(url)); - deferredPostMessage({ - command: 'GET_HEADER', - content: { - url: url, - header: 'content-security-policy', - }, - }, function (content) { - var CSP_headers = parseCSP(content); - cb(hasOnlyOfficeHeaders(CSP_headers) || CSP_headers); + if (ooEnabled) { + assert(function (cb, msg) { // FIXME possibly superseded by more advanced CSP tests? + msg.appendChild(CSP_WARNING(sheetURL)); + deferredPostMessage({ + command: 'GET_HEADER', + content: { + url: sheetURL, + header: 'content-security-policy', + }, + }, function (content) { + var CSP_headers = parseCSP(content); + cb(hasOnlyOfficeHeaders(CSP_headers) || CSP_headers); + }); }); - }); + } /* assert(function (cb, msg) { diff --git a/www/code/inner.js b/www/code/inner.js index 4be60351a..fa97426d4 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -610,6 +610,7 @@ define([ Framework.create({ toolbarContainer: '#cme_toolbox', contentContainer: '#cp-app-code-editor', + skipLink: '.CodeMirror', thumbnail: { getContainer: getThumbnailContainer, filter: function (el, before) { diff --git a/www/common/common-constants.js b/www/common/common-constants.js index bae268b25..1764ffbd1 100644 --- a/www/common/common-constants.js +++ b/www/common/common-constants.js @@ -32,7 +32,7 @@ const factory = function (AppConfig = {}) { MAX_PREMIUM_TEAMS_OWNED: Math.max(AppConfig.maxOwnedTeams || 0, AppConfig.maxPremiumTeamsOwned || 0) || 5, // Apps criticalApps: ['profile', 'settings', 'debug', 'admin', 'support', 'notifications', 'calendar', 'moderation', 'oldadmin'], // XXX oldadmin - earlyAccessApps: ['doc', 'presentation'] + earlyAccessApps: [] }; }; diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 011a23ce4..ab574f101 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1517,6 +1517,9 @@ define([ }); Util.onClickEnter(entry, function(e) { + if ($(e.target).attr('href') === '#') { + e.preventDefault(); + } if (config.isSelect) { return; } e.stopPropagation(); if (typeof(config.action) === "function") { @@ -2407,7 +2410,6 @@ define([ attributes: { 'class': 'cp-language-value', 'data-value': l, - 'href': '#', }, content: [ // supplying content as an array ensures it's a text node, not parsed HTML languages[l] // Pretty name of the language value @@ -3548,14 +3550,15 @@ define([ var text = Messages._getKey('owner_add', [name, title]); + var obj = { pw: msg.content.password || '', f: 1 }; + let newHref = Hash.getNewPadURL(msg.content.href, obj); var link = h('a', { - href: '#' + href: newHref }, Messages.requestEdit_viewPad); $(link).click(function (e) { e.preventDefault(); e.stopPropagation(); - var obj = { pw: msg.content.password || '', f: 1 }; - common.openURL(Hash.getNewPadURL(msg.content.href, obj)); + common.openURL(newHref); }); var div = h('div', [ diff --git a/www/common/common-util.js b/www/common/common-util.js index 281ea666d..4cde5968d 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -86,6 +86,12 @@ Util.mkEvent = function (once) { var handlers = []; var fired = false; + let promiseResolve; + + const promise = new Promise(resolve => { + promiseResolve = resolve; + }); + return { reg: function (cb) { if (once && fired) { return void setTimeout(cb); } @@ -99,10 +105,13 @@ }, fire: function () { if (once && fired) { return; } - fired = true; var args = Array.prototype.slice.call(arguments); + if (!fired) { promiseResolve.apply(null, args); } + fired = true; handlers.forEach(function (h) { h.apply(null, args); }); - } + }, + // Since a promise can only resolve once only the 1st call to fire() is reflected here. Even is `once` is `false`. + promise }; }; diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 3c1a1c6ba..804662430 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -2919,9 +2919,9 @@ define([ ]); var content = h('p', [ h('label', {for: 'cp-app-drive-link-name'}, Messages.fm_link_name), - name = h('input#cp-app-drive-link-name', { autocomplete: 'off', placeholder: Messages.fm_link_name_placeholder, tabindex:'1'}), + name = h('input#cp-app-drive-link-name', { autocomplete: 'off', placeholder: Messages.fm_link_name_placeholder}), h('label', {for: 'cp-app-drive-link-url'}, Messages.fm_link_url), - url = h('input#cp-app-drive-link-url', { type: 'url', autocomplete: 'off', placeholder: Messages.form_input_ph_url,tabindex:'1'}), + url = h('input#cp-app-drive-link-url', { type: 'url', autocomplete: 'off', placeholder: Messages.form_input_ph_url}), warning, ]); @@ -2936,9 +2936,13 @@ define([ }; var $warning = $(warning).hide(); - var $url = $(url).on('change keypress keyup keydown', function () { + var $url = $(url).on('change keypress keydown', function () { var v = $url.val().trim(); - $url.toggleClass('cp-input-invalid', !Util.isValidURL(v)); + if (Util.isValidURL(v)) { + $url.removeClass('cp-input-invalid'); + } else { + $url.addClass('cp-input-invalid'); + } if (v.length > 200) { $warning.show(); return; @@ -2961,8 +2965,7 @@ define([ var $name = $(name); var n = $name.val().trim() || $name.attr('placeholder'); var u = $url.val().trim(); - if (!n || !u) { return true; } - if (!Util.isValidURL(u)) { + if (!n || !u || !Util.isValidURL(u)) { UI.warn(Messages.fm_link_invalid); return true; } @@ -3137,7 +3140,7 @@ define([ var newObj = { tag: 'a', - attributes: { 'class': obj.class, href: '#' }, + attributes: { 'class': obj.class }, content: [obj.icon, obj.name] }; @@ -3179,7 +3182,6 @@ define([ tag: 'a', attributes: { 'class': 'cp-app-drive-rm-filter', - 'href': '#' }, content: [ h('i.fa.fa-times'), @@ -3192,7 +3194,6 @@ define([ var attributes = { 'class': 'cp-app-drive-filter-doc', 'data-type': type, - 'href': '#' }; var premium = common.checkRestrictedApp(type); @@ -3218,7 +3219,6 @@ define([ attributes: { 'class': 'cp-app-drive-filter-doc', 'data-type': 'link', - 'href': '#' }, content: [ getIcon('link')[0], @@ -3230,7 +3230,6 @@ define([ attributes: { 'class': 'cp-app-drive-filter-doc', 'data-type': 'file', - 'href': '#' }, content: [ getIcon('file')[0], @@ -3453,6 +3452,58 @@ define([ return $fihElement; }; + var lexicographicCompare = function(a, b) { + if (!Array.isArray(a)) { + a = [a]; + } + if (!Array.isArray(b)) { + b = [b]; + } + + if(a.length === 0 && b.length === 0) { + return 0; + } else if (a.length === 0) { + return -1; + } else if (b.length === 0) { + return 1; + } else if(a[0] < b[0]) { + return -1; + } else if(a[0] > b[0]) { + return 1; + } else { + // This means `a[0] == b[0]`. Chop off the first elements and compare the rest. + return lexicographicCompare(a.slice(1), b.slice(1)); + } + }; + + var splitStringToTextAndNumbers = function(s) { + var textOrDigitsRe = /(?\D+)?(?\d+)?/g; + var split = []; + + for (var match of s.matchAll(textOrDigitsRe)) { + if (match.groups.text !== undefined) { + split.push(match.groups.text); + } + if (match.groups.digits !== undefined) { + split.push(parseInt(match.groups.digits)); + } + } + + return split; + }; + + var naturalSort = function(a, b) { + if (typeof(a) === "string") { + a = splitStringToTextAndNumbers(a); + } + if (typeof(b) === "string") { + b = splitStringToTextAndNumbers(b); + } + + var comp = lexicographicCompare(a, b); + return comp; + }; + var sortElements = function (folder, path, oldkeys, prop, asc, useId) { var root = path && manager.find(path); if (path[0] === SHARED_FOLDER) { @@ -3497,9 +3548,8 @@ define([ keys.sort(function(a, b) { var _a = props[(a && a.uid) || a]; var _b = props[(b && b.uid) || b]; - if (_a < _b) { return mult * -1; } - if (_b < _a) { return mult; } - return 0; + + return mult * naturalSort(_a, _b); }); return keys; }; @@ -4091,7 +4141,8 @@ define([ ]; sortedTags.forEach(function (tag) { var tagLink = h('a', { href: '#' }, '#' + tag); - $(tagLink).click(function () { + $(tagLink).click(function (e) { + e.preventDefault(); if (displayedCategories.indexOf(SEARCH) !== -1) { APP.displayDirectory([SEARCH, '#' + tag]); } @@ -4529,8 +4580,7 @@ define([ manager.getSharedFolderData(root[a]).title : a; var newB = manager.isSharedFolder(root[b]) ? manager.getSharedFolderData(root[b]).title : b; - return newA < newB ? -1 : - (newA === newB ? 0 : 1); + return naturalSort(newA, newB); }); keys.forEach(function (key) { // Do not display files in the menu @@ -4758,6 +4808,7 @@ define([ data.sharedFolderId = sfId; data.name = Util.fixFileName(folderName); data.folderName = Util.fixFileName(folderName) + '.zip'; + data.common = common; var uo = manager.user.userObject; if (sfId && manager.folders[sfId]) { @@ -4939,7 +4990,7 @@ define([ else if ($this.hasClass('cp-app-drive-context-download')) { if (paths.length !== 1) { return; } var path = paths[0]; - el = manager.find(path.path); + el = $(path.element).data('element') || manager.find(path.path); // folder if (manager.isFolder(el)) { // folder diff --git a/www/common/inner/sidebar-layout.js b/www/common/inner/sidebar-layout.js index 03ce9c151..a356bffaf 100644 --- a/www/common/inner/sidebar-layout.js +++ b/www/common/inner/sidebar-layout.js @@ -64,7 +64,7 @@ define([ let prefix = icon.slice(0, icon.indexOf('-')); cls = `.${prefix}.${icon}`; } - return h(`i${cls}`); + return h(`i${cls}`, { 'aria-hidden': 'true' }); }; blocks.button = (type, icon, text) => { type = type || 'primary'; diff --git a/www/common/make-backup.js b/www/common/make-backup.js index 0dc5c8d47..a49842c5f 100644 --- a/www/common/make-backup.js +++ b/www/common/make-backup.js @@ -357,11 +357,22 @@ define([ max: 0, done: 0, cache: cache, - sframeChan: sframeChan + sframeChan: sframeChan, + common: data.common, }; var filesData = data.sharedFolderId && ctx.sf[data.sharedFolderId] ? ctx.sf[data.sharedFolderId].filesData : ctx.data.filesData; var links = ctx.sf[data.sharedFolderId] && ctx.sf[data.sharedFolderId].static ? ctx.data.static && ctx.sf[data.sharedFolderId].static : ctx.data.static; + if (ctx.common && !ctx.common.isLoggedIn()) { + // Anonymous Drive + ctx.data.root = {}; + let index = 0; + Object.keys(ctx.data.filesData).forEach(file => { + ctx.data.root[index] = file; + index += 1; + }); + } + progress('reading', -1); // Msg.settings_export_reading nThen(function (waitFor) { ctx.waitFor = waitFor; diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index 631aff970..01b640388 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -2271,7 +2271,6 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null tag: 'a', attributes: { 'data-value': val, - href: '#' }, content: val }; @@ -2690,7 +2689,7 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null (content.version <= 3 ? 'v2b/' : CURRENT_VERSION+'/'); var s = h('script', { type:'text/javascript', - src: '/common/onlyoffice/dist/'+version+'web-apps/apps/api/documents/api.js' + src: ApiConfig.httpSafeOrigin + '/common/onlyoffice/dist/'+version+'web-apps/apps/api/documents/api.js' }); $('#cp-app-oo-editor').empty().append(h('div#cp-app-oo-placeholder-a')).append(s); @@ -2726,7 +2725,8 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null }, sfCommon: common, $container: $bar, - $contentContainer: $('#cp-app-oo-container') + $contentContainer: $('#cp-app-oo-container'), + skipLink: 'iframe[name="frameEditor"]|#editor_sdk' }; toolbar = APP.toolbar = Toolbar.create(configTb); toolbar.showColors(); @@ -3132,7 +3132,7 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null var s = h('script', { type:'text/javascript', - src: '/common/onlyoffice/dist/'+version+'web-apps/apps/api/documents/api.js' + src: ApiConfig.httpSafeOrigin + '/common/onlyoffice/dist/'+version+'web-apps/apps/api/documents/api.js' }); $('#cp-app-oo-editor').append(s); diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index 9615ab620..cc842483e 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -723,7 +723,6 @@ define([ tag: 'a', attributes: { 'data-value': _ext, - 'href': '#' }, content: _ext }); @@ -734,7 +733,6 @@ define([ tag: 'a', attributes: { 'data-value': ext, - 'href': '#' }, content: ext }); @@ -743,7 +741,6 @@ define([ tag: 'a', attributes: { 'data-value': '', - 'href': '#' }, content: ' ', }); @@ -978,7 +975,8 @@ define([ realtime: cpNfInner.chainpad, sfCommon: common, $container: $(toolbarContainer), - $contentContainer: $(contentContainer) + $contentContainer: $(contentContainer), + skipLink: options.skipLink, }; toolbar = Toolbar.create(configTb); title.setToolbar(toolbar); diff --git a/www/common/sframe-common-codemirror.js b/www/common/sframe-common-codemirror.js index aa1bde788..e3411dc0e 100644 --- a/www/common/sframe-common-codemirror.js +++ b/www/common/sframe-common-codemirror.js @@ -365,7 +365,6 @@ define([ tag: 'a', attributes: { 'data-value': l.mode, - 'href': '#', }, content: [l.language] // Pretty name of the language value }); @@ -431,7 +430,6 @@ define([ tag: 'a', attributes: { 'data-value': l.name, - 'href': '#', }, content: [l.name] // Pretty name of the language value }); diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 6bf6580dc..700405fd0 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -2463,4 +2463,3 @@ define([ return common; }); - diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 8310deb27..915a922fc 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -1057,6 +1057,11 @@ define([ Mailbox.create(funcs); + // automatically configure all relative links in the inner iframe + // to point to the outer domain by adding a 'base' element to iframe's + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base + document.head.appendChild(h('base', { href: ApiConfig.httpUnsafeOrigin })); + cb(funcs); }); } }; diff --git a/www/common/toolbar.js b/www/common/toolbar.js index c56b2809d..3cbe4fd5f 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -863,6 +863,38 @@ MessengerUI, Messages, Pages, PadTypes) { }; }; + Bar.createSkipLink = function (toolbar, config) { + if (config.readOnly === 1) {return;} + const targetId = config.skipLink; + const $skipLink = $('', { + 'class': 'cp-toolbar-skip-link', + 'href': targetId, + 'tabindex': 0, + 'text': Messages.skipLink + }); + toolbar.$top.append($skipLink); + $skipLink.on('click', function (event) { + event.preventDefault(); + + let split = targetId.split('|'); // split for iframes + let $container = $('body'); + split.some(selector => { + let $targetElement = $container.find(selector); + if ($targetElement.is('iframe')) { + $container = $targetElement.contents(); + return; + } + const $firstFocusable = $targetElement.find('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"]), [contenteditable="true"]').first(); + if ($firstFocusable.length) { + $firstFocusable.trigger('focus'); + } else { + $skipLink.hide(); + } + return true; + }); + }); + return $skipLink; + }; var createLinkToMain = function (toolbar, config) { var $linkContainer = $('', { 'class': LINK_CLS @@ -1457,6 +1489,7 @@ MessengerUI, Messages, Pages, PadTypes) { toolbar['linkToMain'] = createLinkToMain(toolbar, config); + toolbar['skipLink'] = Bar.createSkipLink(toolbar, config); if (!config.realtime) { toolbar.connected = true; } diff --git a/www/common/user-object.js b/www/common/user-object.js index c04987315..999ecd212 100644 --- a/www/common/user-object.js +++ b/www/common/user-object.js @@ -248,7 +248,7 @@ const factory = (Util, Hash, var isFolder = exp.isFolder = function (element) { if (isFolderData(element)) { return false; } - return typeof(element) === "object" || isSharedFolder(element); + return (typeof(element) === "object" && !element.channel) || isSharedFolder(element); }; exp.isFolderEmpty = function (element) { if (!isFolder(element)) { return false; } diff --git a/www/diagram/export.js b/www/diagram/export.js index 09e5da30b..b672308d9 100644 --- a/www/diagram/export.js +++ b/www/diagram/export.js @@ -7,28 +7,6 @@ define([ ], function ( DiagramUtil ) { - const parseDrawioStyle = (styleAttrValue) => { - if (!styleAttrValue) { - return; - } - - const result = {}; - for (const part of styleAttrValue.split(';')) { - const s = part.split(/=(.*)/); - result[s[0]] = s[1]; - } - - return result; - }; - - const stringifyDrawioStyle = (styleAttrValue) => { - const parts = []; - for (const [key, value] of Object.entries(styleAttrValue)) { - parts.push(`${key}=${value}`); - } - return parts.join(';'); - }; - const blobToImage = (blob) => { return new Promise((resolve) => { const reader = new FileReader(); @@ -44,28 +22,18 @@ define([ }; const loadCryptPadImages = (doc) => { - return Array.from(doc .querySelectorAll('mxCell')) - .map((element) => [element, parseDrawioStyle(element.getAttribute('style'))]) - .filter(([, style]) => style && style.image && style.image.startsWith('cryptpad://')) + return Array.from(doc.querySelectorAll('mxCell')) + .map((element) => [element, DiagramUtil.parseDrawioStyle(element.getAttribute('style'))]) + .filter(([, style]) => style.image && style.image.startsWith('cryptpad://')) .map(([element, style]) => { return loadImage(style.image) .then((dataUrl) => { style.image = dataUrl.replace(';base64', ''); // ';' breaks draw.ios style format - element.setAttribute('style', stringifyDrawioStyle(style)); + element.setAttribute('style', DiagramUtil.stringifyDrawioStyle(style)); }); }); }; - const parseXML = (xmlStr) => { - const parser = new DOMParser(); - const doc = parser.parseFromString(xmlStr, "application/xml"); - const errorNode = doc.querySelector("parsererror"); - if (errorNode) { - throw Error("error while parsing " + errorNode); - } - return doc; - }; - return { main: function(userDoc, cb) { delete userDoc.metadata; @@ -74,7 +42,7 @@ define([ let doc; try { - doc = parseXML(xml); + doc = DiagramUtil.parseXML(xml); } catch(e) { console.error(e); return; diff --git a/www/diagram/import.js b/www/diagram/import.js new file mode 100644 index 000000000..5daaed1d9 --- /dev/null +++ b/www/diagram/import.js @@ -0,0 +1,65 @@ + +// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +define([ + '/diagram/util.js', +], function ( + DiagramUtil, +) { + const Nacl = window.nacl; + + const splitAt = function (str, char) { + const pos = str.indexOf(char); + if (pos <= 0) { + return [str, '']; + } + return [str.substring(0, pos), str.substring(pos + 1)]; + }; + + const parseDataUrl = function (url) { + const [prefix, data] = splitAt(url, ','); + const [, metadata] = splitAt(prefix, ':'); + const [mimeType, ] = splitAt(metadata, ';'); + + const u8 = Nacl.util.decodeBase64(data); + return new Blob([u8], { type: mimeType }); + }; + + const saveImagesToCryptPad = async (fileManager, doc) => { + const images = Array.from(doc.querySelectorAll('mxCell')) + .map((element) => ({ + element, + style: DiagramUtil.parseDrawioStyle(element.getAttribute('style')), + })) + .filter(({ style }) => style.image && style.image.startsWith('data:')); + + for(const image of images) { + const blob = parseDataUrl(image.style.image); + + const cryptPadUrl = await DiagramUtil.uploadFile(fileManager, blob); + image.style.image = cryptPadUrl; + image.element.setAttribute('style', DiagramUtil.stringifyDrawioStyle(image.style)); + } + }; + + const importDiagram = async (common, content) => { + let doc; + try { + doc = DiagramUtil.parseXML(content); + } catch(e) { + console.error(e); + return; + } + + const fileManager = DiagramUtil.createSimpleFileManager(common); + + await saveImagesToCryptPad(fileManager, doc); + return DiagramUtil.xmlAsJsonContent(new XMLSerializer().serializeToString(doc)); + }; + + return { + importDiagram + }; +}); diff --git a/www/diagram/inner.js b/www/diagram/inner.js index b6176c1cf..58251b3cd 100644 --- a/www/diagram/inner.js +++ b/www/diagram/inner.js @@ -4,10 +4,10 @@ // This is the initialization loading the CryptPad libraries define([ + '/api/config', 'jquery', '/common/sframe-app-framework.js', '/customize/messages.js', // translation keys - '/components/pako/dist/pako.min.js', '/components/x2js/x2js.js', '/diagram/util.js', '/common/common-ui-elements.js', @@ -15,51 +15,16 @@ define([ 'less!/diagram/app-diagram.less', 'css!/diagram/drawio.css', ], function ( + ApiConfig, $, Framework, Messages, - pako, X2JS, DiagramUtil, UIElements ) { - const Nacl = window.nacl; const APP = window.APP = {}; - // As described here: https://drawio-app.com/extracting-the-xml-from-mxfiles/ - const decompressDrawioXml = function(xmlDocStr) { - var TEXT_NODE = 3; - - var parser = new DOMParser(); - var doc = parser.parseFromString(xmlDocStr, "application/xml"); - - var errorNode = doc.querySelector("parsererror"); - if (errorNode) { - console.error("error while parsing", errorNode); - return xmlDocStr; - } - - doc.firstChild.removeAttribute('modified'); - doc.firstChild.removeAttribute('agent'); - doc.firstChild.removeAttribute('etag'); - - var diagrams = doc.querySelectorAll('diagram'); - - diagrams.forEach(function(diagram) { - if (diagram.childNodes.length === 1 && diagram.firstChild && diagram.firstChild.nodeType === TEXT_NODE) { - const innerText = diagram.firstChild.nodeValue; - const bin = Nacl.util.decodeBase64(innerText); - const xmlUrlStr = pako.inflateRaw(bin, {to: 'string'}); - const xmlStr = decodeURIComponent(xmlUrlStr); - const diagramDoc = parser.parseFromString(xmlStr, "application/xml"); - diagram.replaceChild(diagramDoc.firstChild, diagram.firstChild); - } - }); - - - var result = new XMLSerializer().serializeToString(doc); - return result; - }; const deepEqual = function(o1, o2) { return JSON.stringify(o1) === JSON.stringify(o2); @@ -105,28 +70,8 @@ define([ }); }; - const numbersToNumbers = function(o) { - const type = typeof o; - - if (type === "object") { - for (const key in o) { - o[key] = numbersToNumbers(o[key]); - } - return o; - } else if (type === 'string' && o.match(/^[+-]?(0|(([1-9]\d*)(\.\d+)?))$/)) { - return parseFloat(o, 10); - } else { - return o; - } - }; - - const xmlAsJsonContent = (xml) => { - var decompressedXml = decompressDrawioXml(xml); - return numbersToNumbers(x2js.xml2js(decompressedXml)); - }; - var onDrawioChange = function(newXml) { - var newJson = xmlAsJsonContent(newXml); + var newJson = DiagramUtil.xmlAsJsonContent(newXml); if (!deepEqual(lastContent, newJson)) { lastContent = newJson; framework.localChange(); @@ -150,7 +95,10 @@ define([ return new Promise((resolve) => { framework.insertImage({}, (imageData) => { if (imageData.blob) { - resolve(imageData.blob); + const fileManager = DiagramUtil.createSimpleFileManager(framework._.sfCommon); + DiagramUtil.uploadFile(fileManager, imageData.blob) + .then(url => resolve(url)) + .catch(e => console.error(e)); } else if (imageData.url) { resolve(imageData.url); } else { @@ -179,9 +127,12 @@ define([ framework.setFileImporter( {accept: ['.drawio', 'application/x-drawio']}, - (content) => { - return xmlAsJsonContent(content); - } + (content, file, cb) => { + require(['/diagram/import.js'], (importer) => { + importer.importDiagram(framework._.sfCommon, content, file).then(cb); + }); + }, + true ); framework.setFileExporter( @@ -207,7 +158,7 @@ define([ // starting the CryptPad framework framework.start(); - drawioFrame.src = '/components/drawio/src/main/webapp/index.html?' + drawioFrame.src = ApiConfig.httpSafeOrigin + '/components/drawio/src/main/webapp/index.html?' + new URLSearchParams({ test: 1, stealth: 1, @@ -250,6 +201,7 @@ define([ Framework.create({ toolbarContainer: '#cme_toolbox', contentContainer: '#cp-app-diagram-editor', + skipLink: '#cp-app-diagram-content|body .geSearchSidebar', // validateContent: validateXml, }, function (framework) { onFrameworkReady(framework); diff --git a/www/diagram/util.js b/www/diagram/util.js index 72acda93a..b6cdf0067 100644 --- a/www/diagram/util.js +++ b/www/diagram/util.js @@ -7,11 +7,19 @@ define([ '/file/file-crypto.js', '/common/outer/cache-store.js', '/components/x2js/x2js.js', + '/components/pako/dist/pako.min.js', + '/common/common-hash.js', + '/api/config', + 'jquery', ], function ( Util, FileCrypto, Cache, X2JS, + pako, + Hash, + ApiConfig, + $, ) { const Nacl = window.nacl; const x2js = new X2JS(); @@ -49,10 +57,139 @@ define([ return x2js.js2xml(cleaned); }; + const parseXML = (xmlStr) => { + const parser = new DOMParser(); + const doc = parser.parseFromString(xmlStr, "application/xml"); + const errorNode = doc.querySelector("parsererror"); + if (errorNode) { + throw Error("error while parsing " + errorNode); + } + return doc; + }; + + const numbersToNumbers = function(o) { + const type = typeof o; + + if (type === "object") { + for (const key in o) { + o[key] = numbersToNumbers(o[key]); + } + return o; + } else if (type === 'string' && o.match(/^[+-]?(0|(([1-9]\d*)(\.\d+)?))$/)) { + return parseFloat(o, 10); + } else { + return o; + } + }; + + const xmlAsJsonContent = (xml) => { + var decompressedXml = decompressDrawioXml(xml); + return numbersToNumbers(x2js.xml2js(decompressedXml)); + }; + + // As described here: https://drawio-app.com/extracting-the-xml-from-mxfiles/ + const decompressDrawioXml = function(xmlDocStr) { + var TEXT_NODE = 3; + + var parser = new DOMParser(); + var doc = parser.parseFromString(xmlDocStr, "application/xml"); + + var errorNode = doc.querySelector("parsererror"); + if (errorNode) { + console.error("error while parsing", errorNode); + return xmlDocStr; + } + + doc.firstChild.removeAttribute('modified'); + doc.firstChild.removeAttribute('agent'); + doc.firstChild.removeAttribute('etag'); + + var diagrams = doc.querySelectorAll('diagram'); + + diagrams.forEach(function(diagram) { + if (diagram.childNodes.length === 1 && diagram.firstChild && diagram.firstChild.nodeType === TEXT_NODE) { + const innerText = diagram.firstChild.nodeValue; + const bin = Nacl.util.decodeBase64(innerText); + const xmlUrlStr = pako.inflateRaw(bin, {to: 'string'}); + const xmlStr = decodeURIComponent(xmlUrlStr); + const diagramDoc = parser.parseFromString(xmlStr, "application/xml"); + diagram.replaceChild(diagramDoc.firstChild, diagram.firstChild); + } + }); + + + var result = new XMLSerializer().serializeToString(doc); + return result; + }; + + const parseDrawioStyle = (styleAttrValue) => { + if (!styleAttrValue) { + return {}; + } + + const result = {}; + for (const part of styleAttrValue.split(';')) { + const s = part.split(/=(.*)/); + result[s[0]] = s[1]; + } + + return result; + }; + + const stringifyDrawioStyle = (styleAttrValue) => { + const parts = []; + for (const [key, value] of Object.entries(styleAttrValue)) { + parts.push(`${key}=${value}`); + } + return parts.join(';'); + }; + + const getCryptPadUrlForUploadData = (data) => { + const urlHash = data.url.split('#')[1]; + const secret = Hash.getSecrets('file', urlHash); + + const fileHost = ApiConfig.fileHost || window.location.origin; + const hexFileName = secret.channel; + const src = fileHost + Hash.getBlobPathFromHex(hexFileName); + const key = secret.keys && secret.keys.cryptKey; + const cryptKey = Nacl.util.encodeBase64(key); + return getCryptPadUrl(src, cryptKey, data.fileType); + }; + + const uploadFile = async (fileManager, blob) => { + return new Promise((resolve) => { + fileManager.handleFile(blob, { + callback: (data) => { + const cryptPadUrl = getCryptPadUrlForUploadData(data); + resolve(cryptPadUrl); + } + }); + }); + }; + + const createSimpleFileManager = (common) => { + const fmConfigImages = { + noHandlers: true, + noStore: true, + onUploaded: function (ev, data) { + if (!ev.callback) { return; } + ev.callback(data); + } + }; + return common.createFileManager(fmConfigImages); + }; + return { parseCryptPadUrl, getCryptPadUrl, jsonContentAsXML, + parseXML, + xmlAsJsonContent, + decompressDrawioXml, + parseDrawioStyle, + stringifyDrawioStyle, + uploadFile, + createSimpleFileManager, loadImage: function(href) { return new Promise((resolve, reject) => { diff --git a/www/drive/inner.js b/www/drive/inner.js index f35aa8e72..aa20c9737 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -220,7 +220,8 @@ define([ metadataMgr: metadataMgr, readOnly: privateData.readOnly, sfCommon: common, - $container: APP.$bar + $container: APP.$bar, + skipLink: '#cp-app-drive-tree' }; var toolbar = Toolbar.create(configTb); diff --git a/www/form/inner.js b/www/form/inner.js index 43af934e3..cd4ee4121 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -159,7 +159,6 @@ define([ attributes: { 'class': 'cp-form-type-value', 'data-value': t, - 'href': '#', }, content: Messages['form_text_'+t] }; @@ -262,7 +261,6 @@ define([ attributes: { 'class': 'cp-form-type-value', 'data-value': t, - 'href': '#', }, content: Messages['form_poll_'+t] }; @@ -1295,7 +1293,6 @@ define([ attributes: { 'class': 'cp-form-condition-question', 'data-value': obj.uid, - 'href': '#', }, content: obj.q }; @@ -1316,14 +1313,12 @@ define([ tag: 'a', attributes: { 'data-value': 1, - 'href': '#', }, content: Messages.form_condition_is }, { tag: 'a', attributes: { 'data-value': 0, - 'href': '#', }, content: Messages.form_condition_isnot }]; @@ -1402,7 +1397,6 @@ define([ attributes: { 'class': 'cp-form-condition-value', 'data-value': str, - 'href': '#', }, content: str }; @@ -1576,7 +1570,6 @@ define([ attributes: { 'class': 'cp-form-condition-question', 'data-value': obj.uid, - 'href': '#', }, content: obj.q }; @@ -1604,7 +1597,6 @@ define([ attributes: { 'class': 'cp-form-condition-value', 'data-value': str, - 'href': '#', }, content: str }; @@ -2988,7 +2980,6 @@ define([ attributes: { 'class': 'cp-form-type-value', 'data-value': t.key, - 'href': '#', }, content: t.str }; @@ -3574,7 +3565,7 @@ define([ }); var $send = $(send).click(function () { if (!$radio.find('input[type="radio"]:checked').length) { - return UI.warn(Messages.error); + return UI.warn(Messages.answerType_error); } var results = getFormResults(); @@ -5676,5 +5667,6 @@ define([ Framework.create({ toolbarContainer: '#cp-toolbar', contentContainer: '#cp-app-form-editor', + skipLink: '#cp-app-form-editor' }, andThen); }); diff --git a/www/kanban/inner.js b/www/kanban/inner.js index fa7103aa5..c2db93513 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -1401,6 +1401,7 @@ define([ Framework.create({ toolbarContainer: '#cme_toolbox', contentContainer: '#cp-app-kanban-editor', + skipLink: '#cp-app-kanban-content' }, waitFor(function (framework) { andThen2(framework); })); diff --git a/www/kanban/jkanban_cp.js b/www/kanban/jkanban_cp.js index f6411e36e..1db332c15 100644 --- a/www/kanban/jkanban_cp.js +++ b/www/kanban/jkanban_cp.js @@ -160,12 +160,20 @@ define([ } function __onAddItemClickHandler(nodeItem) { - nodeItem.addEventListener('click', function (e) { + function handleAddItem(e, item) { e.preventDefault(); e.stopPropagation(); - self.options.addItemClick(this); - if (typeof (this.clickfn) === 'function') { - this.clickfn(this); + self.options.addItemClick(item); + if (typeof (item.clickfn) === 'function') { + item.clickfn(item); + } + } + nodeItem.addEventListener('click', function (e) { + handleAddItem(e,this); + }); + nodeItem.addEventListener('keydown', function (e) { + if (e.keyCode === 13) { + handleAddItem(e,this); } }); } @@ -719,6 +727,7 @@ define([ var addTopBoardItem = document.createElement('span'); addTopBoardItem.classList.add('kanban-title-button'); $(addTopBoardItem).attr('tabindex', '0'); + $(addTopBoardItem).attr('aria-label', Messages.addItemTop); addTopBoardItem.setAttribute('data-top', "1"); addTopBoardItem.innerHTML = ''; footerBoard.appendChild(addTopBoardItem); @@ -726,6 +735,7 @@ define([ var addBoardItem = document.createElement('span'); addBoardItem.classList.add('kanban-title-button'); $(addBoardItem).attr('tabindex', '0'); + $(addBoardItem).attr('aria-label', Messages.addItemBottom); addBoardItem.innerHTML = ''; footerBoard.appendChild(addBoardItem); __onAddItemClickHandler(addBoardItem); diff --git a/www/notifications/inner.js b/www/notifications/inner.js index 7abcbac79..8228f73c5 100644 --- a/www/notifications/inner.js +++ b/www/notifications/inner.js @@ -205,7 +205,7 @@ define([ var active = privateData.category || 'all'; common.setHash(active); Object.keys(categories).forEach(function (key) { - var $category = $('
', {'class': 'cp-sidebarlayout-category'}).appendTo($categories); + var $category = $('
', {'class': 'cp-sidebarlayout-category', 'tabindex': 0}).appendTo($categories); if (key === 'all') { $category.append($('', {'class': 'fa fa-bars'})); } if (key === 'friends') { $category.append($('', {'class': 'fa fa-user'})); } if (key === 'pads') { $category.append($('', {'class': 'cptools cptools-richtext'})); } @@ -215,6 +215,11 @@ define([ $category.addClass('cp-leftside-active'); } + $category.keydown(function (e) { + if (e.keyCode === 13) { + $category.click(); + } + }); $category.click(function () { if (!Array.isArray(categories[key]) && categories[key].onClick) { categories[key].onClick(); @@ -240,6 +245,7 @@ define([ $container: APP.$toolbar, pageTitle: Messages.notificationsPage || 'Notifications', metadataMgr: common.getMetadataMgr(), + skipLink: '#cp-sidebarlayout-container', }; APP.toolbar = Toolbar.create(configTb); APP.toolbar.$rightside.hide(); diff --git a/www/pad/inner.js b/www/pad/inner.js index ec0c54aea..f912d00f2 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -1335,6 +1335,7 @@ define([ Framework.create({ toolbarContainer: '#cp-app-pad-toolbar', contentContainer: '#cp-app-pad-editor', + skipLink: '#cke_1_contents .cke_wysiwyg_frame|html', patchTransformer: ChainPad.NaiveJSONTransformer, /*thumbnail: { getContainer: function () { return $('iframe').contents().find('html')[0]; }, diff --git a/www/profile/inner.js b/www/profile/inner.js index 63e2c8d2a..f46e09315 100644 --- a/www/profile/inner.js +++ b/www/profile/inner.js @@ -603,6 +603,7 @@ define([ $container: APP.$toolbar, pageTitle: Messages.profileButton, metadataMgr: common.getMetadataMgr(), + skipLink: '#cp-sidebarlayout-container' }; APP.toolbar = Toolbar.create(configTb); APP.toolbar.$rightside.hide(); diff --git a/www/settings/inner.js b/www/settings/inner.js index 75cfc84cd..b1419531e 100644 --- a/www/settings/inner.js +++ b/www/settings/inner.js @@ -902,7 +902,7 @@ define([ var todo = function () { var val = parseInt($input.val()); - if (typeof(val) !== 'number' || isNaN(val)) { return UI.warn(Messages.error); } + if (typeof(val) !== 'number' || isNaN(val)) { return UI.warn(Messages.limit_error); } if (val === oldVal) { return; } spinner.spin(); common.setAttribute(['general', 'mediatag-size'], val, function (err) { @@ -1196,6 +1196,7 @@ define([ Feedback.send('FULL_DRIVE_EXPORT_START'); var todo = function(data, filename) { var ui = Backup.createExportUI(privateData.origin); + data.common = common; var bu = Backup.create(data, common.getPad, privateData.fileHost, function(blob, errors) { saveAs(blob, filename); @@ -1978,6 +1979,7 @@ define([ $container: APP.$toolbar, pageTitle: Messages.settings_title, metadataMgr: common.getMetadataMgr(), + skipLink: '#cp-sidebarlayout-leftside' }; APP.toolbar = Toolbar.create(configTb); APP.toolbar.$rightside.hide(); diff --git a/www/slide/inner.js b/www/slide/inner.js index c0e0cd97b..98c354f90 100644 --- a/www/slide/inner.js +++ b/www/slide/inner.js @@ -619,7 +619,8 @@ define([ } $(el).css('background-color', ''); } - } + }, + skipLink: '.CodeMirror', }, waitFor(function (fw) { framework = fw; })); nThen(function (waitFor) { diff --git a/www/teams/inner.js b/www/teams/inner.js index 672fb6f19..c5ba62587 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -1564,7 +1564,8 @@ define([ metadataMgr: metadataMgr, readOnly: privateData.readOnly, sfCommon: common, - $container: $bar + $container: $bar, + skipLink: '#cp-sidebarlayout-leftside' }; var toolbar = APP.toolbar = Toolbar.create(configTb); // Update the name in the user menu diff --git a/www/whiteboard/inner.js b/www/whiteboard/inner.js index 0cf758008..6118c93ce 100644 --- a/www/whiteboard/inner.js +++ b/www/whiteboard/inner.js @@ -636,6 +636,7 @@ define([ patchTransformer: ChainPad.NaiveJSONTransformer, toolbarContainer: '#cp-toolbar', contentContainer: '#cp-app-whiteboard-canvas-area', + skipLink: '#cp-app-whiteboard-controls' }, waitFor(function (framework) { andThen2(framework); }));